diff --git a/.esprintrc b/.esprintrc new file mode 100644 index 000000000..9330e00d1 --- /dev/null +++ b/.esprintrc @@ -0,0 +1,9 @@ +{ + "paths": [ + "frontend/src/**/*.js" + ], + "ignored": [ + "**/node_modules/**/*" + ], + "port": 5004 +} diff --git a/.gitignore b/.gitignore index a340f3295..cfac0d699 100644 --- a/.gitignore +++ b/.gitignore @@ -120,8 +120,6 @@ _tests/ setup/Output/ *.~is -UI.Phantom/ - #VS outout folders bin obj @@ -135,5 +133,3 @@ _start _temp_*/**/* src/.idea/ -/npm_start.bat -/npm_start.bat diff --git a/.idea/.name b/.idea/.name deleted file mode 100644 index e402c1d9d..000000000 --- a/.idea/.name +++ /dev/null @@ -1 +0,0 @@ -Lidarr \ No newline at end of file diff --git a/.idea/Sonarr.iml b/.idea/Sonarr.iml deleted file mode 100644 index aeec84bf6..000000000 --- a/.idea/Sonarr.iml +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/codeStyleSettings.xml b/.idea/codeStyleSettings.xml deleted file mode 100644 index 7598f4c8e..000000000 --- a/.idea/codeStyleSettings.xml +++ /dev/null @@ -1,59 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.idea/encodings.xml b/.idea/encodings.xml deleted file mode 100644 index 97626ba45..000000000 --- a/.idea/encodings.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.idea/jsLibraryMappings.xml b/.idea/jsLibraryMappings.xml deleted file mode 100644 index b8387eb1b..000000000 --- a/.idea/jsLibraryMappings.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.idea/libraries/Sonarr_node_modules.xml b/.idea/libraries/Sonarr_node_modules.xml deleted file mode 100644 index 4eeebc5cc..000000000 --- a/.idea/libraries/Sonarr_node_modules.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml deleted file mode 100644 index 19f74da8e..000000000 --- a/.idea/misc.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml deleted file mode 100644 index 7cc2cf51b..000000000 --- a/.idea/modules.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml deleted file mode 100644 index 94a25f7f4..000000000 --- a/.idea/vcs.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.npmrc b/.npmrc new file mode 100644 index 000000000..ad5884817 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +save-prefix="" diff --git a/.yarnrc b/.yarnrc new file mode 100644 index 000000000..fdd705c63 --- /dev/null +++ b/.yarnrc @@ -0,0 +1 @@ +save-prefix "" diff --git a/README.md b/README.md index e9eb6dd13..9b85b3d2f 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,8 @@ -[![Build status](https://ci.appveyor.com/api/projects/status/pu08kh1avj2gl1av?svg=true)](https://ci.appveyor.com/project/majora2007/lidarr) - -[![Codacy Badge](https://api.codacy.com/project/badge/Grade/43c18ff049df442fab086cea020c4642)](https://www.codacy.com/app/majora2007/Lidarr?utm_source=github.com&utm_medium=referral&utm_content=lidarr/Lidarr&utm_campaign=Badge_Grade) - ## Lidarr +[![Build status](https://ci.appveyor.com/api/projects/status/tpm5mj5milne88nc?svg=true)](https://ci.appveyor.com/project/lidarr/lidarr) +[![Codacy Badge](https://api.codacy.com/project/badge/Grade/4e6d014aee9542189b4abb0b1439980f)](https://www.codacy.com/app/Lidarr/Lidarr?utm_source=github.com&utm_medium=referral&utm_content=lidarr/Lidarr&utm_campaign=Badge_Grade) + Lidarr is a music collection manager for Usenet and BitTorrent users. It can monitor multiple RSS feeds for new tracks from your favorite artists and will grab, sort and rename them. It can also be configured to automatically upgrade the quality of files already downloaded when a better quality format becomes available. ## Major Features Include: @@ -46,7 +45,7 @@ Lidarr is a music collection manager for Usenet and BitTorrent users. It can mon ### Development -* Open `NzbDrone.sln` in Visual Studio +* Open `Lidarr.sln` in Visual Studio * Make sure `NzbDrone.Console` is set as the startup project * Change build to 'Debug x86' diff --git a/build-appveyor.cake b/build-appveyor.cake index cce3fbd03..bd0fb3704 100644 --- a/build-appveyor.cake +++ b/build-appveyor.cake @@ -1,4 +1,5 @@ #addin "Cake.Npm" +#addin "Cake.Yarn" #addin "SharpZipLib" #addin "Cake.Compression" @@ -10,9 +11,9 @@ var outputFolderOsxApp = outputFolderOsx + "_app"; var testPackageFolder = "./_tests"; var testSearchPattern = "*.Test/bin/x86/Release"; var sourceFolder = "./src"; -var solutionFile = sourceFolder + "/NzbDrone.sln"; -var updateFolder = outputFolder + "/NzbDrone.Update"; -var updateFolderMono = outputFolderMono + "/NzbDrone.Update"; +var solutionFile = sourceFolder + "/Lidarr.sln"; +var updateFolder = outputFolder + "/Lidarr.Update"; +var updateFolderMono = outputFolderMono + "/Lidarr.Update"; // Artifact variables var artifactsFolder = "./_artifacts"; @@ -135,8 +136,8 @@ Task("PackageMono").Does(() => { DeleteFiles(outputFolderMono + "/sqlite3.*"); DeleteFiles(outputFolderMono + "/MediaInfo.*"); - // Adding NzbDrone.Core.dll.config (for dllmap) - CopyFile(sourceFolder + "/NzbDrone.Core/NzbDrone.Core.dll.config", outputFolderMono + "/NzbDrone.Core.dll.config"); + // Adding Lidarr.Core.dll.config (for dllmap) + CopyFile(sourceFolder + "/NzbDrone.Core/Lidarr.Core.dll.config", outputFolderMono + "/Lidarr.Core.dll.config"); // Adding CurlSharp.dll.config (for dllmap) CopyFile(sourceFolder + "/NzbDrone.Common/CurlSharp.dll.config", outputFolderMono + "/CurlSharp.dll.config"); @@ -147,11 +148,11 @@ Task("PackageMono").Does(() => { MoveFile(outputFolderMono + "/Lidarr.Console.exe.config", outputFolderMono + "/Lidarr.exe.config"); MoveFile(outputFolderMono + "/Lidarr.Console.exe.mdb", outputFolderMono + "/Lidarr.exe.mdb"); - // Remove NzbDrone.Windows.* - DeleteFiles(outputFolderMono + "/NzbDrone.Windows.*"); + // Remove Lidarr.Windows.* + DeleteFiles(outputFolderMono + "/Lidarr.Windows.*"); - // Adding NzbDrone.Mono to updatePackage - CopyFiles(outputFolderMono + "/NzbDrone.Mono.*", updateFolderMono); + // Adding Lidarr.Mono to updatePackage + CopyFiles(outputFolderMono + "/Lidarr.Mono.*", updateFolderMono); }); Task("PackageOsx").Does(() => { @@ -226,8 +227,8 @@ Task("PackageTests").Does(() => { // Clean CleanFolder(testPackageFolder, true); - // Adding NzbDrone.Core.dll.config (for dllmap) - CopyFile(sourceFolder + "/NzbDrone.Core/NzbDrone.Core.dll.config", testPackageFolder + "/NzbDrone.Core.dll.config"); + // Adding Lidarr.Core.dll.config (for dllmap) + CopyFile(sourceFolder + "/NzbDrone.Core/Lidarr.Core.dll.config", testPackageFolder + "/Lidarr.Core.dll.config"); // Adding CurlSharp.dll.config (for dllmap) CopyFile(sourceFolder + "/NzbDrone.Common/CurlSharp.dll.config", testPackageFolder + "/CurlSharp.dll.config"); @@ -238,10 +239,10 @@ Task("PackageTests").Does(() => { Task("CleanupWindowsPackage").Does(() => { // Remove mono - DeleteFiles(outputFolder + "/NzbDrone.Mono.*"); + DeleteFiles(outputFolder + "/Lidarr.Mono.*"); - // Adding NzbDrone.Windows to updatePackage - CopyFiles(outputFolder + "/NzbDrone.Windows.*", updateFolder); + // Adding Lidarr.Windows to updatePackage + CopyFiles(outputFolder + "/Lidarr.Windows.*", updateFolder); }); Task("Build") @@ -267,7 +268,7 @@ Task("ArtifactsWindows").Does(() => { }); Task("ArtifactsWindowsInstaller").Does(() => { - InnoSetup("./setup/nzbdrone.iss", new InnoSetupSettings { + InnoSetup("./setup/lidarr.iss", new InnoSetupSettings { OutputDirectory = artifactsFolder, ToolPath = "./setup/inno/ISCC.exe" }); diff --git a/build.ps1 b/build.ps1 deleted file mode 100644 index 45b8ce783..000000000 --- a/build.ps1 +++ /dev/null @@ -1 +0,0 @@ -Write-Warning "DEPRECATED -- Please use build.sh instead." \ No newline at end of file diff --git a/build.sh b/build.sh index 1775ee34d..297a932e2 100755 --- a/build.sh +++ b/build.sh @@ -7,9 +7,9 @@ outputFolderOsxApp='./_output_osx_app' testPackageFolder='./_tests/' testSearchPattern='*.Test/bin/x86/Release' sourceFolder='./src' -slnFile=$sourceFolder/NzbDrone.sln -updateFolder=$outputFolder/NzbDrone.Update -updateFolderMono=$outputFolderMono/NzbDrone.Update +slnFile=$sourceFolder/Lidarr.sln +updateFolder=$outputFolder/Lidarr.Update +updateFolderMono=$outputFolderMono/Lidarr.Update nuget='tools/nuget/nuget.exe'; CheckExitCode() @@ -39,9 +39,6 @@ CleanFolder() find $path -name "FluentValidation.resources.dll" -exec rm "{}" \; find $path -name "App.config" -exec rm "{}" \; - echo "Removing .less files" - find $path -name "*.less" -exec rm "{}" \; - echo "Removing vshost files" find $path -name "*.vshost.exe" -exec rm "{}" \; @@ -77,6 +74,17 @@ BuildWithXbuild() CheckExitCode xbuild /p:Configuration=Release /p:Platform=x86 /t:Build /p:AllowedReferenceRelatedFileExtensions=.pdb $slnFile } +LintUI() +{ + ProgressStart 'ESLint' + CheckExitCode yarn eslint + ProgressEnd 'ESLint' + + ProgressStart 'Stylelint' + CheckExitCode yarn stylelint + ProgressEnd 'Stylelint' +} + Build() { echo "##teamcity[progressStart 'Build']" @@ -101,13 +109,16 @@ Build() RunGulp() { - echo "##teamcity[progressStart 'npm install']" - npm-cache install npm || CheckExitCode npm install - echo "##teamcity[progressFinish 'npm install']" + ProgressStart 'npm install' + yarn install + #npm-cache install npm || CheckExitCode npm install --no-optional --no-bin-links + ProgressEnd 'npm install' + + LintUI - echo "##teamcity[progressStart 'Running gulp']" - CheckExitCode npm run build - echo "##teamcity[progressFinish 'Running gulp']" + ProgressStart 'Running gulp' + CheckExitCode npm run build -- --production + ProgressEnd 'Running gulp' } CreateMdbs() @@ -147,8 +158,8 @@ PackageMono() rm -f $outputFolderMono/sqlite3.* rm -f $outputFolderMono/MediaInfo.* - echo "Adding NzbDrone.Core.dll.config (for dllmap)" - cp $sourceFolder/NzbDrone.Core/NzbDrone.Core.dll.config $outputFolderMono + echo "Adding Lidarr.Core.dll.config (for dllmap)" + cp $sourceFolder/NzbDrone.Core/Lidarr.Core.dll.config $outputFolderMono echo "Adding CurlSharp.dll.config (for dllmap)" cp $sourceFolder/NzbDrone.Common/CurlSharp.dll.config $outputFolderMono @@ -159,11 +170,11 @@ PackageMono() mv "$file" "${file//.Console/}" done - echo "Removing NzbDrone.Windows" - rm $outputFolderMono/NzbDrone.Windows.* + echo "Removing Lidarr.Windows" + rm $outputFolderMono/Lidarr.Windows.* - echo "Adding NzbDrone.Mono to UpdatePackage" - cp $outputFolderMono/NzbDrone.Mono.* $updateFolderMono + echo "Adding Lidarr.Mono to UpdatePackage" + cp $outputFolderMono/Lidarr.Mono.* $updateFolderMono echo "##teamcity[progressFinish 'Creating Mono Package']" } @@ -223,8 +234,8 @@ PackageTests() CleanFolder $testPackageFolder true - echo "Adding NzbDrone.Core.dll.config (for dllmap)" - cp $sourceFolder/NzbDrone.Core/NzbDrone.Core.dll.config $testPackageFolder + echo "Adding Lidarr.Core.dll.config (for dllmap)" + cp $sourceFolder/NzbDrone.Core/Lidarr.Core.dll.config $testPackageFolder echo "Adding CurlSharp.dll.config (for dllmap)" cp $sourceFolder/NzbDrone.Common/CurlSharp.dll.config $testPackageFolder @@ -237,11 +248,11 @@ PackageTests() CleanupWindowsPackage() { - echo "Removing NzbDrone.Mono" - rm -f $outputFolder/NzbDrone.Mono.* + echo "Removing Lidarr.Mono" + rm -f $outputFolder/Lidarr.Mono.* - echo "Adding NzbDrone.Windows to UpdatePackage" - cp $outputFolder/NzbDrone.Windows.* $updateFolder + echo "Adding Lidarr.Windows to UpdatePackage" + cp $outputFolder/Lidarr.Windows.* $updateFolder } # Use mono or .net depending on OS diff --git a/frontend/.csscomb.json b/frontend/.csscomb.json new file mode 100644 index 000000000..a82e49732 --- /dev/null +++ b/frontend/.csscomb.json @@ -0,0 +1,25 @@ +{ + "remove-empty-rulesets": true, + "always-semicolon": true, + "color-case": "lower", + "block-indent": " ", + "color-shorthand": false, + "element-case": "lower", + "eof-newline": true, + "leading-zero": true, + "quotes": "double", + "sort-order-fallback": "abc", + "space-before-colon": "", + "space-after-colon": " ", + "space-before-combinator": " ", + "space-after-combinator": " ", + "space-between-declarations": "\n", + "space-before-opening-brace": " ", + "space-after-opening-brace": "\n", + "space-after-selector-delimiter": " ", + "space-before-selector-delimiter": "", + "space-before-closing-brace": "\n", + "strip-spaces": true, + "tab-size": true, + "unitless-zero": false +} diff --git a/frontend/.esformatter b/frontend/.esformatter new file mode 100644 index 000000000..600bb0751 --- /dev/null +++ b/frontend/.esformatter @@ -0,0 +1,335 @@ +{ + "indent": { + "value": " ", + "FunctionExpression": 1, + "ArrayExpression": 1, + "ObjectExpression": 1 + }, + "lineBreak": { + "value": "\n", + + "before": { + "ArrayPatternClosing": 0, + "ArrayPatternComma": 0, + "ArrayPatternOpening": 0, + "ArrowFunctionExpressionArrow": 0, + "ArrowFunctionExpressionClosingBrace": ">=1", + "ArrowFunctionExpressionOpeningBrace": 0, + "AssignmentExpression": ">=1", + "AssignmentOperator": 0, + "BlockStatement": 0, + "BreakKeyword": ">=1", + "CallExpression": -1, + "CallExpressionClosingParentheses": -1, + "CallExpressionOpeningParentheses": 0, + "CatchClosingBrace": ">=1", + "CatchKeyword": 0, + "CatchOpeningBrace": 0, + "ClassDeclaration": ">=1", + "ClassDeclarationClosingBrace": ">=1", + "ClassDeclarationOpeningBrace": 0, + "ConditionalExpression": ">=1", + "DeleteOperator": ">=1", + "DoWhileStatement": ">=1", + "DoWhileStatementClosingBrace": ">=1", + "DoWhileStatementOpeningBrace": 0, + "ElseIfStatement": 0, + "ElseIfStatementClosingBrace": ">=1", + "ElseIfStatementOpeningBrace": 0, + "ElseStatement": 0, + "ElseStatementClosingBrace": ">=1", + "ElseStatementOpeningBrace": 0, + "EmptyStatement": -1, + "EndOfFile": -1, + "FinallyClosingBrace": ">=1", + "FinallyKeyword": -1, + "FinallyOpeningBrace": 0, + "ForInStatement": ">=1", + "ForInStatementClosingBrace": ">=1", + "ForInStatementExpressionClosing": 0, + "ForInStatementExpressionOpening": 0, + "ForInStatementOpeningBrace": 0, + "ForStatement": ">=1", + "ForStatementClosingBrace": ">=1", + "ForStatementExpressionClosing": "<2", + "ForStatementExpressionOpening": 0, + "ForStatementOpeningBrace": 0, + "FunctionDeclaration": ">=1", + "FunctionDeclarationClosingBrace": ">=1", + "FunctionDeclarationOpeningBrace": 0, + "FunctionExpression": 0, + "FunctionExpressionClosingBrace": 1, + "FunctionExpressionOpeningBrace":0, + "IIFEClosingParentheses": 0, + "IfStatement": ">=1", + "IfStatementClosingBrace": ">=1", + "IfStatementOpeningBrace": 0, + "LogicalExpression": -1, + "MemberExpressionClosing": 0, + "MemberExpressionOpening": 0, + "MemberExpressionPeriod": -1, + "MethodDefinition": ">=1", + "ObjectExpressionClosingBrace": "<=1", + "ObjectPatternClosingBrace": 0, + "ObjectPatternComma": 0, + "ObjectPatternOpeningBrace": 0, + "ParameterDefault": 0, + "Property": "<=2", + "PropertyValue": 0, + "ReturnStatement": -1, + "SwitchClosingBrace": ">=1", + "SwitchOpeningBrace": 0, + "ThisExpression": -1, + "ThrowStatement": ">=1", + "TryClosingBrace": ">=1", + "TryKeyword": -1, + "TryOpeningBrace": 0, + "VariableDeclaration": ">=1", + "VariableDeclarationSemiColon": 0, + "VariableDeclarationWithoutInit": ">=1", + "VariableName": ">=1", + "VariableValue": 0, + "WhileStatement": ">=1", + "WhileStatementClosingBrace": ">=1", + "WhileStatementOpeningBrace": 0 + }, + + "after": { + "ArrayPatternClosing": 0, + "ArrayPatternComma": 0, + "ArrayPatternOpening": 0, + "ArrowFunctionExpressionArrow": 0, + "ArrowFunctionExpressionClosingBrace": -1, + "ArrowFunctionExpressionOpeningBrace": ">=1", + "AssignmentExpression": ">=1", + "AssignmentOperator": 0, + "BlockStatement": 0, + "BreakKeyword": -1, + "CallExpression": -1, + "CallExpressionClosingParentheses": -1, + "CallExpressionOpeningParentheses": -1, + "CatchClosingBrace": ">=0", + "CatchKeyword": 0, + "CatchOpeningBrace": ">=1", + "ClassDeclaration": ">=1", + "ClassDeclarationClosingBrace": ">=1", + "ClassDeclarationOpeningBrace": ">=1", + "ConditionalExpression": ">=1", + "DeleteOperator": ">=1", + "DoWhileStatement": ">=1", + "DoWhileStatementClosingBrace": 0, + "DoWhileStatementOpeningBrace": ">=1", + "ElseIfStatement": ">=1", + "ElseIfStatementClosingBrace": ">=1", + "ElseIfStatementOpeningBrace": ">=1", + "ElseStatement": ">=1", + "ElseStatementClosingBrace": ">=1", + "ElseStatementOpeningBrace": ">=1", + "EmptyStatement": -1, + "FinallyClosingBrace": ">=1", + "FinallyKeyword": -1, + "FinallyOpeningBrace": ">=1", + "ForInStatement": ">=1", + "ForInStatementClosingBrace": ">=1", + "ForInStatementExpressionClosing": -1, + "ForInStatementExpressionOpening": "<2", + "ForInStatementOpeningBrace": ">=1", + "ForStatement": ">=1", + "ForStatementClosingBrace": ">=1", + "ForStatementExpressionClosing": -1, + "ForStatementExpressionOpening": "<2", + "ForStatementOpeningBrace": ">=1", + "FunctionDeclaration": ">=1", + "FunctionDeclarationClosingBrace": ">=1", + "FunctionDeclarationOpeningBrace": ">=1", + "FunctionExpression": 0, + "FunctionExpressionClosingBrace": -1, + "FunctionExpressionOpeningBrace": 1, + "IIFEOpeningParentheses": 0, + "IfStatement": ">=1", + "IfStatementClosingBrace": ">=1", + "IfStatementOpeningBrace": ">=1", + "LogicalExpression": -1, + "MemberExpressionClosing": 0, + "MemberExpressionOpening": 0, + "MemberExpressionPeriod": 0, + "MethodDefinition": ">=1", + "ObjectExpressionOpeningBrace": "<=1", + "ObjectPatternClosingBrace": 0, + "ObjectPatternComma": 0, + "ObjectPatternOpeningBrace": 0, + "ParameterDefault": 0, + "Property": -1, + "PropertyName": 0, + "ReturnStatement": -1, + "SwitchCaseColon": ">=1", + "SwitchClosingBrace": ">=1", + "SwitchOpeningBrace": ">=1", + "ThisExpression": 0, + "ThrowStatement": ">=1", + "TryClosingBrace": 0, + "TryKeyword": -1, + "TryOpeningBrace": ">=1", + "VariableDeclaration": ">=1", + "VariableDeclarationSemiColon": ">=1", + "VariableValue": -1, + "WhileStatement": ">=1", + "WhileStatementClosingBrace": ">=1", + "WhileStatementOpeningBrace": ">=1" + } + }, + "whiteSpace": { + "value": " ", + "removeTrailing": 1, + "before": { + "ArgumentComma": 0, + "ArgumentList": 0, + "ArgumentListArrayExpression": 0, + "ArgumentListFunctionExpression": 1, + "ArgumentListObjectExpression": 0, + "ArrayExpressionClosing": 0, + "ArrayExpressionComma": 0, + "ArrayExpressionOpening": 1, + "AssignmentOperator": 1, + "BinaryExpression": 0, + "BinaryExpressionOperator": 1, + "BlockComment": 1, + "CallExpression": 1, + "CatchClosingBrace": 1, + "CatchKeyword": 1, + "CatchOpeningBrace": 1, + "CatchParameterList": 0, + "CommaOperator": 0, + "ConditionalExpressionAlternate": 1, + "ConditionalExpressionConsequent": 1, + "DoWhileStatementClosingBrace": 1, + "DoWhileStatementConditional": 1, + "DoWhileStatementOpeningBrace": 1, + "ElseIfStatementClosingBrace": 1, + "ElseIfStatementOpeningBrace": 1, + "ElseStatementClosingBrace": 1, + "ElseStatementOpeningBrace": 1, + "EmptyStatement": 0, + "ExpressionClosingParentheses": 0, + "FinallyClosingBrace": 1, + "FinallyKeyword": -1, + "FinallyOpeningBrace": 1, + "ForInStatement": 1, + "ForInStatementClosingBrace": 1, + "ForInStatementExpressionClosing": 0, + "ForInStatementExpressionOpening": 1, + "ForInStatementOpeningBrace": 1, + "ForStatement": 1, + "ForStatementClosingBrace": 1, + "ForStatementExpressionClosing": 0, + "ForStatementExpressionOpening": 1, + "ForStatementOpeningBrace": 1, + "ForStatementSemicolon": 0, + "FunctionDeclarationClosingBrace": 1, + "FunctionDeclarationOpeningBrace": 1, + "FunctionExpressionClosingBrace": 1, + "FunctionExpressionOpeningBrace": 1, + "IfStatementClosingBrace": 1, + "IfStatementConditionalClosing": 0, + "IfStatementConditionalOpening": 1, + "IfStatementOpeningBrace": 1, + "LineComment": 1, + "LogicalExpressionOperator": 1, + "MemberExpressionClosing": 0, + "ObjectExpressionClosingBrace": 1, + "ParameterComma": 0, + "ParameterList": 0, + "Property": 1, + "PropertyName": 1, + "PropertyValue": 1, + "SwitchDiscriminantClosing": 0, + "SwitchDiscriminantOpening": 1, + "ThrowKeyword": 1, + "TryClosingBrace": 1, + "TryKeyword": -1, + "TryOpeningBrace": 1, + "UnaryExpressionOperator": 0, + "VariableName": 1, + "VariableValue": 1, + "WhileStatementClosingBrace": 1, + "WhileStatementConditionalClosing": 0, + "WhileStatementConditionalOpening": 1, + "WhileStatementOpeningBrace": 1 + }, + "after": { + "ArgumentComma": 1, + "ArgumentList": 0, + "ArgumentListArrayExpression": 1, + "ArgumentListFunctionExpression": 1, + "ArgumentListObjectExpression": 0, + "ArrayExpressionClosing": 0, + "ArrayExpressionComma": 1, + "ArrayExpressionOpening": 0, + "AssignmentOperator": 1, + "BinaryExpression": 0, + "BinaryExpressionOperator": 1, + "BlockComment": 1, + "CallExpression": 0, + "CatchClosingBrace": 1, + "CatchKeyword": 1, + "CatchOpeningBrace": 1, + "CatchParameterList": 0, + "CommaOperator": 1, + "ConditionalExpressionConsequent": 1, + "ConditionalExpressionTest": 1, + "DoWhileStatementBody": 1, + "DoWhileStatementClosingBrace": 1, + "DoWhileStatementOpeningBrace": 1, + "ElseIfStatementClosingBrace": 1, + "ElseIfStatementOpeningBrace": 1, + "ElseStatementClosingBrace": 1, + "ElseStatementOpeningBrace": 1, + "EmptyStatement": 0, + "ExpressionOpeningParentheses": 0, + "FinallyClosingBrace": 1, + "FinallyKeyword": -1, + "FinallyOpeningBrace": 1, + "ForInStatement": 1, + "ForInStatementClosingBrace": 1, + "ForInStatementExpressionClosing": 1, + "ForInStatementExpressionOpening": 0, + "ForInStatementOpeningBrace": 1, + "ForStatement": 1, + "ForStatementClosingBrace": 1, + "ForStatementExpressionClosing": 1, + "ForStatementExpressionOpening": 0, + "ForStatementOpeningBrace": 1, + "ForStatementSemicolon": 1, + "FunctionDeclarationClosingBrace": 0, + "FunctionDeclarationOpeningBrace": 0, + "FunctionExpressionClosingBrace": 0, + "FunctionExpressionOpeningBrace": 0, + "FunctionName": 0, + "FunctionReservedWord": 0, + "IfStatementClosingBrace": 1, + "IfStatementConditionalClosing": 0, + "IfStatementConditionalOpening": 0, + "IfStatementOpeningBrace": 1, + "LogicalExpressionOperator": 1, + "MemberExpressionOpening": 0, + "ObjectExpressionClosingBrace": 0, + "ObjectExpressionOpeningBrace": 1, + "ParameterComma": 1, + "ParameterList": 0, + "PropertyName": 0, + "PropertyValue": 0, + "SwitchDiscriminantClosing": 1, + "SwitchDiscriminantOpening": 0, + "ThrowKeyword": 1, + "TryClosingBrace": 1, + "TryKeyword": -1, + "TryOpeningBrace": 1, + "UnaryExpressionOperator": 0, + "VariableName": 1, + "WhileStatementClosingBrace": 1, + "WhileStatementConditionalClosing": 1, + "WhileStatementConditionalOpening": 0, + "WhileStatementOpeningBrace": 1 + } + } +} diff --git a/frontend/.eslintignore b/frontend/.eslintignore new file mode 100644 index 000000000..d4b43f836 --- /dev/null +++ b/frontend/.eslintignore @@ -0,0 +1 @@ +**/JsLibraries/** diff --git a/frontend/.eslintrc b/frontend/.eslintrc new file mode 100644 index 000000000..5208e3ad6 --- /dev/null +++ b/frontend/.eslintrc @@ -0,0 +1,288 @@ +{ + "parser": "babel-eslint", + + "env": { + "browser": true, + "commonjs": true, + "node": true, + "es6": true + }, + + "globals": { + "expect": false, + "chai": false, + "sinon": false + }, + + "parserOptions": { + "ecmaVersion": 6, + "sourceType": "module", + "ecmaFeatures": { + "modules": true, + "impliedStrict": true + } + }, + + "plugins": [ + "filenames", + "react" + ], + + "rules": { + "filenames/match-exported": ["error"], + + # ECMAScript 6 + + "arrow-body-style": [0], + "arrow-parens": ["error", "always"], + "arrow-spacing": ["error", { "before": true, "after": true }], + "constructor-super": "error", + "generator-star-spacing": "off", + "no-class-assign": "error", + "no-confusing-arrow": "error", + "no-const-assign": "error", + "no-dupe-class-members": "error", + "no-duplicate-imports": "error", + "no-new-symbol": "error", + "no-this-before-super": "error", + "no-useless-escape": "error", + "no-useless-computed-key": "error", + "no-useless-constructor": "error", + "no-var": "warn", + "object-shorthand": ["error", "properties"], + "prefer-arrow-callback": "error", + "prefer-const": "warn", + "prefer-reflect": "off", + "prefer-rest-params": "off", + "prefer-spread": "warn", + "prefer-template": "error", + "require-yield": "off", + "template-curly-spacing": ["error", "never"], + "yield-star-spacing": "off", + + # Possible Errors + + "comma-dangle": "error", + "no-cond-assign": "error", + "no-console": "off", + "no-constant-condition": "warn", + "no-control-regex": "error", + "no-debugger": "off", + "no-dupe-args": "error", + "no-dupe-keys": "error", + "no-duplicate-case": "error", + "no-empty": "warn", + "no-empty-character-class": "error", + "no-ex-assign": "error", + "no-extra-boolean-cast": "error", + "no-extra-parens": ["error", "functions"], + "no-extra-semi": "error", + "no-func-assign": "error", + "no-inner-declarations": "error", + "no-invalid-regexp": "error", + "no-irregular-whitespace": "error", + "no-negated-in-lhs": "error", + "no-obj-calls": "error", + "no-regex-spaces": "error", + "no-sparse-arrays": "error", + "no-unexpected-multiline": "error", + "no-unreachable": "warn", + "no-unsafe-finally": "error", + "use-isnan": "error", + "valid-jsdoc": "off", + "valid-typeof": "error", + + # Best Practices + + "accessor-pairs": "off", + "array-callback-return": "warn", + "block-scoped-var": "warn", + "consistent-return": "off", + "curly": "error", + "default-case": "error", + "dot-location": ["error", "property"], + "dot-notation": "error", + "eqeqeq": ["error", "smart"], + "guard-for-in": "error", + "no-alert": "warn", + "no-caller": "error", + "no-case-declarations": "error", + "no-div-regex": "error", + "no-else-return": "error", + "no-empty-function": ["error", {"allow": ["arrowFunctions"]}], + "no-empty-pattern": "error", + "no-eval": "error", + "no-extend-native": "error", + "no-extra-bind": "error", + "no-fallthrough": "error", + "no-floating-decimal": "error", + "no-implicit-coercion": ["error", { + "boolean": false, + "number": true, + "string": true, + "allow": [/* "!!", "~", "*", "+" */] + }], + "no-implicit-globals": "error", + "no-implied-eval": "error", + "no-invalid-this": "off", + "no-iterator": "error", + "no-labels": "error", + "no-lone-blocks": "error", + "no-loop-func": "error", + "no-magic-numbers": ["off", {"ignoreArrayIndexes": true, "ignore": [0, 1] }], + "no-multi-spaces": "error", + "no-multi-str": "error", + "no-native-reassign": ["error", {"exceptions": ["console"]}], + "no-new": "off", + "no-new-func": "error", + "no-new-wrappers": "error", + "no-octal": "error", + "no-octal-escape": "error", + "no-param-reassign": "off", + "no-process-env": "off", + "no-proto": "error", + "no-redeclare": "error", + "no-return-assign": "warn", + "no-script-url": "error", + "no-self-assign": "error", + "no-self-compare": "error", + "no-sequences": "error", + "no-throw-literal": "error", + "no-unmodified-loop-condition": "error", + "no-unused-expressions": "error", + "no-unused-labels": "error", + "no-useless-call": "error", + "no-useless-concat": "error", + "no-void": "error", + "no-warning-comments": "off", + "no-with": "error", + "radix": ["error", "as-needed"], + "vars-on-top": "off", + "wrap-iife": ["error", "inside"], + "yoda": "error", + + # Strict Mode + + "strict": ["error", "never"], + + # Variables + + "init-declarations": ["error", "always"], + "no-catch-shadow": "error", + "no-delete-var": "error", + "no-label-var": "error", + "no-restricted-globals": "off", + "no-shadow": "error", + "no-shadow-restricted-names": "error", + "no-undef": "error", + "no-undef-init": "off", + "no-undefined": "off", + "no-unused-vars": ["error", { "args": "none" }], + "no-use-before-define": "error", + + # Node.js and CommonJS + + "callback-return": "warn", + "global-require": "error", + "handle-callback-err": "warn", + "no-mixed-requires": "error", + "no-new-require": "error", + "no-path-concat": "error", + "no-process-exit": "error", + + # Stylistic Issues + + "array-bracket-spacing": ["error", "never"], + "block-spacing": ["error", "always"], + "brace-style": ["error", "1tbs", { "allowSingleLine": false }], + "camelcase": "off", + "comma-spacing": ["error", {"before": false, "after": true}], + "comma-style": ["error", "last"], + "computed-property-spacing": ["error", "never"], + "consistent-this": ["error", "self"], + "eol-last": "error", + "func-names": "off", + "func-style": ["error", "declaration"], + "indent": ["error", 2, {"SwitchCase": 1}], + "key-spacing": ["error", {"beforeColon": false, "afterColon": true}], + "keyword-spacing": ["error", {before: true, after: true}], + "lines-around-comment": ["error", { "beforeBlockComment": true, "afterBlockComment": false }], + "max-depth": ["error", {"maximum": 5}], + "max-nested-callbacks": ["error", 4], + "max-params": ["error", 4], + "max-statements": "off", + "max-statements-per-line": ["error", { "max": 1 }], + "new-cap": ["error", {"capIsNewExceptions": ["$.Deferred"]}], + "new-parens": "error", + "newline-after-var": "off", + "newline-before-return": "off", + "newline-per-chained-call": "off", + "no-array-constructor": "error", + "no-bitwise": "error", + "no-continue": "error", + "no-inline-comments": "off", + "no-lonely-if": "warn", + "no-mixed-spaces-and-tabs": "error", + "no-multiple-empty-lines": ["error", {max: 1}], + "no-negated-condition": "warn", + "no-nested-ternary": "error", + "no-new-object": "error", + "no-plusplus": "off", + "no-restricted-syntax": "off", + "no-spaced-func": "error", + "no-ternary": "off", + "no-trailing-spaces": "error", + "no-underscore-dangle": ["error", { "allowAfterThis": true }], + "no-unneeded-ternary": "error", + "no-whitespace-before-property": "error", + "object-curly-spacing": ["error", "always"], + "one-var": ["error", "never"], + "one-var-declaration-per-line": ["error", "always"], + "operator-assignment": ["off", "never"], + "operator-linebreak": ["error", "after"], + "quote-props": ["error", "as-needed"], + "quotes": ["error", "single"], + "require-jsdoc": "off", + "semi": "error", + "semi-spacing": ["error", { "before": false, "after": true }], + "sort-vars": "off", + "space-before-blocks": ["error", "always"], + "space-before-function-paren": ["error", "never"], + "space-in-parens": "off", + "space-infix-ops": "off", + "space-unary-ops": "off", + "spaced-comment": "error", + "wrap-regex": "error", + + # React + + "react/jsx-boolean-value": [2, "always"], + "react/jsx-uses-vars": 2, + "react/jsx-closing-bracket-location": 2, + "react/jsx-tag-spacing": ["error"], + "react/jsx-curly-spacing": [2, "never"], + "react/jsx-equals-spacing": [2, "never"], + "react/jsx-indent-props": [2, 2], + "react/jsx-indent": [2, 2], + "react/jsx-key": 2, + "react/jsx-no-bind": [2, { "allowArrowFunctions": true }], + "react/jsx-no-duplicate-props": [2, { "ignoreCase": true }], + "react/jsx-max-props-per-line": [2, { "maximum": 2 }], + "react/jsx-handler-names": [2, { "eventHandlerPrefix": "on", "eventHandlerPropPrefix": "on" }], + "react/jsx-no-undef": 2, + "react/jsx-pascal-case": 2, + "react/jsx-uses-react": 2, + // Explicitly disabled in case we want to enable them again + "react/no-did-mount-set-state": 0, + "react/no-did-update-set-state": 0, + "react/no-direct-mutation-state": 2, + "react/no-multi-comp": [2, { "ignoreStateless": true }], + "react/no-unknown-property": 2, + "react/prefer-es6-class": 2, + "react/prop-types": 2, + "react/react-in-jsx-scope": 2, + "react/self-closing-comp": 2, + "react/sort-comp": 2, + "react/jsx-wrap-multilines": 2 + } +} diff --git a/frontend/.jsbeautifyrc b/frontend/.jsbeautifyrc new file mode 100644 index 000000000..50aa6aa29 --- /dev/null +++ b/frontend/.jsbeautifyrc @@ -0,0 +1,12 @@ +{ + "js": { + "indent_size": 2, + "indent_char": " ", + "indent_level": 2, + "indent_with_tabs": false, + "preserve_newlines": true, + "brace_style": "collapse", + "max_preserve_newlines": 2, + "jslint_happy": true + } +} \ No newline at end of file diff --git a/frontend/.stylelintrc b/frontend/.stylelintrc new file mode 100644 index 000000000..57775146a --- /dev/null +++ b/frontend/.stylelintrc @@ -0,0 +1,396 @@ +{ +"plugins": [ + "stylelint-order" +], +"ignoreFiles": [ + "frontend/src/Styles/scaffolding.css", + "frontend/src/Content/Fonts/font-awesome.css" +], +"rules": { + "at-rule-empty-line-before": [ + "always", + { + "except": [ + "inside-block" + ] + } + ], + "at-rule-name-case": "lower", + "at-rule-name-newline-after": "always-multi-line", + "at-rule-name-space-after": "always", + "at-rule-no-unknown": [ + true, + { + "ignoreAtRules": [ + "/^add\\-mixin$/", + "/^define\\-mixin$/" + ] + } + ], + "at-rule-no-vendor-prefix": true, + "at-rule-semicolon-newline-after": "always", + "at-rule-semicolon-space-before": "never", + "block-closing-brace-empty-line-before": "never", + "block-closing-brace-newline-after": "always", + "block-closing-brace-newline-before": "always", + "block-closing-brace-space-after": "always-single-line", + "block-closing-brace-space-before": "always-single-line", + "block-no-empty": true, + "block-opening-brace-newline-after": "always", + "block-opening-brace-newline-before": "never-single-line", + "block-opening-brace-space-after": "always-single-line", + "block-opening-brace-space-before": "always", + "color-hex-case": "lower", + "color-hex-length": "short", + "color-named": "never", + "color-no-invalid-hex": true, + "comment-whitespace-inside": "always", + "declaration-bang-space-after": "never", + "declaration-bang-space-before": "always", + "declaration-block-no-duplicate-properties": [ + true, + { + "ignoreProperties": [ + "composes" + ] + } + ], + "declaration-block-no-redundant-longhand-properties": true, + "declaration-block-no-shorthand-property-overrides": true, + "declaration-block-semicolon-newline-after": "always", + "declaration-block-semicolon-newline-before": "never-multi-line", + "declaration-block-semicolon-space-before": "never", + "declaration-block-single-line-max-declarations": 1, + "declaration-block-trailing-semicolon": "always", + "declaration-colon-space-after": "always", + "declaration-colon-space-before": "never", + "font-family-name-quotes": "always-unless-keyword", + "function-calc-no-unspaced-operator": true, + "function-comma-newline-after": "never-multi-line", + "function-comma-newline-before": "never-multi-line", + "function-comma-space-after": "always", + "function-comma-space-before": "never", + "function-linear-gradient-no-nonstandard-direction": true, + "function-name-case": "lower", + "function-parentheses-newline-inside": "never-multi-line", + "function-parentheses-space-inside": "never", + "function-url-quotes": "always", + "function-url-scheme-blacklist": [ + "data" + ], + "function-whitespace-after": "always", + "indentation": 2, + "keyframe-declaration-no-important": true, + "length-zero-no-unit": true, + "max-empty-lines": 1, + "max-line-length": [ + 100, + { + "ignore": [ + "non-comments" + ] + } + ], + "max-nesting-depth": 2, + "media-feature-colon-space-after": "always", + "media-feature-colon-space-before": "never", + "media-feature-name-case": "lower", + "media-feature-name-no-vendor-prefix": true, + "media-feature-range-operator-space-after": "always", + "media-feature-range-operator-space-before": "always", + "no-empty-source": true, + "no-eol-whitespace": true, + "no-extra-semicolons": true, + "no-invalid-double-slash-comments": true, + "no-missing-end-of-source-newline": true, + "number-leading-zero": "always", + "number-no-trailing-zeros": true, + "order/order": [ + "custom-properties", + "dollar-variables", + { + "hasBlock": false, + "name": "add-mixin", + "type": "at-rule" + }, + "declarations", + "rules", + "at-rules" + ], + "order/properties-order": [ + { + "emptyLineBefore": "always", + "properties": [ + "composes" + ] + }, + { + "emptyLineBefore": "always", + "properties": [ + "position", + "top", + "right", + "bottom", + "left", + "z-index", + "display", + "visibility", + "align-content", + "align-items", + "align-self", + "justify-content", + "flex", + "flex-direction", + "flex-order", + "flex-pack", + "flex-align", + "flex-grow", + "flex-shrink", + "flex-basis", + "flex-wrap", + "flex-flow", + "float", + "clear", + "overflow", + "overflow-x", + "overflow-y", + "-webkit-overflow-scrolling", + "clip", + "box-sizing", + "margin", + "margin-top", + "margin-right", + "margin-bottom", + "margin-left", + "padding", + "padding-top", + "padding-right", + "padding-bottom", + "padding-left", + "min-width", + "min-height", + "max-width", + "max-height", + "width", + "height", + "outline", + "outline-width", + "outline-style", + "outline-color", + "outline-offset", + "border", + "border-spacing", + "border-collapse", + "border-width", + "border-style", + "border-color", + "border-top", + "border-top-width", + "border-top-style", + "border-top-color", + "border-right", + "border-right-width", + "border-right-style", + "border-right-color", + "border-bottom", + "border-bottom-width", + "border-bottom-style", + "border-bottom-color", + "border-left", + "border-left-width", + "border-left-style", + "border-left-color", + "border-radius", + "border-top-left-radius", + "border-top-right-radius", + "border-bottom-right-radius", + "border-bottom-left-radius", + "border-image", + "border-image-source", + "border-image-slice", + "border-image-width", + "border-image-outset", + "border-image-repeat", + "border-top-image", + "border-right-image", + "border-bottom-image", + "border-left-image", + "border-corner-image", + "border-top-left-image", + "border-top-right-image", + "border-bottom-right-image", + "border-bottom-left-image", + "background", + "background-color", + "background-image", + "background-attachment", + "background-position", + "background-position-x", + "background-position-y", + "background-clip", + "background-origin", + "background-size", + "background-repeat", + "box-decoration-break", + "box-shadow", + "color", + "table-layout", + "caption-side", + "empty-cells", + "list-style", + "list-style-position", + "list-style-type", + "list-style-image", + "quotes", + "content", + "counter-increment", + "counter-reset", + "-ms-writing-mode", + "vertical-align", + "text-align", + "text-align-last", + "text-decoration", + "text-emphasis", + "text-emphasis-position", + "text-emphasis-style", + "text-emphasis-color", + "text-indent", + "text-justify", + "text-outline", + "text-transform", + "text-wrap", + "text-overflow", + "text-overflow-ellipsis", + "text-overflow-mode", + "text-shadow", + "white-space", + "word-spacing", + "word-wrap", + "word-break", + "tab-size", + "hyphens", + "letter-spacing", + "font", + "font-weight", + "font-style", + "font-variant", + "font-size-adjust", + "font-stretch", + "font-size", + "font-family", + "font-smoothing", + "-moz-osx-font-smoothing", + "-webkit-font-smoothing", + "src", + "line-height", + "opacity", + "filter", + "resize", + "cursor", + "appearance", + "nav-index", + "nav-up", + "nav-right", + "nav-down", + "nav-left", + "transition", + "transition-delay", + "transition-timing-function", + "transition-duration", + "transition-property", + "transform", + "transform-origin", + "transform-style", + "backface-visibility", + "animation", + "animation-name", + "animation-duration", + "animation-play-state", + "animation-timing-function", + "animation-delay", + "animation-iteration-count", + "animation-direction", + "animation-fill-mode", + "pointer-events", + "user-select", + "touch-action", + "-webkit-tap-highlight-color", + "unicode-bidi", + "direction", + "columns", + "column-span", + "column-width", + "column-count", + "column-fill", + "column-gap", + "column-rule", + "column-rule-width", + "column-rule-style", + "column-rule-color", + "break-before", + "break-inside", + "break-after", + "page-break-before", + "page-break-inside", + "page-break-after", + "orphans", + "widows", + "zoom", + "max-zoom", + "min-zoom", + "user-zoom", + "orientation" + ] + } + ], + "property-case": "lower", + "property-no-vendor-prefix": true, + "rule-empty-line-before": [ + "always", + { + "except": [ + "first-nested" + ], + "ignore": [ + "after-comment" + ] + } + ], + "selector-attribute-brackets-space-inside": "never", + "selector-attribute-operator-space-after": "never", + "selector-attribute-operator-space-before": "never", + "selector-attribute-quotes": "never", + "selector-class-pattern": "^[A-Za-z0-9]+$", + "selector-combinator-space-after": "always", + "selector-combinator-space-before": "always", + "selector-descendant-combinator-no-non-space": true, + "selector-list-comma-newline-after": "always", + "selector-list-comma-newline-before": "never-multi-line", + "selector-list-comma-space-before": "never", + "selector-max-attribute": 0, + "selector-max-class": 3, + "selector-max-compound-selectors": 3, + "selector-max-empty-lines": 0, + "selector-max-id": 0, + "selector-max-universal": 0, + "selector-pseudo-class-case": "lower", + "selector-pseudo-class-parentheses-space-inside": "never", + "selector-pseudo-element-case": "lower", + "selector-pseudo-element-colon-notation": "double", + "selector-pseudo-element-no-unknown": true, + "selector-type-case": "lower", + "selector-type-no-unknown": true, + "shorthand-property-no-redundant-values": true, + "string-no-newline": true, + "string-quotes": "single", + "time-min-milliseconds": 100, + "unit-case": "lower", + "unit-no-unknown": true, + "value-list-comma-newline-after": "never-multi-line", + "value-list-comma-newline-before": "never-multi-line", + "value-list-comma-space-after": "always", + "value-list-comma-space-before": "never", + "value-list-max-empty-lines": 0, + "value-no-vendor-prefix": true + } +} diff --git a/frontend/.tern-project b/frontend/.tern-project new file mode 100644 index 000000000..aa9d76407 --- /dev/null +++ b/frontend/.tern-project @@ -0,0 +1,7 @@ +{ + "ecmaVersion": 6, + "libs": [ + "browser", + "jquery" + ] +} diff --git a/frontend/gulp/build.js b/frontend/gulp/build.js new file mode 100644 index 000000000..cfeb5d138 --- /dev/null +++ b/frontend/gulp/build.js @@ -0,0 +1,15 @@ +const gulp = require('gulp'); +const runSequence = require('run-sequence'); + +require('./clean'); +require('./copy'); + +gulp.task('build', () => { + return runSequence('clean', [ + 'webpack', + 'copyHtml', + 'copyFonts', + 'copyImages', + 'copyJs' + ]); +}); diff --git a/frontend/gulp/clean.js b/frontend/gulp/clean.js new file mode 100644 index 000000000..ac2e4026f --- /dev/null +++ b/frontend/gulp/clean.js @@ -0,0 +1,8 @@ +const gulp = require('gulp'); +const del = require('del'); + +const paths = require('./helpers/paths'); + +gulp.task('clean', () => { + return del([paths.dest.root]); +}); diff --git a/frontend/gulp/copy.js b/frontend/gulp/copy.js new file mode 100644 index 000000000..d1d47c97e --- /dev/null +++ b/frontend/gulp/copy.js @@ -0,0 +1,45 @@ +var path = require('path'); +var gulp = require('gulp'); +var print = require('gulp-print'); +var cache = require('gulp-cached'); +var livereload = require('gulp-livereload'); +var paths = require('./helpers/paths.js'); + +gulp.task('copyJs', () => { + return gulp.src( + [ + path.join(paths.src.root, 'polyfills.js') + ]) + .pipe(cache('copyJs')) + .pipe(print()) + .pipe(gulp.dest(paths.dest.root)) + .pipe(livereload()); +}); + +gulp.task('copyHtml', () => { + return gulp.src(paths.src.html) + .pipe(cache('copyHtml')) + .pipe(print()) + .pipe(gulp.dest(paths.dest.root)) + .pipe(livereload()); +}); + +gulp.task('copyFonts', () => { + return gulp.src( + path.join(paths.src.fonts, '**', '*.*') + ) + .pipe(cache('copyFonts')) + .pipe(print()) + .pipe(gulp.dest(paths.dest.fonts)) + .pipe(livereload()); +}); + +gulp.task('copyImages', () => { + return gulp.src( + path.join(paths.src.images, '**', '*.*') + ) + .pipe(cache('copyImages')) + .pipe(print()) + .pipe(gulp.dest(paths.dest.images)) + .pipe(livereload()); +}); diff --git a/frontend/gulp/gulpFile.js b/frontend/gulp/gulpFile.js new file mode 100644 index 000000000..744dd8d7e --- /dev/null +++ b/frontend/gulp/gulpFile.js @@ -0,0 +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'); diff --git a/frontend/gulp/helpers/errorHandler.js b/frontend/gulp/helpers/errorHandler.js new file mode 100644 index 000000000..f3e1c113b --- /dev/null +++ b/frontend/gulp/helpers/errorHandler.js @@ -0,0 +1,6 @@ +const gulpUtil = require('gulp-util'); + +module.exports = function errorHandler(error) { + gulpUtil.log(gulpUtil.colors.red(`Error (${error.plugin}): ${error.message}`)); + this.emit('end'); +}; diff --git a/frontend/gulp/helpers/html-annotate-loader.js b/frontend/gulp/helpers/html-annotate-loader.js new file mode 100644 index 000000000..6c7ce10b8 --- /dev/null +++ b/frontend/gulp/helpers/html-annotate-loader.js @@ -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 =` + + ${source} + `; + + return wrappedSource; +}; diff --git a/frontend/gulp/helpers/paths.js b/frontend/gulp/helpers/paths.js new file mode 100644 index 000000000..b96b5aaeb --- /dev/null +++ b/frontend/gulp/helpers/paths.js @@ -0,0 +1,23 @@ +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/', + exclude: { + libs: `!${root}JsLibraries/**` + } + }, + dest: { + root: './_output/UI/', + content: './_output/UI/Content/', + fonts: './_output/UI/Content/Fonts/', + images: './_output/UI/Content/Images/' + } +}; + +module.exports = paths; diff --git a/frontend/gulp/imageMin.js b/frontend/gulp/imageMin.js new file mode 100644 index 000000000..828143f28 --- /dev/null +++ b/frontend/gulp/imageMin.js @@ -0,0 +1,15 @@ +var gulp = require('gulp'); +var print = require('gulp-print'); +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/')); +}); diff --git a/frontend/gulp/start.js b/frontend/gulp/start.js new file mode 100644 index 000000000..e2f65660b --- /dev/null +++ b/frontend/gulp/start.js @@ -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.lidarr.audio/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 + '/Lidarr/*.*') + .pipe(gulp.dest('./_output/')); + }); + }); + }); +}); + +gulp.task('startSonarr', function() { + var ls = spawn('mono', ['--debug', './_output/Lidarr.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); + }); +}); diff --git a/frontend/gulp/stripBom.js b/frontend/gulp/stripBom.js new file mode 100644 index 000000000..080b86dfe --- /dev/null +++ b/frontend/gulp/stripBom.js @@ -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); +}); diff --git a/frontend/gulp/watch.js b/frontend/gulp/watch.js new file mode 100644 index 000000000..dae893c38 --- /dev/null +++ b/frontend/gulp/watch.js @@ -0,0 +1,27 @@ +var gulp = require('gulp'); +var livereload = require('gulp-livereload'); +var watch = require('gulp-watch'); +var paths = require('./helpers/paths.js'); + +require('./copy.js'); +require('./webpack.js'); + +function watchTask(glob, task) { + var options = { + name: `watch: ${task}`, + verbose: true + }; + return watch(glob, options, () => { + gulp.start(task); + }); +} + +gulp.task('watch', ['copyHtml', 'copyFonts', 'copyImages', 'copyJs'], () => { + livereload.listen(); + + gulp.start('webpackWatch'); + + watchTask(paths.src.html, 'copyHtml'); + watchTask(paths.src.fonts + '**/*.*', 'copyFonts'); + watchTask(paths.src.images + '**/*.*', 'copyImages'); +}); diff --git a/frontend/gulp/webpack.js b/frontend/gulp/webpack.js new file mode 100644 index 000000000..5faec4c87 --- /dev/null +++ b/frontend/gulp/webpack.js @@ -0,0 +1,195 @@ +const gulp = require('gulp'); +const webpackStream = require('webpack-stream'); +const livereload = require('gulp-livereload'); +const path = require('path'); +const webpack = require('webpack'); +const errorHandler = require('./helpers/errorHandler'); +const ExtractTextPlugin = require('extract-text-webpack-plugin'); + +const uiFolder = 'UI'; +const root = path.join(__dirname, '..', 'src'); +const isProduction = process.argv.indexOf('--production') > -1; + +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' +].map(require.resolve); + +const extractCSSPlugin = new ExtractTextPlugin({ + filename: path.join('_output', uiFolder, 'Content', 'styles.css'), + allChunks: true, + disable: false, + ignoreOrder: true +}); + +const config = { + devtool: '#source-map', + stats: { + children: false + }, + watchOptions: { + ignored: /node_modules/ + }, + entry: { + preload: 'preload.js', + vendor: 'vendor.js', + index: 'index.js' + }, + resolve: { + modules: [ + root, + path.join(root, 'Shims'), + 'node_modules' + ], + alias: { + jquery: 'jquery/src/jquery' + } + }, + output: { + filename: path.join('_output', uiFolder, '[name].js'), + sourceMapFilename: '[file].map' + }, + plugins: [ + extractCSSPlugin, + new webpack.optimize.CommonsChunkPlugin({ + name: 'vendor' + }), + + new webpack.DefinePlugin({ + __DEV__: !isProduction, + 'process.env': { + NODE_ENV: isProduction ? JSON.stringify('production') : JSON.stringify('development') + } + }) + ], + resolveLoader: { + modules: [ + 'node_modules', + 'frontend/gulp/webpack/' + ] + }, + // TODO: Do we need this loader? + // eslint: { + // formatter: function(results) { + // return JSON.stringify(results); + // } + // }, + module: { + rules: [ + { + test: /\.js?$/, + exclude: /(node_modules|JsLibraries)/, + 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: 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' + } + } + } + ] + }) + }, + + // Global styles + { + test: /\.css$/, + include: /(node_modules|globals.css)/, + use: [ + 'style-loader', + { + loader: 'css-loader' + } + ] + }, + + // Fonts + { + test: /\.woff(2)?(\?v=[0-9]\.[0-9]\.[0-9])?$/, + use: [ + { + loader: 'url-loader', + options: { + limit: 10240, + mimetype: 'application/font-woff', + emitFile: false, + name: 'Content/Fonts/[name].[ext]' + } + } + ] + }, + + { + test: /\.(ttf|eot|eot?#iefix|svg)(\?v=[0-9]\.[0-9]\.[0-9])?$/, + use: [ + { + loader: 'file-loader', + options: { + emitFile: false, + name: 'Content/Fonts/[name].[ext]' + } + } + ] + } + ] + } +}; + +gulp.task('webpack', () => { + return gulp.src('index.js') + .pipe(webpackStream(config)) + .pipe(gulp.dest('')); +}); + +gulp.task('webpackWatch', () => { + config.watch = true; + return gulp.src('') + .pipe(webpackStream(config)) + .on('error', errorHandler) + .pipe(gulp.dest('')) + .on('error', errorHandler) + .pipe(livereload()) + .on('error', errorHandler); +}); diff --git a/frontend/gulp/webpack/css-variables-loader.js b/frontend/gulp/webpack/css-variables-loader.js new file mode 100644 index 000000000..5683c98be --- /dev/null +++ b/frontend/gulp/webpack/css-variables-loader.js @@ -0,0 +1,11 @@ +const loaderUtils = require('loader-utils'); + +module.exports = function cssVariablesLoader(source) { + const options = loaderUtils.getOptions(this); + + options.cssVarsFiles.forEach((cssVarsFile) => { + this.addDependency(cssVarsFile); + }); + + return source; +}; diff --git a/frontend/postcss.config.js b/frontend/postcss.config.js new file mode 100644 index 000000000..f82554ba8 --- /dev/null +++ b/frontend/postcss.config.js @@ -0,0 +1,33 @@ +const reload = require('require-nocache')(module); + +module.exports = (ctx, configPath, options) => { + const config = { + plugins: { + 'postcss-mixins': { + mixinsDir: [ + 'frontend/src/Styles/Mixins' + ] + }, + 'postcss-simple-vars': { + variables: () => + ctx.options.cssVarsFiles.reduce((acc, vars) => { + return Object.assign(acc, reload(vars)); + }, {}) + }, + 'postcss-nested': {}, + autoprefixer: { + browsers: [ + 'Chrome >= 30', + 'Firefox >= 30', + 'Safari >= 6', + 'Edge >= 12', + 'Explorer >= 11', + 'iOS >= 7', + 'Android >= 4.4' + ] + } + } + }; + + return config; +}; diff --git a/frontend/src/.vscode/settings.json b/frontend/src/.vscode/settings.json new file mode 100644 index 000000000..0fb2bf460 --- /dev/null +++ b/frontend/src/.vscode/settings.json @@ -0,0 +1,4 @@ +// Place your settings in this file to overwrite default and user settings. +{ + "files.insertFinalNewline": true +} \ No newline at end of file diff --git a/frontend/src/Activity/Blacklist/Blacklist.js b/frontend/src/Activity/Blacklist/Blacklist.js new file mode 100644 index 000000000..e3ecd2ff7 --- /dev/null +++ b/frontend/src/Activity/Blacklist/Blacklist.js @@ -0,0 +1,110 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { icons } from 'Helpers/Props'; +import LoadingIndicator from 'Components/Loading/LoadingIndicator'; +import Table from 'Components/Table/Table'; +import TableBody from 'Components/Table/TableBody'; +import TablePager from 'Components/Table/TablePager'; +import PageContent from 'Components/Page/PageContent'; +import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector'; +import PageToolbar from 'Components/Page/Toolbar/PageToolbar'; +import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection'; +import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton'; +import BlacklistRowConnector from './BlacklistRowConnector'; + +class Blacklist extends Component { + + // + // Render + + render() { + const { + isFetching, + isPopulated, + error, + items, + columns, + totalRecords, + isClearingBlacklistExecuting, + onClearBlacklistPress, + ...otherProps + } = this.props; + + return ( + + + + + + + + + { + isFetching && !isPopulated && + + } + + { + !isFetching && !!error && +
Unable to load blacklist
+ } + + { + isPopulated && !error && !items.length && +
+ No history blacklist +
+ } + + { + isPopulated && !error && !!items.length && +
+ + + { + items.map((item) => { + return ( + + ); + }) + } + +
+ + +
+ } +
+
+ ); + } +} + +Blacklist.propTypes = { + isFetching: PropTypes.bool.isRequired, + isPopulated: PropTypes.bool.isRequired, + error: PropTypes.object, + items: PropTypes.arrayOf(PropTypes.object).isRequired, + columns: PropTypes.arrayOf(PropTypes.object).isRequired, + totalRecords: PropTypes.number, + isClearingBlacklistExecuting: PropTypes.bool.isRequired, + onClearBlacklistPress: PropTypes.func.isRequired +}; + +export default Blacklist; diff --git a/frontend/src/Activity/Blacklist/BlacklistConnector.js b/frontend/src/Activity/Blacklist/BlacklistConnector.js new file mode 100644 index 000000000..82c0d7ba4 --- /dev/null +++ b/frontend/src/Activity/Blacklist/BlacklistConnector.js @@ -0,0 +1,133 @@ +import _ from 'lodash'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import { registerPagePopulator, unregisterPagePopulator } from 'Utilities/pagePopulator'; +import createCommandsSelector from 'Store/Selectors/createCommandsSelector'; +import * as blacklistActions from 'Store/Actions/blacklistActions'; +import { executeCommand } from 'Store/Actions/commandActions'; +import * as commandNames from 'Commands/commandNames'; +import Blacklist from './Blacklist'; + +function createMapStateToProps() { + return createSelector( + (state) => state.blacklist, + createCommandsSelector(), + (blacklist, commands) => { + const isClearingBlacklistExecuting = _.some(commands, { name: commandNames.CLEAR_BLACKLIST }); + + return { + isClearingBlacklistExecuting, + ...blacklist + }; + } + ); +} + +const mapDispatchToProps = { + ...blacklistActions, + executeCommand +}; + +class BlacklistConnector extends Component { + + // + // Lifecycle + + componentDidMount() { + registerPagePopulator(this.repopulate); + this.props.gotoBlacklistFirstPage(); + } + + componentDidUpdate(prevProps) { + if (prevProps.isClearingBlacklistExecuting && !this.props.isClearingBlacklistExecuting) { + this.props.gotoBlacklistFirstPage(); + } + } + + componentWillUnmount() { + unregisterPagePopulator(this.repopulate); + } + + // + // Control + + repopulate = () => { + this.props.fetchBlacklist(); + } + + // + // Listeners + + onFirstPagePress = () => { + this.props.gotoBlacklistFirstPage(); + } + + onPreviousPagePress = () => { + this.props.gotoBlacklistPreviousPage(); + } + + onNextPagePress = () => { + this.props.gotoBlacklistNextPage(); + } + + onLastPagePress = () => { + this.props.gotoBlacklistLastPage(); + } + + onPageSelect = (page) => { + this.props.gotoBlacklistPage({ page }); + } + + onSortPress = (sortKey) => { + this.props.setBlacklistSort({ sortKey }); + } + + onTableOptionChange = (payload) => { + this.props.setBlacklistTableOption(payload); + + if (payload.pageSize) { + this.props.gotoBlacklistFirstPage(); + } + } + + onClearBlacklistPress = () => { + this.props.executeCommand({ name: commandNames.CLEAR_BLACKLIST }); + } + + // + // Render + + render() { + return ( + + ); + } +} + +BlacklistConnector.propTypes = { + isClearingBlacklistExecuting: PropTypes.bool.isRequired, + items: PropTypes.arrayOf(PropTypes.object).isRequired, + fetchBlacklist: PropTypes.func.isRequired, + gotoBlacklistFirstPage: PropTypes.func.isRequired, + gotoBlacklistPreviousPage: PropTypes.func.isRequired, + gotoBlacklistNextPage: PropTypes.func.isRequired, + gotoBlacklistLastPage: PropTypes.func.isRequired, + gotoBlacklistPage: PropTypes.func.isRequired, + setBlacklistSort: PropTypes.func.isRequired, + setBlacklistTableOption: PropTypes.func.isRequired, + executeCommand: PropTypes.func.isRequired +}; + +export default connect(createMapStateToProps, mapDispatchToProps)(BlacklistConnector); diff --git a/frontend/src/Activity/Blacklist/BlacklistDetailsModal.js b/frontend/src/Activity/Blacklist/BlacklistDetailsModal.js new file mode 100644 index 000000000..356512a9d --- /dev/null +++ b/frontend/src/Activity/Blacklist/BlacklistDetailsModal.js @@ -0,0 +1,89 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import Button from 'Components/Link/Button'; +import DescriptionList from 'Components/DescriptionList/DescriptionList'; +import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem'; +import Modal from 'Components/Modal/Modal'; +import ModalContent from 'Components/Modal/ModalContent'; +import ModalHeader from 'Components/Modal/ModalHeader'; +import ModalBody from 'Components/Modal/ModalBody'; +import ModalFooter from 'Components/Modal/ModalFooter'; + +class BlacklistDetailsModal extends Component { + + // + // Render + + render() { + const { + isOpen, + sourceTitle, + protocol, + indexer, + message, + onModalClose + } = this.props; + + return ( + + + + Details + + + + + + + + + { + !!message && + + } + + { + !!message && + + } + + + + + + + + + ); + } +} + +BlacklistDetailsModal.propTypes = { + isOpen: PropTypes.bool.isRequired, + sourceTitle: PropTypes.string.isRequired, + protocol: PropTypes.string.isRequired, + indexer: PropTypes.string, + message: PropTypes.string, + onModalClose: PropTypes.func.isRequired +}; + +export default BlacklistDetailsModal; diff --git a/frontend/src/Activity/Blacklist/BlacklistRow.css b/frontend/src/Activity/Blacklist/BlacklistRow.css new file mode 100644 index 000000000..030dfe98a --- /dev/null +++ b/frontend/src/Activity/Blacklist/BlacklistRow.css @@ -0,0 +1,18 @@ +.language, +.quality { + composes: cell from 'Components/Table/Cells/TableRowCell.css'; + + width: 100px; +} + +.indexer { + composes: cell from 'Components/Table/Cells/TableRowCell.css'; + + width: 80px; +} + +.details { + composes: cell from 'Components/Table/Cells/TableRowCell.css'; + + width: 30px; +} diff --git a/frontend/src/Activity/Blacklist/BlacklistRow.js b/frontend/src/Activity/Blacklist/BlacklistRow.js new file mode 100644 index 000000000..1acbe5133 --- /dev/null +++ b/frontend/src/Activity/Blacklist/BlacklistRow.js @@ -0,0 +1,177 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { icons } from 'Helpers/Props'; +import IconButton from 'Components/Link/IconButton'; +import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector'; +import TableRow from 'Components/Table/TableRow'; +import TableRowCell from 'Components/Table/Cells/TableRowCell'; +import EpisodeLanguage from 'Album/EpisodeLanguage'; +import EpisodeQuality from 'Album/EpisodeQuality'; +import ArtistNameLink from 'Artist/ArtistNameLink'; +import BlacklistDetailsModal from './BlacklistDetailsModal'; +import styles from './BlacklistRow.css'; + +class BlacklistRow extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + isDetailsModalOpen: false + }; + } + + // + // Listeners + + onDetailsPress = () => { + this.setState({ isDetailsModalOpen: true }); + } + + onDetailsModalClose = () => { + this.setState({ isDetailsModalOpen: false }); + } + + // + // Render + + render() { + const { + artist, + sourceTitle, + language, + quality, + date, + protocol, + indexer, + message, + columns + } = this.props; + + return ( + + { + columns.map((column) => { + const { + name, + isVisible + } = column; + + if (!isVisible) { + return null; + } + + if (name === 'artist.sortName') { + return ( + + + + ); + } + + if (name === 'sourceTitle') { + return ( + + {sourceTitle} + + ); + } + + if (name === 'language') { + return ( + + + + ); + } + + if (name === 'quality') { + return ( + + + + ); + } + + if (name === 'date') { + return ( + + ); + } + + if (name === 'indexer') { + return ( + + {indexer} + + ); + } + + if (name === 'details') { + return ( + + + + ); + } + + return null; + }) + } + + + + ); + } + +} + +BlacklistRow.propTypes = { + id: PropTypes.number.isRequired, + artist: PropTypes.object.isRequired, + sourceTitle: PropTypes.string.isRequired, + language: PropTypes.object.isRequired, + quality: PropTypes.object.isRequired, + date: PropTypes.string.isRequired, + protocol: PropTypes.string.isRequired, + indexer: PropTypes.string, + message: PropTypes.string, + columns: PropTypes.arrayOf(PropTypes.object).isRequired +}; + +export default BlacklistRow; diff --git a/frontend/src/Activity/Blacklist/BlacklistRowConnector.js b/frontend/src/Activity/Blacklist/BlacklistRowConnector.js new file mode 100644 index 000000000..f4f9217bf --- /dev/null +++ b/frontend/src/Activity/Blacklist/BlacklistRowConnector.js @@ -0,0 +1,17 @@ +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import createArtistSelector from 'Store/Selectors/createArtistSelector'; +import BlacklistRow from './BlacklistRow'; + +function createMapStateToProps() { + return createSelector( + createArtistSelector(), + (artist) => { + return { + artist + }; + } + ); +} + +export default connect(createMapStateToProps)(BlacklistRow); diff --git a/frontend/src/Activity/History/Details/HistoryDetails.js b/frontend/src/Activity/History/Details/HistoryDetails.js new file mode 100644 index 000000000..b2adf1171 --- /dev/null +++ b/frontend/src/Activity/History/Details/HistoryDetails.js @@ -0,0 +1,237 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import formatDateTime from 'Utilities/Date/formatDateTime'; +import formatAge from 'Utilities/Number/formatAge'; +import Link from 'Components/Link/Link'; +import DescriptionList from 'Components/DescriptionList/DescriptionList'; +import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem'; +import DescriptionListItemTitle from 'Components/DescriptionList/DescriptionListItemTitle'; +import DescriptionListItemDescription from 'Components/DescriptionList/DescriptionListItemDescription'; + +function HistoryDetails(props) { + const { + eventType, + sourceTitle, + data, + shortDateFormat, + timeFormat + } = props; + + if (eventType === 'grabbed') { + const { + indexer, + releaseGroup, + nzbInfoUrl, + downloadClient, + downloadId, + age, + ageHours, + ageMinutes, + publishedDate + } = data; + + return ( + + + + { + !!indexer && + + } + + { + !!releaseGroup && + + } + + { + !!nzbInfoUrl && + + + Info URL + + + + {nzbInfoUrl} + + + } + + { + !!downloadClient && + + } + + { + !!downloadId && + + } + + { + !!indexer && + + } + + { + !!publishedDate && + + } + + ); + } + + if (eventType === 'downloadFailed') { + const { + message + } = data; + + return ( + + + + { + !!message && + + } + + ); + } + + if (eventType === 'downloadFolderImported') { + const { + droppedPath, + importedPath + } = data; + + return ( + + + + { + !!droppedPath && + + } + + { + !!importedPath && + + } + + ); + } + + if (eventType === 'trackFileDeleted') { + const { + reason + } = data; + + let reasonMessage = ''; + + switch (reason) { + case 'Manual': + reasonMessage = 'File was deleted by via UI'; + break; + case 'MissingFromDisk': + reasonMessage = 'Lidarr was unable to find the file on disk so it was removed'; + break; + case 'Upgrade': + reasonMessage = 'File was deleted to import an upgrade'; + break; + default: + reasonMessage = ''; + } + + return ( + + + + + + ); + } + + if (eventType === 'trackFileRenamed') { + const { + sourcePath, + sourceRelativePath, + path, + relativePath + } = data; + + return ( + + + + + + + + + + ); + } +} + +HistoryDetails.propTypes = { + eventType: PropTypes.string.isRequired, + sourceTitle: PropTypes.string.isRequired, + data: PropTypes.object.isRequired, + shortDateFormat: PropTypes.string.isRequired, + timeFormat: PropTypes.string.isRequired +}; + +export default HistoryDetails; diff --git a/frontend/src/Activity/History/Details/HistoryDetailsConnector.js b/frontend/src/Activity/History/Details/HistoryDetailsConnector.js new file mode 100644 index 000000000..0848c7905 --- /dev/null +++ b/frontend/src/Activity/History/Details/HistoryDetailsConnector.js @@ -0,0 +1,19 @@ +import _ from 'lodash'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector'; +import HistoryDetails from './HistoryDetails'; + +function createMapStateToProps() { + return createSelector( + createUISettingsSelector(), + (uiSettings) => { + return _.pick(uiSettings, [ + 'shortDateFormat', + 'timeFormat' + ]); + } + ); +} + +export default connect(createMapStateToProps)(HistoryDetails); diff --git a/frontend/src/Activity/History/Details/HistoryDetailsModal.css b/frontend/src/Activity/History/Details/HistoryDetailsModal.css new file mode 100644 index 000000000..bdcb7f918 --- /dev/null +++ b/frontend/src/Activity/History/Details/HistoryDetailsModal.css @@ -0,0 +1,5 @@ +.markAsFailedButton { + composes: button from 'Components/Link/Button.css'; + + margin-right: auto; +} diff --git a/frontend/src/Activity/History/Details/HistoryDetailsModal.js b/frontend/src/Activity/History/Details/HistoryDetailsModal.js new file mode 100644 index 000000000..ca8b9ca3a --- /dev/null +++ b/frontend/src/Activity/History/Details/HistoryDetailsModal.js @@ -0,0 +1,104 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import { kinds } from 'Helpers/Props'; +import Button from 'Components/Link/Button'; +import SpinnerButton from 'Components/Link/SpinnerButton'; +import Modal from 'Components/Modal/Modal'; +import ModalContent from 'Components/Modal/ModalContent'; +import ModalHeader from 'Components/Modal/ModalHeader'; +import ModalBody from 'Components/Modal/ModalBody'; +import ModalFooter from 'Components/Modal/ModalFooter'; +import HistoryDetails from './HistoryDetails'; +import styles from './HistoryDetailsModal.css'; + +function getHeaderTitle(eventType) { + switch (eventType) { + case 'grabbed': + return 'Grabbed'; + case 'downloadFailed': + return 'Download Failed'; + case 'downloadFolderImported': + return 'Track Imported'; + case 'trackFileDeleted': + return 'Track File Deleted'; + case 'trackFileRenamed': + return 'Track File Renamed'; + default: + return 'Unknown'; + } +} + +function HistoryDetailsModal(props) { + const { + isOpen, + eventType, + sourceTitle, + data, + isMarkingAsFailed, + shortDateFormat, + timeFormat, + onMarkAsFailedPress, + onModalClose + } = props; + + return ( + + + + {getHeaderTitle(eventType)} + + + + + + + + { + eventType === 'grabbed' && + + Mark as Failed + + } + + + + + + ); +} + +HistoryDetailsModal.propTypes = { + isOpen: PropTypes.bool.isRequired, + eventType: PropTypes.string.isRequired, + sourceTitle: PropTypes.string.isRequired, + data: PropTypes.object.isRequired, + isMarkingAsFailed: PropTypes.bool.isRequired, + shortDateFormat: PropTypes.string.isRequired, + timeFormat: PropTypes.string.isRequired, + onMarkAsFailedPress: PropTypes.func.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +HistoryDetailsModal.defaultProps = { + isMarkingAsFailed: false +}; + +export default HistoryDetailsModal; diff --git a/frontend/src/Activity/History/History.js b/frontend/src/Activity/History/History.js new file mode 100644 index 000000000..f8990cfbf --- /dev/null +++ b/frontend/src/Activity/History/History.js @@ -0,0 +1,217 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { align, icons } from 'Helpers/Props'; +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 TablePager from 'Components/Table/TablePager'; +import PageContent from 'Components/Page/PageContent'; +import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector'; +import PageToolbar from 'Components/Page/Toolbar/PageToolbar'; +import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection'; +import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton'; +import FilterMenu from 'Components/Menu/FilterMenu'; +import MenuContent from 'Components/Menu/MenuContent'; +import FilterMenuItem from 'Components/Menu/FilterMenuItem'; +import HistoryRowConnector from './HistoryRowConnector'; + +class History extends Component { + + // + // Lifecycle + + shouldComponentUpdate(nextProps) { + // Don't update when fetching has completed if items have changed, + // before albums start fetching or when albums start fetching. + + if ( + ( + this.props.isFetching && + nextProps.isPopulated && + hasDifferentItems(this.props.items, nextProps.items) + ) || + (!this.props.isAlbumsFetching && nextProps.isAlbumsFetching) + ) { + return false; + } + + return true; + } + + // + // Render + + render() { + const { + isFetching, + isPopulated, + error, + items, + columns, + filterKey, + filterValue, + totalRecords, + isAlbumsFetching, + isAlbumsPopulated, + episodesError, + onFilterSelect, + onFirstPagePress, + ...otherProps + } = this.props; + + const isFetchingAny = isFetching || isAlbumsFetching; + const isAllPopulated = isPopulated && (isAlbumsPopulated || !items.length); + const hasError = error || episodesError; + + return ( + + + + + + + + + + + All + + + + Grabbed + + + + Imported + + + + Failed + + + + Deleted + + + + Renamed + + + + + + + + { + isFetchingAny && !isAllPopulated && + + } + + { + !isFetchingAny && hasError && +
Unable to load history
+ } + + { + // If history isPopulated and it's empty show no history found and don't + // wait for the albums to populate because they are never coming. + + isPopulated && !hasError && !items.length && +
+ No history found +
+ } + + { + isAllPopulated && !hasError && !!items.length && +
+ + + { + items.map((item) => { + return ( + + ); + }) + } + +
+ + +
+ } +
+
+ ); + } +} + +History.propTypes = { + isFetching: PropTypes.bool.isRequired, + isPopulated: PropTypes.bool.isRequired, + error: PropTypes.object, + items: PropTypes.arrayOf(PropTypes.object).isRequired, + columns: PropTypes.arrayOf(PropTypes.object).isRequired, + filterKey: PropTypes.string, + filterValue: PropTypes.string, + totalRecords: PropTypes.number, + isAlbumsFetching: PropTypes.bool.isRequired, + isAlbumsPopulated: PropTypes.bool.isRequired, + episodesError: PropTypes.object, + onFilterSelect: PropTypes.func.isRequired, + onFirstPagePress: PropTypes.func.isRequired +}; + +export default History; diff --git a/frontend/src/Activity/History/HistoryConnector.js b/frontend/src/Activity/History/HistoryConnector.js new file mode 100644 index 000000000..c3eb9d8b1 --- /dev/null +++ b/frontend/src/Activity/History/HistoryConnector.js @@ -0,0 +1,138 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import { registerPagePopulator, unregisterPagePopulator } from 'Utilities/pagePopulator'; +import hasDifferentItems from 'Utilities/Object/hasDifferentItems'; +import selectUniqueIds from 'Utilities/Object/selectUniqueIds'; +import * as historyActions from 'Store/Actions/historyActions'; +import { fetchEpisodes, clearEpisodes } from 'Store/Actions/episodeActions'; +import History from './History'; + +function createMapStateToProps() { + return createSelector( + (state) => state.history, + (state) => state.episodes, + (history, episodes) => { + return { + isAlbumsFetching: episodes.isFetching, + isAlbumsPopulated: episodes.isPopulated, + episodesError: episodes.error, + ...history + }; + } + ); +} + +const mapDispatchToProps = { + ...historyActions, + fetchEpisodes, + clearEpisodes +}; + +class HistoryConnector extends Component { + + // + // Lifecycle + + componentDidMount() { + registerPagePopulator(this.repopulate); + this.props.gotoHistoryFirstPage(); + } + + componentDidUpdate(prevProps) { + if (hasDifferentItems(prevProps.items, this.props.items)) { + const albumIds = selectUniqueIds(this.props.items, 'albumId'); + this.props.fetchEpisodes({ albumIds }); + } + } + + componentWillUnmount() { + unregisterPagePopulator(this.repopulate); + this.props.clearHistory(); + this.props.clearEpisodes(); + } + + // + // Control + + repopulate = () => { + this.props.fetchHistory(); + } + + // + // Listeners + + onFirstPagePress = () => { + this.props.gotoHistoryFirstPage(); + } + + onPreviousPagePress = () => { + this.props.gotoHistoryPreviousPage(); + } + + onNextPagePress = () => { + this.props.gotoHistoryNextPage(); + } + + onLastPagePress = () => { + this.props.gotoHistoryLastPage(); + } + + onPageSelect = (page) => { + this.props.gotoHistoryPage({ page }); + } + + onSortPress = (sortKey) => { + this.props.setHistorySort({ sortKey }); + } + + onFilterSelect = (filterKey, filterValue) => { + this.props.setHistoryFilter({ filterKey, filterValue }); + } + + onTableOptionChange = (payload) => { + this.props.setHistoryTableOption(payload); + + if (payload.pageSize) { + this.props.gotoHistoryFirstPage(); + } + } + + // + // Render + + render() { + return ( + + ); + } +} + +HistoryConnector.propTypes = { + items: PropTypes.arrayOf(PropTypes.object).isRequired, + fetchHistory: PropTypes.func.isRequired, + gotoHistoryFirstPage: PropTypes.func.isRequired, + gotoHistoryPreviousPage: PropTypes.func.isRequired, + gotoHistoryNextPage: PropTypes.func.isRequired, + gotoHistoryLastPage: PropTypes.func.isRequired, + gotoHistoryPage: PropTypes.func.isRequired, + setHistorySort: PropTypes.func.isRequired, + setHistoryFilter: PropTypes.func.isRequired, + setHistoryTableOption: PropTypes.func.isRequired, + clearHistory: PropTypes.func.isRequired, + fetchEpisodes: PropTypes.func.isRequired, + clearEpisodes: PropTypes.func.isRequired +}; + +export default connect(createMapStateToProps, mapDispatchToProps)(HistoryConnector); diff --git a/frontend/src/Activity/History/HistoryEventTypeCell.css b/frontend/src/Activity/History/HistoryEventTypeCell.css new file mode 100644 index 000000000..086354783 --- /dev/null +++ b/frontend/src/Activity/History/HistoryEventTypeCell.css @@ -0,0 +1,3 @@ +.cell { + width: 35px; +} diff --git a/frontend/src/Activity/History/HistoryEventTypeCell.js b/frontend/src/Activity/History/HistoryEventTypeCell.js new file mode 100644 index 000000000..065ab0492 --- /dev/null +++ b/frontend/src/Activity/History/HistoryEventTypeCell.js @@ -0,0 +1,82 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import { icons, kinds } from 'Helpers/Props'; +import Icon from 'Components/Icon'; +import TableRowCell from 'Components/Table/Cells/TableRowCell'; +import styles from './HistoryEventTypeCell.css'; + +function getIconName(eventType) { + switch (eventType) { + case 'grabbed': + return icons.DOWNLOADING; + case 'artistFolderImported': + return icons.DRIVE; + case 'downloadFolderImported': + return icons.DOWNLOADED; + case 'downloadFailed': + return icons.DOWNLOADING; + case 'trackFileDeleted': + return icons.DELETE; + case 'trackFileRenamed': + return icons.ORGANIZE; + default: + return icons.UNKNOWN; + } +} + +function getIconKind(eventType) { + switch (eventType) { + case 'downloadFailed': + return kinds.DANGER; + default: + return kinds.DEFAULT; + } +} + +function getTooltip(eventType, data) { + switch (eventType) { + case 'grabbed': + return `Album grabbed from ${data.indexer} and sent to ${data.downloadClient}`; + case 'artistFolderImported': + return 'Track imported from artist folder'; + case 'downloadFolderImported': + return 'Track downloaded successfully and picked up from download client'; + case 'downloadFailed': + return 'Album download failed'; + case 'trackFileDeleted': + return 'Track file deleted'; + case 'trackFileRenamed': + return 'Track file renamed'; + default: + return 'Unknown event'; + } +} + +function HistoryEventTypeCell({ eventType, data }) { + const iconName = getIconName(eventType); + const iconKind = getIconKind(eventType); + const tooltip = getTooltip(eventType, data); + + return ( + + + + ); +} + +HistoryEventTypeCell.propTypes = { + eventType: PropTypes.string.isRequired, + data: PropTypes.object +}; + +HistoryEventTypeCell.defaultProps = { + data: {} +}; + +export default HistoryEventTypeCell; diff --git a/frontend/src/Activity/History/HistoryRow.css b/frontend/src/Activity/History/HistoryRow.css new file mode 100644 index 000000000..83586af58 --- /dev/null +++ b/frontend/src/Activity/History/HistoryRow.css @@ -0,0 +1,23 @@ +.downloadClient { + composes: cell from 'Components/Table/Cells/TableRowCell.css'; + + width: 120px; +} + +.indexer { + composes: cell from 'Components/Table/Cells/TableRowCell.css'; + + width: 80px; +} + +.releaseGroup { + composes: cell from 'Components/Table/Cells/TableRowCell.css'; + + width: 110px; +} + +.details { + composes: cell from 'Components/Table/Cells/TableRowCell.css'; + + width: 30px; +} diff --git a/frontend/src/Activity/History/HistoryRow.js b/frontend/src/Activity/History/HistoryRow.js new file mode 100644 index 000000000..cf48c36d2 --- /dev/null +++ b/frontend/src/Activity/History/HistoryRow.js @@ -0,0 +1,255 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { icons } from 'Helpers/Props'; +import IconButton from 'Components/Link/IconButton'; +import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector'; +import TableRow from 'Components/Table/TableRow'; +import TableRowCell from 'Components/Table/Cells/TableRowCell'; +import episodeEntities from 'Album/episodeEntities'; +import EpisodeTitleLink from 'Album/EpisodeTitleLink'; +import EpisodeLanguage from 'Album/EpisodeLanguage'; +import EpisodeQuality from 'Album/EpisodeQuality'; +import ArtistNameLink from 'Artist/ArtistNameLink'; +import HistoryEventTypeCell from './HistoryEventTypeCell'; +import HistoryDetailsModal from './Details/HistoryDetailsModal'; +import styles from './HistoryRow.css'; + +class HistoryRow extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + isDetailsModalOpen: false + }; + } + + componentDidUpdate(prevProps) { + if ( + prevProps.isMarkingAsFailed && + !this.props.isMarkingAsFailed && + !this.props.markAsFailedError + ) { + this.setState({ isDetailsModalOpen: false }); + } + } + + // + // Listeners + + onDetailsPress = () => { + this.setState({ isDetailsModalOpen: true }); + } + + onDetailsModalClose = () => { + this.setState({ isDetailsModalOpen: false }); + } + + // + // Render + + render() { + const { + albumId, + artist, + album, + track, + language, + quality, + eventType, + sourceTitle, + date, + data, + isMarkingAsFailed, + columns, + shortDateFormat, + timeFormat, + onMarkAsFailedPress + } = this.props; + + if (!album) { + return null; + } + + return ( + + { + columns.map((column) => { + const { + name, + isVisible + } = column; + + if (!isVisible) { + return null; + } + + if (name === 'eventType') { + return ( + + ); + } + + if (name === 'artist.sortName') { + return ( + + + + ); + } + + if (name === 'episodeTitle') { + return ( + + + + ); + } + + if (name === 'trackTitle') { + return ( + + {track.title} + + ); + } + + if (name === 'language') { + return ( + + + + ); + } + + if (name === 'quality') { + return ( + + + + ); + } + + if (name === 'date') { + return ( + + ); + } + + if (name === 'downloadClient') { + return ( + + {data.downloadClient} + + ); + } + + if (name === 'indexer') { + return ( + + {data.indexer} + + ); + } + + if (name === 'releaseGroup') { + return ( + + {data.releaseGroup} + + ); + } + + if (name === 'details') { + return ( + + + + ); + } + + return null; + }) + } + + + + ); + } + +} + +HistoryRow.propTypes = { + albumId: PropTypes.number, + artist: PropTypes.object.isRequired, + album: PropTypes.object, + track: PropTypes.object, + language: PropTypes.object.isRequired, + quality: PropTypes.object.isRequired, + eventType: PropTypes.string.isRequired, + sourceTitle: PropTypes.string.isRequired, + date: PropTypes.string.isRequired, + data: PropTypes.object.isRequired, + isMarkingAsFailed: PropTypes.bool, + markAsFailedError: PropTypes.object, + columns: PropTypes.arrayOf(PropTypes.object).isRequired, + shortDateFormat: PropTypes.string.isRequired, + timeFormat: PropTypes.string.isRequired, + onMarkAsFailedPress: PropTypes.func.isRequired +}; + +HistoryRow.defaultProps = { + track: { + title: '' + } +}; + +export default HistoryRow; diff --git a/frontend/src/Activity/History/HistoryRowConnector.js b/frontend/src/Activity/History/HistoryRowConnector.js new file mode 100644 index 000000000..d8fab748d --- /dev/null +++ b/frontend/src/Activity/History/HistoryRowConnector.js @@ -0,0 +1,75 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import { fetchHistory, markAsFailed } from 'Store/Actions/historyActions'; +import createArtistSelector from 'Store/Selectors/createArtistSelector'; +import createEpisodeSelector from 'Store/Selectors/createEpisodeSelector'; +import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector'; +import HistoryRow from './HistoryRow'; + +function createMapStateToProps() { + return createSelector( + createArtistSelector(), + createEpisodeSelector(), + createUISettingsSelector(), + (artist, album, uiSettings) => { + return { + artist, + album, + shortDateFormat: uiSettings.shortDateFormat, + timeFormat: uiSettings.timeFormat + }; + } + ); +} + +const mapDispatchToProps = { + fetchHistory, + markAsFailed +}; + +class HistoryRowConnector extends Component { + + // + // Lifecycle + + componentDidUpdate(prevProps) { + if ( + prevProps.isMarkingAsFailed && + !this.props.isMarkingAsFailed && + !this.props.markAsFailedError + ) { + this.props.fetchHistory(); + } + } + + // + // Listeners + + onMarkAsFailedPress = () => { + this.props.markAsFailed({ id: this.props.id }); + } + + // + // Render + + render() { + return ( + + ); + } +} + +HistoryRowConnector.propTypes = { + id: PropTypes.number.isRequired, + isMarkingAsFailed: PropTypes.bool, + markAsFailedError: PropTypes.object, + fetchHistory: PropTypes.func.isRequired, + markAsFailed: PropTypes.func.isRequired +}; + +export default connect(createMapStateToProps, mapDispatchToProps)(HistoryRowConnector); diff --git a/frontend/src/Activity/Queue/ProtocolLabel.css b/frontend/src/Activity/Queue/ProtocolLabel.css new file mode 100644 index 000000000..15e8e4fc6 --- /dev/null +++ b/frontend/src/Activity/Queue/ProtocolLabel.css @@ -0,0 +1,13 @@ +.torrent { + composes: label from 'Components/Label.css'; + + border-color: $torrentColor; + background-color: $torrentColor; +} + +.usenet { + composes: label from 'Components/Label.css'; + + border-color: $usenetColor; + background-color: $usenetColor; +} diff --git a/frontend/src/Activity/Queue/ProtocolLabel.js b/frontend/src/Activity/Queue/ProtocolLabel.js new file mode 100644 index 000000000..e8a08943c --- /dev/null +++ b/frontend/src/Activity/Queue/ProtocolLabel.js @@ -0,0 +1,20 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import Label from 'Components/Label'; +import styles from './ProtocolLabel.css'; + +function ProtocolLabel({ protocol }) { + const protocolName = protocol === 'usenet' ? 'nzb' : protocol; + + return ( + + ); +} + +ProtocolLabel.propTypes = { + protocol: PropTypes.string.isRequired +}; + +export default ProtocolLabel; diff --git a/frontend/src/Activity/Queue/Queue.js b/frontend/src/Activity/Queue/Queue.js new file mode 100644 index 000000000..8cfa4c77d --- /dev/null +++ b/frontend/src/Activity/Queue/Queue.js @@ -0,0 +1,264 @@ +import _ from 'lodash'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import hasDifferentItems from 'Utilities/Object/hasDifferentItems'; +import getSelectedIds from 'Utilities/Table/getSelectedIds'; +import selectAll from 'Utilities/Table/selectAll'; +import toggleSelected from 'Utilities/Table/toggleSelected'; +import { icons } from 'Helpers/Props'; +import LoadingIndicator from 'Components/Loading/LoadingIndicator'; +import Table from 'Components/Table/Table'; +import TableBody from 'Components/Table/TableBody'; +import TablePager from 'Components/Table/TablePager'; +import PageContent from 'Components/Page/PageContent'; +import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector'; +import PageToolbar from 'Components/Page/Toolbar/PageToolbar'; +import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection'; +import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton'; +import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator'; +import RemoveQueueItemsModal from './RemoveQueueItemsModal'; +import QueueRowConnector from './QueueRowConnector'; + +class Queue extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + allSelected: false, + allUnselected: false, + lastToggled: null, + selectedState: {}, + isPendingSelected: false, + isConfirmRemoveModalOpen: false + }; + } + + shouldComponentUpdate(nextProps) { + // Don't update when fetching has completed if items have changed, + // before albums start fetching or when albums start fetching. + + if ( + ( + this.props.isFetching && + nextProps.isPopulated && + hasDifferentItems(this.props.items, nextProps.items) + ) || + (!this.props.isAlbumsFetching && nextProps.isAlbumsFetching) + ) { + return false; + } + + return true; + } + + componentDidUpdate(prevProps) { + if (hasDifferentItems(prevProps.items, this.props.items)) { + this.setState({ selectedState: {} }); + return; + } + + const selectedIds = this.getSelectedIds(); + const isPendingSelected = _.some(this.props.items, (item) => { + return selectedIds.indexOf(item.id) > -1 && item.status === 'Delay'; + }); + + if (isPendingSelected !== this.state.isPendingSelected) { + this.setState({ isPendingSelected }); + } + } + + // + // Control + + getSelectedIds = () => { + return getSelectedIds(this.state.selectedState); + } + + // + // Listeners + + onSelectAllChange = ({ value }) => { + this.setState(selectAll(this.state.selectedState, value)); + } + + onSelectedChange = ({ id, value, shiftKey = false }) => { + this.setState((state) => { + return toggleSelected(state, this.props.items, id, value, shiftKey); + }); + } + + onGrabSelectedPress = () => { + this.props.onGrabSelectedPress(this.getSelectedIds()); + } + + onRemoveSelectedPress = () => { + this.setState({ isConfirmRemoveModalOpen: true }); + } + + onRemoveSelectedConfirmed = (blacklist) => { + this.props.onRemoveSelectedPress(this.getSelectedIds(), blacklist); + this.setState({ isConfirmRemoveModalOpen: false }); + } + + onConfirmRemoveModalClose = () => { + this.setState({ isConfirmRemoveModalOpen: false }); + } + + // + // Render + + render() { + const { + isFetching, + isPopulated, + error, + items, + isAlbumsFetching, + isAlbumsPopulated, + episodesError, + columns, + totalRecords, + isGrabbing, + isRemoving, + isCheckForFinishedDownloadExecuting, + onRefreshPress, + ...otherProps + } = this.props; + + const { + allSelected, + allUnselected, + selectedState, + isConfirmRemoveModalOpen, + isPendingSelected + } = this.state; + + const isRefreshing = isFetching || isAlbumsFetching || isCheckForFinishedDownloadExecuting; + const isAllPopulated = isPopulated && (isAlbumsPopulated || !items.length); + const hasError = error || episodesError; + const selectedCount = this.getSelectedIds().length; + const disableSelectedActions = selectedCount === 0; + + return ( + + + + + + + + + + + + + + + { + isRefreshing && !isAllPopulated && + + } + + { + !isRefreshing && hasError && +
+ Failed to load Queue +
+ } + + { + isPopulated && !hasError && !items.length && +
+ Queue is empty +
+ } + + { + isAllPopulated && !hasError && !!items.length && +
+ + + { + items.map((item) => { + return ( + + ); + }) + } + +
+ + +
+ } +
+ + +
+ ); + } +} + +Queue.propTypes = { + isFetching: PropTypes.bool.isRequired, + isPopulated: PropTypes.bool.isRequired, + error: PropTypes.object, + items: PropTypes.arrayOf(PropTypes.object).isRequired, + isAlbumsFetching: PropTypes.bool.isRequired, + isAlbumsPopulated: PropTypes.bool.isRequired, + episodesError: PropTypes.object, + columns: PropTypes.arrayOf(PropTypes.object).isRequired, + totalRecords: PropTypes.number, + isGrabbing: PropTypes.bool.isRequired, + isRemoving: PropTypes.bool.isRequired, + isCheckForFinishedDownloadExecuting: PropTypes.bool.isRequired, + onRefreshPress: PropTypes.func.isRequired, + onGrabSelectedPress: PropTypes.func.isRequired, + onRemoveSelectedPress: PropTypes.func.isRequired +}; + +export default Queue; diff --git a/frontend/src/Activity/Queue/QueueConnector.js b/frontend/src/Activity/Queue/QueueConnector.js new file mode 100644 index 000000000..2e5c36ced --- /dev/null +++ b/frontend/src/Activity/Queue/QueueConnector.js @@ -0,0 +1,162 @@ +import _ from 'lodash'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import { registerPagePopulator, unregisterPagePopulator } from 'Utilities/pagePopulator'; +import hasDifferentItems from 'Utilities/Object/hasDifferentItems'; +import selectUniqueIds from 'Utilities/Object/selectUniqueIds'; +import createCommandsSelector from 'Store/Selectors/createCommandsSelector'; +import { executeCommand } from 'Store/Actions/commandActions'; +import * as queueActions from 'Store/Actions/queueActions'; +import { fetchEpisodes, clearEpisodes } from 'Store/Actions/episodeActions'; +import * as commandNames from 'Commands/commandNames'; +import Queue from './Queue'; + +function createMapStateToProps() { + return createSelector( + (state) => state.episodes, + (state) => state.queue.paged, + createCommandsSelector(), + (episodes, queue, commands) => { + const isCheckForFinishedDownloadExecuting = _.some(commands, { name: commandNames.CHECK_FOR_FINISHED_DOWNLOAD }); + + return { + isAlbumsFetching: episodes.isFetching, + isAlbumsPopulated: episodes.isPopulated, + episodesError: episodes.error, + isCheckForFinishedDownloadExecuting, + ...queue + }; + } + ); +} + +const mapDispatchToProps = { + ...queueActions, + fetchEpisodes, + clearEpisodes, + executeCommand +}; + +class QueueConnector extends Component { + + // + // Lifecycle + + componentDidMount() { + registerPagePopulator(this.repopulate); + this.props.gotoQueueFirstPage(); + } + + componentDidUpdate(prevProps) { + if (hasDifferentItems(prevProps.items, this.props.items)) { + const albumIds = selectUniqueIds(this.props.items, 'albumId'); + this.props.fetchEpisodes({ albumIds }); + + } + } + + componentWillUnmount() { + unregisterPagePopulator(this.repopulate); + this.props.clearQueue(); + this.props.clearEpisodes(); + } + + // + // Control + + repopulate = () => { + this.props.fetchQueue(); + } + + // + // Listeners + + onFirstPagePress = () => { + this.props.gotoQueueFirstPage(); + } + + onPreviousPagePress = () => { + this.props.gotoQueuePreviousPage(); + } + + onNextPagePress = () => { + this.props.gotoQueueNextPage(); + } + + onLastPagePress = () => { + this.props.gotoQueueLastPage(); + } + + onPageSelect = (page) => { + this.props.gotoQueuePage({ page }); + } + + onSortPress = (sortKey) => { + this.props.setQueueSort({ sortKey }); + } + + onTableOptionChange = (payload) => { + this.props.setQueueTableOption(payload); + + if (payload.pageSize) { + this.props.gotoQueueFirstPage(); + } + } + + onRefreshPress = () => { + this.props.executeCommand({ + name: commandNames.CHECK_FOR_FINISHED_DOWNLOAD + }); + } + + onGrabSelectedPress = (ids) => { + this.props.grabQueueItems({ ids }); + } + + onRemoveSelectedPress = (ids, blacklist) => { + this.props.removeQueueItems({ ids, blacklist }); + } + + // + // Render + + render() { + return ( + + ); + } +} + +QueueConnector.propTypes = { + items: PropTypes.arrayOf(PropTypes.object).isRequired, + fetchQueue: PropTypes.func.isRequired, + gotoQueueFirstPage: PropTypes.func.isRequired, + gotoQueuePreviousPage: PropTypes.func.isRequired, + gotoQueueNextPage: PropTypes.func.isRequired, + gotoQueueLastPage: PropTypes.func.isRequired, + gotoQueuePage: PropTypes.func.isRequired, + setQueueSort: PropTypes.func.isRequired, + setQueueTableOption: PropTypes.func.isRequired, + clearQueue: PropTypes.func.isRequired, + grabQueueItems: PropTypes.func.isRequired, + removeQueueItems: PropTypes.func.isRequired, + fetchEpisodes: PropTypes.func.isRequired, + clearEpisodes: PropTypes.func.isRequired, + executeCommand: PropTypes.func.isRequired +}; + +export default connect(createMapStateToProps, mapDispatchToProps)(QueueConnector); diff --git a/frontend/src/Activity/Queue/QueueDetails.js b/frontend/src/Activity/Queue/QueueDetails.js new file mode 100644 index 000000000..f6e360c0a --- /dev/null +++ b/frontend/src/Activity/Queue/QueueDetails.js @@ -0,0 +1,97 @@ +import moment from 'moment'; +import PropTypes from 'prop-types'; +import React from 'react'; +import { icons, kinds } from 'Helpers/Props'; +import Icon from 'Components/Icon'; + +function QueueDetails(props) { + const { + title, + size, + sizeleft, + estimatedCompletionTime, + status: queueStatus, + errorMessage, + progressBar + } = props; + + const status = queueStatus.toLowerCase(); + + const progress = (100 - sizeleft / size * 100); + + if (status === 'pending') { + return ( + + ); + } + + if (status === 'completed') { + if (errorMessage) { + return ( + + ); + } + + // TODO: show an icon when download is complete, but not imported yet? + } + + if (errorMessage) { + return ( + + ); + } + + if (status === 'failed') { + return ( + + ); + } + + if (status === 'warning') { + return ( + + ); + } + + if (progress < 5) { + return ( + + ); + } + + return progressBar; +} + +QueueDetails.propTypes = { + title: PropTypes.string.isRequired, + size: PropTypes.number.isRequired, + sizeleft: PropTypes.number.isRequired, + estimatedCompletionTime: PropTypes.string, + status: PropTypes.string.isRequired, + errorMessage: PropTypes.string, + progressBar: PropTypes.node.isRequired +}; + +export default QueueDetails; diff --git a/frontend/src/Activity/Queue/QueueRow.css b/frontend/src/Activity/Queue/QueueRow.css new file mode 100644 index 000000000..6aa4a1622 --- /dev/null +++ b/frontend/src/Activity/Queue/QueueRow.css @@ -0,0 +1,23 @@ +.quality { + composes: cell from 'Components/Table/Cells/TableRowCell.css'; + + width: 150px; +} + +.protocol { + composes: cell from 'Components/Table/Cells/TableRowCell.css'; + + width: 100px; +} + +.progress { + composes: cell from 'Components/Table/Cells/TableRowCell.css'; + + width: 150px; +} + +.actions { + composes: cell from 'Components/Table/Cells/TableRowCell.css'; + + width: 70px; +} diff --git a/frontend/src/Activity/Queue/QueueRow.js b/frontend/src/Activity/Queue/QueueRow.js new file mode 100644 index 000000000..fcb13a9f8 --- /dev/null +++ b/frontend/src/Activity/Queue/QueueRow.js @@ -0,0 +1,331 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { icons, kinds } from 'Helpers/Props'; +import IconButton from 'Components/Link/IconButton'; +import SpinnerIconButton from 'Components/Link/SpinnerIconButton'; +import ProgressBar from 'Components/ProgressBar'; +import TableRow from 'Components/Table/TableRow'; +import TableRowCell from 'Components/Table/Cells/TableRowCell'; +import TableSelectCell from 'Components/Table/Cells/TableSelectCell'; +import ProtocolLabel from 'Activity/Queue/ProtocolLabel'; +import EpisodeTitleLink from 'Album/EpisodeTitleLink'; +import EpisodeQuality from 'Album/EpisodeQuality'; +import SeasonEpisodeNumber from 'Album/SeasonEpisodeNumber'; +import InteractiveImportModal from 'InteractiveImport/InteractiveImportModal'; +import ArtistNameLink from 'Artist/ArtistNameLink'; +import QueueStatusCell from './QueueStatusCell'; +import TimeleftCell from './TimeleftCell'; +import RemoveQueueItemModal from './RemoveQueueItemModal'; +import styles from './QueueRow.css'; + +class QueueRow extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + isRemoveQueueItemModalOpen: false, + isInteractiveImportModalOpen: false + }; + } + + // + // Listeners + + onRemoveQueueItemPress = () => { + this.setState({ isRemoveQueueItemModalOpen: true }); + } + + onRemoveQueueItemModalConfirmed = (blacklist) => { + this.props.onRemoveQueueItemPress(blacklist); + this.setState({ isRemoveQueueItemModalOpen: false }); + } + + onRemoveQueueItemModalClose = () => { + this.setState({ isRemoveQueueItemModalOpen: false }); + } + + onInteractiveImportPress = () => { + this.setState({ isInteractiveImportModalOpen: true }); + } + + onInteractiveImportModalClose = () => { + this.setState({ isInteractiveImportModalOpen: false }); + } + + // + // Render + + render() { + const { + id, + downloadId, + title, + status, + trackedDownloadStatus, + statusMessages, + errorMessage, + artist, + episode, + quality, + protocol, + indexer, + downloadClient, + estimatedCompletionTime, + timeleft, + size, + sizeleft, + showRelativeDates, + shortDateFormat, + timeFormat, + isGrabbing, + grabError, + isRemoving, + isSelected, + columns, + onSelectedChange, + onGrabPress + } = this.props; + + const { + isRemoveQueueItemModalOpen, + isInteractiveImportModalOpen + } = this.state; + + const progress = 100 - (sizeleft / size * 100); + const showInteractiveImport = status === 'Completed' && trackedDownloadStatus === 'Warning'; + const isPending = status === 'Delay' || status === 'DownloadClientUnavailable'; + + return ( + + + + { + columns.map((column) => { + const { + name, + isVisible + } = column; + + if (!isVisible) { + return null; + } + + if (name === 'status') { + return ( + + ); + } + + if (name === 'artist.sortName') { + return ( + + + + ); + } + + if (name === 'artist') { + return ( + + + + ); + } + + if (name === 'episodeTitle') { + return ( + + + + ); + } + + if (name === 'quality') { + return ( + + + + ); + } + + if (name === 'protocol') { + return ( + + + + ); + } + + if (name === 'indexer') { + return ( + + {indexer} + + ); + } + + if (name === 'downloadClient') { + return ( + + {downloadClient} + + ); + } + + if (name === 'estimatedCompletionTime') { + return ( + + ); + } + + if (name === 'progress') { + return ( + + { + !!progress && + + } + + ); + } + + if (name === 'actions') { + return ( + + { + showInteractiveImport && + + } + + { + isPending && + + } + + + + ); + } + + return null; + }) + } + + + + + + ); + } + +} + +QueueRow.propTypes = { + id: PropTypes.number.isRequired, + downloadId: PropTypes.string, + title: PropTypes.string.isRequired, + status: PropTypes.string.isRequired, + trackedDownloadStatus: PropTypes.string, + statusMessages: PropTypes.arrayOf(PropTypes.object), + errorMessage: PropTypes.string, + artist: PropTypes.object.isRequired, + episode: PropTypes.object.isRequired, + quality: PropTypes.object.isRequired, + protocol: PropTypes.string.isRequired, + indexer: PropTypes.string, + downloadClient: PropTypes.string, + estimatedCompletionTime: PropTypes.string, + timeleft: PropTypes.string, + size: PropTypes.number, + sizeleft: PropTypes.number, + showRelativeDates: PropTypes.bool.isRequired, + shortDateFormat: PropTypes.string.isRequired, + timeFormat: PropTypes.string.isRequired, + isGrabbing: PropTypes.bool.isRequired, + grabError: PropTypes.object, + isRemoving: PropTypes.bool.isRequired, + isSelected: PropTypes.bool, + columns: PropTypes.arrayOf(PropTypes.object).isRequired, + onSelectedChange: PropTypes.func.isRequired, + onGrabPress: PropTypes.func.isRequired, + onRemoveQueueItemPress: PropTypes.func.isRequired +}; + +QueueRow.defaultProps = { + isGrabbing: false, + isRemoving: false +}; + +export default QueueRow; diff --git a/frontend/src/Activity/Queue/QueueRowConnector.js b/frontend/src/Activity/Queue/QueueRowConnector.js new file mode 100644 index 000000000..bc6221d84 --- /dev/null +++ b/frontend/src/Activity/Queue/QueueRowConnector.js @@ -0,0 +1,75 @@ +import _ from 'lodash'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import { grabQueueItem, removeQueueItem } from 'Store/Actions/queueActions'; +import createArtistSelector from 'Store/Selectors/createArtistSelector'; +import createEpisodeSelector from 'Store/Selectors/createEpisodeSelector'; +import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector'; +import QueueRow from './QueueRow'; + +function createMapStateToProps() { + return createSelector( + createArtistSelector(), + createEpisodeSelector(), + createUISettingsSelector(), + (artist, episode, uiSettings) => { + const result = _.pick(uiSettings, [ + 'showRelativeDates', + 'shortDateFormat', + 'timeFormat' + ]); + + result.artist = artist; + result.episode = episode; + + return result; + } + ); +} + +const mapDispatchToProps = { + grabQueueItem, + removeQueueItem +}; + +class QueueRowConnector extends Component { + + // + // Listeners + + onGrabPress = () => { + this.props.grabQueueItem({ id: this.props.id }); + } + + onRemoveQueueItemPress = (blacklist) => { + this.props.removeQueueItem({ id: this.props.id, blacklist }); + } + + // + // Render + + render() { + if (!this.props.episode) { + return null; + } + + return ( + + ); + } +} + +QueueRowConnector.propTypes = { + id: PropTypes.number.isRequired, + episode: PropTypes.object, + grabQueueItem: PropTypes.func.isRequired, + removeQueueItem: PropTypes.func.isRequired +}; + +export default connect(createMapStateToProps, mapDispatchToProps)(QueueRowConnector); diff --git a/frontend/src/Activity/Queue/QueueStatusCell.css b/frontend/src/Activity/Queue/QueueStatusCell.css new file mode 100644 index 000000000..6291ec949 --- /dev/null +++ b/frontend/src/Activity/Queue/QueueStatusCell.css @@ -0,0 +1,5 @@ +.status { + composes: cell from 'Components/Table/Cells/TableRowCell.css'; + + width: 30px; +} diff --git a/frontend/src/Activity/Queue/QueueStatusCell.js b/frontend/src/Activity/Queue/QueueStatusCell.js new file mode 100644 index 000000000..f8cbc65ff --- /dev/null +++ b/frontend/src/Activity/Queue/QueueStatusCell.js @@ -0,0 +1,132 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import { icons, kinds, tooltipPositions } from 'Helpers/Props'; +import Icon from 'Components/Icon'; +import TableRowCell from 'Components/Table/Cells/TableRowCell'; +import Popover from 'Components/Tooltip/Popover'; +import styles from './QueueStatusCell.css'; + +function getDetailedPopoverBody(statusMessages) { + return ( +
+ { + statusMessages.map(({ title, messages }) => { + return ( +
+ {title} +
    + { + messages.map((message) => { + return ( +
  • + {message} +
  • + ); + }) + } +
+
+ ); + }) + } +
+ ); +} + +function QueueStatusCell(props) { + const { + sourceTitle, + status, + trackedDownloadStatus = 'Ok', + statusMessages, + errorMessage + } = props; + + const hasWarning = trackedDownloadStatus === 'Warning'; + const hasError = trackedDownloadStatus === 'Error'; + + // status === 'downloading' + let iconName = icons.DOWNLOADING; + let iconKind = kinds.DEFAULT; + let title = 'Downloading'; + + if (hasWarning) { + iconKind = kinds.WARNING; + } + + if (status === 'Paused') { + iconName = icons.PAUSED; + title = 'Paused'; + } + + if (status === 'Queued') { + iconName = icons.QUEUED; + title = 'Queued'; + } + + if (status === 'Completed') { + iconName = icons.DOWNLOADED; + title = 'Downloaded'; + } + + if (status === 'Delay') { + iconName = icons.PENDING; + title = 'Pending'; + } + + if (status === 'DownloadClientUnavailable') { + iconName = icons.PENDING; + iconKind = kinds.WARNING; + title = 'Pending - Download client is unavailable'; + } + + if (status === 'Failed') { + iconName = icons.DOWNLOADING; + iconKind = kinds.DANGER; + title = 'Download failed'; + } + + if (status === 'Warning') { + iconName = icons.DOWNLOADING; + iconKind = kinds.WARNING; + title = `Download warning: ${errorMessage || 'check download client for more details'}`; + } + + if (hasError) { + if (status === 'Completed') { + iconName = icons.DOWNLOAD; + iconKind = kinds.DANGER; + title = `Import failed: ${sourceTitle}`; + } else { + iconName = icons.DOWNLOADING; + iconKind = kinds.DANGER; + title = 'Download failed'; + } + } + + return ( + + + } + title={title} + body={hasWarning || hasError ? getDetailedPopoverBody(statusMessages) : sourceTitle} + position={tooltipPositions.RIGHT} + /> + + ); +} + +QueueStatusCell.propTypes = { + sourceTitle: PropTypes.string.isRequired, + status: PropTypes.string.isRequired, + trackedDownloadStatus: PropTypes.string, + statusMessages: PropTypes.arrayOf(PropTypes.object), + errorMessage: PropTypes.string +}; + +export default QueueStatusCell; diff --git a/frontend/src/Activity/Queue/RemoveQueueItemModal.css b/frontend/src/Activity/Queue/RemoveQueueItemModal.css new file mode 100644 index 000000000..c9ef59ec1 --- /dev/null +++ b/frontend/src/Activity/Queue/RemoveQueueItemModal.css @@ -0,0 +1,3 @@ +.message { + margin-bottom: 30px; +} diff --git a/frontend/src/Activity/Queue/RemoveQueueItemModal.js b/frontend/src/Activity/Queue/RemoveQueueItemModal.js new file mode 100644 index 000000000..915174dbf --- /dev/null +++ b/frontend/src/Activity/Queue/RemoveQueueItemModal.js @@ -0,0 +1,114 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { inputTypes, kinds, sizes } from 'Helpers/Props'; +import Button from 'Components/Link/Button'; +import Modal from 'Components/Modal/Modal'; +import FormGroup from 'Components/Form/FormGroup'; +import FormLabel from 'Components/Form/FormLabel'; +import FormInputGroup from 'Components/Form/FormInputGroup'; +import ModalContent from 'Components/Modal/ModalContent'; +import ModalHeader from 'Components/Modal/ModalHeader'; +import ModalBody from 'Components/Modal/ModalBody'; +import ModalFooter from 'Components/Modal/ModalFooter'; +import styles from './RemoveQueueItemModal.css'; + +class RemoveQueueItemModal extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + blacklist: false + }; + } + + // + // Listeners + + onBlacklistChange = ({ value }) => { + this.setState({ blacklist: value }); + } + + onRemoveQueueItemConfirmed = () => { + const blacklist = this.state.blacklist; + + this.setState({ blacklist: false }); + this.props.onRemovePress(blacklist); + } + + onModalClose = () => { + this.setState({ blacklist: false }); + this.props.onModalClose(); + } + + // + // Render + + render() { + const { + isOpen, + sourceTitle + } = this.props; + + const blacklist = this.state.blacklist; + + return ( + + + + Remove - {sourceTitle} + + + +
+ Are you sure you want to remove '{sourceTitle}' from the queue? +
+ + + Blacklist Release + + + +
+ + + + + + +
+
+ ); + } +} + +RemoveQueueItemModal.propTypes = { + isOpen: PropTypes.bool.isRequired, + sourceTitle: PropTypes.string.isRequired, + onRemovePress: PropTypes.func.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default RemoveQueueItemModal; diff --git a/frontend/src/Activity/Queue/RemoveQueueItemsModal.css b/frontend/src/Activity/Queue/RemoveQueueItemsModal.css new file mode 100644 index 000000000..c9ef59ec1 --- /dev/null +++ b/frontend/src/Activity/Queue/RemoveQueueItemsModal.css @@ -0,0 +1,3 @@ +.message { + margin-bottom: 30px; +} diff --git a/frontend/src/Activity/Queue/RemoveQueueItemsModal.js b/frontend/src/Activity/Queue/RemoveQueueItemsModal.js new file mode 100644 index 000000000..e97857236 --- /dev/null +++ b/frontend/src/Activity/Queue/RemoveQueueItemsModal.js @@ -0,0 +1,114 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { inputTypes, kinds, sizes } from 'Helpers/Props'; +import Button from 'Components/Link/Button'; +import Modal from 'Components/Modal/Modal'; +import FormGroup from 'Components/Form/FormGroup'; +import FormLabel from 'Components/Form/FormLabel'; +import FormInputGroup from 'Components/Form/FormInputGroup'; +import ModalContent from 'Components/Modal/ModalContent'; +import ModalHeader from 'Components/Modal/ModalHeader'; +import ModalBody from 'Components/Modal/ModalBody'; +import ModalFooter from 'Components/Modal/ModalFooter'; +import styles from './RemoveQueueItemsModal.css'; + +class RemoveQueueItemsModal extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + blacklist: false + }; + } + + // + // Listeners + + onBlacklistChange = ({ value }) => { + this.setState({ blacklist: value }); + } + + onRemoveQueueItemConfirmed = () => { + const blacklist = this.state.blacklist; + + this.setState({ blacklist: false }); + this.props.onRemovePress(blacklist); + } + + onModalClose = () => { + this.setState({ blacklist: false }); + this.props.onModalClose(); + } + + // + // Render + + render() { + const { + isOpen, + selectedCount + } = this.props; + + const blacklist = this.state.blacklist; + + return ( + + + + Remove Selected Item{selectedCount > 1 ? 's' : ''} + + + +
+ Are you sure you want to remove {selectedCount} item{selectedCount > 1 ? 's' : ''} from the queue? +
+ + + Blacklist Release + + + +
+ + + + + + +
+
+ ); + } +} + +RemoveQueueItemsModal.propTypes = { + isOpen: PropTypes.bool.isRequired, + selectedCount: PropTypes.number.isRequired, + onRemovePress: PropTypes.func.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default RemoveQueueItemsModal; diff --git a/frontend/src/Activity/Queue/Status/QueueStatusConnector.js b/frontend/src/Activity/Queue/Status/QueueStatusConnector.js new file mode 100644 index 000000000..c8419a8f8 --- /dev/null +++ b/frontend/src/Activity/Queue/Status/QueueStatusConnector.js @@ -0,0 +1,63 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import { fetchQueueStatus } from 'Store/Actions/queueActions'; +import PageSidebarStatus from 'Components/Page/Sidebar/PageSidebarStatus'; + +function createMapStateToProps() { + return createSelector( + (state) => state.app, + (state) => state.queue.queueStatus, + (app, status) => { + return { + isConnected: app.isConnected, + isReconnecting: app.isReconnecting, + isPopulated: status.isPopulated, + ...status.item + }; + } + ); +} + +const mapDispatchToProps = { + fetchQueueStatus +}; + +class QueueStatusConnector extends Component { + + // + // Lifecycle + + componentDidMount() { + if (!this.props.isPopulated) { + this.props.fetchQueueStatus(); + } + } + + componentDidUpdate(prevProps) { + if (this.props.isConnected && prevProps.isReconnecting) { + this.props.fetchQueueStatus(); + } + } + + // + // Render + + render() { + return ( + + ); + } +} + +QueueStatusConnector.propTypes = { + isConnected: PropTypes.bool.isRequired, + isReconnecting: PropTypes.bool.isRequired, + isPopulated: PropTypes.bool.isRequired, + fetchQueueStatus: PropTypes.func.isRequired +}; + +export default connect(createMapStateToProps, mapDispatchToProps)(QueueStatusConnector); diff --git a/frontend/src/Activity/Queue/TimeleftCell.css b/frontend/src/Activity/Queue/TimeleftCell.css new file mode 100644 index 000000000..eb58cf297 --- /dev/null +++ b/frontend/src/Activity/Queue/TimeleftCell.css @@ -0,0 +1,5 @@ +.timeleft { + composes: cell from 'Components/Table/Cells/TableRowCell.css'; + + width: 100px; +} diff --git a/frontend/src/Activity/Queue/TimeleftCell.js b/frontend/src/Activity/Queue/TimeleftCell.js new file mode 100644 index 000000000..c9515f172 --- /dev/null +++ b/frontend/src/Activity/Queue/TimeleftCell.js @@ -0,0 +1,82 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import formatTime from 'Utilities/Date/formatTime'; +import formatTimeSpan from 'Utilities/Date/formatTimeSpan'; +import getRelativeDate from 'Utilities/Date/getRelativeDate'; +import formatBytes from 'Utilities/Number/formatBytes'; +import TableRowCell from 'Components/Table/Cells/TableRowCell'; +import styles from './TimeleftCell.css'; + +function TimeleftCell(props) { + const { + estimatedCompletionTime, + timeleft, + status, + size, + sizeleft, + showRelativeDates, + shortDateFormat, + timeFormat + } = props; + + if (status === 'Delay') { + const date = getRelativeDate(estimatedCompletionTime, shortDateFormat, showRelativeDates); + const time = formatTime(estimatedCompletionTime, timeFormat, { includeMinuteZero: true }); + + return ( + + - + + ); + } + + if (status === 'DownloadClientUnavailable') { + const date = getRelativeDate(estimatedCompletionTime, shortDateFormat, showRelativeDates); + const time = formatTime(estimatedCompletionTime, timeFormat, { includeMinuteZero: true }); + + return ( + + - + + ); + } + + if (!timeleft) { + return ( + + - + + ); + } + + const totalSize = formatBytes(size); + const remainingSize = formatBytes(sizeleft); + + return ( + + {formatTimeSpan(timeleft)} + + ); +} + +TimeleftCell.propTypes = { + estimatedCompletionTime: PropTypes.string, + timeleft: PropTypes.string, + status: PropTypes.string.isRequired, + size: PropTypes.number.isRequired, + sizeleft: PropTypes.number.isRequired, + showRelativeDates: PropTypes.bool.isRequired, + shortDateFormat: PropTypes.string.isRequired, + timeFormat: PropTypes.string.isRequired +}; + +export default TimeleftCell; diff --git a/frontend/src/Activity/activity.less b/frontend/src/Activity/activity.less new file mode 100644 index 000000000..c6d9b6d2a --- /dev/null +++ b/frontend/src/Activity/activity.less @@ -0,0 +1,27 @@ + +.queue-status-cell .popover { + max-width: 800px; +} + +.queue { + .protocol-cell { + text-align: center; + width: 80px; + } + + .episode-number-cell { + min-width: 90px; + } +} + +.remove-from-queue-modal { + .form-horizontal { + margin-top: 20px; + } +} + +.history-detail-modal { + .info { + word-wrap: break-word; + } +} diff --git a/frontend/src/AddArtist/AddNewArtist/AddNewArtist.css b/frontend/src/AddArtist/AddNewArtist/AddNewArtist.css new file mode 100644 index 000000000..c1ec4fbe3 --- /dev/null +++ b/frontend/src/AddArtist/AddNewArtist/AddNewArtist.css @@ -0,0 +1,54 @@ +.searchContainer { + display: flex; + margin-bottom: 10px; +} + +.searchIconContainer { + width: 58px; + height: 46px; + border: 1px solid $inputBorderColor; + border-right: none; + border-radius: 4px; + border-top-right-radius: 0; + border-bottom-right-radius: 0; + background-color: #edf1f2; + text-align: center; + line-height: 46px; +} + +.searchInput { + composes: text from 'Components/Form/TextInput.css'; + + height: 46px; + border-radius: 0; + font-size: 18px; +} + +.clearLookupButton { + border: 1px solid $inputBorderColor; + border-left: none; + border-top-right-radius: 4px; + border-bottom-right-radius: 4px; + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); +} + +.message { + margin-top: 30px; + text-align: center; +} + +.helpText { + margin-bottom: 10px; + font-weight: 300; + font-size: 24px; +} + +.noResults { + margin-bottom: 10px; + font-weight: 300; + font-size: 30px; +} + +.searchResults { + margin-top: 30px; +} diff --git a/frontend/src/AddArtist/AddNewArtist/AddNewArtist.js b/frontend/src/AddArtist/AddNewArtist/AddNewArtist.js new file mode 100644 index 000000000..59c9e2e7f --- /dev/null +++ b/frontend/src/AddArtist/AddNewArtist/AddNewArtist.js @@ -0,0 +1,182 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { icons } from 'Helpers/Props'; +import Button from 'Components/Link/Button'; +import Link from 'Components/Link/Link'; +import Icon from 'Components/Icon'; +import LoadingIndicator from 'Components/Loading/LoadingIndicator'; +import TextInput from 'Components/Form/TextInput'; +import PageContent from 'Components/Page/PageContent'; +import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector'; +import AddNewArtistSearchResultConnector from './AddNewArtistSearchResultConnector'; +import styles from './AddNewArtist.css'; + +class AddNewArtist extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + term: props.term || '', + isFetching: false + }; + } + + componentDidMount() { + const term = this.state.term; + + if (term) { + this.props.onArtistLookupChange(term); + } + } + + componentDidUpdate(prevProps) { + const { + term, + isFetching + } = this.props; + + if (term && term !== prevProps.term) { + this.setState({ + term, + isFetching: true + }); + this.props.onArtistLookupChange(term); + } else if (isFetching !== prevProps.isFetching) { + this.setState({ + isFetching + }); + } + } + + // + // Listeners + + onSearchInputChange = ({ value }) => { + const hasValue = !!value.trim(); + + this.setState({ term: value, isFetching: hasValue }, () => { + if (hasValue) { + this.props.onArtistLookupChange(value); + } else { + this.props.onClearArtistLookup(); + } + }); + } + + onClearArtistLookupPress = () => { + this.setState({ term: '' }); + this.props.onClearArtistLookup(); + } + + // + // Render + + render() { + const { + error, + items + } = this.props; + + const term = this.state.term; + const isFetching = this.state.isFetching; + + return ( + + +
+
+ +
+ + + + +
+ + { + isFetching && + + } + + { + !isFetching && !!error && +
Failed to load search results, please try again.
+ } + + { + !isFetching && !error && !!items.length && +
+ { + items.map((item) => { + return ( + + ); + }) + } +
+ } + + { + !isFetching && !error && !items.length && !!term && +
+
Couldn't find any results for '{term}'
+
You can also search using MusicBrainz ID of a show. eg. lidarr:71663
+
+ + Why can't I find my artist? + +
+
+ } + + { + !term && +
+
It's easy to add a new artist, just start typing the name the artist you want to add.
+
You can also search using MusicBrainz ID of a show. eg. lidarr:cc197bad-dc9c-440d-a5b5-d52ba2e14234
+
+ } + +
+ + + ); + } +} + +AddNewArtist.propTypes = { + term: PropTypes.string, + isFetching: PropTypes.bool.isRequired, + error: PropTypes.object, + isAdding: PropTypes.bool.isRequired, + addError: PropTypes.object, + items: PropTypes.arrayOf(PropTypes.object).isRequired, + onArtistLookupChange: PropTypes.func.isRequired, + onClearArtistLookup: PropTypes.func.isRequired +}; + +export default AddNewArtist; diff --git a/frontend/src/AddArtist/AddNewArtist/AddNewArtistConnector.js b/frontend/src/AddArtist/AddNewArtist/AddNewArtistConnector.js new file mode 100644 index 000000000..a2e322b40 --- /dev/null +++ b/frontend/src/AddArtist/AddNewArtist/AddNewArtistConnector.js @@ -0,0 +1,102 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import queryString from 'query-string'; +import { lookupArtist, clearAddArtist } from 'Store/Actions/addArtistActions'; +import { fetchRootFolders } from 'Store/Actions/rootFolderActions'; +import AddNewArtist from './AddNewArtist'; + +function createMapStateToProps() { + return createSelector( + (state) => state.addArtist, + (state) => state.routing.location, + (addArtist, location) => { + const query = queryString.parse(location.search); + + return { + term: query.term, + ...addArtist + }; + } + ); +} + +const mapDispatchToProps = { + lookupArtist, + clearAddArtist, + fetchRootFolders +}; + +class AddNewArtistConnector extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this._artistLookupTimeout = null; + } + + componentDidMount() { + this.props.fetchRootFolders(); + } + + componentWillUnmount() { + if (this._artistLookupTimeout) { + clearTimeout(this._artistLookupTimeout); + } + + this.props.clearAddArtist(); + } + + // + // Listeners + + onArtistLookupChange = (term) => { + if (this._artistLookupTimeout) { + clearTimeout(this._artistLookupTimeout); + } + + if (term.trim() === '') { + this.props.clearAddArtist(); + } else { + this._artistLookupTimeout = setTimeout(() => { + this.props.lookupArtist({ term }); + }, 300); + } + } + + onClearArtistLookup = () => { + this.props.clearAddArtist(); + } + + // + // Render + + render() { + const { + term, + ...otherProps + } = this.props; + + return ( + + ); + } +} + +AddNewArtistConnector.propTypes = { + term: PropTypes.string, + lookupArtist: PropTypes.func.isRequired, + clearAddArtist: PropTypes.func.isRequired, + fetchRootFolders: PropTypes.func.isRequired +}; + +export default connect(createMapStateToProps, mapDispatchToProps)(AddNewArtistConnector); diff --git a/frontend/src/AddArtist/AddNewArtist/AddNewArtistModal.js b/frontend/src/AddArtist/AddNewArtist/AddNewArtistModal.js new file mode 100644 index 000000000..e94a8a229 --- /dev/null +++ b/frontend/src/AddArtist/AddNewArtist/AddNewArtistModal.js @@ -0,0 +1,31 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import Modal from 'Components/Modal/Modal'; +import AddNewArtistModalContentConnector from './AddNewArtistModalContentConnector'; + +function AddNewArtistModal(props) { + const { + isOpen, + onModalClose, + ...otherProps + } = props; + + return ( + + + + ); +} + +AddNewArtistModal.propTypes = { + isOpen: PropTypes.bool.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default AddNewArtistModal; diff --git a/frontend/src/AddArtist/AddNewArtist/AddNewArtistModalContent.css b/frontend/src/AddArtist/AddNewArtist/AddNewArtistModalContent.css new file mode 100644 index 000000000..c478d8b0c --- /dev/null +++ b/frontend/src/AddArtist/AddNewArtist/AddNewArtistModalContent.css @@ -0,0 +1,76 @@ +.container { + display: flex; +} + +.year { + margin-left: 5px; + color: $disabledColor; +} + +.poster { + flex: 0 0 170px; + margin-right: 20px; + height: 250px; +} + +.info { + flex-grow: 1; +} + +.overview { + margin-bottom: 30px; + max-height: 230px; + text-align: justify; +} + +.labelIcon { + margin-left: 8px; +} + +.searchForMissingAlbumsLabelContainer { + display: flex; + margin-top: 2px; +} + +.searchForMissingAlbumsLabel { + margin-right: 8px; + font-weight: normal; +} + +.searchForMissingAlbumsContainer { + composes: container from 'Components/Form/CheckInput.css'; + + flex: 0 1 0; +} + +.searchForMissingAlbumsInput { + composes: input from 'Components/Form/CheckInput.css'; + + margin-top: 0; +} + +.modalFooter { + composes: modalFooter from 'Components/Modal/ModalFooter.css'; +} + +.addButton { + @add-mixin truncate; + composes: button from 'Components/Link/SpinnerButton.css'; +} + +.hideLanguageProfile { + composes: group from 'Components/Form/FormGroup.css'; + + display: none; +} + +@media only screen and (max-width: $breakpointSmall) { + .modalFooter { + display: block; + text-align: center; + } + + .addButton { + margin-top: 10px; + } +} diff --git a/frontend/src/AddArtist/AddNewArtist/AddNewArtistModalContent.js b/frontend/src/AddArtist/AddNewArtist/AddNewArtistModalContent.js new file mode 100644 index 000000000..6f0b0d838 --- /dev/null +++ b/frontend/src/AddArtist/AddNewArtist/AddNewArtistModalContent.js @@ -0,0 +1,239 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import TextTruncate from 'react-text-truncate'; +import { icons, kinds, inputTypes, tooltipPositions } from 'Helpers/Props'; +import Icon from 'Components/Icon'; +import SpinnerButton from 'Components/Link/SpinnerButton'; +import Form from 'Components/Form/Form'; +import FormGroup from 'Components/Form/FormGroup'; +import FormLabel from 'Components/Form/FormLabel'; +import FormInputGroup from 'Components/Form/FormInputGroup'; +import CheckInput from 'Components/Form/CheckInput'; +import ModalContent from 'Components/Modal/ModalContent'; +import ModalHeader from 'Components/Modal/ModalHeader'; +import ModalBody from 'Components/Modal/ModalBody'; +import ModalFooter from 'Components/Modal/ModalFooter'; +import Popover from 'Components/Tooltip/Popover'; +import ArtistPoster from 'Artist/ArtistPoster'; +import ArtistMonitoringOptionsPopoverContent from 'AddArtist/ArtistMonitoringOptionsPopoverContent'; +import styles from './AddNewArtistModalContent.css'; + +class AddNewArtistModalContent extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + searchForMissingAlbums: false + }; + } + + // + // Listeners + + onSearchForMissingAlbumsChange = ({ value }) => { + this.setState({ searchForMissingAlbums: value }); + } + + onQualityProfileIdChange = ({ value }) => { + this.props.onInputChange({ name: 'qualityProfileId', value: parseInt(value) }); + } + + onLanguageProfileIdChange = ({ value }) => { + this.props.onInputChange({ name: 'languageProfileId', value: parseInt(value) }); + } + + onAddArtistPress = () => { + this.props.onAddArtistPress(this.state.searchForMissingAlbums); + } + + // + // Render + + render() { + const { + artistName, + overview, + images, + isAdding, + rootFolderPath, + monitor, + qualityProfileId, + languageProfileId, + albumFolder, + primaryAlbumTypes, + secondaryAlbumTypes, + tags, + showLanguageProfile, + isSmallScreen, + onModalClose, + onInputChange + } = this.props; + + return ( + + + {artistName} + + + +
+ { + !isSmallScreen && +
+ +
+ } + +
+
+ +
+ +
+ + Root Folder + + + + + + + Monitor + + + } + title="Monitoring Options" + body={} + position={tooltipPositions.RIGHT} + /> + + + + + + + Quality Profile + + + + + + Language Profile + + + + + + Album Folder + + + + + + Tags + + + +
+
+
+
+ + + + + + Add {artistName} + + +
+ ); + } +} + +AddNewArtistModalContent.propTypes = { + artistName: PropTypes.string.isRequired, + overview: PropTypes.string, + images: PropTypes.arrayOf(PropTypes.object).isRequired, + isAdding: PropTypes.bool.isRequired, + addError: PropTypes.object, + rootFolderPath: PropTypes.object, + monitor: PropTypes.object.isRequired, + qualityProfileId: PropTypes.object, + languageProfileId: PropTypes.object, + albumFolder: PropTypes.object.isRequired, + primaryAlbumTypes: PropTypes.object.isRequired, + secondaryAlbumTypes: PropTypes.object.isRequired, + tags: PropTypes.object.isRequired, + showLanguageProfile: PropTypes.bool.isRequired, + isSmallScreen: PropTypes.bool.isRequired, + onModalClose: PropTypes.func.isRequired, + onInputChange: PropTypes.func.isRequired, + onAddArtistPress: PropTypes.func.isRequired +}; + +export default AddNewArtistModalContent; diff --git a/frontend/src/AddArtist/AddNewArtist/AddNewArtistModalContentConnector.js b/frontend/src/AddArtist/AddNewArtist/AddNewArtistModalContentConnector.js new file mode 100644 index 000000000..b971432c3 --- /dev/null +++ b/frontend/src/AddArtist/AddNewArtist/AddNewArtistModalContentConnector.js @@ -0,0 +1,111 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import { setAddArtistDefault, addArtist } from 'Store/Actions/addArtistActions'; +import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector'; +import selectSettings from 'Store/Selectors/selectSettings'; +import AddNewArtistModalContent from './AddNewArtistModalContent'; + +function createMapStateToProps() { + return createSelector( + (state) => state.addArtist, + (state) => state.settings.languageProfiles, + createDimensionsSelector(), + (addArtistState, languageProfiles, dimensions) => { + const { + isAdding, + addError, + defaults + } = addArtistState; + + const { + settings, + validationErrors, + validationWarnings + } = selectSettings(defaults, {}, addError); + + return { + isAdding, + addError, + showLanguageProfile: languageProfiles.length > 1, + isSmallScreen: dimensions.isSmallScreen, + validationErrors, + validationWarnings, + ...settings + }; + } + ); +} + +const mapDispatchToProps = { + setAddArtistDefault, + addArtist +}; + +class AddNewArtistModalContentConnector extends Component { + + // + // Listeners + + onInputChange = ({ name, value }) => { + this.props.setAddArtistDefault({ [name]: value }); + } + + onAddArtistPress = (searchForMissingAlbums) => { + const { + foreignArtistId, + rootFolderPath, + monitor, + qualityProfileId, + languageProfileId, + albumFolder, + primaryAlbumTypes, + secondaryAlbumTypes, + tags + } = this.props; + + this.props.addArtist({ + foreignArtistId, + rootFolderPath: rootFolderPath.value, + monitor: monitor.value, + qualityProfileId: qualityProfileId.value, + languageProfileId: languageProfileId.value, + albumFolder: albumFolder.value, + primaryAlbumTypes: primaryAlbumTypes.value, + secondaryAlbumTypes: secondaryAlbumTypes.value, + tags: tags.value, + searchForMissingAlbums + }); + } + + // + // Render + + render() { + return ( + + ); + } +} + +AddNewArtistModalContentConnector.propTypes = { + foreignArtistId: PropTypes.string.isRequired, + rootFolderPath: PropTypes.object, + monitor: PropTypes.object.isRequired, + qualityProfileId: PropTypes.object, + languageProfileId: PropTypes.object, + albumFolder: PropTypes.object.isRequired, + primaryAlbumTypes: PropTypes.object.isRequired, + secondaryAlbumTypes: PropTypes.object.isRequired, + tags: PropTypes.object.isRequired, + onModalClose: PropTypes.func.isRequired, + setAddArtistDefault: PropTypes.func.isRequired, + addArtist: PropTypes.func.isRequired +}; + +export default connect(createMapStateToProps, mapDispatchToProps)(AddNewArtistModalContentConnector); diff --git a/frontend/src/AddArtist/AddNewArtist/AddNewArtistSearchResult.css b/frontend/src/AddArtist/AddNewArtist/AddNewArtistSearchResult.css new file mode 100644 index 000000000..c56765538 --- /dev/null +++ b/frontend/src/AddArtist/AddNewArtist/AddNewArtistSearchResult.css @@ -0,0 +1,42 @@ +.searchResult { + display: flex; + margin: 20px 0; + padding: 20px; + width: 100%; + background-color: $white; + color: inherit; + transition: background 500ms; + + &:hover { + background-color: #eaf2ff; + color: inherit; + text-decoration: none; + } +} + +.poster { + flex: 0 0 170px; + margin-right: 20px; + height: 250px; +} + +.name { + font-weight: 300; + font-size: 36px; +} + +.year { + margin-left: 10px; + color: $disabledColor; +} + +.alreadyExistsIcon { + margin-left: 10px; + color: #37bc9b; +} + +.overview { + overflow: hidden; + margin-top: 20px; + text-align: justify; +} diff --git a/frontend/src/AddArtist/AddNewArtist/AddNewArtistSearchResult.js b/frontend/src/AddArtist/AddNewArtist/AddNewArtistSearchResult.js new file mode 100644 index 000000000..5cae6fbb4 --- /dev/null +++ b/frontend/src/AddArtist/AddNewArtist/AddNewArtistSearchResult.js @@ -0,0 +1,209 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import TextTruncate from 'react-text-truncate'; +import dimensions from 'Styles/Variables/dimensions'; +import fonts from 'Styles/Variables/fonts'; +import { icons, kinds, sizes } from 'Helpers/Props'; +import HeartRating from 'Components/HeartRating'; +import Icon from 'Components/Icon'; +import Label from 'Components/Label'; +import Link from 'Components/Link/Link'; +import ArtistPoster from 'Artist/ArtistPoster'; +import AddNewArtistModal from './AddNewArtistModal'; +import styles from './AddNewArtistSearchResult.css'; + +const columnPadding = parseInt(dimensions.artistIndexColumnPadding); +const columnPaddingSmallScreen = parseInt(dimensions.artistIndexColumnPaddingSmallScreen); +const defaultFontSize = parseInt(fonts.defaultFontSize); +const lineHeight = parseFloat(fonts.lineHeight); + +function calculateHeight(rowHeight, isSmallScreen) { + let height = rowHeight - 45; + + if (isSmallScreen) { + height -= columnPaddingSmallScreen; + } else { + height -= columnPadding; + } + + return height; +} + +class AddNewArtistSearchResult extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + isNewAddArtistModalOpen: false + }; + } + + componentDidUpdate(prevProps) { + if (!prevProps.isExistingArtist && this.props.isExistingArtist) { + this.onAddSerisModalClose(); + } + } + + // + // Listeners + + onPress = () => { + this.setState({ isNewAddArtistModalOpen: true }); + } + + onAddSerisModalClose = () => { + this.setState({ isNewAddArtistModalOpen: false }); + } + + // + // Render + + render() { + const { + foreignArtistId, + artistName, + nameSlug, + year, + disambiguation, + artistType, + status, + overview, + albumCount, + ratings, + images, + isExistingArtist, + isSmallScreen + } = this.props; + + const linkProps = isExistingArtist ? { to: `/artist/${nameSlug}` } : { onPress: this.onPress }; + let albums = '1 Album'; + + if (albumCount > 1) { + albums = `${albumCount} Albums`; + } + + const height = calculateHeight(230, isSmallScreen); + + return ( + + { + !isSmallScreen && + + } + +
+
+ {artistName} + + { + !name.contains(year) && !!year && + ({year}) + } + + { + !!disambiguation && + ({disambiguation}) + } + + { + isExistingArtist && + + } +
+ +
+ + + { + !!artistType && + + } + + { + !!albumCount && + + } + + { + status === 'ended' && + + } +
+ +
+
+ +
+
+
+ + + + ); + } +} + +AddNewArtistSearchResult.propTypes = { + foreignArtistId: PropTypes.string.isRequired, + artistName: PropTypes.string.isRequired, + nameSlug: PropTypes.string.isRequired, + year: PropTypes.number, + disambiguation: PropTypes.string, + artistType: PropTypes.string, + status: PropTypes.string.isRequired, + overview: PropTypes.string, + albumCount: PropTypes.number, + ratings: PropTypes.object.isRequired, + images: PropTypes.arrayOf(PropTypes.object).isRequired, + isExistingArtist: PropTypes.bool.isRequired, + isSmallScreen: PropTypes.bool.isRequired +}; + +export default AddNewArtistSearchResult; diff --git a/frontend/src/AddArtist/AddNewArtist/AddNewArtistSearchResultConnector.js b/frontend/src/AddArtist/AddNewArtist/AddNewArtistSearchResultConnector.js new file mode 100644 index 000000000..45165c04d --- /dev/null +++ b/frontend/src/AddArtist/AddNewArtist/AddNewArtistSearchResultConnector.js @@ -0,0 +1,20 @@ +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import createExistingArtistSelector from 'Store/Selectors/createExistingArtistSelector'; +import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector'; +import AddNewArtistSearchResult from './AddNewArtistSearchResult'; + +function createMapStateToProps() { + return createSelector( + createExistingArtistSelector(), + createDimensionsSelector(), + (isExistingArtist, dimensions) => { + return { + isExistingArtist, + isSmallScreen: dimensions.isSmallScreen + }; + } + ); +} + +export default connect(createMapStateToProps)(AddNewArtistSearchResult); diff --git a/frontend/src/AddArtist/ArtistMonitoringOptionsPopoverContent.js b/frontend/src/AddArtist/ArtistMonitoringOptionsPopoverContent.js new file mode 100644 index 000000000..89851ce4d --- /dev/null +++ b/frontend/src/AddArtist/ArtistMonitoringOptionsPopoverContent.js @@ -0,0 +1,46 @@ +import React from 'react'; +import DescriptionList from 'Components/DescriptionList/DescriptionList'; +import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem'; + +function ArtistMonitoringOptionsPopoverContent() { + return ( + + + + + + + + + + + + + + + + ); +} + +export default ArtistMonitoringOptionsPopoverContent; diff --git a/frontend/src/AddArtist/ImportArtist/Import/ImportArtist.js b/frontend/src/AddArtist/ImportArtist/Import/ImportArtist.js new file mode 100644 index 000000000..3c4bea3b6 --- /dev/null +++ b/frontend/src/AddArtist/ImportArtist/Import/ImportArtist.js @@ -0,0 +1,173 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import getSelectedIds from 'Utilities/Table/getSelectedIds'; +import selectAll from 'Utilities/Table/selectAll'; +import toggleSelected from 'Utilities/Table/toggleSelected'; +import LoadingIndicator from 'Components/Loading/LoadingIndicator'; +import PageContent from 'Components/Page/PageContent'; +import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector'; +import ImportArtistTableConnector from './ImportArtistTableConnector'; +import ImportArtistFooterConnector from './ImportArtistFooterConnector'; + +class ImportArtist extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + allSelected: false, + allUnselected: false, + lastToggled: null, + selectedState: {}, + contentBody: null, + scrollTop: 0 + }; + } + + // + // Control + + setContentBodyRef = (ref) => { + this.setState({ contentBody: ref }); + } + + // + // Listeners + + getSelectedIds = () => { + return getSelectedIds(this.state.selectedState, { parseIds: false }); + } + + onSelectAllChange = ({ value }) => { + // Only select non-dupes + this.setState(selectAll(this.state.selectedState, value)); + } + + onSelectedChange = ({ id, value, shiftKey = false }) => { + this.setState((state) => { + return toggleSelected(state, this.props.items, id, value, shiftKey); + }); + } + + onRemoveSelectedStateItem = (id) => { + this.setState((state) => { + const selectedState = Object.assign({}, state.selectedState); + delete selectedState[id]; + + return { + ...state, + selectedState + }; + }); + } + + onInputChange = ({ name, value }) => { + this.props.onInputChange(this.getSelectedIds(), name, value); + } + + onImportPress = () => { + this.props.onImportPress(this.getSelectedIds()); + } + + onScroll = ({ scrollTop }) => { + this.setState({ scrollTop }); + } + + // + // Render + + render() { + const { + rootFolderId, + path, + rootFoldersFetching, + rootFoldersPopulated, + rootFoldersError, + unmappedFolders, + showLanguageProfile + } = this.props; + + const { + allSelected, + allUnselected, + selectedState, + contentBody + } = this.state; + + return ( + + + { + rootFoldersFetching && !rootFoldersPopulated && + + } + + { + !rootFoldersFetching && !!rootFoldersError && +
Unable to load root folders
+ } + + { + !rootFoldersError && rootFoldersPopulated && !unmappedFolders.length && +
+ All artist in {path} have been imported +
+ } + + { + !rootFoldersError && rootFoldersPopulated && !!unmappedFolders.length && contentBody && + + } +
+ + { + !rootFoldersError && rootFoldersPopulated && !!unmappedFolders.length && + + } +
+ ); + } +} + +ImportArtist.propTypes = { + rootFolderId: PropTypes.number.isRequired, + path: PropTypes.string, + rootFoldersFetching: PropTypes.bool.isRequired, + rootFoldersPopulated: PropTypes.bool.isRequired, + rootFoldersError: PropTypes.object, + unmappedFolders: PropTypes.arrayOf(PropTypes.object), + items: PropTypes.arrayOf(PropTypes.object), + showLanguageProfile: PropTypes.bool.isRequired, + onInputChange: PropTypes.func.isRequired, + onImportPress: PropTypes.func.isRequired +}; + +ImportArtist.defaultProps = { + unmappedFolders: [] +}; + +export default ImportArtist; diff --git a/frontend/src/AddArtist/ImportArtist/Import/ImportArtistConnector.js b/frontend/src/AddArtist/ImportArtist/Import/ImportArtistConnector.js new file mode 100644 index 000000000..504cb54dd --- /dev/null +++ b/frontend/src/AddArtist/ImportArtist/Import/ImportArtistConnector.js @@ -0,0 +1,121 @@ +import _ from 'lodash'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import { setImportArtistValue, importArtist, clearImportArtist } from 'Store/Actions/importArtistActions'; +import { fetchRootFolders } from 'Store/Actions/rootFolderActions'; +import { setAddArtistDefault } from 'Store/Actions/addArtistActions'; +import createRouteMatchShape from 'Helpers/Props/Shapes/createRouteMatchShape'; +import ImportArtist from './ImportArtist'; + +function createMapStateToProps() { + return createSelector( + (state, { match }) => match, + (state) => state.rootFolders, + (state) => state.addArtist, + (state) => state.importArtist, + (state) => state.settings.languageProfiles, + (match, rootFolders, addArtist, importArtistState, languageProfiles) => { + const { + isFetching: rootFoldersFetching, + isPopulated: rootFoldersPopulated, + error: rootFoldersError, + items + } = rootFolders; + + const rootFolderId = parseInt(match.params.rootFolderId); + + const result = { + rootFolderId, + rootFoldersFetching, + rootFoldersPopulated, + rootFoldersError, + showLanguageProfile: languageProfiles.items.length > 1 + }; + + if (items.length) { + const rootFolder = _.find(items, { id: rootFolderId }); + + return { + ...result, + ...rootFolder, + items: importArtistState.items + }; + } + + return result; + } + ); +} + +const mapDispatchToProps = { + setImportArtistValue, + importArtist, + clearImportArtist, + fetchRootFolders, + setAddArtistDefault +}; + +class ImportArtistConnector extends Component { + + // + // Lifecycle + + componentDidMount() { + if (!this.props.rootFoldersPopulated) { + this.props.fetchRootFolders(); + } + } + + componentWillUnmount() { + this.props.clearImportArtist(); + } + + // + // Listeners + + onInputChange = (ids, name, value) => { + this.props.setAddArtistDefault({ [name]: value }); + + ids.forEach((id) => { + this.props.setImportArtistValue({ + id, + [name]: value + }); + }); + } + + onImportPress = (ids) => { + this.props.importArtist({ ids }); + } + + // + // Render + + render() { + return ( + + ); + } +} + +const routeMatchShape = createRouteMatchShape({ + rootFolderId: PropTypes.string.isRequired +}); + +ImportArtistConnector.propTypes = { + match: routeMatchShape.isRequired, + rootFoldersPopulated: PropTypes.bool.isRequired, + setImportArtistValue: PropTypes.func.isRequired, + importArtist: PropTypes.func.isRequired, + clearImportArtist: PropTypes.func.isRequired, + fetchRootFolders: PropTypes.func.isRequired, + setAddArtistDefault: PropTypes.func.isRequired +}; + +export default connect(createMapStateToProps, mapDispatchToProps)(ImportArtistConnector); diff --git a/frontend/src/AddArtist/ImportArtist/Import/ImportArtistFooter.css b/frontend/src/AddArtist/ImportArtist/Import/ImportArtistFooter.css new file mode 100644 index 000000000..1df1b8c90 --- /dev/null +++ b/frontend/src/AddArtist/ImportArtist/Import/ImportArtistFooter.css @@ -0,0 +1,27 @@ +.inputContainer { + margin-right: 20px; + min-width: 150px; +} + +.label { + margin-bottom: 3px; + font-weight: bold; +} + +.importButtonContainer { + display: flex; + align-items: center; +} + +.importButton { + composes: button from 'Components/Link/SpinnerButton.css'; + + height: 35px; +} + +.loading { + composes: loading from 'Components/Loading/LoadingIndicator.css'; + + margin: 0 10px 0 12px; + text-align: left; +} diff --git a/frontend/src/AddArtist/ImportArtist/Import/ImportArtistFooter.js b/frontend/src/AddArtist/ImportArtist/Import/ImportArtistFooter.js new file mode 100644 index 000000000..76fe864f8 --- /dev/null +++ b/frontend/src/AddArtist/ImportArtist/Import/ImportArtistFooter.js @@ -0,0 +1,261 @@ +import _ from 'lodash'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { inputTypes, kinds } from 'Helpers/Props'; +import SpinnerButton from 'Components/Link/SpinnerButton'; +import LoadingIndicator from 'Components/Loading/LoadingIndicator'; +import CheckInput from 'Components/Form/CheckInput'; +import FormInputGroup from 'Components/Form/FormInputGroup'; +import PageContentFooter from 'Components/Page/PageContentFooter'; +import styles from './ImportArtistFooter.css'; + +const MIXED = 'mixed'; + +class ImportArtistFooter extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + const { + defaultMonitor, + defaultQualityProfileId, + defaultLanguageProfileId, + defaultAlbumFolder, + defaultPrimaryAlbumTypes, + defaultSecondaryAlbumTypes + } = props; + + this.state = { + monitor: defaultMonitor, + qualityProfileId: defaultQualityProfileId, + languageProfileId: defaultLanguageProfileId, + albumFolder: defaultAlbumFolder, + primaryAlbumTypes: defaultPrimaryAlbumTypes, + secondaryAlbumTypes: defaultSecondaryAlbumTypes + }; + } + + componentDidUpdate(prevProps, prevState) { + const { + defaultMonitor, + defaultQualityProfileId, + defaultLanguageProfileId, + defaultAlbumFolder, + defaultPrimaryAlbumTypes, + defaultSecondaryAlbumTypes, + isMonitorMixed, + isQualityProfileIdMixed, + isLanguageProfileIdMixed, + isAlbumFolderMixed, + isPrimaryAlbumTypesMixed, + isSecondaryAlbumTypesMixed + } = this.props; + + const { + monitor, + qualityProfileId, + languageProfileId, + albumFolder, + primaryAlbumTypes, + secondaryAlbumTypes + } = this.state; + + const newState = {}; + + if (isMonitorMixed && monitor !== MIXED) { + newState.monitor = MIXED; + } else if (!isMonitorMixed && monitor !== defaultMonitor) { + newState.monitor = defaultMonitor; + } + + if (isQualityProfileIdMixed && qualityProfileId !== MIXED) { + newState.qualityProfileId = MIXED; + } else if (!isQualityProfileIdMixed && qualityProfileId !== defaultQualityProfileId) { + newState.qualityProfileId = defaultQualityProfileId; + } + + if (isLanguageProfileIdMixed && languageProfileId !== MIXED) { + newState.languageProfileId = MIXED; + } else if (!isLanguageProfileIdMixed && languageProfileId !== defaultLanguageProfileId) { + newState.languageProfileId = defaultLanguageProfileId; + } + + if (isAlbumFolderMixed && albumFolder != null) { + newState.albumFolder = null; + } else if (!isAlbumFolderMixed && albumFolder !== defaultAlbumFolder) { + newState.albumFolder = defaultAlbumFolder; + } + + if (isPrimaryAlbumTypesMixed && primaryAlbumTypes != null) { + newState.primaryAlbumTypes = null; + } else if (!isPrimaryAlbumTypesMixed && primaryAlbumTypes !== defaultPrimaryAlbumTypes) { + newState.primaryAlbumTypes = defaultPrimaryAlbumTypes; + } + + if (isSecondaryAlbumTypesMixed && secondaryAlbumTypes != null) { + newState.secondaryAlbumTypes = null; + } else if (!isSecondaryAlbumTypesMixed && secondaryAlbumTypes !== defaultSecondaryAlbumTypes) { + newState.secondaryAlbumTypes = defaultSecondaryAlbumTypes; + } + + if (!_.isEmpty(newState)) { + this.setState(newState); + } + } + + // + // Listeners + + onInputChange = ({ name, value }) => { + this.setState({ [name]: value }); + this.props.onInputChange({ name, value }); + } + + // + // Render + + render() { + const { + selectedCount, + isImporting, + isLookingUpArtist, + isMonitorMixed, + isQualityProfileIdMixed, + isLanguageProfileIdMixed, + showLanguageProfile, + onImportPress + } = this.props; + + const { + monitor, + qualityProfileId, + languageProfileId, + albumFolder, + primaryAlbumTypes, + secondaryAlbumTypes + } = this.state; + + return ( + +
+
+ Monitor +
+ + +
+ +
+
+ Quality Profile +
+ + +
+ + { + showLanguageProfile && + +
+
+ Language Profile +
+ + +
+ } + +
+
+ Album Folder +
+ + +
+ +
+
+   +
+ +
+ + Import {selectedCount} Artist(s) + + + { + isLookingUpArtist && + + } + + { + isLookingUpArtist && + 'Processing Folders' + } +
+
+
+ ); + } +} + +ImportArtistFooter.propTypes = { + selectedCount: PropTypes.number.isRequired, + isImporting: PropTypes.bool.isRequired, + isLookingUpArtist: PropTypes.bool.isRequired, + defaultMonitor: PropTypes.string.isRequired, + defaultQualityProfileId: PropTypes.number, + defaultLanguageProfileId: PropTypes.number, + defaultAlbumFolder: PropTypes.bool.isRequired, + defaultPrimaryAlbumTypes: PropTypes.arrayOf(PropTypes.string).isRequired, + defaultSecondaryAlbumTypes: PropTypes.arrayOf(PropTypes.string).isRequired, + isMonitorMixed: PropTypes.bool.isRequired, + isQualityProfileIdMixed: PropTypes.bool.isRequired, + isLanguageProfileIdMixed: PropTypes.bool.isRequired, + isAlbumFolderMixed: PropTypes.bool.isRequired, + isPrimaryAlbumTypesMixed: PropTypes.bool.isRequired, + isSecondaryAlbumTypesMixed: PropTypes.bool.isRequired, + showLanguageProfile: PropTypes.bool.isRequired, + onInputChange: PropTypes.func.isRequired, + onImportPress: PropTypes.func.isRequired +}; + +export default ImportArtistFooter; diff --git a/frontend/src/AddArtist/ImportArtist/Import/ImportArtistFooterConnector.js b/frontend/src/AddArtist/ImportArtist/Import/ImportArtistFooterConnector.js new file mode 100644 index 000000000..938efd916 --- /dev/null +++ b/frontend/src/AddArtist/ImportArtist/Import/ImportArtistFooterConnector.js @@ -0,0 +1,61 @@ +import _ from 'lodash'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import ImportArtistFooter from './ImportArtistFooter'; + +function isMixed(items, selectedIds, defaultValue, key) { + return _.some(items, (artist) => { + return selectedIds.indexOf(artist.id) > -1 && artist[key] !== defaultValue; + }); +} + +function createMapStateToProps() { + return createSelector( + (state) => state.addArtist, + (state) => state.importArtist, + (state, { selectedIds }) => selectedIds, + (addArtist, importArtist, selectedIds) => { + const { + monitor: defaultMonitor, + qualityProfileId: defaultQualityProfileId, + languageProfileId: defaultLanguageProfileId, + albumFolder: defaultAlbumFolder, + primaryAlbumTypes: defaultPrimaryAlbumTypes, + secondaryAlbumTypes: defaultSecondaryAlbumTypes + } = addArtist.defaults; + + const items = importArtist.items; + + const isLookingUpArtist = _.some(importArtist.items, (artist) => { + return !artist.isPopulated && artist.error == null; + }); + + const isMonitorMixed = isMixed(items, selectedIds, defaultMonitor, 'monitor'); + const isQualityProfileIdMixed = isMixed(items, selectedIds, defaultQualityProfileId, 'qualityProfileId'); + const isLanguageProfileIdMixed = isMixed(items, selectedIds, defaultLanguageProfileId, 'languageProfileId'); + const isAlbumFolderMixed = isMixed(items, selectedIds, defaultAlbumFolder, 'albumFolder'); + const isPrimaryAlbumTypesMixed = isMixed(items, selectedIds, defaultPrimaryAlbumTypes, 'primaryAlbumTypes'); + const isSecondaryAlbumTypesMixed = isMixed(items, selectedIds, defaultSecondaryAlbumTypes, 'secondaryAlbumTypes'); + + return { + selectedCount: selectedIds.length, + isImporting: importArtist.isImporting, + isLookingUpArtist, + defaultMonitor, + defaultQualityProfileId, + defaultLanguageProfileId, + defaultAlbumFolder, + defaultPrimaryAlbumTypes, + defaultSecondaryAlbumTypes, + isMonitorMixed, + isQualityProfileIdMixed, + isLanguageProfileIdMixed, + isAlbumFolderMixed, + isPrimaryAlbumTypesMixed, + isSecondaryAlbumTypesMixed + }; + } + ); +} + +export default connect(createMapStateToProps)(ImportArtistFooter); diff --git a/frontend/src/AddArtist/ImportArtist/Import/ImportArtistHeader.css b/frontend/src/AddArtist/ImportArtist/Import/ImportArtistHeader.css new file mode 100644 index 000000000..a42c0c696 --- /dev/null +++ b/frontend/src/AddArtist/ImportArtist/Import/ImportArtistHeader.css @@ -0,0 +1,38 @@ +.folder { + composes: headerCell from 'Components/Table/VirtualTableHeaderCell.css'; + + flex: 1 0 200px; +} + +.monitor { + composes: headerCell from 'Components/Table/VirtualTableHeaderCell.css'; + + flex: 0 1 200px; + min-width: 185px; +} + +.qualityProfile, +.languageProfile { + composes: headerCell from 'Components/Table/VirtualTableHeaderCell.css'; + + flex: 0 1 250px; + min-width: 170px; +} + +.albumFolder { + composes: headerCell from 'Components/Table/VirtualTableHeaderCell.css'; + + flex: 0 1 150px; + min-width: 120px; +} + +.artist { + composes: headerCell from 'Components/Table/VirtualTableHeaderCell.css'; + + flex: 0 1 400px; + min-width: 300px; +} + +.detailsIcon { + margin-left: 8px; +} diff --git a/frontend/src/AddArtist/ImportArtist/Import/ImportArtistHeader.js b/frontend/src/AddArtist/ImportArtist/Import/ImportArtistHeader.js new file mode 100644 index 000000000..edb07beb4 --- /dev/null +++ b/frontend/src/AddArtist/ImportArtist/Import/ImportArtistHeader.js @@ -0,0 +1,96 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import { icons, tooltipPositions } from 'Helpers/Props'; +import Icon from 'Components/Icon'; +import Popover from 'Components/Tooltip/Popover'; +import VirtualTableHeader from 'Components/Table/VirtualTableHeader'; +import VirtualTableHeaderCell from 'Components/Table/VirtualTableHeaderCell'; +import VirtualTableSelectAllHeaderCell from 'Components/Table/VirtualTableSelectAllHeaderCell'; +import ArtistMonitoringOptionsPopoverContent from 'AddArtist/ArtistMonitoringOptionsPopoverContent'; +// import SeriesTypePopoverContent from 'AddArtist/SeriesTypePopoverContent'; +import styles from './ImportArtistHeader.css'; + +function ImportArtistHeader(props) { + const { + showLanguageProfile, + allSelected, + allUnselected, + onSelectAllChange + } = props; + + return ( + + + + + Folder + + + + Monitor + + + } + title="Monitoring Options" + body={} + position={tooltipPositions.RIGHT} + /> + + + + Quality Profile + + + { + showLanguageProfile && + + Language Profile + + } + + + Album Folder + + + + Artist + + + ); +} + +ImportArtistHeader.propTypes = { + showLanguageProfile: PropTypes.bool.isRequired, + allSelected: PropTypes.bool.isRequired, + allUnselected: PropTypes.bool.isRequired, + onSelectAllChange: PropTypes.func.isRequired +}; + +export default ImportArtistHeader; diff --git a/frontend/src/AddArtist/ImportArtist/Import/ImportArtistRow.css b/frontend/src/AddArtist/ImportArtist/Import/ImportArtistRow.css new file mode 100644 index 000000000..26e0998f3 --- /dev/null +++ b/frontend/src/AddArtist/ImportArtist/Import/ImportArtistRow.css @@ -0,0 +1,45 @@ +.selectInput { + composes: input from 'Components/Form/CheckInput.css'; +} + +.folder { + composes: cell from 'Components/Table/Cells/VirtualTableRowCell.css'; + + flex: 1 0 200px; + line-height: 36px; +} + +.monitor { + composes: cell from 'Components/Table/Cells/VirtualTableRowCell.css'; + + flex: 0 1 200px; + min-width: 185px; +} + +.qualityProfile, +.languageProfile { + composes: cell from 'Components/Table/Cells/VirtualTableRowCell.css'; + + flex: 0 1 250px; + min-width: 170px; +} + +.albumFolder { + composes: cell from 'Components/Table/Cells/VirtualTableRowCell.css'; + + flex: 0 1 150px; + min-width: 120px; +} + +.artist { + composes: cell from 'Components/Table/Cells/VirtualTableRowCell.css'; + + flex: 0 1 400px; + min-width: 300px; +} + +.hideLanguageProfile { + composes: cell from 'Components/Table/Cells/VirtualTableRowCell.css'; + + display: none; +} diff --git a/frontend/src/AddArtist/ImportArtist/Import/ImportArtistRow.js b/frontend/src/AddArtist/ImportArtist/Import/ImportArtistRow.js new file mode 100644 index 000000000..ddfc3cd10 --- /dev/null +++ b/frontend/src/AddArtist/ImportArtist/Import/ImportArtistRow.js @@ -0,0 +1,110 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import { inputTypes } from 'Helpers/Props'; +import FormInputGroup from 'Components/Form/FormInputGroup'; +import VirtualTableRow from 'Components/Table/VirtualTableRow'; +import VirtualTableRowCell from 'Components/Table/Cells/VirtualTableRowCell'; +import VirtualTableSelectCell from 'Components/Table/Cells/VirtualTableSelectCell'; +import ImportArtistSelectArtistConnector from './SelectArtist/ImportArtistSelectArtistConnector'; +import styles from './ImportArtistRow.css'; + +function ImportArtistRow(props) { + const { + style, + id, + monitor, + qualityProfileId, + languageProfileId, + albumFolder, + selectedArtist, + isExistingArtist, + showLanguageProfile, + isSelected, + onSelectedChange, + onInputChange + } = props; + + return ( + + + + + {id} + + + + + + + + + + + + + + + + + + + + + + + ); +} + +ImportArtistRow.propTypes = { + style: PropTypes.object.isRequired, + id: PropTypes.string.isRequired, + monitor: PropTypes.string.isRequired, + qualityProfileId: PropTypes.number.isRequired, + languageProfileId: PropTypes.number.isRequired, + albumFolder: PropTypes.bool.isRequired, + selectedArtist: PropTypes.object, + isExistingArtist: PropTypes.bool.isRequired, + items: PropTypes.arrayOf(PropTypes.object).isRequired, + queued: PropTypes.bool.isRequired, + showLanguageProfile: PropTypes.bool.isRequired, + isSelected: PropTypes.bool, + onSelectedChange: PropTypes.func.isRequired, + onInputChange: PropTypes.func.isRequired +}; + +ImportArtistRow.defaultsProps = { + items: [] +}; + +export default ImportArtistRow; diff --git a/frontend/src/AddArtist/ImportArtist/Import/ImportArtistRowConnector.js b/frontend/src/AddArtist/ImportArtist/Import/ImportArtistRowConnector.js new file mode 100644 index 000000000..219b82e86 --- /dev/null +++ b/frontend/src/AddArtist/ImportArtist/Import/ImportArtistRowConnector.js @@ -0,0 +1,89 @@ +import _ from 'lodash'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import { queueLookupArtist, setImportArtistValue } from 'Store/Actions/importArtistActions'; +import createAllArtistSelector from 'Store/Selectors/createAllArtistSelector'; +import ImportArtistRow from './ImportArtistRow'; + +function createImportArtistItemSelector() { + return createSelector( + (state, { id }) => id, + (state) => state.importArtist.items, + (id, items) => { + return _.find(items, { id }) || {}; + } + ); +} + +function createMapStateToProps() { + return createSelector( + createImportArtistItemSelector(), + createAllArtistSelector(), + (item, artist) => { + const selectedArtist = item && item.selectedArtist; + const isExistingArtist = !!selectedArtist && _.some(artist, { foreignArtistId: selectedArtist.foreignArtistId }); + + return { + ...item, + isExistingArtist + }; + } + ); +} + +const mapDispatchToProps = { + queueLookupArtist, + setImportArtistValue +}; + +class ImportArtistRowConnector extends Component { + + // + // Listeners + + onInputChange = ({ name, value }) => { + this.props.setImportArtistValue({ + id: this.props.id, + [name]: value + }); + } + + // + // Render + + render() { + // Don't show the row until we have the information we require for it. + + const { + items, + monitor, + albumFolder + } = this.props; + + if (!items || !monitor || !albumFolder == null) { + return null; + } + + return ( + + ); + } +} + +ImportArtistRowConnector.propTypes = { + rootFolderId: PropTypes.number.isRequired, + id: PropTypes.string.isRequired, + monitor: PropTypes.string, + albumFolder: PropTypes.bool, + items: PropTypes.arrayOf(PropTypes.object), + queueLookupArtist: PropTypes.func.isRequired, + setImportArtistValue: PropTypes.func.isRequired +}; + +export default connect(createMapStateToProps, mapDispatchToProps)(ImportArtistRowConnector); diff --git a/frontend/src/AddArtist/ImportArtist/Import/ImportArtistSelected.css b/frontend/src/AddArtist/ImportArtist/Import/ImportArtistSelected.css new file mode 100644 index 000000000..efc6dccb3 --- /dev/null +++ b/frontend/src/AddArtist/ImportArtist/Import/ImportArtistSelected.css @@ -0,0 +1,3 @@ +.input { + composes: input from 'Components/Form/CheckInput.css'; +} diff --git a/frontend/src/AddArtist/ImportArtist/Import/ImportArtistTable.js b/frontend/src/AddArtist/ImportArtist/Import/ImportArtistTable.js new file mode 100644 index 000000000..724be157d --- /dev/null +++ b/frontend/src/AddArtist/ImportArtist/Import/ImportArtistTable.js @@ -0,0 +1,216 @@ +import _ from 'lodash'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import VirtualTable from 'Components/Table/VirtualTable'; +import ImportArtistHeader from './ImportArtistHeader'; +import ImportArtistRowConnector from './ImportArtistRowConnector'; + +class ImportArtistTable extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this._table = null; + } + + componentDidMount() { + const { + unmappedFolders, + defaultMonitor, + defaultQualityProfileId, + defaultLanguageProfileId, + defaultAlbumFolder, + defaultPrimaryAlbumTypes, + defaultSecondaryAlbumTypes, + onArtistLookup, + onSetImportArtistValue + } = this.props; + + const values = { + monitor: defaultMonitor, + qualityProfileId: defaultQualityProfileId, + languageProfileId: defaultLanguageProfileId, + albumFolder: defaultAlbumFolder, + primaryAlbumTypes: defaultPrimaryAlbumTypes, + secondaryAlbumTypes: defaultSecondaryAlbumTypes + }; + + unmappedFolders.forEach((unmappedFolder) => { + const id = unmappedFolder.name; + + onArtistLookup(id, unmappedFolder.path); + + onSetImportArtistValue({ + id, + ...values + }); + }); + } + + // This isn't great, but it's the most reliable way to ensure the items + // are checked off even if they aren't actually visible since the cells + // are virtualized. + + componentDidUpdate(prevProps) { + const { + items, + selectedState, + onSelectedChange, + onRemoveSelectedStateItem + } = this.props; + + prevProps.items.forEach((prevItem) => { + const { + id + } = prevItem; + + const item = _.find(items, { id }); + + if (!item) { + onRemoveSelectedStateItem(id); + return; + } + + const selectedArtist = item.selectedArtist; + const isSelected = selectedState[id]; + + const isExistingArtist = !!selectedArtist && + _.some(prevProps.allArtists, { foreignArtistId: selectedArtist.foreignArtistId }); + + // Props doesn't have a selected artist or + // the selected artist is an existing artist. + if ((selectedArtist && !prevItem.selectedArtist) || (isExistingArtist && !prevItem.selectedArtist)) { + onSelectedChange({ id, value: false }); + + return; + } + + // State is selected, but a artist isn't selected or + // the selected artist is an existing artist. + if (isSelected && (!selectedArtist || isExistingArtist)) { + onSelectedChange({ id, value: false }); + + return; + } + + // A artist is being selected that wasn't previously selected. + if (selectedArtist && selectedArtist !== prevItem.selectedArtist) { + onSelectedChange({ id, value: true }); + + return; + } + }); + + // Forces the table to re-render if the selected state + // has changed otherwise it will be stale. + + if (prevProps.selectedState !== selectedState && this._table) { + this._table.forceUpdateGrid(); + } + } + + // + // Control + + setTableRef = (ref) => { + this._table = ref; + } + + rowRenderer = ({ key, rowIndex, style }) => { + const { + rootFolderId, + items, + selectedState, + showLanguageProfile, + onSelectedChange + } = this.props; + + const item = items[rowIndex]; + + return ( + + ); + } + + // + // Render + + render() { + const { + items, + allSelected, + allUnselected, + isSmallScreen, + contentBody, + showLanguageProfile, + scrollTop, + onSelectAllChange, + onScroll + } = this.props; + + if (!items.length) { + return null; + } + + return ( + + } + onScroll={onScroll} + /> + ); + } +} + +ImportArtistTable.propTypes = { + rootFolderId: PropTypes.number.isRequired, + items: PropTypes.arrayOf(PropTypes.object), + unmappedFolders: PropTypes.arrayOf(PropTypes.object), + defaultMonitor: PropTypes.string.isRequired, + defaultQualityProfileId: PropTypes.number, + defaultLanguageProfileId: PropTypes.number, + defaultAlbumFolder: PropTypes.bool.isRequired, + defaultPrimaryAlbumTypes: PropTypes.arrayOf(PropTypes.string).isRequired, + defaultSecondaryAlbumTypes: PropTypes.arrayOf(PropTypes.string).isRequired, + allSelected: PropTypes.bool.isRequired, + allUnselected: PropTypes.bool.isRequired, + selectedState: PropTypes.object.isRequired, + isSmallScreen: PropTypes.bool.isRequired, + allArtists: PropTypes.arrayOf(PropTypes.object), + contentBody: PropTypes.object.isRequired, + showLanguageProfile: PropTypes.bool.isRequired, + scrollTop: PropTypes.number.isRequired, + onSelectAllChange: PropTypes.func.isRequired, + onSelectedChange: PropTypes.func.isRequired, + onRemoveSelectedStateItem: PropTypes.func.isRequired, + onArtistLookup: PropTypes.func.isRequired, + onSetImportArtistValue: PropTypes.func.isRequired, + onScroll: PropTypes.func.isRequired +}; + +export default ImportArtistTable; diff --git a/frontend/src/AddArtist/ImportArtist/Import/ImportArtistTableConnector.js b/frontend/src/AddArtist/ImportArtist/Import/ImportArtistTableConnector.js new file mode 100644 index 000000000..c232db77f --- /dev/null +++ b/frontend/src/AddArtist/ImportArtist/Import/ImportArtistTableConnector.js @@ -0,0 +1,45 @@ +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import { queueLookupArtist, setImportArtistValue } from 'Store/Actions/importArtistActions'; +import createAllArtistSelector from 'Store/Selectors/createAllArtistSelector'; +import ImportArtistTable from './ImportArtistTable'; + +function createMapStateToProps() { + return createSelector( + (state) => state.addArtist, + (state) => state.importArtist, + (state) => state.app.dimensions, + createAllArtistSelector(), + (addArtist, importArtist, dimensions, allArtists) => { + return { + defaultMonitor: addArtist.defaults.monitor, + defaultQualityProfileId: addArtist.defaults.qualityProfileId, + defaultLanguageProfileId: addArtist.defaults.languageProfileId, + defaultAlbumFolder: addArtist.defaults.albumFolder, + defaultPrimaryAlbumTypes: addArtist.defaults.primaryAlbumTypes, + defaultSecondaryAlbumTypes: addArtist.defaults.secondaryAlbumTypes, + items: importArtist.items, + isSmallScreen: dimensions.isSmallScreen, + allArtists + }; + } + ); +} + +function createMapDispatchToProps(dispatch, props) { + return { + onArtistLookup(name, path) { + dispatch(queueLookupArtist({ + name, + path, + term: name + })); + }, + + onSetImportArtistValue(values) { + dispatch(setImportArtistValue(values)); + } + }; +} + +export default connect(createMapStateToProps, createMapDispatchToProps)(ImportArtistTable); diff --git a/frontend/src/AddArtist/ImportArtist/Import/SelectArtist/ImportArtistName.css b/frontend/src/AddArtist/ImportArtist/Import/SelectArtist/ImportArtistName.css new file mode 100644 index 000000000..263e91fda --- /dev/null +++ b/frontend/src/AddArtist/ImportArtist/Import/SelectArtist/ImportArtistName.css @@ -0,0 +1,22 @@ +.artistNameContainer { + display: flex; + align-items: center; +} + +.artistName { + margin-right: 5px; +} + +.disambiguation { + margin-right: 5px; + color: $disabledColor; +} + +.year { + margin-left: 5px; + color: $disabledColor; +} + +.existing { + margin-left: 5px; +} diff --git a/frontend/src/AddArtist/ImportArtist/Import/SelectArtist/ImportArtistName.js b/frontend/src/AddArtist/ImportArtist/Import/SelectArtist/ImportArtistName.js new file mode 100644 index 000000000..25d4edd16 --- /dev/null +++ b/frontend/src/AddArtist/ImportArtist/Import/SelectArtist/ImportArtistName.js @@ -0,0 +1,43 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import { kinds } from 'Helpers/Props'; +import Label from 'Components/Label'; +import styles from './ImportArtistName.css'; + +function ImportArtistName(props) { + const { + artistName, + disambiguation, + // year, + isExistingArtist + } = props; + + return ( +
+
+ {artistName} +
+
+ {disambiguation} +
+ + { + isExistingArtist && + + } +
+ ); +} + +ImportArtistName.propTypes = { + artistName: PropTypes.string.isRequired, + disambiguation: PropTypes.string, + // year: PropTypes.number.isRequired, + isExistingArtist: PropTypes.bool.isRequired +}; + +export default ImportArtistName; diff --git a/frontend/src/AddArtist/ImportArtist/Import/SelectArtist/ImportArtistSearchResult.css b/frontend/src/AddArtist/ImportArtist/Import/SelectArtist/ImportArtistSearchResult.css new file mode 100644 index 000000000..421a55237 --- /dev/null +++ b/frontend/src/AddArtist/ImportArtist/Import/SelectArtist/ImportArtistSearchResult.css @@ -0,0 +1,8 @@ +.artist { + padding: 10px 20px; + width: 100%; + + &:hover { + background-color: $menuItemHoverColor; + } +} diff --git a/frontend/src/AddArtist/ImportArtist/Import/SelectArtist/ImportArtistSearchResult.js b/frontend/src/AddArtist/ImportArtist/Import/SelectArtist/ImportArtistSearchResult.js new file mode 100644 index 000000000..aa489f0fb --- /dev/null +++ b/frontend/src/AddArtist/ImportArtist/Import/SelectArtist/ImportArtistSearchResult.js @@ -0,0 +1,52 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import Link from 'Components/Link/Link'; +import ImportArtistName from './ImportArtistName'; +import styles from './ImportArtistSearchResult.css'; + +class ImportArtistSearchResult extends Component { + + // + // Listeners + + onPress = () => { + this.props.onPress(this.props.foreignArtistId); + } + + // + // Render + + render() { + const { + artistName, + disambiguation, + // year, + isExistingArtist + } = this.props; + + return ( + + + + ); + } +} + +ImportArtistSearchResult.propTypes = { + foreignArtistId: PropTypes.string.isRequired, + artistName: PropTypes.string.isRequired, + disambiguation: PropTypes.string, + // year: PropTypes.number.isRequired, + isExistingArtist: PropTypes.bool.isRequired, + onPress: PropTypes.func.isRequired +}; + +export default ImportArtistSearchResult; diff --git a/frontend/src/AddArtist/ImportArtist/Import/SelectArtist/ImportArtistSearchResultConnector.js b/frontend/src/AddArtist/ImportArtist/Import/SelectArtist/ImportArtistSearchResultConnector.js new file mode 100644 index 000000000..cdbcc03b3 --- /dev/null +++ b/frontend/src/AddArtist/ImportArtist/Import/SelectArtist/ImportArtistSearchResultConnector.js @@ -0,0 +1,17 @@ +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import createExistingArtistSelector from 'Store/Selectors/createExistingArtistSelector'; +import ImportArtistSearchResult from './ImportArtistSearchResult'; + +function createMapStateToProps() { + return createSelector( + createExistingArtistSelector(), + (isExistingArtist) => { + return { + isExistingArtist + }; + } + ); +} + +export default connect(createMapStateToProps)(ImportArtistSearchResult); diff --git a/frontend/src/AddArtist/ImportArtist/Import/SelectArtist/ImportArtistSelectArtist.css b/frontend/src/AddArtist/ImportArtist/Import/SelectArtist/ImportArtistSelectArtist.css new file mode 100644 index 000000000..4d9159a70 --- /dev/null +++ b/frontend/src/AddArtist/ImportArtist/Import/SelectArtist/ImportArtistSelectArtist.css @@ -0,0 +1,71 @@ +.tether { + z-index: 2000; +} + +.button { + composes: link from 'Components/Link/Link.css'; + + position: relative; + display: flex; + align-items: center; + padding: 6px 16px; + width: 100%; + height: 35px; + border: 1px solid $inputBorderColor; + border-radius: 4px; + background-color: $white; + box-shadow: inset 0 1px 1px $inputBoxShadowColor; +} + +.loading { + display: inline-block; +} + +.warningIcon { + margin-right: 8px; +} + +.existing { + margin-left: 5px; +} + +.dropdownArrowContainer { + position: absolute; + right: 16px; +} + +.contentContainer { + margin-top: 4px; + padding: 0 8px; + width: 400px; +} + +.content { + padding: 4px; + border: 1px solid $inputBorderColor; + border-radius: 4px; + background-color: $white; +} + +.searchContainer { + display: flex; +} + +.searchIconContainer { + width: 58px; + border: 1px solid $inputBorderColor; + border-right: none; + border-radius: 4px; + border-top-right-radius: 0; + border-bottom-right-radius: 0; + background-color: #edf1f2; + text-align: center; + line-height: 33px; +} + +.searchInput { + composes: text from 'Components/Form/TextInput.css'; + + border-top-left-radius: 0; + border-bottom-left-radius: 0; +} diff --git a/frontend/src/AddArtist/ImportArtist/Import/SelectArtist/ImportArtistSelectArtist.js b/frontend/src/AddArtist/ImportArtist/Import/SelectArtist/ImportArtistSelectArtist.js new file mode 100644 index 000000000..e0c883449 --- /dev/null +++ b/frontend/src/AddArtist/ImportArtist/Import/SelectArtist/ImportArtistSelectArtist.js @@ -0,0 +1,267 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import ReactDOM from 'react-dom'; +import TetherComponent from 'react-tether'; +import { icons, kinds } from 'Helpers/Props'; +import Icon from 'Components/Icon'; +import SpinnerIcon from 'Components/SpinnerIcon'; +import Link from 'Components/Link/Link'; +import LoadingIndicator from 'Components/Loading/LoadingIndicator'; +import TextInput from 'Components/Form/TextInput'; +import ImportArtistSearchResultConnector from './ImportArtistSearchResultConnector'; +import ImportArtistName from './ImportArtistName'; +import styles from './ImportArtistSelectArtist.css'; + +const tetherOptions = { + skipMoveElement: true, + constraints: [ + { + to: 'window', + attachment: 'together', + pin: true + } + ], + attachment: 'top center', + targetAttachment: 'bottom center' +}; + +class ImportArtistSelectArtist extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this._artistLookupTimeout = null; + + this.state = { + term: props.id, + isOpen: false + }; + } + + // + // Control + + _setButtonRef = (ref) => { + this._buttonRef = ref; + } + + _setContentRef = (ref) => { + this._contentRef = ref; + } + + _addListener() { + window.addEventListener('click', this.onWindowClick); + } + + _removeListener() { + window.removeEventListener('click', this.onWindowClick); + } + + // + // Listeners + + onWindowClick = (event) => { + const button = ReactDOM.findDOMNode(this._buttonRef); + const content = ReactDOM.findDOMNode(this._contentRef); + + if (!button) { + return; + } + + if (!button.contains(event.target) && content && !content.contains(event.target) && this.state.isOpen) { + this.setState({ isOpen: false }); + this._removeListener(); + } + } + + onPress = () => { + if (this.state.isOpen) { + this._removeListener(); + } else { + this._addListener(); + } + + this.setState({ isOpen: !this.state.isOpen }); + } + + onSearchInputChange = ({ value }) => { + if (this._artistLookupTimeout) { + clearTimeout(this._artistLookupTimeout); + } + + this.setState({ term: value }, () => { + this._artistLookupTimeout = setTimeout(() => { + this.props.onSearchInputChange(value); + }, 200); + }); + } + + onArtistSelect = (foreignArtistId) => { + this.setState({ isOpen: false }); + + this.props.onArtistSelect(foreignArtistId); + } + + // + // Render + + render() { + const { + selectedArtist, + isExistingArtist, + isFetching, + isPopulated, + error, + items, + queued + } = this.props; + + const errorMessage = error && + error.responseJSON && + error.responseJSON.message; + + return ( + + + { + queued && !isPopulated && + + } + + { + isPopulated && selectedArtist && isExistingArtist && + + } + + { + isPopulated && selectedArtist && + + } + + { + isPopulated && !selectedArtist && +
+ + + No match found! +
+ } + + { + !isFetching && !!error && +
+ + + Search failed, please try again later. +
+ } + +
+ +
+ + + { + this.state.isOpen && +
+
+
+
+ +
+ + +
+ +
+ { + items.map((item) => { + return ( + + ); + }) + } +
+
+
+ } +
+ ); + } +} + +ImportArtistSelectArtist.propTypes = { + id: PropTypes.string.isRequired, + selectedArtist: PropTypes.object, + isExistingArtist: PropTypes.bool.isRequired, + isFetching: PropTypes.bool.isRequired, + isPopulated: PropTypes.bool.isRequired, + error: PropTypes.object, + items: PropTypes.arrayOf(PropTypes.object).isRequired, + queued: PropTypes.bool.isRequired, + onSearchInputChange: PropTypes.func.isRequired, + onArtistSelect: PropTypes.func.isRequired +}; + +ImportArtistSelectArtist.defaultProps = { + isFetching: true, + isPopulated: false, + items: [], + queued: true +}; + +export default ImportArtistSelectArtist; diff --git a/frontend/src/AddArtist/ImportArtist/Import/SelectArtist/ImportArtistSelectArtistConnector.js b/frontend/src/AddArtist/ImportArtist/Import/SelectArtist/ImportArtistSelectArtistConnector.js new file mode 100644 index 000000000..21662faa7 --- /dev/null +++ b/frontend/src/AddArtist/ImportArtist/Import/SelectArtist/ImportArtistSelectArtistConnector.js @@ -0,0 +1,71 @@ +import _ from 'lodash'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import { queueLookupArtist, setImportArtistValue } from 'Store/Actions/importArtistActions'; +import createImportArtistItemSelector from 'Store/Selectors/createImportArtistItemSelector'; +import ImportArtistSelectArtist from './ImportArtistSelectArtist'; + +function createMapStateToProps() { + return createSelector( + createImportArtistItemSelector(), + (item) => { + return item; + } + ); +} + +const mapDispatchToProps = { + queueLookupArtist, + setImportArtistValue +}; + +class ImportArtistSelectArtistConnector extends Component { + + // + // Listeners + + onSearchInputChange = (term) => { + this.props.queueLookupArtist({ + name: this.props.id, + term + }); + } + + onArtistSelect = (foreignArtistId) => { + const { + id, + items + } = this.props; + + this.props.setImportArtistValue({ + id, + selectedArtist: _.find(items, { foreignArtistId }) + }); + } + + // + // Render + + render() { + return ( + + ); + } +} + +ImportArtistSelectArtistConnector.propTypes = { + id: PropTypes.string.isRequired, + items: PropTypes.arrayOf(PropTypes.object), + selectedArtist: PropTypes.object, + isSelected: PropTypes.bool, + queueLookupArtist: PropTypes.func.isRequired, + setImportArtistValue: PropTypes.func.isRequired +}; + +export default connect(createMapStateToProps, mapDispatchToProps)(ImportArtistSelectArtistConnector); diff --git a/frontend/src/AddArtist/ImportArtist/ImportArtist.js b/frontend/src/AddArtist/ImportArtist/ImportArtist.js new file mode 100644 index 000000000..ce5ec27ee --- /dev/null +++ b/frontend/src/AddArtist/ImportArtist/ImportArtist.js @@ -0,0 +1,30 @@ +import React, { Component } from 'react'; +import { Route } from 'react-router-dom'; +import Switch from 'Components/Router/Switch'; +import ImportArtistSelectFolderConnector from 'AddArtist/ImportArtist/SelectFolder/ImportArtistSelectFolderConnector'; +import ImportArtistConnector from 'AddArtist/ImportArtist/Import/ImportArtistConnector'; + +class ImportArtist extends Component { + + // + // Render + + render() { + return ( + + + + + + ); + } +} + +export default ImportArtist; diff --git a/frontend/src/AddArtist/ImportArtist/SelectFolder/ImportArtistRootFolderRow.css b/frontend/src/AddArtist/ImportArtist/SelectFolder/ImportArtistRootFolderRow.css new file mode 100644 index 000000000..d9c5ccb01 --- /dev/null +++ b/frontend/src/AddArtist/ImportArtist/SelectFolder/ImportArtistRootFolderRow.css @@ -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; +} diff --git a/frontend/src/AddArtist/ImportArtist/SelectFolder/ImportArtistRootFolderRow.js b/frontend/src/AddArtist/ImportArtist/SelectFolder/ImportArtistRootFolderRow.js new file mode 100644 index 000000000..8a4e7b982 --- /dev/null +++ b/frontend/src/AddArtist/ImportArtist/SelectFolder/ImportArtistRootFolderRow.js @@ -0,0 +1,64 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import formatBytes from 'Utilities/Number/formatBytes'; +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 './ImportArtistRootFolderRow.css'; + +function ImportArtistRootFolderRow(props) { + const { + id, + path, + freeSpace, + unmappedFolders, + onDeletePress + } = props; + + const unmappedFoldersCount = unmappedFolders.length || '-'; + + return ( + + + + {path} + + + + + {formatBytes(freeSpace) || '-'} + + + + {unmappedFoldersCount} + + + + + + + ); +} + +ImportArtistRootFolderRow.propTypes = { + id: PropTypes.number.isRequired, + path: PropTypes.string.isRequired, + freeSpace: PropTypes.number.isRequired, + unmappedFolders: PropTypes.arrayOf(PropTypes.object).isRequired, + onDeletePress: PropTypes.func.isRequired +}; + +ImportArtistRootFolderRow.defaultProps = { + freeSpace: 0, + unmappedFolders: [] +}; + +export default ImportArtistRootFolderRow; diff --git a/frontend/src/AddArtist/ImportArtist/SelectFolder/ImportArtistRootFolderRowConnector.js b/frontend/src/AddArtist/ImportArtist/SelectFolder/ImportArtistRootFolderRowConnector.js new file mode 100644 index 000000000..194e6e37c --- /dev/null +++ b/frontend/src/AddArtist/ImportArtist/SelectFolder/ImportArtistRootFolderRowConnector.js @@ -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 ImportArtistRootFolderRow from './ImportArtistRootFolderRow'; + +function createMapStateToProps() { + return createSelector( + () => { + return { + }; + } + ); +} + +const mapDispatchToProps = { + deleteRootFolder +}; + +class ImportArtistRootFolderRowConnector extends Component { + + // + // Listeners + + onDeletePress = () => { + this.props.deleteRootFolder({ id: this.props.id }); + } + + // + // Render + + render() { + return ( + + ); + } +} + +ImportArtistRootFolderRowConnector.propTypes = { + id: PropTypes.number.isRequired, + deleteRootFolder: PropTypes.func.isRequired +}; + +export default connect(createMapStateToProps, mapDispatchToProps)(ImportArtistRootFolderRowConnector); diff --git a/frontend/src/AddArtist/ImportArtist/SelectFolder/ImportArtistSelectFolder.css b/frontend/src/AddArtist/ImportArtist/SelectFolder/ImportArtistSelectFolder.css new file mode 100644 index 000000000..030da96fb --- /dev/null +++ b/frontend/src/AddArtist/ImportArtist/SelectFolder/ImportArtistSelectFolder.css @@ -0,0 +1,32 @@ +.header { + margin-bottom: 40px; + text-align: center; + font-weight: 300; + font-size: 36px; +} + +.tips { + font-size: 20px; +} + +.tip { + font-size: $defaultFontSize; +} + +.code { + font-size: 12px; + font-family: $monoSpaceFontFamily; +} + +.recentFolders { + margin-top: 40px; +} + +.startImport { + margin-top: 40px; + text-align: center; +} + +.importButtonIcon { + margin-right: 8px; +} diff --git a/frontend/src/AddArtist/ImportArtist/SelectFolder/ImportArtistSelectFolder.js b/frontend/src/AddArtist/ImportArtist/SelectFolder/ImportArtistSelectFolder.js new file mode 100644 index 000000000..63eabcfa6 --- /dev/null +++ b/frontend/src/AddArtist/ImportArtist/SelectFolder/ImportArtistSelectFolder.js @@ -0,0 +1,185 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { kinds, sizes } from 'Helpers/Props'; +import Button from 'Components/Link/Button'; +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 Table from 'Components/Table/Table'; +import TableBody from 'Components/Table/TableBody'; +import ImportArtistRootFolderRowConnector from './ImportArtistRootFolderRowConnector'; +import styles from './ImportArtistSelectFolder.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 ImportArtistSelectFolder extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + isAddNewRootFolderModalOpen: false + }; + } + + // + // Lifecycle + + onAddNewRootFolderPress = () => { + this.setState({ isAddNewRootFolderModalOpen: true }); + } + + onNewRootFolderSelect = ({ value }) => { + this.props.onNewRootFolderSelect(value); + } + + onAddRootFolderModalClose = () => { + this.setState({ isAddNewRootFolderModalOpen: false }); + } + + // + // Render + + render() { + const { + isFetching, + isPopulated, + error, + items + } = this.props; + + return ( + + + { + isFetching && !isPopulated && + + } + + { + !isFetching && !!error && +
Unable to load root folders
+ } + + { + !error && isPopulated && +
+
+ Import artist(s) you already have +
+ +
+ Some tips to ensure the import goes smoothly: +
    +
  • + Point Lidarr to the folder containing all of your music not a specific artist. eg. "\music\" and not "\music\alien ant farm\" +
  • +
+
+ + { + items.length > 0 ? +
+
+ + + { + items.map((rootFolder) => { + return ( + + ); + }) + } + +
+
+ + +
: + +
+ +
+ } + + +
+ } +
+
+ ); + } +} + +ImportArtistSelectFolder.propTypes = { + isFetching: PropTypes.bool.isRequired, + isPopulated: PropTypes.bool.isRequired, + error: PropTypes.object, + items: PropTypes.arrayOf(PropTypes.object).isRequired, + onNewRootFolderSelect: PropTypes.func.isRequired, + onDeleteRootFolderPress: PropTypes.func.isRequired +}; + +export default ImportArtistSelectFolder; diff --git a/frontend/src/AddArtist/ImportArtist/SelectFolder/ImportArtistSelectFolderConnector.js b/frontend/src/AddArtist/ImportArtist/SelectFolder/ImportArtistSelectFolderConnector.js new file mode 100644 index 000000000..da95019cb --- /dev/null +++ b/frontend/src/AddArtist/ImportArtist/SelectFolder/ImportArtistSelectFolderConnector.js @@ -0,0 +1,86 @@ +import _ from 'lodash'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import { push } from 'react-router-redux'; +import { fetchRootFolders, addRootFolder, deleteRootFolder } from 'Store/Actions/rootFolderActions'; +import ImportArtistSelectFolder from './ImportArtistSelectFolder'; + +function createMapStateToProps() { + return createSelector( + (state) => state.rootFolders, + (rootFolders) => { + return rootFolders; + } + ); +} + +const mapDispatchToProps = { + fetchRootFolders, + addRootFolder, + deleteRootFolder, + push +}; + +class ImportArtistSelectFolderConnector extends Component { + + // + // Lifecycle + + componentDidMount() { + this.props.fetchRootFolders(); + } + + componentDidUpdate(prevProps) { + const { + items, + isSaving, + saveError + } = this.props; + + if (prevProps.isSaving && !isSaving && !saveError) { + const newRootFolders = _.differenceBy(items, prevProps.items, (item) => item.id); + + if (newRootFolders.length === 1) { + this.props.push(`${window.Sonarr.urlBase}/add/import/${newRootFolders[0].id}`); + } + } + } + + // + // Listeners + + onNewRootFolderSelect = (path) => { + this.props.addRootFolder({ path }); + } + + onDeleteRootFolderPress = (id) => { + this.props.deleteRootFolder({ id }); + } + + // + // Render + + render() { + return ( + + ); + } +} + +ImportArtistSelectFolderConnector.propTypes = { + isSaving: PropTypes.bool.isRequired, + saveError: PropTypes.object, + items: PropTypes.arrayOf(PropTypes.object).isRequired, + fetchRootFolders: PropTypes.func.isRequired, + addRootFolder: PropTypes.func.isRequired, + deleteRootFolder: PropTypes.func.isRequired, + push: PropTypes.func.isRequired +}; + +export default connect(createMapStateToProps, mapDispatchToProps)(ImportArtistSelectFolderConnector); diff --git a/frontend/src/Album/EpisodeDetailsModal.js b/frontend/src/Album/EpisodeDetailsModal.js new file mode 100644 index 000000000..945c2fb8e --- /dev/null +++ b/frontend/src/Album/EpisodeDetailsModal.js @@ -0,0 +1,37 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import Modal from 'Components/Modal/Modal'; +import EpisodeDetailsModalContentConnector from './EpisodeDetailsModalContentConnector'; + +class EpisodeDetailsModal extends Component { + + // + // Render + + render() { + const { + isOpen, + onModalClose, + ...otherProps + } = this.props; + + return ( + + + + ); + } +} + +EpisodeDetailsModal.propTypes = { + isOpen: PropTypes.bool.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default EpisodeDetailsModal; diff --git a/frontend/src/Album/EpisodeDetailsModalContent.css b/frontend/src/Album/EpisodeDetailsModalContent.css new file mode 100644 index 000000000..9d428208d --- /dev/null +++ b/frontend/src/Album/EpisodeDetailsModalContent.css @@ -0,0 +1,44 @@ +.artistName { + margin-left: 5px; +} + +.separator { + margin: 0 5px; +} + +.tabs { + margin-top: -32px; +} + +.tabList { + margin: 0 0 10px; + padding: 0; +} + +.tab { + position: relative; + bottom: -1px; + display: inline-block; + padding: 6px 12px; + border: 1px solid transparent; + border-top: none; + list-style: none; + cursor: pointer; +} + +.selectedTab { + border-color: $borderColor; + border-radius: 0 0 5px 5px; + background-color: rgba(239, 239, 239, 0.4); + color: $black; +} + +.tabPanel { + margin-top: 20px; +} + +.openSeriesButton { + composes: button from 'Components/Link/Button.css'; + + margin-right: auto; +} diff --git a/frontend/src/Album/EpisodeDetailsModalContent.js b/frontend/src/Album/EpisodeDetailsModalContent.js new file mode 100644 index 000000000..ceb212a0f --- /dev/null +++ b/frontend/src/Album/EpisodeDetailsModalContent.js @@ -0,0 +1,196 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { Tab, Tabs, TabList, TabPanel } from 'react-tabs'; +import episodeEntities from 'Album/episodeEntities'; +import Button from 'Components/Link/Button'; +import ModalContent from 'Components/Modal/ModalContent'; +import ModalHeader from 'Components/Modal/ModalHeader'; +import ModalBody from 'Components/Modal/ModalBody'; +import ModalFooter from 'Components/Modal/ModalFooter'; +import MonitorToggleButton from 'Components/MonitorToggleButton'; +import EpisodeSummaryConnector from './Summary/EpisodeSummaryConnector'; +import AlbumHistoryConnector from './History/AlbumHistoryConnector'; +import EpisodeSearchConnector from './Search/EpisodeSearchConnector'; +import styles from './EpisodeDetailsModalContent.css'; + +const tabs = [ + 'details', + 'history', + 'search' +]; + +class EpisodeDetailsModalContent extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + selectedTab: props.selectedTab + }; + } + + // + // Listeners + + onTabSelect = (index, lastIndex) => { + this.setState({ selectedTab: tabs[index] }); + } + + // + // Render + + render() { + const { + albumId, + episodeEntity, + artistId, + artistName, + nameSlug, + albumLabel, + artistMonitored, + episodeTitle, + releaseDate, + monitored, + isSaving, + showOpenArtistButton, + startInteractiveSearch, + onMonitorAlbumPress, + onModalClose + } = this.props; + + const artistLink = `/artist/${nameSlug}`; + + return ( + + + + + + {artistName} + + + - + + {episodeTitle} + + + + + + + Details + + + + History + + + + Search + + + + + + + + + + + + + + + + + + + { + showOpenArtistButton && + + } + + + + + ); + } +} + +EpisodeDetailsModalContent.propTypes = { + albumId: PropTypes.number.isRequired, + episodeEntity: PropTypes.string.isRequired, + artistId: PropTypes.number.isRequired, + artistName: PropTypes.string.isRequired, + nameSlug: PropTypes.string.isRequired, + artistMonitored: PropTypes.bool.isRequired, + releaseDate: PropTypes.string.isRequired, + albumLabel: PropTypes.arrayOf(PropTypes.string).isRequired, + episodeTitle: PropTypes.string.isRequired, + monitored: PropTypes.bool.isRequired, + isSaving: PropTypes.bool, + showOpenArtistButton: PropTypes.bool, + selectedTab: PropTypes.string.isRequired, + startInteractiveSearch: PropTypes.bool.isRequired, + onMonitorAlbumPress: PropTypes.func.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +EpisodeDetailsModalContent.defaultProps = { + selectedTab: 'details', + albumLabel: ['Unknown'], + episodeEntity: episodeEntities.EPISODES, + startInteractiveSearch: false +}; + +export default EpisodeDetailsModalContent; diff --git a/frontend/src/Album/EpisodeDetailsModalContentConnector.js b/frontend/src/Album/EpisodeDetailsModalContentConnector.js new file mode 100644 index 000000000..0e84426f5 --- /dev/null +++ b/frontend/src/Album/EpisodeDetailsModalContentConnector.js @@ -0,0 +1,119 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import { clearReleases } from 'Store/Actions/releaseActions'; +import { toggleEpisodeMonitored } from 'Store/Actions/episodeActions'; +import createEpisodeSelector from 'Store/Selectors/createEpisodeSelector'; +import createArtistSelector from 'Store/Selectors/createArtistSelector'; +import episodeEntities from 'Album/episodeEntities'; +import { fetchTracks, clearTracks } from 'Store/Actions/trackActions'; +import { fetchTrackFiles, clearTrackFiles } from 'Store/Actions/trackFileActions'; +import EpisodeDetailsModalContent from './EpisodeDetailsModalContent'; + +function createMapStateToProps() { + return createSelector( + createEpisodeSelector(), + createArtistSelector(), + (album, artist) => { + const { + artistName, + nameSlug, + monitored: artistMonitored + } = artist; + + return { + artistName, + nameSlug, + artistMonitored, + ...album + }; + } + ); +} + +const mapDispatchToProps = { + clearReleases, + fetchTracks, + clearTracks, + fetchTrackFiles, + clearTrackFiles, + toggleEpisodeMonitored +}; + +class EpisodeDetailsModalContentConnector extends Component { + + // + // Lifecycle + componentDidMount() { + this._populate(); + } + + componentWillUnmount() { + // Clear pending releases here so we can reshow the search + // results even after switching tabs. + this._unpopulate(); + this.props.clearReleases(); + } + + // + // Control + + _populate() { + const artistId = this.props.artistId; + const albumId = this.props.albumId; + this.props.fetchTracks({ artistId, albumId }); + // this.props.fetchTrackFiles({ artistId, albumId }); + } + + _unpopulate() { + this.props.clearTracks(); + // this.props.clearTrackFiles(); + } + + // + // Listeners + + onMonitorAlbumPress = (monitored) => { + const { + albumId, + episodeEntity + } = this.props; + + this.props.toggleEpisodeMonitored({ + episodeEntity, + albumId, + monitored + }); + } + + // + // Render + + render() { + return ( + + ); + } +} + +EpisodeDetailsModalContentConnector.propTypes = { + albumId: PropTypes.number.isRequired, + episodeEntity: PropTypes.string.isRequired, + artistId: PropTypes.number.isRequired, + fetchTracks: PropTypes.func.isRequired, + clearTracks: PropTypes.func.isRequired, + fetchTrackFiles: PropTypes.func.isRequired, + clearTrackFiles: PropTypes.func.isRequired, + clearReleases: PropTypes.func.isRequired, + toggleEpisodeMonitored: PropTypes.func.isRequired +}; + +EpisodeDetailsModalContentConnector.defaultProps = { + episodeEntity: episodeEntities.EPISODES +}; + +export default connect(createMapStateToProps, mapDispatchToProps)(EpisodeDetailsModalContentConnector); diff --git a/frontend/src/Album/EpisodeLanguage.js b/frontend/src/Album/EpisodeLanguage.js new file mode 100644 index 000000000..fc784ef51 --- /dev/null +++ b/frontend/src/Album/EpisodeLanguage.js @@ -0,0 +1,23 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import Label from 'Components/Label'; + +function EpisodeLanguage(props) { + const language = props.language; + + if (!language) { + return null; + } + + return ( + + ); +} + +EpisodeLanguage.propTypes = { + language: PropTypes.object +}; + +export default EpisodeLanguage; diff --git a/frontend/src/Album/EpisodeNumber.css b/frontend/src/Album/EpisodeNumber.css new file mode 100644 index 000000000..1c5072d02 --- /dev/null +++ b/frontend/src/Album/EpisodeNumber.css @@ -0,0 +1,7 @@ +.absoluteEpisodeNumber { + margin-left: 5px; +} + +.warning { + margin-left: 8px; +} diff --git a/frontend/src/Album/EpisodeNumber.js b/frontend/src/Album/EpisodeNumber.js new file mode 100644 index 000000000..88d1cb0e8 --- /dev/null +++ b/frontend/src/Album/EpisodeNumber.js @@ -0,0 +1,106 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import { icons, kinds, tooltipPositions } from 'Helpers/Props'; +import Icon from 'Components/Icon'; +import Popover from 'Components/Tooltip/Popover'; +import SceneInfo from './SceneInfo'; +import styles from './EpisodeNumber.css'; + +function EpisodeNumber(props) { + const { + episodeNumber, + absoluteEpisodeNumber, + sceneSeasonNumber, + sceneEpisodeNumber, + sceneAbsoluteEpisodeNumber, + unverifiedSceneNumbering, + alternateTitles, + artistType + } = props; + + const hasSceneInformation = sceneSeasonNumber !== undefined || + sceneEpisodeNumber !== undefined || + (artistType === 'anime' && sceneAbsoluteEpisodeNumber !== undefined) || + !!alternateTitles.length; + + return ( + + { + hasSceneInformation ? + + {episodeNumber} + + { + artistType === 'anime' && !!absoluteEpisodeNumber && + + ({absoluteEpisodeNumber}) + + } + + } + title="Scene Information" + body={ + + } + position={tooltipPositions.RIGHT} + /> : + + {episodeNumber} + + { + artistType === 'anime' && !!absoluteEpisodeNumber && + + ({absoluteEpisodeNumber}) + + } + + } + + { + unverifiedSceneNumbering && + + } + + { + artistType === 'anime' && !absoluteEpisodeNumber && + + } + + ); +} + +EpisodeNumber.propTypes = { + seasonNumber: PropTypes.number.isRequired, + episodeNumber: PropTypes.number.isRequired, + absoluteEpisodeNumber: PropTypes.number, + sceneSeasonNumber: PropTypes.number, + sceneEpisodeNumber: PropTypes.number, + sceneAbsoluteEpisodeNumber: PropTypes.number, + unverifiedSceneNumbering: PropTypes.bool.isRequired, + alternateTitles: PropTypes.arrayOf(PropTypes.object).isRequired, + artistType: PropTypes.string +}; + +EpisodeNumber.defaultProps = { + unverifiedSceneNumbering: false +}; + +export default EpisodeNumber; diff --git a/frontend/src/Album/EpisodeQuality.js b/frontend/src/Album/EpisodeQuality.js new file mode 100644 index 000000000..1202f8706 --- /dev/null +++ b/frontend/src/Album/EpisodeQuality.js @@ -0,0 +1,54 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import formatBytes from 'Utilities/Number/formatBytes'; +import { kinds } from 'Helpers/Props'; +import Label from 'Components/Label'; + +function getTooltip(title, quality, size) { + const revision = quality.revision; + + if (revision.real && revision.real > 0) { + title += ' [REAL]'; + } + + if (revision.version && revision.version > 1) { + title += ' [PROPER]'; + } + + if (size) { + title += ` - ${formatBytes(size)}`; + } + + return title; +} + +function EpisodeQuality(props) { + const { + title, + quality, + size, + isCutoffNotMet + } = props; + + return ( + + ); +} + +EpisodeQuality.propTypes = { + title: PropTypes.string, + quality: PropTypes.object.isRequired, + size: PropTypes.number, + isCutoffNotMet: PropTypes.bool +}; + +EpisodeQuality.defaultProps = { + title: '' +}; + +export default EpisodeQuality; diff --git a/frontend/src/Album/EpisodeSearchCell.css b/frontend/src/Album/EpisodeSearchCell.css new file mode 100644 index 000000000..5e99b51eb --- /dev/null +++ b/frontend/src/Album/EpisodeSearchCell.css @@ -0,0 +1,6 @@ +.episodeSearchCell { + composes: cell from 'Components/Table/Cells/TableRowCell.css'; + + width: 70px; + white-space: nowrap; +} diff --git a/frontend/src/Album/EpisodeSearchCell.js b/frontend/src/Album/EpisodeSearchCell.js new file mode 100644 index 000000000..eebc9aebf --- /dev/null +++ b/frontend/src/Album/EpisodeSearchCell.js @@ -0,0 +1,83 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { icons } from 'Helpers/Props'; +import IconButton from 'Components/Link/IconButton'; +import SpinnerIconButton from 'Components/Link/SpinnerIconButton'; +import TableRowCell from 'Components/Table/Cells/TableRowCell'; +import EpisodeDetailsModal from './EpisodeDetailsModal'; +import styles from './EpisodeSearchCell.css'; + +class EpisodeSearchCell extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + isDetailsModalOpen: false + }; + } + + // + // Listeners + + onManualSearchPress = () => { + this.setState({ isDetailsModalOpen: true }); + } + + onDetailsModalClose = () => { + this.setState({ isDetailsModalOpen: false }); + } + + // + // Render + + render() { + const { + albumId, + artistId, + episodeTitle, + isSearching, + onSearchPress, + ...otherProps + } = this.props; + + return ( + + + + + + + + ); + } +} + +EpisodeSearchCell.propTypes = { + albumId: PropTypes.number.isRequired, + artistId: PropTypes.number.isRequired, + episodeTitle: PropTypes.string.isRequired, + isSearching: PropTypes.bool.isRequired, + onSearchPress: PropTypes.func.isRequired +}; + +export default EpisodeSearchCell; diff --git a/frontend/src/Album/EpisodeSearchCellConnector.js b/frontend/src/Album/EpisodeSearchCellConnector.js new file mode 100644 index 000000000..20bffae07 --- /dev/null +++ b/frontend/src/Album/EpisodeSearchCellConnector.js @@ -0,0 +1,47 @@ +import _ from 'lodash'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import createArtistSelector from 'Store/Selectors/createArtistSelector'; +import createCommandsSelector from 'Store/Selectors/createCommandsSelector'; +import { executeCommand } from 'Store/Actions/commandActions'; +import * as commandNames from 'Commands/commandNames'; +import EpisodeSearchCell from './EpisodeSearchCell'; + +function createMapStateToProps() { + return createSelector( + (state, { albumId }) => albumId, + (state, { sceneSeasonNumber }) => sceneSeasonNumber, + createArtistSelector(), + createCommandsSelector(), + (albumId, sceneSeasonNumber, artist, commands) => { + const isSearching = _.some(commands, (command) => { + const episodeSearch = command.name === commandNames.ALBUM_SEARCH; + + if (!episodeSearch) { + return false; + } + + return command.body.albumIds.indexOf(albumId) > -1; + }); + + return { + artistMonitored: artist.monitored, + artistType: artist.artistType, + isSearching + }; + } + ); +} + +function createMapDispatchToProps(dispatch, props) { + return { + onSearchPress(name, path) { + dispatch(executeCommand({ + name: commandNames.ALBUM_SEARCH, + albumIds: [props.albumId] + })); + } + }; +} + +export default connect(createMapStateToProps, createMapDispatchToProps)(EpisodeSearchCell); diff --git a/frontend/src/Album/EpisodeStatus.css b/frontend/src/Album/EpisodeStatus.css new file mode 100644 index 000000000..3833887df --- /dev/null +++ b/frontend/src/Album/EpisodeStatus.css @@ -0,0 +1,4 @@ +.center { + display: flex; + justify-content: center; +} diff --git a/frontend/src/Album/EpisodeStatus.js b/frontend/src/Album/EpisodeStatus.js new file mode 100644 index 000000000..9cdbd1923 --- /dev/null +++ b/frontend/src/Album/EpisodeStatus.js @@ -0,0 +1,127 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import isBefore from 'Utilities/Date/isBefore'; +import { icons, kinds, sizes } from 'Helpers/Props'; +import Icon from 'Components/Icon'; +import ProgressBar from 'Components/ProgressBar'; +import QueueDetails from 'Activity/Queue/QueueDetails'; +import EpisodeQuality from './EpisodeQuality'; +import styles from './EpisodeStatus.css'; + +function EpisodeStatus(props) { + const { + airDateUtc, + monitored, + grabbed, + queueItem, + trackFile + } = props; + + const hasTrackFile = !!trackFile; + const isQueued = !!queueItem; + const hasAired = isBefore(airDateUtc); + + if (isQueued) { + const { + sizeleft, + size + } = queueItem; + + const progress = (100 - sizeleft / size * 100); + + return ( +
+ + } + /> +
+ ); + } + + if (grabbed) { + return ( +
+ +
+ ); + } + + if (hasTrackFile) { + const quality = trackFile.quality; + const isCutoffNotMet = trackFile.qualityCutoffNotMet; + + return ( +
+ +
+ ); + } + + if (!airDateUtc) { + return ( +
+ +
+ ); + } + + if (!monitored) { + return ( +
+ +
+ ); + } + + if (hasAired) { + return ( +
+ +
+ ); + } + + return ( +
+ +
+ ); +} + +EpisodeStatus.propTypes = { + airDateUtc: PropTypes.string, + monitored: PropTypes.bool, + grabbed: PropTypes.bool, + queueItem: PropTypes.object, + trackFile: PropTypes.object +}; + +export default EpisodeStatus; diff --git a/frontend/src/Album/EpisodeStatusConnector.js b/frontend/src/Album/EpisodeStatusConnector.js new file mode 100644 index 000000000..f19513b8b --- /dev/null +++ b/frontend/src/Album/EpisodeStatusConnector.js @@ -0,0 +1,53 @@ +import _ from 'lodash'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import createEpisodeSelector from 'Store/Selectors/createEpisodeSelector'; +import createQueueItemSelector from 'Store/Selectors/createQueueItemSelector'; +import createTrackFileSelector from 'Store/Selectors/createTrackFileSelector'; +import EpisodeStatus from './EpisodeStatus'; + +function createMapStateToProps() { + return createSelector( + createEpisodeSelector(), + createQueueItemSelector(), + createTrackFileSelector(), + (episode, queueItem, trackFile) => { + const result = _.pick(episode, [ + 'airDateUtc', + 'monitored', + 'grabbed' + ]); + + result.queueItem = queueItem; + result.trackFile = trackFile; + + return result; + } + ); +} + +const mapDispatchToProps = { +}; + +class EpisodeStatusConnector extends Component { + + // + // Render + + render() { + return ( + + ); + } +} + +EpisodeStatusConnector.propTypes = { + albumId: PropTypes.number.isRequired, + trackFileId: PropTypes.number.isRequired +}; + +export default connect(createMapStateToProps, mapDispatchToProps)(EpisodeStatusConnector); diff --git a/frontend/src/Album/EpisodeTitleLink.css b/frontend/src/Album/EpisodeTitleLink.css new file mode 100644 index 000000000..6022be8a4 --- /dev/null +++ b/frontend/src/Album/EpisodeTitleLink.css @@ -0,0 +1,8 @@ +.link { + composes: link from 'Components/Link/Link.css'; + + &:hover { + color: $linkHoverColor; + text-decoration: underline; + } +} diff --git a/frontend/src/Album/EpisodeTitleLink.js b/frontend/src/Album/EpisodeTitleLink.js new file mode 100644 index 000000000..39efb7c8f --- /dev/null +++ b/frontend/src/Album/EpisodeTitleLink.js @@ -0,0 +1,68 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import Link from 'Components/Link/Link'; +import EpisodeDetailsModal from 'Album/EpisodeDetailsModal'; +import styles from './EpisodeTitleLink.css'; + +class EpisodeTitleLink extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + isDetailsModalOpen: false + }; + } + + // + // Listeners + + onLinkPress = () => { + this.setState({ isDetailsModalOpen: true }); + } + + onModalClose = () => { + this.setState({ isDetailsModalOpen: false }); + } + + // + // Render + + render() { + const { + episodeTitle, + ...otherProps + } = this.props; + + return ( +
+ + {episodeTitle} + + + +
+ ); + } +} + +EpisodeTitleLink.propTypes = { + episodeTitle: PropTypes.string.isRequired +}; + +EpisodeTitleLink.defaultProps = { + showArtistButton: false +}; + +export default EpisodeTitleLink; diff --git a/frontend/src/Album/History/AlbumHistory.js b/frontend/src/Album/History/AlbumHistory.js new file mode 100644 index 000000000..11dfac91b --- /dev/null +++ b/frontend/src/Album/History/AlbumHistory.js @@ -0,0 +1,112 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import LoadingIndicator from 'Components/Loading/LoadingIndicator'; +import Table from 'Components/Table/Table'; +import TableBody from 'Components/Table/TableBody'; +import AlbumHistoryRow from './AlbumHistoryRow'; + +const columns = [ + { + name: 'eventType', + isVisible: true + }, + { + name: 'sourceTitle', + label: 'Source Title', + isVisible: true + }, + { + name: 'quality', + label: 'Quality', + isVisible: true + }, + { + name: 'date', + label: 'Date', + isVisible: true + }, + { + name: 'details', + label: 'Details', + isVisible: true + }, + { + name: 'actions', + label: 'Actions', + isVisible: true + } +]; + +class AlbumHistory extends Component { + + // + // Render + + render() { + const { + isFetching, + isPopulated, + error, + items, + onMarkAsFailedPress + } = this.props; + + const hasItems = !!items.length; + + if (isFetching) { + return ( + + ); + } + + if (!isFetching && !!error) { + return ( +
Unable to load album history.
+ ); + } + + if (isPopulated && !hasItems && !error) { + return ( +
No album history.
+ ); + } + + if (isPopulated && hasItems && !error) { + return ( + + + { + items.map((item) => { + return ( + + ); + }) + } + +
+ ); + } + + return null; + } +} + +AlbumHistory.propTypes = { + isFetching: PropTypes.bool.isRequired, + isPopulated: PropTypes.bool.isRequired, + error: PropTypes.object, + items: PropTypes.arrayOf(PropTypes.object).isRequired, + onMarkAsFailedPress: PropTypes.func.isRequired +}; + +AlbumHistory.defaultProps = { + selectedTab: 'details' +}; + +export default AlbumHistory; diff --git a/frontend/src/Album/History/AlbumHistoryConnector.js b/frontend/src/Album/History/AlbumHistoryConnector.js new file mode 100644 index 000000000..0218e72db --- /dev/null +++ b/frontend/src/Album/History/AlbumHistoryConnector.js @@ -0,0 +1,63 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import { fetchAlbumHistory, clearAlbumHistory, albumHistoryMarkAsFailed } from 'Store/Actions/albumHistoryActions'; +import AlbumHistory from './AlbumHistory'; + +function createMapStateToProps() { + return createSelector( + (state) => state.albumHistory, + (albumHistory) => { + return albumHistory; + } + ); +} + +const mapDispatchToProps = { + fetchAlbumHistory, + clearAlbumHistory, + albumHistoryMarkAsFailed +}; + +class AlbumHistoryConnector extends Component { + + // + // Lifecycle + + componentDidMount() { + this.props.fetchAlbumHistory({ albumId: this.props.albumId }); + } + + componentWillUnmount() { + this.props.clearAlbumHistory(); + } + + // + // Listeners + + onMarkAsFailedPress = (historyId) => { + this.props.albumHistoryMarkAsFailed({ historyId, albumId: this.props.albumId }); + } + + // + // Render + + render() { + return ( + + ); + } +} + +AlbumHistoryConnector.propTypes = { + albumId: PropTypes.number.isRequired, + fetchAlbumHistory: PropTypes.func.isRequired, + clearAlbumHistory: PropTypes.func.isRequired, + albumHistoryMarkAsFailed: PropTypes.func.isRequired +}; + +export default connect(createMapStateToProps, mapDispatchToProps)(AlbumHistoryConnector); diff --git a/frontend/src/Album/History/AlbumHistoryRow.css b/frontend/src/Album/History/AlbumHistoryRow.css new file mode 100644 index 000000000..8c3fb8272 --- /dev/null +++ b/frontend/src/Album/History/AlbumHistoryRow.css @@ -0,0 +1,6 @@ +.details, +.actions { + composes: cell from 'Components/Table/Cells/TableRowCell.css'; + + width: 65px; +} diff --git a/frontend/src/Album/History/AlbumHistoryRow.js b/frontend/src/Album/History/AlbumHistoryRow.js new file mode 100644 index 000000000..5b7c3c4ff --- /dev/null +++ b/frontend/src/Album/History/AlbumHistoryRow.js @@ -0,0 +1,139 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import titleCase from 'Utilities/String/titleCase'; +import { icons, kinds, tooltipPositions } from 'Helpers/Props'; +import Icon from 'Components/Icon'; +import IconButton from 'Components/Link/IconButton'; +import ConfirmModal from 'Components/Modal/ConfirmModal'; +import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector'; +import TableRow from 'Components/Table/TableRow'; +import TableRowCell from 'Components/Table/Cells/TableRowCell'; +import Popover from 'Components/Tooltip/Popover'; +import EpisodeQuality from 'Album/EpisodeQuality'; +import HistoryDetailsConnector from 'Activity/History/Details/HistoryDetailsConnector'; +import HistoryEventTypeCell from 'Activity/History/HistoryEventTypeCell'; +import styles from './AlbumHistoryRow.css'; + +class AlbumHistoryRow extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + isMarkAsFailedModalOpen: false + }; + } + + // + // Listeners + + onMarkAsFailedPress = () => { + this.setState({ isMarkAsFailedModalOpen: true }); + } + + onConfirmMarkAsFailed = () => { + this.props.onMarkAsFailedPress(this.props.id); + this.setState({ isMarkAsFailedModalOpen: false }); + } + + onMarkAsFailedModalClose = () => { + this.setState({ isMarkAsFailedModalOpen: false }); + } + + // + // Render + + render() { + const { + eventType, + sourceTitle, + quality, + qualityCutoffNotMet, + date, + data + } = this.props; + + const { + isMarkAsFailedModalOpen + } = this.state; + + return ( + + + + + {sourceTitle} + + + + + + + + + + + } + title={titleCase(eventType)} + body={ + + } + position={tooltipPositions.LEFT} + /> + + + + { + eventType === 'grabbed' && + + } + + + + + ); + } +} + +AlbumHistoryRow.propTypes = { + id: PropTypes.number.isRequired, + eventType: PropTypes.string.isRequired, + sourceTitle: PropTypes.string.isRequired, + quality: PropTypes.object.isRequired, + qualityCutoffNotMet: PropTypes.bool.isRequired, + date: PropTypes.string.isRequired, + data: PropTypes.object.isRequired, + onMarkAsFailedPress: PropTypes.func.isRequired +}; + +export default AlbumHistoryRow; diff --git a/frontend/src/Album/SceneInfo.css b/frontend/src/Album/SceneInfo.css new file mode 100644 index 000000000..af4908d4d --- /dev/null +++ b/frontend/src/Album/SceneInfo.css @@ -0,0 +1,11 @@ +.title { + composes: title from 'Components/DescriptionList/DescriptionListItemTitle.css'; + + width: 80px; +} + +.description { + composes: title from 'Components/DescriptionList/DescriptionListItemDescription.css'; + + margin-left: 100px; +} diff --git a/frontend/src/Album/SceneInfo.js b/frontend/src/Album/SceneInfo.js new file mode 100644 index 000000000..0c9ffa8cd --- /dev/null +++ b/frontend/src/Album/SceneInfo.js @@ -0,0 +1,83 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import DescriptionList from 'Components/DescriptionList/DescriptionList'; +import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem'; +import styles from './SceneInfo.css'; + +function SceneInfo(props) { + const { + sceneSeasonNumber, + sceneEpisodeNumber, + sceneAbsoluteEpisodeNumber, + alternateTitles, + artistType + } = props; + + return ( + + { + sceneSeasonNumber !== undefined && + + } + + { + sceneEpisodeNumber !== undefined && + + } + + { + artistType === 'anime' && sceneAbsoluteEpisodeNumber !== undefined && + + } + + { + !!alternateTitles.length && + + { + alternateTitles.map((alternateTitle) => { + return ( +
+ {alternateTitle.title} +
+ ); + }) + } +
+ } + /> + } + + ); +} + +SceneInfo.propTypes = { + sceneSeasonNumber: PropTypes.number, + sceneEpisodeNumber: PropTypes.number, + sceneAbsoluteEpisodeNumber: PropTypes.number, + alternateTitles: PropTypes.arrayOf(PropTypes.object).isRequired, + artistType: PropTypes.string +}; + +export default SceneInfo; diff --git a/frontend/src/Album/Search/EpisodeSearch.css b/frontend/src/Album/Search/EpisodeSearch.css new file mode 100644 index 000000000..2f7ddfd19 --- /dev/null +++ b/frontend/src/Album/Search/EpisodeSearch.css @@ -0,0 +1,16 @@ +.buttonContainer { + display: flex; + justify-content: center; + + margin-top: 10px; +} + +.button { + composes: button from 'Components/Link/Button.css'; + + width: 300px; +} + +.buttonIcon { + margin-right: 5px; +} diff --git a/frontend/src/Album/Search/EpisodeSearch.js b/frontend/src/Album/Search/EpisodeSearch.js new file mode 100644 index 000000000..f3ab8fdec --- /dev/null +++ b/frontend/src/Album/Search/EpisodeSearch.js @@ -0,0 +1,55 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import { icons, kinds, sizes } from 'Helpers/Props'; +import Button from 'Components/Link/Button'; +import Icon from 'Components/Icon'; +import styles from './EpisodeSearch.css'; + +function EpisodeSearch(props) { + const { + onQuickSearchPress, + onInteractiveSearchPress + } = props; + + return ( +
+
+ +
+ +
+ +
+
+ ); +} + +EpisodeSearch.propTypes = { + onQuickSearchPress: PropTypes.func.isRequired, + onInteractiveSearchPress: PropTypes.func.isRequired +}; + +export default EpisodeSearch; diff --git a/frontend/src/Album/Search/EpisodeSearchConnector.js b/frontend/src/Album/Search/EpisodeSearchConnector.js new file mode 100644 index 000000000..0759ec1f0 --- /dev/null +++ b/frontend/src/Album/Search/EpisodeSearchConnector.js @@ -0,0 +1,90 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import { executeCommand } from 'Store/Actions/commandActions'; +import * as commandNames from 'Commands/commandNames'; +import EpisodeSearch from './EpisodeSearch'; +import InteractiveEpisodeSearchConnector from './InteractiveEpisodeSearchConnector'; + +function createMapStateToProps() { + return createSelector( + (state) => state.releases, + (releases) => { + return { + isPopulated: releases.isPopulated + }; + } + ); +} + +const mapDispatchToProps = { + executeCommand +}; + +class EpisodeSearchConnector extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + isInteractiveSearchOpen: props.startInteractiveSearch + }; + } + + componentDidMount() { + if (this.props.isPopulated) { + this.setState({ isInteractiveSearchOpen: true }); + } + } + + // + // Listeners + + onQuickSearchPress = () => { + this.props.executeCommand({ + name: commandNames.ALBUM_SEARCH, + albumIds: [this.props.albumId] + }); + + this.props.onModalClose(); + } + + onInteractiveSearchPress = () => { + this.setState({ isInteractiveSearchOpen: true }); + } + + // + // Render + + render() { + if (this.state.isInteractiveSearchOpen) { + return ( + + ); + } + + return ( + + ); + } +} + +EpisodeSearchConnector.propTypes = { + albumId: PropTypes.number.isRequired, + isPopulated: PropTypes.bool.isRequired, + startInteractiveSearch: PropTypes.bool.isRequired, + onModalClose: PropTypes.func.isRequired, + executeCommand: PropTypes.func.isRequired +}; + +export default connect(createMapStateToProps, mapDispatchToProps)(EpisodeSearchConnector); diff --git a/frontend/src/Album/Search/InteractiveEpisodeSearch.js b/frontend/src/Album/Search/InteractiveEpisodeSearch.js new file mode 100644 index 000000000..eb8f8493f --- /dev/null +++ b/frontend/src/Album/Search/InteractiveEpisodeSearch.js @@ -0,0 +1,130 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import { icons, sortDirections } from 'Helpers/Props'; +import LoadingIndicator from 'Components/Loading/LoadingIndicator'; +import Icon from 'Components/Icon'; +import Table from 'Components/Table/Table'; +import TableBody from 'Components/Table/TableBody'; +import InteractiveEpisodeSearchRow from './InteractiveEpisodeSearchRow'; + +const columns = [ + { + name: 'protocol', + label: 'Source', + isSortable: true, + isVisible: true + }, + { + name: 'age', + label: 'Age', + isSortable: true, + isVisible: true + }, + { + name: 'title', + label: 'Title', + isSortable: true, + isVisible: true + }, + { + name: 'indexer', + label: 'Indexer', + isSortable: true, + isVisible: true + }, + { + name: 'size', + label: 'Size', + isSortable: true, + isVisible: true + }, + { + name: 'peers', + label: 'Peers', + isSortable: true, + isVisible: true + }, + { + name: 'qualityWeight', + label: 'Quality', + isSortable: true, + isVisible: true + }, + { + name: 'rejections', + label: React.createElement(Icon, { name: icons.DANGER }), + isSortable: true, + fixedSortDirection: sortDirections.ASCENDING, + isVisible: true + }, + { + name: 'releaseWeight', + label: React.createElement(Icon, { name: icons.DOWNLOAD }), + isSortable: true, + fixedSortDirection: sortDirections.ASCENDING, + isVisible: true + } +]; + +function InteractiveEpisodeSearch(props) { + const { + isFetching, + isPopulated, + error, + items, + sortKey, + sortDirection, + longDateFormat, + timeFormat, + onSortPress, + onGrabPress + } = props; + + if (isFetching) { + return ; + } else if (!isFetching && !!error) { + return
Unable to load results for this episode search. Try again later.
; + } else if (isPopulated && !items.length) { + return
No results found.
; + } + + return ( + + + { + items.map((item) => { + return ( + + ); + }) + } + +
+ ); +} + +InteractiveEpisodeSearch.propTypes = { + isFetching: PropTypes.bool.isRequired, + isPopulated: PropTypes.bool.isRequired, + error: PropTypes.object, + items: PropTypes.arrayOf(PropTypes.object).isRequired, + sortKey: PropTypes.string, + sortDirection: PropTypes.string, + longDateFormat: PropTypes.string.isRequired, + timeFormat: PropTypes.string.isRequired, + onSortPress: PropTypes.func.isRequired, + onGrabPress: PropTypes.func.isRequired +}; + +export default InteractiveEpisodeSearch; diff --git a/frontend/src/Album/Search/InteractiveEpisodeSearchConnector.js b/frontend/src/Album/Search/InteractiveEpisodeSearchConnector.js new file mode 100644 index 000000000..a52755a79 --- /dev/null +++ b/frontend/src/Album/Search/InteractiveEpisodeSearchConnector.js @@ -0,0 +1,90 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { createSelector } from 'reselect'; +import connectSection from 'Store/connectSection'; +import { fetchReleases, setReleasesSort, grabRelease } from 'Store/Actions/releaseActions'; +import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector'; +import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector'; +import InteractiveEpisodeSearch from './InteractiveEpisodeSearch'; + +function createMapStateToProps() { + return createSelector( + createClientSideCollectionSelector(), + createUISettingsSelector(), + (releases, uiSettings) => { + return { + longDateFormat: uiSettings.longDateFormat, + timeFormat: uiSettings.timeFormat, + ...releases + }; + } + ); +} + +const mapDispatchToProps = { + fetchReleases, + setReleasesSort, + grabRelease +}; + +class InteractiveEpisodeSearchConnector extends Component { + + // + // Lifecycle + + componentDidMount() { + const { + albumId, + isPopulated + } = this.props; + + // If search results are not yet isPopulated fetch them, + // otherwise re-show the existing props. + + if (!isPopulated) { + this.props.fetchReleases({ + albumId + }); + } + } + + // + // Listeners + + onSortPress = (sortKey, sortDirection) => { + this.props.setReleasesSort({ sortKey, sortDirection }); + } + + onGrabPress = (guid, indexerId) => { + this.props.grabRelease({ guid, indexerId }); + } + + // + // Render + + render() { + return ( + + ); + } +} + +InteractiveEpisodeSearchConnector.propTypes = { + albumId: PropTypes.number.isRequired, + isPopulated: PropTypes.bool.isRequired, + fetchReleases: PropTypes.func.isRequired, + setReleasesSort: PropTypes.func.isRequired, + grabRelease: PropTypes.func.isRequired +}; + +export default connectSection( + createMapStateToProps, + mapDispatchToProps, + undefined, + undefined, + { section: 'releases' } +)(InteractiveEpisodeSearchConnector); diff --git a/frontend/src/Album/Search/InteractiveEpisodeSearchRow.css b/frontend/src/Album/Search/InteractiveEpisodeSearchRow.css new file mode 100644 index 000000000..c77b73e7d --- /dev/null +++ b/frontend/src/Album/Search/InteractiveEpisodeSearchRow.css @@ -0,0 +1,25 @@ +.title { + composes: cell from 'Components/Table/Cells/TableRowCell.css'; + + word-break: break-all; +} + +.quality { + composes: cell from 'Components/Table/Cells/TableRowCell.css'; + + text-align: center; +} + +.rejected, +.download { + composes: cell from 'Components/Table/Cells/TableRowCell.css'; + + width: 50px; +} + +.age, +.size { + composes: cell from 'Components/Table/Cells/TableRowCell.css'; + + white-space: nowrap; +} diff --git a/frontend/src/Album/Search/InteractiveEpisodeSearchRow.js b/frontend/src/Album/Search/InteractiveEpisodeSearchRow.js new file mode 100644 index 000000000..a755b329e --- /dev/null +++ b/frontend/src/Album/Search/InteractiveEpisodeSearchRow.js @@ -0,0 +1,204 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import formatDateTime from 'Utilities/Date/formatDateTime'; +import formatAge from 'Utilities/Number/formatAge'; +import formatBytes from 'Utilities/Number/formatBytes'; +import { icons, kinds, tooltipPositions } from 'Helpers/Props'; +import Icon from 'Components/Icon'; +import SpinnerIconButton from 'Components/Link/SpinnerIconButton'; +import Link from 'Components/Link/Link'; +import TableRow from 'Components/Table/TableRow'; +import TableRowCell from 'Components/Table/Cells/TableRowCell'; +import Popover from 'Components/Tooltip/Popover'; +import EpisodeQuality from 'Album/EpisodeQuality'; +import ProtocolLabel from 'Activity/Queue/ProtocolLabel'; +import Peers from './Peers'; +import styles from './InteractiveEpisodeSearchRow.css'; + +function getDownloadIcon(isGrabbing, isGrabbed, grabError) { + if (isGrabbing) { + return icons.SPINNER; + } else if (isGrabbed) { + return icons.DOWNLOADING; + } else if (grabError) { + return icons.DOWNLOADING; + } + + return icons.DOWNLOAD; +} + +function getDownloadTooltip(isGrabbing, isGrabbed, grabError) { + if (isGrabbing) { + return ''; + } else if (isGrabbed) { + return 'Added to downloaded queue'; + } else if (grabError) { + return grabError; + } + + return 'Add to downloaded queue'; +} + +class InteractiveEpisodeSearchRow extends Component { + + // + // Listeners + + onGrabPress = () => { + const { + guid, + indexerId, + onGrabPress + }= this.props; + + onGrabPress(guid, indexerId); + } + + // + // Render + + render() { + const { + protocol, + age, + ageHours, + ageMinutes, + publishDate, + title, + infoUrl, + indexer, + size, + seeders, + leechers, + quality, + rejections, + downloadAllowed, + isGrabbing, + isGrabbed, + longDateFormat, + timeFormat, + grabError + } = this.props; + + return ( + + + + + + + {formatAge(age, ageHours, ageMinutes)} + + + + + {title} + + + + + {indexer} + + + + {formatBytes(size)} + + + + { + protocol === 'torrent' && + + } + + + + + + + + { + !!rejections.length && + + } + title="Release Rejected" + body={ +
    + { + rejections.map((rejection, index) => { + return ( +
  • + {rejection} +
  • + ); + }) + } +
+ } + position={tooltipPositions.LEFT} + /> + } +
+ + + { + downloadAllowed && + + } + +
+ ); + } +} + +InteractiveEpisodeSearchRow.propTypes = { + guid: PropTypes.string.isRequired, + protocol: PropTypes.string.isRequired, + age: PropTypes.number.isRequired, + ageHours: PropTypes.number.isRequired, + ageMinutes: PropTypes.number.isRequired, + publishDate: PropTypes.string.isRequired, + title: PropTypes.string.isRequired, + infoUrl: PropTypes.string.isRequired, + indexer: PropTypes.string.isRequired, + indexerId: PropTypes.number.isRequired, + size: PropTypes.number.isRequired, + seeders: PropTypes.number, + leechers: PropTypes.number, + quality: PropTypes.object.isRequired, + rejections: PropTypes.arrayOf(PropTypes.string).isRequired, + downloadAllowed: PropTypes.bool.isRequired, + isGrabbing: PropTypes.bool.isRequired, + isGrabbed: PropTypes.bool.isRequired, + grabError: PropTypes.string, + longDateFormat: PropTypes.string.isRequired, + timeFormat: PropTypes.string.isRequired, + onGrabPress: PropTypes.func.isRequired +}; + +InteractiveEpisodeSearchRow.defaultProps = { + isGrabbing: false, + isGrabbed: false +}; + +export default InteractiveEpisodeSearchRow; diff --git a/frontend/src/Album/Search/Peers.js b/frontend/src/Album/Search/Peers.js new file mode 100644 index 000000000..66f7cc9f5 --- /dev/null +++ b/frontend/src/Album/Search/Peers.js @@ -0,0 +1,57 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import { kinds } from 'Helpers/Props'; +import Label from 'Components/Label'; + +function getKind(seeders) { + if (seeders > 50) { + return kinds.PRIMARY; + } + + if (seeders > 10) { + return kinds.INFO; + } + + if (seeders > 0) { + return kinds.WARNING; + } + + return kinds.DANGER; +} + +function getPeersTooltipPart(peers, peersUnit) { + if (peers == null) { + return `unknown ${peersUnit}s`; + } + + if (peers === 1) { + return `1 ${peersUnit}`; + } + + return `${peers} ${peersUnit}s`; +} + +function Peers(props) { + const { + seeders, + leechers + } = props; + + const kind = getKind(seeders); + + return ( + + ); +} + +Peers.propTypes = { + seeders: PropTypes.number, + leechers: PropTypes.number +}; + +export default Peers; diff --git a/frontend/src/Album/SeasonEpisodeNumber.css b/frontend/src/Album/SeasonEpisodeNumber.css new file mode 100644 index 000000000..f86e1de6b --- /dev/null +++ b/frontend/src/Album/SeasonEpisodeNumber.css @@ -0,0 +1,3 @@ +.absoluteEpisodeNumber { + margin-left: 5px; +} diff --git a/frontend/src/Album/SeasonEpisodeNumber.js b/frontend/src/Album/SeasonEpisodeNumber.js new file mode 100644 index 000000000..5055e1a26 --- /dev/null +++ b/frontend/src/Album/SeasonEpisodeNumber.js @@ -0,0 +1,51 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import padNumber from 'Utilities/Number/padNumber'; +import styles from './SeasonEpisodeNumber.css'; + +function SeasonEpisodeNumber(props) { + const { + seasonNumber, + episodeNumber, + absoluteEpisodeNumber, + airDate, + artistType + } = props; + + if (artistType === 'daily' && airDate) { + return ( + {airDate} + ); + } + + if (artistType === 'anime') { + return ( + + {seasonNumber}x{padNumber(episodeNumber, 2)} + + { + absoluteEpisodeNumber && + + ({absoluteEpisodeNumber}) + + } + + ); + } + + return ( + + {seasonNumber}x{padNumber(episodeNumber, 2)} + + ); +} + +SeasonEpisodeNumber.propTypes = { + seasonNumber: PropTypes.number.isRequired, + episodeNumber: PropTypes.number.isRequired, + absoluteEpisodeNumber: PropTypes.number, + airDate: PropTypes.string, + artistType: PropTypes.string +}; + +export default SeasonEpisodeNumber; diff --git a/frontend/src/Album/Summary/EpisodeAiring.js b/frontend/src/Album/Summary/EpisodeAiring.js new file mode 100644 index 000000000..4d3e10137 --- /dev/null +++ b/frontend/src/Album/Summary/EpisodeAiring.js @@ -0,0 +1,84 @@ +import moment from 'moment'; +import PropTypes from 'prop-types'; +import React from 'react'; +import formatTime from 'Utilities/Date/formatTime'; +import isInNextWeek from 'Utilities/Date/isInNextWeek'; +import isToday from 'Utilities/Date/isToday'; +import isTomorrow from 'Utilities/Date/isTomorrow'; +import { kinds, sizes } from 'Helpers/Props'; +import Label from 'Components/Label'; + +function EpisodeAiring(props) { + const { + releaseDate, + albumLabel, + shortDateFormat, + showRelativeDates, + timeFormat + } = props; + + const networkLabel = ( + + ); + + if (!releaseDate) { + return ( + + TBA on {networkLabel} + + ); + } + + if (!showRelativeDates) { + return ( + + {moment(releaseDate).format(shortDateFormat)} on {networkLabel} + + ); + } + + if (isToday(releaseDate)) { + return ( + + Today on {networkLabel} + + ); + } + + if (isTomorrow(releaseDate)) { + return ( + + Tomorrow on {networkLabel} + + ); + } + + if (isInNextWeek(releaseDate)) { + return ( + + {moment(releaseDate).format('dddd')} on {networkLabel} + + ); + } + + return ( + + {moment(releaseDate).format(shortDateFormat)} on {networkLabel} + + ); +} + +EpisodeAiring.propTypes = { + releaseDate: PropTypes.string.isRequired, + albumLabel: PropTypes.arrayOf(PropTypes.string).isRequired, + shortDateFormat: PropTypes.string.isRequired, + showRelativeDates: PropTypes.bool.isRequired, + timeFormat: PropTypes.string.isRequired +}; + +export default EpisodeAiring; diff --git a/frontend/src/Album/Summary/EpisodeAiringConnector.js b/frontend/src/Album/Summary/EpisodeAiringConnector.js new file mode 100644 index 000000000..508467efb --- /dev/null +++ b/frontend/src/Album/Summary/EpisodeAiringConnector.js @@ -0,0 +1,20 @@ +import _ from 'lodash'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector'; +import EpisodeAiring from './EpisodeAiring'; + +function createMapStateToProps() { + return createSelector( + createUISettingsSelector(), + (uiSettings) => { + return _.pick(uiSettings, [ + 'shortDateFormat', + 'showRelativeDates', + 'timeFormat' + ]); + } + ); +} + +export default connect(createMapStateToProps)(EpisodeAiring); diff --git a/frontend/src/Album/Summary/EpisodeSummary.css b/frontend/src/Album/Summary/EpisodeSummary.css new file mode 100644 index 000000000..c05234997 --- /dev/null +++ b/frontend/src/Album/Summary/EpisodeSummary.css @@ -0,0 +1,48 @@ +.infoTitle { + display: inline-block; + width: 100px; + font-weight: bold; +} + +.overview, +.files { + margin-top: 20px; +} + +.filesHeader { + display: flex; + font-weight: bold; +} + +.filesHeader { + display: flex; + margin-bottom: 10px; + border-bottom: 1px solid $borderColor; +} + +.fileRow { + display: flex; +} + +.path { + @add-mixin truncate; + + flex: 1 0 1px; +} + +.size, +.quality { + flex: 0 0 125px; +} + +.actions { + flex: 0 0 20px; + text-align: center; +} + +@media only screen and (max-width: $breakpointMedium) { + .size, + .quality { + flex: 0 0 80px; + } +} diff --git a/frontend/src/Album/Summary/EpisodeSummary.js b/frontend/src/Album/Summary/EpisodeSummary.js new file mode 100644 index 000000000..4b82ec4af --- /dev/null +++ b/frontend/src/Album/Summary/EpisodeSummary.js @@ -0,0 +1,155 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import formatBytes from 'Utilities/Number/formatBytes'; +import { icons, kinds, sizes } from 'Helpers/Props'; +import IconButton from 'Components/Link/IconButton'; +import ConfirmModal from 'Components/Modal/ConfirmModal'; +import Label from 'Components/Label'; +import QualityProfileNameConnector from 'Settings/Profiles/Quality/QualityProfileNameConnector'; +import EpisodeQuality from 'Album/EpisodeQuality'; +import Table from 'Components/Table/Table'; +import TableBody from 'Components/Table/TableBody'; +import EpisodeAiringConnector from './EpisodeAiringConnector'; +import TrackDetailRow from './TrackDetailRow'; +import styles from './EpisodeSummary.css'; + +class EpisodeSummary extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + isRemoveTrackFileModalOpen: false + }; + } + + // + // Listeners + + onRemoveTrackFilePress = () => { + this.setState({ isRemoveTrackFileModalOpen: true }); + } + + onConfirmRemoveTrackFile = () => { + this.props.onDeleteTrackFile(); + this.setState({ isRemoveTrackFileModalOpen: false }); + } + + onRemoveTrackFileModalClose = () => { + this.setState({ isRemoveTrackFileModalOpen: false }); + } + + // + // Render + + render() { + const { + qualityProfileId, + overview, + releaseDate, + albumLabel, + path, + items, + size, + quality, + qualityCutoffNotMet, + columns + } = this.props; + + const hasOverview = !!overview; + + return ( +
+
+ Releases + + +
+ +
+ Quality Profile + + +
+ +
+ { + hasOverview ? + overview : + 'No album overview.' + } +
+ +
+ { +
+ { + items.length ? + + + { + items.map((item) => { + return ( + + ); + }) + } + +
: + +
+ No tracks in this group +
+ } +
+ } +
+ + +
+ ); + } +} + +EpisodeSummary.propTypes = { + qualityProfileId: PropTypes.number.isRequired, + overview: PropTypes.string, + albumLabel: PropTypes.arrayOf(PropTypes.string), + releaseDate: PropTypes.string.isRequired, + items: PropTypes.arrayOf(PropTypes.object).isRequired, + columns: PropTypes.arrayOf(PropTypes.object).isRequired, + path: PropTypes.string, + size: PropTypes.number, + quality: PropTypes.object, + qualityCutoffNotMet: PropTypes.bool, + onDeleteTrackFile: PropTypes.func.isRequired +}; + +export default EpisodeSummary; diff --git a/frontend/src/Album/Summary/EpisodeSummaryConnector.js b/frontend/src/Album/Summary/EpisodeSummaryConnector.js new file mode 100644 index 000000000..090c0dba6 --- /dev/null +++ b/frontend/src/Album/Summary/EpisodeSummaryConnector.js @@ -0,0 +1,43 @@ +import _ from 'lodash'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import { deleteTrackFile } from 'Store/Actions/trackFileActions'; +import createEpisodeSelector from 'Store/Selectors/createEpisodeSelector'; +import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector'; +import createArtistSelector from 'Store/Selectors/createArtistSelector'; +import createCommandsSelector from 'Store/Selectors/createCommandsSelector'; +import EpisodeSummary from './EpisodeSummary'; + +function createMapStateToProps() { + return createSelector( + (state) => state.tracks, + createEpisodeSelector(), + createCommandsSelector(), + createDimensionsSelector(), + (tracks, episode, commands, dimensions) => { + const items = _.filter(tracks.items, { albumId: episode.id }); + + return { + network: episode.label, + qualityProfileId: episode.profileId, + releaseDate: episode.releaseDate, + overview: episode.overview, + items, + columns: tracks.columns + }; + } + ); +} + +function createMapDispatchToProps(dispatch, props) { + return { + onDeleteTrackFile() { + dispatch(deleteTrackFile({ + id: props.trackFileId, + episodeEntity: props.episodeEntity + })); + } + }; +} + +export default connect(createMapStateToProps, createMapDispatchToProps)(EpisodeSummary); diff --git a/frontend/src/Album/Summary/TrackDetailRow.css b/frontend/src/Album/Summary/TrackDetailRow.css new file mode 100644 index 000000000..7dacb6a1e --- /dev/null +++ b/frontend/src/Album/Summary/TrackDetailRow.css @@ -0,0 +1,26 @@ +.title { + composes: cell from 'Components/Table/Cells/TableRowCell.css'; + + white-space: nowrap; +} + +.monitored { + composes: cell from 'Components/Table/Cells/TableRowCell.css'; + + width: 42px; +} + +.trackNumber { + composes: cell from 'Components/Table/Cells/TableRowCell.css'; + + width: 50px; +} + +.language, +.audio, +.video, +.status { + composes: cell from 'Components/Table/Cells/TableRowCell.css'; + + width: 100px; +} \ No newline at end of file diff --git a/frontend/src/Album/Summary/TrackDetailRow.js b/frontend/src/Album/Summary/TrackDetailRow.js new file mode 100644 index 000000000..26a2ba232 --- /dev/null +++ b/frontend/src/Album/Summary/TrackDetailRow.js @@ -0,0 +1,123 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import TableRow from 'Components/Table/TableRow'; +import TableRowCell from 'Components/Table/Cells/TableRowCell'; +import formatTimeSpan from 'Utilities/Date/formatTimeSpan'; +import MediaInfoConnector from 'TrackFile/MediaInfoConnector'; +import * as mediaInfoTypes from 'TrackFile/mediaInfoTypes'; +import EpisodeStatusConnector from 'Album/EpisodeStatusConnector'; + +import styles from './TrackDetailRow.css'; + +class TrackDetailRow extends Component { + + // + // Lifecycle + + // + // Listeners + + // + // Render + + render() { + const { + id, + title, + trackNumber, + duration, + columns, + trackFileId + } = this.props; + + return ( + + { + columns.map((column) => { + const { + name, + isVisible + } = column; + + if (!isVisible) { + return null; + } + + if (name === 'trackNumber') { + return ( + + {trackNumber} + + ); + } + + if (name === 'title') { + return ( + + {title} + + ); + } + + if (name === 'duration') { + return ( + + { + formatTimeSpan(duration) + } + + ); + } + + if (name === 'audioInfo') { + return ( + + + + ); + } + + if (name === 'status') { + return ( + + + + ); + } + + return null; + }) + } + + ); + } +} + +TrackDetailRow.propTypes = { + id: PropTypes.number.isRequired, + title: PropTypes.string.isRequired, + duration: PropTypes.number.isRequired, + trackFileId: PropTypes.number.isRequired, + columns: PropTypes.arrayOf(PropTypes.object).isRequired, + trackNumber: PropTypes.number.isRequired +}; + +export default TrackDetailRow; diff --git a/frontend/src/Album/episodeEntities.js b/frontend/src/Album/episodeEntities.js new file mode 100644 index 000000000..175b6fc54 --- /dev/null +++ b/frontend/src/Album/episodeEntities.js @@ -0,0 +1,13 @@ +export const CALENDAR = 'calendar'; +export const EPISODES = 'episodes'; +export const INTERACTIVE_IMPORT = 'interactiveImport.interactiveImportAlbums'; +export const WANTED_CUTOFF_UNMET = 'wanted.cutoffUnmet'; +export const WANTED_MISSING = 'wanted.missing'; + +export default { + CALENDAR, + EPISODES, + INTERACTIVE_IMPORT, + WANTED_CUTOFF_UNMET, + WANTED_MISSING +}; diff --git a/frontend/src/AlbumStudio/AlbumStudio.js b/frontend/src/AlbumStudio/AlbumStudio.js new file mode 100644 index 000000000..8b62a0d81 --- /dev/null +++ b/frontend/src/AlbumStudio/AlbumStudio.js @@ -0,0 +1,257 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import getSelectedIds from 'Utilities/Table/getSelectedIds'; +import selectAll from 'Utilities/Table/selectAll'; +import toggleSelected from 'Utilities/Table/toggleSelected'; +import { align, sortDirections } from 'Helpers/Props'; +import LoadingIndicator from 'Components/Loading/LoadingIndicator'; +import PageContent from 'Components/Page/PageContent'; +import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector'; +import PageToolbar from 'Components/Page/Toolbar/PageToolbar'; +import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection'; +import FilterMenu from 'Components/Menu/FilterMenu'; +import MenuContent from 'Components/Menu/MenuContent'; +import FilterMenuItem from 'Components/Menu/FilterMenuItem'; +import Table from 'Components/Table/Table'; +import TableBody from 'Components/Table/TableBody'; +import NoArtist from 'Artist/NoArtist'; +import AlbumStudioRowConnector from './AlbumStudioRowConnector'; +import AlbumStudioFooter from './AlbumStudioFooter'; + +const columns = [ + { + name: 'status', + isVisible: true + }, + { + name: 'sortName', + label: 'Name', + isSortable: true, + isVisible: true + }, + { + name: 'monitored', + isVisible: true + }, + { + name: 'albumCount', + label: 'Albums', + isSortable: true, + isVisible: true + } +]; + +class AlbumStudio extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + allSelected: false, + allUnselected: false, + lastToggled: null, + selectedState: {} + }; + } + + componentDidUpdate(prevProps) { + const { + isSaving, + saveError + } = this.props; + + if (prevProps.isSaving && !isSaving && !saveError) { + this.onSelectAllChange({ value: false }); + } + } + + // + // Control + + getSelectedIds = () => { + return getSelectedIds(this.state.selectedState); + } + + // + // Listeners + + onSelectAllChange = ({ value }) => { + this.setState(selectAll(this.state.selectedState, value)); + } + + onSelectedChange = ({ id, value, shiftKey = false }) => { + this.setState((state) => { + return toggleSelected(state, this.props.items, id, value, shiftKey); + }); + } + + onUpdateSelectedPress = (changes) => { + this.props.onUpdateSelectedPress({ + artistIds: this.getSelectedIds(), + ...changes + }); + } + + // + // Render + + render() { + const { + isFetching, + isPopulated, + error, + items, + filterKey, + filterValue, + sortKey, + sortDirection, + isSaving, + saveError, + onSortPress, + onFilterSelect + } = this.props; + + const { + allSelected, + allUnselected, + selectedState + } = this.state; + + return ( + + + + + + + + All + + + + Monitored Only + + + + Continuing Only + + + + Ended Only + + + + Missing Albums + + + + + + + + { + isFetching && !isPopulated && + + } + + { + !isFetching && !!error && +
Unable to load the calendar
+ } + + { + !error && isPopulated && !!items.length && +
+ + + { + items.map((item) => { + return ( + + ); + }) + } + +
+
+ } + + { + !error && isPopulated && !items.length && + + } +
+ + +
+ ); + } +} + +AlbumStudio.propTypes = { + isFetching: PropTypes.bool.isRequired, + isPopulated: PropTypes.bool.isRequired, + error: PropTypes.object, + items: PropTypes.arrayOf(PropTypes.object).isRequired, + sortKey: PropTypes.string, + sortDirection: PropTypes.oneOf(sortDirections.all), + filterKey: PropTypes.string, + filterValue: PropTypes.oneOfType([PropTypes.bool, PropTypes.number, PropTypes.string]), + isSaving: PropTypes.bool.isRequired, + saveError: PropTypes.object, + onSortPress: PropTypes.func.isRequired, + onFilterSelect: PropTypes.func.isRequired, + onUpdateSelectedPress: PropTypes.func.isRequired +}; + +export default AlbumStudio; diff --git a/frontend/src/AlbumStudio/AlbumStudioAlbum.css b/frontend/src/AlbumStudio/AlbumStudioAlbum.css new file mode 100644 index 000000000..603d98ecd --- /dev/null +++ b/frontend/src/AlbumStudio/AlbumStudioAlbum.css @@ -0,0 +1,24 @@ +.season { + display: flex; + align-items: stretch; + overflow: hidden; + margin: 2px 4px; + border: 1px solid $borderColor; + border-radius: 4px; + background-color: #eee; + cursor: default; +} + +.info { + padding: 0 4px; +} + +.episodes { + padding: 0 4px; + background-color: $white; + color: $defaultColor; +} + +.allEpisodes { + background-color: #e0ffe0; +} diff --git a/frontend/src/AlbumStudio/AlbumStudioAlbum.js b/frontend/src/AlbumStudio/AlbumStudioAlbum.js new file mode 100644 index 000000000..d1c9e393d --- /dev/null +++ b/frontend/src/AlbumStudio/AlbumStudioAlbum.js @@ -0,0 +1,90 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import classNames from 'classnames'; +import padNumber from 'Utilities/Number/padNumber'; +import MonitorToggleButton from 'Components/MonitorToggleButton'; +import styles from './AlbumStudioAlbum.css'; + +class AlbumStudioAlbum extends Component { + + // + // Listeners + + onAlbumMonitoredPress = () => { + const { + id, + monitored + } = this.props; + + this.props.onAlbumMonitoredPress(id, !monitored); + } + + // + // Render + + render() { + const { + id, + title, + monitored, + statistics, + isSaving + } = this.props; + + const { + trackFileCount, + totalTrackCount, + percentOfTracks + } = statistics; + + return ( +
+
+ + + + { + `${title}` + } + +
+ +
+ { + totalTrackCount === 0 ? '0/0' : `${trackFileCount}/${totalTrackCount}` + } +
+
+ ); + } +} + +AlbumStudioAlbum.propTypes = { + id: PropTypes.number.isRequired, + title: PropTypes.string.isRequired, + monitored: PropTypes.bool.isRequired, + statistics: PropTypes.object.isRequired, + isSaving: PropTypes.bool.isRequired, + onAlbumMonitoredPress: PropTypes.func.isRequired +}; + +AlbumStudioAlbum.defaultProps = { + isSaving: false, + statistics: { + trackFileCount: 0, + totalTrackCount: 0, + percentOfTracks: 0 + } +}; + +export default AlbumStudioAlbum; diff --git a/frontend/src/AlbumStudio/AlbumStudioConnector.js b/frontend/src/AlbumStudio/AlbumStudioConnector.js new file mode 100644 index 000000000..aba720cb9 --- /dev/null +++ b/frontend/src/AlbumStudio/AlbumStudioConnector.js @@ -0,0 +1,97 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { createSelector } from 'reselect'; +import connectSection from 'Store/connectSection'; +import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector'; +import { setAlbumStudioSort, setAlbumStudioFilter, saveAlbumStudio } from 'Store/Actions/albumStudioActions'; +import { fetchEpisodes, clearEpisodes } from 'Store/Actions/episodeActions'; +import AlbumStudio from './AlbumStudio'; + +function createMapStateToProps() { + return createSelector( + createClientSideCollectionSelector(), + (artist) => { + return { + ...artist + }; + } + ); +} + +const mapDispatchToProps = { + fetchEpisodes, + clearEpisodes, + setAlbumStudioSort, + setAlbumStudioFilter, + saveAlbumStudio +}; + +class AlbumStudioConnector extends Component { + + // + // Lifecycle + + componentDidMount() { + this.populate(); + } + + componentWillUnmount() { + this.unpopulate(); + } + + // + // Control + + populate = () => { + this.props.fetchEpisodes(); + } + + unpopulate = () => { + this.props.clearEpisodes(); + } + + // + // Listeners + + onSortPress = (sortKey) => { + this.props.setAlbumStudioSort({ sortKey }); + } + + onFilterSelect = (filterKey, filterValue, filterType) => { + this.props.setAlbumStudioFilter({ filterKey, filterValue, filterType }); + } + + onUpdateSelectedPress = (payload) => { + this.props.saveAlbumStudio(payload); + } + + // + // Render + + render() { + return ( + + ); + } +} + +AlbumStudioConnector.propTypes = { + setAlbumStudioSort: PropTypes.func.isRequired, + setAlbumStudioFilter: PropTypes.func.isRequired, + fetchEpisodes: PropTypes.func.isRequired, + clearEpisodes: PropTypes.func.isRequired, + saveAlbumStudio: PropTypes.func.isRequired +}; + +export default connectSection( + createMapStateToProps, + mapDispatchToProps, + undefined, + undefined, + { section: 'artist', uiSection: 'albumStudio' } +)(AlbumStudioConnector); diff --git a/frontend/src/AlbumStudio/AlbumStudioFooter.css b/frontend/src/AlbumStudio/AlbumStudioFooter.css new file mode 100644 index 000000000..c18eb660f --- /dev/null +++ b/frontend/src/AlbumStudio/AlbumStudioFooter.css @@ -0,0 +1,14 @@ +.inputContainer { + margin-right: 20px; +} + +.label { + margin-bottom: 3px; + font-weight: bold; +} + +.updateSelectedButton { + composes: button from 'Components/Link/SpinnerButton.css'; + + height: 35px; +} diff --git a/frontend/src/AlbumStudio/AlbumStudioFooter.js b/frontend/src/AlbumStudio/AlbumStudioFooter.js new file mode 100644 index 000000000..d5eb300cd --- /dev/null +++ b/frontend/src/AlbumStudio/AlbumStudioFooter.js @@ -0,0 +1,145 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { kinds } from 'Helpers/Props'; +import SpinnerButton from 'Components/Link/SpinnerButton'; +import MonitorAlbumsSelectInput from 'Components/Form/MonitorAlbumsSelectInput'; +import SelectInput from 'Components/Form/SelectInput'; +import PageContentFooter from 'Components/Page/PageContentFooter'; +import styles from './AlbumStudioFooter.css'; + +const NO_CHANGE = 'noChange'; + +class AlbumStudioFooter extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + monitored: NO_CHANGE, + monitor: NO_CHANGE + }; + } + + componentDidUpdate(prevProps) { + const { + isSaving, + saveError + } = prevProps; + + if (prevProps.isSaving && !isSaving && !saveError) { + this.setState({ + monitored: NO_CHANGE, + monitor: NO_CHANGE + }); + } + } + + // + // Listeners + + onInputChange = ({ name, value }) => { + this.setState({ [name]: value }); + } + + onUpdateSelectedPress = () => { + const { + monitor, + monitored + } = this.state; + + const changes = {}; + + if (monitored !== NO_CHANGE) { + changes.monitored = monitored === 'monitored'; + } + + if (monitor !== NO_CHANGE) { + changes.monitor = monitor; + } + + this.props.onUpdateSelectedPress(changes); + } + + // + // Render + + render() { + const { + selectedCount, + isSaving + } = this.props; + + const { + monitored, + monitor + } = this.state; + + const monitoredOptions = [ + { key: NO_CHANGE, value: 'No Change', disabled: true }, + { key: 'monitored', value: 'Monitored' }, + { key: 'unmonitored', value: 'Unmonitored' } + ]; + + const noChanges = monitored === NO_CHANGE && monitor === NO_CHANGE; + + return ( + +
+
+ Monitor Artist +
+ + +
+ +
+
+ Monitor Albums +
+ + +
+ +
+
+ {selectedCount} Artist(s) Selected +
+ + + Update Selected + +
+
+ ); + } +} + +AlbumStudioFooter.propTypes = { + selectedCount: PropTypes.number.isRequired, + isSaving: PropTypes.bool.isRequired, + saveError: PropTypes.object, + onUpdateSelectedPress: PropTypes.func.isRequired +}; + +export default AlbumStudioFooter; diff --git a/frontend/src/AlbumStudio/AlbumStudioRow.css b/frontend/src/AlbumStudio/AlbumStudioRow.css new file mode 100644 index 000000000..a053c6bef --- /dev/null +++ b/frontend/src/AlbumStudio/AlbumStudioRow.css @@ -0,0 +1,20 @@ +.status, +.monitored { + composes: cell from 'Components/Table/Cells/TableRowCell.css'; + + width: 50px; +} + +.title { + composes: cell from 'Components/Table/Cells/TableRowCell.css'; + + width: 1px; + white-space: nowrap; +} + +.seasons { + composes: cell from 'Components/Table/Cells/TableRowCell.css'; + + display: flex; + flex-wrap: wrap; +} diff --git a/frontend/src/AlbumStudio/AlbumStudioRow.js b/frontend/src/AlbumStudio/AlbumStudioRow.js new file mode 100644 index 000000000..d63dc83ee --- /dev/null +++ b/frontend/src/AlbumStudio/AlbumStudioRow.js @@ -0,0 +1,101 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { icons } from 'Helpers/Props'; +import Icon from 'Components/Icon'; +import MonitorToggleButton from 'Components/MonitorToggleButton'; +import TableRow from 'Components/Table/TableRow'; +import TableRowCell from 'Components/Table/Cells/TableRowCell'; +import TableSelectCell from 'Components/Table/Cells/TableSelectCell'; +import ArtistNameLink from 'Artist/ArtistNameLink'; +import AlbumStudioAlbum from './AlbumStudioAlbum'; +import styles from './AlbumStudioRow.css'; + +class AlbumStudioRow extends Component { + + // + // Render + + render() { + const { + artistId, + status, + nameSlug, + artistName, + monitored, + albums, + isSaving, + isSelected, + onSelectedChange, + onArtistMonitoredPress, + onAlbumMonitoredPress + } = this.props; + + return ( + + + + + + + + + + + + + + + + + { + albums.map((season) => { + return ( + + ); + }) + } + + + ); + } +} + +AlbumStudioRow.propTypes = { + artistId: PropTypes.number.isRequired, + status: PropTypes.string.isRequired, + nameSlug: PropTypes.string.isRequired, + artistName: PropTypes.string.isRequired, + monitored: PropTypes.bool.isRequired, + albums: PropTypes.arrayOf(PropTypes.object).isRequired, + isSaving: PropTypes.bool.isRequired, + isSelected: PropTypes.bool, + onSelectedChange: PropTypes.func.isRequired, + onArtistMonitoredPress: PropTypes.func.isRequired, + onAlbumMonitoredPress: PropTypes.func.isRequired +}; + +AlbumStudioRow.defaultProps = { + isSaving: false +}; + +export default AlbumStudioRow; diff --git a/frontend/src/AlbumStudio/AlbumStudioRowConnector.js b/frontend/src/AlbumStudio/AlbumStudioRowConnector.js new file mode 100644 index 000000000..fa54b9064 --- /dev/null +++ b/frontend/src/AlbumStudio/AlbumStudioRowConnector.js @@ -0,0 +1,85 @@ +import _ from 'lodash'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import createArtistSelector from 'Store/Selectors/createArtistSelector'; +import { toggleArtistMonitored, toggleSeasonMonitored } from 'Store/Actions/artistActions'; +import { toggleEpisodeMonitored } from 'Store/Actions/episodeActions'; +import AlbumStudioRow from './AlbumStudioRow'; + +function createMapStateToProps() { + return createSelector( + (state) => state.episodes, + createArtistSelector(), + (episodes, artist) => { + const albumsInArtist = _.filter(episodes.items, { artistId: artist.id }); + const sortedAlbums = _.orderBy(albumsInArtist, 'releaseDate', 'desc'); + + return { + ...artist, + artistId: artist.id, + artistName: artist.artistName, + nameSlug: artist.nameSlug, + monitored: artist.monitored, + status: artist.status, + isSaving: artist.isSaving, + albums: sortedAlbums + }; + } + ); +} + +const mapDispatchToProps = { + toggleArtistMonitored, + toggleSeasonMonitored, + toggleEpisodeMonitored +}; + +class AlbumStudioRowConnector extends Component { + + // + // Listeners + + onArtistMonitoredPress = () => { + const { + artistId, + monitored + } = this.props; + + this.props.toggleArtistMonitored({ + artistId, + monitored: !monitored + }); + } + + onAlbumMonitoredPress = (albumId, monitored) => { + this.props.toggleEpisodeMonitored({ + albumId, + monitored + }); + } + + // + // Render + + render() { + return ( + + ); + } +} + +AlbumStudioRowConnector.propTypes = { + artistId: PropTypes.number.isRequired, + monitored: PropTypes.bool.isRequired, + toggleArtistMonitored: PropTypes.func.isRequired, + toggleSeasonMonitored: PropTypes.func.isRequired, + toggleEpisodeMonitored: PropTypes.func.isRequired +}; + +export default connect(createMapStateToProps, mapDispatchToProps)(AlbumStudioRowConnector); diff --git a/frontend/src/App/App.js b/frontend/src/App/App.js new file mode 100644 index 000000000..5a8d6a95c --- /dev/null +++ b/frontend/src/App/App.js @@ -0,0 +1,252 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import DocumentTitle from 'react-document-title'; +import { Provider } from 'react-redux'; +import { Route, Redirect } from 'react-router-dom'; +import { ConnectedRouter } from 'react-router-redux'; +import getPathWithUrlBase from 'Utilities/getPathWithUrlBase'; +import NotFound from 'Components/NotFound'; +import Switch from 'Components/Router/Switch'; +import PageConnector from 'Components/Page/PageConnector'; +import ArtistIndexConnector from 'Artist/Index/ArtistIndexConnector'; +import AddNewArtistConnector from 'AddArtist/AddNewArtist/AddNewArtistConnector'; +import ImportArtist from 'AddArtist/ImportArtist/ImportArtist'; +import ArtistEditorConnector from 'Artist/Editor/ArtistEditorConnector'; +import AlbumStudioConnector from 'AlbumStudio/AlbumStudioConnector'; +import ArtistDetailsPageConnector from 'Artist/Details/ArtistDetailsPageConnector'; +import CalendarPageConnector from 'Calendar/CalendarPageConnector'; +import HistoryConnector from 'Activity/History/HistoryConnector'; +import QueueConnector from 'Activity/Queue/QueueConnector'; +import BlacklistConnector from 'Activity/Blacklist/BlacklistConnector'; +import MissingConnector from 'Wanted/Missing/MissingConnector'; +import CutoffUnmetConnector from 'Wanted/CutoffUnmet/CutoffUnmetConnector'; +import Settings from 'Settings/Settings'; +import MediaManagementConnector from 'Settings/MediaManagement/MediaManagementConnector'; +import Profiles from 'Settings/Profiles/Profiles'; +import Quality from 'Settings/Quality/Quality'; +import IndexerSettings from 'Settings/Indexers/IndexerSettings'; +import DownloadClientSettings from 'Settings/DownloadClients/DownloadClientSettings'; +import NotificationSettings from 'Settings/Notifications/NotificationSettings'; +import MetadataSettings from 'Settings/Metadata/MetadataSettings'; +import GeneralSettingsConnector from 'Settings/General/GeneralSettingsConnector'; +import UISettingsConnector from 'Settings/UI/UISettingsConnector'; +import Status from 'System/Status/Status'; +import TasksConnector from 'System/Tasks/TasksConnector'; +import BackupsConnector from 'System/Backup/BackupsConnector'; +import UpdatesConnector from 'System/Updates/UpdatesConnector'; +import LogsTableConnector from 'System/Events/LogsTableConnector'; +import Logs from 'System/Logs/Logs'; + +function App({ store, history }) { + return ( + + + + + + {/* + Artist + */} + + + + { + window.Sonarr.urlBase && + { + return ( + + ); + }} + /> + } + + + + + + + + + + + + {/* + Calendar + */} + + + + {/* + Activity + */} + + + + + + + + {/* + Wanted + */} + + + + + + {/* + Settings + */} + + + + + + + + + + + + + + + + + + + + + + {/* + System + */} + + + + + + + + + + + + + + {/* + Not Found + */} + + + + + + + + + ); +} + +App.propTypes = { + store: PropTypes.object.isRequired, + history: PropTypes.object.isRequired +}; + +export default App; diff --git a/frontend/src/App/AppUpdatedModal.js b/frontend/src/App/AppUpdatedModal.js new file mode 100644 index 000000000..285b87ec8 --- /dev/null +++ b/frontend/src/App/AppUpdatedModal.js @@ -0,0 +1,29 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import Modal from 'Components/Modal/Modal'; +import AppUpdatedModalContentConnector from './AppUpdatedModalContentConnector'; + +function AppUpdatedModal(props) { + const { + isOpen, + onModalClose + } = props; + + return ( + + + + ); +} + +AppUpdatedModal.propTypes = { + isOpen: PropTypes.bool.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default AppUpdatedModal; diff --git a/frontend/src/App/AppUpdatedModalConnector.js b/frontend/src/App/AppUpdatedModalConnector.js new file mode 100644 index 000000000..a21afbc5a --- /dev/null +++ b/frontend/src/App/AppUpdatedModalConnector.js @@ -0,0 +1,12 @@ +import { connect } from 'react-redux'; +import AppUpdatedModal from './AppUpdatedModal'; + +function createMapDispatchToProps(dispatch, props) { + return { + onModalClose() { + location.reload(); + } + }; +} + +export default connect(null, createMapDispatchToProps)(AppUpdatedModal); diff --git a/frontend/src/App/AppUpdatedModalContent.css b/frontend/src/App/AppUpdatedModalContent.css new file mode 100644 index 000000000..37b89c9be --- /dev/null +++ b/frontend/src/App/AppUpdatedModalContent.css @@ -0,0 +1,15 @@ +.version { + margin: 0 3px; + font-weight: bold; +} + +.maintenance { + margin-top: 20px; +} + +.changes { + margin-top: 20px; + padding-bottom: 5px; + border-bottom: 1px solid #e5e5e5; + font-size: 18px; +} diff --git a/frontend/src/App/AppUpdatedModalContent.js b/frontend/src/App/AppUpdatedModalContent.js new file mode 100644 index 000000000..9597d538f --- /dev/null +++ b/frontend/src/App/AppUpdatedModalContent.js @@ -0,0 +1,98 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import { kinds } from 'Helpers/Props'; +import LoadingIndicator from 'Components/Loading/LoadingIndicator'; +import Button from 'Components/Link/Button'; +import ModalContent from 'Components/Modal/ModalContent'; +import ModalHeader from 'Components/Modal/ModalHeader'; +import ModalBody from 'Components/Modal/ModalBody'; +import ModalFooter from 'Components/Modal/ModalFooter'; +import UpdateChanges from 'System/Updates/UpdateChanges'; +import styles from './AppUpdatedModalContent.css'; + +function AppUpdatedModalContent(props) { + const { + version, + isPopulated, + error, + items, + onSeeChangesPress, + onModalClose + } = props; + + const update = items[0]; + + return ( + + + Lidarr Updated + + + +
+ Version {version} of Lidarr has been installed, in order to get the latest changes you'll need to reload Lidarr. +
+ + { + isPopulated && !error && !!update && +
+ { + !update.changes && +
Maintenance release
+ } + + { + !!update.changes && +
+
+ What's new? +
+ + + + +
+ } +
+ } + + { + !isPopulated && !error && + + } +
+ + + + + + +
+ ); +} + +AppUpdatedModalContent.propTypes = { + version: PropTypes.string.isRequired, + isPopulated: PropTypes.bool.isRequired, + error: PropTypes.object, + items: PropTypes.arrayOf(PropTypes.object).isRequired, + onSeeChangesPress: PropTypes.func.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default AppUpdatedModalContent; diff --git a/frontend/src/App/AppUpdatedModalContentConnector.js b/frontend/src/App/AppUpdatedModalContentConnector.js new file mode 100644 index 000000000..b252868ce --- /dev/null +++ b/frontend/src/App/AppUpdatedModalContentConnector.js @@ -0,0 +1,76 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import { fetchUpdates } from 'Store/Actions/systemActions'; +import AppUpdatedModalContent from './AppUpdatedModalContent'; + +function createMapStateToProps() { + return createSelector( + (state) => state.app.version, + (state) => state.system.updates, + (version, updates) => { + const { + isPopulated, + error, + items + } = updates; + + return { + version, + isPopulated, + error, + items + }; + } + ); +} + +function createMapDispatchToProps(dispatch, props) { + return { + dispatchFetchUpdates() { + dispatch(fetchUpdates()); + }, + + onSeeChangesPress() { + window.location = `${window.Sonarr.urlBase}/system/updates`; + } + }; +} + +class AppUpdatedModalContentConnector extends Component { + + // + // Lifecycle + + componentDidMount() { + this.props.dispatchFetchUpdates(); + } + + componentDidUpdate(prevProps) { + if (prevProps.version !== this.props.version) { + this.props.dispatchFetchUpdates(); + } + } + + // + // Render + + render() { + const { + dispatchFetchUpdates, + ...otherProps + } = this.props; + + return ( + + ); + } +} + +AppUpdatedModalContentConnector.propTypes = { + version: PropTypes.string.isRequired, + dispatchFetchUpdates: PropTypes.func.isRequired +}; + +export default connect(createMapStateToProps, createMapDispatchToProps)(AppUpdatedModalContentConnector); diff --git a/frontend/src/App/ConnectionLostModal.css b/frontend/src/App/ConnectionLostModal.css new file mode 100644 index 000000000..f0a9d220f --- /dev/null +++ b/frontend/src/App/ConnectionLostModal.css @@ -0,0 +1,3 @@ +.automatic { + margin-top: 20px; +} diff --git a/frontend/src/App/ConnectionLostModal.js b/frontend/src/App/ConnectionLostModal.js new file mode 100644 index 000000000..9178d2ab8 --- /dev/null +++ b/frontend/src/App/ConnectionLostModal.js @@ -0,0 +1,55 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import { kinds } from 'Helpers/Props'; +import Button from 'Components/Link/Button'; +import Modal from 'Components/Modal/Modal'; +import ModalContent from 'Components/Modal/ModalContent'; +import ModalHeader from 'Components/Modal/ModalHeader'; +import ModalBody from 'Components/Modal/ModalBody'; +import ModalFooter from 'Components/Modal/ModalFooter'; +import styles from './ConnectionLostModal.css'; + +function ConnectionLostModal(props) { + const { + isOpen, + onModalClose + } = props; + + return ( + + + + Connnection Lost + + + +
+ Lidarr has lost it's connection to the backend and will need to be reloaded to restore functionality. +
+ +
+ Lidarr will try to connect automatically, or you can click reload below. +
+
+ + + +
+
+ ); +} + +ConnectionLostModal.propTypes = { + isOpen: PropTypes.bool.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default ConnectionLostModal; diff --git a/frontend/src/App/ConnectionLostModalConnector.js b/frontend/src/App/ConnectionLostModalConnector.js new file mode 100644 index 000000000..8ab8e3cd0 --- /dev/null +++ b/frontend/src/App/ConnectionLostModalConnector.js @@ -0,0 +1,12 @@ +import { connect } from 'react-redux'; +import ConnectionLostModal from './ConnectionLostModal'; + +function createMapDispatchToProps(dispatch, props) { + return { + onModalClose() { + location.reload(); + } + }; +} + +export default connect(undefined, createMapDispatchToProps)(ConnectionLostModal); diff --git a/frontend/src/Artist/ArtistBanner.js b/frontend/src/Artist/ArtistBanner.js new file mode 100644 index 000000000..6bc5d7fe6 --- /dev/null +++ b/frontend/src/Artist/ArtistBanner.js @@ -0,0 +1,168 @@ +import _ from 'lodash'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import LazyLoad from 'react-lazyload'; + +const bannerPlaceholder = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAA+gAAAC5AgMAAADG9/24AAAADFBMVEUyMjI7Ozs1NTU4ODjgOsZvAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH4QkRBgAc5PUQ8QAAB7tJREFUeNrtnb1rHEsMwMdjArtrUrpf3uMg2cOl+5QpXWRvjQkXlyHVK8MVYXFl3F+/GAzrNfdSuXkQnH9ie/Oqw32aFM6bkTQfd85rHrwi0qjJJql+pxlJo9FISv0XqX8mKkmSJEmSJEmSJEmSJEmSJEmS5NeVYrh7HDqJ5DeY23kQB64/u7zW91amzgXqvRqjfGYvarnfxqncuaQlf72Zxv4gSOluudOfjRy1Hzh1L+nPj+KU3jh0MWr3Sp87dDFq98An/msmg3zPW/aFR6/vRaBPglML6Mci0H0g92MYfrjvRgJ5TrDvhuFyGIZv9NdTOev93dDry1Z59mM56/2hU8WZcefFjZgVT/Z90SlEV9VKio3HeKYZLLSGII6WPP+oBv3ZwkL3iE4JG/ZRjUYb16mAripUO/c4Hl3bd/juCF19FuHeJn6nK90VBh0s3SjBvcFW/wTrvfBa118EbHY8qmMOtmgdupoKOLTvAmOH1k059LKAX+Qra/TncExH4hcBXV3Zf/+Dv5Vb43cfod/wt3PLsN5VF6HDiudt56IbB23Ql5/oZ8A7CfZWbgF61sap62XdlOZTZ2rF3c7ltNW1+f7LuDez/uc6uDfO8dwkbPXcKF8vPS9sds527pC2utH0uCb0Jmz2t8wNvN3q2jj4ntDJna94m3g4sb7HH8Gue0RH4Je8z61g4GGr79Wz1qHjbi94m/jcH1IOccsj+lt/sOFr4m0EPz+3XyNueURvSmfn+Ebx1rctMvOxU8foqOwVa+9mfdvHDH+DdYQOxAesvZslvc/wo4vQZy6eY+vdrCVrut/AyTW9CWvO3E1ra4L6i5Fxoka7MDan45vTOmx2MPGc0QH52Tb6kTPxXNGtW5/Tnl+oGP2N/dstY8eeO2M+bqM3zvVxRT+gS8Vdl5/z6CaCzfx/c41o3pP2+030UzrAHDNGv8d4FvMVAf1I4c07V/QlnNsyG9TN23ID/ZjObnO+6CZmydS+y8oG9Dfk2Gd80WcW3Rv44e5bZOLtD8EUHbRq0V0F/DAMn8fQ2UUv2UayEMxplaFvM0G7QR/ufqBcmH/gG85Z9BM8rMO5bdiUDu4ceaLvUjqWfJveQm8hpvnKFv1jOLxUTtkoBWf0nIK5Cd6wO207dAznTlmjH+K630KvuKPbOHYffJsOexykx0iWJ7pNRf+ttEXvW1U49F7ZbJ26RHSe6ehn5NRGMPBVQAfP12EQf8QZPTMxXac30M1RxtaWXLBFn3j0eRsHNIBuCycLtqfWCQZrBcZ0W+gVhXts0RtEXyit4zDOoE/N/5yNzNF3oVB0A93I6ise7bijr1XwbYiurySg7xjfpp+g375mjj5D9HYD3fr6YvkKcxU80Q8duvVt2+gjob/ljX7yFH1a80dvDfpia8GXqp3Wr1XJXevljvFt2QZ6a6tJ2Gvd2Pa8Xuu2iNB7o++r+lUJlQas93oOl04be73Ut7y1Ts4tn/0EfaxZOzcKaXIsqNiI4QtE5x7N7Z08RZ9CKpb38cWs9V26b4sDWURnr3W9foq+gpM8a3S4V+rhKUCcqrBXTufMUxU2QWUTkFtZGls3pjgnqJ4FdB1lZAd8/qMEZGRtAlJvJqONb2syzuj2CuK+1YU5reg2CzGNyqq6fpOpku8VRI7lchX+7SIy8NdQSaKn3K8bsWJOlZGBX2Ed0bUIdBXftJpzG1h2vjetWFqgHXrhczTWtx2h8llXVTytI4FHnaes0bGMqNhC1+jUXmFMx7iCaq6qy4HqxR7dFfMUtc34LQCVDPppcM13Kie5RmTGJYNUKBraDL57xEKalS0UzTgXiiqn1X3fRxR0bBf6TLEvD4aKkslTdO5F4fQUIHseo5ugfrShewbWjvsriHxT65WAByAHVCT7u0fvoKDC+rZClZyf/eSAngUTj1pfCXjshU/8smDiPTr7J374sDPDfI1Hd4cX1g874TmvRc+30U8V9+e88Ig7U6V26Ofk21rzg1ScH3Hj033bTXIZab2iGG6PdWOaQzLx7cShQ0FFfdxqlfFu2DAhxw4vf5zWV3hYZ96m47l7u0+DMAgdu1Dxbs4St+Rx6MbAwzIveLfk8Y2Y9J5HP1tij3DmjZii9lujQy9GXO/M22/5pmvUVdWiV3RkYd50zbfaI7Vb9GmDD0C4t9qLGiy+JPTVKT4A4d5gMY866N4i+p9KuUM767aavpkqLnmDDl10S8W/mSq20O3I3n8x6AXufP4tdKlxcpkZ4HN43FZ0mT3GCmicTO2ysW2uVXmFWp/yb5cdN0kHrb/ADwFN0uPW+ICOt+0SWuPjkW2tPTr+CjcSphjiGIzWodPQGwljMGj4Se/Q0bfJGH4Sj7zx6DJG3tCgo4HQoYiukDHoKB5vZdCtb9MrIUM7DyK169Zu+mEUMtQsjLJrtT7vWl2IGWXnBxjaFwFnraQBhmFs5dDqi66SNLYyGlY6XMgaVip4RG00mHh2LWwwcRhHPVsJG0cdhpDProQNIQ+j52e30kbPa19B5T64H9Wfqn0mTelB7Y04pWMFfCQf5JDTjYOTuSClu5QUSa9EyU0gf5BF7iaP2zxdq6TJjUydgxTD3aPvm5wkSZIkSZIkSZIkSZIkSZIk+Z9kv/534U2+Uyf0XwP9H83PZBlAqkdjAAAAAElFTkSuQmCC'; + +function findBanner(images) { + return _.find(images, { coverType: 'banner' }); +} + +function getBannerUrl(banner, size) { + if (banner) { + if (banner.url.contains('lastWrite=') || (/^https?:/).test(banner.url)) { + // Remove protocol + let url = banner.url.replace(/^https?:/, ''); + url = url.replace('banner.jpg', `banner-${size}.jpg`); + + return url; + } + } +} + +class ArtistBanner extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + const pixelRatio = Math.floor(window.devicePixelRatio); + + const { + images, + size + } = props; + + const banner = findBanner(images); + + this.state = { + pixelRatio, + banner, + bannerUrl: getBannerUrl(banner, pixelRatio * size), + isLoaded: false, + hasError: false + }; + } + + componentDidUpdate(prevProps) { + const { + images, + size + } = this.props; + + const { + banner, + pixelRatio + } = this.state; + + const nextBanner = findBanner(images); + + if (nextBanner && (!banner || nextBanner.url !== banner.url)) { + this.setState({ + banner: nextBanner, + bannerUrl: getBannerUrl(nextBanner, pixelRatio * size), + hasError: false + // Don't reset isLoaded, as we want to immediately try to + // show the new image, whether an image was shown previously + // or the placeholder was shown. + }); + } + } + + // + // Listeners + + onError = () => { + this.setState({ hasError: true }); + } + + onLoad = () => { + this.setState({ + isLoaded: true, + hasError: false + }); + } + + // + // Render + + render() { + const { + className, + style, + size, + lazy, + overflow + } = this.props; + + const { + bannerUrl, + hasError, + isLoaded + } = this.state; + + if (hasError || !bannerUrl) { + return ( + + ); + } + + if (lazy) { + return ( + + } + > + + + ); + } + + return ( + + ); + } +} + +ArtistBanner.propTypes = { + className: PropTypes.string, + style: PropTypes.object, + images: PropTypes.arrayOf(PropTypes.object).isRequired, + size: PropTypes.number.isRequired, + lazy: PropTypes.bool.isRequired, + overflow: PropTypes.bool.isRequired +}; + +ArtistBanner.defaultProps = { + size: 70, + lazy: true, + overflow: false +}; + +export default ArtistBanner; diff --git a/frontend/src/Artist/ArtistLogo.js b/frontend/src/Artist/ArtistLogo.js new file mode 100644 index 000000000..05e665186 --- /dev/null +++ b/frontend/src/Artist/ArtistLogo.js @@ -0,0 +1,160 @@ +import _ from 'lodash'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import LazyLoad from 'react-lazyload'; + +const logoPlaceholder = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAPcAAAD3AgMAAAC84irAAAAADFBMVEUyMjI7Ozs1NTU4ODjgOsZvAAAAAWJLR0QAiAUdSAAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB+EJEBIzDdm9OfoAAAbkSURBVGje7Zq9b9s4FMBZFgUkBR27C3cw0MromL1jxwyVZASB67G4qWPgoSAyBdm9CwECKCp8nbIccGj/Ce/BTUb3Lh3aI997pCjnTnyyt0JcIif5+ZHvPZLvQ0KMYxzjGMc4xjGOcYxjHOP4JUfSfP7RVPvSH3MYX/eC5aecxne1v+w95WebFs/rwVO/8+h8PnT6t3ln/DFQuJ06/SyHiX9pxa7o5/lewkuLDxLvhM8tPki8g07dU8Gnj5zGlw7P79n4pDVYi8/YuHO4n03z0z6XXDom4G3TXDdN840+LobN/W1Ty2slHD8bNvevlUgutLmTj4NmT3pf6mMGcJGth+gefaZsDCjB2Wj65wN8ZmnAGnE6eFieI1FvcEISLjIUr9hm+w7PFeHiE9t0E7dyIatE48odXTPu0j/A3BMnXf7NXDxudTxbE2VxMWVu+sfwf3i1ZMLiaQLf+iWIP4VtjtTzFhc35vfveZrb4nPt4R95ulu1cxeVh8Psw7rzbgWp8dWHyr83WJpbgjypjS5XeZnqRxmJNUd3MS1d6ue/tOn0WuayNd2CoTlaeqwnIVeOgcWHdHdMS9cSN1vCy3bxZwzFm6VL7QA14WTudVj1sFvf4ReZNSCO0IvwngXFV3hkFcriuPokrPrYbYxjVAHiZ24zLYIeP7/E4xZUgHiZWt29D9ptGemHR7mPo9B10HLGbucRfs/Ww2f2CD4L2u0+wofKwwvrd0XoqCmr38CAZa1d58LesEpvgqtN4MCR1mVj2nZWOiweVB/CAXuyi59Y1auA2eekg6Xw8Tfm013A8LFV8mYXL61ZF4Hb8Zx8d9vBtbdG7s99XvOOZlF38QVtmlkAv0ffxTOjxU/o5p8FvKbSszw2ik87+Iz23Lwf134RiWf2tG3xN2T4oh8vDO4U33z+5qnefFnR77OA2wheh2WfbJBHeI/XgtNJEaHdtJNrvPn8E8eV/kW/2xn8FDc77LemOyq4J1XvSbds7SZ3cAV+86UXP283TGaFUk4ZwmNyugne8FaqxdHtFkH8GNewg2cc3PjsM7CbbNdMwQJ47aL3mP5H308ar5XOn2nUwpx+4hrx/z+qn5DBNqD4rMUpWACnPwnhkfa9SnZwvX1MnHLVi08cPle+0wBuAsykd8dO0KkS9L0dPCO37MVLxJc6nPHdTeNT/ZeLDQN/DEFpBzc33Bfckhx8K1q7IS5vuPgjbTf5AL97zcALxFUHN76QrF7heTHru54RN3bbxTeEn4Xx04f4NOfhSuPLncmnQk3z1yLlSE8fabtFHVyZyIQlXes8zrdSJR5ea7k3+asUooXg2mO4oDprT/XdHpROhouL/8A3edBw5DYxBhYdn08Q53jd0elDfApHbHjL6Hk/pvvNd1rEWdLl9iG+hpMgiMMdVEM64B8X5nq6ZBwX5rCSeK/4uInJROiwetLi0jtpG0yJBPOkTVQXryEPKqMQbq6JeyUTvUOkilq/EVGmo5NIpP3XRIzhXIafrjzF30JUIqecKxIjOpF6il9jbHTLxjs3rN5voPH+GxbDA1m7GrM9a4zdTigdCUUXD2MSSEAXQRxDo2QHl2iwV+h7gchqLrLrhmKxH/Z6nqLUQD5AYSHWAEwk+Z1Ck1vEAmEhBaVtufDtj8Zmv6U+PQNBqbDf/szVR5XNvQteSAzRyeQhzgnIKR2Invq43gQb4+oRaJCTTcRd6RkzGXlJQe3vDq8gsDB2S0QaSoViwKNW9Sh9zUzEMA2MWtU7nJUGYhIa4bnjcLthgkkopMAGj3dxXgoMCbg+laTFL8luSn9pFkrAMf031cmVJz0jXzsKFm6OSfVqYnEILPKZDjeicPFhQoaHbMhKX+NmZ5Q+ntr8n5obhGPVKlx48cs+FteKP3MlswWv6CSPHK4Dmntm0ckreW0snmxKbsnLFdyo4mrwjLYJo+Dmyn0k3uDTEpMRTrnPKza+IHy9wGSEU2yMvSrvHeJ/Qt2UV+p0hVacvsah0psKXqEVy7y2tPu3xhM1oMxLReY00tAlJG9JFZktzCwyU4lbuqQ7U22VN1zi9gvsIP05PjAL7H55H/C6rREzyvu41bbS4VXb1OV0FLG1YVsa1J1gtzaosVJbHO3Gb6z4bR2H89s61FRqCIcgL+E3lfyWlsaN3eR6QDP0pSdeKqOEZjOgoda285SUl5W+Jga181wz0WQFF2poM7FtZTZKXlXZ0Fam10htroY3Ug9s43pN5OJ2jyZy28Iu1nu0sNsGenGzRwO9bd8Xd/u0793LA8Vmn5cHnPhiH+Gt+HIv4Ye+tnHoSyMHvrJy6Aszh76uc+DLQuLQV5XGMY5xjGMc4xjHOMYxjnH80uNfW99BeoyzJCoAAAAASUVORK5CYII='; + +function findLogo(images) { + return _.find(images, { coverType: 'logo' }); +} + +function getLogoUrl(logo, size) { + if (logo) { + // Remove protocol + let url = logo.url.replace(/^https?:/, ''); + url = url.replace('logo.jpg', `logo-${size}.jpg`); + + return url; + } +} + +class ArtistLogo extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + const pixelRatio = Math.floor(window.devicePixelRatio); + + const { + images, + size + } = props; + + const logo = findLogo(images); + + this.state = { + pixelRatio, + logo, + logoUrl: getLogoUrl(logo, pixelRatio * size), + hasError: false, + isLoaded: false + }; + } + + componentDidUpdate(prevProps) { + const { + images, + size + } = this.props; + + const { + pixelRatio + } = this.state; + + const logo = findLogo(images); + + if (logo && logo.url !== this.state.logo.url) { + this.setState({ + logo, + logoUrl: getLogoUrl(logo, pixelRatio * size), + hasError: false, + isLoaded: false + }); + } + } + + // + // Listeners + + onError = () => { + this.setState({ hasError: true }); + } + + onLoad = () => { + this.setState({ isLoaded: true }); + } + + // + // Render + + render() { + const { + className, + style, + size, + lazy, + overflow + } = this.props; + + const { + logoUrl, + hasError, + isLoaded + } = this.state; + + if (hasError || !logoUrl) { + return ( + + ); + } + + if (lazy) { + return ( + + } + > + + + ); + } + + return ( + + ); + } +} + +ArtistLogo.propTypes = { + className: PropTypes.string, + style: PropTypes.object, + images: PropTypes.arrayOf(PropTypes.object).isRequired, + size: PropTypes.number.isRequired, + lazy: PropTypes.bool.isRequired, + overflow: PropTypes.bool.isRequired +}; + +ArtistLogo.defaultProps = { + size: 250, + lazy: true, + overflow: false +}; + +export default ArtistLogo; diff --git a/frontend/src/Artist/ArtistNameLink.js b/frontend/src/Artist/ArtistNameLink.js new file mode 100644 index 000000000..aafc97912 --- /dev/null +++ b/frontend/src/Artist/ArtistNameLink.js @@ -0,0 +1,20 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import Link from 'Components/Link/Link'; + +function ArtistNameLink({ nameSlug, artistName }) { + const link = `/artist/${nameSlug}`; + + return ( + + {artistName} + + ); +} + +ArtistNameLink.propTypes = { + nameSlug: PropTypes.string.isRequired, + artistName: PropTypes.string.isRequired +}; + +export default ArtistNameLink; diff --git a/frontend/src/Artist/ArtistPoster.js b/frontend/src/Artist/ArtistPoster.js new file mode 100644 index 000000000..25b744c80 --- /dev/null +++ b/frontend/src/Artist/ArtistPoster.js @@ -0,0 +1,168 @@ +import _ from 'lodash'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import LazyLoad from 'react-lazyload'; + +const posterPlaceholder = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAPcAAAD3AgMAAAC84irAAAAADFBMVEUyMjI7Ozs1NTU4ODjgOsZvAAAAAWJLR0QAiAUdSAAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB+EJEBIzDdm9OfoAAAbkSURBVGje7Zq9b9s4FMBZFgUkBR27C3cw0MromL1jxwyVZASB67G4qWPgoSAyBdm9CwECKCp8nbIccGj/Ce/BTUb3Lh3aI997pCjnTnyyt0JcIif5+ZHvPZLvQ0KMYxzjGMc4xjGOcYxjHOP4JUfSfP7RVPvSH3MYX/eC5aecxne1v+w95WebFs/rwVO/8+h8PnT6t3ln/DFQuJ06/SyHiX9pxa7o5/lewkuLDxLvhM8tPki8g07dU8Gnj5zGlw7P79n4pDVYi8/YuHO4n03z0z6XXDom4G3TXDdN840+LobN/W1Ty2slHD8bNvevlUgutLmTj4NmT3pf6mMGcJGth+gefaZsDCjB2Wj65wN8ZmnAGnE6eFieI1FvcEISLjIUr9hm+w7PFeHiE9t0E7dyIatE48odXTPu0j/A3BMnXf7NXDxudTxbE2VxMWVu+sfwf3i1ZMLiaQLf+iWIP4VtjtTzFhc35vfveZrb4nPt4R95ulu1cxeVh8Psw7rzbgWp8dWHyr83WJpbgjypjS5XeZnqRxmJNUd3MS1d6ue/tOn0WuayNd2CoTlaeqwnIVeOgcWHdHdMS9cSN1vCy3bxZwzFm6VL7QA14WTudVj1sFvf4ReZNSCO0IvwngXFV3hkFcriuPokrPrYbYxjVAHiZ24zLYIeP7/E4xZUgHiZWt29D9ptGemHR7mPo9B10HLGbucRfs/Ww2f2CD4L2u0+wofKwwvrd0XoqCmr38CAZa1d58LesEpvgqtN4MCR1mVj2nZWOiweVB/CAXuyi59Y1auA2eekg6Xw8Tfm013A8LFV8mYXL61ZF4Hb8Zx8d9vBtbdG7s99XvOOZlF38QVtmlkAv0ffxTOjxU/o5p8FvKbSszw2ik87+Iz23Lwf134RiWf2tG3xN2T4oh8vDO4U33z+5qnefFnR77OA2wheh2WfbJBHeI/XgtNJEaHdtJNrvPn8E8eV/kW/2xn8FDc77LemOyq4J1XvSbds7SZ3cAV+86UXP283TGaFUk4ZwmNyugne8FaqxdHtFkH8GNewg2cc3PjsM7CbbNdMwQJ47aL3mP5H308ar5XOn2nUwpx+4hrx/z+qn5DBNqD4rMUpWACnPwnhkfa9SnZwvX1MnHLVi08cPle+0wBuAsykd8dO0KkS9L0dPCO37MVLxJc6nPHdTeNT/ZeLDQN/DEFpBzc33Bfckhx8K1q7IS5vuPgjbTf5AL97zcALxFUHN76QrF7heTHru54RN3bbxTeEn4Xx04f4NOfhSuPLncmnQk3z1yLlSE8fabtFHVyZyIQlXes8zrdSJR5ea7k3+asUooXg2mO4oDprT/XdHpROhouL/8A3edBw5DYxBhYdn08Q53jd0elDfApHbHjL6Hk/pvvNd1rEWdLl9iG+hpMgiMMdVEM64B8X5nq6ZBwX5rCSeK/4uInJROiwetLi0jtpG0yJBPOkTVQXryEPKqMQbq6JeyUTvUOkilq/EVGmo5NIpP3XRIzhXIafrjzF30JUIqecKxIjOpF6il9jbHTLxjs3rN5voPH+GxbDA1m7GrM9a4zdTigdCUUXD2MSSEAXQRxDo2QHl2iwV+h7gchqLrLrhmKxH/Z6nqLUQD5AYSHWAEwk+Z1Ck1vEAmEhBaVtufDtj8Zmv6U+PQNBqbDf/szVR5XNvQteSAzRyeQhzgnIKR2Invq43gQb4+oRaJCTTcRd6RkzGXlJQe3vDq8gsDB2S0QaSoViwKNW9Sh9zUzEMA2MWtU7nJUGYhIa4bnjcLthgkkopMAGj3dxXgoMCbg+laTFL8luSn9pFkrAMf031cmVJz0jXzsKFm6OSfVqYnEILPKZDjeicPFhQoaHbMhKX+NmZ5Q+ntr8n5obhGPVKlx48cs+FteKP3MlswWv6CSPHK4Dmntm0ckreW0snmxKbsnLFdyo4mrwjLYJo+Dmyn0k3uDTEpMRTrnPKza+IHy9wGSEU2yMvSrvHeJ/Qt2UV+p0hVacvsah0psKXqEVy7y2tPu3xhM1oMxLReY00tAlJG9JFZktzCwyU4lbuqQ7U22VN1zi9gvsIP05PjAL7H55H/C6rREzyvu41bbS4VXb1OV0FLG1YVsa1J1gtzaosVJbHO3Gb6z4bR2H89s61FRqCIcgL+E3lfyWlsaN3eR6QDP0pSdeKqOEZjOgoda285SUl5W+Jga181wz0WQFF2poM7FtZTZKXlXZ0Fam10htroY3Ug9s43pN5OJ2jyZy28Iu1nu0sNsGenGzRwO9bd8Xd/u0793LA8Vmn5cHnPhiH+Gt+HIv4Ye+tnHoSyMHvrJy6Aszh76uc+DLQuLQV5XGMY5xjGMc4xjHOMYxjnH80uNfW99BeoyzJCoAAAAASUVORK5CYII='; + +function findPoster(images) { + return _.find(images, { coverType: 'poster' }); +} + +function getPosterUrl(poster, size) { + if (poster) { + if (poster.url.contains('lastWrite=') || (/^https?:/).test(poster.url)) { + // Remove protocol + let url = poster.url.replace(/^https?:/, ''); + url = url.replace('poster.jpg', `poster-${size}.jpg`); + + return url; + } + } +} + +class ArtistPoster extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + const pixelRatio = Math.floor(window.devicePixelRatio); + + const { + images, + size + } = props; + + const poster = findPoster(images); + + this.state = { + pixelRatio, + poster, + posterUrl: getPosterUrl(poster, pixelRatio * size), + isLoaded: false, + hasError: false + }; + } + + componentDidUpdate(prevProps) { + const { + images, + size + } = this.props; + + const { + poster, + pixelRatio + } = this.state; + + const nextPoster = findPoster(images); + + if (nextPoster && (!poster || nextPoster.url !== poster.url)) { + this.setState({ + poster: nextPoster, + posterUrl: getPosterUrl(nextPoster, pixelRatio * size), + hasError: false + // Don't reset isLoaded, as we want to immediately try to + // show the new image, whether an image was shown previously + // or the placeholder was shown. + }); + } + } + + // + // Listeners + + onError = () => { + this.setState({ hasError: true }); + } + + onLoad = () => { + this.setState({ + isLoaded: true, + hasError: false + }); + } + + // + // Render + + render() { + const { + className, + style, + size, + lazy, + overflow + } = this.props; + + const { + posterUrl, + hasError, + isLoaded + } = this.state; + + if (hasError || !posterUrl) { + return ( + + ); + } + + if (lazy) { + return ( + + } + > + + + ); + } + + return ( + + ); + } +} + +ArtistPoster.propTypes = { + className: PropTypes.string, + style: PropTypes.object, + images: PropTypes.arrayOf(PropTypes.object).isRequired, + size: PropTypes.number.isRequired, + lazy: PropTypes.bool.isRequired, + overflow: PropTypes.bool.isRequired +}; + +ArtistPoster.defaultProps = { + size: 250, + lazy: true, + overflow: false +}; + +export default ArtistPoster; diff --git a/frontend/src/Artist/Delete/DeleteArtist.less b/frontend/src/Artist/Delete/DeleteArtist.less new file mode 100644 index 000000000..4cf99e1b2 --- /dev/null +++ b/frontend/src/Artist/Delete/DeleteArtist.less @@ -0,0 +1,39 @@ +@import "Content/icons"; + +.delete-artist-modal { + + i { + margin-right : 5px; + //.fa-icon-color(white); + + } + + .path { + white-space : nowrap; + font-size : 16px; + padding-bottom : 20px; + } + + .delete-files-info, + .delete-label { + color : @brand-danger-dark; + } + + .delete-files-info { + display : none; + } + + .checkbox { + display : inline-block; + } + + .c-checkbox:hover .check { + border-color : @brand-danger-dark; + } + + input[type=checkbox]:checked + span { + background-color : @brand-danger-dark; + border-color : @brand-danger-dark; + } + +} \ No newline at end of file diff --git a/frontend/src/Artist/Delete/DeleteArtistModal.js b/frontend/src/Artist/Delete/DeleteArtistModal.js new file mode 100644 index 000000000..5b6490c66 --- /dev/null +++ b/frontend/src/Artist/Delete/DeleteArtistModal.js @@ -0,0 +1,33 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import { sizes } from 'Helpers/Props'; +import Modal from 'Components/Modal/Modal'; +import DeleteArtistModalContentConnector from './DeleteArtistModalContentConnector'; + +function DeleteArtistModal(props) { + const { + isOpen, + onModalClose, + ...otherProps + } = props; + + return ( + + + + ); +} + +DeleteArtistModal.propTypes = { + isOpen: PropTypes.bool.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default DeleteArtistModal; diff --git a/frontend/src/Artist/Delete/DeleteArtistModalContent.css b/frontend/src/Artist/Delete/DeleteArtistModalContent.css new file mode 100644 index 000000000..dbfef0871 --- /dev/null +++ b/frontend/src/Artist/Delete/DeleteArtistModalContent.css @@ -0,0 +1,12 @@ +.pathContainer { + margin-bottom: 20px; +} + +.pathIcon { + margin-right: 8px; +} + +.deleteFilesMessage { + margin-top: 20px; + color: $dangerColor; +} diff --git a/frontend/src/Artist/Delete/DeleteArtistModalContent.js b/frontend/src/Artist/Delete/DeleteArtistModalContent.js new file mode 100644 index 000000000..444b75bc8 --- /dev/null +++ b/frontend/src/Artist/Delete/DeleteArtistModalContent.js @@ -0,0 +1,139 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import formatBytes from 'Utilities/Number/formatBytes'; +import { icons, inputTypes, kinds } from 'Helpers/Props'; +import Button from 'Components/Link/Button'; +import Icon from 'Components/Icon'; +import FormGroup from 'Components/Form/FormGroup'; +import FormLabel from 'Components/Form/FormLabel'; +import FormInputGroup from 'Components/Form/FormInputGroup'; +import ModalContent from 'Components/Modal/ModalContent'; +import ModalHeader from 'Components/Modal/ModalHeader'; +import ModalBody from 'Components/Modal/ModalBody'; +import ModalFooter from 'Components/Modal/ModalFooter'; +import styles from './DeleteArtistModalContent.css'; + +class DeleteArtistModalContent extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + deleteFiles: false + }; + } + + // + // Listeners + + onDeleteFilesChange = ({ value }) => { + this.setState({ deleteFiles: value }); + } + + onDeleteArtistConfirmed = () => { + const deleteFiles = this.state.deleteFiles; + + this.setState({ deleteFiles: false }); + this.props.onDeletePress(deleteFiles); + } + + // + // Render + + render() { + const { + artistName, + path, + trackFileCount, + sizeOnDisk, + onModalClose + } = this.props; + + const deleteFiles = this.state.deleteFiles; + let deleteFilesLabel = `Delete ${trackFileCount} Track Files`; + let deleteFilesHelpText = 'Delete the track files and artist folder'; + + if (trackFileCount === 0) { + deleteFilesLabel = 'Delete Artist Folder'; + deleteFilesHelpText = 'Delete the artist folder and it\'s contents'; + } + + return ( + + + Delete - {artistName} + + + +
+ + + {path} +
+ + + {deleteFilesLabel} + + + + + { + deleteFiles && +
+
The artist folder {path} and all it's content will be deleted.
+ + { + !!trackFileCount && +
{trackFileCount} track files totaling {formatBytes(sizeOnDisk)}
+ } +
+ } + +
+ + + + + + +
+ ); + } +} + +DeleteArtistModalContent.propTypes = { + artistName: PropTypes.string.isRequired, + path: PropTypes.string.isRequired, + trackFileCount: PropTypes.number.isRequired, + sizeOnDisk: PropTypes.number.isRequired, + onDeletePress: PropTypes.func.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +DeleteArtistModalContent.defaultProps = { + trackFileCount: 0 +}; + +export default DeleteArtistModalContent; diff --git a/frontend/src/Artist/Delete/DeleteArtistModalContentConnector.js b/frontend/src/Artist/Delete/DeleteArtistModalContentConnector.js new file mode 100644 index 000000000..938ac5a96 --- /dev/null +++ b/frontend/src/Artist/Delete/DeleteArtistModalContentConnector.js @@ -0,0 +1,55 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import createArtistSelector from 'Store/Selectors/createArtistSelector'; +import { deleteArtist } from 'Store/Actions/artistActions'; +import DeleteArtistModalContent from './DeleteArtistModalContent'; + +function createMapStateToProps() { + return createSelector( + createArtistSelector(), + (artist) => { + return artist; + } + ); +} + +const mapDispatchToProps = { + deleteArtist +}; + +class DeleteArtistModalContentConnector extends Component { + + // + // Listeners + + onDeletePress = (deleteFiles) => { + this.props.deleteArtist({ + id: this.props.artistId, + deleteFiles + }); + + this.props.onModalClose(true); + } + + // + // Render + + render() { + return ( + + ); + } +} + +DeleteArtistModalContentConnector.propTypes = { + artistId: PropTypes.number.isRequired, + onModalClose: PropTypes.func.isRequired, + deleteArtist: PropTypes.func.isRequired +}; + +export default connect(createMapStateToProps, mapDispatchToProps)(DeleteArtistModalContentConnector); diff --git a/frontend/src/Artist/Details/AlbumRow.css b/frontend/src/Artist/Details/AlbumRow.css new file mode 100644 index 000000000..255ee76c9 --- /dev/null +++ b/frontend/src/Artist/Details/AlbumRow.css @@ -0,0 +1,17 @@ +.title { + composes: cell from 'Components/Table/Cells/TableRowCell.css'; + + white-space: nowrap; +} + +.monitored { + composes: cell from 'Components/Table/Cells/TableRowCell.css'; + + width: 42px; +} + +.status { + composes: cell from 'Components/Table/Cells/TableRowCell.css'; + + width: 100px; +} diff --git a/frontend/src/Artist/Details/AlbumRow.js b/frontend/src/Artist/Details/AlbumRow.js new file mode 100644 index 000000000..0ae589625 --- /dev/null +++ b/frontend/src/Artist/Details/AlbumRow.js @@ -0,0 +1,226 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import MonitorToggleButton from 'Components/MonitorToggleButton'; +import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector'; +import { kinds, sizes } from 'Helpers/Props'; +import TableRow from 'Components/Table/TableRow'; +import Label from 'Components/Label'; +import TableRowCell from 'Components/Table/Cells/TableRowCell'; +import formatTimeSpan from 'Utilities/Date/formatTimeSpan'; +import EpisodeSearchCellConnector from 'Album/EpisodeSearchCellConnector'; +import EpisodeTitleLink from 'Album/EpisodeTitleLink'; + +import styles from './AlbumRow.css'; + +function getEpisodeCountKind(monitored, trackFileCount, episodeCount) { + if (trackFileCount === episodeCount && episodeCount > 0) { + return kinds.SUCCESS; + } + + if (!monitored) { + return kinds.WARNING; + } + + return kinds.DANGER; +} + +class AlbumRow extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + isDetailsModalOpen: false + }; + } + + // + // Listeners + + onManualSearchPress = () => { + this.setState({ isDetailsModalOpen: true }); + } + + onDetailsModalClose = () => { + this.setState({ isDetailsModalOpen: false }); + } + + onMonitorAlbumPress = (monitored, options) => { + this.props.onMonitorAlbumPress(this.props.id, monitored, options); + } + + // + // Render + + render() { + const { + id, + artistId, + monitored, + statistics, + duration, + releaseDate, + title, + isSaving, + artistMonitored, + path, + columns + } = this.props; + + const { + trackCount, + trackFileCount, + totalTrackCount + } = statistics; + + return ( + + { + columns.map((column) => { + const { + name, + isVisible + } = column; + + if (!isVisible) { + return null; + } + + if (name === 'monitored') { + return ( + + + + ); + } + + if (name === 'title') { + return ( + + + + ); + } + + if (name === 'path') { + return ( + + { + path + } + + ); + } + + if (name === 'trackCount') { + return ( + + { + statistics.totalTrackCount + } + + ); + } + + if (name === 'duration') { + return ( + + { + formatTimeSpan(duration) + } + + ); + } + + if (name === 'releaseDate') { + return ( + + ); + } + + if (name === 'status') { + return ( + + + + ); + } + + if (name === 'actions') { + return ( + + ); + } + + return null; + }) + } + + ); + } +} + +AlbumRow.propTypes = { + id: PropTypes.number.isRequired, + artistId: PropTypes.number.isRequired, + monitored: PropTypes.bool.isRequired, + releaseDate: PropTypes.string.isRequired, + duration: PropTypes.number.isRequired, + title: PropTypes.string.isRequired, + isSaving: PropTypes.bool, + unverifiedSceneNumbering: PropTypes.bool, + artistMonitored: PropTypes.bool.isRequired, + statistics: PropTypes.object.isRequired, + path: PropTypes.string, + mediaInfo: PropTypes.object, + alternateTitles: PropTypes.arrayOf(PropTypes.object).isRequired, + columns: PropTypes.arrayOf(PropTypes.object).isRequired, + onMonitorAlbumPress: PropTypes.func.isRequired +}; + +AlbumRow.defaultProps = { + statistics: { + trackCount: 0, + trackFileCount: 0 + } +}; + +export default AlbumRow; diff --git a/frontend/src/Artist/Details/AlbumRowConnector.js b/frontend/src/Artist/Details/AlbumRowConnector.js new file mode 100644 index 000000000..cff75e50f --- /dev/null +++ b/frontend/src/Artist/Details/AlbumRowConnector.js @@ -0,0 +1,28 @@ +/* eslint max-params: 0 */ +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import createArtistSelector from 'Store/Selectors/createArtistSelector'; +import createTrackFileSelector from 'Store/Selectors/createTrackFileSelector'; +import createCommandsSelector from 'Store/Selectors/createCommandsSelector'; +import AlbumRow from './AlbumRow'; + +function createMapStateToProps() { + return createSelector( + (state, { id }) => id, + (state, { sceneSeasonNumber }) => sceneSeasonNumber, + createArtistSelector(), + createTrackFileSelector(), + createCommandsSelector(), + (id, sceneSeasonNumber, artist, trackFile, commands) => { + const alternateTitles = sceneSeasonNumber ? _.filter(artist.alternateTitles, { sceneSeasonNumber }) : []; + + return { + artistMonitored: artist.monitored, + trackFilePath: trackFile ? trackFile.path : null, + trackFileRelativePath: trackFile ? trackFile.relativePath : null, + alternateTitles + }; + } + ); +} +export default connect(createMapStateToProps)(AlbumRow); diff --git a/frontend/src/Artist/Details/ArtistAlternateTitles.css b/frontend/src/Artist/Details/ArtistAlternateTitles.css new file mode 100644 index 000000000..1af1ae68b --- /dev/null +++ b/frontend/src/Artist/Details/ArtistAlternateTitles.css @@ -0,0 +1,3 @@ +.alternateTitle { + white-space: nowrap; +} diff --git a/frontend/src/Artist/Details/ArtistAlternateTitles.js b/frontend/src/Artist/Details/ArtistAlternateTitles.js new file mode 100644 index 000000000..e1fde52e6 --- /dev/null +++ b/frontend/src/Artist/Details/ArtistAlternateTitles.js @@ -0,0 +1,28 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import styles from './ArtistAlternateTitles.css'; + +function ArtistAlternateTitles({ alternateTitles }) { + return ( +
    + { + alternateTitles.map((alternateTitle) => { + return ( +
  • + {alternateTitle} +
  • + ); + }) + } +
+ ); +} + +ArtistAlternateTitles.propTypes = { + alternateTitles: PropTypes.arrayOf(PropTypes.string).isRequired +}; + +export default ArtistAlternateTitles; diff --git a/frontend/src/Artist/Details/ArtistDetails.css b/frontend/src/Artist/Details/ArtistDetails.css new file mode 100644 index 000000000..1fd2f3940 --- /dev/null +++ b/frontend/src/Artist/Details/ArtistDetails.css @@ -0,0 +1,132 @@ +.innerContentBody { + padding: 0; +} + +.header { + position: relative; + width: 100%; + height: 375px; +} + +.backdrop { + position: absolute; + z-index: -1; + width: 100%; + height: 100%; + background-size: cover; +} + +.backdropOverlay { + position: absolute; + width: 100%; + height: 100%; + background: $black; + opacity: 0.7; +} + +.headerContent { + display: flex; + padding: 30px; + width: 100%; + height: 100%; + color: $white; +} + +.logo { + flex-shrink: 0; + margin-right: 35px; + width: 250px; + height: 97px; +} + +.poster { + flex-shrink: 0; + margin-right: 35px; + width: 250px; + height: 250px; +} + +.info { + flex-grow: 1; + overflow: hidden; +} + +.titleContainer { + display: flex; + justify-content: space-between; +} + +.title { + margin-bottom: 5px; + font-weight: 300; + font-size: 50px; + line-height: 50px; +} + +.alternateTitlesIconContainer { + margin-left: 20px; + line-height: 50px; +} + +.artistNavigationButtons { + white-space: no-wrap; +} + +.artistNavigationButton { + composes: button from 'Components/Link/IconButton.css'; + + margin-left: 5px; + color: #e1e2e3; + white-space: nowrap; +} + +.details { + font-weight: 300; + font-size: 20px; +} + +.runtime { + margin-right: 15px; +} + +.detailsLabel { + composes: label from 'Components/Label.css'; + + margin: 5px 10px 5px 0; +} + +.path, +.sizeOnDisk, +.qualityProfileName, +.links, +.tags { + margin-left: 8px; + font-weight: 300; + font-size: 17px; +} + +.path { + vertical-align: text-top; + font-size: $defaultFontSize; + font-family: $monoSpaceFontFamily; +} + +.contentContainer { + padding: 20px; +} + +@media only screen and (max-width: $breakpointSmall) { + .contentContainer { + padding: 20px 0; + } + + .headerContent { + padding: 15px; + } +} + +@media only screen and (max-width: $breakpointLarge) { + .poster { + display: none; + } +} diff --git a/frontend/src/Artist/Details/ArtistDetails.js b/frontend/src/Artist/Details/ArtistDetails.js new file mode 100644 index 000000000..74af7b438 --- /dev/null +++ b/frontend/src/Artist/Details/ArtistDetails.js @@ -0,0 +1,591 @@ +import _ from 'lodash'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import TextTruncate from 'react-text-truncate'; +import formatBytes from 'Utilities/Number/formatBytes'; +import selectAll from 'Utilities/Table/selectAll'; +import toggleSelected from 'Utilities/Table/toggleSelected'; +import { align, icons, kinds, sizes, tooltipPositions } from 'Helpers/Props'; +import HeartRating from 'Components/HeartRating'; +import Icon from 'Components/Icon'; +import IconButton from 'Components/Link/IconButton'; +import Label from 'Components/Label'; +import LoadingIndicator from 'Components/Loading/LoadingIndicator'; +import PageContent from 'Components/Page/PageContent'; +import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector'; +import PageToolbar from 'Components/Page/Toolbar/PageToolbar'; +import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection'; +import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator'; +import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton'; +import Popover from 'Components/Tooltip/Popover'; +import Tooltip from 'Components/Tooltip/Tooltip'; +import TrackFileEditorModal from 'TrackFile/Editor/TrackFileEditorModal'; +import OrganizePreviewModalConnector from 'Organize/OrganizePreviewModalConnector'; +import QualityProfileNameConnector from 'Settings/Profiles/Quality/QualityProfileNameConnector'; +import ArtistPoster from 'Artist/ArtistPoster'; +import EditArtistModalConnector from 'Artist/Edit/EditArtistModalConnector'; +import DeleteArtistModal from 'Artist/Delete/DeleteArtistModal'; +import ArtistAlternateTitles from './ArtistAlternateTitles'; +import ArtistDetailsSeasonConnector from './ArtistDetailsSeasonConnector'; +import ArtistTagsConnector from './ArtistTagsConnector'; +import ArtistDetailsLinks from './ArtistDetailsLinks'; +import styles from './ArtistDetails.css'; + +const albumTypes = [ + { + name: 'album', + label: 'Album', + isVisible: true + }, + { + name: 'ep', + label: 'EP', + isVisible: true + }, + { + name: 'single', + label: 'Single', + isVisible: true + }, + { + name: 'broadcast', + label: 'Broadcast', + isVisible: true + }, + { + name: 'other', + label: 'Other', + isVisible: true + } +]; + +function getFanartUrl(images) { + const fanartImage = _.find(images, { coverType: 'fanart' }); + if (fanartImage) { + // Remove protocol + return fanartImage.url.replace(/^https?:/, ''); + } +} + +function getExpandedState(newState) { + return { + allExpanded: newState.allSelected, + allCollapsed: newState.allUnselected, + expandedState: newState.selectedState + }; +} + +class ArtistDetails extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + isOrganizeModalOpen: false, + isManageEpisodesOpen: false, + isEditArtistModalOpen: false, + isDeleteArtistModalOpen: false, + allExpanded: false, + allCollapsed: false, + expandedState: {} + }; + } + + // + // Listeners + + onOrganizePress = () => { + this.setState({ isOrganizeModalOpen: true }); + } + + onOrganizeModalClose = () => { + this.setState({ isOrganizeModalOpen: false }); + } + + onManageEpisodesPress = () => { + this.setState({ isManageEpisodesOpen: true }); + } + + onManageEpisodesModalClose = () => { + this.setState({ isManageEpisodesOpen: false }); + } + + onEditArtistPress = () => { + this.setState({ isEditArtistModalOpen: true }); + } + + onEditArtistModalClose = () => { + this.setState({ isEditArtistModalOpen: false }); + } + + onDeleteArtistPress = () => { + this.setState({ + isEditArtistModalOpen: false, + isDeleteArtistModalOpen: true + }); + } + + onDeleteArtistModalClose = () => { + this.setState({ isDeleteArtistModalOpen: false }); + } + + onExpandAllPress = () => { + const { + allExpanded, + expandedState + } = this.state; + + this.setState(getExpandedState(selectAll(expandedState, !allExpanded))); + } + + onExpandPress = (albumId, isExpanded) => { + this.setState((state) => { + const convertedState = { + allSelected: state.allExpanded, + allUnselected: state.allCollapsed, + selectedState: state.expandedState + }; + + const newState = toggleSelected(convertedState, [], albumId, isExpanded, false); + + return getExpandedState(newState); + }); + } + + // + // Render + + render() { + const { + id, + foreignArtistId, + artistName, + ratings, + path, + sizeOnDisk, + trackFileCount, + qualityProfileId, + monitored, + status, + overview, + links, + images, + albums, + alternateTitles, + tags, + isRefreshing, + isSearching, + isFetching, + isPopulated, + episodesError, + trackFilesError, + previousArtist, + nextArtist, + onRefreshPress, + onSearchPress + } = this.props; + + const { + isOrganizeModalOpen, + isManageEpisodesOpen, + isEditArtistModalOpen, + isDeleteArtistModalOpen, + allExpanded, + allCollapsed, + expandedState + } = this.state; + + const continuing = status === 'continuing'; + + let trackFilesCountMessage = 'No track files'; + + if (trackFileCount === 1) { + trackFilesCountMessage = '1 track file'; + } else if (trackFileCount > 1) { + trackFilesCountMessage = `${trackFileCount} track files`; + } + + let expandIcon = icons.EXPAND_INDETERMINATE; + + if (allExpanded) { + expandIcon = icons.COLLAPSE; + } else if (allCollapsed) { + expandIcon = icons.EXPAND; + } + + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+ +
+ + +
+
+
+ {artistName} + + { + !!alternateTitles.length && + + + } + title="Alternate Titles" + body={} + position={tooltipPositions.BOTTOM} + /> + + } +
+ +
+ + + +
+
+ +
+
+ +
+
+ +
+ + + + + + + + + + + + + + + Links + + + } + tooltip={ + + } + kind={kinds.INVERSE} + position={tooltipPositions.BOTTOM} + /> + + { + !!tags.length && + + + + + Tags + + + } + tooltip={} + kind={kinds.INVERSE} + position={tooltipPositions.BOTTOM} + /> + + } +
+ +
+ +
+
+
+
+ +
+ { + !isPopulated && !episodesError && !trackFilesError && + + } + + { + !isFetching && episodesError && +
Loading episodes failed
+ } + + { + !isFetching && trackFilesError && +
Loading episode files failed
+ } + + { + isPopulated && !!albumTypes.length && +
+ { + albumTypes.slice(0).map((season) => { + return ( + + ); + }) + } +
+ } + +
+ + + + + + + + + + + ); + } +} + +ArtistDetails.propTypes = { + id: PropTypes.number.isRequired, + foreignArtistId: PropTypes.string.isRequired, + artistName: PropTypes.string.isRequired, + ratings: PropTypes.object.isRequired, + path: PropTypes.string.isRequired, + sizeOnDisk: PropTypes.number.isRequired, + trackFileCount: PropTypes.number, + qualityProfileId: PropTypes.number.isRequired, + monitored: PropTypes.bool.isRequired, + status: PropTypes.string.isRequired, + overview: PropTypes.string.isRequired, + links: PropTypes.arrayOf(PropTypes.object).isRequired, + images: PropTypes.arrayOf(PropTypes.object).isRequired, + albums: PropTypes.arrayOf(PropTypes.object).isRequired, + alternateTitles: PropTypes.arrayOf(PropTypes.string).isRequired, + tags: PropTypes.arrayOf(PropTypes.number).isRequired, + isRefreshing: PropTypes.bool.isRequired, + isSearching: PropTypes.bool.isRequired, + isFetching: PropTypes.bool.isRequired, + isPopulated: PropTypes.bool.isRequired, + episodesError: PropTypes.object, + trackFilesError: PropTypes.object, + previousArtist: PropTypes.object.isRequired, + nextArtist: PropTypes.object.isRequired, + onRefreshPress: PropTypes.func.isRequired, + onSearchPress: PropTypes.func.isRequired +}; + +ArtistDetails.defaultProps = { + isSaving: false +}; + +export default ArtistDetails; diff --git a/frontend/src/Artist/Details/ArtistDetailsConnector.js b/frontend/src/Artist/Details/ArtistDetailsConnector.js new file mode 100644 index 000000000..29efad73d --- /dev/null +++ b/frontend/src/Artist/Details/ArtistDetailsConnector.js @@ -0,0 +1,193 @@ +import _ from 'lodash'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import { findCommand } from 'Utilities/Command'; +import { registerPagePopulator, unregisterPagePopulator } from 'Utilities/pagePopulator'; +import createAllArtistSelector from 'Store/Selectors/createAllArtistSelector'; +import createCommandsSelector from 'Store/Selectors/createCommandsSelector'; +import { fetchEpisodes, clearEpisodes } from 'Store/Actions/episodeActions'; +import { fetchTrackFiles, clearTrackFiles } from 'Store/Actions/trackFileActions'; +import { fetchQueueDetails, clearQueueDetails } from 'Store/Actions/queueActions'; +import { executeCommand } from 'Store/Actions/commandActions'; +import * as commandNames from 'Commands/commandNames'; +import ArtistDetails from './ArtistDetails'; + +function createMapStateToProps() { + return createSelector( + (state, { nameSlug }) => nameSlug, + (state) => state.episodes, + (state) => state.trackFiles, + createAllArtistSelector(), + createCommandsSelector(), + (nameSlug, episodes, trackFiles, allArtists, commands) => { + const sortedArtist = _.orderBy(allArtists, 'sortName'); + const artistIndex = _.findIndex(sortedArtist, { nameSlug }); + const artist = sortedArtist[artistIndex]; + + if (!artist) { + return {}; + } + + const previousArtist = sortedArtist[artistIndex - 1] || _.last(sortedArtist); + const nextArtist = sortedArtist[artistIndex + 1] || _.first(sortedArtist); + const isArtistRefreshing = !!findCommand(commands, { name: commandNames.REFRESH_ARTIST, artistId: artist.id }); + const allArtistRefreshing = _.some(commands, (command) => command.name === commandNames.REFRESH_ARTIST && !command.body.artistId); + const isRefreshing = isArtistRefreshing || allArtistRefreshing; + const isSearching = !!findCommand(commands, { name: commandNames.ARTIST_SEARCH, artistId: artist.id }); + const isRenamingFiles = !!findCommand(commands, { name: commandNames.RENAME_FILES, artistId: artist.id }); + const isRenamingArtistCommand = findCommand(commands, { name: commandNames.RENAME_ARTIST }); + const isRenamingArtist = !!(isRenamingArtistCommand && isRenamingArtistCommand.body.artistId.indexOf(artist.id) > -1); + + const isFetching = episodes.isFetching || trackFiles.isFetching; + const isPopulated = episodes.isPopulated && trackFiles.isPopulated; + const episodesError = episodes.error; + const trackFilesError = trackFiles.error; + const alternateTitles = _.reduce(artist.alternateTitles, (acc, alternateTitle) => { + if ((alternateTitle.seasonNumber === -1 || alternateTitle.seasonNumber === undefined) && + (alternateTitle.sceneSeasonNumber === -1 || alternateTitle.sceneSeasonNumber === undefined)) { + acc.push(alternateTitle.title); + } + + return acc; + }, []); + + return { + ...artist, + alternateTitles, + isArtistRefreshing, + allArtistRefreshing, + isRefreshing, + isSearching, + isRenamingFiles, + isRenamingArtist, + isFetching, + isPopulated, + episodesError, + trackFilesError, + previousArtist, + nextArtist + }; + } + ); +} + +const mapDispatchToProps = { + fetchEpisodes, + clearEpisodes, + fetchTrackFiles, + clearTrackFiles, + fetchQueueDetails, + clearQueueDetails, + executeCommand +}; + +class ArtistDetailsConnector extends Component { + + // + // Lifecycle + + componentDidMount() { + registerPagePopulator(this.populate); + this.populate(); + } + + componentDidUpdate(prevProps) { + const { + id, + isArtistRefreshing, + allArtistRefreshing, + isRenamingFiles, + isRenamingArtist + } = this.props; + + if ( + (prevProps.isArtistRefreshing && !isArtistRefreshing) || + (prevProps.allArtistRefreshing && !allArtistRefreshing) || + (prevProps.isRenamingFiles && !isRenamingFiles) || + (prevProps.isRenamingArtist && !isRenamingArtist) + ) { + this.populate(); + } + + // If the id has changed we need to clear the episodes/episode + // files and fetch from the server. + + if (prevProps.id !== id) { + this.unpopulate(); + this.populate(); + } + } + + componentWillUnmount() { + unregisterPagePopulator(this.populate); + this.unpopulate(); + } + + // + // Control + + populate = () => { + const artistId = this.props.id; + + this.props.fetchEpisodes({ artistId }); + this.props.fetchTrackFiles({ artistId }); + this.props.fetchQueueDetails({ artistId }); + } + + unpopulate = () => { + this.props.clearEpisodes(); + this.props.clearTrackFiles(); + this.props.clearQueueDetails(); + } + + // + // Listeners + + onRefreshPress = () => { + this.props.executeCommand({ + name: commandNames.REFRESH_ARTIST, + artistId: this.props.id + }); + } + + onSearchPress = () => { + this.props.executeCommand({ + name: commandNames.ARTIST_SEARCH, + artistId: this.props.id + }); + } + + // + // Render + + render() { + return ( + + ); + } +} + +ArtistDetailsConnector.propTypes = { + id: PropTypes.number.isRequired, + nameSlug: PropTypes.string.isRequired, + isArtistRefreshing: PropTypes.bool.isRequired, + allArtistRefreshing: PropTypes.bool.isRequired, + isRefreshing: PropTypes.bool.isRequired, + isRenamingFiles: PropTypes.bool.isRequired, + isRenamingArtist: PropTypes.bool.isRequired, + fetchEpisodes: PropTypes.func.isRequired, + clearEpisodes: PropTypes.func.isRequired, + fetchTrackFiles: PropTypes.func.isRequired, + clearTrackFiles: PropTypes.func.isRequired, + fetchQueueDetails: PropTypes.func.isRequired, + clearQueueDetails: PropTypes.func.isRequired, + executeCommand: PropTypes.func.isRequired +}; + +export default connect(createMapStateToProps, mapDispatchToProps)(ArtistDetailsConnector); diff --git a/frontend/src/Artist/Details/ArtistDetailsLinks.css b/frontend/src/Artist/Details/ArtistDetailsLinks.css new file mode 100644 index 000000000..0f65b9154 --- /dev/null +++ b/frontend/src/Artist/Details/ArtistDetailsLinks.css @@ -0,0 +1,13 @@ +.links { + margin: 0; +} + +.link { + white-space: nowrap; +} + +.linkLabel { + composes: label from 'Components/Label.css'; + + cursor: pointer; +} diff --git a/frontend/src/Artist/Details/ArtistDetailsLinks.js b/frontend/src/Artist/Details/ArtistDetailsLinks.js new file mode 100644 index 000000000..655b6b294 --- /dev/null +++ b/frontend/src/Artist/Details/ArtistDetailsLinks.js @@ -0,0 +1,63 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import { kinds, sizes } from 'Helpers/Props'; +import Label from 'Components/Label'; +import Link from 'Components/Link/Link'; +import styles from './ArtistDetailsLinks.css'; + +function ArtistDetailsLinks(props) { + const { + foreignArtistId, + links + } = props; + + return ( +
+ + + + + + {links.map((link, index) => { + return ( + + + + + {(index > 0 && index % 5 === 0) && +

+ } + +
+ ); + })} + +
+ + ); +} + +ArtistDetailsLinks.propTypes = { + foreignArtistId: PropTypes.string.isRequired, + links: PropTypes.arrayOf(PropTypes.object).isRequired +}; + +export default ArtistDetailsLinks; diff --git a/frontend/src/Artist/Details/ArtistDetailsPageConnector.js b/frontend/src/Artist/Details/ArtistDetailsPageConnector.js new file mode 100644 index 000000000..3219267bf --- /dev/null +++ b/frontend/src/Artist/Details/ArtistDetailsPageConnector.js @@ -0,0 +1,76 @@ +import _ from 'lodash'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import { push } from 'react-router-redux'; +import createAllArtistSelector from 'Store/Selectors/createAllArtistSelector'; +import NotFound from 'Components/NotFound'; +import ArtistDetailsConnector from './ArtistDetailsConnector'; + +function createMapStateToProps() { + return createSelector( + (state, { match }) => match, + createAllArtistSelector(), + (match, allArtists) => { + const nameSlug = match.params.nameSlug; + const artistIndex = _.findIndex(allArtists, { nameSlug }); + + if (artistIndex > -1) { + return { + nameSlug + }; + } + + return {}; + } + ); +} + +const mapDispatchToProps = { + push +}; + +class ArtistDetailsPageConnector extends Component { + + // + // Lifecycle + + componentDidUpdate(prevProps) { + if (!this.props.nameSlug) { + this.props.push(`${window.Sonarr.urlBase}/`); + return; + } + } + + // + // Render + + render() { + const { + nameSlug + } = this.props; + + if (!nameSlug) { + return ( + + ); + } + + return ( + + ); + } +} + +ArtistDetailsPageConnector.propTypes = { + nameSlug: PropTypes.string, + match: PropTypes.shape({ params: PropTypes.shape({ nameSlug: PropTypes.string.isRequired }).isRequired }).isRequired, + push: PropTypes.func.isRequired +}; + +export default connect(createMapStateToProps, mapDispatchToProps)(ArtistDetailsPageConnector); diff --git a/frontend/src/Artist/Details/ArtistDetailsSeason.css b/frontend/src/Artist/Details/ArtistDetailsSeason.css new file mode 100644 index 000000000..4cb0065e9 --- /dev/null +++ b/frontend/src/Artist/Details/ArtistDetailsSeason.css @@ -0,0 +1,119 @@ +.albumType { + margin-bottom: 20px; + border: 1px solid $borderColor; + border-radius: 4px; + background-color: $white; + + &:last-of-type { + margin-bottom: 0; + } +} + +.header { + position: relative; + display: flex; + align-items: center; + width: 100%; + font-size: 24px; +} + +.albumTypeLabel { + margin-right: 5px; + margin-left: 5px; +} + +.albumCount { + font-size: 18px; + font-style: italic; + color: #8895aa; +} + +.episodeCountContainer { + margin-left: 10px; + vertical-align: text-bottom; +} + +.expandButton { + composes: link from 'Components/Link/Link.css'; + + flex-grow: 1; + margin: 0 20px; + text-align: center; +} + +.left { + display: flex; + align-items: center; + flex: 0 1 300px; +} + +.left, +.actions { + padding: 15px 10px; +} + +.actionsMenu { + composes: menu from 'Components/Menu/Menu.css'; + + flex: 0 0 45px; +} + +.actionsMenuContent { + composes: menuContent from 'Components/Menu/MenuContent.css'; + + white-space: nowrap; + font-size: 14px; +} + +.actionMenuIcon { + margin-right: 8px; +} + +.actionButton { + composes: button from 'Components/Link/IconButton.css'; + + width: 30px; +} + +.episodes { + padding-top: 15px; + border-top: 1px solid $borderColor; +} + +.collapseButtonContainer { + padding: 10px 15px; + width: 100%; + border-top: 1px solid $borderColor; + border-bottom-right-radius: 4px; + border-bottom-left-radius: 4px; + background-color: #fafafa; + text-align: center; +} + +.expandButtonIcon { + composes: actionButton; + + position: absolute; + top: 50%; + left: 50%; + margin-top: -12px; + margin-left: -15px; +} + +.noEpisodes { + margin-bottom: 15px; + text-align: center; +} + +@media only screen and (max-width: $breakpointSmall) { + .season { + border-right: 0; + border-left: 0; + border-radius: 0; + } + + .expandButtonIcon { + position: static; + margin: 0; + } +} diff --git a/frontend/src/Artist/Details/ArtistDetailsSeason.js b/frontend/src/Artist/Details/ArtistDetailsSeason.js new file mode 100644 index 000000000..ccffc29fe --- /dev/null +++ b/frontend/src/Artist/Details/ArtistDetailsSeason.js @@ -0,0 +1,318 @@ +import _ from 'lodash'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import isAfter from 'Utilities/Date/isAfter'; +import isBefore from 'Utilities/Date/isBefore'; +import getToggledRange from 'Utilities/Table/getToggledRange'; +import { align, icons, kinds, sizes } from 'Helpers/Props'; +import Icon from 'Components/Icon'; +import IconButton from 'Components/Link/IconButton'; +import Link from 'Components/Link/Link'; +import SpinnerIcon from 'Components/SpinnerIcon'; +import SpinnerIconButton from 'Components/Link/SpinnerIconButton'; +import Menu from 'Components/Menu/Menu'; +import MenuButton from 'Components/Menu/MenuButton'; +import MenuContent from 'Components/Menu/MenuContent'; +import MenuItem from 'Components/Menu/MenuItem'; +import Table from 'Components/Table/Table'; +import TableBody from 'Components/Table/TableBody'; +import TrackFileEditorModal from 'TrackFile/Editor/TrackFileEditorModal'; +import OrganizePreviewModalConnector from 'Organize/OrganizePreviewModalConnector'; +import AlbumRowConnector from './AlbumRowConnector'; +import styles from './ArtistDetailsSeason.css'; + +class ArtistDetailsSeason extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + isOrganizeModalOpen: false, + isManageEpisodesOpen: false, + lastToggledEpisode: null + }; + } + + componentDidMount() { + this._expandByDefault(); + } + + componentDidUpdate(prevProps) { + if (prevProps.artistId !== this.props.artistId) { + this._expandByDefault(); + } + } + + // + // Control + + _expandByDefault() { + const { + name, + onExpandPress, + items + } = this.props; + + const expand = _.some(items, (item) => { + return isAfter(item.releaseDate) || + isAfter(item.releaseDate, { days: -30 }); + }); + + onExpandPress(name, expand && name > 0); + } + + // + // Listeners + + onOrganizePress = () => { + this.setState({ isOrganizeModalOpen: true }); + } + + onOrganizeModalClose = () => { + this.setState({ isOrganizeModalOpen: false }); + } + + onManageEpisodesPress = () => { + this.setState({ isManageEpisodesOpen: true }); + } + + onManageEpisodesModalClose = () => { + this.setState({ isManageEpisodesOpen: false }); + } + + onExpandPress = () => { + const { + name, + isExpanded + } = this.props; + + this.props.onExpandPress(name, !isExpanded); + } + + onMonitorAlbumPress = (albumId, monitored, { shiftKey }) => { + const lastToggled = this.state.lastToggledEpisode; + const albumIds = [albumId]; + + if (shiftKey && lastToggled) { + const { lower, upper } = getToggledRange(this.props.items, albumId, lastToggled); + const items = this.props.items; + + for (let i = lower; i < upper; i++) { + albumIds.push(items[i].id); + } + } + + this.setState({ lastToggledEpisode: albumId }); + + this.props.onMonitorAlbumPress(_.uniq(albumIds), monitored); + } + + // + // Render + + render() { + const { + artistId, + label, + items, + columns, + isSaving, + isExpanded, + isSearching, + artistMonitored, + isSmallScreen, + onTableOptionChange, + onMonitorSeasonPress, + onSearchPress + } = this.props; + + const { + isOrganizeModalOpen, + isManageEpisodesOpen + } = this.state; + + return ( +
+
+
+ { +
+ + {label} + + + + ({items.length} Releases) + +
+ } + +
+ + + + + + { + !isSmallScreen && +   + } + + + { + isSmallScreen ? + + + + + + + + + + Search + + + + + + Preview Rename + + + + + + Manage Tracks + + + : + +
+ + +
+ } + +
+ +
+ { + isExpanded && +
+ { + items.length ? + + + { + items.map((item) => { + return ( + + ); + }) + } + +
: + +
+ No albums in this group +
+ } +
+ +
+
+ } +
+ + + + +
+ ); + } +} + +ArtistDetailsSeason.propTypes = { + artistId: PropTypes.number.isRequired, + name: PropTypes.string.isRequired, + label: PropTypes.string.isRequired, + items: PropTypes.arrayOf(PropTypes.object).isRequired, + columns: PropTypes.arrayOf(PropTypes.object).isRequired, + isSaving: PropTypes.bool, + isExpanded: PropTypes.bool, + isSearching: PropTypes.bool.isRequired, + artistMonitored: PropTypes.bool.isRequired, + isSmallScreen: PropTypes.bool.isRequired, + onTableOptionChange: PropTypes.func.isRequired, + onMonitorSeasonPress: PropTypes.func.isRequired, + onExpandPress: PropTypes.func.isRequired, + onMonitorAlbumPress: PropTypes.func.isRequired, + onSearchPress: PropTypes.func.isRequired +}; + +export default ArtistDetailsSeason; diff --git a/frontend/src/Artist/Details/ArtistDetailsSeasonConnector.js b/frontend/src/Artist/Details/ArtistDetailsSeasonConnector.js new file mode 100644 index 000000000..49dfb78e4 --- /dev/null +++ b/frontend/src/Artist/Details/ArtistDetailsSeasonConnector.js @@ -0,0 +1,113 @@ +import _ from 'lodash'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import { findCommand } from 'Utilities/Command'; +import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector'; +import createArtistSelector from 'Store/Selectors/createArtistSelector'; +import createCommandsSelector from 'Store/Selectors/createCommandsSelector'; +import { toggleSeasonMonitored } from 'Store/Actions/artistActions'; +import { toggleEpisodesMonitored, setEpisodesTableOption } from 'Store/Actions/episodeActions'; +import { executeCommand } from 'Store/Actions/commandActions'; +import * as commandNames from 'Commands/commandNames'; +import ArtistDetailsSeason from './ArtistDetailsSeason'; + +function createMapStateToProps() { + return createSelector( + (state, { label }) => label, + (state) => state.episodes, + createArtistSelector(), + createCommandsSelector(), + createDimensionsSelector(), + (label, episodes, artist, commands, dimensions) => { + const isSearching = !!findCommand(commands, { + name: commandNames.SEASON_SEARCH, + artistId: artist.id, + label + }); + + const episodesInSeason = _.filter(episodes.items, { albumType: label }); + const sortedEpisodes = _.orderBy(episodesInSeason, 'releaseDate', 'desc'); + + return { + items: sortedEpisodes, + columns: episodes.columns, + isSearching, + artistMonitored: artist.monitored, + isSmallScreen: dimensions.isSmallScreen + }; + } + ); +} + +const mapDispatchToProps = { + toggleSeasonMonitored, + toggleEpisodesMonitored, + setEpisodesTableOption, + executeCommand +}; + +class ArtistDetailsSeasonConnector extends Component { + + // + // Listeners + + onTableOptionChange = (payload) => { + this.props.setEpisodesTableOption(payload); + } + + onMonitorSeasonPress = (monitored) => { + const { + artistId + } = this.props; + + this.props.toggleSeasonMonitored({ + artistId, + monitored + }); + } + + onSearchPress = () => { + const { + artistId + } = this.props; + + this.props.executeCommand({ + name: commandNames.SEASON_SEARCH, + artistId + }); + } + + onMonitorAlbumPress = (albumIds, monitored) => { + this.props.toggleEpisodesMonitored({ + albumIds, + monitored + }); + } + + // + // Render + + render() { + return ( + + ); + } +} + +ArtistDetailsSeasonConnector.propTypes = { + artistId: PropTypes.number.isRequired, + toggleSeasonMonitored: PropTypes.func.isRequired, + toggleEpisodesMonitored: PropTypes.func.isRequired, + setEpisodesTableOption: PropTypes.func.isRequired, + executeCommand: PropTypes.func.isRequired +}; + +export default connect(createMapStateToProps, mapDispatchToProps)(ArtistDetailsSeasonConnector); diff --git a/frontend/src/Artist/Details/ArtistTags.css b/frontend/src/Artist/Details/ArtistTags.css new file mode 100644 index 000000000..ec340a041 --- /dev/null +++ b/frontend/src/Artist/Details/ArtistTags.css @@ -0,0 +1,8 @@ +.tags { + margin: 0; + padding-left: 20px; +} + +.tag { + white-space: nowrap; +} diff --git a/frontend/src/Artist/Details/ArtistTags.js b/frontend/src/Artist/Details/ArtistTags.js new file mode 100644 index 000000000..7ea841a36 --- /dev/null +++ b/frontend/src/Artist/Details/ArtistTags.js @@ -0,0 +1,30 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import { kinds, sizes } from 'Helpers/Props'; +import Label from 'Components/Label'; + +function ArtistTags({ tags }) { + return ( +
+ { + tags.map((tag) => { + return ( + + ); + }) + } +
+ ); +} + +ArtistTags.propTypes = { + tags: PropTypes.arrayOf(PropTypes.string).isRequired +}; + +export default ArtistTags; diff --git a/frontend/src/Artist/Details/ArtistTagsConnector.js b/frontend/src/Artist/Details/ArtistTagsConnector.js new file mode 100644 index 000000000..1ecde26cd --- /dev/null +++ b/frontend/src/Artist/Details/ArtistTagsConnector.js @@ -0,0 +1,30 @@ +import _ from 'lodash'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import createArtistSelector from 'Store/Selectors/createArtistSelector'; +import createTagsSelector from 'Store/Selectors/createTagsSelector'; +import ArtistTags from './ArtistTags'; + +function createMapStateToProps() { + return createSelector( + createArtistSelector(), + createTagsSelector(), + (artist, tagList) => { + const tags = _.reduce(artist.tags, (acc, tag) => { + const matchingTag = _.find(tagList, { id: tag }); + + if (matchingTag) { + acc.push(matchingTag.label); + } + + return acc; + }, []); + + return { + tags + }; + } + ); +} + +export default connect(createMapStateToProps)(ArtistTags); diff --git a/frontend/src/Artist/Edit/EditArtistModal.js b/frontend/src/Artist/Edit/EditArtistModal.js new file mode 100644 index 000000000..6e99a2f53 --- /dev/null +++ b/frontend/src/Artist/Edit/EditArtistModal.js @@ -0,0 +1,25 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import Modal from 'Components/Modal/Modal'; +import EditArtistModalContentConnector from './EditArtistModalContentConnector'; + +function EditArtistModal({ isOpen, onModalClose, ...otherProps }) { + return ( + + + + ); +} + +EditArtistModal.propTypes = { + isOpen: PropTypes.bool.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default EditArtistModal; diff --git a/frontend/src/Artist/Edit/EditArtistModalConnector.js b/frontend/src/Artist/Edit/EditArtistModalConnector.js new file mode 100644 index 000000000..9e62a4780 --- /dev/null +++ b/frontend/src/Artist/Edit/EditArtistModalConnector.js @@ -0,0 +1,39 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { clearPendingChanges } from 'Store/Actions/baseActions'; +import EditArtistModal from './EditArtistModal'; + +const mapDispatchToProps = { + clearPendingChanges +}; + +class EditArtistModalConnector extends Component { + + // + // Listeners + + onModalClose = () => { + this.props.clearPendingChanges({ section: 'artist' }); + this.props.onModalClose(); + } + + // + // Render + + render() { + return ( + + ); + } +} + +EditArtistModalConnector.propTypes = { + onModalClose: PropTypes.func.isRequired, + clearPendingChanges: PropTypes.func.isRequired +}; + +export default connect(undefined, mapDispatchToProps)(EditArtistModalConnector); diff --git a/frontend/src/Artist/Edit/EditArtistModalContent.css b/frontend/src/Artist/Edit/EditArtistModalContent.css new file mode 100644 index 000000000..a3c7f464c --- /dev/null +++ b/frontend/src/Artist/Edit/EditArtistModalContent.css @@ -0,0 +1,5 @@ +.deleteButton { + composes: button from 'Components/Link/Button.css'; + + margin-right: auto; +} diff --git a/frontend/src/Artist/Edit/EditArtistModalContent.js b/frontend/src/Artist/Edit/EditArtistModalContent.js new file mode 100644 index 000000000..ae45f6332 --- /dev/null +++ b/frontend/src/Artist/Edit/EditArtistModalContent.js @@ -0,0 +1,164 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { inputTypes, kinds } from 'Helpers/Props'; +import Button from 'Components/Link/Button'; +import SpinnerButton from 'Components/Link/SpinnerButton'; +import ModalContent from 'Components/Modal/ModalContent'; +import ModalHeader from 'Components/Modal/ModalHeader'; +import ModalBody from 'Components/Modal/ModalBody'; +import ModalFooter from 'Components/Modal/ModalFooter'; +import Form from 'Components/Form/Form'; +import FormGroup from 'Components/Form/FormGroup'; +import FormLabel from 'Components/Form/FormLabel'; +import FormInputGroup from 'Components/Form/FormInputGroup'; +import styles from './EditArtistModalContent.css'; + +class EditArtistModalContent extends Component { + + // + // Render + + render() { + const { + artistName, + item, + isSaving, + showLanguageProfile, + onInputChange, + onSavePress, + onModalClose, + onDeleteArtistPress, + ...otherProps + } = this.props; + + const { + monitored, + albumFolder, + qualityProfileId, + languageProfileId, + path, + tags + } = item; + + return ( + + + Edit - {artistName} + + + +
+ + Monitored + + + + + + Use Album Folder + + + + + + Quality Profile + + + + + { + showLanguageProfile && + + Language Profile + + + + } + + + Path + + + + + + Tags + + + +
+
+ + + + + + + Save + + +
+ ); + } +} + +EditArtistModalContent.propTypes = { + artistId: PropTypes.number.isRequired, + artistName: PropTypes.string.isRequired, + item: PropTypes.object.isRequired, + isSaving: PropTypes.bool.isRequired, + showLanguageProfile: PropTypes.bool.isRequired, + onInputChange: PropTypes.func.isRequired, + onSavePress: PropTypes.func.isRequired, + onModalClose: PropTypes.func.isRequired, + onDeleteArtistPress: PropTypes.func.isRequired +}; + +export default EditArtistModalContent; diff --git a/frontend/src/Artist/Edit/EditArtistModalContentConnector.js b/frontend/src/Artist/Edit/EditArtistModalContentConnector.js new file mode 100644 index 000000000..8676584a3 --- /dev/null +++ b/frontend/src/Artist/Edit/EditArtistModalContentConnector.js @@ -0,0 +1,97 @@ +import _ from 'lodash'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import selectSettings from 'Store/Selectors/selectSettings'; +import createArtistSelector from 'Store/Selectors/createArtistSelector'; +import { setArtistValue, saveArtist } from 'Store/Actions/artistActions'; +import EditArtistModalContent from './EditArtistModalContent'; + +function createMapStateToProps() { + return createSelector( + (state) => state.artist, + (state) => state.settings.languageProfiles, + createArtistSelector(), + (artistState, languageProfiles, artist) => { + const { + isSaving, + saveError, + pendingChanges + } = artistState; + + const artistSettings = _.pick(artist, [ + 'monitored', + 'albumFolder', + 'qualityProfileId', + 'languageProfileId', + 'path', + 'tags' + ]); + + const settings = selectSettings(artistSettings, pendingChanges, saveError); + + return { + artistName: artist.artistName, + isSaving, + saveError, + pendingChanges, + item: settings.settings, + showLanguageProfile: languageProfiles.items.length > 1, + ...settings + }; + } + ); +} + +const mapDispatchToProps = { + setArtistValue, + saveArtist +}; + +class EditArtistModalContentConnector extends Component { + + // + // Lifecycle + + componentDidUpdate(prevProps, prevState) { + if (prevProps.isSaving && !this.props.isSaving && !this.props.saveError) { + this.props.onModalClose(); + } + } + + // + // Listeners + + onInputChange = ({ name, value }) => { + this.props.setArtistValue({ name, value }); + } + + onSavePress = () => { + this.props.saveArtist({ id: this.props.artistId }); + } + + // + // Render + + render() { + return ( + + ); + } +} + +EditArtistModalContentConnector.propTypes = { + artistId: PropTypes.number, + isSaving: PropTypes.bool.isRequired, + saveError: PropTypes.object, + setArtistValue: PropTypes.func.isRequired, + saveArtist: PropTypes.func.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default connect(createMapStateToProps, mapDispatchToProps)(EditArtistModalContentConnector); diff --git a/frontend/src/Artist/Editor/ArtistEditor.js b/frontend/src/Artist/Editor/ArtistEditor.js new file mode 100644 index 000000000..17b0fa91a --- /dev/null +++ b/frontend/src/Artist/Editor/ArtistEditor.js @@ -0,0 +1,322 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import getSelectedIds from 'Utilities/Table/getSelectedIds'; +import selectAll from 'Utilities/Table/selectAll'; +import toggleSelected from 'Utilities/Table/toggleSelected'; +import { align, sortDirections } from 'Helpers/Props'; +import LoadingIndicator from 'Components/Loading/LoadingIndicator'; +import PageContent from 'Components/Page/PageContent'; +import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector'; +import PageToolbar from 'Components/Page/Toolbar/PageToolbar'; +import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection'; +import FilterMenu from 'Components/Menu/FilterMenu'; +import MenuContent from 'Components/Menu/MenuContent'; +import FilterMenuItem from 'Components/Menu/FilterMenuItem'; +import Table from 'Components/Table/Table'; +import TableBody from 'Components/Table/TableBody'; +import NoArtist from 'Artist/NoArtist'; +import ArtistEditorRowConnector from './ArtistEditorRowConnector'; +import ArtistEditorFooter from './ArtistEditorFooter'; +import OrganizeArtistModal from './Organize/OrganizeArtistModal'; + +function getColumns(showLanguageProfile) { + return [ + { + name: 'status', + isVisible: true + }, + { + name: 'sortName', + label: 'Name', + isSortable: true, + isVisible: true + }, + { + name: 'qualityProfileId', + label: 'Quality Profile', + isSortable: true, + isVisible: true + }, + { + name: 'languageProfileId', + label: 'Language Profile', + isSortable: true, + isVisible: showLanguageProfile + }, + { + name: 'albumFolder', + label: 'Album Folder', + isSortable: true, + isVisible: true + }, + { + name: 'path', + label: 'Path', + isSortable: true, + isVisible: true + }, + { + name: 'tags', + label: 'Tags', + isSortable: false, + isVisible: true + } + ]; +} + +class ArtistEditor extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + allSelected: false, + allUnselected: false, + lastToggled: null, + selectedState: {}, + isOrganizingArtistModalOpen: false, + columns: getColumns(props.showLanguageProfile) + }; + } + + componentDidUpdate(prevProps) { + const { + isDeleting, + deleteError + } = this.props; + + const hasFinishedDeleting = prevProps.isDeleting && + !isDeleting && + !deleteError; + + if (hasFinishedDeleting) { + this.onSelectAllChange({ value: false }); + } + } + + // + // Control + + getSelectedIds = () => { + return getSelectedIds(this.state.selectedState); + } + + // + // Listeners + + onSelectAllChange = ({ value }) => { + this.setState(selectAll(this.state.selectedState, value)); + } + + onSelectedChange = ({ id, value, shiftKey = false }) => { + this.setState((state) => { + return toggleSelected(state, this.props.items, id, value, shiftKey); + }); + } + + onSaveSelected = (changes) => { + this.props.onSaveSelected({ + artistIds: this.getSelectedIds(), + ...changes + }); + } + + onOrganizeArtistPress = () => { + this.setState({ isOrganizingArtistModalOpen: true }); + } + + onOrganizeArtistModalClose = (organized) => { + this.setState({ isOrganizingArtistModalOpen: false }); + + if (organized === true) { + this.onSelectAllChange({ value: false }); + } + } + + // + // Render + + render() { + const { + isFetching, + isPopulated, + error, + items, + filterKey, + filterValue, + sortKey, + sortDirection, + isSaving, + saveError, + isDeleting, + deleteError, + isOrganizingArtist, + showLanguageProfile, + onSortPress, + onFilterSelect + } = this.props; + + const { + allSelected, + allUnselected, + selectedState, + columns + } = this.state; + + const selectedArtistIds = this.getSelectedIds(); + + return ( + + + + + + + + All + + + + Monitored Only + + + + Continuing Only + + + + Ended Only + + + + Missing Albums + + + + + + + + { + isFetching && !isPopulated && + + } + + { + !isFetching && !!error && +
Unable to load the calendar
+ } + + { + !error && isPopulated && !!items.length && +
+ + + { + items.map((item) => { + return ( + + ); + }) + } + +
+
+ } + + { + !error && isPopulated && !items.length && + + } +
+ + + + +
+ ); + } +} + +ArtistEditor.propTypes = { + isFetching: PropTypes.bool.isRequired, + isPopulated: PropTypes.bool.isRequired, + error: PropTypes.object, + items: PropTypes.arrayOf(PropTypes.object).isRequired, + sortKey: PropTypes.string, + sortDirection: PropTypes.oneOf(sortDirections.all), + filterKey: PropTypes.string, + filterValue: PropTypes.oneOfType([PropTypes.bool, PropTypes.number, PropTypes.string]), + isSaving: PropTypes.bool.isRequired, + saveError: PropTypes.object, + isDeleting: PropTypes.bool.isRequired, + deleteError: PropTypes.object, + isOrganizingArtist: PropTypes.bool.isRequired, + showLanguageProfile: PropTypes.bool.isRequired, + onSortPress: PropTypes.func.isRequired, + onFilterSelect: PropTypes.func.isRequired, + onSaveSelected: PropTypes.func.isRequired +}; + +export default ArtistEditor; diff --git a/frontend/src/Artist/Editor/ArtistEditorConnector.js b/frontend/src/Artist/Editor/ArtistEditorConnector.js new file mode 100644 index 000000000..e99e09861 --- /dev/null +++ b/frontend/src/Artist/Editor/ArtistEditorConnector.js @@ -0,0 +1,86 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { createSelector } from 'reselect'; +import connectSection from 'Store/connectSection'; +import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector'; +import createCommandSelector from 'Store/Selectors/createCommandSelector'; +import { setArtistEditorSort, setArtistEditorFilter, saveArtistEditor } from 'Store/Actions/artistEditorActions'; +import { fetchRootFolders } from 'Store/Actions/rootFolderActions'; +import * as commandNames from 'Commands/commandNames'; +import ArtistEditor from './ArtistEditor'; + +function createMapStateToProps() { + return createSelector( + (state) => state.settings.languageProfiles, + createClientSideCollectionSelector(), + createCommandSelector(commandNames.RENAME_ARTIST), + (languageProfiles, artist, isOrganizingArtist) => { + return { + isOrganizingArtist, + showLanguageProfile: languageProfiles.items.length > 1, + ...artist + }; + } + ); +} + +const mapDispatchToProps = { + setArtistEditorSort, + setArtistEditorFilter, + saveArtistEditor, + fetchRootFolders +}; + +class ArtistEditorConnector extends Component { + + // + // Lifecycle + + componentDidMount() { + this.props.fetchRootFolders(); + } + + // + // Listeners + + onSortPress = (sortKey) => { + this.props.setArtistEditorSort({ sortKey }); + } + + onFilterSelect = (filterKey, filterValue, filterType) => { + this.props.setArtistEditorFilter({ filterKey, filterValue, filterType }); + } + + onSaveSelected = (payload) => { + this.props.saveArtistEditor(payload); + } + + // + // Render + + render() { + return ( + + ); + } +} + +ArtistEditorConnector.propTypes = { + setArtistEditorSort: PropTypes.func.isRequired, + setArtistEditorFilter: PropTypes.func.isRequired, + saveArtistEditor: PropTypes.func.isRequired, + fetchRootFolders: PropTypes.func.isRequired +}; + +export default connectSection( + createMapStateToProps, + mapDispatchToProps, + undefined, + undefined, + { section: 'artist', uiSection: 'artistEditor' } +)(ArtistEditorConnector); diff --git a/frontend/src/Artist/Editor/ArtistEditorFooter.css b/frontend/src/Artist/Editor/ArtistEditorFooter.css new file mode 100644 index 000000000..cc01c33ea --- /dev/null +++ b/frontend/src/Artist/Editor/ArtistEditorFooter.css @@ -0,0 +1,57 @@ +.inputContainer { + margin-right: 20px; + min-width: 150px; +} + +.buttonContainer { + display: flex; + justify-content: flex-end; + flex-grow: 1; +} + +.buttonContainerContent { + flex-grow: 0; +} + +.buttons { + display: flex; + justify-content: flex-end; + flex-grow: 1; +} + +.organizeSelectedButton, +.tagsButton { + composes: button from 'Components/Link/SpinnerButton.css'; + + margin-right: 10px; + height: 35px; +} + +.deleteSelectedButton { + composes: button from 'Components/Link/SpinnerButton.css'; + + margin-left: 50px; + height: 35px; +} + +@media only screen and (max-width: $breakpointSmall) { + .inputContainer { + margin-right: 0; + } + + .buttonContainer { + justify-content: flex-start; + } + + .buttonContainerContent { + flex-grow: 1; + } + + .buttons { + justify-content: space-between; + } + + .selectedArtistLabel { + text-align: left; + } +} diff --git a/frontend/src/Artist/Editor/ArtistEditorFooter.js b/frontend/src/Artist/Editor/ArtistEditorFooter.js new file mode 100644 index 000000000..382c5d092 --- /dev/null +++ b/frontend/src/Artist/Editor/ArtistEditorFooter.js @@ -0,0 +1,295 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { kinds } from 'Helpers/Props'; +import SelectInput from 'Components/Form/SelectInput'; +import LanguageProfileSelectInputConnector from 'Components/Form/LanguageProfileSelectInputConnector'; +import QualityProfileSelectInputConnector from 'Components/Form/QualityProfileSelectInputConnector'; +import RootFolderSelectInputConnector from 'Components/Form/RootFolderSelectInputConnector'; +import SpinnerButton from 'Components/Link/SpinnerButton'; +import PageContentFooter from 'Components/Page/PageContentFooter'; +import TagsModal from './Tags/TagsModal'; +import DeleteArtistModal from './Delete/DeleteArtistModal'; +import ArtistEditorFooterLabel from './ArtistEditorFooterLabel'; +import styles from './ArtistEditorFooter.css'; + +const NO_CHANGE = 'noChange'; + +class ArtistEditorFooter extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + monitored: NO_CHANGE, + qualityProfileId: NO_CHANGE, + languageProfileId: NO_CHANGE, + albumFolder: NO_CHANGE, + rootFolderPath: NO_CHANGE, + savingTags: false, + isDeleteArtistModalOpen: false, + isTagsModalOpen: false + }; + } + + componentDidUpdate(prevProps) { + const { + isSaving, + saveError + } = this.props; + + if (prevProps.isSaving && !isSaving && !saveError) { + this.setState({ + monitored: NO_CHANGE, + qualityProfileId: NO_CHANGE, + languageProfileId: NO_CHANGE, + albumFolder: NO_CHANGE, + rootFolderPath: NO_CHANGE, + savingTags: false + }); + } + } + + // + // Listeners + + onInputChange = ({ name, value }) => { + this.setState({ [name]: value }); + + if (value === NO_CHANGE) { + return; + } + + switch (name) { + case 'monitored': + this.props.onSaveSelected({ [name]: value === 'monitored' }); + break; + case 'albumFolder': + this.props.onSaveSelected({ [name]: value === 'yes' }); + break; + default: + this.props.onSaveSelected({ [name]: value }); + } + } + + onApplyTagsPress = (tags, applyTags) => { + this.setState({ + savingTags: true, + isTagsModalOpen: false + }); + + this.props.onSaveSelected({ + tags, + applyTags + }); + } + + onDeleteSelectedPress = () => { + this.setState({ isDeleteArtistModalOpen: true }); + } + + onDeleteArtistModalClose = () => { + this.setState({ isDeleteArtistModalOpen: false }); + } + + onTagsPress = () => { + this.setState({ isTagsModalOpen: true }); + } + + onTagsModalClose = () => { + this.setState({ isTagsModalOpen: false }); + } + + // + // Render + + render() { + const { + artistIds, + selectedCount, + isSaving, + isDeleting, + isOrganizingArtist, + showLanguageProfile, + onOrganizeArtistPress + } = this.props; + + const { + monitored, + qualityProfileId, + languageProfileId, + albumFolder, + rootFolderPath, + savingTags, + isTagsModalOpen, + isDeleteArtistModalOpen + } = this.state; + + const monitoredOptions = [ + { key: NO_CHANGE, value: 'No Change', disabled: true }, + { key: 'monitored', value: 'Monitored', disabled: true }, + { key: 'unmonitored', value: 'Unmonitored' } + ]; + + const albumFolderOptions = [ + { key: NO_CHANGE, value: 'No Change', disabled: true }, + { key: 'yes', value: 'Yes' }, + { key: 'no', value: 'No' } + ]; + + return ( + +
+ + + +
+ +
+ + + +
+ + { + showLanguageProfile && +
+ + + +
+ } + +
+ + + +
+ +
+ + + +
+ +
+
+ + +
+
+ + Rename Files + + + + Set Tags + +
+ + + Delete + +
+
+
+ + + + +
+ ); + } +} + +ArtistEditorFooter.propTypes = { + artistIds: PropTypes.arrayOf(PropTypes.number).isRequired, + selectedCount: PropTypes.number.isRequired, + isSaving: PropTypes.bool.isRequired, + saveError: PropTypes.object, + isDeleting: PropTypes.bool.isRequired, + deleteError: PropTypes.object, + isOrganizingArtist: PropTypes.bool.isRequired, + showLanguageProfile: PropTypes.bool.isRequired, + onSaveSelected: PropTypes.func.isRequired, + onOrganizeArtistPress: PropTypes.func.isRequired +}; + +export default ArtistEditorFooter; diff --git a/frontend/src/Artist/Editor/ArtistEditorFooterLabel.css b/frontend/src/Artist/Editor/ArtistEditorFooterLabel.css new file mode 100644 index 000000000..9b4b40be6 --- /dev/null +++ b/frontend/src/Artist/Editor/ArtistEditorFooterLabel.css @@ -0,0 +1,8 @@ +.label { + margin-bottom: 3px; + font-weight: bold; +} + +.savingIcon { + margin-left: 8px; +} diff --git a/frontend/src/Artist/Editor/ArtistEditorFooterLabel.js b/frontend/src/Artist/Editor/ArtistEditorFooterLabel.js new file mode 100644 index 000000000..1c6be745d --- /dev/null +++ b/frontend/src/Artist/Editor/ArtistEditorFooterLabel.js @@ -0,0 +1,40 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import { icons } from 'Helpers/Props'; +import SpinnerIcon from 'Components/SpinnerIcon'; +import styles from './ArtistEditorFooterLabel.css'; + +function ArtistEditorFooterLabel(props) { + const { + className, + label, + isSaving + } = props; + + return ( +
+ {label} + + { + isSaving && + + } +
+ ); +} + +ArtistEditorFooterLabel.propTypes = { + className: PropTypes.string.isRequired, + label: PropTypes.string.isRequired, + isSaving: PropTypes.bool.isRequired +}; + +ArtistEditorFooterLabel.defaultProps = { + className: styles.label +}; + +export default ArtistEditorFooterLabel; diff --git a/frontend/src/Artist/Editor/ArtistEditorRow.css b/frontend/src/Artist/Editor/ArtistEditorRow.css new file mode 100644 index 000000000..17711c50c --- /dev/null +++ b/frontend/src/Artist/Editor/ArtistEditorRow.css @@ -0,0 +1,5 @@ +.albumFolder { + composes: cell from 'Components/Table/Cells/TableRowCell.css'; + + width: 150px; +} diff --git a/frontend/src/Artist/Editor/ArtistEditorRow.js b/frontend/src/Artist/Editor/ArtistEditorRow.js new file mode 100644 index 000000000..ddfd8a893 --- /dev/null +++ b/frontend/src/Artist/Editor/ArtistEditorRow.js @@ -0,0 +1,114 @@ +import _ from 'lodash'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import titleCase from 'Utilities/String/titleCase'; +import TagListConnector from 'Components/TagListConnector'; +import CheckInput from 'Components/Form/CheckInput'; +import TableRow from 'Components/Table/TableRow'; +import TableRowCell from 'Components/Table/Cells/TableRowCell'; +import TableSelectCell from 'Components/Table/Cells/TableSelectCell'; +import ArtistNameLink from 'Artist/ArtistNameLink'; +import ArtistStatusCell from 'Artist/Index/Table/ArtistStatusCell'; +import styles from './ArtistEditorRow.css'; + +class ArtistEditorRow extends Component { + + // + // Listeners + + onAlbumFolderChange = () => { + // Mock handler to satisfy `onChange` being required for `CheckInput`. + // + } + + // + // Render + + render() { + const { + id, + status, + nameSlug, + artistName, + monitored, + languageProfile, + qualityProfile, + albumFolder, + path, + tags, + columns, + isSelected, + onSelectedChange + } = this.props; + + return ( + + + + + + + + + + + {qualityProfile.name} + + + { + _.find(columns, { name: 'languageProfileId' }).isVisible && + + {languageProfile.name} + + } + + + + + + + {path} + + + + + + + ); + } +} + +ArtistEditorRow.propTypes = { + id: PropTypes.number.isRequired, + status: PropTypes.string.isRequired, + nameSlug: PropTypes.string.isRequired, + artistName: PropTypes.string.isRequired, + monitored: PropTypes.bool.isRequired, + languageProfile: PropTypes.object.isRequired, + qualityProfile: PropTypes.object.isRequired, + albumFolder: PropTypes.bool.isRequired, + path: PropTypes.string.isRequired, + tags: PropTypes.arrayOf(PropTypes.number).isRequired, + columns: PropTypes.arrayOf(PropTypes.object).isRequired, + isSelected: PropTypes.bool, + onSelectedChange: PropTypes.func.isRequired +}; + +export default ArtistEditorRow; diff --git a/frontend/src/Artist/Editor/ArtistEditorRowConnector.js b/frontend/src/Artist/Editor/ArtistEditorRowConnector.js new file mode 100644 index 000000000..4ed496a01 --- /dev/null +++ b/frontend/src/Artist/Editor/ArtistEditorRowConnector.js @@ -0,0 +1,34 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import createLanguageProfileSelector from 'Store/Selectors/createLanguageProfileSelector'; +import createQualityProfileSelector from 'Store/Selectors/createQualityProfileSelector'; +import ArtistEditorRow from './ArtistEditorRow'; + +function createMapStateToProps() { + return createSelector( + createLanguageProfileSelector(), + createQualityProfileSelector(), + (languageProfile, qualityProfile) => { + return { + languageProfile, + qualityProfile + }; + } + ); +} + +function ArtistEditorRowConnector(props) { + return ( + + ); +} + +ArtistEditorRowConnector.propTypes = { + qualityProfileId: PropTypes.number.isRequired +}; + +export default connect(createMapStateToProps)(ArtistEditorRowConnector); diff --git a/frontend/src/Artist/Editor/Delete/DeleteArtistModal.js b/frontend/src/Artist/Editor/Delete/DeleteArtistModal.js new file mode 100644 index 000000000..11fd79d5d --- /dev/null +++ b/frontend/src/Artist/Editor/Delete/DeleteArtistModal.js @@ -0,0 +1,31 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import Modal from 'Components/Modal/Modal'; +import DeleteArtistModalContentConnector from './DeleteArtistModalContentConnector'; + +function DeleteArtistModal(props) { + const { + isOpen, + onModalClose, + ...otherProps + } = props; + + return ( + + + + ); +} + +DeleteArtistModal.propTypes = { + isOpen: PropTypes.bool.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default DeleteArtistModal; diff --git a/frontend/src/Artist/Editor/Delete/DeleteArtistModalContent.css b/frontend/src/Artist/Editor/Delete/DeleteArtistModalContent.css new file mode 100644 index 000000000..950fdc27d --- /dev/null +++ b/frontend/src/Artist/Editor/Delete/DeleteArtistModalContent.css @@ -0,0 +1,13 @@ +.message { + margin-top: 20px; + margin-bottom: 10px; +} + +.pathContainer { + margin-left: 5px; +} + +.path { + margin-left: 5px; + color: $dangerColor; +} diff --git a/frontend/src/Artist/Editor/Delete/DeleteArtistModalContent.js b/frontend/src/Artist/Editor/Delete/DeleteArtistModalContent.js new file mode 100644 index 000000000..87088b472 --- /dev/null +++ b/frontend/src/Artist/Editor/Delete/DeleteArtistModalContent.js @@ -0,0 +1,123 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { inputTypes, kinds } from 'Helpers/Props'; +import FormGroup from 'Components/Form/FormGroup'; +import FormLabel from 'Components/Form/FormLabel'; +import FormInputGroup from 'Components/Form/FormInputGroup'; +import Button from 'Components/Link/Button'; +import ModalContent from 'Components/Modal/ModalContent'; +import ModalHeader from 'Components/Modal/ModalHeader'; +import ModalBody from 'Components/Modal/ModalBody'; +import ModalFooter from 'Components/Modal/ModalFooter'; +import styles from './DeleteArtistModalContent.css'; + +class DeleteArtistModalContent extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + deleteFiles: false + }; + } + + // + // Listeners + + onDeleteFilesChange = ({ value }) => { + this.setState({ deleteFiles: value }); + } + + onDeleteArtistConfirmed = () => { + const deleteFiles = this.state.deleteFiles; + + this.setState({ deleteFiles: false }); + this.props.onDeleteSelectedPress(deleteFiles); + } + + // + // Render + + render() { + const { + artist, + onModalClose + } = this.props; + const deleteFiles = this.state.deleteFiles; + + return ( + + + Delete Selected Artist + + + +
+ + {`Delete Artist Folder${artist.length > 1 ? 's' : ''}`} + + 1 ? 's' : ''} and all contents`} + kind={kinds.DANGER} + onChange={this.onDeleteFilesChange} + /> + +
+ +
+ {`Are you sure you want to delete ${artist.length} selected artist${artist.length > 1 ? 's' : ''}${deleteFiles ? ' and all contents' : ''}?`} +
+ +
    + { + artist.map((s) => { + return ( +
  • + {s.artistName} + + { + deleteFiles && + + - + + {s.path} + + + } +
  • + ); + }) + } +
+
+ + + + + + +
+ ); + } +} + +DeleteArtistModalContent.propTypes = { + artist: PropTypes.arrayOf(PropTypes.object).isRequired, + onModalClose: PropTypes.func.isRequired, + onDeleteSelectedPress: PropTypes.func.isRequired +}; + +export default DeleteArtistModalContent; diff --git a/frontend/src/Artist/Editor/Delete/DeleteArtistModalContentConnector.js b/frontend/src/Artist/Editor/Delete/DeleteArtistModalContentConnector.js new file mode 100644 index 000000000..8c61976e8 --- /dev/null +++ b/frontend/src/Artist/Editor/Delete/DeleteArtistModalContentConnector.js @@ -0,0 +1,45 @@ +import _ from 'lodash'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import createAllArtistSelector from 'Store/Selectors/createAllArtistSelector'; +import { bulkDeleteArtist } from 'Store/Actions/artistEditorActions'; +import DeleteArtistModalContent from './DeleteArtistModalContent'; + +function createMapStateToProps() { + return createSelector( + (state, { artistIds }) => artistIds, + createAllArtistSelector(), + (artistIds, allArtists) => { + const selectedArtist = _.intersectionWith(allArtists, artistIds, (s, id) => { + return s.id === id; + }); + + const sortedArtist = _.orderBy(selectedArtist, 'sortName'); + const artist = _.map(sortedArtist, (s) => { + return { + artistName: s.artistName, + path: s.path + }; + }); + + return { + artist + }; + } + ); +} + +function createMapDispatchToProps(dispatch, props) { + return { + onDeleteSelectedPress(deleteFiles) { + dispatch(bulkDeleteArtist({ + artistIds: props.artistIds, + deleteFiles + })); + + props.onModalClose(); + } + }; +} + +export default connect(createMapStateToProps, createMapDispatchToProps)(DeleteArtistModalContent); diff --git a/frontend/src/Artist/Editor/Organize/OrganizeArtistModal.js b/frontend/src/Artist/Editor/Organize/OrganizeArtistModal.js new file mode 100644 index 000000000..412396355 --- /dev/null +++ b/frontend/src/Artist/Editor/Organize/OrganizeArtistModal.js @@ -0,0 +1,31 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import Modal from 'Components/Modal/Modal'; +import OrganizeArtistModalContentConnector from './OrganizeArtistModalContentConnector'; + +function OrganizeArtistModal(props) { + const { + isOpen, + onModalClose, + ...otherProps + } = props; + + return ( + + + + ); +} + +OrganizeArtistModal.propTypes = { + isOpen: PropTypes.bool.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default OrganizeArtistModal; diff --git a/frontend/src/Artist/Editor/Organize/OrganizeArtistModalContent.css b/frontend/src/Artist/Editor/Organize/OrganizeArtistModalContent.css new file mode 100644 index 000000000..0b896f4ef --- /dev/null +++ b/frontend/src/Artist/Editor/Organize/OrganizeArtistModalContent.css @@ -0,0 +1,8 @@ +.renameIcon { + margin-left: 5px; +} + +.message { + margin-top: 20px; + margin-bottom: 10px; +} diff --git a/frontend/src/Artist/Editor/Organize/OrganizeArtistModalContent.js b/frontend/src/Artist/Editor/Organize/OrganizeArtistModalContent.js new file mode 100644 index 000000000..5f90eca90 --- /dev/null +++ b/frontend/src/Artist/Editor/Organize/OrganizeArtistModalContent.js @@ -0,0 +1,74 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import { icons, kinds } from 'Helpers/Props'; +import Alert from 'Components/Alert'; +import Button from 'Components/Link/Button'; +import Icon from 'Components/Icon'; +import ModalContent from 'Components/Modal/ModalContent'; +import ModalHeader from 'Components/Modal/ModalHeader'; +import ModalBody from 'Components/Modal/ModalBody'; +import ModalFooter from 'Components/Modal/ModalFooter'; +import styles from './OrganizeArtistModalContent.css'; + +function OrganizeArtistModalContent(props) { + const { + artistNames, + onModalClose, + onOrganizeArtistPress + } = props; + + return ( + + + Organize Selected Artist + + + + + Tip: To preview a rename... select "Cancel" then click any artist name and use the + + + +
+ Are you sure you want to organize all files in the {artistNames.length} selected artist? +
+ +
    + { + artistNames.map((artistName) => { + return ( +
  • + {artistName} +
  • + ); + }) + } +
+
+ + + + + + +
+ ); +} + +OrganizeArtistModalContent.propTypes = { + artistNames: PropTypes.arrayOf(PropTypes.string).isRequired, + onModalClose: PropTypes.func.isRequired, + onOrganizeArtistPress: PropTypes.func.isRequired +}; + +export default OrganizeArtistModalContent; diff --git a/frontend/src/Artist/Editor/Organize/OrganizeArtistModalContentConnector.js b/frontend/src/Artist/Editor/Organize/OrganizeArtistModalContentConnector.js new file mode 100644 index 000000000..6be1eb961 --- /dev/null +++ b/frontend/src/Artist/Editor/Organize/OrganizeArtistModalContentConnector.js @@ -0,0 +1,67 @@ +import _ from 'lodash'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import createAllArtistSelector from 'Store/Selectors/createAllArtistSelector'; +import { executeCommand } from 'Store/Actions/commandActions'; +import * as commandNames from 'Commands/commandNames'; +import OrganizeArtistModalContent from './OrganizeArtistModalContent'; + +function createMapStateToProps() { + return createSelector( + (state, { artistIds }) => artistIds, + createAllArtistSelector(), + (artistIds, allArtists) => { + const artist = _.intersectionWith(allArtists, artistIds, (s, id) => { + return s.id === id; + }); + + const sortedArtist = _.orderBy(artist, 'sortName'); + const artistNames = _.map(sortedArtist, 'artistName'); + + return { + artistNames + }; + } + ); +} + +const mapDispatchToProps = { + executeCommand +}; + +class OrganizeArtistModalContentConnector extends Component { + + // + // Listeners + + onOrganizeArtistPress = () => { + this.props.executeCommand({ + name: commandNames.RENAME_ARTIST, + artistIds: this.props.artistIds + }); + + this.props.onModalClose(true); + } + + // + // Render + + render(props) { + return ( + + ); + } +} + +OrganizeArtistModalContentConnector.propTypes = { + artistIds: PropTypes.arrayOf(PropTypes.number).isRequired, + onModalClose: PropTypes.func.isRequired, + executeCommand: PropTypes.func.isRequired +}; + +export default connect(createMapStateToProps, mapDispatchToProps)(OrganizeArtistModalContentConnector); diff --git a/frontend/src/Artist/Editor/Tags/TagsModal.js b/frontend/src/Artist/Editor/Tags/TagsModal.js new file mode 100644 index 000000000..0f6c2d7ec --- /dev/null +++ b/frontend/src/Artist/Editor/Tags/TagsModal.js @@ -0,0 +1,31 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import Modal from 'Components/Modal/Modal'; +import TagsModalContentConnector from './TagsModalContentConnector'; + +function TagsModal(props) { + const { + isOpen, + onModalClose, + ...otherProps + } = props; + + return ( + + + + ); +} + +TagsModal.propTypes = { + isOpen: PropTypes.bool.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default TagsModal; diff --git a/frontend/src/Artist/Editor/Tags/TagsModalContent.css b/frontend/src/Artist/Editor/Tags/TagsModalContent.css new file mode 100644 index 000000000..63be9aadd --- /dev/null +++ b/frontend/src/Artist/Editor/Tags/TagsModalContent.css @@ -0,0 +1,12 @@ +.renameIcon { + margin-left: 5px; +} + +.message { + margin-top: 20px; + margin-bottom: 10px; +} + +.result { + padding-top: 4px; +} diff --git a/frontend/src/Artist/Editor/Tags/TagsModalContent.js b/frontend/src/Artist/Editor/Tags/TagsModalContent.js new file mode 100644 index 000000000..b982fee0e --- /dev/null +++ b/frontend/src/Artist/Editor/Tags/TagsModalContent.js @@ -0,0 +1,187 @@ +import _ from 'lodash'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { inputTypes, kinds, sizes } from 'Helpers/Props'; +import Label from 'Components/Label'; +import Button from 'Components/Link/Button'; +import ModalContent from 'Components/Modal/ModalContent'; +import ModalHeader from 'Components/Modal/ModalHeader'; +import ModalBody from 'Components/Modal/ModalBody'; +import ModalFooter from 'Components/Modal/ModalFooter'; +import Form from 'Components/Form/Form'; +import FormGroup from 'Components/Form/FormGroup'; +import FormLabel from 'Components/Form/FormLabel'; +import FormInputGroup from 'Components/Form/FormInputGroup'; +import styles from './TagsModalContent.css'; + +class TagsModalContent extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + tags: [], + applyTags: 'add' + }; + } + + // + // Lifecycle + + onInputChange = ({ name, value }) => { + this.setState({ [name]: value }); + } + + onApplyTagsPress = () => { + const { + tags, + applyTags + } = this.state; + + this.props.onApplyTagsPress(tags, applyTags); + } + + // + // Render + + render() { + const { + artistTags, + tagList, + onModalClose + } = this.props; + + const { + tags, + applyTags + } = this.state; + + const applyTagsOptions = [ + { key: 'add', value: 'Add' }, + { key: 'remove', value: 'Remove' }, + { key: 'replace', value: 'Replace' } + ]; + + return ( + + + Tags + + + +
+ + Tags + + + + + + Apply Tags + + + + + + Result + +
+ { + artistTags.map((t) => { + const tag = _.find(tagList, { id: t }); + + if (!tag) { + return null; + } + + const removeTag = (applyTags === 'remove' && tags.indexOf(t) > -1) || + (applyTags === 'replace' && tags.indexOf(t) === -1); + + return ( + + ); + }) + } + + { + (applyTags === 'add' || applyTags === 'replace') && + tags.map((t) => { + const tag = _.find(tagList, { id: t }); + + if (!tag) { + return null; + } + + if (artistTags.indexOf(t) > -1) { + return null; + } + + return ( + + ); + }) + } +
+
+
+
+ + + + + + +
+ ); + } +} + +TagsModalContent.propTypes = { + artistTags: PropTypes.arrayOf(PropTypes.number).isRequired, + tagList: PropTypes.arrayOf(PropTypes.object).isRequired, + onModalClose: PropTypes.func.isRequired, + onApplyTagsPress: PropTypes.func.isRequired +}; + +export default TagsModalContent; diff --git a/frontend/src/Artist/Editor/Tags/TagsModalContentConnector.js b/frontend/src/Artist/Editor/Tags/TagsModalContentConnector.js new file mode 100644 index 000000000..6741e8b5c --- /dev/null +++ b/frontend/src/Artist/Editor/Tags/TagsModalContentConnector.js @@ -0,0 +1,36 @@ +import _ from 'lodash'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import createAllArtistSelector from 'Store/Selectors/createAllArtistSelector'; +import createTagsSelector from 'Store/Selectors/createTagsSelector'; +import TagsModalContent from './TagsModalContent'; + +function createMapStateToProps() { + return createSelector( + (state, { artistIds }) => artistIds, + createAllArtistSelector(), + createTagsSelector(), + (artistIds, allArtists, tagList) => { + const artist = _.intersectionWith(allArtists, artistIds, (s, id) => { + return s.id === id; + }); + + const artistTags = _.uniq(_.concat(..._.map(artist, 'tags'))); + + return { + artistTags, + tagList + }; + } + ); +} + +function createMapDispatchToProps(dispatch, props) { + return { + onAction() { + // Do something + } + }; +} + +export default connect(createMapStateToProps, createMapDispatchToProps)(TagsModalContent); diff --git a/frontend/src/Artist/Index/ArtistIndex.css b/frontend/src/Artist/Index/ArtistIndex.css new file mode 100644 index 000000000..9e67ef538 --- /dev/null +++ b/frontend/src/Artist/Index/ArtistIndex.css @@ -0,0 +1,66 @@ +.pageContentBodyWrapper { + display: flex; + flex: 1 0 1px; + overflow: hidden; +} + +.contentBody { + composes: contentBody from 'Components/Page/PageContentBody.css'; + + display: flex; + flex-direction: column; +} + +.postersInnerContentBody { + composes: innerContentBody from 'Components/Page/PageContentBody.css'; + + display: flex; + flex-direction: column; + flex-grow: 1; + + /* 5px less padding than normal to handle poster's 5px margin */ + padding: calc($pageContentBodyPadding - 5px); +} + +.bannersInnerContentBody { + composes: innerContentBody from 'Components/Page/PageContentBody.css'; + + display: flex; + flex-direction: column; + flex-grow: 1; + + /* 5px less padding than normal to handle poster's 5px margin */ + padding: calc($pageContentBodyPadding - 5px); +} + +.tableInnerContentBody { + composes: innerContentBody from 'Components/Page/PageContentBody.css'; + + display: flex; + flex-direction: column; + flex-grow: 1; +} + +.contentBodyContainer { + display: flex; + flex-direction: column; + flex-grow: 1; +} + +@media only screen and (max-width: $breakpointSmall) { + .pageContentBodyWrapper { + flex-basis: auto; + } + + .contentBody { + flex-basis: 1px; + } + + .postersInnerContentBody { + padding: calc($pageContentBodyPaddingSmallScreen - 5px); + } + + .bannersInnerContentBody { + padding: calc($pageContentBodyPaddingSmallScreen - 5px); + } +} diff --git a/frontend/src/Artist/Index/ArtistIndex.js b/frontend/src/Artist/Index/ArtistIndex.js new file mode 100644 index 000000000..827fe2499 --- /dev/null +++ b/frontend/src/Artist/Index/ArtistIndex.js @@ -0,0 +1,389 @@ +import _ from 'lodash'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import hasDifferentItems from 'Utilities/Object/hasDifferentItems'; +import { align, icons, sortDirections } from 'Helpers/Props'; +import LoadingIndicator from 'Components/Loading/LoadingIndicator'; +import PageContent from 'Components/Page/PageContent'; +import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector'; +import PageJumpBar from 'Components/Page/PageJumpBar'; +import PageToolbar from 'Components/Page/Toolbar/PageToolbar'; +import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator'; +import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection'; +import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton'; +import NoArtist from 'Artist/NoArtist'; +import ArtistIndexTableConnector from './Table/ArtistIndexTableConnector'; +import ArtistIndexPosterOptionsModal from './Posters/Options/ArtistIndexPosterOptionsModal'; +import ArtistIndexPostersConnector from './Posters/ArtistIndexPostersConnector'; +import ArtistIndexBannerOptionsModal from './Banners/Options/ArtistIndexBannerOptionsModal'; +import ArtistIndexBannersConnector from './Banners/ArtistIndexBannersConnector'; +import ArtistIndexOverviewOptionsModal from './Overview/Options/ArtistIndexOverviewOptionsModal'; +import ArtistIndexOverviewsConnector from './Overview/ArtistIndexOverviewsConnector'; +import ArtistIndexFooter from './ArtistIndexFooter'; +import ArtistIndexFilterMenu from './Menus/ArtistIndexFilterMenu'; +import ArtistIndexSortMenu from './Menus/ArtistIndexSortMenu'; +import ArtistIndexViewMenu from './Menus/ArtistIndexViewMenu'; +import styles from './ArtistIndex.css'; + +function getViewComponent(view) { + if (view === 'posters') { + return ArtistIndexPostersConnector; + } + + if (view === 'banners') { + return ArtistIndexBannersConnector; + } + + if (view === 'overview') { + return ArtistIndexOverviewsConnector; + } + + return ArtistIndexTableConnector; +} + +class ArtistIndex extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this._viewComponent = null; + + this.state = { + contentBody: null, + jumpBarItems: [], + isPosterOptionsModalOpen: false, + isBannerOptionsModalOpen: false, + isOverviewOptionsModalOpen: false, + isRendered: false + }; + } + + componentDidMount() { + this.setJumpBarItems(); + } + + componentDidUpdate(prevProps) { + const { + items, + sortKey, + sortDirection + } = this.props; + + if ( + hasDifferentItems(prevProps.items, items) || + sortKey !== prevProps.sortKey || + sortDirection !== prevProps.sortDirection + ) { + this.setJumpBarItems(); + } + } + + // + // Control + + setContentBodyRef = (ref) => { + this.setState({ contentBody: ref }); + } + + setViewComponentRef = (ref) => { + this._viewComponent = ref; + } + + setJumpBarItems() { + const { + items, + sortKey, + sortDirection + } = this.props; + + // Reset if not sorting by sortName + if (sortKey !== 'sortName') { + this.setState({ jumpBarItems: [] }); + return; + } + + const characters = _.reduce(items, (acc, item) => { + const firstCharacter = item.sortName.charAt(0); + + if (isNaN(firstCharacter)) { + acc.push(firstCharacter); + } else { + acc.push('#'); + } + + return acc; + }, []).sort(); + + // Reverse if sorting descending + if (sortDirection === sortDirections.DESCENDING) { + characters.reverse(); + } + + this.setState({ jumpBarItems: _.sortedUniq(characters) }); + } + + // + // Listeners + + onPosterOptionsPress = () => { + this.setState({ isPosterOptionsModalOpen: true }); + } + + onPosterOptionsModalClose = () => { + this.setState({ isPosterOptionsModalOpen: false }); + } + + onBannerOptionsPress = () => { + this.setState({ isBannerOptionsModalOpen: true }); + } + + onBannerOptionsModalClose = () => { + this.setState({ isBannerOptionsModalOpen: false }); + } + + onOverviewOptionsPress = () => { + this.setState({ isOverviewOptionsModalOpen: true }); + } + + onOverviewOptionsModalClose = () => { + this.setState({ isOverviewOptionsModalOpen: false }); + } + + onJumpBarItemPress = (item) => { + const viewComponent = this._viewComponent.getWrappedInstance(); + viewComponent.scrollToFirstCharacter(item); + } + + onRender = () => { + this.setState({ isRendered: true }, () => { + const { + scrollTop, + isSmallScreen + } = this.props; + + if (isSmallScreen) { + // Seems to result in the view being off by 125px (distance to the top of the page) + // document.documentElement.scrollTop = document.body.scrollTop = scrollTop; + + // This works, but then jumps another 1px after scrolling + document.documentElement.scrollTop = scrollTop; + } + }); + } + + onScroll = ({ scrollTop }) => { + this.props.onScroll({ scrollTop }); + } + + // + // Render + + render() { + const { + isFetching, + isPopulated, + error, + items, + filterKey, + filterValue, + sortKey, + sortDirection, + view, + isRefreshingArtist, + isRssSyncExecuting, + scrollTop, + onSortSelect, + onFilterSelect, + onViewSelect, + onRefreshArtistPress, + onRssSyncPress, + ...otherProps + } = this.props; + + const { + contentBody, + jumpBarItems, + isPosterOptionsModalOpen, + isBannerOptionsModalOpen, + isOverviewOptionsModalOpen, + isRendered + } = this.state; + + const ViewComponent = getViewComponent(view); + const isLoaded = !error && isPopulated && !!items.length && contentBody; + + return ( + + + + + + + + + + + + { + view === 'posters' && + + } + + { + view === 'banners' && + + } + + { + view === 'overview' && + + } + + { + (view === 'posters' || view === 'banners' || view === 'overview') && + + + } + + + + + + + + + +
+ + { + isFetching && !isPopulated && + + } + + { + !isFetching && !!error && +
Unable to load artist
+ } + + { + isLoaded && +
+ + + +
+ } + + { + !error && isPopulated && !items.length && + + } +
+ + { + isLoaded && !!jumpBarItems.length && + + } +
+ + + + + + +
+ ); + } +} + +ArtistIndex.propTypes = { + isFetching: PropTypes.bool.isRequired, + isPopulated: PropTypes.bool.isRequired, + error: PropTypes.object, + items: PropTypes.arrayOf(PropTypes.object).isRequired, + filterKey: PropTypes.string, + filterValue: PropTypes.oneOfType([PropTypes.bool, PropTypes.number, PropTypes.string]), + sortKey: PropTypes.string, + sortDirection: PropTypes.oneOf(sortDirections.all), + view: PropTypes.string.isRequired, + isRefreshingArtist: PropTypes.bool.isRequired, + isRssSyncExecuting: PropTypes.bool.isRequired, + scrollTop: PropTypes.number.isRequired, + isSmallScreen: PropTypes.bool.isRequired, + onSortSelect: PropTypes.func.isRequired, + onFilterSelect: PropTypes.func.isRequired, + onViewSelect: PropTypes.func.isRequired, + onRefreshArtistPress: PropTypes.func.isRequired, + onRssSyncPress: PropTypes.func.isRequired, + onScroll: PropTypes.func.isRequired +}; + +export default ArtistIndex; diff --git a/frontend/src/Artist/Index/ArtistIndexConnector.js b/frontend/src/Artist/Index/ArtistIndexConnector.js new file mode 100644 index 000000000..688852942 --- /dev/null +++ b/frontend/src/Artist/Index/ArtistIndexConnector.js @@ -0,0 +1,166 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import dimensions from 'Styles/Variables/dimensions'; +import createCommandSelector from 'Store/Selectors/createCommandSelector'; +import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector'; +import { fetchArtist } from 'Store/Actions/artistActions'; +import scrollPositions from 'Store/scrollPositions'; +import { setArtistSort, setArtistFilter, setArtistView } from 'Store/Actions/artistIndexActions'; +import { executeCommand } from 'Store/Actions/commandActions'; +import * as commandNames from 'Commands/commandNames'; +import withScrollPosition from 'Components/withScrollPosition'; +import ArtistIndex from './ArtistIndex'; + +const POSTERS_PADDING = 15; +const POSTERS_PADDING_SMALL_SCREEN = 5; +const BANNERS_PADDING = 15; +const BANNERS_PADDING_SMALL_SCREEN = 5; +const TABLE_PADDING = parseInt(dimensions.pageContentBodyPadding); +const TABLE_PADDING_SMALL_SCREEN = parseInt(dimensions.pageContentBodyPaddingSmallScreen); + +// If the scrollTop is greater than zero it needs to be offset +// by the padding so when it is set initially so it is correct +// after React Virtualized takes the padding into account. + +function getScrollTop(view, scrollTop, isSmallScreen) { + if (scrollTop === 0) { + return 0; + } + + let padding = isSmallScreen ? TABLE_PADDING_SMALL_SCREEN : TABLE_PADDING; + + if (view === 'posters') { + padding = isSmallScreen ? POSTERS_PADDING_SMALL_SCREEN : POSTERS_PADDING; + } + + if (view === 'banners') { + padding = isSmallScreen ? BANNERS_PADDING_SMALL_SCREEN : BANNERS_PADDING; + } + + return scrollTop + padding; +} + +function createMapStateToProps() { + return createSelector( + (state) => state.artist, + (state) => state.artistIndex, + createCommandSelector(commandNames.REFRESH_ARTIST), + createCommandSelector(commandNames.RSS_SYNC), + createDimensionsSelector(), + (artist, artistIndex, isRefreshingArtist, isRssSyncExecuting, dimensionsState) => { + return { + isRefreshingArtist, + isRssSyncExecuting, + isSmallScreen: dimensionsState.isSmallScreen, + ...artist, + ...artistIndex + }; + } + ); +} + +const mapDispatchToProps = { + fetchArtist, + setArtistSort, + setArtistFilter, + setArtistView, + executeCommand +}; + +class ArtistIndexConnector extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + const { + view, + scrollTop, + isSmallScreen + } = props; + + this.state = { + scrollTop: getScrollTop(view, scrollTop, isSmallScreen) + }; + } + + componentDidMount() { + this.props.fetchArtist(); + } + + // + // Listeners + + onSortSelect = (sortKey) => { + this.props.setArtistSort({ sortKey }); + } + + onFilterSelect = (filterKey, filterValue, filterType) => { + this.props.setArtistFilter({ filterKey, filterValue, filterType }); + } + + onViewSelect = (view) => { + // Reset the scroll position before changing the view + this.setState({ scrollTop: 0 }, () => { + this.props.setArtistView({ view }); + }); + } + + onScroll = ({ scrollTop }) => { + this.setState({ + scrollTop + }, () => { + scrollPositions.artistIndex = scrollTop; + }); + } + + onRefreshArtistPress = () => { + this.props.executeCommand({ + name: commandNames.REFRESH_ARTIST + }); + } + + onRssSyncPress = () => { + this.props.executeCommand({ + name: commandNames.RSS_SYNC + }); + } + + // + // Render + + render() { + return ( + + ); + } +} + +ArtistIndexConnector.propTypes = { + isSmallScreen: PropTypes.bool.isRequired, + view: PropTypes.string.isRequired, + scrollTop: PropTypes.number.isRequired, + fetchArtist: PropTypes.func.isRequired, + setArtistSort: PropTypes.func.isRequired, + setArtistFilter: PropTypes.func.isRequired, + setArtistView: PropTypes.func.isRequired, + executeCommand: PropTypes.func.isRequired +}; + +export default withScrollPosition( + connect(createMapStateToProps, mapDispatchToProps)(ArtistIndexConnector), + 'artistIndex' +); diff --git a/frontend/src/Artist/Index/ArtistIndexFooter.css b/frontend/src/Artist/Index/ArtistIndexFooter.css new file mode 100644 index 000000000..3aa369576 --- /dev/null +++ b/frontend/src/Artist/Index/ArtistIndexFooter.css @@ -0,0 +1,66 @@ +.footer { + display: flex; + flex-wrap: wrap; + margin-top: 20px; + font-size: $smallFontSize; +} + +.legendItem { + display: flex; + margin-bottom: 4px; + line-height: 16px; +} + +.legendItemColor { + margin-right: 8px; + width: 30px; + height: 16px; + border-radius: 4px; +} + +.continuing { + composes: legendItemColor; + + background-color: $primaryColor; +} + +.ended { + composes: legendItemColor; + + background-color: $successColor; +} + +.missingMonitored { + composes: legendItemColor; + + background-color: $dangerColor; +} + +.missingUnmonitored { + composes: legendItemColor; + + background-color: $warningColor; +} + +.statistics { + display: flex; + justify-content: space-between; + flex-wrap: wrap; +} + +@media (max-width: $breakpointLarge) { + .statistics { + display: block; + } +} + +@media (max-width: $breakpointSmall) { + .footer { + display: block; + } + + .statistics { + display: flex; + margin-top: 20px; + } +} diff --git a/frontend/src/Artist/Index/ArtistIndexFooter.js b/frontend/src/Artist/Index/ArtistIndexFooter.js new file mode 100644 index 000000000..c7610a079 --- /dev/null +++ b/frontend/src/Artist/Index/ArtistIndexFooter.js @@ -0,0 +1,104 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import DescriptionList from 'Components/DescriptionList/DescriptionList'; +import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem'; +import styles from './ArtistIndexFooter.css'; + +function ArtistIndexFooter({ artist }) { + const count = artist.length; + let tracks = 0; + let trackFiles = 0; + let ended = 0; + let continuing = 0; + let monitored = 0; + + artist.forEach((s) => { + tracks += s.trackCount || 0; + trackFiles += s.trackFileCount || 0; + + if (s.status === 'ended') { + ended++; + } else { + continuing++; + } + + if (s.monitored) { + monitored++; + } + }); + + return ( +
+
+
+
+
Continuing (All tracks downloaded)
+
+ +
+
+
Ended (All tracks downloaded)
+
+ +
+
+
Missing Tracks (Artist monitored)
+
+ +
+
+
Missing Tracks (Artist not monitored)
+
+
+ +
+ + + + + + + + + + + + + + + + + + + +
+
+ ); +} + +ArtistIndexFooter.propTypes = { + artist: PropTypes.arrayOf(PropTypes.object).isRequired +}; + +export default ArtistIndexFooter; diff --git a/frontend/src/Artist/Index/ArtistIndexItemConnector.js b/frontend/src/Artist/Index/ArtistIndexItemConnector.js new file mode 100644 index 000000000..216c3fbbe --- /dev/null +++ b/frontend/src/Artist/Index/ArtistIndexItemConnector.js @@ -0,0 +1,77 @@ +import _ from 'lodash'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import createCommandsSelector from 'Store/Selectors/createCommandsSelector'; +import createQualityProfileSelector from 'Store/Selectors/createQualityProfileSelector'; +import createLanguageProfileSelector from 'Store/Selectors/createLanguageProfileSelector'; +import { executeCommand } from 'Store/Actions/commandActions'; +import * as commandNames from 'Commands/commandNames'; + +function createMapStateToProps() { + return createSelector( + (state, { id }) => id, + (state, { albums }) => albums, + createQualityProfileSelector(), + createLanguageProfileSelector(), + createCommandsSelector(), + (artistId, albums, qualityProfile, languageProfile, commands) => { + const isRefreshingArtist = _.some(commands, (command) => { + return command.name === commandNames.REFRESH_ARTIST && + command.body.artistId === artistId; + }); + + const latestAlbum = _.first(_.orderBy(albums, 'releaseDate', 'desc')); + + return { + qualityProfile, + languageProfile, + latestAlbum, + isRefreshingArtist + }; + } + ); +} + +const mapDispatchToProps = { + executeCommand +}; + +class ArtistIndexItemConnector extends Component { + + // + // Listeners + + onRefreshArtistPress = () => { + this.props.executeCommand({ + name: commandNames.REFRESH_ARTIST, + artistId: this.props.id + }); + } + + // + // Render + + render() { + const { + component: ItemComponent, + ...otherProps + } = this.props; + + return ( + + ); + } +} + +ArtistIndexItemConnector.propTypes = { + id: PropTypes.number.isRequired, + component: PropTypes.func.isRequired, + executeCommand: PropTypes.func.isRequired +}; + +export default connect(createMapStateToProps, mapDispatchToProps)(ArtistIndexItemConnector); diff --git a/frontend/src/Artist/Index/ArtistIndexPage.js b/frontend/src/Artist/Index/ArtistIndexPage.js new file mode 100644 index 000000000..e69de29bb diff --git a/frontend/src/Artist/Index/Banners/ArtistIndexBanner.css b/frontend/src/Artist/Index/Banners/ArtistIndexBanner.css new file mode 100644 index 000000000..a80a4c2f0 --- /dev/null +++ b/frontend/src/Artist/Index/Banners/ArtistIndexBanner.css @@ -0,0 +1,85 @@ +$hoverScale: 1.05; + +.container { + padding: 10px; +} + +.content { + transition: all 200ms ease-in; + + &:hover { + z-index: 2; + box-shadow: 0 0 12px $black; + transition: all 200ms ease-in; + + .controls { + opacity: 0.9; + transition: opacity 200ms linear 150ms; + } + } +} + +.bannerContainer { + position: relative; +} + +.link { + composes: link from 'Components/Link/Link.css'; + + display: block; + background-color: $defaultColor; +} + +.nextAiring { + background-color: $defaultColor; + color: $white; + text-align: center; + font-size: $smallFontSize; +} + +.title { + @add-mixin truncate; + + background-color: $defaultColor; + color: $white; + text-align: center; + font-size: $smallFontSize; +} + +.ended { + position: absolute; + top: 0; + right: 0; + width: 0; + height: 0; + border-width: 0 25px 25px 0; + border-style: solid; + border-color: transparent $dangerColor transparent transparent; + color: $white; +} + +.controls { + position: absolute; + bottom: 10px; + left: 10px; + border-radius: 4px; + background-color: #216044; + color: $white; + font-size: $smallFontSize; + opacity: 0; + transition: opacity 0; +} + +.action { + composes: button from 'Components/Link/IconButton.css'; + + &:hover { + color: #ccc; + } +} + +@media only screen and (max-width: $breakpointSmall) { + .container { + padding: 5px; + } +} diff --git a/frontend/src/Artist/Index/Banners/ArtistIndexBanner.js b/frontend/src/Artist/Index/Banners/ArtistIndexBanner.js new file mode 100644 index 000000000..45858ba59 --- /dev/null +++ b/frontend/src/Artist/Index/Banners/ArtistIndexBanner.js @@ -0,0 +1,231 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import getRelativeDate from 'Utilities/Date/getRelativeDate'; +import { icons } from 'Helpers/Props'; +import IconButton from 'Components/Link/IconButton'; +import SpinnerIconButton from 'Components/Link/SpinnerIconButton'; +import Label from 'Components/Label'; +import Link from 'Components/Link/Link'; +import ArtistBanner from 'Artist/ArtistBanner'; +import EditArtistModalConnector from 'Artist/Edit/EditArtistModalConnector'; +import DeleteArtistModal from 'Artist/Delete/DeleteArtistModal'; +import ArtistIndexProgressBar from 'Artist/Index/ProgressBar/ArtistIndexProgressBar'; +import ArtistIndexBannerInfo from './ArtistIndexBannerInfo'; +import styles from './ArtistIndexBanner.css'; + +class ArtistIndexBanner extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + isEditArtistModalOpen: false, + isDeleteArtistModalOpen: false + }; + } + + // + // Listeners + + onEditArtistPress = () => { + this.setState({ isEditArtistModalOpen: true }); + } + + onEditArtistModalClose = () => { + this.setState({ isEditArtistModalOpen: false }); + } + + onDeleteArtistPress = () => { + this.setState({ + isEditArtistModalOpen: false, + isDeleteArtistModalOpen: true + }); + } + + onDeleteArtistModalClose = () => { + this.setState({ isDeleteArtistModalOpen: false }); + } + + // + // Render + + render() { + const { + style, + id, + artistName, + monitored, + status, + nameSlug, + nextAiring, + trackCount, + trackFileCount, + images, + bannerWidth, + bannerHeight, + detailedProgressBar, + showTitle, + showQualityProfile, + qualityProfile, + showRelativeDates, + shortDateFormat, + timeFormat, + isRefreshingArtist, + onRefreshArtistPress, + ...otherProps + } = this.props; + + const { + isEditArtistModalOpen, + isDeleteArtistModalOpen + } = this.state; + + const link = `/artist/${nameSlug}`; + + const elementStyle = { + width: `${bannerWidth}px`, + height: `${bannerHeight}px` + }; + + return ( +
+
+
+ + + { + status === 'ended' && +
+ } + + + + +
+ + + + { + showTitle && +
+ {artistName} +
+ } + + { + showQualityProfile && +
+ {qualityProfile.name} +
+ } + +
+ { + getRelativeDate( + nextAiring, + shortDateFormat, + showRelativeDates, + { + timeFormat, + timeForToday: true + } + ) + } +
+ + + + + + +
+
+ ); + } +} + +ArtistIndexBanner.propTypes = { + style: PropTypes.object.isRequired, + id: PropTypes.number.isRequired, + artistName: PropTypes.string.isRequired, + monitored: PropTypes.bool.isRequired, + status: PropTypes.string.isRequired, + nameSlug: PropTypes.string.isRequired, + nextAiring: PropTypes.string, + trackCount: PropTypes.number, + trackFileCount: PropTypes.number, + images: PropTypes.arrayOf(PropTypes.object).isRequired, + bannerWidth: PropTypes.number.isRequired, + bannerHeight: PropTypes.number.isRequired, + detailedProgressBar: PropTypes.bool.isRequired, + showTitle: PropTypes.bool.isRequired, + showQualityProfile: PropTypes.bool.isRequired, + qualityProfile: PropTypes.object.isRequired, + showRelativeDates: PropTypes.bool.isRequired, + shortDateFormat: PropTypes.string.isRequired, + timeFormat: PropTypes.string.isRequired, + isRefreshingArtist: PropTypes.bool.isRequired, + onRefreshArtistPress: PropTypes.func.isRequired +}; + +ArtistIndexBanner.defaultProps = { + trackCount: 0, + trackFileCount: 0 +}; + +export default ArtistIndexBanner; diff --git a/frontend/src/Artist/Index/Banners/ArtistIndexBannerInfo.css b/frontend/src/Artist/Index/Banners/ArtistIndexBannerInfo.css new file mode 100644 index 000000000..cab3dec61 --- /dev/null +++ b/frontend/src/Artist/Index/Banners/ArtistIndexBannerInfo.css @@ -0,0 +1,6 @@ +.info { + background-color: $defaultColor; + color: $white; + text-align: center; + font-size: $smallFontSize; +} diff --git a/frontend/src/Artist/Index/Banners/ArtistIndexBannerInfo.js b/frontend/src/Artist/Index/Banners/ArtistIndexBannerInfo.js new file mode 100644 index 000000000..f641de0e1 --- /dev/null +++ b/frontend/src/Artist/Index/Banners/ArtistIndexBannerInfo.js @@ -0,0 +1,115 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import getRelativeDate from 'Utilities/Date/getRelativeDate'; +import formatBytes from 'Utilities/Number/formatBytes'; +import styles from './ArtistIndexBannerInfo.css'; + +function ArtistIndexBannerInfo(props) { + const { + qualityProfile, + showQualityProfile, + previousAiring, + added, + albumCount, + path, + sizeOnDisk, + sortKey, + showRelativeDates, + shortDateFormat, + timeFormat + } = props; + + if (sortKey === 'qualityProfileId' && !showQualityProfile) { + return ( +
+ {qualityProfile.name} +
+ ); + } + + if (sortKey === 'previousAiring' && previousAiring) { + return ( +
+ { + getRelativeDate( + previousAiring, + shortDateFormat, + showRelativeDates, + { + timeFormat, + timeForToday: true + } + ) + } +
+ ); + } + + if (sortKey === 'added' && added) { + const addedDate = getRelativeDate( + added, + shortDateFormat, + showRelativeDates, + { + timeFormat, + timeForToday: false + } + ); + + return ( +
+ {`Added ${addedDate}`} +
+ ); + } + + if (sortKey === 'albumCount') { + let albums = '1 album'; + + if (albumCount === 0) { + albums = 'No albums'; + } else if (albumCount > 1) { + albums = `${albumCount} albums`; + } + + return ( +
+ {albums} +
+ ); + } + + if (sortKey === 'path') { + return ( +
+ {path} +
+ ); + } + + if (sortKey === 'sizeOnDisk') { + return ( +
+ {formatBytes(sizeOnDisk)} +
+ ); + } + + return null; +} + +ArtistIndexBannerInfo.propTypes = { + qualityProfile: PropTypes.object.isRequired, + showQualityProfile: PropTypes.bool.isRequired, + previousAiring: PropTypes.string, + added: PropTypes.string, + albumCount: PropTypes.number.isRequired, + path: PropTypes.string.isRequired, + sizeOnDisk: PropTypes.number, + sortKey: PropTypes.string.isRequired, + showRelativeDates: PropTypes.bool.isRequired, + shortDateFormat: PropTypes.string.isRequired, + timeFormat: PropTypes.string.isRequired +}; + +export default ArtistIndexBannerInfo; diff --git a/frontend/src/Artist/Index/Banners/ArtistIndexBanners.css b/frontend/src/Artist/Index/Banners/ArtistIndexBanners.css new file mode 100644 index 000000000..9c6520fb5 --- /dev/null +++ b/frontend/src/Artist/Index/Banners/ArtistIndexBanners.css @@ -0,0 +1,3 @@ +.grid { + flex: 1 0 auto; +} diff --git a/frontend/src/Artist/Index/Banners/ArtistIndexBanners.js b/frontend/src/Artist/Index/Banners/ArtistIndexBanners.js new file mode 100644 index 000000000..986632ab4 --- /dev/null +++ b/frontend/src/Artist/Index/Banners/ArtistIndexBanners.js @@ -0,0 +1,327 @@ +import _ from 'lodash'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import ReactDOM from 'react-dom'; +import Measure from 'react-measure'; +import { Grid, WindowScroller } from 'react-virtualized'; +import hasDifferentItems from 'Utilities/Object/hasDifferentItems'; +import dimensions from 'Styles/Variables/dimensions'; +import { sortDirections } from 'Helpers/Props'; +import ArtistIndexItemConnector from 'Artist/Index/ArtistIndexItemConnector'; +import ArtistIndexBanner from './ArtistIndexBanner'; +import styles from './ArtistIndexBanners.css'; + +// container dimensions +const columnPadding = parseInt(dimensions.artistIndexColumnPadding); +const columnPaddingSmallScreen = parseInt(dimensions.artistIndexColumnPaddingSmallScreen); +const progressBarHeight = parseInt(dimensions.progressBarSmallHeight); +const detailedProgressBarHeight = parseInt(dimensions.progressBarMediumHeight); + +const additionalColumnCount = { + small: 3, + medium: 2, + large: 1 +}; + +function calculateColumnWidth(width, bannerSize, isSmallScreen) { + const maxiumColumnWidth = isSmallScreen ? 344 : 364; + const columns = Math.floor(width / maxiumColumnWidth); + const remainder = width % maxiumColumnWidth; + + if (remainder === 0 && bannerSize === 'large') { + return maxiumColumnWidth; + } + + return Math.floor(width / (columns + additionalColumnCount[bannerSize])); +} + +function calculateRowHeight(bannerHeight, sortKey, isSmallScreen, bannerOptions) { + const { + detailedProgressBar, + showTitle, + showQualityProfile + } = bannerOptions; + + const nextAiringHeight = 19; + + const heights = [ + bannerHeight, + detailedProgressBar ? detailedProgressBarHeight : progressBarHeight, + nextAiringHeight, + isSmallScreen ? columnPaddingSmallScreen : columnPadding + ]; + + if (showTitle) { + heights.push(19); + } + + if (showQualityProfile) { + heights.push(19); + } + + switch (sortKey) { + case 'seasons': + case 'previousAiring': + case 'added': + case 'path': + case 'sizeOnDisk': + heights.push(19); + break; + case 'qualityProfileId': + if (!showQualityProfile) { + heights.push(19); + } + break; + default: + // No need to add a height of 0 + } + + return heights.reduce((acc, height) => acc + height, 0); +} + +function calculateHeight(bannerWidth) { + return Math.ceil((88/476) * bannerWidth); +} + +class ArtistIndexBanners extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + width: 0, + columnWidth: 364, + columnCount: 1, + bannerWidth: 476, + bannerHeight: 88, + rowHeight: calculateRowHeight(88, null, props.isSmallScreen, {}) + }; + + this._isInitialized = false; + this._grid = null; + } + + componentDidMount() { + this._contentBodyNode = ReactDOM.findDOMNode(this.props.contentBody); + } + + componentDidUpdate(prevProps) { + const { + items, + filterKey, + filterValue, + sortKey, + sortDirection, + bannerOptions + } = this.props; + + const itemsChanged = hasDifferentItems(prevProps.items, items); + + if ( + prevProps.sortKey !== sortKey || + prevProps.bannerOptions !== bannerOptions || + itemsChanged + ) { + this.calculateGrid(); + } + + if ( + prevProps.filterKey !== filterKey || + prevProps.filterValue !== filterValue || + prevProps.sortKey !== sortKey || + prevProps.sortDirection !== sortDirection || + itemsChanged + ) { + this._grid.recomputeGridSize(); + } + } + + // + // Control + + scrollToFirstCharacter(character) { + const items = this.props.items; + const { + columnCount, + rowHeight + } = this.state; + + const index = _.findIndex(items, (item) => { + const firstCharacter = item.sortName.charAt(0); + + if (character === '#') { + return !isNaN(firstCharacter); + } + + return firstCharacter === character; + }); + + if (index != null) { + const row = Math.floor(index / columnCount); + const scrollTop = rowHeight * row; + + this.props.onScroll({ scrollTop }); + } + } + + setGridRef = (ref) => { + this._grid = ref; + } + + calculateGrid = (width = this.state.width, isSmallScreen) => { + const { + sortKey, + bannerOptions + } = this.props; + + const padding = isSmallScreen ? columnPaddingSmallScreen : columnPadding; + const columnWidth = calculateColumnWidth(width, bannerOptions.size, isSmallScreen); + const columnCount = Math.max(Math.floor(width / columnWidth), 1); + const bannerWidth = columnWidth - padding; + const bannerHeight = calculateHeight(bannerWidth); + const rowHeight = calculateRowHeight(bannerHeight, sortKey, isSmallScreen, bannerOptions); + + this.setState({ + width, + columnWidth, + columnCount, + bannerWidth, + bannerHeight, + rowHeight + }); + } + + cellRenderer = ({ key, rowIndex, columnIndex, style }) => { + const { + items, + sortKey, + bannerOptions, + showRelativeDates, + shortDateFormat, + timeFormat + } = this.props; + + const { + bannerWidth, + bannerHeight, + columnCount + } = this.state; + + const { + detailedProgressBar, + showTitle, + showQualityProfile + } = bannerOptions; + + const artist = items[rowIndex * columnCount + columnIndex]; + + if (!artist) { + return null; + } + + return ( + + ); + } + + // + // Listeners + + onMeasure = ({ width }) => { + this.calculateGrid(width, this.props.isSmallScreen); + } + + onSectionRendered = () => { + if (!this._isInitialized && this._contentBodyNode) { + this.props.onRender(); + this._isInitialized = true; + } + } + + // + // Render + + render() { + const { + items, + scrollTop, + isSmallScreen, + onScroll + } = this.props; + + const { + width, + columnWidth, + columnCount, + rowHeight + } = this.state; + + const rowCount = Math.ceil(items.length / columnCount); + + return ( + + + {({ height, isScrolling }) => { + return ( + + ); + } + } + + + ); + } +} + +ArtistIndexBanners.propTypes = { + items: PropTypes.arrayOf(PropTypes.object).isRequired, + filterKey: PropTypes.string, + filterValue: PropTypes.oneOfType([PropTypes.bool, PropTypes.number, PropTypes.string]), + sortKey: PropTypes.string, + sortDirection: PropTypes.oneOf(sortDirections.all), + bannerOptions: PropTypes.object.isRequired, + scrollTop: PropTypes.number.isRequired, + contentBody: PropTypes.object.isRequired, + showRelativeDates: PropTypes.bool.isRequired, + shortDateFormat: PropTypes.string.isRequired, + isSmallScreen: PropTypes.bool.isRequired, + timeFormat: PropTypes.string.isRequired, + onRender: PropTypes.func.isRequired, + onScroll: PropTypes.func.isRequired +}; + +export default ArtistIndexBanners; diff --git a/frontend/src/Artist/Index/Banners/ArtistIndexBannersConnector.js b/frontend/src/Artist/Index/Banners/ArtistIndexBannersConnector.js new file mode 100644 index 000000000..21384039e --- /dev/null +++ b/frontend/src/Artist/Index/Banners/ArtistIndexBannersConnector.js @@ -0,0 +1,33 @@ +import { createSelector } from 'reselect'; +import connectSection from 'Store/connectSection'; +import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector'; +import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector'; +import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector'; +import ArtistIndexBanners from './ArtistIndexBanners'; + +function createMapStateToProps() { + return createSelector( + (state) => state.artistIndex.bannerOptions, + createClientSideCollectionSelector(), + createUISettingsSelector(), + createDimensionsSelector(), + (bannerOptions, artist, uiSettings, dimensions) => { + return { + bannerOptions, + showRelativeDates: uiSettings.showRelativeDates, + shortDateFormat: uiSettings.shortDateFormat, + timeFormat: uiSettings.timeFormat, + isSmallScreen: dimensions.isSmallScreen, + ...artist + }; + } + ); +} + +export default connectSection( + createMapStateToProps, + undefined, + undefined, + { withRef: true }, + { section: 'artist', uiSection: 'artistIndex' } +)(ArtistIndexBanners); diff --git a/frontend/src/Artist/Index/Banners/Options/ArtistIndexBannerOptionsModal.js b/frontend/src/Artist/Index/Banners/Options/ArtistIndexBannerOptionsModal.js new file mode 100644 index 000000000..34c8abfcf --- /dev/null +++ b/frontend/src/Artist/Index/Banners/Options/ArtistIndexBannerOptionsModal.js @@ -0,0 +1,25 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import Modal from 'Components/Modal/Modal'; +import ArtistIndexBannerOptionsModalContentConnector from './ArtistIndexBannerOptionsModalContentConnector'; + +function ArtistIndexBannerOptionsModal({ isOpen, onModalClose, ...otherProps }) { + return ( + + + + ); +} + +ArtistIndexBannerOptionsModal.propTypes = { + isOpen: PropTypes.bool.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default ArtistIndexBannerOptionsModal; diff --git a/frontend/src/Artist/Index/Banners/Options/ArtistIndexBannerOptionsModalContent.js b/frontend/src/Artist/Index/Banners/Options/ArtistIndexBannerOptionsModalContent.js new file mode 100644 index 000000000..d320acea5 --- /dev/null +++ b/frontend/src/Artist/Index/Banners/Options/ArtistIndexBannerOptionsModalContent.js @@ -0,0 +1,173 @@ +import _ from 'lodash'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { inputTypes } from 'Helpers/Props'; +import Button from 'Components/Link/Button'; +import Form from 'Components/Form/Form'; +import FormGroup from 'Components/Form/FormGroup'; +import FormLabel from 'Components/Form/FormLabel'; +import FormInputGroup from 'Components/Form/FormInputGroup'; +import ModalContent from 'Components/Modal/ModalContent'; +import ModalHeader from 'Components/Modal/ModalHeader'; +import ModalBody from 'Components/Modal/ModalBody'; +import ModalFooter from 'Components/Modal/ModalFooter'; + +const bannerSizeOptions = [ + { key: 'small', value: 'Small' }, + { key: 'medium', value: 'Medium' }, + { key: 'large', value: 'Large' } +]; + +class ArtistIndexBannerOptionsModalContent extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + detailedProgressBar: props.detailedProgressBar, + size: props.size, + showTitle: props.showTitle, + showQualityProfile: props.showQualityProfile + }; + } + + componentDidUpdate(prevProps) { + const { + detailedProgressBar, + size, + showTitle, + showQualityProfile + } = this.props; + + const state = {}; + + if (detailedProgressBar !== prevProps.detailedProgressBar) { + state.detailedProgressBar = detailedProgressBar; + } + + if (size !== prevProps.size) { + state.size = size; + } + + if (showTitle !== prevProps.showTitle) { + state.showTitle = showTitle; + } + + if (showQualityProfile !== prevProps.showQualityProfile) { + state.showQualityProfile = showQualityProfile; + } + + if (!_.isEmpty(state)) { + this.setState(state); + } + } + + // + // Listeners + + onChangeOption = ({ name, value }) => { + this.setState({ + [name]: value + }, () => { + this.props.onChangeOption({ [name]: value }); + }); + } + + // + // Render + + render() { + const { + onModalClose + } = this.props; + + const { + detailedProgressBar, + size, + showTitle, + showQualityProfile + } = this.state; + + return ( + + + Options + + + +
+ + Size + + + + + + Detailed Progress Bar + + + + + + Show Name + + + + + + Show Quality Profile + + + +
+
+ + + + +
+ ); + } +} + +ArtistIndexBannerOptionsModalContent.propTypes = { + size: PropTypes.string.isRequired, + showTitle: PropTypes.bool.isRequired, + showQualityProfile: PropTypes.bool.isRequired, + detailedProgressBar: PropTypes.bool.isRequired, + onChangeOption: PropTypes.func.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default ArtistIndexBannerOptionsModalContent; diff --git a/frontend/src/Artist/Index/Banners/Options/ArtistIndexBannerOptionsModalContentConnector.js b/frontend/src/Artist/Index/Banners/Options/ArtistIndexBannerOptionsModalContentConnector.js new file mode 100644 index 000000000..0ea742781 --- /dev/null +++ b/frontend/src/Artist/Index/Banners/Options/ArtistIndexBannerOptionsModalContentConnector.js @@ -0,0 +1,23 @@ +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import { setArtistBannerOption } from 'Store/Actions/artistIndexActions'; +import ArtistIndexBannerOptionsModalContent from './ArtistIndexBannerOptionsModalContent'; + +function createMapStateToProps() { + return createSelector( + (state) => state.artistIndex, + (artistIndex) => { + return artistIndex.bannerOptions; + } + ); +} + +function createMapDispatchToProps(dispatch, props) { + return { + onChangeOption(payload) { + dispatch(setArtistBannerOption(payload)); + } + }; +} + +export default connect(createMapStateToProps, createMapDispatchToProps)(ArtistIndexBannerOptionsModalContent); diff --git a/frontend/src/Artist/Index/Menus/ArtistIndexFilterMenu.js b/frontend/src/Artist/Index/Menus/ArtistIndexFilterMenu.js new file mode 100644 index 000000000..5b78ce44d --- /dev/null +++ b/frontend/src/Artist/Index/Menus/ArtistIndexFilterMenu.js @@ -0,0 +1,76 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import { align } from 'Helpers/Props'; +import FilterMenu from 'Components/Menu/FilterMenu'; +import MenuContent from 'Components/Menu/MenuContent'; +import FilterMenuItem from 'Components/Menu/FilterMenuItem'; + +function ArtistIndexFilterMenu(props) { + const { + filterKey, + filterValue, + onFilterSelect + } = props; + + return ( + + + + All + + + + Monitored Only + + + + Continuing Only + + + + Ended Only + + + + Missing Albums + + + + ); +} + +ArtistIndexFilterMenu.propTypes = { + filterKey: PropTypes.string, + filterValue: PropTypes.oneOfType([PropTypes.bool, PropTypes.number, PropTypes.string]), + onFilterSelect: PropTypes.func.isRequired +}; + +export default ArtistIndexFilterMenu; diff --git a/frontend/src/Artist/Index/Menus/ArtistIndexSortMenu.js b/frontend/src/Artist/Index/Menus/ArtistIndexSortMenu.js new file mode 100644 index 000000000..c4c70e0d2 --- /dev/null +++ b/frontend/src/Artist/Index/Menus/ArtistIndexSortMenu.js @@ -0,0 +1,145 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import { align, sortDirections } from 'Helpers/Props'; +import SortMenu from 'Components/Menu/SortMenu'; +import MenuContent from 'Components/Menu/MenuContent'; +import SortMenuItem from 'Components/Menu/SortMenuItem'; + +function ArtistIndexSortMenu(props) { + const { + sortKey, + sortDirection, + onSortSelect + } = props; + + return ( + + + + Name + + + + Type + + + + Quality Profile + + + + Language Profile + + + + Next Airing + + + + Previous Airing + + + + Added + + + + Albums + + + + Tracks + + + + Track Count + + + + Latest Album + + + + Path + + + + Size on Disk + + + + ); +} + +ArtistIndexSortMenu.propTypes = { + sortKey: PropTypes.string, + sortDirection: PropTypes.oneOf(sortDirections.all), + onSortSelect: PropTypes.func.isRequired +}; + +export default ArtistIndexSortMenu; diff --git a/frontend/src/Artist/Index/Menus/ArtistIndexViewMenu.js b/frontend/src/Artist/Index/Menus/ArtistIndexViewMenu.js new file mode 100644 index 000000000..b227a96b6 --- /dev/null +++ b/frontend/src/Artist/Index/Menus/ArtistIndexViewMenu.js @@ -0,0 +1,58 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import { align } from 'Helpers/Props'; +import ViewMenu from 'Components/Menu/ViewMenu'; +import MenuContent from 'Components/Menu/MenuContent'; +import ViewMenuItem from 'Components/Menu/ViewMenuItem'; + +function ArtistIndexViewMenu(props) { + const { + view, + onViewSelect + } = props; + + return ( + + + + Table + + + + Posters + + + + Banners + + + + Overview + + + + ); +} + +ArtistIndexViewMenu.propTypes = { + view: PropTypes.string.isRequired, + onViewSelect: PropTypes.func.isRequired +}; + +export default ArtistIndexViewMenu; diff --git a/frontend/src/Artist/Index/Overview/ArtistIndexOverview.css b/frontend/src/Artist/Index/Overview/ArtistIndexOverview.css new file mode 100644 index 000000000..cb1b3c0fb --- /dev/null +++ b/frontend/src/Artist/Index/Overview/ArtistIndexOverview.css @@ -0,0 +1,91 @@ +$hoverScale: 1.05; + +.container { + &:hover { + .content { + background-color: $tableRowHoverBackgroundColor; + } + } +} + +.content { + display: flex; + flex-grow: 1; +} + +.poster { + position: relative; +} + +.posterContainer { + position: relative; +} + +.link { + composes: link from 'Components/Link/Link.css'; + + display: block; + color: $defaultColor; + + &:hover { + color: $defaultColor; + text-decoration: none; + } +} + +.ended { + position: absolute; + top: 0; + right: 0; + z-index: 1; + width: 0; + height: 0; + border-width: 0 25px 25px 0; + border-style: solid; + border-color: transparent $dangerColor transparent transparent; + color: $white; +} + +.info { + flex: 1 0 1px; + overflow: hidden; + padding-left: 10px; +} + +.titleRow { + display: flex; + justify-content: space-between; + margin-bottom: 10px; + line-height: 32px; +} + +.title { + @add-mixin truncate; + composes: link; + + flex: 1 0 1px; + font-weight: 300; + font-size: 30px; +} + +.actions { + white-space: nowrap; +} + +.details { + display: flex; + justify-content: space-between; +} + +.overview { + composes: link; + + flex: 0 1 1000px; + overflow: hidden; +} + +@media only screen and (max-width: $breakpointSmall) { + .overview { + display: none; + } +} diff --git a/frontend/src/Artist/Index/Overview/ArtistIndexOverview.js b/frontend/src/Artist/Index/Overview/ArtistIndexOverview.js new file mode 100644 index 000000000..b1567fcff --- /dev/null +++ b/frontend/src/Artist/Index/Overview/ArtistIndexOverview.js @@ -0,0 +1,252 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import Truncate from 'react-truncate'; +import { icons } from 'Helpers/Props'; +import dimensions from 'Styles/Variables/dimensions'; +import fonts from 'Styles/Variables/fonts'; +import IconButton from 'Components/Link/IconButton'; +import Link from 'Components/Link/Link'; +import SpinnerIconButton from 'Components/Link/SpinnerIconButton'; +import ArtistPoster from 'Artist/ArtistPoster'; +import EditArtistModalConnector from 'Artist/Edit/EditArtistModalConnector'; +import DeleteArtistModal from 'Artist/Delete/DeleteArtistModal'; +import ArtistIndexProgressBar from 'Artist/Index/ProgressBar/ArtistIndexProgressBar'; +import ArtistIndexOverviewInfo from './ArtistIndexOverviewInfo'; +import styles from './ArtistIndexOverview.css'; + +const columnPadding = parseInt(dimensions.artistIndexColumnPadding); +const columnPaddingSmallScreen = parseInt(dimensions.artistIndexColumnPaddingSmallScreen); +const defaultFontSize = parseInt(fonts.defaultFontSize); +const lineHeight = parseFloat(fonts.lineHeight); + +function calculateHeight(rowHeight, isSmallScreen) { + let height = rowHeight - 45; + + if (isSmallScreen) { + height -= columnPaddingSmallScreen; + } else { + height -= columnPadding; + } + + return height; +} + +class ArtistIndexOverview extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + isEditArtistModalOpen: false, + isDeleteArtistModalOpen: false + }; + } + + // + // Listeners + + onEditArtistPress = () => { + this.setState({ isEditArtistModalOpen: true }); + } + + onEditArtistModalClose = () => { + this.setState({ isEditArtistModalOpen: false }); + } + + onDeleteArtistPress = () => { + this.setState({ + isEditArtistModalOpen: false, + isDeleteArtistModalOpen: true + }); + } + + onDeleteArtistModalClose = () => { + this.setState({ isDeleteArtistModalOpen: false }); + } + + // + // Render + + render() { + const { + style, + id, + artistName, + overview, + monitored, + status, + nameSlug, + nextAiring, + trackCount, + trackFileCount, + images, + posterWidth, + posterHeight, + qualityProfile, + overviewOptions, + showRelativeDates, + shortDateFormat, + timeFormat, + rowHeight, + isSmallScreen, + isRefreshingArtist, + onRefreshArtistPress, + ...otherProps + } = this.props; + + const { + isEditArtistModalOpen, + isDeleteArtistModalOpen + } = this.state; + + const link = `/artist/${nameSlug}`; + + const elementStyle = { + width: `${posterWidth}px`, + height: `${posterHeight}px` + }; + + const height = calculateHeight(rowHeight, isSmallScreen); + + return ( +
+
+
+
+ { + status === 'ended' && +
+ } + + + + +
+ + +
+ +
+
+ + {artistName} + + +
+ + + +
+
+ +
+ + + {overview} + + + + +
+
+
+ + + + +
+ ); + } +} + +ArtistIndexOverview.propTypes = { + style: PropTypes.object.isRequired, + id: PropTypes.number.isRequired, + artistName: PropTypes.string.isRequired, + overview: PropTypes.string.isRequired, + monitored: PropTypes.bool.isRequired, + status: PropTypes.string.isRequired, + nameSlug: PropTypes.string.isRequired, + nextAiring: PropTypes.string, + trackCount: PropTypes.number, + trackFileCount: PropTypes.number, + images: PropTypes.arrayOf(PropTypes.object).isRequired, + posterWidth: PropTypes.number.isRequired, + posterHeight: PropTypes.number.isRequired, + rowHeight: PropTypes.number.isRequired, + qualityProfile: PropTypes.object.isRequired, + overviewOptions: PropTypes.object.isRequired, + showRelativeDates: PropTypes.bool.isRequired, + shortDateFormat: PropTypes.string.isRequired, + timeFormat: PropTypes.string.isRequired, + isSmallScreen: PropTypes.bool.isRequired, + isRefreshingArtist: PropTypes.bool.isRequired, + onRefreshArtistPress: PropTypes.func.isRequired +}; + +ArtistIndexOverview.defaultProps = { + trackCount: 0, + trackFileCount: 0 +}; + +export default ArtistIndexOverview; diff --git a/frontend/src/Artist/Index/Overview/ArtistIndexOverviewInfo.css b/frontend/src/Artist/Index/Overview/ArtistIndexOverviewInfo.css new file mode 100644 index 000000000..aa25a676c --- /dev/null +++ b/frontend/src/Artist/Index/Overview/ArtistIndexOverviewInfo.css @@ -0,0 +1,23 @@ +.infos { + display: flex; + flex: 0 0 250px; + flex-direction: column; + margin-left: 10px; +} + +.info { + flex: 0 0 $artistIndexOverviewInfoRowHeight; + margin: 2px 0; +} + +.icon { + margin-right: 5px; + width: 25px; + text-align: center; +} + +@media only screen and (max-width: $breakpointSmall) { + .infos { + margin-left: 0; + } +} diff --git a/frontend/src/Artist/Index/Overview/ArtistIndexOverviewInfo.js b/frontend/src/Artist/Index/Overview/ArtistIndexOverviewInfo.js new file mode 100644 index 000000000..31a114e70 --- /dev/null +++ b/frontend/src/Artist/Index/Overview/ArtistIndexOverviewInfo.js @@ -0,0 +1,193 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import getRelativeDate from 'Utilities/Date/getRelativeDate'; +import formatBytes from 'Utilities/Number/formatBytes'; +import { icons } from 'Helpers/Props'; +import dimensions from 'Styles/Variables/dimensions'; +import Icon from 'Components/Icon'; +import styles from './ArtistIndexOverviewInfo.css'; + +const infoRowHeight = parseInt(dimensions.artistIndexOverviewInfoRowHeight); + +function isVisible(name, show, value, sortKey, index) { + if (value == null) { + return false; + } + + return show || sortKey === name; +} + +function ArtistIndexOverviewInfo(props) { + const { + height, + showQualityProfile, + showAdded, + showAlbumCount, + showPath, + showSizeOnDisk, + nextAiring, + qualityProfile, + added, + albumCount, + path, + sizeOnDisk, + sortKey, + showRelativeDates, + shortDateFormat, + timeFormat + } = props; + + let albums = '1 album'; + + if (albumCount === 0) { + albums = 'No albums'; + } else if (albumCount > 1) { + albums = `${albumCount} albums`; + } + + const maxRows = Math.floor(height / (infoRowHeight + 4)); + + return ( +
+ { + !!nextAiring && +
+ + + { + getRelativeDate( + nextAiring, + shortDateFormat, + showRelativeDates, + { + timeFormat, + timeForToday: true + } + ) + } +
+ } + + { + isVisible('qualityProfileId', showQualityProfile, qualityProfile, sortKey) && maxRows > 1 && +
+ + + {qualityProfile.name} +
+ } + + { + isVisible('added', showAdded, added, sortKey) && maxRows > 2 && +
+ + + { + getRelativeDate( + added, + shortDateFormat, + showRelativeDates, + { + timeFormat, + timeForToday: true + } + ) + } +
+ } + + { + isVisible('albumCount', showAlbumCount, albumCount, sortKey) && maxRows > 3 && +
+ + + {albums} +
+ } + + { + isVisible('path', showPath, path, sortKey) && maxRows > 4 && +
+ + + {path} +
+ } + + { + isVisible('sizeOnDisk', showSizeOnDisk, sizeOnDisk, sortKey) && maxRows > 5 && +
+ + + {formatBytes(sizeOnDisk)} +
+ } + +
+ ); +} + +ArtistIndexOverviewInfo.propTypes = { + height: PropTypes.number.isRequired, + showNetwork: PropTypes.bool.isRequired, + showQualityProfile: PropTypes.bool.isRequired, + showAdded: PropTypes.bool.isRequired, + showAlbumCount: PropTypes.bool.isRequired, + showPath: PropTypes.bool.isRequired, + showSizeOnDisk: PropTypes.bool.isRequired, + nextAiring: PropTypes.string, + qualityProfile: PropTypes.object.isRequired, + previousAiring: PropTypes.string, + added: PropTypes.string, + albumCount: PropTypes.number.isRequired, + path: PropTypes.string.isRequired, + sizeOnDisk: PropTypes.number, + sortKey: PropTypes.string.isRequired, + showRelativeDates: PropTypes.bool.isRequired, + shortDateFormat: PropTypes.string.isRequired, + timeFormat: PropTypes.string.isRequired +}; + +export default ArtistIndexOverviewInfo; diff --git a/frontend/src/Artist/Index/Overview/ArtistIndexOverviews.css b/frontend/src/Artist/Index/Overview/ArtistIndexOverviews.css new file mode 100644 index 000000000..9c6520fb5 --- /dev/null +++ b/frontend/src/Artist/Index/Overview/ArtistIndexOverviews.css @@ -0,0 +1,3 @@ +.grid { + flex: 1 0 auto; +} diff --git a/frontend/src/Artist/Index/Overview/ArtistIndexOverviews.js b/frontend/src/Artist/Index/Overview/ArtistIndexOverviews.js new file mode 100644 index 000000000..801a9d35b --- /dev/null +++ b/frontend/src/Artist/Index/Overview/ArtistIndexOverviews.js @@ -0,0 +1,278 @@ +import _ from 'lodash'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import ReactDOM from 'react-dom'; +import Measure from 'react-measure'; +import { Grid, WindowScroller } from 'react-virtualized'; +import hasDifferentItems from 'Utilities/Object/hasDifferentItems'; +import dimensions from 'Styles/Variables/dimensions'; +import { sortDirections } from 'Helpers/Props'; +import ArtistIndexItemConnector from 'Artist/Index/ArtistIndexItemConnector'; +import ArtistIndexOverview from './ArtistIndexOverview'; +import styles from './ArtistIndexOverviews.css'; + +// Poster container dimensions +const columnPadding = parseInt(dimensions.artistIndexColumnPadding); +const columnPaddingSmallScreen = parseInt(dimensions.artistIndexColumnPaddingSmallScreen); +const progressBarHeight = parseInt(dimensions.progressBarSmallHeight); +const detailedProgressBarHeight = parseInt(dimensions.progressBarMediumHeight); + +function calculatePosterWidth(posterSize, isSmallScreen) { + const maxiumPosterWidth = isSmallScreen ? 192 : 202; + + if (posterSize === 'large') { + return maxiumPosterWidth; + } + + if (posterSize === 'medium') { + return Math.floor(maxiumPosterWidth * 0.75); + } + + return Math.floor(maxiumPosterWidth * 0.5); +} + +function calculateRowHeight(posterHeight, sortKey, isSmallScreen, overviewOptions) { + const { + detailedProgressBar + } = overviewOptions; + + const heights = [ + posterHeight, + detailedProgressBar ? detailedProgressBarHeight : progressBarHeight, + isSmallScreen ? columnPaddingSmallScreen : columnPadding + ]; + + return heights.reduce((acc, height) => acc + height, 0); +} + +function calculatePosterHeight(posterWidth) { + return posterWidth; +} + +class ArtistIndexOverviews extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + width: 0, + columnCount: 1, + posterWidth: 238, + posterHeight: 238, + rowHeight: calculateRowHeight(238, null, props.isSmallScreen, {}) + }; + + this._isInitialized = false; + this._grid = null; + } + + componentDidMount() { + this._contentBodyNode = ReactDOM.findDOMNode(this.props.contentBody); + } + + componentDidUpdate(prevProps) { + const { + items, + filterKey, + filterValue, + sortKey, + sortDirection, + overviewOptions + } = this.props; + + const itemsChanged = hasDifferentItems(prevProps.items, items); + const overviewOptionsChanged = !_.isMatch(prevProps.overviewOptions, overviewOptions); + + if ( + prevProps.sortKey !== sortKey || + prevProps.overviewOptions !== overviewOptions || + itemsChanged + ) { + this.calculateGrid(); + } + + if ( + prevProps.filterKey !== filterKey || + prevProps.filterValue !== filterValue || + prevProps.sortKey !== sortKey || + prevProps.sortDirection !== sortDirection || + itemsChanged || + overviewOptionsChanged + ) { + this._grid.recomputeGridSize(); + } + } + + // + // Control + + scrollToFirstCharacter(character) { + const items = this.props.items; + const { + rowHeight + } = this.state; + + const index = _.findIndex(items, (item) => { + const firstCharacter = item.sortTitle.charAt(0); + + if (character === '#') { + return !isNaN(firstCharacter); + } + + return firstCharacter === character; + }); + + if (index != null) { + const scrollTop = rowHeight * index; + + this.props.onScroll({ scrollTop }); + } + } + + setGridRef = (ref) => { + this._grid = ref; + } + + calculateGrid = (width = this.state.width, isSmallScreen) => { + const { + sortKey, + overviewOptions + } = this.props; + + const posterWidth = calculatePosterWidth(overviewOptions.size, isSmallScreen); + const posterHeight = calculatePosterHeight(posterWidth); + const rowHeight = calculateRowHeight(posterHeight, sortKey, isSmallScreen, overviewOptions); + + this.setState({ + width, + posterWidth, + posterHeight, + rowHeight + }); + } + + cellRenderer = ({ key, rowIndex, style }) => { + const { + items, + sortKey, + overviewOptions, + showRelativeDates, + shortDateFormat, + timeFormat, + isSmallScreen + } = this.props; + + const { + posterWidth, + posterHeight, + rowHeight + } = this.state; + + const artist = items[rowIndex]; + + if (!artist) { + return null; + } + + return ( + + ); + } + + // + // Listeners + + onMeasure = ({ width }) => { + this.calculateGrid(width, this.props.isSmallScreen); + } + + onSectionRendered = () => { + if (!this._isInitialized && this._contentBodyNode) { + this.props.onRender(); + this._isInitialized = true; + } + } + + // + // Render + + render() { + const { + items, + scrollTop, + isSmallScreen, + onScroll + } = this.props; + + const { + width, + rowHeight + } = this.state; + + return ( + + + {({ height, isScrolling }) => { + return ( + + ); + } + } + + + ); + } +} + +ArtistIndexOverviews.propTypes = { + items: PropTypes.arrayOf(PropTypes.object).isRequired, + filterKey: PropTypes.string, + filterValue: PropTypes.oneOfType([PropTypes.bool, PropTypes.number, PropTypes.string]), + sortKey: PropTypes.string, + sortDirection: PropTypes.oneOf(sortDirections.all), + overviewOptions: PropTypes.object.isRequired, + scrollTop: PropTypes.number.isRequired, + contentBody: PropTypes.object.isRequired, + showRelativeDates: PropTypes.bool.isRequired, + shortDateFormat: PropTypes.string.isRequired, + isSmallScreen: PropTypes.bool.isRequired, + timeFormat: PropTypes.string.isRequired, + onRender: PropTypes.func.isRequired, + onScroll: PropTypes.func.isRequired +}; + +export default ArtistIndexOverviews; diff --git a/frontend/src/Artist/Index/Overview/ArtistIndexOverviewsConnector.js b/frontend/src/Artist/Index/Overview/ArtistIndexOverviewsConnector.js new file mode 100644 index 000000000..a2075416f --- /dev/null +++ b/frontend/src/Artist/Index/Overview/ArtistIndexOverviewsConnector.js @@ -0,0 +1,33 @@ +import { createSelector } from 'reselect'; +import connectSection from 'Store/connectSection'; +import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector'; +import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector'; +import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector'; +import ArtistIndexOverviews from './ArtistIndexOverviews'; + +function createMapStateToProps() { + return createSelector( + (state) => state.artistIndex.overviewOptions, + createClientSideCollectionSelector(), + createUISettingsSelector(), + createDimensionsSelector(), + (overviewOptions, artist, uiSettings, dimensions) => { + return { + overviewOptions, + showRelativeDates: uiSettings.showRelativeDates, + shortDateFormat: uiSettings.shortDateFormat, + timeFormat: uiSettings.timeFormat, + isSmallScreen: dimensions.isSmallScreen, + ...artist + }; + } + ); +} + +export default connectSection( + createMapStateToProps, + undefined, + undefined, + { withRef: true }, + { section: 'artist', uiSection: 'artistIndex' } +)(ArtistIndexOverviews); diff --git a/frontend/src/Artist/Index/Overview/Options/ArtistIndexOverviewOptionsModal.js b/frontend/src/Artist/Index/Overview/Options/ArtistIndexOverviewOptionsModal.js new file mode 100644 index 000000000..9ca575185 --- /dev/null +++ b/frontend/src/Artist/Index/Overview/Options/ArtistIndexOverviewOptionsModal.js @@ -0,0 +1,25 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import Modal from 'Components/Modal/Modal'; +import ArtistIndexOverviewOptionsModalContentConnector from './ArtistIndexOverviewOptionsModalContentConnector'; + +function ArtistIndexOverviewOptionsModal({ isOpen, onModalClose, ...otherProps }) { + return ( + + + + ); +} + +ArtistIndexOverviewOptionsModal.propTypes = { + isOpen: PropTypes.bool.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default ArtistIndexOverviewOptionsModal; diff --git a/frontend/src/Artist/Index/Overview/Options/ArtistIndexOverviewOptionsModalContent.js b/frontend/src/Artist/Index/Overview/Options/ArtistIndexOverviewOptionsModalContent.js new file mode 100644 index 000000000..b5bf02b45 --- /dev/null +++ b/frontend/src/Artist/Index/Overview/Options/ArtistIndexOverviewOptionsModalContent.js @@ -0,0 +1,247 @@ +import _ from 'lodash'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { inputTypes } from 'Helpers/Props'; +import Button from 'Components/Link/Button'; +import Form from 'Components/Form/Form'; +import FormGroup from 'Components/Form/FormGroup'; +import FormLabel from 'Components/Form/FormLabel'; +import FormInputGroup from 'Components/Form/FormInputGroup'; +import ModalContent from 'Components/Modal/ModalContent'; +import ModalHeader from 'Components/Modal/ModalHeader'; +import ModalBody from 'Components/Modal/ModalBody'; +import ModalFooter from 'Components/Modal/ModalFooter'; + +const posterSizeOptions = [ + { key: 'small', value: 'Small' }, + { key: 'medium', value: 'Medium' }, + { key: 'large', value: 'Large' } +]; + +class ArtistIndexOverviewOptionsModalContent extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + detailedProgressBar: props.detailedProgressBar, + size: props.size, + showQualityProfile: props.showQualityProfile, + showPreviousAiring: props.showPreviousAiring, + showAdded: props.showAdded, + showAlbumCount: props.showAlbumCount, + showPath: props.showPath, + showSizeOnDisk: props.showSizeOnDisk + }; + } + + componentDidUpdate(prevProps) { + const { + detailedProgressBar, + size, + showQualityProfile, + showPreviousAiring, + showAdded, + showAlbumCount, + showPath, + showSizeOnDisk + } = this.props; + + const state = {}; + + if (detailedProgressBar !== prevProps.detailedProgressBar) { + state.detailedProgressBar = detailedProgressBar; + } + + if (size !== prevProps.size) { + state.size = size; + } + + if (showQualityProfile !== prevProps.showQualityProfile) { + state.showQualityProfile = showQualityProfile; + } + + if (showPreviousAiring !== prevProps.showPreviousAiring) { + state.showPreviousAiring = showPreviousAiring; + } + + if (showAdded !== prevProps.showAdded) { + state.showAdded = showAdded; + } + + if (showAlbumCount !== prevProps.showAlbumCount) { + state.showAlbumCount = showAlbumCount; + } + + if (showPath !== prevProps.showPath) { + state.showPath = showPath; + } + + if (showSizeOnDisk !== prevProps.showSizeOnDisk) { + state.showSizeOnDisk = showSizeOnDisk; + } + + if (!_.isEmpty(state)) { + this.setState(state); + } + } + + // + // Listeners + + onChangeOverviewOption = ({ name, value }) => { + this.setState({ + [name]: value + }, () => { + this.props.onChangeOverviewOption({ [name]: value }); + }); + } + + // + // Render + + render() { + const { + onModalClose + } = this.props; + + const { + detailedProgressBar, + size, + showQualityProfile, + showPreviousAiring, + showAdded, + showAlbumCount, + showPath, + showSizeOnDisk + } = this.state; + + return ( + + + Overview Options + + + +
+ + Poster Size + + + + + + Detailed Progress Bar + + + + + + Show Quality Profile + + + + + + Show Previous Airing + + + + + + Show Date Added + + + + + + Show Season Count + + + + + + Show Path + + + + + + Show Size on Disk + + + +
+
+ + + + +
+ ); + } +} + +ArtistIndexOverviewOptionsModalContent.propTypes = { + size: PropTypes.string.isRequired, + showQualityProfile: PropTypes.bool.isRequired, + showPreviousAiring: PropTypes.bool.isRequired, + showAdded: PropTypes.bool.isRequired, + showAlbumCount: PropTypes.bool.isRequired, + showPath: PropTypes.bool.isRequired, + showSizeOnDisk: PropTypes.bool.isRequired, + detailedProgressBar: PropTypes.bool.isRequired, + onChangeOverviewOption: PropTypes.func.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default ArtistIndexOverviewOptionsModalContent; diff --git a/frontend/src/Artist/Index/Overview/Options/ArtistIndexOverviewOptionsModalContentConnector.js b/frontend/src/Artist/Index/Overview/Options/ArtistIndexOverviewOptionsModalContentConnector.js new file mode 100644 index 000000000..70c30dba6 --- /dev/null +++ b/frontend/src/Artist/Index/Overview/Options/ArtistIndexOverviewOptionsModalContentConnector.js @@ -0,0 +1,23 @@ +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import { setArtistOverviewOption } from 'Store/Actions/artistIndexActions'; +import ArtistIndexOverviewOptionsModalContent from './ArtistIndexOverviewOptionsModalContent'; + +function createMapStateToProps() { + return createSelector( + (state) => state.artistIndex, + (artistIndex) => { + return artistIndex.overviewOptions; + } + ); +} + +function createMapDispatchToProps(dispatch, props) { + return { + onChangeOverviewOption(payload) { + dispatch(setArtistOverviewOption(payload)); + } + }; +} + +export default connect(createMapStateToProps, createMapDispatchToProps)(ArtistIndexOverviewOptionsModalContent); diff --git a/frontend/src/Artist/Index/Posters/ArtistIndexPoster.css b/frontend/src/Artist/Index/Posters/ArtistIndexPoster.css new file mode 100644 index 000000000..c687874d4 --- /dev/null +++ b/frontend/src/Artist/Index/Posters/ArtistIndexPoster.css @@ -0,0 +1,85 @@ +$hoverScale: 1.05; + +.container { + padding: 10px; +} + +.content { + transition: all 200ms ease-in; + + &:hover { + z-index: 2; + box-shadow: 0 0 12px $black; + transition: all 200ms ease-in; + + .controls { + opacity: 0.9; + transition: opacity 200ms linear 150ms; + } + } +} + +.posterContainer { + position: relative; +} + +.link { + composes: link from 'Components/Link/Link.css'; + + display: block; + background-color: $defaultColor; +} + +.nextAiring { + background-color: $defaultColor; + color: $white; + text-align: center; + font-size: $smallFontSize; +} + +.title { + @add-mixin truncate; + + background-color: $defaultColor; + color: $white; + text-align: center; + font-size: $smallFontSize; +} + +.ended { + position: absolute; + top: 0; + right: 0; + width: 0; + height: 0; + border-width: 0 25px 25px 0; + border-style: solid; + border-color: transparent $dangerColor transparent transparent; + color: $white; +} + +.controls { + position: absolute; + bottom: 10px; + left: 10px; + border-radius: 4px; + background-color: #216044; + color: $white; + font-size: $smallFontSize; + opacity: 0; + transition: opacity 0; +} + +.action { + composes: button from 'Components/Link/IconButton.css'; + + &:hover { + color: #ccc; + } +} + +@media only screen and (max-width: $breakpointSmall) { + .container { + padding: 5px; + } +} diff --git a/frontend/src/Artist/Index/Posters/ArtistIndexPoster.js b/frontend/src/Artist/Index/Posters/ArtistIndexPoster.js new file mode 100644 index 000000000..6ec411eac --- /dev/null +++ b/frontend/src/Artist/Index/Posters/ArtistIndexPoster.js @@ -0,0 +1,231 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import getRelativeDate from 'Utilities/Date/getRelativeDate'; +import { icons } from 'Helpers/Props'; +import IconButton from 'Components/Link/IconButton'; +import SpinnerIconButton from 'Components/Link/SpinnerIconButton'; +import Label from 'Components/Label'; +import Link from 'Components/Link/Link'; +import ArtistPoster from 'Artist/ArtistPoster'; +import EditArtistModalConnector from 'Artist/Edit/EditArtistModalConnector'; +import DeleteArtistModal from 'Artist/Delete/DeleteArtistModal'; +import ArtistIndexProgressBar from 'Artist/Index/ProgressBar/ArtistIndexProgressBar'; +import ArtistIndexPosterInfo from './ArtistIndexPosterInfo'; +import styles from './ArtistIndexPoster.css'; + +class ArtistIndexPoster extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + isEditArtistModalOpen: false, + isDeleteArtistModalOpen: false + }; + } + + // + // Listeners + + onEditArtistPress = () => { + this.setState({ isEditArtistModalOpen: true }); + } + + onEditArtistModalClose = () => { + this.setState({ isEditArtistModalOpen: false }); + } + + onDeleteArtistPress = () => { + this.setState({ + isEditArtistModalOpen: false, + isDeleteArtistModalOpen: true + }); + } + + onDeleteArtistModalClose = () => { + this.setState({ isDeleteArtistModalOpen: false }); + } + + // + // Render + + render() { + const { + style, + id, + artistName, + monitored, + status, + nameSlug, + nextAiring, + trackCount, + trackFileCount, + images, + posterWidth, + posterHeight, + detailedProgressBar, + showTitle, + showQualityProfile, + qualityProfile, + showRelativeDates, + shortDateFormat, + timeFormat, + isRefreshingArtist, + onRefreshArtistPress, + ...otherProps + } = this.props; + + const { + isEditArtistModalOpen, + isDeleteArtistModalOpen + } = this.state; + + const link = `/artist/${nameSlug}`; + + const elementStyle = { + width: `${posterWidth}px`, + height: `${posterHeight}px` + }; + + return ( +
+
+
+ + + { + status === 'ended' && +
+ } + + + + +
+ + + + { + showTitle && +
+ {artistName} +
+ } + + { + showQualityProfile && +
+ {qualityProfile.name} +
+ } + +
+ { + getRelativeDate( + nextAiring, + shortDateFormat, + showRelativeDates, + { + timeFormat, + timeForToday: true + } + ) + } +
+ + + + + + +
+
+ ); + } +} + +ArtistIndexPoster.propTypes = { + style: PropTypes.object.isRequired, + id: PropTypes.number.isRequired, + artistName: PropTypes.string.isRequired, + monitored: PropTypes.bool.isRequired, + status: PropTypes.string.isRequired, + nameSlug: PropTypes.string.isRequired, + nextAiring: PropTypes.string, + trackCount: PropTypes.number, + trackFileCount: PropTypes.number, + images: PropTypes.arrayOf(PropTypes.object).isRequired, + posterWidth: PropTypes.number.isRequired, + posterHeight: PropTypes.number.isRequired, + detailedProgressBar: PropTypes.bool.isRequired, + showTitle: PropTypes.bool.isRequired, + showQualityProfile: PropTypes.bool.isRequired, + qualityProfile: PropTypes.object.isRequired, + showRelativeDates: PropTypes.bool.isRequired, + shortDateFormat: PropTypes.string.isRequired, + timeFormat: PropTypes.string.isRequired, + isRefreshingArtist: PropTypes.bool.isRequired, + onRefreshArtistPress: PropTypes.func.isRequired +}; + +ArtistIndexPoster.defaultProps = { + trackCount: 0, + trackFileCount: 0 +}; + +export default ArtistIndexPoster; diff --git a/frontend/src/Artist/Index/Posters/ArtistIndexPosterInfo.css b/frontend/src/Artist/Index/Posters/ArtistIndexPosterInfo.css new file mode 100644 index 000000000..cab3dec61 --- /dev/null +++ b/frontend/src/Artist/Index/Posters/ArtistIndexPosterInfo.css @@ -0,0 +1,6 @@ +.info { + background-color: $defaultColor; + color: $white; + text-align: center; + font-size: $smallFontSize; +} diff --git a/frontend/src/Artist/Index/Posters/ArtistIndexPosterInfo.js b/frontend/src/Artist/Index/Posters/ArtistIndexPosterInfo.js new file mode 100644 index 000000000..591961605 --- /dev/null +++ b/frontend/src/Artist/Index/Posters/ArtistIndexPosterInfo.js @@ -0,0 +1,115 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import getRelativeDate from 'Utilities/Date/getRelativeDate'; +import formatBytes from 'Utilities/Number/formatBytes'; +import styles from './ArtistIndexPosterInfo.css'; + +function ArtistIndexPosterInfo(props) { + const { + qualityProfile, + showQualityProfile, + previousAiring, + added, + albumCount, + path, + sizeOnDisk, + sortKey, + showRelativeDates, + shortDateFormat, + timeFormat + } = props; + + if (sortKey === 'qualityProfileId' && !showQualityProfile) { + return ( +
+ {qualityProfile.name} +
+ ); + } + + if (sortKey === 'previousAiring' && previousAiring) { + return ( +
+ { + getRelativeDate( + previousAiring, + shortDateFormat, + showRelativeDates, + { + timeFormat, + timeForToday: true + } + ) + } +
+ ); + } + + if (sortKey === 'added' && added) { + const addedDate = getRelativeDate( + added, + shortDateFormat, + showRelativeDates, + { + timeFormat, + timeForToday: false + } + ); + + return ( +
+ {`Added ${addedDate}`} +
+ ); + } + + if (sortKey === 'albumCount') { + let albums = '1 album'; + + if (albumCount === 0) { + albums = 'No albums'; + } else if (albumCount > 1) { + albums = `${albumCount} albums`; + } + + return ( +
+ {albums} +
+ ); + } + + if (sortKey === 'path') { + return ( +
+ {path} +
+ ); + } + + if (sortKey === 'sizeOnDisk') { + return ( +
+ {formatBytes(sizeOnDisk)} +
+ ); + } + + return null; +} + +ArtistIndexPosterInfo.propTypes = { + qualityProfile: PropTypes.object.isRequired, + showQualityProfile: PropTypes.bool.isRequired, + previousAiring: PropTypes.string, + added: PropTypes.string, + albumCount: PropTypes.number.isRequired, + path: PropTypes.string.isRequired, + sizeOnDisk: PropTypes.number, + sortKey: PropTypes.string.isRequired, + showRelativeDates: PropTypes.bool.isRequired, + shortDateFormat: PropTypes.string.isRequired, + timeFormat: PropTypes.string.isRequired +}; + +export default ArtistIndexPosterInfo; diff --git a/frontend/src/Artist/Index/Posters/ArtistIndexPosters.css b/frontend/src/Artist/Index/Posters/ArtistIndexPosters.css new file mode 100644 index 000000000..9c6520fb5 --- /dev/null +++ b/frontend/src/Artist/Index/Posters/ArtistIndexPosters.css @@ -0,0 +1,3 @@ +.grid { + flex: 1 0 auto; +} diff --git a/frontend/src/Artist/Index/Posters/ArtistIndexPosters.js b/frontend/src/Artist/Index/Posters/ArtistIndexPosters.js new file mode 100644 index 000000000..9f870a55b --- /dev/null +++ b/frontend/src/Artist/Index/Posters/ArtistIndexPosters.js @@ -0,0 +1,327 @@ +import _ from 'lodash'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import ReactDOM from 'react-dom'; +import Measure from 'react-measure'; +import { Grid, WindowScroller } from 'react-virtualized'; +import hasDifferentItems from 'Utilities/Object/hasDifferentItems'; +import dimensions from 'Styles/Variables/dimensions'; +import { sortDirections } from 'Helpers/Props'; +import ArtistIndexItemConnector from 'Artist/Index/ArtistIndexItemConnector'; +import ArtistIndexPoster from './ArtistIndexPoster'; +import styles from './ArtistIndexPosters.css'; + +// Poster container dimensions +const columnPadding = parseInt(dimensions.artistIndexColumnPadding); +const columnPaddingSmallScreen = parseInt(dimensions.artistIndexColumnPaddingSmallScreen); +const progressBarHeight = parseInt(dimensions.progressBarSmallHeight); +const detailedProgressBarHeight = parseInt(dimensions.progressBarMediumHeight); + +const additionalColumnCount = { + small: 3, + medium: 2, + large: 1 +}; + +function calculateColumnWidth(width, posterSize, isSmallScreen) { + const maxiumColumnWidth = isSmallScreen ? 172 : 182; + const columns = Math.floor(width / maxiumColumnWidth); + const remainder = width % maxiumColumnWidth; + + if (remainder === 0 && posterSize === 'large') { + return maxiumColumnWidth; + } + + return Math.floor(width / (columns + additionalColumnCount[posterSize])); +} + +function calculateRowHeight(posterHeight, sortKey, isSmallScreen, posterOptions) { + const { + detailedProgressBar, + showTitle, + showQualityProfile + } = posterOptions; + + const nextAiringHeight = 19; + + const heights = [ + posterHeight, + detailedProgressBar ? detailedProgressBarHeight : progressBarHeight, + nextAiringHeight, + isSmallScreen ? columnPaddingSmallScreen : columnPadding + ]; + + if (showTitle) { + heights.push(19); + } + + if (showQualityProfile) { + heights.push(19); + } + + switch (sortKey) { + case 'seasons': + case 'previousAiring': + case 'added': + case 'path': + case 'sizeOnDisk': + heights.push(19); + break; + case 'qualityProfileId': + if (!showQualityProfile) { + heights.push(19); + } + break; + default: + // No need to add a height of 0 + } + + return heights.reduce((acc, height) => acc + height, 0); +} + +function calculatePosterHeight(posterWidth) { + return Math.ceil(posterWidth); +} + +class ArtistIndexPosters extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + width: 0, + columnWidth: 182, + columnCount: 1, + posterWidth: 238, + posterHeight: 238, + rowHeight: calculateRowHeight(238, null, props.isSmallScreen, {}) + }; + + this._isInitialized = false; + this._grid = null; + } + + componentDidMount() { + this._contentBodyNode = ReactDOM.findDOMNode(this.props.contentBody); + } + + componentDidUpdate(prevProps) { + const { + items, + filterKey, + filterValue, + sortKey, + sortDirection, + posterOptions + } = this.props; + + const itemsChanged = hasDifferentItems(prevProps.items, items); + + if ( + prevProps.sortKey !== sortKey || + prevProps.posterOptions !== posterOptions || + itemsChanged + ) { + this.calculateGrid(); + } + + if ( + prevProps.filterKey !== filterKey || + prevProps.filterValue !== filterValue || + prevProps.sortKey !== sortKey || + prevProps.sortDirection !== sortDirection || + itemsChanged + ) { + this._grid.recomputeGridSize(); + } + } + + // + // Control + + scrollToFirstCharacter(character) { + const items = this.props.items; + const { + columnCount, + rowHeight + } = this.state; + + const index = _.findIndex(items, (item) => { + const firstCharacter = item.sortName.charAt(0); + + if (character === '#') { + return !isNaN(firstCharacter); + } + + return firstCharacter === character; + }); + + if (index != null) { + const row = Math.floor(index / columnCount); + const scrollTop = rowHeight * row; + + this.props.onScroll({ scrollTop }); + } + } + + setGridRef = (ref) => { + this._grid = ref; + } + + calculateGrid = (width = this.state.width, isSmallScreen) => { + const { + sortKey, + posterOptions + } = this.props; + + const padding = isSmallScreen ? columnPaddingSmallScreen : columnPadding; + const columnWidth = calculateColumnWidth(width, posterOptions.size, isSmallScreen); + const columnCount = Math.max(Math.floor(width / columnWidth), 1); + const posterWidth = columnWidth - padding; + const posterHeight = calculatePosterHeight(posterWidth); + const rowHeight = calculateRowHeight(posterHeight, sortKey, isSmallScreen, posterOptions); + + this.setState({ + width, + columnWidth, + columnCount, + posterWidth, + posterHeight, + rowHeight + }); + } + + cellRenderer = ({ key, rowIndex, columnIndex, style }) => { + const { + items, + sortKey, + posterOptions, + showRelativeDates, + shortDateFormat, + timeFormat + } = this.props; + + const { + posterWidth, + posterHeight, + columnCount + } = this.state; + + const { + detailedProgressBar, + showTitle, + showQualityProfile + } = posterOptions; + + const artist = items[rowIndex * columnCount + columnIndex]; + + if (!artist) { + return null; + } + + return ( + + ); + } + + // + // Listeners + + onMeasure = ({ width }) => { + this.calculateGrid(width, this.props.isSmallScreen); + } + + onSectionRendered = () => { + if (!this._isInitialized && this._contentBodyNode) { + this.props.onRender(); + this._isInitialized = true; + } + } + + // + // Render + + render() { + const { + items, + scrollTop, + isSmallScreen, + onScroll + } = this.props; + + const { + width, + columnWidth, + columnCount, + rowHeight + } = this.state; + + const rowCount = Math.ceil(items.length / columnCount); + + return ( + + + {({ height, isScrolling }) => { + return ( + + ); + } + } + + + ); + } +} + +ArtistIndexPosters.propTypes = { + items: PropTypes.arrayOf(PropTypes.object).isRequired, + filterKey: PropTypes.string, + filterValue: PropTypes.oneOfType([PropTypes.bool, PropTypes.number, PropTypes.string]), + sortKey: PropTypes.string, + sortDirection: PropTypes.oneOf(sortDirections.all), + posterOptions: PropTypes.object.isRequired, + scrollTop: PropTypes.number.isRequired, + contentBody: PropTypes.object.isRequired, + showRelativeDates: PropTypes.bool.isRequired, + shortDateFormat: PropTypes.string.isRequired, + isSmallScreen: PropTypes.bool.isRequired, + timeFormat: PropTypes.string.isRequired, + onRender: PropTypes.func.isRequired, + onScroll: PropTypes.func.isRequired +}; + +export default ArtistIndexPosters; diff --git a/frontend/src/Artist/Index/Posters/ArtistIndexPostersConnector.js b/frontend/src/Artist/Index/Posters/ArtistIndexPostersConnector.js new file mode 100644 index 000000000..6ec135987 --- /dev/null +++ b/frontend/src/Artist/Index/Posters/ArtistIndexPostersConnector.js @@ -0,0 +1,33 @@ +import { createSelector } from 'reselect'; +import connectSection from 'Store/connectSection'; +import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector'; +import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector'; +import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector'; +import ArtistIndexPosters from './ArtistIndexPosters'; + +function createMapStateToProps() { + return createSelector( + (state) => state.artistIndex.posterOptions, + createClientSideCollectionSelector(), + createUISettingsSelector(), + createDimensionsSelector(), + (posterOptions, artist, uiSettings, dimensions) => { + return { + posterOptions, + showRelativeDates: uiSettings.showRelativeDates, + shortDateFormat: uiSettings.shortDateFormat, + timeFormat: uiSettings.timeFormat, + isSmallScreen: dimensions.isSmallScreen, + ...artist + }; + } + ); +} + +export default connectSection( + createMapStateToProps, + undefined, + undefined, + { withRef: true }, + { section: 'artist', uiSection: 'artistIndex' } +)(ArtistIndexPosters); diff --git a/frontend/src/Artist/Index/Posters/Options/ArtistIndexPosterOptionsModal.js b/frontend/src/Artist/Index/Posters/Options/ArtistIndexPosterOptionsModal.js new file mode 100644 index 000000000..e1b0a257a --- /dev/null +++ b/frontend/src/Artist/Index/Posters/Options/ArtistIndexPosterOptionsModal.js @@ -0,0 +1,25 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import Modal from 'Components/Modal/Modal'; +import ArtistIndexPosterOptionsModalContentConnector from './ArtistIndexPosterOptionsModalContentConnector'; + +function ArtistIndexPosterOptionsModal({ isOpen, onModalClose, ...otherProps }) { + return ( + + + + ); +} + +ArtistIndexPosterOptionsModal.propTypes = { + isOpen: PropTypes.bool.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default ArtistIndexPosterOptionsModal; diff --git a/frontend/src/Artist/Index/Posters/Options/ArtistIndexPosterOptionsModalContent.js b/frontend/src/Artist/Index/Posters/Options/ArtistIndexPosterOptionsModalContent.js new file mode 100644 index 000000000..6e0a5aa54 --- /dev/null +++ b/frontend/src/Artist/Index/Posters/Options/ArtistIndexPosterOptionsModalContent.js @@ -0,0 +1,173 @@ +import _ from 'lodash'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { inputTypes } from 'Helpers/Props'; +import Button from 'Components/Link/Button'; +import Form from 'Components/Form/Form'; +import FormGroup from 'Components/Form/FormGroup'; +import FormLabel from 'Components/Form/FormLabel'; +import FormInputGroup from 'Components/Form/FormInputGroup'; +import ModalContent from 'Components/Modal/ModalContent'; +import ModalHeader from 'Components/Modal/ModalHeader'; +import ModalBody from 'Components/Modal/ModalBody'; +import ModalFooter from 'Components/Modal/ModalFooter'; + +const posterSizeOptions = [ + { key: 'small', value: 'Small' }, + { key: 'medium', value: 'Medium' }, + { key: 'large', value: 'Large' } +]; + +class ArtistIndexPosterOptionsModalContent extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + detailedProgressBar: props.detailedProgressBar, + size: props.size, + showTitle: props.showTitle, + showQualityProfile: props.showQualityProfile + }; + } + + componentDidUpdate(prevProps) { + const { + detailedProgressBar, + size, + showTitle, + showQualityProfile + } = this.props; + + const state = {}; + + if (detailedProgressBar !== prevProps.detailedProgressBar) { + state.detailedProgressBar = detailedProgressBar; + } + + if (size !== prevProps.size) { + state.size = size; + } + + if (showTitle !== prevProps.showTitle) { + state.showTitle = showTitle; + } + + if (showQualityProfile !== prevProps.showQualityProfile) { + state.showQualityProfile = showQualityProfile; + } + + if (!_.isEmpty(state)) { + this.setState(state); + } + } + + // + // Listeners + + onChangePosterOption = ({ name, value }) => { + this.setState({ + [name]: value + }, () => { + this.props.onChangePosterOption({ [name]: value }); + }); + } + + // + // Render + + render() { + const { + onModalClose + } = this.props; + + const { + detailedProgressBar, + size, + showTitle, + showQualityProfile + } = this.state; + + return ( + + + Poster Options + + + +
+ + Poster Size + + + + + + Detailed Progress Bar + + + + + + Show Name + + + + + + Show Quality Profile + + + +
+
+ + + + +
+ ); + } +} + +ArtistIndexPosterOptionsModalContent.propTypes = { + size: PropTypes.string.isRequired, + showTitle: PropTypes.bool.isRequired, + showQualityProfile: PropTypes.bool.isRequired, + detailedProgressBar: PropTypes.bool.isRequired, + onChangePosterOption: PropTypes.func.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default ArtistIndexPosterOptionsModalContent; diff --git a/frontend/src/Artist/Index/Posters/Options/ArtistIndexPosterOptionsModalContentConnector.js b/frontend/src/Artist/Index/Posters/Options/ArtistIndexPosterOptionsModalContentConnector.js new file mode 100644 index 000000000..72af268ad --- /dev/null +++ b/frontend/src/Artist/Index/Posters/Options/ArtistIndexPosterOptionsModalContentConnector.js @@ -0,0 +1,23 @@ +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import { setArtistPosterOption } from 'Store/Actions/artistIndexActions'; +import ArtistIndexPosterOptionsModalContent from './ArtistIndexPosterOptionsModalContent'; + +function createMapStateToProps() { + return createSelector( + (state) => state.artistIndex, + (artistIndex) => { + return artistIndex.posterOptions; + } + ); +} + +function createMapDispatchToProps(dispatch, props) { + return { + onChangePosterOption(payload) { + dispatch(setArtistPosterOption(payload)); + } + }; +} + +export default connect(createMapStateToProps, createMapDispatchToProps)(ArtistIndexPosterOptionsModalContent); diff --git a/frontend/src/Artist/Index/ProgressBar/ArtistIndexProgressBar.css b/frontend/src/Artist/Index/ProgressBar/ArtistIndexProgressBar.css new file mode 100644 index 000000000..dbf3499ab --- /dev/null +++ b/frontend/src/Artist/Index/ProgressBar/ArtistIndexProgressBar.css @@ -0,0 +1,14 @@ +.progress { + composes: container from 'Components/ProgressBar.css'; + + border-radius: 0; + background-color: #5b5b5b; + color: $white; + transition: width 200ms ease; +} + +.progressBar { + composes: progressBar from 'Components/ProgressBar.css'; + + transition: width 200ms ease; +} diff --git a/frontend/src/Artist/Index/ProgressBar/ArtistIndexProgressBar.js b/frontend/src/Artist/Index/ProgressBar/ArtistIndexProgressBar.js new file mode 100644 index 000000000..89d85f80a --- /dev/null +++ b/frontend/src/Artist/Index/ProgressBar/ArtistIndexProgressBar.js @@ -0,0 +1,45 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import getProgressBarKind from 'Utilities/Series/getProgressBarKind'; +import { sizes } from 'Helpers/Props'; +import ProgressBar from 'Components/ProgressBar'; +import styles from './ArtistIndexProgressBar.css'; + +function ArtistIndexProgressBar(props) { + const { + monitored, + status, + trackCount, + trackFileCount, + posterWidth, + detailedProgressBar + } = props; + + const progress = trackCount ? trackFileCount / trackCount * 100 : 100; + const text = `${trackFileCount} / ${trackCount}`; + + return ( + + ); +} + +ArtistIndexProgressBar.propTypes = { + monitored: PropTypes.bool.isRequired, + status: PropTypes.string.isRequired, + trackCount: PropTypes.number.isRequired, + trackFileCount: PropTypes.number.isRequired, + posterWidth: PropTypes.number.isRequired, + detailedProgressBar: PropTypes.bool.isRequired +}; + +export default ArtistIndexProgressBar; diff --git a/frontend/src/Artist/Index/Table/ArtistIndexActionsCell.js b/frontend/src/Artist/Index/Table/ArtistIndexActionsCell.js new file mode 100644 index 000000000..3f37cd56a --- /dev/null +++ b/frontend/src/Artist/Index/Table/ArtistIndexActionsCell.js @@ -0,0 +1,102 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { icons } from 'Helpers/Props'; +import IconButton from 'Components/Link/IconButton'; +import SpinnerIconButton from 'Components/Link/SpinnerIconButton'; +import VirtualTableRowCell from 'Components/Table/Cells/VirtualTableRowCell'; +import EditArtistModalConnector from 'Artist/Edit/EditArtistModalConnector'; +import DeleteArtistModal from 'Artist/Delete/DeleteArtistModal'; + +class ArtistIndexActionsCell extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + isEditArtistModalOpen: false, + isDeleteArtistModalOpen: false + }; + } + + // + // Listeners + + onEditArtistPress = () => { + this.setState({ isEditArtistModalOpen: true }); + } + + onEditArtistModalClose = () => { + this.setState({ isEditArtistModalOpen: false }); + } + + onDeleteArtistPress = () => { + this.setState({ + isEditArtistModalOpen: false, + isDeleteArtistModalOpen: true + }); + } + + onDeleteArtistModalClose = () => { + this.setState({ isDeleteArtistModalOpen: false }); + } + + // + // Render + + render() { + const { + id, + isRefreshingArtist, + onRefreshArtistPress, + ...otherProps + } = this.props; + + const { + isEditArtistModalOpen, + isDeleteArtistModalOpen + } = this.state; + + return ( + + + + + + + + + + ); + } +} + +ArtistIndexActionsCell.propTypes = { + id: PropTypes.number.isRequired, + isRefreshingArtist: PropTypes.bool.isRequired, + onRefreshArtistPress: PropTypes.func.isRequired +}; + +export default ArtistIndexActionsCell; diff --git a/frontend/src/Artist/Index/Table/ArtistIndexHeader.css b/frontend/src/Artist/Index/Table/ArtistIndexHeader.css new file mode 100644 index 000000000..a128e8ee1 --- /dev/null +++ b/frontend/src/Artist/Index/Table/ArtistIndexHeader.css @@ -0,0 +1,76 @@ +.status { + composes: headerCell from 'Components/Table/VirtualTableHeaderCell.css'; + + flex: 0 0 60px; +} + +.sortName { + composes: headerCell from 'Components/Table/VirtualTableHeaderCell.css'; + + flex: 4 0 110px; +} + +.qualityProfileId, +.languageProfileId { + composes: headerCell from 'Components/Table/VirtualTableHeaderCell.css'; + + flex: 1 0 125px; +} + +.nextAiring, +.previousAiring, +.added { + composes: headerCell from 'Components/Table/VirtualTableHeaderCell.css'; + + flex: 0 0 180px; +} + +.albumCount { + composes: headerCell from 'Components/Table/VirtualTableHeaderCell.css'; + + flex: 0 0 100px; +} + +.trackProgress, +.latestAlbum { + composes: headerCell from 'Components/Table/VirtualTableHeaderCell.css'; + + flex: 0 0 150px; +} + +.artistType, +.trackCount { + composes: headerCell from 'Components/Table/VirtualTableHeaderCell.css'; + + flex: 0 0 120px; +} + +.path { + composes: headerCell from 'Components/Table/VirtualTableHeaderCell.css'; + + flex: 1 0 150px; +} + +.sizeOnDisk { + composes: headerCell from 'Components/Table/VirtualTableHeaderCell.css'; + + flex: 0 0 110px; +} + +.tags { + composes: headerCell from 'Components/Table/VirtualTableHeaderCell.css'; + + flex: 1 0 60px; +} + +.useSceneNumbering { + composes: headerCell from 'Components/Table/VirtualTableHeaderCell.css'; + + flex: 0 0 145px; +} + +.actions { + composes: headerCell from 'Components/Table/VirtualTableHeaderCell.css'; + + flex: 0 0 70px; +} diff --git a/frontend/src/Artist/Index/Table/ArtistIndexHeader.js b/frontend/src/Artist/Index/Table/ArtistIndexHeader.js new file mode 100644 index 000000000..8c1bd8682 --- /dev/null +++ b/frontend/src/Artist/Index/Table/ArtistIndexHeader.js @@ -0,0 +1,106 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { icons } from 'Helpers/Props'; +import IconButton from 'Components/Link/IconButton'; +import VirtualTableHeader from 'Components/Table/VirtualTableHeader'; +import VirtualTableHeaderCell from 'Components/Table/VirtualTableHeaderCell'; +import TableOptionsModal from 'Components/Table/TableOptions/TableOptionsModal'; +import styles from './ArtistIndexHeader.css'; + +class ArtistIndexHeader extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + isTableOptionsModalOpen: false + }; + } + + // + // Listeners + + onTableOptionsPress = () => { + this.setState({ isTableOptionsModalOpen: true }); + } + + onTableOptionsModalClose = () => { + this.setState({ isTableOptionsModalOpen: false }); + } + + // + // Render + + render() { + const { + columns, + onTableOptionChange, + ...otherProps + } = this.props; + + return ( + + { + columns.map((column) => { + const { + name, + label, + isSortable, + isVisible + } = column; + + if (!isVisible) { + return null; + } + + if (name === 'actions') { + return ( + + + + ); + } + + return ( + + {label} + + ); + }) + } + + + + ); + } +} + +ArtistIndexHeader.propTypes = { + columns: PropTypes.arrayOf(PropTypes.object).isRequired, + onTableOptionChange: PropTypes.func.isRequired +}; + +export default ArtistIndexHeader; diff --git a/frontend/src/Artist/Index/Table/ArtistIndexHeaderConnector.js b/frontend/src/Artist/Index/Table/ArtistIndexHeaderConnector.js new file mode 100644 index 000000000..37ddd9ef3 --- /dev/null +++ b/frontend/src/Artist/Index/Table/ArtistIndexHeaderConnector.js @@ -0,0 +1,13 @@ +import { connect } from 'react-redux'; +import { setArtistTableOption } from 'Store/Actions/artistIndexActions'; +import ArtistIndexHeader from './ArtistIndexHeader'; + +function createMapDispatchToProps(dispatch, props) { + return { + onTableOptionChange(payload) { + dispatch(setArtistTableOption(payload)); + } + }; +} + +export default connect(undefined, createMapDispatchToProps)(ArtistIndexHeader); diff --git a/frontend/src/Artist/Index/Table/ArtistIndexRow.css b/frontend/src/Artist/Index/Table/ArtistIndexRow.css new file mode 100644 index 000000000..2d2d94a59 --- /dev/null +++ b/frontend/src/Artist/Index/Table/ArtistIndexRow.css @@ -0,0 +1,85 @@ +.status { + composes: cell from 'Components/Table/Cells/VirtualTableRowCell.css'; + + flex: 0 0 60px; +} + +.sortName { + composes: cell from 'Components/Table/Cells/VirtualTableRowCell.css'; + + flex: 4 0 110px; +} + +.qualityProfileId, +.languageProfileId { + composes: cell from 'Components/Table/Cells/VirtualTableRowCell.css'; + + flex: 1 0 125px; +} + +.nextAiring, +.previousAiring, +.added { + composes: cell from 'Components/Table/Cells/VirtualTableRowCell.css'; + + flex: 0 0 180px; +} + +.albumCount { + composes: cell from 'Components/Table/Cells/VirtualTableRowCell.css'; + + flex: 0 0 100px; +} + +.trackProgress, +.latestAlbum { + composes: cell from 'Components/Table/Cells/VirtualTableRowCell.css'; + + display: flex; + justify-content: center; + flex: 0 0 150px; + flex-direction: column; +} + +.artistType, +.trackCount { + composes: cell from 'Components/Table/Cells/VirtualTableRowCell.css'; + + flex: 0 0 120px; +} + +.path { + composes: cell from 'Components/Table/Cells/VirtualTableRowCell.css'; + + flex: 1 0 150px; +} + +.sizeOnDisk { + composes: cell from 'Components/Table/Cells/VirtualTableRowCell.css'; + + flex: 0 0 110px; +} + +.tags { + composes: cell from 'Components/Table/Cells/VirtualTableRowCell.css'; + + flex: 1 0 60px; +} + +.useSceneNumbering { + composes: cell from 'Components/Table/Cells/VirtualTableRowCell.css'; + + flex: 0 0 145px; +} + +.actions { + composes: cell from 'Components/Table/Cells/VirtualTableRowCell.css'; + + flex: 0 0 70px; +} + +.checkInput { + composes: input from 'Components/Form/CheckInput.css'; + + margin-top: 0; +} diff --git a/frontend/src/Artist/Index/Table/ArtistIndexRow.js b/frontend/src/Artist/Index/Table/ArtistIndexRow.js new file mode 100644 index 000000000..8f6cf1c05 --- /dev/null +++ b/frontend/src/Artist/Index/Table/ArtistIndexRow.js @@ -0,0 +1,372 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import getProgressBarKind from 'Utilities/Series/getProgressBarKind'; +import formatBytes from 'Utilities/Number/formatBytes'; +import { icons } from 'Helpers/Props'; +import IconButton from 'Components/Link/IconButton'; +import SpinnerIconButton from 'Components/Link/SpinnerIconButton'; +import ProgressBar from 'Components/ProgressBar'; +import TagListConnector from 'Components/TagListConnector'; +// import CheckInput from 'Components/Form/CheckInput'; +import VirtualTableRow from 'Components/Table/VirtualTableRow'; +import VirtualTableRowCell from 'Components/Table/Cells/VirtualTableRowCell'; +import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector'; +import ArtistNameLink from 'Artist/ArtistNameLink'; +import EditArtistModalConnector from 'Artist/Edit/EditArtistModalConnector'; +import DeleteArtistModal from 'Artist/Delete/DeleteArtistModal'; +import ArtistStatusCell from './ArtistStatusCell'; +import styles from './ArtistIndexRow.css'; + +class ArtistIndexRow extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + isEditArtistModalOpen: false, + isDeleteArtistModalOpen: false + }; + } + + onEditArtistPress = () => { + this.setState({ isEditArtistModalOpen: true }); + } + + onEditArtistModalClose = () => { + this.setState({ isEditArtistModalOpen: false }); + } + + onDeleteArtistPress = () => { + this.setState({ + isEditArtistModalOpen: false, + isDeleteArtistModalOpen: true + }); + } + + onDeleteArtistModalClose = () => { + this.setState({ isDeleteArtistModalOpen: false }); + } + + onUseSceneNumberingChange = () => { + // Mock handler to satisfy `onChange` being required for `CheckInput`. + // + } + + // + // Render + + render() { + const { + style, + id, + monitored, + status, + artistName, + nameSlug, + artistType, + qualityProfile, + languageProfile, + nextAiring, + previousAiring, + added, + albumCount, + trackCount, + trackFileCount, + totalTrackCount, + latestAlbum, + path, + sizeOnDisk, + tags, + // useSceneNumbering, + columns, + isRefreshingArtist, + onRefreshArtistPress + } = this.props; + + const { + isEditArtistModalOpen, + isDeleteArtistModalOpen + } = this.state; + + return ( + + { + columns.map((column) => { + const { + name, + isVisible + } = column; + + if (!isVisible) { + return null; + } + + if (name === 'status') { + return ( + + ); + } + + if (name === 'sortName') { + return ( + + + + ); + } + + if (name === 'artistType') { + return ( + + {artistType} + + ); + } + + if (name === 'qualityProfileId') { + return ( + + {qualityProfile.name} + + ); + } + + if (name === 'languageProfileId') { + return ( + + {languageProfile.name} + + ); + } + + if (name === 'nextAiring') { + return ( + + ); + } + + if (name === 'previousAiring') { + return ( + + ); + } + + if (name === 'added') { + return ( + + ); + } + + if (name === 'albumCount') { + return ( + + {albumCount} + + ); + } + + if (name === 'trackProgress') { + const progress = trackCount ? trackFileCount / trackCount * 100 : 100; + + return ( + + + + ); + } + + if (name === 'latestAlbum') { + const albumStatistics = latestAlbum.statistics; + const progress = albumStatistics.trackCount ? albumStatistics.trackFileCount / albumStatistics.trackCount * 100 : 100; + + return ( + + + + ); + } + + if (name === 'trackCount') { + return ( + + {totalTrackCount} + + ); + } + + if (name === 'path') { + return ( + + {path} + + ); + } + + if (name === 'sizeOnDisk') { + return ( + + {formatBytes(sizeOnDisk)} + + ); + } + + if (name === 'tags') { + return ( + + + + ); + } + + if (name === 'actions') { + return ( + + + + + + ); + } + + return null; + }) + } + + + + + + ); + } +} + +ArtistIndexRow.propTypes = { + style: PropTypes.object.isRequired, + id: PropTypes.number.isRequired, + monitored: PropTypes.bool.isRequired, + status: PropTypes.string.isRequired, + artistName: PropTypes.string.isRequired, + nameSlug: PropTypes.string.isRequired, + artistType: PropTypes.string, + qualityProfile: PropTypes.object.isRequired, + languageProfile: PropTypes.object.isRequired, + nextAiring: PropTypes.string, + previousAiring: PropTypes.string, + added: PropTypes.string, + albumCount: PropTypes.number.isRequired, + trackCount: PropTypes.number, + trackFileCount: PropTypes.number, + totalTrackCount: PropTypes.number, + latestAlbum: PropTypes.object, + path: PropTypes.string.isRequired, + sizeOnDisk: PropTypes.number, + tags: PropTypes.arrayOf(PropTypes.number).isRequired, + // useSceneNumbering: PropTypes.bool.isRequired, + columns: PropTypes.arrayOf(PropTypes.object).isRequired, + isRefreshingArtist: PropTypes.bool.isRequired, + onRefreshArtistPress: PropTypes.func.isRequired +}; + +ArtistIndexRow.defaultProps = { + trackCount: 0, + trackFileCount: 0 +}; + +export default ArtistIndexRow; diff --git a/frontend/src/Artist/Index/Table/ArtistIndexTable.css b/frontend/src/Artist/Index/Table/ArtistIndexTable.css new file mode 100644 index 000000000..e46160a96 --- /dev/null +++ b/frontend/src/Artist/Index/Table/ArtistIndexTable.css @@ -0,0 +1,5 @@ +.tableContainer { + composes: tableContainer from 'Components/Table/VirtualTable.css'; + + flex: 1 0 auto; +} diff --git a/frontend/src/Artist/Index/Table/ArtistIndexTable.js b/frontend/src/Artist/Index/Table/ArtistIndexTable.js new file mode 100644 index 000000000..acd9b2fa7 --- /dev/null +++ b/frontend/src/Artist/Index/Table/ArtistIndexTable.js @@ -0,0 +1,143 @@ +import _ from 'lodash'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { sortDirections } from 'Helpers/Props'; +import VirtualTable from 'Components/Table/VirtualTable'; +import ArtistIndexItemConnector from 'Artist/Index/ArtistIndexItemConnector'; +import ArtistIndexHeaderConnector from './ArtistIndexHeaderConnector'; +import ArtistIndexRow from './ArtistIndexRow'; +import styles from './ArtistIndexTable.css'; + +class ArtistIndexTable extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this._table = null; + } + + componentDidUpdate(prevProps) { + const { + columns, + filterKey, + filterValue, + sortKey, + sortDirection + } = this.props; + + if (prevProps.columns !== columns || + prevProps.filterKey !== filterKey || + prevProps.filterValue !== filterValue || + prevProps.sortKey !== sortKey || + prevProps.sortDirection !== sortDirection + ) { + this._table.forceUpdateGrid(); + } + } + + // + // Control + + scrollToFirstCharacter(character) { + const items = this.props.items; + + const row = _.findIndex(items, (item) => { + const firstCharacter = item.sortName.charAt(0); + + if (character === '#') { + return !isNaN(firstCharacter); + } + + return firstCharacter === character; + }); + + if (row != null) { + this._table.scrollToRow(row); + } + } + + setTableRef = (ref) => { + this._table = ref; + } + + rowRenderer = ({ key, rowIndex, style }) => { + const { + items, + columns + } = this.props; + + const artist = items[rowIndex]; + + return ( + + ); + } + + // + // Render + + render() { + const { + items, + columns, + sortKey, + sortDirection, + isSmallScreen, + scrollTop, + contentBody, + onSortPress, + onRender, + onScroll + } = this.props; + + return ( + + } + onRender={onRender} + onScroll={onScroll} + /> + ); + } +} + +ArtistIndexTable.propTypes = { + items: PropTypes.arrayOf(PropTypes.object).isRequired, + columns: PropTypes.arrayOf(PropTypes.object).isRequired, + filterKey: PropTypes.string, + filterValue: PropTypes.oneOfType([PropTypes.bool, PropTypes.number, PropTypes.string]), + sortKey: PropTypes.string, + sortDirection: PropTypes.oneOf(sortDirections.all), + scrollTop: PropTypes.number.isRequired, + contentBody: PropTypes.object.isRequired, + isSmallScreen: PropTypes.bool.isRequired, + onSortPress: PropTypes.func.isRequired, + onRender: PropTypes.func.isRequired, + onScroll: PropTypes.func.isRequired +}; + +export default ArtistIndexTable; diff --git a/frontend/src/Artist/Index/Table/ArtistIndexTableConnector.js b/frontend/src/Artist/Index/Table/ArtistIndexTableConnector.js new file mode 100644 index 000000000..c49c0cf07 --- /dev/null +++ b/frontend/src/Artist/Index/Table/ArtistIndexTableConnector.js @@ -0,0 +1,34 @@ +import { createSelector } from 'reselect'; +import connectSection from 'Store/connectSection'; +import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector'; +import { setArtistSort } from 'Store/Actions/artistIndexActions'; +import ArtistIndexTable from './ArtistIndexTable'; + +function createMapStateToProps() { + return createSelector( + (state) => state.app.dimensions, + createClientSideCollectionSelector(), + (dimensions, artist) => { + return { + isSmallScreen: dimensions.isSmallScreen, + ...artist + }; + } + ); +} + +function createMapDispatchToProps(dispatch, props) { + return { + onSortPress(sortKey) { + dispatch(setArtistSort({ sortKey })); + } + }; +} + +export default connectSection( + createMapStateToProps, + createMapDispatchToProps, + undefined, + { withRef: true }, + { section: 'artist', uiSection: 'artistIndex' } +)(ArtistIndexTable); diff --git a/frontend/src/Artist/Index/Table/ArtistStatusCell.css b/frontend/src/Artist/Index/Table/ArtistStatusCell.css new file mode 100644 index 000000000..d19ddf05b --- /dev/null +++ b/frontend/src/Artist/Index/Table/ArtistStatusCell.css @@ -0,0 +1,9 @@ +.status { + composes: cell from 'Components/Table/Cells/TableRowCell.css'; + + width: 60px; +} + +.statusIcon { + width: 20px; +} diff --git a/frontend/src/Artist/Index/Table/ArtistStatusCell.js b/frontend/src/Artist/Index/Table/ArtistStatusCell.js new file mode 100644 index 000000000..ab3042a10 --- /dev/null +++ b/frontend/src/Artist/Index/Table/ArtistStatusCell.js @@ -0,0 +1,50 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import { icons } from 'Helpers/Props'; +import Icon from 'Components/Icon'; +import VirtualTableRowCell from 'Components/Table/Cells/TableRowCell'; +import styles from './ArtistStatusCell.css'; + +function ArtistStatusCell(props) { + const { + className, + monitored, + status, + component: Component, + ...otherProps + } = props; + + return ( + + + + + + ); +} + +ArtistStatusCell.propTypes = { + className: PropTypes.string.isRequired, + monitored: PropTypes.bool.isRequired, + status: PropTypes.string.isRequired, + component: PropTypes.func +}; + +ArtistStatusCell.defaultProps = { + className: styles.status, + component: VirtualTableRowCell +}; + +export default ArtistStatusCell; diff --git a/frontend/src/Artist/Index/Table/artistIndexCellRenderers.js b/frontend/src/Artist/Index/Table/artistIndexCellRenderers.js new file mode 100644 index 000000000..b5ea45e4f --- /dev/null +++ b/frontend/src/Artist/Index/Table/artistIndexCellRenderers.js @@ -0,0 +1,125 @@ +import React from 'react'; +import getProgressBarKind from 'Utilities/Series/getProgressBarKind'; +import ProgressBar from 'Components/ProgressBar'; +import VirtualTableRowCell from 'Components/Table/Cells/VirtualTableRowCell'; +import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector'; +import QualityProfileNameConnector from 'Settings/Profiles/Quality/QualityProfileNameConnector'; +import ArtistNameLink from 'Artist/ArtistNameLink'; +import ArtistIndexItemConnector from 'Artist/Index/ArtistIndexItemConnector'; +import ArtistIndexActionsCell from './ArtistIndexActionsCell'; +import ArtistStatusCell from './ArtistStatusCell'; + +export default function artistIndexCellRenderers(cellProps) { + const { + cellKey, + dataKey, + rowData, + ...otherProps + } = cellProps; + + const { + id, + monitored, + status, + name, + nameSlug, + qualityProfileId, + nextAiring, + previousAiring, + albumCount, + trackCount, + trackFileCount + } = rowData; + + const progress = trackCount ? trackFileCount / trackCount * 100 : 100; + + if (dataKey === 'status') { + return ( + + ); + } + + if (dataKey === 'sortName') { + return ( + + + + + ); + } + + if (dataKey === 'qualityProfileId') { + return ( + + + + ); + } + + if (dataKey === 'nextAiring') { + return ( + + ); + } + + if (dataKey === 'albumCount') { + return ( + + {albumCount} + + ); + } + + if (dataKey === 'trackProgress') { + return ( + + + + ); + } + + if (dataKey === 'actions') { + return ( + + ); + } +} diff --git a/frontend/src/Artist/NoArtist.css b/frontend/src/Artist/NoArtist.css new file mode 100644 index 000000000..38a01f391 --- /dev/null +++ b/frontend/src/Artist/NoArtist.css @@ -0,0 +1,11 @@ +.message { + margin-top: 10px; + margin-bottom: 30px; + text-align: center; + font-size: 20px; +} + +.buttonContainer { + margin-top: 20px; + text-align: center; +} diff --git a/frontend/src/Artist/NoArtist.js b/frontend/src/Artist/NoArtist.js new file mode 100644 index 000000000..b6e90cf63 --- /dev/null +++ b/frontend/src/Artist/NoArtist.js @@ -0,0 +1,34 @@ +import React from 'react'; +import { kinds } from 'Helpers/Props'; +import Button from 'Components/Link/Button'; +import styles from './NoArtist.css'; + +function NoArtist() { + return ( +
+
+ No artist found, to get started you'll want to add a new artist or import some existing ones. +
+ +
+ +
+ +
+ +
+
+ ); +} + +export default NoArtist; diff --git a/frontend/src/Calendar/Agenda/Agenda.css b/frontend/src/Calendar/Agenda/Agenda.css new file mode 100644 index 000000000..0304d9db5 --- /dev/null +++ b/frontend/src/Calendar/Agenda/Agenda.css @@ -0,0 +1,3 @@ +.agenda { + margin-top: 10px; +} diff --git a/frontend/src/Calendar/Agenda/Agenda.js b/frontend/src/Calendar/Agenda/Agenda.js new file mode 100644 index 000000000..33d02cd79 --- /dev/null +++ b/frontend/src/Calendar/Agenda/Agenda.js @@ -0,0 +1,38 @@ +import moment from 'moment'; +import PropTypes from 'prop-types'; +import React from 'react'; +import AgendaEventConnector from './AgendaEventConnector'; +import styles from './Agenda.css'; + +function Agenda(props) { + const { + items + } = props; + + return ( +
+ { + items.map((item, index) => { + const momentDate = moment(item.releaseDate); + const showDate = index === 0 || + !moment(items[index - 1].releaseDate).isSame(momentDate, 'day'); + + return ( + + ); + }) + } +
+ ); +} + +Agenda.propTypes = { + items: PropTypes.arrayOf(PropTypes.object).isRequired +}; + +export default Agenda; diff --git a/frontend/src/Calendar/Agenda/AgendaConnector.js b/frontend/src/Calendar/Agenda/AgendaConnector.js new file mode 100644 index 000000000..b6f238873 --- /dev/null +++ b/frontend/src/Calendar/Agenda/AgendaConnector.js @@ -0,0 +1,14 @@ +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import Agenda from './Agenda'; + +function createMapStateToProps() { + return createSelector( + (state) => state.calendar, + (calendar) => { + return calendar; + } + ); +} + +export default connect(createMapStateToProps)(Agenda); diff --git a/frontend/src/Calendar/Agenda/AgendaEvent.css b/frontend/src/Calendar/Agenda/AgendaEvent.css new file mode 100644 index 000000000..f85b87cef --- /dev/null +++ b/frontend/src/Calendar/Agenda/AgendaEvent.css @@ -0,0 +1,105 @@ +.event { + display: flex; + overflow-x: hidden; + padding: 5px; + border-bottom: 1px solid $borderColor; + font-size: 14px; + + &:hover { + background-color: $tableRowHoverBackgroundColor; + } +} + +.status { + width: 10px; + border-left-width: 4px; + border-left-style: solid; +} + +.date { + flex: 0 0 250px; + font-weight: bold; +} + +.time { + flex: 0 0 120px; + margin-right: 10px; +} + +.artistName, +.albumTitle { + @add-mixin truncate; + + flex: 0 1 300px; + margin-right: 10px; +} + +.albumTitle { + flex: 1 1 1px; +} + +.seasonEpisodeNumber { + flex: 0 0 100px; +} + +.episodeSeparator { + display: none; +} + +.absoluteEpisodeNumber { + margin-left: 3px; +} + +/* + * Status + */ + +.downloaded { + composes: downloaded from 'Calendar/Events/CalendarEvent.css'; +} + +.downloading { + composes: downloading from 'Calendar/Events/CalendarEvent.css'; +} + +.unmonitored { + composes: unmonitored from 'Calendar/Events/CalendarEvent.css'; +} + +.missing { + composes: missing from 'Calendar/Events/CalendarEvent.css'; +} + +.unreleased { + composes: unreleased from 'Calendar/Events/CalendarEvent.css'; +} + +@media only screen and (max-width: $breakpointSmall) { + .event { + position: relative; + flex-wrap: wrap; + padding-left: 10px; + } + + .status { + position: absolute; + top: 7%; + left: 0; + height: 86%; + } + + .date, + .time, + .artistName { + flex: 0 0 100%; + } + + .seasonEpisodeNumber { + flex: 0 0 auto; + } + + .episodeSeparator { + display: inline-block; + margin: 0 5px; + } +} diff --git a/frontend/src/Calendar/Agenda/AgendaEvent.js b/frontend/src/Calendar/Agenda/AgendaEvent.js new file mode 100644 index 000000000..44fac0a88 --- /dev/null +++ b/frontend/src/Calendar/Agenda/AgendaEvent.js @@ -0,0 +1,142 @@ +import moment from 'moment'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import classNames from 'classnames'; +import formatTime from 'Utilities/Date/formatTime'; +import padNumber from 'Utilities/Number/padNumber'; +import { icons } from 'Helpers/Props'; +import getStatusStyle from 'Calendar/getStatusStyle'; +import Icon from 'Components/Icon'; +import Link from 'Components/Link/Link'; +import episodeEntities from 'Album/episodeEntities'; +import EpisodeDetailsModal from 'Album/EpisodeDetailsModal'; +import CalendarEventQueueDetails from 'Calendar/Events/CalendarEventQueueDetails'; +import styles from './AgendaEvent.css'; + +class AgendaEvent extends Component { + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + isDetailsModalOpen: false + }; + } + + // + // Listeners + + onPress = () => { + this.setState({ isDetailsModalOpen: true }); + } + + onDetailsModalClose = () => { + this.setState({ isDetailsModalOpen: false }); + } + + // + // Render + + render() { + const { + id, + artist, + title, + releaseDate, + monitored, + hasFile, + grabbed, + queueItem, + showDate, + timeFormat, + longDateFormat + } = this.props; + + const startTime = moment(releaseDate); + // const endTime = startTime.add(artist.runtime, 'minutes'); + const downloading = !!(queueItem || grabbed); + const isMonitored = artist.monitored && monitored; + const statusStyle = getStatusStyle(id, hasFile, downloading, startTime, isMonitored); + + return ( +
+ +
+ { + showDate && + startTime.format(longDateFormat) + } +
+ +
+ +
+ {formatTime(releaseDate, timeFormat)} +
+ +
+ {artist.artistName} +
+ +
-
+ +
+ {title} +
+ + { + !!queueItem && + + } + + { + !queueItem && grabbed && + + } + + + +
+ ); + } +} + +AgendaEvent.propTypes = { + id: PropTypes.number.isRequired, + artist: PropTypes.object.isRequired, + title: PropTypes.string.isRequired, + releaseDate: PropTypes.string.isRequired, + monitored: PropTypes.bool.isRequired, + hasFile: PropTypes.bool.isRequired, + grabbed: PropTypes.bool, + queueItem: PropTypes.object, + showDate: PropTypes.bool.isRequired, + timeFormat: PropTypes.string.isRequired, + longDateFormat: PropTypes.string.isRequired +}; + +export default AgendaEvent; diff --git a/frontend/src/Calendar/Agenda/AgendaEventConnector.js b/frontend/src/Calendar/Agenda/AgendaEventConnector.js new file mode 100644 index 000000000..76de94184 --- /dev/null +++ b/frontend/src/Calendar/Agenda/AgendaEventConnector.js @@ -0,0 +1,24 @@ +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import createArtistSelector from 'Store/Selectors/createArtistSelector'; +import createQueueItemSelector from 'Store/Selectors/createQueueItemSelector'; +import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector'; +import AgendaEvent from './AgendaEvent'; + +function createMapStateToProps() { + return createSelector( + createArtistSelector(), + createQueueItemSelector(), + createUISettingsSelector(), + (artist, queueItem, uiSettings) => { + return { + artist, + queueItem, + timeFormat: uiSettings.timeFormat, + longDateFormat: uiSettings.longDateFormat + }; + } + ); +} + +export default connect(createMapStateToProps)(AgendaEvent); diff --git a/frontend/src/Calendar/Calendar.css b/frontend/src/Calendar/Calendar.css new file mode 100644 index 000000000..37e6ff618 --- /dev/null +++ b/frontend/src/Calendar/Calendar.css @@ -0,0 +1,8 @@ +.calendar { + flex-grow: 1; + width: 100%; +} + +.calendarContent { + width: 100%; +} diff --git a/frontend/src/Calendar/Calendar.js b/frontend/src/Calendar/Calendar.js new file mode 100644 index 000000000..6ceb1f3bb --- /dev/null +++ b/frontend/src/Calendar/Calendar.js @@ -0,0 +1,64 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import LoadingIndicator from 'Components/Loading/LoadingIndicator'; +import * as calendarViews from './calendarViews'; +import CalendarHeaderConnector from './Header/CalendarHeaderConnector'; +import DaysOfWeekConnector from './Day/DaysOfWeekConnector'; +import CalendarDaysConnector from './Day/CalendarDaysConnector'; +import AgendaConnector from './Agenda/AgendaConnector'; +import styles from './Calendar.css'; + +class Calendar extends Component { + + // + // Render + + render() { + const { + isFetching, + isPopulated, + error, + view + } = this.props; + + return ( +
+ { + isFetching && !isPopulated && + + } + + { + !isFetching && !!error && +
Unable to load the calendar
+ } + + { + !error && isPopulated && view === calendarViews.AGENDA && +
+ + +
+ } + + { + !error && isPopulated && view !== calendarViews.AGENDA && +
+ + + +
+ } +
+ ); + } +} + +Calendar.propTypes = { + isFetching: PropTypes.bool.isRequired, + isPopulated: PropTypes.bool.isRequired, + error: PropTypes.object, + view: PropTypes.string.isRequired +}; + +export default Calendar; diff --git a/frontend/src/Calendar/CalendarConnector.js b/frontend/src/Calendar/CalendarConnector.js new file mode 100644 index 000000000..a5a99f853 --- /dev/null +++ b/frontend/src/Calendar/CalendarConnector.js @@ -0,0 +1,159 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import { registerPagePopulator, unregisterPagePopulator } from 'Utilities/pagePopulator'; +import hasDifferentItems from 'Utilities/Object/hasDifferentItems'; +import selectUniqueIds from 'Utilities/Object/selectUniqueIds'; +import * as calendarActions from 'Store/Actions/calendarActions'; +import { fetchTrackFiles, clearTrackFiles } from 'Store/Actions/trackFileActions'; +import { fetchQueueDetails, clearQueueDetails } from 'Store/Actions/queueActions'; +import Calendar from './Calendar'; + +const UPDATE_DELAY = 3600000; // 1 hour + +function createMapStateToProps() { + return createSelector( + (state) => state.calendar, + (calendar) => { + return calendar; + } + ); +} + +const mapDispatchToProps = { + ...calendarActions, + fetchTrackFiles, + clearTrackFiles, + fetchQueueDetails, + clearQueueDetails +}; + +class CalendarConnector extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.updateTimeoutId = null; + } + + componentDidMount() { + registerPagePopulator(this.repopulate); + this.props.gotoCalendarToday(); + this.scheduleUpdate(); + } + + componentDidUpdate(prevProps) { + const { + items, + time + } = this.props; + + if (hasDifferentItems(prevProps.items, items)) { + const albumIds = selectUniqueIds(items, 'id'); + // const trackFileIds = selectUniqueIds(items, 'trackFileId'); + + this.props.fetchQueueDetails({ albumIds }); + + // if (trackFileIds.length) { + // this.props.fetchTrackFiles({ trackFileIds }); + // } + } + + if (prevProps.time !== time) { + this.scheduleUpdate(); + } + } + + componentWillUnmount() { + unregisterPagePopulator(this.repopulate); + this.props.clearCalendar(); + this.props.clearQueueDetails(); + this.props.clearTrackFiles(); + this.clearUpdateTimeout(); + } + + // + // Control + repopulate = () => { + const { + time, + view + } = this.props; + + this.props.fetchQueueDetails({ time, view }); + this.props.fetchCalendar({ time, view }); + } + + scheduleUpdate = () => { + this.clearUpdateTimeout(); + + this.updateTimeoutId = setTimeout(this.updateCalendar, UPDATE_DELAY); + } + + clearUpdateTimeout = () => { + if (this.updateTimeoutId) { + clearTimeout(this.updateTimeoutId); + } + } + + updateCalendar = () => { + this.props.gotoCalendarToday(); + this.scheduleUpdate(); + } + + // + // Listeners + + onCalendarViewChange = (view) => { + this.props.setCalendarView({ view }); + } + + onTodayPress = () => { + this.props.gotoCalendarToday(); + } + + onPreviousPress = () => { + this.props.gotoCalendarPreviousRange(); + } + + onNextPress = () => { + this.props.gotoCalendarNextRange(); + } + + // + // Render + + render() { + return ( + + ); + } +} + +CalendarConnector.propTypes = { + time: PropTypes.string, + view: PropTypes.string.isRequired, + items: PropTypes.arrayOf(PropTypes.object).isRequired, + setCalendarView: PropTypes.func.isRequired, + gotoCalendarToday: PropTypes.func.isRequired, + gotoCalendarPreviousRange: PropTypes.func.isRequired, + gotoCalendarNextRange: PropTypes.func.isRequired, + clearCalendar: PropTypes.func.isRequired, + fetchCalendar: PropTypes.func.isRequired, + fetchTrackFiles: PropTypes.func.isRequired, + clearTrackFiles: PropTypes.func.isRequired, + fetchQueueDetails: PropTypes.func.isRequired, + clearQueueDetails: PropTypes.func.isRequired +}; + +export default connect(createMapStateToProps, mapDispatchToProps)(CalendarConnector); diff --git a/frontend/src/Calendar/CalendarPage.css b/frontend/src/Calendar/CalendarPage.css new file mode 100644 index 000000000..776f3100f --- /dev/null +++ b/frontend/src/Calendar/CalendarPage.css @@ -0,0 +1,14 @@ +.calendarPageBody { + composes: contentBody from 'Components/Page/PageContentBody.css'; + + display: flex; +} + +.calendarInnerPageBody { + composes: innerContentBody from 'Components/Page/PageContentBody.css'; + + display: flex; + flex-direction: column; + flex-grow: 1; + width: 100%; +} diff --git a/frontend/src/Calendar/CalendarPage.js b/frontend/src/Calendar/CalendarPage.js new file mode 100644 index 000000000..bf1e5cfe8 --- /dev/null +++ b/frontend/src/Calendar/CalendarPage.js @@ -0,0 +1,137 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import Measure from 'react-measure'; +import { align, icons } from 'Helpers/Props'; +import PageContent from 'Components/Page/PageContent'; +import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector'; +import PageToolbar from 'Components/Page/Toolbar/PageToolbar'; +import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection'; +import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton'; +import FilterMenu from 'Components/Menu/FilterMenu'; +import MenuContent from 'Components/Menu/MenuContent'; +import FilterMenuItem from 'Components/Menu/FilterMenuItem'; +import CalendarLinkModal from './iCal/CalendarLinkModal'; +import Legend from './Legend/Legend'; +import CalendarConnector from './CalendarConnector'; +import styles from './CalendarPage.css'; + +const MINIMUM_DAY_WIDTH = 120; + +class CalendarPage extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + isCalendarLinkModalOpen: false, + width: 0 + }; + } + + // + // Listeners + + onMeasure = ({ width }) => { + this.setState({ width }); + const days = Math.max(3, Math.min(7, Math.floor(width / MINIMUM_DAY_WIDTH))); + + this.props.onDaysCountChange(days); + } + + onFilterMenuItemPress = (filterKey, unmonitored) => { + this.props.onUnmonitoredChange(unmonitored); + } + + onGetCalendarLinkPress = () => { + this.setState({ isCalendarLinkModalOpen: true }); + } + + onGetCalendarLinkModalClose = () => { + this.setState({ isCalendarLinkModalOpen: false }); + } + + // + // Render + + render() { + const { + unmonitored, + colorImpairedMode + } = this.props; + + return ( + + + + + + + + + + + All + + + + Monitored Only + + + + + + + + + { + this.state.width > 0 ? + : +
+ } + + + + + + + + ); + } +} + +CalendarPage.propTypes = { + unmonitored: PropTypes.bool.isRequired, + colorImpairedMode: PropTypes.bool.isRequired, + onDaysCountChange: PropTypes.func.isRequired, + onUnmonitoredChange: PropTypes.func.isRequired +}; + +export default CalendarPage; diff --git a/frontend/src/Calendar/CalendarPageConnector.js b/frontend/src/Calendar/CalendarPageConnector.js new file mode 100644 index 000000000..59de66f74 --- /dev/null +++ b/frontend/src/Calendar/CalendarPageConnector.js @@ -0,0 +1,33 @@ +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import { setCalendarDaysCount, setCalendarIncludeUnmonitored } from 'Store/Actions/calendarActions'; +import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector'; +import CalendarPage from './CalendarPage'; + +function createMapStateToProps() { + return createSelector( + (state) => state.calendar, + createUISettingsSelector(), + (calendar, uiSettings) => { + return { + unmonitored: calendar.unmonitored, + showUpcoming: calendar.showUpcoming, + colorImpairedMode: uiSettings.enableColorImpairedMode + }; + } + ); +} + +function createMapDispatchToProps(dispatch, props) { + return { + onDaysCountChange(dayCount) { + dispatch(setCalendarDaysCount({ dayCount })); + }, + + onUnmonitoredChange(unmonitored) { + dispatch(setCalendarIncludeUnmonitored({ unmonitored })); + } + }; +} + +export default connect(createMapStateToProps, createMapDispatchToProps)(CalendarPage); diff --git a/frontend/src/Calendar/Day/CalendarDay.css b/frontend/src/Calendar/Day/CalendarDay.css new file mode 100644 index 000000000..1c7694f0b --- /dev/null +++ b/frontend/src/Calendar/Day/CalendarDay.css @@ -0,0 +1,25 @@ +.day { + flex: 1 0 14.28%; + overflow: hidden; + min-height: 70px; + border-bottom: 1px solid $borderColor; + border-left: 1px solid $borderColor; +} + +.isSingleDay { + width: 100%; +} + +.dayOfMonth { + padding-right: 5px; + border-bottom: 1px solid $borderColor; + text-align: right; +} + +.isToday { + background-color: $calendarTodayBackgroundColor; +} + +.isDifferentMonth { + color: $disabledColor; +} diff --git a/frontend/src/Calendar/Day/CalendarDay.js b/frontend/src/Calendar/Day/CalendarDay.js new file mode 100644 index 000000000..214c13ba5 --- /dev/null +++ b/frontend/src/Calendar/Day/CalendarDay.js @@ -0,0 +1,62 @@ +import moment from 'moment'; +import PropTypes from 'prop-types'; +import React from 'react'; +import classNames from 'classnames'; +import * as calendarViews from 'Calendar/calendarViews'; +import CalendarEventConnector from 'Calendar/Events/CalendarEventConnector'; +import styles from './CalendarDay.css'; + +function CalendarDay(props) { + const { + date, + time, + isTodaysDate, + events, + view, + onEventModalOpenToggle + } = props; + + return ( +
+ { + view === calendarViews.MONTH && +
+ {moment(date).date()} +
+ } +
+ { + events.map((event) => { + return ( + + ); + }) + } +
+
+ ); +} + +CalendarDay.propTypes = { + date: PropTypes.string.isRequired, + time: PropTypes.string.isRequired, + isTodaysDate: PropTypes.bool.isRequired, + events: PropTypes.arrayOf(PropTypes.object).isRequired, + view: PropTypes.string.isRequired, + onEventModalOpenToggle: PropTypes.func.isRequired +}; + +export default CalendarDay; diff --git a/frontend/src/Calendar/Day/CalendarDayConnector.js b/frontend/src/Calendar/Day/CalendarDayConnector.js new file mode 100644 index 000000000..467f799e7 --- /dev/null +++ b/frontend/src/Calendar/Day/CalendarDayConnector.js @@ -0,0 +1,55 @@ +import _ from 'lodash'; +import moment from 'moment'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import CalendarDay from './CalendarDay'; + +function createCalendarEventsConnector() { + return createSelector( + (state, { date }) => date, + (state) => state.calendar, + (date, calendar) => { + const filtered = _.filter(calendar.items, (item) => { + return moment(date).isSame(moment(item.releaseDate), 'day'); + }); + + return _.sortBy(filtered, (item) => moment(item.releaseDate).unix()); + } + ); +} + +function createMapStateToProps() { + return createSelector( + (state) => state.calendar, + createCalendarEventsConnector(), + (calendar, events) => { + return { + time: calendar.time, + view: calendar.view, + events + }; + } + ); +} + +class CalendarDayConnector extends Component { + + // + // Render + + render() { + return ( + + ); + } +} + +CalendarDayConnector.propTypes = { + date: PropTypes.string.isRequired +}; + +export default connect(createMapStateToProps)(CalendarDayConnector); diff --git a/frontend/src/Calendar/Day/CalendarDays.css b/frontend/src/Calendar/Day/CalendarDays.css new file mode 100644 index 000000000..22005e3e6 --- /dev/null +++ b/frontend/src/Calendar/Day/CalendarDays.css @@ -0,0 +1,14 @@ +.days { + display: flex; + border-right: 1px solid $borderColor; +} + +.day, +.week, +.forecast { + flex-wrap: nowrap; +} + +.month { + flex-wrap: wrap; +} diff --git a/frontend/src/Calendar/Day/CalendarDays.js b/frontend/src/Calendar/Day/CalendarDays.js new file mode 100644 index 000000000..0a1a36172 --- /dev/null +++ b/frontend/src/Calendar/Day/CalendarDays.js @@ -0,0 +1,164 @@ +import moment from 'moment'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import classNames from 'classnames'; +import isToday from 'Utilities/Date/isToday'; +import * as calendarViews from 'Calendar/calendarViews'; +import CalendarDayConnector from './CalendarDayConnector'; +import styles from './CalendarDays.css'; + +class CalendarDays extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this._touchStart = null; + + this.state = { + todaysDate: moment().startOf('day').toISOString(), + isEventModalOpen: false + }; + + this.updateTimeoutId = null; + } + + // Lifecycle + + componentDidMount() { + const view = this.props.view; + + if (view === calendarViews.MONTH) { + this.scheduleUpdate(); + } + + window.addEventListener('touchstart', this.onTouchStart); + window.addEventListener('touchend', this.onTouchEnd); + window.addEventListener('touchcancel', this.onTouchCancel); + window.addEventListener('touchmove', this.onTouchMove); + } + + componentWillUnmount() { + this.clearUpdateTimeout(); + + window.removeEventListener('touchstart', this.onTouchStart); + window.removeEventListener('touchend', this.onTouchEnd); + window.removeEventListener('touchcancel', this.onTouchCancel); + window.removeEventListener('touchmove', this.onTouchMove); + } + + // + // Control + + scheduleUpdate = () => { + this.clearUpdateTimeout(); + const todaysDate = moment().startOf('day'); + const diff = moment().diff(todaysDate.clone().add(1, 'day')); + + this.setState({ todaysDate: todaysDate.toISOString() }); + + this.updateTimeoutId = setTimeout(this.scheduleUpdate, diff); + } + + clearUpdateTimeout = () => { + if (this.updateTimeoutId) { + clearTimeout(this.updateTimeoutId); + } + } + + // + // Listeners + + onEventModalOpenToggle = (isEventModalOpen) => { + this.setState({ isEventModalOpen }); + } + + onTouchStart = (event) => { + const touches = event.touches; + const touchStart = touches[0].pageX; + + if (touches.length !== 1) { + return; + } + + if ( + touchStart < 50 || + this.props.isSidebarVisible || + this.state.isEventModalOpen + ) { + return; + } + + this._touchStart = touchStart; + } + + onTouchEnd = (event) => { + const touches = event.changedTouches; + const currentTouch = touches[0].pageX; + + if (!this._touchStart) { + return; + } + + if (currentTouch > this._touchStart && currentTouch - this._touchStart > 100) { + this.props.onNavigatePrevious(); + } else if (currentTouch < this._touchStart && this._touchStart - currentTouch > 100) { + this.props.onNavigateNext(); + } + + this._touchStart = null; + } + + onTouchCancel = (event) => { + this._touchStart = null; + } + + onTouchMove = (event) => { + if (!this._touchStart) { + return; + } + } + + // + // Render + + render() { + const { + dates, + view + } = this.props; + + return ( +
+ { + dates.map((date) => { + return ( + + ); + }) + } +
+ ); + } +} + +CalendarDays.propTypes = { + dates: PropTypes.arrayOf(PropTypes.string).isRequired, + view: PropTypes.string.isRequired, + isSidebarVisible: PropTypes.bool.isRequired, + onNavigatePrevious: PropTypes.func.isRequired, + onNavigateNext: PropTypes.func.isRequired +}; + +export default CalendarDays; diff --git a/frontend/src/Calendar/Day/CalendarDaysConnector.js b/frontend/src/Calendar/Day/CalendarDaysConnector.js new file mode 100644 index 000000000..9dd965146 --- /dev/null +++ b/frontend/src/Calendar/Day/CalendarDaysConnector.js @@ -0,0 +1,32 @@ +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import { gotoCalendarPreviousRange, gotoCalendarNextRange } from 'Store/Actions/calendarActions'; +import CalendarDays from './CalendarDays'; + +function createMapStateToProps() { + return createSelector( + (state) => state.calendar, + (state) => state.app.isSidebarVisible, + (calendar, isSidebarVisible) => { + return { + dates: calendar.dates, + view: calendar.view, + isSidebarVisible + }; + } + ); +} + +function createMapDispatchToProps(dispatch) { + return { + onNavigatePrevious() { + dispatch(gotoCalendarPreviousRange()); + }, + + onNavigateNext() { + dispatch(gotoCalendarNextRange()); + } + }; +} + +export default connect(createMapStateToProps, createMapDispatchToProps)(CalendarDays); diff --git a/frontend/src/Calendar/Day/DayOfWeek.css b/frontend/src/Calendar/Day/DayOfWeek.css new file mode 100644 index 000000000..8c3552e55 --- /dev/null +++ b/frontend/src/Calendar/Day/DayOfWeek.css @@ -0,0 +1,13 @@ +.dayOfWeek { + flex: 1 0 14.28%; + background-color: #e4eaec; + text-align: center; +} + +.isSingleDay { + width: 100%; +} + +.isToday { + background-color: $calendarTodayBackgroundColor; +} diff --git a/frontend/src/Calendar/Day/DayOfWeek.js b/frontend/src/Calendar/Day/DayOfWeek.js new file mode 100644 index 000000000..d97671522 --- /dev/null +++ b/frontend/src/Calendar/Day/DayOfWeek.js @@ -0,0 +1,56 @@ +import moment from 'moment'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import classNames from 'classnames'; +import getRelativeDate from 'Utilities/Date/getRelativeDate'; +import * as calendarViews from 'Calendar/calendarViews'; +import styles from './DayOfWeek.css'; + +class DayOfWeek extends Component { + + // + // Render + + render() { + const { + date, + view, + isTodaysDate, + calendarWeekColumnHeader, + shortDateFormat, + showRelativeDates + } = this.props; + + const highlightToday = view !== calendarViews.MONTH && isTodaysDate; + const momentDate = moment(date); + let formatedDate = momentDate.format('dddd'); + + if (view === calendarViews.WEEK) { + formatedDate = momentDate.format(calendarWeekColumnHeader); + } else if (view === calendarViews.FORECAST) { + formatedDate = getRelativeDate(date, shortDateFormat, showRelativeDates); + } + + return ( +
+ {formatedDate} +
+ ); + } +} + +DayOfWeek.propTypes = { + date: PropTypes.string.isRequired, + view: PropTypes.string.isRequired, + isTodaysDate: PropTypes.bool.isRequired, + calendarWeekColumnHeader: PropTypes.string.isRequired, + shortDateFormat: PropTypes.string.isRequired, + showRelativeDates: PropTypes.bool.isRequired +}; + +export default DayOfWeek; diff --git a/frontend/src/Calendar/Day/DaysOfWeek.css b/frontend/src/Calendar/Day/DaysOfWeek.css new file mode 100644 index 000000000..518664633 --- /dev/null +++ b/frontend/src/Calendar/Day/DaysOfWeek.css @@ -0,0 +1,4 @@ +.daysOfWeek { + display: flex; + margin-top: 10px; +} diff --git a/frontend/src/Calendar/Day/DaysOfWeek.js b/frontend/src/Calendar/Day/DaysOfWeek.js new file mode 100644 index 000000000..a67777f7c --- /dev/null +++ b/frontend/src/Calendar/Day/DaysOfWeek.js @@ -0,0 +1,97 @@ +import moment from 'moment'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import DayOfWeek from './DayOfWeek'; +import * as calendarViews from 'Calendar/calendarViews'; +import styles from './DaysOfWeek.css'; + +class DaysOfWeek extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + todaysDate: moment().startOf('day').toISOString() + }; + + this.updateTimeoutId = null; + } + + // Lifecycle + + componentDidMount() { + const view = this.props.view; + + if (view !== calendarViews.AGENDA || view !== calendarViews.MONTH) { + this.scheduleUpdate(); + } + } + + componentWillUnmount() { + this.clearUpdateTimeout(); + } + + // + // Control + + scheduleUpdate = () => { + this.clearUpdateTimeout(); + const todaysDate = moment().startOf('day'); + const diff = todaysDate.clone().add(1, 'day').diff(moment()); + + this.setState({ + todaysDate: todaysDate.toISOString() + }); + + this.updateTimeoutId = setTimeout(this.scheduleUpdate, diff); + } + + clearUpdateTimeout = () => { + if (this.updateTimeoutId) { + clearTimeout(this.updateTimeoutId); + } + } + + // + // Render + + render() { + const { + dates, + view, + ...otherProps + } = this.props; + + if (view === calendarViews.AGENDA) { + return null; + } + + return ( +
+ { + dates.map((date) => { + return ( + + ); + }) + } +
+ ); + } +} + +DaysOfWeek.propTypes = { + dates: PropTypes.arrayOf(PropTypes.string), + view: PropTypes.string.isRequired +}; + +export default DaysOfWeek; diff --git a/frontend/src/Calendar/Day/DaysOfWeekConnector.js b/frontend/src/Calendar/Day/DaysOfWeekConnector.js new file mode 100644 index 000000000..7f5cdef19 --- /dev/null +++ b/frontend/src/Calendar/Day/DaysOfWeekConnector.js @@ -0,0 +1,22 @@ +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector'; +import DaysOfWeek from './DaysOfWeek'; + +function createMapStateToProps() { + return createSelector( + (state) => state.calendar, + createUISettingsSelector(), + (calendar, UiSettings) => { + return { + dates: calendar.dates.slice(0, 7), + view: calendar.view, + calendarWeekColumnHeader: UiSettings.calendarWeekColumnHeader, + shortDateFormat: UiSettings.shortDateFormat, + showRelativeDates: UiSettings.showRelativeDates + }; + } + ); +} + +export default connect(createMapStateToProps)(DaysOfWeek); diff --git a/frontend/src/Calendar/Events/CalendarEvent.css b/frontend/src/Calendar/Events/CalendarEvent.css new file mode 100644 index 000000000..c30f3562e --- /dev/null +++ b/frontend/src/Calendar/Events/CalendarEvent.css @@ -0,0 +1,70 @@ +.event { + overflow-x: hidden; + margin: 4px 2px; + padding: 5px; + border-bottom: 1px solid $borderColor; + border-left: 4px solid $borderColor; + font-size: 12px; +} + +.info, +.albumInfo { + display: flex; +} + +.artistName, +.albumTitle { + @add-mixin truncate; + + flex: 1 0 1px; + margin-right: 10px; +} + +.artistName { + color: #3a3f51; + font-size: 14px; +} + +.absoluteEpisodeNumber { + margin-left: 3px; +} + +.statusIcon { + margin-left: 3px; +} + +/* + * Status + */ + +.downloaded { + border-left-color: $successColor; +} + +.downloading { + border-left-color: $purple; +} + +.unmonitored { + border-left-color: $gray; + + &:global(.colorImpaired) { + background: repeating-linear-gradient(45deg, transparent, transparent 5px, #eee 5px, #eee 10px); + } +} + +.missing { + border-left-color: $dangerColor; + + &:global(.colorImpaired) { + background: repeating-linear-gradient(90deg, transparent, transparent 5px, #eee 5px, #eee 10px); + } +} + +.unreleased { + border-left-color: $primaryColor; + + &:global(.colorImpaired) { + background: repeating-linear-gradient(90deg, transparent, transparent 5px, #eee 5px, #eee 10px); + } +} diff --git a/frontend/src/Calendar/Events/CalendarEvent.js b/frontend/src/Calendar/Events/CalendarEvent.js new file mode 100644 index 000000000..8e914cc0d --- /dev/null +++ b/frontend/src/Calendar/Events/CalendarEvent.js @@ -0,0 +1,144 @@ +import moment from 'moment'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import classNames from 'classnames'; +import { icons } from 'Helpers/Props'; +import formatTime from 'Utilities/Date/formatTime'; +import padNumber from 'Utilities/Number/padNumber'; +import getStatusStyle from 'Calendar/getStatusStyle'; +import episodeEntities from 'Album/episodeEntities'; +import Icon from 'Components/Icon'; +import Link from 'Components/Link/Link'; +import EpisodeDetailsModal from 'Album/EpisodeDetailsModal'; +import CalendarEventQueueDetails from './CalendarEventQueueDetails'; +import styles from './CalendarEvent.css'; + +class CalendarEvent extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + isDetailsModalOpen: false + }; + } + + // + // Listeners + + onPress = () => { + this.setState({ isDetailsModalOpen: true }, () => { + this.props.onEventModalOpenToggle(true); + }); + } + + onDetailsModalClose = () => { + this.setState({ isDetailsModalOpen: false }, () => { + this.props.onEventModalOpenToggle(false); + }); + } + + // + // Render + + render() { + const { + id, + artist, + title, + // seasonNumber, + // episodeNumber, + // absoluteEpisodeNumber, + releaseDate, + monitored, + // hasFile, + grabbed, + queueItem, + // timeFormat, + colorImpairedMode + } = this.props; + + const startTime = moment(releaseDate); + // const endTime = startTime.add(artist.runtime, 'minutes'); + const downloading = !!(queueItem || grabbed); + const isMonitored = artist.monitored && monitored; + const statusStyle = getStatusStyle(id, downloading, startTime, isMonitored); + // const missingAbsoluteNumber = artist.artistType === 'anime' && seasonNumber > 0 && !absoluteEpisodeNumber; + + return ( +
+ +
+
+ {artist.artistName} +
+ + { + !!queueItem && + + + + } + + { + !queueItem && grabbed && + + } +
+ +
+
+ {title} +
+
+ + + +
+ ); + } +} + +CalendarEvent.propTypes = { + id: PropTypes.number.isRequired, + artist: PropTypes.object.isRequired, + title: PropTypes.string.isRequired, + // seasonNumber: PropTypes.number.isRequired, + // episodeNumber: PropTypes.number.isRequired, + // absoluteEpisodeNumber: PropTypes.number, + releaseDate: PropTypes.string.isRequired, + monitored: PropTypes.bool.isRequired, + // hasFile: PropTypes.bool.isRequired, + grabbed: PropTypes.bool, + queueItem: PropTypes.object, + // timeFormat: PropTypes.string.isRequired, + colorImpairedMode: PropTypes.bool.isRequired, + onEventModalOpenToggle: PropTypes.func.isRequired +}; + +export default CalendarEvent; diff --git a/frontend/src/Calendar/Events/CalendarEventConnector.js b/frontend/src/Calendar/Events/CalendarEventConnector.js new file mode 100644 index 000000000..31706e2f7 --- /dev/null +++ b/frontend/src/Calendar/Events/CalendarEventConnector.js @@ -0,0 +1,24 @@ +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import createArtistSelector from 'Store/Selectors/createArtistSelector'; +import createQueueItemSelector from 'Store/Selectors/createQueueItemSelector'; +import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector'; +import CalendarEvent from './CalendarEvent'; + +function createMapStateToProps() { + return createSelector( + createArtistSelector(), + createQueueItemSelector(), + createUISettingsSelector(), + (artist, queueItem, uiSettings) => { + return { + artist, + queueItem, + timeFormat: uiSettings.timeFormat, + colorImpairedMode: uiSettings.enableColorImpairedMode + }; + } + ); +} + +export default connect(createMapStateToProps)(CalendarEvent); diff --git a/frontend/src/Calendar/Events/CalendarEventQueueDetails.js b/frontend/src/Calendar/Events/CalendarEventQueueDetails.js new file mode 100644 index 000000000..81d81465c --- /dev/null +++ b/frontend/src/Calendar/Events/CalendarEventQueueDetails.js @@ -0,0 +1,50 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import colors from 'Styles/Variables/colors'; +import CircularProgressBar from 'Components/CircularProgressBar'; +import QueueDetails from 'Activity/Queue/QueueDetails'; + +function CalendarEventQueueDetails(props) { + const { + title, + size, + sizeleft, + estimatedCompletionTime, + status, + errorMessage + } = props; + + const progress = (100 - sizeleft / size * 100); + + return ( + + +
+ } + /> + ); +} + +CalendarEventQueueDetails.propTypes = { + title: PropTypes.string.isRequired, + size: PropTypes.number.isRequired, + sizeleft: PropTypes.number.isRequired, + estimatedCompletionTime: PropTypes.string, + status: PropTypes.string.isRequired, + errorMessage: PropTypes.string +}; + +export default CalendarEventQueueDetails; diff --git a/frontend/src/Calendar/Header/CalendarHeader.css b/frontend/src/Calendar/Header/CalendarHeader.css new file mode 100644 index 000000000..1127bb3c3 --- /dev/null +++ b/frontend/src/Calendar/Header/CalendarHeader.css @@ -0,0 +1,53 @@ +.header { + display: flex; +} + +.navigationButtons { + flex: 1 1 33%; + text-align: left; +} + +.todayButton { + composes: button from 'Components/Link/Button.css'; + + margin-left: 5px; +} + +.titleDesktop, +.titleMobile { + text-align: center; + font-size: 18px; +} + +.titleMobile { + margin-bottom: 5px; +} + +.viewButtonsContainer { + display: flex; + justify-content: flex-end; + flex: 1 1 33%; +} + +.viewMenu { + composes: menu from 'Components/Menu/Menu.css'; + + line-height: 31px; +} + +.loading { + composes: loading from 'Components/Loading/LoadingIndicator.css'; + + margin-top: 5px; + margin-right: 10px; +} + +@media only screen and (max-width: $breakpointSmall) { + .navigationButtons { + flex: 1 0 50%; + } + + .viewButtonsContainer { + flex: 0 0 100px; + } +} diff --git a/frontend/src/Calendar/Header/CalendarHeader.js b/frontend/src/Calendar/Header/CalendarHeader.js new file mode 100644 index 000000000..4fea8356d --- /dev/null +++ b/frontend/src/Calendar/Header/CalendarHeader.js @@ -0,0 +1,253 @@ +import moment from 'moment'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { align, icons } from 'Helpers/Props'; +import Button from 'Components/Link/Button'; +import Icon from 'Components/Icon'; +import LoadingIndicator from 'Components/Loading/LoadingIndicator'; +import Menu from 'Components/Menu/Menu'; +import MenuButton from 'Components/Menu/MenuButton'; +import MenuContent from 'Components/Menu/MenuContent'; +import ViewMenuItem from 'Components/Menu/ViewMenuItem'; +import * as calendarViews from 'Calendar/calendarViews'; +import CalendarHeaderViewButton from './CalendarHeaderViewButton'; +import styles from './CalendarHeader.css'; + +function getTitle(time, start, end, view, longDateFormat) { + const timeMoment = moment(time); + const startMoment = moment(start); + const endMoment = moment(end); + + if (view === 'day') { + return timeMoment.format(longDateFormat); + } else if (view === 'month') { + return timeMoment.format('MMMM YYYY'); + } else if (view === 'agenda') { + return 'Agenda'; + } + + let startFormat = 'MMM D YYYY'; + let endFormat = 'MMM D YYYY'; + + if (startMoment.isSame(endMoment, 'month')) { + startFormat = 'MMM D'; + endFormat = 'D YYYY'; + } else if (startMoment.isSame(endMoment, 'year')) { + startFormat = 'MMM D'; + endFormat = 'MMM D YYYY'; + } + + return `${startMoment.format(startFormat)} \u2014 ${endMoment.format(endFormat)}`; +} + +// TODO Convert to a stateful Component so we can track view internally when changed + +class CalendarHeader extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + view: props.view + }; + } + + componentDidUpdate(prevProps) { + const view = this.props.view; + + if (prevProps.view !== view) { + this.setState({ view }); + } + } + + // + // Listeners + + onViewChange = (view) => { + this.setState({ view }, () => { + this.props.onViewChange(view); + }); + } + + // + // Render + + render() { + const { + isFetching, + time, + start, + end, + longDateFormat, + isSmallScreen, + onTodayPress, + onPreviousPress, + onNextPress + } = this.props; + + const view = this.state.view; + + const title = getTitle(time, start, end, view, longDateFormat); + + return ( +
+ { + isSmallScreen && +
+ {title} +
+ } + +
+
+ + + + + +
+ + { + !isSmallScreen && +
+ {title} +
+ } + +
+ { + isFetching && + + } + + { + isSmallScreen ? + + + + + + + + Week + + + + Forecast + + + + Day + + + + Agenda + + + : + +
+ + + + + + + + + +
+ } +
+
+
+ ); + } +} + +CalendarHeader.propTypes = { + isFetching: PropTypes.bool.isRequired, + time: PropTypes.string.isRequired, + start: PropTypes.string.isRequired, + end: PropTypes.string.isRequired, + view: PropTypes.oneOf(calendarViews.all).isRequired, + isSmallScreen: PropTypes.bool.isRequired, + longDateFormat: PropTypes.string.isRequired, + onViewChange: PropTypes.func.isRequired, + onTodayPress: PropTypes.func.isRequired, + onPreviousPress: PropTypes.func.isRequired, + onNextPress: PropTypes.func.isRequired +}; + +export default CalendarHeader; diff --git a/frontend/src/Calendar/Header/CalendarHeaderConnector.js b/frontend/src/Calendar/Header/CalendarHeaderConnector.js new file mode 100644 index 000000000..c96cf2869 --- /dev/null +++ b/frontend/src/Calendar/Header/CalendarHeaderConnector.js @@ -0,0 +1,84 @@ +import _ from 'lodash'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector'; +import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector'; +import { setCalendarView, gotoCalendarToday, gotoCalendarPreviousRange, gotoCalendarNextRange } from 'Store/Actions/calendarActions'; +import CalendarHeader from './CalendarHeader'; + +function createMapStateToProps() { + return createSelector( + (state) => state.calendar, + createDimensionsSelector(), + createUISettingsSelector(), + (calendar, dimensions, uiSettings) => { + const result = _.pick(calendar, [ + 'isFetching', + 'view', + 'time', + 'start', + 'end' + ]); + + result.isSmallScreen = dimensions.isSmallScreen; + result.longDateFormat = uiSettings.longDateFormat; + + return result; + } + ); +} + +const mapDispatchToProps = { + setCalendarView, + gotoCalendarToday, + gotoCalendarPreviousRange, + gotoCalendarNextRange +}; + +class CalendarHeaderConnector extends Component { + + // + // Listeners + + onViewChange = (view) => { + this.props.setCalendarView({ view }); + } + + onTodayPress = () => { + this.props.gotoCalendarToday(); + } + + onPreviousPress = () => { + this.props.gotoCalendarPreviousRange(); + } + + onNextPress = () => { + this.props.gotoCalendarNextRange(); + } + + // + // Render + + render() { + return ( + + ); + } +} + +CalendarHeaderConnector.propTypes = { + setCalendarView: PropTypes.func.isRequired, + gotoCalendarToday: PropTypes.func.isRequired, + gotoCalendarPreviousRange: PropTypes.func.isRequired, + gotoCalendarNextRange: PropTypes.func.isRequired +}; + +export default connect(createMapStateToProps, mapDispatchToProps)(CalendarHeaderConnector); diff --git a/frontend/src/Calendar/Header/CalendarHeaderViewButton.js b/frontend/src/Calendar/Header/CalendarHeaderViewButton.js new file mode 100644 index 000000000..8dd5ae9f0 --- /dev/null +++ b/frontend/src/Calendar/Header/CalendarHeaderViewButton.js @@ -0,0 +1,45 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import titleCase from 'Utilities/String/titleCase'; +import Button from 'Components/Link/Button'; +import * as calendarViews from 'Calendar/calendarViews'; +// import styles from './CalendarHeaderViewButton.css'; + +class CalendarHeaderViewButton extends Component { + + // + // Listeners + + onPress = () => { + this.props.onPress(this.props.view); + } + + // + // Render + + render() { + const { + view, + selectedView, + ...otherProps + } = this.props; + + return ( + + ); + } +} + +CalendarHeaderViewButton.propTypes = { + view: PropTypes.oneOf(calendarViews.all).isRequired, + selectedView: PropTypes.oneOf(calendarViews.all).isRequired, + onPress: PropTypes.func.isRequired +}; + +export default CalendarHeaderViewButton; diff --git a/frontend/src/Calendar/Legend/Legend.css b/frontend/src/Calendar/Legend/Legend.css new file mode 100644 index 000000000..296cbd9d5 --- /dev/null +++ b/frontend/src/Calendar/Legend/Legend.css @@ -0,0 +1,6 @@ +.legend { + display: flex; + flex-wrap: wrap; + margin-top: 10px; + padding: 3px 0; +} diff --git a/frontend/src/Calendar/Legend/Legend.js b/frontend/src/Calendar/Legend/Legend.js new file mode 100644 index 000000000..759ebf5a7 --- /dev/null +++ b/frontend/src/Calendar/Legend/Legend.js @@ -0,0 +1,52 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import LegendItem from './LegendItem'; +import styles from './Legend.css'; + +function Legend({ colorImpairedMode }) { + return ( +
+
+ + + +
+ +
+ + + +
+ +
+ +
+
+ ); +} + +Legend.propTypes = { + colorImpairedMode: PropTypes.bool.isRequired +}; + +export default Legend; diff --git a/frontend/src/Calendar/Legend/LegendItem.css b/frontend/src/Calendar/Legend/LegendItem.css new file mode 100644 index 000000000..fb49d8608 --- /dev/null +++ b/frontend/src/Calendar/Legend/LegendItem.css @@ -0,0 +1,37 @@ +.legendItem { + margin: 3px 0; + margin-right: 6px; + padding-left: 5px; + width: 150px; + border-left-width: 4px; + border-left-style: solid; + cursor: default; +} + +/* + * Status + */ + +.downloaded { + composes: downloaded from 'Calendar/Events/CalendarEvent.css'; +} + +.downloading { + composes: downloading from 'Calendar/Events/CalendarEvent.css'; +} + +.unmonitored { + composes: unmonitored from 'Calendar/Events/CalendarEvent.css'; +} + +.onAir { + composes: onAir from 'Calendar/Events/CalendarEvent.css'; +} + +.missing { + composes: missing from 'Calendar/Events/CalendarEvent.css'; +} + +.unreleased { + composes: unreleased from 'Calendar/Events/CalendarEvent.css'; +} diff --git a/frontend/src/Calendar/Legend/LegendItem.js b/frontend/src/Calendar/Legend/LegendItem.js new file mode 100644 index 000000000..961f48b86 --- /dev/null +++ b/frontend/src/Calendar/Legend/LegendItem.js @@ -0,0 +1,36 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import classNames from 'classnames'; +import titleCase from 'Utilities/String/titleCase'; +import styles from './LegendItem.css'; + +function LegendItem(props) { + const { + name, + status, + tooltip, + colorImpairedMode + } = props; + + return ( +
+ {name ? name : titleCase(status)} +
+ ); +} + +LegendItem.propTypes = { + name: PropTypes.string, + status: PropTypes.string.isRequired, + tooltip: PropTypes.string.isRequired, + colorImpairedMode: PropTypes.bool.isRequired +}; + +export default LegendItem; diff --git a/frontend/src/Calendar/calendarViews.js b/frontend/src/Calendar/calendarViews.js new file mode 100644 index 000000000..929958b66 --- /dev/null +++ b/frontend/src/Calendar/calendarViews.js @@ -0,0 +1,7 @@ +export const DAY = 'day'; +export const WEEK = 'week'; +export const MONTH = 'month'; +export const FORECAST = 'forecast'; +export const AGENDA = 'agenda'; + +export const all = [DAY, WEEK, MONTH, FORECAST, AGENDA]; diff --git a/frontend/src/Calendar/getStatusStyle.js b/frontend/src/Calendar/getStatusStyle.js new file mode 100644 index 000000000..f46c19fb6 --- /dev/null +++ b/frontend/src/Calendar/getStatusStyle.js @@ -0,0 +1,26 @@ +/* eslint max-params: 0 */ +import moment from 'moment'; + +function getStatusStyle(episodeNumber, downloading, startTime, isMonitored) { + const currentTime = moment(); + + // if (hasFile) { + // return 'downloaded'; + // } + + if (downloading) { + return 'downloading'; + } + + if (!isMonitored) { + return 'unmonitored'; + } + + if (currentTime.isAfter(startTime)) { + return 'missing'; + } + + return 'unreleased'; +} + +export default getStatusStyle; diff --git a/frontend/src/Calendar/iCal/CalendarLinkModal.js b/frontend/src/Calendar/iCal/CalendarLinkModal.js new file mode 100644 index 000000000..8cc487c16 --- /dev/null +++ b/frontend/src/Calendar/iCal/CalendarLinkModal.js @@ -0,0 +1,29 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import Modal from 'Components/Modal/Modal'; +import CalendarLinkModalContentConnector from './CalendarLinkModalContentConnector'; + +function CalendarLinkModal(props) { + const { + isOpen, + onModalClose + } = props; + + return ( + + + + ); +} + +CalendarLinkModal.propTypes = { + isOpen: PropTypes.bool.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default CalendarLinkModal; diff --git a/frontend/src/Calendar/iCal/CalendarLinkModalContent.js b/frontend/src/Calendar/iCal/CalendarLinkModalContent.js new file mode 100644 index 000000000..82ea4e776 --- /dev/null +++ b/frontend/src/Calendar/iCal/CalendarLinkModalContent.js @@ -0,0 +1,221 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { icons, inputTypes, kinds, sizes } from 'Helpers/Props'; +import Icon from 'Components/Icon'; +import Button from 'Components/Link/Button'; +import ClipboardButton from 'Components/Link/ClipboardButton'; +import Form from 'Components/Form/Form'; +import FormGroup from 'Components/Form/FormGroup'; +import FormLabel from 'Components/Form/FormLabel'; +import FormInputGroup from 'Components/Form/FormInputGroup'; +import FormInputButton from 'Components/Form/FormInputButton'; +import ModalContent from 'Components/Modal/ModalContent'; +import ModalHeader from 'Components/Modal/ModalHeader'; +import ModalBody from 'Components/Modal/ModalBody'; +import ModalFooter from 'Components/Modal/ModalFooter'; + +function getUrls(state) { + const { + unmonitored, + premieresOnly, + asAllDay, + tags + } = state; + + let icalUrl = `${window.location.host}${window.Sonarr.urlBase}/feed/calendar/Lidarr.ics?`; + + if (unmonitored) { + icalUrl += 'unmonitored=true&'; + } + + if (premieresOnly) { + icalUrl += 'premieresOnly=true&'; + } + + if (asAllDay) { + icalUrl += 'asAllDay=true&'; + } + + if (tags.length) { + icalUrl += `tags=${tags.toString()}&`; + } + + icalUrl += `apikey=${window.Sonarr.apiKey}`; + + const iCalHttpUrl = `${window.location.protocol}//${icalUrl}`; + const iCalWebCalUrl = `webcal://${icalUrl}`; + + return { + iCalHttpUrl, + iCalWebCalUrl + }; +} + +class CalendarLinkModalContent extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + const defaultState = { + unmonitored: false, + premieresOnly: false, + asAllDay: false, + tags: [] + }; + + const urls = getUrls(defaultState); + + this.state = { + ...defaultState, + ...urls + }; + } + + // + // Listeners + + onInputChange = ({ name, value }) => { + const state = { + ...this.state, + [name]: value + }; + + const urls = getUrls(state); + + this.setState({ + [name]: value, + ...urls + }); + } + + onLinkFocus = (event) => { + event.target.select(); + } + + // + // Render + + render() { + const { + onModalClose + } = this.props; + + const { + unmonitored, + premieresOnly, + asAllDay, + tags, + iCalHttpUrl, + iCalWebCalUrl + } = this.state; + + return ( + + + Lidarr Calendar Feed + + + +
+ + Include Unmonitored + + + + + + Season Premieres Only + + + + + + Show as All-Day Events + + + + + + Tags + + + + + + iCal Feed + + , + + + + + ]} + onChange={this.onInputChange} + onFocus={this.onLinkFocus} + /> + +
+
+ + + + +
+ ); + } +} + +CalendarLinkModalContent.propTypes = { + tagList: PropTypes.arrayOf(PropTypes.object).isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default CalendarLinkModalContent; diff --git a/frontend/src/Calendar/iCal/CalendarLinkModalContentConnector.js b/frontend/src/Calendar/iCal/CalendarLinkModalContentConnector.js new file mode 100644 index 000000000..e10c5c3f9 --- /dev/null +++ b/frontend/src/Calendar/iCal/CalendarLinkModalContentConnector.js @@ -0,0 +1,17 @@ +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import createTagsSelector from 'Store/Selectors/createTagsSelector'; +import CalendarLinkModalContent from './CalendarLinkModalContent'; + +function createMapStateToProps() { + return createSelector( + createTagsSelector(), + (tagList) => { + return { + tagList + }; + } + ); +} + +export default connect(createMapStateToProps)(CalendarLinkModalContent); diff --git a/frontend/src/Commands/commandNames.js b/frontend/src/Commands/commandNames.js new file mode 100644 index 000000000..e432c42e2 --- /dev/null +++ b/frontend/src/Commands/commandNames.js @@ -0,0 +1,19 @@ +export const APPLICATION_UPDATE = 'ApplicationUpdate'; +export const BACKUP = 'Backup'; +export const CHECK_FOR_FINISHED_DOWNLOAD = 'CheckForFinishedDownload'; +export const CLEAR_BLACKLIST = 'ClearBlacklist'; +export const CLEAR_LOGS = 'ClearLog'; +export const CUTOFF_UNMET_ALBUM_SEARCH = 'CutoffUnmetAlbumSearch'; +export const DELETE_LOG_FILES = 'DeleteLogFiles'; +export const DELETE_UPDATE_LOG_FILES = 'DeleteUpdateLogFiles'; +export const DOWNLOADED_ALBUMS_SCAN = 'DownloadedAlbumsScan'; +export const ALBUM_SEARCH = 'AlbumSearch'; +export const INTERACTIVE_IMPORT = 'ManualImport'; +export const MISSING_ALBUM_SEARCH = 'MissingAlbumSearch'; +export const REFRESH_ARTIST = 'RefreshArtist'; +export const RENAME_FILES = 'RenameFiles'; +export const RENAME_ARTIST = 'RenameArtist'; +export const RESET_API_KEY = 'ResetApiKey'; +export const RSS_SYNC = 'RssSync'; +export const SEASON_SEARCH = 'AlbumSearch'; +export const ARTIST_SEARCH = 'ArtistSearch'; diff --git a/frontend/src/Components/Alert.css b/frontend/src/Components/Alert.css new file mode 100644 index 000000000..312fbb4f2 --- /dev/null +++ b/frontend/src/Components/Alert.css @@ -0,0 +1,31 @@ +.alert { + display: block; + margin: 5px; + padding: 15px; + border: 1px solid transparent; + border-radius: 4px; +} + +.danger { + border-color: $alertDangerBorderColor; + background-color: $alertDangerBackgroundColor; + color: $alertDangerColor; +} + +.info { + border-color: $alertInfoBorderColor; + background-color: $alertInfoBackgroundColor; + color: $alertInfoColor; +} + +.success { + border-color: $alertSuccessBorderColor; + background-color: $alertSuccessBackgroundColor; + color: $alertSuccessColor; +} + +.warning { + border-color: $alertWarningBorderColor; + background-color: $alertWarningBackgroundColor; + color: $alertWarningColor; +} diff --git a/frontend/src/Components/Alert.js b/frontend/src/Components/Alert.js new file mode 100644 index 000000000..dc19a418c --- /dev/null +++ b/frontend/src/Components/Alert.js @@ -0,0 +1,32 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import classNames from 'classnames'; +import { kinds } from 'Helpers/Props'; +import styles from './Alert.css'; + +function Alert({ className, kind, children, ...otherProps }) { + return ( +
+ {children} +
+ ); +} + +Alert.propTypes = { + className: PropTypes.string.isRequired, + kind: PropTypes.oneOf(kinds.all).isRequired, + children: PropTypes.node.isRequired +}; + +Alert.defaultProps = { + className: styles.alert, + kind: kinds.INFO +}; + +export default Alert; diff --git a/frontend/src/Components/Card.css b/frontend/src/Components/Card.css new file mode 100644 index 000000000..e500ca154 --- /dev/null +++ b/frontend/src/Components/Card.css @@ -0,0 +1,8 @@ +.card { + margin: 10px; + padding: 10px; + border-radius: 3px; + background-color: $white; + box-shadow: 0 0 10px 1px $cardShadowColor; + color: $defaultColor; +} diff --git a/frontend/src/Components/Card.js b/frontend/src/Components/Card.js new file mode 100644 index 000000000..cc45edba3 --- /dev/null +++ b/frontend/src/Components/Card.js @@ -0,0 +1,39 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import Link from 'Components/Link/Link'; +import styles from './Card.css'; + +class Card extends Component { + + // + // Render + + render() { + const { + className, + children, + onPress + } = this.props; + + return ( + + {children} + + ); + } +} + +Card.propTypes = { + className: PropTypes.string.isRequired, + children: PropTypes.node.isRequired, + onPress: PropTypes.func.isRequired +}; + +Card.defaultProps = { + className: styles.card +}; + +export default Card; diff --git a/frontend/src/Components/CircularProgressBar.css b/frontend/src/Components/CircularProgressBar.css new file mode 100644 index 000000000..32b349404 --- /dev/null +++ b/frontend/src/Components/CircularProgressBar.css @@ -0,0 +1,21 @@ +.circularProgressBarContainer { + position: relative; + display: inline-block; + vertical-align: top; + text-align: center; +} + +.circularProgressBar { + position: absolute; + top: 0; + left: 0; + transform: rotate(-90deg); + transform-origin: center center; +} + +.circularProgressBarText { + position: absolute; + width: 100%; + height: 100%; + font-weight: bold; +} diff --git a/frontend/src/Components/CircularProgressBar.js b/frontend/src/Components/CircularProgressBar.js new file mode 100644 index 000000000..95c84f59a --- /dev/null +++ b/frontend/src/Components/CircularProgressBar.js @@ -0,0 +1,139 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import colors from 'Styles/Variables/colors'; +import styles from './CircularProgressBar.css'; + +class CircularProgressBar extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + progress: 0 + }; + } + + componentDidMount() { + this._progressStep(); + } + + componentDidUpdate(prevProps) { + const progress = this.props.progress; + + if (prevProps.progress !== progress) { + this._cancelProgressStep(); + this._progressStep(); + } + } + + componentWillUnmount() { + this._cancelProgressStep(); + } + + // + // Control + + _progressStep() { + this.requestAnimationFrame = window.requestAnimationFrame(() => { + this.setState({ + progress: this.state.progress + 1 + }, () => { + if (this.state.progress < this.props.progress) { + this._progressStep(); + } + }); + }); + } + + _cancelProgressStep() { + if (this.requestAnimationFrame) { + window.cancelAnimationFrame(this.requestAnimationFrame); + } + } + + // + // Render + + render() { + const { + className, + containerClassName, + size, + strokeWidth, + strokeColor, + showProgressText + } = this.props; + + const progress = this.state.progress; + + const center = size / 2; + const radius = center - strokeWidth; + const circumference = Math.PI * (radius * 2); + const sizeInPixels = `${size}px`; + const strokeDashoffset = ((100 - progress) / 100) * circumference; + const progressText = `${Math.round(progress)}%`; + + return ( +
+ + + + + { + showProgressText && +
+ {progressText} +
+ } +
+ ); + } +} + +CircularProgressBar.propTypes = { + className: PropTypes.string, + containerClassName: PropTypes.string, + size: PropTypes.number, + progress: PropTypes.number.isRequired, + strokeWidth: PropTypes.number, + strokeColor: PropTypes.string, + showProgressText: PropTypes.bool +}; + +CircularProgressBar.defaultProps = { + className: styles.circularProgressBar, + containerClassName: styles.circularProgressBarContainer, + size: 60, + strokeWidth: 5, + strokeColor: colors.sonarrBlue, + showProgressText: false +}; + +export default CircularProgressBar; diff --git a/frontend/src/Components/DescriptionList/DescriptionList.css b/frontend/src/Components/DescriptionList/DescriptionList.css new file mode 100644 index 000000000..94cd75ba9 --- /dev/null +++ b/frontend/src/Components/DescriptionList/DescriptionList.css @@ -0,0 +1,4 @@ +.descriptionList { + margin-top: 0; + margin-bottom: 20px; +} diff --git a/frontend/src/Components/DescriptionList/DescriptionList.js b/frontend/src/Components/DescriptionList/DescriptionList.js new file mode 100644 index 000000000..b7a1d1634 --- /dev/null +++ b/frontend/src/Components/DescriptionList/DescriptionList.js @@ -0,0 +1,27 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import styles from './DescriptionList.css'; + +class DescriptionList extends Component { + + // + // Render + + render() { + const { + children + } = this.props; + + return ( +
+ {children} +
+ ); + } +} + +DescriptionList.propTypes = { + children: PropTypes.node +}; + +export default DescriptionList; diff --git a/frontend/src/Components/DescriptionList/DescriptionListItem.js b/frontend/src/Components/DescriptionList/DescriptionListItem.js new file mode 100644 index 000000000..4ba70bf33 --- /dev/null +++ b/frontend/src/Components/DescriptionList/DescriptionListItem.js @@ -0,0 +1,44 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import DescriptionListItemTitle from './DescriptionListItemTitle'; +import DescriptionListItemDescription from './DescriptionListItemDescription'; + +class DescriptionListItem extends Component { + + // + // Render + + render() { + const { + titleClassName, + descriptionClassName, + title, + data + } = this.props; + + return ( + + + {title} + + + + {data} + + + ); + } +} + +DescriptionListItem.propTypes = { + titleClassName: PropTypes.string, + descriptionClassName: PropTypes.string, + title: PropTypes.string, + data: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.node]) +}; + +export default DescriptionListItem; diff --git a/frontend/src/Components/DescriptionList/DescriptionListItemDescription.css b/frontend/src/Components/DescriptionList/DescriptionListItemDescription.css new file mode 100644 index 000000000..b23415a76 --- /dev/null +++ b/frontend/src/Components/DescriptionList/DescriptionListItemDescription.css @@ -0,0 +1,13 @@ +.description { + line-height: $lineHeight; +} + +.description { + margin-left: 0; +} + +@media (min-width: 768px) { + .description { + margin-left: 180px; + } +} diff --git a/frontend/src/Components/DescriptionList/DescriptionListItemDescription.js b/frontend/src/Components/DescriptionList/DescriptionListItemDescription.js new file mode 100644 index 000000000..4ef3c015e --- /dev/null +++ b/frontend/src/Components/DescriptionList/DescriptionListItemDescription.js @@ -0,0 +1,27 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import styles from './DescriptionListItemDescription.css'; + +function DescriptionListItemDescription(props) { + const { + className, + children + } = props; + + return ( +
+ {children} +
+ ); +} + +DescriptionListItemDescription.propTypes = { + className: PropTypes.string.isRequired, + children: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.node]) +}; + +DescriptionListItemDescription.defaultProps = { + className: styles.description +}; + +export default DescriptionListItemDescription; diff --git a/frontend/src/Components/DescriptionList/DescriptionListItemTitle.css b/frontend/src/Components/DescriptionList/DescriptionListItemTitle.css new file mode 100644 index 000000000..e496e463d --- /dev/null +++ b/frontend/src/Components/DescriptionList/DescriptionListItemTitle.css @@ -0,0 +1,18 @@ +.title { + line-height: $lineHeight; +} + +.title { + font-weight: bold; +} + +@media (min-width: 768px) { + .title { + @add-mixin truncate; + + float: left; + clear: left; + width: 160px; + text-align: right; + } +} diff --git a/frontend/src/Components/DescriptionList/DescriptionListItemTitle.js b/frontend/src/Components/DescriptionList/DescriptionListItemTitle.js new file mode 100644 index 000000000..e1632c1cf --- /dev/null +++ b/frontend/src/Components/DescriptionList/DescriptionListItemTitle.js @@ -0,0 +1,27 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import styles from './DescriptionListItemTitle.css'; + +function DescriptionListItemTitle(props) { + const { + className, + children + } = props; + + return ( +
+ {children} +
+ ); +} + +DescriptionListItemTitle.propTypes = { + className: PropTypes.string.isRequired, + children: PropTypes.string +}; + +DescriptionListItemTitle.defaultProps = { + className: styles.title +}; + +export default DescriptionListItemTitle; diff --git a/frontend/src/Components/DragPreviewLayer.css b/frontend/src/Components/DragPreviewLayer.css new file mode 100644 index 000000000..46f721fef --- /dev/null +++ b/frontend/src/Components/DragPreviewLayer.css @@ -0,0 +1,9 @@ +.dragLayer { + position: fixed; + top: 0; + left: 0; + z-index: 9999; + width: 100%; + height: 100%; + pointer-events: none; +} diff --git a/frontend/src/Components/DragPreviewLayer.js b/frontend/src/Components/DragPreviewLayer.js new file mode 100644 index 000000000..a111df70e --- /dev/null +++ b/frontend/src/Components/DragPreviewLayer.js @@ -0,0 +1,22 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import styles from './DragPreviewLayer.css'; + +function DragPreviewLayer({ children, ...otherProps }) { + return ( +
+ {children} +
+ ); +} + +DragPreviewLayer.propTypes = { + children: PropTypes.node, + className: PropTypes.string +}; + +DragPreviewLayer.defaultProps = { + className: styles.dragLayer +}; + +export default DragPreviewLayer; diff --git a/frontend/src/Components/FieldSet.css b/frontend/src/Components/FieldSet.css new file mode 100644 index 000000000..daf3bdf2e --- /dev/null +++ b/frontend/src/Components/FieldSet.css @@ -0,0 +1,19 @@ +.fieldSet { + margin: 0; + margin-bottom: 20px; + padding: 0; + min-width: 0; + border: 0; +} + +.legend { + display: block; + margin-bottom: 21px; + padding: 0; + width: 100%; + border: 0; + border-bottom: 1px solid #e5e5e5; + color: #3a3f51; + font-size: 21px; + line-height: inherit; +} diff --git a/frontend/src/Components/FieldSet.js b/frontend/src/Components/FieldSet.js new file mode 100644 index 000000000..76e68a934 --- /dev/null +++ b/frontend/src/Components/FieldSet.js @@ -0,0 +1,33 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import styles from './FieldSet.css'; + +class FieldSet extends Component { + + // + // Render + + render() { + const { + legend, + children + } = this.props; + + return ( +
+ + {legend} + + {children} +
+ ); + } + +} + +FieldSet.propTypes = { + legend: PropTypes.oneOfType([PropTypes.node, PropTypes.string]), + children: PropTypes.node +}; + +export default FieldSet; diff --git a/frontend/src/Components/FileBrowser/FileBrowserModal.css b/frontend/src/Components/FileBrowser/FileBrowserModal.css new file mode 100644 index 000000000..30b936800 --- /dev/null +++ b/frontend/src/Components/FileBrowser/FileBrowserModal.css @@ -0,0 +1,5 @@ +.modal { + composes: modal from 'Components/Modal/Modal.css'; + + height: 600px; +} diff --git a/frontend/src/Components/FileBrowser/FileBrowserModal.js b/frontend/src/Components/FileBrowser/FileBrowserModal.js new file mode 100644 index 000000000..6b58dbb8c --- /dev/null +++ b/frontend/src/Components/FileBrowser/FileBrowserModal.js @@ -0,0 +1,39 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import Modal from 'Components/Modal/Modal'; +import FileBrowserModalContentConnector from './FileBrowserModalContentConnector'; +import styles from './FileBrowserModal.css'; + +class FileBrowserModal extends Component { + + // + // Render + + render() { + const { + isOpen, + onModalClose, + ...otherProps + } = this.props; + + return ( + + + + ); + } +} + +FileBrowserModal.propTypes = { + isOpen: PropTypes.bool.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default FileBrowserModal; diff --git a/frontend/src/Components/FileBrowser/FileBrowserModalContent.css b/frontend/src/Components/FileBrowser/FileBrowserModalContent.css new file mode 100644 index 000000000..7da2ea225 --- /dev/null +++ b/frontend/src/Components/FileBrowser/FileBrowserModalContent.css @@ -0,0 +1,16 @@ +.modalBody { + composes: modalBody from 'Components/Modal/ModalBody.css'; + + display: flex; + flex-direction: column; +} + +.pathInput { + composes: pathInputWrapper from 'Components/Form/PathInput.css'; + + flex: 0 0 auto; +} + +.scroller { + margin-top: 20px; +} diff --git a/frontend/src/Components/FileBrowser/FileBrowserModalContent.js b/frontend/src/Components/FileBrowser/FileBrowserModalContent.js new file mode 100644 index 000000000..f81019e1c --- /dev/null +++ b/frontend/src/Components/FileBrowser/FileBrowserModalContent.js @@ -0,0 +1,213 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import ReactDOM from 'react-dom'; +import { scrollDirections } from 'Helpers/Props'; +import Button from 'Components/Link/Button'; +import Scroller from 'Components/Scroller/Scroller'; +import ModalContent from 'Components/Modal/ModalContent'; +import ModalHeader from 'Components/Modal/ModalHeader'; +import ModalBody from 'Components/Modal/ModalBody'; +import ModalFooter from 'Components/Modal/ModalFooter'; +import Table from 'Components/Table/Table'; +import TableBody from 'Components/Table/TableBody'; +import PathInput from 'Components/Form/PathInput'; +import FileBrowserRow from './FileBrowserRow'; +import styles from './FileBrowserModalContent.css'; + +const columns = [ + { + name: 'type', + label: 'Type', + isVisible: true + }, + { + name: 'name', + label: 'Name', + isVisible: true + } +]; + +class FileBrowserModalContent extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this._scrollerNode = null; + + this.state = { + isFileBrowserModalOpen: false, + currentPath: props.value + }; + } + + componentDidUpdate(prevProps) { + const { + currentPath + } = this.props; + + if (currentPath !== this.state.currentPath) { + this.setState({ currentPath }); + this._scrollerNode.scrollTop = 0; + } + } + + // + // Control + + setScrollerRef = (ref) => { + if (ref) { + this._scrollerNode = ReactDOM.findDOMNode(ref); + } else { + this._scrollerNode = null; + } + } + + // + // Listeners + + onPathInputChange = ({ value }) => { + this.setState({ currentPath: value }); + } + + onRowPress = (path) => { + this.props.onFetchPaths(path); + } + + onOkPress = () => { + this.props.onChange({ + name: this.props.name, + value: this.state.currentPath + }); + + this.props.onClearPaths(); + this.props.onModalClose(); + } + + // + // Render + + render() { + const { + parent, + directories, + files, + onModalClose, + ...otherProps + } = this.props; + + const emptyParent = parent === ''; + + return ( + + + File Browser + + + + + + + + + { + emptyParent && + + } + + { + !emptyParent && parent && + + } + + { + directories.map((directory) => { + return ( + + ); + }) + } + + { + files.map((file) => { + return ( + + ); + }) + } + +
+
+
+ + + + + + +
+ ); + } +} + +FileBrowserModalContent.propTypes = { + name: PropTypes.string.isRequired, + value: PropTypes.string.isRequired, + parent: PropTypes.string, + currentPath: PropTypes.string.isRequired, + directories: PropTypes.arrayOf(PropTypes.object).isRequired, + files: PropTypes.arrayOf(PropTypes.object).isRequired, + onFetchPaths: PropTypes.func.isRequired, + onClearPaths: PropTypes.func.isRequired, + onChange: PropTypes.func.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default FileBrowserModalContent; diff --git a/frontend/src/Components/FileBrowser/FileBrowserModalContentConnector.js b/frontend/src/Components/FileBrowser/FileBrowserModalContentConnector.js new file mode 100644 index 000000000..adf52fbcd --- /dev/null +++ b/frontend/src/Components/FileBrowser/FileBrowserModalContentConnector.js @@ -0,0 +1,86 @@ +import _ from 'lodash'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import { fetchPaths, clearPaths } from 'Store/Actions/pathActions'; +import FileBrowserModalContent from './FileBrowserModalContent'; + +function createMapStateToProps() { + return createSelector( + (state) => state.paths, + (paths) => { + const { + parent, + currentPath, + directories, + files + } = paths; + + const filteredPaths = _.filter([...directories, ...files], ({ path }) => { + return path.toLowerCase().startsWith(currentPath.toLowerCase()); + }); + + return { + parent, + currentPath, + directories, + files, + paths: filteredPaths + }; + } + ); +} + +const mapDispatchToProps = { + fetchPaths, + clearPaths +}; + +class FileBrowserModalContentConnector extends Component { + + // Lifecycle + + componentDidMount() { + this.props.fetchPaths({ path: this.props.value }); + } + + // + // Listeners + + onFetchPaths = (path) => { + this.props.fetchPaths({ path }); + } + + onClearPaths = () => { + // this.props.clearPaths(); + } + + onModalClose = () => { + this.props.clearPaths(); + this.props.onModalClose(); + } + + // + // Render + + render() { + return ( + + ); + } +} + +FileBrowserModalContentConnector.propTypes = { + value: PropTypes.string, + fetchPaths: PropTypes.func.isRequired, + clearPaths: PropTypes.func.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default connect(createMapStateToProps, mapDispatchToProps)(FileBrowserModalContentConnector); diff --git a/frontend/src/Components/FileBrowser/FileBrowserRow.css b/frontend/src/Components/FileBrowser/FileBrowserRow.css new file mode 100644 index 000000000..a9c34be6a --- /dev/null +++ b/frontend/src/Components/FileBrowser/FileBrowserRow.css @@ -0,0 +1,5 @@ +.type { + composes: cell from 'Components/Table/Cells/TableRowCell.css'; + + width: 32px; +} diff --git a/frontend/src/Components/FileBrowser/FileBrowserRow.js b/frontend/src/Components/FileBrowser/FileBrowserRow.js new file mode 100644 index 000000000..42ac30405 --- /dev/null +++ b/frontend/src/Components/FileBrowser/FileBrowserRow.js @@ -0,0 +1,62 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { icons } from 'Helpers/Props'; +import Icon from 'Components/Icon'; +import TableRowButton from 'Components/Table/TableRowButton'; +import TableRowCell from 'Components/Table/Cells/TableRowCell'; +import styles from './FileBrowserRow.css'; + +function getIconName(type) { + switch (type) { + case 'computer': + return icons.COMPUTER; + case 'drive': + return icons.DRIVE; + case 'file': + return icons.FILE; + case 'parent': + return icons.PARENT; + default: + return icons.FOLDER; + } +} + +class FileBrowserRow extends Component { + + // + // Listeners + + onPress = () => { + this.props.onPress(this.props.path); + } + + // + // Render + + render() { + const { + type, + name + } = this.props; + + return ( + + + + + + {name} + + ); + } + +} + +FileBrowserRow.propTypes = { + type: PropTypes.string.isRequired, + name: PropTypes.string.isRequired, + path: PropTypes.string.isRequired, + onPress: PropTypes.func.isRequired +}; + +export default FileBrowserRow; diff --git a/frontend/src/Components/Form/CaptchaInput.css b/frontend/src/Components/Form/CaptchaInput.css new file mode 100644 index 000000000..e7cd1dc4e --- /dev/null +++ b/frontend/src/Components/Form/CaptchaInput.css @@ -0,0 +1,23 @@ +.captchaInputWrapper { + display: flex; +} + +.input { + composes: input from 'Components/Form/Input.css'; +} + +.hasError { + composes: hasError from 'Components/Form/Input.css'; +} + +.hasWarning { + composes: hasWarning from 'Components/Form/Input.css'; +} + +.hasButton { + composes: hasButton from 'Components/Form/Input.css'; +} + +.recaptchaWrapper { + margin-top: 10px; +} diff --git a/frontend/src/Components/Form/CaptchaInput.js b/frontend/src/Components/Form/CaptchaInput.js new file mode 100644 index 000000000..a8600255a --- /dev/null +++ b/frontend/src/Components/Form/CaptchaInput.js @@ -0,0 +1,86 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import ReCAPTCHA from 'react-google-recaptcha'; +import classNames from 'classnames'; +import { icons } from 'Helpers/Props'; +import Icon from 'Components/Icon'; +import FormInputButton from './FormInputButton'; +import TextInput from './TextInput'; +import styles from './CaptchaInput.css'; + +function CaptchaInput(props) { + const { + className, + name, + value, + hasError, + hasWarning, + refreshing, + siteKey, + secretToken, + onChange, + onRefreshPress, + onCaptchaChange + } = props; + + return ( +
+
+ + + + + +
+ + { + !!siteKey && !!secretToken && +
+ +
+ } +
+ ); +} + +CaptchaInput.propTypes = { + className: PropTypes.string.isRequired, + name: PropTypes.string.isRequired, + value: PropTypes.string.isRequired, + hasError: PropTypes.bool, + hasWarning: PropTypes.bool, + refreshing: PropTypes.bool.isRequired, + siteKey: PropTypes.string, + secretToken: PropTypes.string, + onChange: PropTypes.func.isRequired, + onRefreshPress: PropTypes.func.isRequired, + onCaptchaChange: PropTypes.func.isRequired +}; + +CaptchaInput.defaultProps = { + className: styles.input, + value: '' +}; + +export default CaptchaInput; diff --git a/frontend/src/Components/Form/CaptchaInputConnector.js b/frontend/src/Components/Form/CaptchaInputConnector.js new file mode 100644 index 000000000..17b875c88 --- /dev/null +++ b/frontend/src/Components/Form/CaptchaInputConnector.js @@ -0,0 +1,98 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import { refreshCaptcha, getCaptchaCookie, resetCaptcha } from 'Store/Actions/captchaActions'; +import CaptchaInput from './CaptchaInput'; + +function createMapStateToProps() { + return createSelector( + (state) => state.captcha, + (captcha) => { + return captcha; + } + ); +} + +const mapDispatchToProps = { + refreshCaptcha, + getCaptchaCookie, + resetCaptcha +}; + +class CaptchaInputConnector extends Component { + + // + // Lifecycle + + componentDidUpdate(prevProps) { + const { + name, + token, + onChange + } = this.props; + + if (token && token !== prevProps.token) { + onChange({ name, value: token }); + } + } + + componentWillUnmount = () => { + this.props.resetCaptcha(); + } + + // + // Listeners + + onRefreshPress = () => { + const { + provider, + providerData + } = this.props; + + this.props.refreshCaptcha({ provider, providerData }); + } + + onCaptchaChange = (captchaResponse) => { + // If the captcha has expired `captchaResponse` will be null. + // In the event it's null don't try to get the captchaCookie. + // TODO: Should we clear the cookie? or reset the captcha? + + if (!captchaResponse) { + return; + } + + const { + provider, + providerData + } = this.props; + + this.props.getCaptchaCookie({ provider, providerData, captchaResponse }); + } + + // + // Render + + render() { + return ( + + ); + } +} + +CaptchaInputConnector.propTypes = { + provider: PropTypes.string.isRequired, + providerData: PropTypes.object.isRequired, + name: PropTypes.string.isRequired, + token: PropTypes.string, + onChange: PropTypes.func.isRequired, + refreshCaptcha: PropTypes.func.isRequired, + getCaptchaCookie: PropTypes.func.isRequired, + resetCaptcha: PropTypes.func.isRequired +}; + +export default connect(createMapStateToProps, mapDispatchToProps)(CaptchaInputConnector); diff --git a/frontend/src/Components/Form/CheckInput.css b/frontend/src/Components/Form/CheckInput.css new file mode 100644 index 000000000..5c35e5d2f --- /dev/null +++ b/frontend/src/Components/Form/CheckInput.css @@ -0,0 +1,105 @@ +.container { + position: relative; + display: flex; + flex: 1 1 65%; + user-select: none; +} + +.label { + display: flex; + margin-bottom: 0; + min-height: 21px; + font-weight: normal; + cursor: pointer; +} + +.checkbox { + position: absolute; + opacity: 0; + cursor: pointer; + pointer-events: none; + + &:global(.isDisabled) { + cursor: not-allowed; + } +} + +.input { + flex: 1 0 auto; + margin-top: 7px; + margin-right: 5px; + width: 20px; + height: 20px; + border: 1px solid #ccc; + border-radius: 2px; + background-color: $white; + color: $white; + text-align: center; + line-height: 20px; +} + +.checkbox:focus + .input { + outline: 0; + border-color: $inputFocusBorderColor; + box-shadow: inset 0 1px 1px $inputBoxShadowColor, 0 0 8px $inputFocusBoxShadowColor; +} + +.dangerIsChecked { + border-color: $dangerColor; + background-color: $dangerColor; + + &.isDisabled { + opacity: 0.7; + } +} + +.primaryIsChecked { + border-color: $primaryColor; + background-color: $primaryColor; + + &.isDisabled { + opacity: 0.7; + } +} + +.successIsChecked { + border-color: $successColor; + background-color: $successColor; + + &.isDisabled { + opacity: 0.7; + } +} + +.warningIsChecked { + border-color: $warningColor; + background-color: $warningColor; + + &.isDisabled { + opacity: 0.7; + } +} + +.isNotChecked { + &.isDisabled { + border-color: $disabledCheckInputColor; + background-color: $disabledCheckInputColor; + opacity: 0.7; + } +} + +.isIndeterminate { + border-color: $gray; + background-color: $gray; +} + +.helpText { + composes: helpText from 'Components/Form/FormInputHelpText.css'; + + margin-top: 8px; + margin-left: 5px; +} + +.isDisabled { + cursor: not-allowed; +} diff --git a/frontend/src/Components/Form/CheckInput.js b/frontend/src/Components/Form/CheckInput.js new file mode 100644 index 000000000..0fc0be9cd --- /dev/null +++ b/frontend/src/Components/Form/CheckInput.js @@ -0,0 +1,187 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import classNames from 'classnames'; +import { icons, kinds } from 'Helpers/Props'; +import Icon from 'Components/Icon'; +import FormInputHelpText from './FormInputHelpText'; +import styles from './CheckInput.css'; + +class CheckInput extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this._checkbox = null; + } + + componentDidMount() { + this.setIndeterminate(); + } + + componentDidUpdate() { + this.setIndeterminate(); + } + + // + // Control + + setIndeterminate() { + if (!this._checkbox) { + return; + } + + const { + value, + uncheckedValue, + checkedValue + } = this.props; + + this._checkbox.indeterminate = value !== uncheckedValue && value !== checkedValue; + } + + toggleChecked = (checked, shiftKey) => { + const { + name, + value, + checkedValue, + uncheckedValue + } = this.props; + + const newValue = checked ? checkedValue : uncheckedValue; + + if (value !== newValue) { + this.props.onChange({ + name, + value: newValue, + shiftKey + }); + } + } + + // + // Listeners + + setRef = (ref) => { + this._checkbox = ref; + } + + onClick = (event) => { + const shiftKey = event.nativeEvent.shiftKey; + const checked = !this._checkbox.checked; + + event.preventDefault(); + this.toggleChecked(checked, shiftKey); + } + + onChange = (event) => { + const checked = event.target.checked; + const shiftKey = event.nativeEvent.shiftKey; + + this.toggleChecked(checked, shiftKey); + } + + // + // Render + + render() { + const { + className, + containerClassName, + name, + value, + checkedValue, + uncheckedValue, + helpText, + helpTextWarning, + isDisabled, + kind + } = this.props; + + const isChecked = value === checkedValue; + const isUnchecked = value === uncheckedValue; + const isIndeterminate = !isChecked && !isUnchecked; + const isCheckClass = `${kind}IsChecked`; + + return ( +
+ +
+ ); + } +} + +CheckInput.propTypes = { + className: PropTypes.string.isRequired, + containerClassName: PropTypes.string.isRequired, + name: PropTypes.string.isRequired, + checkedValue: PropTypes.bool, + uncheckedValue: PropTypes.bool, + value: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]), + helpText: PropTypes.string, + helpTextWarning: PropTypes.string, + isDisabled: PropTypes.bool, + kind: PropTypes.oneOf(kinds.all).isRequired, + onChange: PropTypes.func.isRequired +}; + +CheckInput.defaultProps = { + className: styles.input, + containerClassName: styles.container, + checkedValue: true, + uncheckedValue: false, + kind: kinds.PRIMARY +}; + +export default CheckInput; diff --git a/frontend/src/Components/Form/EnhancedSelectInput.css b/frontend/src/Components/Form/EnhancedSelectInput.css new file mode 100644 index 000000000..4662cc581 --- /dev/null +++ b/frontend/src/Components/Form/EnhancedSelectInput.css @@ -0,0 +1,66 @@ +.tether { + z-index: 2000; +} + +.enhancedSelect { + composes: input from 'Components/Form/Input.css'; + composes: link from 'Components/Link/Link.css'; + + position: relative; + display: flex; + align-items: center; + padding: 6px 16px; + width: 100%; + height: 35px; + border: 1px solid $inputBorderColor; + border-radius: 4px; + background-color: $white; + box-shadow: inset 0 1px 1px $inputBoxShadowColor; + color: $black; + cursor: default; +} + +.hasError { + composes: hasError from 'Components/Form/Input.css'; +} + +.hasWarning { + composes: hasWarning from 'Components/Form/Input.css'; +} + +.isDisabled { + opacity: 0.7; + cursor: not-allowed; +} + +.dropdownArrowContainer { + margin-left: 12px; +} + +.optionsContainer { + width: auto; +} + +.options { + border: 1px solid $inputBorderColor; + border-radius: 4px; + background-color: $white; +} + +.optionsModal { + display: flex; + justify-content: center; + max-width: 90%; + width: 350px !important; + height: auto !important; +} + +.optionsInnerModalBody { + composes: innerModalBody from 'Components/Modal/ModalBody.css'; + + padding: 0; + width: 100%; + border: 1px solid $inputBorderColor; + border-radius: 4px; + background-color: $white; +} diff --git a/frontend/src/Components/Form/EnhancedSelectInput.js b/frontend/src/Components/Form/EnhancedSelectInput.js new file mode 100644 index 000000000..6310b4854 --- /dev/null +++ b/frontend/src/Components/Form/EnhancedSelectInput.js @@ -0,0 +1,399 @@ +import _ from 'lodash'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import ReactDOM from 'react-dom'; +import Measure from 'react-measure'; +import TetherComponent from 'react-tether'; +import classNames from 'classnames'; +import isMobileUtil from 'Utilities/isMobile'; +import * as keyCodes from 'Utilities/Constants/keyCodes'; +import { icons } from 'Helpers/Props'; +import Icon from 'Components/Icon'; +import Link from 'Components/Link/Link'; +import Modal from 'Components/Modal/Modal'; +import ModalBody from 'Components/Modal/ModalBody'; +import EnhancedSelectInputSelectedValue from './EnhancedSelectInputSelectedValue'; +import EnhancedSelectInputOption from './EnhancedSelectInputOption'; +import styles from './EnhancedSelectInput.css'; + +const tetherOptions = { + skipMoveElement: true, + constraints: [ + { + to: 'window', + attachment: 'together', + pin: true + } + ], + attachment: 'top left', + targetAttachment: 'bottom left' +}; + +function isArrowKey(keyCode) { + return keyCode === keyCodes.UP_ARROW || keyCode === keyCodes.DOWN_ARROW; +} + +function getSelectedOption(selectedIndex, values) { + return values[selectedIndex]; +} + +function findIndex(startingIndex, direction, values) { + let indexToTest = startingIndex + direction; + + while (indexToTest !== startingIndex) { + if (indexToTest < 0) { + indexToTest = values.length - 1; + } else if (indexToTest >= values.length) { + indexToTest = 0; + } + + if (getSelectedOption(indexToTest, values).isDisabled) { + indexToTest = indexToTest + direction; + } else { + return indexToTest; + } + } +} + +function previousIndex(selectedIndex, values) { + return findIndex(selectedIndex, -1, values); +} + +function nextIndex(selectedIndex, values) { + return findIndex(selectedIndex, 1, values); +} + +function getSelectedIndex(props) { + const { + value, + values + } = props; + + return values.findIndex((v) => { + return v.key === value; + }); +} + +function getKey(selectedIndex, values) { + return values[selectedIndex].key; +} + +class EnhancedSelectInput extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + isOpen: false, + selectedIndex: getSelectedIndex(props), + width: 0, + isMobile: isMobileUtil() + }; + } + + componentDidUpdate(prevProps) { + if (prevProps.value !== this.props.value) { + this.setState({ + selectedIndex: getSelectedIndex(this.props) + }); + } + } + + // + // Control + + _setButtonRef = (ref) => { + this._buttonRef = ref; + } + + _setOptionsRef = (ref) => { + this._optionsRef = ref; + } + + _addListener() { + window.addEventListener('click', this.onWindowClick); + } + + _removeListener() { + window.removeEventListener('click', this.onWindowClick); + } + + // + // Listeners + + onWindowClick = (event) => { + const button = ReactDOM.findDOMNode(this._buttonRef); + const options = ReactDOM.findDOMNode(this._optionsRef); + + if (!button || this.state.isMobile) { + return; + } + + if ( + !button.contains(event.target) && + options && + !options.contains(event.target) && + this.state.isOpen + ) { + this.setState({ isOpen: false }); + this._removeListener(); + } + } + + onBlur = () => { + this.setState({ + selectedIndex: getSelectedIndex(this.props) + }); + } + + onKeyDown = (event) => { + const { + values + } = this.props; + + const { + isOpen, + selectedIndex + } = this.state; + + const keyCode = event.keyCode; + const newState = {}; + + if (!isOpen) { + if (isArrowKey(keyCode)) { + event.preventDefault(); + newState.isOpen = true; + } + + if ( + selectedIndex == null || + getSelectedOption(selectedIndex, values).isDisabled + ) { + if (keyCode === keyCodes.UP_ARROW) { + newState.selectedIndex = previousIndex(0, values); + } else if (keyCode === keyCodes.DOWN_ARROW) { + newState.selectedIndex = nextIndex(values.length - 1, values); + } + } + + this.setState(newState); + return; + } + + if (keyCode === keyCodes.UP_ARROW) { + event.preventDefault(); + newState.selectedIndex = previousIndex(selectedIndex, values); + } + + if (keyCode === keyCodes.DOWN_ARROW) { + event.preventDefault(); + newState.selectedIndex = nextIndex(selectedIndex, values); + } + + if (keyCode === keyCodes.ENTER) { + event.preventDefault(); + newState.isOpen = false; + this.onSelect(getKey(selectedIndex, values)); + } + + if (keyCode === keyCodes.TAB) { + newState.isOpen = false; + this.onSelect(getKey(selectedIndex, values)); + } + + if (keyCode === keyCodes.ESCAPE) { + event.preventDefault(); + event.stopPropagation(); + newState.isOpen = false; + newState.selectedIndex = getSelectedIndex(this.props); + } + + if (!_.isEmpty(newState)) { + this.setState(newState); + } + } + + onPress = () => { + if (this.state.isOpen) { + this._removeListener(); + } else { + this._addListener(); + } + + this.setState({ isOpen: !this.state.isOpen }); + } + + onSelect = (value) => { + this.setState({ isOpen: false }); + + this.props.onChange({ + name: this.props.name, + value + }); + } + + onMeasure = ({ width }) => { + this.setState({ width }); + } + + onOptionsModalClose = () => { + this.setState({ isOpen: false }); + } + + // + // Render + + render() { + const { + className, + disabledClassName, + values, + isDisabled, + hasError, + hasWarning, + selectedValueOptions, + selectedValueComponent: SelectedValueComponent, + optionComponent: OptionComponent + } = this.props; + + const { + selectedIndex, + width, + isOpen, + isMobile + } = this.state; + + const selectedOption = getSelectedOption(selectedIndex, values); + + return ( +
+ + + + + {selectedOption ? selectedOption.value : null} + + +
+ +
+ +
+ + { + isOpen && !isMobile && +
+
+ { + values.map((v, index) => { + return ( + + {v.value} + + ); + }) + } +
+
+ } +
+ + { + isMobile && + + + { + values.map((v, index) => { + return ( + + {v.value} + + ); + }) + } + + + } +
+ ); + } +} + +EnhancedSelectInput.propTypes = { + className: PropTypes.string, + disabledClassName: PropTypes.string, + name: PropTypes.string.isRequired, + value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired, + values: PropTypes.arrayOf(PropTypes.object).isRequired, + isDisabled: PropTypes.bool, + hasError: PropTypes.bool, + hasWarning: PropTypes.bool, + selectedValueOptions: PropTypes.object.isRequired, + selectedValueComponent: PropTypes.oneOfType([PropTypes.string, PropTypes.func]).isRequired, + optionComponent: PropTypes.func, + onChange: PropTypes.func.isRequired +}; + +EnhancedSelectInput.defaultProps = { + className: styles.enhancedSelect, + disabledClassName: styles.isDisabled, + isDisabled: false, + selectedValueOptions: {}, + selectedValueComponent: EnhancedSelectInputSelectedValue, + optionComponent: EnhancedSelectInputOption +}; + +export default EnhancedSelectInput; diff --git a/frontend/src/Components/Form/EnhancedSelectInputOption.css b/frontend/src/Components/Form/EnhancedSelectInputOption.css new file mode 100644 index 000000000..dedf7beaa --- /dev/null +++ b/frontend/src/Components/Form/EnhancedSelectInputOption.css @@ -0,0 +1,37 @@ +.option { + display: flex; + align-items: center; + justify-content: space-between; + padding: 5px 10px; + width: 100%; + cursor: default; + + &:hover { + background-color: #f9f9f9; + } +} + +.isSelected { + background-color: #e2e2e2; + + &.isMobile { + background-color: inherit; + + .iconContainer { + color: $primaryColor; + } + } +} + +.isDisabled { + background-color: #aaa; +} + +.isMobile { + height: 50px; + border-bottom: 1px solid $borderColor; + + &:last-child { + border: none; + } +} diff --git a/frontend/src/Components/Form/EnhancedSelectInputOption.js b/frontend/src/Components/Form/EnhancedSelectInputOption.js new file mode 100644 index 000000000..a1a161c79 --- /dev/null +++ b/frontend/src/Components/Form/EnhancedSelectInputOption.js @@ -0,0 +1,77 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import classNames from 'classnames'; +import { icons } from 'Helpers/Props'; +import Icon from 'Components/Icon'; +import Link from 'Components/Link/Link'; +import styles from './EnhancedSelectInputOption.css'; + +class EnhancedSelectInputOption extends Component { + + // + // Listeners + + onPress = () => { + const { + id, + onSelect + } = this.props; + + onSelect(id); + } + + // + // Render + + render() { + const { + className, + isSelected, + isDisabled, + isMobile, + children + } = this.props; + + return ( + + {children} + + { + isMobile && +
+ +
+ } + + ); + } +} + +EnhancedSelectInputOption.propTypes = { + className: PropTypes.string.isRequired, + id: PropTypes.string.isRequired, + isSelected: PropTypes.bool.isRequired, + isDisabled: PropTypes.bool.isRequired, + isMobile: PropTypes.bool.isRequired, + children: PropTypes.node.isRequired, + onSelect: PropTypes.func.isRequired +}; + +EnhancedSelectInputOption.defaultProps = { + className: styles.option, + isDisabled: false +}; + +export default EnhancedSelectInputOption; diff --git a/frontend/src/Components/Form/EnhancedSelectInputSelectedValue.css b/frontend/src/Components/Form/EnhancedSelectInputSelectedValue.css new file mode 100644 index 000000000..aab9f1b7d --- /dev/null +++ b/frontend/src/Components/Form/EnhancedSelectInputSelectedValue.css @@ -0,0 +1,3 @@ +.selectedValue { + flex: 1 1 auto; +} diff --git a/frontend/src/Components/Form/EnhancedSelectInputSelectedValue.js b/frontend/src/Components/Form/EnhancedSelectInputSelectedValue.js new file mode 100644 index 000000000..2343fedc2 --- /dev/null +++ b/frontend/src/Components/Form/EnhancedSelectInputSelectedValue.js @@ -0,0 +1,27 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import styles from './EnhancedSelectInputSelectedValue.css'; + +function EnhancedSelectInputSelectedValue(props) { + const { + className, + children + } = props; + + return ( +
+ {children} +
+ ); +} + +EnhancedSelectInputSelectedValue.propTypes = { + className: PropTypes.string.isRequired, + children: PropTypes.node +}; + +EnhancedSelectInputSelectedValue.defaultProps = { + className: styles.selectedValue +}; + +export default EnhancedSelectInputSelectedValue; diff --git a/frontend/src/Components/Form/Form.css b/frontend/src/Components/Form/Form.css new file mode 100644 index 000000000..987b1a0a1 --- /dev/null +++ b/frontend/src/Components/Form/Form.css @@ -0,0 +1,11 @@ +.form { + +} + +.error { + color: $dangerColor; +} + +.warning { + color: $warningColor; +} diff --git a/frontend/src/Components/Form/Form.js b/frontend/src/Components/Form/Form.js new file mode 100644 index 000000000..9a605297a --- /dev/null +++ b/frontend/src/Components/Form/Form.js @@ -0,0 +1,53 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import { kinds } from 'Helpers/Props'; +import Alert from 'Components/Alert'; + +function Form({ children, validationErrors, validationWarnings, ...otherProps }) { + return ( +
+
+ { + validationErrors.map((error, index) => { + return ( + + {error.errorMessage} + + ); + }) + } + + { + validationWarnings.map((warning, index) => { + return ( + + {warning.errorMessage} + + ); + }) + } +
+ + {children} +
+ ); +} + +Form.propTypes = { + children: PropTypes.node.isRequired, + validationErrors: PropTypes.arrayOf(PropTypes.object).isRequired, + validationWarnings: PropTypes.arrayOf(PropTypes.object).isRequired +}; + +Form.defaultProps = { + validationErrors: [], + validationWarnings: [] +}; + +export default Form; diff --git a/frontend/src/Components/Form/FormGroup.css b/frontend/src/Components/Form/FormGroup.css new file mode 100644 index 000000000..41a56eff0 --- /dev/null +++ b/frontend/src/Components/Form/FormGroup.css @@ -0,0 +1,24 @@ +.group { + display: flex; + margin-bottom: 20px; +} + +/* Sizes */ + +.small { + max-width: $formGroupSmallWidth; +} + +.medium { + max-width: $formGroupMediumWidth; +} + +.large { + max-width: $formGroupLargeWidth; +} + +@media only screen and (max-width: $breakpointLarge) { + .group { + display: block; + } +} diff --git a/frontend/src/Components/Form/FormGroup.js b/frontend/src/Components/Form/FormGroup.js new file mode 100644 index 000000000..edec4b86d --- /dev/null +++ b/frontend/src/Components/Form/FormGroup.js @@ -0,0 +1,56 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import classNames from 'classnames'; +import { map } from 'Helpers/elementChildren'; +import { sizes } from 'Helpers/Props'; +import styles from './FormGroup.css'; + +function FormGroup(props) { + const { + className, + children, + size, + advancedSettings, + isAdvanced, + ...otherProps + } = props; + + if (!advancedSettings && isAdvanced) { + return null; + } + + const childProps = isAdvanced ? { isAdvanced } : {}; + + return ( +
+ { + map(children, (child) => { + return React.cloneElement(child, childProps); + }) + } +
+ ); +} + +FormGroup.propTypes = { + className: PropTypes.string.isRequired, + children: PropTypes.node.isRequired, + size: PropTypes.string.isRequired, + advancedSettings: PropTypes.bool.isRequired, + isAdvanced: PropTypes.bool.isRequired +}; + +FormGroup.defaultProps = { + className: styles.group, + size: sizes.SMALL, + advancedSettings: false, + isAdvanced: false +}; + +export default FormGroup; diff --git a/frontend/src/Components/Form/FormInputButton.css b/frontend/src/Components/Form/FormInputButton.css new file mode 100644 index 000000000..27a1923be --- /dev/null +++ b/frontend/src/Components/Form/FormInputButton.css @@ -0,0 +1,12 @@ +.button { + composes: button from 'Components/Link/Button.css'; + + border-left: none; + border-top-left-radius: 0; + border-bottom-left-radius: 0; +} + +.middleButton { + border-top-right-radius: 0; + border-bottom-right-radius: 0; +} diff --git a/frontend/src/Components/Form/FormInputButton.js b/frontend/src/Components/Form/FormInputButton.js new file mode 100644 index 000000000..4b6491663 --- /dev/null +++ b/frontend/src/Components/Form/FormInputButton.js @@ -0,0 +1,54 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import classNames from 'classnames'; +import Button from 'Components/Link/Button'; +import SpinnerButton from 'Components/Link/SpinnerButton'; +import { kinds } from 'Helpers/Props'; +import styles from './FormInputButton.css'; + +function FormInputButton(props) { + const { + className, + canSpin, + isLastButton, + ...otherProps + } = props; + + if (canSpin) { + return ( + + ); + } + + return ( + + ); +} + +SpinnerButton.propTypes = { + className: PropTypes.string.isRequired, + isSpinning: PropTypes.bool.isRequired, + isDisabled: PropTypes.bool, + spinnerIcon: PropTypes.string.isRequired, + children: PropTypes.node +}; + +SpinnerButton.defaultProps = { + className: styles.button, + spinnerIcon: icons.SPINNER +}; + +export default SpinnerButton; diff --git a/frontend/src/Components/Link/SpinnerErrorButton.css b/frontend/src/Components/Link/SpinnerErrorButton.css new file mode 100644 index 000000000..5f4e68545 --- /dev/null +++ b/frontend/src/Components/Link/SpinnerErrorButton.css @@ -0,0 +1,23 @@ +.iconContainer { + composes: spinnerContainer from 'Components/Link/SpinnerButton.css'; +} + +.icon { + z-index: 1; +} + +.label { + composes: label from 'Components/Link/SpinnerButton.css'; +} + +.showIcon { + .iconContainer { + left: 50%; + visibility: visible; + } + + .label { + left: 100%; + opacity: 0; + } +} diff --git a/frontend/src/Components/Link/SpinnerErrorButton.js b/frontend/src/Components/Link/SpinnerErrorButton.js new file mode 100644 index 000000000..87cf55d95 --- /dev/null +++ b/frontend/src/Components/Link/SpinnerErrorButton.js @@ -0,0 +1,162 @@ +import _ from 'lodash'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { icons, kinds } from 'Helpers/Props'; +import Icon from 'Components/Icon'; +import SpinnerButton from 'Components/Link/SpinnerButton'; +import styles from './SpinnerErrorButton.css'; + +function getTestResult(error) { + if (!error) { + return { + wasSuccessful: true, + hasWarning: false, + hasError: false + }; + } + + if (error.status !== 400) { + return { + wasSuccessful: false, + hasWarning: false, + hasError: true + }; + } + + const failures = error.responseJSON; + + const hasWarning = _.some(failures, { isWarning: true }); + const hasError = _.some(failures, (failure) => !failure.isWarning); + + return { + wasSuccessful: false, + hasWarning, + hasError + }; +} + +class SpinnerErrorButton extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this._testResultTimeout = null; + + this.state = { + wasSuccessful: false, + hasWarning: false, + hasError: false + }; + } + + componentDidUpdate(prevProps) { + const { + isSpinning, + error + } = this.props; + + if (prevProps.isSpinning && !isSpinning) { + const testResult = getTestResult(error); + + this.setState(testResult, () => { + const { + wasSuccessful, + hasWarning, + hasError + } = testResult; + + if (wasSuccessful || hasWarning || hasError) { + this._testResultTimeout = setTimeout(this.resetState, 3000); + } + }); + } + } + + componentWillUnmount() { + if (this._testResultTimeout) { + clearTimeout(this._testResultTimeout); + } + } + + // + // Control + + resetState = () => { + this.setState({ + wasSuccessful: false, + hasWarning: false, + hasError: false + }); + } + + // + // Render + + render() { + const { + isSpinning, + error, + children, + ...otherProps + } = this.props; + + const { + wasSuccessful, + hasWarning, + hasError + } = this.state; + + const showIcon = wasSuccessful || hasWarning || hasError; + + let iconName = icons.CHECK; + let iconKind = kinds.SUCCESS; + + if (hasWarning) { + iconName = icons.WARNING; + iconKind = kinds.WARNING; + } + + if (hasError) { + iconName = icons.DANGER; + iconKind = kinds.DANGER; + } + + return ( + + + { + showIcon && + + + + } + + { + + { + children + } + + } + + + ); + } +} + +SpinnerErrorButton.propTypes = { + isSpinning: PropTypes.bool.isRequired, + error: PropTypes.object, + children: PropTypes.node.isRequired +}; + +export default SpinnerErrorButton; diff --git a/frontend/src/Components/Link/SpinnerIconButton.js b/frontend/src/Components/Link/SpinnerIconButton.js new file mode 100644 index 000000000..8f62d6031 --- /dev/null +++ b/frontend/src/Components/Link/SpinnerIconButton.js @@ -0,0 +1,37 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import { icons } from 'Helpers/Props'; +import IconButton from './IconButton'; + +function SpinnerIconButton(props) { + const { + name, + spinningName, + isDisabled, + isSpinning, + ...otherProps + } = props; + + return ( + + ); +} + +SpinnerIconButton.propTypes = { + name: PropTypes.string.isRequired, + spinningName: PropTypes.string.isRequired, + isDisabled: PropTypes.bool.isRequired, + isSpinning: PropTypes.bool.isRequired +}; + +SpinnerIconButton.defaultProps = { + spinningName: icons.SPINNER, + isDisabled: false, + isSpinning: false +}; + +export default SpinnerIconButton; diff --git a/frontend/src/Components/Loading/LoadingIndicator.css b/frontend/src/Components/Loading/LoadingIndicator.css new file mode 100644 index 000000000..fd224b1d6 --- /dev/null +++ b/frontend/src/Components/Loading/LoadingIndicator.css @@ -0,0 +1,49 @@ +.loading { + margin-top: 20px; + text-align: center; +} + +.rippleContainer { + position: relative; + display: inline-block; +} + +.ripple:nth-child(0) { + animation-delay: -0.8s; +} + +.ripple:nth-child(1) { + animation-delay: -0.6s; +} + +.ripple:nth-child(2) { + animation-delay: -0.4s; +} + +.ripple:nth-child(3) { + animation-delay: -0.2s; +} + +.ripple { + position: absolute; + border: 2px solid #3a3f51; + border-radius: 100%; + animation: rippleContainer 1.25s 0s infinite cubic-bezier(0.21, 0.53, 0.56, 0.8); + animation-fill-mode: both; +} + +@keyframes rippleContainer { + 0% { + opacity: 1; + transform: scale(0.1); + } + + 70% { + opacity: 0.7; + transform: scale(1); + } + + 100% { + opacity: 0; + } +} diff --git a/frontend/src/Components/Loading/LoadingIndicator.js b/frontend/src/Components/Loading/LoadingIndicator.js new file mode 100644 index 000000000..5f9a15b1a --- /dev/null +++ b/frontend/src/Components/Loading/LoadingIndicator.js @@ -0,0 +1,48 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import styles from './LoadingIndicator.css'; + +function LoadingIndicator({ className, size }) { + const sizeInPx = `${size}px`; + const width = sizeInPx; + const height = sizeInPx; + + return ( +
+
+
+ +
+ +
+
+
+ ); +} + +LoadingIndicator.propTypes = { + className: PropTypes.string, + size: PropTypes.number +}; + +LoadingIndicator.defaultProps = { + className: styles.loading, + size: 50 +}; + +export default LoadingIndicator; diff --git a/frontend/src/Components/Loading/LoadingMessage.css b/frontend/src/Components/Loading/LoadingMessage.css new file mode 100644 index 000000000..a7b39e76f --- /dev/null +++ b/frontend/src/Components/Loading/LoadingMessage.css @@ -0,0 +1,6 @@ +.loadingMessage { + margin: 50px 10px 0; + text-align: center; + font-weight: 300; + font-size: 36px; +} diff --git a/frontend/src/Components/Loading/LoadingMessage.js b/frontend/src/Components/Loading/LoadingMessage.js new file mode 100644 index 000000000..b27b49f00 --- /dev/null +++ b/frontend/src/Components/Loading/LoadingMessage.js @@ -0,0 +1,36 @@ +import React from 'react'; +import styles from './LoadingMessage.css'; + +const messages = [ + 'Downloading more RAM', + 'Now in Technicolor', + 'Previously on Lidarr...', + 'Bleep Bloop.', + 'Locating the required gigapixels to render...', + 'Spinning up the hamster wheel...', + 'At least you\'re not on hold', + 'Hum something loud while others stare', + 'Loading humorous message... Please Wait', + 'I could\'ve been faster in Python', + 'Don\'t forget to rewind your episodes', + 'Congratulations! you are the 1000th visitor.', + 'HELP!, I\'m being held hostage and forced to write these stupid lines!', + 'RE-calibrating the internet...', + 'I\'ll be here all week', + 'Don\'t forget to tip your waitress', + 'Apply directly to the forehead', + 'Loading Battlestation' +]; + +function LoadingMessage() { + const index = Math.floor(Math.random() * messages.length); + const message = messages[index]; + + return ( +
+ {message} +
+ ); +} + +export default LoadingMessage; diff --git a/frontend/src/Components/Menu/FilterMenu.css b/frontend/src/Components/Menu/FilterMenu.css new file mode 100644 index 000000000..34991aed9 --- /dev/null +++ b/frontend/src/Components/Menu/FilterMenu.css @@ -0,0 +1,9 @@ +.filterMenu { + composes: menu from './Menu.css'; +} + +@media only screen and (max-width: $breakpointSmall) { + .filterMenu { + margin-right: 10px; + } +} diff --git a/frontend/src/Components/Menu/FilterMenu.js b/frontend/src/Components/Menu/FilterMenu.js new file mode 100644 index 000000000..6a76db432 --- /dev/null +++ b/frontend/src/Components/Menu/FilterMenu.js @@ -0,0 +1,38 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import { icons } from 'Helpers/Props'; +import Menu from 'Components/Menu/Menu'; +import ToolbarMenuButton from 'Components/Menu/ToolbarMenuButton'; +import styles from './FilterMenu.css'; + +function FilterMenu(props) { + const { + className, + children, + ...otherProps + } = props; + + return ( + + + {children} + + ); +} + +FilterMenu.propTypes = { + className: PropTypes.string, + children: PropTypes.node.isRequired +}; + +FilterMenu.defaultProps = { + className: styles.filterMenu +}; + +export default FilterMenu; diff --git a/frontend/src/Components/Menu/FilterMenuItem.js b/frontend/src/Components/Menu/FilterMenuItem.js new file mode 100644 index 000000000..54c293c49 --- /dev/null +++ b/frontend/src/Components/Menu/FilterMenuItem.js @@ -0,0 +1,57 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import SelectedMenuItem from './SelectedMenuItem'; + +class FilterMenuItem extends Component { + + // + // Listeners + + onPress = () => { + const { + name, + value, + onPress + } = this.props; + + onPress(name, value); + } + + // + // Render + + render() { + const { + name, + value, + filterKey, + filterValue, + ...otherProps + } = this.props; + + const isSelected = name === filterKey && value === filterValue; + + return ( + + ); + } +} + +FilterMenuItem.propTypes = { + name: PropTypes.string, + value: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.bool]), + filterKey: PropTypes.string, + filterValue: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.bool]), + onPress: PropTypes.func.isRequired +}; + +FilterMenuItem.defaultProps = { + name: null, + value: null +}; + +export default FilterMenuItem; diff --git a/frontend/src/Components/Menu/Menu.css b/frontend/src/Components/Menu/Menu.css new file mode 100644 index 000000000..9cce48fee --- /dev/null +++ b/frontend/src/Components/Menu/Menu.css @@ -0,0 +1,7 @@ +.tether { + z-index: 2000; +} + +.menu { + position: relative; +} diff --git a/frontend/src/Components/Menu/Menu.js b/frontend/src/Components/Menu/Menu.js new file mode 100644 index 000000000..06e38dcf7 --- /dev/null +++ b/frontend/src/Components/Menu/Menu.js @@ -0,0 +1,203 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import ReactDOM from 'react-dom'; +import TetherComponent from 'react-tether'; +import { align } from 'Helpers/Props'; +import styles from './Menu.css'; + +const baseTetherOptions = { + skipMoveElement: true, + constraints: [ + { + to: 'window', + attachment: 'together', + pin: true + } + ] +}; + +const tetherOptions = { + [align.RIGHT]: { + ...baseTetherOptions, + attachment: 'top right', + targetAttachment: 'bottom right' + }, + + [align.LEFT]: { + ...baseTetherOptions, + attachment: 'top left', + targetAttachment: 'bottom left' + } +}; + +class Menu extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + isMenuOpen: false, + maxHeight: 0 + }; + } + + componentDidMount() { + this.setMaxHeight(); + } + + // + // Control + + getMaxHeight() { + if (!this.props.enforceMaxHeight) { + return; + } + + const menu = ReactDOM.findDOMNode(this.refs.menu); + + if (!menu) { + return; + } + + const { bottom } = menu.getBoundingClientRect(); + const maxHeight = window.innerHeight - bottom; + + return maxHeight; + } + + setMaxHeight() { + this.setState({ + maxHeight: this.getMaxHeight() + }); + } + + _addListener() { + // Listen to resize events on the window and scroll events + // on all elements to ensure the menu is the best size possible. + // Listen for click events on the window to support closing the + // menu on clicks outside. + + window.addEventListener('resize', this.onWindowResize); + window.addEventListener('scroll', this.onWindowScroll, { capture: true }); + window.addEventListener('click', this.onWindowClick); + } + + _removeListener() { + window.removeEventListener('resize', this.onWindowResize); + window.removeEventListener('scroll', this.onWindowScroll, { capture: true }); + window.removeEventListener('click', this.onWindowClick); + } + + // + // Listeners + + onWindowClick = (event) => { + const menu = ReactDOM.findDOMNode(this.refs.menu); + const menuContent = ReactDOM.findDOMNode(this.refs.menuContent); + + if (!menu) { + return; + } + + if ((!menu.contains(event.target) || menuContent.contains(event.target)) && this.state.isMenuOpen) { + this.setState({ isMenuOpen: false }); + this._removeListener(); + } + } + + onWindowResize = () => { + this.setMaxHeight(); + } + + onWindowScroll = () => { + this.setMaxHeight(); + } + + onMenuButtonPress = () => { + const state = { + isMenuOpen: !this.state.isMenuOpen + }; + + if (this.state.isMenuOpen) { + this._removeListener(); + } else { + state.maxHeight = this.getMaxHeight(); + this._addListener(); + } + + this.setState(state); + } + + // + // Render + + render() { + const { + className, + children, + alignMenu + } = this.props; + + const { + maxHeight, + isMenuOpen + } = this.state; + + const childrenArray = React.Children.toArray(children); + const button = React.cloneElement( + childrenArray[0], + { + onPress: this.onMenuButtonPress + } + ); + + const content = React.cloneElement( + childrenArray[1], + { + ref: 'menuContent', + alignMenu, + maxHeight, + isOpen: isMenuOpen + } + ); + + return ( + +
+ {button} +
+ + { + isMenuOpen && + content + } +
+ ); + } +} + +Menu.propTypes = { + className: PropTypes.string, + children: PropTypes.node.isRequired, + alignMenu: PropTypes.oneOf([align.LEFT, align.RIGHT]), + enforceMaxHeight: PropTypes.bool.isRequired +}; + +Menu.defaultProps = { + className: styles.menu, + alignMenu: align.LEFT, + enforceMaxHeight: true +}; + +export default Menu; diff --git a/frontend/src/Components/Menu/MenuButton.css b/frontend/src/Components/Menu/MenuButton.css new file mode 100644 index 000000000..04a7439cd --- /dev/null +++ b/frontend/src/Components/Menu/MenuButton.css @@ -0,0 +1,15 @@ +.menuButton { + display: flex; + align-items: center; + justify-content: center; + height: 100%; + + &::after { + margin-left: 5px; + content: '\25BE'; + } + + &:hover { + color: $toobarButtonHoverColor; + } +} diff --git a/frontend/src/Components/Menu/MenuButton.js b/frontend/src/Components/Menu/MenuButton.js new file mode 100644 index 000000000..d89a52d1d --- /dev/null +++ b/frontend/src/Components/Menu/MenuButton.js @@ -0,0 +1,41 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import Link from 'Components/Link/Link'; +import styles from './MenuButton.css'; + +class MenuButton extends Component { + + // + // Render + + render() { + const { + className, + children, + onPress, + ...otherProps + } = this.props; + + return ( + + {children} + + ); + } +} + +MenuButton.propTypes = { + className: PropTypes.string, + children: PropTypes.node.isRequired, + onPress: PropTypes.func +}; + +MenuButton.defaultProps = { + className: styles.menuButton +}; + +export default MenuButton; diff --git a/frontend/src/Components/Menu/MenuContent.css b/frontend/src/Components/Menu/MenuContent.css new file mode 100644 index 000000000..0acc07390 --- /dev/null +++ b/frontend/src/Components/Menu/MenuContent.css @@ -0,0 +1,11 @@ +.menuContent { + display: flex; + flex-direction: column; + background-color: $toolbarMenuItemBackgroundColor; + line-height: 20px; +} + +.scroller { + display: flex; + flex-direction: column; +} diff --git a/frontend/src/Components/Menu/MenuContent.js b/frontend/src/Components/Menu/MenuContent.js new file mode 100644 index 000000000..1acacf80f --- /dev/null +++ b/frontend/src/Components/Menu/MenuContent.js @@ -0,0 +1,43 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import Scroller from 'Components/Scroller/Scroller'; +import styles from './MenuContent.css'; + +class MenuContent extends Component { + + // + // Render + + render() { + const { + className, + children, + maxHeight + } = this.props; + + return ( +
+ + {children} + +
+ ); + } +} + +MenuContent.propTypes = { + className: PropTypes.string, + children: PropTypes.node.isRequired, + maxHeight: PropTypes.number +}; + +MenuContent.defaultProps = { + className: styles.menuContent +}; + +export default MenuContent; diff --git a/frontend/src/Components/Menu/MenuItem.css b/frontend/src/Components/Menu/MenuItem.css new file mode 100644 index 000000000..bae1a649c --- /dev/null +++ b/frontend/src/Components/Menu/MenuItem.css @@ -0,0 +1,19 @@ +.menuItem { + @add-mixin truncate; + + display: block; + flex-shrink: 0; + padding: 10px 20px; + min-width: 150px; + max-width: 250px; + background-color: $toolbarMenuItemBackgroundColor; + color: $menuItemColor; + line-height: 20px; + + &:hover, + &:focus { + background-color: $toolbarMenuItemHoverBackgroundColor; + color: $menuItemHoverColor; + text-decoration: none; + } +} diff --git a/frontend/src/Components/Menu/MenuItem.js b/frontend/src/Components/Menu/MenuItem.js new file mode 100644 index 000000000..ff083450b --- /dev/null +++ b/frontend/src/Components/Menu/MenuItem.js @@ -0,0 +1,38 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import Link from 'Components/Link/Link'; +import styles from './MenuItem.css'; + +class MenuItem extends Component { + + // + // Render + + render() { + const { + className, + children, + ...otherProps + } = this.props; + + return ( + + {children} + + ); + } +} + +MenuItem.propTypes = { + className: PropTypes.string, + children: PropTypes.node.isRequired +}; + +MenuItem.defaultProps = { + className: styles.menuItem +}; + +export default MenuItem; diff --git a/frontend/src/Components/Menu/SelectedMenuItem.css b/frontend/src/Components/Menu/SelectedMenuItem.css new file mode 100644 index 000000000..739419d69 --- /dev/null +++ b/frontend/src/Components/Menu/SelectedMenuItem.css @@ -0,0 +1,15 @@ +.item { + display: flex; + justify-content: space-between; + white-space: nowrap; +} + +.isSelected { + visibility: visible; + margin-left: 20px; +} + +.isNotSelected { + visibility: hidden; + margin-left: 20px; +} diff --git a/frontend/src/Components/Menu/SelectedMenuItem.js b/frontend/src/Components/Menu/SelectedMenuItem.js new file mode 100644 index 000000000..bc8f41f5a --- /dev/null +++ b/frontend/src/Components/Menu/SelectedMenuItem.js @@ -0,0 +1,63 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { icons } from 'Helpers/Props'; +import Icon from 'Components/Icon'; +import MenuItem from './MenuItem'; +import styles from './SelectedMenuItem.css'; + +class SelectedMenuItem extends Component { + + // + // Listeners + + onPress = () => { + const { + name, + onPress + } = this.props; + + onPress(name); + } + + // + // Render + + render() { + const { + children, + selectedIconName, + isSelected, + ...otherProps + } = this.props; + + return ( + +
+ {children} + + +
+
+ ); + } +} + +SelectedMenuItem.propTypes = { + name: PropTypes.string, + children: PropTypes.node.isRequired, + selectedIconName: PropTypes.string.isRequired, + isSelected: PropTypes.bool.isRequired, + onPress: PropTypes.func.isRequired +}; + +SelectedMenuItem.defaultProps = { + selectedIconName: icons.CHECK +}; + +export default SelectedMenuItem; diff --git a/frontend/src/Components/Menu/SortMenu.js b/frontend/src/Components/Menu/SortMenu.js new file mode 100644 index 000000000..7a2e931ea --- /dev/null +++ b/frontend/src/Components/Menu/SortMenu.js @@ -0,0 +1,33 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import { icons } from 'Helpers/Props'; +import Menu from 'Components/Menu/Menu'; +import ToolbarMenuButton from 'Components/Menu/ToolbarMenuButton'; + +function SortMenu(props) { + const { + className, + children, + ...otherProps + } = props; + + return ( + + + {children} + + ); +} + +SortMenu.propTypes = { + className: PropTypes.string, + children: PropTypes.node.isRequired +}; + +export default SortMenu; diff --git a/frontend/src/Components/Menu/SortMenuItem.js b/frontend/src/Components/Menu/SortMenuItem.js new file mode 100644 index 000000000..e35864ae6 --- /dev/null +++ b/frontend/src/Components/Menu/SortMenuItem.js @@ -0,0 +1,37 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import { icons, sortDirections } from 'Helpers/Props'; +import SelectedMenuItem from './SelectedMenuItem'; + +function SortMenuItem(props) { + const { + name, + sortKey, + sortDirection, + ...otherProps + } = props; + + const isSelected = name === sortKey; + + return ( + + ); +} + +SortMenuItem.propTypes = { + name: PropTypes.string, + sortKey: PropTypes.string, + sortDirection: PropTypes.oneOf(sortDirections.all), + onPress: PropTypes.func.isRequired +}; + +SortMenuItem.defaultProps = { + name: null +}; + +export default SortMenuItem; diff --git a/frontend/src/Components/Menu/ToolbarMenuButton.css b/frontend/src/Components/Menu/ToolbarMenuButton.css new file mode 100644 index 000000000..ef08e2843 --- /dev/null +++ b/frontend/src/Components/Menu/ToolbarMenuButton.css @@ -0,0 +1,13 @@ +.menuButton { + composes: menuButton from './MenuButton.css'; + + width: $toolbarButtonWidth; + height: $toolbarHeight; + text-align: center; +} + +.label { + height: 14px; + color: $toolbarLabelColor; + font-size: $extraSmallFontSize; +} diff --git a/frontend/src/Components/Menu/ToolbarMenuButton.js b/frontend/src/Components/Menu/ToolbarMenuButton.js new file mode 100644 index 000000000..33f7802de --- /dev/null +++ b/frontend/src/Components/Menu/ToolbarMenuButton.js @@ -0,0 +1,38 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import Icon from 'Components/Icon'; +import MenuButton from 'Components/Menu/MenuButton'; +import styles from './ToolbarMenuButton.css'; + +function ToolbarMenuButton(props) { + const { + iconName, + text, + ...otherProps + } = props; + + return ( + +
+ + +
+ {text} +
+
+
+ ); +} + +ToolbarMenuButton.propTypes = { + iconName: PropTypes.string.isRequired, + text: PropTypes.string +}; + +export default ToolbarMenuButton; diff --git a/frontend/src/Components/Menu/ViewMenu.js b/frontend/src/Components/Menu/ViewMenu.js new file mode 100644 index 000000000..9c39bb3e2 --- /dev/null +++ b/frontend/src/Components/Menu/ViewMenu.js @@ -0,0 +1,30 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import { icons } from 'Helpers/Props'; +import Menu from 'Components/Menu/Menu'; +import ToolbarMenuButton from 'Components/Menu/ToolbarMenuButton'; + +function ViewMenu(props) { + const { + children, + ...otherProps + } = props; + + return ( + + + {children} + + ); +} + +ViewMenu.propTypes = { + children: PropTypes.node.isRequired +}; + +export default ViewMenu; diff --git a/frontend/src/Components/Menu/ViewMenuItem.js b/frontend/src/Components/Menu/ViewMenuItem.js new file mode 100644 index 000000000..d355d6e94 --- /dev/null +++ b/frontend/src/Components/Menu/ViewMenuItem.js @@ -0,0 +1,28 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import SelectedMenuItem from './SelectedMenuItem'; + +function ViewMenuItem(props) { + const { + name, + selectedView, + ...otherProps + } = props; + + const isSelected = name === selectedView; + + return ( + + ); +} + +ViewMenuItem.propTypes = { + name: PropTypes.string, + selectedView: PropTypes.string.isRequired +}; + +export default ViewMenuItem; diff --git a/frontend/src/Components/Modal/ConfirmModal.js b/frontend/src/Components/Modal/ConfirmModal.js new file mode 100644 index 000000000..5bb783d43 --- /dev/null +++ b/frontend/src/Components/Modal/ConfirmModal.js @@ -0,0 +1,88 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import { kinds, sizes } from 'Helpers/Props'; +import Button from 'Components/Link/Button'; +import SpinnerButton from 'Components/Link/SpinnerButton'; +import Modal from 'Components/Modal/Modal'; +import ModalContent from 'Components/Modal/ModalContent'; +import ModalHeader from 'Components/Modal/ModalHeader'; +import ModalBody from 'Components/Modal/ModalBody'; +import ModalFooter from 'Components/Modal/ModalFooter'; + +function ConfirmModal(props) { + const { + isOpen, + kind, + size, + title, + message, + confirmLabel, + cancelLabel, + hideCancelButton, + isSpinning, + onConfirm, + onCancel + } = props; + + return ( + + + {title} + + + {message} + + + + { + !hideCancelButton && + + } + + + {confirmLabel} + + + + + ); +} + +ConfirmModal.propTypes = { + className: PropTypes.string, + isOpen: PropTypes.bool.isRequired, + kind: PropTypes.oneOf(kinds.all), + size: PropTypes.oneOf(sizes.all), + title: PropTypes.string.isRequired, + message: PropTypes.oneOfType([PropTypes.string, PropTypes.node]).isRequired, + confirmLabel: PropTypes.string, + cancelLabel: PropTypes.string, + hideCancelButton: PropTypes.bool, + isSpinning: PropTypes.bool.isRequired, + onConfirm: PropTypes.func.isRequired, + onCancel: PropTypes.func.isRequired +}; + +ConfirmModal.defaultProps = { + kind: kinds.PRIMARY, + size: sizes.MEDIUM, + confirmLabel: 'OK', + cancelLabel: 'Cancel', + isSpinning: false +}; + +export default ConfirmModal; diff --git a/frontend/src/Components/Modal/Modal.css b/frontend/src/Components/Modal/Modal.css new file mode 100644 index 000000000..4872a011e --- /dev/null +++ b/frontend/src/Components/Modal/Modal.css @@ -0,0 +1,79 @@ +.modalContainer { + position: absolute; + top: 0; + z-index: 1000; + width: 100%; + height: 100%; +} + +.modalBackdrop { + display: flex; + align-items: center; + justify-content: center; + width: 100%; + height: 100%; + background-color: $modalBackdropBackgroundColor; + opacity: 1; +} + +.modal { + position: relative; + display: flex; + max-height: 90%; + border-radius: 6px; + opacity: 1; +} + +.modalOpen { + /* Prevent the body from scrolling when the modal is open */ + overflow: hidden !important; +} + +/* + * Sizes + */ + +.small { + composes: modal; + + width: 480px; +} + +.medium { + composes: modal; + + width: 720px; +} + +.large { + composes: modal; + + width: 1080px; +} + +@media only screen and (max-width: $breakpointLarge) { + .modal.large { + width: 90%; + } +} + +@media only screen and (max-width: $breakpointMedium) { + .modal.small, + .modal.medium { + width: 90%; + } +} + +@media only screen and (max-width: $breakpointSmall) { + .modalContainer { + position: fixed; + } + + .modal.small, + .modal.medium, + .modal.large { + max-height: 100%; + width: 100%; + height: 100%; + } +} diff --git a/frontend/src/Components/Modal/Modal.js b/frontend/src/Components/Modal/Modal.js new file mode 100644 index 000000000..1696da829 --- /dev/null +++ b/frontend/src/Components/Modal/Modal.js @@ -0,0 +1,196 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import ReactDOM from 'react-dom'; +import Portal from 'react-portal'; +import classNames from 'classnames'; +import elementClass from 'element-class'; +import getUniqueElememtId from 'Utilities/getUniqueElementId'; +import * as keyCodes from 'Utilities/Constants/keyCodes'; +import { sizes } from 'Helpers/Props'; +import styles from './Modal.css'; + +const openModals = []; + +function removeFromOpenModals(id) { + const index = openModals.indexOf(id); + + if (index >= 0) { + openModals.splice(index, 1); + } +} + +class Modal extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this._modalId = getUniqueElememtId(); + } + + componentDidMount() { + if (this.props.isOpen) { + this._openModal(); + } + } + + componentDidUpdate(prevProps) { + const { + isOpen + } = this.props; + + if (!prevProps.isOpen && isOpen) { + this._openModal(); + } else if (prevProps.isOpen && !isOpen) { + this._closeModal(); + } + } + + componentWillUnmount() { + if (this.props.isOpen) { + this._closeModal(); + } + } + + // + // Control + + _openModal() { + openModals.push(this._modalId); + window.addEventListener('keydown', this.onKeyDown); + + if (openModals.length === 1) { + elementClass(document.body).add(styles.modalOpen); + } + } + + _closeModal() { + removeFromOpenModals(this._modalId); + window.removeEventListener('keydown', this.onKeyDown); + + if (openModals.length === 0) { + elementClass(document.body).remove(styles.modalOpen); + } + } + + _isBackdropTarget(event) { + const targetElement = this._findEventTarget(event); + + if (targetElement) { + const modalElement = ReactDOM.findDOMNode(this.refs.modal); + + return !modalElement || !modalElement.contains(targetElement); + } + + return false; + } + + _findEventTarget(event) { + const changedTouches = event.changedTouches; + + if (!changedTouches) { + return event.target; + } + + if (changedTouches.length === 1) { + const touch = changedTouches[0]; + + return document.elementFromPoint(touch.clientX, touch.clientY); + } + } + + // + // Listeners + + onBackdropBeginPress = (event) => { + this._isBackdropPressed = this._isBackdropTarget(event); + } + + onBackdropEndPress = (event) => { + if (this._isBackdropPressed && this._isBackdropTarget(event)) { + this.props.onModalClose(); + } + + this._isBackdropPressed = false; + } + + onKeyDown = (event) => { + const keyCode = event.keyCode; + + if (keyCode === keyCodes.ESCAPE) { + if (openModals.indexOf(this._modalId) === openModals.length - 1) { + event.preventDefault(); + event.stopPropagation(); + + this.props.onModalClose(); + } + } + } + + onClosePress = (event) => { + this.props.onModalClose(); + } + + // + // Render + + render() { + const { + className, + backdropClassName, + size, + children, + isOpen + } = this.props; + + return ( + +
+ { + isOpen && +
+
+
+ {children} +
+
+
+ } +
+
+ ); + } +} + +Modal.propTypes = { + className: PropTypes.string, + backdropClassName: PropTypes.string, + size: PropTypes.oneOf(sizes.all), + children: PropTypes.node, + isOpen: PropTypes.bool.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +Modal.defaultProps = { + className: styles.modal, + backdropClassName: styles.modalBackdrop, + size: sizes.LARGE +}; + +export default Modal; diff --git a/frontend/src/Components/Modal/ModalBody.css b/frontend/src/Components/Modal/ModalBody.css new file mode 100644 index 000000000..2e55a91cb --- /dev/null +++ b/frontend/src/Components/Modal/ModalBody.css @@ -0,0 +1,14 @@ +$modalBodyPadding: 30px; + +.modalBody { + flex: 1 0 1px; + padding: $modalBodyPadding; +} + +.modalScroller { + flex-grow: 1; +} + +.innerModalBody { + padding: $modalBodyPadding; +} diff --git a/frontend/src/Components/Modal/ModalBody.js b/frontend/src/Components/Modal/ModalBody.js new file mode 100644 index 000000000..a35f2ecf5 --- /dev/null +++ b/frontend/src/Components/Modal/ModalBody.js @@ -0,0 +1,59 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { scrollDirections } from 'Helpers/Props'; +import Scroller from 'Components/Scroller/Scroller'; +import styles from './ModalBody.css'; + +class ModalBody extends Component { + + // + // Render + + render() { + const { + innerClassName, + scrollDirection, + children, + ...otherProps + } = this.props; + + let className = this.props.className; + const hasScroller = scrollDirection !== scrollDirections.NONE; + + if (!className) { + className = hasScroller ? styles.modalScroller : styles.modalBody; + } + + return ( + + { + hasScroller ? +
+ {children} +
: + children + } +
+ ); + } + +} + +ModalBody.propTypes = { + className: PropTypes.string, + innerClassName: PropTypes.string, + children: PropTypes.node, + scrollDirection: PropTypes.oneOf([scrollDirections.NONE, scrollDirections.HORIZONTAL, scrollDirections.VERTICAL]) +}; + +ModalBody.defaultProps = { + innerClassName: styles.innerModalBody, + scrollDirection: scrollDirections.VERTICAL +}; + +export default ModalBody; diff --git a/frontend/src/Components/Modal/ModalContent.css b/frontend/src/Components/Modal/ModalContent.css new file mode 100644 index 000000000..afd798dfa --- /dev/null +++ b/frontend/src/Components/Modal/ModalContent.css @@ -0,0 +1,23 @@ +.modalContent { + position: relative; + display: flex; + flex-direction: column; + flex-grow: 1; + width: 100%; + background-color: $modalBackgroundColor; +} + +.closeButton { + position: absolute; + top: 0; + right: 0; + z-index: 1; + width: 60px; + height: 60px; + text-align: center; + line-height: 60px; + + &:hover { + color: $modalCloseButtonHoverColor; + } +} diff --git a/frontend/src/Components/Modal/ModalContent.js b/frontend/src/Components/Modal/ModalContent.js new file mode 100644 index 000000000..cc165dda2 --- /dev/null +++ b/frontend/src/Components/Modal/ModalContent.js @@ -0,0 +1,46 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import { icons } from 'Helpers/Props'; +import Link from 'Components/Link/Link'; +import Icon from 'Components/Icon'; +import styles from './ModalContent.css'; + +function ModalContent(props) { + const { + className, + children, + onModalClose, + ...otherProps + } = props; + + return ( +
+ + + + + {children} +
+ ); +} + +ModalContent.propTypes = { + className: PropTypes.string, + children: PropTypes.node, + onModalClose: PropTypes.func.isRequired +}; + +ModalContent.defaultProps = { + className: styles.modalContent +}; + +export default ModalContent; diff --git a/frontend/src/Components/Modal/ModalFooter.css b/frontend/src/Components/Modal/ModalFooter.css new file mode 100644 index 000000000..3b817d2bf --- /dev/null +++ b/frontend/src/Components/Modal/ModalFooter.css @@ -0,0 +1,23 @@ +.modalFooter { + display: flex; + align-items: center; + justify-content: flex-end; + flex-shrink: 0; + padding: 15px 30px; + border-top: 1px solid $borderColor; + + a, + button { + margin-left: 10px; + + &:first-child { + margin-left: 0; + } + } +} + +@media only screen and (max-width: $breakpointSmall) { + .modalFooter { + padding: 15px; + } +} diff --git a/frontend/src/Components/Modal/ModalFooter.js b/frontend/src/Components/Modal/ModalFooter.js new file mode 100644 index 000000000..0cf8811d3 --- /dev/null +++ b/frontend/src/Components/Modal/ModalFooter.js @@ -0,0 +1,32 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import styles from './ModalFooter.css'; + +class ModalFooter extends Component { + + // + // Render + + render() { + const { + children, + ...otherProps + } = this.props; + + return ( +
+ {children} +
+ ); + } + +} + +ModalFooter.propTypes = { + children: PropTypes.node +}; + +export default ModalFooter; diff --git a/frontend/src/Components/Modal/ModalHeader.css b/frontend/src/Components/Modal/ModalHeader.css new file mode 100644 index 000000000..eab77a9f8 --- /dev/null +++ b/frontend/src/Components/Modal/ModalHeader.css @@ -0,0 +1,8 @@ +.modalHeader { + @add-mixin truncate; + + flex-shrink: 0; + padding: 15px 50px 15px 30px; + border-bottom: 1px solid $borderColor; + font-size: 18px; +} diff --git a/frontend/src/Components/Modal/ModalHeader.js b/frontend/src/Components/Modal/ModalHeader.js new file mode 100644 index 000000000..52879b57d --- /dev/null +++ b/frontend/src/Components/Modal/ModalHeader.js @@ -0,0 +1,32 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import styles from './ModalHeader.css'; + +class ModalHeader extends Component { + + // + // Render + + render() { + const { + children, + ...otherProps + } = this.props; + + return ( +
+ {children} +
+ ); + } + +} + +ModalHeader.propTypes = { + children: PropTypes.node +}; + +export default ModalHeader; diff --git a/frontend/src/Components/MonitorToggleButton.css b/frontend/src/Components/MonitorToggleButton.css new file mode 100644 index 000000000..e2c68bed1 --- /dev/null +++ b/frontend/src/Components/MonitorToggleButton.css @@ -0,0 +1,13 @@ +.toggleButton { + composes: button from 'Components/Link/IconButton.css'; + + padding: 0; + font-size: inherit; +} + +.disabledButton { + composes: button from 'Components/Link/IconButton.css'; + + color: $disabledColor; + cursor: not-allowed; +} diff --git a/frontend/src/Components/MonitorToggleButton.js b/frontend/src/Components/MonitorToggleButton.js new file mode 100644 index 000000000..8802cb1a2 --- /dev/null +++ b/frontend/src/Components/MonitorToggleButton.js @@ -0,0 +1,76 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { icons } from 'Helpers/Props'; +import Icon from 'Components/Icon'; +import SpinnerIconButton from 'Components/Link/SpinnerIconButton'; +import styles from './MonitorToggleButton.css'; + +class MonitorToggleButton extends Component { + + // + // Listeners + + onPress = (event) => { + const shiftKey = event.nativeEvent.shiftKey; + + this.props.onPress(!this.props.monitored, { shiftKey }); + } + + // + // Render + + render() { + const { + className, + monitored, + isDisabled, + isSaving, + size, + ...otherProps + } = this.props; + + const monitoredMessage = 'Monitored, click to unmonitor'; + const unmonitoredMessage = 'Unmonitored, click to monitor'; + const iconName = monitored ? icons.MONITORED : icons.UNMONITORED; + + if (isDisabled) { + return ( + + ); + } + + return ( + + ); + } +} + +MonitorToggleButton.propTypes = { + className: PropTypes.string.isRequired, + monitored: PropTypes.bool.isRequired, + size: PropTypes.number, + isDisabled: PropTypes.bool.isRequired, + isSaving: PropTypes.bool.isRequired, + onPress: PropTypes.func.isRequired +}; + +MonitorToggleButton.defaultProps = { + className: styles.toggleButton, + isDisabled: false, + isSaving: false +}; + +export default MonitorToggleButton; diff --git a/frontend/src/Components/NotFound.css b/frontend/src/Components/NotFound.css new file mode 100644 index 000000000..9aaf1114f --- /dev/null +++ b/frontend/src/Components/NotFound.css @@ -0,0 +1,14 @@ +.container { + text-align: center; +} + +.message { + margin: 50px 0; + text-align: center; + font-weight: 300; + font-size: 36px; +} + +.image { + height: 350px; +} diff --git a/frontend/src/Components/NotFound.js b/frontend/src/Components/NotFound.js new file mode 100644 index 000000000..7043da46f --- /dev/null +++ b/frontend/src/Components/NotFound.js @@ -0,0 +1,31 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import PageContent from 'Components/Page/PageContent'; +import styles from './NotFound.css'; + +function NotFound({ message }) { + return ( + +
+
+ {message} +
+ + +
+
+ ); +} + +NotFound.propTypes = { + message: PropTypes.string.isRequired +}; + +NotFound.defaultProps = { + message: 'You must be lost, nothing to see here.' +}; + +export default NotFound; diff --git a/frontend/src/Components/Page/ErrorPage.css b/frontend/src/Components/Page/ErrorPage.css new file mode 100644 index 000000000..e62a82a6b --- /dev/null +++ b/frontend/src/Components/Page/ErrorPage.css @@ -0,0 +1,12 @@ +.page { + composes: page from './Page.css'; + + margin-top: 20px; + text-align: center; + font-size: 20px; +} + +.version { + margin-top: 20px; + font-size: 16px; +} diff --git a/frontend/src/Components/Page/ErrorPage.js b/frontend/src/Components/Page/ErrorPage.js new file mode 100644 index 000000000..325575ff0 --- /dev/null +++ b/frontend/src/Components/Page/ErrorPage.js @@ -0,0 +1,52 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import getErrorMessage from 'Utilities/Object/getErrorMessage'; +import styles from './ErrorPage.css'; + +function ErrorPage(props) { + const { + version, + isLocalStorageSupported, + artistError, + tagsError, + qualityProfilesError, + uiSettingsError + } = props; + + let errorMessage = 'Failed to load Lidarr'; + + if (!isLocalStorageSupported) { + errorMessage = 'Local Storage is not supported or disabled. A plugin or private browsing may have disabled it.'; + } else if (artistError) { + errorMessage = getErrorMessage(artistError, 'Failed to load artist from API'); + } else if (tagsError) { + errorMessage = getErrorMessage(artistError, 'Failed to load artist from API'); + } else if (qualityProfilesError) { + errorMessage = getErrorMessage(qualityProfilesError, 'Failed to load quality profiles from API'); + } else if (uiSettingsError) { + errorMessage = getErrorMessage(uiSettingsError, 'Failed to load UI settings from API'); + } + + return ( +
+
+ {errorMessage} +
+ +
+ Version {version} +
+
+ ); +} + +ErrorPage.propTypes = { + version: PropTypes.string.isRequired, + isLocalStorageSupported: PropTypes.bool.isRequired, + artistError: PropTypes.object, + tagsError: PropTypes.object, + qualityProfilesError: PropTypes.object, + uiSettingsError: PropTypes.object +}; + +export default ErrorPage; diff --git a/frontend/src/Components/Page/Header/ArtistSearchInput.css b/frontend/src/Components/Page/Header/ArtistSearchInput.css new file mode 100644 index 000000000..1cc1c3b2b --- /dev/null +++ b/frontend/src/Components/Page/Header/ArtistSearchInput.css @@ -0,0 +1,98 @@ +.wrapper { + display: flex; +} + +.icon { + line-height: 24px !important; +} + +.input { + margin-left: 8px; + width: 200px; + border: none; + border-bottom: solid 1px $white; + background-color: transparent; + box-shadow: none; + color: $white; + transition: border 0.3s ease-out; + + &::placeholder { + color: $white; + transition: color 0.3s ease-out; + } + + &:focus { + outline: 0; + border-bottom-color: transparent; + + &::placeholder { + color: transparent; + } + } +} + +.container { + position: relative; + flex-grow: 1; +} + +.artistContainer { + @add-mixin scrollbar; + @add-mixin scrollbarTrack; + @add-mixin scrollbarThumb; +} + +.containerOpen { + .artistContainer { + position: absolute; + top: 42px; + z-index: 1; + overflow-y: auto; + min-width: 100%; + max-height: 230px; + border: 1px solid $themeDarkColor; + border-radius: 4px; + border-top-left-radius: 0; + border-top-right-radius: 0; + background-color: $themeDarkColor; + box-shadow: inset 0 1px 1px $inputBoxShadowColor; + color: $menuItemColor; + } +} + +.list { + margin: 5px 0; + padding-left: 0; + list-style-type: none; +} + +.listItem { + padding: 0 16px; + white-space: nowrap; +} + +.highlighted { + background-color: $themeLightColor; +} + +.sectionTitle { + padding: 5px 8px; + color: $disabledColor; +} + +.addNewArtistSuggestion { + padding: 0 3px; + cursor: pointer; +} + +@media only screen and (max-width: $breakpointSmall) { + .input { + min-width: 150px; + max-width: 200px; + } + + .container { + min-width: 0; + max-width: 200px; + } +} diff --git a/frontend/src/Components/Page/Header/ArtistSearchInput.js b/frontend/src/Components/Page/Header/ArtistSearchInput.js new file mode 100644 index 000000000..c0fe5ab20 --- /dev/null +++ b/frontend/src/Components/Page/Header/ArtistSearchInput.js @@ -0,0 +1,250 @@ +import _ from 'lodash'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import Autosuggest from 'react-autosuggest'; +import jdu from 'jdu'; +import { icons } from 'Helpers/Props'; +import Icon from 'Components/Icon'; +import keyboardShortcuts, { shortcuts } from 'Components/keyboardShortcuts'; +import ArtistSearchResult from './ArtistSearchResult'; +import styles from './ArtistSearchInput.css'; + +const ADD_NEW_TYPE = 'addNew'; + +class ArtistSearchInput extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this._autosuggest = null; + + this.state = { + value: '', + suggestions: [] + }; + } + + componentDidMount() { + this.props.bindShortcut(shortcuts.ARTIST_SEARCH_INPUT.key, this.focusInput); + } + + // + // Control + + setAutosuggestRef = (ref) => { + this._autosuggest = ref; + } + + focusInput = (event) => { + event.preventDefault(); + this._autosuggest.input.focus(); + } + + getSectionSuggestions(section) { + return section.suggestions; + } + + renderSectionTitle(section) { + return ( +
+ {section.title} +
+ ); + } + + getSuggestionValue({ title }) { + return title; + } + + renderSuggestion(item, { query }) { + if (item.type === ADD_NEW_TYPE) { + return ( +
+ Search for {query} +
+ ); + } + + return ( + + ); + } + + goToArtist(artist) { + this.setState({ value: '' }); + this.props.onGoToArtist(artist.nameSlug); + } + + reset() { + this.setState({ + value: '', + suggestions: [] + }); + } + + // + // Listeners + + onChange = (event, { newValue }) => { + this.setState({ value: newValue }); + } + + onKeyDown = (event) => { + if (event.key !== 'Tab' && event.key !== 'Enter') { + return; + } + + const { + suggestions, + value + } = this.state; + + const { + highlightedSectionIndex, + highlightedSuggestionIndex + } = this._autosuggest.state; + + if (!suggestions.length || highlightedSectionIndex) { + this.props.onGoToAddNewArtist(value); + this._autosuggest.input.blur(); + + return; + } + + // If an suggestion is not selected go to the first artist, + // otherwise go to the selected artist. + + if (highlightedSuggestionIndex == null) { + this.goToArtist(suggestions[0]); + } else { + this.goToArtist(suggestions[highlightedSuggestionIndex]); + } + } + + onBlur = () => { + this.reset(); + } + + onSuggestionsFetchRequested = ({ value }) => { + const lowerCaseValue = jdu.replace(value).toLowerCase(); + + const suggestions = _.filter(this.props.artist, (artist) => { + // Check the title first and if there isn't a match fallback to the alternate titles + + const titleMatch = jdu.replace(artist.artistName).toLowerCase().contains(lowerCaseValue); + + return titleMatch || _.some(artist.alternateTitles, (alternateTitle) => { + return jdu.replace(alternateTitle.title).toLowerCase().contains(lowerCaseValue); + }); + }); + + this.setState({ suggestions }); + } + + onSuggestionsClearRequested = () => { + this.reset(); + } + + onSuggestionSelected = (event, { suggestion, sectionIndex }) => { + if (suggestion.type === ADD_NEW_TYPE) { + this.props.onGoToAddNewArtist(this.state.value); + } else { + this.goToArtist(suggestion); + } + } + + // + // Render + + render() { + const { + value, + suggestions + } = this.state; + + const suggestionGroups = []; + + if (suggestions.length) { + suggestionGroups.push({ + title: 'Existing Artist', + suggestions + }); + } + + if (suggestions.length <= 3) { + suggestionGroups.push({ + title: 'Add New Artist', + suggestions: [ + { + type: ADD_NEW_TYPE, + title: value + } + ] + }); + } + + const inputProps = { + ref: this.setInputRef, + className: styles.input, + name: 'artistSearch', + value, + placeholder: 'Search', + autoComplete: 'off', + spellCheck: false, + onChange: this.onChange, + onKeyDown: this.onKeyDown, + onBlur: this.onBlur, + onFocus: this.onFocus + }; + + const theme = { + container: styles.container, + containerOpen: styles.containerOpen, + suggestionsContainer: styles.artistContainer, + suggestionsList: styles.list, + suggestion: styles.listItem, + suggestionHighlighted: styles.highlighted + }; + + return ( +
+ + + +
+ ); + } +} + +ArtistSearchInput.propTypes = { + artist: PropTypes.arrayOf(PropTypes.object).isRequired, + onGoToArtist: PropTypes.func.isRequired, + onGoToAddNewArtist: PropTypes.func.isRequired, + bindShortcut: PropTypes.func.isRequired +}; + +export default keyboardShortcuts(ArtistSearchInput); diff --git a/frontend/src/Components/Page/Header/ArtistSearchInputConnector.js b/frontend/src/Components/Page/Header/ArtistSearchInputConnector.js new file mode 100644 index 000000000..3bf1a1678 --- /dev/null +++ b/frontend/src/Components/Page/Header/ArtistSearchInputConnector.js @@ -0,0 +1,31 @@ +import _ from 'lodash'; +import { connect } from 'react-redux'; +import { push } from 'react-router-redux'; +import { createSelector } from 'reselect'; +import createAllArtistSelector from 'Store/Selectors/createAllArtistSelector'; +import ArtistSearchInput from './ArtistSearchInput'; + +function createMapStateToProps() { + return createSelector( + createAllArtistSelector(), + (artist) => { + return { + artist: _.sortBy(artist, 'sortName') + }; + } + ); +} + +function createMapDispatchToProps(dispatch, props) { + return { + onGoToArtist(nameSlug) { + dispatch(push(`${window.Sonarr.urlBase}/artist/${nameSlug}`)); + }, + + onGoToAddNewArtist(query) { + dispatch(push(`${window.Sonarr.urlBase}/add/new?term=${encodeURIComponent(query)}`)); + } + }; +} + +export default connect(createMapStateToProps, createMapDispatchToProps)(ArtistSearchInput); diff --git a/frontend/src/Components/Page/Header/ArtistSearchResult.css b/frontend/src/Components/Page/Header/ArtistSearchResult.css new file mode 100644 index 000000000..87988a35b --- /dev/null +++ b/frontend/src/Components/Page/Header/ArtistSearchResult.css @@ -0,0 +1,34 @@ +.result { + display: flex; + padding: 3px; + cursor: pointer; +} + +.poster { + width: 35px; + height: 50px; +} + +.titles { + flex: 1 1 1px; +} + +.title { + flex: 1 1 1px; + margin-left: 5px; +} + +.alternateTitle { + flex: 1 1 1px; + margin-left: 5px; + color: $disabledColor; + font-size: $smallFontSize; +} + +@media only screen and (max-width: $breakpointSmall) { + .titles, + .title, + .alternateTitle { + @add-mixin truncate; + } +} diff --git a/frontend/src/Components/Page/Header/ArtistSearchResult.js b/frontend/src/Components/Page/Header/ArtistSearchResult.js new file mode 100644 index 000000000..95186d039 --- /dev/null +++ b/frontend/src/Components/Page/Header/ArtistSearchResult.js @@ -0,0 +1,59 @@ +import _ from 'lodash'; +import PropTypes from 'prop-types'; +import React from 'react'; +import ArtistPoster from 'Artist/ArtistPoster'; +import styles from './ArtistSearchResult.css'; + +function getMatchingAlternateTile(alternateTitles, query) { + return _.first(alternateTitles, (alternateTitle) => { + return alternateTitle.title.toLowerCase().contains(query.toLowerCase()); + }); +} + +function ArtistSearchResult(props) { + const { + query, + artistName, + // alternateTitles, + images + } = props; + + const index = artistName.toLowerCase().indexOf(query.toLowerCase()); + // const alternateTitle = index === -1 ? + // getMatchingAlternateTile(alternateTitles, query) : + // null; + + return ( +
+ + +
+
+ {artistName} +
+ + { + // !!alternateTitle && + //
+ // {alternateTitle.title} + //
+ } +
+
+ ); +} + +ArtistSearchResult.propTypes = { + query: PropTypes.string.isRequired, + artistName: PropTypes.string.isRequired, + // alternateTitles: PropTypes.arrayOf(PropTypes.object).isRequired, + images: PropTypes.arrayOf(PropTypes.object).isRequired +}; + +export default ArtistSearchResult; diff --git a/frontend/src/Components/Page/Header/KeyboardShortcutsModal.js b/frontend/src/Components/Page/Header/KeyboardShortcutsModal.js new file mode 100644 index 000000000..a1d106b58 --- /dev/null +++ b/frontend/src/Components/Page/Header/KeyboardShortcutsModal.js @@ -0,0 +1,31 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import { sizes } from 'Helpers/Props'; +import Modal from 'Components/Modal/Modal'; +import KeyboardShortcutsModalContentConnector from './KeyboardShortcutsModalContentConnector'; + +function KeyboardShortcutsModal(props) { + const { + isOpen, + onModalClose + } = props; + + return ( + + + + ); +} + +KeyboardShortcutsModal.propTypes = { + isOpen: PropTypes.bool.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default KeyboardShortcutsModal; diff --git a/frontend/src/Components/Page/Header/KeyboardShortcutsModalContent.css b/frontend/src/Components/Page/Header/KeyboardShortcutsModalContent.css new file mode 100644 index 000000000..4425e0e0d --- /dev/null +++ b/frontend/src/Components/Page/Header/KeyboardShortcutsModalContent.css @@ -0,0 +1,15 @@ +.shortcut { + display: flex; + justify-content: space-between; + padding: 5px 20px; + font-size: 18px; +} + +.key { + padding: 2px 4px; + border-radius: 3px; + background-color: $defaultColor; + box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.25); + color: $white; + font-size: 16px; +} diff --git a/frontend/src/Components/Page/Header/KeyboardShortcutsModalContent.js b/frontend/src/Components/Page/Header/KeyboardShortcutsModalContent.js new file mode 100644 index 000000000..9c07e047c --- /dev/null +++ b/frontend/src/Components/Page/Header/KeyboardShortcutsModalContent.js @@ -0,0 +1,90 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import { shortcuts } from 'Components/keyboardShortcuts'; +import Button from 'Components/Link/Button'; +import ModalContent from 'Components/Modal/ModalContent'; +import ModalHeader from 'Components/Modal/ModalHeader'; +import ModalBody from 'Components/Modal/ModalBody'; +import ModalFooter from 'Components/Modal/ModalFooter'; +import styles from './KeyboardShortcutsModalContent.css'; + +function getShortcuts() { + const allShortcuts = []; + + Object.keys(shortcuts).forEach((key) => { + allShortcuts.push(shortcuts[key]); + }); + + return allShortcuts; +} + +function getShortcutKey(combo, isOsx) { + const comboMatch = combo.match(/(.+?)\+(.)/); + + if (!comboMatch) { + return combo; + } + + const modifier = comboMatch[1]; + const key = comboMatch[2]; + let osModifier = modifier; + + if (modifier === 'mod') { + osModifier = isOsx ? 'cmd' : 'ctrl'; + } + + return `${osModifier} + ${key}`; +} + +function KeyboardShortcutsModalContent(props) { + const { + isOsx, + onModalClose + } = props; + + const allShortcuts = getShortcuts(); + + return ( + + + Keyboard Shortcuts + + + + { + allShortcuts.map((shortcut) => { + return ( +
+
+ {getShortcutKey(shortcut.key, isOsx)} +
+ +
+ {shortcut.name} +
+
+ ); + }) + } +
+ + + + +
+ ); +} + +KeyboardShortcutsModalContent.propTypes = { + isOsx: PropTypes.bool.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default KeyboardShortcutsModalContent; diff --git a/frontend/src/Components/Page/Header/KeyboardShortcutsModalContentConnector.js b/frontend/src/Components/Page/Header/KeyboardShortcutsModalContentConnector.js new file mode 100644 index 000000000..d80877153 --- /dev/null +++ b/frontend/src/Components/Page/Header/KeyboardShortcutsModalContentConnector.js @@ -0,0 +1,17 @@ +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import createSystemStatusSelector from 'Store/Selectors/createSystemStatusSelector'; +import KeyboardShortcutsModalContent from './KeyboardShortcutsModalContent'; + +function createMapStateToProps() { + return createSelector( + createSystemStatusSelector(), + (systemStatus) => { + return { + isOsx: systemStatus.isOsx + }; + } + ); +} + +export default connect(createMapStateToProps)(KeyboardShortcutsModalContent); diff --git a/frontend/src/Components/Page/Header/PageHeader.css b/frontend/src/Components/Page/Header/PageHeader.css new file mode 100644 index 000000000..3bfcbc10b --- /dev/null +++ b/frontend/src/Components/Page/Header/PageHeader.css @@ -0,0 +1,60 @@ +.header { + z-index: 3; + display: flex; + align-items: center; + flex: 0 0 auto; + height: $headerHeight; + background-color: #00a65b; + color: $white; +} + +.logoContainer { + display: flex; + justify-content: center; + flex: 0 0 $sidebarWidth; +} + +.logo { + width: 32px; + height: 32px; +} + +.sidebarToggleContainer { + display: none; + justify-content: center; + flex: 0 0 45px; + margin-right: 14px; +} + +.right { + display: flex; + justify-content: flex-end; + flex-grow: 1; +} + +.donate { + composes: link from 'Components/Link/Link.css'; + + width: 30px; + color: $themeRed; + text-align: center; + line-height: 60px; + + &:hover { + color: #9c1f30; + } +} + +@media only screen and (max-width: $breakpointSmall) { + .logoContainer { + flex: 0 0 60px; + } + + .sidebarToggleContainer { + display: flex; + } + + .donate { + display: none; + } +} diff --git a/frontend/src/Components/Page/Header/PageHeader.js b/frontend/src/Components/Page/Header/PageHeader.js new file mode 100644 index 000000000..2d6b3dbd6 --- /dev/null +++ b/frontend/src/Components/Page/Header/PageHeader.js @@ -0,0 +1,96 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { icons } from 'Helpers/Props'; +import keyboardShortcuts, { shortcuts } from 'Components/keyboardShortcuts'; +import IconButton from 'Components/Link/IconButton'; +import Link from 'Components/Link/Link'; +import ArtistSearchInputConnector from './ArtistSearchInputConnector'; +import PageHeaderActionsMenuConnector from './PageHeaderActionsMenuConnector'; +import KeyboardShortcutsModal from './KeyboardShortcutsModal'; +import styles from './PageHeader.css'; + +class PageHeader extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props); + + this.state = { + isKeyboardShortcutsModalOpen: false + }; + } + + componentDidMount() { + this.props.bindShortcut(shortcuts.OPEN_KEYBOARD_SHORTCUTS_MODAL.key, this.openKeyboardShortcutsModal); + } + + // + // Control + + openKeyboardShortcutsModal = () => { + this.setState({ isKeyboardShortcutsModalOpen: true }); + } + + // + // Listeners + + onKeyboardShortcutsModalClose = () => { + this.setState({ isKeyboardShortcutsModalOpen: false }); + } + + // + // Render + + render() { + const { + onSidebarToggle + } = this.props; + + return ( +
+
+ + + +
+ + +
+ +
+ + + +
+ + +
+ + +
+ ); + } +} + +PageHeader.propTypes = { + onSidebarToggle: PropTypes.func.isRequired, + bindShortcut: PropTypes.func.isRequired +}; + +export default keyboardShortcuts(PageHeader); diff --git a/frontend/src/Components/Page/Header/PageHeaderActionsMenu.css b/frontend/src/Components/Page/Header/PageHeaderActionsMenu.css new file mode 100644 index 000000000..44aa20453 --- /dev/null +++ b/frontend/src/Components/Page/Header/PageHeaderActionsMenu.css @@ -0,0 +1,27 @@ +.menuButton { + margin-right: 15px; + width: 30px; + height: 60px; + text-align: center; + + &:hover { + color: $themeDarkColor; + } +} + +.itemIcon { + margin-right: 8px; +} + +.separator { + overflow: hidden; + height: 1px; + background-color: $themeDarkColor; +} + +@media only screen and (max-width: $breakpointSmall) { + .menuButton { + margin-right: 5px; + } +} + diff --git a/frontend/src/Components/Page/Header/PageHeaderActionsMenu.js b/frontend/src/Components/Page/Header/PageHeaderActionsMenu.js new file mode 100644 index 000000000..a261c1b01 --- /dev/null +++ b/frontend/src/Components/Page/Header/PageHeaderActionsMenu.js @@ -0,0 +1,75 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import { align, icons, kinds } from 'Helpers/Props'; +import Icon from 'Components/Icon'; +import Menu from 'Components/Menu/Menu'; +import MenuButton from 'Components/Menu/MenuButton'; +import MenuContent from 'Components/Menu/MenuContent'; +import MenuItem from 'Components/Menu/MenuItem'; +import styles from './PageHeaderActionsMenu.css'; + +function PageHeaderActionsMenu(props) { + const { + formsAuth, + onRestartPress, + onShutdownPress + } = props; + + return ( +
+ + + + + + + + + Restart + + + + + Shutdown + + + { + formsAuth && +
+ } + + { + formsAuth && + + + Logout + + } + +
+
+ ); +} + +PageHeaderActionsMenu.propTypes = { + formsAuth: PropTypes.bool.isRequired, + onRestartPress: PropTypes.func.isRequired, + onShutdownPress: PropTypes.func.isRequired +}; + +export default PageHeaderActionsMenu; diff --git a/frontend/src/Components/Page/Header/PageHeaderActionsMenuConnector.js b/frontend/src/Components/Page/Header/PageHeaderActionsMenuConnector.js new file mode 100644 index 000000000..66d131521 --- /dev/null +++ b/frontend/src/Components/Page/Header/PageHeaderActionsMenuConnector.js @@ -0,0 +1,56 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import { restart, shutdown } from 'Store/Actions/systemActions'; +import PageHeaderActionsMenu from './PageHeaderActionsMenu'; + +function createMapStateToProps() { + return createSelector( + (state) => state.system.status, + (status) => { + return { + formsAuth: status.item.authentication === 'forms' + }; + } + ); +} + +const mapDispatchToProps = { + restart, + shutdown +}; + +class PageHeaderActionsMenuConnector extends Component { + + // + // Listeners + + onRestartPress = () => { + this.props.restart(); + } + + onShutdownPress = () => { + this.props.shutdown(); + } + + // + // Render + + render() { + return ( + + ); + } +} + +PageHeaderActionsMenuConnector.propTypes = { + restart: PropTypes.func.isRequired, + shutdown: PropTypes.func.isRequired +}; + +export default connect(createMapStateToProps, mapDispatchToProps)(PageHeaderActionsMenuConnector); diff --git a/frontend/src/Components/Page/LoadingPage.css b/frontend/src/Components/Page/LoadingPage.css new file mode 100644 index 000000000..dd5852e61 --- /dev/null +++ b/frontend/src/Components/Page/LoadingPage.css @@ -0,0 +1,3 @@ +.page { + composes: page from './Page.css'; +} diff --git a/frontend/src/Components/Page/LoadingPage.js b/frontend/src/Components/Page/LoadingPage.js new file mode 100644 index 000000000..398b70c4b --- /dev/null +++ b/frontend/src/Components/Page/LoadingPage.js @@ -0,0 +1,15 @@ +import React from 'react'; +import LoadingIndicator from 'Components/Loading/LoadingIndicator'; +import LoadingMessage from 'Components/Loading/LoadingMessage'; +import styles from './LoadingPage.css'; + +function LoadingPage() { + return ( +
+ + +
+ ); +} + +export default LoadingPage; diff --git a/frontend/src/Components/Page/Page.css b/frontend/src/Components/Page/Page.css new file mode 100644 index 000000000..9facbfc22 --- /dev/null +++ b/frontend/src/Components/Page/Page.css @@ -0,0 +1,18 @@ +.page { + display: flex; + flex-direction: column; + height: 100%; +} + +.main { + position: relative; /* need this to position inner content - is this really needed? */ + display: flex; + flex: 1 1 auto; +} + +@media only screen and (max-width: $breakpointSmall) { + .page { + flex-grow: 1; + height: initial; + } +} diff --git a/frontend/src/Components/Page/Page.js b/frontend/src/Components/Page/Page.js new file mode 100644 index 000000000..fc7502fd4 --- /dev/null +++ b/frontend/src/Components/Page/Page.js @@ -0,0 +1,130 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import locationShape from 'Helpers/Props/Shapes/locationShape'; +import SignalRConnector from 'Components/SignalRConnector'; +import AppUpdatedModalConnector from 'App/AppUpdatedModalConnector'; +import ConnectionLostModalConnector from 'App/ConnectionLostModalConnector'; +import PageHeader from './Header/PageHeader'; +import PageSidebar from './Sidebar/PageSidebar'; +import styles from './Page.css'; + +class Page extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + isUpdatedModalOpen: false, + isConnectionLostModalOpen: false + }; + } + + componentDidMount() { + window.addEventListener('resize', this.onResize); + } + + componentDidUpdate(prevProps) { + const { + isDisconnected, + isUpdated + } = this.props; + + if (!prevProps.isUpdated && isUpdated) { + this.setState({ isUpdatedModalOpen: true }); + } + + if (prevProps.isDisconnected !== isDisconnected) { + this.setState({ isConnectionLostModalOpen: isDisconnected }); + } + } + + componentWillUnmount() { + window.removeEventListener('resize', this.onResize); + } + + // + // Listeners + + onResize = () => { + this.props.onResize({ + width: window.innerWidth, + height: window.innerHeight + }); + } + + onUpdatedModalClose = () => { + this.setState({ isUpdatedModalOpen: false }); + } + + onConnectionLostModalClose = () => { + this.setState({ isConnectionLostModalOpen: false }); + } + + // + // Render + + render() { + const { + className, + location, + children, + isSmallScreen, + isSidebarVisible, + onSidebarToggle, + onSidebarVisibleChange + } = this.props; + + return ( +
+ + + + +
+ + + {children} +
+ + + + +
+ ); + } +} + +Page.propTypes = { + className: PropTypes.string, + location: locationShape.isRequired, + children: PropTypes.node.isRequired, + isSmallScreen: PropTypes.bool.isRequired, + isSidebarVisible: PropTypes.bool.isRequired, + isUpdated: PropTypes.bool.isRequired, + isDisconnected: PropTypes.bool.isRequired, + onResize: PropTypes.func.isRequired, + onSidebarToggle: PropTypes.func.isRequired, + onSidebarVisibleChange: PropTypes.func.isRequired +}; + +Page.defaultProps = { + className: styles.page +}; + +export default Page; diff --git a/frontend/src/Components/Page/PageConnector.js b/frontend/src/Components/Page/PageConnector.js new file mode 100644 index 000000000..416b194b9 --- /dev/null +++ b/frontend/src/Components/Page/PageConnector.js @@ -0,0 +1,179 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { withRouter } from 'react-router-dom'; +import { createSelector } from 'reselect'; +import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector'; +import { saveDimensions, setIsSidebarVisible } from 'Store/Actions/appActions'; +import { fetchArtist } from 'Store/Actions/artistActions'; +import { fetchTags } from 'Store/Actions/tagActions'; +import { fetchQualityProfiles, fetchLanguageProfiles, fetchUISettings } from 'Store/Actions/settingsActions'; +import { fetchStatus } from 'Store/Actions/systemActions'; +import ErrorPage from './ErrorPage'; +import LoadingPage from './LoadingPage'; +import Page from './Page'; + +function testLocalStorage() { + const key = 'sonarrTest'; + + try { + localStorage.setItem(key, key); + localStorage.removeItem(key); + + return true; + } catch (e) { + return false; + } +} + +function createMapStateToProps() { + return createSelector( + (state) => state.artist, + (state) => state.tags, + (state) => state.settings, + (state) => state.app, + createDimensionsSelector(), + (artist, tags, settings, app, dimensions) => { + const isPopulated = artist.isPopulated && + tags.isPopulated && + settings.qualityProfiles.isPopulated && + settings.ui.isPopulated; + + const hasError = !!artist.error || + !!tags.error || + !!settings.qualityProfiles.error || + !!settings.ui.error; + + return { + isPopulated, + hasError, + artistError: artist.error, + tagsError: tags.error, + qualityProfilesError: settings.qualityProfiles.error, + uiSettingsError: settings.ui.error, + isSmallScreen: dimensions.isSmallScreen, + isSidebarVisible: app.isSidebarVisible, + version: app.version, + isUpdated: app.isUpdated, + isDisconnected: app.isDisconnected + }; + } + ); +} + +function createMapDispatchToProps(dispatch, props) { + return { + dispatchFetchSeries() { + dispatch(fetchArtist()); + }, + dispatchFetchTags() { + dispatch(fetchTags()); + }, + dispatchFetchQualityProfiles() { + dispatch(fetchQualityProfiles()); + }, + dispatchFetchLanguageProfiles() { + dispatch(fetchLanguageProfiles()); + }, + dispatchFetchUISettings() { + dispatch(fetchUISettings()); + }, + dispatchFetchStatus() { + dispatch(fetchStatus()); + }, + onResize(dimensions) { + dispatch(saveDimensions(dimensions)); + }, + onSidebarVisibleChange(isSidebarVisible) { + dispatch(setIsSidebarVisible({ isSidebarVisible })); + } + }; +} + +class PageConnector extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + isLocalStorageSupported: testLocalStorage() + }; + } + + componentDidMount() { + if (!this.props.isPopulated) { + this.props.dispatchFetchSeries(); + this.props.dispatchFetchTags(); + this.props.dispatchFetchQualityProfiles(); + this.props.dispatchFetchLanguageProfiles(); + this.props.dispatchFetchUISettings(); + this.props.dispatchFetchStatus(); + } + } + + // + // Listeners + + onSidebarToggle = () => { + this.props.onSidebarVisibleChange(!this.props.isSidebarVisible); + } + + // + // Render + + render() { + const { + isPopulated, + hasError, + dispatchFetchSeries, + dispatchFetchTags, + dispatchFetchQualityProfiles, + dispatchFetchLanguageProfiles, + dispatchFetchUISettings, + dispatchFetchStatus, + ...otherProps + } = this.props; + + if (hasError || !this.state.isLocalStorageSupported) { + return ( + + ); + } + + if (isPopulated) { + return ( + + ); + } + + return ( + + ); + } +} + +PageConnector.propTypes = { + isPopulated: PropTypes.bool.isRequired, + hasError: PropTypes.bool.isRequired, + isSidebarVisible: PropTypes.bool.isRequired, + dispatchFetchSeries: PropTypes.func.isRequired, + dispatchFetchTags: PropTypes.func.isRequired, + dispatchFetchQualityProfiles: PropTypes.func.isRequired, + dispatchFetchLanguageProfiles: PropTypes.func.isRequired, + dispatchFetchUISettings: PropTypes.func.isRequired, + dispatchFetchStatus: PropTypes.func.isRequired, + onSidebarVisibleChange: PropTypes.func.isRequired +}; + +export default withRouter( + connect(createMapStateToProps, createMapDispatchToProps)(PageConnector) +); diff --git a/frontend/src/Components/Page/PageContent.css b/frontend/src/Components/Page/PageContent.css new file mode 100644 index 000000000..4580077c3 --- /dev/null +++ b/frontend/src/Components/Page/PageContent.css @@ -0,0 +1,8 @@ +.content { + position: relative; + display: flex; + flex-direction: column; + flex-grow: 1; + overflow-x: hidden; + width: 100%; +} diff --git a/frontend/src/Components/Page/PageContent.js b/frontend/src/Components/Page/PageContent.js new file mode 100644 index 000000000..a416d268c --- /dev/null +++ b/frontend/src/Components/Page/PageContent.js @@ -0,0 +1,32 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import DocumentTitle from 'react-document-title'; +import styles from './PageContent.css'; + +function PageContent(props) { + const { + className, + title, + children + } = props; + + return ( + +
+ {children} +
+
+ ); +} + +PageContent.propTypes = { + className: PropTypes.string, + title: PropTypes.string, + children: PropTypes.node.isRequired +}; + +PageContent.defaultProps = { + className: styles.content +}; + +export default PageContent; diff --git a/frontend/src/Components/Page/PageContentBody.css b/frontend/src/Components/Page/PageContentBody.css new file mode 100644 index 000000000..8b41754dd --- /dev/null +++ b/frontend/src/Components/Page/PageContentBody.css @@ -0,0 +1,19 @@ +.contentBody { + /* 1px for flex-basis so the div grows correctly in Edge/Firefox */ + flex: 1 0 1px; +} + +.innerContentBody { + padding: $pageContentBodyPadding; +} + +@media only screen and (max-width: $breakpointSmall) { + .contentBody { + flex-basis: auto; + overflow-y: hidden !important; + } + + .innerContentBody { + padding: $pageContentBodyPaddingSmallScreen; + } +} diff --git a/frontend/src/Components/Page/PageContentBody.js b/frontend/src/Components/Page/PageContentBody.js new file mode 100644 index 000000000..81bd9b29b --- /dev/null +++ b/frontend/src/Components/Page/PageContentBody.js @@ -0,0 +1,52 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { scrollDirections } from 'Helpers/Props'; +import OverlayScroller from 'Components/Scroller/OverlayScroller'; +import Scroller from 'Components/Scroller/Scroller'; +import styles from './PageContentBody.css'; + +class PageContentBody extends Component { + + // + // Render + + render() { + const { + className, + innerClassName, + isSmallScreen, + children, + dispatch, + ...otherProps + } = this.props; + + const ScrollerComponent = isSmallScreen ? Scroller : OverlayScroller; + + return ( + +
+ {children} +
+
+ ); + } +} + +PageContentBody.propTypes = { + className: PropTypes.string, + innerClassName: PropTypes.string, + isSmallScreen: PropTypes.bool.isRequired, + children: PropTypes.node.isRequired, + dispatch: PropTypes.func +}; + +PageContentBody.defaultProps = { + className: styles.contentBody, + innerClassName: styles.innerContentBody +}; + +export default PageContentBody; diff --git a/frontend/src/Components/Page/PageContentBodyConnector.js b/frontend/src/Components/Page/PageContentBodyConnector.js new file mode 100644 index 000000000..b5cdfbb21 --- /dev/null +++ b/frontend/src/Components/Page/PageContentBodyConnector.js @@ -0,0 +1,17 @@ +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector'; +import PageContentBody from './PageContentBody'; + +function createMapStateToProps() { + return createSelector( + createDimensionsSelector(), + (dimensions) => { + return { + isSmallScreen: dimensions.isSmallScreen + }; + } + ); +} + +export default connect(createMapStateToProps)(PageContentBody); diff --git a/frontend/src/Components/Page/PageContentFooter.css b/frontend/src/Components/Page/PageContentFooter.css new file mode 100644 index 000000000..74bdb3811 --- /dev/null +++ b/frontend/src/Components/Page/PageContentFooter.css @@ -0,0 +1,26 @@ +.contentFooter { + display: flex; + flex: 0 0 auto; + padding: 20px; + background-color: #f1f1f1; +} + +@media only screen and (max-width: $breakpointSmall) { + .contentFooter { + display: block; + + div { + margin-top: 10px; + + &:first-child { + margin-top: 0; + } + } + } +} + +@media only screen and (max-width: $breakpointLarge) { + .contentFooter { + flex-wrap: wrap; + } +} diff --git a/frontend/src/Components/Page/PageContentFooter.js b/frontend/src/Components/Page/PageContentFooter.js new file mode 100644 index 000000000..1f6e2d21a --- /dev/null +++ b/frontend/src/Components/Page/PageContentFooter.js @@ -0,0 +1,33 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import styles from './PageContentFooter.css'; + +class PageContentFooter extends Component { + + // + // Render + + render() { + const { + className, + children + } = this.props; + + return ( +
+ {children} +
+ ); + } +} + +PageContentFooter.propTypes = { + className: PropTypes.string, + children: PropTypes.node.isRequired +}; + +PageContentFooter.defaultProps = { + className: styles.contentFooter +}; + +export default PageContentFooter; diff --git a/frontend/src/Components/Page/PageJumpBar.css b/frontend/src/Components/Page/PageJumpBar.css new file mode 100644 index 000000000..9a116fb54 --- /dev/null +++ b/frontend/src/Components/Page/PageJumpBar.css @@ -0,0 +1,22 @@ +.jumpBar { + display: flex; + align-content: stretch; + align-items: stretch; + align-self: stretch; + justify-content: center; + flex: 0 0 30px; +} + +.jumpBarItems { + display: flex; + justify-content: space-around; + flex: 0 0 100%; + flex-direction: column; + overflow: hidden; +} + +@media only screen and (max-width: $breakpointSmall) { + .jumpBar { + display: none; + } +} diff --git a/frontend/src/Components/Page/PageJumpBar.js b/frontend/src/Components/Page/PageJumpBar.js new file mode 100644 index 000000000..25844ec94 --- /dev/null +++ b/frontend/src/Components/Page/PageJumpBar.js @@ -0,0 +1,133 @@ +import _ from 'lodash'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import Measure from 'react-measure'; +import dimensions from 'Styles/Variables/dimensions'; +import PageJumpBarItem from './PageJumpBarItem'; +import styles from './PageJumpBar.css'; + +const ITEM_HEIGHT = parseInt(dimensions.jumpBarItemHeight); + +class PageJumpBar extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + height: 0, + visibleItems: props.items + }; + } + + componentDidMount() { + this.computeVisibleItems(); + } + + componentDidUpdate(prevProps, prevState) { + if ( + prevProps.items !== this.props.items || + prevState.height !== this.state.height + ) { + this.computeVisibleItems(); + } + } + + // + // Control + + computeVisibleItems() { + const { + items, + minimumItems + } = this.props; + + const height = this.state.height; + const maximumItems = Math.floor(height / ITEM_HEIGHT); + const diff = items.length - maximumItems; + + if (diff < 0) { + this.setState({ visibleItems: items }); + return; + } + + if (items.length < minimumItems) { + this.setState({ visibleItems: items }); + return; + } + + const removeDiff = Math.ceil(items.length / maximumItems); + + const visibleItems = _.reduce(items, (acc, item, index) => { + if (index % removeDiff === 0) { + acc.push(item); + } + + return acc; + }, []); + + this.setState({ visibleItems }); + } + + // + // Listeners + + onMeasure = ({ height }) => { + this.setState({ height }); + } + + // + // Render + + render() { + const { + minimumItems, + onItemPress + } = this.props; + + const { + visibleItems + } = this.state; + + if (!visibleItems.length || visibleItems.length < minimumItems) { + return null; + } + + return ( +
+ +
+ { + visibleItems.map((item) => { + return ( + + ); + }) + } +
+
+
+ ); + } +} + +PageJumpBar.propTypes = { + items: PropTypes.arrayOf(PropTypes.string).isRequired, + minimumItems: PropTypes.number.isRequired, + onItemPress: PropTypes.func.isRequired +}; + +PageJumpBar.defaultProps = { + minimumItems: 5 +}; + +export default PageJumpBar; diff --git a/frontend/src/Components/Page/PageJumpBarItem.css b/frontend/src/Components/Page/PageJumpBarItem.css new file mode 100644 index 000000000..e829dd31a --- /dev/null +++ b/frontend/src/Components/Page/PageJumpBarItem.css @@ -0,0 +1,14 @@ +.jumpBarItem { + flex: 1 0 $jumpBarItemHeight; + border-bottom: 1px solid $borderColor; + text-align: center; + font-weight: bold; + + &:hover { + color: #777; + } + + &:last-child { + border: none; + } +} diff --git a/frontend/src/Components/Page/PageJumpBarItem.js b/frontend/src/Components/Page/PageJumpBarItem.js new file mode 100644 index 000000000..aeffe4ddd --- /dev/null +++ b/frontend/src/Components/Page/PageJumpBarItem.js @@ -0,0 +1,40 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import Link from 'Components/Link/Link'; +import styles from './PageJumpBarItem.css'; + +class PageJumpBarItem extends Component { + + // + // Listeners + + onPress = () => { + const { + label, + onItemPress + } = this.props; + + onItemPress(label); + } + + // + // Render + + render() { + return ( + + {this.props.label.toUpperCase()} + + ); + } +} + +PageJumpBarItem.propTypes = { + label: PropTypes.string.isRequired, + onItemPress: PropTypes.func.isRequired +}; + +export default PageJumpBarItem; diff --git a/frontend/src/Components/Page/PageSectionContent.js b/frontend/src/Components/Page/PageSectionContent.js new file mode 100644 index 000000000..774b88669 --- /dev/null +++ b/frontend/src/Components/Page/PageSectionContent.js @@ -0,0 +1,39 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import LoadingIndicator from 'Components/Loading/LoadingIndicator'; + +function PageSectionContent(props) { + const { + isFetching, + isPopulated, + error, + errorMessage, + children + } = props; + + if (isFetching) { + return ( + + ); + } else if (!isFetching && !!error) { + return ( +
{errorMessage}
+ ); + } else if (isPopulated && !error) { + return ( +
{children}
+ ); + } + + return null; +} + +PageSectionContent.propTypes = { + isFetching: PropTypes.bool.isRequired, + isPopulated: PropTypes.bool.isRequired, + error: PropTypes.object, + errorMessage: PropTypes.string.isRequired, + children: PropTypes.node.isRequired +}; + +export default PageSectionContent; diff --git a/frontend/src/Components/Page/Sidebar/Messages/Message.css b/frontend/src/Components/Page/Sidebar/Messages/Message.css new file mode 100644 index 000000000..7d53adb69 --- /dev/null +++ b/frontend/src/Components/Page/Sidebar/Messages/Message.css @@ -0,0 +1,42 @@ +.message { + display: flex; + border-left: 3px solid $infoColor; +} + +.iconContainer, +.text { + display: flex; + justify-content: center; + flex-direction: column; + padding: 2px 0; + color: $sidebarColor; +} + +.iconContainer { + flex: 0 0 25px; + margin-left: 24px; + padding: 10px 0; +} + +.text { + margin-right: 24px; + font-size: 13px; +} + +/* Types */ + +.error { + border-left-color: $dangerColor; +} + +.info { + border-left-color: $infoColor; +} + +.success { + border-left-color: $successColor; +} + +.warning { + border-left-color: $warningColor; +} diff --git a/frontend/src/Components/Page/Sidebar/Messages/Message.js b/frontend/src/Components/Page/Sidebar/Messages/Message.js new file mode 100644 index 000000000..ed3a8325c --- /dev/null +++ b/frontend/src/Components/Page/Sidebar/Messages/Message.js @@ -0,0 +1,70 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import classNames from 'classnames'; +import { icons } from 'Helpers/Props'; +import Icon from 'Components/Icon'; +import styles from './Message.css'; + +function getIconName(name) { + switch (name) { + case 'ApplicationUpdate': + return icons.RESTART; + case 'Backup': + return icons.BACKUP; + case 'CheckHealth': + return icons.HEALTH; + case 'EpisodeSearch': + return icons.SEARCH; + case 'Housekeeping': + return icons.HOUSEKEEPING; + case 'RefreshArtist': + return icons.REFRESH; + case 'RssSync': + return icons.RSS; + case 'SeasonSearch': + return icons.SEARCH; + case 'ArtistSearch': + return icons.SEARCH; + case 'UpdateSceneMapping': + return icons.REFRESH; + default: + return icons.SPINNER; + } +} + +function Message(props) { + const { + name, + message, + type + } = props; + + return ( +
+
+ +
+ +
+ {message} +
+
+ ); +} + +Message.propTypes = { + name: PropTypes.string.isRequired, + message: PropTypes.string.isRequired, + type: PropTypes.string.isRequired +}; + +export default Message; diff --git a/frontend/src/Components/Page/Sidebar/Messages/MessageConnector.js b/frontend/src/Components/Page/Sidebar/Messages/MessageConnector.js new file mode 100644 index 000000000..06c545c27 --- /dev/null +++ b/frontend/src/Components/Page/Sidebar/Messages/MessageConnector.js @@ -0,0 +1,67 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { hideMessage } from 'Store/Actions/appActions'; +import Message from './Message'; + +const mapDispatchToProps = { + hideMessage +}; + +class MessageConnector extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this._hideTimeoutId = null; + this.scheduleHideMessage(props.hideAfter); + } + + componentDidUpdate() { + this.scheduleHideMessage(this.props.hideAfter); + } + + // + // Control + + scheduleHideMessage = (hideAfter) => { + if (this._hideTimeoutId) { + clearTimeout(this._hideTimeoutId); + } + + if (hideAfter) { + this._hideTimeoutId = setTimeout(this.hideMessage, hideAfter * 1000); + } + } + + hideMessage = () => { + this.props.hideMessage({ id: this.props.id }); + } + + // + // Render + + render() { + return ( + + ); + } +} + +MessageConnector.propTypes = { + id: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired, + hideAfter: PropTypes.number.isRequired, + hideMessage: PropTypes.func.isRequired +}; + +MessageConnector.defaultProps = { + // Hide messages after 60 seconds if there is no activity + // hideAfter: 60 +}; + +export default connect(undefined, mapDispatchToProps)(MessageConnector); diff --git a/frontend/src/Components/Page/Sidebar/Messages/Messages.css b/frontend/src/Components/Page/Sidebar/Messages/Messages.css new file mode 100644 index 000000000..ef01ad02c --- /dev/null +++ b/frontend/src/Components/Page/Sidebar/Messages/Messages.css @@ -0,0 +1,11 @@ +.messages { + margin-top: auto; + margin-bottom: 20px; + padding-top: 20px; +} + +@media only screen and (max-width: $breakpointSmall) { + .messages { + margin-bottom: 0; + } +} diff --git a/frontend/src/Components/Page/Sidebar/Messages/Messages.js b/frontend/src/Components/Page/Sidebar/Messages/Messages.js new file mode 100644 index 000000000..ec8876f6e --- /dev/null +++ b/frontend/src/Components/Page/Sidebar/Messages/Messages.js @@ -0,0 +1,27 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import MessageConnector from './MessageConnector'; +import styles from './Messages.css'; + +function Messages({ messages }) { + return ( +
+ { + messages.map((message) => { + return ( + + ); + }) + } +
+ ); +} + +Messages.propTypes = { + messages: PropTypes.arrayOf(PropTypes.object).isRequired +}; + +export default Messages; diff --git a/frontend/src/Components/Page/Sidebar/Messages/MessagesConnector.js b/frontend/src/Components/Page/Sidebar/Messages/MessagesConnector.js new file mode 100644 index 000000000..5d20d9194 --- /dev/null +++ b/frontend/src/Components/Page/Sidebar/Messages/MessagesConnector.js @@ -0,0 +1,16 @@ +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import Messages from './Messages'; + +function createMapStateToProps() { + return createSelector( + (state) => state.app.messages.items, + (messages) => { + return { + messages: messages.slice().reverse() + }; + } + ); +} + +export default connect(createMapStateToProps)(Messages); diff --git a/frontend/src/Components/Page/Sidebar/PageSidebar.css b/frontend/src/Components/Page/Sidebar/PageSidebar.css new file mode 100644 index 000000000..fdbd80320 --- /dev/null +++ b/frontend/src/Components/Page/Sidebar/PageSidebar.css @@ -0,0 +1,34 @@ +.sidebarContainer { + flex: 0 0 $sidebarWidth; + overflow: hidden; + width: $sidebarWidth; + background-color: $sidebarBackgroundColor; + transition: transform 300ms ease-in-out; + transform: translateX(0); +} + +.sidebar { + display: flex; + flex-direction: column; + overflow: hidden; + background-color: $sidebarBackgroundColor; + color: $white; +} + +@media only screen and (max-width: $breakpointSmall) { + .sidebarContainer { + position: fixed; + top: 0; + z-index: 2; + height: 100vh; + } + + .sidebar { + position: fixed; + z-index: 2; + overflow-y: auto; + width: 100%; + height: 100%; + } +} + diff --git a/frontend/src/Components/Page/Sidebar/PageSidebar.js b/frontend/src/Components/Page/Sidebar/PageSidebar.js new file mode 100644 index 000000000..50ddc3ae7 --- /dev/null +++ b/frontend/src/Components/Page/Sidebar/PageSidebar.js @@ -0,0 +1,514 @@ +import _ from 'lodash'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import ReactDOM from 'react-dom'; +import classNames from 'classnames'; +import { icons } from 'Helpers/Props'; +import locationShape from 'Helpers/Props/Shapes/locationShape'; +import dimensions from 'Styles/Variables/dimensions'; +import OverlayScroller from 'Components/Scroller/OverlayScroller'; +import Scroller from 'Components/Scroller/Scroller'; +import QueueStatusConnector from 'Activity/Queue/Status/QueueStatusConnector'; +import HealthStatusConnector from 'System/Status/Health/HealthStatusConnector'; +import MessagesConnector from './Messages/MessagesConnector'; +import PageSidebarItem from './PageSidebarItem'; +import styles from './PageSidebar.css'; + +const HEADER_HEIGHT = parseInt(dimensions.headerHeight); +const SIDEBAR_WIDTH = parseInt(dimensions.sidebarWidth); + +const links = [ + { + iconName: icons.ARTIST_CONTINUING, + title: 'Artist', + to: '/', + alias: '/artist', + children: [ + { + title: 'Add New', + to: '/add/new' + }, + { + title: 'Import', + to: '/add/import' + }, + { + title: 'Mass Editor', + to: '/artisteditor' + }, + { + title: 'Album Studio', + to: '/albumstudio' + } + ] + }, + + { + iconName: icons.CALENDAR, + title: 'Calendar', + to: '/calendar' + }, + + { + iconName: icons.ACTIVITY, + title: 'Activity', + to: '/activity/queue', + children: [ + { + title: 'Queue', + to: '/activity/queue', + statusComponent: QueueStatusConnector + }, + { + title: 'History', + to: '/activity/history' + }, + { + title: 'Blacklist', + to: '/activity/blacklist' + } + ] + }, + + { + iconName: icons.WARNING, + title: 'Wanted', + to: '/wanted/missing', + children: [ + { + title: 'Missing', + to: '/wanted/missing' + }, + { + title: 'Cutoff Unmet', + to: '/wanted/cutoffunmet' + } + ] + }, + + { + iconName: icons.SETTINGS, + title: 'Settings', + to: '/settings', + children: [ + { + title: 'Media Management', + to: '/settings/mediamanagement' + }, + { + title: 'Profiles', + to: '/settings/profiles' + }, + { + title: 'Quality', + to: '/settings/quality' + }, + { + title: 'Indexers', + to: '/settings/indexers' + }, + { + title: 'Download Clients', + to: '/settings/downloadclients' + }, + { + title: 'Connect', + to: '/settings/connect' + }, + { + title: 'Metadata', + to: '/settings/metadata' + }, + { + title: 'General', + to: '/settings/general' + }, + { + title: 'UI', + to: '/settings/ui' + } + ] + }, + + { + iconName: icons.SYSTEM, + title: 'System', + to: '/system/status', + children: [ + { + title: 'Status', + to: '/system/status', + statusComponent: HealthStatusConnector + }, + { + title: 'Tasks', + to: '/system/tasks' + }, + { + title: 'Backup', + to: '/system/backup' + }, + { + title: 'Updates', + to: '/system/updates' + }, + { + title: 'Events', + to: '/system/events' + }, + { + title: 'Log Files', + to: '/system/logs/files' + } + ] + } +]; + +function getActiveParent(pathname) { + let activeParent = links[0].to; + + links.forEach((link) => { + if (link.to && link.to === pathname) { + activeParent = link.to; + + return false; + } + + const children = link.children; + + if (children) { + children.forEach((childLink) => { + if (pathname.startsWith(childLink.to)) { + activeParent = link.to; + + return false; + } + }); + } + + if ( + (link.to !== '/' && pathname.startsWith(link.to)) || + (link.alias && pathname.startsWith(link.alias)) + ) { + activeParent = link.to; + + return false; + } + }); + + return activeParent; +} + +function hasActiveChildLink(link, pathname) { + const children = link.children; + + if (!children || !children.length) { + return false; + } + + return _.some(children, (child) => { + return child.to === pathname; + }); +} + +function getPositioning() { + const windowScroll = window.scrollY == null ? document.documentElement.scrollTop : window.scrollY; + const top = Math.max(HEADER_HEIGHT - windowScroll, 0); + const height = window.innerHeight - top; + + return { + top: `${top}px`, + height: `${height}px` + }; +} + +class PageSidebar extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this._touchStartX = null; + this._touchStartY = null; + this._sidebarRef = null; + + this.state = { + top: dimensions.headerHeight, + height: `${window.innerHeight - HEADER_HEIGHT}px`, + transition: null, + transform: props.isSidebarVisible ? 0 : SIDEBAR_WIDTH * -1 + }; + } + + componentDidMount() { + if (this.props.isSmallScreen) { + window.addEventListener('click', this.onWindowClick, { capture: true }); + window.addEventListener('scroll', this.onWindowScroll); + window.addEventListener('touchstart', this.onTouchStart); + window.addEventListener('touchmove', this.onTouchMove); + window.addEventListener('touchend', this.onTouchEnd); + window.addEventListener('touchcancel', this.onTouchCancel); + } + } + + componentDidUpdate(prevProps) { + const { + isSidebarVisible + } = this.props; + + const transform = this.state.transform; + + if (prevProps.isSidebarVisible !== isSidebarVisible) { + this._setSidebarTransform(isSidebarVisible); + } else if (transform === 0 && !isSidebarVisible) { + this.props.onSidebarVisibleChange(true); + } else if (transform === -SIDEBAR_WIDTH && isSidebarVisible) { + this.props.onSidebarVisibleChange(false); + } + } + + componentWillUnmount() { + if (this.props.isSmallScreen) { + window.removeEventListener('click', this.onWindowClick, { capture: true }); + window.removeEventListener('scroll', this.onWindowScroll); + window.removeEventListener('touchstart', this.onTouchStart); + window.removeEventListener('touchmove', this.onTouchMove); + window.removeEventListener('touchend', this.onTouchEnd); + window.removeEventListener('touchcancel', this.onTouchCancel); + } + } + + // + // Control + + _setSidebarRef = (ref) => { + this._sidebarRef = ref; + } + + _setSidebarTransform(isSidebarVisible, transition, callback) { + this.setState({ + transition, + transform: isSidebarVisible ? 0 : SIDEBAR_WIDTH * -1 + }, callback); + } + + // + // Listeners + + onWindowClick = (event) => { + const sidebar = ReactDOM.findDOMNode(this._sidebarRef); + const toggleButton = document.getElementById('sidebar-toggle-button'); + + if (!sidebar) { + return; + } + + if ( + !sidebar.contains(event.target) && + !toggleButton.contains(event.target) && + this.props.isSidebarVisible + ) { + event.preventDefault(); + event.stopPropagation(); + this.props.onSidebarVisibleChange(false); + } + } + + onWindowScroll = () => { + this.setState(getPositioning()); + } + + onTouchStart = (event) => { + const touches = event.touches; + const touchStartX = touches[0].pageX; + const touchStartY = touches[0].pageY; + const isSidebarVisible = this.props.isSidebarVisible; + + if (touches.length !== 1) { + return; + } + + if (isSidebarVisible && (touchStartX > 210 || touchStartX < 180)) { + return; + } else if (!isSidebarVisible && touchStartX > 30) { + return; + } + + this._touchStartX = touchStartX; + this._touchStartY = touchStartY; + } + + onTouchMove = (event) => { + const touches = event.touches; + const currentTouchX = touches[0].pageX; + const currentTouchY = touches[0].pageY; + + if (!this._touchStartX) { + return; + } + + if (Math.abs(this._touchStartY - currentTouchY) > 20) { + this.setState({ + transition: 'none', + transform: 0 + }); + + return; + } + + if (Math.abs(this._touchStartX - currentTouchX) < 20) { + return; + } + + const transform = Math.min(currentTouchX - SIDEBAR_WIDTH, 0); + + this.setState({ + transition: 'none', + transform + }); + } + + onTouchEnd = (event) => { + const touches = event.changedTouches; + const currentTouch = touches[0].pageX; + + if (!this._touchStartX) { + return; + } + + if (currentTouch > this._touchStartX && currentTouch > 50) { + this._setSidebarTransform(true, 'none'); + } else if (currentTouch < this._touchStartX && currentTouch < 80) { + this._setSidebarTransform(false, 'transform 50ms ease-in-out'); + } else { + this._setSidebarTransform(this.props.isSidebarVisible); + } + + this._touchStartX = null; + this._touchStartY = null; + } + + onTouchCancel = (event) => { + this._touchStartX = null; + this._touchStartY = null; + } + + onItemPress = () => { + this.props.onSidebarVisibleChange(false); + } + + // + // Render + + render() { + const { + location, + isSmallScreen + } = this.props; + + const { + top, + height, + transition, + transform + } = this.state; + + const urlBase = window.Sonarr.urlBase; + const pathname = urlBase ? location.pathname.substr(urlBase.length) || '/' : location.pathname; + const activeParent = getActiveParent(pathname); + + let containerStyle = {}; + let sidebarStyle = {}; + + if (isSmallScreen) { + containerStyle = { + transition, + transform: `translateX(${transform}px)` + }; + + sidebarStyle = { + top, + height + }; + } + + const ScrollerComponent = isSmallScreen ? Scroller : OverlayScroller; + + return ( +
+ +
+ { + links.map((link) => { + const childWithStatusComponent = _.find(link.children, (child) => { + return !!child.statusComponent; + }); + + const childStatusComponent = childWithStatusComponent ? + childWithStatusComponent.statusComponent : + null; + + const isActiveParent = activeParent === link.to; + const hasActiveChild = hasActiveChildLink(link, pathname); + + return ( + + { + link.children && link.to === activeParent && + link.children.map((child) => { + return ( + + ); + }) + } + + ); + }) + } +
+ + +
+
+ ); + } +} + +PageSidebar.propTypes = { + location: locationShape.isRequired, + isSmallScreen: PropTypes.bool.isRequired, + isSidebarVisible: PropTypes.bool.isRequired, + onSidebarVisibleChange: PropTypes.func.isRequired +}; + +export default PageSidebar; diff --git a/frontend/src/Components/Page/Sidebar/PageSidebarItem.css b/frontend/src/Components/Page/Sidebar/PageSidebarItem.css new file mode 100644 index 000000000..450161705 --- /dev/null +++ b/frontend/src/Components/Page/Sidebar/PageSidebarItem.css @@ -0,0 +1,48 @@ +.item { + border-left: 3px solid transparent; + color: $sidebarColor; + transition: border-left 0.3s ease-in-out; +} + +.isActiveItem { + border-left: 3px solid $themeBlue; +} + +.link { + display: block; + padding: 12px 24px; + color: $sidebarColor; + + &:hover, + &:focus { + color: $themeBlue; + text-decoration: none; + } +} + +.childLink { + composes: link; + + padding: 10px 24px; +} + +.isActiveLink { + color: $themeBlue; +} + +.isActiveParentLink { + background-color: $sidebarActiveBackgroundColor; +} + +.iconContainer { + display: inline-block; + width: 25px; +} + +.noIcon { + margin-left: 25px; +} + +.status { + float: right; +} diff --git a/frontend/src/Components/Page/Sidebar/PageSidebarItem.js b/frontend/src/Components/Page/Sidebar/PageSidebarItem.js new file mode 100644 index 000000000..cfc694395 --- /dev/null +++ b/frontend/src/Components/Page/Sidebar/PageSidebarItem.js @@ -0,0 +1,106 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import classNames from 'classnames'; +import { map } from 'Helpers/elementChildren'; +import Icon from 'Components/Icon'; +import Link from 'Components/Link/Link'; +import styles from './PageSidebarItem.css'; + +class PageSidebarItem extends Component { + + // + // Listeners + + onPress = () => { + const { + isChildItem, + isParentItem, + onPress + } = this.props; + + if (isChildItem || !isParentItem) { + onPress(); + } + } + + // + // Render + + render() { + const { + iconName, + title, + to, + isActive, + isActiveParent, + isChildItem, + statusComponent: StatusComponent, + children + } = this.props; + + return ( +
+ + { + !!iconName && + + + + } + + + {title} + + + { + !!StatusComponent && + + + + } + + + { + children && + map(children, (child) => { + return React.cloneElement(child, { isChildItem: true }); + }) + } +
+ ); + } +} + +PageSidebarItem.propTypes = { + iconName: PropTypes.string, + title: PropTypes.string.isRequired, + to: PropTypes.string.isRequired, + isActive: PropTypes.bool, + isActiveParent: PropTypes.bool, + isParentItem: PropTypes.bool.isRequired, + isChildItem: PropTypes.bool.isRequired, + statusComponent: PropTypes.func, + children: PropTypes.node, + onPress: PropTypes.func +}; + +PageSidebarItem.defaultProps = { + isChildItem: false +}; + +export default PageSidebarItem; diff --git a/frontend/src/Components/Page/Sidebar/PageSidebarStatus.css b/frontend/src/Components/Page/Sidebar/PageSidebarStatus.css new file mode 100644 index 000000000..4dd0cc678 --- /dev/null +++ b/frontend/src/Components/Page/Sidebar/PageSidebarStatus.css @@ -0,0 +1,3 @@ +.status { + composes: label from 'Components/Label.css'; +} diff --git a/frontend/src/Components/Page/Sidebar/PageSidebarStatus.js b/frontend/src/Components/Page/Sidebar/PageSidebarStatus.js new file mode 100644 index 000000000..c1ea615ed --- /dev/null +++ b/frontend/src/Components/Page/Sidebar/PageSidebarStatus.js @@ -0,0 +1,35 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import { kinds, sizes } from 'Helpers/Props'; +import Label from 'Components/Label'; + +function PageSidebarStatus({ count, errors, warnings }) { + if (!count) { + return null; + } + + let kind = kinds.INFO; + + if (errors) { + kind = kinds.DANGER; + } else if (warnings) { + kind = kinds.WARNING; + } + + return ( + + ); +} + +PageSidebarStatus.propTypes = { + count: PropTypes.number, + errors: PropTypes.bool, + warnings: PropTypes.bool +}; + +export default PageSidebarStatus; diff --git a/frontend/src/Components/Page/Toolbar/PageToolbar.css b/frontend/src/Components/Page/Toolbar/PageToolbar.css new file mode 100644 index 000000000..e040bc884 --- /dev/null +++ b/frontend/src/Components/Page/Toolbar/PageToolbar.css @@ -0,0 +1,16 @@ +.toolbar { + display: flex; + justify-content: space-between; + flex: 0 0 auto; + padding: 0 20px; + height: $toolbarHeight; + background-color: $toolbarBackgroundColor; + color: $toolbarColor; + line-height: 60px; +} + +@media only screen and (max-width: $breakpointSmall) { + .toolbar { + padding: 0 10px; + } +} diff --git a/frontend/src/Components/Page/Toolbar/PageToolbar.js b/frontend/src/Components/Page/Toolbar/PageToolbar.js new file mode 100644 index 000000000..728f1b0d9 --- /dev/null +++ b/frontend/src/Components/Page/Toolbar/PageToolbar.js @@ -0,0 +1,33 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import styles from './PageToolbar.css'; + +class PageToolbar extends Component { + + // + // Render + + render() { + const { + className, + children + } = this.props; + + return ( +
+ {children} +
+ ); + } +} + +PageToolbar.propTypes = { + className: PropTypes.string, + children: PropTypes.node.isRequired +}; + +PageToolbar.defaultProps = { + className: styles.toolbar +}; + +export default PageToolbar; diff --git a/frontend/src/Components/Page/Toolbar/PageToolbarButton.css b/frontend/src/Components/Page/Toolbar/PageToolbarButton.css new file mode 100644 index 000000000..e788303a2 --- /dev/null +++ b/frontend/src/Components/Page/Toolbar/PageToolbarButton.css @@ -0,0 +1,28 @@ +.toolbarButton { + composes: link from 'Components/Link/Link.css'; + + width: $toolbarButtonWidth; + text-align: center; + + &:hover { + color: $toobarButtonHoverColor; + } +} + +.isDisabled { + color: $disabledColor; +} + +.labelContainer { + display: flex; + align-items: center; + justify-content: center; + min-height: 16px; +} + +.label { + padding: 0 3px; + color: $toolbarLabelColor; + font-size: $extraSmallFontSize; + line-height: calc($extraSmallFontSize + 1px); +} diff --git a/frontend/src/Components/Page/Toolbar/PageToolbarButton.js b/frontend/src/Components/Page/Toolbar/PageToolbarButton.js new file mode 100644 index 000000000..752ca15ac --- /dev/null +++ b/frontend/src/Components/Page/Toolbar/PageToolbarButton.js @@ -0,0 +1,56 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import classNames from 'classnames'; +import { icons } from 'Helpers/Props'; +import Icon from 'Components/Icon'; +import Link from 'Components/Link/Link'; +import styles from './PageToolbarButton.css'; + +function PageToolbarButton(props) { + const { + label, + iconName, + spinningName, + isDisabled, + isSpinning, + ...otherProps + } = props; + + return ( + + + +
+
+ {label} +
+
+ + ); +} + +PageToolbarButton.propTypes = { + label: PropTypes.string.isRequired, + iconName: PropTypes.string.isRequired, + spinningName: PropTypes.string, + isSpinning: PropTypes.bool, + isDisabled: PropTypes.bool +}; + +PageToolbarButton.defaultProps = { + spinningName: icons.SPINNER, + isDisabled: false, + isSpinning: false +}; + +export default PageToolbarButton; diff --git a/frontend/src/Components/Page/Toolbar/PageToolbarSection.css b/frontend/src/Components/Page/Toolbar/PageToolbarSection.css new file mode 100644 index 000000000..2767c163c --- /dev/null +++ b/frontend/src/Components/Page/Toolbar/PageToolbarSection.css @@ -0,0 +1,26 @@ +.sectionContainer { + display: flex; + flex: 1 1 10%; +} + +.section { + display: flex; + align-items: center; + flex-grow: 1; +} + +.left { + justify-content: flex-start; +} + +.center { + justify-content: center; +} + +.right { + justify-content: flex-end; +} + +.overflowMenuItemIcon { + margin-right: 8px; +} diff --git a/frontend/src/Components/Page/Toolbar/PageToolbarSection.js b/frontend/src/Components/Page/Toolbar/PageToolbarSection.js new file mode 100644 index 000000000..82e30f6eb --- /dev/null +++ b/frontend/src/Components/Page/Toolbar/PageToolbarSection.js @@ -0,0 +1,221 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import Measure from 'react-measure'; +import classNames from 'classnames'; +import { forEach } from 'Helpers/elementChildren'; +import { align, icons } from 'Helpers/Props'; +import dimensions from 'Styles/Variables/dimensions'; +import SpinnerIcon from 'Components/SpinnerIcon'; +import Menu from 'Components/Menu/Menu'; +import MenuContent from 'Components/Menu/MenuContent'; +import MenuItem from 'Components/Menu/MenuItem'; +import ToolbarMenuButton from 'Components/Menu/ToolbarMenuButton'; +import styles from './PageToolbarSection.css'; + +const BUTTON_WIDTH = parseInt(dimensions.toolbarButtonWidth); +const SEPARATOR_MARGIN = parseInt(dimensions.toolbarSeparatorMargin); +const SEPARATOR_WIDTH = 2 * SEPARATOR_MARGIN + 1; +const SEPARATOR_NAME = 'PageToolbarSeparator'; + +function calculateOverflowItems(children, isMeasured, width, collapseButtons) { + let buttonCount = 0; + let separatorCount = 0; + const validChildren = []; + + forEach(children, (child) => { + const name = child.type.name; + + if (name === SEPARATOR_NAME) { + separatorCount++; + } else { + buttonCount++; + } + + validChildren.push(child); + }); + + const buttonsWidth = buttonCount * BUTTON_WIDTH; + const separatorsWidth = separatorCount + SEPARATOR_WIDTH; + const totalWidth = buttonsWidth + separatorsWidth; + + // If the width of buttons and separators is less than + // the available width return all valid children. + + if ( + !isMeasured || + !collapseButtons || + totalWidth < width + ) { + return { + buttons: validChildren, + buttonCount, + overflowItems: [] + }; + } + + const maxButtons = Math.max(Math.floor((width - separatorsWidth) / BUTTON_WIDTH), 1); + const buttons = []; + const overflowItems = []; + let actualButtons = 0; + + // Return all buttons if only one is being pushed to the overflow menu. + if (buttonCount - 1 === maxButtons) { + return { + buttons: validChildren, + buttonCount, + overflowItems: [] + }; + } + + validChildren.forEach((child, index) => { + if (actualButtons < maxButtons) { + if (child.type.name !== SEPARATOR_NAME) { + buttons.push(child); + actualButtons++; + } + } else if (child.type.name !== SEPARATOR_NAME) { + overflowItems.push(child.props); + } + }); + + return { + buttons, + buttonCount, + overflowItems + }; +} + +class PageToolbarSection extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + isMeasured: false, + width: 0, + buttons: [], + overflowItems: [] + }; + } + + // + // Listeners + + onMeasure = ({ width }) => { + this.setState({ + isMeasured: true, + width + }); + } + + // + // Render + + render() { + const { + children, + alignContent, + collapseButtons + } = this.props; + + const { + isMeasured, + width + } = this.state; + + const { + buttons, + buttonCount, + overflowItems + } = calculateOverflowItems(children, isMeasured, width, collapseButtons); + + return ( + +
+ { + isMeasured ? +
+ { + buttons.map((button) => { + return button; + }) + } + + { + !!overflowItems.length && + + + + + { + overflowItems.map((item) => { + const { + iconName, + spinningName, + label, + isDisabled, + isSpinning, + ...otherProps + } = item; + + return ( + + + {label} + + ); + }) + } + + + } +
: + null + } +
+
+ ); + } + +} + +PageToolbarSection.propTypes = { + children: PropTypes.node, + alignContent: PropTypes.oneOf([align.LEFT, align.CENTER, align.RIGHT]), + collapseButtons: PropTypes.bool.isRequired +}; + +PageToolbarSection.defaultProps = { + alignContent: align.LEFT, + collapseButtons: true +}; + +export default PageToolbarSection; diff --git a/frontend/src/Components/Page/Toolbar/PageToolbarSeparator.css b/frontend/src/Components/Page/Toolbar/PageToolbarSeparator.css new file mode 100644 index 000000000..968673593 --- /dev/null +++ b/frontend/src/Components/Page/Toolbar/PageToolbarSeparator.css @@ -0,0 +1,12 @@ +.separator { + margin: 10px $toolbarSeparatorMargin; + height: 40px; + border-right: 1px solid #e5e5e5; + opacity: 0.35; +} + +@media only screen and (max-width: $breakpointSmall) { + .separator { + margin: 10px 5px; + } +} diff --git a/frontend/src/Components/Page/Toolbar/PageToolbarSeparator.js b/frontend/src/Components/Page/Toolbar/PageToolbarSeparator.js new file mode 100644 index 000000000..754248f99 --- /dev/null +++ b/frontend/src/Components/Page/Toolbar/PageToolbarSeparator.js @@ -0,0 +1,17 @@ +import React, { Component } from 'react'; +import styles from './PageToolbarSeparator.css'; + +class PageToolbarSeparator extends Component { + + // + // Render + + render() { + return ( +
+ ); + } + +} + +export default PageToolbarSeparator; diff --git a/frontend/src/Components/ProgressBar.css b/frontend/src/Components/ProgressBar.css new file mode 100644 index 000000000..2f0019043 --- /dev/null +++ b/frontend/src/Components/ProgressBar.css @@ -0,0 +1,93 @@ +.container { + position: relative; + overflow: hidden; + width: 100%; + border-radius: 4px; + background-color: #f5f5f5; + box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1); +} + +.progressBar { + position: relative; + z-index: 1; + float: left; + width: 0; + height: 100%; + box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15); + color: $white; + transition: width 0.6s ease; +} + +.frontTextContainer { + z-index: 1; + color: $white; +} + +.backTextContainer, +.frontTextContainer { + position: absolute; + overflow: hidden; + width: 0; + height: 100%; +} + +.backText, +.frontText { + display: flex; + align-items: center; + justify-content: center; + text-align: center; + font-size: 12px; + cursor: default; +} + +.primary { + background-color: $primaryColor; +} + +.danger { + background-color: $dangerColor; +} + +.success { + background-color: $successColor; +} + +.purple { + background-color: $purple; +} + +.warning { + background-color: $warningColor; +} + +.info { + background-color: $infoColor; +} + +.small { + height: $progressBarSmallHeight; + + .backText, + .frontText { + height: $progressBarSmallHeight; + } +} + +.medium { + height: $progressBarMediumHeight; + + .backText, + .frontText { + height: $progressBarMediumHeight; + } +} + +.large { + height: $progressBarLargeHeight; + + .backText, + .frontText { + height: $progressBarLargeHeight; + } +} diff --git a/frontend/src/Components/ProgressBar.js b/frontend/src/Components/ProgressBar.js new file mode 100644 index 000000000..112bfb575 --- /dev/null +++ b/frontend/src/Components/ProgressBar.js @@ -0,0 +1,100 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import classNames from 'classnames'; +import { kinds, sizes } from 'Helpers/Props'; +import styles from './ProgressBar.css'; + +function ProgressBar(props) { + const { + className, + containerClassName, + title, + progress, + precision, + showText, + text, + kind, + size, + width + } = props; + + const progressPercent = `${progress.toFixed(precision)}%`; + const progressText = text || progressPercent; + const actualWidth = width ? `${width}px` : '100%'; + + return ( +
+ { + showText && !!width && +
+
+
+ {progressText} +
+
+
+ } + +
+ { + showText && +
+
+
+ {progressText} +
+
+
+ } +
+ ); +} + +ProgressBar.propTypes = { + className: PropTypes.string, + containerClassName: PropTypes.string, + title: PropTypes.string, + progress: PropTypes.number.isRequired, + precision: PropTypes.number.isRequired, + showText: PropTypes.bool.isRequired, + text: PropTypes.string, + kind: PropTypes.oneOf(kinds.all).isRequired, + size: PropTypes.oneOf(sizes.all).isRequired, + width: PropTypes.number +}; + +ProgressBar.defaultProps = { + className: styles.progressBar, + containerClassName: styles.container, + precision: 1, + showText: false, + kind: kinds.PRIMARY, + size: sizes.MEDIUM +}; + +export default ProgressBar; diff --git a/frontend/src/Components/Router/Switch.js b/frontend/src/Components/Router/Switch.js new file mode 100644 index 000000000..0c0004a50 --- /dev/null +++ b/frontend/src/Components/Router/Switch.js @@ -0,0 +1,44 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { Switch as RouterSwitch } from 'react-router-dom'; +import getPathWithUrlBase from 'Utilities/getPathWithUrlBase'; +import { map } from 'Helpers/elementChildren'; + +class Switch extends Component { + + // + // Render + + render() { + const { + children + } = this.props; + + return ( + + { + map(children, (child) => { + const { + path: childPath, + addUrlBase = true + } = child.props; + + if (!childPath) { + return child; + } + + const path = addUrlBase ? getPathWithUrlBase(childPath) : childPath; + + return React.cloneElement(child, { path }); + }) + } + + ); + } +} + +Switch.propTypes = { + children: PropTypes.node.isRequired +}; + +export default Switch; diff --git a/frontend/src/Components/Scroller/OverlayScroller.css b/frontend/src/Components/Scroller/OverlayScroller.css new file mode 100644 index 000000000..707a9ac6f --- /dev/null +++ b/frontend/src/Components/Scroller/OverlayScroller.css @@ -0,0 +1,15 @@ +.scroller { + /* Placeholder */ +} + +.thumb { + min-height: 50px; + border: 1px solid transparent; + border-radius: 5px; + background-color: $scrollbarBackgroundColor; + background-clip: padding-box; + + &:hover { + background-color: $scrollbarHoverBackgroundColor; + } +} diff --git a/frontend/src/Components/Scroller/OverlayScroller.js b/frontend/src/Components/Scroller/OverlayScroller.js new file mode 100644 index 000000000..9cc9edec0 --- /dev/null +++ b/frontend/src/Components/Scroller/OverlayScroller.js @@ -0,0 +1,115 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { Scrollbars } from 'react-custom-scrollbars'; +import { scrollDirections } from 'Helpers/Props'; +import styles from './OverlayScroller.css'; + +class OverlayScroller extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this._scroller = null; + } + + componentDidUpdate(prevProps) { + const { + scrollTop + } = this.props; + + if (scrollTop != null && scrollTop !== prevProps.scrollTop) { + this._scroller.scrollTop(scrollTop); + } + } + + // + // Control + + _setScrollRef = (ref) => { + this._scroller = ref; + } + + _renderThumb = (props) => { + return ( +
+ ); + } + + _renderView = (props) => { + return ( +
+ ); + } + + // + // Listers + + onScroll = (event) => { + const { + scrollTop, + scrollLeft + } = event.currentTarget; + + const onScroll = this.props.onScroll; + + if (onScroll) { + onScroll({ scrollTop, scrollLeft }); + } + } + + // + // Render + + render() { + const { + autoHide, + autoScroll, + children + } = this.props; + + return ( + + {children} + + ); + } + +} + +OverlayScroller.propTypes = { + className: PropTypes.string, + trackClassName: PropTypes.string, + scrollTop: PropTypes.number, + scrollDirection: PropTypes.oneOf([scrollDirections.NONE, scrollDirections.HORIZONTAL, scrollDirections.VERTICAL]).isRequired, + autoHide: PropTypes.bool.isRequired, + autoScroll: PropTypes.bool.isRequired, + children: PropTypes.node, + onScroll: PropTypes.func +}; + +OverlayScroller.defaultProps = { + className: styles.scroller, + trackClassName: styles.thumb, + scrollDirection: scrollDirections.VERTICAL, + autoHide: false, + autoScroll: true +}; + +export default OverlayScroller; diff --git a/frontend/src/Components/Scroller/Scroller.css b/frontend/src/Components/Scroller/Scroller.css new file mode 100644 index 000000000..c8783a8de --- /dev/null +++ b/frontend/src/Components/Scroller/Scroller.css @@ -0,0 +1,28 @@ +.scroller { + @add-mixin scrollbar; + @add-mixin scrollbarTrack; + @add-mixin scrollbarThumb; +} + +.none { + overflow-x: hidden; + overflow-y: hidden; +} + +.vertical { + overflow-x: hidden; + overflow-y: scroll; + + &.autoScroll { + overflow-y: auto; + } +} + +.horizontal { + overflow-x: scroll; + overflow-y: hidden; + + &.autoScroll { + overflow-x: auto; + } +} diff --git a/frontend/src/Components/Scroller/Scroller.js b/frontend/src/Components/Scroller/Scroller.js new file mode 100644 index 000000000..701ac0cf4 --- /dev/null +++ b/frontend/src/Components/Scroller/Scroller.js @@ -0,0 +1,81 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import classNames from 'classnames'; +import { scrollDirections } from 'Helpers/Props'; +import styles from './Scroller.css'; + +class Scroller extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this._scroller = null; + } + + componentDidMount() { + const { + scrollTop + } = this.props; + + if (this.props.scrollTop != null) { + this._scroller.scrollTop = scrollTop; + } + } + + // + // Control + + _setScrollerRef = (ref) => { + this._scroller = ref; + } + + // + // Render + + render() { + const { + className, + scrollDirection, + autoScroll, + children, + scrollTop, + onScroll, + ...otherProps + } = this.props; + + return ( +
+ {children} +
+ ); + } + +} + +Scroller.propTypes = { + className: PropTypes.string, + scrollDirection: PropTypes.oneOf([scrollDirections.NONE, scrollDirections.HORIZONTAL, scrollDirections.VERTICAL]).isRequired, + autoScroll: PropTypes.bool.isRequired, + scrollTop: PropTypes.number, + children: PropTypes.node, + onScroll: PropTypes.func +}; + +Scroller.defaultProps = { + scrollDirection: scrollDirections.VERTICAL, + autoScroll: true +}; + +export default Scroller; diff --git a/frontend/src/Components/SignalRConnector.js b/frontend/src/Components/SignalRConnector.js new file mode 100644 index 000000000..9aa8bc691 --- /dev/null +++ b/frontend/src/Components/SignalRConnector.js @@ -0,0 +1,369 @@ +import $ from 'jquery'; +import 'signalr'; +import PropTypes from 'prop-types'; +import { Component } from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import { repopulatePage } from 'Utilities/pagePopulator'; +import { updateCommand, finishCommand } from 'Store/Actions/commandActions'; +import { setAppValue, setVersion } from 'Store/Actions/appActions'; +import { update, updateItem, removeItem } from 'Store/Actions/baseActions'; +import { fetchHealth } from 'Store/Actions/systemActions'; +import { fetchQueue, fetchQueueDetails } from 'Store/Actions/queueActions'; + +function getState(status) { + switch (status) { + case 0: + return 'connecting'; + case 1: + return 'connected'; + case 2: + return 'reconnecting'; + case 4: + return 'disconnected'; + default: + throw new Error(`invalid status ${status}`); + } +} + +function createMapStateToProps() { + return createSelector( + (state) => state.app.isReconnecting, + (state) => state.app.isDisconnected, + (state) => state.queue.paged.isPopulated, + (isReconnecting, isDisconnected, isQueuePopulated) => { + return { + isReconnecting, + isDisconnected, + isQueuePopulated + }; + } + ); +} + +const mapDispatchToProps = { + updateCommand, + finishCommand, + setAppValue, + setVersion, + update, + updateItem, + removeItem, + fetchHealth, + fetchQueue, + fetchQueueDetails +}; + +class SignalRConnector extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.signalRconnectionOptions = { transport: ['webSockets', 'longPolling'] }; + this.signalRconnection = null; + this.retryInterval = 5; + this.retryTimeoutId = null; + } + + componentDidMount() { + console.log('Starting signalR'); + + this.signalRconnection = $.connection('/signalr', { apiKey: window.Sonarr.apiKey }); + + this.signalRconnection.stateChanged(this.onStateChanged); + this.signalRconnection.received(this.onReceived); + this.signalRconnection.reconnecting(this.onReconnecting); + this.signalRconnection.disconnected(this.onDisconnected); + + this.signalRconnection.start(this.signalRconnectionOptions); + } + + componentWillUnmount() { + this.signalRconnection.stop(); + this.signalRconnection = null; + } + + // + // Control + + retryConnection = () => { + if (this.retryInterval >= 30) { + this.setState({ + isDisconnected: true + }); + } + + this.retryTimeoutId = setTimeout(() => { + this.signalRconnection.start(this.signalRconnectionOptions); + this.retryInterval = Math.min(this.retryInterval + 5, 30); + }, this.retryInterval * 1000); + } + + handleMessage = (message) => { + const { + name, + body + } = message; + + if (name === 'calendar') { + this.handleCalendar(body); + return; + } + + if (name === 'command') { + this.handleCommand(body); + return; + } + + if (name === 'episode') { + this.handleEpisode(body); + return; + } + + if (name === 'track') { + this.handleTrack(body); + return; + } + + if (name === 'episodefile') { + this.handleTrackFile(body); + return; + } + + if (name === 'health') { + this.handleHealth(body); + return; + } + + if (name === 'artist') { + this.handleArtist(body); + return; + } + + if (name === 'queue') { + this.handleQueue(body); + return; + } + + if (name === 'queue/details') { + this.handleQueueDetails(body); + return; + } + + if (name === 'queue/status') { + this.handleQueueStatus(body); + return; + } + + if (name === 'version') { + this.handleVersion(body); + return; + } + + if (name === 'wanted/cutoff') { + this.handleWantedCutoff(body); + return; + } + + if (name === 'wanted/missing') { + this.handleWantedMissing(body); + return; + } + } + + handleCalendar = (body) => { + if (body.action === 'updated') { + this.props.updateItem({ + section: 'calendar', + updateOnly: true, + ...body.resource + }); + } + } + + handleCommand = (body) => { + const resource = body.resource; + const state = resource.state; + + // Both sucessful and failed commands need to be + // completed, otherwise they spin until they timeout. + + if (state === 'completed' || state === 'failed') { + this.props.finishCommand(resource); + } else { + this.props.updateCommand(resource); + } + } + + handleEpisode = (body) => { + if (body.action === 'updated') { + this.props.updateItem({ + section: 'episodes', + updateOnly: true, + ...body.resource + }); + } + } + + handleTrack = (body) => { + if (body.action === 'updated') { + this.props.updateItem({ + section: 'tracks', + updateOnly: true, + ...body.resource + }); + } + } + + handleTrackFile = (body) => { + if (body.action === 'updated') { + this.props.updateItem({ + section: 'trackFiles', + ...body.resource + }); + } + } + + handleHealth = (body) => { + this.props.fetchHealth(); + } + + handleArtist = (body) => { + const action = body.action; + const section = 'artist'; + + if (action === 'updated') { + this.props.updateItem({ section, ...body.resource }); + } else if (action === 'deleted') { + this.props.removeItem({ section, id: body.resource.id }); + } + } + + handleQueue = (body) => { + if (this.props.isQueuePopulated) { + this.props.fetchQueue(); + } + } + + handleQueueDetails = (body) => { + this.props.fetchQueueDetails(); + } + + handleQueueStatus = (body) => { + this.props.update({ section: 'queueStatus', data: body.resource }); + } + + handleVersion = (body) => { + const version = body.Version; + + this.props.setVersion({ version }); + } + + handleWantedCutoff = (body) => { + if (body.action === 'updated') { + this.props.updateItem({ + section: 'cutoffUnmet', + updateOnly: true, + ...body.resource + }); + } + } + + handleWantedMissing = (body) => { + if (body.action === 'updated') { + this.props.updateItem({ + section: 'missing', + updateOnly: true, + ...body.resource + }); + } + } + + // + // Listeners + + onStateChanged = (change) => { + const state = getState(change.newState); + console.log(`SignalR: ${state}`); + + if (state === 'connected') { + // Repopulate the page (if a repopulator is set) to ensure things + // are in sync after reconnecting. + + if (this.props.isReconnecting || this.props.isDisconnected) { + repopulatePage(); + } + + this.props.setAppValue({ + isConnected: true, + isReconnecting: false, + isDisconnected: false + }); + + this.retryInterval = 5; + + if (this.retryTimeoutId) { + clearTimeout(this.retryTimeoutId); + } + } + } + + onReceived = (message) => { + console.debug('SignalR: received', message.name, message.body); + + this.handleMessage(message); + } + + onReconnecting = () => { + if (window.Sonarr.unloading) { + return; + } + + this.props.setAppValue({ + isReconnecting: true + }); + } + + onDisconnected = () => { + if (window.Sonarr.unloading) { + return; + } + + this.props.setAppValue({ + isConnected: false, + isReconnecting: true + // Don't set isDisconnected yet, it'll be set it if it's disconnected + // for ~105 seconds (retry interval reaches 30 seconds) + }); + + this.retryConnection(); + } + + // + // Render + + render() { + return null; + } +} + +SignalRConnector.propTypes = { + isReconnecting: PropTypes.bool.isRequired, + isDisconnected: PropTypes.bool.isRequired, + isQueuePopulated: PropTypes.bool.isRequired, + updateCommand: PropTypes.func.isRequired, + finishCommand: PropTypes.func.isRequired, + setAppValue: PropTypes.func.isRequired, + setVersion: PropTypes.func.isRequired, + update: PropTypes.func.isRequired, + updateItem: PropTypes.func.isRequired, + removeItem: PropTypes.func.isRequired, + fetchHealth: PropTypes.func.isRequired, + fetchQueue: PropTypes.func.isRequired, + fetchQueueDetails: PropTypes.func.isRequired +}; + +export default connect(createMapStateToProps, mapDispatchToProps)(SignalRConnector); diff --git a/frontend/src/Components/SpinnerIcon.js b/frontend/src/Components/SpinnerIcon.js new file mode 100644 index 000000000..4c5cbb700 --- /dev/null +++ b/frontend/src/Components/SpinnerIcon.js @@ -0,0 +1,32 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import { icons } from 'Helpers/Props'; +import Icon from './Icon'; + +function SpinnerIcon(props) { + const { + name, + spinningName, + isSpinning, + ...otherProps + } = props; + + return ( + + ); +} + +SpinnerIcon.propTypes = { + name: PropTypes.string.isRequired, + spinningName: PropTypes.string.isRequired, + isSpinning: PropTypes.bool.isRequired +}; + +SpinnerIcon.defaultProps = { + spinningName: icons.SPINNER +}; + +export default SpinnerIcon; diff --git a/frontend/src/Components/Table/Cells/RelativeDateCell.css b/frontend/src/Components/Table/Cells/RelativeDateCell.css new file mode 100644 index 000000000..7be20ce5d --- /dev/null +++ b/frontend/src/Components/Table/Cells/RelativeDateCell.css @@ -0,0 +1,5 @@ +.cell { + composes: cell from './TableRowCell.css'; + + width: 180px; +} diff --git a/frontend/src/Components/Table/Cells/RelativeDateCell.js b/frontend/src/Components/Table/Cells/RelativeDateCell.js new file mode 100644 index 000000000..874ae4aca --- /dev/null +++ b/frontend/src/Components/Table/Cells/RelativeDateCell.js @@ -0,0 +1,60 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import formatDateTime from 'Utilities/Date/formatDateTime'; +import getRelativeDate from 'Utilities/Date/getRelativeDate'; +import TableRowCell from './TableRowCell'; +import styles from './relativeDateCell.css'; + +function RelativeDateCell(props) { + const { + className, + date, + includeSeconds, + showRelativeDates, + shortDateFormat, + longDateFormat, + timeFormat, + component: Component, + dispatch, + ...otherProps + } = props; + + if (!date) { + return ( + + ); + } + + return ( + + {getRelativeDate(date, shortDateFormat, showRelativeDates, { timeFormat, includeSeconds, timeForToday: true })} + + ); +} + +RelativeDateCell.propTypes = { + className: PropTypes.string.isRequired, + date: PropTypes.string, + includeSeconds: PropTypes.bool.isRequired, + showRelativeDates: PropTypes.bool.isRequired, + shortDateFormat: PropTypes.string.isRequired, + longDateFormat: PropTypes.string.isRequired, + timeFormat: PropTypes.string.isRequired, + component: PropTypes.func, + dispatch: PropTypes.func +}; + +RelativeDateCell.defaultProps = { + className: styles.cell, + includeSeconds: false, + component: TableRowCell +}; + +export default RelativeDateCell; diff --git a/frontend/src/Components/Table/Cells/RelativeDateCellConnector.js b/frontend/src/Components/Table/Cells/RelativeDateCellConnector.js new file mode 100644 index 000000000..ed996abbe --- /dev/null +++ b/frontend/src/Components/Table/Cells/RelativeDateCellConnector.js @@ -0,0 +1,21 @@ +import _ from 'lodash'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector'; +import RelativeDateCell from './RelativeDateCell'; + +function createMapStateToProps() { + return createSelector( + createUISettingsSelector(), + (uiSettings) => { + return _.pick(uiSettings, [ + 'showRelativeDates', + 'shortDateFormat', + 'longDateFormat', + 'timeFormat' + ]); + } + ); +} + +export default connect(createMapStateToProps, null)(RelativeDateCell); diff --git a/frontend/src/Components/Table/Cells/TableRowCell.css b/frontend/src/Components/Table/Cells/TableRowCell.css new file mode 100644 index 000000000..1c3e6fc5a --- /dev/null +++ b/frontend/src/Components/Table/Cells/TableRowCell.css @@ -0,0 +1,11 @@ +.cell { + padding: 8px; + border-top: 1px solid #eee; + line-height: 1.52857143; +} + +@media only screen and (max-width: $breakpointSmall) { + .cell { + white-space: nowrap; + } +} diff --git a/frontend/src/Components/Table/Cells/TableRowCell.js b/frontend/src/Components/Table/Cells/TableRowCell.js new file mode 100644 index 000000000..f66bbf3aa --- /dev/null +++ b/frontend/src/Components/Table/Cells/TableRowCell.js @@ -0,0 +1,37 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import styles from './TableRowCell.css'; + +class TableRowCell extends Component { + + // + // Render + + render() { + const { + className, + children, + ...otherProps + } = this.props; + + return ( + + {children} + + ); + } +} + +TableRowCell.propTypes = { + className: PropTypes.string.isRequired, + children: PropTypes.oneOfType([PropTypes.string, PropTypes.node]) +}; + +TableRowCell.defaultProps = { + className: styles.cell +}; + +export default TableRowCell; diff --git a/frontend/src/Components/Table/Cells/TableRowCellButton.css b/frontend/src/Components/Table/Cells/TableRowCellButton.css new file mode 100644 index 000000000..f01e7cba6 --- /dev/null +++ b/frontend/src/Components/Table/Cells/TableRowCellButton.css @@ -0,0 +1,4 @@ +.cell { + composes: cell from './TableRowCell.css'; + composes: link from 'Components/Link/Link.css'; +} diff --git a/frontend/src/Components/Table/Cells/TableRowCellButton.js b/frontend/src/Components/Table/Cells/TableRowCellButton.js new file mode 100644 index 000000000..ff50d3bc9 --- /dev/null +++ b/frontend/src/Components/Table/Cells/TableRowCellButton.js @@ -0,0 +1,25 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import Link from 'Components/Link/Link'; +import TableRowCell from './TableRowCell'; +import styles from './TableRowCellButton.css'; + +function TableRowCellButton({ className, ...otherProps }) { + return ( + + ); +} + +TableRowCellButton.propTypes = { + className: PropTypes.string.isRequired +}; + +TableRowCellButton.defaultProps = { + className: styles.cell +}; + +export default TableRowCellButton; diff --git a/frontend/src/Components/Table/Cells/TableSelectCell.css b/frontend/src/Components/Table/Cells/TableSelectCell.css new file mode 100644 index 000000000..21ab944d7 --- /dev/null +++ b/frontend/src/Components/Table/Cells/TableSelectCell.css @@ -0,0 +1,11 @@ +.selectCell { + composes: cell from 'Components/Table/Cells/TableRowCell.css'; + + width: 30px; +} + +.input { + composes: input from 'Components/Form/CheckInput.css'; + + margin: 0; +} diff --git a/frontend/src/Components/Table/Cells/TableSelectCell.js b/frontend/src/Components/Table/Cells/TableSelectCell.js new file mode 100644 index 000000000..b82cf9168 --- /dev/null +++ b/frontend/src/Components/Table/Cells/TableSelectCell.js @@ -0,0 +1,71 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import CheckInput from 'Components/Form/CheckInput'; +import TableRowCell from './TableRowCell'; +import styles from './TableSelectCell.css'; + +class TableSelectCell extends Component { + + // + // Lifecycle + + componentDidMount() { + const { + id, + isSelected, + onSelectedChange + } = this.props; + + onSelectedChange({ id, value: isSelected }); + } + + // + // Listeners + + onChange = ({ value, shiftKey }, a, b, c, d) => { + const { + id, + onSelectedChange + } = this.props; + + onSelectedChange({ id, value, shiftKey }); + } + + // + // Render + + render() { + const { + className, + id, + isSelected, + ...otherProps + } = this.props; + + return ( + + + + ); + } +} + +TableSelectCell.propTypes = { + className: PropTypes.string.isRequired, + id: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired, + isSelected: PropTypes.bool.isRequired, + onSelectedChange: PropTypes.func.isRequired +}; + +TableSelectCell.defaultProps = { + className: styles.selectCell, + isSelected: false +}; + +export default TableSelectCell; diff --git a/frontend/src/Components/Table/Cells/VirtualTableRowCell.css b/frontend/src/Components/Table/Cells/VirtualTableRowCell.css new file mode 100644 index 000000000..e4cffe1c4 --- /dev/null +++ b/frontend/src/Components/Table/Cells/VirtualTableRowCell.css @@ -0,0 +1,14 @@ +.cell { + @add-mixin truncate; + composes: cell from 'Components/Table/Cells/TableRowCell.css'; + + flex-grow: 0; + flex-shrink: 1; + white-space: nowrap; +} + +@media only screen and (max-width: $breakpointSmall) { + .cell { + white-space: nowrap; + } +} diff --git a/frontend/src/Components/Table/Cells/VirtualTableRowCell.js b/frontend/src/Components/Table/Cells/VirtualTableRowCell.js new file mode 100644 index 000000000..42999216f --- /dev/null +++ b/frontend/src/Components/Table/Cells/VirtualTableRowCell.js @@ -0,0 +1,29 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import styles from './VirtualTableRowCell.css'; + +function VirtualTableRowCell(props) { + const { + className, + children + } = props; + + return ( +
+ {children} +
+ ); +} + +VirtualTableRowCell.propTypes = { + className: PropTypes.string.isRequired, + children: PropTypes.oneOfType([PropTypes.string, PropTypes.node]) +}; + +VirtualTableRowCell.defaultProps = { + className: styles.cell +}; + +export default VirtualTableRowCell; diff --git a/frontend/src/Components/Table/Cells/VirtualTableSelectCell.css b/frontend/src/Components/Table/Cells/VirtualTableSelectCell.css new file mode 100644 index 000000000..e1016aa8a --- /dev/null +++ b/frontend/src/Components/Table/Cells/VirtualTableSelectCell.css @@ -0,0 +1,11 @@ +.cell { + composes: cell from 'Components/Table/Cells/VirtualTableRowCell.css'; + + flex: 0 0 36px; +} + +.input { + composes: input from 'Components/Form/CheckInput.css'; + + margin: 0; +} diff --git a/frontend/src/Components/Table/Cells/VirtualTableSelectCell.js b/frontend/src/Components/Table/Cells/VirtualTableSelectCell.js new file mode 100644 index 000000000..a773aab58 --- /dev/null +++ b/frontend/src/Components/Table/Cells/VirtualTableSelectCell.js @@ -0,0 +1,82 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import CheckInput from 'Components/Form/CheckInput'; +import VirtualTableRowCell from './VirtualTableRowCell'; +import styles from './VirtualTableSelectCell.css'; + +export function virtualTableSelectCellRenderer(cellProps) { + const { + cellKey, + rowData, + columnData, + ...otherProps + } = cellProps; + + return ( + + ); +} + +class VirtualTableSelectCell extends Component { + + // + // Listeners + + onChange = ({ value, shiftKey }) => { + const { + id, + onSelectedChange + } = this.props; + + onSelectedChange({ id, value, shiftKey }); + } + + // + // Render + + render() { + const { + inputClassName, + id, + isSelected, + isDisabled, + ...otherProps + } = this.props; + + return ( + + + + ); + } +} + +VirtualTableSelectCell.propTypes = { + inputClassName: PropTypes.string.isRequired, + id: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired, + isSelected: PropTypes.bool.isRequired, + isDisabled: PropTypes.bool.isRequired, + onSelectedChange: PropTypes.func.isRequired +}; + +VirtualTableSelectCell.defaultProps = { + inputClassName: styles.input, + isSelected: false +}; + +export default VirtualTableSelectCell; diff --git a/frontend/src/Components/Table/Table.css b/frontend/src/Components/Table/Table.css new file mode 100644 index 000000000..46d49826a --- /dev/null +++ b/frontend/src/Components/Table/Table.css @@ -0,0 +1,16 @@ +.tableContainer { + overflow-x: auto; +} + +.table { + max-width: 100%; + width: 100%; + border-collapse: collapse; +} + +@media only screen and (max-width: $breakpointSmall) { + .tableContainer { + overflow-y: hidden; + width: 100%; + } +} diff --git a/frontend/src/Components/Table/Table.js b/frontend/src/Components/Table/Table.js new file mode 100644 index 000000000..f66eec49a --- /dev/null +++ b/frontend/src/Components/Table/Table.js @@ -0,0 +1,157 @@ +import _ from 'lodash'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { icons, scrollDirections } from 'Helpers/Props'; +import IconButton from 'Components/Link/IconButton'; +import Scroller from 'Components/Scroller/Scroller'; +import TableOptionsModal from 'Components/Table/TableOptions/TableOptionsModal'; +import TableHeader from './TableHeader'; +import TableHeaderCell from './TableHeaderCell'; +import TableSelectAllHeaderCell from './TableSelectAllHeaderCell'; +import styles from './Table.css'; + +const tableHeaderCellProps = [ + 'sortKey', + 'sortDirection' +]; + +function getTableHeaderCellProps(props) { + return _.reduce(tableHeaderCellProps, (result, key) => { + if (props.hasOwnProperty(key)) { + result[key] = props[key]; + } + + return result; + }, {}); +} + +class Table extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + isTableOptionsModalOpen: false + }; + } + + // + // Listeners + + onTableOptionsPress = () => { + this.setState({ isTableOptionsModalOpen: true }); + } + + onTableOptionsModalClose = () => { + this.setState({ isTableOptionsModalOpen: false }); + } + + // + // Render + + render() { + const { + className, + selectAll, + columns, + pageSize, + canModifyColumns, + children, + onSortPress, + onTableOptionChange, + ...otherProps + } = this.props; + + return ( + + + + { + selectAll && + + } + + { + columns.map((column) => { + const { + name, + isVisible + } = column; + + if (!isVisible) { + return null; + } + + if ((name === 'actions' || name === 'details') && onTableOptionChange) { + return ( + + + + ); + } + + return ( + + {column.label} + + ); + }) + } + + { + !!onTableOptionChange && + + } + + + {children} +
+
+ ); + } +} + +Table.propTypes = { + className: PropTypes.string, + selectAll: PropTypes.bool.isRequired, + columns: PropTypes.arrayOf(PropTypes.object).isRequired, + pageSize: PropTypes.number, + canModifyColumns: PropTypes.bool, + children: PropTypes.node, + onSortPress: PropTypes.func, + onTableOptionChange: PropTypes.func +}; + +Table.defaultProps = { + className: styles.table, + selectAll: false +}; + +export default Table; diff --git a/frontend/src/Components/Table/TableBody.js b/frontend/src/Components/Table/TableBody.js new file mode 100644 index 000000000..5cc60d6f4 --- /dev/null +++ b/frontend/src/Components/Table/TableBody.js @@ -0,0 +1,25 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; + +class TableBody extends Component { + + // + // Render + + render() { + const { + children + } = this.props; + + return ( + {children} + ); + } + +} + +TableBody.propTypes = { + children: PropTypes.node +}; + +export default TableBody; diff --git a/frontend/src/Components/Table/TableHeader.js b/frontend/src/Components/Table/TableHeader.js new file mode 100644 index 000000000..81943e919 --- /dev/null +++ b/frontend/src/Components/Table/TableHeader.js @@ -0,0 +1,28 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; + +class TableHeader extends Component { + + // + // Render + + render() { + const { + children + } = this.props; + + return ( + + + {children} + + + ); + } +} + +TableHeader.propTypes = { + children: PropTypes.node +}; + +export default TableHeader; diff --git a/frontend/src/Components/Table/TableHeaderCell.css b/frontend/src/Components/Table/TableHeaderCell.css new file mode 100644 index 000000000..c2c4f58c8 --- /dev/null +++ b/frontend/src/Components/Table/TableHeaderCell.css @@ -0,0 +1,16 @@ +.headerCell { + padding: 8px; + border: none !important; + text-align: left; + font-weight: bold; +} + +.sortIcon { + margin-left: 10px; +} + +@media only screen and (max-width: $breakpointSmall) { + .headerCell { + white-space: nowrap; + } +} diff --git a/frontend/src/Components/Table/TableHeaderCell.js b/frontend/src/Components/Table/TableHeaderCell.js new file mode 100644 index 000000000..73c4b7ec2 --- /dev/null +++ b/frontend/src/Components/Table/TableHeaderCell.js @@ -0,0 +1,94 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { icons, sortDirections } from 'Helpers/Props'; +import Link from 'Components/Link/Link'; +import Icon from 'Components/Icon'; +import styles from './TableHeaderCell.css'; + +class TableHeaderCell extends Component { + + // + // Listeners + + onPress = () => { + const { + name, + fixedSortDirection + } = this.props; + + if (fixedSortDirection) { + this.props.onSortPress(name, fixedSortDirection); + } else { + this.props.onSortPress(name); + } + } + + // + // Render + + render() { + const { + className, + name, + isSortable, + isVisible, + isModifiable, + sortKey, + sortDirection, + fixedSortDirection, + children, + onSortPress, + ...otherProps + } = this.props; + + const isSorting = isSortable && sortKey === name; + const sortIcon = sortDirection === sortDirections.ASCENDING ? + icons.SORT_ASCENDING : + icons.SORT_DESCENDING; + + return ( + isSortable ? + + {children} + + { + isSorting && + + } + : + + + {children} + + ); + } +} + +TableHeaderCell.propTypes = { + className: PropTypes.string, + name: PropTypes.string.isRequired, + label: PropTypes.oneOfType([PropTypes.string, PropTypes.object]), + isSortable: PropTypes.bool, + isVisible: PropTypes.bool, + isModifiable: PropTypes.bool, + sortKey: PropTypes.string, + fixedSortDirection: PropTypes.string, + sortDirection: PropTypes.string, + children: PropTypes.node, + onSortPress: PropTypes.func +}; + +TableHeaderCell.defaultProps = { + className: styles.headerCell, + isSortable: false +}; + +export default TableHeaderCell; diff --git a/frontend/src/Components/Table/TableOptions/TableOptionsColumn.css b/frontend/src/Components/Table/TableOptions/TableOptionsColumn.css new file mode 100644 index 000000000..204773c3d --- /dev/null +++ b/frontend/src/Components/Table/TableOptions/TableOptionsColumn.css @@ -0,0 +1,48 @@ +.column { + display: flex; + align-items: stretch; + width: 100%; + border: 1px solid #aaa; + border-radius: 4px; + background: #fafafa; +} + +.checkContainer { + position: relative; + margin-right: 4px; + margin-bottom: 7px; + margin-left: 8px; +} + +.label { + display: flex; + flex-grow: 1; + margin-bottom: 0; + margin-left: 2px; + font-weight: normal; + line-height: 36px; + cursor: pointer; +} + +.dragHandle { + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + margin-left: auto; + width: $dragHandleWidth; + text-align: center; + cursor: grab; +} + +.dragIcon { + top: 0; +} + +.isDragging { + opacity: 0.25; +} + +.notDragable { + padding: 4px 0; +} diff --git a/frontend/src/Components/Table/TableOptions/TableOptionsColumn.js b/frontend/src/Components/Table/TableOptions/TableOptionsColumn.js new file mode 100644 index 000000000..6a8e345f8 --- /dev/null +++ b/frontend/src/Components/Table/TableOptions/TableOptionsColumn.js @@ -0,0 +1,68 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import classNames from 'classnames'; +import { icons } from 'Helpers/Props'; +import Icon from 'Components/Icon'; +import CheckInput from 'Components/Form/CheckInput'; +import styles from './TableOptionsColumn.css'; + +function TableOptionsColumn(props) { + const { + name, + label, + isVisible, + isModifiable, + isDragging, + connectDragSource, + onVisibleChange + } = props; + + return ( +
+
+ + + { + !!connectDragSource && + connectDragSource( +
+ +
+ ) + } +
+
+ ); +} + +TableOptionsColumn.propTypes = { + name: PropTypes.string.isRequired, + label: PropTypes.string.isRequired, + isVisible: PropTypes.bool.isRequired, + isModifiable: PropTypes.bool.isRequired, + index: PropTypes.number.isRequired, + isDragging: PropTypes.bool, + connectDragSource: PropTypes.func, + onVisibleChange: PropTypes.func.isRequired +}; + +export default TableOptionsColumn; diff --git a/frontend/src/Components/Table/TableOptions/TableOptionsColumnDragPreview.css b/frontend/src/Components/Table/TableOptions/TableOptionsColumnDragPreview.css new file mode 100644 index 000000000..b927d9bce --- /dev/null +++ b/frontend/src/Components/Table/TableOptions/TableOptionsColumnDragPreview.css @@ -0,0 +1,4 @@ +.dragPreview { + width: 380px; + opacity: 0.75; +} diff --git a/frontend/src/Components/Table/TableOptions/TableOptionsColumnDragPreview.js b/frontend/src/Components/Table/TableOptions/TableOptionsColumnDragPreview.js new file mode 100644 index 000000000..03169f00c --- /dev/null +++ b/frontend/src/Components/Table/TableOptions/TableOptionsColumnDragPreview.js @@ -0,0 +1,78 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { DragLayer } from 'react-dnd'; +import dimensions from 'Styles/Variables/dimensions.js'; +import { TABLE_COLUMN } from 'Helpers/dragTypes'; +import DragPreviewLayer from 'Components/DragPreviewLayer'; +import TableOptionsColumn from './TableOptionsColumn'; +import styles from './TableOptionsColumnDragPreview.css'; + +const formGroupSmallWidth = parseInt(dimensions.formGroupSmallWidth); +const formLabelWidth = parseInt(dimensions.formLabelWidth); +const formLabelRightMarginWidth = parseInt(dimensions.formLabelRightMarginWidth); +const dragHandleWidth = parseInt(dimensions.dragHandleWidth); + +function collectDragLayer(monitor) { + return { + item: monitor.getItem(), + itemType: monitor.getItemType(), + currentOffset: monitor.getSourceClientOffset() + }; +} + +class TableOptionsColumnDragPreview extends Component { + + // + // Render + + render() { + const { + item, + itemType, + currentOffset + } = this.props; + + if (!currentOffset || itemType !== TABLE_COLUMN) { + return null; + } + + // The offset is shifted because the drag handle is on the right edge of the + // list item and the preview is wider than the drag handle. + + const { x, y } = currentOffset; + const handleOffset = formGroupSmallWidth - formLabelWidth - formLabelRightMarginWidth - dragHandleWidth; + const transform = `translate3d(${x - handleOffset}px, ${y}px, 0)`; + + const style = { + position: 'absolute', + WebkitTransform: transform, + msTransform: transform, + transform + }; + + return ( + +
+ +
+
+ ); + } +} + +TableOptionsColumnDragPreview.propTypes = { + item: PropTypes.object, + itemType: PropTypes.string, + currentOffset: PropTypes.shape({ + x: PropTypes.number.isRequired, + y: PropTypes.number.isRequired + }) +}; + +export default DragLayer(collectDragLayer)(TableOptionsColumnDragPreview); diff --git a/frontend/src/Components/Table/TableOptions/TableOptionsColumnDragSource.css b/frontend/src/Components/Table/TableOptions/TableOptionsColumnDragSource.css new file mode 100644 index 000000000..9354a35c0 --- /dev/null +++ b/frontend/src/Components/Table/TableOptions/TableOptionsColumnDragSource.css @@ -0,0 +1,18 @@ +.columnDragSource { + padding: 4px 0; +} + +.columnPlaceholder { + width: 100%; + height: 36px; + border: 1px dotted #aaa; + border-radius: 4px; +} + +.columnPlaceholderBefore { + margin-bottom: 8px; +} + +.columnPlaceholderAfter { + margin-top: 8px; +} diff --git a/frontend/src/Components/Table/TableOptions/TableOptionsColumnDragSource.js b/frontend/src/Components/Table/TableOptions/TableOptionsColumnDragSource.js new file mode 100644 index 000000000..80f03e430 --- /dev/null +++ b/frontend/src/Components/Table/TableOptions/TableOptionsColumnDragSource.js @@ -0,0 +1,164 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { findDOMNode } from 'react-dom'; +import { DragSource, DropTarget } from 'react-dnd'; +import classNames from 'classnames'; +import { TABLE_COLUMN } from 'Helpers/dragTypes'; +import TableOptionsColumn from './TableOptionsColumn'; +import styles from './TableOptionsColumnDragSource.css'; + +const columnDragSource = { + beginDrag(column) { + return column; + }, + + endDrag(props, monitor, component) { + props.onColumnDragEnd(monitor.getItem(), monitor.didDrop()); + } +}; + +const columnDropTarget = { + hover(props, monitor, component) { + const dragIndex = monitor.getItem().index; + const hoverIndex = props.index; + + const hoverBoundingRect = findDOMNode(component).getBoundingClientRect(); + const hoverMiddleY = (hoverBoundingRect.bottom - hoverBoundingRect.top) / 2; + const clientOffset = monitor.getClientOffset(); + const hoverClientY = clientOffset.y - hoverBoundingRect.top; + + if (dragIndex === hoverIndex) { + return; + } + + // When moving up, only trigger if drag position is above 50% and + // when moving down, only trigger if drag position is below 50%. + // If we're moving down the hoverIndex needs to be increased + // by one so it's ordered properly. Otherwise the hoverIndex will work. + + // Dragging downwards + if (dragIndex < hoverIndex && hoverClientY < hoverMiddleY) { + return; + } + + // Dragging upwards + if (dragIndex > hoverIndex && hoverClientY > hoverMiddleY) { + return; + } + + props.onColumnDragMove(dragIndex, hoverIndex); + } +}; + +function collectDragSource(connect, monitor) { + return { + connectDragSource: connect.dragSource(), + isDragging: monitor.isDragging() + }; +} + +function collectDropTarget(connect, monitor) { + return { + connectDropTarget: connect.dropTarget(), + isOver: monitor.isOver() + }; +} + +class TableOptionsColumnDragSource extends Component { + + // + // Render + + render() { + const { + name, + label, + isVisible, + isModifiable, + index, + isDragging, + isDraggingUp, + isDraggingDown, + isOver, + connectDragSource, + connectDropTarget, + onVisibleChange + } = this.props; + + const isBefore = !isDragging && isDraggingUp && isOver; + const isAfter = !isDragging && isDraggingDown && isOver; + + // if (isDragging && !isOver) { + // return null; + // } + + return connectDropTarget( +
+ { + isBefore && +
+ } + + + + { + isAfter && +
+ } +
+ ); + } +} + +TableOptionsColumnDragSource.propTypes = { + name: PropTypes.string.isRequired, + label: PropTypes.string.isRequired, + isVisible: PropTypes.bool.isRequired, + isModifiable: PropTypes.bool.isRequired, + index: PropTypes.number.isRequired, + isDragging: PropTypes.bool, + isDraggingUp: PropTypes.bool, + isDraggingDown: PropTypes.bool, + isOver: PropTypes.bool, + connectDragSource: PropTypes.func, + connectDropTarget: PropTypes.func, + onVisibleChange: PropTypes.func.isRequired, + onColumnDragMove: PropTypes.func.isRequired, + onColumnDragEnd: PropTypes.func.isRequired +}; + +export default DropTarget( + TABLE_COLUMN, + columnDropTarget, + collectDropTarget +)(DragSource( + TABLE_COLUMN, + columnDragSource, + collectDragSource +)(TableOptionsColumnDragSource)); diff --git a/frontend/src/Components/Table/TableOptions/TableOptionsModal.css b/frontend/src/Components/Table/TableOptions/TableOptionsModal.css new file mode 100644 index 000000000..35544f32b --- /dev/null +++ b/frontend/src/Components/Table/TableOptions/TableOptionsModal.css @@ -0,0 +1,5 @@ +.columns { + margin-top: 10px; + width: 100%; + user-select: none; +} diff --git a/frontend/src/Components/Table/TableOptions/TableOptionsModal.js b/frontend/src/Components/Table/TableOptions/TableOptionsModal.js new file mode 100644 index 000000000..53d695e31 --- /dev/null +++ b/frontend/src/Components/Table/TableOptions/TableOptionsModal.js @@ -0,0 +1,242 @@ +import _ from 'lodash'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { DragDropContext } from 'react-dnd'; +import HTML5Backend from 'react-dnd-html5-backend'; +import { inputTypes } from 'Helpers/Props'; +import Button from 'Components/Link/Button'; +import Form from 'Components/Form/Form'; +import FormGroup from 'Components/Form/FormGroup'; +import FormLabel from 'Components/Form/FormLabel'; +import FormInputHelpText from 'Components/Form/FormInputHelpText'; +import FormInputGroup from 'Components/Form/FormInputGroup'; +import Modal from 'Components/Modal/Modal'; +import ModalContent from 'Components/Modal/ModalContent'; +import ModalHeader from 'Components/Modal/ModalHeader'; +import ModalBody from 'Components/Modal/ModalBody'; +import ModalFooter from 'Components/Modal/ModalFooter'; +import TableOptionsColumn from './TableOptionsColumn'; +import TableOptionsColumnDragSource from './TableOptionsColumnDragSource'; +import TableOptionsColumnDragPreview from './TableOptionsColumnDragPreview'; +import styles from './TableOptionsModal.css'; + +class TableOptionsModal extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + hasPageSize: !!props.pageSize, + pageSize: props.pageSize, + pageSizeError: null, + dragIndex: null, + dropIndex: null + }; + } + + componentDidUpdate(prevProps) { + if (prevProps.pageSize !== this.state.pageSize) { + this.setState({ pageSize: this.props.pageSize }); + } + } + + // + // Listeners + + onPageSizeChange = ({ value }) => { + let pageSizeError = null; + + if (value < 5) { + pageSizeError = 'Page size must be at least 5'; + } else if (value > 250) { + pageSizeError = 'Page size must not exceed 250'; + } else { + this.props.onTableOptionChange({ pageSize: value }); + } + + this.setState({ + pageSize: value, + pageSizeError + }); + } + + onVisibleChange = ({ name, value }) => { + const columns = _.cloneDeep(this.props.columns); + + const column = _.find(columns, { name }); + column.isVisible = value; + + this.props.onTableOptionChange({ columns }); + } + + onColumnDragMove = (dragIndex, dropIndex) => { + if (this.state.dragIndex !== dragIndex || this.state.dropIndex !== dropIndex) { + this.setState({ + dragIndex, + dropIndex + }); + } + } + + onColumnDragEnd = ({ id }, didDrop) => { + const { + dragIndex, + dropIndex + } = this.state; + + if (didDrop && dropIndex !== null) { + const columns = _.cloneDeep(this.props.columns); + const items = columns.splice(dragIndex, 1); + columns.splice(dropIndex, 0, items[0]); + + this.props.onTableOptionChange({ columns }); + } + + this.setState({ + dragIndex: null, + dropIndex: null + }); + } + + // + // Render + + render() { + const { + isOpen, + columns, + canModifyColumns, + onModalClose + } = this.props; + + const { + hasPageSize, + pageSize, + pageSizeError, + dragIndex, + dropIndex + } = this.state; + + const isDragging = dropIndex !== null; + const isDraggingUp = isDragging && dropIndex < dragIndex; + const isDraggingDown = isDragging && dropIndex > dragIndex; + + return ( + + + + Table Options + + + +
+ { + hasPageSize && + + Page Size + + + + } + + { + canModifyColumns && + + Columns + +
+ + +
+ { + columns.map((column, index) => { + const { + name, + label, + columnLabel, + isVisible, + isModifiable + } = column; + + if (isModifiable !== false) { + return ( + + ); + } + + return ( + + ); + }) + } + + +
+
+
+ } +
+
+ + + +
+
+ ); + } +} + +TableOptionsModal.propTypes = { + isOpen: PropTypes.bool.isRequired, + columns: PropTypes.arrayOf(PropTypes.object).isRequired, + pageSize: PropTypes.number, + canModifyColumns: PropTypes.bool.isRequired, + onTableOptionChange: PropTypes.func.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +TableOptionsModal.defaultProps = { + canModifyColumns: true +}; + +export default DragDropContext(HTML5Backend)(TableOptionsModal); diff --git a/frontend/src/Components/Table/TablePager.css b/frontend/src/Components/Table/TablePager.css new file mode 100644 index 000000000..e3fb645bd --- /dev/null +++ b/frontend/src/Components/Table/TablePager.css @@ -0,0 +1,70 @@ +.pager { + display: flex; + align-items: center; + justify-content: space-between; +} + +.loadingContainer, +.controlsContainer, +.recordsContainer { + flex: 0 1 33%; +} + +.controlsContainer { + display: flex; + justify-content: center; +} + +.recordsContainer { + display: flex; + justify-content: flex-end; +} + +.loading { + composes: loading from 'Components/Loading/LoadingIndicator.css'; + + margin: 0; + margin-left: 5px; + text-align: left; +} + +.controls { + display: flex; + align-items: center; + text-align: center; +} + +.pageNumber { + line-height: 30px; +} + +.pageLink { + padding: 0; + width: 30px; + height: 30px; + line-height: 30px; +} + +.records { + color: $disabledColor; +} + +.disabledPageButton { + color: $disabledColor; +} + +@media only screen and (max-width: $breakpointSmall) { + .pager { + flex-wrap: wrap; + } + + .loadingContainer, + .recordsContainer { + flex: 0 1 50%; + } + + .controlsContainer { + flex: 0 1 100%; + order: -1; + } +} diff --git a/frontend/src/Components/Table/TablePager.js b/frontend/src/Components/Table/TablePager.js new file mode 100644 index 000000000..e3bc10be7 --- /dev/null +++ b/frontend/src/Components/Table/TablePager.js @@ -0,0 +1,173 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import classNames from 'classnames'; +import { icons } from 'Helpers/Props'; +import Icon from 'Components/Icon'; +import Link from 'Components/Link/Link'; +import LoadingIndicator from 'Components/Loading/LoadingIndicator'; +import SelectInput from 'Components/Form/SelectInput'; +import styles from './TablePager.css'; + +class TablePager extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + isShowingPageSelect: false + }; + } + + // + // Listeners + + onOpenPageSelectClick = () => { + this.setState({ isShowingPageSelect: true }); + } + + onPageSelect = ({ value: page }) => { + this.setState({ isShowingPageSelect: false }); + this.props.onPageSelect(parseInt(page)); + } + + // + // Render + + render() { + const { + page, + totalPages, + totalRecords, + isFetching, + onFirstPagePress, + onPreviousPagePress, + onNextPagePress, + onLastPagePress + } = this.props; + + const isShowingPageSelect = this.state.isShowingPageSelect; + const pages = Array.from(new Array(totalPages), (x, i) => { + const pageNumber = i + 1; + + return { + key: pageNumber, + value: pageNumber + }; + }); + + if (!page) { + return null; + } + + const isFirstPage = page === 1; + const isLastPage = page === totalPages; + + return ( +
+
+ { + isFetching && + + } +
+ +
+
+ + + + + + + + +
+ { + !isShowingPageSelect && + + {page} / {totalPages} + + } + + { + isShowingPageSelect && + + } +
+ + + + + + + + +
+
+ +
+
+ Total records: {totalRecords} +
+
+
+ ); + } + +} + +TablePager.propTypes = { + page: PropTypes.number, + totalPages: PropTypes.number, + totalRecords: PropTypes.number, + isFetching: PropTypes.bool, + onFirstPagePress: PropTypes.func.isRequired, + onPreviousPagePress: PropTypes.func.isRequired, + onNextPagePress: PropTypes.func.isRequired, + onLastPagePress: PropTypes.func.isRequired, + onPageSelect: PropTypes.func.isRequired +}; + +export default TablePager; diff --git a/frontend/src/Components/Table/TableRow.css b/frontend/src/Components/Table/TableRow.css new file mode 100644 index 000000000..9664733b4 --- /dev/null +++ b/frontend/src/Components/Table/TableRow.css @@ -0,0 +1,7 @@ +.row { + transition: background-color 500ms; + + &:hover { + background-color: #fafbfc; + } +} diff --git a/frontend/src/Components/Table/TableRow.js b/frontend/src/Components/Table/TableRow.js new file mode 100644 index 000000000..06bbbaee9 --- /dev/null +++ b/frontend/src/Components/Table/TableRow.js @@ -0,0 +1,31 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import styles from './TableRow.css'; + +function TableRow(props) { + const { + className, + children, + ...otherProps + } = props; + + return ( + + {children} + + ); +} + +TableRow.propTypes = { + className: PropTypes.string.isRequired, + children: PropTypes.node +}; + +TableRow.defaultProps = { + className: styles.row +}; + +export default TableRow; diff --git a/frontend/src/Components/Table/TableRowButton.css b/frontend/src/Components/Table/TableRowButton.css new file mode 100644 index 000000000..70a2238ca --- /dev/null +++ b/frontend/src/Components/Table/TableRowButton.css @@ -0,0 +1,4 @@ +.row { + composes: link from 'Components/Link/Link.css'; + composes: row from './TableRow.css'; +} diff --git a/frontend/src/Components/Table/TableRowButton.js b/frontend/src/Components/Table/TableRowButton.js new file mode 100644 index 000000000..7ff679673 --- /dev/null +++ b/frontend/src/Components/Table/TableRowButton.js @@ -0,0 +1,16 @@ +import React from 'react'; +import Link from 'Components/Link/Link'; +import TableRow from './TableRow'; +import styles from './TableRowButton.css'; + +function TableRowButton(props) { + return ( + + ); +} + +export default TableRowButton; diff --git a/frontend/src/Components/Table/TableSelectAllHeaderCell.css b/frontend/src/Components/Table/TableSelectAllHeaderCell.css new file mode 100644 index 000000000..6090e6e9c --- /dev/null +++ b/frontend/src/Components/Table/TableSelectAllHeaderCell.css @@ -0,0 +1,11 @@ +.selectAllHeaderCell { + composes: headerCell from 'Components/Table/VirtualTableHeaderCell.css'; + + width: 30px; +} + +.input { + composes: input from 'Components/Form/CheckInput.css'; + + margin: 0; +} diff --git a/frontend/src/Components/Table/TableSelectAllHeaderCell.js b/frontend/src/Components/Table/TableSelectAllHeaderCell.js new file mode 100644 index 000000000..c889c32ae --- /dev/null +++ b/frontend/src/Components/Table/TableSelectAllHeaderCell.js @@ -0,0 +1,47 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import CheckInput from 'Components/Form/CheckInput'; +import VirtualTableHeaderCell from './TableHeaderCell'; +import styles from './TableSelectAllHeaderCell.css'; + +function getValue(allSelected, allUnselected) { + if (allSelected) { + return true; + } else if (allUnselected) { + return false; + } + + return null; +} + +function TableSelectAllHeaderCell(props) { + const { + allSelected, + allUnselected, + onSelectAllChange + } = props; + + const value = getValue(allSelected, allUnselected); + + return ( + + + + ); +} + +TableSelectAllHeaderCell.propTypes = { + allSelected: PropTypes.bool.isRequired, + allUnselected: PropTypes.bool.isRequired, + onSelectAllChange: PropTypes.func.isRequired +}; + +export default TableSelectAllHeaderCell; diff --git a/frontend/src/Components/Table/VirtualTable.css b/frontend/src/Components/Table/VirtualTable.css new file mode 100644 index 000000000..3287c5643 --- /dev/null +++ b/frontend/src/Components/Table/VirtualTable.css @@ -0,0 +1,3 @@ +.tableContainer { + width: 100%; +} diff --git a/frontend/src/Components/Table/VirtualTable.js b/frontend/src/Components/Table/VirtualTable.js new file mode 100644 index 000000000..df23d3ff7 --- /dev/null +++ b/frontend/src/Components/Table/VirtualTable.js @@ -0,0 +1,173 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import ReactDOM from 'react-dom'; +import Measure from 'react-measure'; +import { WindowScroller } from 'react-virtualized'; +import { scrollDirections } from 'Helpers/Props'; +import Scroller from 'Components/Scroller/Scroller'; +import VirtualTableBody from './VirtualTableBody'; +import styles from './VirtualTable.css'; + +const ROW_HEIGHT = 38; + +function overscanIndicesGetter(options) { + const { + cellCount, + overscanCellsCount, + startIndex, + stopIndex + } = options; + + // The default getter takes the scroll direction into account, + // but that can cause issues. Ignore the scroll direction and + // always over return more items. + + const overscanStartIndex = startIndex - overscanCellsCount; + const overscanStopIndex = stopIndex + overscanCellsCount; + + return { + overscanStartIndex: Math.max(0, overscanStartIndex), + overscanStopIndex: Math.min(cellCount - 1, overscanStopIndex) + }; +} + +class VirtualTable extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + width: 0 + }; + + this._isInitialized = false; + this._table = null; + } + + componentDidMount() { + this._contentBodyNode = ReactDOM.findDOMNode(this.props.contentBody); + } + + // + // Control + + rowGetter = ({ index }) => { + return this.props.items[index]; + } + + setTableRef = (ref) => { + this._table = ref; + } + + forceUpdateGrid = () => { + this._table.recomputeGridSize(); + } + + scrollToRow = (rowIndex) => { + const scrollTop = (rowIndex + 1) * ROW_HEIGHT + 20; + + // this._table.scrollToCell({ columnIndex: 0, rowIndex }); + this.props.onScroll({ scrollTop }); + } + + // + // Listeners + + onMeasure = ({ width }) => { + this.setState({ + width + }); + } + + onSectionRendered = () => { + if (!this._isInitialized && this._contentBodyNode) { + this.props.onRender(); + this._isInitialized = true; + } + } + + // + // Render + + render() { + const { + className, + items, + isSmallScreen, + header, + headerHeight, + scrollTop, + rowRenderer, + onScroll, + ...otherProps + } = this.props; + + const { + width + } = this.state; + + return ( + + + {({ height, isScrolling }) => { + return ( + + {header} + + + + ); + } + } + + + ); + } +} + +VirtualTable.propTypes = { + className: PropTypes.string.isRequired, + items: PropTypes.arrayOf(PropTypes.object).isRequired, + scrollTop: PropTypes.number.isRequired, + contentBody: PropTypes.object.isRequired, + isSmallScreen: PropTypes.bool.isRequired, + header: PropTypes.node.isRequired, + headerHeight: PropTypes.number.isRequired, + rowRenderer: PropTypes.func.isRequired, + onRender: PropTypes.func.isRequired, + onScroll: PropTypes.func.isRequired +}; + +VirtualTable.defaultProps = { + className: styles.tableContainer, + headerHeight: 38, + onRender: () => {} +}; + +export default VirtualTable; diff --git a/frontend/src/Components/Table/VirtualTableBody.css b/frontend/src/Components/Table/VirtualTableBody.css new file mode 100644 index 000000000..12768646d --- /dev/null +++ b/frontend/src/Components/Table/VirtualTableBody.css @@ -0,0 +1,3 @@ +.tableBodyContainer { + position: relative; +} diff --git a/frontend/src/Components/Table/VirtualTableBody.js b/frontend/src/Components/Table/VirtualTableBody.js new file mode 100644 index 000000000..c73508895 --- /dev/null +++ b/frontend/src/Components/Table/VirtualTableBody.js @@ -0,0 +1,48 @@ +import React from 'react'; +import { Grid } from 'react-virtualized'; +import styles from './VirtualTableBody.css'; + +class VirtualTableBody extends Grid { + + // + // Render + + render() { + const { + autoContainerWidth, + containerStyle + } = this.props; + + const { isScrolling } = this.state; + + const totalColumnsWidth = this._columnSizeAndPositionManager.getTotalSize(); + const totalRowsHeight = this._rowSizeAndPositionManager.getTotalSize(); + const childrenToDisplay = this._childrenToDisplay; + + if (childrenToDisplay.length > 0) { + return ( +
+
+ {childrenToDisplay} +
+
+ ); + } + + return ( +
+ ); + } +} + +export default VirtualTableBody; diff --git a/frontend/src/Components/Table/VirtualTableHeader.css b/frontend/src/Components/Table/VirtualTableHeader.css new file mode 100644 index 000000000..4b757c1f8 --- /dev/null +++ b/frontend/src/Components/Table/VirtualTableHeader.css @@ -0,0 +1,3 @@ +.header { + display: flex; +} diff --git a/frontend/src/Components/Table/VirtualTableHeader.js b/frontend/src/Components/Table/VirtualTableHeader.js new file mode 100644 index 000000000..cf6a0f47b --- /dev/null +++ b/frontend/src/Components/Table/VirtualTableHeader.js @@ -0,0 +1,17 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import styles from './VirtualTableHeader.css'; + +function VirtualTableHeader({ children }) { + return ( +
+ {children} +
+ ); +} + +VirtualTableHeader.propTypes = { + children: PropTypes.node +}; + +export default VirtualTableHeader; diff --git a/frontend/src/Components/Table/VirtualTableHeaderCell.css b/frontend/src/Components/Table/VirtualTableHeaderCell.css new file mode 100644 index 000000000..c2c4f58c8 --- /dev/null +++ b/frontend/src/Components/Table/VirtualTableHeaderCell.css @@ -0,0 +1,16 @@ +.headerCell { + padding: 8px; + border: none !important; + text-align: left; + font-weight: bold; +} + +.sortIcon { + margin-left: 10px; +} + +@media only screen and (max-width: $breakpointSmall) { + .headerCell { + white-space: nowrap; + } +} diff --git a/frontend/src/Components/Table/VirtualTableHeaderCell.js b/frontend/src/Components/Table/VirtualTableHeaderCell.js new file mode 100644 index 000000000..bf51062e9 --- /dev/null +++ b/frontend/src/Components/Table/VirtualTableHeaderCell.js @@ -0,0 +1,107 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { icons, sortDirections } from 'Helpers/Props'; +import Link from 'Components/Link/Link'; +import Icon from 'Components/Icon'; +import styles from './VirtualTableHeaderCell.css'; + +export function headerRenderer(headerProps) { + const { + columnData = {}, + dataKey, + label + } = headerProps; + + return ( + + {label} + + ); +} + +class VirtualTableHeaderCell extends Component { + + // + // Listeners + + onPress = () => { + const { + name, + fixedSortDirection + } = this.props; + + if (fixedSortDirection) { + this.props.onSortPress(name, fixedSortDirection); + } else { + this.props.onSortPress(name); + } + } + + // + // Render + + render() { + const { + className, + name, + isSortable, + sortKey, + sortDirection, + fixedSortDirection, + children, + onSortPress, + ...otherProps + } = this.props; + + const isSorting = isSortable && sortKey === name; + const sortIcon = sortDirection === sortDirections.ASCENDING ? + icons.SORT_ASCENDING : + icons.SORT_DESCENDING; + + return ( + isSortable ? + + {children} + + { + isSorting && + + } + : + +
+ {children} +
+ ); + } +} + +VirtualTableHeaderCell.propTypes = { + className: PropTypes.string, + name: PropTypes.string.isRequired, + label: PropTypes.oneOfType([PropTypes.string, PropTypes.object]), + isSortable: PropTypes.bool, + sortKey: PropTypes.string, + fixedSortDirection: PropTypes.string, + sortDirection: PropTypes.string, + children: PropTypes.node, + onSortPress: PropTypes.func +}; + +VirtualTableHeaderCell.defaultProps = { + className: styles.headerCell, + isSortable: false +}; + +export default VirtualTableHeaderCell; diff --git a/frontend/src/Components/Table/VirtualTableRow.css b/frontend/src/Components/Table/VirtualTableRow.css new file mode 100644 index 000000000..f4c825b64 --- /dev/null +++ b/frontend/src/Components/Table/VirtualTableRow.css @@ -0,0 +1,14 @@ +.row { + display: flex; + transition: background-color 500ms; + + &:hover { + background-color: #fafbfc; + } +} + +@media only screen and (max-width: $breakpointMedium) { + .row { + overflow-x: visible !important; + } +} diff --git a/frontend/src/Components/Table/VirtualTableRow.js b/frontend/src/Components/Table/VirtualTableRow.js new file mode 100644 index 000000000..0a423902e --- /dev/null +++ b/frontend/src/Components/Table/VirtualTableRow.js @@ -0,0 +1,34 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import styles from './VirtualTableRow.css'; + +function VirtualTableRow(props) { + const { + className, + children, + style, + ...otherProps + } = props; + + return ( +
+ {children} +
+ ); +} + +VirtualTableRow.propTypes = { + className: PropTypes.string.isRequired, + style: PropTypes.object.isRequired, + children: PropTypes.node +}; + +VirtualTableRow.defaultProps = { + className: styles.row +}; + +export default VirtualTableRow; diff --git a/frontend/src/Components/Table/VirtualTableSelectAllHeaderCell.css b/frontend/src/Components/Table/VirtualTableSelectAllHeaderCell.css new file mode 100644 index 000000000..1f3f7fb30 --- /dev/null +++ b/frontend/src/Components/Table/VirtualTableSelectAllHeaderCell.css @@ -0,0 +1,11 @@ +.selectAllHeaderCell { + composes: headerCell from 'Components/Table/TableHeaderCell.css'; + + flex: 0 0 36px; +} + +.input { + composes: input from 'Components/Form/CheckInput.css'; + + margin: 0; +} diff --git a/frontend/src/Components/Table/VirtualTableSelectAllHeaderCell.js b/frontend/src/Components/Table/VirtualTableSelectAllHeaderCell.js new file mode 100644 index 000000000..58b246763 --- /dev/null +++ b/frontend/src/Components/Table/VirtualTableSelectAllHeaderCell.js @@ -0,0 +1,47 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import CheckInput from 'Components/Form/CheckInput'; +import VirtualTableHeaderCell from './VirtualTableHeaderCell'; +import styles from './VirtualTableSelectAllHeaderCell.css'; + +function getValue(allSelected, allUnselected) { + if (allSelected) { + return true; + } else if (allUnselected) { + return false; + } + + return null; +} + +function VirtualTableSelectAllHeaderCell(props) { + const { + allSelected, + allUnselected, + onSelectAllChange + } = props; + + const value = getValue(allSelected, allUnselected); + + return ( + + + + ); +} + +VirtualTableSelectAllHeaderCell.propTypes = { + allSelected: PropTypes.bool.isRequired, + allUnselected: PropTypes.bool.isRequired, + onSelectAllChange: PropTypes.func.isRequired +}; + +export default VirtualTableSelectAllHeaderCell; diff --git a/frontend/src/Components/TagList.css b/frontend/src/Components/TagList.css new file mode 100644 index 000000000..c1e5567bd --- /dev/null +++ b/frontend/src/Components/TagList.css @@ -0,0 +1,3 @@ +.tags { + flex: 1 0 auto; +} diff --git a/frontend/src/Components/TagList.js b/frontend/src/Components/TagList.js new file mode 100644 index 000000000..485651bdc --- /dev/null +++ b/frontend/src/Components/TagList.js @@ -0,0 +1,38 @@ +import _ from 'lodash'; +import PropTypes from 'prop-types'; +import React from 'react'; +import { kinds } from 'Helpers/Props'; +import Label from './Label'; +import styles from './TagList.css'; + +function TagList({ tags, tagList }) { + return ( +
+ { + tags.map((t) => { + const tag = _.find(tagList, { id: t }); + + if (!tag) { + return null; + } + + return ( + + ); + }) + } +
+ ); +} + +TagList.propTypes = { + tags: PropTypes.arrayOf(PropTypes.number).isRequired, + tagList: PropTypes.arrayOf(PropTypes.object).isRequired +}; + +export default TagList; diff --git a/frontend/src/Components/TagListConnector.js b/frontend/src/Components/TagListConnector.js new file mode 100644 index 000000000..be7e618e3 --- /dev/null +++ b/frontend/src/Components/TagListConnector.js @@ -0,0 +1,17 @@ +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import createTagsSelector from 'Store/Selectors/createTagsSelector'; +import TagList from './TagList'; + +function createMapStateToProps() { + return createSelector( + createTagsSelector(), + (tagList) => { + return { + tagList + }; + } + ); +} + +export default connect(createMapStateToProps)(TagList); diff --git a/frontend/src/Components/Tooltip/Popover.css b/frontend/src/Components/Tooltip/Popover.css new file mode 100644 index 000000000..cb742eca0 --- /dev/null +++ b/frontend/src/Components/Tooltip/Popover.css @@ -0,0 +1,104 @@ +.tether { + z-index: 2000; +} + +.popoverContainer { + margin: 10px 15px; +} + +.popover { + position: relative; + background-color: $white; + box-shadow: 0 5px 10px $popoverShadowColor; +} + +.arrow, +.arrow::after { + position: absolute; + display: block; + width: 0; + height: 0; + border-width: 11px; + border-style: solid; + border-color: transparent; +} + +.arrow::after { + border-width: 10px; + content: ''; +} + +.top { + bottom: -11px; + left: 50%; + margin-left: -11px; + border-top-color: $popoverArrowBorderColor; + border-bottom-width: 0; + + &::after { + bottom: 1px; + margin-left: -10px; + border-top-color: $white; + border-bottom-width: 0; + content: ' '; + } +} + +.right { + top: 50%; + left: -11px; + margin-top: -11px; + border-right-color: $popoverArrowBorderColor; + border-left-width: 0; + + &::after { + bottom: -10px; + left: 1px; + border-right-color: $white; + border-left-width: 0; + content: ' '; + } +} + +.bottom { + top: -11px; + left: 50%; + margin-left: -11px; + border-top-width: 0; + border-bottom-color: $popoverArrowBorderColor; + + &::after { + top: 1px; + margin-left: -10px; + border-top-width: 0; + border-bottom-color: $white; + content: ' '; + } +} + +.left { + top: 50%; + right: -11px; + margin-top: -11px; + border-right-width: 0; + border-left-color: $popoverArrowBorderColor; + + &::after { + right: 1px; + bottom: -10px; + border-right-width: 0; + border-left-color: $white; + content: ' '; + } +} + +.title { + padding: 10px 20px; + border-bottom: 1px solid $popoverTitleBorderColor; + background-color: $popoverTitleBackgroundColor; + font-size: 16px; +} + +.body { + padding: 20px; +} diff --git a/frontend/src/Components/Tooltip/Popover.js b/frontend/src/Components/Tooltip/Popover.js new file mode 100644 index 000000000..0cb2520e5 --- /dev/null +++ b/frontend/src/Components/Tooltip/Popover.js @@ -0,0 +1,136 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import TetherComponent from 'react-tether'; +import classNames from 'classnames'; +import { tooltipPositions } from 'Helpers/Props'; +import styles from './Popover.css'; + +const baseTetherOptions = { + skipMoveElement: true, + constraints: [ + { + to: 'window', + attachment: 'together', + pin: true + } + ] +}; + +const tetherOptions = { + [tooltipPositions.TOP]: { + ...baseTetherOptions, + attachment: 'bottom center', + targetAttachment: 'top center' + }, + + [tooltipPositions.RIGHT]: { + ...baseTetherOptions, + attachment: 'middle left', + targetAttachment: 'middle right' + }, + + [tooltipPositions.BOTTOM]: { + ...baseTetherOptions, + attachment: 'top center', + targetAttachment: 'bottom center' + }, + + [tooltipPositions.LEFT]: { + ...baseTetherOptions, + attachment: 'middle right', + targetAttachment: 'middle left' + } +}; + +class Popover extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + isOpen: false + }; + } + + // + // Listeners + + onClick = () => { + this.setState({ isOpen: !this.state.isOpen }); + } + + onMouseEnter = () => { + this.setState({ isOpen: true }); + } + + onMouseLeave = () => { + this.setState({ isOpen: false }); + } + + // + // Render + + render() { + const { + anchor, + title, + body, + position + } = this.props; + + return ( + + + {anchor} + + + { + this.state.isOpen && +
+
+
+ +
+ {title} +
+ +
+ {body} +
+
+
+ } + + ); + } +} + +Popover.propTypes = { + anchor: PropTypes.node.isRequired, + title: PropTypes.string.isRequired, + body: PropTypes.oneOfType([PropTypes.string, PropTypes.node]).isRequired, + position: PropTypes.oneOf(tooltipPositions.all) +}; + +Popover.defaultProps = { + position: tooltipPositions.TOP +}; + +export default Popover; diff --git a/frontend/src/Components/Tooltip/Tooltip.css b/frontend/src/Components/Tooltip/Tooltip.css new file mode 100644 index 000000000..d1d798e0f --- /dev/null +++ b/frontend/src/Components/Tooltip/Tooltip.css @@ -0,0 +1,161 @@ +.tether { + z-index: 2000; +} + +.tooltipContainer { + margin: 10px 15px; +} + +.tooltip { + position: relative; + + &.default { + background-color: $white; + box-shadow: 0 5px 10px $popoverShadowColor; + } + + &.inverse { + background-color: $themeDarkColor; + box-shadow: 0 5px 10px $popoverShadowInverseColor; + } +} + +.arrow, +.arrow::after { + position: absolute; + display: block; + width: 0; + height: 0; + border-width: 11px; + border-style: solid; + border-color: transparent; +} + +.arrow::after { + border-width: 10px; + content: ''; +} + +.top { + bottom: -11px; + left: 50%; + margin-left: -11px; + border-bottom-width: 0; + + &::after { + bottom: 1px; + margin-left: -10px; + border-bottom-width: 0; + content: ' '; + + &.default { + border-top-color: $popoverArrowBorderColor; + } + + &.inverse { + border-top-color: $popoverArrowBorderInverseColor; + } + } + + &.default { + border-top-color: $popoverArrowBorderColor; + } + + &.inverse { + border-top-color: $popoverArrowBorderInverseColor; + } +} + +.right { + top: 50%; + left: -11px; + margin-top: -11px; + border-left-width: 0; + + &::after { + bottom: -10px; + left: 1px; + border-left-width: 0; + content: ' '; + + &.default { + border-right-color: $popoverArrowBorderColor; + } + + &.inverse { + border-right-color: $popoverArrowBorderInverseColor; + } + } + + &.default { + border-right-color: $popoverArrowBorderColor; + } + + &.inverse { + border-right-color: $popoverArrowBorderInverseColor; + } +} + +.bottom { + top: -11px; + left: 50%; + margin-left: -11px; + border-top-width: 0; + + &::after { + top: 1px; + margin-left: -10px; + border-top-width: 0; + content: ' '; + + &.default { + border-bottom-color: $popoverArrowBorderColor; + } + + &.inverse { + border-bottom-color: $popoverArrowBorderInverseColor; + } + } + + &.default { + border-bottom-color: $popoverArrowBorderColor; + } + + &.inverse { + border-bottom-color: $popoverArrowBorderInverseColor; + } +} + +.left { + top: 50%; + right: -11px; + margin-top: -11px; + border-right-width: 0; + + &::after { + right: 1px; + bottom: -10px; + border-right-width: 0; + content: ' '; + + &.default { + border-left-color: $popoverArrowBorderColor; + } + + &.inverse { + border-left-color: $popoverArrowBorderInverseColor; + } + } + + &.default { + border-left-color: $popoverArrowBorderColor; + } + + &.inverse { + border-left-color: $popoverArrowBorderInverseColor; + } +} + +.body { + padding: 5px; +} diff --git a/frontend/src/Components/Tooltip/Tooltip.js b/frontend/src/Components/Tooltip/Tooltip.js new file mode 100644 index 000000000..5b32c5783 --- /dev/null +++ b/frontend/src/Components/Tooltip/Tooltip.js @@ -0,0 +1,151 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import TetherComponent from 'react-tether'; +import classNames from 'classnames'; +import { kinds, tooltipPositions } from 'Helpers/Props'; +import styles from './Tooltip.css'; + +const baseTetherOptions = { + skipMoveElement: true, + constraints: [ + { + to: 'window', + attachment: 'together', + pin: true + } + ] +}; + +const tetherOptions = { + [tooltipPositions.TOP]: { + ...baseTetherOptions, + attachment: 'bottom center', + targetAttachment: 'top center' + }, + + [tooltipPositions.RIGHT]: { + ...baseTetherOptions, + attachment: 'middle left', + targetAttachment: 'middle right' + }, + + [tooltipPositions.BOTTOM]: { + ...baseTetherOptions, + attachment: 'top center', + targetAttachment: 'bottom center' + }, + + [tooltipPositions.LEFT]: { + ...baseTetherOptions, + attachment: 'middle right', + targetAttachment: 'middle left' + } +}; + +class Tooltip extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this._closeTimeout = null; + + this.state = { + isOpen: false + }; + } + + // + // Listeners + + onClick = () => { + this.setState({ isOpen: !this.state.isOpen }); + } + + onMouseEnter = () => { + if (this._closeTimeout) { + clearTimeout(this._closeTimeout); + } + + this.setState({ isOpen: true }); + } + + onMouseLeave = () => { + this._closeTimeout = setTimeout(() => { + this.setState({ isOpen: false }); + }, 100); + } + + // + // Render + + render() { + const { + anchor, + tooltip, + kind, + position + } = this.props; + + return ( + + + {anchor} + + + { + this.state.isOpen && +
+
+
+ +
+ {tooltip} +
+
+
+ } + + ); + } +} + +Tooltip.propTypes = { + anchor: PropTypes.node.isRequired, + tooltip: PropTypes.oneOfType([PropTypes.string, PropTypes.node]).isRequired, + kind: PropTypes.oneOf([kinds.DEFAULT, kinds.INVERSE]), + position: PropTypes.oneOf(tooltipPositions.all) +}; + +Tooltip.defaultProps = { + kind: kinds.DEFAULT, + position: tooltipPositions.TOP +}; + +export default Tooltip; diff --git a/frontend/src/Components/keyboardShortcuts.js b/frontend/src/Components/keyboardShortcuts.js new file mode 100644 index 000000000..f9ef0c9e3 --- /dev/null +++ b/frontend/src/Components/keyboardShortcuts.js @@ -0,0 +1,100 @@ +import React, { Component } from 'react'; +import Mousetrap from 'mousetrap'; +import getDisplayName from 'Helpers/getDisplayName'; + +export const shortcuts = { + OPEN_KEYBOARD_SHORTCUTS_MODAL: { + key: '?', + name: 'Open This Modal' + }, + + ARTIST_SEARCH_INPUT: { + key: 's', + name: 'Focus Search Box' + }, + + SAVE_SETTINGS: { + key: 'mod+s', + name: 'Save Settings' + } +}; + +function keyboardShortcuts(WrappedComponent) { + class KeyboardShortcuts extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + this._mousetrapBindings = {}; + this._mousetrap = new Mousetrap(); + this._mousetrap.stopCallback = this.stopCallback; + } + + componentWillUnmount() { + this.unbindAllShortcuts(); + this._mousetrap = null; + } + + // + // Control + + bindShortcut = (key, callback, options = {}) => { + this._mousetrap.bind(key, callback); + this._mousetrapBindings[key] = options; + } + + unbindShortcut = (key) => { + delete this._mousetrapBindings[key]; + this._mousetrap.unbind(key); + } + + unbindAllShortcuts = () => { + const keys = Object.keys(this._mousetrapBindings); + + if (!keys.length) { + return; + } + + keys.forEach((binding) => { + this._mousetrap.unbind(binding); + }); + + this._mousetrapBindings = {}; + } + + stopCallback = (event, element, combo) => { + if (this._mousetrapBindings[combo].isGlobal) { + return false; + } + + return ( + element.tagName === 'INPUT' || + element.tagName === 'SELECT' || + element.tagName === 'TEXTAREA' || + (element.contentEditable && element.contentEditable === 'true') + ); + } + + // + // Render + + render() { + return ( + + ); + } + } + + KeyboardShortcuts.displayName = `KeyboardShortcut(${getDisplayName(WrappedComponent)})`; + KeyboardShortcuts.WrappedComponent = WrappedComponent; + + return KeyboardShortcuts; +} + +export default keyboardShortcuts; diff --git a/frontend/src/Components/withScrollPosition.js b/frontend/src/Components/withScrollPosition.js new file mode 100644 index 000000000..110da9ab2 --- /dev/null +++ b/frontend/src/Components/withScrollPosition.js @@ -0,0 +1,30 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import scrollPositions from 'Store/scrollPositions'; + +function withScrollPosition(WrappedComponent, scrollPositionKey) { + function ScrollPosition(props) { + const { + history + } = props; + + const scrollTop = history.action === 'POP' ? + scrollPositions[scrollPositionKey] : + 0; + + return ( + + ); + } + + ScrollPosition.propTypes = { + history: PropTypes.object.isRequired + }; + + return ScrollPosition; +} + +export default withScrollPosition; diff --git a/frontend/src/Content/Fonts/FontAwesome.otf b/frontend/src/Content/Fonts/FontAwesome.otf new file mode 100644 index 000000000..401ec0f36 Binary files /dev/null and b/frontend/src/Content/Fonts/FontAwesome.otf differ diff --git a/frontend/src/Content/Fonts/Roboto-Light.ttf b/frontend/src/Content/Fonts/Roboto-Light.ttf new file mode 100644 index 000000000..94c6bcc67 Binary files /dev/null and b/frontend/src/Content/Fonts/Roboto-Light.ttf differ diff --git a/frontend/src/Content/Fonts/Roboto-Light.woff b/frontend/src/Content/Fonts/Roboto-Light.woff new file mode 100644 index 000000000..ec6bf5749 Binary files /dev/null and b/frontend/src/Content/Fonts/Roboto-Light.woff differ diff --git a/frontend/src/Content/Fonts/Roboto-Light.woff2 b/frontend/src/Content/Fonts/Roboto-Light.woff2 new file mode 100644 index 000000000..288201788 Binary files /dev/null and b/frontend/src/Content/Fonts/Roboto-Light.woff2 differ diff --git a/frontend/src/Content/Fonts/Roboto-Regular.ttf b/frontend/src/Content/Fonts/Roboto-Regular.ttf new file mode 100644 index 000000000..8c082c8de Binary files /dev/null and b/frontend/src/Content/Fonts/Roboto-Regular.ttf differ diff --git a/frontend/src/Content/Fonts/Roboto-Regular.woff b/frontend/src/Content/Fonts/Roboto-Regular.woff new file mode 100644 index 000000000..464d20623 Binary files /dev/null and b/frontend/src/Content/Fonts/Roboto-Regular.woff differ diff --git a/frontend/src/Content/Fonts/Roboto-Regular.woff2 b/frontend/src/Content/Fonts/Roboto-Regular.woff2 new file mode 100644 index 000000000..f96619675 Binary files /dev/null and b/frontend/src/Content/Fonts/Roboto-Regular.woff2 differ diff --git a/src/UI/Content/fonts/ubuntumono-regular.eot b/frontend/src/Content/Fonts/UbuntuMono-Regular.eot similarity index 100% rename from src/UI/Content/fonts/ubuntumono-regular.eot rename to frontend/src/Content/Fonts/UbuntuMono-Regular.eot diff --git a/src/UI/Content/fonts/UbuntuMono-Regular.ttf b/frontend/src/Content/Fonts/UbuntuMono-Regular.ttf similarity index 100% rename from src/UI/Content/fonts/UbuntuMono-Regular.ttf rename to frontend/src/Content/Fonts/UbuntuMono-Regular.ttf diff --git a/src/UI/Content/fonts/ubuntumono-regular.woff b/frontend/src/Content/Fonts/UbuntuMono-Regular.woff similarity index 100% rename from src/UI/Content/fonts/ubuntumono-regular.woff rename to frontend/src/Content/Fonts/UbuntuMono-Regular.woff diff --git a/frontend/src/Content/Fonts/font-awesome.css b/frontend/src/Content/Fonts/font-awesome.css new file mode 100644 index 000000000..2d70abd3c --- /dev/null +++ b/frontend/src/Content/Fonts/font-awesome.css @@ -0,0 +1,2341 @@ +/* stylelint-disable */ + +/*! + * Font Awesome 4.7.0 by @davegandy - http://fontawesome.io - @fontawesome + * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) + */ +/* FONT PATH + * -------------------------- */ +@font-face { + font-family: 'FontAwesome'; + src: url('fontawesome-webfont.eot?v=4.7.0'); + src: url('fontawesome-webfont.eot?#iefix&v=4.7.0') format('embedded-opentype'), url('fontawesome-webfont.woff2?v=4.7.0') format('woff2'), url('fontawesome-webfont.woff?v=4.7.0') format('woff'), url('fontawesome-webfont.ttf?v=4.7.0') format('truetype'), url('fontawesome-webfont.svg?v=4.7.0#fontawesomeregular') format('svg'); + font-weight: normal; + font-style: normal; +} +.fa { + display: inline-block; + font: normal normal normal 14px/1 FontAwesome; + font-size: inherit; + text-rendering: auto; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} +/* makes the font 33% larger relative to the icon container */ +.fa-lg { + font-size: 1.33333333em; + line-height: 0.75em; + vertical-align: -15%; +} +.fa-2x { + font-size: 2em; +} +.fa-3x { + font-size: 3em; +} +.fa-4x { + font-size: 4em; +} +.fa-5x { + font-size: 5em; +} +.fa-fw { + width: 1.28571429em; + text-align: center; +} +.fa-ul { + padding-left: 0; + margin-left: 2.14285714em; + list-style-type: none; +} +.fa-ul > li { + position: relative; +} +.fa-li { + position: absolute; + left: -2.14285714em; + width: 2.14285714em; + top: 0.14285714em; + text-align: center; +} +.fa-li.fa-lg { + left: -1.85714286em; +} +.fa-border { + padding: .2em .25em .15em; + border: solid 0.08em #eeeeee; + border-radius: .1em; +} +.fa-pull-left { + float: left; +} +.fa-pull-right { + float: right; +} +.fa.fa-pull-left { + margin-right: .3em; +} +.fa.fa-pull-right { + margin-left: .3em; +} +/* Deprecated as of 4.4.0 */ +.pull-right { + float: right; +} +.pull-left { + float: left; +} +.fa.pull-left { + margin-right: .3em; +} +.fa.pull-right { + margin-left: .3em; +} +.fa-spin { + -webkit-animation: fa-spin 2s infinite linear; + animation: fa-spin 2s infinite linear; +} +.fa-pulse { + -webkit-animation: fa-spin 1s infinite steps(8); + animation: fa-spin 1s infinite steps(8); +} +@-webkit-keyframes fa-spin { + 0% { + -webkit-transform: rotate(0deg); + transform: rotate(0deg); + } + 100% { + -webkit-transform: rotate(359deg); + transform: rotate(359deg); + } +} +@keyframes fa-spin { + 0% { + -webkit-transform: rotate(0deg); + transform: rotate(0deg); + } + 100% { + -webkit-transform: rotate(359deg); + transform: rotate(359deg); + } +} +.fa-rotate-90 { + -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=1)"; + -webkit-transform: rotate(90deg); + -ms-transform: rotate(90deg); + transform: rotate(90deg); +} +.fa-rotate-180 { + -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=2)"; + -webkit-transform: rotate(180deg); + -ms-transform: rotate(180deg); + transform: rotate(180deg); +} +.fa-rotate-270 { + -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=3)"; + -webkit-transform: rotate(270deg); + -ms-transform: rotate(270deg); + transform: rotate(270deg); +} +.fa-flip-horizontal { + -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1)"; + -webkit-transform: scale(-1, 1); + -ms-transform: scale(-1, 1); + transform: scale(-1, 1); +} +.fa-flip-vertical { + -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)"; + -webkit-transform: scale(1, -1); + -ms-transform: scale(1, -1); + transform: scale(1, -1); +} +:root .fa-rotate-90, +:root .fa-rotate-180, +:root .fa-rotate-270, +:root .fa-flip-horizontal, +:root .fa-flip-vertical { + filter: none; +} +.fa-stack { + position: relative; + display: inline-block; + width: 2em; + height: 2em; + line-height: 2em; + vertical-align: middle; +} +.fa-stack-1x, +.fa-stack-2x { + position: absolute; + left: 0; + width: 100%; + text-align: center; +} +.fa-stack-1x { + line-height: inherit; +} +.fa-stack-2x { + font-size: 2em; +} +.fa-inverse { + color: #ffffff; +} +/* Font Awesome uses the Unicode Private Use Area (PUA) to ensure screen + readers do not read off random characters that represent icons */ +.fa-glass:before { + content: "\f000"; +} +.fa-music:before { + content: "\f001"; +} +.fa-search:before { + content: "\f002"; +} +.fa-envelope-o:before { + content: "\f003"; +} +.fa-heart:before { + content: "\f004"; +} +.fa-star:before { + content: "\f005"; +} +.fa-star-o:before { + content: "\f006"; +} +.fa-user:before { + content: "\f007"; +} +.fa-film:before { + content: "\f008"; +} +.fa-th-large:before { + content: "\f009"; +} +.fa-th:before { + content: "\f00a"; +} +.fa-th-list:before { + content: "\f00b"; +} +.fa-check:before { + content: "\f00c"; +} +.fa-remove:before, +.fa-close:before, +.fa-times:before { + content: "\f00d"; +} +.fa-search-plus:before { + content: "\f00e"; +} +.fa-search-minus:before { + content: "\f010"; +} +.fa-power-off:before { + content: "\f011"; +} +.fa-signal:before { + content: "\f012"; +} +.fa-gear:before, +.fa-cog:before { + content: "\f013"; +} +.fa-trash-o:before { + content: "\f014"; +} +.fa-home:before { + content: "\f015"; +} +.fa-file-o:before { + content: "\f016"; +} +.fa-clock-o:before { + content: "\f017"; +} +.fa-road:before { + content: "\f018"; +} +.fa-download:before { + content: "\f019"; +} +.fa-arrow-circle-o-down:before { + content: "\f01a"; +} +.fa-arrow-circle-o-up:before { + content: "\f01b"; +} +.fa-inbox:before { + content: "\f01c"; +} +.fa-play-circle-o:before { + content: "\f01d"; +} +.fa-rotate-right:before, +.fa-repeat:before { + content: "\f01e"; +} +.fa-refresh:before { + content: "\f021"; +} +.fa-list-alt:before { + content: "\f022"; +} +.fa-lock:before { + content: "\f023"; +} +.fa-flag:before { + content: "\f024"; +} +.fa-headphones:before { + content: "\f025"; +} +.fa-volume-off:before { + content: "\f026"; +} +.fa-volume-down:before { + content: "\f027"; +} +.fa-volume-up:before { + content: "\f028"; +} +.fa-qrcode:before { + content: "\f029"; +} +.fa-barcode:before { + content: "\f02a"; +} +.fa-tag:before { + content: "\f02b"; +} +.fa-tags:before { + content: "\f02c"; +} +.fa-book:before { + content: "\f02d"; +} +.fa-bookmark:before { + content: "\f02e"; +} +.fa-print:before { + content: "\f02f"; +} +.fa-camera:before { + content: "\f030"; +} +.fa-font:before { + content: "\f031"; +} +.fa-bold:before { + content: "\f032"; +} +.fa-italic:before { + content: "\f033"; +} +.fa-text-height:before { + content: "\f034"; +} +.fa-text-width:before { + content: "\f035"; +} +.fa-align-left:before { + content: "\f036"; +} +.fa-align-center:before { + content: "\f037"; +} +.fa-align-right:before { + content: "\f038"; +} +.fa-align-justify:before { + content: "\f039"; +} +.fa-list:before { + content: "\f03a"; +} +.fa-dedent:before, +.fa-outdent:before { + content: "\f03b"; +} +.fa-indent:before { + content: "\f03c"; +} +.fa-video-camera:before { + content: "\f03d"; +} +.fa-photo:before, +.fa-image:before, +.fa-picture-o:before { + content: "\f03e"; +} +.fa-pencil:before { + content: "\f040"; +} +.fa-map-marker:before { + content: "\f041"; +} +.fa-adjust:before { + content: "\f042"; +} +.fa-tint:before { + content: "\f043"; +} +.fa-edit:before, +.fa-pencil-square-o:before { + content: "\f044"; +} +.fa-share-square-o:before { + content: "\f045"; +} +.fa-check-square-o:before { + content: "\f046"; +} +.fa-arrows:before { + content: "\f047"; +} +.fa-step-backward:before { + content: "\f048"; +} +.fa-fast-backward:before { + content: "\f049"; +} +.fa-backward:before { + content: "\f04a"; +} +.fa-play:before { + content: "\f04b"; +} +.fa-pause:before { + content: "\f04c"; +} +.fa-stop:before { + content: "\f04d"; +} +.fa-forward:before { + content: "\f04e"; +} +.fa-fast-forward:before { + content: "\f050"; +} +.fa-step-forward:before { + content: "\f051"; +} +.fa-eject:before { + content: "\f052"; +} +.fa-chevron-left:before { + content: "\f053"; +} +.fa-chevron-right:before { + content: "\f054"; +} +.fa-plus-circle:before { + content: "\f055"; +} +.fa-minus-circle:before { + content: "\f056"; +} +.fa-times-circle:before { + content: "\f057"; +} +.fa-check-circle:before { + content: "\f058"; +} +.fa-question-circle:before { + content: "\f059"; +} +.fa-info-circle:before { + content: "\f05a"; +} +.fa-crosshairs:before { + content: "\f05b"; +} +.fa-times-circle-o:before { + content: "\f05c"; +} +.fa-check-circle-o:before { + content: "\f05d"; +} +.fa-ban:before { + content: "\f05e"; +} +.fa-arrow-left:before { + content: "\f060"; +} +.fa-arrow-right:before { + content: "\f061"; +} +.fa-arrow-up:before { + content: "\f062"; +} +.fa-arrow-down:before { + content: "\f063"; +} +.fa-mail-forward:before, +.fa-share:before { + content: "\f064"; +} +.fa-expand:before { + content: "\f065"; +} +.fa-compress:before { + content: "\f066"; +} +.fa-plus:before { + content: "\f067"; +} +.fa-minus:before { + content: "\f068"; +} +.fa-asterisk:before { + content: "\f069"; +} +.fa-exclamation-circle:before { + content: "\f06a"; +} +.fa-gift:before { + content: "\f06b"; +} +.fa-leaf:before { + content: "\f06c"; +} +.fa-fire:before { + content: "\f06d"; +} +.fa-eye:before { + content: "\f06e"; +} +.fa-eye-slash:before { + content: "\f070"; +} +.fa-warning:before, +.fa-exclamation-triangle:before { + content: "\f071"; +} +.fa-plane:before { + content: "\f072"; +} +.fa-calendar:before { + content: "\f073"; +} +.fa-random:before { + content: "\f074"; +} +.fa-comment:before { + content: "\f075"; +} +.fa-magnet:before { + content: "\f076"; +} +.fa-chevron-up:before { + content: "\f077"; +} +.fa-chevron-down:before { + content: "\f078"; +} +.fa-retweet:before { + content: "\f079"; +} +.fa-shopping-cart:before { + content: "\f07a"; +} +.fa-folder:before { + content: "\f07b"; +} +.fa-folder-open:before { + content: "\f07c"; +} +.fa-arrows-v:before { + content: "\f07d"; +} +.fa-arrows-h:before { + content: "\f07e"; +} +.fa-bar-chart-o:before, +.fa-bar-chart:before { + content: "\f080"; +} +.fa-twitter-square:before { + content: "\f081"; +} +.fa-facebook-square:before { + content: "\f082"; +} +.fa-camera-retro:before { + content: "\f083"; +} +.fa-key:before { + content: "\f084"; +} +.fa-gears:before, +.fa-cogs:before { + content: "\f085"; +} +.fa-comments:before { + content: "\f086"; +} +.fa-thumbs-o-up:before { + content: "\f087"; +} +.fa-thumbs-o-down:before { + content: "\f088"; +} +.fa-star-half:before { + content: "\f089"; +} +.fa-heart-o:before { + content: "\f08a"; +} +.fa-sign-out:before { + content: "\f08b"; +} +.fa-linkedin-square:before { + content: "\f08c"; +} +.fa-thumb-tack:before { + content: "\f08d"; +} +.fa-external-link:before { + content: "\f08e"; +} +.fa-sign-in:before { + content: "\f090"; +} +.fa-trophy:before { + content: "\f091"; +} +.fa-github-square:before { + content: "\f092"; +} +.fa-upload:before { + content: "\f093"; +} +.fa-lemon-o:before { + content: "\f094"; +} +.fa-phone:before { + content: "\f095"; +} +.fa-square-o:before { + content: "\f096"; +} +.fa-bookmark-o:before { + content: "\f097"; +} +.fa-phone-square:before { + content: "\f098"; +} +.fa-twitter:before { + content: "\f099"; +} +.fa-facebook-f:before, +.fa-facebook:before { + content: "\f09a"; +} +.fa-github:before { + content: "\f09b"; +} +.fa-unlock:before { + content: "\f09c"; +} +.fa-credit-card:before { + content: "\f09d"; +} +.fa-feed:before, +.fa-rss:before { + content: "\f09e"; +} +.fa-hdd-o:before { + content: "\f0a0"; +} +.fa-bullhorn:before { + content: "\f0a1"; +} +.fa-bell:before { + content: "\f0f3"; +} +.fa-certificate:before { + content: "\f0a3"; +} +.fa-hand-o-right:before { + content: "\f0a4"; +} +.fa-hand-o-left:before { + content: "\f0a5"; +} +.fa-hand-o-up:before { + content: "\f0a6"; +} +.fa-hand-o-down:before { + content: "\f0a7"; +} +.fa-arrow-circle-left:before { + content: "\f0a8"; +} +.fa-arrow-circle-right:before { + content: "\f0a9"; +} +.fa-arrow-circle-up:before { + content: "\f0aa"; +} +.fa-arrow-circle-down:before { + content: "\f0ab"; +} +.fa-globe:before { + content: "\f0ac"; +} +.fa-wrench:before { + content: "\f0ad"; +} +.fa-tasks:before { + content: "\f0ae"; +} +.fa-filter:before { + content: "\f0b0"; +} +.fa-briefcase:before { + content: "\f0b1"; +} +.fa-arrows-alt:before { + content: "\f0b2"; +} +.fa-group:before, +.fa-users:before { + content: "\f0c0"; +} +.fa-chain:before, +.fa-link:before { + content: "\f0c1"; +} +.fa-cloud:before { + content: "\f0c2"; +} +.fa-flask:before { + content: "\f0c3"; +} +.fa-cut:before, +.fa-scissors:before { + content: "\f0c4"; +} +.fa-copy:before, +.fa-files-o:before { + content: "\f0c5"; +} +.fa-paperclip:before { + content: "\f0c6"; +} +.fa-save:before, +.fa-floppy-o:before { + content: "\f0c7"; +} +.fa-square:before { + content: "\f0c8"; +} +.fa-navicon:before, +.fa-reorder:before, +.fa-bars:before { + content: "\f0c9"; +} +.fa-list-ul:before { + content: "\f0ca"; +} +.fa-list-ol:before { + content: "\f0cb"; +} +.fa-strikethrough:before { + content: "\f0cc"; +} +.fa-underline:before { + content: "\f0cd"; +} +.fa-table:before { + content: "\f0ce"; +} +.fa-magic:before { + content: "\f0d0"; +} +.fa-truck:before { + content: "\f0d1"; +} +.fa-pinterest:before { + content: "\f0d2"; +} +.fa-pinterest-square:before { + content: "\f0d3"; +} +.fa-google-plus-square:before { + content: "\f0d4"; +} +.fa-google-plus:before { + content: "\f0d5"; +} +.fa-money:before { + content: "\f0d6"; +} +.fa-caret-down:before { + content: "\f0d7"; +} +.fa-caret-up:before { + content: "\f0d8"; +} +.fa-caret-left:before { + content: "\f0d9"; +} +.fa-caret-right:before { + content: "\f0da"; +} +.fa-columns:before { + content: "\f0db"; +} +.fa-unsorted:before, +.fa-sort:before { + content: "\f0dc"; +} +.fa-sort-down:before, +.fa-sort-desc:before { + content: "\f0dd"; +} +.fa-sort-up:before, +.fa-sort-asc:before { + content: "\f0de"; +} +.fa-envelope:before { + content: "\f0e0"; +} +.fa-linkedin:before { + content: "\f0e1"; +} +.fa-rotate-left:before, +.fa-undo:before { + content: "\f0e2"; +} +.fa-legal:before, +.fa-gavel:before { + content: "\f0e3"; +} +.fa-dashboard:before, +.fa-tachometer:before { + content: "\f0e4"; +} +.fa-comment-o:before { + content: "\f0e5"; +} +.fa-comments-o:before { + content: "\f0e6"; +} +.fa-flash:before, +.fa-bolt:before { + content: "\f0e7"; +} +.fa-sitemap:before { + content: "\f0e8"; +} +.fa-umbrella:before { + content: "\f0e9"; +} +.fa-paste:before, +.fa-clipboard:before { + content: "\f0ea"; +} +.fa-lightbulb-o:before { + content: "\f0eb"; +} +.fa-exchange:before { + content: "\f0ec"; +} +.fa-cloud-download:before { + content: "\f0ed"; +} +.fa-cloud-upload:before { + content: "\f0ee"; +} +.fa-user-md:before { + content: "\f0f0"; +} +.fa-stethoscope:before { + content: "\f0f1"; +} +.fa-suitcase:before { + content: "\f0f2"; +} +.fa-bell-o:before { + content: "\f0a2"; +} +.fa-coffee:before { + content: "\f0f4"; +} +.fa-cutlery:before { + content: "\f0f5"; +} +.fa-file-text-o:before { + content: "\f0f6"; +} +.fa-building-o:before { + content: "\f0f7"; +} +.fa-hospital-o:before { + content: "\f0f8"; +} +.fa-ambulance:before { + content: "\f0f9"; +} +.fa-medkit:before { + content: "\f0fa"; +} +.fa-fighter-jet:before { + content: "\f0fb"; +} +.fa-beer:before { + content: "\f0fc"; +} +.fa-h-square:before { + content: "\f0fd"; +} +.fa-plus-square:before { + content: "\f0fe"; +} +.fa-angle-double-left:before { + content: "\f100"; +} +.fa-angle-double-right:before { + content: "\f101"; +} +.fa-angle-double-up:before { + content: "\f102"; +} +.fa-angle-double-down:before { + content: "\f103"; +} +.fa-angle-left:before { + content: "\f104"; +} +.fa-angle-right:before { + content: "\f105"; +} +.fa-angle-up:before { + content: "\f106"; +} +.fa-angle-down:before { + content: "\f107"; +} +.fa-desktop:before { + content: "\f108"; +} +.fa-laptop:before { + content: "\f109"; +} +.fa-tablet:before { + content: "\f10a"; +} +.fa-mobile-phone:before, +.fa-mobile:before { + content: "\f10b"; +} +.fa-circle-o:before { + content: "\f10c"; +} +.fa-quote-left:before { + content: "\f10d"; +} +.fa-quote-right:before { + content: "\f10e"; +} +.fa-spinner:before { + content: "\f110"; +} +.fa-circle:before { + content: "\f111"; +} +.fa-mail-reply:before, +.fa-reply:before { + content: "\f112"; +} +.fa-github-alt:before { + content: "\f113"; +} +.fa-folder-o:before { + content: "\f114"; +} +.fa-folder-open-o:before { + content: "\f115"; +} +.fa-smile-o:before { + content: "\f118"; +} +.fa-frown-o:before { + content: "\f119"; +} +.fa-meh-o:before { + content: "\f11a"; +} +.fa-gamepad:before { + content: "\f11b"; +} +.fa-keyboard-o:before { + content: "\f11c"; +} +.fa-flag-o:before { + content: "\f11d"; +} +.fa-flag-checkered:before { + content: "\f11e"; +} +.fa-terminal:before { + content: "\f120"; +} +.fa-code:before { + content: "\f121"; +} +.fa-mail-reply-all:before, +.fa-reply-all:before { + content: "\f122"; +} +.fa-star-half-empty:before, +.fa-star-half-full:before, +.fa-star-half-o:before { + content: "\f123"; +} +.fa-location-arrow:before { + content: "\f124"; +} +.fa-crop:before { + content: "\f125"; +} +.fa-code-fork:before { + content: "\f126"; +} +.fa-unlink:before, +.fa-chain-broken:before { + content: "\f127"; +} +.fa-question:before { + content: "\f128"; +} +.fa-info:before { + content: "\f129"; +} +.fa-exclamation:before { + content: "\f12a"; +} +.fa-superscript:before { + content: "\f12b"; +} +.fa-subscript:before { + content: "\f12c"; +} +.fa-eraser:before { + content: "\f12d"; +} +.fa-puzzle-piece:before { + content: "\f12e"; +} +.fa-microphone:before { + content: "\f130"; +} +.fa-microphone-slash:before { + content: "\f131"; +} +.fa-shield:before { + content: "\f132"; +} +.fa-calendar-o:before { + content: "\f133"; +} +.fa-fire-extinguisher:before { + content: "\f134"; +} +.fa-rocket:before { + content: "\f135"; +} +.fa-maxcdn:before { + content: "\f136"; +} +.fa-chevron-circle-left:before { + content: "\f137"; +} +.fa-chevron-circle-right:before { + content: "\f138"; +} +.fa-chevron-circle-up:before { + content: "\f139"; +} +.fa-chevron-circle-down:before { + content: "\f13a"; +} +.fa-html5:before { + content: "\f13b"; +} +.fa-css3:before { + content: "\f13c"; +} +.fa-anchor:before { + content: "\f13d"; +} +.fa-unlock-alt:before { + content: "\f13e"; +} +.fa-bullseye:before { + content: "\f140"; +} +.fa-ellipsis-h:before { + content: "\f141"; +} +.fa-ellipsis-v:before { + content: "\f142"; +} +.fa-rss-square:before { + content: "\f143"; +} +.fa-play-circle:before { + content: "\f144"; +} +.fa-ticket:before { + content: "\f145"; +} +.fa-minus-square:before { + content: "\f146"; +} +.fa-minus-square-o:before { + content: "\f147"; +} +.fa-level-up:before { + content: "\f148"; +} +.fa-level-down:before { + content: "\f149"; +} +.fa-check-square:before { + content: "\f14a"; +} +.fa-pencil-square:before { + content: "\f14b"; +} +.fa-external-link-square:before { + content: "\f14c"; +} +.fa-share-square:before { + content: "\f14d"; +} +.fa-compass:before { + content: "\f14e"; +} +.fa-toggle-down:before, +.fa-caret-square-o-down:before { + content: "\f150"; +} +.fa-toggle-up:before, +.fa-caret-square-o-up:before { + content: "\f151"; +} +.fa-toggle-right:before, +.fa-caret-square-o-right:before { + content: "\f152"; +} +.fa-euro:before, +.fa-eur:before { + content: "\f153"; +} +.fa-gbp:before { + content: "\f154"; +} +.fa-dollar:before, +.fa-usd:before { + content: "\f155"; +} +.fa-rupee:before, +.fa-inr:before { + content: "\f156"; +} +.fa-cny:before, +.fa-rmb:before, +.fa-yen:before, +.fa-jpy:before { + content: "\f157"; +} +.fa-ruble:before, +.fa-rouble:before, +.fa-rub:before { + content: "\f158"; +} +.fa-won:before, +.fa-krw:before { + content: "\f159"; +} +.fa-bitcoin:before, +.fa-btc:before { + content: "\f15a"; +} +.fa-file:before { + content: "\f15b"; +} +.fa-file-text:before { + content: "\f15c"; +} +.fa-sort-alpha-asc:before { + content: "\f15d"; +} +.fa-sort-alpha-desc:before { + content: "\f15e"; +} +.fa-sort-amount-asc:before { + content: "\f160"; +} +.fa-sort-amount-desc:before { + content: "\f161"; +} +.fa-sort-numeric-asc:before { + content: "\f162"; +} +.fa-sort-numeric-desc:before { + content: "\f163"; +} +.fa-thumbs-up:before { + content: "\f164"; +} +.fa-thumbs-down:before { + content: "\f165"; +} +.fa-youtube-square:before { + content: "\f166"; +} +.fa-youtube:before { + content: "\f167"; +} +.fa-xing:before { + content: "\f168"; +} +.fa-xing-square:before { + content: "\f169"; +} +.fa-youtube-play:before { + content: "\f16a"; +} +.fa-dropbox:before { + content: "\f16b"; +} +.fa-stack-overflow:before { + content: "\f16c"; +} +.fa-instagram:before { + content: "\f16d"; +} +.fa-flickr:before { + content: "\f16e"; +} +.fa-adn:before { + content: "\f170"; +} +.fa-bitbucket:before { + content: "\f171"; +} +.fa-bitbucket-square:before { + content: "\f172"; +} +.fa-tumblr:before { + content: "\f173"; +} +.fa-tumblr-square:before { + content: "\f174"; +} +.fa-long-arrow-down:before { + content: "\f175"; +} +.fa-long-arrow-up:before { + content: "\f176"; +} +.fa-long-arrow-left:before { + content: "\f177"; +} +.fa-long-arrow-right:before { + content: "\f178"; +} +.fa-apple:before { + content: "\f179"; +} +.fa-windows:before { + content: "\f17a"; +} +.fa-android:before { + content: "\f17b"; +} +.fa-linux:before { + content: "\f17c"; +} +.fa-dribbble:before { + content: "\f17d"; +} +.fa-skype:before { + content: "\f17e"; +} +.fa-foursquare:before { + content: "\f180"; +} +.fa-trello:before { + content: "\f181"; +} +.fa-female:before { + content: "\f182"; +} +.fa-male:before { + content: "\f183"; +} +.fa-gittip:before, +.fa-gratipay:before { + content: "\f184"; +} +.fa-sun-o:before { + content: "\f185"; +} +.fa-moon-o:before { + content: "\f186"; +} +.fa-archive:before { + content: "\f187"; +} +.fa-bug:before { + content: "\f188"; +} +.fa-vk:before { + content: "\f189"; +} +.fa-weibo:before { + content: "\f18a"; +} +.fa-renren:before { + content: "\f18b"; +} +.fa-pagelines:before { + content: "\f18c"; +} +.fa-stack-exchange:before { + content: "\f18d"; +} +.fa-arrow-circle-o-right:before { + content: "\f18e"; +} +.fa-arrow-circle-o-left:before { + content: "\f190"; +} +.fa-toggle-left:before, +.fa-caret-square-o-left:before { + content: "\f191"; +} +.fa-dot-circle-o:before { + content: "\f192"; +} +.fa-wheelchair:before { + content: "\f193"; +} +.fa-vimeo-square:before { + content: "\f194"; +} +.fa-turkish-lira:before, +.fa-try:before { + content: "\f195"; +} +.fa-plus-square-o:before { + content: "\f196"; +} +.fa-space-shuttle:before { + content: "\f197"; +} +.fa-slack:before { + content: "\f198"; +} +.fa-envelope-square:before { + content: "\f199"; +} +.fa-wordpress:before { + content: "\f19a"; +} +.fa-openid:before { + content: "\f19b"; +} +.fa-institution:before, +.fa-bank:before, +.fa-university:before { + content: "\f19c"; +} +.fa-mortar-board:before, +.fa-graduation-cap:before { + content: "\f19d"; +} +.fa-yahoo:before { + content: "\f19e"; +} +.fa-google:before { + content: "\f1a0"; +} +.fa-reddit:before { + content: "\f1a1"; +} +.fa-reddit-square:before { + content: "\f1a2"; +} +.fa-stumbleupon-circle:before { + content: "\f1a3"; +} +.fa-stumbleupon:before { + content: "\f1a4"; +} +.fa-delicious:before { + content: "\f1a5"; +} +.fa-digg:before { + content: "\f1a6"; +} +.fa-pied-piper-pp:before { + content: "\f1a7"; +} +.fa-pied-piper-alt:before { + content: "\f1a8"; +} +.fa-drupal:before { + content: "\f1a9"; +} +.fa-joomla:before { + content: "\f1aa"; +} +.fa-language:before { + content: "\f1ab"; +} +.fa-fax:before { + content: "\f1ac"; +} +.fa-building:before { + content: "\f1ad"; +} +.fa-child:before { + content: "\f1ae"; +} +.fa-paw:before { + content: "\f1b0"; +} +.fa-spoon:before { + content: "\f1b1"; +} +.fa-cube:before { + content: "\f1b2"; +} +.fa-cubes:before { + content: "\f1b3"; +} +.fa-behance:before { + content: "\f1b4"; +} +.fa-behance-square:before { + content: "\f1b5"; +} +.fa-steam:before { + content: "\f1b6"; +} +.fa-steam-square:before { + content: "\f1b7"; +} +.fa-recycle:before { + content: "\f1b8"; +} +.fa-automobile:before, +.fa-car:before { + content: "\f1b9"; +} +.fa-cab:before, +.fa-taxi:before { + content: "\f1ba"; +} +.fa-tree:before { + content: "\f1bb"; +} +.fa-spotify:before { + content: "\f1bc"; +} +.fa-deviantart:before { + content: "\f1bd"; +} +.fa-soundcloud:before { + content: "\f1be"; +} +.fa-database:before { + content: "\f1c0"; +} +.fa-file-pdf-o:before { + content: "\f1c1"; +} +.fa-file-word-o:before { + content: "\f1c2"; +} +.fa-file-excel-o:before { + content: "\f1c3"; +} +.fa-file-powerpoint-o:before { + content: "\f1c4"; +} +.fa-file-photo-o:before, +.fa-file-picture-o:before, +.fa-file-image-o:before { + content: "\f1c5"; +} +.fa-file-zip-o:before, +.fa-file-archive-o:before { + content: "\f1c6"; +} +.fa-file-sound-o:before, +.fa-file-audio-o:before { + content: "\f1c7"; +} +.fa-file-movie-o:before, +.fa-file-video-o:before { + content: "\f1c8"; +} +.fa-file-code-o:before { + content: "\f1c9"; +} +.fa-vine:before { + content: "\f1ca"; +} +.fa-codepen:before { + content: "\f1cb"; +} +.fa-jsfiddle:before { + content: "\f1cc"; +} +.fa-life-bouy:before, +.fa-life-buoy:before, +.fa-life-saver:before, +.fa-support:before, +.fa-life-ring:before { + content: "\f1cd"; +} +.fa-circle-o-notch:before { + content: "\f1ce"; +} +.fa-ra:before, +.fa-resistance:before, +.fa-rebel:before { + content: "\f1d0"; +} +.fa-ge:before, +.fa-empire:before { + content: "\f1d1"; +} +.fa-git-square:before { + content: "\f1d2"; +} +.fa-git:before { + content: "\f1d3"; +} +.fa-y-combinator-square:before, +.fa-yc-square:before, +.fa-hacker-news:before { + content: "\f1d4"; +} +.fa-tencent-weibo:before { + content: "\f1d5"; +} +.fa-qq:before { + content: "\f1d6"; +} +.fa-wechat:before, +.fa-weixin:before { + content: "\f1d7"; +} +.fa-send:before, +.fa-paper-plane:before { + content: "\f1d8"; +} +.fa-send-o:before, +.fa-paper-plane-o:before { + content: "\f1d9"; +} +.fa-history:before { + content: "\f1da"; +} +.fa-circle-thin:before { + content: "\f1db"; +} +.fa-header:before { + content: "\f1dc"; +} +.fa-paragraph:before { + content: "\f1dd"; +} +.fa-sliders:before { + content: "\f1de"; +} +.fa-share-alt:before { + content: "\f1e0"; +} +.fa-share-alt-square:before { + content: "\f1e1"; +} +.fa-bomb:before { + content: "\f1e2"; +} +.fa-soccer-ball-o:before, +.fa-futbol-o:before { + content: "\f1e3"; +} +.fa-tty:before { + content: "\f1e4"; +} +.fa-binoculars:before { + content: "\f1e5"; +} +.fa-plug:before { + content: "\f1e6"; +} +.fa-slideshare:before { + content: "\f1e7"; +} +.fa-twitch:before { + content: "\f1e8"; +} +.fa-yelp:before { + content: "\f1e9"; +} +.fa-newspaper-o:before { + content: "\f1ea"; +} +.fa-wifi:before { + content: "\f1eb"; +} +.fa-calculator:before { + content: "\f1ec"; +} +.fa-paypal:before { + content: "\f1ed"; +} +.fa-google-wallet:before { + content: "\f1ee"; +} +.fa-cc-visa:before { + content: "\f1f0"; +} +.fa-cc-mastercard:before { + content: "\f1f1"; +} +.fa-cc-discover:before { + content: "\f1f2"; +} +.fa-cc-amex:before { + content: "\f1f3"; +} +.fa-cc-paypal:before { + content: "\f1f4"; +} +.fa-cc-stripe:before { + content: "\f1f5"; +} +.fa-bell-slash:before { + content: "\f1f6"; +} +.fa-bell-slash-o:before { + content: "\f1f7"; +} +.fa-trash:before { + content: "\f1f8"; +} +.fa-copyright:before { + content: "\f1f9"; +} +.fa-at:before { + content: "\f1fa"; +} +.fa-eyedropper:before { + content: "\f1fb"; +} +.fa-paint-brush:before { + content: "\f1fc"; +} +.fa-birthday-cake:before { + content: "\f1fd"; +} +.fa-area-chart:before { + content: "\f1fe"; +} +.fa-pie-chart:before { + content: "\f200"; +} +.fa-line-chart:before { + content: "\f201"; +} +.fa-lastfm:before { + content: "\f202"; +} +.fa-lastfm-square:before { + content: "\f203"; +} +.fa-toggle-off:before { + content: "\f204"; +} +.fa-toggle-on:before { + content: "\f205"; +} +.fa-bicycle:before { + content: "\f206"; +} +.fa-bus:before { + content: "\f207"; +} +.fa-ioxhost:before { + content: "\f208"; +} +.fa-angellist:before { + content: "\f209"; +} +.fa-cc:before { + content: "\f20a"; +} +.fa-shekel:before, +.fa-sheqel:before, +.fa-ils:before { + content: "\f20b"; +} +.fa-meanpath:before { + content: "\f20c"; +} +.fa-buysellads:before { + content: "\f20d"; +} +.fa-connectdevelop:before { + content: "\f20e"; +} +.fa-dashcube:before { + content: "\f210"; +} +.fa-forumbee:before { + content: "\f211"; +} +.fa-leanpub:before { + content: "\f212"; +} +.fa-sellsy:before { + content: "\f213"; +} +.fa-shirtsinbulk:before { + content: "\f214"; +} +.fa-simplybuilt:before { + content: "\f215"; +} +.fa-skyatlas:before { + content: "\f216"; +} +.fa-cart-plus:before { + content: "\f217"; +} +.fa-cart-arrow-down:before { + content: "\f218"; +} +.fa-diamond:before { + content: "\f219"; +} +.fa-ship:before { + content: "\f21a"; +} +.fa-user-secret:before { + content: "\f21b"; +} +.fa-motorcycle:before { + content: "\f21c"; +} +.fa-street-view:before { + content: "\f21d"; +} +.fa-heartbeat:before { + content: "\f21e"; +} +.fa-venus:before { + content: "\f221"; +} +.fa-mars:before { + content: "\f222"; +} +.fa-mercury:before { + content: "\f223"; +} +.fa-intersex:before, +.fa-transgender:before { + content: "\f224"; +} +.fa-transgender-alt:before { + content: "\f225"; +} +.fa-venus-double:before { + content: "\f226"; +} +.fa-mars-double:before { + content: "\f227"; +} +.fa-venus-mars:before { + content: "\f228"; +} +.fa-mars-stroke:before { + content: "\f229"; +} +.fa-mars-stroke-v:before { + content: "\f22a"; +} +.fa-mars-stroke-h:before { + content: "\f22b"; +} +.fa-neuter:before { + content: "\f22c"; +} +.fa-genderless:before { + content: "\f22d"; +} +.fa-facebook-official:before { + content: "\f230"; +} +.fa-pinterest-p:before { + content: "\f231"; +} +.fa-whatsapp:before { + content: "\f232"; +} +.fa-server:before { + content: "\f233"; +} +.fa-user-plus:before { + content: "\f234"; +} +.fa-user-times:before { + content: "\f235"; +} +.fa-hotel:before, +.fa-bed:before { + content: "\f236"; +} +.fa-viacoin:before { + content: "\f237"; +} +.fa-train:before { + content: "\f238"; +} +.fa-subway:before { + content: "\f239"; +} +.fa-medium:before { + content: "\f23a"; +} +.fa-yc:before, +.fa-y-combinator:before { + content: "\f23b"; +} +.fa-optin-monster:before { + content: "\f23c"; +} +.fa-opencart:before { + content: "\f23d"; +} +.fa-expeditedssl:before { + content: "\f23e"; +} +.fa-battery-4:before, +.fa-battery:before, +.fa-battery-full:before { + content: "\f240"; +} +.fa-battery-3:before, +.fa-battery-three-quarters:before { + content: "\f241"; +} +.fa-battery-2:before, +.fa-battery-half:before { + content: "\f242"; +} +.fa-battery-1:before, +.fa-battery-quarter:before { + content: "\f243"; +} +.fa-battery-0:before, +.fa-battery-empty:before { + content: "\f244"; +} +.fa-mouse-pointer:before { + content: "\f245"; +} +.fa-i-cursor:before { + content: "\f246"; +} +.fa-object-group:before { + content: "\f247"; +} +.fa-object-ungroup:before { + content: "\f248"; +} +.fa-sticky-note:before { + content: "\f249"; +} +.fa-sticky-note-o:before { + content: "\f24a"; +} +.fa-cc-jcb:before { + content: "\f24b"; +} +.fa-cc-diners-club:before { + content: "\f24c"; +} +.fa-clone:before { + content: "\f24d"; +} +.fa-balance-scale:before { + content: "\f24e"; +} +.fa-hourglass-o:before { + content: "\f250"; +} +.fa-hourglass-1:before, +.fa-hourglass-start:before { + content: "\f251"; +} +.fa-hourglass-2:before, +.fa-hourglass-half:before { + content: "\f252"; +} +.fa-hourglass-3:before, +.fa-hourglass-end:before { + content: "\f253"; +} +.fa-hourglass:before { + content: "\f254"; +} +.fa-hand-grab-o:before, +.fa-hand-rock-o:before { + content: "\f255"; +} +.fa-hand-stop-o:before, +.fa-hand-paper-o:before { + content: "\f256"; +} +.fa-hand-scissors-o:before { + content: "\f257"; +} +.fa-hand-lizard-o:before { + content: "\f258"; +} +.fa-hand-spock-o:before { + content: "\f259"; +} +.fa-hand-pointer-o:before { + content: "\f25a"; +} +.fa-hand-peace-o:before { + content: "\f25b"; +} +.fa-trademark:before { + content: "\f25c"; +} +.fa-registered:before { + content: "\f25d"; +} +.fa-creative-commons:before { + content: "\f25e"; +} +.fa-gg:before { + content: "\f260"; +} +.fa-gg-circle:before { + content: "\f261"; +} +.fa-tripadvisor:before { + content: "\f262"; +} +.fa-odnoklassniki:before { + content: "\f263"; +} +.fa-odnoklassniki-square:before { + content: "\f264"; +} +.fa-get-pocket:before { + content: "\f265"; +} +.fa-wikipedia-w:before { + content: "\f266"; +} +.fa-safari:before { + content: "\f267"; +} +.fa-chrome:before { + content: "\f268"; +} +.fa-firefox:before { + content: "\f269"; +} +.fa-opera:before { + content: "\f26a"; +} +.fa-internet-explorer:before { + content: "\f26b"; +} +.fa-tv:before, +.fa-television:before { + content: "\f26c"; +} +.fa-contao:before { + content: "\f26d"; +} +.fa-500px:before { + content: "\f26e"; +} +.fa-amazon:before { + content: "\f270"; +} +.fa-calendar-plus-o:before { + content: "\f271"; +} +.fa-calendar-minus-o:before { + content: "\f272"; +} +.fa-calendar-times-o:before { + content: "\f273"; +} +.fa-calendar-check-o:before { + content: "\f274"; +} +.fa-industry:before { + content: "\f275"; +} +.fa-map-pin:before { + content: "\f276"; +} +.fa-map-signs:before { + content: "\f277"; +} +.fa-map-o:before { + content: "\f278"; +} +.fa-map:before { + content: "\f279"; +} +.fa-commenting:before { + content: "\f27a"; +} +.fa-commenting-o:before { + content: "\f27b"; +} +.fa-houzz:before { + content: "\f27c"; +} +.fa-vimeo:before { + content: "\f27d"; +} +.fa-black-tie:before { + content: "\f27e"; +} +.fa-fonticons:before { + content: "\f280"; +} +.fa-reddit-alien:before { + content: "\f281"; +} +.fa-edge:before { + content: "\f282"; +} +.fa-credit-card-alt:before { + content: "\f283"; +} +.fa-codiepie:before { + content: "\f284"; +} +.fa-modx:before { + content: "\f285"; +} +.fa-fort-awesome:before { + content: "\f286"; +} +.fa-usb:before { + content: "\f287"; +} +.fa-product-hunt:before { + content: "\f288"; +} +.fa-mixcloud:before { + content: "\f289"; +} +.fa-scribd:before { + content: "\f28a"; +} +.fa-pause-circle:before { + content: "\f28b"; +} +.fa-pause-circle-o:before { + content: "\f28c"; +} +.fa-stop-circle:before { + content: "\f28d"; +} +.fa-stop-circle-o:before { + content: "\f28e"; +} +.fa-shopping-bag:before { + content: "\f290"; +} +.fa-shopping-basket:before { + content: "\f291"; +} +.fa-hashtag:before { + content: "\f292"; +} +.fa-bluetooth:before { + content: "\f293"; +} +.fa-bluetooth-b:before { + content: "\f294"; +} +.fa-percent:before { + content: "\f295"; +} +.fa-gitlab:before { + content: "\f296"; +} +.fa-wpbeginner:before { + content: "\f297"; +} +.fa-wpforms:before { + content: "\f298"; +} +.fa-envira:before { + content: "\f299"; +} +.fa-universal-access:before { + content: "\f29a"; +} +.fa-wheelchair-alt:before { + content: "\f29b"; +} +.fa-question-circle-o:before { + content: "\f29c"; +} +.fa-blind:before { + content: "\f29d"; +} +.fa-audio-description:before { + content: "\f29e"; +} +.fa-volume-control-phone:before { + content: "\f2a0"; +} +.fa-braille:before { + content: "\f2a1"; +} +.fa-assistive-listening-systems:before { + content: "\f2a2"; +} +.fa-asl-interpreting:before, +.fa-american-sign-language-interpreting:before { + content: "\f2a3"; +} +.fa-deafness:before, +.fa-hard-of-hearing:before, +.fa-deaf:before { + content: "\f2a4"; +} +.fa-glide:before { + content: "\f2a5"; +} +.fa-glide-g:before { + content: "\f2a6"; +} +.fa-signing:before, +.fa-sign-language:before { + content: "\f2a7"; +} +.fa-low-vision:before { + content: "\f2a8"; +} +.fa-viadeo:before { + content: "\f2a9"; +} +.fa-viadeo-square:before { + content: "\f2aa"; +} +.fa-snapchat:before { + content: "\f2ab"; +} +.fa-snapchat-ghost:before { + content: "\f2ac"; +} +.fa-snapchat-square:before { + content: "\f2ad"; +} +.fa-pied-piper:before { + content: "\f2ae"; +} +.fa-first-order:before { + content: "\f2b0"; +} +.fa-yoast:before { + content: "\f2b1"; +} +.fa-themeisle:before { + content: "\f2b2"; +} +.fa-google-plus-circle:before, +.fa-google-plus-official:before { + content: "\f2b3"; +} +.fa-fa:before, +.fa-font-awesome:before { + content: "\f2b4"; +} +.fa-handshake-o:before { + content: "\f2b5"; +} +.fa-envelope-open:before { + content: "\f2b6"; +} +.fa-envelope-open-o:before { + content: "\f2b7"; +} +.fa-linode:before { + content: "\f2b8"; +} +.fa-address-book:before { + content: "\f2b9"; +} +.fa-address-book-o:before { + content: "\f2ba"; +} +.fa-vcard:before, +.fa-address-card:before { + content: "\f2bb"; +} +.fa-vcard-o:before, +.fa-address-card-o:before { + content: "\f2bc"; +} +.fa-user-circle:before { + content: "\f2bd"; +} +.fa-user-circle-o:before { + content: "\f2be"; +} +.fa-user-o:before { + content: "\f2c0"; +} +.fa-id-badge:before { + content: "\f2c1"; +} +.fa-drivers-license:before, +.fa-id-card:before { + content: "\f2c2"; +} +.fa-drivers-license-o:before, +.fa-id-card-o:before { + content: "\f2c3"; +} +.fa-quora:before { + content: "\f2c4"; +} +.fa-free-code-camp:before { + content: "\f2c5"; +} +.fa-telegram:before { + content: "\f2c6"; +} +.fa-thermometer-4:before, +.fa-thermometer:before, +.fa-thermometer-full:before { + content: "\f2c7"; +} +.fa-thermometer-3:before, +.fa-thermometer-three-quarters:before { + content: "\f2c8"; +} +.fa-thermometer-2:before, +.fa-thermometer-half:before { + content: "\f2c9"; +} +.fa-thermometer-1:before, +.fa-thermometer-quarter:before { + content: "\f2ca"; +} +.fa-thermometer-0:before, +.fa-thermometer-empty:before { + content: "\f2cb"; +} +.fa-shower:before { + content: "\f2cc"; +} +.fa-bathtub:before, +.fa-s15:before, +.fa-bath:before { + content: "\f2cd"; +} +.fa-podcast:before { + content: "\f2ce"; +} +.fa-window-maximize:before { + content: "\f2d0"; +} +.fa-window-minimize:before { + content: "\f2d1"; +} +.fa-window-restore:before { + content: "\f2d2"; +} +.fa-times-rectangle:before, +.fa-window-close:before { + content: "\f2d3"; +} +.fa-times-rectangle-o:before, +.fa-window-close-o:before { + content: "\f2d4"; +} +.fa-bandcamp:before { + content: "\f2d5"; +} +.fa-grav:before { + content: "\f2d6"; +} +.fa-etsy:before { + content: "\f2d7"; +} +.fa-imdb:before { + content: "\f2d8"; +} +.fa-ravelry:before { + content: "\f2d9"; +} +.fa-eercast:before { + content: "\f2da"; +} +.fa-microchip:before { + content: "\f2db"; +} +.fa-snowflake-o:before { + content: "\f2dc"; +} +.fa-superpowers:before { + content: "\f2dd"; +} +.fa-wpexplorer:before { + content: "\f2de"; +} +.fa-meetup:before { + content: "\f2e0"; +} +.sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + border: 0; +} +.sr-only-focusable:active, +.sr-only-focusable:focus { + position: static; + width: auto; + height: auto; + margin: 0; + overflow: visible; + clip: auto; +} + +/* stylelint-enable */ \ No newline at end of file diff --git a/frontend/src/Content/Fonts/fontawesome-webfont.eot b/frontend/src/Content/Fonts/fontawesome-webfont.eot new file mode 100644 index 000000000..e9f60ca95 Binary files /dev/null and b/frontend/src/Content/Fonts/fontawesome-webfont.eot differ diff --git a/frontend/src/Content/Fonts/fontawesome-webfont.svg b/frontend/src/Content/Fonts/fontawesome-webfont.svg new file mode 100644 index 000000000..855c845e5 --- /dev/null +++ b/frontend/src/Content/Fonts/fontawesome-webfont.svg @@ -0,0 +1,2671 @@ + + + + +Created by FontForge 20120731 at Mon Oct 24 17:37:40 2016 + By ,,, +Copyright Dave Gandy 2016. All rights reserved. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/src/Content/Fonts/fontawesome-webfont.ttf b/frontend/src/Content/Fonts/fontawesome-webfont.ttf new file mode 100644 index 000000000..35acda2fa Binary files /dev/null and b/frontend/src/Content/Fonts/fontawesome-webfont.ttf differ diff --git a/frontend/src/Content/Fonts/fontawesome-webfont.woff b/frontend/src/Content/Fonts/fontawesome-webfont.woff new file mode 100644 index 000000000..400014a4b Binary files /dev/null and b/frontend/src/Content/Fonts/fontawesome-webfont.woff differ diff --git a/frontend/src/Content/Fonts/fontawesome-webfont.woff2 b/frontend/src/Content/Fonts/fontawesome-webfont.woff2 new file mode 100644 index 000000000..4d13fc604 Binary files /dev/null and b/frontend/src/Content/Fonts/fontawesome-webfont.woff2 differ diff --git a/frontend/src/Content/Fonts/fonts.css b/frontend/src/Content/Fonts/fonts.css new file mode 100644 index 000000000..483c61ca8 --- /dev/null +++ b/frontend/src/Content/Fonts/fonts.css @@ -0,0 +1,27 @@ +@font-face { + font-weight: 300; + font-style: normal; + font-family: 'Roboto'; + src: url('Roboto-Light.woff2?v=1.1.0') format('woff2'), url('Roboto-Light.woff?v=1.2.0') format('woff'), url('Roboto-Light.ttf?v=1.1.0') format('truetype'); +} + +@font-face { + font-weight: 400; + font-style: normal; + font-family: 'Roboto'; + src: url('Roboto-Regular.woff2?v=1.2.0') format('woff2'), url('Roboto-Regular.woff?v=1.2.0') format('woff'), url('Roboto-Regular.ttf?v=1.1.0') format('truetype'); +} + +@font-face { + font-weight: normal; + font-style: normal; + font-family: 'Roboto'; + src: url('Roboto-Regular.woff2?v=1.3.0') format('woff2'), url('Roboto-Regular.woff?v=1.2.0') format('woff'), url('Roboto-Regular.ttf?v=1.1.0') format('truetype'); +} + +@font-face { + font-weight: 400; + font-style: normal; + font-family: 'Ubuntu Mono'; + src: url('UbuntuMono-Regular.eot?#iefix') format('embedded-opentype'), url('UbuntuMono-Regular.woff') format('woff'), url('UbuntuMono-Regular.ttf') format('truetype'); +} diff --git a/src/UI/Content/Images/404.png b/frontend/src/Content/Images/404.png similarity index 100% rename from src/UI/Content/Images/404.png rename to frontend/src/Content/Images/404.png diff --git a/frontend/src/Content/Images/Icons/android-chrome-192x192.png b/frontend/src/Content/Images/Icons/android-chrome-192x192.png new file mode 100644 index 000000000..d1485d19d Binary files /dev/null and b/frontend/src/Content/Images/Icons/android-chrome-192x192.png differ diff --git a/frontend/src/Content/Images/Icons/android-chrome-512x512.png b/frontend/src/Content/Images/Icons/android-chrome-512x512.png new file mode 100644 index 000000000..13fd218ba Binary files /dev/null and b/frontend/src/Content/Images/Icons/android-chrome-512x512.png differ diff --git a/frontend/src/Content/Images/Icons/apple-touch-icon.png b/frontend/src/Content/Images/Icons/apple-touch-icon.png new file mode 100644 index 000000000..3c05d62e5 Binary files /dev/null and b/frontend/src/Content/Images/Icons/apple-touch-icon.png differ diff --git a/frontend/src/Content/Images/Icons/browserconfig.xml b/frontend/src/Content/Images/Icons/browserconfig.xml new file mode 100644 index 000000000..993924968 --- /dev/null +++ b/frontend/src/Content/Images/Icons/browserconfig.xml @@ -0,0 +1,9 @@ + + + + + + #00ccff + + + diff --git a/frontend/src/Content/Images/Icons/favicon-16x16.png b/frontend/src/Content/Images/Icons/favicon-16x16.png new file mode 100644 index 000000000..48028037e Binary files /dev/null and b/frontend/src/Content/Images/Icons/favicon-16x16.png differ diff --git a/frontend/src/Content/Images/Icons/favicon-32x32.png b/frontend/src/Content/Images/Icons/favicon-32x32.png new file mode 100644 index 000000000..9f8422fa4 Binary files /dev/null and b/frontend/src/Content/Images/Icons/favicon-32x32.png differ diff --git a/frontend/src/Content/Images/Icons/favicon-debug-16x16.png b/frontend/src/Content/Images/Icons/favicon-debug-16x16.png new file mode 100644 index 000000000..b853a9ef9 Binary files /dev/null and b/frontend/src/Content/Images/Icons/favicon-debug-16x16.png differ diff --git a/frontend/src/Content/Images/Icons/favicon-debug-32x32.png b/frontend/src/Content/Images/Icons/favicon-debug-32x32.png new file mode 100644 index 000000000..787fd2b8f Binary files /dev/null and b/frontend/src/Content/Images/Icons/favicon-debug-32x32.png differ diff --git a/frontend/src/Content/Images/Icons/favicon-debug.ico b/frontend/src/Content/Images/Icons/favicon-debug.ico new file mode 100644 index 000000000..726e812c6 Binary files /dev/null and b/frontend/src/Content/Images/Icons/favicon-debug.ico differ diff --git a/frontend/src/Content/Images/Icons/favicon.ico b/frontend/src/Content/Images/Icons/favicon.ico new file mode 100644 index 000000000..1b0de8423 Binary files /dev/null and b/frontend/src/Content/Images/Icons/favicon.ico differ diff --git a/frontend/src/Content/Images/Icons/manifest.json b/frontend/src/Content/Images/Icons/manifest.json new file mode 100644 index 000000000..d14732f60 --- /dev/null +++ b/frontend/src/Content/Images/Icons/manifest.json @@ -0,0 +1,18 @@ +{ + "name": "", + "icons": [ + { + "src": "/Content/Images/Icons/android-chrome-192x192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "/Content/Images/Icons/android-chrome-512x512.png", + "sizes": "512x512", + "type": "image/png" + } + ], + "theme_color": "#3a3f51", + "background_color": "#3a3f51", + "display": "standalone" +} \ No newline at end of file diff --git a/frontend/src/Content/Images/Icons/mstile-144x144.png b/frontend/src/Content/Images/Icons/mstile-144x144.png new file mode 100644 index 000000000..fdabc8bb7 Binary files /dev/null and b/frontend/src/Content/Images/Icons/mstile-144x144.png differ diff --git a/frontend/src/Content/Images/Icons/mstile-150x150.png b/frontend/src/Content/Images/Icons/mstile-150x150.png new file mode 100644 index 000000000..ebf8d9217 Binary files /dev/null and b/frontend/src/Content/Images/Icons/mstile-150x150.png differ diff --git a/frontend/src/Content/Images/Icons/mstile-310x150.png b/frontend/src/Content/Images/Icons/mstile-310x150.png new file mode 100644 index 000000000..f670c85df Binary files /dev/null and b/frontend/src/Content/Images/Icons/mstile-310x150.png differ diff --git a/frontend/src/Content/Images/Icons/mstile-310x310.png b/frontend/src/Content/Images/Icons/mstile-310x310.png new file mode 100644 index 000000000..714b3ab7c Binary files /dev/null and b/frontend/src/Content/Images/Icons/mstile-310x310.png differ diff --git a/frontend/src/Content/Images/Icons/mstile-70x70.png b/frontend/src/Content/Images/Icons/mstile-70x70.png new file mode 100644 index 000000000..65cbdb737 Binary files /dev/null and b/frontend/src/Content/Images/Icons/mstile-70x70.png differ diff --git a/frontend/src/Content/Images/Icons/safari-pinned-tab.svg b/frontend/src/Content/Images/Icons/safari-pinned-tab.svg new file mode 100644 index 000000000..6fc7fb969 --- /dev/null +++ b/frontend/src/Content/Images/Icons/safari-pinned-tab.svg @@ -0,0 +1,38 @@ + + + + +Created by potrace 1.11, written by Peter Selinger 2001-2013 + + + + + diff --git a/frontend/src/Content/Images/logo.svg b/frontend/src/Content/Images/logo.svg new file mode 100644 index 000000000..ebebe49a9 --- /dev/null +++ b/frontend/src/Content/Images/logo.svg @@ -0,0 +1 @@ + background Layer 1 \ No newline at end of file diff --git a/frontend/src/Content/Images/poster-dark-square.png b/frontend/src/Content/Images/poster-dark-square.png new file mode 100644 index 000000000..2d7608f3d Binary files /dev/null and b/frontend/src/Content/Images/poster-dark-square.png differ diff --git a/src/UI/Content/Images/poster-dark.png b/frontend/src/Content/Images/poster-dark.png similarity index 100% rename from src/UI/Content/Images/poster-dark.png rename to frontend/src/Content/Images/poster-dark.png diff --git a/frontend/src/Helpers/Props/Shapes/createRouteMatchShape.js b/frontend/src/Helpers/Props/Shapes/createRouteMatchShape.js new file mode 100644 index 000000000..11cca7d1b --- /dev/null +++ b/frontend/src/Helpers/Props/Shapes/createRouteMatchShape.js @@ -0,0 +1,11 @@ +import PropTypes from 'prop-types'; + +function createRouteMatchShape(props) { + return PropTypes.shape({ + params: PropTypes.shape({ + ...props + }).isRequired + }); +} + +export default createRouteMatchShape; diff --git a/frontend/src/Helpers/Props/Shapes/locationShape.js b/frontend/src/Helpers/Props/Shapes/locationShape.js new file mode 100644 index 000000000..80b53eb44 --- /dev/null +++ b/frontend/src/Helpers/Props/Shapes/locationShape.js @@ -0,0 +1,11 @@ +import PropTypes from 'prop-types'; + +const locationShape = PropTypes.shape({ + pathname: PropTypes.string.isRequired, + search: PropTypes.string.isRequired, + state: PropTypes.object, + action: PropTypes.string, + key: PropTypes.string +}); + +export default locationShape; diff --git a/frontend/src/Helpers/Props/Shapes/settingShape.js b/frontend/src/Helpers/Props/Shapes/settingShape.js new file mode 100644 index 000000000..cd672de27 --- /dev/null +++ b/frontend/src/Helpers/Props/Shapes/settingShape.js @@ -0,0 +1,34 @@ +import PropTypes from 'prop-types'; + +const settingShape = { + value: PropTypes.oneOf([PropTypes.bool, PropTypes.number, PropTypes.string]), + warnings: PropTypes.arrayOf(PropTypes.string).isRequired, + errors: PropTypes.arrayOf(PropTypes.string).isRequired +}; + +export const arraySettingShape = { + ...settingShape, + value: PropTypes.array.isRequired +}; + +export const boolSettingShape = { + ...settingShape, + value: PropTypes.bool.isRequired +}; + +export const numberSettingShape = { + ...settingShape, + value: PropTypes.number.isRequired +}; + +export const stringSettingShape = { + ...settingShape, + value: PropTypes.string +}; + +export const tagSettingShape = { + ...settingShape, + value: PropTypes.arrayOf(PropTypes.number).isRequired +}; + +export default settingShape; diff --git a/frontend/src/Helpers/Props/align.js b/frontend/src/Helpers/Props/align.js new file mode 100644 index 000000000..f381959c6 --- /dev/null +++ b/frontend/src/Helpers/Props/align.js @@ -0,0 +1,5 @@ +export const LEFT = 'left'; +export const CENTER = 'center'; +export const RIGHT = 'right'; + +export const all = [LEFT, CENTER, RIGHT]; diff --git a/frontend/src/Helpers/Props/filterTypes.js b/frontend/src/Helpers/Props/filterTypes.js new file mode 100644 index 000000000..68663ebfc --- /dev/null +++ b/frontend/src/Helpers/Props/filterTypes.js @@ -0,0 +1,17 @@ +export const CONTAINS = 'contains'; +export const EQUAL = 'equal'; +export const GREATER_THAN = 'greaterThan'; +export const GREATER_THAN_OR_EQUAL = 'greaterThanOrEqual'; +export const LESS_THAN = 'lessThan'; +export const LESS_THAN_OR_EQUAL = 'lessThanOrEqual'; +export const NOT_EQUAL = 'notEqual'; + +export const all = [ + CONTAINS, + EQUAL, + GREATER_THAN, + GREATER_THAN_OR_EQUAL, + LESS_THAN, + LESS_THAN_OR_EQUAL, + NOT_EQUAL +]; diff --git a/frontend/src/Helpers/Props/icons.js b/frontend/src/Helpers/Props/icons.js new file mode 100644 index 000000000..65686cc8d --- /dev/null +++ b/frontend/src/Helpers/Props/icons.js @@ -0,0 +1,90 @@ +export const ACTIONS = 'fa fa-bolt'; +export const ACTIVITY = 'fa fa-clock-o'; +export const ADD = 'fa fa-plus'; +export const ALTERNATE_TITLES = 'fa fa-clone'; +export const ADVANCED_SETTINGS = 'fa fa-cog'; +export const ARROW_LEFT = 'fa fa-arrow-circle-left'; +export const ARROW_RIGHT = 'fa fa-arrow-circle-right'; +export const BACKUP = 'fa fa-file-archive-o'; +export const BUG = 'fa fa-bug'; +export const CALENDAR = 'fa fa-calendar'; +export const CALENDAR_O = 'fa fa-calendar-o'; +export const CARET_DOWN = 'fa fa-caret-down'; +export const CHECK = 'fa fa-check'; +export const CHECK_INDETERMINATE = 'fa fa-minus'; +export const CHECK_CIRCLE = 'fa fa-check-circle'; +export const CIRCLE = 'fa fa-circle'; +export const CIRCLE_OUTLINE = 'fa fa-circle-o'; +export const CLEAR = 'fa fa-trash'; +export const CLIPBOARD = 'fa fa-clipboard'; +export const CLOSE = 'fa fa-times'; +export const COLLAPSE = 'fa fa-chevron-circle-up'; +export const COMPUTER = 'fa fa-desktop'; +export const DANGER = 'fa fa-exclamation-circle'; +export const DELETE = 'fa fa-trash'; +export const DOWNLOAD = 'fa fa-download'; +export const DOWNLOADED = 'fa fa-inbox'; +export const DOWNLOADING = 'fa fa-cloud-download'; +export const DRIVE = 'fa fa-hdd-o'; +export const EDIT = 'fa fa-wrench'; +export const EPISODE_FILE = 'fa fa-file-video-o'; +export const EXPAND = 'fa fa-chevron-circle-down'; +export const EXPAND_INDETERMINATE = 'fa fa-chevron-circle-right'; +export const EXTERNAL_LINK = 'fa fa-external-link'; +export const FATAL = 'fa fa-times-circle'; +export const FILE = 'fa fa-file-o'; +export const FILTER = 'fa fa-filter'; +export const FOLDER = 'fa fa-folder-o'; +export const FOLDER_OPEN = 'fa fa-folder-open'; +export const HEALTH = 'fa fa-medkit'; +export const HEART = 'fa fa-heart'; +export const HOUSEKEEPING = 'fa fa-home'; +export const INFO = 'fa fa-info-circle'; +export const INTERACTIVE = 'fa fa-user'; +export const LOGOUT = 'fa fa-sign-out'; +export const MISSING = 'fa fa-exclamation-triangle'; +export const MONITORED = 'fa fa-bookmark'; +export const NETWORK = 'fa fa-signal'; +export const NAVBAR_COLLAPSE = 'fa fa-bars'; +export const NOT_AIRED = 'fa fa-clock-o'; +export const ORGANIZE = 'fa fa-sitemap'; +export const OVERFLOW = 'fa fa-ellipsis-h'; +export const OVERVIEW = 'fa fa-th-list'; +export const PAGE_FIRST = 'fa fa-fast-backward'; +export const PAGE_PREVIOUS = 'fa fa-backward'; +export const PAGE_NEXT = 'fa fa-forward'; +export const PAGE_LAST = 'fa fa-fast-forward'; +export const PARENT = 'fa fa-level-up'; +export const PAUSED = 'fa fa-pause'; +export const PENDING = 'fa fa-clock-o'; +export const PROFILE = 'fa fa-user'; +export const POSTER = 'fa fa-th'; +export const QUEUED = 'fa fa-cloud'; +export const QUICK = 'fa fa-rocket'; +export const REFRESH = 'fa fa-refresh'; +export const REMOVE = 'fa fa-remove'; +export const RESTART = 'fa fa-repeat'; +export const REORDER = 'fa fa-bars'; +export const RSS = 'fa fa-rss'; +export const SAVE = 'fa fa-floppy-o'; +export const SCHEDULED = 'fa fa-clock-o'; +export const SEARCH = 'fa fa-search'; +export const ARTIST_CONTINUING = 'fa fa-play'; +export const ARTIST_ENDED = 'fa fa-stop'; +export const SETTINGS = 'fa fa-cogs'; +export const SHUTDOWN = 'fa fa-power-off'; +export const SORT = 'fa fa-sort'; +export const SORT_ASCENDING = 'fa fa-sort-asc'; +export const SORT_DESCENDING = 'fa fa-sort-desc'; +export const SPIN = 'fa-spin'; +export const SPINNER = 'fa fa-spinner'; +export const SUBTRACT = 'fa fa-minus'; +export const SYSTEM = 'fa fa-laptop'; +export const TAGS = 'fa fa-tags'; +export const TBA = 'fa fa-question-circle'; +export const UNKNOWN = 'fa fa-question'; +export const UNMONITORED = 'fa fa-bookmark-o'; +export const UPDATE = 'fa fa-retweet'; +export const UNSAVED_SETTING = 'fa fa-dot-circle-o'; +export const VIEW = 'fa fa-eye'; +export const WARNING = 'fa fa-exclamation-triangle'; diff --git a/frontend/src/Helpers/Props/index.js b/frontend/src/Helpers/Props/index.js new file mode 100644 index 000000000..0a989a26f --- /dev/null +++ b/frontend/src/Helpers/Props/index.js @@ -0,0 +1,23 @@ +import * as align from './align'; +import * as inputTypes from './inputTypes'; +import * as filterTypes from './filterTypes'; +import * as icons from './icons'; +import * as kinds from './kinds'; +import * as messageTypes from './messageTypes'; +import * as sizes from './sizes'; +import * as scrollDirections from './scrollDirections'; +import * as sortDirections from './sortDirections'; +import * as tooltipPositions from './tooltipPositions'; + +export { + align, + inputTypes, + filterTypes, + icons, + kinds, + messageTypes, + sizes, + scrollDirections, + sortDirections, + tooltipPositions +}; diff --git a/frontend/src/Helpers/Props/inputTypes.js b/frontend/src/Helpers/Props/inputTypes.js new file mode 100644 index 000000000..124f5b88b --- /dev/null +++ b/frontend/src/Helpers/Props/inputTypes.js @@ -0,0 +1,33 @@ +export const CAPTCHA = 'captcha'; +export const CHECK = 'check'; +export const MONITOR_ALBUMS_SELECT = 'monitorAlbumsSelect'; +export const NUMBER = 'number'; +export const OAUTH = 'oauth'; +export const PASSWORD = 'password'; +export const PATH = 'path'; +export const QUALITY_PROFILE_SELECT = 'qualityProfileSelect'; +export const LANGUAGE_PROFILE_SELECT = 'languageProfileSelect'; +export const ROOT_FOLDER_SELECT = 'rootFolderSelect'; +export const SELECT = 'select'; +export const SERIES_TYPE_SELECT = 'artistTypeSelect'; +export const TAG = 'tag'; +export const TEXT = 'text'; +export const TEXT_TAG = 'textTag'; + +export const all = [ + CAPTCHA, + CHECK, + MONITOR_ALBUMS_SELECT, + NUMBER, + OAUTH, + PASSWORD, + PATH, + QUALITY_PROFILE_SELECT, + LANGUAGE_PROFILE_SELECT, + ROOT_FOLDER_SELECT, + SELECT, + SERIES_TYPE_SELECT, + TAG, + TEXT, + TEXT_TAG +]; diff --git a/frontend/src/Helpers/Props/kinds.js b/frontend/src/Helpers/Props/kinds.js new file mode 100644 index 000000000..cb2d5fabe --- /dev/null +++ b/frontend/src/Helpers/Props/kinds.js @@ -0,0 +1,19 @@ +export const DANGER = 'danger'; +export const DEFAULT = 'default'; +export const INFO = 'info'; +export const INVERSE = 'inverse'; +export const PRIMARY = 'primary'; +export const PURPLE = 'purple'; +export const SUCCESS = 'success'; +export const WARNING = 'warning'; + +export const all = [ + DANGER, + DEFAULT, + INFO, + INVERSE, + PRIMARY, + PURPLE, + SUCCESS, + WARNING +]; diff --git a/frontend/src/Helpers/Props/messageTypes.js b/frontend/src/Helpers/Props/messageTypes.js new file mode 100644 index 000000000..997354f9d --- /dev/null +++ b/frontend/src/Helpers/Props/messageTypes.js @@ -0,0 +1,11 @@ +export const ERROR = 'error'; +export const INFO = 'info'; +export const SUCCESS = 'success'; +export const WARNING = 'warning'; + +export const all = [ + ERROR, + INFO, + SUCCESS, + WARNING +]; diff --git a/frontend/src/Helpers/Props/scrollDirections.js b/frontend/src/Helpers/Props/scrollDirections.js new file mode 100644 index 000000000..5e4a4fe08 --- /dev/null +++ b/frontend/src/Helpers/Props/scrollDirections.js @@ -0,0 +1,5 @@ +export const NONE = 'none'; +export const HORIZONTAL = 'horizontal'; +export const VERTICAL = 'vertical'; + +export const all = [NONE, HORIZONTAL, VERTICAL]; diff --git a/frontend/src/Helpers/Props/sizes.js b/frontend/src/Helpers/Props/sizes.js new file mode 100644 index 000000000..e572e8fc8 --- /dev/null +++ b/frontend/src/Helpers/Props/sizes.js @@ -0,0 +1,5 @@ +export const SMALL = 'small'; +export const MEDIUM = 'medium'; +export const LARGE = 'large'; + +export const all = [SMALL, MEDIUM, LARGE]; diff --git a/frontend/src/Helpers/Props/sortDirections.js b/frontend/src/Helpers/Props/sortDirections.js new file mode 100644 index 000000000..ff3b17bb6 --- /dev/null +++ b/frontend/src/Helpers/Props/sortDirections.js @@ -0,0 +1,4 @@ +export const ASCENDING = 'ascending'; +export const DESCENDING = 'descending'; + +export const all = [ASCENDING, DESCENDING]; diff --git a/frontend/src/Helpers/Props/tooltipPositions.js b/frontend/src/Helpers/Props/tooltipPositions.js new file mode 100644 index 000000000..bca3c4ed4 --- /dev/null +++ b/frontend/src/Helpers/Props/tooltipPositions.js @@ -0,0 +1,11 @@ +export const TOP = 'top'; +export const RIGHT = 'right'; +export const BOTTOM = 'bottom'; +export const LEFT = 'left'; + +export const all = [ + TOP, + RIGHT, + BOTTOM, + LEFT +]; diff --git a/frontend/src/Helpers/dragTypes.js b/frontend/src/Helpers/dragTypes.js new file mode 100644 index 000000000..ed6ba080d --- /dev/null +++ b/frontend/src/Helpers/dragTypes.js @@ -0,0 +1,3 @@ +export const QUALITY_PROFILE_ITEM = 'qualityProfileItem'; +export const DELAY_PROFILE = 'delayProfile'; +export const TABLE_COLUMN = 'tableColumn'; diff --git a/frontend/src/Helpers/elementChildren.js b/frontend/src/Helpers/elementChildren.js new file mode 100644 index 000000000..1c10b2f0e --- /dev/null +++ b/frontend/src/Helpers/elementChildren.js @@ -0,0 +1,149 @@ +// https://github.com/react-bootstrap/react-element-children + +import React from 'react'; + +/** + * Iterates through children that are typically specified as `props.children`, + * but only maps over children that are "valid components". + * + * The mapFunction provided index will be normalised to the components mapped, + * so an invalid component would not increase the index. + * + * @param {?*} children Children tree container. + * @param {function(*, int)} func. + * @param {*} context Context for func. + * @return {object} Object containing the ordered map of results. + */ +export function map(children, func, context) { + let index = 0; + + return React.Children.map(children, (child) => { + if (!React.isValidElement(child)) { + return child; + } + + return func.call(context, child, index++); + }); +} + +/** + * Iterates through children that are "valid components". + * + * The provided forEachFunc(child, index) will be called for each + * leaf child with the index reflecting the position relative to "valid components". + * + * @param {?*} children Children tree container. + * @param {function(*, int)} func. + * @param {*} context Context for context. + */ +export function forEach(children, func, context) { + let index = 0; + + React.Children.forEach(children, (child) => { + if (!React.isValidElement(child)) { + return; + } + + func.call(context, child, index++); + }); +} + +/** + * Count the number of "valid components" in the Children container. + * + * @param {?*} children Children tree container. + * @returns {number} + */ +export function count(children) { + let result = 0; + + React.Children.forEach(children, (child) => { + if (!React.isValidElement(child)) { + return; + } + + ++result; + }); + + return result; +} + +/** + * Finds children that are typically specified as `props.children`, + * but only iterates over children that are "valid components". + * + * The provided forEachFunc(child, index) will be called for each + * leaf child with the index reflecting the position relative to "valid components". + * + * @param {?*} children Children tree container. + * @param {function(*, int)} func. + * @param {*} context Context for func. + * @returns {array} of children that meet the func return statement + */ +export function filter(children, func, context) { + const result = []; + + forEach(children, (child, index) => { + if (func.call(context, child, index)) { + result.push(child); + } + }); + + return result; +} + +export function find(children, func, context) { + let result = null; + + forEach(children, (child, index) => { + if (result) { + return; + } + if (func.call(context, child, index)) { + result = child; + } + }); + + return result; +} + +export function every(children, func, context) { + let result = true; + + forEach(children, (child, index) => { + if (!result) { + return; + } + if (!func.call(context, child, index)) { + result = false; + } + }); + + return result; +} + +export function some(children, func, context) { + let result = false; + + forEach(children, (child, index) => { + if (result) { + return; + } + + if (func.call(context, child, index)) { + result = true; + } + }); + + return result; +} + +export function toArray(children) { + const result = []; + + forEach(children, (child) => { + result.push(child); + }); + + return result; +} diff --git a/frontend/src/Helpers/getDisplayName.js b/frontend/src/Helpers/getDisplayName.js new file mode 100644 index 000000000..512702c87 --- /dev/null +++ b/frontend/src/Helpers/getDisplayName.js @@ -0,0 +1,3 @@ +export default function getDisplayName(Component) { + return Component.displayName || Component.name || 'Component'; +} diff --git a/frontend/src/InteractiveImport/Album/SelectAlbumModal.js b/frontend/src/InteractiveImport/Album/SelectAlbumModal.js new file mode 100644 index 000000000..d4f26f4ff --- /dev/null +++ b/frontend/src/InteractiveImport/Album/SelectAlbumModal.js @@ -0,0 +1,37 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import Modal from 'Components/Modal/Modal'; +import SelectAlbumModalContentConnector from './SelectAlbumModalContentConnector'; + +class SelectAlbumModal extends Component { + + // + // Render + + render() { + const { + isOpen, + onModalClose, + ...otherProps + } = this.props; + + return ( + + + + ); + } +} + +SelectAlbumModal.propTypes = { + isOpen: PropTypes.bool.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default SelectAlbumModal; diff --git a/frontend/src/InteractiveImport/Album/SelectAlbumModalContent.js b/frontend/src/InteractiveImport/Album/SelectAlbumModalContent.js new file mode 100644 index 000000000..c10abb588 --- /dev/null +++ b/frontend/src/InteractiveImport/Album/SelectAlbumModalContent.js @@ -0,0 +1,66 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import Button from 'Components/Link/Button'; +import ModalContent from 'Components/Modal/ModalContent'; +import ModalHeader from 'Components/Modal/ModalHeader'; +import ModalBody from 'Components/Modal/ModalBody'; +import ModalFooter from 'Components/Modal/ModalFooter'; +import LoadingIndicator from 'Components/Loading/LoadingIndicator'; +import SelectAlbumRow from './SelectAlbumRow'; + +class SelectAlbumModalContent extends Component { + + // + // Render + + render() { + const { + items, + onAlbumSelect, + onModalClose, + isFetching + } = this.props; + + return ( + + + Manual Import - Select Album + + + + { + isFetching && + + } + { + items.map((item) => { + return ( + + ); + }) + } + + + + + + + ); + } +} + +SelectAlbumModalContent.propTypes = { + items: PropTypes.arrayOf(PropTypes.object).isRequired, + isFetching: PropTypes.bool.isRequired, + onAlbumSelect: PropTypes.func.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default SelectAlbumModalContent; diff --git a/frontend/src/InteractiveImport/Album/SelectAlbumModalContentConnector.js b/frontend/src/InteractiveImport/Album/SelectAlbumModalContentConnector.js new file mode 100644 index 000000000..8694a5485 --- /dev/null +++ b/frontend/src/InteractiveImport/Album/SelectAlbumModalContentConnector.js @@ -0,0 +1,101 @@ +import _ from 'lodash'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { createSelector } from 'reselect'; +import connectSection from 'Store/connectSection'; +import { + updateInteractiveImportItem, + fetchInteractiveImportAlbums, + setInteractiveImportAlbumsSort, + clearInteractiveImportAlbums +} from 'Store/Actions/interactiveImportActions'; +import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector'; +import SelectAlbumModalContent from './SelectAlbumModalContent'; + +function createMapStateToProps() { + return createSelector( + createClientSideCollectionSelector(), + (episodes) => { + return episodes; + } + ); +} + +const mapDispatchToProps = { + fetchInteractiveImportAlbums, + setInteractiveImportAlbumsSort, + clearInteractiveImportAlbums, + updateInteractiveImportItem +}; + +class SelectAlbumModalContentConnector extends Component { + + // + // Lifecycle + + componentDidMount() { + const { + artistId + } = this.props; + + this.props.fetchInteractiveImportAlbums({ artistId }); + } + + componentWillUnmount() { + // This clears the albums for the queue and hides the queue + // We'll need another place to store albums for manual import + this.props.clearInteractiveImportAlbums(); + } + + // + // Listeners + + onSortPress = (sortKey, sortDirection) => { + this.props.setInteractiveImportAlbumsSort({ sortKey, sortDirection }); + } + + onAlbumSelect = (albumId) => { + const album = _.find(this.props.items, { id: albumId }); + + this.props.ids.forEach((id) => { + this.props.updateInteractiveImportItem({ + id, + album, + tracks: [] + }); + }); + + this.props.onModalClose(true); + } + + // + // Render + + render() { + return ( + + ); + } +} + +SelectAlbumModalContentConnector.propTypes = { + ids: PropTypes.arrayOf(PropTypes.number).isRequired, + artistId: PropTypes.number.isRequired, + items: PropTypes.arrayOf(PropTypes.object).isRequired, + fetchInteractiveImportAlbums: PropTypes.func.isRequired, + setInteractiveImportAlbumsSort: PropTypes.func.isRequired, + clearInteractiveImportAlbums: PropTypes.func.isRequired, + updateInteractiveImportItem: PropTypes.func.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default connectSection( + createMapStateToProps, + mapDispatchToProps, + undefined, + undefined, + { section: 'interactiveImport.interactiveImportAlbums' } +)(SelectAlbumModalContentConnector); diff --git a/frontend/src/InteractiveImport/Album/SelectAlbumRow.css b/frontend/src/InteractiveImport/Album/SelectAlbumRow.css new file mode 100644 index 000000000..c43d879f4 --- /dev/null +++ b/frontend/src/InteractiveImport/Album/SelectAlbumRow.css @@ -0,0 +1,4 @@ +.season { + padding: 8px; + border-bottom: 1px solid $borderColor; +} diff --git a/frontend/src/InteractiveImport/Album/SelectAlbumRow.js b/frontend/src/InteractiveImport/Album/SelectAlbumRow.js new file mode 100644 index 000000000..eb163a7c2 --- /dev/null +++ b/frontend/src/InteractiveImport/Album/SelectAlbumRow.js @@ -0,0 +1,37 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import Link from 'Components/Link/Link'; +import styles from './SelectAlbumRow.css'; + +class SelectAlbumRow extends Component { + + // + // Listeners + + onPress = () => { + this.props.onAlbumSelect(this.props.id); + } + + // + // Render + + render() { + return ( + + {this.props.title} + + ); + } +} + +SelectAlbumRow.propTypes = { + id: PropTypes.number.isRequired, + title: PropTypes.string.isRequired, + onAlbumSelect: PropTypes.func.isRequired +}; + +export default SelectAlbumRow; diff --git a/frontend/src/InteractiveImport/Artist/SelectArtistModal.js b/frontend/src/InteractiveImport/Artist/SelectArtistModal.js new file mode 100644 index 000000000..39dd67300 --- /dev/null +++ b/frontend/src/InteractiveImport/Artist/SelectArtistModal.js @@ -0,0 +1,37 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import Modal from 'Components/Modal/Modal'; +import SelectArtistModalContentConnector from './SelectArtistModalContentConnector'; + +class SelectArtistModal extends Component { + + // + // Render + + render() { + const { + isOpen, + onModalClose, + ...otherProps + } = this.props; + + return ( + + + + ); + } +} + +SelectArtistModal.propTypes = { + isOpen: PropTypes.bool.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default SelectArtistModal; diff --git a/frontend/src/InteractiveImport/Artist/SelectArtistModalContent.css b/frontend/src/InteractiveImport/Artist/SelectArtistModalContent.css new file mode 100644 index 000000000..d297be072 --- /dev/null +++ b/frontend/src/InteractiveImport/Artist/SelectArtistModalContent.css @@ -0,0 +1,18 @@ +.modalBody { + composes: modalBody from 'Components/Modal/ModalBody.css'; + + display: flex; + flex: 1 1 auto; + flex-direction: column; +} + +.filterInput { + composes: text from 'Components/Form/TextInput.css'; + + flex: 0 0 auto; + margin-bottom: 20px; +} + +.scroller { + flex: 1 1 auto; +} diff --git a/frontend/src/InteractiveImport/Artist/SelectArtistModalContent.js b/frontend/src/InteractiveImport/Artist/SelectArtistModalContent.js new file mode 100644 index 000000000..b180d319b --- /dev/null +++ b/frontend/src/InteractiveImport/Artist/SelectArtistModalContent.js @@ -0,0 +1,99 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { scrollDirections } from 'Helpers/Props'; +import Button from 'Components/Link/Button'; +import Scroller from 'Components/Scroller/Scroller'; +import TextInput from 'Components/Form/TextInput'; +import ModalContent from 'Components/Modal/ModalContent'; +import ModalHeader from 'Components/Modal/ModalHeader'; +import ModalBody from 'Components/Modal/ModalBody'; +import ModalFooter from 'Components/Modal/ModalFooter'; +import SelectArtistRow from './SelectArtistRow'; +import styles from './SelectArtistModalContent.css'; + +class SelectArtistModalContent extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + filter: '' + }; + } + + // + // Listeners + + onFilterChange = ({ value }) => { + this.setState({ filter: value.toLowerCase() }); + } + + // + // Render + + render() { + const { + items, + onArtistSelect, + onModalClose + } = this.props; + + const filter = this.state.filter; + + return ( + + + Manual Import - Select Artist + + + + + + + { + items.map((item) => { + return item.artistName.toLowerCase().includes(filter) ? + ( + + ) : + null; + }) + } + + + + + + + + ); + } +} + +SelectArtistModalContent.propTypes = { + items: PropTypes.arrayOf(PropTypes.object).isRequired, + onArtistSelect: PropTypes.func.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default SelectArtistModalContent; diff --git a/frontend/src/InteractiveImport/Artist/SelectArtistModalContentConnector.js b/frontend/src/InteractiveImport/Artist/SelectArtistModalContentConnector.js new file mode 100644 index 000000000..78c5631f0 --- /dev/null +++ b/frontend/src/InteractiveImport/Artist/SelectArtistModalContentConnector.js @@ -0,0 +1,65 @@ +import _ from 'lodash'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import { updateInteractiveImportItem } from 'Store/Actions/interactiveImportActions'; +import createAllArtistSelector from 'Store/Selectors/createAllArtistSelector'; +import SelectArtistModalContent from './SelectArtistModalContent'; + +function createMapStateToProps() { + return createSelector( + createAllArtistSelector(), + (items) => { + return { + items + }; + } + ); +} + +const mapDispatchToProps = { + updateInteractiveImportItem +}; + +class SelectArtistModalContentConnector extends Component { + + // + // Listeners + + onArtistSelect = (artistId) => { + const artist = _.find(this.props.items, { id: artistId }); + + this.props.ids.forEach((id) => { + this.props.updateInteractiveImportItem({ + id, + artist, + album: undefined, + tracks: [] + }); + }); + + this.props.onModalClose(true); + } + + // + // Render + + render() { + return ( + + ); + } +} + +SelectArtistModalContentConnector.propTypes = { + ids: PropTypes.arrayOf(PropTypes.number).isRequired, + items: PropTypes.arrayOf(PropTypes.object).isRequired, + updateInteractiveImportItem: PropTypes.func.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default connect(createMapStateToProps, mapDispatchToProps)(SelectArtistModalContentConnector); diff --git a/frontend/src/InteractiveImport/Artist/SelectArtistRow.css b/frontend/src/InteractiveImport/Artist/SelectArtistRow.css new file mode 100644 index 000000000..376c3fe84 --- /dev/null +++ b/frontend/src/InteractiveImport/Artist/SelectArtistRow.css @@ -0,0 +1,4 @@ +.artist { + padding: 8px; + border-bottom: 1px solid $borderColor; +} diff --git a/frontend/src/InteractiveImport/Artist/SelectArtistRow.js b/frontend/src/InteractiveImport/Artist/SelectArtistRow.js new file mode 100644 index 000000000..dcf252bb6 --- /dev/null +++ b/frontend/src/InteractiveImport/Artist/SelectArtistRow.js @@ -0,0 +1,37 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import Link from 'Components/Link/Link'; +import styles from './SelectArtistRow.css'; + +class SelectArtistRow extends Component { + + // + // Listeners + + onPress = () => { + this.props.onArtistSelect(this.props.id); + } + + // + // Render + + render() { + return ( + + {this.props.artistName} + + ); + } +} + +SelectArtistRow.propTypes = { + id: PropTypes.number.isRequired, + artistName: PropTypes.string.isRequired, + onArtistSelect: PropTypes.func.isRequired +}; + +export default SelectArtistRow; diff --git a/frontend/src/InteractiveImport/Folder/InteractiveImportSelectFolderModalContent.css b/frontend/src/InteractiveImport/Folder/InteractiveImportSelectFolderModalContent.css new file mode 100644 index 000000000..86418a2dd --- /dev/null +++ b/frontend/src/InteractiveImport/Folder/InteractiveImportSelectFolderModalContent.css @@ -0,0 +1,24 @@ +.recentFoldersContainer { + margin-top: 15px; +} + +.buttonsContainer { + margin-top: 30px; +} + +.buttonContainer { + display: flex; + justify-content: center; + + margin-top: 10px; +} + +.button { + composes: button from 'Components/Link/Button.css'; + + width: 300px; +} + +.buttonIcon { + margin-right: 5px; +} diff --git a/frontend/src/InteractiveImport/Folder/InteractiveImportSelectFolderModalContent.js b/frontend/src/InteractiveImport/Folder/InteractiveImportSelectFolderModalContent.js new file mode 100644 index 000000000..cabd33d7c --- /dev/null +++ b/frontend/src/InteractiveImport/Folder/InteractiveImportSelectFolderModalContent.js @@ -0,0 +1,161 @@ +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 PathInputConnector from 'Components/Form/PathInputConnector'; +import ModalContent from 'Components/Modal/ModalContent'; +import ModalHeader from 'Components/Modal/ModalHeader'; +import ModalBody from 'Components/Modal/ModalBody'; +import ModalFooter from 'Components/Modal/ModalFooter'; +import Table from 'Components/Table/Table'; +import TableBody from 'Components/Table/TableBody'; +import RecentFolderRow from './RecentFolderRow'; +import styles from './InteractiveImportSelectFolderModalContent.css'; + +const recentFoldersColumns = [ + { + name: 'folder', + label: 'Folder' + }, + { + name: 'lastUsed', + label: 'Last Used' + } +]; + +class InteractiveImportSelectFolderModalContent extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + folder: '' + }; + } + + // + // Listeners + + onPathChange = ({ value }) => { + this.setState({ folder: value }); + } + + onRecentPathPress = (folder) => { + this.setState({ folder }); + } + + onQuickImportPress = () => { + this.props.onQuickImportPress(this.state.folder); + } + + onInteractiveImportPress = () => { + this.props.onInteractiveImportPress(this.state.folder); + } + + // + // Render + + render() { + const { + recentFolders, + onModalClose + } = this.props; + + const folder = this.state.folder; + + return ( + + + Manual Import - Select Folder + + + + + + { + !!recentFolders.length && +
+ + + { + recentFolders.map((recentFolder) => { + return ( + + ); + }) + } + +
+
+ } + +
+
+ +
+ +
+ +
+
+
+ + + + +
+ ); + } +} + +InteractiveImportSelectFolderModalContent.propTypes = { + recentFolders: PropTypes.arrayOf(PropTypes.object).isRequired, + onQuickImportPress: PropTypes.func.isRequired, + onInteractiveImportPress: PropTypes.func.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default InteractiveImportSelectFolderModalContent; diff --git a/frontend/src/InteractiveImport/Folder/InteractiveImportSelectFolderModalContentConnector.js b/frontend/src/InteractiveImport/Folder/InteractiveImportSelectFolderModalContentConnector.js new file mode 100644 index 000000000..c838f3bab --- /dev/null +++ b/frontend/src/InteractiveImport/Folder/InteractiveImportSelectFolderModalContentConnector.js @@ -0,0 +1,73 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import { addRecentFolder } from 'Store/Actions/interactiveImportActions'; +import { executeCommand } from 'Store/Actions/commandActions'; +import * as commandNames from 'Commands/commandNames'; +import InteractiveImportSelectFolderModalContent from './InteractiveImportSelectFolderModalContent'; + +function createMapStateToProps() { + return createSelector( + (state) => state.interactiveImport.recentFolders, + (recentFolders) => { + return { + recentFolders + }; + } + ); +} + +const mapDispatchToProps = { + addRecentFolder, + executeCommand +}; + +class InteractiveImportSelectFolderModalContentConnector extends Component { + + // + // Listeners + + onQuickImportPress = (folder) => { + this.props.addRecentFolder({ folder }); + + this.props.executeCommand({ + name: commandNames.DOWNLOADED_ALBUMS_SCAN, + path: folder + }); + + this.props.onModalClose(); + } + + onInteractiveImportPress = (folder) => { + this.props.addRecentFolder({ folder }); + this.props.onFolderSelect(folder); + } + + // + // Render + + render() { + if (this.path) { + return null; + } + + return ( + + ); + } +} + +InteractiveImportSelectFolderModalContentConnector.propTypes = { + path: PropTypes.string, + onFolderSelect: PropTypes.func.isRequired, + onModalClose: PropTypes.func.isRequired, + addRecentFolder: PropTypes.func.isRequired, + executeCommand: PropTypes.func.isRequired +}; + +export default connect(createMapStateToProps, mapDispatchToProps)(InteractiveImportSelectFolderModalContentConnector); diff --git a/frontend/src/InteractiveImport/Folder/RecentFolderRow.js b/frontend/src/InteractiveImport/Folder/RecentFolderRow.js new file mode 100644 index 000000000..bc32f5749 --- /dev/null +++ b/frontend/src/InteractiveImport/Folder/RecentFolderRow.js @@ -0,0 +1,41 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import TableRowButton from 'Components/Table/TableRowButton'; +import TableRowCell from 'Components/Table/Cells/TableRowCell'; +import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector'; + +class RecentFolderRow extends Component { + + // + // Listeners + + onPress = () => { + this.props.onPress(this.props.folder); + } + + // + // Render + + render() { + const { + folder, + lastUsed + } = this.props; + + return ( + + {folder} + + + + ); + } +} + +RecentFolderRow.propTypes = { + folder: PropTypes.string.isRequired, + lastUsed: PropTypes.string.isRequired, + onPress: PropTypes.func.isRequired +}; + +export default RecentFolderRow; diff --git a/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContent.css b/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContent.css new file mode 100644 index 000000000..f85538fdb --- /dev/null +++ b/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContent.css @@ -0,0 +1,63 @@ +.footer { + composes: modalFooter from 'Components/Modal/ModalFooter.css'; + + justify-content: space-between; + padding: 15px; +} + +.leftButtons, +.centerButtons, +.rightButtons { + display: flex; + flex: 1 0 33%; + flex-wrap: wrap; +} + +.centerButtons { + justify-content: center; +} + +.rightButtons { + justify-content: flex-end; +} + +.importMode { + composes: select from 'Components/Form/SelectInput.css'; + + width: auto; +} + +.errorMessage { + color: $dangerColor; +} + +@media only screen and (max-width: $breakpointSmall) { + .footer { + .leftButtons, + .centerButtons, + .rightButtons { + flex-direction: column; + } + + .leftButtons { + align-items: flex-start; + } + + .centerButtons { + align-items: center; + } + + .rightButtons { + align-items: flex-end; + } + + a, + button { + margin-left: 0; + + &:first-child { + margin-bottom: 5px; + } + } + } +} diff --git a/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContent.js b/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContent.js new file mode 100644 index 000000000..7c44f9963 --- /dev/null +++ b/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContent.js @@ -0,0 +1,326 @@ +import _ from 'lodash'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import getSelectedIds from 'Utilities/Table/getSelectedIds'; +import selectAll from 'Utilities/Table/selectAll'; +import toggleSelected from 'Utilities/Table/toggleSelected'; +import { icons, kinds } from 'Helpers/Props'; +import Button from 'Components/Link/Button'; +import Icon from 'Components/Icon'; +import LoadingIndicator from 'Components/Loading/LoadingIndicator'; +import SelectInput from 'Components/Form/SelectInput'; +import ModalContent from 'Components/Modal/ModalContent'; +import ModalHeader from 'Components/Modal/ModalHeader'; +import ModalBody from 'Components/Modal/ModalBody'; +import ModalFooter from 'Components/Modal/ModalFooter'; +import Table from 'Components/Table/Table'; +import TableBody from 'Components/Table/TableBody'; +import SelectArtistModal from 'InteractiveImport/Artist/SelectArtistModal'; +import SelectAlbumModal from 'InteractiveImport/Album/SelectAlbumModal'; +import InteractiveImportRow from './InteractiveImportRow'; +import styles from './InteractiveImportModalContent.css'; + +const columns = [ + { + name: 'relativePath', + label: 'Relative Path', + isSortable: true, + isVisible: true + }, + { + name: 'artist', + label: 'Artist', + isSortable: true, + isVisible: true + }, + { + name: 'album', + label: 'Album', + isVisible: true + }, + { + name: 'tracks', + label: 'Track(s)', + isVisible: true + }, + { + name: 'quality', + label: 'Quality', + isSortable: true, + isVisible: true + }, + { + name: 'language', + label: 'Language', + isSortable: true, + isVisible: true + }, + { + name: 'size', + label: 'Size', + isVisible: true + }, + { + name: 'rejections', + label: React.createElement(Icon, { + name: icons.DANGER, + kind: kinds.DANGER + }), + isVisible: true + } +]; + +class InteractiveImportModalContent extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + allSelected: false, + allUnselected: false, + lastToggled: null, + selectedState: {}, + invalidRowsSelected: [], + isSelectArtistModalOpen: false, + isSelectAlbumModalOpen: false + }; + } + + // + // Control + + getSelectedIds = () => { + return getSelectedIds(this.state.selectedState); + } + + // + // Listeners + + onSelectAllChange = ({ value }) => { + this.setState(selectAll(this.state.selectedState, value)); + } + + onSelectedChange = ({ id, value, shiftKey = false }) => { + this.setState((state) => { + return toggleSelected(state, this.props.items, id, value, shiftKey); + }); + } + + onValidRowChange = (id, isValid) => { + this.setState((state) => { + if (isValid) { + return { + invalidRowsSelected: _.without(state.invalidRowsSelected, id) + }; + } + + return { + invalidRowsSelected: [...state.invalidRowsSelected, id] + }; + }); + } + + onImportSelectedPress = () => { + const selected = this.getSelectedIds(); + + this.props.onImportSelectedPress(selected, this.state.importMode); + } + + onImportModeChange = ({ value }) => { + this.props.onImportModeChange(value); + } + + onSelectArtistPress = () => { + this.setState({ isSelectArtistModalOpen: true }); + } + + onSelectAlbumPress = () => { + this.setState({ isSelectAlbumModalOpen: true }); + } + + onSelectArtistModalClose = () => { + this.setState({ isSelectArtistModalOpen: false }); + } + + onSelectAlbumModalClose = () => { + this.setState({ isSelectAlbumModalOpen: false }); + } + + // + // Render + + render() { + const { + downloadId, + title, + folder, + isFetching, + isPopulated, + error, + items, + sortKey, + sortDirection, + importMode, + interactiveImportErrorMessage, + onSortPress, + onModalClose + } = this.props; + + const { + allSelected, + allUnselected, + selectedState, + invalidRowsSelected, + isSelectArtistModalOpen, + isSelectAlbumModalOpen + } = this.state; + + const selectedIds = this.getSelectedIds(); + const selectedItem = selectedIds.length ? _.find(items, { id: selectedIds[0] }) : null; + const errorMessage = error && error.message || 'Unable to load manual import items'; + + const importModeOptions = [ + { key: 'move', value: 'Move Files' }, + { key: 'copy', value: 'Copy Files' } + ]; + + return ( + + + Manual Import - {title || folder} + + + + { + isFetching && + + } + + { + error && +
{errorMessage}
+ } + + { + isPopulated && !!items.length && + + + { + items.map((item) => { + return ( + + ); + }) + } + +
+ } + + { + isPopulated && !items.length && + 'No audio files were found in the selected folder' + } +
+ + + { + !downloadId && +
+ +
+ } + +
+ + + +
+ +
+ + + { + interactiveImportErrorMessage && + {interactiveImportErrorMessage} + } + + +
+
+ + + + +
+ ); + } +} + +InteractiveImportModalContent.propTypes = { + downloadId: PropTypes.string, + importMode: PropTypes.string.isRequired, + title: PropTypes.string, + folder: PropTypes.string, + isFetching: PropTypes.bool.isRequired, + isPopulated: PropTypes.bool.isRequired, + error: PropTypes.object, + items: PropTypes.arrayOf(PropTypes.object).isRequired, + sortKey: PropTypes.string, + sortDirection: PropTypes.string, + interactiveImportErrorMessage: PropTypes.string, + onSortPress: PropTypes.func.isRequired, + onImportModeChange: PropTypes.func.isRequired, + onImportSelectedPress: PropTypes.func.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +InteractiveImportModalContent.defaultProps = { + importMode: 'move' +}; + +export default InteractiveImportModalContent; diff --git a/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContentConnector.js b/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContentConnector.js new file mode 100644 index 000000000..fd07437f1 --- /dev/null +++ b/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContentConnector.js @@ -0,0 +1,155 @@ +import _ from 'lodash'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { createSelector } from 'reselect'; +import connectSection from 'Store/connectSection'; +import { fetchInteractiveImportItems, setInteractiveImportSort, clearInteractiveImport, setInteractiveImportMode } from 'Store/Actions/interactiveImportActions'; +import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector'; +import { executeCommand } from 'Store/Actions/commandActions'; +import * as commandNames from 'Commands/commandNames'; +import InteractiveImportModalContent from './InteractiveImportModalContent'; + +function createMapStateToProps() { + return createSelector( + createClientSideCollectionSelector(), + (interactiveImport) => { + return interactiveImport; + } + ); +} + +const mapDispatchToProps = { + fetchInteractiveImportItems, + setInteractiveImportSort, + setInteractiveImportMode, + clearInteractiveImport, + executeCommand +}; + +class InteractiveImportModalContentConnector extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + interactiveImportErrorMessage: null + }; + } + + componentDidMount() { + const { + downloadId, + folder + } = this.props; + + this.props.fetchInteractiveImportItems({ downloadId, folder }); + } + + componentWillUnmount() { + this.props.clearInteractiveImport(); + } + + // + // Listeners + + onSortPress = (sortKey, sortDirection) => { + this.props.setInteractiveImportSort({ sortKey, sortDirection }); + } + + onImportModeChange = (importMode) => { + this.props.setInteractiveImportMode({ importMode }); + } + + onImportSelectedPress = (selected, importMode) => { + const files = []; + + _.forEach(this.props.items, (item) => { + const isSelected = selected.indexOf(item.id) > -1; + + if (isSelected) { + const { + artist, + album, + tracks, + quality, + language + } = item; + + if (!artist) { + this.setState({ interactiveImportErrorMessage: 'Artist must be chosen for each selected file' }); + return false; + } + + if (!album) { + this.setState({ interactiveImportErrorMessage: 'Album must be chosen for each selected file' }); + return false; + } + + if (!tracks || !tracks.length) { + this.setState({ interactiveImportErrorMessage: 'One or more tracks must be chosen for each selected file' }); + return false; + } + + files.push({ + path: item.path, + artistId: artist.id, + albumId: album.id, + trackIds: _.map(tracks, 'id'), + quality, + language, + downloadId: this.props.downloadId + }); + } + }); + + if (!files.length) { + return; + } + + this.props.executeCommand({ + name: commandNames.INTERACTIVE_IMPORT, + files, + importMode + }); + + this.props.onModalClose(); + } + + // + // Render + + render() { + return ( + + ); + } +} + +InteractiveImportModalContentConnector.propTypes = { + downloadId: PropTypes.string, + folder: PropTypes.string, + items: PropTypes.arrayOf(PropTypes.object).isRequired, + fetchInteractiveImportItems: PropTypes.func.isRequired, + setInteractiveImportSort: PropTypes.func.isRequired, + clearInteractiveImport: PropTypes.func.isRequired, + setInteractiveImportMode: PropTypes.func.isRequired, + executeCommand: PropTypes.func.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default connectSection( + createMapStateToProps, + mapDispatchToProps, + undefined, + undefined, + { section: 'interactiveImport' } +)(InteractiveImportModalContentConnector); diff --git a/frontend/src/InteractiveImport/Interactive/InteractiveImportRow.css b/frontend/src/InteractiveImport/Interactive/InteractiveImportRow.css new file mode 100644 index 000000000..22234718f --- /dev/null +++ b/frontend/src/InteractiveImport/Interactive/InteractiveImportRow.css @@ -0,0 +1,11 @@ +.relativePath { + composes: cell from 'Components/Table/Cells/TableRowCell.css'; + + word-break: break-all; +} + +.quality { + composes: cell from 'Components/Table/Cells/TableRowCell.css'; + + text-align: center; +} diff --git a/frontend/src/InteractiveImport/Interactive/InteractiveImportRow.js b/frontend/src/InteractiveImport/Interactive/InteractiveImportRow.js new file mode 100644 index 000000000..366c72aa8 --- /dev/null +++ b/frontend/src/InteractiveImport/Interactive/InteractiveImportRow.js @@ -0,0 +1,348 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import formatBytes from 'Utilities/Number/formatBytes'; +import hasDifferentItems from 'Utilities/Object/hasDifferentItems'; +import { icons, kinds, tooltipPositions } from 'Helpers/Props'; +import Icon from 'Components/Icon'; +import TableRow from 'Components/Table/TableRow'; +import TableRowCell from 'Components/Table/Cells/TableRowCell'; +import TableRowCellButton from 'Components/Table/Cells/TableRowCellButton'; +import TableSelectCell from 'Components/Table/Cells/TableSelectCell'; +import Popover from 'Components/Tooltip/Popover'; +import EpisodeQuality from 'Album/EpisodeQuality'; +import EpisodeLanguage from 'Album/EpisodeLanguage'; +import SelectArtistModal from 'InteractiveImport/Artist/SelectArtistModal'; +import SelectAlbumModal from 'InteractiveImport/Album/SelectAlbumModal'; +import SelectTrackModal from 'InteractiveImport/Track/SelectTrackModal'; +import SelectQualityModal from 'InteractiveImport/Quality/SelectQualityModal'; +import SelectLanguageModal from 'InteractiveImport/Language/SelectLanguageModal'; +import InteractiveImportRowCellPlaceholder from './InteractiveImportRowCellPlaceholder'; +import styles from './InteractiveImportRow.css'; + +class InteractiveImportRow extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + isSelectArtistModalOpen: false, + isSelectAlbumModalOpen: false, + isSelectTrackModalOpen: false, + isSelectQualityModalOpen: false, + isSelectLanguageModalOpen: false + }; + } + + componentDidMount() { + const { + id, + artist, + album, + tracks, + quality, + language + } = this.props; + + if ( + artist && + album != null && + tracks.length && + quality && + language + ) { + this.props.onSelectedChange({ id, value: true }); + } + } + + componentDidUpdate(prevProps) { + const { + id, + artist, + album, + tracks, + quality, + language, + isSelected, + onValidRowChange + } = this.props; + + if ( + prevProps.artist === artist && + prevProps.album === album && + !hasDifferentItems(prevProps.tracks, tracks) && + prevProps.quality === quality && + prevProps.language === language && + prevProps.isSelected === isSelected + ) { + return; + } + + const isValid = !!( + artist && + album && + tracks.length && + quality && + language + ); + + if (isSelected && !isValid) { + onValidRowChange(id, false); + } else { + onValidRowChange(id, true); + } + } + + // + // Control + + selectRowAfterChange = (value) => { + const { + id, + isSelected + } = this.props; + + if (!isSelected && value === true) { + this.props.onSelectedChange({ id, value }); + } + } + + // + // Listeners + + onSelectArtistPress = () => { + this.setState({ isSelectArtistModalOpen: true }); + } + + onSelectAlbumPress = () => { + this.setState({ isSelectAlbumModalOpen: true }); + } + + onSelectTrackPress = () => { + this.setState({ isSelectTrackModalOpen: true }); + } + + onSelectQualityPress = () => { + this.setState({ isSelectQualityModalOpen: true }); + } + + onSelectLanguagePress = () => { + this.setState({ isSelectLanguageModalOpen: true }); + } + + onSelectArtistModalClose = (changed) => { + this.setState({ isSelectArtistModalOpen: false }); + this.selectRowAfterChange(changed); + } + + onSelectAlbumModalClose = (changed) => { + this.setState({ isSelectAlbumModalOpen: false }); + this.selectRowAfterChange(changed); + } + + onSelectTrackModalClose = (changed) => { + this.setState({ isSelectTrackModalOpen: false }); + this.selectRowAfterChange(changed); + } + + onSelectQualityModalClose = (changed) => { + this.setState({ isSelectQualityModalOpen: false }); + this.selectRowAfterChange(changed); + } + + onSelectLanguageModalClose = (changed) => { + this.setState({ isSelectLanguageModalOpen: false }); + this.selectRowAfterChange(changed); + } + + // + // Render + + render() { + const { + id, + relativePath, + artist, + album, + tracks, + quality, + language, + size, + rejections, + isSelected, + onSelectedChange + } = this.props; + + const { + isSelectArtistModalOpen, + isSelectAlbumModalOpen, + isSelectTrackModalOpen, + isSelectQualityModalOpen, + isSelectLanguageModalOpen + } = this.state; + + const artistName = artist ? artist.artistName : ''; + const albumTitle = album ? album.title : ''; + const trackNumbers = tracks.map((track) => track.trackNumber) + .join(', '); + + const showArtistPlaceholder = isSelected && !artist; + const showAlbumNumberPlaceholder = isSelected && !!artist && !album; + const showTrackNumbersPlaceholder = isSelected && !!album && !tracks.length; + + return ( + + + + + {relativePath} + + + + { + showArtistPlaceholder ? : artistName + } + + + + { + showAlbumNumberPlaceholder ? : albumTitle + } + + + + { + showTrackNumbersPlaceholder ? : trackNumbers + } + + + + + + + + + + + + {formatBytes(size)} + + + + { + !!rejections.length && + + } + title="Release Rejected" + body={ +
    + { + rejections.map((rejection, index) => { + return ( +
  • + {rejection.reason} +
  • + ); + }) + } +
+ } + position={tooltipPositions.LEFT} + /> + } +
+ + + + + + + + 1} + real={quality.revision.real > 0} + onModalClose={this.onSelectQualityModalClose} + /> + + +
+ ); + } + +} + +InteractiveImportRow.propTypes = { + id: PropTypes.number.isRequired, + relativePath: PropTypes.string.isRequired, + artist: PropTypes.object, + album: PropTypes.object, + tracks: PropTypes.arrayOf(PropTypes.object).isRequired, + quality: PropTypes.object, + language: PropTypes.object, + size: PropTypes.number.isRequired, + rejections: PropTypes.arrayOf(PropTypes.object).isRequired, + isSelected: PropTypes.bool, + onSelectedChange: PropTypes.func.isRequired, + onValidRowChange: PropTypes.func.isRequired +}; + +InteractiveImportRow.defaultProps = { + tracks: [] +}; + +export default InteractiveImportRow; diff --git a/frontend/src/InteractiveImport/Interactive/InteractiveImportRowCellPlaceholder.css b/frontend/src/InteractiveImport/Interactive/InteractiveImportRowCellPlaceholder.css new file mode 100644 index 000000000..941988144 --- /dev/null +++ b/frontend/src/InteractiveImport/Interactive/InteractiveImportRowCellPlaceholder.css @@ -0,0 +1,7 @@ +.placeholder { + display: inline-block; + margin: -8px 0; + width: 100%; + height: 25px; + border: 2px dashed $dangerColor; +} diff --git a/frontend/src/InteractiveImport/Interactive/InteractiveImportRowCellPlaceholder.js b/frontend/src/InteractiveImport/Interactive/InteractiveImportRowCellPlaceholder.js new file mode 100644 index 000000000..b6744d156 --- /dev/null +++ b/frontend/src/InteractiveImport/Interactive/InteractiveImportRowCellPlaceholder.js @@ -0,0 +1,10 @@ +import React from 'react'; +import styles from './InteractiveImportRowCellPlaceholder.css'; + +function InteractiveImportRowCellPlaceholder() { + return ( + + ); +} + +export default InteractiveImportRowCellPlaceholder; diff --git a/frontend/src/InteractiveImport/InteractiveImportModal.js b/frontend/src/InteractiveImport/InteractiveImportModal.js new file mode 100644 index 000000000..0ea6fd9cb --- /dev/null +++ b/frontend/src/InteractiveImport/InteractiveImportModal.js @@ -0,0 +1,78 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import Modal from 'Components/Modal/Modal'; +import InteractiveImportSelectFolderModalContentConnector from './Folder/InteractiveImportSelectFolderModalContentConnector'; +import InteractiveImportModalContentConnector from './Interactive/InteractiveImportModalContentConnector'; + +class InteractiveImportModal extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + folder: null + }; + } + + componentDidUpdate(prevProps) { + if (prevProps.isOpen && !this.props.isOpen) { + this.setState({ folder: null }); + } + } + + // + // Listeners + + onFolderSelect = (folder) => { + this.setState({ folder }); + } + + // + // Render + + render() { + const { + isOpen, + folder, + downloadId, + onModalClose, + ...otherProps + } = this.props; + + const folderPath = folder || this.state.folder; + + return ( + + { + folderPath || downloadId ? + : + + } + + ); + } +} + +InteractiveImportModal.propTypes = { + isOpen: PropTypes.bool.isRequired, + folder: PropTypes.string, + downloadId: PropTypes.string, + onModalClose: PropTypes.func.isRequired +}; + +export default InteractiveImportModal; diff --git a/frontend/src/InteractiveImport/Language/SelectLanguageModal.js b/frontend/src/InteractiveImport/Language/SelectLanguageModal.js new file mode 100644 index 000000000..938d26a6d --- /dev/null +++ b/frontend/src/InteractiveImport/Language/SelectLanguageModal.js @@ -0,0 +1,37 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import Modal from 'Components/Modal/Modal'; +import SelectLanguageModalContentConnector from './SelectLanguageModalContentConnector'; + +class SelectLanguageModal extends Component { + + // + // Render + + render() { + const { + isOpen, + onModalClose, + ...otherProps + } = this.props; + + return ( + + + + ); + } +} + +SelectLanguageModal.propTypes = { + isOpen: PropTypes.bool.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default SelectLanguageModal; diff --git a/frontend/src/InteractiveImport/Language/SelectLanguageModalContent.js b/frontend/src/InteractiveImport/Language/SelectLanguageModalContent.js new file mode 100644 index 000000000..ff99ce6bf --- /dev/null +++ b/frontend/src/InteractiveImport/Language/SelectLanguageModalContent.js @@ -0,0 +1,87 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import { inputTypes } from 'Helpers/Props'; +import Button from 'Components/Link/Button'; +import LoadingIndicator from 'Components/Loading/LoadingIndicator'; +import Form from 'Components/Form/Form'; +import FormGroup from 'Components/Form/FormGroup'; +import FormLabel from 'Components/Form/FormLabel'; +import FormInputGroup from 'Components/Form/FormInputGroup'; +import ModalContent from 'Components/Modal/ModalContent'; +import ModalHeader from 'Components/Modal/ModalHeader'; +import ModalBody from 'Components/Modal/ModalBody'; +import ModalFooter from 'Components/Modal/ModalFooter'; + +function SelectLanguageModalContent(props) { + const { + languageId, + isFetching, + isPopulated, + error, + items, + onModalClose, + onLanguageSelect + } = props; + + const languageOptions = items.map(({ language }) => { + return { + key: language.id, + value: language.name + }; + }); + + return ( + + + Manual Import - Select Language + + + + { + isFetching && + + } + + { + !isFetching && !!error && +
Unable to load languages
+ } + + { + isPopulated && !error && +
+ + Language + + + +
+ } +
+ + + + +
+ ); +} + +SelectLanguageModalContent.propTypes = { + languageId: PropTypes.number.isRequired, + isFetching: PropTypes.bool.isRequired, + isPopulated: PropTypes.bool.isRequired, + error: PropTypes.object, + items: PropTypes.arrayOf(PropTypes.object).isRequired, + onLanguageSelect: PropTypes.func.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default SelectLanguageModalContent; diff --git a/frontend/src/InteractiveImport/Language/SelectLanguageModalContentConnector.js b/frontend/src/InteractiveImport/Language/SelectLanguageModalContentConnector.js new file mode 100644 index 000000000..4a9988b68 --- /dev/null +++ b/frontend/src/InteractiveImport/Language/SelectLanguageModalContentConnector.js @@ -0,0 +1,87 @@ +import _ from 'lodash'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import { fetchLanguageProfileSchema } from 'Store/Actions/settingsActions'; +import { updateInteractiveImportItem } from 'Store/Actions/interactiveImportActions'; +import SelectLanguageModalContent from './SelectLanguageModalContent'; + +function createMapStateToProps() { + return createSelector( + (state) => state.settings.languageProfiles, + (languageProfiles) => { + const { + isFetchingSchema: isFetching, + schemaPopulated: isPopulated, + schemaError: error, + schema + } = languageProfiles; + + return { + isFetching, + isPopulated, + error, + items: schema.languages || [] + }; + } + ); +} + +const mapDispatchToProps = { + fetchLanguageProfileSchema, + updateInteractiveImportItem +}; + +class SelectLanguageModalContentConnector extends Component { + + // + // Lifecycle + + componentDidMount = () => { + if (!this.props.isPopulated) { + this.props.fetchLanguageProfileSchema(); + } + } + + // + // Listeners + + onLanguageSelect = ({ value }) => { + const languageId = parseInt(value); + const language = _.find(this.props.items, + (item) => item.language.id === languageId).language; + + this.props.updateInteractiveImportItem({ + id: this.props.id, + language + }); + + this.props.onModalClose(true); + } + + // + // Render + + render() { + return ( + + ); + } +} + +SelectLanguageModalContentConnector.propTypes = { + id: PropTypes.number.isRequired, + isFetching: PropTypes.bool.isRequired, + isPopulated: PropTypes.bool.isRequired, + error: PropTypes.object, + items: PropTypes.arrayOf(PropTypes.object).isRequired, + fetchLanguageProfileSchema: PropTypes.func.isRequired, + updateInteractiveImportItem: PropTypes.func.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default connect(createMapStateToProps, mapDispatchToProps)(SelectLanguageModalContentConnector); diff --git a/frontend/src/InteractiveImport/Quality/SelectQualityModal.js b/frontend/src/InteractiveImport/Quality/SelectQualityModal.js new file mode 100644 index 000000000..d3e31d2dd --- /dev/null +++ b/frontend/src/InteractiveImport/Quality/SelectQualityModal.js @@ -0,0 +1,37 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import Modal from 'Components/Modal/Modal'; +import SelectQualityModalContentConnector from './SelectQualityModalContentConnector'; + +class SelectQualityModal extends Component { + + // + // Render + + render() { + const { + isOpen, + onModalClose, + ...otherProps + } = this.props; + + return ( + + + + ); + } +} + +SelectQualityModal.propTypes = { + isOpen: PropTypes.bool.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default SelectQualityModal; diff --git a/frontend/src/InteractiveImport/Quality/SelectQualityModalContent.js b/frontend/src/InteractiveImport/Quality/SelectQualityModalContent.js new file mode 100644 index 000000000..8649763a9 --- /dev/null +++ b/frontend/src/InteractiveImport/Quality/SelectQualityModalContent.js @@ -0,0 +1,166 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { inputTypes, kinds } from 'Helpers/Props'; +import Button from 'Components/Link/Button'; +import LoadingIndicator from 'Components/Loading/LoadingIndicator'; +import Form from 'Components/Form/Form'; +import FormGroup from 'Components/Form/FormGroup'; +import FormLabel from 'Components/Form/FormLabel'; +import FormInputGroup from 'Components/Form/FormInputGroup'; +import ModalContent from 'Components/Modal/ModalContent'; +import ModalHeader from 'Components/Modal/ModalHeader'; +import ModalBody from 'Components/Modal/ModalBody'; +import ModalFooter from 'Components/Modal/ModalFooter'; + +class SelectQualityModalContent extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + const { + qualityId, + proper, + real + } = props; + + this.state = { + qualityId, + proper, + real + }; + } + + // + // Listeners + + onQualityChange = ({ value }) => { + this.setState({ qualityId: parseInt(value) }); + } + + onProperChange = ({ value }) => { + this.setState({ proper: value }); + } + + onRealChange = ({ value }) => { + this.setState({ real: value }); + } + + onQualitySelect = () => { + this.props.onQualitySelect(this.state); + } + + // + // Render + + render() { + const { + isFetching, + isPopulated, + error, + items, + onModalClose + } = this.props; + + const { + qualityId, + proper, + real + } = this.state; + + const qualityOptions = items.map(({ quality }) => { + return { + key: quality.id, + value: quality.name + }; + }); + + return ( + + + Manual Import - Select Quality + + + + { + isFetching && + + } + + { + !isFetching && !!error && +
Unable to load qualities
+ } + + { + isPopulated && !error && +
+ + Quality + + + + + + Proper + + + + + + Real + + + +
+ } +
+ + + + + + +
+ ); + } +} + +SelectQualityModalContent.propTypes = { + qualityId: PropTypes.number.isRequired, + proper: PropTypes.bool.isRequired, + real: PropTypes.bool.isRequired, + isFetching: PropTypes.bool.isRequired, + isPopulated: PropTypes.bool.isRequired, + error: PropTypes.object, + items: PropTypes.arrayOf(PropTypes.object).isRequired, + onQualitySelect: PropTypes.func.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default SelectQualityModalContent; diff --git a/frontend/src/InteractiveImport/Quality/SelectQualityModalContentConnector.js b/frontend/src/InteractiveImport/Quality/SelectQualityModalContentConnector.js new file mode 100644 index 000000000..ac5b42a7b --- /dev/null +++ b/frontend/src/InteractiveImport/Quality/SelectQualityModalContentConnector.js @@ -0,0 +1,94 @@ +import _ from 'lodash'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import { fetchQualityProfileSchema } from 'Store/Actions/settingsActions'; +import { updateInteractiveImportItem } from 'Store/Actions/interactiveImportActions'; +import SelectQualityModalContent from './SelectQualityModalContent'; + +function createMapStateToProps() { + return createSelector( + (state) => state.settings.qualityProfiles, + (qualityProfiles) => { + const { + isFetchingSchema: isFetching, + schemaPopulated: isPopulated, + schemaError: error, + schema + } = qualityProfiles; + + return { + isFetching, + isPopulated, + error, + items: schema.items || [] + }; + } + ); +} + +const mapDispatchToProps = { + fetchQualityProfileSchema, + updateInteractiveImportItem +}; + +class SelectQualityModalContentConnector extends Component { + + // + // Lifecycle + + componentDidMount = () => { + if (!this.props.isPopulated) { + this.props.fetchQualityProfileSchema(); + } + } + + // + // Listeners + + onQualitySelect = ({ qualityId, proper, real }) => { + const quality = _.find(this.props.items, + (item) => item.quality.id === qualityId).quality; + + const revision = { + version: proper ? 2 : 1, + real: real ? 1 : 0 + }; + + this.props.updateInteractiveImportItem({ + id: this.props.id, + quality: { + quality, + revision + } + }); + + this.props.onModalClose(true); + } + + // + // Render + + render() { + return ( + + ); + } +} + +SelectQualityModalContentConnector.propTypes = { + id: PropTypes.number.isRequired, + isFetching: PropTypes.bool.isRequired, + isPopulated: PropTypes.bool.isRequired, + error: PropTypes.object, + items: PropTypes.arrayOf(PropTypes.object).isRequired, + fetchQualityProfileSchema: PropTypes.func.isRequired, + updateInteractiveImportItem: PropTypes.func.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default connect(createMapStateToProps, mapDispatchToProps)(SelectQualityModalContentConnector); diff --git a/frontend/src/InteractiveImport/Track/SelectTrackModal.js b/frontend/src/InteractiveImport/Track/SelectTrackModal.js new file mode 100644 index 000000000..f8c9c4160 --- /dev/null +++ b/frontend/src/InteractiveImport/Track/SelectTrackModal.js @@ -0,0 +1,37 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import Modal from 'Components/Modal/Modal'; +import SelectTrackModalContentConnector from './SelectTrackModalContentConnector'; + +class SelectTrackModal extends Component { + + // + // Render + + render() { + const { + isOpen, + onModalClose, + ...otherProps + } = this.props; + + return ( + + + + ); + } +} + +SelectTrackModal.propTypes = { + isOpen: PropTypes.bool.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default SelectTrackModal; diff --git a/frontend/src/InteractiveImport/Track/SelectTrackModalContent.js b/frontend/src/InteractiveImport/Track/SelectTrackModalContent.js new file mode 100644 index 000000000..519ea930d --- /dev/null +++ b/frontend/src/InteractiveImport/Track/SelectTrackModalContent.js @@ -0,0 +1,177 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import getSelectedIds from 'Utilities/Table/getSelectedIds'; +import selectAll from 'Utilities/Table/selectAll'; +import toggleSelected from 'Utilities/Table/toggleSelected'; +import { kinds } from 'Helpers/Props'; +import Button from 'Components/Link/Button'; +import LoadingIndicator from 'Components/Loading/LoadingIndicator'; +import ModalContent from 'Components/Modal/ModalContent'; +import ModalHeader from 'Components/Modal/ModalHeader'; +import ModalBody from 'Components/Modal/ModalBody'; +import ModalFooter from 'Components/Modal/ModalFooter'; +import Table from 'Components/Table/Table'; +import TableBody from 'Components/Table/TableBody'; +import SelectTrackRow from './SelectTrackRow'; + +const columns = [ + { + name: 'trackNumber', + label: '#', + isSortable: true, + isVisible: true + }, + { + name: 'title', + label: 'Title', + isVisible: true + } +]; + +class SelectTrackModalContent extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + allSelected: false, + allUnselected: false, + lastToggled: null, + selectedState: {} + }; + } + + // + // Control + + getSelectedIds = () => { + return getSelectedIds(this.state.selectedState); + } + + // + // Listeners + + onSelectAllChange = ({ value }) => { + this.setState(selectAll(this.state.selectedState, value)); + } + + onSelectedChange = ({ id, value, shiftKey = false }) => { + this.setState((state) => { + return toggleSelected(state, this.props.items, id, value, shiftKey); + }); + } + + onTracksSelect = () => { + this.props.onTracksSelect(this.getSelectedIds()); + } + + // + // Render + + render() { + const { + isFetching, + isPopulated, + error, + items, + sortKey, + sortDirection, + onSortPress, + onModalClose + } = this.props; + + const { + allSelected, + allUnselected, + selectedState + } = this.state; + + const errorMessage = error && error.message || 'Unable to load tracks'; + + return ( + + + Manual Import - Select Track(s) + + + + { + isFetching && + + } + + { + error && +
{errorMessage}
+ } + + { + isPopulated && !!items.length && + + + { + items.map((item) => { + return ( + + ); + }) + } + +
+ } + + { + isPopulated && !items.length && + 'No tracks were found for the selected album' + } +
+ + + + + + +
+ ); + } +} + +SelectTrackModalContent.propTypes = { + isFetching: PropTypes.bool.isRequired, + isPopulated: PropTypes.bool.isRequired, + error: PropTypes.object, + items: PropTypes.arrayOf(PropTypes.object).isRequired, + sortKey: PropTypes.string, + sortDirection: PropTypes.string, + onSortPress: PropTypes.func.isRequired, + onTracksSelect: PropTypes.func.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default SelectTrackModalContent; diff --git a/frontend/src/InteractiveImport/Track/SelectTrackModalContentConnector.js b/frontend/src/InteractiveImport/Track/SelectTrackModalContentConnector.js new file mode 100644 index 000000000..52cf7c705 --- /dev/null +++ b/frontend/src/InteractiveImport/Track/SelectTrackModalContentConnector.js @@ -0,0 +1,103 @@ +import _ from 'lodash'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { createSelector } from 'reselect'; +import connectSection from 'Store/connectSection'; +import { fetchTracks, setTracksSort, clearTracks } from 'Store/Actions/trackActions'; +import { updateInteractiveImportItem } from 'Store/Actions/interactiveImportActions'; +import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector'; +import SelectTrackModalContent from './SelectTrackModalContent'; + +function createMapStateToProps() { + return createSelector( + createClientSideCollectionSelector(), + (tracks) => { + return tracks; + } + ); +} + +const mapDispatchToProps = { + fetchTracks, + setTracksSort, + clearTracks, + updateInteractiveImportItem +}; + +class SelectTrackModalContentConnector extends Component { + + // + // Lifecycle + + componentDidMount() { + const { + artistId, + albumId + } = this.props; + + this.props.fetchTracks({ artistId, albumId }); + } + + componentWillUnmount() { + // This clears the tracks for the queue and hides the queue + // We'll need another place to store tracks for manual import + this.props.clearTracks(); + } + + // + // Listeners + + onSortPress = (sortKey, sortDirection) => { + this.props.setTracksSort({ sortKey, sortDirection }); + } + + onTracksSelect = (trackIds) => { + const tracks = _.reduce(this.props.items, (acc, item) => { + if (trackIds.indexOf(item.id) > -1) { + acc.push(item); + } + + return acc; + }, []); + + this.props.updateInteractiveImportItem({ + id: this.props.id, + tracks: _.sortBy(tracks, 'trackNumber') + }); + + this.props.onModalClose(true); + } + + // + // Render + + render() { + return ( + + ); + } +} + +SelectTrackModalContentConnector.propTypes = { + id: PropTypes.number.isRequired, + artistId: PropTypes.number.isRequired, + albumId: PropTypes.number.isRequired, + items: PropTypes.arrayOf(PropTypes.object).isRequired, + fetchTracks: PropTypes.func.isRequired, + setTracksSort: PropTypes.func.isRequired, + clearTracks: PropTypes.func.isRequired, + updateInteractiveImportItem: PropTypes.func.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default connectSection( + createMapStateToProps, + mapDispatchToProps, + undefined, + undefined, + { section: 'tracks' } + )(SelectTrackModalContentConnector); diff --git a/frontend/src/InteractiveImport/Track/SelectTrackRow.js b/frontend/src/InteractiveImport/Track/SelectTrackRow.js new file mode 100644 index 000000000..fda48c917 --- /dev/null +++ b/frontend/src/InteractiveImport/Track/SelectTrackRow.js @@ -0,0 +1,62 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import TableRowButton from 'Components/Table/TableRowButton'; +import TableRowCell from 'Components/Table/Cells/TableRowCell'; +import TableSelectCell from 'Components/Table/Cells/TableSelectCell'; + +class SelectTrackRow extends Component { + + // + // Listeners + + onPress = () => { + const { + id, + isSelected + } = this.props; + + this.props.onSelectedChange({ id, value: !isSelected }); + } + + // + // Render + + render() { + const { + id, + trackNumber, + title, + isSelected, + onSelectedChange + } = this.props; + + return ( + + + + + {trackNumber} + + + + {title} + + + + ); + } +} + +SelectTrackRow.propTypes = { + id: PropTypes.number.isRequired, + trackNumber: PropTypes.number.isRequired, + title: PropTypes.string.isRequired, + isSelected: PropTypes.bool, + onSelectedChange: PropTypes.func.isRequired +}; + +export default SelectTrackRow; diff --git a/frontend/src/Organize/OrganizePreviewModal.js b/frontend/src/Organize/OrganizePreviewModal.js new file mode 100644 index 000000000..647f4ddf8 --- /dev/null +++ b/frontend/src/Organize/OrganizePreviewModal.js @@ -0,0 +1,34 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import Modal from 'Components/Modal/Modal'; +import OrganizePreviewModalContentConnector from './OrganizePreviewModalContentConnector'; + +function OrganizePreviewModal(props) { + const { + isOpen, + onModalClose, + ...otherProps + } = props; + + return ( + + { + isOpen && + + } + + ); +} + +OrganizePreviewModal.propTypes = { + isOpen: PropTypes.bool.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default OrganizePreviewModal; diff --git a/frontend/src/Organize/OrganizePreviewModalConnector.js b/frontend/src/Organize/OrganizePreviewModalConnector.js new file mode 100644 index 000000000..ace733c86 --- /dev/null +++ b/frontend/src/Organize/OrganizePreviewModalConnector.js @@ -0,0 +1,39 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { clearOrganizePreview } from 'Store/Actions/organizePreviewActions'; +import OrganizePreviewModal from './OrganizePreviewModal'; + +const mapDispatchToProps = { + clearOrganizePreview +}; + +class OrganizePreviewModalConnector extends Component { + + // + // Listeners + + onModalClose = () => { + this.props.clearOrganizePreview(); + this.props.onModalClose(); + } + + // + // Render + + render() { + return ( + + ); + } +} + +OrganizePreviewModalConnector.propTypes = { + clearOrganizePreview: PropTypes.func.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default connect(undefined, mapDispatchToProps)(OrganizePreviewModalConnector); diff --git a/frontend/src/Organize/OrganizePreviewModalContent.css b/frontend/src/Organize/OrganizePreviewModalContent.css new file mode 100644 index 000000000..eb1711522 --- /dev/null +++ b/frontend/src/Organize/OrganizePreviewModalContent.css @@ -0,0 +1,24 @@ +.path { + margin-left: 5px; + font-weight: bold; +} + +.trackFormat { + margin-left: 5px; + font-family: $monoSpaceFontFamily; +} + +.previews { + margin-top: 10px; +} + +.selectAllInputContainer { + margin-right: auto; + line-height: 30px; +} + +.selectAllInput { + composes: input from 'Components/Form/CheckInput.css'; + + margin: 0; +} diff --git a/frontend/src/Organize/OrganizePreviewModalContent.js b/frontend/src/Organize/OrganizePreviewModalContent.js new file mode 100644 index 000000000..3bb724cfa --- /dev/null +++ b/frontend/src/Organize/OrganizePreviewModalContent.js @@ -0,0 +1,200 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import getSelectedIds from 'Utilities/Table/getSelectedIds'; +import selectAll from 'Utilities/Table/selectAll'; +import toggleSelected from 'Utilities/Table/toggleSelected'; +import { kinds } from 'Helpers/Props'; +import Alert from 'Components/Alert'; +import Button from 'Components/Link/Button'; +import LoadingIndicator from 'Components/Loading/LoadingIndicator'; +import ModalContent from 'Components/Modal/ModalContent'; +import ModalHeader from 'Components/Modal/ModalHeader'; +import ModalBody from 'Components/Modal/ModalBody'; +import ModalFooter from 'Components/Modal/ModalFooter'; +import CheckInput from 'Components/Form/CheckInput'; +import OrganizePreviewRow from './OrganizePreviewRow'; +import styles from './OrganizePreviewModalContent.css'; + +function getValue(allSelected, allUnselected) { + if (allSelected) { + return true; + } else if (allUnselected) { + return false; + } + + return null; +} + +class OrganizePreviewModalContent extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + allSelected: false, + allUnselected: false, + lastToggled: null, + selectedState: {} + }; + } + + // + // Control + + getSelectedIds = () => { + return getSelectedIds(this.state.selectedState); + } + + // + // Listeners + + onSelectAllChange = ({ value }) => { + this.setState(selectAll(this.state.selectedState, value)); + } + + onSelectedChange = ({ id, value, shiftKey = false }) => { + this.setState((state) => { + return toggleSelected(state, this.props.items, id, value, shiftKey); + }); + } + + onOrganizePress = () => { + this.props.onOrganizePress(this.getSelectedIds()); + } + + // + // Render + + render() { + const { + isFetching, + isPopulated, + error, + items, + renameTracks, + trackFormat, + path, + onModalClose + } = this.props; + + const { + allSelected, + allUnselected, + selectedState + } = this.state; + + const selectAllValue = getValue(allSelected, allUnselected); + + return ( + + + Organize & Rename + + + + { + isFetching && + + } + + { + !isFetching && error && +
Error loading previews
+ } + + { + !isFetching && isPopulated && !items.length && +
+ { + renameTracks ? +
Success! My work is done, no files to rename.
: +
Renaming is disabled, nothing to rename
+ } +
+ } + + { + !isFetching && isPopulated && !!items.length && +
+ +
+ All paths are relative to: + + {path} + +
+ +
+ Naming pattern: + + {trackFormat} + +
+
+ +
+ { + items.map((item) => { + return ( + + ); + }) + } +
+
+ } +
+ + + { + isPopulated && !!items.length && + + } + + + + + +
+ ); + } +} + +OrganizePreviewModalContent.propTypes = { + isFetching: PropTypes.bool.isRequired, + isPopulated: PropTypes.bool.isRequired, + error: PropTypes.object, + items: PropTypes.arrayOf(PropTypes.object).isRequired, + path: PropTypes.string.isRequired, + renameTracks: PropTypes.bool, + trackFormat: PropTypes.string, + onOrganizePress: PropTypes.func.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default OrganizePreviewModalContent; diff --git a/frontend/src/Organize/OrganizePreviewModalContentConnector.js b/frontend/src/Organize/OrganizePreviewModalContentConnector.js new file mode 100644 index 000000000..edaf425ee --- /dev/null +++ b/frontend/src/Organize/OrganizePreviewModalContentConnector.js @@ -0,0 +1,91 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import createArtistSelector from 'Store/Selectors/createArtistSelector'; +import { fetchOrganizePreview } from 'Store/Actions/organizePreviewActions'; +import { fetchNamingSettings } from 'Store/Actions/settingsActions'; +import { executeCommand } from 'Store/Actions/commandActions'; +import * as commandNames from 'Commands/commandNames'; +import OrganizePreviewModalContent from './OrganizePreviewModalContent'; + +function createMapStateToProps() { + return createSelector( + (state) => state.organizePreview, + (state) => state.settings.naming, + createArtistSelector(), + (organizePreview, naming, artist) => { + const props = { ...organizePreview }; + props.isFetching = organizePreview.isFetching || naming.isFetching; + props.isPopulated = organizePreview.isPopulated && naming.isPopulated; + props.error = organizePreview.error || naming.error; + props.renameTracks = naming.item.renameTracks; + props.trackFormat = naming.item['standardTrackFormat']; + props.path = artist.path; + + return props; + } + ); +} + +const mapDispatchToProps = { + fetchOrganizePreview, + fetchNamingSettings, + executeCommand +}; + +class OrganizePreviewModalContentConnector extends Component { + + // + // Lifecycle + + componentDidMount() { + const { + artistId, + albumId + } = this.props; + + this.props.fetchOrganizePreview({ + artistId, + albumId + }); + + this.props.fetchNamingSettings(); + } + + // + // Listeners + + onOrganizePress = (files) => { + this.props.executeCommand({ + name: commandNames.RENAME_FILES, + artistId: this.props.artistId, + files + }); + + this.props.onModalClose(); + } + + // + // Render + + render() { + return ( + + ); + } +} + +OrganizePreviewModalContentConnector.propTypes = { + artistId: PropTypes.number.isRequired, + albumId: PropTypes.number, + fetchOrganizePreview: PropTypes.func.isRequired, + fetchNamingSettings: PropTypes.func.isRequired, + executeCommand: PropTypes.func.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default connect(createMapStateToProps, mapDispatchToProps)(OrganizePreviewModalContentConnector); diff --git a/frontend/src/Organize/OrganizePreviewRow.css b/frontend/src/Organize/OrganizePreviewRow.css new file mode 100644 index 000000000..1b3c8ca47 --- /dev/null +++ b/frontend/src/Organize/OrganizePreviewRow.css @@ -0,0 +1,20 @@ +.row { + display: flex; + margin-bottom: 5px; + padding: 5px 0; + border-bottom: 1px solid $borderColor; + + &:last-of-type { + margin-bottom: 0; + padding-bottom: 0; + border-bottom: none; + } +} + +.selectedContainer { + margin-right: 30px; +} + +.path { + margin-left: 10px; +} diff --git a/frontend/src/Organize/OrganizePreviewRow.js b/frontend/src/Organize/OrganizePreviewRow.js new file mode 100644 index 000000000..340232a98 --- /dev/null +++ b/frontend/src/Organize/OrganizePreviewRow.js @@ -0,0 +1,90 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { icons, kinds } from 'Helpers/Props'; +import Icon from 'Components/Icon'; +import CheckInput from 'Components/Form/CheckInput'; +import styles from './OrganizePreviewRow.css'; + +class OrganizePreviewRow extends Component { + + // + // Lifecycle + + componentDidMount() { + const { + id, + onSelectedChange + } = this.props; + + onSelectedChange({ id, value: true }); + } + + // + // Listeners + + onSelectedChange = ({ value, shiftKey }) => { + const { + id, + onSelectedChange + } = this.props; + + onSelectedChange({ id, value, shiftKey }); + } + + // + // Render + + render() { + const { + id, + existingPath, + newPath, + isSelected + } = this.props; + + return ( +
+ + +
+
+ + + + {existingPath} + +
+ +
+ + + + {newPath} + +
+
+
+ ); + } +} + +OrganizePreviewRow.propTypes = { + id: PropTypes.number.isRequired, + existingPath: PropTypes.string.isRequired, + newPath: PropTypes.string.isRequired, + isSelected: PropTypes.bool, + onSelectedChange: PropTypes.func.isRequired +}; + +export default OrganizePreviewRow; diff --git a/frontend/src/Settings/DownloadClients/DownloadClientSettings.js b/frontend/src/Settings/DownloadClients/DownloadClientSettings.js new file mode 100644 index 000000000..414887fd5 --- /dev/null +++ b/frontend/src/Settings/DownloadClients/DownloadClientSettings.js @@ -0,0 +1,65 @@ +import React, { Component } from 'react'; +import PageContent from 'Components/Page/PageContent'; +import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector'; +import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector'; +import DownloadClientsConnector from './DownloadClients/DownloadClientsConnector'; +import DownloadClientOptionsConnector from './Options/DownloadClientOptionsConnector'; +import RemotePathMappingsConnector from './RemotePathMappings/RemotePathMappingsConnector'; + +class DownloadClientSettings extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + hasPendingChanges: false + }; + } + + // + // Listeners + + setDownloadClientOptionsRef = (ref) => { + this._downloadClientOptions = ref; + } + + onHasPendingChange = (hasPendingChanges) => { + this.setState({ + hasPendingChanges + }); + } + + onSavePress = () => { + this._downloadClientOptions.getWrappedInstance().save(); + } + + // + // Render + + render() { + return ( + + + + + + + + + + + + ); + } +} + +export default DownloadClientSettings; diff --git a/frontend/src/Settings/DownloadClients/DownloadClients/AddDownloadClientItem.css b/frontend/src/Settings/DownloadClients/DownloadClients/AddDownloadClientItem.css new file mode 100644 index 000000000..e1032ddef --- /dev/null +++ b/frontend/src/Settings/DownloadClients/DownloadClients/AddDownloadClientItem.css @@ -0,0 +1,44 @@ +.downloadClient { + composes: card from 'Components/Card.css'; + + position: relative; + width: 300px; + height: 100px; +} + +.underlay { + @add-mixin cover; +} + +.overlay { + @add-mixin linkOverlay; + + padding: 10px; +} + +.name { + text-align: center; + font-weight: lighter; + font-size: 24px; +} + +.actions { + margin-top: 20px; + text-align: right; +} + +.presetsMenu { + composes: menu from 'Components/Menu/Menu.css'; + + display: inline-block; + margin: 0 5px; +} + +.presetsMenuButton { + composes: button from 'Components/Link/Button.css'; + + &::after { + margin-left: 5px; + content: '\25BE'; + } +} diff --git a/frontend/src/Settings/DownloadClients/DownloadClients/AddDownloadClientItem.js b/frontend/src/Settings/DownloadClients/DownloadClients/AddDownloadClientItem.js new file mode 100644 index 000000000..3a2265d28 --- /dev/null +++ b/frontend/src/Settings/DownloadClients/DownloadClients/AddDownloadClientItem.js @@ -0,0 +1,110 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { sizes } from 'Helpers/Props'; +import Button from 'Components/Link/Button'; +import Link from 'Components/Link/Link'; +import Menu from 'Components/Menu/Menu'; +import MenuContent from 'Components/Menu/MenuContent'; +import AddDownloadClientPresetMenuItem from './AddDownloadClientPresetMenuItem'; +import styles from './AddDownloadClientItem.css'; + +class AddDownloadClientItem extends Component { + + // + // Listeners + + onDownloadClientSelect = () => { + const { + implementation + } = this.props; + + this.props.onDownloadClientSelect({ implementation }); + } + + // + // Render + + render() { + const { + implementation, + implementationName, + infoLink, + presets, + onDownloadClientSelect + } = this.props; + + const hasPresets = !!presets && !!presets.length; + + return ( +
+ + +
+
+ {implementationName} +
+ +
+ { + hasPresets && + + + + + + + + { + presets.map((preset) => { + return ( + + ); + }) + } + + + + } + + +
+
+
+ ); + } +} + +AddDownloadClientItem.propTypes = { + implementation: PropTypes.string.isRequired, + implementationName: PropTypes.string.isRequired, + infoLink: PropTypes.string.isRequired, + presets: PropTypes.arrayOf(PropTypes.object), + onDownloadClientSelect: PropTypes.func.isRequired +}; + +export default AddDownloadClientItem; diff --git a/frontend/src/Settings/DownloadClients/DownloadClients/AddDownloadClientModal.js b/frontend/src/Settings/DownloadClients/DownloadClients/AddDownloadClientModal.js new file mode 100644 index 000000000..0c21e7dbd --- /dev/null +++ b/frontend/src/Settings/DownloadClients/DownloadClients/AddDownloadClientModal.js @@ -0,0 +1,25 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import Modal from 'Components/Modal/Modal'; +import AddDownloadClientModalContentConnector from './AddDownloadClientModalContentConnector'; + +function AddDownloadClientModal({ isOpen, onModalClose, ...otherProps }) { + return ( + + + + ); +} + +AddDownloadClientModal.propTypes = { + isOpen: PropTypes.bool.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default AddDownloadClientModal; diff --git a/frontend/src/Settings/DownloadClients/DownloadClients/AddDownloadClientModalContent.css b/frontend/src/Settings/DownloadClients/DownloadClients/AddDownloadClientModalContent.css new file mode 100644 index 000000000..b4d5c6787 --- /dev/null +++ b/frontend/src/Settings/DownloadClients/DownloadClients/AddDownloadClientModalContent.css @@ -0,0 +1,5 @@ +.downloadClients { + display: flex; + justify-content: center; + flex-wrap: wrap; +} diff --git a/frontend/src/Settings/DownloadClients/DownloadClients/AddDownloadClientModalContent.js b/frontend/src/Settings/DownloadClients/DownloadClients/AddDownloadClientModalContent.js new file mode 100644 index 000000000..6ba496c45 --- /dev/null +++ b/frontend/src/Settings/DownloadClients/DownloadClients/AddDownloadClientModalContent.js @@ -0,0 +1,115 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { kinds } from 'Helpers/Props'; +import Alert from 'Components/Alert'; +import Button from 'Components/Link/Button'; +import FieldSet from 'Components/FieldSet'; +import LoadingIndicator from 'Components/Loading/LoadingIndicator'; +import ModalContent from 'Components/Modal/ModalContent'; +import ModalHeader from 'Components/Modal/ModalHeader'; +import ModalBody from 'Components/Modal/ModalBody'; +import ModalFooter from 'Components/Modal/ModalFooter'; +import AddDownloadClientItem from './AddDownloadClientItem'; +import styles from './AddDownloadClientModalContent.css'; + +class AddDownloadClientModalContent extends Component { + + // + // Render + + render() { + const { + isFetching, + error, + isPopulated, + usenetDownloadClients, + torrentDownloadClients, + onDownloadClientSelect, + onModalClose + } = this.props; + + return ( + + + Add DownloadClient + + + + { + isFetching && + + } + + { + !isFetching && !!error && +
Unable to add a new downloadClient, please try again.
+ } + + { + isPopulated && !error && +
+ + +
Lidarr supports any downloadClient that uses the Newznab standard, as well as other downloadClients listed below.
+
For more information on the individual downloadClients, clink on the info buttons.
+
+ +
+
+ { + usenetDownloadClients.map((downloadClient) => { + return ( + + ); + }) + } +
+
+ +
+
+ { + torrentDownloadClients.map((downloadClient) => { + return ( + + ); + }) + } +
+
+
+ } +
+ + + +
+ ); + } +} + +AddDownloadClientModalContent.propTypes = { + isFetching: PropTypes.bool.isRequired, + error: PropTypes.object, + isPopulated: PropTypes.bool.isRequired, + usenetDownloadClients: PropTypes.arrayOf(PropTypes.object).isRequired, + torrentDownloadClients: PropTypes.arrayOf(PropTypes.object).isRequired, + onDownloadClientSelect: PropTypes.func.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default AddDownloadClientModalContent; diff --git a/frontend/src/Settings/DownloadClients/DownloadClients/AddDownloadClientModalContentConnector.js b/frontend/src/Settings/DownloadClients/DownloadClients/AddDownloadClientModalContentConnector.js new file mode 100644 index 000000000..d6015b934 --- /dev/null +++ b/frontend/src/Settings/DownloadClients/DownloadClients/AddDownloadClientModalContentConnector.js @@ -0,0 +1,75 @@ +import _ from 'lodash'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import { fetchDownloadClientSchema, selectDownloadClientSchema } from 'Store/Actions/settingsActions'; +import AddDownloadClientModalContent from './AddDownloadClientModalContent'; + +function createMapStateToProps() { + return createSelector( + (state) => state.settings.downloadClients, + (downloadClients) => { + const { + isFetching, + error, + isPopulated, + schema + } = downloadClients; + + const usenetDownloadClients = _.filter(schema, { protocol: 'usenet' }); + const torrentDownloadClients = _.filter(schema, { protocol: 'torrent' }); + + return { + isFetching, + error, + isPopulated, + usenetDownloadClients, + torrentDownloadClients + }; + } + ); +} + +const mapDispatchToProps = { + fetchDownloadClientSchema, + selectDownloadClientSchema +}; + +class AddDownloadClientModalContentConnector extends Component { + + // + // Lifecycle + + componentDidMount() { + this.props.fetchDownloadClientSchema(); + } + + // + // Listeners + + onDownloadClientSelect = ({ implementation }) => { + this.props.selectDownloadClientSchema({ implementation }); + this.props.onModalClose({ downloadClientSelected: true }); + } + + // + // Render + + render() { + return ( + + ); + } +} + +AddDownloadClientModalContentConnector.propTypes = { + fetchDownloadClientSchema: PropTypes.func.isRequired, + selectDownloadClientSchema: PropTypes.func.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default connect(createMapStateToProps, mapDispatchToProps)(AddDownloadClientModalContentConnector); diff --git a/frontend/src/Settings/DownloadClients/DownloadClients/AddDownloadClientPresetMenuItem.js b/frontend/src/Settings/DownloadClients/DownloadClients/AddDownloadClientPresetMenuItem.js new file mode 100644 index 000000000..f356f8140 --- /dev/null +++ b/frontend/src/Settings/DownloadClients/DownloadClients/AddDownloadClientPresetMenuItem.js @@ -0,0 +1,49 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import MenuItem from 'Components/Menu/MenuItem'; + +class AddDownloadClientPresetMenuItem extends Component { + + // + // Listeners + + onPress = () => { + const { + name, + implementation + } = this.props; + + this.props.onPress({ + name, + implementation + }); + } + + // + // Render + + render() { + const { + name, + implementation, + ...otherProps + } = this.props; + + return ( + + {name} + + ); + } +} + +AddDownloadClientPresetMenuItem.propTypes = { + name: PropTypes.string.isRequired, + implementation: PropTypes.string.isRequired, + onPress: PropTypes.func.isRequired +}; + +export default AddDownloadClientPresetMenuItem; diff --git a/frontend/src/Settings/DownloadClients/DownloadClients/DownloadClient.css b/frontend/src/Settings/DownloadClients/DownloadClients/DownloadClient.css new file mode 100644 index 000000000..cfeacec77 --- /dev/null +++ b/frontend/src/Settings/DownloadClients/DownloadClients/DownloadClient.css @@ -0,0 +1,19 @@ +.downloadClient { + composes: card from 'Components/Card.css'; + + width: 290px; +} + +.name { + @add-mixin truncate; + + margin-bottom: 20px; + font-weight: 300; + font-size: 24px; +} + +.enabled { + display: flex; + flex-wrap: wrap; + margin-top: 5px; +} diff --git a/frontend/src/Settings/DownloadClients/DownloadClients/DownloadClient.js b/frontend/src/Settings/DownloadClients/DownloadClients/DownloadClient.js new file mode 100644 index 000000000..b4a725303 --- /dev/null +++ b/frontend/src/Settings/DownloadClients/DownloadClients/DownloadClient.js @@ -0,0 +1,106 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { kinds } from 'Helpers/Props'; +import Card from 'Components/Card'; +import Label from 'Components/Label'; +import ConfirmModal from 'Components/Modal/ConfirmModal'; +import EditDownloadClientModalConnector from './EditDownloadClientModalConnector'; +import styles from './DownloadClient.css'; + +class DownloadClient extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + isEditDownloadClientModalOpen: false, + isDeleteDownloadClientModalOpen: false + }; + } + + // + // Listeners + + onEditDownloadClientPress = () => { + this.setState({ isEditDownloadClientModalOpen: true }); + } + + onEditDownloadClientModalClose = () => { + this.setState({ isEditDownloadClientModalOpen: false }); + } + + onDeleteDownloadClientPress = () => { + this.setState({ + isEditDownloadClientModalOpen: false, + isDeleteDownloadClientModalOpen: true + }); + } + + onDeleteDownloadClientModalClose= () => { + this.setState({ isDeleteDownloadClientModalOpen: false }); + } + + onConfirmDeleteDownloadClient = () => { + this.props.onConfirmDeleteDownloadClient(this.props.id); + } + + // + // Render + + render() { + const { + id, + name, + enable + } = this.props; + + return ( + +
+ {name} +
+ +
+ +
+ + + + +
+ ); + } +} + +DownloadClient.propTypes = { + id: PropTypes.number.isRequired, + name: PropTypes.string.isRequired, + enable: PropTypes.bool.isRequired, + onConfirmDeleteDownloadClient: PropTypes.func.isRequired +}; + +export default DownloadClient; diff --git a/frontend/src/Settings/DownloadClients/DownloadClients/DownloadClients.css b/frontend/src/Settings/DownloadClients/DownloadClients/DownloadClients.css new file mode 100644 index 000000000..ad53e6311 --- /dev/null +++ b/frontend/src/Settings/DownloadClients/DownloadClients/DownloadClients.css @@ -0,0 +1,20 @@ +.downloadClients { + display: flex; + flex-wrap: wrap; +} + +.addDownloadClient { + composes: downloadClient from './DownloadClient.css'; + + background-color: $cardAlternateBackgroundColor; + color: $gray; + text-align: center; +} + +.center { + display: inline-block; + padding: 5px 20px 0; + border: 1px solid $borderColor; + border-radius: 4px; + background-color: $white; +} diff --git a/frontend/src/Settings/DownloadClients/DownloadClients/DownloadClients.js b/frontend/src/Settings/DownloadClients/DownloadClients/DownloadClients.js new file mode 100644 index 000000000..fe5371e4f --- /dev/null +++ b/frontend/src/Settings/DownloadClients/DownloadClients/DownloadClients.js @@ -0,0 +1,117 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import sortByName from 'Utilities/Array/sortByName'; +import { icons } from 'Helpers/Props'; +import FieldSet from 'Components/FieldSet'; +import Card from 'Components/Card'; +import Icon from 'Components/Icon'; +import PageSectionContent from 'Components/Page/PageSectionContent'; +import DownloadClient from './DownloadClient'; +import AddDownloadClientModal from './AddDownloadClientModal'; +import EditDownloadClientModalConnector from './EditDownloadClientModalConnector'; +import styles from './DownloadClients.css'; + +class DownloadClients extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + isAddDownloadClientModalOpen: false, + isEditDownloadClientModalOpen: false + }; + } + + // + // Listeners + + onAddDownloadClientPress = () => { + this.setState({ isAddDownloadClientModalOpen: true }); + } + + onAddDownloadClientModalClose = ({ downloadClientSelected = false } = {}) => { + this.setState({ + isAddDownloadClientModalOpen: false, + isEditDownloadClientModalOpen: downloadClientSelected + }); + } + + onEditDownloadClientModalClose = () => { + this.setState({ isEditDownloadClientModalOpen: false }); + } + + // + // Render + + render() { + const { + items, + onConfirmDeleteDownloadClient, + ...otherProps + } = this.props; + + const { + isAddDownloadClientModalOpen, + isEditDownloadClientModalOpen + } = this.state; + + return ( +
+ +
+ { + items.sort(sortByName).map((item) => { + return ( + + ); + }) + } + + +
+ +
+
+
+ + + + +
+
+ ); + } +} + +DownloadClients.propTypes = { + isFetching: PropTypes.bool.isRequired, + error: PropTypes.object, + items: PropTypes.arrayOf(PropTypes.object).isRequired, + onConfirmDeleteDownloadClient: PropTypes.func.isRequired +}; + +export default DownloadClients; diff --git a/frontend/src/Settings/DownloadClients/DownloadClients/DownloadClientsConnector.js b/frontend/src/Settings/DownloadClients/DownloadClients/DownloadClientsConnector.js new file mode 100644 index 000000000..d318bc163 --- /dev/null +++ b/frontend/src/Settings/DownloadClients/DownloadClients/DownloadClientsConnector.js @@ -0,0 +1,58 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import { fetchDownloadClients, deleteDownloadClient } from 'Store/Actions/settingsActions'; +import DownloadClients from './DownloadClients'; + +function createMapStateToProps() { + return createSelector( + (state) => state.settings.downloadClients, + (downloadClients) => { + return { + ...downloadClients + }; + } + ); +} + +const mapDispatchToProps = { + fetchDownloadClients, + deleteDownloadClient +}; + +class DownloadClientsConnector extends Component { + + // + // Lifecycle + + componentDidMount() { + this.props.fetchDownloadClients(); + } + + // + // Listeners + + onConfirmDeleteDownloadClient = (id) => { + this.props.deleteDownloadClient({ id }); + } + + // + // Render + + render() { + return ( + + ); + } +} + +DownloadClientsConnector.propTypes = { + fetchDownloadClients: PropTypes.func.isRequired, + deleteDownloadClient: PropTypes.func.isRequired +}; + +export default connect(createMapStateToProps, mapDispatchToProps)(DownloadClientsConnector); diff --git a/frontend/src/Settings/DownloadClients/DownloadClients/EditDownloadClientModal.js b/frontend/src/Settings/DownloadClients/DownloadClients/EditDownloadClientModal.js new file mode 100644 index 000000000..f82e7eea1 --- /dev/null +++ b/frontend/src/Settings/DownloadClients/DownloadClients/EditDownloadClientModal.js @@ -0,0 +1,25 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import Modal from 'Components/Modal/Modal'; +import EditDownloadClientModalContentConnector from './EditDownloadClientModalContentConnector'; + +function EditDownloadClientModal({ isOpen, onModalClose, ...otherProps }) { + return ( + + + + ); +} + +EditDownloadClientModal.propTypes = { + isOpen: PropTypes.bool.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default EditDownloadClientModal; diff --git a/frontend/src/Settings/DownloadClients/DownloadClients/EditDownloadClientModalConnector.js b/frontend/src/Settings/DownloadClients/DownloadClients/EditDownloadClientModalConnector.js new file mode 100644 index 000000000..982b675b8 --- /dev/null +++ b/frontend/src/Settings/DownloadClients/DownloadClients/EditDownloadClientModalConnector.js @@ -0,0 +1,65 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { clearPendingChanges } from 'Store/Actions/baseActions'; +import { cancelTestDownloadClient, cancelSaveDownloadClient } from 'Store/Actions/settingsActions'; +import EditDownloadClientModal from './EditDownloadClientModal'; + +function createMapDispatchToProps(dispatch, props) { + const section = 'downloadClients'; + + return { + dispatchClearPendingChanges() { + dispatch(clearPendingChanges({ section })); + }, + + dispatchCancelTestDownloadClient() { + dispatch(cancelTestDownloadClient({ section })); + }, + + dispatchCancelSaveDownloadClient() { + dispatch(cancelSaveDownloadClient({ section })); + } + }; +} + +class EditDownloadClientModalConnector extends Component { + + // + // Listeners + + onModalClose = () => { + this.props.dispatchClearPendingChanges(); + this.props.dispatchCancelTestDownloadClient(); + this.props.dispatchCancelSaveDownloadClient(); + this.props.onModalClose(); + } + + // + // Render + + render() { + const { + dispatchClearPendingChanges, + dispatchCancelTestDownloadClient, + dispatchCancelSaveDownloadClient, + ...otherProps + } = this.props; + + return ( + + ); + } +} + +EditDownloadClientModalConnector.propTypes = { + onModalClose: PropTypes.func.isRequired, + dispatchClearPendingChanges: PropTypes.func.isRequired, + dispatchCancelTestDownloadClient: PropTypes.func.isRequired, + dispatchCancelSaveDownloadClient: PropTypes.func.isRequired +}; + +export default connect(null, createMapDispatchToProps)(EditDownloadClientModalConnector); diff --git a/frontend/src/Settings/DownloadClients/DownloadClients/EditDownloadClientModalContent.css b/frontend/src/Settings/DownloadClients/DownloadClients/EditDownloadClientModalContent.css new file mode 100644 index 000000000..c73406b57 --- /dev/null +++ b/frontend/src/Settings/DownloadClients/DownloadClients/EditDownloadClientModalContent.css @@ -0,0 +1,11 @@ +.deleteButton { + composes: button from 'Components/Link/Button.css'; + + margin-right: auto; +} + +.message { + composes: alert from 'Components/Alert.css'; + + margin-bottom: 30px; +} diff --git a/frontend/src/Settings/DownloadClients/DownloadClients/EditDownloadClientModalContent.js b/frontend/src/Settings/DownloadClients/DownloadClients/EditDownloadClientModalContent.js new file mode 100644 index 000000000..7475084c8 --- /dev/null +++ b/frontend/src/Settings/DownloadClients/DownloadClients/EditDownloadClientModalContent.js @@ -0,0 +1,177 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { inputTypes, kinds } from 'Helpers/Props'; +import Alert from 'Components/Alert'; +import Button from 'Components/Link/Button'; +import SpinnerErrorButton from 'Components/Link/SpinnerErrorButton'; +import LoadingIndicator from 'Components/Loading/LoadingIndicator'; +import ModalContent from 'Components/Modal/ModalContent'; +import ModalHeader from 'Components/Modal/ModalHeader'; +import ModalBody from 'Components/Modal/ModalBody'; +import ModalFooter from 'Components/Modal/ModalFooter'; +import Form from 'Components/Form/Form'; +import FormGroup from 'Components/Form/FormGroup'; +import FormLabel from 'Components/Form/FormLabel'; +import FormInputGroup from 'Components/Form/FormInputGroup'; +import ProviderFieldFormGroup from 'Components/Form/ProviderFieldFormGroup'; +import styles from './EditDownloadClientModalContent.css'; + +class EditDownloadClientModalContent extends Component { + + // + // Render + + render() { + const { + advancedSettings, + isFetching, + error, + isSaving, + isTesting, + saveError, + item, + onInputChange, + onFieldChange, + onModalClose, + onSavePress, + onTestPress, + onDeleteDownloadClientPress, + ...otherProps + } = this.props; + + const { + id, + name, + enable, + fields, + message + } = item; + + return ( + + + {id ? 'Edit DownloadClient' : 'Add DownloadClient'} + + + + { + isFetching && + + } + + { + !isFetching && !!error && +
Unable to add a new downloadClient, please try again.
+ } + + { + !isFetching && !error && +
+ { + !!message && + + {message.value.message} + + } + + + Name + + + + + + Enable + + + + + { + fields.map((field) => { + return ( + + ); + }) + } + + + } +
+ + { + id && + + } + + + Test + + + + + + Save + + +
+ ); + } +} + +EditDownloadClientModalContent.propTypes = { + advancedSettings: PropTypes.bool.isRequired, + isFetching: PropTypes.bool.isRequired, + error: PropTypes.object, + isSaving: PropTypes.bool.isRequired, + saveError: PropTypes.object, + isTesting: PropTypes.bool.isRequired, + item: PropTypes.object.isRequired, + onInputChange: PropTypes.func.isRequired, + onFieldChange: PropTypes.func.isRequired, + onModalClose: PropTypes.func.isRequired, + onSavePress: PropTypes.func.isRequired, + onTestPress: PropTypes.func.isRequired, + onDeleteDownloadClientPress: PropTypes.func +}; + +export default EditDownloadClientModalContent; diff --git a/frontend/src/Settings/DownloadClients/DownloadClients/EditDownloadClientModalContentConnector.js b/frontend/src/Settings/DownloadClients/DownloadClients/EditDownloadClientModalContentConnector.js new file mode 100644 index 000000000..bf9acb98c --- /dev/null +++ b/frontend/src/Settings/DownloadClients/DownloadClients/EditDownloadClientModalContentConnector.js @@ -0,0 +1,94 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { createSelector } from 'reselect'; +import createProviderSettingsSelector from 'Store/Selectors/createProviderSettingsSelector'; +import { setDownloadClientValue, setDownloadClientFieldValue, saveDownloadClient, testDownloadClient } from 'Store/Actions/settingsActions'; +import connectSection from 'Store/connectSection'; +import EditDownloadClientModalContent from './EditDownloadClientModalContent'; + +function createMapStateToProps() { + return createSelector( + (state) => state.settings.advancedSettings, + createProviderSettingsSelector(), + (advancedSettings, downloadClient) => { + return { + advancedSettings, + ...downloadClient + }; + } + ); +} + +const mapDispatchToProps = { + setDownloadClientValue, + setDownloadClientFieldValue, + saveDownloadClient, + testDownloadClient +}; + +class EditDownloadClientModalContentConnector extends Component { + + // + // Lifecycle + + componentDidUpdate(prevProps, prevState) { + if (prevProps.isSaving && !this.props.isSaving && !this.props.saveError) { + this.props.onModalClose(); + } + } + + // + // Listeners + + onInputChange = ({ name, value }) => { + this.props.setDownloadClientValue({ name, value }); + } + + onFieldChange = ({ name, value }) => { + this.props.setDownloadClientFieldValue({ name, value }); + } + + onSavePress = () => { + this.props.saveDownloadClient({ id: this.props.id }); + } + + onTestPress = () => { + this.props.testDownloadClient({ id: this.props.id }); + } + + // + // Render + + render() { + return ( + + ); + } +} + +EditDownloadClientModalContentConnector.propTypes = { + id: PropTypes.number, + isFetching: PropTypes.bool.isRequired, + isSaving: PropTypes.bool.isRequired, + saveError: PropTypes.object, + item: PropTypes.object.isRequired, + setDownloadClientValue: PropTypes.func.isRequired, + setDownloadClientFieldValue: PropTypes.func.isRequired, + saveDownloadClient: PropTypes.func.isRequired, + testDownloadClient: PropTypes.func.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default connectSection( + createMapStateToProps, + mapDispatchToProps, + undefined, + undefined, + { section: 'downloadClients' } +)(EditDownloadClientModalContentConnector); diff --git a/frontend/src/Settings/DownloadClients/Options/DownloadClientOptions.js b/frontend/src/Settings/DownloadClients/Options/DownloadClientOptions.js new file mode 100644 index 000000000..2ec9b417d --- /dev/null +++ b/frontend/src/Settings/DownloadClients/Options/DownloadClientOptions.js @@ -0,0 +1,118 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import { inputTypes, sizes } from 'Helpers/Props'; +import FieldSet from 'Components/FieldSet'; +import LoadingIndicator from 'Components/Loading/LoadingIndicator'; +import Form from 'Components/Form/Form'; +import FormGroup from 'Components/Form/FormGroup'; +import FormLabel from 'Components/Form/FormLabel'; +import FormInputGroup from 'Components/Form/FormInputGroup'; + +function DownloadClientOptions(props) { + const { + advancedSettings, + isFetching, + error, + settings, + hasSettings, + onInputChange + } = props; + + return ( +
+ { + isFetching && + + } + + { + !isFetching && error && +
Unable to load download client options
+ } + + { + hasSettings && !isFetching && !error && +
+
+
+ + Enable + + + + + + Remove + + + +
+
+ +
+
+ + Redownload + + + + + + Remove + + + +
+
+
+ } +
+ ); +} + +DownloadClientOptions.propTypes = { + advancedSettings: PropTypes.bool.isRequired, + isFetching: PropTypes.bool.isRequired, + error: PropTypes.object, + settings: PropTypes.object.isRequired, + hasSettings: PropTypes.bool.isRequired, + onInputChange: PropTypes.func.isRequired +}; + +export default DownloadClientOptions; diff --git a/frontend/src/Settings/DownloadClients/Options/DownloadClientOptionsConnector.js b/frontend/src/Settings/DownloadClients/Options/DownloadClientOptionsConnector.js new file mode 100644 index 000000000..cee9a544e --- /dev/null +++ b/frontend/src/Settings/DownloadClients/Options/DownloadClientOptionsConnector.js @@ -0,0 +1,92 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { createSelector } from 'reselect'; +import createSettingsSectionSelector from 'Store/Selectors/createSettingsSectionSelector'; +import { fetchDownloadClientOptions, setDownloadClientOptionsValue, saveDownloadClientOptions } from 'Store/Actions/settingsActions'; +import { clearPendingChanges } from 'Store/Actions/baseActions'; +import connectSection from 'Store/connectSection'; +import DownloadClientOptions from './DownloadClientOptions'; + +function createMapStateToProps() { + return createSelector( + (state) => state.settings.advancedSettings, + createSettingsSectionSelector(), + (advancedSettings, sectionSettings) => { + return { + advancedSettings, + ...sectionSettings + }; + } + ); +} + +const mapDispatchToProps = { + fetchDownloadClientOptions, + setDownloadClientOptionsValue, + saveDownloadClientOptions, + clearPendingChanges +}; + +class DownloadClientOptionsConnector extends Component { + + // + // Lifecycle + + componentDidMount() { + this.props.fetchDownloadClientOptions(); + } + + componentDidUpdate(prevProps) { + if (this.props.hasPendingChanges !== prevProps.hasPendingChanges) { + this.props.onHasPendingChange(this.props.hasPendingChanges); + } + } + + componentWillUnmount() { + this.props.clearPendingChanges({ section: this.props.section }); + } + + // + // Control + + save = () => { + this.props.saveDownloadClientOptions(); + } + + // + // Listeners + + onInputChange = ({ name, value }) => { + this.props.setDownloadClientOptionsValue({ name, value }); + } + + // + // Render + + render() { + return ( + + ); + } +} + +DownloadClientOptionsConnector.propTypes = { + section: PropTypes.string.isRequired, + hasPendingChanges: PropTypes.bool.isRequired, + fetchDownloadClientOptions: PropTypes.func.isRequired, + setDownloadClientOptionsValue: PropTypes.func.isRequired, + saveDownloadClientOptions: PropTypes.func.isRequired, + clearPendingChanges: PropTypes.func.isRequired, + onHasPendingChange: PropTypes.func.isRequired +}; + +export default connectSection( + createMapStateToProps, + mapDispatchToProps, + undefined, + { withRef: true }, + { section: 'downloadClientOptions' } +)(DownloadClientOptionsConnector); diff --git a/frontend/src/Settings/DownloadClients/RemotePathMappings/EditRemotePathMappingModal.js b/frontend/src/Settings/DownloadClients/RemotePathMappings/EditRemotePathMappingModal.js new file mode 100644 index 000000000..5ba30d614 --- /dev/null +++ b/frontend/src/Settings/DownloadClients/RemotePathMappings/EditRemotePathMappingModal.js @@ -0,0 +1,25 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import Modal from 'Components/Modal/Modal'; +import EditRemotePathMappingModalContentConnector from './EditRemotePathMappingModalContentConnector'; + +function EditRemotePathMappingModal({ isOpen, onModalClose, ...otherProps }) { + return ( + + + + ); +} + +EditRemotePathMappingModal.propTypes = { + isOpen: PropTypes.bool.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default EditRemotePathMappingModal; diff --git a/frontend/src/Settings/DownloadClients/RemotePathMappings/EditRemotePathMappingModalConnector.js b/frontend/src/Settings/DownloadClients/RemotePathMappings/EditRemotePathMappingModalConnector.js new file mode 100644 index 000000000..bd8bca75f --- /dev/null +++ b/frontend/src/Settings/DownloadClients/RemotePathMappings/EditRemotePathMappingModalConnector.js @@ -0,0 +1,43 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { clearPendingChanges } from 'Store/Actions/baseActions'; +import EditRemotePathMappingModal from './EditRemotePathMappingModal'; + +function mapStateToProps() { + return {}; +} + +const mapDispatchToProps = { + clearPendingChanges +}; + +class EditRemotePathMappingModalConnector extends Component { + + // + // Listeners + + onModalClose = () => { + this.props.clearPendingChanges({ section: 'remotePathMappings' }); + this.props.onModalClose(); + } + + // + // Render + + render() { + return ( + + ); + } +} + +EditRemotePathMappingModalConnector.propTypes = { + onModalClose: PropTypes.func.isRequired, + clearPendingChanges: PropTypes.func.isRequired +}; + +export default connect(mapStateToProps, mapDispatchToProps)(EditRemotePathMappingModalConnector); diff --git a/frontend/src/Settings/DownloadClients/RemotePathMappings/EditRemotePathMappingModalContent.css b/frontend/src/Settings/DownloadClients/RemotePathMappings/EditRemotePathMappingModalContent.css new file mode 100644 index 000000000..0071acc4e --- /dev/null +++ b/frontend/src/Settings/DownloadClients/RemotePathMappings/EditRemotePathMappingModalContent.css @@ -0,0 +1,12 @@ +.body { + composes: modalBody from 'Components/Modal/ModalBody.css'; + + flex: 1 1 430px; +} + +.deleteButton { + composes: button from 'Components/Link/Button.css'; + + margin-right: auto; +} + diff --git a/frontend/src/Settings/DownloadClients/RemotePathMappings/EditRemotePathMappingModalContent.js b/frontend/src/Settings/DownloadClients/RemotePathMappings/EditRemotePathMappingModalContent.js new file mode 100644 index 000000000..2e21ea87b --- /dev/null +++ b/frontend/src/Settings/DownloadClients/RemotePathMappings/EditRemotePathMappingModalContent.js @@ -0,0 +1,149 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import { inputTypes, kinds } from 'Helpers/Props'; +import { stringSettingShape } from 'Helpers/Props/Shapes/settingShape'; +import Button from 'Components/Link/Button'; +import SpinnerErrorButton from 'Components/Link/SpinnerErrorButton'; +import LoadingIndicator from 'Components/Loading/LoadingIndicator'; +import ModalContent from 'Components/Modal/ModalContent'; +import ModalHeader from 'Components/Modal/ModalHeader'; +import ModalBody from 'Components/Modal/ModalBody'; +import ModalFooter from 'Components/Modal/ModalFooter'; +import Form from 'Components/Form/Form'; +import FormGroup from 'Components/Form/FormGroup'; +import FormLabel from 'Components/Form/FormLabel'; +import FormInputGroup from 'Components/Form/FormInputGroup'; +import styles from './EditRemotePathMappingModalContent.css'; + +function EditRemotePathMappingModalContent(props) { + const { + id, + isFetching, + error, + isSaving, + saveError, + item, + onInputChange, + onSavePress, + onModalClose, + onDeleteRemotePathMappingPress, + ...otherProps + } = props; + + const { + host, + remotePath, + localPath + } = item; + + return ( + + + {id ? 'Edit Remote Path Mapping' : 'Add Remote Path Mapping'} + + + + { + isFetching && + + } + + { + !isFetching && !!error && +
Unable to add a new remote path mapping, please try again.
+ } + + { + !isFetching && !error && +
+ + Host + + + + + + Remote Path + + + + + + Local Path + + + +
+ } +
+ + + { + id && + + } + + + + + Save + + +
+ ); +} + +const remotePathMappingShape = { + host: PropTypes.shape(stringSettingShape).isRequired, + remotePath: PropTypes.shape(stringSettingShape).isRequired, + localPath: PropTypes.shape(stringSettingShape).isRequired +}; + +EditRemotePathMappingModalContent.propTypes = { + id: PropTypes.number, + isFetching: PropTypes.bool.isRequired, + error: PropTypes.object, + isSaving: PropTypes.bool.isRequired, + saveError: PropTypes.object, + item: PropTypes.shape(remotePathMappingShape).isRequired, + onInputChange: PropTypes.func.isRequired, + onSavePress: PropTypes.func.isRequired, + onModalClose: PropTypes.func.isRequired, + onDeleteRemotePathMappingPress: PropTypes.func +}; + +export default EditRemotePathMappingModalContent; diff --git a/frontend/src/Settings/DownloadClients/RemotePathMappings/EditRemotePathMappingModalContentConnector.js b/frontend/src/Settings/DownloadClients/RemotePathMappings/EditRemotePathMappingModalContentConnector.js new file mode 100644 index 000000000..00aa7b8ac --- /dev/null +++ b/frontend/src/Settings/DownloadClients/RemotePathMappings/EditRemotePathMappingModalContentConnector.js @@ -0,0 +1,119 @@ +import _ from 'lodash'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import selectSettings from 'Store/Selectors/selectSettings'; +import { setRemotePathMappingValue, saveRemotePathMapping } from 'Store/Actions/settingsActions'; +import EditRemotePathMappingModalContent from './EditRemotePathMappingModalContent'; + +const newRemotePathMapping = { + host: '', + remotePath: '', + localPath: '' +}; + +function createRemotePathMappingSelector() { + return createSelector( + (state, { id }) => id, + (state) => state.settings.remotePathMappings, + (id, remotePathMappings) => { + const { + isFetching, + error, + isSaving, + saveError, + pendingChanges, + items + } = remotePathMappings; + + const mapping = id ? _.find(items, { id }) : newRemotePathMapping; + const settings = selectSettings(mapping, pendingChanges, saveError); + + return { + id, + isFetching, + error, + isSaving, + saveError, + item: settings.settings, + ...settings + }; + } + ); +} + +function createMapStateToProps() { + return createSelector( + createRemotePathMappingSelector(), + (remotePathMapping) => { + return { + ...remotePathMapping + }; + } + ); +} + +const mapDispatchToProps = { + setRemotePathMappingValue, + saveRemotePathMapping +}; + +class EditRemotePathMappingModalContentConnector extends Component { + + // + // Lifecycle + + componentDidMount() { + if (!this.props.id) { + Object.keys(newRemotePathMapping).forEach((name) => { + this.props.setRemotePathMappingValue({ + name, + value: newRemotePathMapping[name] + }); + }); + } + } + + componentDidUpdate(prevProps, prevState) { + if (prevProps.isSaving && !this.props.isSaving && !this.props.saveError) { + this.props.onModalClose(); + } + } + + // + // Listeners + + onInputChange = ({ name, value }) => { + this.props.setRemotePathMappingValue({ name, value }); + } + + onSavePress = () => { + this.props.saveRemotePathMapping({ id: this.props.id }); + } + + // + // Render + + render() { + return ( + + ); + } +} + +EditRemotePathMappingModalContentConnector.propTypes = { + id: PropTypes.number, + isSaving: PropTypes.bool.isRequired, + saveError: PropTypes.object, + item: PropTypes.object.isRequired, + setRemotePathMappingValue: PropTypes.func.isRequired, + saveRemotePathMapping: PropTypes.func.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default connect(createMapStateToProps, mapDispatchToProps)(EditRemotePathMappingModalContentConnector); diff --git a/frontend/src/Settings/DownloadClients/RemotePathMappings/RemotePathMapping.css b/frontend/src/Settings/DownloadClients/RemotePathMappings/RemotePathMapping.css new file mode 100644 index 000000000..a79efda26 --- /dev/null +++ b/frontend/src/Settings/DownloadClients/RemotePathMappings/RemotePathMapping.css @@ -0,0 +1,23 @@ +.remotePathMapping { + display: flex; + align-items: stretch; + margin-bottom: 10px; + height: 30px; + border-bottom: 1px solid $borderColor; + line-height: 30px; +} + +.host { + flex: 0 0 300px; +} + +.path { + flex: 0 0 400px; +} + +.actions { + display: flex; + justify-content: flex-end; + flex: 1 0 auto; + padding-right: 10px; +} diff --git a/frontend/src/Settings/DownloadClients/RemotePathMappings/RemotePathMapping.js b/frontend/src/Settings/DownloadClients/RemotePathMappings/RemotePathMapping.js new file mode 100644 index 000000000..c0c0a988f --- /dev/null +++ b/frontend/src/Settings/DownloadClients/RemotePathMappings/RemotePathMapping.js @@ -0,0 +1,114 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import classNames from 'classnames'; +import { icons, kinds } from 'Helpers/Props'; +import Icon from 'Components/Icon'; +import Link from 'Components/Link/Link'; +import ConfirmModal from 'Components/Modal/ConfirmModal'; +import EditRemotePathMappingModalConnector from './EditRemotePathMappingModalConnector'; +import styles from './RemotePathMapping.css'; + +class RemotePathMapping extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + isEditRemotePathMappingModalOpen: false, + isDeleteRemotePathMappingModalOpen: false + }; + } + + // + // Listeners + + onEditRemotePathMappingPress = () => { + this.setState({ isEditRemotePathMappingModalOpen: true }); + } + + onEditRemotePathMappingModalClose = () => { + this.setState({ isEditRemotePathMappingModalOpen: false }); + } + + onDeleteRemotePathMappingPress = () => { + this.setState({ + isEditRemotePathMappingModalOpen: false, + isDeleteRemotePathMappingModalOpen: true + }); + } + + onDeleteRemotePathMappingModalClose = () => { + this.setState({ isDeleteRemotePathMappingModalOpen: false }); + } + + onConfirmDeleteRemotePathMapping = () => { + this.props.onConfirmDeleteRemotePathMapping(this.props.id); + } + + // + // Render + + render() { + const { + id, + host, + remotePath, + localPath + } = this.props; + + return ( +
+
{host}
+
{remotePath}
+
{localPath}
+ +
+ + + +
+ + + + +
+ ); + } +} + +RemotePathMapping.propTypes = { + id: PropTypes.number.isRequired, + host: PropTypes.string.isRequired, + remotePath: PropTypes.string.isRequired, + localPath: PropTypes.string.isRequired, + onConfirmDeleteRemotePathMapping: PropTypes.func.isRequired +}; + +RemotePathMapping.defaultProps = { + // The drag preview will not connect the drag handle. + connectDragSource: (node) => node +}; + +export default RemotePathMapping; diff --git a/frontend/src/Settings/DownloadClients/RemotePathMappings/RemotePathMappings.css b/frontend/src/Settings/DownloadClients/RemotePathMappings/RemotePathMappings.css new file mode 100644 index 000000000..4ef9dcb0f --- /dev/null +++ b/frontend/src/Settings/DownloadClients/RemotePathMappings/RemotePathMappings.css @@ -0,0 +1,23 @@ +.remotePathMappingsHeader { + display: flex; + margin-bottom: 10px; + font-weight: bold; +} + +.host { + flex: 0 0 300px; +} + +.path { + flex: 0 0 400px; +} + +.addRemotePathMapping { + display: flex; + justify-content: flex-end; + padding-right: 10px; +} + +.addButton { + text-align: center; +} diff --git a/frontend/src/Settings/DownloadClients/RemotePathMappings/RemotePathMappings.js b/frontend/src/Settings/DownloadClients/RemotePathMappings/RemotePathMappings.js new file mode 100644 index 000000000..93d022e02 --- /dev/null +++ b/frontend/src/Settings/DownloadClients/RemotePathMappings/RemotePathMappings.js @@ -0,0 +1,102 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { icons } from 'Helpers/Props'; +import FieldSet from 'Components/FieldSet'; +import Icon from 'Components/Icon'; +import Link from 'Components/Link/Link'; +import PageSectionContent from 'Components/Page/PageSectionContent'; +import RemotePathMapping from './RemotePathMapping'; +import EditRemotePathMappingModalConnector from './EditRemotePathMappingModalConnector'; +import styles from './RemotePathMappings.css'; + +class RemotePathMappings extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + isAddRemotePathMappingModalOpen: false + }; + } + + // + // Listeners + + onAddRemotePathMappingPress = () => { + this.setState({ isAddRemotePathMappingModalOpen: true }); + } + + onModalClose = () => { + this.setState({ isAddRemotePathMappingModalOpen: false }); + } + + // + // Render + + render() { + const { + items, + onConfirmDeleteRemotePathMapping, + ...otherProps + } = this.props; + + return ( +
+ +
+
Host
+
Remote Path
+
Local Path
+
+ +
+ { + items.map((item, index) => { + return ( + + ); + }) + } +
+ +
+ + + +
+ + +
+
+ ); + } +} + +RemotePathMappings.propTypes = { + isFetching: PropTypes.bool.isRequired, + error: PropTypes.object, + items: PropTypes.arrayOf(PropTypes.object).isRequired, + onConfirmDeleteRemotePathMapping: PropTypes.func.isRequired +}; + +export default RemotePathMappings; diff --git a/frontend/src/Settings/DownloadClients/RemotePathMappings/RemotePathMappingsConnector.js b/frontend/src/Settings/DownloadClients/RemotePathMappings/RemotePathMappingsConnector.js new file mode 100644 index 000000000..4900119a3 --- /dev/null +++ b/frontend/src/Settings/DownloadClients/RemotePathMappings/RemotePathMappingsConnector.js @@ -0,0 +1,59 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import { fetchRemotePathMappings, deleteRemotePathMapping } from 'Store/Actions/settingsActions'; +import RemotePathMappings from './RemotePathMappings'; + +function createMapStateToProps() { + return createSelector( + (state) => state.settings.remotePathMappings, + (remotePathMappings) => { + return { + ...remotePathMappings + }; + } + ); +} + +const mapDispatchToProps = { + fetchRemotePathMappings, + deleteRemotePathMapping +}; + +class RemotePathMappingsConnector extends Component { + + // + // Lifecycle + + componentDidMount() { + this.props.fetchRemotePathMappings(); + } + + // + // Listeners + + onConfirmDeleteRemotePathMapping = (id) => { + this.props.deleteRemotePathMapping({ id }); + } + + // + // Render + + render() { + return ( + + ); + } +} + +RemotePathMappingsConnector.propTypes = { + fetchRemotePathMappings: PropTypes.func.isRequired, + deleteRemotePathMapping: PropTypes.func.isRequired +}; + +export default connect(createMapStateToProps, mapDispatchToProps)(RemotePathMappingsConnector); diff --git a/frontend/src/Settings/General/GeneralSettings.js b/frontend/src/Settings/General/GeneralSettings.js new file mode 100644 index 000000000..ba71837a5 --- /dev/null +++ b/frontend/src/Settings/General/GeneralSettings.js @@ -0,0 +1,656 @@ +import _ from 'lodash'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { icons, inputTypes, kinds, sizes } from 'Helpers/Props'; +import LoadingIndicator from 'Components/Loading/LoadingIndicator'; +import FieldSet from 'Components/FieldSet'; +import Icon from 'Components/Icon'; +import ClipboardButton from 'Components/Link/ClipboardButton'; +import PageContent from 'Components/Page/PageContent'; +import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector'; +import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector'; +import Form from 'Components/Form/Form'; +import FormGroup from 'Components/Form/FormGroup'; +import FormLabel from 'Components/Form/FormLabel'; +import FormInputGroup from 'Components/Form/FormInputGroup'; +import FormInputButton from 'Components/Form/FormInputButton'; +import ConfirmModal from 'Components/Modal/ConfirmModal'; + +class GeneralSettings extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + isConfirmApiKeyResetModalOpen: false, + isRestartRequiredModalOpen: false + }; + } + + componentDidUpdate(prevProps) { + const { + settings, + isSaving, + saveError + } = this.props; + + if (isSaving || saveError || !prevProps.isSaving) { + return; + } + + const prevSettings = prevProps.settings; + + const keys = [ + 'bindAddress', + 'port', + 'urlBase', + 'enableSsl', + 'sslPort', + 'sslCertHash', + 'authenticationMethod', + 'username', + 'password', + 'apiKey' + ]; + + const pendingRestart = _.some(keys, (key) => { + const setting = settings[key]; + const prevSetting = prevSettings[key]; + + if (!setting || !prevSetting) { + return false; + } + + const previousValue = prevSetting.previousValue; + const value = setting.value; + + return previousValue != null && previousValue !== value; + }); + + this.setState({ isRestartRequiredModalOpen: pendingRestart }); + } + + // + // Listeners + + onApikeyFocus = (event) => { + event.target.select(); + } + + onResetApiKeyPress = () => { + this.setState({ isConfirmApiKeyResetModalOpen: true }); + } + + onConfirmResetApiKey = () => { + this.setState({ isConfirmApiKeyResetModalOpen: false }); + this.props.onConfirmResetApiKey(); + } + + onCloseResetApiKeyModal = () => { + this.setState({ isConfirmApiKeyResetModalOpen: false }); + } + + onConfirmRestart = () => { + this.setState({ isRestartRequiredModalOpen: false }); + this.props.onConfirmRestart(); + } + + onCloseRestartRequiredModalOpen = () => { + this.setState({ isRestartRequiredModalOpen: false }); + } + + // + // Render + + render() { + const { + advancedSettings, + isFetching, + isPopulated, + error, + settings, + hasSettings, + isResettingApiKey, + isMono, + isWindows, + mode, + onInputChange, + ...otherProps + } = this.props; + + const { + isConfirmApiKeyResetModalOpen, + isRestartRequiredModalOpen + } = this.state; + + const { + bindAddress, + port, + urlBase, + enableSsl, + sslPort, + sslCertHash, + launchBrowser, + authenticationMethod, + username, + password, + apiKey, + proxyEnabled, + proxyType, + proxyHostname, + proxyPort, + proxyUsername, + proxyPassword, + proxyBypassFilter, + proxyBypassLocalAddresses, + logLevel, + analyticsEnabled, + branch, + updateAutomatically, + updateMechanism, + updateScriptPath + } = settings; + + const authenticationMethodOptions = [ + { key: 'none', value: 'None' }, + { key: 'basic', value: 'Basic (Browser Popup)' }, + { key: 'forms', value: 'Forms (Login Page)' } + ]; + + const proxyTypeOptions = [ + { key: 'http', value: 'HTTP(S)' }, + { key: 'socks4', value: 'Socks4' }, + { key: 'socks5', value: 'Socks5 (Support TOR)' } + ]; + + const logLevelOptions = [ + { key: 'info', value: 'Info' }, + { key: 'debug', value: 'Debug' }, + { key: 'trace', value: 'Trace' } + ]; + + const updateOptions = [ + { key: 'builtIn', value: 'Built-In' }, + { key: 'script', value: 'Script' } + ]; + + const authenticationEnabled = authenticationMethod && authenticationMethod.value !== 'none'; + + return ( + + + + + { + isFetching && !isPopulated && + + } + + { + !isFetching && error && +
Unable to load General settings
+ } + + { + hasSettings && isPopulated && !error && +
+
+ + Bind Address + + + + + + Port Number + + + + + + URL Base + + + + + + Enable SSL + + + + + { + enableSsl.value && + + SSL Port + + + + } + + { + isWindows && enableSsl.value && + + SSL Cert Hash + + + + } + + { + mode !== 'service' && + + Open browser on start + + + + } + +
+ +
+ + Authentication + + + + + { + authenticationEnabled && + + Username + + + + } + + { + authenticationEnabled && + + Password + + + + } + + + API Key + + , + + + + + ]} + onChange={onInputChange} + onFocus={this.onApikeyFocus} + {...apiKey} + /> + +
+ +
+ + Use Proxy + + + + + { + proxyEnabled.value && +
+ + Proxy Type + + + + + + Hostname + + + + + + Port + + + + + + Username + + + + + + Password + + + + + + Ignored Addresses + + + + + + Bypass Proxy for Local Addresses + + + +
+ } +
+ +
+ + Log Level + + + +
+ +
+ + Send Anonymous Usage Data + + + +
+ + { + advancedSettings && +
+ + Branch + + + + + { + isMono && +
+ + Automatic + + + + + + Mechanism + + + + + { + updateMechanism.value === 'script' && + + Script Path + + + + } +
+ } +
+ } +
+ } +
+ + + + +
+ ); + } + +} + +GeneralSettings.propTypes = { + advancedSettings: PropTypes.bool.isRequired, + isFetching: PropTypes.bool.isRequired, + isPopulated: PropTypes.bool.isRequired, + error: PropTypes.object, + isSaving: PropTypes.bool.isRequired, + saveError: PropTypes.object, + settings: PropTypes.object.isRequired, + isResettingApiKey: PropTypes.bool.isRequired, + hasSettings: PropTypes.bool.isRequired, + isMono: PropTypes.bool.isRequired, + isWindows: PropTypes.bool.isRequired, + mode: PropTypes.string.isRequired, + onInputChange: PropTypes.func.isRequired, + onConfirmResetApiKey: PropTypes.func.isRequired, + onConfirmRestart: PropTypes.func.isRequired +}; + +export default GeneralSettings; diff --git a/frontend/src/Settings/General/GeneralSettingsConnector.js b/frontend/src/Settings/General/GeneralSettingsConnector.js new file mode 100644 index 000000000..fd0a11743 --- /dev/null +++ b/frontend/src/Settings/General/GeneralSettingsConnector.js @@ -0,0 +1,117 @@ +import _ from 'lodash'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { createSelector } from 'reselect'; +import createSettingsSectionSelector from 'Store/Selectors/createSettingsSectionSelector'; +import createCommandsSelector from 'Store/Selectors/createCommandsSelector'; +import createSystemStatusSelector from 'Store/Selectors/createSystemStatusSelector'; +import { setGeneralSettingsValue, saveGeneralSettings, fetchGeneralSettings } from 'Store/Actions/settingsActions'; +import { clearPendingChanges } from 'Store/Actions/baseActions'; +import { executeCommand } from 'Store/Actions/commandActions'; +import { restart } from 'Store/Actions/systemActions'; +import connectSection from 'Store/connectSection'; +import * as commandNames from 'Commands/commandNames'; +import GeneralSettings from './GeneralSettings'; + +function createMapStateToProps() { + return createSelector( + (state) => state.settings.advancedSettings, + createSettingsSectionSelector(), + createCommandsSelector(), + createSystemStatusSelector(), + (advancedSettings, sectionSettings, commands, systemStatus) => { + const isResettingApiKey = _.some(commands, { name: commandNames.RESET_API_KEY }); + + return { + advancedSettings, + isResettingApiKey, + isMono: systemStatus.isMono, + isWindows: systemStatus.isWindows, + mode: systemStatus.mode, + ...sectionSettings + }; + } + ); +} + +const mapDispatchToProps = { + setGeneralSettingsValue, + saveGeneralSettings, + fetchGeneralSettings, + executeCommand, + restart, + clearPendingChanges +}; + +class GeneralSettingsConnector extends Component { + + // + // Lifecycle + + componentDidMount() { + this.props.fetchGeneralSettings(); + } + + componentDidUpdate(prevProps) { + if (!this.props.isResettingApiKey && prevProps.isResettingApiKey) { + this.props.fetchGeneralSettings(); + } + } + + componentWillUnmount() { + this.props.clearPendingChanges({ section: this.props.section }); + } + + // + // Listeners + + onInputChange = ({ name, value }) => { + this.props.setGeneralSettingsValue({ name, value }); + } + + onSavePress = () => { + this.props.saveGeneralSettings(); + } + + onConfirmResetApiKey = () => { + this.props.executeCommand({ name: commandNames.RESET_API_KEY }); + } + + onConfirmRestart = () => { + this.props.restart(); + } + + // + // Render + + render() { + return ( + + ); + } +} + +GeneralSettingsConnector.propTypes = { + section: PropTypes.string.isRequired, + isResettingApiKey: PropTypes.bool.isRequired, + setGeneralSettingsValue: PropTypes.func.isRequired, + saveGeneralSettings: PropTypes.func.isRequired, + fetchGeneralSettings: PropTypes.func.isRequired, + executeCommand: PropTypes.func.isRequired, + restart: PropTypes.func.isRequired, + clearPendingChanges: PropTypes.func.isRequired +}; + +export default connectSection( + createMapStateToProps, + mapDispatchToProps, + undefined, + undefined, + { section: 'general' } +)(GeneralSettingsConnector); diff --git a/frontend/src/Settings/Indexers/IndexerSettings.js b/frontend/src/Settings/Indexers/IndexerSettings.js new file mode 100644 index 000000000..2e526c080 --- /dev/null +++ b/frontend/src/Settings/Indexers/IndexerSettings.js @@ -0,0 +1,65 @@ +import React, { Component } from 'react'; +import PageContent from 'Components/Page/PageContent'; +import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector'; +import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector'; +import IndexersConnector from './Indexers/IndexersConnector'; +import IndexerOptionsConnector from './Options/IndexerOptionsConnector'; +import RestrictionsConnector from './Restrictions/RestrictionsConnector'; + +class IndexerSettings extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + hasPendingChanges: false + }; + } + + // + // Listeners + + setIndexerOptionsRef = (ref) => { + this._indexerOptions = ref; + } + + onHasPendingChange = (hasPendingChanges) => { + this.setState({ + hasPendingChanges + }); + } + + onSavePress = () => { + this._indexerOptions.getWrappedInstance().save(); + } + + // + // Render + + render() { + return ( + + + + + + + + + + + + ); + } +} + +export default IndexerSettings; diff --git a/frontend/src/Settings/Indexers/Indexers/AddIndexerItem.css b/frontend/src/Settings/Indexers/Indexers/AddIndexerItem.css new file mode 100644 index 000000000..d228b842b --- /dev/null +++ b/frontend/src/Settings/Indexers/Indexers/AddIndexerItem.css @@ -0,0 +1,44 @@ +.indexer { + composes: card from 'Components/Card.css'; + + position: relative; + width: 300px; + height: 100px; +} + +.underlay { + @add-mixin cover; +} + +.overlay { + @add-mixin linkOverlay; + + padding: 10px; +} + +.name { + text-align: center; + font-weight: lighter; + font-size: 24px; +} + +.actions { + margin-top: 20px; + text-align: right; +} + +.presetsMenu { + composes: menu from 'Components/Menu/Menu.css'; + + display: inline-block; + margin: 0 5px; +} + +.presetsMenuButton { + composes: button from 'Components/Link/Button.css'; + + &::after { + margin-left: 5px; + content: '\25BE'; + } +} diff --git a/frontend/src/Settings/Indexers/Indexers/AddIndexerItem.js b/frontend/src/Settings/Indexers/Indexers/AddIndexerItem.js new file mode 100644 index 000000000..21db4ecf1 --- /dev/null +++ b/frontend/src/Settings/Indexers/Indexers/AddIndexerItem.js @@ -0,0 +1,110 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { sizes } from 'Helpers/Props'; +import Button from 'Components/Link/Button'; +import Link from 'Components/Link/Link'; +import Menu from 'Components/Menu/Menu'; +import MenuContent from 'Components/Menu/MenuContent'; +import AddIndexerPresetMenuItem from './AddIndexerPresetMenuItem'; +import styles from './AddIndexerItem.css'; + +class AddIndexerItem extends Component { + + // + // Listeners + + onIndexerSelect = () => { + const { + implementation + } = this.props; + + this.props.onIndexerSelect({ implementation }); + } + + // + // Render + + render() { + const { + implementation, + implementationName, + infoLink, + presets, + onIndexerSelect + } = this.props; + + const hasPresets = !!presets && !!presets.length; + + return ( +
+ + +
+
+ {implementationName} +
+ +
+ { + hasPresets && + + + + + + + + { + presets.map((preset) => { + return ( + + ); + }) + } + + + + } + + +
+
+
+ ); + } +} + +AddIndexerItem.propTypes = { + implementation: PropTypes.string.isRequired, + implementationName: PropTypes.string.isRequired, + infoLink: PropTypes.string.isRequired, + presets: PropTypes.arrayOf(PropTypes.object), + onIndexerSelect: PropTypes.func.isRequired +}; + +export default AddIndexerItem; diff --git a/frontend/src/Settings/Indexers/Indexers/AddIndexerModal.js b/frontend/src/Settings/Indexers/Indexers/AddIndexerModal.js new file mode 100644 index 000000000..d05e8eb9a --- /dev/null +++ b/frontend/src/Settings/Indexers/Indexers/AddIndexerModal.js @@ -0,0 +1,25 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import Modal from 'Components/Modal/Modal'; +import AddIndexerModalContentConnector from './AddIndexerModalContentConnector'; + +function AddIndexerModal({ isOpen, onModalClose, ...otherProps }) { + return ( + + + + ); +} + +AddIndexerModal.propTypes = { + isOpen: PropTypes.bool.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default AddIndexerModal; diff --git a/frontend/src/Settings/Indexers/Indexers/AddIndexerModalContent.css b/frontend/src/Settings/Indexers/Indexers/AddIndexerModalContent.css new file mode 100644 index 000000000..946305dff --- /dev/null +++ b/frontend/src/Settings/Indexers/Indexers/AddIndexerModalContent.css @@ -0,0 +1,5 @@ +.indexers { + display: flex; + justify-content: center; + flex-wrap: wrap; +} diff --git a/frontend/src/Settings/Indexers/Indexers/AddIndexerModalContent.js b/frontend/src/Settings/Indexers/Indexers/AddIndexerModalContent.js new file mode 100644 index 000000000..ea956f0a6 --- /dev/null +++ b/frontend/src/Settings/Indexers/Indexers/AddIndexerModalContent.js @@ -0,0 +1,115 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { kinds } from 'Helpers/Props'; +import Alert from 'Components/Alert'; +import Button from 'Components/Link/Button'; +import FieldSet from 'Components/FieldSet'; +import LoadingIndicator from 'Components/Loading/LoadingIndicator'; +import ModalContent from 'Components/Modal/ModalContent'; +import ModalHeader from 'Components/Modal/ModalHeader'; +import ModalBody from 'Components/Modal/ModalBody'; +import ModalFooter from 'Components/Modal/ModalFooter'; +import AddIndexerItem from './AddIndexerItem'; +import styles from './AddIndexerModalContent.css'; + +class AddIndexerModalContent extends Component { + + // + // Render + + render() { + const { + isFetching, + isPopulated, + error, + usenetIndexers, + torrentIndexers, + onIndexerSelect, + onModalClose + } = this.props; + + return ( + + + Add Indexer + + + + { + isFetching && + + } + + { + !isFetching && !!error && +
Unable to add a new indexer, please try again.
+ } + + { + isPopulated && !error && +
+ + +
Lidarr supports any indexer that uses the Newznab standard, as well as other indexers listed below.
+
For more information on the individual indexers, clink on the info buttons.
+
+ +
+
+ { + usenetIndexers.map((indexer) => { + return ( + + ); + }) + } +
+
+ +
+
+ { + torrentIndexers.map((indexer) => { + return ( + + ); + }) + } +
+
+
+ } +
+ + + +
+ ); + } +} + +AddIndexerModalContent.propTypes = { + isFetching: PropTypes.bool.isRequired, + isPopulated: PropTypes.bool.isRequired, + error: PropTypes.object, + usenetIndexers: PropTypes.arrayOf(PropTypes.object).isRequired, + torrentIndexers: PropTypes.arrayOf(PropTypes.object).isRequired, + onIndexerSelect: PropTypes.func.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default AddIndexerModalContent; diff --git a/frontend/src/Settings/Indexers/Indexers/AddIndexerModalContentConnector.js b/frontend/src/Settings/Indexers/Indexers/AddIndexerModalContentConnector.js new file mode 100644 index 000000000..986466c09 --- /dev/null +++ b/frontend/src/Settings/Indexers/Indexers/AddIndexerModalContentConnector.js @@ -0,0 +1,75 @@ +import _ from 'lodash'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import { fetchIndexerSchema, selectIndexerSchema } from 'Store/Actions/settingsActions'; +import AddIndexerModalContent from './AddIndexerModalContent'; + +function createMapStateToProps() { + return createSelector( + (state) => state.settings.indexers, + (indexers) => { + const { + isFetching, + error, + isPopulated, + schema + } = indexers; + + const usenetIndexers = _.filter(schema, { protocol: 'usenet' }); + const torrentIndexers = _.filter(schema, { protocol: 'torrent' }); + + return { + isFetching, + error, + isPopulated, + usenetIndexers, + torrentIndexers + }; + } + ); +} + +const mapDispatchToProps = { + fetchIndexerSchema, + selectIndexerSchema +}; + +class AddIndexerModalContentConnector extends Component { + + // + // Lifecycle + + componentDidMount() { + this.props.fetchIndexerSchema(); + } + + // + // Listeners + + onIndexerSelect = ({ implementation, name }) => { + this.props.selectIndexerSchema({ implementation, presetName: name }); + this.props.onModalClose({ indexerSelected: true }); + } + + // + // Render + + render() { + return ( + + ); + } +} + +AddIndexerModalContentConnector.propTypes = { + fetchIndexerSchema: PropTypes.func.isRequired, + selectIndexerSchema: PropTypes.func.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default connect(createMapStateToProps, mapDispatchToProps)(AddIndexerModalContentConnector); diff --git a/frontend/src/Settings/Indexers/Indexers/AddIndexerPresetMenuItem.js b/frontend/src/Settings/Indexers/Indexers/AddIndexerPresetMenuItem.js new file mode 100644 index 000000000..ddea8b043 --- /dev/null +++ b/frontend/src/Settings/Indexers/Indexers/AddIndexerPresetMenuItem.js @@ -0,0 +1,49 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import MenuItem from 'Components/Menu/MenuItem'; + +class AddIndexerPresetMenuItem extends Component { + + // + // Listeners + + onPress = () => { + const { + name, + implementation + } = this.props; + + this.props.onPress({ + name, + implementation + }); + } + + // + // Render + + render() { + const { + name, + implementation, + ...otherProps + } = this.props; + + return ( + + {name} + + ); + } +} + +AddIndexerPresetMenuItem.propTypes = { + name: PropTypes.string.isRequired, + implementation: PropTypes.string.isRequired, + onPress: PropTypes.func.isRequired +}; + +export default AddIndexerPresetMenuItem; diff --git a/frontend/src/Settings/Indexers/Indexers/EditIndexerModal.js b/frontend/src/Settings/Indexers/Indexers/EditIndexerModal.js new file mode 100644 index 000000000..fef70e29f --- /dev/null +++ b/frontend/src/Settings/Indexers/Indexers/EditIndexerModal.js @@ -0,0 +1,25 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import Modal from 'Components/Modal/Modal'; +import EditIndexerModalContentConnector from './EditIndexerModalContentConnector'; + +function EditIndexerModal({ isOpen, onModalClose, ...otherProps }) { + return ( + + + + ); +} + +EditIndexerModal.propTypes = { + isOpen: PropTypes.bool.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default EditIndexerModal; diff --git a/frontend/src/Settings/Indexers/Indexers/EditIndexerModalConnector.js b/frontend/src/Settings/Indexers/Indexers/EditIndexerModalConnector.js new file mode 100644 index 000000000..c308f004f --- /dev/null +++ b/frontend/src/Settings/Indexers/Indexers/EditIndexerModalConnector.js @@ -0,0 +1,65 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { clearPendingChanges } from 'Store/Actions/baseActions'; +import { cancelTestIndexer, cancelSaveIndexer } from 'Store/Actions/settingsActions'; +import EditIndexerModal from './EditIndexerModal'; + +function createMapDispatchToProps(dispatch, props) { + const section = 'indexers'; + + return { + dispatchClearPendingChanges() { + dispatch(clearPendingChanges({ section })); + }, + + dispatchCancelTestIndexer() { + dispatch(cancelTestIndexer({ section })); + }, + + dispatchCancelSaveIndexer() { + dispatch(cancelSaveIndexer({ section })); + } + }; +} + +class EditIndexerModalConnector extends Component { + + // + // Listeners + + onModalClose = () => { + this.props.dispatchClearPendingChanges(); + this.props.dispatchCancelTestIndexer(); + this.props.dispatchCancelSaveIndexer(); + this.props.onModalClose(); + } + + // + // Render + + render() { + const { + dispatchClearPendingChanges, + dispatchCancelTestIndexer, + dispatchCancelSaveIndexer, + ...otherProps + } = this.props; + + return ( + + ); + } +} + +EditIndexerModalConnector.propTypes = { + onModalClose: PropTypes.func.isRequired, + dispatchClearPendingChanges: PropTypes.func.isRequired, + dispatchCancelTestIndexer: PropTypes.func.isRequired, + dispatchCancelSaveIndexer: PropTypes.func.isRequired +}; + +export default connect(null, createMapDispatchToProps)(EditIndexerModalConnector); diff --git a/frontend/src/Settings/Indexers/Indexers/EditIndexerModalContent.css b/frontend/src/Settings/Indexers/Indexers/EditIndexerModalContent.css new file mode 100644 index 000000000..a3c7f464c --- /dev/null +++ b/frontend/src/Settings/Indexers/Indexers/EditIndexerModalContent.css @@ -0,0 +1,5 @@ +.deleteButton { + composes: button from 'Components/Link/Button.css'; + + margin-right: auto; +} diff --git a/frontend/src/Settings/Indexers/Indexers/EditIndexerModalContent.js b/frontend/src/Settings/Indexers/Indexers/EditIndexerModalContent.js new file mode 100644 index 000000000..00b41730c --- /dev/null +++ b/frontend/src/Settings/Indexers/Indexers/EditIndexerModalContent.js @@ -0,0 +1,177 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import { inputTypes, kinds } from 'Helpers/Props'; +import Button from 'Components/Link/Button'; +import SpinnerErrorButton from 'Components/Link/SpinnerErrorButton'; +import LoadingIndicator from 'Components/Loading/LoadingIndicator'; +import ModalContent from 'Components/Modal/ModalContent'; +import ModalHeader from 'Components/Modal/ModalHeader'; +import ModalBody from 'Components/Modal/ModalBody'; +import ModalFooter from 'Components/Modal/ModalFooter'; +import Form from 'Components/Form/Form'; +import FormGroup from 'Components/Form/FormGroup'; +import FormLabel from 'Components/Form/FormLabel'; +import FormInputGroup from 'Components/Form/FormInputGroup'; +import ProviderFieldFormGroup from 'Components/Form/ProviderFieldFormGroup'; +import styles from './EditIndexerModalContent.css'; + +function EditIndexerModalContent(props) { + const { + advancedSettings, + isFetching, + error, + isSaving, + isTesting, + saveError, + item, + onInputChange, + onFieldChange, + onModalClose, + onSavePress, + onTestPress, + onDeleteIndexerPress, + ...otherProps + } = props; + + const { + id, + name, + enableRss, + enableSearch, + supportsRss, + supportsSearch, + fields + } = item; + + return ( + + + {id ? 'Edit Indexer' : 'Add Indexer'} + + + + { + isFetching && + + } + + { + !isFetching && !!error && +
Unable to add a new indexer, please try again.
+ } + + { + !isFetching && !error && +
+ + Name + + + + + + Enable RSS + + + + + + Enable Search + + + + + { + fields.map((field) => { + return ( + + ); + }) + } + + + } +
+ + { + id && + + } + + + Test + + + + + + Save + + +
+ ); +} + +EditIndexerModalContent.propTypes = { + advancedSettings: PropTypes.bool.isRequired, + isFetching: PropTypes.bool.isRequired, + error: PropTypes.object, + isSaving: PropTypes.bool.isRequired, + isTesting: PropTypes.bool.isRequired, + saveError: PropTypes.object, + item: PropTypes.object.isRequired, + onInputChange: PropTypes.func.isRequired, + onFieldChange: PropTypes.func.isRequired, + onModalClose: PropTypes.func.isRequired, + onSavePress: PropTypes.func.isRequired, + onTestPress: PropTypes.func.isRequired, + onDeleteIndexerPress: PropTypes.func +}; + +export default EditIndexerModalContent; diff --git a/frontend/src/Settings/Indexers/Indexers/EditIndexerModalContentConnector.js b/frontend/src/Settings/Indexers/Indexers/EditIndexerModalContentConnector.js new file mode 100644 index 000000000..c4d9e597e --- /dev/null +++ b/frontend/src/Settings/Indexers/Indexers/EditIndexerModalContentConnector.js @@ -0,0 +1,94 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { createSelector } from 'reselect'; +import createProviderSettingsSelector from 'Store/Selectors/createProviderSettingsSelector'; +import { setIndexerValue, setIndexerFieldValue, saveIndexer, testIndexer } from 'Store/Actions/settingsActions'; +import connectSection from 'Store/connectSection'; +import EditIndexerModalContent from './EditIndexerModalContent'; + +function createMapStateToProps() { + return createSelector( + (state) => state.settings.advancedSettings, + createProviderSettingsSelector(), + (advancedSettings, indexer) => { + return { + advancedSettings, + ...indexer + }; + } + ); +} + +const mapDispatchToProps = { + setIndexerValue, + setIndexerFieldValue, + saveIndexer, + testIndexer +}; + +class EditIndexerModalContentConnector extends Component { + + // + // Lifecycle + + componentDidUpdate(prevProps, prevState) { + if (prevProps.isSaving && !this.props.isSaving && !this.props.saveError) { + this.props.onModalClose(); + } + } + + // + // Listeners + + onInputChange = ({ name, value }) => { + this.props.setIndexerValue({ name, value }); + } + + onFieldChange = ({ name, value }) => { + this.props.setIndexerFieldValue({ name, value }); + } + + onSavePress = () => { + this.props.saveIndexer({ id: this.props.id }); + } + + onTestPress = () => { + this.props.testIndexer({ id: this.props.id }); + } + + // + // Render + + render() { + return ( + + ); + } +} + +EditIndexerModalContentConnector.propTypes = { + id: PropTypes.number, + isFetching: PropTypes.bool.isRequired, + isSaving: PropTypes.bool.isRequired, + saveError: PropTypes.object, + item: PropTypes.object.isRequired, + setIndexerValue: PropTypes.func.isRequired, + setIndexerFieldValue: PropTypes.func.isRequired, + saveIndexer: PropTypes.func.isRequired, + testIndexer: PropTypes.func.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default connectSection( + createMapStateToProps, + mapDispatchToProps, + undefined, + undefined, + { section: 'indexers' } +)(EditIndexerModalContentConnector); diff --git a/frontend/src/Settings/Indexers/Indexers/Indexer.css b/frontend/src/Settings/Indexers/Indexers/Indexer.css new file mode 100644 index 000000000..d8e1a731e --- /dev/null +++ b/frontend/src/Settings/Indexers/Indexers/Indexer.css @@ -0,0 +1,19 @@ +.indexer { + composes: card from 'Components/Card.css'; + + width: 290px; +} + +.name { + @add-mixin truncate; + + margin-bottom: 20px; + font-weight: 300; + font-size: 24px; +} + +.enabled { + display: flex; + flex-wrap: wrap; + margin-top: 5px; +} diff --git a/frontend/src/Settings/Indexers/Indexers/Indexer.js b/frontend/src/Settings/Indexers/Indexers/Indexer.js new file mode 100644 index 000000000..221a6a239 --- /dev/null +++ b/frontend/src/Settings/Indexers/Indexers/Indexer.js @@ -0,0 +1,131 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { kinds } from 'Helpers/Props'; +import Card from 'Components/Card'; +import Label from 'Components/Label'; +import ConfirmModal from 'Components/Modal/ConfirmModal'; +import EditIndexerModalConnector from './EditIndexerModalConnector'; +import styles from './Indexer.css'; + +function getLabelKind(supports, enabled) { + if (!supports) { + return kinds.DEFAULT; + } + + if (!enabled) { + return kinds.DANGER; + } + + return kinds.SUCCESS; +} + +class Indexer extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + isEditIndexerModalOpen: false, + isDeleteIndexerModalOpen: false + }; + } + + // + // Listeners + + onEditIndexerPress = () => { + this.setState({ isEditIndexerModalOpen: true }); + } + + onEditIndexerModalClose = () => { + this.setState({ isEditIndexerModalOpen: false }); + } + + onDeleteIndexerPress = () => { + this.setState({ + isEditIndexerModalOpen: false, + isDeleteIndexerModalOpen: true + }); + } + + onDeleteIndexerModalClose= () => { + this.setState({ isDeleteIndexerModalOpen: false }); + } + + onConfirmDeleteIndexer = () => { + this.props.onConfirmDeleteIndexer(this.props.id); + } + + // + // Render + + render() { + const { + id, + name, + enableRss, + enableSearch, + supportsRss, + supportsSearch + } = this.props; + + return ( + +
+ {name} +
+ +
+ + + +
+ + + + +
+ ); + } +} + +Indexer.propTypes = { + id: PropTypes.number.isRequired, + name: PropTypes.string.isRequired, + enableRss: PropTypes.bool.isRequired, + enableSearch: PropTypes.bool.isRequired, + supportsRss: PropTypes.bool.isRequired, + supportsSearch: PropTypes.bool.isRequired, + onConfirmDeleteIndexer: PropTypes.func.isRequired +}; + +export default Indexer; diff --git a/frontend/src/Settings/Indexers/Indexers/Indexers.css b/frontend/src/Settings/Indexers/Indexers/Indexers.css new file mode 100644 index 000000000..ec8cb2891 --- /dev/null +++ b/frontend/src/Settings/Indexers/Indexers/Indexers.css @@ -0,0 +1,20 @@ +.indexers { + display: flex; + flex-wrap: wrap; +} + +.addIndexer { + composes: indexer from './Indexer.css'; + + background-color: $cardAlternateBackgroundColor; + color: $gray; + text-align: center; +} + +.center { + display: inline-block; + padding: 5px 20px 0; + border: 1px solid $borderColor; + border-radius: 4px; + background-color: $white; +} diff --git a/frontend/src/Settings/Indexers/Indexers/Indexers.js b/frontend/src/Settings/Indexers/Indexers/Indexers.js new file mode 100644 index 000000000..8b7d37a84 --- /dev/null +++ b/frontend/src/Settings/Indexers/Indexers/Indexers.js @@ -0,0 +1,117 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import sortByName from 'Utilities/Array/sortByName'; +import { icons } from 'Helpers/Props'; +import FieldSet from 'Components/FieldSet'; +import Card from 'Components/Card'; +import Icon from 'Components/Icon'; +import PageSectionContent from 'Components/Page/PageSectionContent'; +import Indexer from './Indexer'; +import AddIndexerModal from './AddIndexerModal'; +import EditIndexerModalConnector from './EditIndexerModalConnector'; +import styles from './Indexers.css'; + +class Indexers extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + isAddIndexerModalOpen: false, + isEditIndexerModalOpen: false + }; + } + + // + // Listeners + + onAddIndexerPress = () => { + this.setState({ isAddIndexerModalOpen: true }); + } + + onAddIndexerModalClose = ({ indexerSelected = false } = {}) => { + this.setState({ + isAddIndexerModalOpen: false, + isEditIndexerModalOpen: indexerSelected + }); + } + + onEditIndexerModalClose = () => { + this.setState({ isEditIndexerModalOpen: false }); + } + + // + // Render + + render() { + const { + items, + onConfirmDeleteIndexer, + ...otherProps + } = this.props; + + const { + isAddIndexerModalOpen, + isEditIndexerModalOpen + } = this.state; + + return ( +
+ +
+ { + items.sort(sortByName).map((item) => { + return ( + + ); + }) + } + + +
+ +
+
+
+ + + + +
+
+ ); + } +} + +Indexers.propTypes = { + isFetching: PropTypes.bool.isRequired, + error: PropTypes.object, + items: PropTypes.arrayOf(PropTypes.object).isRequired, + onConfirmDeleteIndexer: PropTypes.func.isRequired +}; + +export default Indexers; diff --git a/frontend/src/Settings/Indexers/Indexers/IndexersConnector.js b/frontend/src/Settings/Indexers/Indexers/IndexersConnector.js new file mode 100644 index 000000000..415dae32b --- /dev/null +++ b/frontend/src/Settings/Indexers/Indexers/IndexersConnector.js @@ -0,0 +1,58 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import { fetchIndexers, deleteIndexer } from 'Store/Actions/settingsActions'; +import Indexers from './Indexers'; + +function createMapStateToProps() { + return createSelector( + (state) => state.settings.indexers, + (indexers) => { + return { + ...indexers + }; + } + ); +} + +const mapDispatchToProps = { + fetchIndexers, + deleteIndexer +}; + +class IndexersConnector extends Component { + + // + // Lifecycle + + componentDidMount() { + this.props.fetchIndexers(); + } + + // + // Listeners + + onConfirmDeleteIndexer = (id) => { + this.props.deleteIndexer({ id }); + } + + // + // Render + + render() { + return ( + + ); + } +} + +IndexersConnector.propTypes = { + fetchIndexers: PropTypes.func.isRequired, + deleteIndexer: PropTypes.func.isRequired +}; + +export default connect(createMapStateToProps, mapDispatchToProps)(IndexersConnector); diff --git a/frontend/src/Settings/Indexers/Options/IndexerOptions.js b/frontend/src/Settings/Indexers/Options/IndexerOptions.js new file mode 100644 index 000000000..0a39ec7b7 --- /dev/null +++ b/frontend/src/Settings/Indexers/Options/IndexerOptions.js @@ -0,0 +1,93 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import { inputTypes } from 'Helpers/Props'; +import LoadingIndicator from 'Components/Loading/LoadingIndicator'; +import FieldSet from 'Components/FieldSet'; +import Form from 'Components/Form/Form'; +import FormGroup from 'Components/Form/FormGroup'; +import FormLabel from 'Components/Form/FormLabel'; +import FormInputGroup from 'Components/Form/FormInputGroup'; + +function IndexerOptions(props) { + const { + advancedSettings, + isFetching, + error, + settings, + hasSettings, + onInputChange + } = props; + + return ( +
+ { + isFetching && + + } + + { + !isFetching && error && +
Unable to load indexer options
+ } + + { + hasSettings && !isFetching && !error && +
+ + Minimum Age + + + + + + Retention + + + + + + RSS Sync Interval + + + +
+ } +
+ ); +} + +IndexerOptions.propTypes = { + advancedSettings: PropTypes.bool.isRequired, + isFetching: PropTypes.bool.isRequired, + error: PropTypes.object, + settings: PropTypes.object.isRequired, + hasSettings: PropTypes.bool.isRequired, + onInputChange: PropTypes.func.isRequired +}; + +export default IndexerOptions; diff --git a/frontend/src/Settings/Indexers/Options/IndexerOptionsConnector.js b/frontend/src/Settings/Indexers/Options/IndexerOptionsConnector.js new file mode 100644 index 000000000..e6d39edf9 --- /dev/null +++ b/frontend/src/Settings/Indexers/Options/IndexerOptionsConnector.js @@ -0,0 +1,92 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { createSelector } from 'reselect'; +import createSettingsSectionSelector from 'Store/Selectors/createSettingsSectionSelector'; +import { fetchIndexerOptions, setIndexerOptionsValue, saveIndexerOptions } from 'Store/Actions/settingsActions'; +import { clearPendingChanges } from 'Store/Actions/baseActions'; +import connectSection from 'Store/connectSection'; +import IndexerOptions from './IndexerOptions'; + +function createMapStateToProps() { + return createSelector( + (state) => state.settings.advancedSettings, + createSettingsSectionSelector(), + (advancedSettings, sectionSettings) => { + return { + advancedSettings, + ...sectionSettings + }; + } + ); +} + +const mapDispatchToProps = { + fetchIndexerOptions, + setIndexerOptionsValue, + saveIndexerOptions, + clearPendingChanges +}; + +class IndexerOptionsConnector extends Component { + + // + // Lifecycle + + componentDidMount() { + this.props.fetchIndexerOptions(); + } + + componentDidUpdate(prevProps) { + if (this.props.hasPendingChanges !== prevProps.hasPendingChanges) { + this.props.onHasPendingChange(this.props.hasPendingChanges); + } + } + + componentWillUnmount() { + this.props.clearPendingChanges({ section: this.props.section }); + } + + // + // Control + + save = () => { + this.props.saveIndexerOptions(); + } + + // + // Listeners + + onInputChange = ({ name, value }) => { + this.props.setIndexerOptionsValue({ name, value }); + } + + // + // Render + + render() { + return ( + + ); + } +} + +IndexerOptionsConnector.propTypes = { + section: PropTypes.string.isRequired, + hasPendingChanges: PropTypes.bool.isRequired, + fetchIndexerOptions: PropTypes.func.isRequired, + setIndexerOptionsValue: PropTypes.func.isRequired, + saveIndexerOptions: PropTypes.func.isRequired, + clearPendingChanges: PropTypes.func.isRequired, + onHasPendingChange: PropTypes.func.isRequired +}; + +export default connectSection( + createMapStateToProps, + mapDispatchToProps, + undefined, + { withRef: true }, + { section: 'indexerOptions' } +)(IndexerOptionsConnector); diff --git a/frontend/src/Settings/Indexers/Restrictions/EditRestrictionModal.js b/frontend/src/Settings/Indexers/Restrictions/EditRestrictionModal.js new file mode 100644 index 000000000..e9f42df98 --- /dev/null +++ b/frontend/src/Settings/Indexers/Restrictions/EditRestrictionModal.js @@ -0,0 +1,25 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import Modal from 'Components/Modal/Modal'; +import EditRestrictionModalContentConnector from './EditRestrictionModalContentConnector'; + +function EditRestrictionModal({ isOpen, onModalClose, ...otherProps }) { + return ( + + + + ); +} + +EditRestrictionModal.propTypes = { + isOpen: PropTypes.bool.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default EditRestrictionModal; diff --git a/frontend/src/Settings/Indexers/Restrictions/EditRestrictionModalConnector.js b/frontend/src/Settings/Indexers/Restrictions/EditRestrictionModalConnector.js new file mode 100644 index 000000000..4483f7894 --- /dev/null +++ b/frontend/src/Settings/Indexers/Restrictions/EditRestrictionModalConnector.js @@ -0,0 +1,39 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { clearPendingChanges } from 'Store/Actions/baseActions'; +import EditRestrictionModal from './EditRestrictionModal'; + +const mapDispatchToProps = { + clearPendingChanges +}; + +class EditRestrictionModalConnector extends Component { + + // + // Listeners + + onModalClose = () => { + this.props.clearPendingChanges({ section: 'restrictions' }); + this.props.onModalClose(); + } + + // + // Render + + render() { + return ( + + ); + } +} + +EditRestrictionModalConnector.propTypes = { + onModalClose: PropTypes.func.isRequired, + clearPendingChanges: PropTypes.func.isRequired +}; + +export default connect(null, mapDispatchToProps)(EditRestrictionModalConnector); diff --git a/frontend/src/Settings/Indexers/Restrictions/EditRestrictionModalContent.css b/frontend/src/Settings/Indexers/Restrictions/EditRestrictionModalContent.css new file mode 100644 index 000000000..a3c7f464c --- /dev/null +++ b/frontend/src/Settings/Indexers/Restrictions/EditRestrictionModalContent.css @@ -0,0 +1,5 @@ +.deleteButton { + composes: button from 'Components/Link/Button.css'; + + margin-right: auto; +} diff --git a/frontend/src/Settings/Indexers/Restrictions/EditRestrictionModalContent.js b/frontend/src/Settings/Indexers/Restrictions/EditRestrictionModalContent.js new file mode 100644 index 000000000..eea3abad0 --- /dev/null +++ b/frontend/src/Settings/Indexers/Restrictions/EditRestrictionModalContent.js @@ -0,0 +1,126 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import { inputTypes, kinds } from 'Helpers/Props'; +import Button from 'Components/Link/Button'; +import SpinnerErrorButton from 'Components/Link/SpinnerErrorButton'; +import ModalContent from 'Components/Modal/ModalContent'; +import ModalHeader from 'Components/Modal/ModalHeader'; +import ModalBody from 'Components/Modal/ModalBody'; +import ModalFooter from 'Components/Modal/ModalFooter'; +import Form from 'Components/Form/Form'; +import FormGroup from 'Components/Form/FormGroup'; +import FormLabel from 'Components/Form/FormLabel'; +import FormInputGroup from 'Components/Form/FormInputGroup'; +import styles from './EditRestrictionModalContent.css'; + +function EditRestrictionModalContent(props) { + const { + isSaving, + saveError, + item, + onInputChange, + onModalClose, + onSavePress, + onDeleteRestrictionPress, + ...otherProps + } = props; + + const { + id, + required, + ignored, + tags + } = item; + + return ( + + + {id ? 'Edit Restriction' : 'Add Restriction'} + + + +
+ + Must Contain + + + + + + Must Not Contain + + + + + + Tags + + + +
+
+ + { + id && + + } + + + + + Save + + +
+ ); +} + +EditRestrictionModalContent.propTypes = { + isSaving: PropTypes.bool.isRequired, + saveError: PropTypes.object, + item: PropTypes.object.isRequired, + onInputChange: PropTypes.func.isRequired, + onModalClose: PropTypes.func.isRequired, + onSavePress: PropTypes.func.isRequired, + onDeleteRestrictionPress: PropTypes.func +}; + +export default EditRestrictionModalContent; diff --git a/frontend/src/Settings/Indexers/Restrictions/EditRestrictionModalContentConnector.js b/frontend/src/Settings/Indexers/Restrictions/EditRestrictionModalContentConnector.js new file mode 100644 index 000000000..322b0a8d9 --- /dev/null +++ b/frontend/src/Settings/Indexers/Restrictions/EditRestrictionModalContentConnector.js @@ -0,0 +1,111 @@ +import _ from 'lodash'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import selectSettings from 'Store/Selectors/selectSettings'; +import { setRestrictionValue, saveRestriction } from 'Store/Actions/settingsActions'; +import EditRestrictionModalContent from './EditRestrictionModalContent'; + +const newRestriction = { + required: '', + ignored: '', + tags: [] +}; + +function createMapStateToProps() { + return createSelector( + (state, { id }) => id, + (state) => state.settings.restrictions, + (id, restrictions) => { + const { + isFetching, + error, + isSaving, + saveError, + pendingChanges, + items + } = restrictions; + + const profile = id ? _.find(items, { id }) : newRestriction; + const settings = selectSettings(profile, pendingChanges, saveError); + + return { + id, + isFetching, + error, + isSaving, + saveError, + item: settings.settings, + ...settings + }; + } + ); +} + +const mapDispatchToProps = { + setRestrictionValue, + saveRestriction +}; + +class EditRestrictionModalContentConnector extends Component { + + // + // Lifecycle + + componentDidMount() { + if (!this.props.id) { + Object.keys(newRestriction).forEach((name) => { + this.props.setRestrictionValue({ + name, + value: newRestriction[name] + }); + }); + } + } + + componentDidUpdate(prevProps, prevState) { + if (prevProps.isSaving && !this.props.isSaving && !this.props.saveError) { + this.props.onModalClose(); + } + } + + // + // Listeners + + onInputChange = ({ name, value }) => { + this.props.setRestrictionValue({ name, value }); + } + + onSavePress = () => { + this.props.saveRestriction({ id: this.props.id }); + } + + // + // Render + + render() { + return ( + + ); + } +} + +EditRestrictionModalContentConnector.propTypes = { + id: PropTypes.number, + isFetching: PropTypes.bool.isRequired, + isSaving: PropTypes.bool.isRequired, + saveError: PropTypes.object, + item: PropTypes.object.isRequired, + setRestrictionValue: PropTypes.func.isRequired, + saveRestriction: PropTypes.func.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default connect(createMapStateToProps, mapDispatchToProps)(EditRestrictionModalContentConnector); diff --git a/frontend/src/Settings/Indexers/Restrictions/Restriction.css b/frontend/src/Settings/Indexers/Restrictions/Restriction.css new file mode 100644 index 000000000..0e84466f9 --- /dev/null +++ b/frontend/src/Settings/Indexers/Restrictions/Restriction.css @@ -0,0 +1,11 @@ +.restriction { + composes: card from 'Components/Card.css'; + + width: 290px; +} + +.enabled { + display: flex; + flex-wrap: wrap; + margin-top: 5px; +} diff --git a/frontend/src/Settings/Indexers/Restrictions/Restriction.js b/frontend/src/Settings/Indexers/Restrictions/Restriction.js new file mode 100644 index 000000000..bdd457aca --- /dev/null +++ b/frontend/src/Settings/Indexers/Restrictions/Restriction.js @@ -0,0 +1,147 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import split from 'Utilities/String/split'; +import { kinds } from 'Helpers/Props'; +import Card from 'Components/Card'; +import Label from 'Components/Label'; +import TagList from 'Components/TagList'; +import ConfirmModal from 'Components/Modal/ConfirmModal'; +import EditRestrictionModalConnector from './EditRestrictionModalConnector'; +import styles from './Restriction.css'; + +class Restriction extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + isEditRestrictionModalOpen: false, + isDeleteRestrictionModalOpen: false + }; + } + + // + // Listeners + + onEditRestrictionPress = () => { + this.setState({ isEditRestrictionModalOpen: true }); + } + + onEditRestrictionModalClose = () => { + this.setState({ isEditRestrictionModalOpen: false }); + } + + onDeleteRestrictionPress = () => { + this.setState({ + isEditRestrictionModalOpen: false, + isDeleteRestrictionModalOpen: true + }); + } + + onDeleteRestrictionModalClose= () => { + this.setState({ isDeleteRestrictionModalOpen: false }); + } + + onConfirmDeleteRestriction = () => { + this.props.onConfirmDeleteRestriction(this.props.id); + } + + // + // Render + + render() { + const { + id, + required, + ignored, + tags, + tagList + } = this.props; + + return ( + +
+ { + split(required).map((item) => { + if (!item) { + return null; + } + + return ( + + ); + }) + } +
+ +
+ { + split(ignored).map((item) => { + if (!item) { + return null; + } + + return ( + + ); + }) + } +
+ + + + + + +
+ ); + } +} + +Restriction.propTypes = { + id: PropTypes.number.isRequired, + required: PropTypes.string.isRequired, + ignored: PropTypes.string.isRequired, + tags: PropTypes.arrayOf(PropTypes.number).isRequired, + tagList: PropTypes.arrayOf(PropTypes.object).isRequired, + onConfirmDeleteRestriction: PropTypes.func.isRequired +}; + +Restriction.defaultProps = { + required: '', + ignored: '' +}; + +export default Restriction; diff --git a/frontend/src/Settings/Indexers/Restrictions/Restrictions.css b/frontend/src/Settings/Indexers/Restrictions/Restrictions.css new file mode 100644 index 000000000..904a66a57 --- /dev/null +++ b/frontend/src/Settings/Indexers/Restrictions/Restrictions.css @@ -0,0 +1,20 @@ +.restrictions { + display: flex; + flex-wrap: wrap; +} + +.addRestriction { + composes: restriction from './Restriction.css'; + + background-color: $cardAlternateBackgroundColor; + color: $gray; + text-align: center; +} + +.center { + display: inline-block; + padding: 5px 20px 0; + border: 1px solid $borderColor; + border-radius: 4px; + background-color: $white; +} diff --git a/frontend/src/Settings/Indexers/Restrictions/Restrictions.js b/frontend/src/Settings/Indexers/Restrictions/Restrictions.js new file mode 100644 index 000000000..411b95ea8 --- /dev/null +++ b/frontend/src/Settings/Indexers/Restrictions/Restrictions.js @@ -0,0 +1,100 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { icons } from 'Helpers/Props'; +import FieldSet from 'Components/FieldSet'; +import Card from 'Components/Card'; +import Icon from 'Components/Icon'; +import PageSectionContent from 'Components/Page/PageSectionContent'; +import Restriction from './Restriction'; +import EditRestrictionModalConnector from './EditRestrictionModalConnector'; +import styles from './Restrictions.css'; + +class Restrictions extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + isAddRestrictionModalOpen: false + }; + } + + // + // Listeners + + onAddRestrictionPress = () => { + this.setState({ isAddRestrictionModalOpen: true }); + } + + onAddRestrictionModalClose = () => { + this.setState({ isAddRestrictionModalOpen: false }); + } + + // + // Render + + render() { + const { + items, + tagList, + onConfirmDeleteRestriction, + ...otherProps + } = this.props; + + return ( +
+ +
+ +
+ +
+
+ + { + items.map((item) => { + return ( + + ); + }) + } +
+ + +
+
+ ); + } +} + +Restrictions.propTypes = { + isFetching: PropTypes.bool.isRequired, + error: PropTypes.object, + items: PropTypes.arrayOf(PropTypes.object).isRequired, + tagList: PropTypes.arrayOf(PropTypes.object).isRequired, + onConfirmDeleteRestriction: PropTypes.func.isRequired +}; + +export default Restrictions; diff --git a/frontend/src/Settings/Indexers/Restrictions/RestrictionsConnector.js b/frontend/src/Settings/Indexers/Restrictions/RestrictionsConnector.js new file mode 100644 index 000000000..c53c05de2 --- /dev/null +++ b/frontend/src/Settings/Indexers/Restrictions/RestrictionsConnector.js @@ -0,0 +1,61 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import { fetchRestrictions, deleteRestriction } from 'Store/Actions/settingsActions'; +import createTagsSelector from 'Store/Selectors/createTagsSelector'; +import Restrictions from './Restrictions'; + +function createMapStateToProps() { + return createSelector( + (state) => state.settings.restrictions, + createTagsSelector(), + (restrictions, tagList) => { + return { + ...restrictions, + tagList + }; + } + ); +} + +const mapDispatchToProps = { + fetchRestrictions, + deleteRestriction +}; + +class RestrictionsConnector extends Component { + + // + // Lifecycle + + componentDidMount() { + this.props.fetchRestrictions(); + } + + // + // Listeners + + onConfirmDeleteRestriction = (id) => { + this.props.deleteRestriction({ id }); + } + + // + // Render + + render() { + return ( + + ); + } +} + +RestrictionsConnector.propTypes = { + fetchRestrictions: PropTypes.func.isRequired, + deleteRestriction: PropTypes.func.isRequired +}; + +export default connect(createMapStateToProps, mapDispatchToProps)(RestrictionsConnector); diff --git a/frontend/src/Settings/MediaManagement/MediaManagement.js b/frontend/src/Settings/MediaManagement/MediaManagement.js new file mode 100644 index 000000000..21d33bea6 --- /dev/null +++ b/frontend/src/Settings/MediaManagement/MediaManagement.js @@ -0,0 +1,347 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { inputTypes, sizes } from 'Helpers/Props'; +import LoadingIndicator from 'Components/Loading/LoadingIndicator'; +import FieldSet from 'Components/FieldSet'; +import PageContent from 'Components/Page/PageContent'; +import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector'; +import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector'; +import Form from 'Components/Form/Form'; +import FormGroup from 'Components/Form/FormGroup'; +import FormLabel from 'Components/Form/FormLabel'; +import FormInputGroup from 'Components/Form/FormInputGroup'; +import NamingConnector from './Naming/NamingConnector'; + +class MediaManagement extends Component { + + // + // Render + + render() { + const { + advancedSettings, + isFetching, + error, + settings, + hasSettings, + isMono, + onInputChange, + onSavePress, + ...otherProps + } = this.props; + + const fileDateOptions = [ + { key: 'none', value: 'None' }, + { key: 'localAirDate', value: 'Local Air Date' }, + { key: 'utcAirDate', value: 'UTC Air Date' } + ]; + + return ( + + + + + { + isFetching && + + } + + { + !isFetching && error && +
Unable to load Media Management settings
+ } + + { + hasSettings && !isFetching && !error && +
+ + + { + advancedSettings && +
+ + Create empty artist folders + + + +
+ } + + { + advancedSettings && +
+ { + isMono && + + Skip Free Space Check + + + + } + + + Use Hardlinks instead of Copy + + + + + + Import Extra Files + + + + + { + settings.importExtraFiles.value && + + Import Extra Files + + + + } +
+ } + +
+ + Ignore Deleted Tracks + + + + + + Download Propers + + + + + + Analyse audio files + + + + + + Change File Date + + + + + + Recycling Bin + + + +
+ + { + advancedSettings && isMono && +
+ + Set Permissions + + + + + + File chmod mask + + + + + + Folder chmod mask + + + + + + chown User + + + + + + chown Group + + + +
+ } + + } +
+
+ ); + } + +} + +MediaManagement.propTypes = { + advancedSettings: PropTypes.bool.isRequired, + isFetching: PropTypes.bool.isRequired, + error: PropTypes.object, + settings: PropTypes.object.isRequired, + hasSettings: PropTypes.bool.isRequired, + isMono: PropTypes.bool.isRequired, + onSavePress: PropTypes.func.isRequired, + onInputChange: PropTypes.func.isRequired +}; + +export default MediaManagement; diff --git a/frontend/src/Settings/MediaManagement/MediaManagementConnector.js b/frontend/src/Settings/MediaManagement/MediaManagementConnector.js new file mode 100644 index 000000000..5f7ed7141 --- /dev/null +++ b/frontend/src/Settings/MediaManagement/MediaManagementConnector.js @@ -0,0 +1,91 @@ +import _ from 'lodash'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { createSelector } from 'reselect'; +import createSettingsSectionSelector from 'Store/Selectors/createSettingsSectionSelector'; +import createSystemStatusSelector from 'Store/Selectors/createSystemStatusSelector'; +import { fetchMediaManagementSettings, setMediaManagementSettingsValue, saveMediaManagementSettings, saveNamingSettings } from 'Store/Actions/settingsActions'; +import { clearPendingChanges } from 'Store/Actions/baseActions'; +import connectSection from 'Store/connectSection'; +import MediaManagement from './MediaManagement'; + +function createMapStateToProps() { + return createSelector( + (state) => state.settings.advancedSettings, + (state) => state.settings.naming, + createSettingsSectionSelector(), + createSystemStatusSelector(), + (advancedSettings, namingSettings, sectionSettings, systemStatus) => { + return { + advancedSettings, + ...sectionSettings, + hasPendingChanges: !_.isEmpty(namingSettings.pendingChanges) || sectionSettings.hasPendingChanges, + isMono: systemStatus.isMono + }; + } + ); +} + +const mapDispatchToProps = { + fetchMediaManagementSettings, + setMediaManagementSettingsValue, + saveMediaManagementSettings, + saveNamingSettings, + clearPendingChanges +}; + +class MediaManagementConnector extends Component { + + // + // Lifecycle + + componentDidMount() { + this.props.fetchMediaManagementSettings(); + } + + componentWillUnmount() { + this.props.clearPendingChanges({ section: this.props.section }); + } + + // + // Listeners + + onInputChange = ({ name, value }) => { + this.props.setMediaManagementSettingsValue({ name, value }); + } + + onSavePress = () => { + this.props.saveMediaManagementSettings(); + this.props.saveNamingSettings(); + } + + // + // Render + + render() { + return ( + + ); + } +} + +MediaManagementConnector.propTypes = { + section: PropTypes.string.isRequired, + fetchMediaManagementSettings: PropTypes.func.isRequired, + setMediaManagementSettingsValue: PropTypes.func.isRequired, + saveMediaManagementSettings: PropTypes.func.isRequired, + saveNamingSettings: PropTypes.func.isRequired, + clearPendingChanges: PropTypes.func.isRequired +}; + +export default connectSection( + createMapStateToProps, + mapDispatchToProps, + undefined, + undefined, + { section: 'mediaManagement' } +)(MediaManagementConnector); diff --git a/frontend/src/Settings/MediaManagement/Naming/Naming.css b/frontend/src/Settings/MediaManagement/Naming/Naming.css new file mode 100644 index 000000000..a0fbf7f19 --- /dev/null +++ b/frontend/src/Settings/MediaManagement/Naming/Naming.css @@ -0,0 +1,5 @@ +.namingInput { + composes: text from 'Components/Form/TextInput.css'; + + font-family: $monoSpaceFontFamily; +} diff --git a/frontend/src/Settings/MediaManagement/Naming/Naming.js b/frontend/src/Settings/MediaManagement/Naming/Naming.js new file mode 100644 index 000000000..8e060aea0 --- /dev/null +++ b/frontend/src/Settings/MediaManagement/Naming/Naming.js @@ -0,0 +1,264 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { inputTypes, sizes } from 'Helpers/Props'; +import LoadingIndicator from 'Components/Loading/LoadingIndicator'; +import FormInputButton from 'Components/Form/FormInputButton'; +import FieldSet from 'Components/FieldSet'; +import Form from 'Components/Form/Form'; +import FormGroup from 'Components/Form/FormGroup'; +import FormLabel from 'Components/Form/FormLabel'; +import FormInputGroup from 'Components/Form/FormInputGroup'; +import NamingModal from './NamingModal'; +import styles from './Naming.css'; + +class Naming extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + isNamingModalOpen: false, + namingModalOptions: null + }; + } + + // + // Listeners + + onStandardNamingModalOpenClick = () => { + this.setState({ + isNamingModalOpen: true, + namingModalOptions: { + name: 'standardTrackFormat', + album: true, + track: true, + additional: true + } + }); + } + + onArtistFolderNamingModalOpenClick = () => { + this.setState({ + isNamingModalOpen: true, + namingModalOptions: { + name: 'artistFolderFormat' + } + }); + } + + onAlbumFolderNamingModalOpenClick = () => { + this.setState({ + isNamingModalOpen: true, + namingModalOptions: { + name: 'albumFolderFormat', + album: true + } + }); + } + + onNamingModalClose = () => { + this.setState({ isNamingModalOpen: false }); + } + + // + // Render + + render() { + const { + advancedSettings, + isFetching, + error, + settings, + hasSettings, + examples, + examplesPopulated, + onInputChange + } = this.props; + + const { + isNamingModalOpen, + namingModalOptions + } = this.state; + + const renameTracks = hasSettings && settings.renameTracks.value; + + const standardTrackFormatHelpTexts = []; + const standardTrackFormatErrors = []; + const artistFolderFormatHelpTexts = []; + const artistFolderFormatErrors = []; + const albumFolderFormatHelpTexts = []; + const albumFolderFormatErrors = []; + + if (examplesPopulated) { + if (examples.singleTrackExample) { + standardTrackFormatHelpTexts.push(`Single Track: ${examples.singleTrackExample}`); + } else { + standardTrackFormatErrors.push('Single Track: Invalid Format'); + } + + // if (examples.multiEpisodeExample) { + // standardTrackFormatHelpTexts.push(`Multi Episode: ${examples.multiEpisodeExample}`); + // } else { + // standardTrackFormatErrors.push('Multi Episode: Invalid Format'); + // } + + // if (examples.dailyEpisodeExample) { + // dailyEpisodeFormatHelpTexts.push(`Example: ${examples.dailyEpisodeExample}`); + // } else { + // dailyEpisodeFormatErrors.push('Invalid Format'); + // } + + // if (examples.animeEpisodeExample) { + // animeEpisodeFormatHelpTexts.push(`Single Episode: ${examples.animeEpisodeExample}`); + // } else { + // animeEpisodeFormatErrors.push('Single Episode: Invalid Format'); + // } + + // if (examples.animeMultiEpisodeExample) { + // animeEpisodeFormatHelpTexts.push(`Multi Episode: ${examples.animeMultiEpisodeExample}`); + // } else { + // animeEpisodeFormatErrors.push('Multi Episode: Invalid Format'); + // } + + if (examples.artistFolderExample) { + artistFolderFormatHelpTexts.push(`Example: ${examples.artistFolderExample}`); + } else { + artistFolderFormatErrors.push('Invalid Format'); + } + + if (examples.albumFolderExample) { + albumFolderFormatHelpTexts.push(`Example: ${examples.albumFolderExample}`); + } else { + albumFolderFormatErrors.push('Invalid Format'); + } + } + + return ( +
+ { + isFetching && + + } + + { + !isFetching && error && +
Unable to load Naming settings
+ } + + { + hasSettings && !isFetching && !error && +
+ + Rename Tracks + + + + + + Replace Illegal Characters + + + + + { + renameTracks && +
+ + Standard Track Format + + ?} + onChange={onInputChange} + {...settings.standardTrackFormat} + helpTexts={standardTrackFormatHelpTexts} + errors={[...standardTrackFormatErrors, ...settings.standardTrackFormat.errors]} + /> + + +
+ } + + + Artist Folder Format + + ?} + onChange={onInputChange} + {...settings.artistFolderFormat} + helpTexts={['Only used when adding a new artist', ...artistFolderFormatHelpTexts]} + errors={[...artistFolderFormatErrors, ...settings.artistFolderFormat.errors]} + /> + + + + Album Folder Format + + ?} + onChange={onInputChange} + {...settings.albumFolderFormat} + helpTexts={albumFolderFormatHelpTexts} + errors={[...albumFolderFormatErrors, ...settings.albumFolderFormat.errors]} + /> + + + { + namingModalOptions && + + } + + } +
+ ); + } + +} + +Naming.propTypes = { + advancedSettings: PropTypes.bool.isRequired, + isFetching: PropTypes.bool.isRequired, + error: PropTypes.object, + settings: PropTypes.object.isRequired, + hasSettings: PropTypes.bool.isRequired, + examples: PropTypes.object.isRequired, + examplesPopulated: PropTypes.bool.isRequired, + onInputChange: PropTypes.func.isRequired +}; + +export default Naming; diff --git a/frontend/src/Settings/MediaManagement/Naming/NamingConnector.js b/frontend/src/Settings/MediaManagement/Naming/NamingConnector.js new file mode 100644 index 000000000..04d200eb1 --- /dev/null +++ b/frontend/src/Settings/MediaManagement/Naming/NamingConnector.js @@ -0,0 +1,102 @@ +import _ from 'lodash'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { createSelector } from 'reselect'; +import createSettingsSectionSelector from 'Store/Selectors/createSettingsSectionSelector'; +import { fetchNamingSettings, setNamingSettingsValue, fetchNamingExamples } from 'Store/Actions/settingsActions'; +import { clearPendingChanges } from 'Store/Actions/baseActions'; +import connectSection from 'Store/connectSection'; +import Naming from './Naming'; + +function createMapStateToProps() { + return createSelector( + (state) => state.settings.advancedSettings, + (state) => state.settings.namingExamples, + createSettingsSectionSelector(), + (advancedSettings, examples, sectionSettings) => { + return { + advancedSettings, + examples: examples.item, + examplesPopulated: !_.isEmpty(examples.item), + ...sectionSettings + }; + } + ); +} + +const mapDispatchToProps = { + fetchNamingSettings, + setNamingSettingsValue, + fetchNamingExamples, + clearPendingChanges +}; + +class NamingConnector extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this._namingExampleTimeout = null; + } + + componentDidMount() { + this.props.fetchNamingSettings(); + this.props.fetchNamingExamples(); + } + + componentWillUnmount() { + this.props.clearPendingChanges({ section: this.props.section }); + } + + // + // Control + + _fetchNamingExamples = () => { + this.props.fetchNamingExamples(); + } + + // + // Listeners + + onInputChange = ({ name, value }) => { + this.props.setNamingSettingsValue({ name, value }); + + if (this._namingExampleTimeout) { + clearTimeout(this._namingExampleTimeout); + } + + this._namingExampleTimeout = setTimeout(this._fetchNamingExamples, 1000); + } + + // + // Render + + render() { + return ( + + ); + } +} + +NamingConnector.propTypes = { + section: PropTypes.string.isRequired, + fetchNamingSettings: PropTypes.func.isRequired, + setNamingSettingsValue: PropTypes.func.isRequired, + fetchNamingExamples: PropTypes.func.isRequired, + clearPendingChanges: PropTypes.func.isRequired +}; + +export default connectSection( + createMapStateToProps, + mapDispatchToProps, + undefined, + undefined, + { section: 'naming' } +)(NamingConnector); diff --git a/frontend/src/Settings/MediaManagement/Naming/NamingModal.css b/frontend/src/Settings/MediaManagement/Naming/NamingModal.css new file mode 100644 index 000000000..708a763eb --- /dev/null +++ b/frontend/src/Settings/MediaManagement/Naming/NamingModal.css @@ -0,0 +1,17 @@ +.groups { + display: flex; + justify-content: space-between; + flex-wrap: wrap; + margin-bottom: 20px; +} + +.namingSelectContainer { + display: flex; + justify-content: flex-end; +} + +.namingSelect { + composes: select from 'Components/Form/SelectInput.css'; + + width: 200px; +} diff --git a/frontend/src/Settings/MediaManagement/Naming/NamingModal.js b/frontend/src/Settings/MediaManagement/Naming/NamingModal.js new file mode 100644 index 000000000..89b5a53a1 --- /dev/null +++ b/frontend/src/Settings/MediaManagement/Naming/NamingModal.js @@ -0,0 +1,433 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { sizes } from 'Helpers/Props'; +import FieldSet from 'Components/FieldSet'; +import Button from 'Components/Link/Button'; +import SelectInput from 'Components/Form/SelectInput'; +import TextInput from 'Components/Form/TextInput'; +import Modal from 'Components/Modal/Modal'; +import ModalContent from 'Components/Modal/ModalContent'; +import ModalHeader from 'Components/Modal/ModalHeader'; +import ModalBody from 'Components/Modal/ModalBody'; +import ModalFooter from 'Components/Modal/ModalFooter'; +import NamingOption from './NamingOption'; +import styles from './NamingModal.css'; + +class NamingModal extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + case: 'title' + }; + } + + // + // Listeners + + onNamingCaseChange = (event) => { + this.setState({ case: event.value }); + } + + // + // Render + + render() { + const { + name, + value, + isOpen, + advancedSettings, + album, + track, + additional, + onInputChange, + onModalClose + } = this.props; + + const namingOptions = [ + { key: 'title', value: 'Default Case' }, + { key: 'lower', value: 'Lower Case' }, + { key: 'upper', value: 'Upper Case' } + ]; + + const fileNameTokens = [ + { + token: '{Artist Name} - {Album Title} - {track:00} - {Track Title} {Quality Full}', + example: 'Artist Name - Album Title - 01 - Track Title MP3-320 Proper' + }, + { + token: '{Artist.Name}.{Album.Title}.{track:00}.{TrackClean.Title}.{Quality.Full}', + example: 'Artist.Name.Album.Title.01.Track.Title.MP3-320' + } + ]; + + const artistTokens = [ + { token: '{Artist Name}', example: 'Artist Name' }, + { token: '{Artist.Name}', example: 'Artist.Name' }, + { token: '{Artist_Name}', example: 'Artist_Name' }, + + { token: '{Artist NameThe}', example: 'Artist Name, The' }, + + { token: '{Artist CleanName}', example: 'Artist Name' }, + { token: '{Artist.CleanName}', example: 'Artist.Name' }, + { token: '{Artist_CleanName}', example: 'Artist_Name' } + ]; + + const albumTokens = [ + { token: '{Album Title}', example: 'Album Title' }, + { token: '{Album.Title}', example: 'Album.Title' }, + { token: '{Album_Name}', example: 'Album_Name' }, + + { token: '{Album TitleThe}', example: 'Album Title, The' }, + + { token: '{Album CleanTitle}', example: 'Album Title' }, + { token: '{Album.CleanTitle}', example: 'Album.Title' }, + { token: '{Album_CleanTitle}', example: 'Album_Title' } + ]; + + const trackTokens = [ + { token: '{track:0}', example: '1' }, + { token: '{track:00}', example: '01' } + ]; + + const releaseDateTokens = [ + { token: '{Release Year}', example: '2016' } + ]; + + const trackTitleTokens = [ + { token: '{Track Title}', example: 'Track Title' }, + { token: '{Track.Title}', example: 'Track.Title' }, + { token: '{Track_Title}', example: 'Track_Title' }, + { token: '{Track CleanTitle}', example: 'Track Title' }, + { token: '{Track.CleanTitle}', example: 'Track.Title' }, + { token: '{Track_CleanTitle}', example: 'Track_Title' } + ]; + + const qualityTokens = [ + { token: '{Quality Full}', example: 'HDTV 720p Proper' }, + { token: '{Quality-Full}', example: 'HDTV-720p-Proper' }, + { token: '{Quality.Full}', example: 'HDTV.720p.Proper' }, + { token: '{Quality_Full}', example: 'HDTV_720p_Proper' }, + { token: '{Quality Title}', example: 'HDTV 720p' }, + { token: '{Quality-Title}', example: 'HDTV-720p' }, + { token: '{Quality.Title}', example: 'HDTV.720p' }, + { token: '{Quality_Title}', example: 'HDTV_720p' } + ]; + + const mediaInfoTokens = [ + { token: '{MediaInfo Simple}', example: 'x264 DTS' }, + { token: '{MediaInfo.Simple}', example: 'x264.DTS' }, + { token: '{MediaInfo_Simple}', example: 'x264_DTS' }, + { token: '{MediaInfo Full}', example: 'x264 DTS [EN+DE]' }, + { token: '{MediaInfo.Full}', example: 'x264.DTS.[EN+DE]' }, + { token: '{MediaInfo_Full}', example: 'x264_DTS_[EN+DE]' }, + { token: '{MediaInfo VideoCodec}', example: 'x264' }, + { token: '{MediaInfo AudioFormat}', example: 'DTS' }, + { token: '{MediaInfo AudioChannels}', example: '5.1' } + ]; + + const releaseGroupTokens = [ + { token: '{Release Group}', example: 'Rls Grp' }, + { token: '{Release.Group}', example: 'Rls.Grp' }, + { token: '{Release_Group}', example: 'Rls_Grp' } + ]; + + const originalTokens = [ + { token: '{Original Title}', example: 'Artist.Name.S01E01.HDTV.x264-EVOLVE' }, + { token: '{Original Filename}', example: 'artist.name.s01e01.hdtv.x264-EVOLVE' } + ]; + + return ( + + + + File Name Tokens + + + +
+ +
+ + { + !advancedSettings && +
+
+ { + fileNameTokens.map(({ token, example }) => { + return ( + + ); + } + ) + } +
+
+ } + +
+
+ { + artistTokens.map(({ token, example }) => { + return ( + + ); + } + ) + } +
+
+ + { + album && +
+
+
+ { + albumTokens.map(({ token, example }) => { + return ( + + ); + } + ) + } +
+
+ +
+
+ { + releaseDateTokens.map(({ token, example }) => { + return ( + + ); + } + ) + } +
+
+
+ } + + { + track && +
+
+
+ { + trackTokens.map(({ token, example }) => { + return ( + + ); + } + ) + } +
+
+ +
+ } + + { + additional && +
+
+
+ { + trackTitleTokens.map(({ token, example }) => { + return ( + + ); + } + ) + } +
+
+ +
+
+ { + qualityTokens.map(({ token, example }) => { + return ( + + ); + } + ) + } +
+
+ +
+
+ { + mediaInfoTokens.map(({ token, example }) => { + return ( + + ); + } + ) + } +
+
+ +
+
+ { + releaseGroupTokens.map(({ token, example }) => { + return ( + + ); + } + ) + } +
+
+ +
+
+ { + originalTokens.map(({ token, example }) => { + return ( + + ); + } + ) + } +
+
+
+ } +
+ + + + + +
+
+ ); + } +} + +NamingModal.propTypes = { + name: PropTypes.string.isRequired, + value: PropTypes.string.isRequired, + isOpen: PropTypes.bool.isRequired, + advancedSettings: PropTypes.bool.isRequired, + album: PropTypes.bool.isRequired, + track: PropTypes.bool.isRequired, + additional: PropTypes.bool.isRequired, + onInputChange: PropTypes.func.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +NamingModal.defaultProps = { + album: false, + track: false, + additional: false +}; + +export default NamingModal; diff --git a/frontend/src/Settings/MediaManagement/Naming/NamingOption.css b/frontend/src/Settings/MediaManagement/Naming/NamingOption.css new file mode 100644 index 000000000..299c98936 --- /dev/null +++ b/frontend/src/Settings/MediaManagement/Naming/NamingOption.css @@ -0,0 +1,66 @@ +.option { + display: flex; + align-items: center; + flex-wrap: wrap; + margin: 3px; + border: 1px solid $borderColor; + + &:hover { + .token { + background-color: #ddd; + } + + .example { + background-color: #ccc; + } + } +} + +.small { + width: 420px; +} + +.large { + width: 100%; +} + +.token { + flex: 0 0 50%; + padding: 6px 16px; + background-color: #eee; + font-family: $monoSpaceFontFamily; +} + +.example { + flex: 0 0 50%; + padding: 6px 16px; + background-color: #ddd; +} + +.lower { + text-transform: lowercase; +} + +.upper { + text-transform: uppercase; +} + +.isFullFilename { + .token, + .example { + flex: 1 0 auto; + } +} + +@media only screen and (max-width: $breakpointSmall) { + .option.small { + width: 100%; + } +} + +@media only screen and (max-width: $breakpointExtraSmall) { + .token, + .example { + flex: 1 0 auto; + } +} diff --git a/frontend/src/Settings/MediaManagement/Naming/NamingOption.js b/frontend/src/Settings/MediaManagement/Naming/NamingOption.js new file mode 100644 index 000000000..ee8361a14 --- /dev/null +++ b/frontend/src/Settings/MediaManagement/Naming/NamingOption.js @@ -0,0 +1,85 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import classNames from 'classnames'; +import { sizes } from 'Helpers/Props'; +import Link from 'Components/Link/Link'; +import styles from './NamingOption.css'; + +class NamingOption extends Component { + + // + // Listeners + + onPress = () => { + const { + name, + value, + token, + tokenCase, + isFullFilename, + onInputChange + } = this.props; + + let newValue = token; + + if (tokenCase === 'lower') { + newValue = token.toLowerCase(); + } else if (tokenCase === 'upper') { + newValue = token.toUpperCase(); + } + + if (isFullFilename) { + onInputChange({ name, value: newValue }); + } else { + onInputChange({ + name, + value: `${value}${newValue}` + }); + } + } + + // + // Render + render() { + const { + token, + example, + tokenCase, + isFullFilename, + size + } = this.props; + + return ( + +
{token}
+
{example}
+ + ); + } +} + +NamingOption.propTypes = { + name: PropTypes.string.isRequired, + value: PropTypes.string.isRequired, + token: PropTypes.string.isRequired, + example: PropTypes.string.isRequired, + tokenCase: PropTypes.string.isRequired, + isFullFilename: PropTypes.bool.isRequired, + size: PropTypes.oneOf([sizes.SMALL, sizes.LARGE]), + onInputChange: PropTypes.func.isRequired +}; + +NamingOption.defaultProps = { + size: sizes.SMALL, + isFullFilename: false +}; + +export default NamingOption; diff --git a/frontend/src/Settings/Metadata/Metadata/EditMetadataModal.js b/frontend/src/Settings/Metadata/Metadata/EditMetadataModal.js new file mode 100644 index 000000000..98631932a --- /dev/null +++ b/frontend/src/Settings/Metadata/Metadata/EditMetadataModal.js @@ -0,0 +1,25 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import Modal from 'Components/Modal/Modal'; +import EditMetadataModalContentConnector from './EditMetadataModalContentConnector'; + +function EditMetadataModal({ isOpen, onModalClose, ...otherProps }) { + return ( + + + + ); +} + +EditMetadataModal.propTypes = { + isOpen: PropTypes.bool.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default EditMetadataModal; diff --git a/frontend/src/Settings/Metadata/Metadata/EditMetadataModalConnector.js b/frontend/src/Settings/Metadata/Metadata/EditMetadataModalConnector.js new file mode 100644 index 000000000..4f80d5f14 --- /dev/null +++ b/frontend/src/Settings/Metadata/Metadata/EditMetadataModalConnector.js @@ -0,0 +1,45 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { clearPendingChanges } from 'Store/Actions/baseActions'; +import EditMetadataModal from './EditMetadataModal'; + +function createMapDispatchToProps(dispatch, props) { + const section = 'metadata'; + + return { + dispatchClearPendingChanges() { + dispatch(clearPendingChanges({ section })); + } + }; +} + +class EditMetadataModalConnector extends Component { + + // + // Listeners + + onModalClose = () => { + this.props.dispatchClearPendingChanges(); + this.props.onModalClose(); + } + + // + // Render + + render() { + return ( + + ); + } +} + +EditMetadataModalConnector.propTypes = { + onModalClose: PropTypes.func.isRequired, + dispatchClearPendingChanges: PropTypes.func.isRequired +}; + +export default connect(null, createMapDispatchToProps)(EditMetadataModalConnector); diff --git a/frontend/src/Settings/Metadata/Metadata/EditMetadataModalContent.js b/frontend/src/Settings/Metadata/Metadata/EditMetadataModalContent.js new file mode 100644 index 000000000..1e7006068 --- /dev/null +++ b/frontend/src/Settings/Metadata/Metadata/EditMetadataModalContent.js @@ -0,0 +1,103 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import { inputTypes } from 'Helpers/Props'; +import Button from 'Components/Link/Button'; +import SpinnerErrorButton from 'Components/Link/SpinnerErrorButton'; +import ModalContent from 'Components/Modal/ModalContent'; +import ModalHeader from 'Components/Modal/ModalHeader'; +import ModalBody from 'Components/Modal/ModalBody'; +import ModalFooter from 'Components/Modal/ModalFooter'; +import Form from 'Components/Form/Form'; +import FormGroup from 'Components/Form/FormGroup'; +import FormLabel from 'Components/Form/FormLabel'; +import FormInputGroup from 'Components/Form/FormInputGroup'; +import ProviderFieldFormGroup from 'Components/Form/ProviderFieldFormGroup'; + +function EditMetadataModalContent(props) { + const { + isSaving, + saveError, + item, + onInputChange, + onFieldChange, + onModalClose, + onSavePress, + ...otherProps + } = props; + + const { + name, + enable, + fields + } = item; + + return ( + + + Edit {name.value} Metadata + + + +
+ + Enable + + + + + { + fields.map((field) => { + return ( + + ); + }) + } + + +
+ + + + + + Save + + +
+ ); +} + +EditMetadataModalContent.propTypes = { + isSaving: PropTypes.bool.isRequired, + saveError: PropTypes.object, + item: PropTypes.object.isRequired, + onInputChange: PropTypes.func.isRequired, + onFieldChange: PropTypes.func.isRequired, + onModalClose: PropTypes.func.isRequired, + onSavePress: PropTypes.func.isRequired, + onDeleteMetadataPress: PropTypes.func +}; + +export default EditMetadataModalContent; diff --git a/frontend/src/Settings/Metadata/Metadata/EditMetadataModalContentConnector.js b/frontend/src/Settings/Metadata/Metadata/EditMetadataModalContentConnector.js new file mode 100644 index 000000000..2cd7636a0 --- /dev/null +++ b/frontend/src/Settings/Metadata/Metadata/EditMetadataModalContentConnector.js @@ -0,0 +1,93 @@ +import _ from 'lodash'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import selectSettings from 'Store/Selectors/selectSettings'; +import { setMetadataValue, setMetadataFieldValue, saveMetadata } from 'Store/Actions/settingsActions'; +import EditMetadataModalContent from './EditMetadataModalContent'; + +function createMapStateToProps() { + return createSelector( + (state, { id }) => id, + (state) => state.settings.metadata, + (id, metadata) => { + const { + isSaving, + saveError, + pendingChanges, + items + } = metadata; + + const settings = selectSettings(_.find(items, { id }), pendingChanges, saveError); + + return { + id, + isSaving, + saveError, + item: settings.settings, + ...settings + }; + } + ); +} + +const mapDispatchToProps = { + setMetadataValue, + setMetadataFieldValue, + saveMetadata +}; + +class EditMetadataModalContentConnector extends Component { + + // + // Lifecycle + + componentDidUpdate(prevProps, prevState) { + if (prevProps.isSaving && !this.props.isSaving && !this.props.saveError) { + this.props.onModalClose(); + } + } + + // + // Listeners + + onInputChange = ({ name, value }) => { + this.props.setMetadataValue({ name, value }); + } + + onFieldChange = ({ name, value }) => { + this.props.setMetadataFieldValue({ name, value }); + } + + onSavePress = () => { + this.props.saveMetadata({ id: this.props.id }); + } + + // + // Render + + render() { + return ( + + ); + } +} + +EditMetadataModalContentConnector.propTypes = { + id: PropTypes.number, + isSaving: PropTypes.bool.isRequired, + saveError: PropTypes.object, + item: PropTypes.object.isRequired, + setMetadataValue: PropTypes.func.isRequired, + setMetadataFieldValue: PropTypes.func.isRequired, + saveMetadata: PropTypes.func.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default connect(createMapStateToProps, mapDispatchToProps)(EditMetadataModalContentConnector); diff --git a/frontend/src/Settings/Metadata/Metadata/Metadata.css b/frontend/src/Settings/Metadata/Metadata/Metadata.css new file mode 100644 index 000000000..2de4023de --- /dev/null +++ b/frontend/src/Settings/Metadata/Metadata/Metadata.css @@ -0,0 +1,17 @@ +.metadata { + composes: card from 'Components/Card.css'; + + width: 290px; +} + +.name { + margin-bottom: 20px; + font-weight: 300; + font-size: 24px; +} + +.label { + composes: label from 'Components/Label.css'; + + width: 100%; +} diff --git a/frontend/src/Settings/Metadata/Metadata/Metadata.js b/frontend/src/Settings/Metadata/Metadata/Metadata.js new file mode 100644 index 000000000..fb6495f7c --- /dev/null +++ b/frontend/src/Settings/Metadata/Metadata/Metadata.js @@ -0,0 +1,103 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { kinds } from 'Helpers/Props'; +import Card from 'Components/Card'; +import Label from 'Components/Label'; +import EditMetadataModalConnector from './EditMetadataModalConnector'; +import styles from './Metadata.css'; + +function getKind(enable) { + if (enable) { + return kinds.SUCCESS; + } + + return kinds.DANGER; +} + +class Metadata extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + isEditMetadataModalOpen: false + }; + } + + // + // Listeners + + onEditMetadataPress = () => { + this.setState({ isEditMetadataModalOpen: true }); + } + + onEditMetadataModalClose = () => { + this.setState({ isEditMetadataModalOpen: false }); + } + + // + // Render + + render() { + const { + id, + name, + enable, + fields + } = this.props; + + return ( + +
+ {name} +
+ +
+ +
+ +
+ { + fields.map((field) => { + return ( + + ); + }) + } +
+ + +
+ ); + } +} + +Metadata.propTypes = { + id: PropTypes.number.isRequired, + name: PropTypes.string.isRequired, + enable: PropTypes.bool.isRequired, + fields: PropTypes.arrayOf(PropTypes.object).isRequired +}; + +export default Metadata; diff --git a/frontend/src/Settings/Metadata/Metadata/Metadatas.css b/frontend/src/Settings/Metadata/Metadata/Metadatas.css new file mode 100644 index 000000000..fb1bd6080 --- /dev/null +++ b/frontend/src/Settings/Metadata/Metadata/Metadatas.css @@ -0,0 +1,4 @@ +.metadatas { + display: flex; + flex-wrap: wrap; +} diff --git a/frontend/src/Settings/Metadata/Metadata/Metadatas.js b/frontend/src/Settings/Metadata/Metadata/Metadatas.js new file mode 100644 index 000000000..9f015369d --- /dev/null +++ b/frontend/src/Settings/Metadata/Metadata/Metadatas.js @@ -0,0 +1,46 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import sortByName from 'Utilities/Array/sortByName'; +import FieldSet from 'Components/FieldSet'; +import PageSectionContent from 'Components/Page/PageSectionContent'; +import Metadata from './Metadata'; +import styles from './Metadatas.css'; + +function Metadatas(props) { + const { + items, + ...otherProps + } = props; + + return ( +
+ +
+ { + items.sort(sortByName).map((item) => { + return ( + + ); + }) + } +
+
+
+ ); +} + +Metadatas.propTypes = { + isFetching: PropTypes.bool.isRequired, + error: PropTypes.object, + items: PropTypes.arrayOf(PropTypes.object).isRequired +}; + +export default Metadatas; diff --git a/frontend/src/Settings/Metadata/Metadata/MetadatasConnector.js b/frontend/src/Settings/Metadata/Metadata/MetadatasConnector.js new file mode 100644 index 000000000..fb7153950 --- /dev/null +++ b/frontend/src/Settings/Metadata/Metadata/MetadatasConnector.js @@ -0,0 +1,49 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import { fetchMetadata } from 'Store/Actions/settingsActions'; +import Metadatas from './Metadatas'; + +function createMapStateToProps() { + return createSelector( + (state) => state.settings.metadata, + (metadata) => { + return { + ...metadata + }; + } + ); +} + +const mapDispatchToProps = { + fetchMetadata +}; + +class MetadatasConnector extends Component { + + // + // Lifecycle + + componentDidMount() { + this.props.fetchMetadata(); + } + + // + // Render + + render() { + return ( + + ); + } +} + +MetadatasConnector.propTypes = { + fetchMetadata: PropTypes.func.isRequired +}; + +export default connect(createMapStateToProps, mapDispatchToProps)(MetadatasConnector); diff --git a/frontend/src/Settings/Metadata/MetadataProvider/MetadataProvider.js b/frontend/src/Settings/Metadata/MetadataProvider/MetadataProvider.js new file mode 100644 index 000000000..714ad15e2 --- /dev/null +++ b/frontend/src/Settings/Metadata/MetadataProvider/MetadataProvider.js @@ -0,0 +1,75 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import { inputTypes, sizes } from 'Helpers/Props'; +import LoadingIndicator from 'Components/Loading/LoadingIndicator'; +import FieldSet from 'Components/FieldSet'; +import Form from 'Components/Form/Form'; +import FormGroup from 'Components/Form/FormGroup'; +import FormLabel from 'Components/Form/FormLabel'; +import FormInputGroup from 'Components/Form/FormInputGroup'; + +function MetadataProvider(props) { + const { + advancedSettings, + isFetching, + error, + settings, + hasSettings, + onInputChange + } = props; + + return ( + +
+ { + isFetching && + + } + + { + !isFetching && error && +
Unable to load Metadata Provider settings
+ } + + { + hasSettings && !isFetching && !error && +
+ { + advancedSettings && +
+ + Metadata Source + + + +
+ } +
+ } +
+ + ); +} + +MetadataProvider.propTypes = { + advancedSettings: PropTypes.bool.isRequired, + isFetching: PropTypes.bool.isRequired, + error: PropTypes.object, + settings: PropTypes.object.isRequired, + hasSettings: PropTypes.bool.isRequired, + onInputChange: PropTypes.func.isRequired +}; + +export default MetadataProvider; diff --git a/frontend/src/Settings/Metadata/MetadataProvider/MetadataProviderConnector.js b/frontend/src/Settings/Metadata/MetadataProvider/MetadataProviderConnector.js new file mode 100644 index 000000000..153a929e9 --- /dev/null +++ b/frontend/src/Settings/Metadata/MetadataProvider/MetadataProviderConnector.js @@ -0,0 +1,92 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { createSelector } from 'reselect'; +import createSettingsSectionSelector from 'Store/Selectors/createSettingsSectionSelector'; +import { setMetadataProviderValue, saveMetadataProvider, fetchMetadataProvider } from 'Store/Actions/settingsActions'; +import { clearPendingChanges } from 'Store/Actions/baseActions'; +import connectSection from 'Store/connectSection'; +import MetadataProvider from './MetadataProvider'; + +function createMapStateToProps() { + return createSelector( + (state) => state.settings.advancedSettings, + createSettingsSectionSelector(), + (advancedSettings, sectionSettings) => { + return { + advancedSettings, + ...sectionSettings + }; + } + ); +} + +const mapDispatchToProps = { + setMetadataProviderValue, + saveMetadataProvider, + fetchMetadataProvider, + clearPendingChanges +}; + +class MetadataProviderConnector extends Component { + + // + // Lifecycle + + componentDidMount() { + this.props.fetchMetadataProvider(); + } + + componentDidUpdate(prevProps) { + if (this.props.hasPendingChanges !== prevProps.hasPendingChanges) { + this.props.onHasPendingChange(this.props.hasPendingChanges); + } + } + + componentWillUnmount() { + this.props.clearPendingChanges({ section: this.props.section }); + } + + // + // Control + + save = () => { + this.props.saveMetadataProvider(); + } + + // + // Listeners + + onInputChange = ({ name, value }) => { + this.props.setMetadataProviderValue({ name, value }); + } + + // + // Render + + render() { + return ( + + ); + } +} + +MetadataProviderConnector.propTypes = { + section: PropTypes.string.isRequired, + hasPendingChanges: PropTypes.bool.isRequired, + setMetadataProviderValue: PropTypes.func.isRequired, + saveMetadataProvider: PropTypes.func.isRequired, + fetchMetadataProvider: PropTypes.func.isRequired, + clearPendingChanges: PropTypes.func.isRequired, + onHasPendingChange: PropTypes.func.isRequired +}; + +export default connectSection( + createMapStateToProps, + mapDispatchToProps, + undefined, + { withRef: true }, + { section: 'metadataProvider' } + )(MetadataProviderConnector); diff --git a/frontend/src/Settings/Metadata/MetadataSettings.js b/frontend/src/Settings/Metadata/MetadataSettings.js new file mode 100644 index 000000000..3170787b2 --- /dev/null +++ b/frontend/src/Settings/Metadata/MetadataSettings.js @@ -0,0 +1,60 @@ +import React, { Component } from 'react'; +import PageContent from 'Components/Page/PageContent'; +import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector'; +import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector'; +import MetadatasConnector from './Metadata/MetadatasConnector'; +import MetadataProviderConnector from './MetadataProvider/MetadataProviderConnector'; + +class MetadataSettings extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + hasPendingChanges: false + }; + } + + // + // Listeners + + setMetadataProviderRef = (ref) => { + this._metadataProvider = ref; + } + + onHasPendingChange = (hasPendingChanges) => { + this.setState({ + hasPendingChanges + }); + } + + onSavePress = () => { + this._metadataProvider.getWrappedInstance().save(); + } + + // + // Render + render() { + return ( + + + + + + + + + ); + } +} + +export default MetadataSettings; diff --git a/frontend/src/Settings/Notifications/NotificationSettings.js b/frontend/src/Settings/Notifications/NotificationSettings.js new file mode 100644 index 000000000..c9bed6501 --- /dev/null +++ b/frontend/src/Settings/Notifications/NotificationSettings.js @@ -0,0 +1,21 @@ +import React from 'react'; +import PageContent from 'Components/Page/PageContent'; +import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector'; +import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector'; +import NotificationsConnector from './Notifications/NotificationsConnector'; + +function NotificationSettings() { + return ( + + + + + + + + ); +} + +export default NotificationSettings; diff --git a/frontend/src/Settings/Notifications/Notifications/AddNotificationItem.css b/frontend/src/Settings/Notifications/Notifications/AddNotificationItem.css new file mode 100644 index 000000000..e2181150c --- /dev/null +++ b/frontend/src/Settings/Notifications/Notifications/AddNotificationItem.css @@ -0,0 +1,44 @@ +.notification { + composes: card from 'Components/Card.css'; + + position: relative; + width: 300px; + height: 100px; +} + +.underlay { + @add-mixin cover; +} + +.overlay { + @add-mixin linkOverlay; + + padding: 10px; +} + +.name { + text-align: center; + font-weight: lighter; + font-size: 24px; +} + +.actions { + margin-top: 20px; + text-align: right; +} + +.presetsMenu { + composes: menu from 'Components/Menu/Menu.css'; + + display: inline-block; + margin: 0 5px; +} + +.presetsMenuButton { + composes: button from 'Components/Link/Button.css'; + + &::after { + margin-left: 5px; + content: '\25BE'; + } +} diff --git a/frontend/src/Settings/Notifications/Notifications/AddNotificationItem.js b/frontend/src/Settings/Notifications/Notifications/AddNotificationItem.js new file mode 100644 index 000000000..6d90961b0 --- /dev/null +++ b/frontend/src/Settings/Notifications/Notifications/AddNotificationItem.js @@ -0,0 +1,110 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { sizes } from 'Helpers/Props'; +import Button from 'Components/Link/Button'; +import Link from 'Components/Link/Link'; +import Menu from 'Components/Menu/Menu'; +import MenuContent from 'Components/Menu/MenuContent'; +import AddNotificationPresetMenuItem from './AddNotificationPresetMenuItem'; +import styles from './AddNotificationItem.css'; + +class AddNotificationItem extends Component { + + // + // Listeners + + onNotificationSelect = () => { + const { + implementation + } = this.props; + + this.props.onNotificationSelect({ implementation }); + } + + // + // Render + + render() { + const { + implementation, + implementationName, + infoLink, + presets, + onNotificationSelect + } = this.props; + + const hasPresets = !!presets && !!presets.length; + + return ( +
+ + +
+
+ {implementationName} +
+ +
+ { + hasPresets && + + + + + + + + { + presets.map((preset) => { + return ( + + ); + }) + } + + + + } + + +
+
+
+ ); + } +} + +AddNotificationItem.propTypes = { + implementation: PropTypes.string.isRequired, + implementationName: PropTypes.string.isRequired, + infoLink: PropTypes.string.isRequired, + presets: PropTypes.arrayOf(PropTypes.object), + onNotificationSelect: PropTypes.func.isRequired +}; + +export default AddNotificationItem; diff --git a/frontend/src/Settings/Notifications/Notifications/AddNotificationModal.js b/frontend/src/Settings/Notifications/Notifications/AddNotificationModal.js new file mode 100644 index 000000000..45f5e14b6 --- /dev/null +++ b/frontend/src/Settings/Notifications/Notifications/AddNotificationModal.js @@ -0,0 +1,25 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import Modal from 'Components/Modal/Modal'; +import AddNotificationModalContentConnector from './AddNotificationModalContentConnector'; + +function AddNotificationModal({ isOpen, onModalClose, ...otherProps }) { + return ( + + + + ); +} + +AddNotificationModal.propTypes = { + isOpen: PropTypes.bool.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default AddNotificationModal; diff --git a/frontend/src/Settings/Notifications/Notifications/AddNotificationModalContent.css b/frontend/src/Settings/Notifications/Notifications/AddNotificationModalContent.css new file mode 100644 index 000000000..8744e516c --- /dev/null +++ b/frontend/src/Settings/Notifications/Notifications/AddNotificationModalContent.css @@ -0,0 +1,5 @@ +.notifications { + display: flex; + justify-content: center; + flex-wrap: wrap; +} diff --git a/frontend/src/Settings/Notifications/Notifications/AddNotificationModalContent.js b/frontend/src/Settings/Notifications/Notifications/AddNotificationModalContent.js new file mode 100644 index 000000000..adb66d7da --- /dev/null +++ b/frontend/src/Settings/Notifications/Notifications/AddNotificationModalContent.js @@ -0,0 +1,85 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import Button from 'Components/Link/Button'; +import LoadingIndicator from 'Components/Loading/LoadingIndicator'; +import ModalContent from 'Components/Modal/ModalContent'; +import ModalHeader from 'Components/Modal/ModalHeader'; +import ModalBody from 'Components/Modal/ModalBody'; +import ModalFooter from 'Components/Modal/ModalFooter'; +import AddNotificationItem from './AddNotificationItem'; +import styles from './AddNotificationModalContent.css'; + +class AddNotificationModalContent extends Component { + + // + // Render + + render() { + const { + isFetching, + error, + isPopulated, + schema, + onNotificationSelect, + onModalClose + } = this.props; + + return ( + + + Add Notification + + + + { + isFetching && + + } + + { + !isFetching && !!error && +
Unable to add a new notification, please try again.
+ } + + { + isPopulated && !error && +
+
+ { + schema.map((notification) => { + return ( + + ); + }) + } +
+
+ } +
+ + + +
+ ); + } +} + +AddNotificationModalContent.propTypes = { + isFetching: PropTypes.bool.isRequired, + error: PropTypes.object, + isPopulated: PropTypes.bool.isRequired, + schema: PropTypes.arrayOf(PropTypes.object).isRequired, + onNotificationSelect: PropTypes.func.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default AddNotificationModalContent; diff --git a/frontend/src/Settings/Notifications/Notifications/AddNotificationModalContentConnector.js b/frontend/src/Settings/Notifications/Notifications/AddNotificationModalContentConnector.js new file mode 100644 index 000000000..a65670eca --- /dev/null +++ b/frontend/src/Settings/Notifications/Notifications/AddNotificationModalContentConnector.js @@ -0,0 +1,70 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import { fetchNotificationSchema, selectNotificationSchema } from 'Store/Actions/settingsActions'; +import AddNotificationModalContent from './AddNotificationModalContent'; + +function createMapStateToProps() { + return createSelector( + (state) => state.settings.notifications, + (notifications) => { + const { + isFetching, + error, + isPopulated, + schema + } = notifications; + + return { + isFetching, + error, + isPopulated, + schema + }; + } + ); +} + +const mapDispatchToProps = { + fetchNotificationSchema, + selectNotificationSchema +}; + +class AddNotificationModalContentConnector extends Component { + + // + // Lifecycle + + componentDidMount() { + this.props.fetchNotificationSchema(); + } + + // + // Listeners + + onNotificationSelect = ({ implementation, name }) => { + this.props.selectNotificationSchema({ implementation, presetName: name }); + this.props.onModalClose({ notificationSelected: true }); + } + + // + // Render + + render() { + return ( + + ); + } +} + +AddNotificationModalContentConnector.propTypes = { + fetchNotificationSchema: PropTypes.func.isRequired, + selectNotificationSchema: PropTypes.func.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default connect(createMapStateToProps, mapDispatchToProps)(AddNotificationModalContentConnector); diff --git a/frontend/src/Settings/Notifications/Notifications/AddNotificationPresetMenuItem.js b/frontend/src/Settings/Notifications/Notifications/AddNotificationPresetMenuItem.js new file mode 100644 index 000000000..e4df85b8a --- /dev/null +++ b/frontend/src/Settings/Notifications/Notifications/AddNotificationPresetMenuItem.js @@ -0,0 +1,49 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import MenuItem from 'Components/Menu/MenuItem'; + +class AddNotificationPresetMenuItem extends Component { + + // + // Listeners + + onPress = () => { + const { + name, + implementation + } = this.props; + + this.props.onPress({ + name, + implementation + }); + } + + // + // Render + + render() { + const { + name, + implementation, + ...otherProps + } = this.props; + + return ( + + {name} + + ); + } +} + +AddNotificationPresetMenuItem.propTypes = { + name: PropTypes.string.isRequired, + implementation: PropTypes.string.isRequired, + onPress: PropTypes.func.isRequired +}; + +export default AddNotificationPresetMenuItem; diff --git a/frontend/src/Settings/Notifications/Notifications/EditNotificationModal.js b/frontend/src/Settings/Notifications/Notifications/EditNotificationModal.js new file mode 100644 index 000000000..91d9f67cc --- /dev/null +++ b/frontend/src/Settings/Notifications/Notifications/EditNotificationModal.js @@ -0,0 +1,25 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import Modal from 'Components/Modal/Modal'; +import EditNotificationModalContentConnector from './EditNotificationModalContentConnector'; + +function EditNotificationModal({ isOpen, onModalClose, ...otherProps }) { + return ( + + + + ); +} + +EditNotificationModal.propTypes = { + isOpen: PropTypes.bool.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default EditNotificationModal; diff --git a/frontend/src/Settings/Notifications/Notifications/EditNotificationModalConnector.js b/frontend/src/Settings/Notifications/Notifications/EditNotificationModalConnector.js new file mode 100644 index 000000000..77566f9e1 --- /dev/null +++ b/frontend/src/Settings/Notifications/Notifications/EditNotificationModalConnector.js @@ -0,0 +1,65 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { clearPendingChanges } from 'Store/Actions/baseActions'; +import { cancelTestNotification, cancelSaveNotification } from 'Store/Actions/settingsActions'; +import EditNotificationModal from './EditNotificationModal'; + +function createMapDispatchToProps(dispatch, props) { + const section = 'notifications'; + + return { + dispatchClearPendingChanges() { + dispatch(clearPendingChanges({ section })); + }, + + dispatchCancelTestNotification() { + dispatch(cancelTestNotification({ section })); + }, + + dispatchCancelSaveNotification() { + dispatch(cancelSaveNotification({ section })); + } + }; +} + +class EditNotificationModalConnector extends Component { + + // + // Listeners + + onModalClose = () => { + this.props.dispatchClearPendingChanges(); + this.props.dispatchCancelTestNotification(); + this.props.dispatchCancelSaveNotification(); + this.props.onModalClose(); + } + + // + // Render + + render() { + const { + dispatchClearPendingChanges, + dispatchCancelTestNotification, + dispatchCancelSaveNotification, + ...otherProps + } = this.props; + + return ( + + ); + } +} + +EditNotificationModalConnector.propTypes = { + onModalClose: PropTypes.func.isRequired, + dispatchClearPendingChanges: PropTypes.func.isRequired, + dispatchCancelTestNotification: PropTypes.func.isRequired, + dispatchCancelSaveNotification: PropTypes.func.isRequired +}; + +export default connect(null, createMapDispatchToProps)(EditNotificationModalConnector); diff --git a/frontend/src/Settings/Notifications/Notifications/EditNotificationModalContent.css b/frontend/src/Settings/Notifications/Notifications/EditNotificationModalContent.css new file mode 100644 index 000000000..c73406b57 --- /dev/null +++ b/frontend/src/Settings/Notifications/Notifications/EditNotificationModalContent.css @@ -0,0 +1,11 @@ +.deleteButton { + composes: button from 'Components/Link/Button.css'; + + margin-right: auto; +} + +.message { + composes: alert from 'Components/Alert.css'; + + margin-bottom: 30px; +} diff --git a/frontend/src/Settings/Notifications/Notifications/EditNotificationModalContent.js b/frontend/src/Settings/Notifications/Notifications/EditNotificationModalContent.js new file mode 100644 index 000000000..f375275f2 --- /dev/null +++ b/frontend/src/Settings/Notifications/Notifications/EditNotificationModalContent.js @@ -0,0 +1,235 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import { inputTypes, kinds } from 'Helpers/Props'; +import Alert from 'Components/Alert'; +import Button from 'Components/Link/Button'; +import SpinnerErrorButton from 'Components/Link/SpinnerErrorButton'; +import LoadingIndicator from 'Components/Loading/LoadingIndicator'; +import ModalContent from 'Components/Modal/ModalContent'; +import ModalHeader from 'Components/Modal/ModalHeader'; +import ModalBody from 'Components/Modal/ModalBody'; +import ModalFooter from 'Components/Modal/ModalFooter'; +import Form from 'Components/Form/Form'; +import FormGroup from 'Components/Form/FormGroup'; +import FormLabel from 'Components/Form/FormLabel'; +import FormInputGroup from 'Components/Form/FormInputGroup'; +import ProviderFieldFormGroup from 'Components/Form/ProviderFieldFormGroup'; +import styles from './EditNotificationModalContent.css'; + +function EditNotificationModalContent(props) { + const { + advancedSettings, + isFetching, + error, + isSaving, + isTesting, + saveError, + item, + onInputChange, + onFieldChange, + onModalClose, + onSavePress, + onTestPress, + onDeleteNotificationPress, + ...otherProps + } = props; + + const { + id, + name, + onGrab, + onDownload, + onUpgrade, + onRename, + supportsOnGrab, + supportsOnDownload, + supportsOnUpgrade, + supportsOnRename, + tags, + fields, + message + } = item; + + return ( + + + {id ? 'Edit Notification' : 'Add Notification'} + + + + { + isFetching && + + } + + { + !isFetching && !!error && +
Unable to add a new notification, please try again.
+ } + + { + !isFetching && !error && +
+ { + !!message && + + {message.value.message} + + } + + + Name + + + + + + On Grab + + + + + + On Download + + + + + { + onDownload.value && + + On Upgrade + + + + } + + + On Rename + + + + + + Tags + + + + + { + fields.map((field) => { + return ( + + ); + }) + } + + + } +
+ + { + id && + + } + + + Test + + + + + + Save + + +
+ ); +} + +EditNotificationModalContent.propTypes = { + advancedSettings: PropTypes.bool.isRequired, + isFetching: PropTypes.bool.isRequired, + error: PropTypes.object, + isSaving: PropTypes.bool.isRequired, + isTesting: PropTypes.bool.isRequired, + saveError: PropTypes.object, + item: PropTypes.object.isRequired, + onInputChange: PropTypes.func.isRequired, + onFieldChange: PropTypes.func.isRequired, + onModalClose: PropTypes.func.isRequired, + onSavePress: PropTypes.func.isRequired, + onTestPress: PropTypes.func.isRequired, + onDeleteNotificationPress: PropTypes.func +}; + +export default EditNotificationModalContent; diff --git a/frontend/src/Settings/Notifications/Notifications/EditNotificationModalContentConnector.js b/frontend/src/Settings/Notifications/Notifications/EditNotificationModalContentConnector.js new file mode 100644 index 000000000..bca296315 --- /dev/null +++ b/frontend/src/Settings/Notifications/Notifications/EditNotificationModalContentConnector.js @@ -0,0 +1,94 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { createSelector } from 'reselect'; +import createProviderSettingsSelector from 'Store/Selectors/createProviderSettingsSelector'; +import { setNotificationValue, setNotificationFieldValue, saveNotification, testNotification } from 'Store/Actions/settingsActions'; +import connectSection from 'Store/connectSection'; +import EditNotificationModalContent from './EditNotificationModalContent'; + +function createMapStateToProps() { + return createSelector( + (state) => state.settings.advancedSettings, + createProviderSettingsSelector(), + (advancedSettings, notification) => { + return { + advancedSettings, + ...notification + }; + } + ); +} + +const mapDispatchToProps = { + setNotificationValue, + setNotificationFieldValue, + saveNotification, + testNotification +}; + +class EditNotificationModalContentConnector extends Component { + + // + // Lifecycle + + componentDidUpdate(prevProps, prevState) { + if (prevProps.isSaving && !this.props.isSaving && !this.props.saveError) { + this.props.onModalClose(); + } + } + + // + // Listeners + + onInputChange = ({ name, value }) => { + this.props.setNotificationValue({ name, value }); + } + + onFieldChange = ({ name, value }) => { + this.props.setNotificationFieldValue({ name, value }); + } + + onSavePress = () => { + this.props.saveNotification({ id: this.props.id }); + } + + onTestPress = () => { + this.props.testNotification({ id: this.props.id }); + } + + // + // Render + + render() { + return ( + + ); + } +} + +EditNotificationModalContentConnector.propTypes = { + id: PropTypes.number, + isFetching: PropTypes.bool.isRequired, + isSaving: PropTypes.bool.isRequired, + saveError: PropTypes.object, + item: PropTypes.object.isRequired, + setNotificationValue: PropTypes.func.isRequired, + setNotificationFieldValue: PropTypes.func.isRequired, + saveNotification: PropTypes.func.isRequired, + testNotification: PropTypes.func.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default connectSection( + createMapStateToProps, + mapDispatchToProps, + undefined, + undefined, + { section: 'notifications' } +)(EditNotificationModalContentConnector); diff --git a/frontend/src/Settings/Notifications/Notifications/Notification.css b/frontend/src/Settings/Notifications/Notifications/Notification.css new file mode 100644 index 000000000..5a17a4c20 --- /dev/null +++ b/frontend/src/Settings/Notifications/Notifications/Notification.css @@ -0,0 +1,19 @@ +.notification { + composes: card from 'Components/Card.css'; + + width: 290px; +} + +.name { + @add-mixin truncate; + + margin-bottom: 20px; + font-weight: 300; + font-size: 24px; +} + +.enabled { + display: flex; + flex-wrap: wrap; + margin-top: 5px; +} diff --git a/frontend/src/Settings/Notifications/Notifications/Notification.js b/frontend/src/Settings/Notifications/Notifications/Notification.js new file mode 100644 index 000000000..cca3761fc --- /dev/null +++ b/frontend/src/Settings/Notifications/Notifications/Notification.js @@ -0,0 +1,151 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { kinds } from 'Helpers/Props'; +import Card from 'Components/Card'; +import Label from 'Components/Label'; +import ConfirmModal from 'Components/Modal/ConfirmModal'; +import EditNotificationModalConnector from './EditNotificationModalConnector'; +import styles from './Notification.css'; + +function getLabelKind(supports, enabled) { + if (!supports) { + return kinds.DEFAULT; + } + + if (!enabled) { + return kinds.DANGER; + } + + return kinds.SUCCESS; +} + +class Notification extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + isEditNotificationModalOpen: false, + isDeleteNotificationModalOpen: false + }; + } + + // + // Listeners + + onEditNotificationPress = () => { + this.setState({ isEditNotificationModalOpen: true }); + } + + onEditNotificationModalClose = () => { + this.setState({ isEditNotificationModalOpen: false }); + } + + onDeleteNotificationPress = () => { + this.setState({ + isEditNotificationModalOpen: false, + isDeleteNotificationModalOpen: true + }); + } + + onDeleteNotificationModalClose= () => { + this.setState({ isDeleteNotificationModalOpen: false }); + } + + onConfirmDeleteNotification = () => { + this.props.onConfirmDeleteNotification(this.props.id); + } + + // + // Render + + render() { + const { + id, + name, + onGrab, + onDownload, + onUpgrade, + onRename, + supportsOnGrab, + supportsOnDownload, + supportsOnUpgrade, + supportsOnRename + } = this.props; + + return ( + +
+ {name} +
+ + + + + + + + + + + + +
+ ); + } +} + +Notification.propTypes = { + id: PropTypes.number.isRequired, + name: PropTypes.string.isRequired, + onGrab: PropTypes.bool.isRequired, + onDownload: PropTypes.bool.isRequired, + onUpgrade: PropTypes.bool.isRequired, + onRename: PropTypes.bool.isRequired, + supportsOnGrab: PropTypes.bool.isRequired, + supportsOnDownload: PropTypes.bool.isRequired, + supportsOnUpgrade: PropTypes.bool.isRequired, + supportsOnRename: PropTypes.bool.isRequired, + onConfirmDeleteNotification: PropTypes.func.isRequired +}; + +export default Notification; diff --git a/frontend/src/Settings/Notifications/Notifications/Notifications.css b/frontend/src/Settings/Notifications/Notifications/Notifications.css new file mode 100644 index 000000000..26b890e88 --- /dev/null +++ b/frontend/src/Settings/Notifications/Notifications/Notifications.css @@ -0,0 +1,20 @@ +.notifications { + display: flex; + flex-wrap: wrap; +} + +.addNotification { + composes: notification from './Notification.css'; + + background-color: $cardAlternateBackgroundColor; + color: $gray; + text-align: center; +} + +.center { + display: inline-block; + padding: 5px 20px 0; + border: 1px solid $borderColor; + border-radius: 4px; + background-color: $white; +} diff --git a/frontend/src/Settings/Notifications/Notifications/Notifications.js b/frontend/src/Settings/Notifications/Notifications/Notifications.js new file mode 100644 index 000000000..1fad8ef5f --- /dev/null +++ b/frontend/src/Settings/Notifications/Notifications/Notifications.js @@ -0,0 +1,117 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import sortByName from 'Utilities/Array/sortByName'; +import { icons } from 'Helpers/Props'; +import FieldSet from 'Components/FieldSet'; +import Card from 'Components/Card'; +import Icon from 'Components/Icon'; +import PageSectionContent from 'Components/Page/PageSectionContent'; +import Notification from './Notification'; +import AddNotificationModal from './AddNotificationModal'; +import EditNotificationModalConnector from './EditNotificationModalConnector'; +import styles from './Notifications.css'; + +class Notifications extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + isAddNotificationModalOpen: false, + isEditNotificationModalOpen: false + }; + } + + // + // Listeners + + onAddNotificationPress = () => { + this.setState({ isAddNotificationModalOpen: true }); + } + + onAddNotificationModalClose = ({ notificationSelected = false } = {}) => { + this.setState({ + isAddNotificationModalOpen: false, + isEditNotificationModalOpen: notificationSelected + }); + } + + onEditNotificationModalClose = () => { + this.setState({ isEditNotificationModalOpen: false }); + } + + // + // Render + + render() { + const { + items, + onConfirmDeleteNotification, + ...otherProps + } = this.props; + + const { + isAddNotificationModalOpen, + isEditNotificationModalOpen + } = this.state; + + return ( +
+ +
+ { + items.sort(sortByName).map((item) => { + return ( + + ); + }) + } + + +
+ +
+
+
+ + + + +
+
+ ); + } +} + +Notifications.propTypes = { + isFetching: PropTypes.bool.isRequired, + error: PropTypes.object, + items: PropTypes.arrayOf(PropTypes.object).isRequired, + onConfirmDeleteNotification: PropTypes.func.isRequired +}; + +export default Notifications; diff --git a/frontend/src/Settings/Notifications/Notifications/NotificationsConnector.js b/frontend/src/Settings/Notifications/Notifications/NotificationsConnector.js new file mode 100644 index 000000000..b2b5e5166 --- /dev/null +++ b/frontend/src/Settings/Notifications/Notifications/NotificationsConnector.js @@ -0,0 +1,58 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import { fetchNotifications, deleteNotification } from 'Store/Actions/settingsActions'; +import Notifications from './Notifications'; + +function createMapStateToProps() { + return createSelector( + (state) => state.settings.notifications, + (notifications) => { + return { + ...notifications + }; + } + ); +} + +const mapDispatchToProps = { + fetchNotifications, + deleteNotification +}; + +class NotificationsConnector extends Component { + + // + // Lifecycle + + componentDidMount() { + this.props.fetchNotifications(); + } + + // + // Listeners + + onConfirmDeleteNotification = (id) => { + this.props.deleteNotification({ id }); + } + + // + // Render + + render() { + return ( + + ); + } +} + +NotificationsConnector.propTypes = { + fetchNotifications: PropTypes.func.isRequired, + deleteNotification: PropTypes.func.isRequired +}; + +export default connect(createMapStateToProps, mapDispatchToProps)(NotificationsConnector); diff --git a/frontend/src/Settings/PendingChangesModal.js b/frontend/src/Settings/PendingChangesModal.js new file mode 100644 index 000000000..e3b14e228 --- /dev/null +++ b/frontend/src/Settings/PendingChangesModal.js @@ -0,0 +1,62 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import { kinds } from 'Helpers/Props'; +import Button from 'Components/Link/Button'; +import Modal from 'Components/Modal/Modal'; +import ModalContent from 'Components/Modal/ModalContent'; +import ModalHeader from 'Components/Modal/ModalHeader'; +import ModalBody from 'Components/Modal/ModalBody'; +import ModalFooter from 'Components/Modal/ModalFooter'; + +function PendingChangesModal(props) { + const { + isOpen, + onConfirm, + onCancel + } = props; + + return ( + + + Unsaved Changes + + + You have unsaved changes, are you sure you want to leave this page? + + + + + + + + + + ); +} + +PendingChangesModal.propTypes = { + className: PropTypes.string, + isOpen: PropTypes.bool.isRequired, + kind: PropTypes.oneOf(kinds.all), + onConfirm: PropTypes.func.isRequired, + onCancel: PropTypes.func.isRequired +}; + +PendingChangesModal.defaultProps = { + kind: kinds.PRIMARY +}; + +export default PendingChangesModal; diff --git a/frontend/src/Settings/Profiles/Delay/DelayProfile.css b/frontend/src/Settings/Profiles/Delay/DelayProfile.css new file mode 100644 index 000000000..238742efd --- /dev/null +++ b/frontend/src/Settings/Profiles/Delay/DelayProfile.css @@ -0,0 +1,40 @@ +.delayProfile { + display: flex; + align-items: stretch; + margin-bottom: 10px; + height: 30px; + border-bottom: 1px solid $borderColor; + line-height: 30px; +} + +.column { + flex: 0 0 200px; +} + +.actions { + display: flex; +} + +.dragHandle { + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + margin-left: auto; + width: $dragHandleWidth; + text-align: center; + cursor: grab; +} + +.dragIcon { + top: 0; +} + +.isDragging { + opacity: 0.25; +} + +.editButton { + width: $dragHandleWidth; + text-align: center; +} diff --git a/frontend/src/Settings/Profiles/Delay/DelayProfile.js b/frontend/src/Settings/Profiles/Delay/DelayProfile.js new file mode 100644 index 000000000..d72a06467 --- /dev/null +++ b/frontend/src/Settings/Profiles/Delay/DelayProfile.js @@ -0,0 +1,172 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import classNames from 'classnames'; +import titleCase from 'Utilities/String/titleCase'; +import { icons, kinds } from 'Helpers/Props'; +import Icon from 'Components/Icon'; +import Link from 'Components/Link/Link'; +import TagList from 'Components/TagList'; +import ConfirmModal from 'Components/Modal/ConfirmModal'; +import EditDelayProfileModalConnector from './EditDelayProfileModalConnector'; +import styles from './DelayProfile.css'; + +function getDelay(enabled, delay) { + if (!enabled) { + return '-'; + } + + if (!delay) { + return 'No Delay'; + } + + if (delay === 1) { + return '1 Minute'; + } + + // TODO: use better units of time than just minutes + return `${delay} Minutes`; +} + +class DelayProfile extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + isEditDelayProfileModalOpen: false, + isDeleteDelayProfileModalOpen: false + }; + } + + // + // Listeners + + onEditDelayProfilePress = () => { + this.setState({ isEditDelayProfileModalOpen: true }); + } + + onEditDelayProfileModalClose = () => { + this.setState({ isEditDelayProfileModalOpen: false }); + } + + onDeleteDelayProfilePress = () => { + this.setState({ + isEditDelayProfileModalOpen: false, + isDeleteDelayProfileModalOpen: true + }); + } + + onDeleteDelayProfileModalClose = () => { + this.setState({ isDeleteDelayProfileModalOpen: false }); + } + + onConfirmDeleteDelayProfile = () => { + this.props.onConfirmDeleteDelayProfile(this.props.id); + } + + // + // Render + + render() { + const { + id, + enableUsenet, + enableTorrent, + preferredProtocol, + usenetDelay, + torrentDelay, + tags, + tagList, + isDragging, + connectDragSource + } = this.props; + + let preferred = titleCase(preferredProtocol); + + if (!enableUsenet) { + preferred = 'Only Torrent'; + } else if (!enableTorrent) { + preferred = 'Only Usenet'; + } + + return ( +
+
{preferred}
+
{getDelay(enableUsenet, usenetDelay)}
+
{getDelay(enableTorrent, torrentDelay)}
+ + + +
+ + + + + { + id !== 1 && + connectDragSource( +
+ +
+ ) + } +
+ + + + +
+ ); + } +} + +DelayProfile.propTypes = { + id: PropTypes.number.isRequired, + enableUsenet: PropTypes.bool.isRequired, + enableTorrent: PropTypes.bool.isRequired, + preferredProtocol: PropTypes.string.isRequired, + usenetDelay: PropTypes.number.isRequired, + torrentDelay: PropTypes.number.isRequired, + tags: PropTypes.arrayOf(PropTypes.number).isRequired, + tagList: PropTypes.arrayOf(PropTypes.object).isRequired, + isDragging: PropTypes.bool.isRequired, + connectDragSource: PropTypes.func, + onConfirmDeleteDelayProfile: PropTypes.func.isRequired +}; + +DelayProfile.defaultProps = { + // The drag preview will not connect the drag handle. + connectDragSource: (node) => node +}; + +export default DelayProfile; diff --git a/frontend/src/Settings/Profiles/Delay/DelayProfileDragPreview.css b/frontend/src/Settings/Profiles/Delay/DelayProfileDragPreview.css new file mode 100644 index 000000000..cc5a92830 --- /dev/null +++ b/frontend/src/Settings/Profiles/Delay/DelayProfileDragPreview.css @@ -0,0 +1,3 @@ +.dragPreview { + opacity: 0.75; +} diff --git a/frontend/src/Settings/Profiles/Delay/DelayProfileDragPreview.js b/frontend/src/Settings/Profiles/Delay/DelayProfileDragPreview.js new file mode 100644 index 000000000..402ddcc13 --- /dev/null +++ b/frontend/src/Settings/Profiles/Delay/DelayProfileDragPreview.js @@ -0,0 +1,78 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { DragLayer } from 'react-dnd'; +import dimensions from 'Styles/Variables/dimensions.js'; +import { DELAY_PROFILE } from 'Helpers/dragTypes'; +import DragPreviewLayer from 'Components/DragPreviewLayer'; +import DelayProfile from './DelayProfile'; +import styles from './DelayProfileDragPreview.css'; + +const dragHandleWidth = parseInt(dimensions.dragHandleWidth); + +function collectDragLayer(monitor) { + return { + item: monitor.getItem(), + itemType: monitor.getItemType(), + currentOffset: monitor.getSourceClientOffset() + }; +} + +class DelayProfileDragPreview extends Component { + + // + // Render + + render() { + const { + width, + item, + itemType, + currentOffset + } = this.props; + + if (!currentOffset || itemType !== DELAY_PROFILE) { + return null; + } + + // The offset is shifted because the drag handle is on the right edge of the + // list item and the preview is wider than the drag handle. + + const { x, y } = currentOffset; + const handleOffset = width - dragHandleWidth; + const transform = `translate3d(${x - handleOffset}px, ${y}px, 0)`; + + const style = { + width, + position: 'absolute', + WebkitTransform: transform, + msTransform: transform, + transform + }; + + return ( + +
+ +
+
+ ); + } +} + +DelayProfileDragPreview.propTypes = { + width: PropTypes.number.isRequired, + item: PropTypes.object, + itemType: PropTypes.string, + currentOffset: PropTypes.shape({ + x: PropTypes.number.isRequired, + y: PropTypes.number.isRequired + }) +}; + +export default DragLayer(collectDragLayer)(DelayProfileDragPreview); diff --git a/frontend/src/Settings/Profiles/Delay/DelayProfileDragSource.css b/frontend/src/Settings/Profiles/Delay/DelayProfileDragSource.css new file mode 100644 index 000000000..835250678 --- /dev/null +++ b/frontend/src/Settings/Profiles/Delay/DelayProfileDragSource.css @@ -0,0 +1,17 @@ +.delayProfileDragSource { + padding: 4px 0; +} + +.delayProfilePlaceholder { + width: 100%; + height: 30px; + border-bottom: 1px dotted #aaa; +} + +.delayProfilePlaceholderBefore { + margin-bottom: 8px; +} + +.delayProfilePlaceholderAfter { + margin-top: 8px; +} diff --git a/frontend/src/Settings/Profiles/Delay/DelayProfileDragSource.js b/frontend/src/Settings/Profiles/Delay/DelayProfileDragSource.js new file mode 100644 index 000000000..5c1c565e0 --- /dev/null +++ b/frontend/src/Settings/Profiles/Delay/DelayProfileDragSource.js @@ -0,0 +1,148 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { findDOMNode } from 'react-dom'; +import { DragSource, DropTarget } from 'react-dnd'; +import classNames from 'classnames'; +import { DELAY_PROFILE } from 'Helpers/dragTypes'; +import DelayProfile from './DelayProfile'; +import styles from './DelayProfileDragSource.css'; + +const delayProfileDragSource = { + beginDrag(item) { + return item; + }, + + endDrag(props, monitor, component) { + props.onDelayProfileDragEnd(monitor.getItem(), monitor.didDrop()); + } +}; + +const delayProfileDropTarget = { + hover(props, monitor, component) { + const dragIndex = monitor.getItem().order; + const hoverIndex = props.order; + + const hoverBoundingRect = findDOMNode(component).getBoundingClientRect(); + const hoverMiddleY = (hoverBoundingRect.bottom - hoverBoundingRect.top) / 2; + const clientOffset = monitor.getClientOffset(); + const hoverClientY = clientOffset.y - hoverBoundingRect.top; + + if (dragIndex === hoverIndex) { + return; + } + + // When moving up, only trigger if drag position is above 50% and + // when moving down, only trigger if drag position is below 50%. + // If we're moving down the hoverIndex needs to be increased + // by one so it's ordered properly. Otherwise the hoverIndex will work. + + if (dragIndex < hoverIndex && hoverClientY > hoverMiddleY) { + props.onDelayProfileDragMove(dragIndex, hoverIndex + 1); + } else if (dragIndex > hoverIndex && hoverClientY < hoverMiddleY) { + props.onDelayProfileDragMove(dragIndex, hoverIndex); + } + } +}; + +function collectDragSource(connect, monitor) { + return { + connectDragSource: connect.dragSource(), + isDragging: monitor.isDragging() + }; +} + +function collectDropTarget(connect, monitor) { + return { + connectDropTarget: connect.dropTarget(), + isOver: monitor.isOver() + }; +} + +class DelayProfileDragSource extends Component { + + // + // Render + + render() { + const { + id, + order, + isDragging, + isDraggingUp, + isDraggingDown, + isOver, + connectDragSource, + connectDropTarget, + ...otherProps + } = this.props; + + const isBefore = !isDragging && isDraggingUp && isOver; + const isAfter = !isDragging && isDraggingDown && isOver; + + // if (isDragging && !isOver) { + // return null; + // } + + return connectDropTarget( +
+ { + isBefore && +
+ } + + + + { + isAfter && +
+ } +
+ ); + } +} + +DelayProfileDragSource.propTypes = { + id: PropTypes.number.isRequired, + order: PropTypes.number.isRequired, + isDragging: PropTypes.bool, + isDraggingUp: PropTypes.bool, + isDraggingDown: PropTypes.bool, + isOver: PropTypes.bool, + connectDragSource: PropTypes.func, + connectDropTarget: PropTypes.func, + onDelayProfileDragMove: PropTypes.func.isRequired, + onDelayProfileDragEnd: PropTypes.func.isRequired +}; + +export default DropTarget( + DELAY_PROFILE, + delayProfileDropTarget, + collectDropTarget +)(DragSource( + DELAY_PROFILE, + delayProfileDragSource, + collectDragSource +)(DelayProfileDragSource)); diff --git a/frontend/src/Settings/Profiles/Delay/DelayProfiles.css b/frontend/src/Settings/Profiles/Delay/DelayProfiles.css new file mode 100644 index 000000000..3cf3e9020 --- /dev/null +++ b/frontend/src/Settings/Profiles/Delay/DelayProfiles.css @@ -0,0 +1,27 @@ +.delayProfiles { + user-select: none; +} + +.delayProfilesHeader { + display: flex; + margin-bottom: 10px; + font-weight: bold; +} + +.column { + flex: 0 0 200px; +} + +.tags { + flex: 1 0 auto; +} + +.addDelayProfile { + display: flex; + justify-content: flex-end; +} + +.addButton { + width: $dragHandleWidth; + text-align: center; +} diff --git a/frontend/src/Settings/Profiles/Delay/DelayProfiles.js b/frontend/src/Settings/Profiles/Delay/DelayProfiles.js new file mode 100644 index 000000000..7fc0cab2a --- /dev/null +++ b/frontend/src/Settings/Profiles/Delay/DelayProfiles.js @@ -0,0 +1,150 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import Measure from 'react-measure'; +import { icons } from 'Helpers/Props'; +import FieldSet from 'Components/FieldSet'; +import Icon from 'Components/Icon'; +import Link from 'Components/Link/Link'; +import PageSectionContent from 'Components/Page/PageSectionContent'; +import DelayProfileDragSource from './DelayProfileDragSource'; +import DelayProfileDragPreview from './DelayProfileDragPreview'; +import DelayProfile from './DelayProfile'; +import EditDelayProfileModalConnector from './EditDelayProfileModalConnector'; +import styles from './DelayProfiles.css'; + +class DelayProfiles extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + isAddDelayProfileModalOpen: false, + width: 0 + }; + } + + // + // Listeners + + onAddDelayProfilePress = () => { + this.setState({ isAddDelayProfileModalOpen: true }); + } + + onModalClose = () => { + this.setState({ isAddDelayProfileModalOpen: false }); + } + + onMeasure = ({ width }) => { + this.setState({ width }); + } + + // + // Render + + render() { + const { + defaultProfile, + items, + tagList, + dragIndex, + dropIndex, + onConfirmDeleteDelayProfile, + ...otherProps + } = this.props; + + const { + isAddDelayProfileModalOpen, + width + } = this.state; + + const isDragging = dropIndex !== null; + const isDraggingUp = isDragging && dropIndex < dragIndex; + const isDraggingDown = isDragging && dropIndex > dragIndex; + + return ( + +
+ +
+
Protocol
+
Usenet Delay
+
Torrent Delay
+
Tags
+
+ +
+ { + items.map((item, index) => { + return ( + + ); + }) + } + + +
+ + { + defaultProfile && +
+ +
+ } + +
+ + + +
+ + +
+
+
+ ); + } +} + +DelayProfiles.propTypes = { + isFetching: PropTypes.bool.isRequired, + error: PropTypes.object, + defaultProfile: PropTypes.object, + items: PropTypes.arrayOf(PropTypes.object).isRequired, + tagList: PropTypes.arrayOf(PropTypes.object).isRequired, + dragIndex: PropTypes.number, + dropIndex: PropTypes.number, + onConfirmDeleteDelayProfile: PropTypes.func.isRequired +}; + +export default DelayProfiles; diff --git a/frontend/src/Settings/Profiles/Delay/DelayProfilesConnector.js b/frontend/src/Settings/Profiles/Delay/DelayProfilesConnector.js new file mode 100644 index 000000000..16fe5718c --- /dev/null +++ b/frontend/src/Settings/Profiles/Delay/DelayProfilesConnector.js @@ -0,0 +1,105 @@ +import _ from 'lodash'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import { fetchDelayProfiles, deleteDelayProfile, reorderDelayProfile } from 'Store/Actions/settingsActions'; +import createTagsSelector from 'Store/Selectors/createTagsSelector'; +import DelayProfiles from './DelayProfiles'; + +function createMapStateToProps() { + return createSelector( + (state) => state.settings.delayProfiles, + createTagsSelector(), + (delayProfiles, tagList) => { + const defaultProfile = _.find(delayProfiles.items, { id: 1 }); + const items = _.sortBy(_.reject(delayProfiles.items, { id: 1 }), ['order']); + + return { + defaultProfile, + ...delayProfiles, + items, + tagList + }; + } + ); +} + +const mapDispatchToProps = { + fetchDelayProfiles, + deleteDelayProfile, + reorderDelayProfile +}; + +class DelayProfilesConnector extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + dragIndex: null, + dropIndex: null + }; + } + + componentDidMount() { + this.props.fetchDelayProfiles(); + } + + // + // Listeners + + onConfirmDeleteDelayProfile = (id) => { + this.props.deleteDelayProfile({ id }); + } + + onDelayProfileDragMove = (dragIndex, dropIndex) => { + if (this.state.dragIndex !== dragIndex || this.state.dropIndex !== dropIndex) { + this.setState({ + dragIndex, + dropIndex + }); + } + } + + onDelayProfileDragEnd = ({ id }, didDrop) => { + const { + dropIndex + } = this.state; + + if (didDrop && dropIndex !== null) { + this.props.reorderDelayProfile({ id, moveIndex: dropIndex - 1 }); + } + + this.setState({ + dragIndex: null, + dropIndex: null + }); + } + + // + // Render + + render() { + return ( + + ); + } +} + +DelayProfilesConnector.propTypes = { + fetchDelayProfiles: PropTypes.func.isRequired, + deleteDelayProfile: PropTypes.func.isRequired, + reorderDelayProfile: PropTypes.func.isRequired +}; + +export default connect(createMapStateToProps, mapDispatchToProps)(DelayProfilesConnector); diff --git a/frontend/src/Settings/Profiles/Delay/EditDelayProfileModal.js b/frontend/src/Settings/Profiles/Delay/EditDelayProfileModal.js new file mode 100644 index 000000000..a593471ac --- /dev/null +++ b/frontend/src/Settings/Profiles/Delay/EditDelayProfileModal.js @@ -0,0 +1,25 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import Modal from 'Components/Modal/Modal'; +import EditDelayProfileModalContentConnector from './EditDelayProfileModalContentConnector'; + +function EditDelayProfileModal({ isOpen, onModalClose, ...otherProps }) { + return ( + + + + ); +} + +EditDelayProfileModal.propTypes = { + isOpen: PropTypes.bool.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default EditDelayProfileModal; diff --git a/frontend/src/Settings/Profiles/Delay/EditDelayProfileModalConnector.js b/frontend/src/Settings/Profiles/Delay/EditDelayProfileModalConnector.js new file mode 100644 index 000000000..1f696c846 --- /dev/null +++ b/frontend/src/Settings/Profiles/Delay/EditDelayProfileModalConnector.js @@ -0,0 +1,43 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { clearPendingChanges } from 'Store/Actions/baseActions'; +import EditDelayProfileModal from './EditDelayProfileModal'; + +function mapStateToProps() { + return {}; +} + +const mapDispatchToProps = { + clearPendingChanges +}; + +class EditDelayProfileModalConnector extends Component { + + // + // Listeners + + onModalClose = () => { + this.props.clearPendingChanges({ section: 'delayProfiles' }); + this.props.onModalClose(); + } + + // + // Render + + render() { + return ( + + ); + } +} + +EditDelayProfileModalConnector.propTypes = { + onModalClose: PropTypes.func.isRequired, + clearPendingChanges: PropTypes.func.isRequired +}; + +export default connect(mapStateToProps, mapDispatchToProps)(EditDelayProfileModalConnector); diff --git a/frontend/src/Settings/Profiles/Delay/EditDelayProfileModalContent.css b/frontend/src/Settings/Profiles/Delay/EditDelayProfileModalContent.css new file mode 100644 index 000000000..a3c7f464c --- /dev/null +++ b/frontend/src/Settings/Profiles/Delay/EditDelayProfileModalContent.css @@ -0,0 +1,5 @@ +.deleteButton { + composes: button from 'Components/Link/Button.css'; + + margin-right: auto; +} diff --git a/frontend/src/Settings/Profiles/Delay/EditDelayProfileModalContent.js b/frontend/src/Settings/Profiles/Delay/EditDelayProfileModalContent.js new file mode 100644 index 000000000..8dbee2837 --- /dev/null +++ b/frontend/src/Settings/Profiles/Delay/EditDelayProfileModalContent.js @@ -0,0 +1,186 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import { inputTypes, kinds } from 'Helpers/Props'; +import { boolSettingShape, numberSettingShape, tagSettingShape } from 'Helpers/Props/Shapes/settingShape'; +import Button from 'Components/Link/Button'; +import SpinnerErrorButton from 'Components/Link/SpinnerErrorButton'; +import LoadingIndicator from 'Components/Loading/LoadingIndicator'; +import Alert from 'Components/Alert'; +import ModalContent from 'Components/Modal/ModalContent'; +import ModalHeader from 'Components/Modal/ModalHeader'; +import ModalBody from 'Components/Modal/ModalBody'; +import ModalFooter from 'Components/Modal/ModalFooter'; +import Form from 'Components/Form/Form'; +import FormGroup from 'Components/Form/FormGroup'; +import FormLabel from 'Components/Form/FormLabel'; +import FormInputGroup from 'Components/Form/FormInputGroup'; +import styles from './EditDelayProfileModalContent.css'; + +function EditDelayProfileModalContent(props) { + const { + id, + isFetching, + error, + isSaving, + saveError, + item, + protocol, + protocolOptions, + onInputChange, + onProtocolChange, + onSavePress, + onModalClose, + onDeleteDelayProfilePress, + ...otherProps + } = props; + + const { + enableUsenet, + enableTorrent, + usenetDelay, + torrentDelay, + tags + } = item; + + return ( + + + {id ? 'Edit Delay Profile' : 'Add Delay Profile'} + + + + { + isFetching && + + } + + { + !isFetching && !!error && +
Unable to add a new quality profile, please try again.
+ } + + { + !isFetching && !error && +
+ + Protocol + + + + + { + enableUsenet.value && + + Usenet Delay + + + + } + + { + enableTorrent.value && + + Torrent Delay + + + + } + + { + id === 1 ? + + This is the default profile. It applies to all artist that don't have an explicit profile. + : + + + Tags + + + + } +
+ } +
+ + { + id && id > 1 && + + } + + + + + Save + + +
+ ); +} + +const delayProfileShape = { + enableUsenet: PropTypes.shape(boolSettingShape).isRequired, + enableTorrent: PropTypes.shape(boolSettingShape).isRequired, + usenetDelay: PropTypes.shape(numberSettingShape).isRequired, + torrentDelay: PropTypes.shape(numberSettingShape).isRequired, + order: PropTypes.shape(numberSettingShape), + tags: PropTypes.shape(tagSettingShape).isRequired +}; + +EditDelayProfileModalContent.propTypes = { + id: PropTypes.number, + isFetching: PropTypes.bool.isRequired, + error: PropTypes.object, + isSaving: PropTypes.bool.isRequired, + saveError: PropTypes.object, + item: PropTypes.shape(delayProfileShape).isRequired, + protocol: PropTypes.string.isRequired, + protocolOptions: PropTypes.arrayOf(PropTypes.object).isRequired, + onInputChange: PropTypes.func.isRequired, + onProtocolChange: PropTypes.func.isRequired, + onSavePress: PropTypes.func.isRequired, + onModalClose: PropTypes.func.isRequired, + onDeleteDelayProfilePress: PropTypes.func +}; + +export default EditDelayProfileModalContent; diff --git a/frontend/src/Settings/Profiles/Delay/EditDelayProfileModalContentConnector.js b/frontend/src/Settings/Profiles/Delay/EditDelayProfileModalContentConnector.js new file mode 100644 index 000000000..8cd001950 --- /dev/null +++ b/frontend/src/Settings/Profiles/Delay/EditDelayProfileModalContentConnector.js @@ -0,0 +1,178 @@ +import _ from 'lodash'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import selectSettings from 'Store/Selectors/selectSettings'; +import { setDelayProfileValue, saveDelayProfile } from 'Store/Actions/settingsActions'; +import EditDelayProfileModalContent from './EditDelayProfileModalContent'; + +const newDelayProfile = { + enableUsenet: true, + enableTorrent: true, + preferredProtocol: 'usenet', + usenetDelay: 0, + torrentDelay: 0, + tags: [] +}; + +function createDelayProfileSelector() { + return createSelector( + (state, { id }) => id, + (state) => state.settings.delayProfiles, + (id, delayProfiles) => { + const { + isFetching, + error, + isSaving, + saveError, + pendingChanges, + items + } = delayProfiles; + + const profile = id ? _.find(items, { id }) : newDelayProfile; + const settings = selectSettings(profile, pendingChanges, saveError); + + return { + id, + isFetching, + error, + isSaving, + saveError, + item: settings.settings, + ...settings + }; + } + ); +} + +function createMapStateToProps() { + return createSelector( + createDelayProfileSelector(), + (delayProfile) => { + const protocolOptions = [ + { key: 'preferUsenet', value: 'Prefer Usenet' }, + { key: 'preferTorrent', value: 'Prefer Torrent' }, + { key: 'onlyUsenet', value: 'Only Usenet' }, + { key: 'onlyTorrent', value: 'Only Torrent' } + ]; + + const enableUsenet = delayProfile.item.enableUsenet.value; + const enableTorrent = delayProfile.item.enableTorrent.value; + const preferredProtocol = delayProfile.item.preferredProtocol.value; + let protocol = 'preferUsenet'; + + if (preferredProtocol === 'usenet') { + protocol = 'preferUsenet'; + } else { + protocol = 'preferTorrent'; + } + + if (!enableUsenet) { + protocol = 'onlyTorrent'; + } + + if (!enableTorrent) { + protocol = 'onlyUsenet'; + } + + return { + protocol, + protocolOptions, + ...delayProfile + }; + } + ); +} + +const mapDispatchToProps = { + setDelayProfileValue, + saveDelayProfile +}; + +class EditDelayProfileModalContentConnector extends Component { + + // + // Lifecycle + + componentDidMount() { + if (!this.props.id) { + Object.keys(newDelayProfile).forEach((name) => { + this.props.setDelayProfileValue({ + name, + value: newDelayProfile[name] + }); + }); + } + } + + componentDidUpdate(prevProps, prevState) { + if (prevProps.isSaving && !this.props.isSaving && !this.props.saveError) { + this.props.onModalClose(); + } + } + + // + // Listeners + + onInputChange = ({ name, value }) => { + this.props.setDelayProfileValue({ name, value }); + } + + onProtocolChange = ({ value }) => { + switch (value) { + case 'preferUsenet': + this.props.setDelayProfileValue({ name: 'enableUsenet', value: true }); + this.props.setDelayProfileValue({ name: 'enableTorrent', value: true }); + this.props.setDelayProfileValue({ name: 'preferredProtocol', value: 'usenet' }); + break; + case 'preferTorrent': + this.props.setDelayProfileValue({ name: 'enableUsenet', value: true }); + this.props.setDelayProfileValue({ name: 'enableTorrent', value: true }); + this.props.setDelayProfileValue({ name: 'preferredProtocol', value: 'torrent' }); + break; + case 'onlyUsenet': + this.props.setDelayProfileValue({ name: 'enableUsenet', value: true }); + this.props.setDelayProfileValue({ name: 'enableTorrent', value: false }); + this.props.setDelayProfileValue({ name: 'preferredProtocol', value: 'usenet' }); + break; + case 'onlyTorrent': + this.props.setDelayProfileValue({ name: 'enableUsenet', value: false }); + this.props.setDelayProfileValue({ name: 'enableTorrent', value: true }); + this.props.setDelayProfileValue({ name: 'preferredProtocol', value: 'torrent' }); + break; + default: + throw Error(`Unknown protocol option: ${value}`); + } + } + + onSavePress = () => { + this.props.saveDelayProfile({ id: this.props.id }); + } + + // + // Render + + render() { + return ( + + ); + } +} + +EditDelayProfileModalContentConnector.propTypes = { + id: PropTypes.number, + isSaving: PropTypes.bool.isRequired, + saveError: PropTypes.object, + item: PropTypes.object.isRequired, + setDelayProfileValue: PropTypes.func.isRequired, + saveDelayProfile: PropTypes.func.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default connect(createMapStateToProps, mapDispatchToProps)(EditDelayProfileModalContentConnector); diff --git a/frontend/src/Settings/Profiles/Language/EditLanguageProfileModal.js b/frontend/src/Settings/Profiles/Language/EditLanguageProfileModal.js new file mode 100644 index 000000000..7fe5a1823 --- /dev/null +++ b/frontend/src/Settings/Profiles/Language/EditLanguageProfileModal.js @@ -0,0 +1,25 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import Modal from 'Components/Modal/Modal'; +import EditLanguageProfileModalContentConnector from './EditLanguageProfileModalContentConnector'; + +function EditLanguageProfileModal({ isOpen, onModalClose, ...otherProps }) { + return ( + + + + ); +} + +EditLanguageProfileModal.propTypes = { + isOpen: PropTypes.bool.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default EditLanguageProfileModal; diff --git a/frontend/src/Settings/Profiles/Language/EditLanguageProfileModalConnector.js b/frontend/src/Settings/Profiles/Language/EditLanguageProfileModalConnector.js new file mode 100644 index 000000000..44866e2a3 --- /dev/null +++ b/frontend/src/Settings/Profiles/Language/EditLanguageProfileModalConnector.js @@ -0,0 +1,43 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { clearPendingChanges } from 'Store/Actions/baseActions'; +import EditLanguageProfileModal from './EditLanguageProfileModal'; + +function mapStateToProps() { + return {}; +} + +const mapDispatchToProps = { + clearPendingChanges +}; + +class EditLanguageProfileModalConnector extends Component { + + // + // Listeners + + onModalClose = () => { + this.props.clearPendingChanges({ section: 'languageProfiles' }); + this.props.onModalClose(); + } + + // + // Render + + render() { + return ( + + ); + } +} + +EditLanguageProfileModalConnector.propTypes = { + onModalClose: PropTypes.func.isRequired, + clearPendingChanges: PropTypes.func.isRequired +}; + +export default connect(mapStateToProps, mapDispatchToProps)(EditLanguageProfileModalConnector); diff --git a/frontend/src/Settings/Profiles/Language/EditLanguageProfileModalContent.css b/frontend/src/Settings/Profiles/Language/EditLanguageProfileModalContent.css new file mode 100644 index 000000000..74dd1c8b7 --- /dev/null +++ b/frontend/src/Settings/Profiles/Language/EditLanguageProfileModalContent.css @@ -0,0 +1,3 @@ +.deleteButtonContainer { + margin-right: auto; +} diff --git a/frontend/src/Settings/Profiles/Language/EditLanguageProfileModalContent.js b/frontend/src/Settings/Profiles/Language/EditLanguageProfileModalContent.js new file mode 100644 index 000000000..f1686dcce --- /dev/null +++ b/frontend/src/Settings/Profiles/Language/EditLanguageProfileModalContent.js @@ -0,0 +1,149 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import { inputTypes, kinds } from 'Helpers/Props'; +import Button from 'Components/Link/Button'; +import SpinnerErrorButton from 'Components/Link/SpinnerErrorButton'; +import LoadingIndicator from 'Components/Loading/LoadingIndicator'; +import ModalContent from 'Components/Modal/ModalContent'; +import ModalHeader from 'Components/Modal/ModalHeader'; +import ModalBody from 'Components/Modal/ModalBody'; +import ModalFooter from 'Components/Modal/ModalFooter'; +import Form from 'Components/Form/Form'; +import FormGroup from 'Components/Form/FormGroup'; +import FormLabel from 'Components/Form/FormLabel'; +import FormInputGroup from 'Components/Form/FormInputGroup'; +import LanguageProfileItems from './LanguageProfileItems'; +import styles from './EditLanguageProfileModalContent.css'; + +function EditLanguageProfileModalContent(props) { + const { + isFetching, + error, + isSaving, + saveError, + languages, + item, + isInUse, + onInputChange, + onCutoffChange, + onSavePress, + onModalClose, + onDeleteLanguageProfilePress, + ...otherProps + } = props; + + const { + id, + name, + cutoff, + languages: itemLanguages + } = item; + + return ( + + + {id ? 'Edit Language Profile' : 'Add Language Profile'} + + + + { + isFetching && + + } + + { + !isFetching && !!error && +
Unable to add a new language profile, please try again.
+ } + + { + !isFetching && !error && +
+ + Name + + + + + + Cutoff + + + + + + + + } +
+ + { + id && +
+ +
+ } + + + + + Save + +
+
+ ); +} + +EditLanguageProfileModalContent.propTypes = { + isFetching: PropTypes.bool.isRequired, + error: PropTypes.object, + isSaving: PropTypes.bool.isRequired, + saveError: PropTypes.object, + languages: PropTypes.arrayOf(PropTypes.object).isRequired, + item: PropTypes.object.isRequired, + isInUse: PropTypes.bool.isRequired, + onInputChange: PropTypes.func.isRequired, + onCutoffChange: PropTypes.func.isRequired, + onSavePress: PropTypes.func.isRequired, + onModalClose: PropTypes.func.isRequired, + onDeleteLanguageProfilePress: PropTypes.func +}; + +export default EditLanguageProfileModalContent; diff --git a/frontend/src/Settings/Profiles/Language/EditLanguageProfileModalContentConnector.js b/frontend/src/Settings/Profiles/Language/EditLanguageProfileModalContentConnector.js new file mode 100644 index 000000000..13f72f623 --- /dev/null +++ b/frontend/src/Settings/Profiles/Language/EditLanguageProfileModalContentConnector.js @@ -0,0 +1,194 @@ +import _ from 'lodash'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { createSelector } from 'reselect'; +import createProfileInUseSelector from 'Store/Selectors/createProfileInUseSelector'; +import createProviderSettingsSelector from 'Store/Selectors/createProviderSettingsSelector'; +import { fetchLanguageProfileSchema, setLanguageProfileValue, saveLanguageProfile } from 'Store/Actions/settingsActions'; +import connectSection from 'Store/connectSection'; +import EditLanguageProfileModalContent from './EditLanguageProfileModalContent'; + +function createLanguagesSelector() { + return createSelector( + createProviderSettingsSelector(), + (languageProfile) => { + const languages = languageProfile.item.languages; + if (!languages || !languages.value) { + return []; + } + + return _.reduceRight(languages.value, (result, { allowed, language }) => { + if (allowed) { + result.push({ + key: language.id, + value: language.name + }); + } + + return result; + }, []); + } + ); +} + +function createMapStateToProps() { + return createSelector( + createProviderSettingsSelector(), + createLanguagesSelector(), + createProfileInUseSelector('languageProfileId'), + (languageProfile, languages, isInUse) => { + return { + languages, + ...languageProfile, + isInUse + }; + } + ); +} + +const mapDispatchToProps = { + fetchLanguageProfileSchema, + setLanguageProfileValue, + saveLanguageProfile +}; + +class EditLanguageProfileModalContentConnector extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + dragIndex: null, + dropIndex: null + }; + } + + componentDidMount() { + if (!this.props.id) { + this.props.fetchLanguageProfileSchema(); + } + } + + componentDidUpdate(prevProps, prevState) { + if (prevProps.isSaving && !this.props.isSaving && !this.props.saveError) { + this.props.onModalClose(); + } + } + + // + // Listeners + + onInputChange = ({ name, value }) => { + this.props.setLanguageProfileValue({ name, value }); + } + + onCutoffChange = ({ name, value }) => { + const id = parseInt(value); + const item = _.find(this.props.item.languages.value, (i) => i.language.id === id); + + this.props.setLanguageProfileValue({ name, value: item.language }); + } + + onSavePress = () => { + this.props.saveLanguageProfile({ id: this.props.id }); + } + + onLanguageProfileItemAllowedChange = (id, allowed) => { + const languageProfile = _.cloneDeep(this.props.item); + + const item = _.find(languageProfile.languages.value, (i) => i.language.id === id); + item.allowed = allowed; + + this.props.setLanguageProfileValue({ + name: 'languages', + value: languageProfile.languages.value + }); + + const cutoff = languageProfile.cutoff.value; + + // If the cutoff isn't allowed anymore or there isn't a cutoff set one + if (!cutoff || !_.find(languageProfile.languages.value, (i) => i.language.id === cutoff.id).allowed) { + const firstAllowed = _.find(languageProfile.languages.value, { allowed: true }); + + this.props.setLanguageProfileValue({ name: 'cutoff', value: firstAllowed ? firstAllowed.language : null }); + } + } + + onLanguageProfileItemDragMove = (dragIndex, dropIndex) => { + if (this.state.dragIndex !== dragIndex || this.state.dropIndex !== dropIndex) { + this.setState({ + dragIndex, + dropIndex + }); + } + } + + onLanguageProfileItemDragEnd = ({ id }, didDrop) => { + const { + dragIndex, + dropIndex + } = this.state; + + if (didDrop && dropIndex !== null) { + const languageProfile = _.cloneDeep(this.props.item); + + const languages = languageProfile.languages.value.splice(dragIndex, 1); + languageProfile.languages.value.splice(dropIndex, 0, languages[0]); + + this.props.setLanguageProfileValue({ + name: 'languages', + value: languageProfile.languages.value + }); + } + + this.setState({ + dragIndex: null, + dropIndex: null + }); + } + + // + // Render + + render() { + if (_.isEmpty(this.props.item.languages) && !this.props.isFetching) { + return null; + } + + return ( + + ); + } +} + +EditLanguageProfileModalContentConnector.propTypes = { + id: PropTypes.number, + isFetching: PropTypes.bool.isRequired, + isSaving: PropTypes.bool.isRequired, + saveError: PropTypes.object, + item: PropTypes.object.isRequired, + setLanguageProfileValue: PropTypes.func.isRequired, + fetchLanguageProfileSchema: PropTypes.func.isRequired, + saveLanguageProfile: PropTypes.func.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default connectSection( + createMapStateToProps, + mapDispatchToProps, + undefined, + undefined, + { section: 'languageProfiles' } +)(EditLanguageProfileModalContentConnector); diff --git a/frontend/src/Settings/Profiles/Language/LanguageProfile.css b/frontend/src/Settings/Profiles/Language/LanguageProfile.css new file mode 100644 index 000000000..d23bd1734 --- /dev/null +++ b/frontend/src/Settings/Profiles/Language/LanguageProfile.css @@ -0,0 +1,19 @@ +.languageProfile { + composes: card from 'Components/Card.css'; + + width: 300px; +} + +.name { + @add-mixin truncate; + + margin-bottom: 20px; + font-weight: 300; + font-size: 24px; +} + +.languages { + display: flex; + flex-wrap: wrap; + margin-top: 5px; +} diff --git a/frontend/src/Settings/Profiles/Language/LanguageProfile.js b/frontend/src/Settings/Profiles/Language/LanguageProfile.js new file mode 100644 index 000000000..a13a5509d --- /dev/null +++ b/frontend/src/Settings/Profiles/Language/LanguageProfile.js @@ -0,0 +1,124 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { kinds } from 'Helpers/Props'; +import Card from 'Components/Card'; +import Label from 'Components/Label'; +import ConfirmModal from 'Components/Modal/ConfirmModal'; +import EditLanguageProfileModalConnector from './EditLanguageProfileModalConnector'; +import styles from './LanguageProfile.css'; + +class LanguageProfile extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + isEditLanguageProfileModalOpen: false, + isDeleteLanguageProfileModalOpen: false + }; + } + + // + // Listeners + + onEditLanguageProfilePress = () => { + this.setState({ isEditLanguageProfileModalOpen: true }); + } + + onEditLanguageProfileModalClose = () => { + this.setState({ isEditLanguageProfileModalOpen: false }); + } + + onDeleteLanguageProfilePress = () => { + this.setState({ + isEditLanguageProfileModalOpen: false, + isDeleteLanguageProfileModalOpen: true + }); + } + + onDeleteLanguageProfileModalClose = () => { + this.setState({ isDeleteLanguageProfileModalOpen: false }); + } + + onConfirmDeleteLanguageProfile = () => { + this.props.onConfirmDeleteLanguageProfile(this.props.id); + } + + // + // Render + + render() { + const { + id, + name, + cutoff, + languages, + isDeleting + } = this.props; + + return ( + +
+ {name} +
+ +
+ { + languages.map((item) => { + if (!item.allowed) { + return null; + } + + const isCutoff = item.language.id === cutoff.id; + + return ( + + ); + }) + } +
+ + + + +
+ ); + } +} + +LanguageProfile.propTypes = { + id: PropTypes.number.isRequired, + name: PropTypes.string.isRequired, + cutoff: PropTypes.object.isRequired, + languages: PropTypes.arrayOf(PropTypes.object).isRequired, + isDeleting: PropTypes.bool.isRequired, + onConfirmDeleteLanguageProfile: PropTypes.func.isRequired +}; + +export default LanguageProfile; diff --git a/frontend/src/Settings/Profiles/Language/LanguageProfileItem.css b/frontend/src/Settings/Profiles/Language/LanguageProfileItem.css new file mode 100644 index 000000000..a10233929 --- /dev/null +++ b/frontend/src/Settings/Profiles/Language/LanguageProfileItem.css @@ -0,0 +1,44 @@ +.languageProfileItem { + display: flex; + align-items: stretch; + width: 100%; + border: 1px solid #aaa; + border-radius: 4px; + background: #fafafa; +} + +.checkContainer { + position: relative; + margin-right: 4px; + margin-bottom: 7px; + margin-left: 8px; +} + +.languageName { + display: flex; + flex-grow: 1; + margin-bottom: 0; + margin-left: 2px; + font-weight: normal; + line-height: 36px; + cursor: pointer; +} + +.dragHandle { + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + margin-left: auto; + width: $dragHandleWidth; + text-align: center; + cursor: grab; +} + +.dragIcon { + top: 0; +} + +.isDragging { + opacity: 0.25; +} diff --git a/frontend/src/Settings/Profiles/Language/LanguageProfileItem.js b/frontend/src/Settings/Profiles/Language/LanguageProfileItem.js new file mode 100644 index 000000000..2a3671268 --- /dev/null +++ b/frontend/src/Settings/Profiles/Language/LanguageProfileItem.js @@ -0,0 +1,83 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import classNames from 'classnames'; +import { icons } from 'Helpers/Props'; +import Icon from 'Components/Icon'; +import CheckInput from 'Components/Form/CheckInput'; +import styles from './LanguageProfileItem.css'; + +class LanguageProfileItem extends Component { + + // + // Listeners + + onAllowedChange = ({ value }) => { + const { + languageId, + onLanguageProfileItemAllowedChange + } = this.props; + + onLanguageProfileItemAllowedChange(languageId, value); + } + + // + // Render + + render() { + const { + name, + allowed, + isDragging, + connectDragSource + } = this.props; + + return ( +
+ + + { + connectDragSource( +
+ +
+ ) + } +
+ ); + } +} + +LanguageProfileItem.propTypes = { + languageId: PropTypes.number.isRequired, + name: PropTypes.string.isRequired, + allowed: PropTypes.bool.isRequired, + sortIndex: PropTypes.number.isRequired, + isDragging: PropTypes.bool.isRequired, + connectDragSource: PropTypes.func, + onLanguageProfileItemAllowedChange: PropTypes.func +}; + +LanguageProfileItem.defaultProps = { + // The drag preview will not connect the drag handle. + connectDragSource: (node) => node +}; + +export default LanguageProfileItem; diff --git a/frontend/src/Settings/Profiles/Language/LanguageProfileItemDragPreview.css b/frontend/src/Settings/Profiles/Language/LanguageProfileItemDragPreview.css new file mode 100644 index 000000000..b927d9bce --- /dev/null +++ b/frontend/src/Settings/Profiles/Language/LanguageProfileItemDragPreview.css @@ -0,0 +1,4 @@ +.dragPreview { + width: 380px; + opacity: 0.75; +} diff --git a/frontend/src/Settings/Profiles/Language/LanguageProfileItemDragPreview.js b/frontend/src/Settings/Profiles/Language/LanguageProfileItemDragPreview.js new file mode 100644 index 000000000..0b6b27d8e --- /dev/null +++ b/frontend/src/Settings/Profiles/Language/LanguageProfileItemDragPreview.js @@ -0,0 +1,88 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { DragLayer } from 'react-dnd'; +import dimensions from 'Styles/Variables/dimensions.js'; +import { QUALITY_PROFILE_ITEM } from 'Helpers/dragTypes'; +import DragPreviewLayer from 'Components/DragPreviewLayer'; +import LanguageProfileItem from './LanguageProfileItem'; +import styles from './LanguageProfileItemDragPreview.css'; + +const formGroupSmallWidth = parseInt(dimensions.formGroupSmallWidth); +const formLabelWidth = parseInt(dimensions.formLabelWidth); +const formLabelRightMarginWidth = parseInt(dimensions.formLabelRightMarginWidth); +const dragHandleWidth = parseInt(dimensions.dragHandleWidth); + +function collectDragLayer(monitor) { + return { + item: monitor.getItem(), + itemType: monitor.getItemType(), + currentOffset: monitor.getSourceClientOffset() + }; +} + +class LanguageProfileItemDragPreview extends Component { + + // + // Render + + render() { + const { + item, + itemType, + currentOffset + } = this.props; + + if (!currentOffset || itemType !== QUALITY_PROFILE_ITEM) { + return null; + } + + // The offset is shifted because the drag handle is on the right edge of the + // list item and the preview is wider than the drag handle. + + const { x, y } = currentOffset; + const handleOffset = formGroupSmallWidth - formLabelWidth - formLabelRightMarginWidth - dragHandleWidth; + const transform = `translate3d(${x - handleOffset}px, ${y}px, 0)`; + + const style = { + position: 'absolute', + WebkitTransform: transform, + msTransform: transform, + transform + }; + + const { + languageId, + name, + allowed, + sortIndex + } = item; + + return ( + +
+ +
+
+ ); + } +} + +LanguageProfileItemDragPreview.propTypes = { + item: PropTypes.object, + itemType: PropTypes.string, + currentOffset: PropTypes.shape({ + x: PropTypes.number.isRequired, + y: PropTypes.number.isRequired + }) +}; + +export default DragLayer(collectDragLayer)(LanguageProfileItemDragPreview); diff --git a/frontend/src/Settings/Profiles/Language/LanguageProfileItemDragSource.css b/frontend/src/Settings/Profiles/Language/LanguageProfileItemDragSource.css new file mode 100644 index 000000000..f59379129 --- /dev/null +++ b/frontend/src/Settings/Profiles/Language/LanguageProfileItemDragSource.css @@ -0,0 +1,18 @@ +.languageProfileItemDragSource { + padding: 4px 0; +} + +.languageProfileItemPlaceholder { + width: 100%; + height: 36px; + border: 1px dotted #aaa; + border-radius: 4px; +} + +.languageProfileItemPlaceholderBefore { + margin-bottom: 8px; +} + +.languageProfileItemPlaceholderAfter { + margin-top: 8px; +} diff --git a/frontend/src/Settings/Profiles/Language/LanguageProfileItemDragSource.js b/frontend/src/Settings/Profiles/Language/LanguageProfileItemDragSource.js new file mode 100644 index 000000000..304363726 --- /dev/null +++ b/frontend/src/Settings/Profiles/Language/LanguageProfileItemDragSource.js @@ -0,0 +1,157 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { findDOMNode } from 'react-dom'; +import { DragSource, DropTarget } from 'react-dnd'; +import classNames from 'classnames'; +import { QUALITY_PROFILE_ITEM } from 'Helpers/dragTypes'; +import LanguageProfileItem from './LanguageProfileItem'; +import styles from './LanguageProfileItemDragSource.css'; + +const languageProfileItemDragSource = { + beginDrag({ languageId, name, allowed, sortIndex }) { + return { + languageId, + name, + allowed, + sortIndex + }; + }, + + endDrag(props, monitor, component) { + props.onLanguageProfileItemDragEnd(monitor.getItem(), monitor.didDrop()); + } +}; + +const languageProfileItemDropTarget = { + hover(props, monitor, component) { + const dragIndex = monitor.getItem().sortIndex; + const hoverIndex = props.sortIndex; + + const hoverBoundingRect = findDOMNode(component).getBoundingClientRect(); + const hoverMiddleY = (hoverBoundingRect.bottom - hoverBoundingRect.top) / 2; + const clientOffset = monitor.getClientOffset(); + const hoverClientY = clientOffset.y - hoverBoundingRect.top; + + // Moving up, only trigger if drag position is above 50% + if (dragIndex < hoverIndex && hoverClientY > hoverMiddleY) { + return; + } + + // Moving down, only trigger if drag position is below 50% + if (dragIndex > hoverIndex && hoverClientY < hoverMiddleY) { + return; + } + + props.onLanguageProfileItemDragMove(dragIndex, hoverIndex); + } +}; + +function collectDragSource(connect, monitor) { + return { + connectDragSource: connect.dragSource(), + isDragging: monitor.isDragging() + }; +} + +function collectDropTarget(connect, monitor) { + return { + connectDropTarget: connect.dropTarget(), + isOver: monitor.isOver() + }; +} + +class LanguageProfileItemDragSource extends Component { + + // + // Render + + render() { + const { + languageId, + name, + allowed, + sortIndex, + isDragging, + isDraggingUp, + isDraggingDown, + isOver, + connectDragSource, + connectDropTarget, + onLanguageProfileItemAllowedChange + } = this.props; + + const isBefore = !isDragging && isDraggingUp && isOver; + const isAfter = !isDragging && isDraggingDown && isOver; + + // if (isDragging && !isOver) { + // return null; + // } + + return connectDropTarget( +
+ { + isBefore && +
+ } + + + + { + isAfter && +
+ } +
+ ); + } +} + +LanguageProfileItemDragSource.propTypes = { + languageId: PropTypes.number.isRequired, + name: PropTypes.string.isRequired, + allowed: PropTypes.bool.isRequired, + sortIndex: PropTypes.number.isRequired, + isDragging: PropTypes.bool, + isDraggingUp: PropTypes.bool, + isDraggingDown: PropTypes.bool, + isOver: PropTypes.bool, + connectDragSource: PropTypes.func, + connectDropTarget: PropTypes.func, + onLanguageProfileItemAllowedChange: PropTypes.func.isRequired, + onLanguageProfileItemDragMove: PropTypes.func.isRequired, + onLanguageProfileItemDragEnd: PropTypes.func.isRequired +}; + +export default DropTarget( + QUALITY_PROFILE_ITEM, + languageProfileItemDropTarget, + collectDropTarget +)(DragSource( + QUALITY_PROFILE_ITEM, + languageProfileItemDragSource, + collectDragSource +)(LanguageProfileItemDragSource)); diff --git a/frontend/src/Settings/Profiles/Language/LanguageProfileItems.css b/frontend/src/Settings/Profiles/Language/LanguageProfileItems.css new file mode 100644 index 000000000..48b30f326 --- /dev/null +++ b/frontend/src/Settings/Profiles/Language/LanguageProfileItems.css @@ -0,0 +1,6 @@ +.languages { + margin-top: 10px; + /* TODO: This should consider the number of languages in the list */ + min-height: 550px; + user-select: none; +} diff --git a/frontend/src/Settings/Profiles/Language/LanguageProfileItems.js b/frontend/src/Settings/Profiles/Language/LanguageProfileItems.js new file mode 100644 index 000000000..831743cbe --- /dev/null +++ b/frontend/src/Settings/Profiles/Language/LanguageProfileItems.js @@ -0,0 +1,103 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import FormGroup from 'Components/Form/FormGroup'; +import FormLabel from 'Components/Form/FormLabel'; +import FormInputHelpText from 'Components/Form/FormInputHelpText'; +import LanguageProfileItemDragSource from './LanguageProfileItemDragSource'; +import LanguageProfileItemDragPreview from './LanguageProfileItemDragPreview'; +import styles from './LanguageProfileItems.css'; + +class LanguageProfileItems extends Component { + + // + // Render + + render() { + const { + dragIndex, + dropIndex, + languageProfileItems, + errors, + warnings, + ...otherProps + } = this.props; + + const isDragging = dropIndex !== null; + const isDraggingUp = isDragging && dropIndex > dragIndex; + const isDraggingDown = isDragging && dropIndex < dragIndex; + + return ( + + Languages +
+ + + { + errors.map((error, index) => { + return ( + + ); + }) + } + + { + warnings.map((warning, index) => { + return ( + + ); + }) + } + +
+ { + languageProfileItems.map(({ allowed, language }, index) => { + return ( + + ); + }).reverse() + } + + +
+
+
+ ); + } +} + +LanguageProfileItems.propTypes = { + dragIndex: PropTypes.number, + dropIndex: PropTypes.number, + languageProfileItems: PropTypes.arrayOf(PropTypes.object).isRequired, + errors: PropTypes.arrayOf(PropTypes.object), + warnings: PropTypes.arrayOf(PropTypes.object) +}; + +LanguageProfileItems.defaultProps = { + errors: [], + warnings: [] +}; + +export default LanguageProfileItems; diff --git a/frontend/src/Settings/Profiles/Language/LanguageProfileNameConnector.js b/frontend/src/Settings/Profiles/Language/LanguageProfileNameConnector.js new file mode 100644 index 000000000..61a7153b5 --- /dev/null +++ b/frontend/src/Settings/Profiles/Language/LanguageProfileNameConnector.js @@ -0,0 +1,31 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import createLanguageProfileSelector from 'Store/Selectors/createLanguageProfileSelector'; + +function createMapStateToProps() { + return createSelector( + createLanguageProfileSelector(), + (languageProfile) => { + return { + name: languageProfile.name + }; + } + ); +} + +function LanguageProfileNameConnector({ name, ...otherProps }) { + return ( + + {name} + + ); +} + +LanguageProfileNameConnector.propTypes = { + languageProfileId: PropTypes.number.isRequired, + name: PropTypes.string.isRequired +}; + +export default connect(createMapStateToProps)(LanguageProfileNameConnector); diff --git a/frontend/src/Settings/Profiles/Language/LanguageProfiles.css b/frontend/src/Settings/Profiles/Language/LanguageProfiles.css new file mode 100644 index 000000000..5a2fb73bd --- /dev/null +++ b/frontend/src/Settings/Profiles/Language/LanguageProfiles.css @@ -0,0 +1,21 @@ +.languageProfiles { + display: flex; + flex-wrap: wrap; +} + +.addLanguageProfile { + composes: languageProfile from './LanguageProfile.css'; + + background-color: $cardAlternateBackgroundColor; + color: $gray; + text-align: center; + font-size: 45px; +} + +.center { + display: inline-block; + padding: 5px 20px 0; + border: 1px solid $borderColor; + border-radius: 4px; + background-color: $white; +} diff --git a/frontend/src/Settings/Profiles/Language/LanguageProfiles.js b/frontend/src/Settings/Profiles/Language/LanguageProfiles.js new file mode 100644 index 000000000..2c2df29aa --- /dev/null +++ b/frontend/src/Settings/Profiles/Language/LanguageProfiles.js @@ -0,0 +1,102 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import sortByName from 'Utilities/Array/sortByName'; +import { icons } from 'Helpers/Props'; +import FieldSet from 'Components/FieldSet'; +import Card from 'Components/Card'; +import Icon from 'Components/Icon'; +import PageSectionContent from 'Components/Page/PageSectionContent'; +import LanguageProfile from './LanguageProfile'; +import EditLanguageProfileModalConnector from './EditLanguageProfileModalConnector'; +import styles from './LanguageProfiles.css'; + +class LanguageProfiles extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + isLanguageProfileModalOpen: false + }; + } + + // + // Listeners + + onEditLanguageProfilePress = () => { + this.setState({ isLanguageProfileModalOpen: true }); + } + + onModalClose = () => { + this.setState({ isLanguageProfileModalOpen: false }); + } + + // + // Render + + render() { + const { + items, + isDeleting, + onConfirmDeleteLanguageProfile, + ...otherProps + } = this.props; + + return ( +
+ +
+ { + items.sort(sortByName).map((item) => { + return ( + + ); + }) + } + + +
+ +
+
+
+ + +
+
+ ); + } +} + +LanguageProfiles.propTypes = { + advancedSettings: PropTypes.bool.isRequired, + isFetching: PropTypes.bool.isRequired, + error: PropTypes.object, + items: PropTypes.arrayOf(PropTypes.object).isRequired, + isDeleting: PropTypes.bool.isRequired, + onConfirmDeleteLanguageProfile: PropTypes.func.isRequired +}; + +export default LanguageProfiles; diff --git a/frontend/src/Settings/Profiles/Language/LanguageProfilesConnector.js b/frontend/src/Settings/Profiles/Language/LanguageProfilesConnector.js new file mode 100644 index 000000000..2dc8967eb --- /dev/null +++ b/frontend/src/Settings/Profiles/Language/LanguageProfilesConnector.js @@ -0,0 +1,60 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import { fetchLanguageProfiles, deleteLanguageProfile } from 'Store/Actions/settingsActions'; +import LanguageProfiles from './LanguageProfiles'; + +function createMapStateToProps() { + return createSelector( + (state) => state.settings.advancedSettings, + (state) => state.settings.languageProfiles, + (advancedSettings, languageProfiles) => { + return { + advancedSettings, + ...languageProfiles + }; + } + ); +} + +const mapDispatchToProps = { + fetchLanguageProfiles, + deleteLanguageProfile +}; + +class LanguageProfilesConnector extends Component { + + // + // Lifecycle + + componentDidMount() { + this.props.fetchLanguageProfiles(); + } + + // + // Listeners + + onConfirmDeleteLanguageProfile = (id) => { + this.props.deleteLanguageProfile({ id }); + } + + // + // Render + + render() { + return ( + + ); + } +} + +LanguageProfilesConnector.propTypes = { + fetchLanguageProfiles: PropTypes.func.isRequired, + deleteLanguageProfile: PropTypes.func.isRequired +}; + +export default connect(createMapStateToProps, mapDispatchToProps)(LanguageProfilesConnector); diff --git a/frontend/src/Settings/Profiles/Profiles.js b/frontend/src/Settings/Profiles/Profiles.js new file mode 100644 index 000000000..ed288ca9b --- /dev/null +++ b/frontend/src/Settings/Profiles/Profiles.js @@ -0,0 +1,36 @@ +import React, { Component } from 'react'; +import { DragDropContext } from 'react-dnd'; +import HTML5Backend from 'react-dnd-html5-backend'; +import PageContent from 'Components/Page/PageContent'; +import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector'; +import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector'; +import QualityProfilesConnector from './Quality/QualityProfilesConnector'; +import LanguageProfilesConnector from './Language/LanguageProfilesConnector'; +import DelayProfilesConnector from './Delay/DelayProfilesConnector'; + +class Profiles extends Component { + + // + // Render + + render() { + return ( + + + + + + + + + + ); + } +} + +// Only a single DragDropContext can exist so it's done here to allow editing +// quality profiles and reordering delay profiles to work. + +export default DragDropContext(HTML5Backend)(Profiles); diff --git a/frontend/src/Settings/Profiles/Quality/EditQualityProfileModal.js b/frontend/src/Settings/Profiles/Quality/EditQualityProfileModal.js new file mode 100644 index 000000000..13539e302 --- /dev/null +++ b/frontend/src/Settings/Profiles/Quality/EditQualityProfileModal.js @@ -0,0 +1,25 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import Modal from 'Components/Modal/Modal'; +import EditQualityProfileModalContentConnector from './EditQualityProfileModalContentConnector'; + +function EditQualityProfileModal({ isOpen, onModalClose, ...otherProps }) { + return ( + + + + ); +} + +EditQualityProfileModal.propTypes = { + isOpen: PropTypes.bool.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default EditQualityProfileModal; diff --git a/frontend/src/Settings/Profiles/Quality/EditQualityProfileModalConnector.js b/frontend/src/Settings/Profiles/Quality/EditQualityProfileModalConnector.js new file mode 100644 index 000000000..5ec77950f --- /dev/null +++ b/frontend/src/Settings/Profiles/Quality/EditQualityProfileModalConnector.js @@ -0,0 +1,43 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { clearPendingChanges } from 'Store/Actions/baseActions'; +import EditQualityProfileModal from './EditQualityProfileModal'; + +function mapStateToProps() { + return {}; +} + +const mapDispatchToProps = { + clearPendingChanges +}; + +class EditQualityProfileModalConnector extends Component { + + // + // Listeners + + onModalClose = () => { + this.props.clearPendingChanges({ section: 'qualityProfiles' }); + this.props.onModalClose(); + } + + // + // Render + + render() { + return ( + + ); + } +} + +EditQualityProfileModalConnector.propTypes = { + onModalClose: PropTypes.func.isRequired, + clearPendingChanges: PropTypes.func.isRequired +}; + +export default connect(mapStateToProps, mapDispatchToProps)(EditQualityProfileModalConnector); diff --git a/frontend/src/Settings/Profiles/Quality/EditQualityProfileModalContent.css b/frontend/src/Settings/Profiles/Quality/EditQualityProfileModalContent.css new file mode 100644 index 000000000..74dd1c8b7 --- /dev/null +++ b/frontend/src/Settings/Profiles/Quality/EditQualityProfileModalContent.css @@ -0,0 +1,3 @@ +.deleteButtonContainer { + margin-right: auto; +} diff --git a/frontend/src/Settings/Profiles/Quality/EditQualityProfileModalContent.js b/frontend/src/Settings/Profiles/Quality/EditQualityProfileModalContent.js new file mode 100644 index 000000000..21c504e4f --- /dev/null +++ b/frontend/src/Settings/Profiles/Quality/EditQualityProfileModalContent.js @@ -0,0 +1,149 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import { inputTypes, kinds } from 'Helpers/Props'; +import Button from 'Components/Link/Button'; +import SpinnerErrorButton from 'Components/Link/SpinnerErrorButton'; +import LoadingIndicator from 'Components/Loading/LoadingIndicator'; +import ModalContent from 'Components/Modal/ModalContent'; +import ModalHeader from 'Components/Modal/ModalHeader'; +import ModalBody from 'Components/Modal/ModalBody'; +import ModalFooter from 'Components/Modal/ModalFooter'; +import Form from 'Components/Form/Form'; +import FormGroup from 'Components/Form/FormGroup'; +import FormLabel from 'Components/Form/FormLabel'; +import FormInputGroup from 'Components/Form/FormInputGroup'; +import QualityProfileItems from './QualityProfileItems'; +import styles from './EditQualityProfileModalContent.css'; + +function EditQualityProfileModalContent(props) { + const { + isFetching, + error, + isSaving, + saveError, + qualities, + item, + isInUse, + onInputChange, + onCutoffChange, + onSavePress, + onModalClose, + onDeleteQualityProfilePress, + ...otherProps + } = props; + + const { + id, + name, + cutoff, + items + } = item; + + return ( + + + {id ? 'Edit Quality Profile' : 'Add Quality Profile'} + + + + { + isFetching && + + } + + { + !isFetching && !!error && +
Unable to add a new quality profile, please try again.
+ } + + { + !isFetching && !error && +
+ + Name + + + + + + Cutoff + + + + + + + + } +
+ + { + id && +
+ +
+ } + + + + + Save + +
+
+ ); +} + +EditQualityProfileModalContent.propTypes = { + isFetching: PropTypes.bool.isRequired, + error: PropTypes.object, + isSaving: PropTypes.bool.isRequired, + saveError: PropTypes.object, + qualities: PropTypes.arrayOf(PropTypes.object).isRequired, + item: PropTypes.object.isRequired, + isInUse: PropTypes.bool.isRequired, + onInputChange: PropTypes.func.isRequired, + onCutoffChange: PropTypes.func.isRequired, + onSavePress: PropTypes.func.isRequired, + onModalClose: PropTypes.func.isRequired, + onDeleteQualityProfilePress: PropTypes.func +}; + +export default EditQualityProfileModalContent; diff --git a/frontend/src/Settings/Profiles/Quality/EditQualityProfileModalContentConnector.js b/frontend/src/Settings/Profiles/Quality/EditQualityProfileModalContentConnector.js new file mode 100644 index 000000000..9de5080ca --- /dev/null +++ b/frontend/src/Settings/Profiles/Quality/EditQualityProfileModalContentConnector.js @@ -0,0 +1,194 @@ +import _ from 'lodash'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { createSelector } from 'reselect'; +import createProfileInUseSelector from 'Store/Selectors/createProfileInUseSelector'; +import createProviderSettingsSelector from 'Store/Selectors/createProviderSettingsSelector'; +import { fetchQualityProfileSchema, setQualityProfileValue, saveQualityProfile } from 'Store/Actions/settingsActions'; +import connectSection from 'Store/connectSection'; +import EditQualityProfileModalContent from './EditQualityProfileModalContent'; + +function createQualitiesSelector() { + return createSelector( + createProviderSettingsSelector(), + (qualityProfile) => { + const items = qualityProfile.item.items; + if (!items || !items.value) { + return []; + } + + return _.reduceRight(items.value, (result, { allowed, quality }) => { + if (allowed) { + result.push({ + key: quality.id, + value: quality.name + }); + } + + return result; + }, []); + } + ); +} + +function createMapStateToProps() { + return createSelector( + createProviderSettingsSelector(), + createQualitiesSelector(), + createProfileInUseSelector('qualityProfileId'), + (qualityProfile, qualities, isInUse) => { + return { + qualities, + ...qualityProfile, + isInUse + }; + } + ); +} + +const mapDispatchToProps = { + fetchQualityProfileSchema, + setQualityProfileValue, + saveQualityProfile +}; + +class EditQualityProfileModalContentConnector extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + dragIndex: null, + dropIndex: null + }; + } + + componentDidMount() { + if (!this.props.id) { + this.props.fetchQualityProfileSchema(); + } + } + + componentDidUpdate(prevProps, prevState) { + if (prevProps.isSaving && !this.props.isSaving && !this.props.saveError) { + this.props.onModalClose(); + } + } + + // + // Listeners + + onInputChange = ({ name, value }) => { + this.props.setQualityProfileValue({ name, value }); + } + + onCutoffChange = ({ name, value }) => { + const id = parseInt(value); + const item = _.find(this.props.item.items.value, (i) => i.quality.id === id); + + this.props.setQualityProfileValue({ name, value: item.quality }); + } + + onSavePress = () => { + this.props.saveQualityProfile({ id: this.props.id }); + } + + onQualityProfileItemAllowedChange = (id, allowed) => { + const qualityProfile = _.cloneDeep(this.props.item); + + const item = _.find(qualityProfile.items.value, (i) => i.quality.id === id); + item.allowed = allowed; + + this.props.setQualityProfileValue({ + name: 'items', + value: qualityProfile.items.value + }); + + const cutoff = qualityProfile.cutoff.value; + + // If the cutoff isn't allowed anymore or there isn't a cutoff set one + if (!cutoff || !_.find(qualityProfile.items.value, (i) => i.quality.id === cutoff.id).allowed) { + const firstAllowed = _.find(qualityProfile.items.value, { allowed: true }); + + this.props.setQualityProfileValue({ name: 'cutoff', value: firstAllowed ? firstAllowed.quality : null }); + } + } + + onQualityProfileItemDragMove = (dragIndex, dropIndex) => { + if (this.state.dragIndex !== dragIndex || this.state.dropIndex !== dropIndex) { + this.setState({ + dragIndex, + dropIndex + }); + } + } + + onQualityProfileItemDragEnd = ({ id }, didDrop) => { + const { + dragIndex, + dropIndex + } = this.state; + + if (didDrop && dropIndex !== null) { + const qualityProfile = _.cloneDeep(this.props.item); + + const items = qualityProfile.items.value.splice(dragIndex, 1); + qualityProfile.items.value.splice(dropIndex, 0, items[0]); + + this.props.setQualityProfileValue({ + name: 'items', + value: qualityProfile.items.value + }); + } + + this.setState({ + dragIndex: null, + dropIndex: null + }); + } + + // + // Render + + render() { + if (_.isEmpty(this.props.item.items) && !this.props.isFetching) { + return null; + } + + return ( + + ); + } +} + +EditQualityProfileModalContentConnector.propTypes = { + id: PropTypes.number, + isFetching: PropTypes.bool.isRequired, + isSaving: PropTypes.bool.isRequired, + saveError: PropTypes.object, + item: PropTypes.object.isRequired, + setQualityProfileValue: PropTypes.func.isRequired, + fetchQualityProfileSchema: PropTypes.func.isRequired, + saveQualityProfile: PropTypes.func.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default connectSection( + createMapStateToProps, + mapDispatchToProps, + undefined, + undefined, + { section: 'qualityProfiles' } +)(EditQualityProfileModalContentConnector); diff --git a/frontend/src/Settings/Profiles/Quality/QualityProfile.css b/frontend/src/Settings/Profiles/Quality/QualityProfile.css new file mode 100644 index 000000000..1ac9041bd --- /dev/null +++ b/frontend/src/Settings/Profiles/Quality/QualityProfile.css @@ -0,0 +1,19 @@ +.qualityProfile { + composes: card from 'Components/Card.css'; + + width: 300px; +} + +.name { + @add-mixin truncate; + + margin-bottom: 20px; + font-weight: 300; + font-size: 24px; +} + +.qualities { + display: flex; + flex-wrap: wrap; + margin-top: 5px; +} diff --git a/frontend/src/Settings/Profiles/Quality/QualityProfile.js b/frontend/src/Settings/Profiles/Quality/QualityProfile.js new file mode 100644 index 000000000..211656ab1 --- /dev/null +++ b/frontend/src/Settings/Profiles/Quality/QualityProfile.js @@ -0,0 +1,124 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { kinds } from 'Helpers/Props'; +import Card from 'Components/Card'; +import Label from 'Components/Label'; +import ConfirmModal from 'Components/Modal/ConfirmModal'; +import EditQualityProfileModalConnector from './EditQualityProfileModalConnector'; +import styles from './QualityProfile.css'; + +class QualityProfile extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + isEditQualityProfileModalOpen: false, + isDeleteQualityProfileModalOpen: false + }; + } + + // + // Listeners + + onEditQualityProfilePress = () => { + this.setState({ isEditQualityProfileModalOpen: true }); + } + + onEditQualityProfileModalClose = () => { + this.setState({ isEditQualityProfileModalOpen: false }); + } + + onDeleteQualityProfilePress = () => { + this.setState({ + isEditQualityProfileModalOpen: false, + isDeleteQualityProfileModalOpen: true + }); + } + + onDeleteQualityProfileModalClose = () => { + this.setState({ isDeleteQualityProfileModalOpen: false }); + } + + onConfirmDeleteQualityProfile = () => { + this.props.onConfirmDeleteQualityProfile(this.props.id); + } + + // + // Render + + render() { + const { + id, + name, + cutoff, + items, + isDeleting + } = this.props; + + return ( + +
+ {name} +
+ +
+ { + items.map((item) => { + if (!item.allowed) { + return null; + } + + const isCutoff = item.quality.id === cutoff.id; + + return ( + + ); + }) + } +
+ + + + +
+ ); + } +} + +QualityProfile.propTypes = { + id: PropTypes.number.isRequired, + name: PropTypes.string.isRequired, + cutoff: PropTypes.object.isRequired, + items: PropTypes.arrayOf(PropTypes.object).isRequired, + isDeleting: PropTypes.bool.isRequired, + onConfirmDeleteQualityProfile: PropTypes.func.isRequired +}; + +export default QualityProfile; diff --git a/frontend/src/Settings/Profiles/Quality/QualityProfileItem.css b/frontend/src/Settings/Profiles/Quality/QualityProfileItem.css new file mode 100644 index 000000000..90d48a2c5 --- /dev/null +++ b/frontend/src/Settings/Profiles/Quality/QualityProfileItem.css @@ -0,0 +1,44 @@ +.qualityProfileItem { + display: flex; + align-items: stretch; + width: 100%; + border: 1px solid #aaa; + border-radius: 4px; + background: #fafafa; +} + +.checkContainer { + position: relative; + margin-right: 4px; + margin-bottom: 7px; + margin-left: 8px; +} + +.qualityName { + display: flex; + flex-grow: 1; + margin-bottom: 0; + margin-left: 2px; + font-weight: normal; + line-height: 36px; + cursor: pointer; +} + +.dragHandle { + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + margin-left: auto; + width: $dragHandleWidth; + text-align: center; + cursor: grab; +} + +.dragIcon { + top: 0; +} + +.isDragging { + opacity: 0.25; +} diff --git a/frontend/src/Settings/Profiles/Quality/QualityProfileItem.js b/frontend/src/Settings/Profiles/Quality/QualityProfileItem.js new file mode 100644 index 000000000..1a0fa0114 --- /dev/null +++ b/frontend/src/Settings/Profiles/Quality/QualityProfileItem.js @@ -0,0 +1,83 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import classNames from 'classnames'; +import { icons } from 'Helpers/Props'; +import Icon from 'Components/Icon'; +import CheckInput from 'Components/Form/CheckInput'; +import styles from './QualityProfileItem.css'; + +class QualityProfileItem extends Component { + + // + // Listeners + + onAllowedChange = ({ value }) => { + const { + qualityId, + onQualityProfileItemAllowedChange + } = this.props; + + onQualityProfileItemAllowedChange(qualityId, value); + } + + // + // Render + + render() { + const { + name, + allowed, + isDragging, + connectDragSource + } = this.props; + + return ( +
+ + + { + connectDragSource( +
+ +
+ ) + } +
+ ); + } +} + +QualityProfileItem.propTypes = { + qualityId: PropTypes.number.isRequired, + name: PropTypes.string.isRequired, + allowed: PropTypes.bool.isRequired, + sortIndex: PropTypes.number.isRequired, + isDragging: PropTypes.bool.isRequired, + connectDragSource: PropTypes.func, + onQualityProfileItemAllowedChange: PropTypes.func +}; + +QualityProfileItem.defaultProps = { + // The drag preview will not connect the drag handle. + connectDragSource: (node) => node +}; + +export default QualityProfileItem; diff --git a/frontend/src/Settings/Profiles/Quality/QualityProfileItemDragPreview.css b/frontend/src/Settings/Profiles/Quality/QualityProfileItemDragPreview.css new file mode 100644 index 000000000..b927d9bce --- /dev/null +++ b/frontend/src/Settings/Profiles/Quality/QualityProfileItemDragPreview.css @@ -0,0 +1,4 @@ +.dragPreview { + width: 380px; + opacity: 0.75; +} diff --git a/frontend/src/Settings/Profiles/Quality/QualityProfileItemDragPreview.js b/frontend/src/Settings/Profiles/Quality/QualityProfileItemDragPreview.js new file mode 100644 index 000000000..1fd249714 --- /dev/null +++ b/frontend/src/Settings/Profiles/Quality/QualityProfileItemDragPreview.js @@ -0,0 +1,88 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { DragLayer } from 'react-dnd'; +import dimensions from 'Styles/Variables/dimensions.js'; +import { QUALITY_PROFILE_ITEM } from 'Helpers/dragTypes'; +import DragPreviewLayer from 'Components/DragPreviewLayer'; +import QualityProfileItem from './QualityProfileItem'; +import styles from './QualityProfileItemDragPreview.css'; + +const formGroupSmallWidth = parseInt(dimensions.formGroupSmallWidth); +const formLabelWidth = parseInt(dimensions.formLabelWidth); +const formLabelRightMarginWidth = parseInt(dimensions.formLabelRightMarginWidth); +const dragHandleWidth = parseInt(dimensions.dragHandleWidth); + +function collectDragLayer(monitor) { + return { + item: monitor.getItem(), + itemType: monitor.getItemType(), + currentOffset: monitor.getSourceClientOffset() + }; +} + +class QualityProfileItemDragPreview extends Component { + + // + // Render + + render() { + const { + item, + itemType, + currentOffset + } = this.props; + + if (!currentOffset || itemType !== QUALITY_PROFILE_ITEM) { + return null; + } + + // The offset is shifted because the drag handle is on the right edge of the + // list item and the preview is wider than the drag handle. + + const { x, y } = currentOffset; + const handleOffset = formGroupSmallWidth - formLabelWidth - formLabelRightMarginWidth - dragHandleWidth; + const transform = `translate3d(${x - handleOffset}px, ${y}px, 0)`; + + const style = { + position: 'absolute', + WebkitTransform: transform, + msTransform: transform, + transform + }; + + const { + qualityId, + name, + allowed, + sortIndex + } = item; + + return ( + +
+ +
+
+ ); + } +} + +QualityProfileItemDragPreview.propTypes = { + item: PropTypes.object, + itemType: PropTypes.string, + currentOffset: PropTypes.shape({ + x: PropTypes.number.isRequired, + y: PropTypes.number.isRequired + }) +}; + +export default DragLayer(collectDragLayer)(QualityProfileItemDragPreview); diff --git a/frontend/src/Settings/Profiles/Quality/QualityProfileItemDragSource.css b/frontend/src/Settings/Profiles/Quality/QualityProfileItemDragSource.css new file mode 100644 index 000000000..5b9f36fe9 --- /dev/null +++ b/frontend/src/Settings/Profiles/Quality/QualityProfileItemDragSource.css @@ -0,0 +1,18 @@ +.qualityProfileItemDragSource { + padding: 4px 0; +} + +.qualityProfileItemPlaceholder { + width: 100%; + height: 36px; + border: 1px dotted #aaa; + border-radius: 4px; +} + +.qualityProfileItemPlaceholderBefore { + margin-bottom: 8px; +} + +.qualityProfileItemPlaceholderAfter { + margin-top: 8px; +} diff --git a/frontend/src/Settings/Profiles/Quality/QualityProfileItemDragSource.js b/frontend/src/Settings/Profiles/Quality/QualityProfileItemDragSource.js new file mode 100644 index 000000000..ed8adc107 --- /dev/null +++ b/frontend/src/Settings/Profiles/Quality/QualityProfileItemDragSource.js @@ -0,0 +1,157 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { findDOMNode } from 'react-dom'; +import { DragSource, DropTarget } from 'react-dnd'; +import classNames from 'classnames'; +import { QUALITY_PROFILE_ITEM } from 'Helpers/dragTypes'; +import QualityProfileItem from './QualityProfileItem'; +import styles from './QualityProfileItemDragSource.css'; + +const qualityProfileItemDragSource = { + beginDrag({ qualityId, name, allowed, sortIndex }) { + return { + qualityId, + name, + allowed, + sortIndex + }; + }, + + endDrag(props, monitor, component) { + props.onQualityProfileItemDragEnd(monitor.getItem(), monitor.didDrop()); + } +}; + +const qualityProfileItemDropTarget = { + hover(props, monitor, component) { + const dragIndex = monitor.getItem().sortIndex; + const hoverIndex = props.sortIndex; + + const hoverBoundingRect = findDOMNode(component).getBoundingClientRect(); + const hoverMiddleY = (hoverBoundingRect.bottom - hoverBoundingRect.top) / 2; + const clientOffset = monitor.getClientOffset(); + const hoverClientY = clientOffset.y - hoverBoundingRect.top; + + // Moving up, only trigger if drag position is above 50% + if (dragIndex < hoverIndex && hoverClientY > hoverMiddleY) { + return; + } + + // Moving down, only trigger if drag position is below 50% + if (dragIndex > hoverIndex && hoverClientY < hoverMiddleY) { + return; + } + + props.onQualityProfileItemDragMove(dragIndex, hoverIndex); + } +}; + +function collectDragSource(connect, monitor) { + return { + connectDragSource: connect.dragSource(), + isDragging: monitor.isDragging() + }; +} + +function collectDropTarget(connect, monitor) { + return { + connectDropTarget: connect.dropTarget(), + isOver: monitor.isOver() + }; +} + +class QualityProfileItemDragSource extends Component { + + // + // Render + + render() { + const { + qualityId, + name, + allowed, + sortIndex, + isDragging, + isDraggingUp, + isDraggingDown, + isOver, + connectDragSource, + connectDropTarget, + onQualityProfileItemAllowedChange + } = this.props; + + const isBefore = !isDragging && isDraggingUp && isOver; + const isAfter = !isDragging && isDraggingDown && isOver; + + // if (isDragging && !isOver) { + // return null; + // } + + return connectDropTarget( +
+ { + isBefore && +
+ } + + + + { + isAfter && +
+ } +
+ ); + } +} + +QualityProfileItemDragSource.propTypes = { + qualityId: PropTypes.number.isRequired, + name: PropTypes.string.isRequired, + allowed: PropTypes.bool.isRequired, + sortIndex: PropTypes.number.isRequired, + isDragging: PropTypes.bool, + isDraggingUp: PropTypes.bool, + isDraggingDown: PropTypes.bool, + isOver: PropTypes.bool, + connectDragSource: PropTypes.func, + connectDropTarget: PropTypes.func, + onQualityProfileItemAllowedChange: PropTypes.func.isRequired, + onQualityProfileItemDragMove: PropTypes.func.isRequired, + onQualityProfileItemDragEnd: PropTypes.func.isRequired +}; + +export default DropTarget( + QUALITY_PROFILE_ITEM, + qualityProfileItemDropTarget, + collectDropTarget +)(DragSource( + QUALITY_PROFILE_ITEM, + qualityProfileItemDragSource, + collectDragSource +)(QualityProfileItemDragSource)); diff --git a/frontend/src/Settings/Profiles/Quality/QualityProfileItems.css b/frontend/src/Settings/Profiles/Quality/QualityProfileItems.css new file mode 100644 index 000000000..344df5b08 --- /dev/null +++ b/frontend/src/Settings/Profiles/Quality/QualityProfileItems.css @@ -0,0 +1,6 @@ +.qualities { + margin-top: 10px; + /* TODO: This should consider the number of qualities in the list */ + min-height: 550px; + user-select: none; +} diff --git a/frontend/src/Settings/Profiles/Quality/QualityProfileItems.js b/frontend/src/Settings/Profiles/Quality/QualityProfileItems.js new file mode 100644 index 000000000..5a58da630 --- /dev/null +++ b/frontend/src/Settings/Profiles/Quality/QualityProfileItems.js @@ -0,0 +1,103 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import FormGroup from 'Components/Form/FormGroup'; +import FormLabel from 'Components/Form/FormLabel'; +import FormInputHelpText from 'Components/Form/FormInputHelpText'; +import QualityProfileItemDragSource from './QualityProfileItemDragSource'; +import QualityProfileItemDragPreview from './QualityProfileItemDragPreview'; +import styles from './QualityProfileItems.css'; + +class QualityProfileItems extends Component { + + // + // Render + + render() { + const { + dragIndex, + dropIndex, + qualityProfileItems, + errors, + warnings, + ...otherProps + } = this.props; + + const isDragging = dropIndex !== null; + const isDraggingUp = isDragging && dropIndex > dragIndex; + const isDraggingDown = isDragging && dropIndex < dragIndex; + + return ( + + Qualities +
+ + + { + errors.map((error, index) => { + return ( + + ); + }) + } + + { + warnings.map((warning, index) => { + return ( + + ); + }) + } + +
+ { + qualityProfileItems.map(({ allowed, quality }, index) => { + return ( + + ); + }).reverse() + } + + +
+
+
+ ); + } +} + +QualityProfileItems.propTypes = { + dragIndex: PropTypes.number, + dropIndex: PropTypes.number, + qualityProfileItems: PropTypes.arrayOf(PropTypes.object).isRequired, + errors: PropTypes.arrayOf(PropTypes.object), + warnings: PropTypes.arrayOf(PropTypes.object) +}; + +QualityProfileItems.defaultProps = { + errors: [], + warnings: [] +}; + +export default QualityProfileItems; diff --git a/frontend/src/Settings/Profiles/Quality/QualityProfileNameConnector.js b/frontend/src/Settings/Profiles/Quality/QualityProfileNameConnector.js new file mode 100644 index 000000000..bf13815ff --- /dev/null +++ b/frontend/src/Settings/Profiles/Quality/QualityProfileNameConnector.js @@ -0,0 +1,31 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import createQualityProfileSelector from 'Store/Selectors/createQualityProfileSelector'; + +function createMapStateToProps() { + return createSelector( + createQualityProfileSelector(), + (qualityProfile) => { + return { + name: qualityProfile.name + }; + } + ); +} + +function QualityProfileNameConnector({ name, ...otherProps }) { + return ( + + {name} + + ); +} + +QualityProfileNameConnector.propTypes = { + qualityProfileId: PropTypes.number.isRequired, + name: PropTypes.string.isRequired +}; + +export default connect(createMapStateToProps)(QualityProfileNameConnector); diff --git a/frontend/src/Settings/Profiles/Quality/QualityProfiles.css b/frontend/src/Settings/Profiles/Quality/QualityProfiles.css new file mode 100644 index 000000000..9644a7c2d --- /dev/null +++ b/frontend/src/Settings/Profiles/Quality/QualityProfiles.css @@ -0,0 +1,21 @@ +.qualityProfiles { + display: flex; + flex-wrap: wrap; +} + +.addQualityProfile { + composes: qualityProfile from './QualityProfile.css'; + + background-color: $cardAlternateBackgroundColor; + color: $gray; + text-align: center; + font-size: 45px; +} + +.center { + display: inline-block; + padding: 5px 20px 0; + border: 1px solid $borderColor; + border-radius: 4px; + background-color: $white; +} diff --git a/frontend/src/Settings/Profiles/Quality/QualityProfiles.js b/frontend/src/Settings/Profiles/Quality/QualityProfiles.js new file mode 100644 index 000000000..75f049695 --- /dev/null +++ b/frontend/src/Settings/Profiles/Quality/QualityProfiles.js @@ -0,0 +1,102 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import sortByName from 'Utilities/Array/sortByName'; +import { icons } from 'Helpers/Props'; +import FieldSet from 'Components/FieldSet'; +import Card from 'Components/Card'; +import Icon from 'Components/Icon'; +import PageSectionContent from 'Components/Page/PageSectionContent'; +import QualityProfile from './QualityProfile'; +import EditQualityProfileModalConnector from './EditQualityProfileModalConnector'; +import styles from './QualityProfiles.css'; + +class QualityProfiles extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + isQualityProfileModalOpen: false + }; + } + + // + // Listeners + + onEditQualityProfilePress = () => { + this.setState({ isQualityProfileModalOpen: true }); + } + + onModalClose = () => { + this.setState({ isQualityProfileModalOpen: false }); + } + + // + // Render + + render() { + const { + items, + isDeleting, + onConfirmDeleteQualityProfile, + ...otherProps + } = this.props; + + return ( +
+ +
+ { + items.sort(sortByName).map((item) => { + return ( + + ); + }) + } + + +
+ +
+
+
+ + +
+
+ ); + } +} + +QualityProfiles.propTypes = { + advancedSettings: PropTypes.bool.isRequired, + isFetching: PropTypes.bool.isRequired, + error: PropTypes.object, + isDeleting: PropTypes.bool.isRequired, + items: PropTypes.arrayOf(PropTypes.object).isRequired, + onConfirmDeleteQualityProfile: PropTypes.func.isRequired +}; + +export default QualityProfiles; diff --git a/frontend/src/Settings/Profiles/Quality/QualityProfilesConnector.js b/frontend/src/Settings/Profiles/Quality/QualityProfilesConnector.js new file mode 100644 index 000000000..4bb1529ee --- /dev/null +++ b/frontend/src/Settings/Profiles/Quality/QualityProfilesConnector.js @@ -0,0 +1,60 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import { fetchQualityProfiles, deleteQualityProfile } from 'Store/Actions/settingsActions'; +import QualityProfiles from './QualityProfiles'; + +function createMapStateToProps() { + return createSelector( + (state) => state.settings.advancedSettings, + (state) => state.settings.qualityProfiles, + (advancedSettings, qualityProfiles) => { + return { + advancedSettings, + ...qualityProfiles + }; + } + ); +} + +const mapDispatchToProps = { + fetchQualityProfiles, + deleteQualityProfile +}; + +class QualityProfilesConnector extends Component { + + // + // Lifecycle + + componentDidMount() { + this.props.fetchQualityProfiles(); + } + + // + // Listeners + + onConfirmDeleteQualityProfile = (id) => { + this.props.deleteQualityProfile({ id }); + } + + // + // Render + + render() { + return ( + + ); + } +} + +QualityProfilesConnector.propTypes = { + fetchQualityProfiles: PropTypes.func.isRequired, + deleteQualityProfile: PropTypes.func.isRequired +}; + +export default connect(createMapStateToProps, mapDispatchToProps)(QualityProfilesConnector); diff --git a/frontend/src/Settings/Quality/Definition/QualityDefinition.css b/frontend/src/Settings/Quality/Definition/QualityDefinition.css new file mode 100644 index 000000000..69e8018f5 --- /dev/null +++ b/frontend/src/Settings/Quality/Definition/QualityDefinition.css @@ -0,0 +1,93 @@ +.qualityDefinition { + display: flex; + align-content: stretch; + margin: 5px 0; + padding-top: 5px; + height: 45px; + border-top: 1px solid $borderColor; +} + +.quality, +.title { + flex: 0 1 250px; + padding-right: 20px; + line-height: 40px; +} + +.sizeLimit { + flex: 0 1 500px; + padding-right: 30px; +} + +.slider { + width: 100%; + height: 20px; +} + +.bar { + top: 9px; + margin: 0 5px; + height: 3px; + background-color: $sliderAccentColor; + box-shadow: 0 0 0 #000; + + &:nth-child(odd) { + background-color: #ddd; + } +} + +.handle { + top: 1px; + z-index: 0 !important; + width: 18px; + height: 18px; + border: 3px solid $sliderAccentColor; + border-radius: 50%; + background-color: $white; + text-align: center; + cursor: pointer; +} + +.sizes { + display: flex; + justify-content: space-between; +} + +.megabytesPerMinute { + display: flex; + justify-content: space-between; + flex: 0 0 250px; +} + +.sizeInput { + composes: text from 'Components/Form/TextInput.css'; + + display: inline-block; + margin-left: 5px; + padding: 6px; + width: 75px; +} + +@media only screen and (max-width: $breakpointSmall) { + .qualityDefinition { + flex-wrap: wrap; + height: auto; + + &:first-child { + border-top: none; + } + } + + .qualityDefinition:first-child { + border-top: none; + } + + .quality { + font-weight: bold; + line-height: inherit; + } + + .sizeLimit { + margin-top: 10px; + } +} diff --git a/frontend/src/Settings/Quality/Definition/QualityDefinition.js b/frontend/src/Settings/Quality/Definition/QualityDefinition.js new file mode 100644 index 000000000..b1c715d8c --- /dev/null +++ b/frontend/src/Settings/Quality/Definition/QualityDefinition.js @@ -0,0 +1,166 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import ReactSlider from 'react-slider'; +import formatBytes from 'Utilities/Number/formatBytes'; +import { kinds } from 'Helpers/Props'; +import Label from 'Components/Label'; +import NumberInput from 'Components/Form/NumberInput'; +import TextInput from 'Components/Form/TextInput'; +import styles from './QualityDefinition.css'; + +const slider = { + min: 0, + max: 200, + step: 0.1 +}; + +function getValue(value) { + if (value < slider.min) { + return slider.min; + } + + if (value > slider.max) { + return slider.max; + } + + return value; +} + +class QualityDefinition extends Component { + + // + // Listeners + + onSizeChange = ([minSize, maxSize]) => { + maxSize = maxSize === slider.max ? null : maxSize; + + this.props.onSizeChange({ minSize, maxSize }); + } + + onMinSizeChange = ({ value }) => { + const minSize = getValue(value); + + this.props.onSizeChange({ + minSize, + maxSize: this.props.maxSize + }); + } + + onMaxSizeChange = ({ value }) => { + const maxSize = value === slider.max ? null : getValue(value); + + this.props.onSizeChange({ + minSize: this.props.minSize, + maxSize + }); + } + + // + // Render + + render() { + const { + id, + quality, + title, + minSize, + maxSize, + advancedSettings, + onTitleChange + } = this.props; + + const minBytes = minSize * 1024 * 1024; + const minThirty = formatBytes(minBytes * 30, 2); + const minSixty = formatBytes(minBytes * 60, 2); + + const maxBytes = maxSize && maxSize * 1024 * 1024; + const maxThirty = maxBytes ? formatBytes(maxBytes * 30, 2) : 'Unlimited'; + const maxSixty = maxBytes ? formatBytes(maxBytes * 60, 2) : 'Unlimited'; + + return ( +
+
+ {quality.name} +
+ +
+ +
+ +
+ + +
+
+ + +
+ +
+ + +
+
+
+ + { + advancedSettings && +
+
+ Min + + +
+ +
+ Max + + +
+
+ } +
+ ); + } +} + +QualityDefinition.propTypes = { + id: PropTypes.number.isRequired, + quality: PropTypes.object.isRequired, + title: PropTypes.string.isRequired, + minSize: PropTypes.number, + maxSize: PropTypes.number, + advancedSettings: PropTypes.bool.isRequired, + onTitleChange: PropTypes.func.isRequired, + onSizeChange: PropTypes.func.isRequired +}; + +export default QualityDefinition; diff --git a/frontend/src/Settings/Quality/Definition/QualityDefinitionConnector.js b/frontend/src/Settings/Quality/Definition/QualityDefinitionConnector.js new file mode 100644 index 000000000..be83cc069 --- /dev/null +++ b/frontend/src/Settings/Quality/Definition/QualityDefinitionConnector.js @@ -0,0 +1,70 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { setQualityDefinitionValue } from 'Store/Actions/settingsActions'; +import { clearPendingChanges } from 'Store/Actions/baseActions'; +import QualityDefinition from './QualityDefinition'; + +function mapStateToProps(state) { + return { + advancedSettings: state.settings.advancedSettings + }; +} + +const mapDispatchToProps = { + setQualityDefinitionValue, + clearPendingChanges +}; + +class QualityDefinitionConnector extends Component { + + componentWillUnmount() { + this.props.clearPendingChanges({ section: 'qualityDefinitions' }); + } + + // + // Listeners + + onTitleChange = ({ value }) => { + this.props.setQualityDefinitionValue({ id: this.props.id, name: 'title', value }); + } + + onSizeChange = ({ minSize, maxSize }) => { + const { + id, + minSize: currentMinSize, + maxSize: currentMaxSize + } = this.props; + + if (minSize !== currentMinSize) { + this.props.setQualityDefinitionValue({ id, name: 'minSize', value: minSize }); + } + + if (minSize !== currentMaxSize) { + this.props.setQualityDefinitionValue({ id, name: 'maxSize', value: maxSize }); + } + } + + // + // Render + + render() { + return ( + + ); + } +} + +QualityDefinitionConnector.propTypes = { + id: PropTypes.number.isRequired, + minSize: PropTypes.number, + maxSize: PropTypes.number, + setQualityDefinitionValue: PropTypes.func.isRequired, + clearPendingChanges: PropTypes.func.isRequired +}; + +export default connect(mapStateToProps, mapDispatchToProps)(QualityDefinitionConnector); diff --git a/frontend/src/Settings/Quality/Definition/QualityDefinitions.css b/frontend/src/Settings/Quality/Definition/QualityDefinitions.css new file mode 100644 index 000000000..689017684 --- /dev/null +++ b/frontend/src/Settings/Quality/Definition/QualityDefinitions.css @@ -0,0 +1,41 @@ +.header { + display: flex; + font-weight: bold; +} + +.quality, +.title { + flex: 0 1 250px; +} + +.sizeLimit { + flex: 0 1 500px; +} + +.megabytesPerMinute { + flex: 0 0 250px; +} + +.sizeLimitHelpTextContainer { + display: flex; + justify-content: flex-end; + margin-top: 20px; + max-width: 1000px; +} + +.sizeLimitHelpText { + max-width: 500px; + color: $helpTextColor; +} + +@media only screen and (max-width: $breakpointSmall) { + .header { + display: none; + } + + .definitions { + &:first-child { + border-top: none; + } + } +} diff --git a/frontend/src/Settings/Quality/Definition/QualityDefinitions.js b/frontend/src/Settings/Quality/Definition/QualityDefinitions.js new file mode 100644 index 000000000..dab22ea60 --- /dev/null +++ b/frontend/src/Settings/Quality/Definition/QualityDefinitions.js @@ -0,0 +1,65 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import FieldSet from 'Components/FieldSet'; +import PageSectionContent from 'Components/Page/PageSectionContent'; +import QualityDefinitionConnector from './QualityDefinitionConnector'; +import styles from './QualityDefinitions.css'; + +class QualityDefinitions extends Component { + + // + // Render + + render() { + const { + items, + ...otherProps + } = this.props; + + return ( +
+ +
+
Quality
+
Title
+
Size Limit
+
Megabytes Per Minute
+
+ +
+ { + items.map((item) => { + return ( + + ); + }) + } +
+ +
+
+ Limits are automatically adjusted for the album duration. +
+
+
+
+ ); + } +} + +QualityDefinitions.propTypes = { + isFetching: PropTypes.bool.isRequired, + error: PropTypes.object, + defaultProfile: PropTypes.object, + items: PropTypes.arrayOf(PropTypes.object).isRequired +}; + +export default QualityDefinitions; diff --git a/frontend/src/Settings/Quality/Definition/QualityDefinitionsConnector.js b/frontend/src/Settings/Quality/Definition/QualityDefinitionsConnector.js new file mode 100644 index 000000000..2170919be --- /dev/null +++ b/frontend/src/Settings/Quality/Definition/QualityDefinitionsConnector.js @@ -0,0 +1,78 @@ +import _ from 'lodash'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import { fetchQualityDefinitions, saveQualityDefinitions } from 'Store/Actions/settingsActions'; +import QualityDefinitions from './QualityDefinitions'; + +function createMapStateToProps() { + return createSelector( + (state) => state.settings.qualityDefinitions, + (qualityDefinitions) => { + const items = qualityDefinitions.items.map((item) => { + const pendingChanges = qualityDefinitions.pendingChanges[item.id] || {}; + + return Object.assign({}, item, pendingChanges); + }); + + return { + ...qualityDefinitions, + items, + hasPendingChanges: !_.isEmpty(qualityDefinitions.pendingChanges) + }; + } + ); +} + +const mapDispatchToProps = { + fetchQualityDefinitions, + saveQualityDefinitions +}; + +class QualityDefinitionsConnector extends Component { + + // + // Lifecycle + + componentDidMount() { + this.props.fetchQualityDefinitions(); + } + + componentDidUpdate(prevProps) { + const { + hasPendingChanges + } = this.props; + + if (hasPendingChanges !== prevProps.hasPendingChanges) { + this.props.onHasPendingChange(hasPendingChanges); + } + } + + // + // Control + + save = () => { + this.props.saveQualityDefinitions(); + } + + // + // Render + + render() { + return ( + + ); + } +} + +QualityDefinitionsConnector.propTypes = { + hasPendingChanges: PropTypes.bool.isRequired, + fetchQualityDefinitions: PropTypes.func.isRequired, + saveQualityDefinitions: PropTypes.func.isRequired, + onHasPendingChange: PropTypes.func.isRequired +}; + +export default connect(createMapStateToProps, mapDispatchToProps, null, { withRef: true })(QualityDefinitionsConnector); diff --git a/frontend/src/Settings/Quality/Quality.js b/frontend/src/Settings/Quality/Quality.js new file mode 100644 index 000000000..ed58dac67 --- /dev/null +++ b/frontend/src/Settings/Quality/Quality.js @@ -0,0 +1,59 @@ +import React, { Component } from 'react'; +import PageContent from 'Components/Page/PageContent'; +import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector'; +import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector'; +import QualityDefinitionsConnector from './Definition/QualityDefinitionsConnector'; + +class Quality extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + hasPendingChanges: false + }; + } + + // + // Listeners + + setQualityDefinitionsRef = (ref) => { + this._qualityDefinitions = ref; + } + + onHasPendingChange = (hasPendingChanges) => { + this.setState({ + hasPendingChanges + }); + } + + onSavePress = () => { + this._qualityDefinitions.getWrappedInstance().save(); + } + + // + // Render + + render() { + return ( + + + + + + + + ); + } +} + +export default Quality; diff --git a/frontend/src/Settings/Settings.css b/frontend/src/Settings/Settings.css new file mode 100644 index 000000000..497ef47c0 --- /dev/null +++ b/frontend/src/Settings/Settings.css @@ -0,0 +1,18 @@ +.link { + composes: link from 'Components/Link/Link.css'; + + border-bottom: 1px solid #e5e5e5; + color: #3a3f51; + font-size: 21px; + + &:hover { + color: #616573; + text-decoration: none; + } +} + +.summary { + margin-top: 10px; + margin-bottom: 30px; + color: $dimColor; +} diff --git a/frontend/src/Settings/Settings.js b/frontend/src/Settings/Settings.js new file mode 100644 index 000000000..c39830f63 --- /dev/null +++ b/frontend/src/Settings/Settings.js @@ -0,0 +1,122 @@ +import React from 'react'; +import Link from 'Components/Link/Link'; +import PageContent from 'Components/Page/PageContent'; +import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector'; +import SettingsToolbarConnector from './SettingsToolbarConnector'; +import styles from './Settings.css'; + +function Settings() { + return ( + + + + + + Media Management + + +
+ Naming and file management settings +
+ + + Profiles + + +
+ Quality, Language and Delay profiles +
+ + + Quality + + +
+ Quality sizes and naming +
+ + + Indexers + + +
+ Indexers and release restrictions +
+ + + Download Clients + + +
+ Download clients, download handling and remote path mappings +
+ + + Connect + + +
+ Notifications, connections to media servers/players and custom scripts +
+ + + Metadata + + +
+ Create metadata files when episodes are imported or artist are refreshed +
+ + + General + + +
+ Port, SSL, username/password, proxy, analytics and updates +
+ + + UI + + +
+ Calendar, date and color impaired options +
+
+
+ ); +} + +Settings.propTypes = { +}; + +export default Settings; diff --git a/frontend/src/Settings/SettingsToolbar.css b/frontend/src/Settings/SettingsToolbar.css new file mode 100644 index 000000000..2d3aa1c6f --- /dev/null +++ b/frontend/src/Settings/SettingsToolbar.css @@ -0,0 +1,7 @@ +.advancedSettings { + composes: toolbarButton from 'Components/Page/Toolbar/PageToolbarButton.css'; +} + +.advancedSettingsEnabled { + color: $toobarButtonHoverColor; +} diff --git a/frontend/src/Settings/SettingsToolbar.js b/frontend/src/Settings/SettingsToolbar.js new file mode 100644 index 000000000..0d758ce23 --- /dev/null +++ b/frontend/src/Settings/SettingsToolbar.js @@ -0,0 +1,104 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import classNames from 'classnames'; +import { icons } from 'Helpers/Props'; +import keyboardShortcuts, { shortcuts } from 'Components/keyboardShortcuts'; +import PageToolbar from 'Components/Page/Toolbar/PageToolbar'; +import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection'; +import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton'; +import PendingChangesModal from './PendingChangesModal'; +import styles from './SettingsToolbar.css'; + +class SettingsToolbar extends Component { + + // + // Lifecycle + + componentDidMount() { + this.props.bindShortcut(shortcuts.SAVE_SETTINGS.key, this.saveSettings, { isGlobal: true }); + } + + // + // Control + + saveSettings = (event) => { + event.preventDefault(); + + const { + hasPendingChanges, + onSavePress + } = this.props; + + if (hasPendingChanges) { + onSavePress(); + } + } + + // + // Render + + render() { + const { + advancedSettings, + showSave, + isSaving, + hasPendingChanges, + hasPendingLocation, + onSavePress, + onConfirmNavigation, + onCancelNavigation, + onAdvancedSettingsPress + } = this.props; + + return ( + + + + + { + showSave && + + } + + + + ); + } +} + +SettingsToolbar.propTypes = { + advancedSettings: PropTypes.bool.isRequired, + showSave: PropTypes.bool.isRequired, + isSaving: PropTypes.bool, + hasPendingLocation: PropTypes.bool.isRequired, + hasPendingChanges: PropTypes.bool, + onSavePress: PropTypes.func, + onAdvancedSettingsPress: PropTypes.func.isRequired, + onConfirmNavigation: PropTypes.func.isRequired, + onCancelNavigation: PropTypes.func.isRequired, + bindShortcut: PropTypes.func.isRequired +}; + +SettingsToolbar.defaultProps = { + showSave: true +}; + +export default keyboardShortcuts(SettingsToolbar); diff --git a/frontend/src/Settings/SettingsToolbarConnector.js b/frontend/src/Settings/SettingsToolbarConnector.js new file mode 100644 index 000000000..8bfb3dad5 --- /dev/null +++ b/frontend/src/Settings/SettingsToolbarConnector.js @@ -0,0 +1,147 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { withRouter } from 'react-router-dom'; +import { toggleAdvancedSettings } from 'Store/Actions/settingsActions'; +import SettingsToolbar from './SettingsToolbar'; + +function mapStateToProps(state) { + return { + advancedSettings: state.settings.advancedSettings + }; +} + +const mapDispatchToProps = { + toggleAdvancedSettings +}; + +class SettingsToolbarConnector extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + nextLocation: null, + nextLocationAction: null, + confirmed: false + }; + + this._unblock = null; + } + + componentDidMount() { + this._unblock = this.props.history.block(this.routerWillLeave); + } + + componentWillUnmount() { + if (this._unblock) { + this._unblock(); + } + } + + // + // Control + + routerWillLeave = (nextLocation, nextLocationAction) => { + if (this.state.confirmed) { + this.setState({ + nextLocation: null, + nextLocationAction: null, + confirmed: false + }); + + return true; + } + + if (this.props.hasPendingChanges ) { + this.setState({ + nextLocation, + nextLocationAction + }); + + return false; + } + + return true; + } + + // + // Listeners + + onAdvancedSettingsPress = () => { + this.props.toggleAdvancedSettings(); + } + + onConfirmNavigation = () => { + const { + nextLocation, + nextLocationAction + } = this.state; + + const history = this.props.history; + + const path = `${nextLocation.pathname}${nextLocation.search}`; + + this.setState({ + confirmed: true + }, () => { + if (nextLocationAction === 'PUSH') { + history.push(path); + } else { + // Unfortunately back and forward both use POP, + // which means we don't actually know which direction + // the user wanted to go, assuming back. + + history.goBack(); + } + }); + } + + onCancelNavigation = () => { + this.setState({ + nextLocation: null, + nextLocationAction: null, + confirmed: false + }); + } + + // + // Render + + render() { + const hasPendingLocation = this.state.nextLocation !== null; + + return ( + + ); + } +} + +const historyShape = { + block: PropTypes.func.isRequired, + goBack: PropTypes.func.isRequired, + push: PropTypes.func.isRequired +}; + +SettingsToolbarConnector.propTypes = { + hasPendingChanges: PropTypes.bool.isRequired, + history: PropTypes.shape(historyShape).isRequired, + onSavePress: PropTypes.func, + toggleAdvancedSettings: PropTypes.func.isRequired +}; + +SettingsToolbarConnector.defaultProps = { + hasPendingChanges: false +}; + +export default withRouter(connect(mapStateToProps, mapDispatchToProps)(SettingsToolbarConnector)); diff --git a/frontend/src/Settings/UI/UISettings.js b/frontend/src/Settings/UI/UISettings.js new file mode 100644 index 000000000..c443e4819 --- /dev/null +++ b/frontend/src/Settings/UI/UISettings.js @@ -0,0 +1,197 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { inputTypes } from 'Helpers/Props'; +import LoadingIndicator from 'Components/Loading/LoadingIndicator'; +import FieldSet from 'Components/FieldSet'; +import PageContent from 'Components/Page/PageContent'; +import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector'; +import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector'; +import Form from 'Components/Form/Form'; +import FormGroup from 'Components/Form/FormGroup'; +import FormLabel from 'Components/Form/FormLabel'; +import FormInputGroup from 'Components/Form/FormInputGroup'; + +class UISettings extends Component { + + // + // Render + + render() { + const { + isFetching, + error, + settings, + hasSettings, + onInputChange, + onSavePress, + ...otherProps + } = this.props; + + const firstDayOfWeekOptions = [ + { key: 0, value: 'Sunday' }, + { key: 1, value: 'Monday' } + ]; + + const weekColumnOptions = [ + { key: 'ddd M/D', value: 'Tue 3/25' }, + { key: 'ddd MM/DD', value: 'Tue 03/25' }, + { key: 'ddd D/M', value: 'Tue 25/3' }, + { key: 'ddd DD/MM', value: 'Tue 25/03' } + ]; + + const shortDateFormatOptions = [ + { key: 'MMM D YYYY', value: 'Mar 25 2014' }, + { key: 'DD MMM YYYY', value: '25 Mar 2014' }, + { key: 'MM/D/YYYY', value: '03/25/2014' }, + { key: 'MM/DD/YYYY', value: '03/25/2014' }, + { key: 'DD/MM/YYYY', value: '25/03/2014' }, + { key: 'YYYY-MM-DD', value: '2014-03-25' } + ]; + + const longDateFormatOptions = [ + { key: 'dddd, MMMM D YYYY', value: 'Tuesday, March 25, 2014' }, + { key: 'dddd, D MMMM YYYY', value: 'Tuesday, 25 March, 2014' } + ]; + + const timeFormatOptions = [ + { key: 'h(:mm)a', value: '5pm/5:30pm' }, + { key: 'HH:mm', value: '17:00/17:30' } + ]; + + return ( + + + + + { + isFetching && + + } + + { + !isFetching && error && +
Unable to load UI settings
+ } + + { + hasSettings && !isFetching && !error && +
+
+ + First Day of Week + + + + + + Week Column Header + + + +
+ +
+ + Short Date Format + + + + + + Long Date Format + + + + + + Time Format + + + + + + Show Relative Dates + + +
+ +
+ + Enable Color-Impaired mode + + +
+
+ } +
+
+ ); + } + +} + +UISettings.propTypes = { + isFetching: PropTypes.bool.isRequired, + error: PropTypes.object, + settings: PropTypes.object.isRequired, + hasSettings: PropTypes.bool.isRequired, + onSavePress: PropTypes.func.isRequired, + onInputChange: PropTypes.func.isRequired +}; + +export default UISettings; diff --git a/frontend/src/Settings/UI/UISettingsConnector.js b/frontend/src/Settings/UI/UISettingsConnector.js new file mode 100644 index 000000000..70c9bdd14 --- /dev/null +++ b/frontend/src/Settings/UI/UISettingsConnector.js @@ -0,0 +1,82 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { createSelector } from 'reselect'; +import createSettingsSectionSelector from 'Store/Selectors/createSettingsSectionSelector'; +import { setUISettingsValue, saveUISettings, fetchUISettings } from 'Store/Actions/settingsActions'; +import { clearPendingChanges } from 'Store/Actions/baseActions'; +import connectSection from 'Store/connectSection'; +import UISettings from './UISettings'; + +function createMapStateToProps() { + return createSelector( + (state) => state.settings.advancedSettings, + createSettingsSectionSelector(), + (advancedSettings, sectionSettings) => { + return { + advancedSettings, + ...sectionSettings + }; + } + ); +} + +const mapDispatchToProps = { + setUISettingsValue, + saveUISettings, + fetchUISettings, + clearPendingChanges +}; + +class UISettingsConnector extends Component { + + // + // Lifecycle + + componentDidMount() { + this.props.fetchUISettings(); + } + + componentWillUnmount() { + this.props.clearPendingChanges({ section: this.props.section }); + } + + // + // Listeners + + onInputChange = ({ name, value }) => { + this.props.setUISettingsValue({ name, value }); + } + + onSavePress = () => { + this.props.saveUISettings(); + } + + // + // Render + + render() { + return ( + + ); + } +} + +UISettingsConnector.propTypes = { + section: PropTypes.string.isRequired, + setUISettingsValue: PropTypes.func.isRequired, + saveUISettings: PropTypes.func.isRequired, + fetchUISettings: PropTypes.func.isRequired, + clearPendingChanges: PropTypes.func.isRequired +}; + +export default connectSection( + createMapStateToProps, + mapDispatchToProps, + undefined, + undefined, + { section: 'ui' } +)(UISettingsConnector); diff --git a/frontend/src/Shared/piwikCheck.js b/frontend/src/Shared/piwikCheck.js new file mode 100644 index 000000000..b9e18fdbb --- /dev/null +++ b/frontend/src/Shared/piwikCheck.js @@ -0,0 +1,10 @@ +if (window.Sonarr.analytics) { + const d = document; + const g = d.createElement('script'); + const s = d.getElementsByTagName('script')[0]; + g.type = 'text/javascript'; + g.async = true; + g.defer = true; + g.src = '//piwik.sonarr.tv/piwik.js'; + s.parentNode.insertBefore(g, s); +} diff --git a/frontend/src/Shims/jquery.js b/frontend/src/Shims/jquery.js new file mode 100644 index 000000000..d0234889c --- /dev/null +++ b/frontend/src/Shims/jquery.js @@ -0,0 +1,10 @@ +import $ from 'jquery'; +import ajax from 'jQuery/jquery.ajax'; + +ajax($); + +const jquery = $; +window.$ = $; +window.jQuery = $; + +export default jquery; diff --git a/frontend/src/Store/Actions/Creators/createBatchToggleAlbumMonitoredHandler.js b/frontend/src/Store/Actions/Creators/createBatchToggleAlbumMonitoredHandler.js new file mode 100644 index 000000000..6d93633a8 --- /dev/null +++ b/frontend/src/Store/Actions/Creators/createBatchToggleAlbumMonitoredHandler.js @@ -0,0 +1,41 @@ +import $ from 'jquery'; +import updateAlbums from 'Utilities/Album/updateAlbums'; + +function createBatchToggleAlbumMonitoredHandler(section, getFromState) { + return function(payload) { + return function(dispatch, getState) { + const { + albumIds, + monitored + } = payload; + + const state = getFromState(getState()); + + updateAlbums(dispatch, section, state.items, albumIds, { + isSaving: true + }); + + const promise = $.ajax({ + url: '/album/monitor', + method: 'PUT', + data: JSON.stringify({ albumIds, monitored }), + dataType: 'json' + }); + + promise.done(() => { + updateAlbums(dispatch, section, state.items, albumIds, { + isSaving: false, + monitored + }); + }); + + promise.fail(() => { + updateAlbums(dispatch, section, state.items, albumIds, { + isSaving: false + }); + }); + }; + }; +} + +export default createBatchToggleAlbumMonitoredHandler; diff --git a/frontend/src/Store/Actions/Creators/createFetchHandler.js b/frontend/src/Store/Actions/Creators/createFetchHandler.js new file mode 100644 index 000000000..5bf31b92e --- /dev/null +++ b/frontend/src/Store/Actions/Creators/createFetchHandler.js @@ -0,0 +1,46 @@ +import $ from 'jquery'; +import { batchActions } from 'redux-batched-actions'; +import { set, update, updateItem } from '../baseActions'; + +function createFetchHandler(section, url) { + return function(payload = {}) { + return function(dispatch, getState) { + dispatch(set({ section, isFetching: true })); + + const { + id, + ...otherPayload + } = payload; + + const promise = $.ajax({ + url: id == null ? url : `${url}/${id}`, + data: otherPayload, + traditional: true + }); + + promise.done((data) => { + dispatch(batchActions([ + id == null ? update({ section, data }) : updateItem({ section, ...data }), + + set({ + section, + isFetching: false, + isPopulated: true, + error: null + }) + ])); + }); + + promise.fail((xhr) => { + dispatch(set({ + section, + isFetching: false, + isPopulated: false, + error: xhr + })); + }); + }; + }; +} + +export default createFetchHandler; diff --git a/frontend/src/Store/Actions/Creators/createFetchSchemaHandler.js b/frontend/src/Store/Actions/Creators/createFetchSchemaHandler.js new file mode 100644 index 000000000..e58811ee4 --- /dev/null +++ b/frontend/src/Store/Actions/Creators/createFetchSchemaHandler.js @@ -0,0 +1,35 @@ +import $ from 'jquery'; +import { set } from '../baseActions'; + +function createFetchSchemaHandler(section, url) { + return function(payload) { + return function(dispatch, getState) { + dispatch(set({ section, isFetchingSchema: true })); + + const promise = $.ajax({ + url + }); + + promise.done((data) => { + dispatch(set({ + section, + isFetchingSchema: false, + schemaPopulated: true, + schemaError: null, + schema: data + })); + }); + + promise.fail((xhr) => { + dispatch(set({ + section, + isFetchingSchema: false, + schemaPopulated: true, + schemaError: xhr + })); + }); + }; + }; +} + +export default createFetchSchemaHandler; diff --git a/frontend/src/Store/Actions/Creators/createFetchServerSideCollectionHandler.js b/frontend/src/Store/Actions/Creators/createFetchServerSideCollectionHandler.js new file mode 100644 index 000000000..4f8d779a8 --- /dev/null +++ b/frontend/src/Store/Actions/Creators/createFetchServerSideCollectionHandler.js @@ -0,0 +1,54 @@ +import _ from 'lodash'; +import $ from 'jquery'; +import { batchActions } from 'redux-batched-actions'; +import { set, updateServerSideCollection } from '../baseActions'; + +function createFetchServerSideCollectionHandler(section, url, getFromState) { + return function(payload = {}) { + return function(dispatch, getState) { + dispatch(set({ section, isFetching: true })); + + const state = getFromState(getState()); + const sectionState = state.hasOwnProperty(section) ? state[section] : state; + const page = payload.page || sectionState.page || 1; + + const data = Object.assign({ page }, + _.pick(sectionState, [ + 'pageSize', + 'sortDirection', + 'sortKey', + 'filterKey', + 'filterValue' + ])); + + const promise = $.ajax({ + url, + data + }); + + promise.done((response) => { + dispatch(batchActions([ + updateServerSideCollection({ section, data: response }), + + set({ + section, + isFetching: false, + isPopulated: true, + error: null + }) + ])); + }); + + promise.fail((xhr) => { + dispatch(set({ + section, + isFetching: false, + isPopulated: false, + error: xhr + })); + }); + }; + }; +} + +export default createFetchServerSideCollectionHandler; diff --git a/frontend/src/Store/Actions/Creators/createRemoveItemHandler.js b/frontend/src/Store/Actions/Creators/createRemoveItemHandler.js new file mode 100644 index 000000000..f09a05948 --- /dev/null +++ b/frontend/src/Store/Actions/Creators/createRemoveItemHandler.js @@ -0,0 +1,47 @@ +import $ from 'jquery'; +import { batchActions } from 'redux-batched-actions'; +import { set, removeItem } from '../baseActions'; + +function createRemoveItemHandler(section, url) { + return function(payload) { + return function(dispatch, getState) { + const { + id, + ...queryParms + } = payload; + + dispatch(set({ section, isDeleting: true })); + + const ajaxOptions = { + url: `${url}/${id}?${$.param(queryParms, true)}`, + method: 'DELETE' + }; + + const promise = $.ajax(ajaxOptions); + + promise.done((data) => { + dispatch(batchActions([ + removeItem({ section, id }), + + set({ + section, + isDeleting: false, + deleteError: null + }) + ])); + }); + + promise.fail((xhr) => { + dispatch(set({ + section, + isDeleting: false, + deleteError: xhr + })); + }); + + return promise; + }; + }; +} + +export default createRemoveItemHandler; diff --git a/frontend/src/Store/Actions/Creators/createSaveHandler.js b/frontend/src/Store/Actions/Creators/createSaveHandler.js new file mode 100644 index 000000000..76048192e --- /dev/null +++ b/frontend/src/Store/Actions/Creators/createSaveHandler.js @@ -0,0 +1,44 @@ +import $ from 'jquery'; +import { batchActions } from 'redux-batched-actions'; +import { set, update } from '../baseActions'; + +function createSaveHandler(section, url, getFromState) { + return function(payload) { + return function(dispatch, getState) { + dispatch(set({ section, isSaving: true })); + + const state = getFromState(getState()); + const saveData = Object.assign({}, state.item, state.pendingChanges); + + const promise = $.ajax({ + url, + method: 'PUT', + dataType: 'json', + data: JSON.stringify(saveData) + }); + + promise.done((data) => { + dispatch(batchActions([ + update({ section, data }), + + set({ + section, + isSaving: false, + saveError: null, + pendingChanges: {} + }) + ])); + }); + + promise.fail((xhr) => { + dispatch(set({ + section, + isSaving: false, + saveError: xhr + })); + }); + }; + }; +} + +export default createSaveHandler; diff --git a/frontend/src/Store/Actions/Creators/createSaveProviderHandler.js b/frontend/src/Store/Actions/Creators/createSaveProviderHandler.js new file mode 100644 index 000000000..6b6d89314 --- /dev/null +++ b/frontend/src/Store/Actions/Creators/createSaveProviderHandler.js @@ -0,0 +1,68 @@ +import { batchActions } from 'redux-batched-actions'; +import createAjaxRequest from 'Utilities/createAjaxRequest'; +import getProviderState from 'Utilities/State/getProviderState'; +import { set, updateItem } from '../baseActions'; + +const abortCurrentRequests = {}; + +export function createCancelSaveProviderHandler(section) { + return function(payload) { + return function(dispatch, getState) { + if (abortCurrentRequests[section]) { + abortCurrentRequests[section](); + abortCurrentRequests[section] = null; + } + }; + }; +} + +function createSaveProviderHandler(section, url, getFromState) { + return function(payload) { + return function(dispatch, getState) { + dispatch(set({ section, isSaving: true })); + + const id = payload.id; + const saveData = getProviderState(payload, getState, getFromState); + + const ajaxOptions = { + url, + method: 'POST', + contentType: 'application/json', + dataType: 'json', + data: JSON.stringify(saveData) + }; + + if (id) { + ajaxOptions.url = `${url}/${id}`; + ajaxOptions.method = 'PUT'; + } + + const { request, abortRequest } = createAjaxRequest()(ajaxOptions); + + abortCurrentRequests[section] = abortRequest; + + request.done((data) => { + dispatch(batchActions([ + updateItem({ section, ...data }), + + set({ + section, + isSaving: false, + saveError: null, + pendingChanges: {} + }) + ])); + }); + + request.fail((xhr) => { + dispatch(set({ + section, + isSaving: false, + saveError: xhr.aborted ? null : xhr + })); + }); + }; + }; +} + +export default createSaveProviderHandler; diff --git a/frontend/src/Store/Actions/Creators/createServerSideCollectionHandlers.js b/frontend/src/Store/Actions/Creators/createServerSideCollectionHandlers.js new file mode 100644 index 000000000..91cef5d5e --- /dev/null +++ b/frontend/src/Store/Actions/Creators/createServerSideCollectionHandlers.js @@ -0,0 +1,52 @@ +import serverSideCollectionHandlers from 'Utilities/serverSideCollectionHandlers'; +import pages from 'Utilities/pages'; +import createFetchServerSideCollectionHandler from './createFetchServerSideCollectionHandler'; +import createSetServerSideCollectionPageHandler from './createSetServerSideCollectionPageHandler'; +import createSetServerSideCollectionSortHandler from './createSetServerSideCollectionSortHandler'; +import createSetServerSideCollectionFilterHandler from './createSetServerSideCollectionFilterHandler'; + +function createServerSideCollectionHandlers(section, url, getFromState, handlers) { + const actionHandlers = {}; + const fetchHandlerType = handlers[serverSideCollectionHandlers.FETCH]; + const fetchHandler = createFetchServerSideCollectionHandler(section, url, getFromState); + actionHandlers[fetchHandlerType] = fetchHandler; + + if (handlers.hasOwnProperty(serverSideCollectionHandlers.FIRST_PAGE)) { + const handlerType = handlers[serverSideCollectionHandlers.FIRST_PAGE]; + actionHandlers[handlerType] = createSetServerSideCollectionPageHandler(section, pages.FIRST, getFromState, fetchHandler); + } + + if (handlers.hasOwnProperty(serverSideCollectionHandlers.PREVIOUS_PAGE)) { + const handlerType = handlers[serverSideCollectionHandlers.PREVIOUS_PAGE]; + actionHandlers[handlerType] = createSetServerSideCollectionPageHandler(section, pages.PREVIOUS, getFromState, fetchHandler); + } + + if (handlers.hasOwnProperty(serverSideCollectionHandlers.NEXT_PAGE)) { + const handlerType = handlers[serverSideCollectionHandlers.NEXT_PAGE]; + actionHandlers[handlerType] = createSetServerSideCollectionPageHandler(section, pages.NEXT, getFromState, fetchHandler); + } + + if (handlers.hasOwnProperty(serverSideCollectionHandlers.LAST_PAGE)) { + const handlerType = handlers[serverSideCollectionHandlers.LAST_PAGE]; + actionHandlers[handlerType] = createSetServerSideCollectionPageHandler(section, pages.LAST, getFromState, fetchHandler); + } + + if (handlers.hasOwnProperty(serverSideCollectionHandlers.EXACT_PAGE)) { + const handlerType = handlers[serverSideCollectionHandlers.EXACT_PAGE]; + actionHandlers[handlerType] = createSetServerSideCollectionPageHandler(section, pages.EXACT, getFromState, fetchHandler); + } + + if (handlers.hasOwnProperty(serverSideCollectionHandlers.SORT)) { + const handlerType = handlers[serverSideCollectionHandlers.SORT]; + actionHandlers[handlerType] = createSetServerSideCollectionSortHandler(section, getFromState, fetchHandler); + } + + if (handlers.hasOwnProperty(serverSideCollectionHandlers.FILTER)) { + const handlerType = handlers[serverSideCollectionHandlers.FILTER]; + actionHandlers[handlerType] = createSetServerSideCollectionFilterHandler(section, getFromState, fetchHandler); + } + + return actionHandlers; +} + +export default createServerSideCollectionHandlers; diff --git a/frontend/src/Store/Actions/Creators/createSetServerSideCollectionFilterHandler.js b/frontend/src/Store/Actions/Creators/createSetServerSideCollectionFilterHandler.js new file mode 100644 index 000000000..0aaa342db --- /dev/null +++ b/frontend/src/Store/Actions/Creators/createSetServerSideCollectionFilterHandler.js @@ -0,0 +1,12 @@ +import { set } from '../baseActions'; + +function createSetServerSideCollectionFilterHandler(section, getFromState, fetchHandler) { + return function(payload) { + return function(dispatch, getState) { + dispatch(set({ section, ...payload })); + dispatch(fetchHandler({ page: 1 })); + }; + }; +} + +export default createSetServerSideCollectionFilterHandler; diff --git a/frontend/src/Store/Actions/Creators/createSetServerSideCollectionPageHandler.js b/frontend/src/Store/Actions/Creators/createSetServerSideCollectionPageHandler.js new file mode 100644 index 000000000..88682f118 --- /dev/null +++ b/frontend/src/Store/Actions/Creators/createSetServerSideCollectionPageHandler.js @@ -0,0 +1,37 @@ +import pages from 'Utilities/pages'; + +function createSetServerSideCollectionPageHandler(section, page, getFromState, fetchHandler) { + return function(payload) { + return function(dispatch, getState) { + const state = getFromState(getState()); + const sectionState = state.hasOwnProperty(section) ? state[section] : state; + const currentPage = sectionState.page || 1; + let nextPage = 0; + + switch (page) { + case pages.FIRST: + nextPage = 1; + break; + case pages.PREVIOUS: + nextPage = currentPage - 1; + break; + case pages.NEXT: + nextPage = currentPage + 1; + break; + case pages.LAST: + nextPage = sectionState.totalPages; + break; + default: + nextPage = payload.page; + } + + // If we prefer to update the page immediately we should + // set the page and not pass a page to the fetch handler. + + // dispatch(set({ section, page: nextPage })); + dispatch(fetchHandler({ page: nextPage })); + }; + }; +} + +export default createSetServerSideCollectionPageHandler; diff --git a/frontend/src/Store/Actions/Creators/createSetServerSideCollectionSortHandler.js b/frontend/src/Store/Actions/Creators/createSetServerSideCollectionSortHandler.js new file mode 100644 index 000000000..6ee0ac4b2 --- /dev/null +++ b/frontend/src/Store/Actions/Creators/createSetServerSideCollectionSortHandler.js @@ -0,0 +1,28 @@ +import { sortDirections } from 'Helpers/Props'; +import { set } from '../baseActions'; + +function createSetServerSideCollectionSortHandler(section, getFromState, fetchHandler) { + return function(payload) { + return function(dispatch, getState) { + const state = getFromState(getState()); + const sectionState = state.hasOwnProperty(section) ? state[section] : state; + const sortKey = payload.sortKey || sectionState.sortKey; + let sortDirection = payload.sortDirection; + + if (!sortDirection) { + if (payload.sortKey === sectionState.sortKey) { + sortDirection = sectionState.sortDirection === sortDirections.ASCENDING ? + sortDirections.DESCENDING : + sortDirections.ASCENDING; + } else { + sortDirection = sectionState.sortDirection; + } + } + + dispatch(set({ section, sortKey, sortDirection })); + dispatch(fetchHandler()); + }; + }; +} + +export default createSetServerSideCollectionSortHandler; diff --git a/frontend/src/Store/Actions/Creators/createTestProviderHandler.js b/frontend/src/Store/Actions/Creators/createTestProviderHandler.js new file mode 100644 index 000000000..f3cf354bc --- /dev/null +++ b/frontend/src/Store/Actions/Creators/createTestProviderHandler.js @@ -0,0 +1,56 @@ +import createAjaxRequest from 'Utilities/createAjaxRequest'; +import getProviderState from 'Utilities/State/getProviderState'; +import { set } from '../baseActions'; + +const abortCurrentRequests = {}; + +export function createCancelTestProviderHandler(section) { + return function(payload) { + return function(dispatch, getState) { + if (abortCurrentRequests[section]) { + abortCurrentRequests[section](); + abortCurrentRequests[section] = null; + } + }; + }; +} + +function createTestProviderHandler(section, url, getFromState) { + return function(payload) { + return function(dispatch, getState) { + dispatch(set({ section, isTesting: true })); + + const testData = getProviderState(payload, getState, getFromState); + + const ajaxOptions = { + url: `${url}/test`, + method: 'POST', + contentType: 'application/json', + dataType: 'json', + data: JSON.stringify(testData) + }; + + const { request, abortRequest } = createAjaxRequest()(ajaxOptions); + + abortCurrentRequests[section] = abortRequest; + + request.done((data) => { + dispatch(set({ + section, + isTesting: false, + saveError: null + })); + }); + + request.fail((xhr) => { + dispatch(set({ + section, + isTesting: false, + saveError: xhr.aborted ? null : xhr + })); + }); + }; + }; +} + +export default createTestProviderHandler; diff --git a/frontend/src/Store/Actions/Creators/createToggleAlbumMonitoredHandler.js b/frontend/src/Store/Actions/Creators/createToggleAlbumMonitoredHandler.js new file mode 100644 index 000000000..94581aec9 --- /dev/null +++ b/frontend/src/Store/Actions/Creators/createToggleAlbumMonitoredHandler.js @@ -0,0 +1,41 @@ +import $ from 'jquery'; +import updateAlbums from 'Utilities/Album/updateAlbums'; + +function createToggleAlbumMonitoredHandler(section, getFromState) { + return function(payload) { + return function(dispatch, getState) { + const { + albumId, + monitored + } = payload; + + const state = getFromState(getState()); + + updateAlbums(dispatch, section, state.items, [albumId], { + isSaving: true + }); + + const promise = $.ajax({ + url: `/album/${albumId}`, + method: 'PUT', + data: JSON.stringify({ monitored }), + dataType: 'json' + }); + + promise.done(() => { + updateAlbums(dispatch, section, state.items, [albumId], { + isSaving: false, + monitored + }); + }); + + promise.fail(() => { + updateAlbums(dispatch, section, state.items, [albumId], { + isSaving: false + }); + }); + }; + }; +} + +export default createToggleAlbumMonitoredHandler; diff --git a/frontend/src/Store/Actions/actionTypes.js b/frontend/src/Store/Actions/actionTypes.js new file mode 100644 index 000000000..2c7a4dc50 --- /dev/null +++ b/frontend/src/Store/Actions/actionTypes.js @@ -0,0 +1,416 @@ +// +// BASE + +export const SET = 'SET'; + +export const UPDATE = 'UPDATE'; +export const UPDATE_ITEM = 'UPDATE_ITEM'; +export const UPDATE_SERVER_SIDE_COLLECTION = 'UPDATE_SERVER_SIDE_COLLECTION'; + +export const SET_SETTING_VALUE = 'SET_SETTING_VALUE'; +export const CLEAR_PENDING_CHANGES = 'CLEAR_PENDING_CHANGES'; +export const SAVE_SETTINGS = 'SAVE_SETTINGS'; + +export const REMOVE_ITEM = 'REMOVE_ITEM'; + +// +// App + +export const SHOW_MESSAGE = 'SHOW_MESSAGE'; +export const HIDE_MESSAGE = 'HIDE_MESSAGE'; +export const SAVE_DIMENSIONS = 'SAVE_DIMENSIONS'; +export const SET_VERSION = 'SET_VERSION'; +export const SET_APP_VALUE = 'SET_APP_VALUE'; +export const SET_IS_SIDEBAR_VISIBLE = 'SET_IS_SIDEBAR_VISIBLE'; + +// +// Add Artist + +export const LOOKUP_ARTIST = 'LOOKUP_ARTIST'; +export const ADD_ARTIST = 'ADD_ARTIST'; +export const SET_ADD_ARTIST_VALUE = 'SET_ADD_ARTIST_VALUE'; +export const CLEAR_ADD_ARTIST = 'CLEAR_ADD_ARTIST'; +export const SET_ADD_ARTIST_DEFAULT = 'SET_ADD_ARTIST_DEFAULT'; + +// +// Import Artist + +export const QUEUE_LOOKUP_ARTIST = 'QUEUE_LOOKUP_ARTIST'; +export const START_LOOKUP_ARTIST = 'START_LOOKUP_ARTIST'; +export const CLEAR_IMPORT_ARTIST = 'CLEAR_IMPORT_ARTIST'; +export const SET_IMPORT_ARTIST_VALUE = 'SET_IMPORT_ARTIST_VALUE'; +export const IMPORT_ARTIST = 'IMPORT_ARTIST'; + +// +// Artist + +export const FETCH_ARTIST = 'FETCH_ARTIST'; +export const SET_ARTIST_VALUE = 'SET_ARTIST_VALUE'; +export const SAVE_ARTIST = 'SAVE_ARTIST'; +export const DELETE_ARTIST = 'DELETE_ARTIST'; + +export const SET_ARTIST_SORT = 'SET_ARTIST_SORT'; +export const SET_ARTIST_FILTER = 'SET_ARTIST_FILTER'; +export const SET_ARTIST_VIEW = 'SET_ARTIST_VIEW'; +export const SET_ARTIST_TABLE_OPTION = 'SET_ARTIST_TABLE_OPTION'; +export const SET_ARTIST_POSTER_OPTION = 'SET_ARTIST_POSTER_OPTION'; +export const SET_ARTIST_BANNER_OPTION = 'SET_ARTIST_BANNER_OPTION'; +export const SET_ARTIST_OVERVIEW_OPTION = 'SET_ARTIST_OVERVIEW_OPTION'; + +export const TOGGLE_ARTIST_MONITORED = 'TOGGLE_ARTIST_MONITORED'; +export const TOGGLE_ALBUM_MONITORED = 'TOGGLE_ALBUM_MONITORED'; + +// +// Artist Editor + +export const SET_ARTIST_EDITOR_SORT = 'SET_ARTIST_EDITOR_SORT'; +export const SET_ARTIST_EDITOR_FILTER = 'SET_ARTIST_EDITOR_FILTER'; +export const SAVE_ARTIST_EDITOR = 'SAVE_ARTIST_EDITOR'; +export const BULK_DELETE_ARTIST = 'BULK_DELETE_ARTIST'; + +// +// Season Pass + +export const SET_ALBUM_STUDIO_SORT = 'SET_ALBUM_STUDIO_SORT'; +export const SET_ALBUM_STUDIO_FILTER = 'SET_ALBUM_STUDIO_FILTER'; +export const SAVE_ALBUM_STUDIO = 'SAVE_ALBUM_STUDIO'; + +// +// Episodes + +export const FETCH_EPISODES = 'FETCH_EPISODES'; +export const SET_EPISODES_SORT = 'SET_EPISODES_SORT'; +export const SET_EPISODES_TABLE_OPTION = 'SET_EPISODES_TABLE_OPTION'; +export const CLEAR_EPISODES = 'CLEAR_EPISODES'; +export const TOGGLE_EPISODE_MONITORED = 'TOGGLE_EPISODE_MONITORED'; +export const TOGGLE_EPISODES_MONITORED = 'TOGGLE_EPISODES_MONITORED'; + +// +// Tracks + +export const FETCH_TRACKS = 'FETCH_TRACKS'; +export const SET_TRACKS_SORT = 'SET_TRACKS_SORT'; +export const SET_TRACKS_TABLE_OPTION = 'SET_TRACKS_TABLE_OPTION'; +export const CLEAR_TRACKS = 'CLEAR_TRACKS'; + +// +// Episode Files + +export const FETCH_TRACK_FILES = 'FETCH_TRACK_FILES'; +export const CLEAR_TRACK_FILES = 'CLEAR_TRACK_FILES'; +export const DELETE_TRACK_FILE = 'DELETE_TRACK_FILE'; +export const DELETE_TRACK_FILES = 'DELETE_TRACK_FILES'; +export const UPDATE_TRACK_FILES = 'UPDATE_TRACK_FILES'; + +// +// Album History + +export const FETCH_ALBUM_HISTORY = 'FETCH_ALBUM_HISTORY'; +export const CLEAR_ALBUM_HISTORY = 'CLEAR_ALBUM_HISTORY'; +export const ALBUM_HISTORY_MARK_AS_FAILED = 'ALBUM_HISTORY_MARK_AS_FAILED'; + +// +// Releases + +export const FETCH_RELEASES = 'FETCH_RELEASES'; +export const SET_RELEASES_SORT = 'SET_RELEASES_SORT'; +export const CLEAR_RELEASES = 'CLEAR_RELEASES'; +export const GRAB_RELEASE = 'GRAB_RELEASE'; +export const UPDATE_RELEASE = 'UPDATE_RELEASE'; + +// +// Calendar + +export const FETCH_CALENDAR = 'FETCH_CALENDAR'; +export const SET_CALENDAR_DAYS_COUNT = 'SET_CALENDAR_DAYS_COUNT'; +export const SET_CALENDAR_INCLUDE_UNMONITORED = 'SET_CALENDAR_INCLUDE_UNMONITORED'; +export const SET_CALENDAR_VIEW = 'SET_CALENDAR_VIEW'; +export const GOTO_CALENDAR_TODAY = 'GOTO_CALENDAR_TODAY'; +export const GOTO_CALENDAR_PREVIOUS_RANGE = 'GOTO_CALENDAR_PREVIOUS_RANGE'; +export const GOTO_CALENDAR_NEXT_RANGE = 'GOTO_CALENDAR_NEXT_RANGE'; +export const CLEAR_CALENDAR = 'CLEAR_CALENDAR'; + +// +// History + +export const FETCH_HISTORY = 'FETCH_HISTORY'; +export const GOTO_FIRST_HISTORY_PAGE = 'GOTO_FIRST_HISTORY_PAGE'; +export const GOTO_PREVIOUS_HISTORY_PAGE = 'GOTO_PREVIOUS_HISTORY_PAGE'; +export const GOTO_NEXT_HISTORY_PAGE = 'GOTO_NEXT_HISTORY_PAGE'; +export const GOTO_LAST_HISTORY_PAGE = 'GOTO_LAST_HISTORY_PAGE'; +export const GOTO_HISTORY_PAGE = 'GOTO_HISTORY_PAGE'; +export const SET_HISTORY_SORT = 'SET_HISTORY_SORT'; +export const SET_HISTORY_FILTER = 'SET_HISTORY_FILTER'; +export const SET_HISTORY_TABLE_OPTION = 'SET_HISTORY_TABLE_OPTION'; +export const CLEAR_HISTORY = 'CLEAR_HISTORY'; + +export const MARK_AS_FAILED = 'MARK_AS_FAILED'; + +// +// Queue + +export const FETCH_QUEUE_STATUS = 'FETCH_QUEUE_STATUS'; + +export const FETCH_QUEUE_DETAILS = 'FETCH_QUEUE_DETAILS'; +export const CLEAR_QUEUE_DETAILS = 'CLEAR_QUEUE_DETAILS'; + +export const FETCH_QUEUE = 'FETCH_QUEUE'; +export const GOTO_FIRST_QUEUE_PAGE = 'GOTO_FIRST_QUEUE_PAGE'; +export const GOTO_PREVIOUS_QUEUE_PAGE = 'GOTO_PREVIOUS_QUEUE_PAGE'; +export const GOTO_NEXT_QUEUE_PAGE = 'GOTO_NEXT_QUEUE_PAGE'; +export const GOTO_LAST_QUEUE_PAGE = 'GOTO_LAST_QUEUE_PAGE'; +export const GOTO_QUEUE_PAGE = 'GOTO_QUEUE_PAGE'; +export const SET_QUEUE_SORT = 'SET_QUEUE_SORT'; +export const SET_QUEUE_TABLE_OPTION = 'SET_QUEUE_TABLE_OPTION'; +export const CLEAR_QUEUE = 'CLEAR_QUEUE'; + +export const GRAB_QUEUE_ITEM = 'GRAB_QUEUE_ITEM'; +export const GRAB_QUEUE_ITEMS = 'GRAB_QUEUE_ITEMS'; +export const REMOVE_QUEUE_ITEM = 'REMOVE_QUEUE_ITEM'; +export const REMOVE_QUEUE_ITEMS = 'REMOVE_QUEUE_ITEMS'; + +// +// Blacklist + +export const FETCH_BLACKLIST = 'FETCH_BLACKLIST'; +export const GOTO_FIRST_BLACKLIST_PAGE = 'GOTO_FIRST_BLACKLIST_PAGE'; +export const GOTO_PREVIOUS_BLACKLIST_PAGE = 'GOTO_PREVIOUS_BLACKLIST_PAGE'; +export const GOTO_NEXT_BLACKLIST_PAGE = 'GOTO_NEXT_BLACKLIST_PAGE'; +export const GOTO_LAST_BLACKLIST_PAGE = 'GOTO_LAST_BLACKLIST_PAGE'; +export const GOTO_BLACKLIST_PAGE = 'GOTO_BLACKLIST_PAGE'; +export const SET_BLACKLIST_SORT = 'SET_BLACKLIST_SORT'; +export const SET_BLACKLIST_TABLE_OPTION = 'SET_BLACKLIST_TABLE_OPTION'; + +// +// Wanted + +export const FETCH_MISSING = 'FETCH_MISSING'; +export const GOTO_FIRST_MISSING_PAGE = 'GOTO_FIRST_MISSING_PAGE'; +export const GOTO_PREVIOUS_MISSING_PAGE = 'GOTO_PREVIOUS_MISSING_PAGE'; +export const GOTO_NEXT_MISSING_PAGE = 'GOTO_NEXT_MISSING_PAGE'; +export const GOTO_LAST_MISSING_PAGE = 'GOTO_LAST_MISSING_PAGE'; +export const GOTO_MISSING_PAGE = 'GOTO_MISSING_PAGE'; +export const SET_MISSING_SORT = 'SET_MISSING_SORT'; +export const SET_MISSING_FILTER = 'SET_MISSING_FILTER'; +export const SET_MISSING_TABLE_OPTION = 'SET_MISSING_TABLE_OPTION'; +export const CLEAR_MISSING = 'CLEAR_MISSING'; + +export const BATCH_TOGGLE_MISSING_ALBUMS = 'BATCH_TOGGLE_MISSING_ALBUMS'; + +export const FETCH_CUTOFF_UNMET = 'FETCH_CUTOFF_UNMET'; +export const GOTO_FIRST_CUTOFF_UNMET_PAGE = 'GOTO_FIRST_CUTOFF_UNMET_PAGE'; +export const GOTO_PREVIOUS_CUTOFF_UNMET_PAGE = 'GOTO_PREVIOUS_CUTOFF_UNMET_PAGE'; +export const GOTO_NEXT_CUTOFF_UNMET_PAGE = 'GOTO_NEXT_CUTOFF_UNMET_PAGE'; +export const GOTO_LAST_CUTOFF_UNMET_PAGE = 'GOTO_LAST_CUTOFF_UNMET_PAGE'; +export const GOTO_CUTOFF_UNMET_PAGE = 'GOTO_CUTOFF_UNMET_PAGE'; +export const SET_CUTOFF_UNMET_SORT = 'SET_CUTOFF_UNMET_SORT'; +export const SET_CUTOFF_UNMET_FILTER = 'SET_CUTOFF_UNMET_FILTER'; +export const SET_CUTOFF_UNMET_TABLE_OPTION = 'SET_CUTOFF_UNMET_TABLE_OPTION'; +export const CLEAR_CUTOFF_UNMET = 'CLEAR_CUTOFF_UNMET'; + +export const BATCH_TOGGLE_CUTOFF_UNMET_ALBUMS = 'BATCH_TOGGLE_CUTOFF_UNMET_ALBUMS'; + +// +// Settings + +export const TOGGLE_ADVANCED_SETTINGS = 'TOGGLE_ADVANCED_SETTINGS'; + +export const FETCH_UI_SETTINGS = 'FETCH_UI_SETTINGS'; +export const SET_UI_SETTINGS_VALUE = 'SET_UI_SETTINGS_VALUE'; +export const SAVE_UI_SETTINGS = 'SAVE_UI_SETTINGS'; + +export const FETCH_MEDIA_MANAGEMENT_SETTINGS = 'FETCH_MEDIA_MANAGEMENT_SETTINGS'; +export const SET_MEDIA_MANAGEMENT_SETTINGS_VALUE = 'SET_MEDIA_MANAGEMENT_SETTINGS_VALUE'; +export const SAVE_MEDIA_MANAGEMENT_SETTINGS = 'SAVE_MEDIA_MANAGEMENT_SETTINGS'; + +export const FETCH_NAMING_SETTINGS = 'FETCH_NAMING_SETTINGS'; +export const SET_NAMING_SETTINGS_VALUE = 'SET_NAMING_SETTINGS_VALUE'; +export const SAVE_NAMING_SETTINGS = 'SAVE_NAMING_SETTINGS'; +export const FETCH_NAMING_EXAMPLES = 'FETCH_NAMING_EXAMPLES'; + +export const FETCH_QUALITY_PROFILES = 'FETCH_QUALITY_PROFILES'; +export const FETCH_QUALITY_PROFILE_SCHEMA = 'FETCH_QUALITY_PROFILE_SCHEMA'; +export const SET_QUALITY_PROFILE_VALUE = 'SET_QUALITY_PROFILE_VALUE'; +export const SAVE_QUALITY_PROFILE = 'SAVE_QUALITY_PROFILE'; +export const DELETE_QUALITY_PROFILE = 'DELETE_QUALITY_PROFILE'; + +export const FETCH_LANGUAGE_PROFILES = 'FETCH_LANGUAGE_PROFILES'; +export const FETCH_LANGUAGE_PROFILE_SCHEMA = 'FETCH_LANGUAGE_PROFILE_SCHEMA'; +export const SET_LANGUAGE_PROFILE_VALUE = 'SET_LANGUAGE_PROFILE_VALUE'; +export const SAVE_LANGUAGE_PROFILE = 'SAVE_LANGUAGE_PROFILE'; +export const DELETE_LANGUAGE_PROFILE = 'DELETE_LANGUAGE_PROFILE'; + +export const FETCH_DELAY_PROFILES = 'FETCH_DELAY_PROFILES'; +export const SET_DELAY_PROFILE_VALUE = 'SET_DELAY_PROFILE_VALUE'; +export const SAVE_DELAY_PROFILE = 'SAVE_DELAY_PROFILE'; +export const DELETE_DELAY_PROFILE = 'DELETE_DELAY_PROFILE'; +export const REORDER_DELAY_PROFILE = 'REORDER_DELAY_PROFILE'; + +export const FETCH_QUALITY_DEFINITIONS = 'FETCH_QUALITY_DEFINITIONS'; +export const SET_QUALITY_DEFINITION_VALUE = 'SET_QUALITY_DEFINITION_VALUE'; +export const SAVE_QUALITY_DEFINITIONS = 'SAVE_QUALITY_DEFINITIONS'; + +export const FETCH_INDEXERS = 'FETCH_INDEXERS'; +export const FETCH_INDEXER_SCHEMA = 'FETCH_INDEXER_SCHEMA'; +export const SELECT_INDEXER_SCHEMA = 'SELECT_INDEXER_SCHEMA'; +export const SET_INDEXER_VALUE = 'SET_INDEXER_VALUE'; +export const SET_INDEXER_FIELD_VALUE = 'SET_INDEXER_FIELD_VALUE'; +export const SAVE_INDEXER = 'SAVE_INDEXER'; +export const CANCEL_SAVE_INDEXER = 'CANCEL_SAVE_INDEXER'; +export const DELETE_INDEXER = 'DELETE_INDEXER'; +export const TEST_INDEXER = 'TEST_INDEXER'; +export const CANCEL_TEST_INDEXER = 'CANCEL_TEST_INDEXER'; + +export const FETCH_INDEXER_OPTIONS = 'FETCH_INDEXER_OPTIONS'; +export const SET_INDEXER_OPTIONS_VALUE = 'SET_INDEXER_OPTIONS_VALUE'; +export const SAVE_INDEXER_OPTIONS = 'SAVE_INDEXER_OPTIONS'; + +export const FETCH_RESTRICTIONS = 'FETCH_RESTRICTIONS'; +export const SET_RESTRICTION_VALUE = 'SET_RESTRICTION_VALUE'; +export const SAVE_RESTRICTION = 'SAVE_RESTRICTION'; +export const DELETE_RESTRICTION = 'DELETE_RESTRICTION'; + +export const FETCH_DOWNLOAD_CLIENTS = 'FETCH_DOWNLOAD_CLIENTS'; +export const FETCH_DOWNLOAD_CLIENT_SCHEMA = 'FETCH_DOWNLOAD_CLIENT_SCHEMA'; +export const SELECT_DOWNLOAD_CLIENT_SCHEMA = 'SELECT_DOWNLOAD_CLIENT_SCHEMA'; +export const SET_DOWNLOAD_CLIENT_VALUE = 'SET_DOWNLOAD_CLIENT_VALUE'; +export const SET_DOWNLOAD_CLIENT_FIELD_VALUE = 'SET_DOWNLOAD_CLIENT_FIELD_VALUE'; +export const SAVE_DOWNLOAD_CLIENT = 'SAVE_DOWNLOAD_CLIENT'; +export const CANCEL_SAVE_DOWNLOAD_CLIENT = 'CANCEL_SAVE_DOWNLOAD_CLIENT'; +export const DELETE_DOWNLOAD_CLIENT = 'DELETE_DOWNLOAD_CLIENT'; +export const TEST_DOWNLOAD_CLIENT = 'TEST_DOWNLOAD_CLIENT'; +export const CANCEL_TEST_DOWNLOAD_CLIENT = 'CANCEL_TEST_DOWNLOAD_CLIENT'; + +export const FETCH_DOWNLOAD_CLIENT_OPTIONS = 'FETCH_DOWNLOAD_CLIENT_OPTIONS'; +export const SET_DOWNLOAD_CLIENT_OPTIONS_VALUE = 'SET_DOWNLOAD_CLIENT_OPTIONS_VALUE'; +export const SAVE_DOWNLOAD_CLIENT_OPTIONS = 'SAVE_DOWNLOAD_CLIENT_OPTIONS'; + +export const FETCH_REMOTE_PATH_MAPPINGS = 'FETCH_REMOTE_PATH_MAPPINGS'; +export const SET_REMOTE_PATH_MAPPING_VALUE = 'SET_REMOTE_PATH_MAPPING_VALUE'; +export const SAVE_REMOTE_PATH_MAPPING = 'SAVE_REMOTE_PATH_MAPPING'; +export const DELETE_REMOTE_PATH_MAPPING = 'DELETE_REMOTE_PATH_MAPPING'; + +export const FETCH_NOTIFICATIONS = 'FETCH_NOTIFICATIONS'; +export const FETCH_NOTIFICATION_SCHEMA = 'FETCH_NOTIFICATION_SCHEMA'; +export const SELECT_NOTIFICATION_SCHEMA = 'SELECT_NOTIFICATION_SCHEMA'; +export const SET_NOTIFICATION_VALUE = 'SET_NOTIFICATION_VALUE'; +export const SET_NOTIFICATION_FIELD_VALUE = 'SET_NOTIFICATION_FIELD_VALUE'; +export const SAVE_NOTIFICATION = 'SAVE_NOTIFICATION'; +export const CANCEL_SAVE_NOTIFICATION = 'CANCEL_SAVE_NOTIFICATION'; +export const DELETE_NOTIFICATION = 'DELETE_NOTIFICATION'; +export const CANCEL_TEST_NOTIFICATION = 'CANCEL_TEST_NOTIFICATION'; + +export const FETCH_METADATA = 'FETCH_METADATA'; +export const SET_METADATA_VALUE = 'SET_METADATA_VALUE'; +export const SET_METADATA_FIELD_VALUE = 'SET_METADATA_FIELD_VALUE'; +export const SAVE_METADATA = 'SAVE_METADATA'; + +export const FETCH_METADATA_PROVIDER = 'FETCH_METADATA_PROVIDER'; +export const SET_METADATA_PROVIDER_VALUE = 'SET_METADATA_PROVIDER_VALUE'; +export const SAVE_METADATA_PROVIDER = 'SAVE_METADATA_PROVIDER'; + +// +// System + +export const FETCH_STATUS = 'FETCH_STATUS'; +export const FETCH_HEALTH = 'FETCH_HEALTH'; +export const FETCH_DISK_SPACE = 'FETCH_DISK_SPACE'; + +export const FETCH_TASK = 'FETCH_TASK'; +export const FETCH_TASKS = 'FETCH_TASKS'; +export const FETCH_BACKUPS = 'FETCH_BACKUPS'; +export const FETCH_UPDATES = 'FETCH_UPDATES'; + +export const FETCH_LOGS = 'FETCH_LOGS'; +export const GOTO_FIRST_LOGS_PAGE = 'GOTO_FIRST_LOGS_PAGE'; +export const GOTO_PREVIOUS_LOGS_PAGE = 'GOTO_PREVIOUS_LOGS_PAGE'; +export const GOTO_NEXT_LOGS_PAGE = 'GOTO_NEXT_LOGS_PAGE'; +export const GOTO_LAST_LOGS_PAGE = 'GOTO_LAST_LOGS_PAGE'; +export const GOTO_LOGS_PAGE = 'GOTO_LOGS_PAGE'; +export const SET_LOGS_SORT = 'SET_LOGS_SORT'; +export const SET_LOGS_FILTER = 'SET_LOGS_FILTER'; +export const SET_LOGS_TABLE_OPTION = 'SET_LOGS_TABLE_OPTION'; + +export const FETCH_LOG_FILES = 'FETCH_LOG_FILES'; +export const FETCH_UPDATE_LOG_FILES = 'FETCH_UPDATE_LOG_FILES'; + +export const FETCH_GENERAL_SETTINGS = 'FETCH_GENERAL_SETTINGS'; +export const SET_GENERAL_SETTINGS_VALUE = 'SET_GENERAL_SETTINGS_VALUE'; +export const SAVE_GENERAL_SETTINGS = 'SAVE_GENERAL_SETTINGS'; + +export const RESTART = 'RESTART'; +export const SHUTDOWN = 'SHUTDOWN'; + +// +// Commands + +export const FETCH_COMMANDS = 'FETCH_COMMANDS'; +export const EXECUTE_COMMAND = 'EXECUTE_COMMAND'; +export const ADD_COMMAND = 'ADD_COMMAND'; +export const UPDATE_COMMAND = 'UPDATE_COMMAND'; +export const FINISH_COMMAND = 'FINISH_COMMAND'; +export const REMOVE_COMMAND = 'REMOVE_COMMAND'; +export const REGISTER_FINISH_COMMAND_HANDLER = 'REGISTER_FINISH_COMMAND_HANDLER'; +export const UNREGISTER_FINISH_COMMAND_HANDLER = 'UNREGISTER_FINISH_COMMAND_HANDLER'; + +// +// Paths + +export const FETCH_PATHS = 'FETCH_PATHS'; +export const UPDATE_PATHS = 'UPDATE_PATHS'; +export const CLEAR_PATHS = 'CLEAR_PATHS'; + +// +// Languages + +export const FETCH_LANGUAGES = 'FETCH_LANGUAGES'; + +// +// Tags + +export const FETCH_TAGS = 'FETCH_TAGS'; +export const ADD_TAG = 'ADD_TAG'; + +// +// Captcha + +export const REFRESH_CAPTCHA = 'REFRESH_CAPTCHA'; +export const GET_CAPTCHA_COOKIE = 'GET_CAPTCHA_COOKIE'; +export const SET_CAPTCHA_VALUE = 'SET_CAPTCHA_VALUE'; +export const RESET_CAPTCHA = 'RESET_CAPTCHA'; + +// +// OAuth + +export const START_OAUTH = 'START_OAUTH'; +export const GET_OAUTH_TOKEN = 'GET_OAUTH_TOKEN'; +export const SET_OAUTH_VALUE = 'SET_OAUTH_VALUE'; +export const RESET_OAUTH = 'RESET_OAUTH'; + +// +// Interactive Import + +export const FETCH_INTERACTIVE_IMPORT_ITEMS = 'FETCH_INTERACTIVE_IMPORT_ITEMS'; +export const UPDATE_INTERACTIVE_IMPORT_ITEM = 'UPDATE_INTERACTIVE_IMPORT_ITEM'; +export const SET_INTERACTIVE_IMPORT_SORT = 'SET_INTERACTIVE_IMPORT_SORT'; +export const CLEAR_INTERACTIVE_IMPORT = 'CLEAR_INTERACTIVE_IMPORT'; +export const ADD_RECENT_FOLDER = 'ADD_RECENT_FOLDER'; +export const REMOVE_RECENT_FOLDER = 'REMOVE_RECENT_FOLDER'; +export const SET_INTERACTIVE_IMPORT_MODE = 'SET_INTERACTIVE_IMPORT_MODE'; + +export const FETCH_INTERACTIVE_IMPORT_ALBUMS = 'FETCH_INTERACTIVE_IMPORT_ALBUMS'; +export const SET_INTERACTIVE_IMPORT_ALBUMS_SORT = 'SET_INTERACTIVE_IMPORT_ALBUMS_SORT'; +export const CLEAR_INTERACTIVE_IMPORT_ALBUMS = 'CLEAR_INTERACTIVE_IMPORT_ALBUMS'; + +// +// Root Folders + +export const FETCH_ROOT_FOLDERS = 'FETCH_ROOT_FOLDERS'; +export const ADD_ROOT_FOLDER = 'ADD_ROOT_FOLDER'; +export const DELETE_ROOT_FOLDER = 'DELETE_ROOT_FOLDER'; + +// +// Organize Preview + +export const FETCH_ORGANIZE_PREVIEW = 'FETCH_ORGANIZE_PREVIEW'; +export const CLEAR_ORGANIZE_PREVIEW = 'CLEAR_ORGANIZE_PREVIEW'; diff --git a/frontend/src/Store/Actions/addArtistActionHandlers.js b/frontend/src/Store/Actions/addArtistActionHandlers.js new file mode 100644 index 000000000..09ca0f9bf --- /dev/null +++ b/frontend/src/Store/Actions/addArtistActionHandlers.js @@ -0,0 +1,94 @@ +import _ from 'lodash'; +import $ from 'jquery'; +import { batchActions } from 'redux-batched-actions'; +import createAjaxRequest from 'Utilities/createAjaxRequest'; +import getNewSeries from 'Utilities/Series/getNewSeries'; +import * as types from './actionTypes'; +import { set, update, updateItem } from './baseActions'; + +let abortCurrentRequest = null; +const section = 'addArtist'; + +const addArtistActionHandlers = { + [types.LOOKUP_ARTIST]: function(payload) { + return function(dispatch, getState) { + dispatch(set({ section, isFetching: true })); + + if (abortCurrentRequest) { + abortCurrentRequest(); + } + + const { request, abortRequest } = createAjaxRequest()({ + url: '/artist/lookup', + data: { + term: payload.term + } + }); + + abortCurrentRequest = abortRequest; + + request.done((data) => { + dispatch(batchActions([ + update({ section, data }), + + set({ + section, + isFetching: false, + isPopulated: true, + error: null + }) + ])); + }); + + request.fail((xhr) => { + dispatch(set({ + section, + isFetching: false, + isPopulated: false, + error: xhr.aborted ? null : xhr + })); + }); + }; + }, + + [types.ADD_ARTIST]: function(payload) { + return function(dispatch, getState) { + dispatch(set({ section, isAdding: true })); + + const foreignArtistId = payload.foreignArtistId; + const items = getState().addArtist.items; + const newSeries = getNewSeries(_.cloneDeep(_.find(items, { foreignArtistId })), payload); + + const promise = $.ajax({ + url: '/artist', + method: 'POST', + contentType: 'application/json', + data: JSON.stringify(newSeries) + }); + + promise.done((data) => { + dispatch(batchActions([ + updateItem({ section: 'artist', ...data }), + + set({ + section, + isAdding: false, + isAdded: true, + addError: null + }) + ])); + }); + + promise.fail((xhr) => { + dispatch(set({ + section, + isAdding: false, + isAdded: false, + addError: xhr + })); + }); + }; + } +}; + +export default addArtistActionHandlers; diff --git a/frontend/src/Store/Actions/addArtistActions.js b/frontend/src/Store/Actions/addArtistActions.js new file mode 100644 index 000000000..e0ee67420 --- /dev/null +++ b/frontend/src/Store/Actions/addArtistActions.js @@ -0,0 +1,15 @@ +import { createAction } from 'redux-actions'; +import * as types from './actionTypes'; +import addArtistActionHandlers from './addArtistActionHandlers'; + +export const lookupArtist = addArtistActionHandlers[types.LOOKUP_ARTIST]; +export const addArtist = addArtistActionHandlers[types.ADD_ARTIST]; +export const clearAddArtist = createAction(types.CLEAR_ADD_ARTIST); +export const setAddArtistDefault = createAction(types.SET_ADD_ARTIST_DEFAULT); + +export const setAddArtistValue = createAction(types.SET_ADD_ARTIST_VALUE, (payload) => { + return { + section: 'addArtist', + ...payload + }; +}); diff --git a/frontend/src/Store/Actions/albumHistoryActionHandlers.js b/frontend/src/Store/Actions/albumHistoryActionHandlers.js new file mode 100644 index 000000000..8d1952fa7 --- /dev/null +++ b/frontend/src/Store/Actions/albumHistoryActionHandlers.js @@ -0,0 +1,75 @@ +import $ from 'jquery'; +import { batchActions } from 'redux-batched-actions'; +import { sortDirections } from 'Helpers/Props'; +import * as types from './actionTypes'; +import { set, update } from './baseActions'; +import { fetchAlbumHistory } from './albumHistoryActions'; + +const albumHistoryActionHandlers = { + [types.FETCH_ALBUM_HISTORY]: function(payload) { + const section = 'albumHistory'; + + return function(dispatch, getState) { + dispatch(set({ section, isFetching: true })); + + const queryParams = { + pageSize: 1000, + page: 1, + filterKey: 'albumId', + filterValue: payload.albumId, + sortKey: 'date', + sortDirection: sortDirections.DESCENDING + }; + + const promise = $.ajax({ + url: '/history', + data: queryParams + }); + + promise.done((data) => { + dispatch(batchActions([ + update({ section, data: data.records }), + + set({ + section, + isFetching: false, + isPopulated: true, + error: null + }) + ])); + }); + + promise.fail((xhr) => { + dispatch(set({ + section, + isFetching: false, + isPopulated: false, + error: xhr + })); + }); + }; + }, + + [types.ALBUM_HISTORY_MARK_AS_FAILED]: function(payload) { + return function(dispatch, getState) { + const { + historyId, + albumId + } = payload; + + const promise = $.ajax({ + url: '/history/failed', + method: 'POST', + data: { + id: historyId + } + }); + + promise.done(() => { + dispatch(fetchAlbumHistory({ albumId })); + }); + }; + } +}; + +export default albumHistoryActionHandlers; diff --git a/frontend/src/Store/Actions/albumHistoryActions.js b/frontend/src/Store/Actions/albumHistoryActions.js new file mode 100644 index 000000000..3e15c4b1f --- /dev/null +++ b/frontend/src/Store/Actions/albumHistoryActions.js @@ -0,0 +1,7 @@ +import { createAction } from 'redux-actions'; +import * as types from './actionTypes'; +import albumHistoryActionHandlers from './albumHistoryActionHandlers'; + +export const fetchAlbumHistory = albumHistoryActionHandlers[types.FETCH_ALBUM_HISTORY]; +export const clearAlbumHistory = createAction(types.CLEAR_ALBUM_HISTORY); +export const albumHistoryMarkAsFailed = albumHistoryActionHandlers[types.ALBUM_HISTORY_MARK_AS_FAILED]; diff --git a/frontend/src/Store/Actions/albumStudioActionHandlers.js b/frontend/src/Store/Actions/albumStudioActionHandlers.js new file mode 100644 index 000000000..b20b96704 --- /dev/null +++ b/frontend/src/Store/Actions/albumStudioActionHandlers.js @@ -0,0 +1,83 @@ +import _ from 'lodash'; +import $ from 'jquery'; +import getMonitoringOptions from 'Utilities/Series/getMonitoringOptions'; +import * as types from './actionTypes'; +import { set } from './baseActions'; +import { fetchArtist } from './artistActions'; + +const section = 'albumStudio'; + +const albumStudioActionHandlers = { + [types.SAVE_ALBUM_STUDIO]: function(payload) { + return function(dispatch, getState) { + const { + artistIds, + monitored, + monitor + } = payload; + + let monitoringOptions = null; + const artist = []; + const allArtists = getState().artist.items; + + artistIds.forEach((id) => { + const s = _.find(allArtists, { id }); + const artistToUpdate = { id }; + + if (payload.hasOwnProperty('monitored')) { + artistToUpdate.monitored = monitored; + } + + if (monitor) { + const { + albums, + options: artistMonitoringOptions + } = getMonitoringOptions(_.cloneDeep(s.albums), monitor); + + if (!monitoringOptions) { + monitoringOptions = artistMonitoringOptions; + } + + artistToUpdate.albums = albums; + } + + artist.push(artistToUpdate); + }); + + dispatch(set({ + section, + isSaving: true + })); + + const promise = $.ajax({ + url: '/albumStudio', + method: 'POST', + data: JSON.stringify({ + artist, + monitoringOptions + }), + dataType: 'json' + }); + + promise.done((data) => { + dispatch(fetchArtist()); + + dispatch(set({ + section, + isSaving: false, + saveError: null + })); + }); + + promise.fail((xhr) => { + dispatch(set({ + section, + isSaving: false, + saveError: xhr + })); + }); + }; + } +}; + +export default albumStudioActionHandlers; diff --git a/frontend/src/Store/Actions/albumStudioActions.js b/frontend/src/Store/Actions/albumStudioActions.js new file mode 100644 index 000000000..7d90bbcc8 --- /dev/null +++ b/frontend/src/Store/Actions/albumStudioActions.js @@ -0,0 +1,7 @@ +import { createAction } from 'redux-actions'; +import * as types from './actionTypes'; +import albumStudioActionHandlers from './albumStudioActionHandlers'; + +export const setAlbumStudioSort = createAction(types.SET_ALBUM_STUDIO_SORT); +export const setAlbumStudioFilter = createAction(types.SET_ALBUM_STUDIO_FILTER); +export const saveAlbumStudio = albumStudioActionHandlers[types.SAVE_ALBUM_STUDIO]; diff --git a/frontend/src/Store/Actions/appActions.js b/frontend/src/Store/Actions/appActions.js new file mode 100644 index 000000000..3d0191a21 --- /dev/null +++ b/frontend/src/Store/Actions/appActions.js @@ -0,0 +1,27 @@ +import { createAction } from 'redux-actions'; +import * as types from './actionTypes'; + +export const saveDimensions = createAction(types.SAVE_DIMENSIONS); +export const setVersion = createAction(types.SET_VERSION); +export const setIsSidebarVisible = createAction(types.SET_IS_SIDEBAR_VISIBLE); + +export const setAppValue = createAction(types.SET_APP_VALUE, (payload) => { + return { + section: 'app', + ...payload + }; +}); + +export const showMessage = createAction(types.SHOW_MESSAGE, (payload) => { + return { + section: 'messages', + ...payload + }; +}); + +export const hideMessage = createAction(types.HIDE_MESSAGE, (payload) => { + return { + section: 'messages', + ...payload + }; +}); diff --git a/frontend/src/Store/Actions/artistActionHandlers.js b/frontend/src/Store/Actions/artistActionHandlers.js new file mode 100644 index 000000000..d455c40af --- /dev/null +++ b/frontend/src/Store/Actions/artistActionHandlers.js @@ -0,0 +1,132 @@ +import _ from 'lodash'; +import $ from 'jquery'; +import { batchActions } from 'redux-batched-actions'; +import * as types from './actionTypes'; +import createFetchHandler from './Creators/createFetchHandler'; +import createSaveProviderHandler from './Creators/createSaveProviderHandler'; +import createRemoveItemHandler from './Creators/createRemoveItemHandler'; +import { updateItem } from './baseActions'; + +const section = 'artist'; + +const artistActionHandlers = { + [types.FETCH_ARTIST]: createFetchHandler(section, '/artist'), + + [types.SAVE_ARTIST]: createSaveProviderHandler( + section, + '/artist', + (state) => state.artist), + + [types.DELETE_ARTIST]: createRemoveItemHandler( + section, + '/artist', + (state) => state.artist), + + [types.TOGGLE_ARTIST_MONITORED]: function(payload) { + return function(dispatch, getState) { + const { + artistId: id, + monitored + } = payload; + + const artist = _.find(getState().artist.items, { id }); + + dispatch(updateItem({ + id, + section, + isSaving: true + })); + + const promise = $.ajax({ + url: `/artist/${id}`, + method: 'PUT', + data: JSON.stringify({ + ...artist, + monitored + }), + dataType: 'json' + }); + + promise.done((data) => { + dispatch(updateItem({ + id, + section, + isSaving: false, + monitored + })); + }); + + promise.fail((xhr) => { + dispatch(updateItem({ + id, + section, + isSaving: false + })); + }); + }; + }, + + [types.TOGGLE_ALBUM_MONITORED]: function(payload) { + return function(dispatch, getState) { + const { + artistId: id, + seasonNumber, + monitored + } = payload; + + const artist = _.find(getState().artist.items, { id }); + const seasons = _.cloneDeep(artist.seasons); + const season = _.find(seasons, { seasonNumber }); + + season.isSaving = true; + + dispatch(updateItem({ + id, + section, + seasons + })); + + season.monitored = monitored; + + const promise = $.ajax({ + url: `/artist/${id}`, + method: 'PUT', + data: JSON.stringify({ + ...artist, + seasons + }), + dataType: 'json' + }); + + promise.done((data) => { + const episodes = _.filter(getState().episodes.items, { artistId: id, seasonNumber }); + + dispatch(batchActions([ + updateItem({ + id, + section, + ...data + }), + + ...episodes.map((episode) => { + return updateItem({ + id: episode.id, + section: 'episodes', + monitored + }); + }) + ])); + }); + + promise.fail((xhr) => { + dispatch(updateItem({ + id, + section, + seasons: artist.seasons + })); + }); + }; + } +}; + +export default artistActionHandlers; diff --git a/frontend/src/Store/Actions/artistActions.js b/frontend/src/Store/Actions/artistActions.js new file mode 100644 index 000000000..bf805763b --- /dev/null +++ b/frontend/src/Store/Actions/artistActions.js @@ -0,0 +1,16 @@ +import { createAction } from 'redux-actions'; +import * as types from './actionTypes'; +import artistActionHandlers from './artistActionHandlers'; + +export const fetchArtist = artistActionHandlers[types.FETCH_ARTIST]; +export const saveArtist = artistActionHandlers[types.SAVE_ARTIST]; +export const deleteArtist = artistActionHandlers[types.DELETE_ARTIST]; +export const toggleArtistMonitored = artistActionHandlers[types.TOGGLE_ARTIST_MONITORED]; +export const toggleSeasonMonitored = artistActionHandlers[types.TOGGLE_ALBUM_MONITORED]; + +export const setArtistValue = createAction(types.SET_ARTIST_VALUE, (payload) => { + return { + section: 'artist', + ...payload + }; +}); diff --git a/frontend/src/Store/Actions/artistEditorActionHandlers.js b/frontend/src/Store/Actions/artistEditorActionHandlers.js new file mode 100644 index 000000000..81bc2ecdc --- /dev/null +++ b/frontend/src/Store/Actions/artistEditorActionHandlers.js @@ -0,0 +1,86 @@ +import $ from 'jquery'; +import { batchActions } from 'redux-batched-actions'; +import * as types from './actionTypes'; +import { set, updateItem } from './baseActions'; + +const section = 'artistEditor'; + +const artistEditorActionHandlers = { + [types.SAVE_ARTIST_EDITOR]: function(payload) { + return function(dispatch, getState) { + dispatch(set({ + section, + isSaving: true + })); + + const promise = $.ajax({ + url: '/artist/editor', + method: 'PUT', + data: JSON.stringify(payload), + dataType: 'json' + }); + + promise.done((data) => { + dispatch(batchActions([ + ...data.map((artist) => { + return updateItem({ + id: artist.id, + section: 'artist', + ...artist + }); + }), + + set({ + section, + isSaving: false, + saveError: null + }) + ])); + }); + + promise.fail((xhr) => { + dispatch(set({ + section, + isSaving: false, + saveError: xhr + })); + }); + }; + }, + + [types.BULK_DELETE_ARTIST]: function(payload) { + return function(dispatch, getState) { + dispatch(set({ + section, + isDeleting: true + })); + + const promise = $.ajax({ + url: '/artist/editor', + method: 'DELETE', + data: JSON.stringify(payload), + dataType: 'json' + }); + + promise.done(() => { + // SignaR will take care of removing the serires from the collection + + dispatch(set({ + section, + isDeleting: false, + deleteError: null + })); + }); + + promise.fail((xhr) => { + dispatch(set({ + section, + isDeleting: false, + deleteError: xhr + })); + }); + }; + } +}; + +export default artistEditorActionHandlers; diff --git a/frontend/src/Store/Actions/artistEditorActions.js b/frontend/src/Store/Actions/artistEditorActions.js new file mode 100644 index 000000000..5dbaf4f26 --- /dev/null +++ b/frontend/src/Store/Actions/artistEditorActions.js @@ -0,0 +1,8 @@ +import { createAction } from 'redux-actions'; +import * as types from './actionTypes'; +import artistEditorActionHandlers from './artistEditorActionHandlers'; + +export const setArtistEditorSort = createAction(types.SET_ARTIST_EDITOR_SORT); +export const setArtistEditorFilter = createAction(types.SET_ARTIST_EDITOR_FILTER); +export const saveArtistEditor = artistEditorActionHandlers[types.SAVE_ARTIST_EDITOR]; +export const bulkDeleteArtist = artistEditorActionHandlers[types.BULK_DELETE_ARTIST]; diff --git a/frontend/src/Store/Actions/artistIndexActions.js b/frontend/src/Store/Actions/artistIndexActions.js new file mode 100644 index 000000000..2e15d6c0b --- /dev/null +++ b/frontend/src/Store/Actions/artistIndexActions.js @@ -0,0 +1,10 @@ +import { createAction } from 'redux-actions'; +import * as types from './actionTypes'; + +export const setArtistSort = createAction(types.SET_ARTIST_SORT); +export const setArtistFilter = createAction(types.SET_ARTIST_FILTER); +export const setArtistView = createAction(types.SET_ARTIST_VIEW); +export const setArtistTableOption = createAction(types.SET_ARTIST_TABLE_OPTION); +export const setArtistPosterOption = createAction(types.SET_ARTIST_POSTER_OPTION); +export const setArtistBannerOption = createAction(types.SET_ARTIST_BANNER_OPTION); +export const setArtistOverviewOption = createAction(types.SET_ARTIST_OVERVIEW_OPTION); diff --git a/frontend/src/Store/Actions/baseActions.js b/frontend/src/Store/Actions/baseActions.js new file mode 100644 index 000000000..e2d7e9d7e --- /dev/null +++ b/frontend/src/Store/Actions/baseActions.js @@ -0,0 +1,13 @@ +import { createAction } from 'redux-actions'; +import * as types from './actionTypes'; + +export const set = createAction(types.SET); + +export const update = createAction(types.UPDATE); +export const updateItem = createAction(types.UPDATE_ITEM); +export const updateServerSideCollection = createAction(types.UPDATE_SERVER_SIDE_COLLECTION); + +export const setSettingValue = createAction(types.SET_SETTING_VALUE); +export const clearPendingChanges = createAction(types.CLEAR_PENDING_CHANGES); + +export const removeItem = createAction(types.REMOVE_ITEM); diff --git a/frontend/src/Store/Actions/blacklistActionHandlers.js b/frontend/src/Store/Actions/blacklistActionHandlers.js new file mode 100644 index 000000000..0a4e4a1f6 --- /dev/null +++ b/frontend/src/Store/Actions/blacklistActionHandlers.js @@ -0,0 +1,17 @@ +import serverSideCollectionHandlers from 'Utilities/serverSideCollectionHandlers'; +import * as types from './actionTypes'; +import createServerSideCollectionHandlers from './Creators/createServerSideCollectionHandlers'; + +const blacklistActionHandlers = { + ...createServerSideCollectionHandlers('blacklist', '/blacklist', (state) => state, { + [serverSideCollectionHandlers.FETCH]: types.FETCH_BLACKLIST, + [serverSideCollectionHandlers.FIRST_PAGE]: types.GOTO_FIRST_BLACKLIST_PAGE, + [serverSideCollectionHandlers.PREVIOUS_PAGE]: types.GOTO_PREVIOUS_BLACKLIST_PAGE, + [serverSideCollectionHandlers.NEXT_PAGE]: types.GOTO_NEXT_BLACKLIST_PAGE, + [serverSideCollectionHandlers.LAST_PAGE]: types.GOTO_LAST_BLACKLIST_PAGE, + [serverSideCollectionHandlers.EXACT_PAGE]: types.GOTO_BLACKLIST_PAGE, + [serverSideCollectionHandlers.SORT]: types.SET_BLACKLIST_SORT + }) +}; + +export default blacklistActionHandlers; diff --git a/frontend/src/Store/Actions/blacklistActions.js b/frontend/src/Store/Actions/blacklistActions.js new file mode 100644 index 000000000..d931842d1 --- /dev/null +++ b/frontend/src/Store/Actions/blacklistActions.js @@ -0,0 +1,12 @@ +import { createAction } from 'redux-actions'; +import * as types from './actionTypes'; +import blacklistActionHandlers from './blacklistActionHandlers'; + +export const fetchBlacklist = blacklistActionHandlers[types.FETCH_BLACKLIST]; +export const gotoBlacklistFirstPage = blacklistActionHandlers[types.GOTO_FIRST_BLACKLIST_PAGE]; +export const gotoBlacklistPreviousPage = blacklistActionHandlers[types.GOTO_PREVIOUS_BLACKLIST_PAGE]; +export const gotoBlacklistNextPage = blacklistActionHandlers[types.GOTO_NEXT_BLACKLIST_PAGE]; +export const gotoBlacklistLastPage = blacklistActionHandlers[types.GOTO_LAST_BLACKLIST_PAGE]; +export const gotoBlacklistPage = blacklistActionHandlers[types.GOTO_BLACKLIST_PAGE]; +export const setBlacklistSort = blacklistActionHandlers[types.SET_BLACKLIST_SORT]; +export const setBlacklistTableOption = createAction(types.SET_BLACKLIST_TABLE_OPTION); diff --git a/frontend/src/Store/Actions/calendarActionHandlers.js b/frontend/src/Store/Actions/calendarActionHandlers.js new file mode 100644 index 000000000..bb619858a --- /dev/null +++ b/frontend/src/Store/Actions/calendarActionHandlers.js @@ -0,0 +1,264 @@ +import _ from 'lodash'; +import $ from 'jquery'; +import moment from 'moment'; +import { batchActions } from 'redux-batched-actions'; +import * as calendarViews from 'Calendar/calendarViews'; +import * as types from './actionTypes'; +import { set, update } from './baseActions'; +import { fetchCalendar } from './calendarActions'; + +const viewRanges = { + [calendarViews.DAY]: 'day', + [calendarViews.WEEK]: 'week', + [calendarViews.MONTH]: 'month', + [calendarViews.FORECAST]: 'day' +}; + +function getDays(start, end) { + const startTime = moment(start); + const endTime = moment(end); + const difference = endTime.diff(startTime, 'days'); + + // Difference is one less than the number of days we need to account for. + return _.times(difference + 1, (i) => { + return startTime.clone().add(i, 'days').toISOString(); + }); +} + +function getDates(time, view, firstDayOfWeek, dayCount) { + const weekName = firstDayOfWeek === 0 ? 'week' : 'isoWeek'; + + let start = time.clone().startOf('day'); + let end = time.clone().endOf('day'); + + if (view === calendarViews.WEEK) { + start = time.clone().startOf(weekName); + end = time.clone().endOf(weekName); + } + + if (view === calendarViews.FORECAST) { + start = time.clone().subtract(1, 'day').startOf('day'); + end = time.clone().add(dayCount - 2, 'days').endOf('day'); + } + + if (view === calendarViews.MONTH) { + start = time.clone().startOf('month').startOf(weekName); + end = time.clone().endOf('month').endOf(weekName); + } + + if (view === calendarViews.AGENDA) { + start = time.clone().subtract(1, 'day').startOf('day'); + end = time.clone().add(1, 'month').endOf('day'); + } + + return { + start: start.toISOString(), + end: end.toISOString(), + time: time.toISOString(), + dates: getDays(start, end) + }; +} + +function getPopulatableRange(startDate, endDate, view) { + switch (view) { + case calendarViews.DAY: + return { + start: moment(startDate).subtract(1, 'day').toISOString(), + end: moment(endDate).add(1, 'day').toISOString() + }; + case calendarViews.WEEK: + case calendarViews.FORECAST: + return { + start: moment(startDate).subtract(1, 'week').toISOString(), + end: moment(endDate).add(1, 'week').toISOString() + }; + default: + return { + start: startDate, + end: endDate + }; + } +} + +function isRangePopulated(start, end, state) { + const { + start: currentStart, + end: currentEnd, + view: currentView + } = state; + + if (!currentStart || !currentEnd) { + return false; + } + + const { + start: currentPopulatedStart, + end: currentPopulatedEnd + } = getPopulatableRange(currentStart, currentEnd, currentView); + + if ( + moment(start).isAfter(currentPopulatedStart) && + moment(start).isBefore(currentPopulatedEnd) + ) { + return true; + } + + return false; +} + +const section = 'calendar'; + +const calendarActionHandlers = { + [types.FETCH_CALENDAR]: function(payload) { + return function(dispatch, getState) { + const state = getState(); + const unmonitored = state.calendar.unmonitored; + + const { + time, + view + } = payload; + + const dayCount = state.calendar.dayCount; + const dates = getDates(moment(time), view, state.settings.ui.item.firstDayOfWeek, dayCount); + const { start, end } = getPopulatableRange(dates.start, dates.end, view); + const isPrePopulated = isRangePopulated(start, end, state.calendar); + + const basesAttrs = { + section, + isFetching: true + }; + + const attrs = isPrePopulated ? + { + view, + ...basesAttrs, + ...dates + } : + basesAttrs; + + dispatch(set(attrs)); + + const promise = $.ajax({ + url: '/calendar', + data: { + unmonitored, + start, + end + } + }); + + promise.done((data) => { + dispatch(batchActions([ + update({ section, data }), + + set({ + section, + view, + ...dates, + isFetching: false, + isPopulated: true, + error: null + }) + ])); + }); + + promise.fail((xhr) => { + dispatch(set({ + section, + isFetching: false, + isPopulated: false, + error: xhr + })); + }); + }; + }, + + [types.SET_CALENDAR_DAYS_COUNT]: function(payload) { + return function(dispatch, getState) { + if (payload.dayCount === getState().calendar.dayCount) { + return; + } + + dispatch(set({ + section, + dayCount: payload.dayCount + })); + + const state = getState(); + const { time, view } = state.calendar; + + dispatch(fetchCalendar({ time, view })); + }; + }, + + [types.SET_CALENDAR_INCLUDE_UNMONITORED]: function(payload) { + return function(dispatch, getState) { + dispatch(set({ + section, + unmonitored: payload.unmonitored + })); + + const state = getState(); + const { time, view } = state.calendar; + + dispatch(fetchCalendar({ time, view })); + }; + }, + + [types.SET_CALENDAR_VIEW]: function(payload) { + return function(dispatch, getState) { + const state = getState(); + const view = payload.view; + const time = view === calendarViews.FORECAST ? + moment() : + state.calendar.time; + + dispatch(fetchCalendar({ time, view })); + }; + }, + + [types.GOTO_CALENDAR_TODAY]: function(payload) { + return function(dispatch, getState) { + const state = getState(); + const view = state.calendar.view; + const time = moment(); + + dispatch(fetchCalendar({ time, view })); + }; + }, + + [types.GOTO_CALENDAR_PREVIOUS_RANGE]: function(payload) { + return function(dispatch, getState) { + const state = getState(); + + const { + view, + dayCount + } = state.calendar; + + const amount = view === calendarViews.FORECAST ? dayCount : 1; + const time = moment(state.calendar.time).subtract(amount, viewRanges[view]); + + dispatch(fetchCalendar({ time, view })); + }; + }, + + [types.GOTO_CALENDAR_NEXT_RANGE]: function(payload) { + return function(dispatch, getState) { + const state = getState(); + + const { + view, + dayCount + } = state.calendar; + + const amount = view === calendarViews.FORECAST ? dayCount : 1; + const time = moment(state.calendar.time).add(amount, viewRanges[view]); + + dispatch(fetchCalendar({ time, view })); + }; + } +}; + +export default calendarActionHandlers; diff --git a/frontend/src/Store/Actions/calendarActions.js b/frontend/src/Store/Actions/calendarActions.js new file mode 100644 index 000000000..452769343 --- /dev/null +++ b/frontend/src/Store/Actions/calendarActions.js @@ -0,0 +1,12 @@ +import { createAction } from 'redux-actions'; +import * as types from './actionTypes'; +import calendarActionHandlers from './calendarActionHandlers'; + +export const fetchCalendar = calendarActionHandlers[types.FETCH_CALENDAR]; +export const setCalendarDaysCount = calendarActionHandlers[types.SET_CALENDAR_DAYS_COUNT]; +export const setCalendarIncludeUnmonitored = calendarActionHandlers[types.SET_CALENDAR_INCLUDE_UNMONITORED]; +export const setCalendarView = calendarActionHandlers[types.SET_CALENDAR_VIEW]; +export const gotoCalendarToday = calendarActionHandlers[types.GOTO_CALENDAR_TODAY]; +export const gotoCalendarPreviousRange = calendarActionHandlers[types.GOTO_CALENDAR_PREVIOUS_RANGE]; +export const gotoCalendarNextRange = calendarActionHandlers[types.GOTO_CALENDAR_NEXT_RANGE]; +export const clearCalendar = createAction(types.CLEAR_CALENDAR); diff --git a/frontend/src/Store/Actions/captchaActionHandlers.js b/frontend/src/Store/Actions/captchaActionHandlers.js new file mode 100644 index 000000000..6e00a840e --- /dev/null +++ b/frontend/src/Store/Actions/captchaActionHandlers.js @@ -0,0 +1,67 @@ +import requestAction from 'Utilities/requestAction'; +import * as types from './actionTypes'; +import { setCaptchaValue } from './captchaActions'; + +const captchaActionHandlers = { + [types.REFRESH_CAPTCHA]: function(payload) { + return (dispatch, getState) => { + const actionPayload = { + action: 'checkCaptcha', + ...payload + }; + + dispatch(setCaptchaValue({ + refreshing: true + })); + + const promise = requestAction(actionPayload); + + promise.done((data) => { + if (!data.captchaRequest) { + dispatch(setCaptchaValue({ + refreshing: false + })); + } + + dispatch(setCaptchaValue({ + refreshing: false, + ...data.captchaRequest + })); + }); + + promise.fail(() => { + dispatch(setCaptchaValue({ + refreshing: false + })); + }); + }; + }, + + [types.GET_CAPTCHA_COOKIE]: function(payload) { + return (dispatch, getState) => { + const state = getState().captcha; + + const queryParams = { + responseUrl: state.responseUrl, + ray: state.ray, + captchaResponse: payload.captchaResponse + }; + + const actionPayload = { + action: 'getCaptchaCookie', + queryParams, + ...payload + }; + + const promise = requestAction(actionPayload); + + promise.done((data) => { + dispatch(setCaptchaValue({ + token: data.captchaToken + })); + }); + }; + } +}; + +export default captchaActionHandlers; diff --git a/frontend/src/Store/Actions/captchaActions.js b/frontend/src/Store/Actions/captchaActions.js new file mode 100644 index 000000000..e87a2a088 --- /dev/null +++ b/frontend/src/Store/Actions/captchaActions.js @@ -0,0 +1,8 @@ +import { createAction } from 'redux-actions'; +import * as types from './actionTypes'; +import captchaActionHandlers from './captchaActionHandlers'; + +export const refreshCaptcha = captchaActionHandlers[types.REFRESH_CAPTCHA]; +export const getCaptchaCookie = captchaActionHandlers[types.GET_CAPTCHA_COOKIE]; +export const setCaptchaValue = createAction(types.SET_CAPTCHA_VALUE); +export const resetCaptcha = createAction(types.RESET_CAPTCHA); diff --git a/frontend/src/Store/Actions/commandActionHandlers.js b/frontend/src/Store/Actions/commandActionHandlers.js new file mode 100644 index 000000000..e0f8baea0 --- /dev/null +++ b/frontend/src/Store/Actions/commandActionHandlers.js @@ -0,0 +1,141 @@ +import $ from 'jquery'; +import { batchActions } from 'redux-batched-actions'; +import { isSameCommand } from 'Utilities/Command'; +import { messageTypes } from 'Helpers/Props'; +import * as types from './actionTypes'; +import createFetchHandler from './Creators/createFetchHandler'; +import { showMessage, hideMessage } from './appActions'; +import { updateItem } from './baseActions'; +import { addCommand, removeCommand } from './commandActions'; + +let lastCommand = null; +let lastCommandTimeout = null; +const removeCommandTimeoutIds = {}; + +function showCommandMessage(payload, dispatch) { + const { + id, + name, + manual, + message, + body = {}, + state + } = payload; + + const { + sendUpdatesToClient, + suppressMessages + } = body; + + if (!message || !body || !sendUpdatesToClient || suppressMessages) { + return; + } + + let type = messageTypes.INFO; + let hideAfter = 0; + + if (state === 'completed') { + type = messageTypes.SUCCESS; + hideAfter = 4; + } else if (state === 'failed') { + type = messageTypes.ERROR; + hideAfter = manual ? 10 : 4; + } + + dispatch(showMessage({ + id, + name, + message, + type, + hideAfter + })); +} + +function scheduleRemoveCommand(command, dispatch) { + const { + id, + state + } = command; + + if (state === 'queued') { + return; + } + + const timeoutId = removeCommandTimeoutIds[id]; + + if (timeoutId) { + clearTimeout(timeoutId); + } + + removeCommandTimeoutIds[id] = setTimeout(() => { + dispatch(batchActions([ + removeCommand({ section: 'commands', id }), + hideMessage({ id }) + ])); + + delete removeCommandTimeoutIds[id]; + }, 30000); +} + +const commandActionHandlers = { + [types.FETCH_COMMANDS]: createFetchHandler('commands', '/command'), + + [types.EXECUTE_COMMAND](payload) { + return (dispatch, getState) => { + // TODO: show a message for the user + if (lastCommand && isSameCommand(lastCommand, payload)) { + console.warn('Please wait at least 5 seconds before running this command again'); + } + + lastCommand = payload; + + // clear last command after 5 seconds. + if (lastCommandTimeout) { + clearTimeout(lastCommandTimeout); + } + + lastCommandTimeout = setTimeout(() => { + lastCommand = null; + }, 5000); + + const promise = $.ajax({ + url: '/command', + method: 'POST', + data: JSON.stringify(payload) + }); + + promise.done((data) => { + dispatch(addCommand(data)); + }); + }; + }, + + [types.UPDATE_COMMAND](payload) { + return (dispatch, getState) => { + dispatch(updateItem({ section: 'commands', ...payload })); + + showCommandMessage(payload, dispatch); + scheduleRemoveCommand(payload, dispatch); + }; + }, + + [types.FINISH_COMMAND](payload) { + return (dispatch, getState) => { + const state = getState(); + const handlers = state.commands.handlers; + Object.keys(handlers).forEach((key) => { + const handler = handlers[key]; + + if (handler.name === payload.name) { + dispatch(handler.handler(payload)); + } + }); + + dispatch(removeCommand({ section: 'commands', ...payload })); + showCommandMessage(payload, dispatch); + }; + } + +}; + +export default commandActionHandlers; diff --git a/frontend/src/Store/Actions/commandActions.js b/frontend/src/Store/Actions/commandActions.js new file mode 100644 index 000000000..84b6d4fdb --- /dev/null +++ b/frontend/src/Store/Actions/commandActions.js @@ -0,0 +1,14 @@ +import { createAction } from 'redux-actions'; +import * as types from './actionTypes'; +import commandActionHandlers from './commandActionHandlers'; + +export const fetchCommands = commandActionHandlers[types.FETCH_COMMANDS]; +export const executeCommand = commandActionHandlers[types.EXECUTE_COMMAND]; +export const updateCommand = commandActionHandlers[types.UPDATE_COMMAND]; +export const finishCommand = commandActionHandlers[types.FINISH_COMMAND]; + +export const addCommand = createAction(types.ADD_COMMAND); +export const removeCommand = createAction(types.REMOVE_COMMAND); + +export const registerFinishCommandHandler = createAction(types.REGISTER_FINISH_COMMAND_HANDLER); +export const unregisterFinishCommandHandler = createAction(types.UNREGISTER_FINISH_COMMAND_HANDLER); diff --git a/frontend/src/Store/Actions/episodeActionHandlers.js b/frontend/src/Store/Actions/episodeActionHandlers.js new file mode 100644 index 000000000..b74f5c3fa --- /dev/null +++ b/frontend/src/Store/Actions/episodeActionHandlers.js @@ -0,0 +1,111 @@ +import _ from 'lodash'; +import $ from 'jquery'; +import { batchActions } from 'redux-batched-actions'; +import episodeEntities from 'Album/episodeEntities'; +import createFetchHandler from './Creators/createFetchHandler'; +import * as types from './actionTypes'; +import { updateItem } from './baseActions'; + +const section = 'episodes'; + +const episodeActionHandlers = { + [types.FETCH_EPISODES]: createFetchHandler(section, '/album'), + + [types.TOGGLE_EPISODE_MONITORED]: function(payload) { + return function(dispatch, getState) { + const { + albumId, + episodeEntity = episodeEntities.EPISODES, + monitored + } = payload; + + const episodeSection = _.last(episodeEntity.split('.')); + + dispatch(updateItem({ + id: albumId, + section: episodeSection, + isSaving: true + })); + + const promise = $.ajax({ + url: `/album/${albumId}`, + method: 'PUT', + data: JSON.stringify({ monitored }), + dataType: 'json' + }); + + promise.done((data) => { + dispatch(updateItem({ + id: albumId, + section: episodeSection, + isSaving: false, + monitored + })); + }); + + promise.fail((xhr) => { + dispatch(updateItem({ + id: albumId, + section: episodeSection, + isSaving: false + })); + }); + }; + }, + + [types.TOGGLE_EPISODES_MONITORED]: function(payload) { + return function(dispatch, getState) { + const { + albumIds, + episodeEntity = episodeEntities.EPISODES, + monitored + } = payload; + + const episodeSection = _.last(episodeEntity.split('.')); + + dispatch(batchActions( + albumIds.map((albumId) => { + return updateItem({ + id: albumId, + section: episodeSection, + isSaving: true + }); + }) + )); + + const promise = $.ajax({ + url: '/album/monitor', + method: 'PUT', + data: JSON.stringify({ albumIds, monitored }), + dataType: 'json' + }); + + promise.done((data) => { + dispatch(batchActions( + albumIds.map((albumId) => { + return updateItem({ + id: albumId, + section: episodeSection, + isSaving: false, + monitored + }); + }) + )); + }); + + promise.fail((xhr) => { + dispatch(batchActions( + albumIds.map((albumId) => { + return updateItem({ + id: albumId, + section: episodeSection, + isSaving: false + }); + }) + )); + }); + }; + } +}; + +export default episodeActionHandlers; diff --git a/frontend/src/Store/Actions/episodeActions.js b/frontend/src/Store/Actions/episodeActions.js new file mode 100644 index 000000000..b0abe85eb --- /dev/null +++ b/frontend/src/Store/Actions/episodeActions.js @@ -0,0 +1,10 @@ +import { createAction } from 'redux-actions'; +import * as types from './actionTypes'; +import episodeActionHandlers from './episodeActionHandlers'; + +export const fetchEpisodes = episodeActionHandlers[types.FETCH_EPISODES]; +export const setEpisodesSort = createAction(types.SET_EPISODES_SORT); +export const setEpisodesTableOption = createAction(types.SET_EPISODES_TABLE_OPTION); +export const clearEpisodes = createAction(types.CLEAR_EPISODES); +export const toggleEpisodeMonitored = episodeActionHandlers[types.TOGGLE_EPISODE_MONITORED]; +export const toggleEpisodesMonitored = episodeActionHandlers[types.TOGGLE_EPISODES_MONITORED]; diff --git a/frontend/src/Store/Actions/historyActionHandlers.js b/frontend/src/Store/Actions/historyActionHandlers.js new file mode 100644 index 000000000..44cff0fc3 --- /dev/null +++ b/frontend/src/Store/Actions/historyActionHandlers.js @@ -0,0 +1,60 @@ +import $ from 'jquery'; +import serverSideCollectionHandlers from 'Utilities/serverSideCollectionHandlers'; +import createServerSideCollectionHandlers from './Creators/createServerSideCollectionHandlers'; +import * as types from './actionTypes'; +import { updateItem } from './baseActions'; + +const section = 'history'; + +const historyActionHandlers = { + ...createServerSideCollectionHandlers('history', '/history', (state) => state.history, { + [serverSideCollectionHandlers.FETCH]: types.FETCH_HISTORY, + [serverSideCollectionHandlers.FIRST_PAGE]: types.GOTO_FIRST_HISTORY_PAGE, + [serverSideCollectionHandlers.PREVIOUS_PAGE]: types.GOTO_PREVIOUS_HISTORY_PAGE, + [serverSideCollectionHandlers.NEXT_PAGE]: types.GOTO_NEXT_HISTORY_PAGE, + [serverSideCollectionHandlers.LAST_PAGE]: types.GOTO_LAST_HISTORY_PAGE, + [serverSideCollectionHandlers.EXACT_PAGE]: types.GOTO_HISTORY_PAGE, + [serverSideCollectionHandlers.SORT]: types.SET_HISTORY_SORT, + [serverSideCollectionHandlers.FILTER]: types.SET_HISTORY_FILTER + }), + + [types.MARK_AS_FAILED]: function(payload) { + return function(dispatch, getState) { + const id = payload.id; + + dispatch(updateItem({ + section, + id, + isMarkingAsFailed: true + })); + + const promise = $.ajax({ + url: '/history/failed', + method: 'POST', + data: { + id + } + }); + + promise.done(() => { + dispatch(updateItem({ + section, + id, + isMarkingAsFailed: false, + markAsFailedError: null + })); + }); + + promise.fail((xhr) => { + dispatch(updateItem({ + section, + id, + isMarkingAsFailed: false, + markAsFailedError: xhr + })); + }); + }; + } +}; + +export default historyActionHandlers; diff --git a/frontend/src/Store/Actions/historyActions.js b/frontend/src/Store/Actions/historyActions.js new file mode 100644 index 000000000..d4fe71be9 --- /dev/null +++ b/frontend/src/Store/Actions/historyActions.js @@ -0,0 +1,16 @@ +import { createAction } from 'redux-actions'; +import * as types from './actionTypes'; +import historyActionHandlers from './historyActionHandlers'; + +export const fetchHistory = historyActionHandlers[types.FETCH_HISTORY]; +export const gotoHistoryFirstPage = historyActionHandlers[types.GOTO_FIRST_HISTORY_PAGE]; +export const gotoHistoryPreviousPage = historyActionHandlers[types.GOTO_PREVIOUS_HISTORY_PAGE]; +export const gotoHistoryNextPage = historyActionHandlers[types.GOTO_NEXT_HISTORY_PAGE]; +export const gotoHistoryLastPage = historyActionHandlers[types.GOTO_LAST_HISTORY_PAGE]; +export const gotoHistoryPage = historyActionHandlers[types.GOTO_HISTORY_PAGE]; +export const setHistorySort = historyActionHandlers[types.SET_HISTORY_SORT]; +export const setHistoryFilter = historyActionHandlers[types.SET_HISTORY_FILTER]; +export const setHistoryTableOption = createAction(types.SET_HISTORY_TABLE_OPTION); +export const clearHistory = createAction(types.CLEAR_HISTORY); + +export const markAsFailed = historyActionHandlers[types.MARK_AS_FAILED]; diff --git a/frontend/src/Store/Actions/importArtistActionHandlers.js b/frontend/src/Store/Actions/importArtistActionHandlers.js new file mode 100644 index 000000000..3fd7099e1 --- /dev/null +++ b/frontend/src/Store/Actions/importArtistActionHandlers.js @@ -0,0 +1,172 @@ +import _ from 'lodash'; +import $ from 'jquery'; +import { batchActions } from 'redux-batched-actions'; +import getNewSeries from 'Utilities/Series/getNewSeries'; +import * as types from './actionTypes'; +import { set, updateItem, removeItem } from './baseActions'; +import { startLookupArtist } from './importArtistActions'; +import { fetchRootFolders } from './rootFolderActions'; + +const section = 'importArtist'; +let concurrentLookups = 0; + +const importArtistActionHandlers = { + [types.QUEUE_LOOKUP_ARTIST]: function(payload) { + return function(dispatch, getState) { + const { + name, + path, + term + } = payload; + + const state = getState().importArtist; + const item = _.find(state.items, { id: name }) || { + id: name, + term, + path, + isFetching: false, + isPopulated: false, + error: null + }; + + dispatch(updateItem({ + section, + ...item, + term, + queued: true, + items: [] + })); + + if (term && term.length > 2) { + dispatch(startLookupArtist()); + } + }; + }, + + [types.START_LOOKUP_ARTIST]: function(payload) { + return function(dispatch, getState) { + if (concurrentLookups >= 1) { + return; + } + + const state = getState().importArtist; + const queued = _.find(state.items, { queued: true }); + + if (!queued) { + return; + } + + concurrentLookups++; + + dispatch(updateItem({ + section, + id: queued.id, + isFetching: true + })); + + const promise = $.ajax({ + url: '/artist/lookup', + data: { + term: queued.term + } + }); + + promise.done((data) => { + dispatch(updateItem({ + section, + id: queued.id, + isFetching: false, + isPopulated: true, + error: null, + items: data, + queued: false, + selectedArtist: queued.selectedArtist || data[0] + })); + }); + + promise.fail((xhr) => { + dispatch(updateItem({ + section, + id: queued.id, + isFetching: false, + isPopulated: false, + error: xhr, + queued: false + })); + }); + + promise.always(() => { + concurrentLookups--; + dispatch(startLookupArtist()); + }); + }; + }, + + [types.IMPORT_ARTIST]: function(payload) { + return function(dispatch, getState) { + dispatch(set({ section, isImporting: true })); + + const ids = payload.ids; + const items = getState().importArtist.items; + const addedIds = []; + + const allNewSeries = ids.reduce((acc, id) => { + const item = _.find(items, { id }); + const selectedArtist = item.selectedArtist; + + // Make sure we have a selected artist and + // the same artist hasn't been added yet. + if (selectedArtist && !_.some(acc, { foreignArtistId: selectedArtist.foreignArtistId })) { + const newSeries = getNewSeries(_.cloneDeep(selectedArtist), item); + newSeries.path = item.path; + + addedIds.push(id); + acc.push(newSeries); + } + + return acc; + }, []); + + const promise = $.ajax({ + url: '/artist/import', + method: 'POST', + contentType: 'application/json', + data: JSON.stringify(allNewSeries) + }); + + promise.done((data) => { + dispatch(batchActions([ + set({ + section, + isImporting: false, + isImported: true + }), + + ...data.map((artist) => updateItem({ section: 'artist', ...artist })), + + ...addedIds.map((id) => removeItem({ section, id })) + ])); + + dispatch(fetchRootFolders()); + }); + + promise.fail((xhr) => { + dispatch(batchActions( + set({ + section, + isImporting: false, + isImported: true + }), + + addedIds.map((id) => updateItem({ + section, + id, + importError: xhr + })) + )); + }); + }; + } +}; + +export default importArtistActionHandlers; diff --git a/frontend/src/Store/Actions/importArtistActions.js b/frontend/src/Store/Actions/importArtistActions.js new file mode 100644 index 000000000..7bbf8a78b --- /dev/null +++ b/frontend/src/Store/Actions/importArtistActions.js @@ -0,0 +1,16 @@ +import { createAction } from 'redux-actions'; +import * as types from './actionTypes'; +import importArtistActionHandlers from './importArtistActionHandlers'; + +export const queueLookupArtist = importArtistActionHandlers[types.QUEUE_LOOKUP_ARTIST]; +export const startLookupArtist = importArtistActionHandlers[types.START_LOOKUP_ARTIST]; +export const importArtist = importArtistActionHandlers[types.IMPORT_ARTIST]; +export const clearImportArtist = createAction(types.CLEAR_IMPORT_ARTIST); + +export const setImportArtistValue = createAction(types.SET_IMPORT_ARTIST_VALUE, (payload) => { + return { + + section: 'importArtist', + ...payload + }; +}); diff --git a/frontend/src/Store/Actions/interactiveImportActionHandlers.js b/frontend/src/Store/Actions/interactiveImportActionHandlers.js new file mode 100644 index 000000000..811916bc0 --- /dev/null +++ b/frontend/src/Store/Actions/interactiveImportActionHandlers.js @@ -0,0 +1,51 @@ +import $ from 'jquery'; +import { batchActions } from 'redux-batched-actions'; +import createFetchHandler from './Creators/createFetchHandler'; +import * as types from './actionTypes'; +import { set, update } from './baseActions'; + +const section = 'interactiveImport'; + +const interactiveImportActionHandlers = { + [types.FETCH_INTERACTIVE_IMPORT_ITEMS]: function(payload) { + return function(dispatch, getState) { + if (!payload.downloadId && !payload.folder) { + dispatch(set({ section, error: { message: '`downloadId` or `folder` is required.' } })); + return; + } + + dispatch(set({ section, isFetching: true })); + + const promise = $.ajax({ + url: '/manualimport', + data: payload + }); + + promise.done((data) => { + dispatch(batchActions([ + update({ section, data }), + + set({ + section, + isFetching: false, + isPopulated: true, + error: null + }) + ])); + }); + + promise.fail((xhr) => { + dispatch(set({ + section, + isFetching: false, + isPopulated: false, + error: xhr + })); + }); + }; + }, + + [types.FETCH_INTERACTIVE_IMPORT_ALBUMS]: createFetchHandler('interactiveImportAlbums', '/album') +}; + +export default interactiveImportActionHandlers; diff --git a/frontend/src/Store/Actions/interactiveImportActions.js b/frontend/src/Store/Actions/interactiveImportActions.js new file mode 100644 index 000000000..2a8da6efb --- /dev/null +++ b/frontend/src/Store/Actions/interactiveImportActions.js @@ -0,0 +1,15 @@ +import { createAction } from 'redux-actions'; +import * as types from './actionTypes'; +import interactiveImportActionHandlers from './interactiveImportActionHandlers'; + +export const fetchInteractiveImportItems = interactiveImportActionHandlers[types.FETCH_INTERACTIVE_IMPORT_ITEMS]; +export const setInteractiveImportSort = createAction(types.SET_INTERACTIVE_IMPORT_SORT); +export const updateInteractiveImportItem = createAction(types.UPDATE_INTERACTIVE_IMPORT_ITEM); +export const clearInteractiveImport = createAction(types.CLEAR_INTERACTIVE_IMPORT); +export const addRecentFolder = createAction(types.ADD_RECENT_FOLDER); +export const removeRecentFolder = createAction(types.REMOVE_RECENT_FOLDER); +export const setInteractiveImportMode = createAction(types.SET_INTERACTIVE_IMPORT_MODE); + +export const fetchInteractiveImportAlbums = interactiveImportActionHandlers[types.FETCH_INTERACTIVE_IMPORT_ALBUMS]; +export const setInteractiveImportAlbumsSort = createAction(types.SET_INTERACTIVE_IMPORT_ALBUMS_SORT); +export const clearInteractiveImportAlbums = createAction(types.CLEAR_INTERACTIVE_IMPORT_ALBUMS); diff --git a/frontend/src/Store/Actions/oAuthActionHandlers.js b/frontend/src/Store/Actions/oAuthActionHandlers.js new file mode 100644 index 000000000..aa2d5e038 --- /dev/null +++ b/frontend/src/Store/Actions/oAuthActionHandlers.js @@ -0,0 +1,81 @@ +/* eslint callback-return: 0 */ +import _ from 'lodash'; +import $ from 'jquery'; +import requestAction from 'Utilities/requestAction'; +import * as types from './actionTypes'; +import { setOAuthValue } from './oAuthActions'; + +function showOAuthWindow(url) { + const deferred = $.Deferred(); + const selfWindow = window; + + window.open(url); + + selfWindow.onCompleteOauth = function(query, callback) { + delete selfWindow.onCompleteOauth; + + const queryParams = {}; + const splitQuery = query.substring(1).split('&'); + + _.each(splitQuery, (param) => { + const paramSplit = param.split('='); + + queryParams[paramSplit[0]] = paramSplit[1]; + }); + + callback(); + deferred.resolve(queryParams); + }; + + return deferred.promise(); +} + +const oAuthActionHandlers = { + + [types.START_OAUTH]: function(payload) { + return (dispatch, getState) => { + const actionPayload = { + action: 'startOAuth', + queryParams: { callbackUrl: `${window.location.origin}/oauth.html` }, + ...payload + }; + + dispatch(setOAuthValue({ + authorizing: true + })); + + const promise = requestAction(actionPayload) + .then((response) => { + return showOAuthWindow(response.oauthUrl); + }) + .then((queryParams) => { + return requestAction({ + action: 'getOAuthToken', + queryParams, + ...payload + }); + }) + .then((response) => { + const { + accessToken, + accessTokenSecret + } = response; + + dispatch(setOAuthValue({ + authorizing: false, + accessToken, + accessTokenSecret + })); + }); + + promise.fail(() => { + dispatch(setOAuthValue({ + authorizing: false + })); + }); + }; + } + +}; + +export default oAuthActionHandlers; diff --git a/frontend/src/Store/Actions/oAuthActions.js b/frontend/src/Store/Actions/oAuthActions.js new file mode 100644 index 000000000..93de6f11f --- /dev/null +++ b/frontend/src/Store/Actions/oAuthActions.js @@ -0,0 +1,7 @@ +import { createAction } from 'redux-actions'; +import * as types from './actionTypes'; +import oAuthActionHandlers from './oAuthActionHandlers'; + +export const startOAuth = oAuthActionHandlers[types.START_OAUTH]; +export const setOAuthValue = createAction(types.SET_OAUTH_VALUE); +export const resetOAuth = createAction(types.RESET_OAUTH); diff --git a/frontend/src/Store/Actions/organizePreviewActionHandlers.js b/frontend/src/Store/Actions/organizePreviewActionHandlers.js new file mode 100644 index 000000000..d0901b5ea --- /dev/null +++ b/frontend/src/Store/Actions/organizePreviewActionHandlers.js @@ -0,0 +1,8 @@ +import createFetchHandler from './Creators/createFetchHandler'; +import * as types from './actionTypes'; + +const organizePreviewActionHandlers = { + [types.FETCH_ORGANIZE_PREVIEW]: createFetchHandler('organizePreview', '/rename') +}; + +export default organizePreviewActionHandlers; diff --git a/frontend/src/Store/Actions/organizePreviewActions.js b/frontend/src/Store/Actions/organizePreviewActions.js new file mode 100644 index 000000000..602028ff4 --- /dev/null +++ b/frontend/src/Store/Actions/organizePreviewActions.js @@ -0,0 +1,6 @@ +import { createAction } from 'redux-actions'; +import * as types from './actionTypes'; +import organizePreviewActionHandlers from './organizePreviewActionHandlers'; + +export const fetchOrganizePreview = organizePreviewActionHandlers[types.FETCH_ORGANIZE_PREVIEW]; +export const clearOrganizePreview = createAction(types.CLEAR_ORGANIZE_PREVIEW); diff --git a/frontend/src/Store/Actions/pathActionHandlers.js b/frontend/src/Store/Actions/pathActionHandlers.js new file mode 100644 index 000000000..11ebfaf21 --- /dev/null +++ b/frontend/src/Store/Actions/pathActionHandlers.js @@ -0,0 +1,43 @@ +import $ from 'jquery'; +import * as types from './actionTypes'; +import { set } from './baseActions'; +import { updatePaths } from './pathActions'; + +const section = 'paths'; + +const pathActionHandlers = { + [types.FETCH_PATHS](payload) { + return (dispatch, getState) => { + dispatch(set({ section, isFetching: true })); + + const promise = $.ajax({ + url: '/filesystem', + data: { + path: payload.path + } + }); + + promise.done((data) => { + dispatch(updatePaths({ path: payload.path, ...data })); + + dispatch(set({ + section, + isFetching: false, + isPopulated: true, + error: null + })); + }); + + promise.fail((xhr) => { + dispatch(set({ + section, + isFetching: false, + isPopulated: false, + error: xhr + })); + }); + }; + } +}; + +export default pathActionHandlers; diff --git a/frontend/src/Store/Actions/pathActions.js b/frontend/src/Store/Actions/pathActions.js new file mode 100644 index 000000000..fd4e76f02 --- /dev/null +++ b/frontend/src/Store/Actions/pathActions.js @@ -0,0 +1,7 @@ +import { createAction } from 'redux-actions'; +import * as types from './actionTypes'; +import pathActionHandlers from './pathActionHandlers'; + +export const fetchPaths = pathActionHandlers[types.FETCH_PATHS]; +export const updatePaths = createAction(types.UPDATE_PATHS); +export const clearPaths = createAction(types.CLEAR_PATHS); diff --git a/frontend/src/Store/Actions/queueActionHandlers.js b/frontend/src/Store/Actions/queueActionHandlers.js new file mode 100644 index 000000000..494942b61 --- /dev/null +++ b/frontend/src/Store/Actions/queueActionHandlers.js @@ -0,0 +1,233 @@ +import _ from 'lodash'; +import $ from 'jquery'; +import { batchActions } from 'redux-batched-actions'; +import serverSideCollectionHandlers from 'Utilities/serverSideCollectionHandlers'; +import createFetchHandler from './Creators/createFetchHandler'; +import createServerSideCollectionHandlers from './Creators/createServerSideCollectionHandlers'; +import * as types from './actionTypes'; +import { set, updateItem } from './baseActions'; +import { fetchQueue } from './queueActions'; + +const fetchQueueDetailsHandler = createFetchHandler('details', '/queue/details'); + +const queueActionHandlers = { + [types.FETCH_QUEUE_STATUS]: createFetchHandler('queueStatus', '/queue/status'), + + [types.FETCH_QUEUE_DETAILS]: function(payload) { + return function(dispatch, getState) { + let params = payload; + + // If the payload params are empty try to get params from state. + + if (params && !_.isEmpty(params)) { + dispatch(set({ section: 'details', params })); + } else { + params = getState().queue.details.params; + } + + // Ensure there are params before trying to fetch the queue + // so we don't make a bad request to the server. + + if (params && !_.isEmpty(params)) { + const fetchFunction = fetchQueueDetailsHandler(params); + fetchFunction(dispatch, getState); + } + }; + }, + + ...createServerSideCollectionHandlers('paged', '/queue', (state) => state.queue, { + [serverSideCollectionHandlers.FETCH]: types.FETCH_QUEUE, + [serverSideCollectionHandlers.FIRST_PAGE]: types.GOTO_FIRST_QUEUE_PAGE, + [serverSideCollectionHandlers.PREVIOUS_PAGE]: types.GOTO_PREVIOUS_QUEUE_PAGE, + [serverSideCollectionHandlers.NEXT_PAGE]: types.GOTO_NEXT_QUEUE_PAGE, + [serverSideCollectionHandlers.LAST_PAGE]: types.GOTO_LAST_QUEUE_PAGE, + [serverSideCollectionHandlers.EXACT_PAGE]: types.GOTO_QUEUE_PAGE, + [serverSideCollectionHandlers.SORT]: types.SET_QUEUE_SORT + }), + + [types.GRAB_QUEUE_ITEM]: function(payload) { + const section = 'paged'; + + const { + id + } = payload; + + return function(dispatch, getState) { + dispatch(updateItem({ section, id, isGrabbing: true })); + + const promise = $.ajax({ + url: `/queue/grab/${id}`, + method: 'POST' + }); + + promise.done((data) => { + dispatch(batchActions([ + dispatch(fetchQueue()), + + set({ + section, + isGrabbing: false, + grabError: null + }) + ])); + }); + + promise.fail((xhr) => { + dispatch(updateItem({ + section, + id, + isGrabbing: false, + grabError: xhr + })); + }); + }; + }, + + [types.GRAB_QUEUE_ITEMS]: function(payload) { + const section = 'paged'; + + const { + ids + } = payload; + + return function(dispatch, getState) { + dispatch(batchActions([ + ...ids.map((id) => { + return updateItem({ + section, + id, + isGrabbing: true + }); + }), + + set({ + section, + isGrabbing: true + }) + ])); + + const promise = $.ajax({ + url: '/queue/grab/bulk', + method: 'POST', + dataType: 'json', + data: JSON.stringify(payload) + }); + + promise.done((data) => { + dispatch(batchActions([ + dispatch(fetchQueue()), + + ...ids.map((id) => { + return updateItem({ + section, + id, + isGrabbing: false, + grabError: null + }); + }), + + set({ + section, + isGrabbing: false, + grabError: null + }) + ])); + }); + + promise.fail((xhr) => { + dispatch(batchActions([ + ...ids.map((id) => { + return updateItem({ + section, + id, + isGrabbing: false, + grabError: null + }); + }), + + set({ section, isGrabbing: false }) + ])); + }); + }; + }, + + [types.REMOVE_QUEUE_ITEM]: function(payload) { + const section = 'paged'; + + const { + id, + blacklist + } = payload; + + return function(dispatch, getState) { + dispatch(updateItem({ section, id, isRemoving: true })); + + const promise = $.ajax({ + url: `/queue/${id}?blacklist=${blacklist}`, + method: 'DELETE' + }); + + promise.done((data) => { + dispatch(fetchQueue()); + }); + + promise.fail((xhr) => { + dispatch(updateItem({ section, id, isRemoving: false })); + }); + }; + }, + + [types.REMOVE_QUEUE_ITEMS]: function(payload) { + const section = 'paged'; + + const { + ids, + blacklist + } = payload; + + return function(dispatch, getState) { + dispatch(batchActions([ + ...ids.map((id) => { + return updateItem({ + section, + id, + isRemoving: true + }); + }), + + set({ section, isRemoving: true }) + ])); + + const promise = $.ajax({ + url: `/queue/bulk?blacklist=${blacklist}`, + method: 'DELETE', + dataType: 'json', + data: JSON.stringify({ ids }) + }); + + promise.done((data) => { + dispatch(batchActions([ + set({ section, isRemoving: false }), + fetchQueue() + ])); + }); + + promise.fail((xhr) => { + dispatch(batchActions([ + ...ids.map((id) => { + return updateItem({ + section, + id, + isRemoving: false + }); + }), + + set({ section, isRemoving: false }) + ])); + }); + }; + } + +}; + +export default queueActionHandlers; diff --git a/frontend/src/Store/Actions/queueActions.js b/frontend/src/Store/Actions/queueActions.js new file mode 100644 index 000000000..51dda41df --- /dev/null +++ b/frontend/src/Store/Actions/queueActions.js @@ -0,0 +1,23 @@ +import { createAction } from 'redux-actions'; +import * as types from './actionTypes'; +import queueActionHandlers from './queueActionHandlers'; + +export const fetchQueueStatus = queueActionHandlers[types.FETCH_QUEUE_STATUS]; + +export const fetchQueueDetails = queueActionHandlers[types.FETCH_QUEUE_DETAILS]; +export const clearQueueDetails = createAction(types.CLEAR_QUEUE_DETAILS); + +export const fetchQueue = queueActionHandlers[types.FETCH_QUEUE]; +export const gotoQueueFirstPage = queueActionHandlers[types.GOTO_FIRST_QUEUE_PAGE]; +export const gotoQueuePreviousPage = queueActionHandlers[types.GOTO_PREVIOUS_QUEUE_PAGE]; +export const gotoQueueNextPage = queueActionHandlers[types.GOTO_NEXT_QUEUE_PAGE]; +export const gotoQueueLastPage = queueActionHandlers[types.GOTO_LAST_QUEUE_PAGE]; +export const gotoQueuePage = queueActionHandlers[types.GOTO_QUEUE_PAGE]; +export const setQueueSort = queueActionHandlers[types.SET_QUEUE_SORT]; +export const setQueueTableOption = createAction(types.SET_QUEUE_TABLE_OPTION); +export const clearQueue = createAction(types.CLEAR_QUEUE); + +export const grabQueueItem = queueActionHandlers[types.GRAB_QUEUE_ITEM]; +export const grabQueueItems = queueActionHandlers[types.GRAB_QUEUE_ITEMS]; +export const removeQueueItem = queueActionHandlers[types.REMOVE_QUEUE_ITEM]; +export const removeQueueItems = queueActionHandlers[types.REMOVE_QUEUE_ITEMS]; diff --git a/frontend/src/Store/Actions/releaseActionHandlers.js b/frontend/src/Store/Actions/releaseActionHandlers.js new file mode 100644 index 000000000..15ebaa2b0 --- /dev/null +++ b/frontend/src/Store/Actions/releaseActionHandlers.js @@ -0,0 +1,47 @@ +import $ from 'jquery'; +import createFetchHandler from './Creators/createFetchHandler'; +import * as types from './actionTypes'; +import { updateRelease } from './releaseActions'; + +const section = 'releases'; + +const releaseActionHandlers = { + [types.FETCH_RELEASES]: createFetchHandler(section, '/release'), + + [types.GRAB_RELEASE]: function(payload) { + return function(dispatch, getState) { + const guid = payload.guid; + + dispatch(updateRelease({ guid, isGrabbing: true })); + + const promise = $.ajax({ + url: '/release', + method: 'POST', + contentType: 'application/json', + data: JSON.stringify(payload) + }); + + promise.done((data) => { + dispatch(updateRelease({ + guid, + isGrabbing: false, + isGrabbed: true, + grabError: null + })); + }); + + promise.fail((xhr) => { + const grabError = xhr.responseJSON && xhr.responseJSON.message || 'Failed to add to download queue'; + + dispatch(updateRelease({ + guid, + isGrabbing: false, + isGrabbed: false, + grabError + })); + }); + }; + } +}; + +export default releaseActionHandlers; diff --git a/frontend/src/Store/Actions/releaseActions.js b/frontend/src/Store/Actions/releaseActions.js new file mode 100644 index 000000000..580ffe804 --- /dev/null +++ b/frontend/src/Store/Actions/releaseActions.js @@ -0,0 +1,9 @@ +import { createAction } from 'redux-actions'; +import * as types from './actionTypes'; +import releaseActionHandlers from './releaseActionHandlers'; + +export const fetchReleases = releaseActionHandlers[types.FETCH_RELEASES]; +export const setReleasesSort = createAction(types.SET_RELEASES_SORT); +export const clearReleases = createAction(types.CLEAR_RELEASES); +export const grabRelease = releaseActionHandlers[types.GRAB_RELEASE]; +export const updateRelease = createAction(types.UPDATE_RELEASE); diff --git a/frontend/src/Store/Actions/rootFolderActionHandlers.js b/frontend/src/Store/Actions/rootFolderActionHandlers.js new file mode 100644 index 000000000..bf0800f31 --- /dev/null +++ b/frontend/src/Store/Actions/rootFolderActionHandlers.js @@ -0,0 +1,60 @@ +import $ from 'jquery'; +import { batchActions } from 'redux-batched-actions'; +import * as types from './actionTypes'; +import createFetchHandler from './Creators/createFetchHandler'; +import createRemoveItemHandler from './Creators/createRemoveItemHandler'; +import { set, updateItem } from './baseActions'; + +const section = 'rootFolders'; + +const rootFolderActionHandlers = { + [types.FETCH_ROOT_FOLDERS]: createFetchHandler('rootFolders', '/rootFolder'), + + [types.DELETE_ROOT_FOLDER]: createRemoveItemHandler( + 'rootFolders', + '/rootFolder', + (state) => state.rootFolders), + + [types.ADD_ROOT_FOLDER]: function(payload) { + return function(dispatch, getState) { + const path = payload.path; + + dispatch(set({ + section, + isSaving: true + })); + + const promise = $.ajax({ + url: '/rootFolder', + method: 'POST', + data: JSON.stringify({ path }), + dataType: 'json' + }); + + promise.done((data) => { + dispatch(batchActions([ + updateItem({ + section, + ...data + }), + + set({ + section, + isSaving: false, + saveError: null + }) + ])); + }); + + promise.fail((xhr) => { + dispatch(set({ + section, + isSaving: false, + saveError: xhr + })); + }); + }; + } +}; + +export default rootFolderActionHandlers; diff --git a/frontend/src/Store/Actions/rootFolderActions.js b/frontend/src/Store/Actions/rootFolderActions.js new file mode 100644 index 000000000..0d0b8112a --- /dev/null +++ b/frontend/src/Store/Actions/rootFolderActions.js @@ -0,0 +1,6 @@ +import * as types from './actionTypes'; +import rootFolderActionHandlers from './rootFolderActionHandlers'; + +export const fetchRootFolders = rootFolderActionHandlers[types.FETCH_ROOT_FOLDERS]; +export const addRootFolder = rootFolderActionHandlers[types.ADD_ROOT_FOLDER]; +export const deleteRootFolder = rootFolderActionHandlers[types.DELETE_ROOT_FOLDER]; diff --git a/frontend/src/Store/Actions/settingsActionHandlers.js b/frontend/src/Store/Actions/settingsActionHandlers.js new file mode 100644 index 000000000..abf1bdd90 --- /dev/null +++ b/frontend/src/Store/Actions/settingsActionHandlers.js @@ -0,0 +1,273 @@ +import _ from 'lodash'; +import $ from 'jquery'; +import { batchActions } from 'redux-batched-actions'; +import * as types from './actionTypes'; +import createFetchHandler from './Creators/createFetchHandler'; +import createFetchSchemaHandler from './Creators/createFetchSchemaHandler'; +import createSaveHandler from './Creators/createSaveHandler'; +import createSaveProviderHandler, { createCancelSaveProviderHandler } from './Creators/createSaveProviderHandler'; +import createRemoveItemHandler from './Creators/createRemoveItemHandler'; +import createTestProviderHandler, { createCancelTestProviderHandler } from './Creators/createTestProviderHandler'; +import { set, update, clearPendingChanges } from './baseActions'; + +const settingsActionHandlers = { + [types.FETCH_UI_SETTINGS]: createFetchHandler('ui', '/config/ui'), + [types.SAVE_UI_SETTINGS]: createSaveHandler('ui', '/config/ui', (state) => state.settings.ui), + + [types.FETCH_MEDIA_MANAGEMENT_SETTINGS]: createFetchHandler('mediaManagement', '/config/mediamanagement'), + [types.SAVE_MEDIA_MANAGEMENT_SETTINGS]: createSaveHandler('mediaManagement', '/config/mediamanagement', (state) => state.settings.mediaManagement), + + [types.FETCH_NAMING_SETTINGS]: createFetchHandler('naming', '/config/naming'), + [types.SAVE_NAMING_SETTINGS]: createSaveHandler('naming', '/config/naming', (state) => state.settings.naming), + + [types.FETCH_NAMING_EXAMPLES]: function(payload) { + const section = 'namingExamples'; + + return function(dispatch, getState) { + dispatch(set({ section, isFetching: true })); + + const naming = getState().settings.naming; + + const promise = $.ajax({ + url: '/config/naming/examples', + data: Object.assign({}, naming.item, naming.pendingChanges) + }); + + promise.done((data) => { + dispatch(batchActions([ + update({ section, data }), + + set({ + section, + isFetching: false, + isPopulated: true, + error: null + }) + ])); + }); + + promise.fail((xhr) => { + dispatch(set({ + section, + isFetching: false, + isPopulated: false, + error: xhr + })); + }); + }; + }, + + [types.REORDER_DELAY_PROFILE]: function(payload) { + const section = 'delayProfiles'; + + return function(dispatch, getState) { + const { id, moveIndex } = payload; + const moveOrder = moveIndex + 1; + const delayProfiles = getState().settings.delayProfiles.items; + const moving = _.find(delayProfiles, { id }); + + // Don't move if the order hasn't changed + if (moving.order === moveOrder) { + return; + } + + const after = moveIndex > 0 ? _.find(delayProfiles, { order: moveIndex }) : null; + const afterQueryParam = after ? `after=${after.id}` : ''; + + const promise = $.ajax({ + method: 'PUT', + url: `/delayprofile/reorder/${id}?${afterQueryParam}` + }); + + promise.done((data) => { + dispatch(update({ section, data })); + }); + }; + }, + + [types.FETCH_QUALITY_PROFILES]: createFetchHandler('qualityProfiles', '/qualityprofile'), + [types.FETCH_QUALITY_PROFILE_SCHEMA]: createFetchSchemaHandler('qualityProfiles', '/qualityprofile/schema'), + + [types.SAVE_QUALITY_PROFILE]: createSaveProviderHandler( + 'qualityProfiles', + '/qualityprofile', + (state) => state.settings.qualityProfiles), + + [types.DELETE_QUALITY_PROFILE]: createRemoveItemHandler( + 'qualityProfiles', + '/qualityprofile', + (state) => state.settings.qualityProfiles), + + [types.FETCH_LANGUAGE_PROFILES]: createFetchHandler('languageProfiles', '/languageprofile'), + [types.FETCH_LANGUAGE_PROFILE_SCHEMA]: createFetchSchemaHandler('languageProfiles', '/languageprofile/schema'), + + [types.SAVE_LANGUAGE_PROFILE]: createSaveProviderHandler( + 'languageProfiles', + '/languageprofile', + (state) => state.settings.languageProfiles), + + [types.DELETE_LANGUAGE_PROFILE]: createRemoveItemHandler( + 'languageProfiles', + '/languageprofile', + (state) => state.settings.languageProfiles), + + [types.FETCH_DELAY_PROFILES]: createFetchHandler('delayProfiles', '/delayprofile'), + + [types.SAVE_DELAY_PROFILE]: createSaveProviderHandler( + 'delayProfiles', + '/delayprofile', + (state) => state.settings.delayProfiles), + + [types.DELETE_DELAY_PROFILE]: createRemoveItemHandler( + 'delayProfiles', + '/delayprofile', + (state) => state.settings.delayProfiles), + + [types.FETCH_QUALITY_DEFINITIONS]: createFetchHandler('qualityDefinitions', '/qualitydefinition'), + [types.SAVE_QUALITY_DEFINITIONS]: createSaveHandler('qualityDefinitions', '/qualitydefinition', (state) => state.settings.qualitydefinitions), + + [types.SAVE_QUALITY_DEFINITIONS]: function() { + const section = 'qualityDefinitions'; + + return function(dispatch, getState) { + const qualityDefinitions = getState().settings.qualityDefinitions; + + const upatedDefinitions = Object.keys(qualityDefinitions.pendingChanges).map((key) => { + const id = parseInt(key); + const pendingChanges = qualityDefinitions.pendingChanges[id] || {}; + const item = _.find(qualityDefinitions.items, { id }); + + return Object.assign({}, item, pendingChanges); + }); + + // If there is nothing to save don't bother isSaving + if (!upatedDefinitions || !upatedDefinitions.length) { + return; + } + + const promise = $.ajax({ + method: 'PUT', + url: '/qualityDefinition/update', + data: JSON.stringify(upatedDefinitions) + }); + + promise.done((data) => { + dispatch(batchActions([ + update({ section, data }), + clearPendingChanges({ section: 'qualityDefinitions' }) + ])); + }); + }; + }, + + [types.FETCH_INDEXERS]: createFetchHandler('indexers', '/indexer'), + [types.FETCH_INDEXER_SCHEMA]: createFetchSchemaHandler('indexers', '/indexer/schema'), + + [types.SAVE_INDEXER]: createSaveProviderHandler( + 'indexers', + '/indexer', + (state) => state.settings.indexers), + + [types.CANCEL_SAVE_INDEXER]: createCancelSaveProviderHandler('indexers'), + + [types.DELETE_INDEXER]: createRemoveItemHandler( + 'indexers', + '/indexer', + (state) => state.settings.indexers), + + [types.TEST_INDEXER]: createTestProviderHandler( + 'indexers', + '/indexer', + (state) => state.settings.indexers), + + [types.CANCEL_TEST_INDEXER]: createCancelTestProviderHandler('indexers'), + + [types.FETCH_INDEXER_OPTIONS]: createFetchHandler('indexerOptions', '/config/indexer'), + [types.SAVE_INDEXER_OPTIONS]: createSaveHandler('indexerOptions', '/config/indexer', (state) => state.settings.indexerOptions), + + [types.FETCH_RESTRICTIONS]: createFetchHandler('restrictions', '/restriction'), + + [types.SAVE_RESTRICTION]: createSaveProviderHandler( + 'restrictions', + '/restriction', + (state) => state.settings.restrictions), + + [types.DELETE_RESTRICTION]: createRemoveItemHandler( + 'restrictions', + '/restriction', + (state) => state.settings.restrictions), + + [types.FETCH_DOWNLOAD_CLIENTS]: createFetchHandler('downloadClients', '/downloadclient'), + [types.FETCH_DOWNLOAD_CLIENT_SCHEMA]: createFetchSchemaHandler('downloadClients', '/downloadclient/schema'), + + [types.SAVE_DOWNLOAD_CLIENT]: createSaveProviderHandler( + 'downloadClients', + '/downloadclient', + (state) => state.settings.downloadClients), + + [types.CANCEL_SAVE_DOWNLOAD_CLIENT]: createCancelSaveProviderHandler('downloadClients'), + + [types.DELETE_DOWNLOAD_CLIENT]: createRemoveItemHandler( + 'downloadClients', + '/downloadclient', + (state) => state.settings.downloadClients), + + [types.TEST_DOWNLOAD_CLIENT]: createTestProviderHandler( + 'downloadClients', + '/downloadclient', + (state) => state.settings.downloadClients), + + [types.CANCEL_TEST_DOWNLOAD_CLIENT]: createCancelTestProviderHandler('downloadClients'), + + [types.FETCH_DOWNLOAD_CLIENT_OPTIONS]: createFetchHandler('downloadClientOptions', '/config/downloadclient'), + [types.SAVE_DOWNLOAD_CLIENT_OPTIONS]: createSaveHandler('downloadClientOptions', '/config/downloadclient', (state) => state.settings.downloadClientOptions), + + [types.FETCH_REMOTE_PATH_MAPPINGS]: createFetchHandler('remotePathMappings', '/remotepathmapping'), + + [types.SAVE_REMOTE_PATH_MAPPING]: createSaveProviderHandler( + 'remotePathMappings', + '/remotepathmapping', + (state) => state.settings.remotePathMappings), + + [types.DELETE_REMOTE_PATH_MAPPING]: createRemoveItemHandler( + 'remotePathMappings', + '/remotepathmapping', + (state) => state.settings.remotePathMappings), + + [types.FETCH_NOTIFICATIONS]: createFetchHandler('notifications', '/notification'), + [types.FETCH_NOTIFICATION_SCHEMA]: createFetchSchemaHandler('notifications', '/notification/schema'), + + [types.SAVE_NOTIFICATION]: createSaveProviderHandler( + 'notifications', + '/notification', + (state) => state.settings.notifications), + + [types.CANCEL_SAVE_NOTIFICATION]: createCancelSaveProviderHandler('notifications'), + + [types.DELETE_NOTIFICATION]: createRemoveItemHandler( + 'notifications', + '/notification', + (state) => state.settings.notifications), + + [types.TEST_NOTIFICATION]: createTestProviderHandler( + 'notifications', + '/notification', + (state) => state.settings.notifications), + + [types.CANCEL_TEST_NOTIFICATION]: createCancelTestProviderHandler('notifications'), + + [types.FETCH_METADATA]: createFetchHandler('metadata', '/metadata'), + + [types.SAVE_METADATA]: createSaveProviderHandler( + 'metadata', + '/metadata', + (state) => state.settings.metadata), + + [types.FETCH_METADATA_PROVIDER]: createFetchHandler('metadataProvider', '/config/metadataProvider'), + [types.SAVE_METADATA_PROVIDER]: createSaveHandler('metadataProvider', '/config/metadataProvider', (state) => state.settings.metadataProvider), + + [types.FETCH_GENERAL_SETTINGS]: createFetchHandler('general', '/config/host'), + [types.SAVE_GENERAL_SETTINGS]: createSaveHandler('general', '/config/host', (state) => state.settings.general) +}; + +export default settingsActionHandlers; diff --git a/frontend/src/Store/Actions/settingsActions.js b/frontend/src/Store/Actions/settingsActions.js new file mode 100644 index 000000000..64acc7926 --- /dev/null +++ b/frontend/src/Store/Actions/settingsActions.js @@ -0,0 +1,222 @@ +import { createAction } from 'redux-actions'; +import * as types from './actionTypes'; +import settingsActionHandlers from './settingsActionHandlers'; + +export const toggleAdvancedSettings = createAction(types.TOGGLE_ADVANCED_SETTINGS); + +export const fetchUISettings = settingsActionHandlers[types.FETCH_UI_SETTINGS]; +export const saveUISettings = settingsActionHandlers[types.SAVE_UI_SETTINGS]; +export const setUISettingsValue = createAction(types.SET_UI_SETTINGS_VALUE, (payload) => { + return { + section: 'ui', + ...payload + }; +}); + +export const fetchMediaManagementSettings = settingsActionHandlers[types.FETCH_MEDIA_MANAGEMENT_SETTINGS]; +export const saveMediaManagementSettings = settingsActionHandlers[types.SAVE_MEDIA_MANAGEMENT_SETTINGS]; +export const setMediaManagementSettingsValue = createAction(types.SET_MEDIA_MANAGEMENT_SETTINGS_VALUE, (payload) => { + return { + section: 'mediaManagement', + ...payload + }; +}); + +export const fetchNamingSettings = settingsActionHandlers[types.FETCH_NAMING_SETTINGS]; +export const saveNamingSettings = settingsActionHandlers[types.SAVE_NAMING_SETTINGS]; +export const setNamingSettingsValue = createAction(types.SET_NAMING_SETTINGS_VALUE, (payload) => { + return { + section: 'naming', + ...payload + }; +}); + +export const fetchNamingExamples = settingsActionHandlers[types.FETCH_NAMING_EXAMPLES]; + +export const fetchQualityProfiles = settingsActionHandlers[types.FETCH_QUALITY_PROFILES]; +export const fetchQualityProfileSchema = settingsActionHandlers[types.FETCH_QUALITY_PROFILE_SCHEMA]; +export const saveQualityProfile = settingsActionHandlers[types.SAVE_QUALITY_PROFILE]; +export const deleteQualityProfile = settingsActionHandlers[types.DELETE_QUALITY_PROFILE]; + +export const setQualityProfileValue = createAction(types.SET_QUALITY_PROFILE_VALUE, (payload) => { + return { + section: 'qualityProfiles', + ...payload + }; +}); + +export const fetchLanguageProfiles = settingsActionHandlers[types.FETCH_LANGUAGE_PROFILES]; +export const fetchLanguageProfileSchema = settingsActionHandlers[types.FETCH_LANGUAGE_PROFILE_SCHEMA]; +export const saveLanguageProfile = settingsActionHandlers[types.SAVE_LANGUAGE_PROFILE]; +export const deleteLanguageProfile = settingsActionHandlers[types.DELETE_LANGUAGE_PROFILE]; + +export const setLanguageProfileValue = createAction(types.SET_LANGUAGE_PROFILE_VALUE, (payload) => { + return { + section: 'languageProfiles', + ...payload + }; +}); + +export const fetchDelayProfiles = settingsActionHandlers[types.FETCH_DELAY_PROFILES]; +export const saveDelayProfile = settingsActionHandlers[types.SAVE_DELAY_PROFILE]; +export const deleteDelayProfile = settingsActionHandlers[types.DELETE_DELAY_PROFILE]; +export const reorderDelayProfile = settingsActionHandlers[types.REORDER_DELAY_PROFILE]; + +export const setDelayProfileValue = createAction(types.SET_DELAY_PROFILE_VALUE, (payload) => { + return { + section: 'delayProfiles', + ...payload + }; +}); + +export const fetchQualityDefinitions = settingsActionHandlers[types.FETCH_QUALITY_DEFINITIONS]; +export const saveQualityDefinitions = settingsActionHandlers[types.SAVE_QUALITY_DEFINITIONS]; + +export const setQualityDefinitionValue = createAction(types.SET_QUALITY_DEFINITION_VALUE); + +export const fetchIndexers = settingsActionHandlers[types.FETCH_INDEXERS]; +export const fetchIndexerSchema = settingsActionHandlers[types.FETCH_INDEXER_SCHEMA]; +export const selectIndexerSchema = createAction(types.SELECT_INDEXER_SCHEMA); + +export const saveIndexer = settingsActionHandlers[types.SAVE_INDEXER]; +export const cancelSaveIndexer = settingsActionHandlers[types.CANCEL_SAVE_INDEXER]; +export const deleteIndexer = settingsActionHandlers[types.DELETE_INDEXER]; +export const testIndexer = settingsActionHandlers[types.TEST_INDEXER]; +export const cancelTestIndexer = settingsActionHandlers[types.CANCEL_TEST_INDEXER]; + +export const setIndexerValue = createAction(types.SET_INDEXER_VALUE, (payload) => { + return { + section: 'indexers', + ...payload + }; +}); + +export const setIndexerFieldValue = createAction(types.SET_INDEXER_FIELD_VALUE, (payload) => { + return { + section: 'indexers', + ...payload + }; +}); + +export const fetchIndexerOptions = settingsActionHandlers[types.FETCH_INDEXER_OPTIONS]; +export const saveIndexerOptions = settingsActionHandlers[types.SAVE_INDEXER_OPTIONS]; +export const setIndexerOptionsValue = createAction(types.SET_INDEXER_OPTIONS_VALUE, (payload) => { + return { + section: 'indexerOptions', + ...payload + }; +}); + +export const fetchRestrictions = settingsActionHandlers[types.FETCH_RESTRICTIONS]; +export const saveRestriction = settingsActionHandlers[types.SAVE_RESTRICTION]; +export const deleteRestriction = settingsActionHandlers[types.DELETE_RESTRICTION]; + +export const setRestrictionValue = createAction(types.SET_RESTRICTION_VALUE, (payload) => { + return { + section: 'restrictions', + ...payload + }; +}); + +export const fetchDownloadClients = settingsActionHandlers[types.FETCH_DOWNLOAD_CLIENTS]; +export const fetchDownloadClientSchema = settingsActionHandlers[types.FETCH_DOWNLOAD_CLIENT_SCHEMA]; +export const selectDownloadClientSchema = createAction(types.SELECT_DOWNLOAD_CLIENT_SCHEMA); + +export const saveDownloadClient = settingsActionHandlers[types.SAVE_DOWNLOAD_CLIENT]; +export const cancelSaveDownloadClient = settingsActionHandlers[types.CANCEL_SAVE_DOWNLOAD_CLIENT]; +export const deleteDownloadClient = settingsActionHandlers[types.DELETE_DOWNLOAD_CLIENT]; +export const testDownloadClient = settingsActionHandlers[types.TEST_DOWNLOAD_CLIENT]; +export const cancelTestDownloadClient = settingsActionHandlers[types.CANCEL_TEST_DOWNLOAD_CLIENT]; + +export const setDownloadClientValue = createAction(types.SET_DOWNLOAD_CLIENT_VALUE, (payload) => { + return { + section: 'downloadClients', + ...payload + }; +}); + +export const setDownloadClientFieldValue = createAction(types.SET_DOWNLOAD_CLIENT_FIELD_VALUE, (payload) => { + return { + section: 'downloadClients', + ...payload + }; +}); + +export const fetchDownloadClientOptions = settingsActionHandlers[types.FETCH_DOWNLOAD_CLIENT_OPTIONS]; +export const saveDownloadClientOptions = settingsActionHandlers[types.SAVE_DOWNLOAD_CLIENT_OPTIONS]; +export const setDownloadClientOptionsValue = createAction(types.SET_DOWNLOAD_CLIENT_OPTIONS_VALUE, (payload) => { + return { + section: 'downloadClientOptions', + ...payload + }; +}); + +export const fetchRemotePathMappings = settingsActionHandlers[types.FETCH_REMOTE_PATH_MAPPINGS]; +export const saveRemotePathMapping = settingsActionHandlers[types.SAVE_REMOTE_PATH_MAPPING]; +export const deleteRemotePathMapping = settingsActionHandlers[types.DELETE_REMOTE_PATH_MAPPING]; + +export const setRemotePathMappingValue = createAction(types.SET_REMOTE_PATH_MAPPING_VALUE, (payload) => { + return { + section: 'remotePathMappings', + ...payload + }; +}); + +export const fetchNotifications = settingsActionHandlers[types.FETCH_NOTIFICATIONS]; +export const fetchNotificationSchema = settingsActionHandlers[types.FETCH_NOTIFICATION_SCHEMA]; +export const selectNotificationSchema = createAction(types.SELECT_NOTIFICATION_SCHEMA); + +export const saveNotification = settingsActionHandlers[types.SAVE_NOTIFICATION]; +export const cancelSaveNotification = settingsActionHandlers[types.CANCEL_SAVE_NOTIFICATION]; +export const deleteNotification = settingsActionHandlers[types.DELETE_NOTIFICATION]; +export const testNotification = settingsActionHandlers[types.TEST_NOTIFICATION]; +export const cancelTestNotification = settingsActionHandlers[types.CANCEL_TEST_NOTIFICATION]; + +export const setNotificationValue = createAction(types.SET_NOTIFICATION_VALUE, (payload) => { + return { + section: 'notifications', + ...payload + }; +}); + +export const setNotificationFieldValue = createAction(types.SET_NOTIFICATION_FIELD_VALUE, (payload) => { + return { + section: 'notifications', + ...payload + }; +}); + +export const fetchMetadata = settingsActionHandlers[types.FETCH_METADATA]; +export const saveMetadata = settingsActionHandlers[types.SAVE_METADATA]; + +export const setMetadataValue = createAction(types.SET_METADATA_VALUE, (payload) => { + return { + section: 'metadata', + ...payload + }; +}); + +export const setMetadataFieldValue = createAction(types.SET_METADATA_FIELD_VALUE, (payload) => { + return { + section: 'metadata', + ...payload + }; +}); + +export const fetchMetadataProvider = settingsActionHandlers[types.FETCH_METADATA_PROVIDER]; +export const saveMetadataProvider = settingsActionHandlers[types.SAVE_METADATA_PROVIDER]; +export const setMetadataProviderValue = createAction(types.SET_METADATA_PROVIDER_VALUE, (payload) => { + return { + section: 'metadataProvider', + ...payload + }; +}); + +export const fetchGeneralSettings = settingsActionHandlers[types.FETCH_GENERAL_SETTINGS]; +export const saveGeneralSettings = settingsActionHandlers[types.SAVE_GENERAL_SETTINGS]; +export const setGeneralSettingsValue = createAction(types.SET_GENERAL_SETTINGS_VALUE, (payload) => { + return { + section: 'general', + ...payload + }; +}); diff --git a/frontend/src/Store/Actions/systemActionHandlers.js b/frontend/src/Store/Actions/systemActionHandlers.js new file mode 100644 index 000000000..d40674da3 --- /dev/null +++ b/frontend/src/Store/Actions/systemActionHandlers.js @@ -0,0 +1,48 @@ +import $ from 'jquery'; +import serverSideCollectionHandlers from 'Utilities/serverSideCollectionHandlers'; +import * as types from './actionTypes'; +import createFetchHandler from './Creators/createFetchHandler'; +import createServerSideCollectionHandlers from './Creators/createServerSideCollectionHandlers'; + +const systemActionHandlers = { + [types.FETCH_STATUS]: createFetchHandler('status', '/system/status'), + [types.FETCH_HEALTH]: createFetchHandler('health', '/health'), + [types.FETCH_DISK_SPACE]: createFetchHandler('diskSpace', '/diskspace'), + [types.FETCH_TASK]: createFetchHandler('tasks', '/system/task'), + [types.FETCH_TASKS]: createFetchHandler('tasks', '/system/task'), + [types.FETCH_BACKUPS]: createFetchHandler('backups', '/system/backup'), + [types.FETCH_UPDATES]: createFetchHandler('updates', '/update'), + [types.FETCH_LOG_FILES]: createFetchHandler('logFiles', '/log/file'), + [types.FETCH_UPDATE_LOG_FILES]: createFetchHandler('updateLogFiles', '/log/file/update'), + + ...createServerSideCollectionHandlers('logs', '/log', (state) => state.system, { + [serverSideCollectionHandlers.FETCH]: types.FETCH_LOGS, + [serverSideCollectionHandlers.FIRST_PAGE]: types.GOTO_FIRST_LOGS_PAGE, + [serverSideCollectionHandlers.PREVIOUS_PAGE]: types.GOTO_PREVIOUS_LOGS_PAGE, + [serverSideCollectionHandlers.NEXT_PAGE]: types.GOTO_NEXT_LOGS_PAGE, + [serverSideCollectionHandlers.LAST_PAGE]: types.GOTO_LAST_LOGS_PAGE, + [serverSideCollectionHandlers.EXACT_PAGE]: types.GOTO_LOGS_PAGE, + [serverSideCollectionHandlers.SORT]: types.SET_LOGS_SORT, + [serverSideCollectionHandlers.FILTER]: types.SET_LOGS_FILTER + }), + + [types.RESTART]: function() { + return function() { + $.ajax({ + url: '/system/restart', + method: 'POST' + }); + }; + }, + + [types.SHUTDOWN]: function() { + return function() { + $.ajax({ + url: '/system/shutdown', + method: 'POST' + }); + }; + } +}; + +export default systemActionHandlers; diff --git a/frontend/src/Store/Actions/systemActions.js b/frontend/src/Store/Actions/systemActions.js new file mode 100644 index 000000000..b5614d2d3 --- /dev/null +++ b/frontend/src/Store/Actions/systemActions.js @@ -0,0 +1,28 @@ +import { createAction } from 'redux-actions'; +import * as types from './actionTypes'; +import systemActionHandlers from './systemActionHandlers'; + +export const fetchStatus = systemActionHandlers[types.FETCH_STATUS]; +export const fetchHealth = systemActionHandlers[types.FETCH_HEALTH]; +export const fetchDiskSpace = systemActionHandlers[types.FETCH_DISK_SPACE]; + +export const fetchTask = systemActionHandlers[types.FETCH_TASK]; +export const fetchTasks = systemActionHandlers[types.FETCH_TASKS]; +export const fetchBackups = systemActionHandlers[types.FETCH_BACKUPS]; +export const fetchUpdates = systemActionHandlers[types.FETCH_UPDATES]; + +export const fetchLogs = systemActionHandlers[types.FETCH_LOGS]; +export const gotoLogsFirstPage = systemActionHandlers[types.GOTO_FIRST_LOGS_PAGE]; +export const gotoLogsPreviousPage = systemActionHandlers[types.GOTO_PREVIOUS_LOGS_PAGE]; +export const gotoLogsNextPage = systemActionHandlers[types.GOTO_NEXT_LOGS_PAGE]; +export const gotoLogsLastPage = systemActionHandlers[types.GOTO_LAST_LOGS_PAGE]; +export const gotoLogsPage = systemActionHandlers[types.GOTO_LOGS_PAGE]; +export const setLogsSort = systemActionHandlers[types.SET_LOGS_SORT]; +export const setLogsFilter = systemActionHandlers[types.SET_LOGS_FILTER]; +export const setLogsTableOption = createAction(types.SET_LOGS_TABLE_OPTION); + +export const fetchLogFiles = systemActionHandlers[types.FETCH_LOG_FILES]; +export const fetchUpdateLogFiles = systemActionHandlers[types.FETCH_UPDATE_LOG_FILES]; + +export const restart = systemActionHandlers[types.RESTART]; +export const shutdown = systemActionHandlers[types.SHUTDOWN]; diff --git a/frontend/src/Store/Actions/tagActionHandlers.js b/frontend/src/Store/Actions/tagActionHandlers.js new file mode 100644 index 000000000..c4e007f6c --- /dev/null +++ b/frontend/src/Store/Actions/tagActionHandlers.js @@ -0,0 +1,28 @@ +import $ from 'jquery'; +import * as types from './actionTypes'; +import { update } from './baseActions'; +import createFetchHandler from './Creators/createFetchHandler'; + +const tagActionHandlers = { + [types.FETCH_TAGS]: createFetchHandler('tags', '/tag'), + + [types.ADD_TAG]: function(payload) { + return (dispatch, getState) => { + const promise = $.ajax({ + url: '/tag', + method: 'POST', + data: JSON.stringify(payload.tag) + }); + + promise.done((data) => { + const tags = getState().tags.items.slice(); + tags.push(data); + + dispatch(update({ section: 'tags', data: tags })); + payload.onTagCreated(data); + }); + }; + } +}; + +export default tagActionHandlers; diff --git a/frontend/src/Store/Actions/tagActions.js b/frontend/src/Store/Actions/tagActions.js new file mode 100644 index 000000000..45f0141ce --- /dev/null +++ b/frontend/src/Store/Actions/tagActions.js @@ -0,0 +1,5 @@ +import * as types from './actionTypes'; +import tagActionHandlers from './tagActionHandlers'; + +export const fetchTags = tagActionHandlers[types.FETCH_TAGS]; +export const addTag = tagActionHandlers[types.ADD_TAG]; diff --git a/frontend/src/Store/Actions/trackActionHandlers.js b/frontend/src/Store/Actions/trackActionHandlers.js new file mode 100644 index 000000000..efda3ae40 --- /dev/null +++ b/frontend/src/Store/Actions/trackActionHandlers.js @@ -0,0 +1,11 @@ +import createFetchHandler from './Creators/createFetchHandler'; +import * as types from './actionTypes'; + +const section = 'tracks'; + +const trackActionHandlers = { + [types.FETCH_TRACKS]: createFetchHandler(section, '/track') + +}; + +export default trackActionHandlers; diff --git a/frontend/src/Store/Actions/trackActions.js b/frontend/src/Store/Actions/trackActions.js new file mode 100644 index 000000000..57d503c12 --- /dev/null +++ b/frontend/src/Store/Actions/trackActions.js @@ -0,0 +1,8 @@ +import { createAction } from 'redux-actions'; +import * as types from './actionTypes'; +import trackActionHandlers from './trackActionHandlers'; + +export const fetchTracks = trackActionHandlers[types.FETCH_TRACKS]; +export const setTracksSort = createAction(types.SET_TRACKS_SORT); +export const setTracksTableOption = createAction(types.SET_TRACKS_TABLE_OPTION); +export const clearTracks = createAction(types.CLEAR_TRACKS); diff --git a/frontend/src/Store/Actions/trackFileActionHandlers.js b/frontend/src/Store/Actions/trackFileActionHandlers.js new file mode 100644 index 000000000..bbb09a39e --- /dev/null +++ b/frontend/src/Store/Actions/trackFileActionHandlers.js @@ -0,0 +1,164 @@ +import _ from 'lodash'; +import $ from 'jquery'; +import { batchActions } from 'redux-batched-actions'; +import episodeEntities from 'Album/episodeEntities'; +import createFetchHandler from './Creators/createFetchHandler'; +import createRemoveItemHandler from './Creators/createRemoveItemHandler'; +import * as types from './actionTypes'; +import { set, removeItem, updateItem } from './baseActions'; + +const section = 'trackFiles'; +const deleteTrackFile = createRemoveItemHandler(section, '/trackFile'); + +const trackFileActionHandlers = { + [types.FETCH_TRACK_FILES]: createFetchHandler(section, '/trackFile'), + + [types.DELETE_TRACK_FILE]: function(payload) { + return function(dispatch, getState) { + const { + id: trackFileId, + episodeEntity = episodeEntities.EPISODES + } = payload; + + const episodeSection = _.last(episodeEntity.split('.')); + + const deletePromise = deleteTrackFile(payload)(dispatch, getState); + + deletePromise.done(() => { + const episodes = getState().episodes.items; + const episodesWithRemovedFiles = _.filter(episodes, { trackFileId }); + + dispatch(batchActions([ + ...episodesWithRemovedFiles.map((episode) => { + return updateItem({ + section: episodeSection, + ...episode, + trackFileId: 0, + hasFile: false + }); + }) + ])); + }); + }; + }, + + [types.DELETE_TRACK_FILES]: function(payload) { + return function(dispatch, getState) { + const { + trackFileIds + } = payload; + + dispatch(set({ section, isDeleting: true })); + + const promise = $.ajax({ + url: '/trackFile/bulk', + method: 'DELETE', + dataType: 'json', + data: JSON.stringify({ trackFileIds }) + }); + + promise.done(() => { + const episodes = getState().episodes.items; + const episodesWithRemovedFiles = trackFileIds.reduce((acc, trackFileId) => { + acc.push(..._.filter(episodes, { trackFileId })); + + return acc; + }, []); + + dispatch(batchActions([ + ...trackFileIds.map((id) => { + return removeItem({ section, id }); + }), + + ...episodesWithRemovedFiles.map((episode) => { + return updateItem({ + section: 'episodes', + ...episode, + trackFileId: 0, + hasFile: false + }); + }), + + set({ + section, + isDeleting: false, + deleteError: null + }) + ])); + }); + + promise.fail((xhr) => { + dispatch(set({ + section, + isDeleting: false, + deleteError: xhr + })); + }); + }; + }, + + [types.UPDATE_TRACK_FILES]: function(payload) { + return function(dispatch, getState) { + const { + trackFileIds, + language, + quality + } = payload; + + dispatch(set({ section, isSaving: true })); + + const data = { + trackFileIds + }; + + if (language) { + data.language = language; + } + + if (quality) { + data.quality = quality; + } + + const promise = $.ajax({ + url: '/trackFile/editor', + method: 'PUT', + dataType: 'json', + data: JSON.stringify(data) + }); + + promise.done(() => { + dispatch(batchActions([ + ...trackFileIds.map((id) => { + const props = {}; + + if (language) { + props.language = language; + } + + if (quality) { + props.quality = quality; + } + + return updateItem({ section, id, ...props }); + }), + + set({ + section, + isSaving: false, + saveError: null + }) + ])); + }); + + promise.fail((xhr) => { + dispatch(set({ + section, + isSaving: false, + saveError: xhr + })); + }); + }; + } +}; + +export default trackFileActionHandlers; diff --git a/frontend/src/Store/Actions/trackFileActions.js b/frontend/src/Store/Actions/trackFileActions.js new file mode 100644 index 000000000..fa4b6544a --- /dev/null +++ b/frontend/src/Store/Actions/trackFileActions.js @@ -0,0 +1,9 @@ +import { createAction } from 'redux-actions'; +import * as types from './actionTypes'; +import trackFileActionHandlers from './trackFileActionHandlers'; + +export const fetchTrackFiles = trackFileActionHandlers[types.FETCH_TRACK_FILES]; +export const deleteTrackFile = trackFileActionHandlers[types.DELETE_TRACK_FILE]; +export const deleteTrackFiles = trackFileActionHandlers[types.DELETE_TRACK_FILES]; +export const updateTrackFiles = trackFileActionHandlers[types.UPDATE_TRACK_FILES]; +export const clearTrackFiles = createAction(types.CLEAR_TRACK_FILES); diff --git a/frontend/src/Store/Actions/wantedActionHandlers.js b/frontend/src/Store/Actions/wantedActionHandlers.js new file mode 100644 index 000000000..60cb121b4 --- /dev/null +++ b/frontend/src/Store/Actions/wantedActionHandlers.js @@ -0,0 +1,34 @@ +import serverSideCollectionHandlers from 'Utilities/serverSideCollectionHandlers'; +import createBatchToggleAlbumMonitoredHandler from './Creators/createBatchToggleAlbumMonitoredHandler'; +import createServerSideCollectionHandlers from './Creators/createServerSideCollectionHandlers'; +import * as types from './actionTypes'; + +const wantedActionHandlers = { + ...createServerSideCollectionHandlers('missing', '/wanted/missing', (state) => state.wanted, { + [serverSideCollectionHandlers.FETCH]: types.FETCH_MISSING, + [serverSideCollectionHandlers.FIRST_PAGE]: types.GOTO_FIRST_MISSING_PAGE, + [serverSideCollectionHandlers.PREVIOUS_PAGE]: types.GOTO_PREVIOUS_MISSING_PAGE, + [serverSideCollectionHandlers.NEXT_PAGE]: types.GOTO_NEXT_MISSING_PAGE, + [serverSideCollectionHandlers.LAST_PAGE]: types.GOTO_LAST_MISSING_PAGE, + [serverSideCollectionHandlers.EXACT_PAGE]: types.GOTO_MISSING_PAGE, + [serverSideCollectionHandlers.SORT]: types.SET_MISSING_SORT, + [serverSideCollectionHandlers.FILTER]: types.SET_MISSING_FILTER + }), + + [types.BATCH_TOGGLE_MISSING_ALBUMS]: createBatchToggleAlbumMonitoredHandler('missing', (state) => state.wanted.missing), + + ...createServerSideCollectionHandlers('cutoffUnmet', '/wanted/cutoff', (state) => state.wanted, { + [serverSideCollectionHandlers.FETCH]: types.FETCH_CUTOFF_UNMET, + [serverSideCollectionHandlers.FIRST_PAGE]: types.GOTO_FIRST_CUTOFF_UNMET_PAGE, + [serverSideCollectionHandlers.PREVIOUS_PAGE]: types.GOTO_PREVIOUS_CUTOFF_UNMET_PAGE, + [serverSideCollectionHandlers.NEXT_PAGE]: types.GOTO_NEXT_CUTOFF_UNMET_PAGE, + [serverSideCollectionHandlers.LAST_PAGE]: types.GOTO_LAST_CUTOFF_UNMET_PAGE, + [serverSideCollectionHandlers.EXACT_PAGE]: types.GOTO_CUTOFF_UNMET_PAGE, + [serverSideCollectionHandlers.SORT]: types.SET_CUTOFF_UNMET_SORT, + [serverSideCollectionHandlers.FILTER]: types.SET_CUTOFF_UNMET_FILTER + }), + + [types.BATCH_TOGGLE_CUTOFF_UNMET_ALBUMS]: createBatchToggleAlbumMonitoredHandler('cutoffUnmet', (state) => state.wanted.cutoffUnmet) +}; + +export default wantedActionHandlers; diff --git a/frontend/src/Store/Actions/wantedActions.js b/frontend/src/Store/Actions/wantedActions.js new file mode 100644 index 000000000..75aac6e60 --- /dev/null +++ b/frontend/src/Store/Actions/wantedActions.js @@ -0,0 +1,35 @@ +import { createAction } from 'redux-actions'; +import * as types from './actionTypes'; +import wantedActionHandlers from './wantedActionHandlers'; + +// +// Missing + +export const fetchMissing = wantedActionHandlers[types.FETCH_MISSING]; +export const gotoMissingFirstPage = wantedActionHandlers[types.GOTO_FIRST_MISSING_PAGE]; +export const gotoMissingPreviousPage = wantedActionHandlers[types.GOTO_PREVIOUS_MISSING_PAGE]; +export const gotoMissingNextPage = wantedActionHandlers[types.GOTO_NEXT_MISSING_PAGE]; +export const gotoMissingLastPage = wantedActionHandlers[types.GOTO_LAST_MISSING_PAGE]; +export const gotoMissingPage = wantedActionHandlers[types.GOTO_MISSING_PAGE]; +export const setMissingSort = wantedActionHandlers[types.SET_MISSING_SORT]; +export const setMissingFilter = wantedActionHandlers[types.SET_MISSING_FILTER]; +export const setMissingTableOption = createAction(types.SET_MISSING_TABLE_OPTION); +export const clearMissing = createAction(types.CLEAR_MISSING); + +export const batchToggleMissingAlbums = wantedActionHandlers[types.BATCH_TOGGLE_MISSING_ALBUMS]; + +// +// Cutoff Unmet + +export const fetchCutoffUnmet = wantedActionHandlers[types.FETCH_CUTOFF_UNMET]; +export const gotoCutoffUnmetFirstPage = wantedActionHandlers[types.GOTO_FIRST_CUTOFF_UNMET_PAGE]; +export const gotoCutoffUnmetPreviousPage = wantedActionHandlers[types.GOTO_PREVIOUS_CUTOFF_UNMET_PAGE]; +export const gotoCutoffUnmetNextPage = wantedActionHandlers[types.GOTO_NEXT_CUTOFF_UNMET_PAGE]; +export const gotoCutoffUnmetLastPage = wantedActionHandlers[types.GOTO_LAST_CUTOFF_UNMET_PAGE]; +export const gotoCutoffUnmetPage = wantedActionHandlers[types.GOTO_CUTOFF_UNMET_PAGE]; +export const setCutoffUnmetSort = wantedActionHandlers[types.SET_CUTOFF_UNMET_SORT]; +export const setCutoffUnmetFilter = wantedActionHandlers[types.SET_CUTOFF_UNMET_FILTER]; +export const setCutoffUnmetTableOption= createAction(types.SET_CUTOFF_UNMET_TABLE_OPTION); +export const clearCutoffUnmet= createAction(types.CLEAR_CUTOFF_UNMET); + +export const batchToggleCutoffUnmetAlbums = wantedActionHandlers[types.BATCH_TOGGLE_CUTOFF_UNMET_ALBUMS]; diff --git a/frontend/src/Store/Middleware/middlewares.js b/frontend/src/Store/Middleware/middlewares.js new file mode 100644 index 000000000..5cf93b2a6 --- /dev/null +++ b/frontend/src/Store/Middleware/middlewares.js @@ -0,0 +1,22 @@ +import { applyMiddleware, compose } from 'redux'; +import thunk from 'redux-thunk'; +import { routerMiddleware } from 'react-router-redux'; +import sentryMiddleware from './sentryMiddleware'; +import persistState from './persistState'; + +export default function(history) { + const middlewares = []; + const ravenMiddleware = sentryMiddleware(); + + if (ravenMiddleware) { + middlewares.push(ravenMiddleware); + } + + middlewares.push(routerMiddleware(history)); + middlewares.push(thunk); + + return compose( + applyMiddleware(...middlewares), + persistState + ); +} diff --git a/frontend/src/Store/Middleware/persistState.js b/frontend/src/Store/Middleware/persistState.js new file mode 100644 index 000000000..456efc470 --- /dev/null +++ b/frontend/src/Store/Middleware/persistState.js @@ -0,0 +1,121 @@ +import _ from 'lodash'; +import persistState from 'redux-localstorage'; +import * as addArtistReducers from 'Store/Reducers/addArtistReducers'; +import * as episodeReducers from 'Store/Reducers/episodeReducers'; +import * as trackReducers from 'Store/Reducers/trackReducers'; +import * as artistIndexReducers from 'Store/Reducers/artistIndexReducers'; +import * as artistEditorReducers from 'Store/Reducers/artistEditorReducers'; +import * as albumStudioReducers from 'Store/Reducers/albumStudioReducers'; +import * as calendarReducers from 'Store/Reducers/calendarReducers'; +import * as historyReducers from 'Store/Reducers/historyReducers'; +import * as blacklistReducers from 'Store/Reducers/blacklistReducers'; +import * as wantedReducers from 'Store/Reducers/wantedReducers'; +import * as settingsReducers from 'Store/Reducers/settingsReducers'; +import * as systemReducers from 'Store/Reducers/systemReducers'; +import * as interactiveImportReducers from 'Store/Reducers/interactiveImportReducers'; +import * as queueReducers from 'Store/Reducers/queueReducers'; + +const reducers = [ + addArtistReducers, + episodeReducers, + trackReducers, + artistIndexReducers, + artistEditorReducers, + albumStudioReducers, + calendarReducers, + historyReducers, + blacklistReducers, + wantedReducers, + settingsReducers, + systemReducers, + interactiveImportReducers, + queueReducers +]; + +const columnPaths = []; + +const paths = _.reduce(reducers, (acc, reducer) => { + reducer.persistState.forEach((path) => { + if (path.match(/\.columns$/)) { + columnPaths.push(path); + } + + acc.push(path); + }); + + return acc; +}, []); + +function mergeColumns(path, initialState, persistedState, computedState) { + const initialColumns = _.get(initialState, path); + const persistedColumns = _.get(persistedState, path); + + if (!persistedColumns || !persistedColumns.length) { + return; + } + + const columns = []; + + initialColumns.forEach((initialColumn) => { + const persistedColumnIndex = _.findIndex(persistedColumns, { name: initialColumn.name }); + const column = Object.assign({}, initialColumn); + const persistedColumn = persistedColumnIndex > -1 ? persistedColumns[persistedColumnIndex] : undefined; + + if (persistedColumn) { + column.isVisible = persistedColumn.isVisible; + } + + // If there is a persisted column, it's index doesn't exceed the column list + // and it's modifiable, insert it in the proper position. + + if (persistedColumn && columns.length - 1 > persistedColumnIndex && persistedColumn.isModifiable !== false) { + columns.splice(persistedColumnIndex, 0, column); + } else { + columns.push(column); + } + + // Set the columns in the persisted state + _.set(computedState, path, columns); + }); +} + +function slicer(paths_) { + return (state) => { + const subset = {}; + + paths_.forEach((path) => { + _.set(subset, path, _.get(state, path)); + }); + + return subset; + }; +} + +function serialize(obj) { + return JSON.stringify(obj, null, 2); +} + +function merge(initialState, persistedState) { + if (!persistedState) { + return initialState; + } + + const computedState = {}; + + _.merge(computedState, initialState, persistedState); + + columnPaths.forEach((columnPath) => { + mergeColumns(columnPath, initialState, persistedState, computedState); + }); + + return computedState; +} + +const config = { + slicer, + serialize, + merge, + key: 'sonarr' +}; + +export default persistState(paths, config); diff --git a/frontend/src/Store/Middleware/sentryMiddleware.js b/frontend/src/Store/Middleware/sentryMiddleware.js new file mode 100644 index 000000000..1608d19ca --- /dev/null +++ b/frontend/src/Store/Middleware/sentryMiddleware.js @@ -0,0 +1,48 @@ +import _ from 'lodash'; +import Raven from 'raven-js'; +import createRavenMiddleware from 'raven-for-redux'; +import parseUrl from 'Utilities/String/parseUrl'; + +function cleanseUrl(url) { + const properties = parseUrl(url); + + return `${properties.pathname}${properties.search}`; +} + +function cleanseData(data) { + const result = _.cloneDeep(data); + + result.culprit = cleanseUrl(result.culprit); + result.request.url = cleanseUrl(result.request.url); + + return result; +} + +export default function sentryMiddleware() { + const { + analytics, + branch, + version, + release, + isProduction + } = window.Sonarr; + + if (!analytics) { + return; + } + + const dsn = isProduction ? 'https://c3a5b33e08de4e18b7d0505e942dbc95@sentry.io/216290' : + 'https://c3a5b33e08de4e18b7d0505e942dbc95@sentry.io/216290'; + + Raven.config(dsn).install(); + + return createRavenMiddleware(Raven, { + environment: isProduction ? 'production' : 'development', + release, + tags: { + branch, + version + }, + dataCallback: cleanseData + }); +} diff --git a/frontend/src/Store/Reducers/Creators/createAddItemReducer.js b/frontend/src/Store/Reducers/Creators/createAddItemReducer.js new file mode 100644 index 000000000..d0e75c758 --- /dev/null +++ b/frontend/src/Store/Reducers/Creators/createAddItemReducer.js @@ -0,0 +1,23 @@ +import getSectionState from 'Utilities/State/getSectionState'; +import updateSectionState from 'Utilities/State/updateSectionState'; + +function createAddItemReducer(section) { + return (state, { payload }) => { + const { + section: payloadSection, + ...otherProps + } = payload; + + if (section === payloadSection) { + const newState = getSectionState(state, section); + + newState.items = [...newState.items, { ...otherProps }]; + + return updateSectionState(state, section, newState); + } + + return state; + }; +} + +export default createAddItemReducer; diff --git a/frontend/src/Store/Reducers/Creators/createClearPendingChangesReducer.js b/frontend/src/Store/Reducers/Creators/createClearPendingChangesReducer.js new file mode 100644 index 000000000..6ff6e7b25 --- /dev/null +++ b/frontend/src/Store/Reducers/Creators/createClearPendingChangesReducer.js @@ -0,0 +1,21 @@ +import getSectionState from 'Utilities/State/getSectionState'; +import updateSectionState from 'Utilities/State/updateSectionState'; + +function createClearPendingChangesReducer(section) { + return (state, { payload }) => { + if (section === payload.section) { + const newState = getSectionState(state, section); + newState.pendingChanges = {}; + + if (newState.hasOwnProperty('saveError')) { + newState.saveError = null; + } + + return updateSectionState(state, section, newState); + } + + return state; + }; +} + +export default createClearPendingChangesReducer; diff --git a/frontend/src/Store/Reducers/Creators/createClearReducer.js b/frontend/src/Store/Reducers/Creators/createClearReducer.js new file mode 100644 index 000000000..2952973a9 --- /dev/null +++ b/frontend/src/Store/Reducers/Creators/createClearReducer.js @@ -0,0 +1,12 @@ +import getSectionState from 'Utilities/State/getSectionState'; +import updateSectionState from 'Utilities/State/updateSectionState'; + +function createClearReducer(section, defaultState) { + return (state) => { + const newState = Object.assign(getSectionState(state, section), defaultState); + + return updateSectionState(state, section, newState); + }; +} + +export default createClearReducer; diff --git a/frontend/src/Store/Reducers/Creators/createReducers.js b/frontend/src/Store/Reducers/Creators/createReducers.js new file mode 100644 index 000000000..13ed584e8 --- /dev/null +++ b/frontend/src/Store/Reducers/Creators/createReducers.js @@ -0,0 +1,20 @@ +function createReducers(sections, createReducer) { + const reducers = {}; + + sections.forEach((section) => { + reducers[section] = createReducer(section); + }); + + return (state, action) => { + const section = action.payload.section; + const reducer = reducers[section]; + + if (reducer) { + return reducer(state, action); + } + + return state; + }; +} + +export default createReducers; diff --git a/frontend/src/Store/Reducers/Creators/createRemoveItemReducer.js b/frontend/src/Store/Reducers/Creators/createRemoveItemReducer.js new file mode 100644 index 000000000..c09655b0c --- /dev/null +++ b/frontend/src/Store/Reducers/Creators/createRemoveItemReducer.js @@ -0,0 +1,20 @@ +import _ from 'lodash'; +import getSectionState from 'Utilities/State/getSectionState'; +import updateSectionState from 'Utilities/State/updateSectionState'; + +function createRemoveItemReducer(section) { + return (state, { payload }) => { + if (section === payload.section) { + const newState = getSectionState(state, section); + + newState.items = [...newState.items]; + _.remove(newState.items, { id: payload.id }); + + return updateSectionState(state, section, newState); + } + + return state; + }; +} + +export default createRemoveItemReducer; diff --git a/frontend/src/Store/Reducers/Creators/createSetClientSideCollectionFilterReducer.js b/frontend/src/Store/Reducers/Creators/createSetClientSideCollectionFilterReducer.js new file mode 100644 index 000000000..d756a736e --- /dev/null +++ b/frontend/src/Store/Reducers/Creators/createSetClientSideCollectionFilterReducer.js @@ -0,0 +1,17 @@ +import getSectionState from 'Utilities/State/getSectionState'; +import updateSectionState from 'Utilities/State/updateSectionState'; +import { filterTypes } from 'Helpers/Props'; + +function createSetClientSideCollectionFilterReducer(section) { + return (state, { payload }) => { + const newState = getSectionState(state, section); + + newState.filterKey = payload.filterKey; + newState.filterValue = payload.filterValue; + newState.filterType = payload.filterType || filterTypes.EQUAL; + + return updateSectionState(state, section, newState); + }; +} + +export default createSetClientSideCollectionFilterReducer; diff --git a/frontend/src/Store/Reducers/Creators/createSetClientSideCollectionSortReducer.js b/frontend/src/Store/Reducers/Creators/createSetClientSideCollectionSortReducer.js new file mode 100644 index 000000000..1bc048a80 --- /dev/null +++ b/frontend/src/Store/Reducers/Creators/createSetClientSideCollectionSortReducer.js @@ -0,0 +1,29 @@ +import { sortDirections } from 'Helpers/Props'; +import getSectionState from 'Utilities/State/getSectionState'; +import updateSectionState from 'Utilities/State/updateSectionState'; + +function createSetClientSideCollectionSortReducer(section) { + return (state, { payload }) => { + const newState = getSectionState(state, section); + + const sortKey = payload.sortKey || newState.sortKey; + let sortDirection = payload.sortDirection; + + if (!sortDirection) { + if (payload.sortKey === newState.sortKey) { + sortDirection = newState.sortDirection === sortDirections.ASCENDING ? + sortDirections.DESCENDING : + sortDirections.ASCENDING; + } else { + sortDirection = newState.sortDirection; + } + } + + newState.sortKey = sortKey; + newState.sortDirection = sortDirection; + + return updateSectionState(state, section, newState); + }; +} + +export default createSetClientSideCollectionSortReducer; diff --git a/frontend/src/Store/Reducers/Creators/createSetProviderFieldValueReducer.js b/frontend/src/Store/Reducers/Creators/createSetProviderFieldValueReducer.js new file mode 100644 index 000000000..3af58dd3b --- /dev/null +++ b/frontend/src/Store/Reducers/Creators/createSetProviderFieldValueReducer.js @@ -0,0 +1,23 @@ +import getSectionState from 'Utilities/State/getSectionState'; +import updateSectionState from 'Utilities/State/updateSectionState'; + +function createSetProviderFieldValueReducer(section) { + return (state, { payload }) => { + if (section === payload.section) { + const { name, value } = payload; + const newState = getSectionState(state, section); + newState.pendingChanges = Object.assign({}, newState.pendingChanges); + const fields = Object.assign({}, newState.pendingChanges.fields || {}); + + fields[name] = value; + + newState.pendingChanges.fields = fields; + + return updateSectionState(state, section, newState); + } + + return state; + }; +} + +export default createSetProviderFieldValueReducer; diff --git a/frontend/src/Store/Reducers/Creators/createSetReducer.js b/frontend/src/Store/Reducers/Creators/createSetReducer.js new file mode 100644 index 000000000..2c2126c03 --- /dev/null +++ b/frontend/src/Store/Reducers/Creators/createSetReducer.js @@ -0,0 +1,23 @@ +import _ from 'lodash'; +import getSectionState from 'Utilities/State/getSectionState'; +import updateSectionState from 'Utilities/State/updateSectionState'; + +const blacklistedProperties = [ + 'section', + 'id' +]; + +function createSetReducer(section) { + return (state, { payload }) => { + if (section === payload.section) { + const newState = Object.assign(getSectionState(state, section), + _.omit(payload, blacklistedProperties)); + + return updateSectionState(state, section, newState); + } + + return state; + }; +} + +export default createSetReducer; diff --git a/frontend/src/Store/Reducers/Creators/createSetSettingValueReducer.js b/frontend/src/Store/Reducers/Creators/createSetSettingValueReducer.js new file mode 100644 index 000000000..33ac23044 --- /dev/null +++ b/frontend/src/Store/Reducers/Creators/createSetSettingValueReducer.js @@ -0,0 +1,36 @@ +import _ from 'lodash'; +import getSectionState from 'Utilities/State/getSectionState'; +import updateSectionState from 'Utilities/State/updateSectionState'; + +function createSetSettingValueReducer(section) { + return (state, { payload }) => { + if (section === payload.section) { + const { name, value } = payload; + const newState = getSectionState(state, section); + newState.pendingChanges = Object.assign({}, newState.pendingChanges); + + const currentValue = newState.item ? newState.item[name] : null; + const pendingState = newState.pendingChanges; + + let parsedValue = null; + + if (_.isNumber(currentValue)) { + parsedValue = parseInt(value); + } else { + parsedValue = value; + } + + if (currentValue === parsedValue) { + delete pendingState[name]; + } else { + pendingState[name] = parsedValue; + } + + return updateSectionState(state, section, newState); + } + + return state; + }; +} + +export default createSetSettingValueReducer; diff --git a/frontend/src/Store/Reducers/Creators/createSetTableOptionReducer.js b/frontend/src/Store/Reducers/Creators/createSetTableOptionReducer.js new file mode 100644 index 000000000..f353be97c --- /dev/null +++ b/frontend/src/Store/Reducers/Creators/createSetTableOptionReducer.js @@ -0,0 +1,20 @@ +import _ from 'lodash'; +import getSectionState from 'Utilities/State/getSectionState'; +import updateSectionState from 'Utilities/State/updateSectionState'; + +const whitelistedProperties = [ + 'pageSize', + 'columns' +]; + +function createSetTableOptionReducer(section) { + return (state, { payload }) => { + const newState = Object.assign( + getSectionState(state, section), + _.pick(payload, whitelistedProperties)); + + return updateSectionState(state, section, newState); + }; +} + +export default createSetTableOptionReducer; diff --git a/frontend/src/Store/Reducers/Creators/createUpdateItemReducer.js b/frontend/src/Store/Reducers/Creators/createUpdateItemReducer.js new file mode 100644 index 000000000..aba730afd --- /dev/null +++ b/frontend/src/Store/Reducers/Creators/createUpdateItemReducer.js @@ -0,0 +1,36 @@ +import _ from 'lodash'; +import getSectionState from 'Utilities/State/getSectionState'; +import updateSectionState from 'Utilities/State/updateSectionState'; + +function createUpdateItemReducer(section, idProp = 'id') { + return (state, { payload }) => { + const { + section: payloadSection, + updateOnly = false, + ...otherProps + } = payload; + + if (section === payloadSection) { + const newState = getSectionState(state, section); + const items = newState.items; + const index = _.findIndex(items, { [idProp]: payload[idProp] }); + + newState.items = [...items]; + + // TODO: Move adding to it's own reducer + if (index >= 0) { + const item = items[index]; + + newState.items.splice(index, 1, { ...item, ...otherProps }); + } else if (!updateOnly) { + newState.items.push({ ...otherProps }); + } + + return updateSectionState(state, section, newState); + } + + return state; + }; +} + +export default createUpdateItemReducer; diff --git a/frontend/src/Store/Reducers/Creators/createUpdateReducer.js b/frontend/src/Store/Reducers/Creators/createUpdateReducer.js new file mode 100644 index 000000000..ea566ad9b --- /dev/null +++ b/frontend/src/Store/Reducers/Creators/createUpdateReducer.js @@ -0,0 +1,23 @@ +import _ from 'lodash'; +import getSectionState from 'Utilities/State/getSectionState'; +import updateSectionState from 'Utilities/State/updateSectionState'; + +function createUpdateReducer(section) { + return (state, { payload }) => { + if (section === payload.section) { + const newState = getSectionState(state, section); + + if (_.isArray(payload.data)) { + newState.items = payload.data; + } else { + newState.item = payload.data; + } + + return updateSectionState(state, section, newState); + } + + return state; + }; +} + +export default createUpdateReducer; diff --git a/frontend/src/Store/Reducers/Creators/createUpdateServerSideCollectionReducer.js b/frontend/src/Store/Reducers/Creators/createUpdateServerSideCollectionReducer.js new file mode 100644 index 000000000..235a1016a --- /dev/null +++ b/frontend/src/Store/Reducers/Creators/createUpdateServerSideCollectionReducer.js @@ -0,0 +1,24 @@ +import _ from 'lodash'; +import getSectionState from 'Utilities/State/getSectionState'; +import updateSectionState from 'Utilities/State/updateSectionState'; + +function createUpdateServerSideCollectionReducer(section) { + return (state, { payload }) => { + if (section === payload.section) { + const data = payload.data; + const newState = getSectionState(state, section); + + const serverState = _.omit(data, ['records']); + const calculatedState = { + totalPages: Math.max(Math.ceil(data.totalRecords / data.pageSize), 1), + items: data.records + }; + + return updateSectionState(state, section, Object.assign(newState, serverState, calculatedState)); + } + + return state; + }; +} + +export default createUpdateServerSideCollectionReducer; diff --git a/frontend/src/Store/Reducers/addArtistReducers.js b/frontend/src/Store/Reducers/addArtistReducers.js new file mode 100644 index 000000000..951d362ef --- /dev/null +++ b/frontend/src/Store/Reducers/addArtistReducers.js @@ -0,0 +1,69 @@ +import { handleActions } from 'redux-actions'; +import getSectionState from 'Utilities/State/getSectionState'; +import updateSectionState from 'Utilities/State/updateSectionState'; +import * as types from 'Store/Actions/actionTypes'; +import createSetReducer from './Creators/createSetReducer'; +import createSetSettingValueReducer from './Creators/createSetSettingValueReducer'; +import createUpdateReducer from './Creators/createUpdateReducer'; +import createUpdateItemReducer from './Creators/createUpdateItemReducer'; +import createRemoveItemReducer from './Creators/createRemoveItemReducer'; + +export const defaultState = { + isFetching: false, + isPopulated: false, + error: null, + isAdding: false, + isAdded: false, + addError: null, + items: [], + + defaults: { + rootFolderPath: '', + monitor: 'allEpisodes', + qualityProfileId: 0, + languageProfileId: 0, + primaryAlbumTypes: ['Album', 'EP'], + secondaryAlbumTypes: ['Studio'], + albumFolder: true, + tags: [] + } +}; + +export const persistState = [ + 'addArtist.defaults' +]; + +const reducerSection = 'addArtist'; + +const addArtistReducers = handleActions({ + + [types.SET]: createSetReducer(reducerSection), + [types.UPDATE]: createUpdateReducer(reducerSection), + [types.UPDATE_ITEM]: createUpdateItemReducer(reducerSection), + [types.REMOVE_ITEM]: createRemoveItemReducer(reducerSection), + + [types.SET_ADD_ARTIST_VALUE]: createSetSettingValueReducer(reducerSection), + + [types.SET_ADD_ARTIST_DEFAULT]: function(state, { payload }) { + const newState = getSectionState(state, reducerSection); + + newState.defaults = { + ...newState.defaults, + ...payload + }; + + return updateSectionState(state, reducerSection, newState); + }, + + [types.CLEAR_ADD_ARTIST]: function(state) { + const { + defaults, + ...otherDefaultState + } = defaultState; + + return Object.assign({}, state, otherDefaultState); + } + +}, defaultState); + +export default addArtistReducers; diff --git a/frontend/src/Store/Reducers/albumHistoryReducers.js b/frontend/src/Store/Reducers/albumHistoryReducers.js new file mode 100644 index 000000000..44b89b1ee --- /dev/null +++ b/frontend/src/Store/Reducers/albumHistoryReducers.js @@ -0,0 +1,26 @@ +import { handleActions } from 'redux-actions'; +import * as types from 'Store/Actions/actionTypes'; +import createSetReducer from './Creators/createSetReducer'; +import createUpdateReducer from './Creators/createUpdateReducer'; + +export const defaultState = { + isFetching: false, + isPopulated: false, + error: null, + items: [] +}; + +const reducerSection = 'albumHistory'; + +const albumHistoryReducers = handleActions({ + + [types.SET]: createSetReducer(reducerSection), + [types.UPDATE]: createUpdateReducer(reducerSection), + + [types.CLEAR_ALBUM_HISTORY]: (state) => { + return Object.assign({}, state, defaultState); + } + +}, defaultState); + +export default albumHistoryReducers; diff --git a/frontend/src/Store/Reducers/albumStudioReducers.js b/frontend/src/Store/Reducers/albumStudioReducers.js new file mode 100644 index 000000000..658d02a4e --- /dev/null +++ b/frontend/src/Store/Reducers/albumStudioReducers.js @@ -0,0 +1,39 @@ +import { handleActions } from 'redux-actions'; +import * as types from 'Store/Actions/actionTypes'; +import { filterTypes, sortDirections } from 'Helpers/Props'; +import createSetReducer from './Creators/createSetReducer'; +import createSetClientSideCollectionSortReducer from './Creators/createSetClientSideCollectionSortReducer'; +import createSetClientSideCollectionFilterReducer from './Creators/createSetClientSideCollectionFilterReducer'; + +export const defaultState = { + isSaving: false, + saveError: null, + sortKey: 'sortName', + sortDirection: sortDirections.ASCENDING, + secondarySortKey: 'sortName', + secondarySortDirection: sortDirections.ASCENDING, + filterKey: null, + filterValue: null, + filterType: filterTypes.EQUAL +}; + +export const persistState = [ + 'albumStudio.sortKey', + 'albumStudio.sortDirection', + 'albumStudio.filterKey', + 'albumStudio.filterValue', + 'albumStudio.filterType' +]; + +const reducerSection = 'albumStudio'; + +const albumStudioReducers = handleActions({ + + [types.SET]: createSetReducer(reducerSection), + + [types.SET_ALBUM_STUDIO_SORT]: createSetClientSideCollectionSortReducer(reducerSection), + [types.SET_ALBUM_STUDIO_FILTER]: createSetClientSideCollectionFilterReducer(reducerSection) + +}, defaultState); + +export default albumStudioReducers; diff --git a/frontend/src/Store/Reducers/appReducers.js b/frontend/src/Store/Reducers/appReducers.js new file mode 100644 index 000000000..f574495e3 --- /dev/null +++ b/frontend/src/Store/Reducers/appReducers.js @@ -0,0 +1,74 @@ +import { handleActions } from 'redux-actions'; +import * as types from 'Store/Actions/actionTypes'; +import createSetReducer from './Creators/createSetReducer'; +import createUpdateItemReducer from './Creators/createUpdateItemReducer'; +import createRemoveItemReducer from './Creators/createRemoveItemReducer'; + +function getDimensions(width, height) { + const dimensions = { + width, + height, + isExtraSmallScreen: width <= 480, + isSmallScreen: width <= 768, + isMediumScreen: width <= 992, + isLargeScreen: width <= 1200 + }; + + return dimensions; +} + +export const defaultState = { + dimensions: getDimensions(window.innerWidth, window.innerHeight), + messages: { + items: [] + }, + version: window.Sonarr.version, + isUpdated: false, + isConnected: true, + isReconnecting: false, + isDisconnected: false, + isSidebarVisible: !getDimensions(window.innerWidth, window.innerHeight).isSmallScreen +}; + +const appReducers = handleActions({ + + [types.SAVE_DIMENSIONS]: function(state, { payload }) { + const { + width, + height + } = payload; + + const dimensions = getDimensions(width, height); + + return Object.assign({}, state, { dimensions }); + }, + + [types.SHOW_MESSAGE]: createUpdateItemReducer('messages'), + [types.HIDE_MESSAGE]: createRemoveItemReducer('messages'), + + [types.SET_APP_VALUE]: createSetReducer('app'), + [types.SET_VERSION]: function(state, { payload }) { + const version = payload.version; + + const newState = { + version + }; + + if (state.version !== version) { + newState.isUpdated = true; + } + + return Object.assign({}, state, newState); + }, + + [types.SET_IS_SIDEBAR_VISIBLE]: function(state, { payload }) { + const newState = { + isSidebarVisible: payload.isSidebarVisible + }; + + return Object.assign({}, state, newState); + } + +}, defaultState); + +export default appReducers; diff --git a/frontend/src/Store/Reducers/artistEditorReducers.js b/frontend/src/Store/Reducers/artistEditorReducers.js new file mode 100644 index 000000000..6eaa07fa4 --- /dev/null +++ b/frontend/src/Store/Reducers/artistEditorReducers.js @@ -0,0 +1,41 @@ +import { handleActions } from 'redux-actions'; +import * as types from 'Store/Actions/actionTypes'; +import { filterTypes, sortDirections } from 'Helpers/Props'; +import createSetReducer from './Creators/createSetReducer'; +import createSetClientSideCollectionSortReducer from './Creators/createSetClientSideCollectionSortReducer'; +import createSetClientSideCollectionFilterReducer from './Creators/createSetClientSideCollectionFilterReducer'; + +export const defaultState = { + isSaving: false, + saveError: null, + isDeleting: false, + deleteError: null, + sortKey: 'sortName', + sortDirection: sortDirections.ASCENDING, + secondarySortKey: 'sortName', + secondarySortDirection: sortDirections.ASCENDING, + filterKey: null, + filterValue: null, + filterType: filterTypes.EQUAL +}; + +export const persistState = [ + 'artistEditor.sortKey', + 'artistEditor.sortDirection', + 'artistEditor.filterKey', + 'artistEditor.filterValue', + 'artistEditor.filterType' +]; + +const reducerSection = 'artistEditor'; + +const artistEditorReducers = handleActions({ + + [types.SET]: createSetReducer(reducerSection), + + [types.SET_ARTIST_EDITOR_SORT]: createSetClientSideCollectionSortReducer(reducerSection), + [types.SET_ARTIST_EDITOR_FILTER]: createSetClientSideCollectionFilterReducer(reducerSection) + +}, defaultState); + +export default artistEditorReducers; diff --git a/frontend/src/Store/Reducers/artistIndexReducers.js b/frontend/src/Store/Reducers/artistIndexReducers.js new file mode 100644 index 000000000..cdc45e7e8 --- /dev/null +++ b/frontend/src/Store/Reducers/artistIndexReducers.js @@ -0,0 +1,247 @@ +import moment from 'moment'; +import { handleActions } from 'redux-actions'; +import * as types from 'Store/Actions/actionTypes'; +import { filterTypes, sortDirections } from 'Helpers/Props'; +import createSetReducer from './Creators/createSetReducer'; +import createSetTableOptionReducer from './Creators/createSetTableOptionReducer'; +import createSetClientSideCollectionSortReducer from './Creators/createSetClientSideCollectionSortReducer'; +import createSetClientSideCollectionFilterReducer from './Creators/createSetClientSideCollectionFilterReducer'; + +export const defaultState = { + sortKey: 'sortName', + sortDirection: sortDirections.ASCENDING, + secondarySortKey: 'sortName', + secondarySortDirection: sortDirections.ASCENDING, + filterKey: null, + filterValue: null, + filterType: filterTypes.EQUAL, + view: 'posters', + + posterOptions: { + detailedProgressBar: false, + size: 'large', + showTitle: false, + showQualityProfile: true + }, + + bannerOptions: { + detailedProgressBar: false, + size: 'large', + showTitle: false, + showQualityProfile: true + }, + + overviewOptions: { + detailedProgressBar: false, + size: 'medium', + showNetwork: true, + showQualityProfile: true, + showPreviousAiring: false, + showAdded: false, + showAlbumCount: true, + showPath: false, + showSizeOnDisk: false + }, + + columns: [ + { + name: 'status', + columnLabel: 'Status', + isVisible: true, + isModifiable: false + }, + { + name: 'sortName', + label: 'Artist Name', + isSortable: true, + isVisible: true, + isModifiable: false + }, + { + name: 'artistType', + label: 'Type', + isSortable: true, + isVisible: true, + isModifiable: false + }, + { + name: 'qualityProfileId', + label: 'Quality Profile', + isSortable: true, + isVisible: true + }, + { + name: 'languageProfileId', + label: 'Language Profile', + isSortable: true, + isVisible: false + }, + { + name: 'nextAiring', + label: 'Next Airing', + isSortable: true, + isVisible: true + }, + { + name: 'previousAiring', + label: 'Previous Airing', + isSortable: true, + isVisible: false + }, + { + name: 'added', + label: 'Added', + isSortable: true, + isVisible: false + }, + { + name: 'albumCount', + label: 'Albums', + isSortable: true, + isVisible: true + }, + { + name: 'trackProgress', + label: 'Tracks', + isSortable: true, + isVisible: true + }, + { + name: 'trackCount', + label: 'Track Count', + isSortable: true, + isVisible: false + }, + { + name: 'latestAlbum', + label: 'Latest Album', + isSortable: true, + isVisible: false + }, + { + name: 'path', + label: 'Path', + isSortable: true, + isVisible: false + }, + { + name: 'sizeOnDisk', + label: 'Size on Disk', + isSortable: true, + isVisible: false + }, + { + name: 'tags', + label: 'Tags', + isSortable: false, + isVisible: false + }, + { + name: 'actions', + columnLabel: 'Actions', + isVisible: true, + isModifiable: false + } + ], + + sortPredicates: { + nextAiring: function(item, direction) { + const nextAiring = item.nextAiring; + + if (nextAiring) { + return moment(nextAiring).unix(); + } + + if (direction === sortDirections.DESCENDING) { + return 0; + } + + return Number.MAX_VALUE; + }, + + trackProgress: function(item) { + const { + trackCount = 0, + trackFileCount + } = item; + + const progress = trackCount ? trackFileCount / trackCount * 100 : 100; + + return progress + trackCount / 1000000; + } + }, + + filterPredicates: { + missing: function(item) { + return item.trackCount - item.trackFileCount > 0; + } + } +}; + +export const persistState = [ + 'artistIndex.sortKey', + 'artistIndex.sortDirection', + 'artistIndex.filterKey', + 'artistIndex.filterValue', + 'artistIndex.filterType', + 'artistIndex.view', + 'artistIndex.columns', + 'artistIndex.posterOptions', + 'artistIndex.bannerOptions', + 'artistIndex.overviewOptions' +]; + +const reducerSection = 'artistIndex'; + +const artistIndexReducers = handleActions({ + + [types.SET]: createSetReducer(reducerSection), + + [types.SET_ARTIST_SORT]: createSetClientSideCollectionSortReducer(reducerSection), + [types.SET_ARTIST_FILTER]: createSetClientSideCollectionFilterReducer(reducerSection), + + [types.SET_ARTIST_VIEW]: function(state, { payload }) { + return Object.assign({}, state, { view: payload.view }); + }, + + [types.SET_ARTIST_TABLE_OPTION]: createSetTableOptionReducer(reducerSection), + + [types.SET_ARTIST_POSTER_OPTION]: function(state, { payload }) { + const posterOptions = state.posterOptions; + + return { + ...state, + posterOptions: { + ...posterOptions, + ...payload + } + }; + }, + + [types.SET_ARTIST_BANNER_OPTION]: function(state, { payload }) { + const bannerOptions = state.bannerOptions; + + return { + ...state, + bannerOptions: { + ...bannerOptions, + ...payload + } + }; + }, + + [types.SET_ARTIST_OVERVIEW_OPTION]: function(state, { payload }) { + const overviewOptions = state.overviewOptions; + + return { + ...state, + overviewOptions: { + ...overviewOptions, + ...payload + } + }; + } + +}, defaultState); + +export default artistIndexReducers; diff --git a/frontend/src/Store/Reducers/artistReducers.js b/frontend/src/Store/Reducers/artistReducers.js new file mode 100644 index 000000000..c60b6d9e6 --- /dev/null +++ b/frontend/src/Store/Reducers/artistReducers.js @@ -0,0 +1,37 @@ +import { handleActions } from 'redux-actions'; +import * as types from 'Store/Actions/actionTypes'; +import { sortDirections } from 'Helpers/Props'; +import createSetReducer from './Creators/createSetReducer'; +import createSetSettingValueReducer from './Creators/createSetSettingValueReducer'; +import createClearPendingChangesReducer from './Creators/createClearPendingChangesReducer'; +import createUpdateReducer from './Creators/createUpdateReducer'; +import createUpdateItemReducer from './Creators/createUpdateItemReducer'; +import createRemoveItemReducer from './Creators/createRemoveItemReducer'; + +export const defaultState = { + isFetching: false, + isPopulated: false, + error: null, + isSaving: false, + saveError: null, + items: [], + sortKey: 'sortName', + sortDirection: sortDirections.ASCENDING, + pendingChanges: {} +}; + +const reducerSection = 'artist'; + +const artistReducers = handleActions({ + + [types.SET]: createSetReducer(reducerSection), + [types.UPDATE]: createUpdateReducer(reducerSection), + [types.UPDATE_ITEM]: createUpdateItemReducer(reducerSection), + [types.REMOVE_ITEM]: createRemoveItemReducer(reducerSection), + + [types.SET_ARTIST_VALUE]: createSetSettingValueReducer(reducerSection), + [types.CLEAR_PENDING_CHANGES]: createClearPendingChangesReducer(reducerSection) + +}, defaultState); + +export default artistReducers; diff --git a/frontend/src/Store/Reducers/blacklistReducers.js b/frontend/src/Store/Reducers/blacklistReducers.js new file mode 100644 index 000000000..bee1f12d6 --- /dev/null +++ b/frontend/src/Store/Reducers/blacklistReducers.js @@ -0,0 +1,80 @@ +import { handleActions } from 'redux-actions'; +import * as types from 'Store/Actions/actionTypes'; +import { sortDirections } from 'Helpers/Props'; +import createSetReducer from './Creators/createSetReducer'; +import createSetTableOptionReducer from './Creators/createSetTableOptionReducer'; +import createUpdateReducer from './Creators/createUpdateReducer'; +import createUpdateServerSideCollectionReducer from './Creators/createUpdateServerSideCollectionReducer'; + +const reducerSection = 'blacklist'; + +export const defaultState = { + isFetching: false, + isPopulated: false, + pageSize: 20, + sortKey: 'date', + sortDirection: sortDirections.DESCENDING, + error: null, + items: [], + + columns: [ + { + name: 'artist.sortName', + label: 'Artist Name', + isSortable: true, + isVisible: true + }, + { + name: 'sourceTitle', + label: 'Source Title', + isSortable: true, + isVisible: true + }, + { + name: 'language', + label: 'Language', + isVisible: false + }, + { + name: 'quality', + label: 'Quality', + isVisible: true + }, + { + name: 'date', + label: 'Date', + isSortable: true, + isVisible: true + }, + { + name: 'indexer', + label: 'Indexer', + isSortable: true, + isVisible: false + }, + { + name: 'details', + columnLabel: 'Details', + isVisible: true, + isModifiable: false + } + ] +}; + +export const persistState = [ + 'blacklist.pageSize', + 'blacklist.sortKey', + 'blacklist.sortDirection', + 'blacklist.columns' +]; + +const blacklistReducers = handleActions({ + + [types.SET]: createSetReducer(reducerSection), + [types.UPDATE]: createUpdateReducer(reducerSection), + [types.UPDATE_SERVER_SIDE_COLLECTION]: createUpdateServerSideCollectionReducer(reducerSection), + [types.SET_BLACKLIST_TABLE_OPTION]: createSetTableOptionReducer(reducerSection) + +}, defaultState); + +export default blacklistReducers; diff --git a/frontend/src/Store/Reducers/calendarReducers.js b/frontend/src/Store/Reducers/calendarReducers.js new file mode 100644 index 000000000..4dea88e97 --- /dev/null +++ b/frontend/src/Store/Reducers/calendarReducers.js @@ -0,0 +1,48 @@ +import { handleActions } from 'redux-actions'; +import * as types from 'Store/Actions/actionTypes'; +import createSetReducer from './Creators/createSetReducer'; +import createUpdateReducer from './Creators/createUpdateReducer'; +import createUpdateItemReducer from './Creators/createUpdateItemReducer'; + +export const defaultState = { + isFetching: false, + isPopulated: false, + start: null, + end: null, + dates: [], + dayCount: 7, + view: window.innerWidth > 768 ? 'week' : 'day', + unmonitored: false, + showUpcoming: true, + error: null, + items: [] +}; + +export const persistState = [ + 'calendar.view', + 'calendar.unmonitored', + 'calendar.showUpcoming' +]; + +const section = 'calendar'; + +const calendarReducers = handleActions({ + + [types.SET]: createSetReducer(section), + [types.UPDATE]: createUpdateReducer(section), + [types.UPDATE_ITEM]: createUpdateItemReducer(section), + + [types.CLEAR_CALENDAR]: (state) => { + const { + view, + unmonitored, + showUpcoming, + ...otherDefaultState + } = defaultState; + + return Object.assign({}, state, otherDefaultState); + } + +}, defaultState); + +export default calendarReducers; diff --git a/frontend/src/Store/Reducers/captchaReducers.js b/frontend/src/Store/Reducers/captchaReducers.js new file mode 100644 index 000000000..67372839f --- /dev/null +++ b/frontend/src/Store/Reducers/captchaReducers.js @@ -0,0 +1,32 @@ +import { handleActions } from 'redux-actions'; +import * as types from 'Store/Actions/actionTypes'; +import getSectionState from 'Utilities/State/getSectionState'; +import updateSectionState from 'Utilities/State/updateSectionState'; + +export const defaultState = { + refreshing: false, + token: null, + siteKey: null, + secretToken: null, + ray: null, + stoken: null, + responseUrl: null +}; + +const section = 'captcha'; + +const captchaReducers = handleActions({ + + [types.SET_CAPTCHA_VALUE]: function(state, { payload }) { + const newState = Object.assign(getSectionState(state, section), payload); + + return updateSectionState(state, section, newState); + }, + + [types.RESET_CAPTCHA]: function(state) { + return updateSectionState(state, section, defaultState); + } + +}, defaultState); + +export default captchaReducers; diff --git a/frontend/src/Store/Reducers/commandReducers.js b/frontend/src/Store/Reducers/commandReducers.js new file mode 100644 index 000000000..b2b474e65 --- /dev/null +++ b/frontend/src/Store/Reducers/commandReducers.js @@ -0,0 +1,64 @@ +import _ from 'lodash'; +import { handleActions } from 'redux-actions'; +import * as types from 'Store/Actions/actionTypes'; +import createSetReducer from './Creators/createSetReducer'; +import createUpdateReducer from './Creators/createUpdateReducer'; +import createUpdateItemReducer from './Creators/createUpdateItemReducer'; + +export const defaultState = { + isFetching: false, + isPopulated: false, + error: null, + items: [], + handlers: {} +}; + +const reducerSection = 'commands'; + +const commandReducers = handleActions({ + + [types.SET]: createSetReducer(reducerSection), + [types.UPDATE]: createUpdateReducer(reducerSection), + [types.UPDATE_ITEM]: createUpdateItemReducer(reducerSection), + + [types.ADD_COMMAND]: (state, { payload }) => { + const newState = Object.assign({}, state); + newState.items = [...state.items, payload]; + + return newState; + }, + + [types.REMOVE_COMMAND]: (state, { payload }) => { + const newState = Object.assign({}, state); + newState.items = [...state.items]; + + const index = _.findIndex(newState.items, { id: payload.id }); + + if (index > -1) { + newState.items.splice(index, 1); + } + + return newState; + }, + + [types.REGISTER_FINISH_COMMAND_HANDLER]: (state, { payload }) => { + const newState = Object.assign({}, state); + + newState.handlers[payload.key] = { + name: payload.name, + handler: payload.handler + }; + + return newState; + }, + + [types.UNREGISTER_FINISH_COMMAND_HANDLER]: (state, { payload }) => { + const newState = Object.assign({}, state); + delete newState.handlers[payload.key]; + + return newState; + } + +}, defaultState); + +export default commandReducers; diff --git a/frontend/src/Store/Reducers/episodeReducers.js b/frontend/src/Store/Reducers/episodeReducers.js new file mode 100644 index 000000000..479ea3fe0 --- /dev/null +++ b/frontend/src/Store/Reducers/episodeReducers.js @@ -0,0 +1,91 @@ +import { handleActions } from 'redux-actions'; +import * as types from 'Store/Actions/actionTypes'; +import { sortDirections } from 'Helpers/Props'; +import createSetReducer from './Creators/createSetReducer'; +import createSetTableOptionReducer from './Creators/createSetTableOptionReducer'; +import createUpdateReducer from './Creators/createUpdateReducer'; +import createUpdateItemReducer from './Creators/createUpdateItemReducer'; +import createSetClientSideCollectionSortReducer from './Creators/createSetClientSideCollectionSortReducer'; + +export const defaultState = { + isFetching: false, + isPopulated: false, + error: null, + sortKey: 'releaseDate', + sortDirection: sortDirections.DESCENDING, + items: [], + + columns: [ + { + name: 'monitored', + columnLabel: 'Monitored', + isVisible: true, + isModifiable: false + }, + { + name: 'title', + label: 'Title', + isVisible: true + }, + { + name: 'path', + label: 'Path', + isVisible: false + }, + { + name: 'releaseDate', + label: 'Release Date', + isVisible: true + }, + { + name: 'trackCount', + label: 'Track Count', + isVisible: false + }, + { + name: 'duration', + label: 'Duration', + isVisible: false + }, + { + name: 'status', + label: 'Status', + isVisible: true + }, + { + name: 'actions', + columnLabel: 'Actions', + isVisible: true, + isModifiable: false + } + ] +}; + +export const persistState = [ + 'episodes.columns' +]; + +const reducerSection = 'episodes'; + +const episodeReducers = handleActions({ + + [types.SET]: createSetReducer(reducerSection), + [types.UPDATE]: createUpdateReducer(reducerSection), + [types.UPDATE_ITEM]: createUpdateItemReducer(reducerSection), + + [types.SET_EPISODES_TABLE_OPTION]: createSetTableOptionReducer(reducerSection), + + [types.CLEAR_EPISODES]: (state) => { + return Object.assign({}, state, { + isFetching: false, + isPopulated: false, + error: null, + items: [] + }); + }, + + [types.SET_EPISODES_SORT]: createSetClientSideCollectionSortReducer(reducerSection) + +}, defaultState); + +export default episodeReducers; diff --git a/frontend/src/Store/Reducers/historyReducers.js b/frontend/src/Store/Reducers/historyReducers.js new file mode 100644 index 000000000..8104c3814 --- /dev/null +++ b/frontend/src/Store/Reducers/historyReducers.js @@ -0,0 +1,113 @@ +import { handleActions } from 'redux-actions'; +import * as types from 'Store/Actions/actionTypes'; +import { sortDirections } from 'Helpers/Props'; +import createClearReducer from './Creators/createClearReducer'; +import createSetReducer from './Creators/createSetReducer'; +import createSetTableOptionReducer from './Creators/createSetTableOptionReducer'; +import createUpdateReducer from './Creators/createUpdateReducer'; +import createUpdateItemReducer from './Creators/createUpdateItemReducer'; +import createUpdateServerSideCollectionReducer from './Creators/createUpdateServerSideCollectionReducer'; + +export const defaultState = { + isFetching: false, + isPopulated: false, + error: null, + pageSize: 20, + sortKey: 'date', + sortDirection: sortDirections.DESCENDING, + filterKey: null, + filterValue: null, + items: [], + + columns: [ + { + name: 'eventType', + columnLabel: 'Event Type', + isVisible: true, + isModifiable: false + }, + { + name: 'artist.sortName', + label: 'Artist', + isSortable: true, + isVisible: true + }, + { + name: 'episodeTitle', + label: 'Album Title', + isVisible: true + }, + { + name: 'trackTitle', + label: 'Track Title', + isVisible: true + }, + { + name: 'language', + label: 'Language', + isVisible: false + }, + { + name: 'quality', + label: 'Quality', + isVisible: true + }, + { + name: 'date', + label: 'Date', + isSortable: true, + isVisible: true + }, + { + name: 'downloadClient', + label: 'Download Client', + isVisible: false + }, + { + name: 'indexer', + label: 'Indexer', + isVisible: false + }, + { + name: 'releaseGroup', + label: 'Release Group', + isVisible: false + }, + { + name: 'details', + columnLabel: 'Details', + isVisible: true, + isModifiable: false + } + ] +}; + +export const persistState = [ + 'history.pageSize', + 'history.sortKey', + 'history.sortDirection', + 'history.filterKey', + 'history.filterValue' +]; + +const serverSideCollectionName = 'history'; + +const historyReducers = handleActions({ + + [types.SET]: createSetReducer(serverSideCollectionName), + [types.UPDATE]: createUpdateReducer(serverSideCollectionName), + [types.UPDATE_ITEM]: createUpdateItemReducer(serverSideCollectionName), + [types.UPDATE_SERVER_SIDE_COLLECTION]: createUpdateServerSideCollectionReducer(serverSideCollectionName), + + [types.SET_HISTORY_TABLE_OPTION]: createSetTableOptionReducer(serverSideCollectionName), + + [types.CLEAR_HISTORY]: createClearReducer('history', { + isFetching: false, + isPopulated: false, + error: null, + items: [] + }) + +}, defaultState); + +export default historyReducers; diff --git a/frontend/src/Store/Reducers/importArtistReducers.js b/frontend/src/Store/Reducers/importArtistReducers.js new file mode 100644 index 000000000..fddf32838 --- /dev/null +++ b/frontend/src/Store/Reducers/importArtistReducers.js @@ -0,0 +1,35 @@ +import { handleActions } from 'redux-actions'; +import * as types from 'Store/Actions/actionTypes'; +import createSetReducer from './Creators/createSetReducer'; +import createUpdateReducer from './Creators/createUpdateReducer'; +import createUpdateItemReducer from './Creators/createUpdateItemReducer'; +import createRemoveItemReducer from './Creators/createRemoveItemReducer'; + +export const defaultState = { + isFetching: false, + isPopulated: false, + error: null, + isImporting: false, + isImported: false, + importError: null, + items: [] +}; + +const reducerSection = 'importArtist'; + +const importArtistReducers = handleActions({ + + [types.SET]: createSetReducer(reducerSection), + [types.UPDATE]: createUpdateReducer(reducerSection), + [types.UPDATE_ITEM]: createUpdateItemReducer(reducerSection), + [types.REMOVE_ITEM]: createRemoveItemReducer(reducerSection), + + [types.CLEAR_IMPORT_ARTIST]: function(state) { + return Object.assign({}, state, defaultState); + }, + + [types.SET_IMPORT_ARTIST_VALUE]: createUpdateItemReducer(reducerSection) + +}, defaultState); + +export default importArtistReducers; diff --git a/frontend/src/Store/Reducers/index.js b/frontend/src/Store/Reducers/index.js new file mode 100644 index 000000000..917419459 --- /dev/null +++ b/frontend/src/Store/Reducers/index.js @@ -0,0 +1,91 @@ +import { combineReducers } from 'redux'; +import { enableBatching } from 'redux-batched-actions'; +import { routerReducer } from 'react-router-redux'; +import app, { defaultState as defaultappState } from './appReducers'; +import addArtist, { defaultState as defaultAddArtistState } from './addArtistReducers'; +import importArtist, { defaultState as defaultImportArtistState } from './importArtistReducers'; +import artist, { defaultState as defaultArtistState } from './artistReducers'; +import artistIndex, { defaultState as defaultArtistIndexState } from './artistIndexReducers'; +import artistEditor, { defaultState as defaultArtistEditorState } from './artistEditorReducers'; +import albumStudio, { defaultState as defaultAlbumStudioState } from './albumStudioReducers'; +import calendar, { defaultState as defaultCalendarState } from './calendarReducers'; +import history, { defaultState as defaultHistoryState } from './historyReducers'; +import queue, { defaultState as defaultQueueState } from './queueReducers'; +import blacklist, { defaultState as defaultBlacklistState } from './blacklistReducers'; +import episodes, { defaultState as defaultEpisodesState } from './episodeReducers'; +import tracks, { defaultState as defaultTracksState } from './trackReducers'; +import trackFiles, { defaultState as defaultTrackFilesState } from './trackFileReducers'; +import albumHistory, { defaultState as defaultAlbumHistoryState } from './albumHistoryReducers'; +import releases, { defaultState as defaultReleasesState } from './releaseReducers'; +import wanted, { defaultState as defaultWantedState } from './wantedReducers'; +import settings, { defaultState as defaultSettingsState } from './settingsReducers'; +import system, { defaultState as defaultSystemState } from './systemReducers'; +import commands, { defaultState as defaultCommandsState } from './commandReducers'; +import paths, { defaultState as defaultPathsState } from './pathReducers'; +import tags, { defaultState as defaultTagsState } from './tagReducers'; +import captcha, { defaultState as defaultCaptchaState } from './captchaReducers'; +import oAuth, { defaultState as defaultOAuthState } from './oAuthReducers'; +import interactiveImport, { defaultState as defaultInteractiveImportState } from './interactiveImportReducers'; +import rootFolders, { defaultState as defaultRootFoldersState } from './rootFolderReducers'; +import organizePreview, { defaultState as defaultOrganizePreviewState } from './organizePreviewReducers'; + +export const defaultState = { + app: defaultappState, + addArtist: defaultAddArtistState, + importArtist: defaultImportArtistState, + artist: defaultArtistState, + artistIndex: defaultArtistIndexState, + artistEditor: defaultArtistEditorState, + albumStudio: defaultAlbumStudioState, + calendar: defaultCalendarState, + history: defaultHistoryState, + queue: defaultQueueState, + blacklist: defaultBlacklistState, + episodes: defaultEpisodesState, + tracks: defaultTracksState, + trackFiles: defaultTrackFilesState, + albumHistory: defaultAlbumHistoryState, + releases: defaultReleasesState, + wanted: defaultWantedState, + settings: defaultSettingsState, + system: defaultSystemState, + commands: defaultCommandsState, + paths: defaultPathsState, + tags: defaultTagsState, + captcha: defaultCaptchaState, + oAuth: defaultOAuthState, + interactiveImport: defaultInteractiveImportState, + rootFolders: defaultRootFoldersState, + organizePreview: defaultOrganizePreviewState +}; + +export default enableBatching(combineReducers({ + app, + addArtist, + importArtist, + artist, + artistIndex, + artistEditor, + albumStudio, + calendar, + history, + queue, + blacklist, + episodes, + tracks, + trackFiles, + albumHistory, + releases, + wanted, + settings, + system, + commands, + paths, + tags, + captcha, + oAuth, + interactiveImport, + rootFolders, + organizePreview, + routing: routerReducer +})); diff --git a/frontend/src/Store/Reducers/interactiveImportReducers.js b/frontend/src/Store/Reducers/interactiveImportReducers.js new file mode 100644 index 000000000..b1230befe --- /dev/null +++ b/frontend/src/Store/Reducers/interactiveImportReducers.js @@ -0,0 +1,119 @@ +import _ from 'lodash'; +import moment from 'moment'; +import { handleActions } from 'redux-actions'; +import updateSectionState from 'Utilities/State/updateSectionState'; +import * as types from 'Store/Actions/actionTypes'; +import { sortDirections } from 'Helpers/Props'; +import createSetReducer from './Creators/createSetReducer'; +import createUpdateReducer from './Creators/createUpdateReducer'; +import createReducers from './Creators/createReducers'; +import createSetClientSideCollectionSortReducer from './Creators/createSetClientSideCollectionSortReducer'; + +export const defaultState = { + isFetching: false, + isPopulated: false, + error: null, + items: [], + sortKey: 'quality', + sortDirection: sortDirections.DESCENDING, + recentFolders: [], + importMode: 'move', + sortPredicates: { + artist: function(item, direction) { + const artist = item.artist; + + return artist ? artist.sortName : ''; + }, + + quality: function(item, direction) { + return item.quality.qualityWeight; + } + }, + + interactiveImportAlbums: { + isFetching: false, + isPopulated: false, + error: null, + sortKey: 'albumTitle', + sortDirection: sortDirections.DESCENDING, + items: [] + } +}; + +export const persistState = [ + 'interactiveImport.recentFolders', + 'interactiveImport.importMode' +]; + +const reducerSection = 'interactiveImport'; +const episodesSection = 'interactiveImportAlbums'; + +const interactiveImportReducers = handleActions({ + + [types.SET]: createReducers([reducerSection, episodesSection], createSetReducer), + [types.UPDATE]: createReducers([reducerSection, episodesSection], createUpdateReducer), + + [types.UPDATE_INTERACTIVE_IMPORT_ITEM]: (state, { payload }) => { + const id = payload.id; + const newState = Object.assign({}, state); + const items = newState.items; + const index = _.findIndex(items, { id }); + const item = Object.assign({}, items[index], payload); + + newState.items = [...items]; + newState.items.splice(index, 1, item); + + return newState; + }, + + [types.ADD_RECENT_FOLDER]: function(state, { payload }) { + const folder = payload.folder; + const recentFolder = { folder, lastUsed: moment().toISOString() }; + const recentFolders = [...state.recentFolders]; + const index = _.findIndex(recentFolders, { folder }); + + if (index > -1) { + recentFolders.splice(index, 1, recentFolder); + } else { + recentFolders.push(recentFolder); + } + + return Object.assign({}, state, { recentFolders }); + }, + + [types.REMOVE_RECENT_FOLDER]: function(state, { payload }) { + const folder = payload.folder; + const recentFolders = _.remove([...state.recentFolders], { folder }); + + return Object.assign({}, state, { recentFolders }); + }, + + [types.CLEAR_INTERACTIVE_IMPORT]: function(state) { + const newState = { + ...defaultState, + recentFolders: state.recentFolders, + importMode: state.importMode + }; + + return newState; + }, + + [types.SET_INTERACTIVE_IMPORT_SORT]: createSetClientSideCollectionSortReducer(reducerSection), + + [types.SET_INTERACTIVE_IMPORT_MODE]: function(state, { payload }) { + return Object.assign({}, state, { importMode: payload.importMode }); + }, + + [types.SET_INTERACTIVE_IMPORT_ALBUMS_SORT]: createSetClientSideCollectionSortReducer(episodesSection), + + [types.CLEAR_INTERACTIVE_IMPORT_ALBUMS]: (state) => { + const section = episodesSection; + + return updateSectionState(state, section, { + ...defaultState.interactiveImportAlbums + }); + } + +}, defaultState); + +export default interactiveImportReducers; diff --git a/frontend/src/Store/Reducers/oAuthReducers.js b/frontend/src/Store/Reducers/oAuthReducers.js new file mode 100644 index 000000000..291faf285 --- /dev/null +++ b/frontend/src/Store/Reducers/oAuthReducers.js @@ -0,0 +1,28 @@ +import { handleActions } from 'redux-actions'; +import * as types from 'Store/Actions/actionTypes'; +import getSectionState from 'Utilities/State/getSectionState'; +import updateSectionState from 'Utilities/State/updateSectionState'; + +export const defaultState = { + authorizing: false, + accessToken: null, + accessTokenSecret: null +}; + +const section = 'oAuth'; + +const oAuthReducers = handleActions({ + + [types.SET_OAUTH_VALUE]: function(state, { payload }) { + const newState = Object.assign(getSectionState(state, section), payload); + + return updateSectionState(state, section, newState); + }, + + [types.RESET_OAUTH]: function(state) { + return updateSectionState(state, section, defaultState); + } + +}, defaultState); + +export default oAuthReducers; diff --git a/frontend/src/Store/Reducers/organizePreviewReducers.js b/frontend/src/Store/Reducers/organizePreviewReducers.js new file mode 100644 index 000000000..c4e182c09 --- /dev/null +++ b/frontend/src/Store/Reducers/organizePreviewReducers.js @@ -0,0 +1,26 @@ +import { handleActions } from 'redux-actions'; +import * as types from 'Store/Actions/actionTypes'; +import createSetReducer from './Creators/createSetReducer'; +import createUpdateReducer from './Creators/createUpdateReducer'; + +export const defaultState = { + isFetching: false, + isPopulated: false, + error: null, + items: [] +}; + +const reducerSection = 'organizePreview'; + +const organizePreviewReducers = handleActions({ + + [types.SET]: createSetReducer(reducerSection), + [types.UPDATE]: createUpdateReducer(reducerSection), + + [types.CLEAR_ORGANIZE_PREVIEW]: (state) => { + return Object.assign({}, state, defaultState); + } + +}, defaultState); + +export default organizePreviewReducers; diff --git a/frontend/src/Store/Reducers/pathReducers.js b/frontend/src/Store/Reducers/pathReducers.js new file mode 100644 index 000000000..c10ad89c7 --- /dev/null +++ b/frontend/src/Store/Reducers/pathReducers.js @@ -0,0 +1,45 @@ +import { handleActions } from 'redux-actions'; +import * as types from 'Store/Actions/actionTypes'; +import createSetReducer from './Creators/createSetReducer'; + +export const defaultState = { + currentPath: '', + isPopulated: false, + isFetching: false, + error: null, + directories: [], + files: [], + parent: null +}; + +const reducerSection = 'paths'; + +const pathReducers = handleActions({ + + [types.SET]: createSetReducer(reducerSection), + + [types.UPDATE_PATHS]: (state, { payload }) => { + const newState = Object.assign({}, state); + + newState.currentPath = payload.path; + newState.directories = payload.directories; + newState.files = payload.files; + newState.parent = payload.parent; + + return newState; + }, + + [types.CLEAR_PATHS]: (state, { payload }) => { + const newState = Object.assign({}, state); + + newState.path = ''; + newState.directories = []; + newState.files = []; + newState.parent = ''; + + return newState; + } + +}, defaultState); + +export default pathReducers; diff --git a/frontend/src/Store/Reducers/queueReducers.js b/frontend/src/Store/Reducers/queueReducers.js new file mode 100644 index 000000000..cd129741d --- /dev/null +++ b/frontend/src/Store/Reducers/queueReducers.js @@ -0,0 +1,141 @@ +import { handleActions } from 'redux-actions'; +import { sortDirections } from 'Helpers/Props'; +import * as types from 'Store/Actions/actionTypes'; +import createClearReducer from './Creators/createClearReducer'; +import createSetReducer from './Creators/createSetReducer'; +import createSetTableOptionReducer from './Creators/createSetTableOptionReducer'; +import createUpdateReducer from './Creators/createUpdateReducer'; +import createUpdateItemReducer from './Creators/createUpdateItemReducer'; +import createReducers from './Creators/createReducers'; +import createUpdateServerSideCollectionReducer from './Creators/createUpdateServerSideCollectionReducer'; + +export const defaultState = { + queueStatus: { + isFetching: false, + isPopulated: false, + error: null, + item: {} + }, + + details: { + isFetching: false, + isPopulated: false, + error: null, + items: [], + params: {} + }, + + paged: { + isFetching: false, + isPopulated: false, + pageSize: 20, + sortKey: 'timeleft', + sortDirection: sortDirections.ASCENDING, + error: null, + items: [], + isGrabbing: false, + isRemoving: false, + + columns: [ + { + name: 'status', + columnLabel: 'Status', + isVisible: true, + isModifiable: false + }, + { + name: 'artist.sortName', + label: 'Artist', + isSortable: true, + isVisible: true + }, + { + name: 'episodeTitle', + label: 'Album Title', + isVisible: true + }, + { + name: 'quality', + label: 'Quality', + isSortable: true, + isVisible: true + }, + { + name: 'protocol', + label: 'Protocol', + isVisible: false + }, + { + name: 'indexer', + label: 'Indexer', + isVisible: false + }, + { + name: 'downloadClient', + label: 'Download Client', + isVisible: false + }, + { + name: 'estimatedCompletionTime', + label: 'Timeleft', + isSortable: true, + isVisible: true + }, + { + name: 'progress', + label: 'Progress', + isSortable: true, + isVisible: true + }, + { + name: 'actions', + columnLabel: 'Actions', + isVisible: true, + isModifiable: false + } + ] + }, + + queueEpisodes: { + isPopulated: false, + items: [] + } +}; + +export const persistState = [ + 'queue.paged.pageSize', + 'queue.paged.sortKey', + 'queue.paged.sortDirection', + 'queue.paged.columns' +]; + +const propertyNames = [ + 'queueStatus', + 'details', + 'episodes' +]; + +const paged = 'paged'; + +const queueReducers = handleActions({ + + [types.SET]: createReducers([...propertyNames, paged], createSetReducer), + [types.UPDATE]: createReducers([...propertyNames, paged], createUpdateReducer), + [types.UPDATE_ITEM]: createReducers(['queueEpisodes', paged], createUpdateItemReducer), + + [types.CLEAR_QUEUE_DETAILS]: createClearReducer('details', defaultState.details), + + [types.UPDATE_SERVER_SIDE_COLLECTION]: createUpdateServerSideCollectionReducer(paged), + + [types.SET_QUEUE_TABLE_OPTION]: createSetTableOptionReducer(paged), + + [types.CLEAR_QUEUE]: createClearReducer('paged', { + isFetching: false, + isPopulated: false, + error: null, + items: [] + }) + +}, defaultState); + +export default queueReducers; diff --git a/frontend/src/Store/Reducers/releaseReducers.js b/frontend/src/Store/Reducers/releaseReducers.js new file mode 100644 index 000000000..237049578 --- /dev/null +++ b/frontend/src/Store/Reducers/releaseReducers.js @@ -0,0 +1,71 @@ +import _ from 'lodash'; +import { handleActions } from 'redux-actions'; +import * as types from 'Store/Actions/actionTypes'; +import { sortDirections } from 'Helpers/Props'; +import createSetReducer from './Creators/createSetReducer'; +import createUpdateReducer from './Creators/createUpdateReducer'; +import createSetClientSideCollectionSortReducer from './Creators/createSetClientSideCollectionSortReducer'; + +export const defaultState = { + isFetching: false, + isPopulated: false, + error: null, + items: [], + sortKey: 'releaseWeight', + sortDirection: sortDirections.ASCENDING, + sortPredicates: { + peers: function(item, direction) { + const seeders = item.seeders || 0; + const leechers = item.leechers || 0; + + return seeders * 1000000 + leechers; + }, + + rejections: function(item, direction) { + const rejections = item.rejections; + const releaseWeight = item.releaseWeight; + + if (rejections.length !== 0) { + return releaseWeight + 1000000; + } + + return releaseWeight; + } + } +}; + +const reducerSection = 'releases'; + +const releaseReducers = handleActions({ + + [types.SET]: createSetReducer(reducerSection), + [types.UPDATE]: createUpdateReducer(reducerSection), + + [types.CLEAR_RELEASES]: (state) => { + return Object.assign({}, state, defaultState); + }, + + [types.UPDATE_RELEASE]: (state, { payload }) => { + const guid = payload.guid; + const newState = Object.assign({}, state); + const items = newState.items; + + // Return early if there aren't any items (the user closed the modal) + if (!items.length) { + return; + } + + const index = _.findIndex(items, { guid }); + const item = Object.assign({}, items[index], payload); + + newState.items = [...items]; + newState.items.splice(index, 1, item); + + return newState; + }, + + [types.SET_RELEASES_SORT]: createSetClientSideCollectionSortReducer(reducerSection) + +}, defaultState); + +export default releaseReducers; diff --git a/frontend/src/Store/Reducers/rootFolderReducers.js b/frontend/src/Store/Reducers/rootFolderReducers.js new file mode 100644 index 000000000..bd5c18bfa --- /dev/null +++ b/frontend/src/Store/Reducers/rootFolderReducers.js @@ -0,0 +1,28 @@ +import { handleActions } from 'redux-actions'; +import * as types from 'Store/Actions/actionTypes'; +import createSetReducer from './Creators/createSetReducer'; +import createUpdateReducer from './Creators/createUpdateReducer'; +import createUpdateItemReducer from './Creators/createUpdateItemReducer'; +import createRemoveItemReducer from './Creators/createRemoveItemReducer'; + +export const defaultState = { + isFetching: false, + isPopulated: false, + error: null, + isSaving: false, + saveError: null, + items: [] +}; + +const reducerSection = 'rootFolders'; + +const rootFolderReducers = handleActions({ + + [types.SET]: createSetReducer(reducerSection), + [types.UPDATE]: createUpdateReducer(reducerSection), + [types.UPDATE_ITEM]: createUpdateItemReducer(reducerSection), + [types.REMOVE_ITEM]: createRemoveItemReducer(reducerSection) + +}, defaultState); + +export default rootFolderReducers; diff --git a/frontend/src/Store/Reducers/settingsReducers.js b/frontend/src/Store/Reducers/settingsReducers.js new file mode 100644 index 000000000..2e92ef6c1 --- /dev/null +++ b/frontend/src/Store/Reducers/settingsReducers.js @@ -0,0 +1,350 @@ +import _ from 'lodash'; +import { handleActions } from 'redux-actions'; +import getSectionState from 'Utilities/State/getSectionState'; +import selectProviderSchema from 'Utilities/State/selectProviderSchema'; +import updateSectionState from 'Utilities/State/updateSectionState'; +import * as types from 'Store/Actions/actionTypes'; +import createSetReducer from './Creators/createSetReducer'; +import createSetSettingValueReducer from './Creators/createSetSettingValueReducer'; +import createSetProviderFieldValueReducer from './Creators/createSetProviderFieldValueReducer'; +import createClearPendingChangesReducer from './Creators/createClearPendingChangesReducer'; +import createUpdateReducer from './Creators/createUpdateReducer'; +import createUpdateItemReducer from './Creators/createUpdateItemReducer'; +import createRemoveItemReducer from './Creators/createRemoveItemReducer'; +import createReducers from './Creators/createReducers'; + +export const defaultState = { + ui: { + isFetching: false, + isPopulated: false, + error: null, + pendingChanges: {}, + isSaving: false, + saveError: null, + item: {} + }, + + mediaManagement: { + isFetching: false, + isPopulated: false, + error: null, + pendingChanges: {}, + isSaving: false, + saveError: null, + item: {} + }, + + naming: { + isFetching: false, + isPopulated: false, + error: null, + pendingChanges: {}, + isSaving: false, + saveError: null, + item: {} + }, + + namingExamples: { + isFetching: false, + isPopulated: false, + error: null, + item: {} + }, + + qualityProfiles: { + isFetching: false, + isPopulated: false, + error: null, + isDeleting: false, + deleteError: null, + isFetchingSchema: false, + schemaPopulated: false, + schemaError: null, + schema: {}, + isSaving: false, + saveError: null, + items: [], + pendingChanges: {} + }, + + languageProfiles: { + isFetching: false, + isPopulated: false, + error: null, + isDeleting: false, + deleteError: null, + isFetchingSchema: false, + schemaPopulated: false, + schemaError: null, + schema: {}, + isSaving: false, + saveError: null, + items: [], + pendingChanges: {} + }, + + delayProfiles: { + isFetching: false, + isPopulated: false, + error: null, + items: [], + isSaving: false, + saveError: null, + pendingChanges: {} + }, + + qualityDefinitions: { + isFetching: false, + isPopulated: false, + error: null, + items: [], + isSaving: false, + saveError: null, + pendingChanges: {} + }, + + indexers: { + isFetching: false, + isPopulated: false, + error: null, + isFetchingSchema: false, + schemaPopulated: false, + schemaError: null, + schema: [], + selectedSchema: {}, + isSaving: false, + saveError: null, + isTesting: false, + items: [], + pendingChanges: {} + }, + + indexerOptions: { + isFetching: false, + isPopulated: false, + error: null, + pendingChanges: {}, + isSaving: false, + saveError: null, + item: {} + }, + + restrictions: { + isFetching: false, + isPopulated: false, + error: null, + isSaving: false, + saveError: null, + items: [], + pendingChanges: {} + }, + + downloadClients: { + isFetching: false, + isPopulated: false, + error: null, + isFetchingSchema: false, + schemaPopulated: false, + schemaError: null, + schema: [], + selectedSchema: {}, + isSaving: false, + saveError: null, + isTesting: false, + items: [], + pendingChanges: {} + }, + + downloadClientOptions: { + isFetching: false, + isPopulated: false, + error: null, + pendingChanges: {}, + isSaving: false, + saveError: null, + item: {} + }, + + remotePathMappings: { + isFetching: false, + isPopulated: false, + error: null, + items: [], + isSaving: false, + saveError: null, + pendingChanges: {} + }, + + notifications: { + isFetching: false, + isPopulated: false, + error: null, + isFetchingSchema: false, + schemaPopulated: false, + schemaError: null, + schema: [], + selectedSchema: {}, + isSaving: false, + saveError: null, + isTesting: false, + items: [], + pendingChanges: {} + }, + + metadata: { + isFetching: false, + isPopulated: false, + error: null, + isSaving: false, + saveError: null, + items: [], + pendingChanges: {} + }, + + metadataProvider: { + isFetching: false, + isPopulated: false, + error: null, + pendingChanges: {}, + isSaving: false, + saveError: null, + item: {} + }, + + general: { + isFetching: false, + isPopulated: false, + error: null, + pendingChanges: {}, + isSaving: false, + saveError: null, + item: {} + }, + + advancedSettings: false +}; + +export const persistState = [ + 'settings.advancedSettings' +]; + +const propertyNames = [ + 'ui', + 'mediaManagement', + 'naming', + 'namingExamples', + 'qualityDefinitions', + 'indexerOptions', + 'downloadClientOptions', + 'general', + 'metadataProvider' +]; + +const providerPropertyNames = [ + 'qualityProfiles', + 'languageProfiles', + 'delayProfiles', + 'indexers', + 'restrictions', + 'downloadClients', + 'remotePathMappings', + 'notifications', + 'metadata' +]; + +const settingsReducers = handleActions({ + + [types.TOGGLE_ADVANCED_SETTINGS]: (state, { payload }) => { + return Object.assign({}, state, { advancedSettings: !state.advancedSettings }); + }, + + [types.SET]: createReducers([...propertyNames, ...providerPropertyNames], createSetReducer), + [types.UPDATE]: createReducers([...propertyNames, ...providerPropertyNames], createUpdateReducer), + [types.UPDATE_ITEM]: createReducers(providerPropertyNames, createUpdateItemReducer), + [types.CLEAR_PENDING_CHANGES]: createReducers([...propertyNames, ...providerPropertyNames], createClearPendingChangesReducer), + + [types.REMOVE_ITEM]: createReducers(providerPropertyNames, createRemoveItemReducer), + + [types.SET_UI_SETTINGS_VALUE]: createSetSettingValueReducer('ui'), + [types.SET_MEDIA_MANAGEMENT_SETTINGS_VALUE]: createSetSettingValueReducer('mediaManagement'), + [types.SET_NAMING_SETTINGS_VALUE]: createSetSettingValueReducer('naming'), + [types.SET_QUALITY_PROFILE_VALUE]: createSetSettingValueReducer('qualityProfiles'), + [types.SET_LANGUAGE_PROFILE_VALUE]: createSetSettingValueReducer('languageProfiles'), + [types.SET_DELAY_PROFILE_VALUE]: createSetSettingValueReducer('delayProfiles'), + + [types.SET_QUALITY_DEFINITION_VALUE]: function(state, { payload }) { + const section = 'qualityDefinitions'; + const { id, name, value } = payload; + const newState = getSectionState(state, section); + newState.pendingChanges = _.cloneDeep(newState.pendingChanges); + + const pendingState = newState.pendingChanges[id] || {}; + const currentValue = _.find(newState.items, { id })[name]; + + if (currentValue === value) { + delete pendingState[name]; + } else { + pendingState[name] = value; + } + + if (_.isEmpty(pendingState)) { + delete newState.pendingChanges[id]; + } else { + newState.pendingChanges[id] = pendingState; + } + + return updateSectionState(state, section, newState); + }, + + [types.SET_INDEXER_VALUE]: createSetSettingValueReducer('indexers'), + [types.SET_INDEXER_FIELD_VALUE]: createSetProviderFieldValueReducer('indexers'), + [types.SET_INDEXER_OPTIONS_VALUE]: createSetSettingValueReducer('indexerOptions'), + [types.SET_RESTRICTION_VALUE]: createSetSettingValueReducer('restrictions'), + + [types.SELECT_INDEXER_SCHEMA]: function(state, { payload }) { + return selectProviderSchema(state, 'indexers', payload, (selectedSchema) => { + selectedSchema.enableRss = selectedSchema.supportsRss; + selectedSchema.enableSearch = selectedSchema.supportsSearch; + + return selectedSchema; + }); + }, + + [types.SET_DOWNLOAD_CLIENT_VALUE]: createSetSettingValueReducer('downloadClients'), + [types.SET_DOWNLOAD_CLIENT_FIELD_VALUE]: createSetProviderFieldValueReducer('downloadClients'), + + [types.SELECT_DOWNLOAD_CLIENT_SCHEMA]: function(state, { payload }) { + return selectProviderSchema(state, 'downloadClients', payload, (selectedSchema) => { + selectedSchema.enable = true; + + return selectedSchema; + }); + }, + + [types.SET_DOWNLOAD_CLIENT_OPTIONS_VALUE]: createSetSettingValueReducer('downloadClientOptions'), + [types.SET_REMOTE_PATH_MAPPING_VALUE]: createSetSettingValueReducer('remotePathMappings'), + + [types.SET_NOTIFICATION_VALUE]: createSetSettingValueReducer('notifications'), + [types.SET_NOTIFICATION_FIELD_VALUE]: createSetProviderFieldValueReducer('notifications'), + + [types.SELECT_NOTIFICATION_SCHEMA]: function(state, { payload }) { + return selectProviderSchema(state, 'notifications', payload, (selectedSchema) => { + selectedSchema.onGrab = selectedSchema.supportsOnGrab; + selectedSchema.onDownload = selectedSchema.supportsOnDownload; + selectedSchema.onUpgrade = selectedSchema.supportsOnUpgrade; + selectedSchema.onRename = selectedSchema.supportsOnRename; + + return selectedSchema; + }); + }, + + [types.SET_METADATA_VALUE]: createSetSettingValueReducer('metadata'), + [types.SET_METADATA_FIELD_VALUE]: createSetProviderFieldValueReducer('metadata'), + + [types.SET_METADATA_PROVIDER_VALUE]: createSetSettingValueReducer('metadataProvider'), + + [types.SET_GENERAL_SETTINGS_VALUE]: createSetSettingValueReducer('general') + +}, defaultState); + +export default settingsReducers; diff --git a/frontend/src/Store/Reducers/systemReducers.js b/frontend/src/Store/Reducers/systemReducers.js new file mode 100644 index 000000000..31aa2ab08 --- /dev/null +++ b/frontend/src/Store/Reducers/systemReducers.js @@ -0,0 +1,146 @@ +import { handleActions } from 'redux-actions'; +import * as types from 'Store/Actions/actionTypes'; +import { sortDirections } from 'Helpers/Props'; +import createSetReducer from './Creators/createSetReducer'; +import createSetTableOptionReducer from './Creators/createSetTableOptionReducer'; +import createUpdateReducer from './Creators/createUpdateReducer'; +import createUpdateItemReducer from './Creators/createUpdateItemReducer'; +import createUpdateServerSideCollectionReducer from './Creators/createUpdateServerSideCollectionReducer'; +import createReducers from './Creators/createReducers'; + +export const defaultState = { + status: { + isFetching: false, + isPopulated: false, + error: null, + item: {} + }, + + health: { + isFetching: false, + isPopulated: false, + error: null, + items: [] + }, + + diskSpace: { + isFetching: false, + isPopulated: false, + error: null, + items: [] + }, + + tasks: { + isFetching: false, + isPopulated: false, + error: null, + items: [] + }, + + backups: { + isFetching: false, + isPopulated: false, + error: null, + items: [] + }, + + updates: { + isFetching: false, + isPopulated: false, + error: null, + items: [] + }, + + logs: { + isFetching: false, + isPopulated: false, + pageSize: 50, + sortKey: 'time', + sortDirection: sortDirections.DESCENDING, + filterKey: null, + filterValue: null, + error: null, + items: [], + + columns: [ + { + name: 'level', + isSortable: true, + isVisible: true + }, + { + name: 'logger', + label: 'Component', + isSortable: true, + isVisible: true + }, + { + name: 'message', + label: 'Message', + isVisible: true + }, + { + name: 'time', + label: 'Time', + isSortable: true, + isVisible: true + }, + { + name: 'actions', + columnLabel: 'Actions', + isSortable: true, + isVisible: true, + isModifiable: false + } + ] + }, + + logFiles: { + isFetching: false, + isPopulated: false, + error: null, + items: [] + }, + + updateLogFiles: { + isFetching: false, + isPopulated: false, + error: null, + items: [] + } +}; + +export const persistState = [ + 'system.logs.pageSize', + 'system.logs.sortKey', + 'system.logs.sortDirection', + 'system.logs.filterKey', + 'system.logs.filterValue' +]; + +const collectionNames = [ + 'health', + 'diskSpace', + 'tasks', + 'backups', + 'updates', + 'logFiles', + 'updateLogFiles' +]; + +const serverSideCollectionNames = [ + 'logs' +]; + +const systemReducers = handleActions({ + + [types.SET]: createReducers(['status', ...collectionNames, ...serverSideCollectionNames], createSetReducer), + [types.UPDATE]: createReducers(['status', ...collectionNames, ...serverSideCollectionNames], createUpdateReducer), + [types.UPDATE_ITEM]: createUpdateItemReducer('tasks'), + [types.UPDATE_SERVER_SIDE_COLLECTION]: createReducers(serverSideCollectionNames, createUpdateServerSideCollectionReducer), + + [types.SET_LOGS_TABLE_OPTION]: createSetTableOptionReducer('logs') + +}, defaultState); + +export default systemReducers; diff --git a/frontend/src/Store/Reducers/tagReducers.js b/frontend/src/Store/Reducers/tagReducers.js new file mode 100644 index 000000000..6aa822fd7 --- /dev/null +++ b/frontend/src/Store/Reducers/tagReducers.js @@ -0,0 +1,22 @@ +import { handleActions } from 'redux-actions'; +import * as types from 'Store/Actions/actionTypes'; +import createSetReducer from './Creators/createSetReducer'; +import createUpdateReducer from './Creators/createUpdateReducer'; + +export const defaultState = { + isFetching: false, + isPopulated: false, + error: null, + items: [] +}; + +const reducerSection = 'tags'; + +const tagReducers = handleActions({ + + [types.SET]: createSetReducer(reducerSection), + [types.UPDATE]: createUpdateReducer(reducerSection) + +}, defaultState); + +export default tagReducers; diff --git a/frontend/src/Store/Reducers/trackFileReducers.js b/frontend/src/Store/Reducers/trackFileReducers.js new file mode 100644 index 000000000..0db17a70f --- /dev/null +++ b/frontend/src/Store/Reducers/trackFileReducers.js @@ -0,0 +1,34 @@ +import { handleActions } from 'redux-actions'; +import * as types from 'Store/Actions/actionTypes'; +import createSetReducer from './Creators/createSetReducer'; +import createUpdateReducer from './Creators/createUpdateReducer'; +import createUpdateItemReducer from './Creators/createUpdateItemReducer'; +import createRemoveItemReducer from './Creators/createRemoveItemReducer'; + +export const defaultState = { + isFetching: false, + isPopulated: false, + error: null, + isDeleting: false, + deleteError: null, + isSaving: false, + saveError: null, + items: [] +}; + +const reducerSection = 'trackFiles'; + +const trackFileReducers = handleActions({ + + [types.SET]: createSetReducer(reducerSection), + [types.UPDATE]: createUpdateReducer(reducerSection), + [types.UPDATE_ITEM]: createUpdateItemReducer(reducerSection), + [types.REMOVE_ITEM]: createRemoveItemReducer(reducerSection), + + [types.CLEAR_TRACK_FILES]: (state) => { + return Object.assign({}, state, defaultState); + } + +}, defaultState); + +export default trackFileReducers; diff --git a/frontend/src/Store/Reducers/trackReducers.js b/frontend/src/Store/Reducers/trackReducers.js new file mode 100644 index 000000000..3f684fd9d --- /dev/null +++ b/frontend/src/Store/Reducers/trackReducers.js @@ -0,0 +1,80 @@ +import { handleActions } from 'redux-actions'; +import * as types from 'Store/Actions/actionTypes'; +import { sortDirections } from 'Helpers/Props'; +import createSetReducer from './Creators/createSetReducer'; +import createSetTableOptionReducer from './Creators/createSetTableOptionReducer'; +import createUpdateReducer from './Creators/createUpdateReducer'; +import createUpdateItemReducer from './Creators/createUpdateItemReducer'; +import createSetClientSideCollectionSortReducer from './Creators/createSetClientSideCollectionSortReducer'; + +export const defaultState = { + isFetching: false, + isPopulated: false, + error: null, + sortKey: 'trackNumber', + sortDirection: sortDirections.DESCENDING, + items: [], + + columns: [ + { + name: 'trackNumber', + label: '#', + isVisible: true + }, + { + name: 'title', + label: 'Title', + isVisible: true + }, + { + name: 'duration', + label: 'Duration', + isVisible: true + }, + { + name: 'audioInfo', + label: 'Audio Info', + isVisible: true + }, + { + name: 'status', + label: 'Status', + isVisible: true + }, + { + name: 'actions', + columnLabel: 'Actions', + isVisible: true, + isModifiable: false + } + ] +}; + +export const persistState = [ + 'tracks.columns' +]; + +const reducerSection = 'tracks'; + +const trackReducers = handleActions({ + + [types.SET]: createSetReducer(reducerSection), + [types.UPDATE]: createUpdateReducer(reducerSection), + [types.UPDATE_ITEM]: createUpdateItemReducer(reducerSection), + + [types.SET_TRACKS_TABLE_OPTION]: createSetTableOptionReducer(reducerSection), + + [types.CLEAR_TRACKS]: (state) => { + return Object.assign({}, state, { + isFetching: false, + isPopulated: false, + error: null, + items: [] + }); + }, + + [types.SET_TRACKS_SORT]: createSetClientSideCollectionSortReducer(reducerSection) + +}, defaultState); + +export default trackReducers; diff --git a/frontend/src/Store/Reducers/wantedReducers.js b/frontend/src/Store/Reducers/wantedReducers.js new file mode 100644 index 000000000..c126901c2 --- /dev/null +++ b/frontend/src/Store/Reducers/wantedReducers.js @@ -0,0 +1,161 @@ +import { handleActions } from 'redux-actions'; +import * as types from 'Store/Actions/actionTypes'; +import { sortDirections } from 'Helpers/Props'; +import createClearReducer from './Creators/createClearReducer'; +import createSetReducer from './Creators/createSetReducer'; +import createSetTableOptionReducer from './Creators/createSetTableOptionReducer'; +import createUpdateReducer from './Creators/createUpdateReducer'; +import createUpdateItemReducer from './Creators/createUpdateItemReducer'; +import createUpdateServerSideCollectionReducer from './Creators/createUpdateServerSideCollectionReducer'; +import createReducers from './Creators/createReducers'; + +export const defaultState = { + missing: { + isFetching: false, + isPopulated: false, + pageSize: 20, + sortKey: 'releaseDate', + sortDirection: sortDirections.DESCENDING, + filterKey: 'monitored', + filterValue: 'true', + error: null, + items: [], + + columns: [ + { + name: 'artist.sortName', + label: 'Artist Name', + isSortable: true, + isVisible: true + }, + // { + // name: 'episode', + // label: 'Episode', + // isVisible: true + // }, + { + name: 'albumTitle', + label: 'Album Title', + isVisible: true + }, + { + name: 'releaseDate', + label: 'Release Date', + isSortable: true, + isVisible: true + }, + // { + // name: 'status', + // label: 'Status', + // isVisible: true + // }, + { + name: 'actions', + columnLabel: 'Actions', + isVisible: true, + isModifiable: false + } + ] + }, + + cutoffUnmet: { + isFetching: false, + isPopulated: false, + pageSize: 20, + sortKey: 'airDateUtc', + sortDirection: sortDirections.DESCENDING, + filterKey: 'monitored', + filterValue: true, + error: null, + items: [], + + columns: [ + { + name: 'artist.sortName', + label: 'Artist Name', + isSortable: true, + isVisible: true + }, + // { + // name: 'episode', + // label: 'Episode', + // isVisible: true + // }, + { + name: 'albumTitle', + label: 'Album Title', + isVisible: true + }, + { + name: 'releaseDate', + label: 'Release Date', + isSortable: true, + isVisible: true + }, + { + name: 'language', + label: 'Language', + isVisible: false + }, + // { + // name: 'status', + // label: 'Status', + // isVisible: true + // }, + { + name: 'actions', + columnLabel: 'Actions', + isVisible: true, + isModifiable: false + } + ] + } +}; + +export const persistState = [ + 'wanted.missing.pageSize', + 'wanted.missing.sortKey', + 'wanted.missing.sortDirection', + 'wanted.missing.filterKey', + 'wanted.missing.filterValue', + 'wanted.missing.columns', + 'wanted.cutoffUnmet.pageSize', + 'wanted.cutoffUnmet.sortKey', + 'wanted.cutoffUnmet.sortDirection', + 'wanted.cutoffUnmet.filterKey', + 'wanted.cutoffUnmet.filterValue', + 'wanted.cutoffUnmet.columns' +]; + +const serverSideCollectionNames = [ + 'missing', + 'cutoffUnmet' +]; + +const wantedReducers = handleActions({ + + [types.SET]: createReducers(serverSideCollectionNames, createSetReducer), + [types.UPDATE]: createReducers(serverSideCollectionNames, createUpdateReducer), + [types.UPDATE_ITEM]: createReducers(serverSideCollectionNames, createUpdateItemReducer), + [types.UPDATE_SERVER_SIDE_COLLECTION]: createReducers(serverSideCollectionNames, createUpdateServerSideCollectionReducer), + + [types.SET_MISSING_TABLE_OPTION]: createSetTableOptionReducer('missing'), + [types.SET_CUTOFF_UNMET_TABLE_OPTION]: createSetTableOptionReducer('cutoffUnmet'), + + [types.CLEAR_MISSING]: createClearReducer('missing', { + isFetching: false, + isPopulated: false, + error: null, + items: [] + }), + + [types.CLEAR_CUTOFF_UNMET]: createClearReducer('cutoffUnmet', { + isFetching: false, + isPopulated: false, + error: null, + items: [] + }) + +}, defaultState); + +export default wantedReducers; diff --git a/frontend/src/Store/Selectors/createAllArtistSelector.js b/frontend/src/Store/Selectors/createAllArtistSelector.js new file mode 100644 index 000000000..38b1bcef1 --- /dev/null +++ b/frontend/src/Store/Selectors/createAllArtistSelector.js @@ -0,0 +1,12 @@ +import { createSelector } from 'reselect'; + +function createAllArtistSelector() { + return createSelector( + (state) => state.artist, + (artist) => { + return artist.items; + } + ); +} + +export default createAllArtistSelector; diff --git a/frontend/src/Store/Selectors/createArtistSelector.js b/frontend/src/Store/Selectors/createArtistSelector.js new file mode 100644 index 000000000..bc49a9349 --- /dev/null +++ b/frontend/src/Store/Selectors/createArtistSelector.js @@ -0,0 +1,15 @@ +import _ from 'lodash'; +import { createSelector } from 'reselect'; +import createAllArtistSelector from './createAllArtistSelector'; + +function createArtistSelector() { + return createSelector( + (state, { artistId }) => artistId, + createAllArtistSelector(), + (artistId, artist) => { + return _.find(artist, { id: artistId }); + } + ); +} + +export default createArtistSelector; diff --git a/frontend/src/Store/Selectors/createClientSideCollectionSelector.js b/frontend/src/Store/Selectors/createClientSideCollectionSelector.js new file mode 100644 index 000000000..56b75c80c --- /dev/null +++ b/frontend/src/Store/Selectors/createClientSideCollectionSelector.js @@ -0,0 +1,117 @@ +import _ from 'lodash'; +import { createSelector } from 'reselect'; +import { filterTypes, sortDirections } from 'Helpers/Props'; + +const filterTypePredicates = { + [filterTypes.CONTAINS]: function(value, filterValue) { + return value.toLowerCase().indexOf(filterValue.toLowerCase()) > -1; + }, + + [filterTypes.EQUAL]: function(value, filterValue) { + return value === filterValue; + }, + + [filterTypes.GREATER_THAN]: function(value, filterValue) { + return value > filterValue; + }, + + [filterTypes.GREATER_THAN_OR_EQUAL]: function(value, filterValue) { + return value >= filterValue; + }, + + [filterTypes.LESS_THAN]: function(value, filterValue) { + return value < filterValue; + }, + + [filterTypes.LESS_THAN_OR_EQUAL]: function(value, filterValue) { + return value <= filterValue; + }, + + [filterTypes.NOT_EQUAL]: function(value, filterValue) { + return value !== filterValue; + } +}; + +function getSortClause(sortKey, sortDirection, sortPredicates) { + if (sortPredicates && sortPredicates.hasOwnProperty(sortKey)) { + return function(item) { + return sortPredicates[sortKey](item, sortDirection); + }; + } + + return function(item) { + return item[sortKey]; + }; +} + +function filter(items, state) { + const { + filterKey, + filterValue, + filterType, + filterPredicates + } = state; + + if (!filterKey || !filterValue) { + return items; + } + + return _.filter(items, (item) => { + if (filterPredicates && filterPredicates.hasOwnProperty(filterKey)) { + return filterPredicates[filterKey](item); + } + + if (item.hasOwnProperty(filterKey)) { + return filterTypePredicates[filterType](item[filterKey], filterValue); + } + + return false; + }); +} + +function sort(items, state) { + const { + sortKey, + sortDirection, + sortPredicates, + secondarySortKey, + secondarySortDirection + } = state; + + const clauses = []; + const orders = []; + + clauses.push(getSortClause(sortKey, sortDirection, sortPredicates)); + orders.push(sortDirection === sortDirections.ASCENDING ? 'asc' : 'desc'); + + if (secondarySortKey && + secondarySortDirection && + (sortKey !== secondarySortKey || + sortDirection !== secondarySortDirection)) { + clauses.push(getSortClause(secondarySortKey, secondarySortDirection, sortPredicates)); + orders.push(secondarySortDirection === sortDirections.ASCENDING ? 'asc' : 'desc'); + } + + return _.orderBy(items, clauses, orders); +} + +function createClientSideCollectionSelector() { + return createSelector( + (state, { section }) => _.get(state, section), + (state, { uiSection }) => _.get(state, uiSection), + (sectionState, uiSectionState = {}) => { + const state = Object.assign({}, sectionState, uiSectionState); + + const filtered = filter(state.items, state); + const sorted = sort(filtered, state); + + return { + ...sectionState, + ...uiSectionState, + items: sorted + }; + } + ); +} + +export default createClientSideCollectionSelector; diff --git a/frontend/src/Store/Selectors/createCommandExecutingSelector.js b/frontend/src/Store/Selectors/createCommandExecutingSelector.js new file mode 100644 index 000000000..ac79194ad --- /dev/null +++ b/frontend/src/Store/Selectors/createCommandExecutingSelector.js @@ -0,0 +1,14 @@ +import _ from 'lodash'; +import { createSelector } from 'reselect'; +import createCommandsSelector from './createCommandsSelector'; + +function createCommandExecutingSelector(name) { + return createSelector( + createCommandsSelector(), + (commands) => { + return _.some(commands, { name }); + } + ); +} + +export default createCommandExecutingSelector; diff --git a/frontend/src/Store/Selectors/createCommandSelector.js b/frontend/src/Store/Selectors/createCommandSelector.js new file mode 100644 index 000000000..ff5bfe50a --- /dev/null +++ b/frontend/src/Store/Selectors/createCommandSelector.js @@ -0,0 +1,14 @@ +import _ from 'lodash'; +import { createSelector } from 'reselect'; +import createCommandsSelector from './createCommandsSelector'; + +function createCommandSelector(name, contraints = {}) { + return createSelector( + createCommandsSelector(), + (commands) => { + return _.some(commands, { name, ...contraints }); + } + ); +} + +export default createCommandSelector; diff --git a/frontend/src/Store/Selectors/createCommandsSelector.js b/frontend/src/Store/Selectors/createCommandsSelector.js new file mode 100644 index 000000000..7b9edffd9 --- /dev/null +++ b/frontend/src/Store/Selectors/createCommandsSelector.js @@ -0,0 +1,12 @@ +import { createSelector } from 'reselect'; + +function createCommandsSelector() { + return createSelector( + (state) => state.commands, + (commands) => { + return commands.items; + } + ); +} + +export default createCommandsSelector; diff --git a/frontend/src/Store/Selectors/createDimensionsSelector.js b/frontend/src/Store/Selectors/createDimensionsSelector.js new file mode 100644 index 000000000..ce26b2e2c --- /dev/null +++ b/frontend/src/Store/Selectors/createDimensionsSelector.js @@ -0,0 +1,12 @@ +import { createSelector } from 'reselect'; + +function createDimensionsSelector() { + return createSelector( + (state) => state.app.dimensions, + (dimensions) => { + return dimensions; + } + ); +} + +export default createDimensionsSelector; diff --git a/frontend/src/Store/Selectors/createEpisodeSelector.js b/frontend/src/Store/Selectors/createEpisodeSelector.js new file mode 100644 index 000000000..9b987a581 --- /dev/null +++ b/frontend/src/Store/Selectors/createEpisodeSelector.js @@ -0,0 +1,15 @@ +import _ from 'lodash'; +import { createSelector } from 'reselect'; +import episodeEntities from 'Album/episodeEntities'; + +function createEpisodeSelector() { + return createSelector( + (state, { albumId }) => albumId, + (state, { episodeEntity = episodeEntities.EPISODES }) => _.get(state, episodeEntity, { items: [] }), + (albumId, episodes) => { + return _.find(episodes.items, { id: albumId }); + } + ); +} + +export default createEpisodeSelector; diff --git a/frontend/src/Store/Selectors/createExistingArtistSelector.js b/frontend/src/Store/Selectors/createExistingArtistSelector.js new file mode 100644 index 000000000..4811f2034 --- /dev/null +++ b/frontend/src/Store/Selectors/createExistingArtistSelector.js @@ -0,0 +1,15 @@ +import _ from 'lodash'; +import { createSelector } from 'reselect'; +import createAllArtistSelector from './createAllArtistSelector'; + +function createExistingArtistSelector() { + return createSelector( + (state, { foreignArtistId }) => foreignArtistId, + createAllArtistSelector(), + (foreignArtistId, artist) => { + return _.some(artist, { foreignArtistId }); + } + ); +} + +export default createExistingArtistSelector; diff --git a/frontend/src/Store/Selectors/createImportArtistItemSelector.js b/frontend/src/Store/Selectors/createImportArtistItemSelector.js new file mode 100644 index 000000000..6d72dc547 --- /dev/null +++ b/frontend/src/Store/Selectors/createImportArtistItemSelector.js @@ -0,0 +1,27 @@ +import _ from 'lodash'; +import { createSelector } from 'reselect'; +import createAllArtistSelector from './createAllArtistSelector'; + +function createImportArtistItemSelector() { + return createSelector( + (state, { id }) => id, + (state) => state.addArtist, + (state) => state.importArtist, + createAllArtistSelector(), + (id, addArtist, importArtist, artist) => { + const item = _.find(importArtist.items, { id }) || {}; + const selectedArtist = item && item.selectedArtist; + const isExistingArtist = !!selectedArtist && _.some(artist, { foreignArtistId: selectedArtist.foreignArtistId }); + + return { + defaultMonitor: addArtist.defaults.monitor, + defaultQualityProfileId: addArtist.defaults.qualityProfileId, + defaultAlbumFolder: addArtist.defaults.albumFolder, + ...item, + isExistingArtist + }; + } + ); +} + +export default createImportArtistItemSelector; diff --git a/frontend/src/Store/Selectors/createLanguageProfileSelector.js b/frontend/src/Store/Selectors/createLanguageProfileSelector.js new file mode 100644 index 000000000..2ad04d506 --- /dev/null +++ b/frontend/src/Store/Selectors/createLanguageProfileSelector.js @@ -0,0 +1,14 @@ +import _ from 'lodash'; +import { createSelector } from 'reselect'; + +function createLanguageProfileSelector() { + return createSelector( + (state, { languageProfileId }) => languageProfileId, + (state) => state.settings.languageProfiles.items, + (languageProfileId, languageProfiles) => { + return _.find(languageProfiles, { id: languageProfileId }); + } + ); +} + +export default createLanguageProfileSelector; diff --git a/frontend/src/Store/Selectors/createProfileInUseSelector.js b/frontend/src/Store/Selectors/createProfileInUseSelector.js new file mode 100644 index 000000000..309408459 --- /dev/null +++ b/frontend/src/Store/Selectors/createProfileInUseSelector.js @@ -0,0 +1,19 @@ +import _ from 'lodash'; +import { createSelector } from 'reselect'; +import createAllArtistSelector from './createAllArtistSelector'; + +function createProfileInUseSelector(profileProp) { + return createSelector( + (state, { id }) => id, + createAllArtistSelector(), + (id, artist) => { + if (!id) { + return false; + } + + return _.some(artist, { [profileProp]: id }); + } + ); +} + +export default createProfileInUseSelector; diff --git a/frontend/src/Store/Selectors/createProviderSettingsSelector.js b/frontend/src/Store/Selectors/createProviderSettingsSelector.js new file mode 100644 index 000000000..c1018b661 --- /dev/null +++ b/frontend/src/Store/Selectors/createProviderSettingsSelector.js @@ -0,0 +1,59 @@ +import _ from 'lodash'; +import { createSelector } from 'reselect'; +import selectSettings from 'Store/Selectors/selectSettings'; + +function createProviderSettingsSelector() { + return createSelector( + (state, { id }) => id, + (state, { section }) => state.settings[section], + (id, section) => { + if (!id) { + const item = _.isArray(section.schema) ? section.selectedSchema : section.schema; + const settings = selectSettings(Object.assign({ name: '' }, item), section.pendingChanges, section.saveError); + + const { + isFetchingSchema: isFetching, + schemaError: error, + isSaving, + saveError, + isTesting, + pendingChanges + } = section; + + return { + isFetching, + error, + isSaving, + saveError, + isTesting, + pendingChanges, + ...settings, + item: settings.settings + }; + } + + const { + isFetching, + error, + isSaving, + saveError, + isTesting, + pendingChanges + } = section; + + const settings = selectSettings(_.find(section.items, { id }), pendingChanges, saveError); + + return { + isFetching, + error, + isSaving, + saveError, + isTesting, + item: settings.settings, + ...settings + }; + } + ); +} + +export default createProviderSettingsSelector; diff --git a/frontend/src/Store/Selectors/createQualityProfileSelector.js b/frontend/src/Store/Selectors/createQualityProfileSelector.js new file mode 100644 index 000000000..9308d63ac --- /dev/null +++ b/frontend/src/Store/Selectors/createQualityProfileSelector.js @@ -0,0 +1,14 @@ +import _ from 'lodash'; +import { createSelector } from 'reselect'; + +function createQualityProfileSelector() { + return createSelector( + (state, { qualityProfileId }) => qualityProfileId, + (state) => state.settings.qualityProfiles.items, + (qualityProfileId, qualityProfiles) => { + return _.find(qualityProfiles, { id: qualityProfileId }); + } + ); +} + +export default createQualityProfileSelector; diff --git a/frontend/src/Store/Selectors/createQueueItemSelector.js b/frontend/src/Store/Selectors/createQueueItemSelector.js new file mode 100644 index 000000000..2e902d7de --- /dev/null +++ b/frontend/src/Store/Selectors/createQueueItemSelector.js @@ -0,0 +1,20 @@ +import _ from 'lodash'; +import { createSelector } from 'reselect'; + +function createQueueItemSelector() { + return createSelector( + (state, { albumId }) => albumId, + (state) => state.queue.details, + (albumId, details) => { + if (!albumId) { + return null; + } + + return _.find(details.items, (item) => { + return item.album.id === albumId; + }); + } + ); +} + +export default createQueueItemSelector; diff --git a/frontend/src/Store/Selectors/createSettingsSectionSelector.js b/frontend/src/Store/Selectors/createSettingsSectionSelector.js new file mode 100644 index 000000000..7ff0d2708 --- /dev/null +++ b/frontend/src/Store/Selectors/createSettingsSectionSelector.js @@ -0,0 +1,32 @@ +import { createSelector } from 'reselect'; +import selectSettings from 'Store/Selectors/selectSettings'; + +function createSettingsSectionSelector() { + return createSelector( + (state, { section }) => state.settings[section], + (sectionSettings) => { + const { + isFetching, + isPopulated, + error, + item, + pendingChanges, + isSaving, + saveError + } = sectionSettings; + + const settings = selectSettings(item, pendingChanges, saveError); + + return { + isFetching, + isPopulated, + error, + isSaving, + saveError, + ...settings + }; + } + ); +} + +export default createSettingsSectionSelector; diff --git a/frontend/src/Store/Selectors/createSystemStatusSelector.js b/frontend/src/Store/Selectors/createSystemStatusSelector.js new file mode 100644 index 000000000..df586bbb9 --- /dev/null +++ b/frontend/src/Store/Selectors/createSystemStatusSelector.js @@ -0,0 +1,12 @@ +import { createSelector } from 'reselect'; + +function createSystemStatusSelector() { + return createSelector( + (state) => state.system.status, + (status) => { + return status.item; + } + ); +} + +export default createSystemStatusSelector; diff --git a/frontend/src/Store/Selectors/createTagsSelector.js b/frontend/src/Store/Selectors/createTagsSelector.js new file mode 100644 index 000000000..fbfd91cdb --- /dev/null +++ b/frontend/src/Store/Selectors/createTagsSelector.js @@ -0,0 +1,12 @@ +import { createSelector } from 'reselect'; + +function createTagsSelector() { + return createSelector( + (state) => state.tags.items, + (tags) => { + return tags; + } + ); +} + +export default createTagsSelector; diff --git a/frontend/src/Store/Selectors/createTrackFileSelector.js b/frontend/src/Store/Selectors/createTrackFileSelector.js new file mode 100644 index 000000000..fb55fdc94 --- /dev/null +++ b/frontend/src/Store/Selectors/createTrackFileSelector.js @@ -0,0 +1,18 @@ +import _ from 'lodash'; +import { createSelector } from 'reselect'; + +function createTrackFileSelector() { + return createSelector( + (state, { trackFileId }) => trackFileId, + (state) => state.trackFiles, + (trackFileId, trackFiles) => { + if (!trackFileId) { + return null; + } + + return _.find(trackFiles.items, { id: trackFileId }); + } + ); +} + +export default createTrackFileSelector; diff --git a/frontend/src/Store/Selectors/createTrackSelector.js b/frontend/src/Store/Selectors/createTrackSelector.js new file mode 100644 index 000000000..be57e6ca0 --- /dev/null +++ b/frontend/src/Store/Selectors/createTrackSelector.js @@ -0,0 +1,14 @@ +import _ from 'lodash'; +import { createSelector } from 'reselect'; + +function createTrackSelector() { + return createSelector( + (state, { trackId }) => trackId, + (state) => state.tracks, + (trackId, tracks) => { + return _.find(tracks.items, { id: trackId }); + } + ); +} + +export default createTrackSelector; diff --git a/frontend/src/Store/Selectors/createUISettingsSelector.js b/frontend/src/Store/Selectors/createUISettingsSelector.js new file mode 100644 index 000000000..b256d0e98 --- /dev/null +++ b/frontend/src/Store/Selectors/createUISettingsSelector.js @@ -0,0 +1,12 @@ +import { createSelector } from 'reselect'; + +function createUISettingsSelector() { + return createSelector( + (state) => state.settings.ui, + (ui) => { + return ui.item; + } + ); +} + +export default createUISettingsSelector; diff --git a/frontend/src/Store/Selectors/selectSettings.js b/frontend/src/Store/Selectors/selectSettings.js new file mode 100644 index 000000000..74e5444c9 --- /dev/null +++ b/frontend/src/Store/Selectors/selectSettings.js @@ -0,0 +1,97 @@ +import _ from 'lodash'; + +function getValidationFailures(saveError) { + if (!saveError || saveError.status !== 400) { + return []; + } + + return _.cloneDeep(saveError.responseJSON); +} + +function mapFailure(failure) { + return { + message: failure.errorMessage, + link: failure.infoLink, + detailedMessage: failure.detailedDescription + }; +} + +function selectSettings(item, pendingChanges, saveError) { + const validationFailures = getValidationFailures(saveError); + + // Merge all settings from the item along with pending + // changes to ensure any settings that were not included + // with the item are included. + const allSettings = Object.assign({}, item, pendingChanges); + + const settings = _.reduce(allSettings, (result, value, key) => { + if (key === 'fields') { + return result; + } + + const setting = { + value: item[key], + errors: _.map(_.remove(validationFailures, (failure) => { + return failure.propertyName.toLowerCase() === key.toLowerCase() && !failure.isWarning; + }), mapFailure), + + warnings: _.map(_.remove(validationFailures, (failure) => { + return failure.propertyName.toLowerCase() === key.toLowerCase() && failure.isWarning; + }), mapFailure) + }; + + if (pendingChanges.hasOwnProperty(key)) { + setting.previousValue = setting.value; + setting.value = pendingChanges[key]; + setting.pending = true; + } + + result[key] = setting; + return result; + }, {}); + + const fields = _.reduce(item.fields, (result, f) => { + const field = Object.assign({ pending: false }, f); + const hasPendingFieldChange = pendingChanges.fields && pendingChanges.fields.hasOwnProperty(field.name); + + if (hasPendingFieldChange) { + field.previousValue = field.value; + field.value = pendingChanges.fields[field.name]; + field.pending = true; + } + + field.errors = _.map(_.remove(validationFailures, (failure) => { + return failure.propertyName.toLowerCase() === field.name.toLowerCase() && !failure.isWarning; + }), mapFailure); + + field.warnings = _.map(_.remove(validationFailures, (failure) => { + return failure.propertyName.toLowerCase() === field.name.toLowerCase() && failure.isWarning; + }), mapFailure); + + result.push(field); + return result; + }, []); + + if (fields.length) { + settings.fields = fields; + } + + const validationErrors = _.filter(validationFailures, (failure) => { + return !failure.isWarning; + }); + + const validationWarnings = _.filter(validationFailures, (failure) => { + return failure.isWarning; + }); + + return { + settings, + validationErrors, + validationWarnings, + hasPendingChanges: !_.isEmpty(pendingChanges), + hasSettings: !_.isEmpty(settings), + pendingChanges + }; +} + +export default selectSettings; diff --git a/frontend/src/Store/connectSection.js b/frontend/src/Store/connectSection.js new file mode 100644 index 000000000..5d309cc5e --- /dev/null +++ b/frontend/src/Store/connectSection.js @@ -0,0 +1,57 @@ +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import getDisplayName from 'Helpers/getDisplayName'; + +function connectSection(mapStateToProps, mapDispatchToProps, mergeProps, options = {}, sectionOptions = {}) { + return function wrap(WrappedComponent) { + const ConnectedComponent = connect(mapStateToProps, mapDispatchToProps, mergeProps, options)(WrappedComponent); + + class Section extends Component { + + // + // Control + + getWrappedInstance = () => { + if (this._wrappedInstance) { + return this._wrappedInstance.getWrappedInstance(); + } + } + + // + // Listeners + + setWrappedInstanceRef = (ref) => { + this._wrappedInstance = ref; + } + + // + // Render + + render() { + if (options.withRef) { + return ( + + ); + } + + return ( + + ); + } + } + + Section.displayName = `Section(${getDisplayName(WrappedComponent)})`; + Section.WrappedComponent = WrappedComponent; + + return Section; + }; +} + +export default connectSection; diff --git a/frontend/src/Store/createAppStore.js b/frontend/src/Store/createAppStore.js new file mode 100644 index 000000000..168bbc954 --- /dev/null +++ b/frontend/src/Store/createAppStore.js @@ -0,0 +1,15 @@ +import { createStore } from 'redux'; +import reducers, { defaultState } from 'Store/Reducers'; +import middlewares from 'Store/Middleware/middlewares'; + +function createAppStore(history) { + const appStore = createStore( + reducers, + defaultState, + middlewares(history) + ); + + return appStore; +} + +export default createAppStore; diff --git a/frontend/src/Store/scrollPositions.js b/frontend/src/Store/scrollPositions.js new file mode 100644 index 000000000..287a58593 --- /dev/null +++ b/frontend/src/Store/scrollPositions.js @@ -0,0 +1,5 @@ +const scrollPositions = { + artistIndex: 0 +}; + +export default scrollPositions; diff --git a/src/UI/Shared/Styles/clickable.less b/frontend/src/Styles/Mixins/clickable.css similarity index 100% rename from src/UI/Shared/Styles/clickable.less rename to frontend/src/Styles/Mixins/clickable.css diff --git a/frontend/src/Styles/Mixins/cover.css b/frontend/src/Styles/Mixins/cover.css new file mode 100644 index 000000000..e44c99be6 --- /dev/null +++ b/frontend/src/Styles/Mixins/cover.css @@ -0,0 +1,8 @@ +@define-mixin cover { + position: absolute; + top: 0; + left: 0; + display: block; + width: 100%; + height: 100%; +} diff --git a/frontend/src/Styles/Mixins/linkOverlay.css b/frontend/src/Styles/Mixins/linkOverlay.css new file mode 100644 index 000000000..74c3fd753 --- /dev/null +++ b/frontend/src/Styles/Mixins/linkOverlay.css @@ -0,0 +1,11 @@ +@define-mixin linkOverlay { + @add-mixin cover; + + pointer-events: none; + user-select: none; + + a, + button { + pointer-events: all; + } +} diff --git a/frontend/src/Styles/Mixins/scroller.css b/frontend/src/Styles/Mixins/scroller.css new file mode 100644 index 000000000..62a619103 --- /dev/null +++ b/frontend/src/Styles/Mixins/scroller.css @@ -0,0 +1,26 @@ +@define-mixin scrollbar { + &::-webkit-scrollbar { + width: 6px; + height: 6px; + } +} + +@define-mixin scrollbarTrack { + &&::-webkit-scrollbar-track { + background-color: transparent; + } +} + +@define-mixin scrollbarThumb { + &::-webkit-scrollbar-thumb { + min-height: 50px; + border: 1px solid transparent; + border-radius: 5px; + background-color: $scrollbarBackgroundColor; + background-clip: padding-box; + + &:hover { + background-color: $scrollbarHoverBackgroundColor; + } + } +} diff --git a/frontend/src/Styles/Mixins/truncate.css b/frontend/src/Styles/Mixins/truncate.css new file mode 100644 index 000000000..d0762a029 --- /dev/null +++ b/frontend/src/Styles/Mixins/truncate.css @@ -0,0 +1,18 @@ +/** + * From: https://github.com/suitcss/utils-text/blob/master/lib/text.css + * + * Text truncation + * + * Prevent text from wrapping onto multiple lines, and truncate with an + * ellipsis. + * + * 1. Ensure that the node has a maximum width after which truncation can + * occur. + */ + + @define-mixin truncate { + overflow: hidden !important; + max-width: 100%; /* 1 */ + text-overflow: ellipsis !important; + white-space: nowrap !important; +} diff --git a/frontend/src/Styles/Variables/animations.js b/frontend/src/Styles/Variables/animations.js new file mode 100644 index 000000000..52d12827a --- /dev/null +++ b/frontend/src/Styles/Variables/animations.js @@ -0,0 +1,8 @@ +// Use CommonJS since this is consumed by PostCSS via webpack (node.js). + +module.exports = { + // Durations + defaultSpeed: '0.2s', + slowSpeed: '0.6s', + fastSpeed: '0.1s' +}; diff --git a/frontend/src/Styles/Variables/colors.js b/frontend/src/Styles/Variables/colors.js new file mode 100644 index 000000000..402358e66 --- /dev/null +++ b/frontend/src/Styles/Variables/colors.js @@ -0,0 +1,172 @@ +module.exports = { + defaultColor: '#333', + disabledColor: '#999', + dimColor: '#555', + black: '#000', + white: '#fff', + primaryColor: '#0b8750', + selectedColor: '#f9be03', + successColor: '#27c24c', + dangerColor: '#f05050', + warningColor: '#ffa500', + infoColor: '#00A65B', + purple: '#7a43b6', + nzbdronePurple: '#7932ea', + nzbdronePink: '#f43565', + sonarrBlue: '#00A65B', + helpTextColor: '#909293', + gray: '#adadad', + + // Theme Colors + + themeBlue: '#00A65B', + themeRed: '#c4273c', + themeDarkColor: '#216044', + themeLightColor: '#216044', + + torrentColor: '#00853d', + usenetColor: '#17b1d9', + + // Links + defaultLinkHoverColor: '#fff', + linkColor: '#0b8750', + linkHoverColor: '#1b72e2', + + // Sidebar + + sidebarColor: '#e1e2e3', + sidebarBackgroundColor: '#216044', + sidebarActiveBackgroundColor: '#353535', + + // Toolbar + toolbarColor: '#e1e2e3', + toolbarBackgroundColor: '#216044', + toolbarMenuItemBackgroundColor: '#4D8069', + toolbarMenuItemHoverBackgroundColor: '#216044', + toolbarLabelColor: '#8895aa', + + // Accents + borderColor: '#e5e5e5', + inputBorderColor: '#dde6e9', + inputBoxShadowColor: 'rgba(0, 0, 0, 0.075)', + inputFocusBorderColor: '#66afe9', + inputFocusBoxShadowColor: 'rgba(102, 175, 233, 0.6)', + inputErrorBorderColor: '#f05050', + inputErrorBoxShadowColor: 'rgba(240, 80, 80, 0.6)', + inputWarningBorderColor: '#ffa500', + inputWarningBoxShadowColor: 'rgba(255, 165, 0, 0.6)', + + // + // Buttons + + defaultBackgroundColor: '#fff', + defaultBorderColor: '#eaeaea', + defaultHoverBackgroundColor: '#f5f5f5', + defaultHoverBorderColor: '#d6d6d6;', + + primaryBackgroundColor: '#0b8750', + primaryBorderColor: '#216044', + primaryHoverBackgroundColor: '#097948', + primaryHoverBorderColor: '#1D563D;', + + successBackgroundColor: '#27c24c', + successBorderColor: '#26be4a', + successHoverBackgroundColor: '#24b145', + successHoverBorderColor: '#1f9c3d;', + + warningBackgroundColor: '#ff902b', + warningBorderColor: '#ff8d26', + warningHoverBackgroundColor: '#ff8517', + warningHoverBorderColor: '#fc7800;', + + dangerBackgroundColor: '#f05050', + dangerBorderColor: '#f04b4b', + dangerHoverBackgroundColor: '#ee3d3d', + dangerHoverBorderColor: '#ec2626;', + + iconButtonHoverColor: '#666', + iconButtonHoverLightColor: '#ccc', + + // + // Modal + + modalBackdropBackgroundColor: 'rgba(0, 0, 0, 0.6)', + modalBackgroundColor: '#fff', + modalCloseButtonHoverColor: '#888', + + // + // Menu + menuItemColor: '#e1e2e3', + menuItemHoverColor: '#fbfcfc', + + // + // Toolbar + + toobarButtonHoverColor: '#00A65B', + toobarButtonSelectedColor: '#00A65B', + + // + // Scroller + + scrollbarBackgroundColor: '#9ea4b9', + scrollbarHoverBackgroundColor: '#656d8c', + + // + // Card + + cardShadowColor: '#e1e1e1', + cardAlternateBackgroundColor: '#f5f5f5', + + // + // Alert + + alertDangerBorderColor: '#ebccd1', + alertDangerBackgroundColor: '#f2dede', + alertDangerColor: '#a94442', + + alertInfoBorderColor: '#bce8f1', + alertInfoBackgroundColor: '#d9edf7', + alertInfoColor: '#31708f', + + alertSuccessBorderColor: '#d6e9c6', + alertSuccessBackgroundColor: '#dff0d8', + alertSuccessColor: '#3c763d', + + alertWarningBorderColor: '#faebcc', + alertWarningBackgroundColor: '#fcf8e3', + alertWarningColor: '#8a6d3b', + + // + // Slider + + sliderAccentColor: '#0b8750', + + // + // Form + + advancedFormLabelColor: '#ff902b', + disabledCheckInputColor: '#ddd', + + // + // Popover + + popoverTitleBackgroundColor: '#f7f7f7', + popoverTitleBorderColor: '#ebebeb', + popoverShadowColor: 'rgba(0, 0, 0, 0.2)', + popoverArrowBorderColor: 'rgba(0, 0, 0, 0.25)', + + popoverTitleBackgroundInverseColor: '#3a3f51', + popoverTitleBorderInverseColor: '#216044', + popoverShadowInverseColor: 'rgba(0, 0, 0, 0.2)', + popoverArrowBorderInverseColor: 'rgba(58, 63, 81, 0.75)', + + // + // Calendar + + calendarTodayBackgroundColor: '#ddd', + + // + // Table + + tableRowHoverBackgroundColor: '#fafbfc' +}; diff --git a/frontend/src/Styles/Variables/dimensions.js b/frontend/src/Styles/Variables/dimensions.js new file mode 100644 index 000000000..54282902c --- /dev/null +++ b/frontend/src/Styles/Variables/dimensions.js @@ -0,0 +1,45 @@ +module.exports = { + // Page + pageContentBodyPadding: '20px', + pageContentBodyPaddingSmallScreen: '10px', + + // Header + headerHeight: '60px', + + // Sidebar + sidebarWidth: '210px', + + // Toolbar + toolbarHeight: '60px', + toolbarButtonWidth: '60px', + toolbarSeparatorMargin: '20px', + + // Break Points + breakpointExtraSmall: '480px', + breakpointSmall: '768px', + breakpointMedium: '992px', + breakpointLarge: '1200px', + + // Form + formGroupSmallWidth: '650px', + formGroupMediumWidth: '800px', + formGroupLargeWidth: '1200px', + formLabelWidth: '250px', + formLabelRightMarginWidth: '20px', + + // Drag + dragHandleWidth: '40px', + + // Progress Bar + progressBarSmallHeight: '5px', + progressBarMediumHeight: '15px', + progressBarLargeHeight: '20px', + + // Jump Bar + jumpBarItemHeight: '25px', + + // Artist + artistIndexColumnPadding: '20px', + artistIndexColumnPaddingSmallScreen: '10px', + artistIndexOverviewInfoRowHeight: '21px' +}; diff --git a/frontend/src/Styles/Variables/fonts.js b/frontend/src/Styles/Variables/fonts.js new file mode 100644 index 000000000..6221ce5f1 --- /dev/null +++ b/frontend/src/Styles/Variables/fonts.js @@ -0,0 +1,13 @@ +module.exports = { + // Families + defaultFontFamily: 'Roboto, "open sans", "Helvetica Neue", Helvetica, Arial, sans-serif', + monoSpaceFontFamily: '"Ubuntu Mono", Menlo, Monaco, Consolas, "Courier New", monospace;', + + // Sizes + extraSmallFontSize: '11px', + smallFontSize: '12px', + defaultFontSize: '14px', + largeFontSize: '16px', + + lineHeight: '1.528571429' +}; diff --git a/frontend/src/Styles/globals.css b/frontend/src/Styles/globals.css new file mode 100644 index 000000000..83e7649ae --- /dev/null +++ b/frontend/src/Styles/globals.css @@ -0,0 +1,8 @@ +/* stylelint-disable */ + +@import '~normalize.css/normalize.css'; +@import 'scaffolding.css'; +@import '../Content/Fonts/fonts.css'; +@import '../Content/Fonts/font-awesome.css'; + +/* stylelint-enable */ \ No newline at end of file diff --git a/frontend/src/Styles/scaffolding.css b/frontend/src/Styles/scaffolding.css new file mode 100644 index 000000000..8d95f8d12 --- /dev/null +++ b/frontend/src/Styles/scaffolding.css @@ -0,0 +1,45 @@ +/* stylelint-disable */ +* { + box-sizing: border-box; +} + +*::before, +*::after { + box-sizing: border-box; +} + +*:focus { + outline: none; +} +/* stylelint-enable */ + +html, +body { + color: #515253; + font-family: 'Roboto', 'open sans', 'Helvetica Neue', 'Helvetica', 'Arial', sans-serif; +} + +body { + font-size: 14px; + line-height: 1.528571429; /* 20/14 */ +} + +/* Override normalize */ + +button, +input, +optgroup, +select, +textarea { + margin: 0; + font-size: inherit; + font-family: inherit; + line-height: 1.528571429; /* 20/14 */ +} + +/* Better defaults for unordererd lists */ + +ul { + margin: 0; + padding-left: 20px; +} diff --git a/frontend/src/System/Backup/Backups.css b/frontend/src/System/Backup/Backups.css new file mode 100644 index 000000000..fb280312c --- /dev/null +++ b/frontend/src/System/Backup/Backups.css @@ -0,0 +1,5 @@ +.type { + composes: cell from 'Components/Table/Cells/TableRowCell.css'; + + width: 20px; +} diff --git a/frontend/src/System/Backup/Backups.js b/frontend/src/System/Backup/Backups.js new file mode 100644 index 000000000..02b8c3166 --- /dev/null +++ b/frontend/src/System/Backup/Backups.js @@ -0,0 +1,148 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { icons } from 'Helpers/Props'; +import Icon from 'Components/Icon'; +import Link from 'Components/Link/Link'; +import LoadingIndicator from 'Components/Loading/LoadingIndicator'; +import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector'; +import Table from 'Components/Table/Table'; +import TableBody from 'Components/Table/TableBody'; +import TableRow from 'Components/Table/TableRow'; +import TableRowCell from 'Components/Table/Cells/TableRowCell'; +import PageContent from 'Components/Page/PageContent'; +import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector'; +import PageToolbar from 'Components/Page/Toolbar/PageToolbar'; +import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection'; +import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton'; +import styles from './Backups.css'; + +const columns = [ + { + name: 'type', + isVisible: true + }, + { + name: 'name', + label: 'Name', + isVisible: true + }, + { + name: 'time', + label: 'Time', + isVisible: true + } +]; + +class Backups extends Component { + + // + // Render + + render() { + const { + isFetching, + items, + backupExecuting, + onBackupPress + } = this.props; + + const hasBackups = !isFetching && items.length > 0; + const noBackups = !isFetching && !items.length; + + return ( + + + + + + + + + { + isFetching && + + } + + { + noBackups && +
No backups are available
+ } + + { + hasBackups && + + + { + items.map((item) => { + const { + id, + type, + name, + path, + time + } = item; + + let iconClassName = icons.SCHEDULED; + let iconTooltip = 'Scheduled'; + + if (type === 'manual') { + iconClassName = icons.INTERACTIVE; + iconTooltip = 'Manual'; + } else if (item === 'update') { + iconClassName = icons.UPDATE; + iconTooltip = 'Before update'; + } + + return ( + + + { + + } + + + + + {name} + + + + + + ); + }) + } + +
+ } +
+
+ ); + } + +} + +Backups.propTypes = { + isFetching: PropTypes.bool.isRequired, + items: PropTypes.array.isRequired, + backupExecuting: PropTypes.bool.isRequired, + onBackupPress: PropTypes.func.isRequired +}; + +export default Backups; diff --git a/frontend/src/System/Backup/BackupsConnector.js b/frontend/src/System/Backup/BackupsConnector.js new file mode 100644 index 000000000..f6d5469eb --- /dev/null +++ b/frontend/src/System/Backup/BackupsConnector.js @@ -0,0 +1,81 @@ +import _ from 'lodash'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import createCommandsSelector from 'Store/Selectors/createCommandsSelector'; +import { fetchBackups } from 'Store/Actions/systemActions'; +import { executeCommand } from 'Store/Actions/commandActions'; +import * as commandNames from 'Commands/commandNames'; +import Backups from './Backups'; + +function createMapStateToProps() { + return createSelector( + (state) => state.system.backups, + createCommandsSelector(), + (backups, commands) => { + const { + isFetching, + items + } = backups; + + const backupExecuting = _.some(commands, { name: commandNames.BACKUP }); + + return { + isFetching, + items, + backupExecuting + }; + } + ); +} + +const mapDispatchToProps = { + fetchBackups, + executeCommand +}; + +class BackupsConnector extends Component { + + // + // Lifecycle + + componentDidMount() { + this.props.fetchBackups(); + } + + componentDidUpdate(prevProps) { + if (prevProps.backupExecuting && !this.props.backupExecuting) { + this.props.fetchBackups(); + } + } + + // + // Listeners + + onBackupPress = () => { + this.props.executeCommand({ + name: commandNames.BACKUP + }); + } + + // + // Render + + render() { + return ( + + ); + } +} + +BackupsConnector.propTypes = { + backupExecuting: PropTypes.bool.isRequired, + fetchBackups: PropTypes.func.isRequired, + executeCommand: PropTypes.func.isRequired +}; + +export default connect(createMapStateToProps, mapDispatchToProps)(BackupsConnector); diff --git a/frontend/src/System/Events/LogsTable.js b/frontend/src/System/Events/LogsTable.js new file mode 100644 index 000000000..7ed1f6cf1 --- /dev/null +++ b/frontend/src/System/Events/LogsTable.js @@ -0,0 +1,176 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { align, icons } from 'Helpers/Props'; +import LoadingIndicator from 'Components/Loading/LoadingIndicator'; +import Table from 'Components/Table/Table'; +import TableBody from 'Components/Table/TableBody'; +import TablePager from 'Components/Table/TablePager'; +import PageContent from 'Components/Page/PageContent'; +import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector'; +import PageToolbar from 'Components/Page/Toolbar/PageToolbar'; +import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection'; +import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton'; +import FilterMenu from 'Components/Menu/FilterMenu'; +import FilterMenuItem from 'Components/Menu/FilterMenuItem'; +import MenuContent from 'Components/Menu/MenuContent'; +import LogsTableRow from './LogsTableRow'; + +class LogsTable extends Component { + + // + // Listeners + + onFilterMenuItemPress = (filterKey, filterValue) => { + this.props.onFilterSelect(filterKey, filterValue); + } + + // + // Render + + render() { + const { + isFetching, + isPopulated, + error, + items, + columns, + filterKey, + filterValue, + totalRecords, + clearLogExecuting, + onRefreshPress, + onClearLogsPress, + ...otherProps + } = this.props; + + return ( + + + + + + + + + + + + + All + + + + Info + + + + Warn + + + + Error + + + + + + + + { + isFetching && !isPopulated && + + } + + { + isPopulated && !error && !items.length && +
+ No logs found +
+ } + + { + isPopulated && !error && !!items.length && +
+ + + { + items.map((item) => { + return ( + + ); + }) + } + +
+ + +
+ } +
+
+ ); + } + +} + +LogsTable.propTypes = { + isFetching: PropTypes.bool.isRequired, + isPopulated: PropTypes.bool.isRequired, + error: PropTypes.object, + items: PropTypes.arrayOf(PropTypes.object).isRequired, + columns: PropTypes.arrayOf(PropTypes.object).isRequired, + filterKey: PropTypes.string, + filterValue: PropTypes.string, + totalRecords: PropTypes.number, + clearLogExecuting: PropTypes.bool.isRequired, + onFilterSelect: PropTypes.func.isRequired, + onRefreshPress: PropTypes.func.isRequired, + onClearLogsPress: PropTypes.func.isRequired +}; + +export default LogsTable; diff --git a/frontend/src/System/Events/LogsTableConnector.js b/frontend/src/System/Events/LogsTableConnector.js new file mode 100644 index 000000000..428ea90fd --- /dev/null +++ b/frontend/src/System/Events/LogsTableConnector.js @@ -0,0 +1,130 @@ +import _ from 'lodash'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import createCommandsSelector from 'Store/Selectors/createCommandsSelector'; +import { executeCommand } from 'Store/Actions/commandActions'; +import * as systemActions from 'Store/Actions/systemActions'; +import * as commandNames from 'Commands/commandNames'; +import LogsTable from './LogsTable'; + +function createMapStateToProps() { + return createSelector( + (state) => state.system.logs, + createCommandsSelector(), + (logs, commands) => { + const clearLogExecuting = _.some(commands, { name: commandNames.CLEAR_LOGS }); + + return { + clearLogExecuting, + ...logs + }; + } + ); +} + +const mapDispatchToProps = { + executeCommand, + ...systemActions +}; + +class LogsTableConnector extends Component { + + // + // Lifecycle + + componentDidMount() { + this.props.fetchLogs(); + } + + componentDidUpdate(prevProps) { + if (prevProps.clearLogExecuting && !this.props.clearLogExecuting) { + this.props.gotoLogsFirstPage(); + } + } + + // + // Listeners + + onFirstPagePress = () => { + this.props.gotoLogsFirstPage(); + } + + onPreviousPagePress = () => { + this.props.gotoLogsPreviousPage(); + } + + onNextPagePress = () => { + this.props.gotoLogsNextPage(); + } + + onLastPagePress = () => { + this.props.gotoLogsLastPage(); + } + + onPageSelect = (page) => { + this.props.gotoLogsPage({ page }); + } + + onSortPress = (sortKey) => { + this.props.setLogsSort({ sortKey }); + } + + onFilterSelect = (filterKey, filterValue) => { + this.props.setLogsFilter({ filterKey, filterValue }); + } + + onTableOptionChange = (payload) => { + this.props.setLogsTableOption(payload); + + if (payload.pageSize) { + this.props.gotoLogsFirstPage(); + } + } + + onRefreshPress = () => { + this.props.gotoLogsFirstPage(); + } + + onClearLogsPress = () => { + this.props.executeCommand({ name: commandNames.CLEAR_LOGS }); + } + + // + // Render + + render() { + return ( + + ); + } +} + +LogsTableConnector.propTypes = { + clearLogExecuting: PropTypes.bool.isRequired, + fetchLogs: PropTypes.func.isRequired, + gotoLogsFirstPage: PropTypes.func.isRequired, + gotoLogsPreviousPage: PropTypes.func.isRequired, + gotoLogsNextPage: PropTypes.func.isRequired, + gotoLogsLastPage: PropTypes.func.isRequired, + gotoLogsPage: PropTypes.func.isRequired, + setLogsSort: PropTypes.func.isRequired, + setLogsFilter: PropTypes.func.isRequired, + setLogsTableOption: PropTypes.func.isRequired, + executeCommand: PropTypes.func.isRequired +}; + +export default connect(createMapStateToProps, mapDispatchToProps)(LogsTableConnector); diff --git a/frontend/src/System/Events/LogsTableDetailsModal.css b/frontend/src/System/Events/LogsTableDetailsModal.css new file mode 100644 index 000000000..127c1139f --- /dev/null +++ b/frontend/src/System/Events/LogsTableDetailsModal.css @@ -0,0 +1,17 @@ +.detailsText { + composes: scroller from 'Components/Scroller/Scroller.css'; + + display: block; + margin: 0 0 10.5px; + padding: 10px; + border: 1px solid #ccc; + border-radius: 4px; + background-color: #f5f5f5; + color: #3a3f51; + white-space: pre; + word-wrap: break-word; + word-break: break-all; + font-size: 13px; + font-family: $monoSpaceFontFamily; + line-height: 1.52857143; +} diff --git a/frontend/src/System/Events/LogsTableDetailsModal.js b/frontend/src/System/Events/LogsTableDetailsModal.js new file mode 100644 index 000000000..de6a881df --- /dev/null +++ b/frontend/src/System/Events/LogsTableDetailsModal.js @@ -0,0 +1,74 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import { scrollDirections } from 'Helpers/Props'; +import Button from 'Components/Link/Button'; +import Scroller from 'Components/Scroller/Scroller'; +import Modal from 'Components/Modal/Modal'; +import ModalContent from 'Components/Modal/ModalContent'; +import ModalHeader from 'Components/Modal/ModalHeader'; +import ModalBody from 'Components/Modal/ModalBody'; +import ModalFooter from 'Components/Modal/ModalFooter'; +import styles from './LogsTableDetailsModal.css'; + +function LogsTableDetailsModal(props) { + const { + isOpen, + message, + exception, + onModalClose + } = props; + + return ( + + + + Details + + + +
Message
+ + + {message} + + + { + !!exception && +
+
Exception
+ + {exception} + +
+ } +
+ + + + +
+
+ ); +} + +LogsTableDetailsModal.propTypes = { + isOpen: PropTypes.bool.isRequired, + message: PropTypes.string.isRequired, + exception: PropTypes.string, + onModalClose: PropTypes.func.isRequired +}; + +export default LogsTableDetailsModal; diff --git a/frontend/src/System/Events/LogsTableRow.css b/frontend/src/System/Events/LogsTableRow.css new file mode 100644 index 000000000..557d690f0 --- /dev/null +++ b/frontend/src/System/Events/LogsTableRow.css @@ -0,0 +1,35 @@ +.level { + composes: cell from 'Components/Table/Cells/TableRowCell.css'; + + width: 20px; +} + +.info { + color: #1e90ff; +} + +.debug { + color: #808080; +} + +.trace { + color: #d3d3d3; +} + +.warn { + color: $warningColor; +} + +.error { + color: $dangerColor; +} + +.fatal { + color: $purple; +} + +.actions { + composes: cell from 'Components/Table/Cells/TableRowCell.css'; + + width: 45px; +} diff --git a/frontend/src/System/Events/LogsTableRow.js b/frontend/src/System/Events/LogsTableRow.js new file mode 100644 index 000000000..6321c5748 --- /dev/null +++ b/frontend/src/System/Events/LogsTableRow.js @@ -0,0 +1,152 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { icons } from 'Helpers/Props'; +import Icon from 'Components/Icon'; +import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector'; +import TableRowButton from 'Components/Table/TableRowButton'; +import TableRowCell from 'Components/Table/Cells/TableRowCell'; +import LogsTableDetailsModal from './LogsTableDetailsModal'; +import styles from './LogsTableRow.css'; + +function getIconName(level) { + switch (level) { + case 'trace': + case 'debug': + case 'info': + return icons.INFO; + case 'warn': + return icons.DANGER; + case 'error': + return icons.BUG; + case 'fatal': + return icons.FATAL; + default: + return icons.UNKNOWN; + } +} + +class LogsTableRow extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + isDetailsModalOpen: false + }; + } + + // + // Listeners + + onPress = () => { + this.setState({ isDetailsModalOpen: true }); + } + + onModalClose = () => { + this.setState({ isDetailsModalOpen: false }); + } + + // + // Render + + render() { + const { + level, + logger, + message, + time, + exception, + columns + } = this.props; + + return ( + + { + columns.map((column) => { + const { + name, + isVisible + } = column; + + if (!isVisible) { + return null; + } + + if (name === 'level') { + return ( + + + + ); + } + + if (name === 'logger') { + return ( + + {logger} + + ); + } + + if (name === 'message') { + return ( + + {message} + + ); + } + + if (name === 'time') { + return ( + + ); + } + + if (name === 'actions') { + return ( + + ); + } + + return null; + }) + } + + + + ); + } + +} + +LogsTableRow.propTypes = { + level: PropTypes.string.isRequired, + logger: PropTypes.string.isRequired, + message: PropTypes.string.isRequired, + time: PropTypes.string.isRequired, + exception: PropTypes.string, + columns: PropTypes.arrayOf(PropTypes.object).isRequired +}; + +export default LogsTableRow; diff --git a/frontend/src/System/Logs/Files/LogFiles.js b/frontend/src/System/Logs/Files/LogFiles.js new file mode 100644 index 000000000..47482b3fe --- /dev/null +++ b/frontend/src/System/Logs/Files/LogFiles.js @@ -0,0 +1,139 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { icons } from 'Helpers/Props'; +import Alert from 'Components/Alert'; +import Link from 'Components/Link/Link'; +import LoadingIndicator from 'Components/Loading/LoadingIndicator'; +import Table from 'Components/Table/Table'; +import PageContent from 'Components/Page/PageContent'; +import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector'; +import PageToolbar from 'Components/Page/Toolbar/PageToolbar'; +import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection'; +import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator'; +import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton'; +import TableBody from 'Components/Table/TableBody'; +import LogsNavMenu from '../LogsNavMenu'; +import LogFilesTableRow from './LogFilesTableRow'; + +const columns = [ + { + name: 'filename', + label: 'Filename', + isVisible: true + }, + { + name: 'lastWriteTime', + label: 'Last Write Time', + isVisible: true + }, + { + name: 'download', + isVisible: true + } +]; + +class LogFiles extends Component { + + // + // Render + + render() { + const { + isFetching, + items, + deleteFilesExecuting, + currentLogView, + location, + onRefreshPress, + onDeleteFilesPress, + ...otherProps + } = this.props; + + return ( + + + + + + + + + + + + + + +
+ Log files are located in: {location} +
+ + { + currentLogView === 'Log Files' && +
+ The log level defaults to 'Info' and can be changed in General Settings +
+ } +
+ + { + isFetching && + + } + + { + !isFetching && !!items.length && +
+ + + { + items.map((item) => { + return ( + + ); + }) + } + +
+
+ } + + { + !isFetching && !items.length && +
No log files
+ } +
+
+ ); + } + +} + +LogFiles.propTypes = { + isFetching: PropTypes.bool.isRequired, + items: PropTypes.array.isRequired, + deleteFilesExecuting: PropTypes.bool.isRequired, + currentLogView: PropTypes.string.isRequired, + location: PropTypes.string.isRequired, + onRefreshPress: PropTypes.func.isRequired, + onDeleteFilesPress: PropTypes.func.isRequired +}; + +export default LogFiles; diff --git a/frontend/src/System/Logs/Files/LogFilesConnector.js b/frontend/src/System/Logs/Files/LogFilesConnector.js new file mode 100644 index 000000000..16d8d23b9 --- /dev/null +++ b/frontend/src/System/Logs/Files/LogFilesConnector.js @@ -0,0 +1,93 @@ +import _ from 'lodash'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import combinePath from 'Utilities/String/combinePath'; +import createCommandsSelector from 'Store/Selectors/createCommandsSelector'; +import { executeCommand } from 'Store/Actions/commandActions'; +import { fetchLogFiles } from 'Store/Actions/systemActions'; +import * as commandNames from 'Commands/commandNames'; +import LogFiles from './LogFiles'; + +function createMapStateToProps() { + return createSelector( + (state) => state.system.logFiles, + (state) => state.system.status.item, + createCommandsSelector(), + (logFiles, status, commands) => { + const { + isFetching, + items + } = logFiles; + + const { + appData, + isWindows + } = status; + + const deleteFilesExecuting = _.some(commands, { name: commandNames.DELETE_LOG_FILES }); + + return { + isFetching, + items, + deleteFilesExecuting, + currentLogView: 'Log Files', + location: combinePath(isWindows, appData, ['logs']) + }; + } + ); +} + +const mapDispatchToProps = { + fetchLogFiles, + executeCommand +}; + +class LogFilesConnector extends Component { + + // + // Lifecycle + + componentDidMount() { + this.props.fetchLogFiles(); + } + + componentDidUpdate(prevProps) { + if (prevProps.deleteFilesExecuting && !this.props.deleteFilesExecuting) { + this.props.fetchLogFiles(); + } + } + + // + // Listeners + + onRefreshPress = () => { + this.props.fetchLogFiles(); + } + + onDeleteFilesPress = () => { + this.props.executeCommand({ name: commandNames.DELETE_LOG_FILES }); + } + + // + // Render + + render() { + return ( + + ); + } +} + +LogFilesConnector.propTypes = { + deleteFilesExecuting: PropTypes.bool.isRequired, + fetchLogFiles: PropTypes.func.isRequired, + executeCommand: PropTypes.func.isRequired +}; + +export default connect(createMapStateToProps, mapDispatchToProps)(LogFilesConnector); diff --git a/frontend/src/System/Logs/Files/LogFilesTableRow.css b/frontend/src/System/Logs/Files/LogFilesTableRow.css new file mode 100644 index 000000000..779794b7d --- /dev/null +++ b/frontend/src/System/Logs/Files/LogFilesTableRow.css @@ -0,0 +1,5 @@ +.download { + composes: cell from 'Components/Table/Cells/TableRowCell.css'; + + width: 100px; +} diff --git a/frontend/src/System/Logs/Files/LogFilesTableRow.js b/frontend/src/System/Logs/Files/LogFilesTableRow.js new file mode 100644 index 000000000..7ae61a531 --- /dev/null +++ b/frontend/src/System/Logs/Files/LogFilesTableRow.js @@ -0,0 +1,50 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import Link from 'Components/Link/Link'; +import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector'; +import TableRow from 'Components/Table/TableRow'; +import TableRowCell from 'Components/Table/Cells/TableRowCell'; +import styles from './LogFilesTableRow.css'; + +class LogFilesTableRow extends Component { + + // + // Render + + render() { + const { + filename, + lastWriteTime, + downloadUrl + } = this.props; + + return ( + + {filename} + + + + + + Download + + + + ); + } + +} + +LogFilesTableRow.propTypes = { + filename: PropTypes.string.isRequired, + lastWriteTime: PropTypes.string.isRequired, + downloadUrl: PropTypes.string.isRequired +}; + +export default LogFilesTableRow; diff --git a/frontend/src/System/Logs/Logs.js b/frontend/src/System/Logs/Logs.js new file mode 100644 index 000000000..fa0be453e --- /dev/null +++ b/frontend/src/System/Logs/Logs.js @@ -0,0 +1,30 @@ +import React, { Component } from 'react'; +import { Route } from 'react-router-dom'; +import Switch from 'Components/Router/Switch'; +import LogFilesConnector from './Files/LogFilesConnector'; +import UpdateLogFilesConnector from './Updates/UpdateLogFilesConnector'; + +class Logs extends Component { + + // + // Render + + render() { + return ( + + + + + + ); + } +} + +export default Logs; diff --git a/frontend/src/System/Logs/LogsNavMenu.js b/frontend/src/System/Logs/LogsNavMenu.js new file mode 100644 index 000000000..b69630248 --- /dev/null +++ b/frontend/src/System/Logs/LogsNavMenu.js @@ -0,0 +1,71 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import Menu from 'Components/Menu/Menu'; +import MenuButton from 'Components/Menu/MenuButton'; +import MenuContent from 'Components/Menu/MenuContent'; +import MenuItem from 'Components/Menu/MenuItem'; + +class LogsNavMenu extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + isMenuOpen: false + }; + } + + // + // Listeners + + onMenuButtonPress = () => { + this.setState({ isMenuOpen: !this.state.isMenuOpen }); + } + + onMenuItemPress = () => { + this.setState({ isMenuOpen: false }); + } + + // + // Render + + render() { + const { + current + } = this.props; + + return ( + + + {current} + + + + Log Files + + + + Updater Log Files + + + + ); + } +} + +LogsNavMenu.propTypes = { + current: PropTypes.string.isRequired +}; + +export default LogsNavMenu; diff --git a/frontend/src/System/Logs/Updates/UpdateLogFilesConnector.js b/frontend/src/System/Logs/Updates/UpdateLogFilesConnector.js new file mode 100644 index 000000000..d49b82152 --- /dev/null +++ b/frontend/src/System/Logs/Updates/UpdateLogFilesConnector.js @@ -0,0 +1,93 @@ +import _ from 'lodash'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import combinePath from 'Utilities/String/combinePath'; +import createCommandsSelector from 'Store/Selectors/createCommandsSelector'; +import { executeCommand } from 'Store/Actions/commandActions'; +import { fetchUpdateLogFiles } from 'Store/Actions/systemActions'; +import * as commandNames from 'Commands/commandNames'; +import LogFiles from '../Files/LogFiles'; + +function createMapStateToProps() { + return createSelector( + (state) => state.system.updateLogFiles, + (state) => state.system.status.item, + createCommandsSelector(), + (updateLogFiles, status, commands) => { + const { + isFetching, + items + } = updateLogFiles; + + const deleteFilesExecuting = _.some(commands, { name: commandNames.DELETE_UPDATE_LOG_FILES }); + + const { + appData, + isWindows + } = status; + + return { + isFetching, + items, + deleteFilesExecuting, + currentLogView: 'Updater Log Files', + location: combinePath(isWindows, appData, ['UpdateLogs']) + }; + } + ); +} + +const mapDispatchToProps = { + fetchUpdateLogFiles, + executeCommand +}; + +class UpdateLogFilesConnector extends Component { + + // + // Lifecycle + + componentDidMount() { + this.props.fetchUpdateLogFiles(); + } + + componentDidUpdate(prevProps) { + if (prevProps.deleteFilesExecuting && !this.props.deleteFilesExecuting) { + this.props.fetchUpdateLogFiles(); + } + } + + // + // Listeners + + onRefreshPress = () => { + this.props.fetchUpdateLogFiles(); + } + + onDeleteFilesPress = () => { + this.props.executeCommand({ name: commandNames.DELETE_UPDATE_LOG_FILES }); + } + + // + // Render + + render() { + return ( + + ); + } +} + +UpdateLogFilesConnector.propTypes = { + deleteFilesExecuting: PropTypes.bool.isRequired, + fetchUpdateLogFiles: PropTypes.func.isRequired, + executeCommand: PropTypes.func.isRequired +}; + +export default connect(createMapStateToProps, mapDispatchToProps)(UpdateLogFilesConnector); diff --git a/frontend/src/System/Status/About/About.js b/frontend/src/System/Status/About/About.js new file mode 100644 index 000000000..51c0afde6 --- /dev/null +++ b/frontend/src/System/Status/About/About.js @@ -0,0 +1,71 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import titleCase from 'Utilities/String/titleCase'; +import FieldSet from 'Components/FieldSet'; +import DescriptionList from 'Components/DescriptionList/DescriptionList'; +import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem'; + +class About extends Component { + + // + // Render + + render() { + const { + version, + isMonoRuntime, + runtimeVersion, + appData, + startupPath, + mode + } = this.props; + + return ( +
+ + + + { + isMonoRuntime && + + } + + + + + + + +
+ ); + } + +} + +About.propTypes = { + version: PropTypes.string, + isMonoRuntime: PropTypes.bool, + runtimeVersion: PropTypes.string, + appData: PropTypes.string, + startupPath: PropTypes.string, + mode: PropTypes.string +}; + +export default About; diff --git a/frontend/src/System/Status/About/AboutConnector.js b/frontend/src/System/Status/About/AboutConnector.js new file mode 100644 index 000000000..8d5c2ce0f --- /dev/null +++ b/frontend/src/System/Status/About/AboutConnector.js @@ -0,0 +1,48 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import { fetchStatus } from 'Store/Actions/systemActions'; +import About from './About'; + +function createMapStateToProps() { + return createSelector( + (state) => state.system.status, + (status) => { + return { + ...status.item + }; + } + ); +} + +const mapDispatchToProps = { + fetchStatus +}; + +class AboutConnector extends Component { + + // + // Lifecycle + + componentDidMount() { + this.props.fetchStatus(); + } + + // + // Render + + render() { + return ( + + ); + } +} + +AboutConnector.propTypes = { + fetchStatus: PropTypes.func.isRequired +}; + +export default connect(createMapStateToProps, mapDispatchToProps)(AboutConnector); diff --git a/frontend/src/System/Status/DiskSpace/DiskSpace.css b/frontend/src/System/Status/DiskSpace/DiskSpace.css new file mode 100644 index 000000000..70ef6f884 --- /dev/null +++ b/frontend/src/System/Status/DiskSpace/DiskSpace.css @@ -0,0 +1,5 @@ +.space { + composes: cell from 'Components/Table/Cells/TableRowCell.css'; + + width: 150px; +} diff --git a/frontend/src/System/Status/DiskSpace/DiskSpace.js b/frontend/src/System/Status/DiskSpace/DiskSpace.js new file mode 100644 index 000000000..d2c10706e --- /dev/null +++ b/frontend/src/System/Status/DiskSpace/DiskSpace.js @@ -0,0 +1,122 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { kinds, sizes } from 'Helpers/Props'; +import formatBytes from 'Utilities/Number/formatBytes'; +import LoadingIndicator from 'Components/Loading/LoadingIndicator'; +import FieldSet from 'Components/FieldSet'; +import Table from 'Components/Table/Table'; +import TableBody from 'Components/Table/TableBody'; +import TableRow from 'Components/Table/TableRow'; +import TableRowCell from 'Components/Table/Cells/TableRowCell'; +import ProgressBar from 'Components/ProgressBar'; +import styles from './DiskSpace.css'; + +const columns = [ + { + name: 'path', + label: 'Location', + isVisible: true + }, + { + name: 'freeSpace', + label: 'Free Space', + isVisible: true + }, + { + name: 'totalSpace', + label: 'Total Space', + isVisible: true + }, + { + name: 'progress', + isVisible: true + } +]; + +class DiskSpace extends Component { + + // + // Render + + render() { + const { + isFetching, + items + } = this.props; + + return ( +
+ { + isFetching && + + } + + { + !isFetching && + + + { + items.map((item) => { + const { + freeSpace, + totalSpace + } = item; + + const diskUsage = (100 - freeSpace / totalSpace * 100); + let diskUsageKind = kinds.PRIMARY; + + if (diskUsage > 90) { + diskUsageKind = kinds.DANGER; + } else if (diskUsage > 80) { + diskUsageKind = kinds.WARNING; + } + + return ( + + + {item.path} + + { + item.label && + ` (${item.label})` + } + + + + {formatBytes(freeSpace)} + + + + {formatBytes(totalSpace)} + + + + + + + ); + }) + } + +
+ } +
+ ); + } + +} + +DiskSpace.propTypes = { + isFetching: PropTypes.bool.isRequired, + items: PropTypes.array.isRequired +}; + +export default DiskSpace; diff --git a/frontend/src/System/Status/DiskSpace/DiskSpaceConnector.js b/frontend/src/System/Status/DiskSpace/DiskSpaceConnector.js new file mode 100644 index 000000000..3049b2ead --- /dev/null +++ b/frontend/src/System/Status/DiskSpace/DiskSpaceConnector.js @@ -0,0 +1,54 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import { fetchDiskSpace } from 'Store/Actions/systemActions'; +import DiskSpace from './DiskSpace'; + +function createMapStateToProps() { + return createSelector( + (state) => state.system.diskSpace, + (diskSpace) => { + const { + isFetching, + items + } = diskSpace; + + return { + isFetching, + items + }; + } + ); +} + +const mapDispatchToProps = { + fetchDiskSpace +}; + +class DiskSpaceConnector extends Component { + + // + // Lifecycle + + componentDidMount() { + this.props.fetchDiskSpace(); + } + + // + // Render + + render() { + return ( + + ); + } +} + +DiskSpaceConnector.propTypes = { + fetchDiskSpace: PropTypes.func.isRequired +}; + +export default connect(createMapStateToProps, mapDispatchToProps)(DiskSpaceConnector); diff --git a/frontend/src/System/Status/Health/Health.css b/frontend/src/System/Status/Health/Health.css new file mode 100644 index 000000000..1aad8ee77 --- /dev/null +++ b/frontend/src/System/Status/Health/Health.css @@ -0,0 +1,21 @@ +.legend { + display: flex; + justify-content: space-between; +} + +.loading { + composes: loading from 'Components/Loading/LoadingIndicator.css'; + + margin-top: 2px; + margin-left: 10px; + text-align: left; +} + +.status { + width: 20px; +} + +.healthOk { + margin-bottom: 25px; +} + diff --git a/frontend/src/System/Status/Health/Health.js b/frontend/src/System/Status/Health/Health.js new file mode 100644 index 000000000..ff2165048 --- /dev/null +++ b/frontend/src/System/Status/Health/Health.js @@ -0,0 +1,170 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import titleCase from 'Utilities/String/titleCase'; +import { icons, kinds } from 'Helpers/Props'; +import Icon from 'Components/Icon'; +import Link from 'Components/Link/Link'; +import LoadingIndicator from 'Components/Loading/LoadingIndicator'; +import FieldSet from 'Components/FieldSet'; +import Table from 'Components/Table/Table'; +import TableBody from 'Components/Table/TableBody'; +import TableRow from 'Components/Table/TableRow'; +import TableRowCell from 'Components/Table/Cells/TableRowCell'; +import styles from './Health.css'; + +function getInternalLink(source) { + switch (source) { + case 'IndexerRssCheck': + case 'IndexerSearchCheck': + case 'IndexerStatusCheck': + return ( + + Settings + + ); + case 'DownloadClientCheck': + case 'ImportMechanismCheck': + return ( + + Settings + + ); + case 'RootFolderCheck': + return ( +
+ + Artist Editor + +
+ ); + case 'UpdateCheck': + return ( + + Updates + + ); + default: + return; + } +} + +const columns = [ + { + className: styles.status, + name: 'type', + isVisible: true + }, + { + name: 'message', + label: 'Message', + isVisible: true + }, + { + name: 'wikiLink', + label: 'Wiki', + isVisible: true + }, + { + name: 'internalLink', + isVisible: true + } +]; + +class Health extends Component { + + // + // Render + + render() { + const { + isFetching, + isPopulated, + items + } = this.props; + + const healthIssues = !!items.length; + + return ( +
+ Health + + { + isFetching && isPopulated && + + } +
+ } + > + { + isFetching && !isPopulated && + + } + + { + !healthIssues && +
+ No issues with your configuration +
+ } + + { + healthIssues && + + + { + items.map((item) => { + const internalLink = getInternalLink(item.source); + + return ( + + + + + + {item.message} + + + + Wiki + + + + + { + internalLink + } + + + ); + }) + } + +
+ } + + ); + } + +} + +Health.propTypes = { + isFetching: PropTypes.bool.isRequired, + isPopulated: PropTypes.bool.isRequired, + items: PropTypes.array.isRequired +}; + +export default Health; diff --git a/frontend/src/System/Status/Health/HealthConnector.js b/frontend/src/System/Status/Health/HealthConnector.js new file mode 100644 index 000000000..67f6a39dc --- /dev/null +++ b/frontend/src/System/Status/Health/HealthConnector.js @@ -0,0 +1,56 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import { fetchHealth } from 'Store/Actions/systemActions'; +import Health from './Health'; + +function createMapStateToProps() { + return createSelector( + (state) => state.system.health, + (health) => { + const { + isFetching, + isPopulated, + items + } = health; + + return { + isFetching, + isPopulated, + items + }; + } + ); +} + +const mapDispatchToProps = { + fetchHealth +}; + +class HealthConnector extends Component { + + // + // Lifecycle + + componentDidMount() { + this.props.fetchHealth(); + } + + // + // Render + + render() { + return ( + + ); + } +} + +HealthConnector.propTypes = { + fetchHealth: PropTypes.func.isRequired +}; + +export default connect(createMapStateToProps, mapDispatchToProps)(HealthConnector); diff --git a/frontend/src/System/Status/Health/HealthStatusConnector.js b/frontend/src/System/Status/Health/HealthStatusConnector.js new file mode 100644 index 000000000..181eae916 --- /dev/null +++ b/frontend/src/System/Status/Health/HealthStatusConnector.js @@ -0,0 +1,79 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import { fetchHealth } from 'Store/Actions/systemActions'; +import PageSidebarStatus from 'Components/Page/Sidebar/PageSidebarStatus'; + +function createMapStateToProps() { + return createSelector( + (state) => state.app, + (state) => state.system.health, + (app, health) => { + const count = health.items.length; + let errors = false; + let warnings = false; + + health.items.forEach((item) => { + if (item.type === 'error') { + errors = true; + } + + if (item.type === 'warning') { + warnings = true; + } + }); + + return { + isConnected: app.isConnected, + isReconnecting: app.isReconnecting, + isPopulated: health.isPopulated, + count, + errors, + warnings + }; + } + ); +} + +const mapDispatchToProps = { + fetchHealth +}; + +class HealthStatusConnector extends Component { + + // + // Lifecycle + + componentDidMount() { + if (!this.props.isPopulated) { + this.props.fetchHealth(); + } + } + + componentDidUpdate(prevProps) { + if (this.props.isConnected && prevProps.isReconnecting) { + this.props.fetchHealth(); + } + } + + // + // Render + + render() { + return ( + + ); + } +} + +HealthStatusConnector.propTypes = { + isConnected: PropTypes.bool.isRequired, + isReconnecting: PropTypes.bool.isRequired, + isPopulated: PropTypes.bool.isRequired, + fetchHealth: PropTypes.func.isRequired +}; + +export default connect(createMapStateToProps, mapDispatchToProps)(HealthStatusConnector); diff --git a/frontend/src/System/Status/MoreInfo/MoreInfo.js b/frontend/src/System/Status/MoreInfo/MoreInfo.js new file mode 100644 index 000000000..e573f1bfa --- /dev/null +++ b/frontend/src/System/Status/MoreInfo/MoreInfo.js @@ -0,0 +1,69 @@ +import React, { Component } from 'react'; +import Link from 'Components/Link/Link'; +import FieldSet from 'Components/FieldSet'; +import DescriptionList from 'Components/DescriptionList/DescriptionList'; +import DescriptionListItemTitle from 'Components/DescriptionList/DescriptionListItemTitle'; +import DescriptionListItemDescription from 'Components/DescriptionList/DescriptionListItemDescription'; + +class MoreInfo extends Component { + + // + // Render + + render() { + return ( +
+ + Home page + + lidarr.audio + + + Wiki + + wiki.lidarr.audio + + + Reddit + + Lidarr + + + Discord + + #lidarr on Discord + + + Donations + + Donate to Lidarr + + + Donations (Sonarr) + + Donate to Sonarr + + + Source + + github.com/Lidarr/Lidarr + + + Feature Requests + + github.com/Lidarr/Lidarr/issues + + + +
+ ); + } +} + +MoreInfo.propTypes = { + +}; + +export default MoreInfo; diff --git a/frontend/src/System/Status/Status.js b/frontend/src/System/Status/Status.js new file mode 100644 index 000000000..f0b157515 --- /dev/null +++ b/frontend/src/System/Status/Status.js @@ -0,0 +1,29 @@ +import React, { Component } from 'react'; +import PageContent from 'Components/Page/PageContent'; +import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector'; +import HealthConnector from './Health/HealthConnector'; +import DiskSpaceConnector from './DiskSpace/DiskSpaceConnector'; +import AboutConnector from './About/AboutConnector'; +import MoreInfo from './MoreInfo/MoreInfo'; + +class Status extends Component { + + // + // Render + + render() { + return ( + + + + + + + + + ); + } + +} + +export default Status; diff --git a/frontend/src/System/Tasks/TaskRow.css b/frontend/src/System/Tasks/TaskRow.css new file mode 100644 index 000000000..dc83cfd69 --- /dev/null +++ b/frontend/src/System/Tasks/TaskRow.css @@ -0,0 +1,18 @@ +.interval { + composes: cell from 'Components/Table/Cells/TableRowCell.css'; + + width: 150px; +} + +.lastExecution, +.nextExecution { + composes: cell from 'Components/Table/Cells/TableRowCell.css'; + + width: 180px; +} + +.actions { + composes: cell from 'Components/Table/Cells/TableRowCell.css'; + + width: 20px; +} diff --git a/frontend/src/System/Tasks/TaskRow.js b/frontend/src/System/Tasks/TaskRow.js new file mode 100644 index 000000000..f118842a7 --- /dev/null +++ b/frontend/src/System/Tasks/TaskRow.js @@ -0,0 +1,94 @@ +import moment from 'moment'; +import PropTypes from 'prop-types'; +import React from 'react'; +import formatDate from 'Utilities/Date/formatDate'; +import formatDateTime from 'Utilities/Date/formatDateTime'; +import { icons } from 'Helpers/Props'; +import SpinnerIconButton from 'Components/Link/SpinnerIconButton'; +import TableRow from 'Components/Table/TableRow'; +import TableRowCell from 'Components/Table/Cells/TableRowCell'; +import styles from './TaskRow.css'; + +function TaskRow(props) { + const { + name, + interval, + lastExecution, + nextExecution, + isExecuting, + showRelativeDates, + shortDateFormat, + longDateFormat, + timeFormat, + onExecutePress + } = props; + + const disabled = interval === 0; + const executeNow = !disabled && moment().isAfter(nextExecution); + const hasNextExecutionTime = !disabled && !executeNow; + const duration = moment.duration(interval, 'minutes').humanize().replace(/an?(?=\s)/, '1'); + + return ( + + {name} + + {disabled ? 'disabled' : duration} + + + + {showRelativeDates ? moment(lastExecution).fromNow() : formatDate(lastExecution, shortDateFormat)} + + + { + disabled && + - + } + + { + executeNow && + now + } + + { + hasNextExecutionTime && + + {showRelativeDates ? moment(nextExecution).fromNow() : formatDate(nextExecution, shortDateFormat)} + + } + + + + + + ); +} + +TaskRow.propTypes = { + name: PropTypes.string.isRequired, + interval: PropTypes.number.isRequired, + lastExecution: PropTypes.string.isRequired, + nextExecution: PropTypes.string.isRequired, + isExecuting: PropTypes.bool.isRequired, + showRelativeDates: PropTypes.bool.isRequired, + shortDateFormat: PropTypes.string.isRequired, + longDateFormat: PropTypes.string.isRequired, + timeFormat: PropTypes.string.isRequired, + onExecutePress: PropTypes.func.isRequired +}; + +export default TaskRow; diff --git a/frontend/src/System/Tasks/TaskRowConnector.js b/frontend/src/System/Tasks/TaskRowConnector.js new file mode 100644 index 000000000..035364034 --- /dev/null +++ b/frontend/src/System/Tasks/TaskRowConnector.js @@ -0,0 +1,91 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import { findCommand } from 'Utilities/Command'; +import { executeCommand } from 'Store/Actions/commandActions'; +import { fetchTask } from 'Store/Actions/systemActions'; +import createCommandsSelector from 'Store/Selectors/createCommandsSelector'; +import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector'; +import TaskRow from './TaskRow'; + +function createMapStateToProps() { + return createSelector( + (state, { taskName }) => taskName, + createCommandsSelector(), + createUISettingsSelector(), + (taskName, commands, uiSettings) => { + const isExecuting = !!findCommand(commands, { name: taskName }); + + return { + isExecuting, + showRelativeDates: uiSettings.showRelativeDates, + shortDateFormat: uiSettings.shortDateFormat, + longDateFormat: uiSettings.longDateFormat, + timeFormat: uiSettings.timeFormat + }; + } + ); +} + +function createMapDispatchToProps(dispatch, props) { + const taskName = props.taskName; + + return { + dispatchFetchTask() { + dispatch(fetchTask({ + id: props.id + })); + }, + + onExecutePress() { + dispatch(executeCommand({ + name: taskName + })); + } + }; +} + +class TaskRowConnector extends Component { + + // + // Lifecycle + + componentDidUpdate(prevProps) { + const { + isExecuting, + dispatchFetchTask + } = this.props; + + if (!isExecuting && prevProps.isExecuting) { + // Give the host a moment to update after the command completes + setTimeout(() => { + dispatchFetchTask(); + }, 1000); + } + } + + // + // Render + + render() { + const { + dispatchFetchTask, + ...otherProps + } = this.props; + + return ( + + ); + } +} + +TaskRowConnector.propTypes = { + id: PropTypes.number.isRequired, + isExecuting: PropTypes.bool.isRequired, + dispatchFetchTask: PropTypes.func.isRequired +}; + +export default connect(createMapStateToProps, createMapDispatchToProps)(TaskRowConnector); diff --git a/frontend/src/System/Tasks/Tasks.js b/frontend/src/System/Tasks/Tasks.js new file mode 100644 index 000000000..ae2d75dbb --- /dev/null +++ b/frontend/src/System/Tasks/Tasks.js @@ -0,0 +1,89 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import PageContent from 'Components/Page/PageContent'; +import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector'; +import LoadingIndicator from 'Components/Loading/LoadingIndicator'; +import Table from 'Components/Table/Table'; +import TableBody from 'Components/Table/TableBody'; +import TaskRowConnector from './TaskRowConnector'; + +const columns = [ + { + name: 'name', + label: 'Name', + isVisible: true + }, + { + name: 'interval', + label: 'Interval', + isVisible: true + }, + { + name: 'lastExecution', + label: 'Last Execution', + isVisible: true + }, + { + name: 'nextExecution', + label: 'Next Execution', + isVisible: true + }, + { + name: 'actions', + isVisible: true + } +]; + +class Tasks extends Component { + + // + // Render + + render() { + const { + isFetching, + isPopulated, + items + } = this.props; + + return ( + + + { + isFetching && !isPopulated && + + } + + { + isPopulated && + + + { + items.map((item) => { + return ( + + ); + }) + } + +
+ } +
+
+ ); + } + +} + +Tasks.propTypes = { + isFetching: PropTypes.bool.isRequired, + isPopulated: PropTypes.bool.isRequired, + items: PropTypes.array.isRequired +}; + +export default Tasks; diff --git a/frontend/src/System/Tasks/TasksConnector.js b/frontend/src/System/Tasks/TasksConnector.js new file mode 100644 index 000000000..492040674 --- /dev/null +++ b/frontend/src/System/Tasks/TasksConnector.js @@ -0,0 +1,46 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import { fetchTasks } from 'Store/Actions/systemActions'; +import Tasks from './Tasks'; + +function createMapStateToProps() { + return createSelector( + (state) => state.system.tasks, + (tasks) => { + return tasks; + } + ); +} + +const mapDispatchToProps = { + fetchTasks +}; + +class TasksConnector extends Component { + + // + // Lifecycle + + componentDidMount() { + this.props.fetchTasks(); + } + + // + // Render + + render() { + return ( + + ); + } +} + +TasksConnector.propTypes = { + fetchTasks: PropTypes.func.isRequired +}; + +export default connect(createMapStateToProps, mapDispatchToProps)(TasksConnector); diff --git a/frontend/src/System/Updates/UpdateChanges.css b/frontend/src/System/Updates/UpdateChanges.css new file mode 100644 index 000000000..d21897373 --- /dev/null +++ b/frontend/src/System/Updates/UpdateChanges.css @@ -0,0 +1,4 @@ +.title { + margin-top: 10px; + font-size: 16px; +} diff --git a/frontend/src/System/Updates/UpdateChanges.js b/frontend/src/System/Updates/UpdateChanges.js new file mode 100644 index 000000000..63c7e0d85 --- /dev/null +++ b/frontend/src/System/Updates/UpdateChanges.js @@ -0,0 +1,45 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import styles from './UpdateChanges.css'; + +class UpdateChanges extends Component { + + // + // Render + + render() { + const { + title, + changes + } = this.props; + + if (changes.length === 0) { + return null; + } + + return ( +
+
{title}
+
    + { + changes.map((change, index) => { + return ( +
  • + {change} +
  • + ); + }) + } +
+
+ ); + } + +} + +UpdateChanges.propTypes = { + title: PropTypes.string.isRequired, + changes: PropTypes.arrayOf(PropTypes.string) +}; + +export default UpdateChanges; diff --git a/frontend/src/System/Updates/Updates.css b/frontend/src/System/Updates/Updates.css new file mode 100644 index 000000000..307028426 --- /dev/null +++ b/frontend/src/System/Updates/Updates.css @@ -0,0 +1,46 @@ +.upToDate { + display: flex; + margin-bottom: 20px; +} + +.upToDateIcon { + color: #37bc9b; + font-size: 30px; +} + +.upToDateMessage { + padding-left: 5px; + font-size: 18px; + line-height: 30px; +} + +.update { + margin-top: 20px; +} + +.info { + display: flex; + align-items: center; + margin-bottom: 10px; + padding-bottom: 5px; + border-bottom: 1px solid #e5e5e5; +} + +.version { + font-size: 21px; +} + +.space { + padding: 0 5px; +} + +.date { + font-size: 16px; +} + +.branch { + composes: label from 'Components/Label.css'; + + margin-left: 10px; + font-size: 14px; +} diff --git a/frontend/src/System/Updates/Updates.js b/frontend/src/System/Updates/Updates.js new file mode 100644 index 000000000..08a12e6d1 --- /dev/null +++ b/frontend/src/System/Updates/Updates.js @@ -0,0 +1,149 @@ +import _ from 'lodash'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { kinds } from 'Helpers/Props'; +import formatDate from 'Utilities/Date/formatDate'; +import LoadingIndicator from 'Components/Loading/LoadingIndicator'; +import SpinnerButton from 'Components/Link/SpinnerButton'; +import Icon from 'Components/Icon'; +import Label from 'Components/Label'; +import PageContent from 'Components/Page/PageContent'; +import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector'; +import UpdateChanges from './UpdateChanges'; +import styles from './Updates.css'; + +class Updates extends Component { + + // + // Render + + render() { + const { + isPopulated, + error, + items, + isInstallingUpdate, + shortDateFormat, + onInstallLatestPress + } = this.props; + + const hasUpdates = isPopulated && !error && items.length > 0; + const noUpdates = isPopulated && !error && !items.length; + const hasUpdateToInstall = hasUpdates && _.some(items, { installable: true, latest: true }); + const noUpdateToInstall = hasUpdates && !hasUpdateToInstall; + + return ( + + + { + !isPopulated && + + } + + { + noUpdates && +
No updates are available
+ } + + { + hasUpdateToInstall && + + Install Latest + + } + + { + noUpdateToInstall && +
+ +
+ The latest version of Lidarr is already installed +
+
+ } + + { + hasUpdates && +
+ { + items.map((update) => { + const hasChanges = !!update.changes; + + return ( +
+
+
{update.version}
+
+
{formatDate(update.releaseDate, shortDateFormat)}
+ + { + update.branch !== 'master' && + + } +
+ + { + !hasChanges && +
Maintenance release
+ } + + { + hasChanges && +
+ + + +
+ } +
+ ); + }) + } +
+ } + + { + !!error && +
+ Failed to fetch updates +
+ } +
+
+ ); + } + +} + +Updates.propTypes = { + isPopulated: PropTypes.bool.isRequired, + error: PropTypes.object, + items: PropTypes.array.isRequired, + isInstallingUpdate: PropTypes.bool.isRequired, + shortDateFormat: PropTypes.string.isRequired, + onInstallLatestPress: PropTypes.func.isRequired +}; + +export default Updates; diff --git a/frontend/src/System/Updates/UpdatesConnector.js b/frontend/src/System/Updates/UpdatesConnector.js new file mode 100644 index 000000000..0ac6cf239 --- /dev/null +++ b/frontend/src/System/Updates/UpdatesConnector.js @@ -0,0 +1,74 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import { fetchUpdates } from 'Store/Actions/systemActions'; +import { executeCommand } from 'Store/Actions/commandActions'; +import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector'; +import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector'; +import * as commandNames from 'Commands/commandNames'; +import Updates from './Updates'; + +function createMapStateToProps() { + return createSelector( + (state) => state.system.updates, + createUISettingsSelector(), + createCommandExecutingSelector(commandNames.APPLICATION_UPDATE), + (updates, uiSettings, isInstallingUpdate) => { + const { + isPopulated, + error, + items + } = updates; + + return { + isPopulated, + error, + items, + isInstallingUpdate, + shortDateFormat: uiSettings.shortDateFormat + }; + } + ); +} + +const mapDispatchToProps = { + fetchUpdates, + executeCommand +}; + +class UpdatesConnector extends Component { + + // + // Lifecycle + + componentDidMount() { + this.props.fetchUpdates(); + } + + // + // Listeners + + onInstallLatestPress = () => { + this.props.executeCommand({ name: commandNames.APPLICATION_UPDATE }); + } + + // + // Render + + render() { + return ( + + ); + } +} + +UpdatesConnector.propTypes = { + fetchUpdates: PropTypes.func.isRequired, + executeCommand: PropTypes.func.isRequired +}; + +export default connect(createMapStateToProps, mapDispatchToProps)(UpdatesConnector); diff --git a/frontend/src/TrackFile/Editor/TrackFileEditorModal.js b/frontend/src/TrackFile/Editor/TrackFileEditorModal.js new file mode 100644 index 000000000..7f52aca05 --- /dev/null +++ b/frontend/src/TrackFile/Editor/TrackFileEditorModal.js @@ -0,0 +1,34 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import Modal from 'Components/Modal/Modal'; +import TrackFileEditorModalContentConnector from './TrackFileEditorModalContentConnector'; + +function TrackFileEditorModal(props) { + const { + isOpen, + onModalClose, + ...otherProps + } = props; + + return ( + + { + isOpen && + + } + + ); +} + +TrackFileEditorModal.propTypes = { + isOpen: PropTypes.bool.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default TrackFileEditorModal; diff --git a/frontend/src/TrackFile/Editor/TrackFileEditorModalContent.css b/frontend/src/TrackFile/Editor/TrackFileEditorModalContent.css new file mode 100644 index 000000000..49e946826 --- /dev/null +++ b/frontend/src/TrackFile/Editor/TrackFileEditorModalContent.css @@ -0,0 +1,8 @@ +.actions { + display: flex; + margin-right: auto; +} + +.selectInput { + margin-left: 10px; +} diff --git a/frontend/src/TrackFile/Editor/TrackFileEditorModalContent.js b/frontend/src/TrackFile/Editor/TrackFileEditorModalContent.js new file mode 100644 index 000000000..a1365dc99 --- /dev/null +++ b/frontend/src/TrackFile/Editor/TrackFileEditorModalContent.js @@ -0,0 +1,268 @@ +import _ from 'lodash'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import getSelectedIds from 'Utilities/Table/getSelectedIds'; +import selectAll from 'Utilities/Table/selectAll'; +import toggleSelected from 'Utilities/Table/toggleSelected'; +import { kinds } from 'Helpers/Props'; +import ConfirmModal from 'Components/Modal/ConfirmModal'; +import Button from 'Components/Link/Button'; +import SpinnerButton from 'Components/Link/SpinnerButton'; +import SelectInput from 'Components/Form/SelectInput'; +import ModalContent from 'Components/Modal/ModalContent'; +import ModalHeader from 'Components/Modal/ModalHeader'; +import ModalBody from 'Components/Modal/ModalBody'; +import ModalFooter from 'Components/Modal/ModalFooter'; +import Table from 'Components/Table/Table'; +import TableBody from 'Components/Table/TableBody'; +import TrackFileEditorRow from './TrackFileEditorRow'; +import styles from './TrackFileEditorModalContent.css'; + +const columns = [ + { + name: 'trackNumber', + label: 'Track', + isVisible: true + }, + { + name: 'relativePath', + label: 'Relative Path', + isVisible: true + }, + { + name: 'language', + label: 'Language', + isVisible: true + }, + { + name: 'quality', + label: 'Quality', + isVisible: true + } +]; + +class TrackFileEditorModalContent extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + allSelected: false, + allUnselected: false, + lastToggled: null, + selectedState: {}, + isConfirmDeleteModalOpen: false + }; + } + + componentDidUpdate(prevProps) { + if (prevProps.items !== this.props.items) { + this.onSelectAllChange({ value: false }); + } + } + + // + // Control + + getSelectedIds = () => { + const selectedIds = getSelectedIds(this.state.selectedState); + + return _.uniq(_.map(selectedIds, (id) => { + return _.find(this.props.items, { id }).trackFileId; + })); + } + + // + // Listeners + + onSelectAllChange = ({ value }) => { + this.setState(selectAll(this.state.selectedState, value)); + } + + onSelectedChange = ({ id, value, shiftKey = false }) => { + this.setState((state) => { + return toggleSelected(state, this.props.items, id, value, shiftKey); + }); + } + + onDeletePress = () => { + this.setState({ isConfirmDeleteModalOpen: true }); + } + + onConfirmDelete = () => { + this.setState({ isConfirmDeleteModalOpen: false }); + this.props.onDeletePress(this.getSelectedIds()); + } + + onConfirmDeleteModalClose = () => { + this.setState({ isConfirmDeleteModalOpen: false }); + } + + onLanguageChange = ({ value }) => { + const selectedIds = this.getSelectedIds(); + + if (!selectedIds.length) { + return; + } + + this.props.onLanguageChange(selectedIds, parseInt(value)); + } + + onQualityChange = ({ value }) => { + const selectedIds = this.getSelectedIds(); + + if (!selectedIds.length) { + return; + } + + this.props.onQualityChange(selectedIds, parseInt(value)); + } + + // + // Render + + render() { + const { + isDeleting, + items, + languages, + qualities, + onModalClose + } = this.props; + + const { + allSelected, + allUnselected, + selectedState, + isConfirmDeleteModalOpen + } = this.state; + + const languageOptions = _.reduceRight(languages, (acc, language) => { + acc.push({ + key: language.id, + value: language.name + }); + + return acc; + }, [{ key: 'selectLanguage', value: 'Select Language', disabled: true }]); + + const qualityOptions = _.reduceRight(qualities, (acc, quality) => { + acc.push({ + key: quality.id, + value: quality.name + }); + + return acc; + }, [{ key: 'selectQuality', value: 'Select Quality', disabled: true }]); + + const hasSelectedFiles = this.getSelectedIds().length > 0; + + return ( + + + Manage Tracks + + + + { + !items.length && +
+ No track files to manage. +
+ } + + { + !!items.length && + + + { + items.map((item) => { + return ( + + ); + }) + } + +
+ } +
+ + +
+ + Delete + + +
+ +
+ +
+ +
+
+ + +
+ + +
+ ); + } +} + +TrackFileEditorModalContent.propTypes = { + isDeleting: PropTypes.bool.isRequired, + items: PropTypes.arrayOf(PropTypes.object).isRequired, + languages: PropTypes.arrayOf(PropTypes.object).isRequired, + qualities: PropTypes.arrayOf(PropTypes.object).isRequired, + onDeletePress: PropTypes.func.isRequired, + onLanguageChange: PropTypes.func.isRequired, + onQualityChange: PropTypes.func.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default TrackFileEditorModalContent; diff --git a/frontend/src/TrackFile/Editor/TrackFileEditorModalContentConnector.js b/frontend/src/TrackFile/Editor/TrackFileEditorModalContentConnector.js new file mode 100644 index 000000000..1fd99480c --- /dev/null +++ b/frontend/src/TrackFile/Editor/TrackFileEditorModalContentConnector.js @@ -0,0 +1,183 @@ +/* eslint max-params: 0 */ +import _ from 'lodash'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import createArtistSelector from 'Store/Selectors/createArtistSelector'; +import { deleteTrackFiles, updateTrackFiles } from 'Store/Actions/trackFileActions'; +import { fetchTracks, clearTracks } from 'Store/Actions/trackActions'; +import { fetchLanguageProfileSchema, fetchQualityProfileSchema } from 'Store/Actions/settingsActions'; +import TrackFileEditorModalContent from './TrackFileEditorModalContent'; + +function createMapStateToProps() { + return createSelector( + (state, { albumId }) => albumId, + (state) => state.tracks, + (state) => state.trackFiles, + (state) => state.settings.languageProfiles.schema, + (state) => state.settings.qualityProfiles.schema, + createArtistSelector(), + ( + albumId, + tracks, + trackFiles, + languageProfilesSchema, + qualityProfileSchema, + artist + ) => { + const filtered = _.filter(tracks.items, (track) => { + if (albumId >= 0 && track.albumId !== albumId) { + return false; + } + + if (!track.trackFileId) { + return false; + } + + return _.some(trackFiles.items, { id: track.trackFileId }); + }); + + const sorted = _.orderBy(filtered, ['albumId', 'trackNumber'], ['desc', 'asc']); + + const items = _.map(sorted, (track) => { + const trackFile = _.find(trackFiles.items, { id: track.trackFileId }); + + return { + relativePath: trackFile.relativePath, + language: trackFile.language, + quality: trackFile.quality, + ...track + }; + }); + + const languages = _.map(languageProfilesSchema.languages, 'language'); + const qualities = _.map(qualityProfileSchema.items, 'quality'); + + return { + items, + artistType: artist.artistType, + isDeleting: trackFiles.isDeleting, + isSaving: trackFiles.isSaving, + languages, + qualities + }; + } + ); +} + +function createMapDispatchToProps(dispatch, props) { + return { + dispatchClearTracks() { + dispatch(clearTracks()); + }, + + dispatchFetchTracks(updateProps) { + dispatch(fetchTracks(updateProps)); + }, + + dispatchFetchLanguageProfileSchema(name, path) { + dispatch(fetchLanguageProfileSchema()); + }, + + dispatchFetchQualityProfileSchema(name, path) { + dispatch(fetchQualityProfileSchema()); + }, + + dispatchUpdateTrackFiles(updateProps) { + dispatch(updateTrackFiles(updateProps)); + }, + + onDeletePress(trackFileIds) { + dispatch(deleteTrackFiles({ trackFileIds })); + }, + + onQualityChange(trackFileIds, qualityId) { + const quality = { + quality: _.find(this.props.qualities, { id: qualityId }), + revision: { + version: 1, + real: 0 + } + }; + + dispatch(updateTrackFiles({ trackFileIds, quality })); + } + }; +} + +class TrackFileEditorModalContentConnector extends Component { + + // + // Lifecycle + + componentDidMount() { + const artistId = this.props.artistId; + + this.props.dispatchFetchTracks({ artistId }); + + this.props.dispatchFetchLanguageProfileSchema(); + this.props.dispatchFetchQualityProfileSchema(); + } + + componentWillUnmount() { + this.props.dispatchClearTracks(); + } + + // + // Render + + // + // Listeners + + onLanguageChange = (trackFileIds, languageId) => { + const language = _.find(this.props.languages, { id: languageId }); + + this.props.dispatchUpdateTrackFiles({ trackFileIds, language }); + } + + onQualityChange = (trackFileIds, qualityId) => { + const quality = { + quality: _.find(this.props.qualities, { id: qualityId }), + revision: { + version: 1, + real: 0 + } + }; + + this.props.dispatchUpdateTrackFiles({ trackFileIds, quality }); + } + + render() { + const { + dispatchFetchLanguageProfileSchema, + dispatchFetchQualityProfileSchema, + dispatchUpdateTrackFiles, + dispatchFetchTracks, + dispatchClearTracks, + ...otherProps + } = this.props; + + return ( + + ); + } +} + +TrackFileEditorModalContentConnector.propTypes = { + artistId: PropTypes.number.isRequired, + albumId: PropTypes.number, + languages: PropTypes.arrayOf(PropTypes.object).isRequired, + qualities: PropTypes.arrayOf(PropTypes.object).isRequired, + dispatchFetchTracks: PropTypes.func.isRequired, + dispatchClearTracks: PropTypes.func.isRequired, + dispatchFetchLanguageProfileSchema: PropTypes.func.isRequired, + dispatchFetchQualityProfileSchema: PropTypes.func.isRequired, + dispatchUpdateTrackFiles: PropTypes.func.isRequired +}; + +export default connect(createMapStateToProps, createMapDispatchToProps)(TrackFileEditorModalContentConnector); diff --git a/frontend/src/TrackFile/Editor/TrackFileEditorRow.css b/frontend/src/TrackFile/Editor/TrackFileEditorRow.css new file mode 100644 index 000000000..f86e1de6b --- /dev/null +++ b/frontend/src/TrackFile/Editor/TrackFileEditorRow.css @@ -0,0 +1,3 @@ +.absoluteEpisodeNumber { + margin-left: 5px; +} diff --git a/frontend/src/TrackFile/Editor/TrackFileEditorRow.js b/frontend/src/TrackFile/Editor/TrackFileEditorRow.js new file mode 100644 index 000000000..2e1d3f2cd --- /dev/null +++ b/frontend/src/TrackFile/Editor/TrackFileEditorRow.js @@ -0,0 +1,64 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import padNumber from 'Utilities/Number/padNumber'; +import Label from 'Components/Label'; +import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector'; +import TableRow from 'Components/Table/TableRow'; +import TableRowCell from 'Components/Table/Cells/TableRowCell'; +import TableSelectCell from 'Components/Table/Cells/TableSelectCell'; +import EpisodeQuality from 'Album/EpisodeQuality'; +import styles from './TrackFileEditorRow'; + +function TrackFileEditorRow(props) { + const { + id, + trackNumber, + relativePath, + language, + quality, + isSelected, + onSelectedChange + } = props; + + return ( + + + + + {padNumber(trackNumber, 2)} + + + + {relativePath} + + + + + + + + + + + ); +} + +TrackFileEditorRow.propTypes = { + id: PropTypes.number.isRequired, + trackNumber: PropTypes.number.isRequired, + relativePath: PropTypes.string.isRequired, + language: PropTypes.object.isRequired, + quality: PropTypes.object.isRequired, + isSelected: PropTypes.bool, + onSelectedChange: PropTypes.func.isRequired +}; + +export default TrackFileEditorRow; diff --git a/frontend/src/TrackFile/MediaInfo.js b/frontend/src/TrackFile/MediaInfo.js new file mode 100644 index 000000000..75b264d58 --- /dev/null +++ b/frontend/src/TrackFile/MediaInfo.js @@ -0,0 +1,52 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import * as mediaInfoTypes from './mediaInfoTypes'; + +function MediaInfo(props) { + const { + type, + audioChannels, + audioCodec, + videoCodec + } = props; + + if (type === mediaInfoTypes.AUDIO) { + return ( + + { + !!audioCodec && + audioCodec + } + + { + !!audioCodec && !!audioChannels && + ' - ' + } + + { + !!audioChannels && + audioChannels.toFixed(1) + } + + ); + } + + if (type === mediaInfoTypes.VIDEO) { + return ( + + {videoCodec} + + ); + } + + return null; +} + +MediaInfo.propTypes = { + type: PropTypes.string.isRequired, + audioChannels: PropTypes.number, + audioCodec: PropTypes.string, + videoCodec: PropTypes.string +}; + +export default MediaInfo; diff --git a/frontend/src/TrackFile/MediaInfoConnector.js b/frontend/src/TrackFile/MediaInfoConnector.js new file mode 100644 index 000000000..5f3a1386b --- /dev/null +++ b/frontend/src/TrackFile/MediaInfoConnector.js @@ -0,0 +1,21 @@ +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import createTrackFileSelector from 'Store/Selectors/createTrackFileSelector'; +import MediaInfo from './MediaInfo'; + +function createMapStateToProps() { + return createSelector( + createTrackFileSelector(), + (trackFile) => { + if (trackFile) { + return { + ...trackFile.mediaInfo + }; + } + + return {}; + } + ); +} + +export default connect(createMapStateToProps)(MediaInfo); diff --git a/frontend/src/TrackFile/TrackFileLanguageConnector.js b/frontend/src/TrackFile/TrackFileLanguageConnector.js new file mode 100644 index 000000000..9a1a3b1bf --- /dev/null +++ b/frontend/src/TrackFile/TrackFileLanguageConnector.js @@ -0,0 +1,17 @@ +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import createTrackFileSelector from 'Store/Selectors/createTrackFileSelector'; +import EpisodeLanguage from 'Album/EpisodeLanguage'; + +function createMapStateToProps() { + return createSelector( + createTrackFileSelector(), + (trackFile) => { + return { + language: trackFile ? trackFile.language : undefined + }; + } + ); +} + +export default connect(createMapStateToProps)(EpisodeLanguage); diff --git a/frontend/src/TrackFile/mediaInfoTypes.js b/frontend/src/TrackFile/mediaInfoTypes.js new file mode 100644 index 000000000..5e5a78e64 --- /dev/null +++ b/frontend/src/TrackFile/mediaInfoTypes.js @@ -0,0 +1,2 @@ +export const AUDIO = 'audio'; +export const VIDEO = 'video'; diff --git a/frontend/src/Utilities/Album/updateAlbums.js b/frontend/src/Utilities/Album/updateAlbums.js new file mode 100644 index 000000000..16d800de9 --- /dev/null +++ b/frontend/src/Utilities/Album/updateAlbums.js @@ -0,0 +1,21 @@ +import _ from 'lodash'; +import { update } from 'Store/Actions/baseActions'; + +function updateAlbums(dispatch, section, episodes, albumIds, options) { + const data = _.reduce(episodes, (result, item) => { + if (albumIds.indexOf(item.id) > -1) { + result.push({ + ...item, + ...options + }); + } else { + result.push(item); + } + + return result; + }, []); + + dispatch(update({ section, data })); +} + +export default updateAlbums; diff --git a/frontend/src/Utilities/Array/sortByName.js b/frontend/src/Utilities/Array/sortByName.js new file mode 100644 index 000000000..1956d3bac --- /dev/null +++ b/frontend/src/Utilities/Array/sortByName.js @@ -0,0 +1,5 @@ +function sortByName(a, b) { + return a.name.localeCompare(b.name); +} + +export default sortByName; diff --git a/frontend/src/Utilities/Command/findCommand.js b/frontend/src/Utilities/Command/findCommand.js new file mode 100644 index 000000000..cf7d5444a --- /dev/null +++ b/frontend/src/Utilities/Command/findCommand.js @@ -0,0 +1,10 @@ +import _ from 'lodash'; +import isSameCommand from './isSameCommand'; + +function findCommand(commands, options) { + return _.findLast(commands, (command) => { + return isSameCommand(command.body, options); + }); +} + +export default findCommand; diff --git a/frontend/src/Utilities/Command/index.js b/frontend/src/Utilities/Command/index.js new file mode 100644 index 000000000..66043bf03 --- /dev/null +++ b/frontend/src/Utilities/Command/index.js @@ -0,0 +1,5 @@ +export { default as findCommand } from './findCommand'; +export { default as isCommandComplete } from './isCommandComplete'; +export { default as isCommandExecuting } from './isCommandExecuting'; +export { default as isCommandFailed } from './isCommandFailed'; +export { default as isSameCommand } from './isSameCommand'; diff --git a/frontend/src/Utilities/Command/isCommandComplete.js b/frontend/src/Utilities/Command/isCommandComplete.js new file mode 100644 index 000000000..e64737188 --- /dev/null +++ b/frontend/src/Utilities/Command/isCommandComplete.js @@ -0,0 +1,9 @@ +function isCommandComplete(command) { + if (!command) { + return false; + } + + return command.state === 'complete'; +} + +export default isCommandComplete; diff --git a/frontend/src/Utilities/Command/isCommandExecuting.js b/frontend/src/Utilities/Command/isCommandExecuting.js new file mode 100644 index 000000000..4e2e6d8c4 --- /dev/null +++ b/frontend/src/Utilities/Command/isCommandExecuting.js @@ -0,0 +1,9 @@ +function isCommandExecuting(command) { + if (!command) { + return false; + } + + return command.state === 'queued' || command.state === 'started'; +} + +export default isCommandExecuting; diff --git a/frontend/src/Utilities/Command/isCommandFailed.js b/frontend/src/Utilities/Command/isCommandFailed.js new file mode 100644 index 000000000..f48d790e3 --- /dev/null +++ b/frontend/src/Utilities/Command/isCommandFailed.js @@ -0,0 +1,12 @@ +function isCommandFailed(command) { + if (!command) { + return false; + } + + return command.state === 'failed' || + command.state === 'aborted' || + command.state === 'cancelled' || + command.state === 'orphaned'; +} + +export default isCommandFailed; diff --git a/frontend/src/Utilities/Command/isSameCommand.js b/frontend/src/Utilities/Command/isSameCommand.js new file mode 100644 index 000000000..d0acb24b5 --- /dev/null +++ b/frontend/src/Utilities/Command/isSameCommand.js @@ -0,0 +1,24 @@ +import _ from 'lodash'; + +function isSameCommand(commandA, commandB) { + if (commandA.name.toLocaleLowerCase() !== commandB.name.toLocaleLowerCase()) { + return false; + } + + for (const key in commandB) { + if (key !== 'name') { + const value = commandB[key]; + if (Array.isArray(value)) { + if (_.difference(value, commandA[key]).length > 0) { + return false; + } + } else if (value !== commandA[key]) { + return false; + } + } + } + + return true; +} + +export default isSameCommand; diff --git a/frontend/src/Utilities/Constants/keyCodes.js b/frontend/src/Utilities/Constants/keyCodes.js new file mode 100644 index 000000000..9285b10fe --- /dev/null +++ b/frontend/src/Utilities/Constants/keyCodes.js @@ -0,0 +1,7 @@ +export const TAB = 9; +export const ENTER = 13; +export const SHIFT = 16; +export const CONTROL = 17; +export const ESCAPE = 27; +export const UP_ARROW = 38; +export const DOWN_ARROW = 40; diff --git a/frontend/src/Utilities/Date/formatDate.js b/frontend/src/Utilities/Date/formatDate.js new file mode 100644 index 000000000..92eb57840 --- /dev/null +++ b/frontend/src/Utilities/Date/formatDate.js @@ -0,0 +1,11 @@ +import moment from 'moment'; + +function formatDate(date, dateFormat) { + if (!date) { + return ''; + } + + return moment(date).format(dateFormat); +} + +export default formatDate; diff --git a/frontend/src/Utilities/Date/formatDateTime.js b/frontend/src/Utilities/Date/formatDateTime.js new file mode 100644 index 000000000..f36f4f3e0 --- /dev/null +++ b/frontend/src/Utilities/Date/formatDateTime.js @@ -0,0 +1,39 @@ +import moment from 'moment'; +import formatTime from './formatTime'; +import isToday from './isToday'; +import isTomorrow from './isTomorrow'; +import isYesterday from './isYesterday'; + +function getRelativeDay(date, includeRelativeDate) { + if (!includeRelativeDate) { + return ''; + } + + if (isYesterday(date)) { + return 'Yesterday, '; + } + + if (isToday(date)) { + return 'Today, '; + } + + if (isTomorrow(date)) { + return 'Tomorrow, '; + } + + return ''; +} + +function formatDateTime(date, dateFormat, timeFormat, { includeSeconds = false, includeRelativeDay = false } = {}) { + if (!date) { + return ''; + } + + const relativeDay = getRelativeDay(date, includeRelativeDay); + const formattedDate = moment(date).format(dateFormat); + const formattedTime = formatTime(date, timeFormat, { includeMinuteZero: true, includeSeconds }); + + return `${relativeDay}${formattedDate} ${formattedTime}`; +} + +export default formatDateTime; diff --git a/frontend/src/Utilities/Date/formatTime.js b/frontend/src/Utilities/Date/formatTime.js new file mode 100644 index 000000000..89c908d1f --- /dev/null +++ b/frontend/src/Utilities/Date/formatTime.js @@ -0,0 +1,19 @@ +import moment from 'moment'; + +function formatTime(date, timeFormat, { includeMinuteZero = false, includeSeconds = false } = {}) { + if (!date) { + return ''; + } + + if (includeSeconds) { + timeFormat = timeFormat.replace(/\(?:mm\)?/, ':mm:ss'); + } else if (includeMinuteZero) { + timeFormat = timeFormat.replace('(:mm)', ':mm'); + } else { + timeFormat = timeFormat.replace('(:mm)', ''); + } + + return moment(date).format(timeFormat); +} + +export default formatTime; diff --git a/frontend/src/Utilities/Date/formatTimeSpan.js b/frontend/src/Utilities/Date/formatTimeSpan.js new file mode 100644 index 000000000..ef1a278e5 --- /dev/null +++ b/frontend/src/Utilities/Date/formatTimeSpan.js @@ -0,0 +1,24 @@ +import moment from 'moment'; +import padNumber from 'Utilities/Number/padNumber'; + +function formatTimeSpan(timeSpan) { + if (!timeSpan) { + return ''; + } + + const duration = moment.duration(timeSpan); + const days = duration.get('days'); + const hours = padNumber(duration.get('hours'), 2); + const minutes = padNumber(duration.get('minutes'), 2); + const seconds = padNumber(duration.get('seconds'), 2); + + const time = `${hours}:${minutes}:${seconds}`; + + if (days > 0) { + return `${days}d ${time}`; + } + + return time; +} + +export default formatTimeSpan; diff --git a/frontend/src/Utilities/Date/getRelativeDate.js b/frontend/src/Utilities/Date/getRelativeDate.js new file mode 100644 index 000000000..0d5ab4f58 --- /dev/null +++ b/frontend/src/Utilities/Date/getRelativeDate.js @@ -0,0 +1,40 @@ +import moment from 'moment'; +import formatTime from 'Utilities/Date/formatTime'; +import isInNextWeek from 'Utilities/Date/isInNextWeek'; +import isToday from 'Utilities/Date/isToday'; +import isTomorrow from 'Utilities/Date/isTomorrow'; +import isYesterday from 'Utilities/Date/isYesterday'; + +function getRelativeDate(date, shortDateFormat, showRelativeDates, { timeFormat, includeSeconds = false, timeForToday = false } = {}) { + if (!date) { + return null; + } + + if (!showRelativeDates) { + return moment(date).format(shortDateFormat); + } + + if (isYesterday(date)) { + return 'Yesterday'; + } + + if (isToday(date)) { + if (timeForToday && timeFormat) { + return formatTime(date, timeFormat, { includeMinuteZero: true, includeSeconds }); + } + + return 'Today'; + } + + if (isTomorrow(date)) { + return 'Tomorrow'; + } + + if (isInNextWeek(date)) { + return moment(date).format('dddd'); + } + + return moment(date).format(shortDateFormat); +} + +export default getRelativeDate; diff --git a/frontend/src/Utilities/Date/isAfter.js b/frontend/src/Utilities/Date/isAfter.js new file mode 100644 index 000000000..4bbd8660b --- /dev/null +++ b/frontend/src/Utilities/Date/isAfter.js @@ -0,0 +1,17 @@ +import moment from 'moment'; + +function isAfter(date, offsets = {}) { + if (!date) { + return false; + } + + const offsetTime = moment(); + + Object.keys(offsets).forEach((key) => { + offsetTime.add(offsets[key], key); + }); + + return moment(date).isAfter(offsetTime); +} + +export default isAfter; diff --git a/frontend/src/Utilities/Date/isBefore.js b/frontend/src/Utilities/Date/isBefore.js new file mode 100644 index 000000000..3e1e81f67 --- /dev/null +++ b/frontend/src/Utilities/Date/isBefore.js @@ -0,0 +1,17 @@ +import moment from 'moment'; + +function isBefore(date, offsets = {}) { + if (!date) { + return false; + } + + const offsetTime = moment(); + + Object.keys(offsets).forEach((key) => { + offsetTime.add(offsets[key], key); + }); + + return moment(date).isBefore(offsetTime); +} + +export default isBefore; diff --git a/frontend/src/Utilities/Date/isInNextWeek.js b/frontend/src/Utilities/Date/isInNextWeek.js new file mode 100644 index 000000000..7b5fd7cc7 --- /dev/null +++ b/frontend/src/Utilities/Date/isInNextWeek.js @@ -0,0 +1,11 @@ +import moment from 'moment'; + +function isInNextWeek(date) { + if (!date) { + return false; + } + const now = moment(); + return moment(date).isBetween(now, now.clone().add(6, 'days').endOf('day')); +} + +export default isInNextWeek; diff --git a/frontend/src/Utilities/Date/isSameWeek.js b/frontend/src/Utilities/Date/isSameWeek.js new file mode 100644 index 000000000..14b76ffb7 --- /dev/null +++ b/frontend/src/Utilities/Date/isSameWeek.js @@ -0,0 +1,11 @@ +import moment from 'moment'; + +function isSameWeek(date) { + if (!date) { + return false; + } + + return moment(date).isSame(moment(), 'week'); +} + +export default isSameWeek; diff --git a/frontend/src/Utilities/Date/isToday.js b/frontend/src/Utilities/Date/isToday.js new file mode 100644 index 000000000..31502951f --- /dev/null +++ b/frontend/src/Utilities/Date/isToday.js @@ -0,0 +1,11 @@ +import moment from 'moment'; + +function isToday(date) { + if (!date) { + return false; + } + + return moment(date).isSame(moment(), 'day'); +} + +export default isToday; diff --git a/frontend/src/Utilities/Date/isTomorrow.js b/frontend/src/Utilities/Date/isTomorrow.js new file mode 100644 index 000000000..d22386dbd --- /dev/null +++ b/frontend/src/Utilities/Date/isTomorrow.js @@ -0,0 +1,11 @@ +import moment from 'moment'; + +function isTomorrow(date) { + if (!date) { + return false; + } + + return moment(date).isSame(moment().add(1, 'day'), 'day'); +} + +export default isTomorrow; diff --git a/frontend/src/Utilities/Date/isYesterday.js b/frontend/src/Utilities/Date/isYesterday.js new file mode 100644 index 000000000..9de21d82a --- /dev/null +++ b/frontend/src/Utilities/Date/isYesterday.js @@ -0,0 +1,11 @@ +import moment from 'moment'; + +function isYesterday(date) { + if (!date) { + return false; + } + + return moment(date).isSame(moment().subtract(1, 'day'), 'day'); +} + +export default isYesterday; diff --git a/frontend/src/Utilities/Number/formatAge.js b/frontend/src/Utilities/Number/formatAge.js new file mode 100644 index 000000000..b8a4aacc5 --- /dev/null +++ b/frontend/src/Utilities/Number/formatAge.js @@ -0,0 +1,17 @@ +function formatAge(age, ageHours, ageMinutes) { + age = Math.round(age); + ageHours = parseFloat(ageHours); + ageMinutes = ageMinutes && parseFloat(ageMinutes); + + if (age < 2 && ageHours) { + if (ageHours < 2 && !!ageMinutes) { + return `${ageMinutes.toFixed(0)} ${ageHours === 1 ? 'minute' : 'minutes'}`; + } + + return `${ageHours.toFixed(1)} ${ageHours === 1 ? 'hour' : 'hours'}`; + } + + return `${age} ${age === 1 ? 'day' : 'days'}`; +} + +export default formatAge; diff --git a/frontend/src/Utilities/Number/formatBytes.js b/frontend/src/Utilities/Number/formatBytes.js new file mode 100644 index 000000000..1ff1b5a97 --- /dev/null +++ b/frontend/src/Utilities/Number/formatBytes.js @@ -0,0 +1,16 @@ +import filesize from 'filesize'; + +function formatBytes(input) { + const size = Number(input); + + if (isNaN(size)) { + return ''; + } + + return filesize(size, { + base: 2, + round: 1 + }); +} + +export default formatBytes; diff --git a/frontend/src/Utilities/Number/padNumber.js b/frontend/src/Utilities/Number/padNumber.js new file mode 100644 index 000000000..53ae69cac --- /dev/null +++ b/frontend/src/Utilities/Number/padNumber.js @@ -0,0 +1,10 @@ +function padNumber(input, width, paddingCharacter = 0) { + if (input == null) { + return ''; + } + + input = `${input}`; + return input.length >= width ? input : new Array(width - input.length + 1).join(paddingCharacter) + input; +} + +export default padNumber; diff --git a/frontend/src/Utilities/Object/getErrorMessage.js b/frontend/src/Utilities/Object/getErrorMessage.js new file mode 100644 index 000000000..1ba874660 --- /dev/null +++ b/frontend/src/Utilities/Object/getErrorMessage.js @@ -0,0 +1,11 @@ +function getErrorMessage(xhr, fallbackErrorMessage) { + if (!xhr || !xhr.responseJSON || !xhr.responseJSON.message) { + return fallbackErrorMessage; + } + + const message = xhr.responseJSON.message; + + return message || fallbackErrorMessage; +} + +export default getErrorMessage; diff --git a/frontend/src/Utilities/Object/hasDifferentItems.js b/frontend/src/Utilities/Object/hasDifferentItems.js new file mode 100644 index 000000000..f89c99a10 --- /dev/null +++ b/frontend/src/Utilities/Object/hasDifferentItems.js @@ -0,0 +1,10 @@ +import _ from 'lodash'; + +function hasDifferentItems(prevItems, currentItems, idProp = 'id') { + const diff1 = _.differenceBy(prevItems, currentItems, (item) => item[idProp]); + const diff2 = _.differenceBy(currentItems, prevItems, (item) => item[idProp]); + + return diff1.length > 0 || diff2.length > 0; +} + +export default hasDifferentItems; diff --git a/frontend/src/Utilities/Object/selectUniqueIds.js b/frontend/src/Utilities/Object/selectUniqueIds.js new file mode 100644 index 000000000..c2c0c17e3 --- /dev/null +++ b/frontend/src/Utilities/Object/selectUniqueIds.js @@ -0,0 +1,15 @@ +import _ from 'lodash'; + +function selectUniqueIds(items, idProp) { + const ids = _.reduce(items, (result, item) => { + if (item[idProp]) { + result.push(item[idProp]); + } + + return result; + }, []); + + return _.uniq(ids); +} + +export default selectUniqueIds; diff --git a/frontend/src/Utilities/ResolutionUtility.js b/frontend/src/Utilities/ResolutionUtility.js new file mode 100644 index 000000000..358448ca9 --- /dev/null +++ b/frontend/src/Utilities/ResolutionUtility.js @@ -0,0 +1,26 @@ +import $ from 'jquery'; + +module.exports = { + resolutions: { + desktopLarge: 1200, + desktop: 992, + tablet: 768, + mobile: 480 + }, + + isDesktopLarge() { + return $(window).width() < this.resolutions.desktopLarge; + }, + + isDesktop() { + return $(window).width() < this.resolutions.desktop; + }, + + isTablet() { + return $(window).width() < this.resolutions.tablet; + }, + + isMobile() { + return $(window).width() < this.resolutions.mobile; + } +}; diff --git a/frontend/src/Utilities/Series/getMonitoringOptions.js b/frontend/src/Utilities/Series/getMonitoringOptions.js new file mode 100644 index 000000000..741236f8d --- /dev/null +++ b/frontend/src/Utilities/Series/getMonitoringOptions.js @@ -0,0 +1,70 @@ +import _ from 'lodash'; + +function monitorSeasons(seasons, startingSeason) { + seasons.forEach((season) => { + if (season.seasonNumber >= startingSeason) { + season.monitored = true; + } else { + season.monitored = false; + } + }); +} + +function getMonitoringOptions(albums, monitor) { + if (!albums.length) { + return { + albums: [], + options: { + ignoreEpisodesWithFiles: false, + ignoreEpisodesWithoutFiles: false + } + }; + } + + const firstSeason = _.minBy(_.reject(albums, { seasonNumber: 0 }), 'seasonNumber').seasonNumber; + const lastSeason = _.maxBy(albums, 'seasonNumber').seasonNumber; + + monitorSeasons(albums, firstSeason); + + const monitoringOptions = { + ignoreEpisodesWithFiles: false, + ignoreEpisodesWithoutFiles: false + }; + + switch (monitor) { + case 'future': + monitoringOptions.ignoreEpisodesWithFiles = true; + monitoringOptions.ignoreEpisodesWithoutFiles = true; + break; + case 'latest': + monitorSeasons(albums, lastSeason); + break; + case 'first': + monitorSeasons(albums, lastSeason + 1); + _.find(albums, { seasonNumber: firstSeason }).monitored = true; + break; + case 'missing': + monitoringOptions.ignoreEpisodesWithFiles = true; + break; + case 'existing': + monitoringOptions.ignoreEpisodesWithoutFiles = true; + break; + case 'none': + monitorSeasons(albums, lastSeason + 1); + break; + default: + break; + } + + return { + seasons: _.map(albums, (season) => { + return _.pick(season, [ + 'seasonNumber', + 'monitored' + ]); + }), + options: monitoringOptions + }; +} + +export default getMonitoringOptions; diff --git a/frontend/src/Utilities/Series/getNewSeries.js b/frontend/src/Utilities/Series/getNewSeries.js new file mode 100644 index 000000000..d9424776c --- /dev/null +++ b/frontend/src/Utilities/Series/getNewSeries.js @@ -0,0 +1,38 @@ +import getMonitoringOptions from 'Utilities/Series/getMonitoringOptions'; + +function getNewSeries(artist, payload) { + const { + rootFolderPath, + monitor, + qualityProfileId, + languageProfileId, + artistType, + albumFolder, + primaryAlbumTypes, + secondaryAlbumTypes, + tags, + searchForMissingAlbums = false + } = payload; + + // const { + // seasons, + // options: addOptions + // } = getMonitoringOptions(artist.seasons, monitor); + + // addOptions.searchForMissingAlbums = searchForMissingAlbums; + // artist.addOptions = addOptions; + // artist.seasons = seasons; + artist.monitored = true; + artist.qualityProfileId = qualityProfileId; + artist.languageProfileId = languageProfileId; + artist.rootFolderPath = rootFolderPath; + artist.artistType = artistType; + artist.albumFolder = albumFolder; + artist.primaryAlbumTypes = primaryAlbumTypes; + artist.secondaryAlbumTypes = secondaryAlbumTypes; + artist.tags = tags; + + return artist; +} + +export default getNewSeries; diff --git a/frontend/src/Utilities/Series/getProgressBarKind.js b/frontend/src/Utilities/Series/getProgressBarKind.js new file mode 100644 index 000000000..eb3b2dd6e --- /dev/null +++ b/frontend/src/Utilities/Series/getProgressBarKind.js @@ -0,0 +1,15 @@ +import { kinds } from 'Helpers/Props'; + +function getProgressBarKind(status, monitored, progress) { + if (progress === 100) { + return status === 'ended' ? kinds.SUCCESS : kinds.PRIMARY; + } + + if (monitored) { + return kinds.DANGER; + } + + return kinds.WARNING; +} + +export default getProgressBarKind; diff --git a/frontend/src/Utilities/State/getProviderState.js b/frontend/src/Utilities/State/getProviderState.js new file mode 100644 index 000000000..7d6e5cb1d --- /dev/null +++ b/frontend/src/Utilities/State/getProviderState.js @@ -0,0 +1,30 @@ +import _ from 'lodash'; + +function getProviderState(payload, getState, getFromState) { + const id = payload.id; + const state = getFromState(getState()); + const pendingChanges = Object.assign({}, state.pendingChanges); + const pendingFields = state.pendingChanges.fields || {}; + delete pendingChanges.fields; + + const item = id ? _.find(state.items, { id }) : state.selectedSchema || state.schema || {}; + + if (item.fields) { + pendingChanges.fields = _.reduce(item.fields, (result, field) => { + const value = pendingFields.hasOwnProperty(field.name) ? + pendingFields[field.name] : + field.value; + + result.push({ + ...field, + value + }); + + return result; + }, []); + } + + return Object.assign({}, item, pendingChanges); +} + +export default getProviderState; diff --git a/frontend/src/Utilities/State/getSectionState.js b/frontend/src/Utilities/State/getSectionState.js new file mode 100644 index 000000000..c188d9eaa --- /dev/null +++ b/frontend/src/Utilities/State/getSectionState.js @@ -0,0 +1,9 @@ +function getSectionState(state, section) { + if (state.hasOwnProperty(section)) { + return Object.assign({}, state[section]); + } + + return Object.assign({}, state); +} + +export default getSectionState; diff --git a/frontend/src/Utilities/State/selectProviderSchema.js b/frontend/src/Utilities/State/selectProviderSchema.js new file mode 100644 index 000000000..c8a31760c --- /dev/null +++ b/frontend/src/Utilities/State/selectProviderSchema.js @@ -0,0 +1,34 @@ +import _ from 'lodash'; +import getSectionState from 'Utilities/State/getSectionState'; +import updateSectionState from 'Utilities/State/updateSectionState'; + +function applySchemaDefaults(selectedSchema, schemaDefaults) { + if (!schemaDefaults) { + return selectedSchema; + } else if (_.isFunction(schemaDefaults)) { + return schemaDefaults(selectedSchema); + } + + return Object.assign(selectedSchema, schemaDefaults); +} + +function selectProviderSchema(state, section, payload, schemaDefaults) { + const newState = getSectionState(state, section); + + const { + implementation, + presetName + } = payload; + + const selectedImplementation = _.find(newState.schema, { implementation }); + + const selectedSchema = presetName ? + _.find(selectedImplementation.presets, { name: presetName }) : + selectedImplementation; + + newState.selectedSchema = applySchemaDefaults(_.cloneDeep(selectedSchema), schemaDefaults); + + return updateSectionState(state, section, newState); +} + +export default selectProviderSchema; diff --git a/frontend/src/Utilities/State/updateSectionState.js b/frontend/src/Utilities/State/updateSectionState.js new file mode 100644 index 000000000..c7407257d --- /dev/null +++ b/frontend/src/Utilities/State/updateSectionState.js @@ -0,0 +1,9 @@ +function updateSectionState(state, section, newState) { + if (state.hasOwnProperty(section)) { + return Object.assign({}, state, { [section]: newState }); + } + + return Object.assign({}, state, newState); +} + +export default updateSectionState; diff --git a/frontend/src/Utilities/String/combinePath.js b/frontend/src/Utilities/String/combinePath.js new file mode 100644 index 000000000..9e4e9abe8 --- /dev/null +++ b/frontend/src/Utilities/String/combinePath.js @@ -0,0 +1,5 @@ +export default function combinePath(isWindows, basePath, paths = []) { + const slash = isWindows ? '\\' : '/'; + + return `${basePath}${slash}${paths.join(slash)}`; +} diff --git a/frontend/src/Utilities/String/parseUrl.js b/frontend/src/Utilities/String/parseUrl.js new file mode 100644 index 000000000..99377cd7a --- /dev/null +++ b/frontend/src/Utilities/String/parseUrl.js @@ -0,0 +1,34 @@ +import _ from 'lodash'; +import qs from 'qs'; + +// See: https://developer.mozilla.org/en-US/docs/Web/API/HTMLHyperlinkElementUtils +const anchor = document.createElement('a'); + +export default function parseUrl(url) { + anchor.href = url; + + // The `origin`, `password`, and `username` properties are unavailable in + // Opera Presto. We synthesize `origin` if it's not present. While `password` + // and `username` are ignored intentionally. + const properties = _.pick( + anchor, + 'hash', + 'host', + 'hostname', + 'href', + 'origin', + 'pathname', + 'port', + 'protocol', + 'search' + ); + + properties.isAbsolute = (/^[\w:]*\/\//).test(url); + + if (properties.search) { + // Remove leading ? from querystring before parsing. + properties.params = qs.parse(properties.search.substring(1)); + } + + return properties; +} diff --git a/frontend/src/Utilities/String/split.js b/frontend/src/Utilities/String/split.js new file mode 100644 index 000000000..0e57e7545 --- /dev/null +++ b/frontend/src/Utilities/String/split.js @@ -0,0 +1,17 @@ +import _ from 'lodash'; + +function split(input, separator = ',') { + if (!input) { + return []; + } + + return _.reduce(input.split(separator), (result, s) => { + if (s) { + result.push(s); + } + + return result; + }, []); +} + +export default split; diff --git a/frontend/src/Utilities/String/titleCase.js b/frontend/src/Utilities/String/titleCase.js new file mode 100644 index 000000000..531e4df68 --- /dev/null +++ b/frontend/src/Utilities/String/titleCase.js @@ -0,0 +1,11 @@ +function titleCase(input) { + if (!input) { + return ''; + } + + return input.replace(/\w\S*/g, (match) => { + return match.charAt(0).toUpperCase() + match.substr(1).toLowerCase(); + }); +} + +export default titleCase; diff --git a/frontend/src/Utilities/Table/areAllSelected.js b/frontend/src/Utilities/Table/areAllSelected.js new file mode 100644 index 000000000..26102f89b --- /dev/null +++ b/frontend/src/Utilities/Table/areAllSelected.js @@ -0,0 +1,17 @@ +export default function areAllSelected(selectedState) { + let allSelected = true; + let allUnselected = true; + + Object.keys(selectedState).forEach((key) => { + if (selectedState[key]) { + allUnselected = false; + } else { + allSelected = false; + } + }); + + return { + allSelected, + allUnselected + }; +} diff --git a/frontend/src/Utilities/Table/getSelectedIds.js b/frontend/src/Utilities/Table/getSelectedIds.js new file mode 100644 index 000000000..705f13a5d --- /dev/null +++ b/frontend/src/Utilities/Table/getSelectedIds.js @@ -0,0 +1,15 @@ +import _ from 'lodash'; + +function getSelectedIds(selectedState, { parseIds = true } = {}) { + return _.reduce(selectedState, (result, value, id) => { + if (value) { + const parsedId = parseIds ? parseInt(id) : id; + + result.push(parsedId); + } + + return result; + }, []); +} + +export default getSelectedIds; diff --git a/frontend/src/Utilities/Table/getToggledRange.js b/frontend/src/Utilities/Table/getToggledRange.js new file mode 100644 index 000000000..c0cc44fe5 --- /dev/null +++ b/frontend/src/Utilities/Table/getToggledRange.js @@ -0,0 +1,23 @@ +import _ from 'lodash'; + +function getToggledRange(items, id, lastToggled) { + const lastToggledIndex = _.findIndex(items, { id: lastToggled }); + const changedIndex = _.findIndex(items, { id }); + let lower = 0; + let upper = 0; + + if (lastToggledIndex > changedIndex) { + lower = changedIndex; + upper = lastToggledIndex + 1; + } else { + lower = lastToggledIndex; + upper = changedIndex; + } + + return { + lower, + upper + }; +} + +export default getToggledRange; diff --git a/frontend/src/Utilities/Table/removeOldSelectedState.js b/frontend/src/Utilities/Table/removeOldSelectedState.js new file mode 100644 index 000000000..ff3a4fe11 --- /dev/null +++ b/frontend/src/Utilities/Table/removeOldSelectedState.js @@ -0,0 +1,16 @@ +import areAllSelected from './areAllSelected'; + +export default function removeOldSelectedState(state, prevItems) { + const selectedState = { + ...state.selectedState + }; + + prevItems.forEach((item) => { + delete selectedState[item.id]; + }); + + return { + ...areAllSelected(selectedState), + selectedState + }; +} diff --git a/frontend/src/Utilities/Table/selectAll.js b/frontend/src/Utilities/Table/selectAll.js new file mode 100644 index 000000000..ffaaeaddf --- /dev/null +++ b/frontend/src/Utilities/Table/selectAll.js @@ -0,0 +1,17 @@ +import _ from 'lodash'; + +function selectAll(selectedState, selected) { + const newSelectedState = _.reduce(Object.keys(selectedState), (result, item) => { + result[item] = selected; + return result; + }, {}); + + return { + allSelected: selected, + allUnselected: !selected, + lastToggled: null, + selectedState: newSelectedState + }; +} + +export default selectAll; diff --git a/frontend/src/Utilities/Table/toggleSelected.js b/frontend/src/Utilities/Table/toggleSelected.js new file mode 100644 index 000000000..4b19dc268 --- /dev/null +++ b/frontend/src/Utilities/Table/toggleSelected.js @@ -0,0 +1,26 @@ +import areAllSelected from './areAllSelected'; +import getToggledRange from './getToggledRange'; + +function toggleSelected(state, items, id, selected, shiftKey) { + const lastToggled = state.lastToggled; + const selectedState = { + ...state.selectedState, + [id]: selected + }; + + if (shiftKey && lastToggled) { + const { lower, upper } = getToggledRange(items, id, lastToggled); + + for (let i = lower; i < upper; i++) { + selectedState[items[i].id] = selected; + } + } + + return { + ...areAllSelected(selectedState), + lastToggled: id, + selectedState + }; +} + +export default toggleSelected; diff --git a/frontend/src/Utilities/createAjaxRequest.js b/frontend/src/Utilities/createAjaxRequest.js new file mode 100644 index 000000000..fc75d65b8 --- /dev/null +++ b/frontend/src/Utilities/createAjaxRequest.js @@ -0,0 +1,32 @@ +import $ from 'jquery'; + +export default function createAjaxRequest() { + return function(ajaxOptions) { + const requestXHR = new window.XMLHttpRequest(); + let aborted = false; + let complete = false; + + function abortRequest() { + if (!complete) { + aborted = true; + requestXHR.abort(); + } + } + + const request = $.ajax({ + xhr: () => requestXHR, + ...ajaxOptions + }).then(null, (xhr, textStatus, errorThrown) => { + xhr.aborted = aborted; + + return $.Deferred().reject(xhr, textStatus, errorThrown).promise(); + }).always(() => { + complete = true; + }); + + return { + request, + abortRequest + }; + }; +} diff --git a/frontend/src/Utilities/getPathWithUrlBase.js b/frontend/src/Utilities/getPathWithUrlBase.js new file mode 100644 index 000000000..60533d3d3 --- /dev/null +++ b/frontend/src/Utilities/getPathWithUrlBase.js @@ -0,0 +1,3 @@ +export default function getPathWithUrlBase(path) { + return `${window.Sonarr.urlBase}${path}`; +} diff --git a/frontend/src/Utilities/getUniqueElementId.js b/frontend/src/Utilities/getUniqueElementId.js new file mode 100644 index 000000000..dae5150b7 --- /dev/null +++ b/frontend/src/Utilities/getUniqueElementId.js @@ -0,0 +1,7 @@ +let i = 0; + +// returns a HTML 4.0 compliant element IDs (http://stackoverflow.com/a/79022) + +export default function getUniqueElementId() { + return `id-${i++}`; +} diff --git a/frontend/src/Utilities/isMobile.js b/frontend/src/Utilities/isMobile.js new file mode 100644 index 000000000..489020a23 --- /dev/null +++ b/frontend/src/Utilities/isMobile.js @@ -0,0 +1,7 @@ +import MobileDetect from 'mobile-detect'; + +export default function isMobile() { + const mobileDetect = new MobileDetect(window.navigator.userAgent); + + return mobileDetect.mobile() != null; +} diff --git a/frontend/src/Utilities/pagePopulator.js b/frontend/src/Utilities/pagePopulator.js new file mode 100644 index 000000000..3fb26b1db --- /dev/null +++ b/frontend/src/Utilities/pagePopulator.js @@ -0,0 +1,17 @@ +let currentPopulator = null; + +export function registerPagePopulator(populator) { + currentPopulator = populator; +} + +export function unregisterPagePopulator(populator) { + if (currentPopulator === populator) { + currentPopulator = null; + } +} + +export function repopulatePage() { + if (currentPopulator) { + currentPopulator(); + } +} diff --git a/frontend/src/Utilities/pages.js b/frontend/src/Utilities/pages.js new file mode 100644 index 000000000..1355442d9 --- /dev/null +++ b/frontend/src/Utilities/pages.js @@ -0,0 +1,9 @@ +const pages = { + FIRST: 'first', + PREVIOUS: 'previous', + NEXT: 'next', + LAST: 'last', + EXACT: 'exact' +}; + +export default pages; diff --git a/frontend/src/Utilities/requestAction.js b/frontend/src/Utilities/requestAction.js new file mode 100644 index 000000000..3f2564a7b --- /dev/null +++ b/frontend/src/Utilities/requestAction.js @@ -0,0 +1,40 @@ +import $ from 'jquery'; +import _ from 'lodash'; + +function flattenProviderData(providerData) { + return _.reduce(Object.keys(providerData), (result, key) => { + const property = providerData[key]; + + if (key === 'fields') { + result[key] = property; + } else { + result[key] = property.value; + } + + return result; + }, {}); +} + +function requestAction(payload) { + const { + provider, + action, + providerData, + queryParams + } = payload; + + const ajaxOptions = { + url: `/${provider}/action/${action}`, + contentType: 'application/json', + method: 'POST', + data: JSON.stringify(flattenProviderData(providerData)) + }; + + if (queryParams) { + ajaxOptions.url += `?${$.param(queryParams, true)}`; + } + + return $.ajax(ajaxOptions); +} + +export default requestAction; diff --git a/frontend/src/Utilities/sectionTypes.js b/frontend/src/Utilities/sectionTypes.js new file mode 100644 index 000000000..5479b32b9 --- /dev/null +++ b/frontend/src/Utilities/sectionTypes.js @@ -0,0 +1,6 @@ +const sectionTypes = { + COLLECTION: 'collection', + MODEL: 'model' +}; + +export default sectionTypes; diff --git a/frontend/src/Utilities/serverSideCollectionHandlers.js b/frontend/src/Utilities/serverSideCollectionHandlers.js new file mode 100644 index 000000000..03fa39c00 --- /dev/null +++ b/frontend/src/Utilities/serverSideCollectionHandlers.js @@ -0,0 +1,12 @@ +const serverSideCollectionHandlers = { + FETCH: 'fetch', + FIRST_PAGE: 'firstPage', + PREVIOUS_PAGE: 'previousPage', + NEXT_PAGE: 'nextPage', + LAST_PAGE: 'lastPage', + EXACT_PAGE: 'exactPage', + SORT: 'sort', + FILTER: 'filter' +}; + +export default serverSideCollectionHandlers; diff --git a/frontend/src/Wanted/CutoffUnmet/CutoffUnmet.js b/frontend/src/Wanted/CutoffUnmet/CutoffUnmet.js new file mode 100644 index 000000000..76ec74085 --- /dev/null +++ b/frontend/src/Wanted/CutoffUnmet/CutoffUnmet.js @@ -0,0 +1,285 @@ +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, kinds } from 'Helpers/Props'; +import LoadingIndicator from 'Components/Loading/LoadingIndicator'; +import Table from 'Components/Table/Table'; +import TableBody from 'Components/Table/TableBody'; +import TablePager from 'Components/Table/TablePager'; +import PageContent from 'Components/Page/PageContent'; +import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector'; +import PageToolbar from 'Components/Page/Toolbar/PageToolbar'; +import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection'; +import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator'; +import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton'; +import FilterMenu from 'Components/Menu/FilterMenu'; +import MenuContent from 'Components/Menu/MenuContent'; +import FilterMenuItem from 'Components/Menu/FilterMenuItem'; +import ConfirmModal from 'Components/Modal/ConfirmModal'; +import CutoffUnmetRowConnector from './CutoffUnmetRowConnector'; + +class CutoffUnmet extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + allSelected: false, + allUnselected: false, + lastToggled: null, + selectedState: {}, + isConfirmSearchAllCutoffUnmetModalOpen: false, + isInteractiveImportModalOpen: false + }; + } + + componentDidUpdate(prevProps) { + if (hasDifferentItems(prevProps.items, this.props.items)) { + this.setState((state) => { + return removeOldSelectedState(state, prevProps.items); + }); + } + } + + // + // Control + + getSelectedIds = () => { + return getSelectedIds(this.state.selectedState); + } + + // + // Listeners + + onFilterMenuItemPress = (filterKey, filterValue) => { + this.props.onFilterSelect(filterKey, filterValue); + } + + onSelectAllChange = ({ value }) => { + this.setState(selectAll(this.state.selectedState, value)); + } + + onSelectedChange = ({ id, value, shiftKey = false }) => { + this.setState((state) => { + return toggleSelected(state, this.props.items, id, value, shiftKey); + }); + } + + onSearchSelectedPress = () => { + const selected = this.getSelectedIds(); + + this.props.onSearchSelectedPress(selected); + } + + onToggleSelectedPress = () => { + const selected = this.getSelectedIds(); + + this.props.onToggleSelectedPress(selected); + } + + onSearchAllCutoffUnmetPress = () => { + this.setState({ isConfirmSearchAllCutoffUnmetModalOpen: true }); + } + + onSearchAllCutoffUnmetConfirmed = () => { + this.props.onSearchAllCutoffUnmetPress(); + this.setState({ isConfirmSearchAllCutoffUnmetModalOpen: false }); + } + + onConfirmSearchAllCutoffUnmetModalClose = () => { + this.setState({ isConfirmSearchAllCutoffUnmetModalOpen: false }); + } + + // + // Render + + render() { + const { + isFetching, + isPopulated, + error, + items, + columns, + totalRecords, + isSearchingForAlbums, + isSearchingForCutoffUnmetAlbums, + isSaving, + filterKey, + filterValue, + ...otherProps + } = this.props; + + const { + allSelected, + allUnselected, + selectedState, + isConfirmSearchAllCutoffUnmetModalOpen + } = this.state; + + const itemsSelected = !!this.getSelectedIds().length; + + return ( + + + + + + + + + + + + + + + + + + + Monitored + + + + Unmonitored + + + + + + + + { + isFetching && !isPopulated && + + } + + { + !isFetching && error && +
+ Error fetching cutoff unmet +
+ } + + { + isPopulated && !error && !items.length && +
+ No cutoff unmet items +
+ } + + { + isPopulated && !error && !!items.length && +
+ + + { + items.map((item) => { + return ( + + ); + }) + } + +
+ + + + +
+ Are you sure you want to search for all {totalRecords} Cutoff Unmet episodes? +
+
+ This cannot be cancelled once started without restarting Lidarr. +
+
+ } + confirmLabel="Search" + onConfirm={this.onSearchAllCutoffUnmetConfirmed} + onCancel={this.onConfirmSearchAllCutoffUnmetModalClose} + /> +
+ } + + + ); + } +} + +CutoffUnmet.propTypes = { + isFetching: PropTypes.bool.isRequired, + isPopulated: PropTypes.bool.isRequired, + error: PropTypes.object, + items: PropTypes.arrayOf(PropTypes.object).isRequired, + columns: PropTypes.arrayOf(PropTypes.object).isRequired, + totalRecords: PropTypes.number, + isSearchingForAlbums: PropTypes.bool.isRequired, + isSearchingForCutoffUnmetAlbums: PropTypes.bool.isRequired, + isSaving: PropTypes.bool.isRequired, + filterKey: PropTypes.string, + filterValue: PropTypes.oneOfType([PropTypes.bool, PropTypes.number, PropTypes.string]), + onFilterSelect: PropTypes.func.isRequired, + onSearchSelectedPress: PropTypes.func.isRequired, + onToggleSelectedPress: PropTypes.func.isRequired, + onSearchAllCutoffUnmetPress: PropTypes.func.isRequired +}; + +export default CutoffUnmet; diff --git a/frontend/src/Wanted/CutoffUnmet/CutoffUnmetConnector.js b/frontend/src/Wanted/CutoffUnmet/CutoffUnmetConnector.js new file mode 100644 index 000000000..423bdad53 --- /dev/null +++ b/frontend/src/Wanted/CutoffUnmet/CutoffUnmetConnector.js @@ -0,0 +1,190 @@ +import _ from 'lodash'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import { registerPagePopulator, unregisterPagePopulator } from 'Utilities/pagePopulator'; +import hasDifferentItems from 'Utilities/Object/hasDifferentItems'; +import selectUniqueIds from 'Utilities/Object/selectUniqueIds'; +import createCommandsSelector from 'Store/Selectors/createCommandsSelector'; +import * as wantedActions from 'Store/Actions/wantedActions'; +import { executeCommand } from 'Store/Actions/commandActions'; +import { fetchQueueDetails, clearQueueDetails } from 'Store/Actions/queueActions'; +import { fetchTrackFiles, clearTrackFiles } from 'Store/Actions/trackFileActions'; +import * as commandNames from 'Commands/commandNames'; +import CutoffUnmet from './CutoffUnmet'; + +function createMapStateToProps() { + return createSelector( + (state) => state.wanted.cutoffUnmet, + createCommandsSelector(), + (cutoffUnmet, commands) => { + const isSearchingForAlbums = _.some(commands, { name: commandNames.ALBUM_SEARCH }); + const isSearchingForCutoffUnmetAlbums = _.some(commands, { name: commandNames.CUTOFF_UNMET_ALBUM_SEARCH }); + + return { + isSearchingForAlbums, + isSearchingForCutoffUnmetAlbums, + isSaving: _.some(cutoffUnmet.items, { isSaving: true }), + ...cutoffUnmet + }; + } + ); +} + +const mapDispatchToProps = { + ...wantedActions, + executeCommand, + fetchQueueDetails, + clearQueueDetails, + fetchTrackFiles, + clearTrackFiles +}; + +class CutoffUnmetConnector extends Component { + + // + // Lifecycle + + componentDidMount() { + registerPagePopulator(this.repopulate); + this.props.gotoCutoffUnmetFirstPage(); + } + + componentDidUpdate(prevProps) { + if (hasDifferentItems(prevProps.items, this.props.items)) { + const albumIds = selectUniqueIds(this.props.items, 'id'); + const trackFileIds = selectUniqueIds(this.props.items, 'trackFileId'); + + this.props.fetchQueueDetails({ albumIds }); + + if (trackFileIds.length) { + this.props.fetchTrackFiles({ trackFileIds }); + } + } + } + + componentWillUnmount() { + unregisterPagePopulator(this.repopulate); + this.props.clearCutoffUnmet(); + this.props.clearQueueDetails(); + this.props.clearTrackFiles(); + } + + // + // Control + + repopulate = () => { + this.props.fetchCutoffUnmet(); + } + + // + // Listeners + + onFirstPagePress = () => { + this.props.gotoCutoffUnmetFirstPage(); + } + + onPreviousPagePress = () => { + this.props.gotoCutoffUnmetPreviousPage(); + } + + onNextPagePress = () => { + this.props.gotoCutoffUnmetNextPage(); + } + + onLastPagePress = () => { + this.props.gotoCutoffUnmetLastPage(); + } + + onPageSelect = (page) => { + this.props.gotoCutoffUnmetPage({ page }); + } + + onSortPress = (sortKey) => { + this.props.setCutoffUnmetSort({ sortKey }); + } + + onFilterSelect = (filterKey, filterValue) => { + this.props.setCutoffUnmetFilter({ filterKey, filterValue }); + } + + onTableOptionChange = (payload) => { + this.props.setCutoffUnmetTableOption(payload); + + if (payload.pageSize) { + this.props.gotoCutoffUnmetFirstPage(); + } + } + + onSearchSelectedPress = (selected) => { + this.props.executeCommand({ + name: commandNames.ALBUM_SEARCH, + albumIds: selected + }); + } + + onToggleSelectedPress = (selected) => { + const { + filterKey, + filterValue + } = this.props; + + this.props.batchToggleCutoffUnmetAlbums({ + albumIds: selected, + monitored: filterKey !== 'monitored' || !filterValue + }); + } + + onSearchAllCutoffUnmetPress = () => { + this.props.executeCommand({ + name: commandNames.CUTOFF_UNMET_ALBUM_SEARCH + }); + } + + // + // Render + + render() { + return ( + + ); + } +} + +CutoffUnmetConnector.propTypes = { + items: PropTypes.arrayOf(PropTypes.object).isRequired, + filterKey: PropTypes.string.isRequired, + filterValue: PropTypes.oneOfType([PropTypes.bool, PropTypes.number, PropTypes.string]), + fetchCutoffUnmet: PropTypes.func.isRequired, + gotoCutoffUnmetFirstPage: PropTypes.func.isRequired, + gotoCutoffUnmetPreviousPage: PropTypes.func.isRequired, + gotoCutoffUnmetNextPage: PropTypes.func.isRequired, + gotoCutoffUnmetLastPage: PropTypes.func.isRequired, + gotoCutoffUnmetPage: PropTypes.func.isRequired, + setCutoffUnmetSort: PropTypes.func.isRequired, + setCutoffUnmetFilter: PropTypes.func.isRequired, + setCutoffUnmetTableOption: PropTypes.func.isRequired, + batchToggleCutoffUnmetAlbums: PropTypes.func.isRequired, + clearCutoffUnmet: PropTypes.func.isRequired, + executeCommand: PropTypes.func.isRequired, + fetchQueueDetails: PropTypes.func.isRequired, + clearQueueDetails: PropTypes.func.isRequired, + fetchTrackFiles: PropTypes.func.isRequired, + clearTrackFiles: PropTypes.func.isRequired +}; + +export default connect(createMapStateToProps, mapDispatchToProps)(CutoffUnmetConnector); diff --git a/frontend/src/Wanted/CutoffUnmet/CutoffUnmetRow.css b/frontend/src/Wanted/CutoffUnmet/CutoffUnmetRow.css new file mode 100644 index 000000000..934076d15 --- /dev/null +++ b/frontend/src/Wanted/CutoffUnmet/CutoffUnmetRow.css @@ -0,0 +1,7 @@ +.episode, +.language, +.status { + composes: cell from 'Components/Table/Cells/TableRowCell.css'; + + width: 100px; +} diff --git a/frontend/src/Wanted/CutoffUnmet/CutoffUnmetRow.js b/frontend/src/Wanted/CutoffUnmet/CutoffUnmetRow.js new file mode 100644 index 000000000..1d224eb8e --- /dev/null +++ b/frontend/src/Wanted/CutoffUnmet/CutoffUnmetRow.js @@ -0,0 +1,139 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import episodeEntities from 'Album/episodeEntities'; +import EpisodeTitleLink from 'Album/EpisodeTitleLink'; +import EpisodeStatusConnector from 'Album/EpisodeStatusConnector'; +import EpisodeSearchCellConnector from 'Album/EpisodeSearchCellConnector'; +import TrackFileLanguageConnector from 'TrackFile/TrackFileLanguageConnector'; +import ArtistNameLink from 'Artist/ArtistNameLink'; +import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector'; +import TableRow from 'Components/Table/TableRow'; +import TableRowCell from 'Components/Table/Cells/TableRowCell'; +import TableSelectCell from 'Components/Table/Cells/TableSelectCell'; +import styles from './CutoffUnmetRow.css'; + +function CutoffUnmetRow(props) { + const { + id, + trackFileId, + artist, + releaseDate, + title, + isSelected, + columns, + onSelectedChange + } = props; + + return ( + + + + { + columns.map((column) => { + const { + name, + isVisible + } = column; + + if (!isVisible) { + return null; + } + + if (name === 'artist.sortName') { + return ( + + + + ); + } + + if (name === 'albumTitle') { + return ( + + + + ); + } + + if (name === 'releaseDate') { + return ( + + ); + } + + if (name === 'language') { + return ( + + + + ); + } + + if (name === 'status') { + return ( + + + + ); + } + + if (name === 'actions') { + return ( + + ); + } + + return null; + }) + } + + ); +} + +CutoffUnmetRow.propTypes = { + id: PropTypes.number.isRequired, + trackFileId: PropTypes.number, + artist: PropTypes.object.isRequired, + releaseDate: PropTypes.string.isRequired, + title: PropTypes.string.isRequired, + isSelected: PropTypes.bool, + columns: PropTypes.arrayOf(PropTypes.object).isRequired, + onSelectedChange: PropTypes.func.isRequired +}; + +export default CutoffUnmetRow; diff --git a/frontend/src/Wanted/CutoffUnmet/CutoffUnmetRowConnector.js b/frontend/src/Wanted/CutoffUnmet/CutoffUnmetRowConnector.js new file mode 100644 index 000000000..625055c57 --- /dev/null +++ b/frontend/src/Wanted/CutoffUnmet/CutoffUnmetRowConnector.js @@ -0,0 +1,17 @@ +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import createArtistSelector from 'Store/Selectors/createArtistSelector'; +import CutoffUnmetRow from './CutoffUnmetRow'; + +function createMapStateToProps() { + return createSelector( + createArtistSelector(), + (artist) => { + return { + artist + }; + } + ); +} + +export default connect(createMapStateToProps)(CutoffUnmetRow); diff --git a/frontend/src/Wanted/Missing/Missing.js b/frontend/src/Wanted/Missing/Missing.js new file mode 100644 index 000000000..0914e3c3a --- /dev/null +++ b/frontend/src/Wanted/Missing/Missing.js @@ -0,0 +1,307 @@ +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, kinds } from 'Helpers/Props'; +import LoadingIndicator from 'Components/Loading/LoadingIndicator'; +import Table from 'Components/Table/Table'; +import TableBody from 'Components/Table/TableBody'; +import TablePager from 'Components/Table/TablePager'; +import PageContent from 'Components/Page/PageContent'; +import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector'; +import PageToolbar from 'Components/Page/Toolbar/PageToolbar'; +import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection'; +import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator'; +import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton'; +import FilterMenu from 'Components/Menu/FilterMenu'; +import MenuContent from 'Components/Menu/MenuContent'; +import FilterMenuItem from 'Components/Menu/FilterMenuItem'; +import ConfirmModal from 'Components/Modal/ConfirmModal'; +import InteractiveImportModal from 'InteractiveImport/InteractiveImportModal'; +import MissingRowConnector from './MissingRowConnector'; + +class Missing extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + allSelected: false, + allUnselected: false, + lastToggled: null, + selectedState: {}, + isConfirmSearchAllMissingModalOpen: false, + isInteractiveImportModalOpen: false + }; + } + + componentDidUpdate(prevProps) { + if (hasDifferentItems(prevProps.items, this.props.items)) { + this.setState((state) => { + return removeOldSelectedState(state, prevProps.items); + }); + } + } + + // + // Control + + getSelectedIds = () => { + return getSelectedIds(this.state.selectedState); + } + + // + // Listeners + + onFilterMenuItemPress = (filterKey, filterValue) => { + this.props.onFilterSelect(filterKey, filterValue); + } + + onSelectAllChange = ({ value }) => { + this.setState(selectAll(this.state.selectedState, value)); + } + + onSelectedChange = ({ id, value, shiftKey = false }) => { + this.setState((state) => { + return toggleSelected(state, this.props.items, id, value, shiftKey); + }); + } + + onSearchSelectedPress = () => { + const selected = this.getSelectedIds(); + + this.props.onSearchSelectedPress(selected); + } + + onToggleSelectedPress = () => { + const selected = this.getSelectedIds(); + + this.props.onToggleSelectedPress(selected); + } + + onSearchAllMissingPress = () => { + this.setState({ isConfirmSearchAllMissingModalOpen: true }); + } + + onSearchAllMissingConfirmed = () => { + this.props.onSearchAllMissingPress(); + this.setState({ isConfirmSearchAllMissingModalOpen: false }); + } + + onConfirmSearchAllMissingModalClose = () => { + this.setState({ isConfirmSearchAllMissingModalOpen: false }); + } + + onInteractiveImportPress = () => { + this.setState({ isInteractiveImportModalOpen: true }); + } + + onInteractiveImportModalClose = () => { + this.setState({ isInteractiveImportModalOpen: false }); + } + + // + // Render + + render() { + const { + isFetching, + isPopulated, + error, + items, + columns, + totalRecords, + isSearchingForAlbums, + isSearchingForMissingAlbums, + isSaving, + filterKey, + filterValue, + ...otherProps + } = this.props; + + const { + allSelected, + allUnselected, + selectedState, + isConfirmSearchAllMissingModalOpen, + isInteractiveImportModalOpen + } = this.state; + + const itemsSelected = !!this.getSelectedIds().length; + + return ( + + + + + + + + + + + + + + + + + + + + + + Monitored + + + + Unmonitored + + + + + + + + { + isFetching && !isPopulated && + + } + + { + !isFetching && error && +
+ Error fetching missing items +
+ } + + { + isPopulated && !error && !items.length && +
+ No missing items +
+ } + + { + isPopulated && !error && !!items.length && +
+ + + { + items.map((item) => { + return ( + + ); + }) + } + +
+ + + + +
+ Are you sure you want to search for all {totalRecords} missing albums? +
+
+ This cannot be cancelled once started without restarting Lidarr. +
+
+ } + confirmLabel="Search" + onConfirm={this.onSearchAllMissingConfirmed} + onCancel={this.onConfirmSearchAllMissingModalClose} + /> + + +
+ } + + + ); + } +} + +Missing.propTypes = { + isFetching: PropTypes.bool.isRequired, + isPopulated: PropTypes.bool.isRequired, + error: PropTypes.object, + items: PropTypes.arrayOf(PropTypes.object).isRequired, + columns: PropTypes.arrayOf(PropTypes.object).isRequired, + totalRecords: PropTypes.number, + isSearchingForAlbums: PropTypes.bool.isRequired, + isSearchingForMissingAlbums: PropTypes.bool.isRequired, + isSaving: PropTypes.bool.isRequired, + filterKey: PropTypes.string, + filterValue: PropTypes.oneOfType([PropTypes.bool, PropTypes.number, PropTypes.string]), + onFilterSelect: PropTypes.func.isRequired, + onSearchSelectedPress: PropTypes.func.isRequired, + onToggleSelectedPress: PropTypes.func.isRequired, + onSearchAllMissingPress: PropTypes.func.isRequired +}; + +export default Missing; diff --git a/frontend/src/Wanted/Missing/MissingConnector.js b/frontend/src/Wanted/Missing/MissingConnector.js new file mode 100644 index 000000000..80db02eb0 --- /dev/null +++ b/frontend/src/Wanted/Missing/MissingConnector.js @@ -0,0 +1,178 @@ +import _ from 'lodash'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import { registerPagePopulator, unregisterPagePopulator } from 'Utilities/pagePopulator'; +import hasDifferentItems from 'Utilities/Object/hasDifferentItems'; +import selectUniqueIds from 'Utilities/Object/selectUniqueIds'; +import createCommandsSelector from 'Store/Selectors/createCommandsSelector'; +import * as wantedActions from 'Store/Actions/wantedActions'; +import { executeCommand } from 'Store/Actions/commandActions'; +import { fetchQueueDetails, clearQueueDetails } from 'Store/Actions/queueActions'; +import * as commandNames from 'Commands/commandNames'; +import Missing from './Missing'; + +function createMapStateToProps() { + return createSelector( + (state) => state.wanted.missing, + createCommandsSelector(), + (missing, commands) => { + const isSearchingForAlbums = _.some(commands, { name: commandNames.ALBUM_SEARCH }); + const isSearchingForMissingAlbums = _.some(commands, { name: commandNames.MISSING_ALBUM_SEARCH }); + + return { + isSearchingForAlbums, + isSearchingForMissingAlbums, + isSaving: _.some(missing.items, { isSaving: true }), + ...missing + }; + } + ); +} + +const mapDispatchToProps = { + ...wantedActions, + executeCommand, + fetchQueueDetails, + clearQueueDetails +}; + +class MissingConnector extends Component { + + // + // Lifecycle + + componentDidMount() { + registerPagePopulator(this.repopulate); + this.props.gotoMissingFirstPage(); + } + + componentDidUpdate(prevProps) { + if (hasDifferentItems(prevProps.items, this.props.items)) { + const albumIds = selectUniqueIds(this.props.items, 'id'); + this.props.fetchQueueDetails({ albumIds }); + } + } + + componentWillUnmount() { + unregisterPagePopulator(this.repopulate); + this.props.clearMissing(); + this.props.clearQueueDetails(); + } + + // + // Control + + repopulate = () => { + this.props.fetchMissing(); + } + + // + // Listeners + + onFirstPagePress = () => { + this.props.gotoMissingFirstPage(); + } + + onPreviousPagePress = () => { + this.props.gotoMissingPreviousPage(); + } + + onNextPagePress = () => { + this.props.gotoMissingNextPage(); + } + + onLastPagePress = () => { + this.props.gotoMissingLastPage(); + } + + onPageSelect = (page) => { + this.props.gotoMissingPage({ page }); + } + + onSortPress = (sortKey) => { + this.props.setMissingSort({ sortKey }); + } + + onFilterSelect = (filterKey, filterValue) => { + this.props.setMissingFilter({ filterKey, filterValue }); + } + + onTableOptionChange = (payload) => { + this.props.setMissingTableOption(payload); + + if (payload.pageSize) { + this.props.gotoMissingFirstPage(); + } + } + + onSearchSelectedPress = (selected) => { + this.props.executeCommand({ + name: commandNames.ALBUM_SEARCH, + albumIds: selected + }); + } + + onToggleSelectedPress = (selected) => { + const { + filterKey, + filterValue + } = this.props; + + this.props.batchToggleMissingAlbums({ + albumIds: selected, + monitored: filterKey !== 'monitored' || !filterValue + }); + } + + onSearchAllMissingPress = () => { + this.props.executeCommand({ + name: commandNames.MISSING_ALBUM_SEARCH + }); + } + + // + // Render + + render() { + return ( + + ); + } +} + +MissingConnector.propTypes = { + items: PropTypes.arrayOf(PropTypes.object).isRequired, + filterKey: PropTypes.string.isRequired, + filterValue: PropTypes.oneOfType([PropTypes.bool, PropTypes.number, PropTypes.string]), + fetchMissing: PropTypes.func.isRequired, + gotoMissingFirstPage: PropTypes.func.isRequired, + gotoMissingPreviousPage: PropTypes.func.isRequired, + gotoMissingNextPage: PropTypes.func.isRequired, + gotoMissingLastPage: PropTypes.func.isRequired, + gotoMissingPage: PropTypes.func.isRequired, + setMissingSort: PropTypes.func.isRequired, + setMissingFilter: PropTypes.func.isRequired, + setMissingTableOption: PropTypes.func.isRequired, + clearMissing: PropTypes.func.isRequired, + batchToggleMissingAlbums: PropTypes.func.isRequired, + executeCommand: PropTypes.func.isRequired, + fetchQueueDetails: PropTypes.func.isRequired, + clearQueueDetails: PropTypes.func.isRequired +}; + +export default connect(createMapStateToProps, mapDispatchToProps)(MissingConnector); diff --git a/frontend/src/Wanted/Missing/MissingRow.css b/frontend/src/Wanted/Missing/MissingRow.css new file mode 100644 index 000000000..3ec895d66 --- /dev/null +++ b/frontend/src/Wanted/Missing/MissingRow.css @@ -0,0 +1,6 @@ +.episode, +.status { + composes: cell from 'Components/Table/Cells/TableRowCell.css'; + + width: 100px; +} diff --git a/frontend/src/Wanted/Missing/MissingRow.js b/frontend/src/Wanted/Missing/MissingRow.js new file mode 100644 index 000000000..41c217482 --- /dev/null +++ b/frontend/src/Wanted/Missing/MissingRow.js @@ -0,0 +1,144 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import episodeEntities from 'Album/episodeEntities'; +import EpisodeTitleLink from 'Album/EpisodeTitleLink'; +import EpisodeStatusConnector from 'Album/EpisodeStatusConnector'; +import SeasonEpisodeNumber from 'Album/SeasonEpisodeNumber'; +import EpisodeSearchCellConnector from 'Album/EpisodeSearchCellConnector'; +import ArtistNameLink from 'Artist/ArtistNameLink'; +import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector'; +import TableRow from 'Components/Table/TableRow'; +import TableRowCell from 'Components/Table/Cells/TableRowCell'; +import TableSelectCell from 'Components/Table/Cells/TableSelectCell'; +import styles from './MissingRow.css'; + +function MissingRow(props) { + const { + id, + // trackFileId, + artist, + releaseDate, + title, + isSelected, + columns, + onSelectedChange + } = props; + + return ( + + + + { + columns.map((column) => { + const { + name, + isVisible + } = column; + + if (!isVisible) { + return null; + } + + if (name === 'artist.sortName') { + return ( + + + + ); + } + + // if (name === 'episode') { + // return ( + // + // + // + // ); + // } + + if (name === 'albumTitle') { + return ( + + + + ); + } + + if (name === 'releaseDate') { + return ( + + ); + } + + // if (name === 'status') { + // return ( + // + // + // + // ); + // } + + if (name === 'actions') { + return ( + + ); + } + + return null; + }) + } + + ); +} + +MissingRow.propTypes = { + id: PropTypes.number.isRequired, + // trackFileId: PropTypes.number, + artist: PropTypes.object.isRequired, + releaseDate: PropTypes.string.isRequired, + title: PropTypes.string.isRequired, + isSelected: PropTypes.bool, + columns: PropTypes.arrayOf(PropTypes.object).isRequired, + onSelectedChange: PropTypes.func.isRequired +}; + +export default MissingRow; diff --git a/frontend/src/Wanted/Missing/MissingRowConnector.js b/frontend/src/Wanted/Missing/MissingRowConnector.js new file mode 100644 index 000000000..f0a30d9cd --- /dev/null +++ b/frontend/src/Wanted/Missing/MissingRowConnector.js @@ -0,0 +1,17 @@ +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import createArtistSelector from 'Store/Selectors/createArtistSelector'; +import MissingRow from './MissingRow'; + +function createMapStateToProps() { + return createSelector( + createArtistSelector(), + (artist) => { + return { + artist + }; + } + ); +} + +export default connect(createMapStateToProps)(MissingRow); diff --git a/frontend/src/index.css b/frontend/src/index.css new file mode 100644 index 000000000..04e0e11ef --- /dev/null +++ b/frontend/src/index.css @@ -0,0 +1,15 @@ +html, +body { + height: 100%; /* needed for proper layout */ +} + +body { + overflow: hidden; + background-color: #f5f7fa; +} + +@media only screen and (max-width: $breakpointSmall) { + body { + overflow-y: auto; + } +} diff --git a/frontend/src/index.html b/frontend/src/index.html new file mode 100644 index 000000000..fd93b8500 --- /dev/null +++ b/frontend/src/index.html @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Lidarr (Preview) + + + + + + + +
+ + + + + + + + + diff --git a/frontend/src/index.js b/frontend/src/index.js new file mode 100644 index 000000000..396a7971c --- /dev/null +++ b/frontend/src/index.js @@ -0,0 +1,18 @@ +import React from 'react'; +import { render } from 'react-dom'; +import createHistory from 'history/createBrowserHistory'; +import createAppStore from 'Store/createAppStore'; +import App from './App/App'; +import 'Styles/globals.css'; +import './index.css'; + +const history = createHistory(); +const store = createAppStore(history); + +render( + , + document.getElementById('root') +); diff --git a/frontend/src/jQuery/jquery.ajax.js b/frontend/src/jQuery/jquery.ajax.js new file mode 100644 index 000000000..9b217801e --- /dev/null +++ b/frontend/src/jQuery/jquery.ajax.js @@ -0,0 +1,47 @@ +import $ from 'jquery'; + +const absUrlRegex = /^(https?:)?\/\//i; +const apiRoot = window.Sonarr.apiRoot; +const urlBase = window.Sonarr.urlBase; + +function isRelative(xhr) { + return !absUrlRegex.test(xhr.url); +} + +function moveBodyToQuery(xhr) { + if (xhr.data && xhr.type === 'DELETE') { + if (xhr.url.contains('?')) { + xhr.url += '&'; + } else { + xhr.url += '?'; + } + xhr.url += $.param(xhr.data); + delete xhr.data; + } +} + +function addRootUrl(xhr) { + const url = xhr.url; + if (url.startsWith('/signalr')) { + xhr.url = urlBase + xhr.url; + } else { + xhr.url = apiRoot + xhr.url; + } +} + +function addApiKey(xhr) { + xhr.headers = xhr.headers || {}; + xhr.headers['X-Api-Key'] = window.Sonarr.apiKey; +} + +export default function() { + const originalAjax = $.ajax; + $.ajax = function(xhr) { + if (xhr && isRelative(xhr)) { + moveBodyToQuery(xhr); + addRootUrl(xhr); + addApiKey(xhr); + } + return originalAjax.apply(this, arguments); + }; +} diff --git a/frontend/src/login.html b/frontend/src/login.html new file mode 100644 index 000000000..ca88b5141 --- /dev/null +++ b/frontend/src/login.html @@ -0,0 +1,220 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Login - Lidarr + + + + + +
+
+
+
+ +
+ +
+ + +
+
+ +
+ +
+ +
+ +
+ + + + + + Forgot your password? +
+ + + +
+
+
+ +
+ © + 2010-2017 + - + Lidarr +
+
+
+ + + + diff --git a/src/UI/oauth.html b/frontend/src/oauth.html similarity index 84% rename from src/UI/oauth.html rename to frontend/src/oauth.html index fe6ddf864..16a34dbf3 100644 --- a/src/UI/oauth.html +++ b/frontend/src/oauth.html @@ -2,7 +2,7 @@ - oauth landing page + OAuth landing page @@ -10,4 +10,4 @@ Shouldn't see this - \ No newline at end of file + diff --git a/frontend/src/polyfills.js b/frontend/src/polyfills.js new file mode 100644 index 000000000..b5d17d598 --- /dev/null +++ b/frontend/src/polyfills.js @@ -0,0 +1,41 @@ +/* eslint no-empty-function: 0 no-extend-native: 0 */ + +window.console = window.console || {}; +window.console.log = window.console.log || function() {}; +window.console.group = window.console.group || function() {}; +window.console.groupEnd = window.console.groupEnd || function() {}; +window.console.debug = window.console.debug || function() {}; +window.console.warn = window.console.warn || function() {}; +window.console.assert = window.console.assert || function() {}; + +if (!String.prototype.startsWith) { + Object.defineProperty(String.prototype, 'startsWith', { + enumerable: false, + configurable: false, + writable: false, + value(searchString, position) { + position = position || 0; + return this.indexOf(searchString, position) === position; + } + }); +} + +if (!String.prototype.endsWith) { + Object.defineProperty(String.prototype, 'endsWith', { + enumerable: false, + configurable: false, + writable: false, + value(searchString, position) { + position = position || this.length; + position = position - searchString.length; + const lastIndex = this.lastIndexOf(searchString); + return lastIndex !== -1 && lastIndex === position; + } + }); +} + +if (!('contains' in String.prototype)) { + String.prototype.contains = function(str, startIndex) { + return String.prototype.indexOf.call(this, str, startIndex) !== -1; + }; +} diff --git a/frontend/src/preload.js b/frontend/src/preload.js new file mode 100644 index 000000000..674699db9 --- /dev/null +++ b/frontend/src/preload.js @@ -0,0 +1,4 @@ +/* eslint no-undef: 0 */ +import 'Shims/jquery'; + +__webpack_public_path__ = `${window.Sonarr.urlBase}/`; diff --git a/frontend/src/vendor.js b/frontend/src/vendor.js new file mode 100644 index 000000000..2b08817be --- /dev/null +++ b/frontend/src/vendor.js @@ -0,0 +1,5 @@ +/* Base */ +// require('jquery'); +require('lodash'); +require('moment'); +// require('signalR'); diff --git a/gulp/build.js b/gulp/build.js deleted file mode 100644 index 23f457baf..000000000 --- a/gulp/build.js +++ /dev/null @@ -1,18 +0,0 @@ -var gulp = require('gulp'); -var runSequence = require('run-sequence'); - -require('./clean'); -require('./less'); -require('./handlebars'); -require('./copy'); - -gulp.task('build', function() { - return runSequence('clean', [ - 'webpack', - 'less', - 'handlebars', - 'copyHtml', - 'copyContent', - 'copyJs' - ]); -}); diff --git a/gulp/clean.js b/gulp/clean.js deleted file mode 100644 index 58f9c223f..000000000 --- a/gulp/clean.js +++ /dev/null @@ -1,8 +0,0 @@ -var gulp = require('gulp'); -var del = require('del'); - -var paths = require('./paths'); - -gulp.task('clean', function(cb) { - del([paths.dest.root], cb); -}); diff --git a/gulp/copy.js b/gulp/copy.js deleted file mode 100644 index 9962defef..000000000 --- a/gulp/copy.js +++ /dev/null @@ -1,31 +0,0 @@ -var gulp = require('gulp'); -var print = require('gulp-print'); -var cache = require('gulp-cached'); -var livereload = require('gulp-livereload'); - -var paths = require('./paths.js'); - -gulp.task('copyJs', function () { - return gulp.src( - [ - paths.src.root + 'polyfills.js', - paths.src.root + 'JsLibraries/handlebars.runtime.js' - ]) - .pipe(cache('copyJs')) - .pipe(print()) - .pipe(gulp.dest(paths.dest.root)) - .pipe(livereload()); -}); - -gulp.task('copyHtml', function () { - return gulp.src(paths.src.html) - .pipe(cache('copyHtml')) - .pipe(gulp.dest(paths.dest.root)) - .pipe(livereload()); -}); - -gulp.task('copyContent', function () { - return gulp.src([paths.src.content + '**/*.*', '!**/*.less', '!**/*.css']) - .pipe(gulp.dest(paths.dest.content)) - .pipe(livereload()); -}); diff --git a/gulp/errorHandler.js b/gulp/errorHandler.js deleted file mode 100644 index db24e1a66..000000000 --- a/gulp/errorHandler.js +++ /dev/null @@ -1,7 +0,0 @@ -module.exports = { - onError : function(error) { - //If you want details of the error in the console - console.log(error.toString()); - this.emit('end'); - } -}; \ No newline at end of file diff --git a/gulp/gulpFile.js b/gulp/gulpFile.js deleted file mode 100644 index 428fad285..000000000 --- a/gulp/gulpFile.js +++ /dev/null @@ -1,11 +0,0 @@ -require('./watch.js'); -require('./build.js'); -require('./clean.js'); -require('./jshint.js'); -require('./handlebars.js'); -require('./copy.js'); -require('./less.js'); -require('./stripBom.js'); -require('./imageMin.js'); -require('./webpack.js'); -require('./start.js'); diff --git a/gulp/handlebars.js b/gulp/handlebars.js deleted file mode 100644 index aab62f438..000000000 --- a/gulp/handlebars.js +++ /dev/null @@ -1,55 +0,0 @@ -var gulp = require('gulp'); -var handlebars = require('gulp-handlebars'); -var declare = require('gulp-declare'); -var concat = require('gulp-concat'); -var wrap = require("gulp-wrap"); -var livereload = require('gulp-livereload'); -var path = require('path'); -var streamqueue = require('streamqueue'); -var stripbom = require('gulp-stripbom'); - -var paths = require('./paths.js'); - -gulp.task('handlebars', function() { - - var coreStream = gulp.src([ - paths.src.templates, - '!*/**/*Partial.*' - ]) - .pipe(stripbom({ showLog : false })) - .pipe(handlebars()) - .pipe(declare({ - namespace : 'T', - noRedeclare : true, - processName : function(filePath) { - - filePath = path.relative(paths.src.root, filePath); - - return filePath.replace(/\\/g, '/') - .toLocaleLowerCase() - .replace('template', '') - .replace('.js', ''); - } - })); - - var partialStream = gulp.src([paths.src.partials]) - .pipe(stripbom({ showLog : false })) - .pipe(handlebars()) - .pipe(wrap('Handlebars.template(<%= contents %>)')) - .pipe(wrap('Handlebars.registerPartial(<%= processPartialName(file.relative) %>, <%= contents %>)', {}, { - imports : { - processPartialName : function(fileName) { - return JSON.stringify( - path.basename(fileName, '.js') - ); - } - } - })); - - return streamqueue({ objectMode : true }, - partialStream, - coreStream - ).pipe(concat('templates.js')) - .pipe(gulp.dest(paths.dest.root)) - .pipe(livereload()); -}); diff --git a/gulp/imageMin.js b/gulp/imageMin.js deleted file mode 100644 index 6c8236e03..000000000 --- a/gulp/imageMin.js +++ /dev/null @@ -1,15 +0,0 @@ -var gulp = require('gulp'); -var print = require('gulp-print'); -var paths = require('./paths.js'); - -gulp.task('imageMin', function() { - 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/')); -}); \ No newline at end of file diff --git a/gulp/jshint.js b/gulp/jshint.js deleted file mode 100644 index 650ad02c2..000000000 --- a/gulp/jshint.js +++ /dev/null @@ -1,15 +0,0 @@ -var gulp = require('gulp'); -var jshint = require('gulp-jshint'); -var stylish = require('jshint-stylish'); -var cache = require('gulp-cached'); -var paths = require('./paths.js'); - -gulp.task('jshint', function() { - return gulp.src([ - paths.src.scripts, - paths.src.exclude.libs - ]) - .pipe(cache('jshint')) - .pipe(jshint()) - .pipe(jshint.reporter(stylish)); -}); diff --git a/gulp/less.js b/gulp/less.js deleted file mode 100644 index c91ebd538..000000000 --- a/gulp/less.js +++ /dev/null @@ -1,50 +0,0 @@ -var gulp = require('gulp'); - -var less = require('gulp-less'); -var postcss = require('gulp-postcss'); -var sourcemaps = require('gulp-sourcemaps'); -var autoprefixer = require('autoprefixer-core'); -var livereload = require('gulp-livereload'); - -var print = require('gulp-print'); -var paths = require('./paths'); -var errorHandler = require('./errorHandler'); - -gulp.task('less', function() { - - var src = [ - paths.src.content + 'bootstrap.less', - paths.src.content + 'theme.less', - paths.src.content + 'overrides.less', - paths.src.content + 'bootstrap.toggle-switch.css', - paths.src.content + 'fullcalendar.css', - paths.src.content + 'Messenger/messenger.css', - paths.src.content + 'Messenger/messenger.flat.css', - paths.src.root + 'Artist/artist.less', - paths.src.root + 'Activity/activity.less', - paths.src.root + 'AddArtist/addArtist.less', - paths.src.root + 'Calendar/calendar.less', - paths.src.root + 'Cells/cells.less', - paths.src.root + 'ManualImport/manualimport.less', - paths.src.root + 'Settings/settings.less', - paths.src.root + 'System/Logs/logs.less', - paths.src.root + 'System/Update/update.less', - paths.src.root + 'System/Info/info.less' - ]; - - return gulp.src(src) - .pipe(print()) - .pipe(sourcemaps.init()) - .pipe(less({ - dumpLineNumbers : 'false', - compress : true, - yuicompress : true, - ieCompat : true, - strictImports : true - })) - .pipe(postcss([ autoprefixer({ browsers: ['last 2 versions'] }) ])) - .on('error', errorHandler.onError) - .pipe(sourcemaps.write(paths.dest.content)) - .pipe(gulp.dest(paths.dest.content)) - .pipe(livereload()); -}); diff --git a/gulp/paths.js b/gulp/paths.js deleted file mode 100644 index e05aa1d2b..000000000 --- a/gulp/paths.js +++ /dev/null @@ -1,21 +0,0 @@ -var paths = { - src : { - root : './src/UI/', - templates : './src/UI/**/*.hbs', - html : './src/UI/*.html', - partials : './src/UI/**/*Partial.hbs', - scripts : './src/UI/**/*.js', - less : ['./src/UI/**/*.less'], - content : './src/UI/Content/', - images : './src/UI/Content/Images/**/*', - exclude : { - libs : '!./src/UI/JsLibraries/**' - } - }, - dest : { - root : './_output/UI/', - content : './_output/UI/Content/' - } -}; - -module.exports = paths; diff --git a/gulp/start.js b/gulp/start.js deleted file mode 100644 index 296eb6836..000000000 --- a/gulp/start.js +++ /dev/null @@ -1,112 +0,0 @@ -// will download and run Lidarr (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 print = require('gulp-print'); -var spawn = require('child_process').spawn; - -function download(url, dest, cb) { - console.log('Downloading ' + url + ' to ' + dest); - var file = fs.createWriteStream(dest); - var request = 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.lidarr.audio/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 () { - - //gulp.src('/Users/kayone/git/Sonarr/_start/2.0.0.3288/NzbDrone/*.*') - // .pipe(print()) - // .pipe(gulp.dest('./_output - - //return; - try { - fs.mkdirSync('./_start/'); - } catch (e) { - if (e.code != 'EEXIST') { - throw e; - } - } - - getLatest(function (package) { - var packagePath = "./_start/" + package.filename; - var dirName = "./_start/" + package.version; - download(package.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); - }); -}); diff --git a/gulp/stripBom.js b/gulp/stripBom.js deleted file mode 100644 index 085e6b753..000000000 --- a/gulp/stripBom.js +++ /dev/null @@ -1,21 +0,0 @@ -var gulp = require('gulp'); -var paths = require('./paths.js'); -var stripbom = require('gulp-stripbom'); - -var stripBom = function (dest) { - gulp.src([paths.src.scripts, paths.src.exclude.libs]) - .pipe(stripbom({ showLog: false })) - .pipe(gulp.dest(dest)); - - gulp.src(paths.src.less) - .pipe(stripbom({ showLog: false })) - .pipe(gulp.dest(dest)); - - gulp.src(paths.src.templates) - .pipe(stripbom({ showLog: false })) - .pipe(gulp.dest(dest)); -}; - -gulp.task('stripBom', function () { - stripBom(paths.src.root); -}); diff --git a/gulp/watch.js b/gulp/watch.js deleted file mode 100644 index f9145a464..000000000 --- a/gulp/watch.js +++ /dev/null @@ -1,20 +0,0 @@ -var gulp = require('gulp'); -var livereload = require('gulp-livereload'); - -var paths = require('./paths.js'); - -require('./jshint.js'); -require('./handlebars.js'); -require('./less.js'); -require('./copy.js'); -require('./webpack.js'); - -gulp.task('watch', ['jshint', 'handlebars', 'less', 'copyHtml', 'copyContent', 'copyJs'], function () { - livereload.listen(); - gulp.start('webpackWatch'); - gulp.watch([paths.src.scripts, paths.src.exclude.libs], ['jshint', 'copyJs']); - gulp.watch(paths.src.templates, ['handlebars']); - gulp.watch([paths.src.less, paths.src.exclude.libs], ['less']); - gulp.watch([paths.src.html], ['copyHtml']); - gulp.watch([paths.src.content + '**/*.*', '!**/*.less'], ['copyContent']); -}); \ No newline at end of file diff --git a/gulp/webpack.js b/gulp/webpack.js deleted file mode 100644 index 64570593c..000000000 --- a/gulp/webpack.js +++ /dev/null @@ -1,13 +0,0 @@ -var gulp = require('gulp'); -var webpackStream = require('webpack-stream'); -var livereload = require('gulp-livereload'); -var webpackConfig = require('../webpack.config'); - -gulp.task('webpack', function() { - return gulp.src('main.js').pipe(webpackStream(webpackConfig)).pipe(gulp.dest('')); -}); - -gulp.task('webpackWatch', function() { - webpackConfig.watch = true; - return gulp.src('main.js').pipe(webpackStream(webpackConfig)).pipe(gulp.dest('')).pipe(livereload()); -}); diff --git a/gulpFile.js b/gulpFile.js index 28dc9b0f1..73636a918 100644 --- a/gulpFile.js +++ b/gulpFile.js @@ -1 +1 @@ -require('./gulp/gulpFile.js'); +require('./frontend/gulp/gulpFile.js'); diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 000000000..3e102a488 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,10437 @@ +{ + "name": "lidarr", + "version": "1.0.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@gulp-sourcemaps/identity-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@gulp-sourcemaps/identity-map/-/identity-map-1.0.1.tgz", + "integrity": "sha1-z6I7xYQPkQTOMqZedNt+epdLvuE=", + "requires": { + "acorn": "5.1.2", + "css": "2.2.1", + "normalize-path": "2.1.1", + "source-map": "0.5.7", + "through2": "2.0.3" + }, + "dependencies": { + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" + } + } + }, + "@gulp-sourcemaps/map-sources": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@gulp-sourcemaps/map-sources/-/map-sources-1.0.0.tgz", + "integrity": "sha1-iQrnxdjId/bThIYCFazp1+yUW9o=", + "requires": { + "normalize-path": "2.1.1", + "through2": "2.0.3" + } + }, + "acorn": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.1.2.tgz", + "integrity": "sha512-o96FZLJBPY1lvTuJylGA9Bk3t/GKPPJG8H0ydQQl01crzwJgspa4AEIq/pVTXigmK0PHVQhiAtn8WMBLL9D2WA==" + }, + "acorn-dynamic-import": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/acorn-dynamic-import/-/acorn-dynamic-import-2.0.2.tgz", + "integrity": "sha1-x1K9IQvvZ5UBtsbLf8hPj0cVjMQ=", + "requires": { + "acorn": "4.0.13" + }, + "dependencies": { + "acorn": { + "version": "4.0.13", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-4.0.13.tgz", + "integrity": "sha1-EFSVrlNh1pe9GVyCUZLhrX8lN4c=" + } + } + }, + "acorn-jsx": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-3.0.1.tgz", + "integrity": "sha1-r9+UiPsezvyDSPb7IvRk4ypYs2s=", + "requires": { + "acorn": "3.3.0" + }, + "dependencies": { + "acorn": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-3.3.0.tgz", + "integrity": "sha1-ReN/s56No/JbruP/U2niu18iAXo=" + } + } + }, + "acorn-to-esprima": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/acorn-to-esprima/-/acorn-to-esprima-2.0.8.tgz", + "integrity": "sha1-AD8MZC65ITL0F9NwjxStqCrfLrE=" + }, + "add-px-to-style": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/add-px-to-style/-/add-px-to-style-1.0.0.tgz", + "integrity": "sha1-0ME1RB+oAUqBN5BFMQlvZ/KPJjo=" + }, + "ajv": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.2.3.tgz", + "integrity": "sha1-wG9Zh3jETGsWGrr+NGa4GtGBTtI=", + "requires": { + "co": "4.6.0", + "fast-deep-equal": "1.0.0", + "json-schema-traverse": "0.3.1", + "json-stable-stringify": "1.0.1" + } + }, + "ajv-keywords": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-2.1.0.tgz", + "integrity": "sha1-opbhf3v658HOT34N5T0pyzIWLfA=" + }, + "align-text": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/align-text/-/align-text-0.1.4.tgz", + "integrity": "sha1-DNkKVhCT810KmSVsIrcGlDP60Rc=", + "requires": { + "kind-of": "3.2.2", + "longest": "1.0.1", + "repeat-string": "1.6.1" + } + }, + "alphanum-sort": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/alphanum-sort/-/alphanum-sort-1.0.2.tgz", + "integrity": "sha1-l6ERlkmyEa0zaR2fn0hqjsn74KM=" + }, + "amdefine": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz", + "integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=" + }, + "ansi-escapes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.0.0.tgz", + "integrity": "sha512-O/klc27mWNUigtv0F8NJWbLF00OcegQalkqKURWdosW08YZKi4m6CnSUSvIZG1otNJbTWhN01Hhz389DW7mvDQ==" + }, + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" + }, + "ansi-styles": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.0.tgz", + "integrity": "sha512-NnSOmMEYtVR2JVMIGTzynRkkaxtiq1xnFBcdQD/DnNCYPoEPsVJhM98BDyaoNOQIi7p4okdi3E27eN7GQbsUug==", + "requires": { + "color-convert": "1.9.0" + } + }, + "anymatch": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-1.3.2.tgz", + "integrity": "sha512-0XNayC8lTHQ2OI8aljNCN3sSx6hsr/1+rlcDAotXJR7C1oZZHCNsfpbKwMjRA3Uqb5tF1Rae2oloTr4xpq+WjA==", + "requires": { + "micromatch": "2.3.11", + "normalize-path": "2.1.1" + } + }, + "archy": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/archy/-/archy-1.0.0.tgz", + "integrity": "sha1-+cjBN1fMHde8N5rHeyxipcKGjEA=" + }, + "argparse": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.9.tgz", + "integrity": "sha1-c9g7wmP4bpf4zE9rrhsOkKfSLIY=", + "requires": { + "sprintf-js": "1.0.3" + } + }, + "arr-diff": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-2.0.0.tgz", + "integrity": "sha1-jzuCf5Vai9ZpaX5KQlasPOrjVs8=", + "requires": { + "arr-flatten": "1.1.0" + } + }, + "arr-flatten": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz", + "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==" + }, + "array-differ": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/array-differ/-/array-differ-1.0.0.tgz", + "integrity": "sha1-7/UuN1gknTO+QCuLuOVkuytdQDE=" + }, + "array-each": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/array-each/-/array-each-1.0.1.tgz", + "integrity": "sha1-p5SvDAWrF1KEbudTofIRoFugxE8=" + }, + "array-find-index": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-find-index/-/array-find-index-1.0.2.tgz", + "integrity": "sha1-3wEKoSh+Fku9pvlyOwqWoexBh6E=" + }, + "array-includes": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.0.3.tgz", + "integrity": "sha1-GEtI9i2S10UrsxsyMWXH+L0CJm0=", + "requires": { + "define-properties": "1.1.2", + "es-abstract": "1.9.0" + } + }, + "array-slice": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/array-slice/-/array-slice-1.0.0.tgz", + "integrity": "sha1-5zA08A3MH0CHYAj9IP6ud71LfC8=" + }, + "array-union": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz", + "integrity": "sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk=", + "requires": { + "array-uniq": "1.0.3" + } + }, + "array-uniq": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz", + "integrity": "sha1-r2rId6Jcx/dOBYiUdThY39sk/bY=" + }, + "array-unique": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.2.1.tgz", + "integrity": "sha1-odl8yvy8JiXMcPrc6zalDFiwGlM=" + }, + "arrify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", + "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=" + }, + "asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=" + }, + "asn1.js": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-4.9.1.tgz", + "integrity": "sha1-SLokC0WpKA6UdImQull9IWYX/UA=", + "requires": { + "bn.js": "4.11.8", + "inherits": "2.0.3", + "minimalistic-assert": "1.0.0" + } + }, + "assert": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/assert/-/assert-1.4.1.tgz", + "integrity": "sha1-mZEtWRg2tab1s0XA8H7vwI/GXZE=", + "requires": { + "util": "0.10.3" + } + }, + "async": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/async/-/async-2.5.0.tgz", + "integrity": "sha512-e+lJAJeNWuPCNyxZKOBdaJGyLGHugXVQtrAwtuAe2vhxTYxFTKE73p8JuTmdH0qdQZtDvI4dhJwjZc5zsfIsYw==", + "requires": { + "lodash": "4.17.4" + } + }, + "async-each": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.1.tgz", + "integrity": "sha1-GdOGodntxufByF04iu28xW0zYC0=" + }, + "atob": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/atob/-/atob-1.1.3.tgz", + "integrity": "sha1-lfE2KbEsOlGl0hWr3OKqnzL4B3M=" + }, + "autoprefixer": { + "version": "7.1.5", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-7.1.5.tgz", + "integrity": "sha512-sMN453qIm8Z+tunzYWW+Y490wWkICHhCYm/VohLjjl+N7ARSFuF5au7E6tr7oEbeeXj8mNjpSw2kxjJaO6YCOw==", + "requires": { + "browserslist": "2.5.1", + "caniuse-lite": "1.0.30000748", + "normalize-range": "0.1.2", + "num2fraction": "1.2.2", + "postcss": "6.0.13", + "postcss-value-parser": "3.3.0" + } + }, + "babel-code-frame": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz", + "integrity": "sha1-Y/1D99weO7fONZR9uP42mj9Yx0s=", + "requires": { + "chalk": "1.1.3", + "esutils": "2.0.2", + "js-tokens": "3.0.2" + }, + "dependencies": { + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=" + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "requires": { + "ansi-styles": "2.2.1", + "escape-string-regexp": "1.0.5", + "has-ansi": "2.0.0", + "strip-ansi": "3.0.1", + "supports-color": "2.0.0" + } + }, + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" + } + } + }, + "babel-core": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-core/-/babel-core-6.26.0.tgz", + "integrity": "sha1-rzL3izGm/O8RnIew/Y2XU/A6C7g=", + "requires": { + "babel-code-frame": "6.26.0", + "babel-generator": "6.26.0", + "babel-helpers": "6.24.1", + "babel-messages": "6.23.0", + "babel-register": "6.26.0", + "babel-runtime": "6.26.0", + "babel-template": "6.26.0", + "babel-traverse": "6.26.0", + "babel-types": "6.26.0", + "babylon": "6.18.0", + "convert-source-map": "1.5.0", + "debug": "2.6.9", + "json5": "0.5.1", + "lodash": "4.17.4", + "minimatch": "3.0.4", + "path-is-absolute": "1.0.1", + "private": "0.1.8", + "slash": "1.0.0", + "source-map": "0.5.7" + }, + "dependencies": { + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" + } + } + }, + "babel-eslint": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/babel-eslint/-/babel-eslint-8.0.1.tgz", + "integrity": "sha512-h3moF6PCTQE06UjMMG+ydZSBvZ4Q7rqPE/5WAUOvUyHYUTqxm8JVhjZRiG1avI/tGVOK4BnZLDQapyLzh8DeKg==", + "requires": { + "babel-code-frame": "7.0.0-beta.0", + "babel-traverse": "7.0.0-beta.0", + "babel-types": "7.0.0-beta.0", + "babylon": "7.0.0-beta.22" + }, + "dependencies": { + "babel-code-frame": { + "version": "7.0.0-beta.0", + "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-7.0.0-beta.0.tgz", + "integrity": "sha512-/xr1ADm5bnTjjN+xwoXb7lF4v2rnxMzNZzFU7h8SxB+qB6+IqSTOOqVcpaPTUC2Non/MbQxS3OIZnJpQ2X21aQ==", + "requires": { + "chalk": "2.2.0", + "esutils": "2.0.2", + "js-tokens": "3.0.2" + } + }, + "babel-messages": { + "version": "7.0.0-beta.0", + "resolved": "https://registry.npmjs.org/babel-messages/-/babel-messages-7.0.0-beta.0.tgz", + "integrity": "sha512-eXdShsm9ZTh9AQhlIaAn6HR3xWpxCnK9ZwIDA9QyjnwTgMctGxHHflw4b4RJ3/ZjTL0Vrmvm0tQXPkp49mTAUw==" + }, + "babel-traverse": { + "version": "7.0.0-beta.0", + "resolved": "https://registry.npmjs.org/babel-traverse/-/babel-traverse-7.0.0-beta.0.tgz", + "integrity": "sha512-IKzuTqUcQtMRZ0Vv5RjIrGGj33eBKmNTNeRexWSyjPPuAciyNkva1rt7WXPfHfkb+dX7coRAIUhzeTUEzhnwdA==", + "requires": { + "babel-code-frame": "7.0.0-beta.0", + "babel-helper-function-name": "7.0.0-beta.0", + "babel-messages": "7.0.0-beta.0", + "babel-types": "7.0.0-beta.0", + "babylon": "7.0.0-beta.22", + "debug": "3.1.0", + "globals": "10.1.0", + "invariant": "2.2.2", + "lodash": "4.17.4" + } + }, + "babel-types": { + "version": "7.0.0-beta.0", + "resolved": "https://registry.npmjs.org/babel-types/-/babel-types-7.0.0-beta.0.tgz", + "integrity": "sha512-rJc2kV9iPJGLlqIY71AM3nPcdkoeLRCDuR07GFgfd3lFl4TsBQq76TxYQQIZ2MONg1HpsqmuoCXr9aZ1Oa4wYw==", + "requires": { + "esutils": "2.0.2", + "lodash": "4.17.4", + "to-fast-properties": "2.0.0" + } + }, + "babylon": { + "version": "7.0.0-beta.22", + "resolved": "https://registry.npmjs.org/babylon/-/babylon-7.0.0-beta.22.tgz", + "integrity": "sha512-Yl7iT8QGrS8OfR7p6R12AJexQm+brKwrryai4VWZ7NHUbPoZ5al3+klhvl/14shXZiLa7uK//OIFuZ1/RKHgoA==" + }, + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "requires": { + "ms": "2.0.0" + } + }, + "globals": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-10.1.0.tgz", + "integrity": "sha1-RCWhiBvg0za0qCOoKnvnJdXdmHw=" + }, + "to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=" + } + } + }, + "babel-generator": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-generator/-/babel-generator-6.26.0.tgz", + "integrity": "sha1-rBriAHC3n248odMmlhMFN3TyDcU=", + "requires": { + "babel-messages": "6.23.0", + "babel-runtime": "6.26.0", + "babel-types": "6.26.0", + "detect-indent": "4.0.0", + "jsesc": "1.3.0", + "lodash": "4.17.4", + "source-map": "0.5.7", + "trim-right": "1.0.1" + }, + "dependencies": { + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" + } + } + }, + "babel-helper-bindify-decorators": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-bindify-decorators/-/babel-helper-bindify-decorators-6.24.1.tgz", + "integrity": "sha1-FMGeXxQte0fxmlJDHlKxzLxAozA=", + "requires": { + "babel-runtime": "6.26.0", + "babel-traverse": "6.26.0", + "babel-types": "6.26.0" + } + }, + "babel-helper-builder-binary-assignment-operator-visitor": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-builder-binary-assignment-operator-visitor/-/babel-helper-builder-binary-assignment-operator-visitor-6.24.1.tgz", + "integrity": "sha1-zORReto1b0IgvK6KAsKzRvmlZmQ=", + "requires": { + "babel-helper-explode-assignable-expression": "6.24.1", + "babel-runtime": "6.26.0", + "babel-types": "6.26.0" + } + }, + "babel-helper-builder-react-jsx": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-helper-builder-react-jsx/-/babel-helper-builder-react-jsx-6.26.0.tgz", + "integrity": "sha1-Of+DE7dci2Xc7/HzHTg+D/KkCKA=", + "requires": { + "babel-runtime": "6.26.0", + "babel-types": "6.26.0", + "esutils": "2.0.2" + } + }, + "babel-helper-call-delegate": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-call-delegate/-/babel-helper-call-delegate-6.24.1.tgz", + "integrity": "sha1-7Oaqzdx25Bw0YfiL/Fdb0Nqi340=", + "requires": { + "babel-helper-hoist-variables": "6.24.1", + "babel-runtime": "6.26.0", + "babel-traverse": "6.26.0", + "babel-types": "6.26.0" + } + }, + "babel-helper-define-map": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-helper-define-map/-/babel-helper-define-map-6.26.0.tgz", + "integrity": "sha1-pfVtq0GiX5fstJjH66ypgZ+Vvl8=", + "requires": { + "babel-helper-function-name": "6.24.1", + "babel-runtime": "6.26.0", + "babel-types": "6.26.0", + "lodash": "4.17.4" + }, + "dependencies": { + "babel-helper-function-name": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-function-name/-/babel-helper-function-name-6.24.1.tgz", + "integrity": "sha1-00dbjAPtmCQqJbSDUasYOZ01gKk=", + "requires": { + "babel-helper-get-function-arity": "6.24.1", + "babel-runtime": "6.26.0", + "babel-template": "6.26.0", + "babel-traverse": "6.26.0", + "babel-types": "6.26.0" + } + }, + "babel-helper-get-function-arity": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-get-function-arity/-/babel-helper-get-function-arity-6.24.1.tgz", + "integrity": "sha1-j3eCqpNAfEHTqlCQj4mwMbG2hT0=", + "requires": { + "babel-runtime": "6.26.0", + "babel-types": "6.26.0" + } + } + } + }, + "babel-helper-explode-assignable-expression": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-explode-assignable-expression/-/babel-helper-explode-assignable-expression-6.24.1.tgz", + "integrity": "sha1-8luCz33BBDPFX3BZLVdGQArCLKo=", + "requires": { + "babel-runtime": "6.26.0", + "babel-traverse": "6.26.0", + "babel-types": "6.26.0" + } + }, + "babel-helper-explode-class": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-explode-class/-/babel-helper-explode-class-6.24.1.tgz", + "integrity": "sha1-fcKjkQ3uAHBW4eMdZAztPVTqqes=", + "requires": { + "babel-helper-bindify-decorators": "6.24.1", + "babel-runtime": "6.26.0", + "babel-traverse": "6.26.0", + "babel-types": "6.26.0" + } + }, + "babel-helper-function-name": { + "version": "7.0.0-beta.0", + "resolved": "https://registry.npmjs.org/babel-helper-function-name/-/babel-helper-function-name-7.0.0-beta.0.tgz", + "integrity": "sha512-DaQccFBBWBEzMdqbKmNXamY0m1yLHJGOdbbEsNoGdJrrU7wAF3wwowtDDPzF0ZT3SqJXPgZW/P2kgBX9moMuAA==", + "requires": { + "babel-helper-get-function-arity": "7.0.0-beta.0", + "babel-template": "7.0.0-beta.0", + "babel-traverse": "7.0.0-beta.0", + "babel-types": "7.0.0-beta.0" + }, + "dependencies": { + "babel-code-frame": { + "version": "7.0.0-beta.0", + "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-7.0.0-beta.0.tgz", + "integrity": "sha512-/xr1ADm5bnTjjN+xwoXb7lF4v2rnxMzNZzFU7h8SxB+qB6+IqSTOOqVcpaPTUC2Non/MbQxS3OIZnJpQ2X21aQ==", + "requires": { + "chalk": "2.2.0", + "esutils": "2.0.2", + "js-tokens": "3.0.2" + } + }, + "babel-messages": { + "version": "7.0.0-beta.0", + "resolved": "https://registry.npmjs.org/babel-messages/-/babel-messages-7.0.0-beta.0.tgz", + "integrity": "sha512-eXdShsm9ZTh9AQhlIaAn6HR3xWpxCnK9ZwIDA9QyjnwTgMctGxHHflw4b4RJ3/ZjTL0Vrmvm0tQXPkp49mTAUw==" + }, + "babel-template": { + "version": "7.0.0-beta.0", + "resolved": "https://registry.npmjs.org/babel-template/-/babel-template-7.0.0-beta.0.tgz", + "integrity": "sha512-tmdH+MmmU0F6Ur8humpevSmFzYKbrN3Oru0g5Qyg4R6+sxjnzZmnvzUbsP0aKMr7tB0Ua6xhEb9arKTOsEMkyA==", + "requires": { + "babel-traverse": "7.0.0-beta.0", + "babel-types": "7.0.0-beta.0", + "babylon": "7.0.0-beta.22", + "lodash": "4.17.4" + } + }, + "babel-traverse": { + "version": "7.0.0-beta.0", + "resolved": "https://registry.npmjs.org/babel-traverse/-/babel-traverse-7.0.0-beta.0.tgz", + "integrity": "sha512-IKzuTqUcQtMRZ0Vv5RjIrGGj33eBKmNTNeRexWSyjPPuAciyNkva1rt7WXPfHfkb+dX7coRAIUhzeTUEzhnwdA==", + "requires": { + "babel-code-frame": "7.0.0-beta.0", + "babel-helper-function-name": "7.0.0-beta.0", + "babel-messages": "7.0.0-beta.0", + "babel-types": "7.0.0-beta.0", + "babylon": "7.0.0-beta.22", + "debug": "3.1.0", + "globals": "10.1.0", + "invariant": "2.2.2", + "lodash": "4.17.4" + } + }, + "babel-types": { + "version": "7.0.0-beta.0", + "resolved": "https://registry.npmjs.org/babel-types/-/babel-types-7.0.0-beta.0.tgz", + "integrity": "sha512-rJc2kV9iPJGLlqIY71AM3nPcdkoeLRCDuR07GFgfd3lFl4TsBQq76TxYQQIZ2MONg1HpsqmuoCXr9aZ1Oa4wYw==", + "requires": { + "esutils": "2.0.2", + "lodash": "4.17.4", + "to-fast-properties": "2.0.0" + } + }, + "babylon": { + "version": "7.0.0-beta.22", + "resolved": "https://registry.npmjs.org/babylon/-/babylon-7.0.0-beta.22.tgz", + "integrity": "sha512-Yl7iT8QGrS8OfR7p6R12AJexQm+brKwrryai4VWZ7NHUbPoZ5al3+klhvl/14shXZiLa7uK//OIFuZ1/RKHgoA==" + }, + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "requires": { + "ms": "2.0.0" + } + }, + "globals": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-10.1.0.tgz", + "integrity": "sha1-RCWhiBvg0za0qCOoKnvnJdXdmHw=" + }, + "to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=" + } + } + }, + "babel-helper-get-function-arity": { + "version": "7.0.0-beta.0", + "resolved": "https://registry.npmjs.org/babel-helper-get-function-arity/-/babel-helper-get-function-arity-7.0.0-beta.0.tgz", + "integrity": "sha512-csqAic15/2Vm1951nJxkkL9K8E6ojyNF/eAOjk7pqJlO8kvgrccGNFCV9eDwcGHDPe5AjvJGwVSAcQ5fit9wuA==", + "requires": { + "babel-types": "7.0.0-beta.0" + }, + "dependencies": { + "babel-types": { + "version": "7.0.0-beta.0", + "resolved": "https://registry.npmjs.org/babel-types/-/babel-types-7.0.0-beta.0.tgz", + "integrity": "sha512-rJc2kV9iPJGLlqIY71AM3nPcdkoeLRCDuR07GFgfd3lFl4TsBQq76TxYQQIZ2MONg1HpsqmuoCXr9aZ1Oa4wYw==", + "requires": { + "esutils": "2.0.2", + "lodash": "4.17.4", + "to-fast-properties": "2.0.0" + } + }, + "to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=" + } + } + }, + "babel-helper-hoist-variables": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-hoist-variables/-/babel-helper-hoist-variables-6.24.1.tgz", + "integrity": "sha1-HssnaJydJVE+rbyZFKc/VAi+enY=", + "requires": { + "babel-runtime": "6.26.0", + "babel-types": "6.26.0" + } + }, + "babel-helper-optimise-call-expression": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-optimise-call-expression/-/babel-helper-optimise-call-expression-6.24.1.tgz", + "integrity": "sha1-96E0J7qfc/j0+pk8VKl4gtEkQlc=", + "requires": { + "babel-runtime": "6.26.0", + "babel-types": "6.26.0" + } + }, + "babel-helper-regex": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-helper-regex/-/babel-helper-regex-6.26.0.tgz", + "integrity": "sha1-MlxZ+QL4LyS3T6zu0DY5VPZJXnI=", + "requires": { + "babel-runtime": "6.26.0", + "babel-types": "6.26.0", + "lodash": "4.17.4" + } + }, + "babel-helper-remap-async-to-generator": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-remap-async-to-generator/-/babel-helper-remap-async-to-generator-6.24.1.tgz", + "integrity": "sha1-XsWBgnrXI/7N04HxySg5BnbkVRs=", + "requires": { + "babel-helper-function-name": "6.24.1", + "babel-runtime": "6.26.0", + "babel-template": "6.26.0", + "babel-traverse": "6.26.0", + "babel-types": "6.26.0" + }, + "dependencies": { + "babel-helper-function-name": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-function-name/-/babel-helper-function-name-6.24.1.tgz", + "integrity": "sha1-00dbjAPtmCQqJbSDUasYOZ01gKk=", + "requires": { + "babel-helper-get-function-arity": "6.24.1", + "babel-runtime": "6.26.0", + "babel-template": "6.26.0", + "babel-traverse": "6.26.0", + "babel-types": "6.26.0" + } + }, + "babel-helper-get-function-arity": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-get-function-arity/-/babel-helper-get-function-arity-6.24.1.tgz", + "integrity": "sha1-j3eCqpNAfEHTqlCQj4mwMbG2hT0=", + "requires": { + "babel-runtime": "6.26.0", + "babel-types": "6.26.0" + } + } + } + }, + "babel-helper-replace-supers": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-replace-supers/-/babel-helper-replace-supers-6.24.1.tgz", + "integrity": "sha1-v22/5Dk40XNpohPKiov3S2qQqxo=", + "requires": { + "babel-helper-optimise-call-expression": "6.24.1", + "babel-messages": "6.23.0", + "babel-runtime": "6.26.0", + "babel-template": "6.26.0", + "babel-traverse": "6.26.0", + "babel-types": "6.26.0" + } + }, + "babel-helpers": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helpers/-/babel-helpers-6.24.1.tgz", + "integrity": "sha1-NHHenK7DiOXIUOWX5Yom3fN2ArI=", + "requires": { + "babel-runtime": "6.26.0", + "babel-template": "6.26.0" + } + }, + "babel-loader": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-7.1.2.tgz", + "integrity": "sha512-jRwlFbINAeyDStqK6Dd5YuY0k5YuzQUvlz2ZamuXrXmxav3pNqe9vfJ402+2G+OmlJSXxCOpB6Uz0INM7RQe2A==", + "requires": { + "find-cache-dir": "1.0.0", + "loader-utils": "1.1.0", + "mkdirp": "0.5.1" + } + }, + "babel-messages": { + "version": "6.23.0", + "resolved": "https://registry.npmjs.org/babel-messages/-/babel-messages-6.23.0.tgz", + "integrity": "sha1-8830cDhYA1sqKVHG7F7fbGLyYw4=", + "requires": { + "babel-runtime": "6.26.0" + } + }, + "babel-plugin-check-es2015-constants": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/babel-plugin-check-es2015-constants/-/babel-plugin-check-es2015-constants-6.22.0.tgz", + "integrity": "sha1-NRV7EBQm/S/9PaP3XH0ekYNbv4o=", + "requires": { + "babel-runtime": "6.26.0" + } + }, + "babel-plugin-syntax-async-functions": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-async-functions/-/babel-plugin-syntax-async-functions-6.13.0.tgz", + "integrity": "sha1-ytnK0RkbWtY0vzCuCHI5HgZHvpU=" + }, + "babel-plugin-syntax-async-generators": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-async-generators/-/babel-plugin-syntax-async-generators-6.13.0.tgz", + "integrity": "sha1-a8lj67FuzLrmuStZbrfzXDQqi5o=" + }, + "babel-plugin-syntax-class-properties": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-class-properties/-/babel-plugin-syntax-class-properties-6.13.0.tgz", + "integrity": "sha1-1+sjt5oxf4VDlixQW4J8fWysJ94=" + }, + "babel-plugin-syntax-decorators": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-decorators/-/babel-plugin-syntax-decorators-6.13.0.tgz", + "integrity": "sha1-MSVjtNvePMgGzuPkFszurd0RrAs=" + }, + "babel-plugin-syntax-dynamic-import": { + "version": "6.18.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-dynamic-import/-/babel-plugin-syntax-dynamic-import-6.18.0.tgz", + "integrity": "sha1-jWomIpyDdFqZgqRBBRVyyqF5sdo=" + }, + "babel-plugin-syntax-exponentiation-operator": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-exponentiation-operator/-/babel-plugin-syntax-exponentiation-operator-6.13.0.tgz", + "integrity": "sha1-nufoM3KQ2pUoggGmpX9BcDF4MN4=" + }, + "babel-plugin-syntax-flow": { + "version": "6.18.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-flow/-/babel-plugin-syntax-flow-6.18.0.tgz", + "integrity": "sha1-TDqyCiryaqIM0lmVw5jE63AxDI0=" + }, + "babel-plugin-syntax-jsx": { + "version": "6.18.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-jsx/-/babel-plugin-syntax-jsx-6.18.0.tgz", + "integrity": "sha1-CvMqmm4Tyno/1QaeYtew9Y0NiUY=" + }, + "babel-plugin-syntax-object-rest-spread": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-object-rest-spread/-/babel-plugin-syntax-object-rest-spread-6.13.0.tgz", + "integrity": "sha1-/WU28rzhODb/o6VFjEkDpZe7O/U=" + }, + "babel-plugin-syntax-trailing-function-commas": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-trailing-function-commas/-/babel-plugin-syntax-trailing-function-commas-6.22.0.tgz", + "integrity": "sha1-ugNgk3+NBuQBgKQ/4NVhb/9TLPM=" + }, + "babel-plugin-transform-async-generator-functions": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-async-generator-functions/-/babel-plugin-transform-async-generator-functions-6.24.1.tgz", + "integrity": "sha1-8FiQAUX9PpkHpt3yjaWfIVJYpds=", + "requires": { + "babel-helper-remap-async-to-generator": "6.24.1", + "babel-plugin-syntax-async-generators": "6.13.0", + "babel-runtime": "6.26.0" + } + }, + "babel-plugin-transform-async-to-generator": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-async-to-generator/-/babel-plugin-transform-async-to-generator-6.24.1.tgz", + "integrity": "sha1-ZTbjeK/2yx1VF6wOQOs+n8jQh2E=", + "requires": { + "babel-helper-remap-async-to-generator": "6.24.1", + "babel-plugin-syntax-async-functions": "6.13.0", + "babel-runtime": "6.26.0" + } + }, + "babel-plugin-transform-class-properties": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-class-properties/-/babel-plugin-transform-class-properties-6.24.1.tgz", + "integrity": "sha1-anl2PqYdM9NvN7YRqp3vgagbRqw=", + "requires": { + "babel-helper-function-name": "6.24.1", + "babel-plugin-syntax-class-properties": "6.13.0", + "babel-runtime": "6.26.0", + "babel-template": "6.26.0" + }, + "dependencies": { + "babel-helper-function-name": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-function-name/-/babel-helper-function-name-6.24.1.tgz", + "integrity": "sha1-00dbjAPtmCQqJbSDUasYOZ01gKk=", + "requires": { + "babel-helper-get-function-arity": "6.24.1", + "babel-runtime": "6.26.0", + "babel-template": "6.26.0", + "babel-traverse": "6.26.0", + "babel-types": "6.26.0" + } + }, + "babel-helper-get-function-arity": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-get-function-arity/-/babel-helper-get-function-arity-6.24.1.tgz", + "integrity": "sha1-j3eCqpNAfEHTqlCQj4mwMbG2hT0=", + "requires": { + "babel-runtime": "6.26.0", + "babel-types": "6.26.0" + } + } + } + }, + "babel-plugin-transform-decorators": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-decorators/-/babel-plugin-transform-decorators-6.24.1.tgz", + "integrity": "sha1-eIAT2PjGtSIr33s0Q5Df13Vp4k0=", + "requires": { + "babel-helper-explode-class": "6.24.1", + "babel-plugin-syntax-decorators": "6.13.0", + "babel-runtime": "6.26.0", + "babel-template": "6.26.0", + "babel-types": "6.26.0" + } + }, + "babel-plugin-transform-decorators-legacy": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-decorators-legacy/-/babel-plugin-transform-decorators-legacy-1.3.4.tgz", + "integrity": "sha1-dBtY9sW86eYCfgiC2cmU8E82aSU=", + "requires": { + "babel-plugin-syntax-decorators": "6.13.0", + "babel-runtime": "6.26.0", + "babel-template": "6.26.0" + } + }, + "babel-plugin-transform-es2015-arrow-functions": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-arrow-functions/-/babel-plugin-transform-es2015-arrow-functions-6.22.0.tgz", + "integrity": "sha1-RSaSy3EdX3ncf4XkQM5BufJE0iE=", + "requires": { + "babel-runtime": "6.26.0" + } + }, + "babel-plugin-transform-es2015-block-scoped-functions": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-block-scoped-functions/-/babel-plugin-transform-es2015-block-scoped-functions-6.22.0.tgz", + "integrity": "sha1-u8UbSflk1wy42OC5ToICRs46YUE=", + "requires": { + "babel-runtime": "6.26.0" + } + }, + "babel-plugin-transform-es2015-block-scoping": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-block-scoping/-/babel-plugin-transform-es2015-block-scoping-6.26.0.tgz", + "integrity": "sha1-1w9SmcEwjQXBL0Y4E7CgnnOxiV8=", + "requires": { + "babel-runtime": "6.26.0", + "babel-template": "6.26.0", + "babel-traverse": "6.26.0", + "babel-types": "6.26.0", + "lodash": "4.17.4" + } + }, + "babel-plugin-transform-es2015-classes": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-classes/-/babel-plugin-transform-es2015-classes-6.24.1.tgz", + "integrity": "sha1-WkxYpQyclGHlZLSyo7+ryXolhNs=", + "requires": { + "babel-helper-define-map": "6.26.0", + "babel-helper-function-name": "6.24.1", + "babel-helper-optimise-call-expression": "6.24.1", + "babel-helper-replace-supers": "6.24.1", + "babel-messages": "6.23.0", + "babel-runtime": "6.26.0", + "babel-template": "6.26.0", + "babel-traverse": "6.26.0", + "babel-types": "6.26.0" + }, + "dependencies": { + "babel-helper-function-name": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-function-name/-/babel-helper-function-name-6.24.1.tgz", + "integrity": "sha1-00dbjAPtmCQqJbSDUasYOZ01gKk=", + "requires": { + "babel-helper-get-function-arity": "6.24.1", + "babel-runtime": "6.26.0", + "babel-template": "6.26.0", + "babel-traverse": "6.26.0", + "babel-types": "6.26.0" + } + }, + "babel-helper-get-function-arity": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-get-function-arity/-/babel-helper-get-function-arity-6.24.1.tgz", + "integrity": "sha1-j3eCqpNAfEHTqlCQj4mwMbG2hT0=", + "requires": { + "babel-runtime": "6.26.0", + "babel-types": "6.26.0" + } + } + } + }, + "babel-plugin-transform-es2015-computed-properties": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-computed-properties/-/babel-plugin-transform-es2015-computed-properties-6.24.1.tgz", + "integrity": "sha1-b+Ko0WiV1WNPTNmZttNICjCBWbM=", + "requires": { + "babel-runtime": "6.26.0", + "babel-template": "6.26.0" + } + }, + "babel-plugin-transform-es2015-destructuring": { + "version": "6.23.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-destructuring/-/babel-plugin-transform-es2015-destructuring-6.23.0.tgz", + "integrity": "sha1-mXux8auWf2gtKwh2/jWNYOdlxW0=", + "requires": { + "babel-runtime": "6.26.0" + } + }, + "babel-plugin-transform-es2015-duplicate-keys": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-duplicate-keys/-/babel-plugin-transform-es2015-duplicate-keys-6.24.1.tgz", + "integrity": "sha1-c+s9MQypaePvnskcU3QabxV2Qj4=", + "requires": { + "babel-runtime": "6.26.0", + "babel-types": "6.26.0" + } + }, + "babel-plugin-transform-es2015-for-of": { + "version": "6.23.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-for-of/-/babel-plugin-transform-es2015-for-of-6.23.0.tgz", + "integrity": "sha1-9HyVsrYT3x0+zC/bdXNiPHUkhpE=", + "requires": { + "babel-runtime": "6.26.0" + } + }, + "babel-plugin-transform-es2015-function-name": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-function-name/-/babel-plugin-transform-es2015-function-name-6.24.1.tgz", + "integrity": "sha1-g0yJhTvDaxrw86TF26qU/Y6sqos=", + "requires": { + "babel-helper-function-name": "6.24.1", + "babel-runtime": "6.26.0", + "babel-types": "6.26.0" + }, + "dependencies": { + "babel-helper-function-name": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-function-name/-/babel-helper-function-name-6.24.1.tgz", + "integrity": "sha1-00dbjAPtmCQqJbSDUasYOZ01gKk=", + "requires": { + "babel-helper-get-function-arity": "6.24.1", + "babel-runtime": "6.26.0", + "babel-template": "6.26.0", + "babel-traverse": "6.26.0", + "babel-types": "6.26.0" + } + }, + "babel-helper-get-function-arity": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-get-function-arity/-/babel-helper-get-function-arity-6.24.1.tgz", + "integrity": "sha1-j3eCqpNAfEHTqlCQj4mwMbG2hT0=", + "requires": { + "babel-runtime": "6.26.0", + "babel-types": "6.26.0" + } + } + } + }, + "babel-plugin-transform-es2015-literals": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-literals/-/babel-plugin-transform-es2015-literals-6.22.0.tgz", + "integrity": "sha1-T1SgLWzWbPkVKAAZox0xklN3yi4=", + "requires": { + "babel-runtime": "6.26.0" + } + }, + "babel-plugin-transform-es2015-modules-amd": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-modules-amd/-/babel-plugin-transform-es2015-modules-amd-6.24.1.tgz", + "integrity": "sha1-Oz5UAXI5hC1tGcMBHEvS8AoA0VQ=", + "requires": { + "babel-plugin-transform-es2015-modules-commonjs": "6.26.0", + "babel-runtime": "6.26.0", + "babel-template": "6.26.0" + } + }, + "babel-plugin-transform-es2015-modules-commonjs": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-modules-commonjs/-/babel-plugin-transform-es2015-modules-commonjs-6.26.0.tgz", + "integrity": "sha1-DYOUApt9xqvhqX7xgeAHWN0uXYo=", + "requires": { + "babel-plugin-transform-strict-mode": "6.24.1", + "babel-runtime": "6.26.0", + "babel-template": "6.26.0", + "babel-types": "6.26.0" + } + }, + "babel-plugin-transform-es2015-modules-systemjs": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-modules-systemjs/-/babel-plugin-transform-es2015-modules-systemjs-6.24.1.tgz", + "integrity": "sha1-/4mhQrkRmpBhlfXxBuzzBdlAfSM=", + "requires": { + "babel-helper-hoist-variables": "6.24.1", + "babel-runtime": "6.26.0", + "babel-template": "6.26.0" + } + }, + "babel-plugin-transform-es2015-modules-umd": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-modules-umd/-/babel-plugin-transform-es2015-modules-umd-6.24.1.tgz", + "integrity": "sha1-rJl+YoXNGO1hdq22B9YCNErThGg=", + "requires": { + "babel-plugin-transform-es2015-modules-amd": "6.24.1", + "babel-runtime": "6.26.0", + "babel-template": "6.26.0" + } + }, + "babel-plugin-transform-es2015-object-super": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-object-super/-/babel-plugin-transform-es2015-object-super-6.24.1.tgz", + "integrity": "sha1-JM72muIcuDp/hgPa0CH1cusnj40=", + "requires": { + "babel-helper-replace-supers": "6.24.1", + "babel-runtime": "6.26.0" + } + }, + "babel-plugin-transform-es2015-parameters": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-parameters/-/babel-plugin-transform-es2015-parameters-6.24.1.tgz", + "integrity": "sha1-V6w1GrScrxSpfNE7CfZv3wpiXys=", + "requires": { + "babel-helper-call-delegate": "6.24.1", + "babel-helper-get-function-arity": "6.24.1", + "babel-runtime": "6.26.0", + "babel-template": "6.26.0", + "babel-traverse": "6.26.0", + "babel-types": "6.26.0" + }, + "dependencies": { + "babel-helper-get-function-arity": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-get-function-arity/-/babel-helper-get-function-arity-6.24.1.tgz", + "integrity": "sha1-j3eCqpNAfEHTqlCQj4mwMbG2hT0=", + "requires": { + "babel-runtime": "6.26.0", + "babel-types": "6.26.0" + } + } + } + }, + "babel-plugin-transform-es2015-shorthand-properties": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-shorthand-properties/-/babel-plugin-transform-es2015-shorthand-properties-6.24.1.tgz", + "integrity": "sha1-JPh11nIch2YbvZmkYi5R8U3jiqA=", + "requires": { + "babel-runtime": "6.26.0", + "babel-types": "6.26.0" + } + }, + "babel-plugin-transform-es2015-spread": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-spread/-/babel-plugin-transform-es2015-spread-6.22.0.tgz", + "integrity": "sha1-1taKmfia7cRTbIGlQujdnxdG+NE=", + "requires": { + "babel-runtime": "6.26.0" + } + }, + "babel-plugin-transform-es2015-sticky-regex": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-sticky-regex/-/babel-plugin-transform-es2015-sticky-regex-6.24.1.tgz", + "integrity": "sha1-AMHNsaynERLN8M9hJsLta0V8zbw=", + "requires": { + "babel-helper-regex": "6.26.0", + "babel-runtime": "6.26.0", + "babel-types": "6.26.0" + } + }, + "babel-plugin-transform-es2015-template-literals": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-template-literals/-/babel-plugin-transform-es2015-template-literals-6.22.0.tgz", + "integrity": "sha1-qEs0UPfp+PH2g51taH2oS7EjbY0=", + "requires": { + "babel-runtime": "6.26.0" + } + }, + "babel-plugin-transform-es2015-typeof-symbol": { + "version": "6.23.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-typeof-symbol/-/babel-plugin-transform-es2015-typeof-symbol-6.23.0.tgz", + "integrity": "sha1-3sCfHN3/lLUqxz1QXITfWdzOs3I=", + "requires": { + "babel-runtime": "6.26.0" + } + }, + "babel-plugin-transform-es2015-unicode-regex": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-unicode-regex/-/babel-plugin-transform-es2015-unicode-regex-6.24.1.tgz", + "integrity": "sha1-04sS9C6nMj9yk4fxinxa4frrNek=", + "requires": { + "babel-helper-regex": "6.26.0", + "babel-runtime": "6.26.0", + "regexpu-core": "2.0.0" + } + }, + "babel-plugin-transform-exponentiation-operator": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-exponentiation-operator/-/babel-plugin-transform-exponentiation-operator-6.24.1.tgz", + "integrity": "sha1-KrDJx/MJj6SJB3cruBP+QejeOg4=", + "requires": { + "babel-helper-builder-binary-assignment-operator-visitor": "6.24.1", + "babel-plugin-syntax-exponentiation-operator": "6.13.0", + "babel-runtime": "6.26.0" + } + }, + "babel-plugin-transform-flow-strip-types": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-flow-strip-types/-/babel-plugin-transform-flow-strip-types-6.22.0.tgz", + "integrity": "sha1-hMtnKTXUNxT9wyvOhFaNh0Qc988=", + "requires": { + "babel-plugin-syntax-flow": "6.18.0", + "babel-runtime": "6.26.0" + } + }, + "babel-plugin-transform-object-rest-spread": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-object-rest-spread/-/babel-plugin-transform-object-rest-spread-6.26.0.tgz", + "integrity": "sha1-DzZpLVD+9rfi1LOsFHgTepY7ewY=", + "requires": { + "babel-plugin-syntax-object-rest-spread": "6.13.0", + "babel-runtime": "6.26.0" + } + }, + "babel-plugin-transform-react-display-name": { + "version": "6.25.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-react-display-name/-/babel-plugin-transform-react-display-name-6.25.0.tgz", + "integrity": "sha1-Z+K/Hx6ck6sI25Z5LgU5K/LMKNE=", + "requires": { + "babel-runtime": "6.26.0" + } + }, + "babel-plugin-transform-react-jsx": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-react-jsx/-/babel-plugin-transform-react-jsx-6.24.1.tgz", + "integrity": "sha1-hAoCjn30YN/DotKfDA2R9jduZqM=", + "requires": { + "babel-helper-builder-react-jsx": "6.26.0", + "babel-plugin-syntax-jsx": "6.18.0", + "babel-runtime": "6.26.0" + } + }, + "babel-plugin-transform-react-jsx-self": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-react-jsx-self/-/babel-plugin-transform-react-jsx-self-6.22.0.tgz", + "integrity": "sha1-322AqdomEqEh5t3XVYvL7PBuY24=", + "requires": { + "babel-plugin-syntax-jsx": "6.18.0", + "babel-runtime": "6.26.0" + } + }, + "babel-plugin-transform-react-jsx-source": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-react-jsx-source/-/babel-plugin-transform-react-jsx-source-6.22.0.tgz", + "integrity": "sha1-ZqwSFT9c0tF7PBkmj0vwGX9E7NY=", + "requires": { + "babel-plugin-syntax-jsx": "6.18.0", + "babel-runtime": "6.26.0" + } + }, + "babel-plugin-transform-regenerator": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-regenerator/-/babel-plugin-transform-regenerator-6.26.0.tgz", + "integrity": "sha1-4HA2lvveJ/Cj78rPi03KL3s6jy8=", + "requires": { + "regenerator-transform": "0.10.1" + } + }, + "babel-plugin-transform-strict-mode": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-strict-mode/-/babel-plugin-transform-strict-mode-6.24.1.tgz", + "integrity": "sha1-1fr3qleKZbvlkc9e2uBKDGcCB1g=", + "requires": { + "babel-runtime": "6.26.0", + "babel-types": "6.26.0" + } + }, + "babel-preset-decorators-legacy": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/babel-preset-decorators-legacy/-/babel-preset-decorators-legacy-1.0.0.tgz", + "integrity": "sha1-h3cuxTA8Wjt0jORQyEAJdWYtFzE=", + "requires": { + "babel-plugin-transform-decorators-legacy": "1.3.4" + } + }, + "babel-preset-es2015": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-preset-es2015/-/babel-preset-es2015-6.24.1.tgz", + "integrity": "sha1-1EBQ1rwsn+6nAqrzjXJ6AhBTiTk=", + "requires": { + "babel-plugin-check-es2015-constants": "6.22.0", + "babel-plugin-transform-es2015-arrow-functions": "6.22.0", + "babel-plugin-transform-es2015-block-scoped-functions": "6.22.0", + "babel-plugin-transform-es2015-block-scoping": "6.26.0", + "babel-plugin-transform-es2015-classes": "6.24.1", + "babel-plugin-transform-es2015-computed-properties": "6.24.1", + "babel-plugin-transform-es2015-destructuring": "6.23.0", + "babel-plugin-transform-es2015-duplicate-keys": "6.24.1", + "babel-plugin-transform-es2015-for-of": "6.23.0", + "babel-plugin-transform-es2015-function-name": "6.24.1", + "babel-plugin-transform-es2015-literals": "6.22.0", + "babel-plugin-transform-es2015-modules-amd": "6.24.1", + "babel-plugin-transform-es2015-modules-commonjs": "6.26.0", + "babel-plugin-transform-es2015-modules-systemjs": "6.24.1", + "babel-plugin-transform-es2015-modules-umd": "6.24.1", + "babel-plugin-transform-es2015-object-super": "6.24.1", + "babel-plugin-transform-es2015-parameters": "6.24.1", + "babel-plugin-transform-es2015-shorthand-properties": "6.24.1", + "babel-plugin-transform-es2015-spread": "6.22.0", + "babel-plugin-transform-es2015-sticky-regex": "6.24.1", + "babel-plugin-transform-es2015-template-literals": "6.22.0", + "babel-plugin-transform-es2015-typeof-symbol": "6.23.0", + "babel-plugin-transform-es2015-unicode-regex": "6.24.1", + "babel-plugin-transform-regenerator": "6.26.0" + } + }, + "babel-preset-flow": { + "version": "6.23.0", + "resolved": "https://registry.npmjs.org/babel-preset-flow/-/babel-preset-flow-6.23.0.tgz", + "integrity": "sha1-5xIYiHCFrpoktb5Baa/7WZgWxJ0=", + "requires": { + "babel-plugin-transform-flow-strip-types": "6.22.0" + } + }, + "babel-preset-react": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-preset-react/-/babel-preset-react-6.24.1.tgz", + "integrity": "sha1-umnfrqRfw+xjm2pOzqbhdwLJE4A=", + "requires": { + "babel-plugin-syntax-jsx": "6.18.0", + "babel-plugin-transform-react-display-name": "6.25.0", + "babel-plugin-transform-react-jsx": "6.24.1", + "babel-plugin-transform-react-jsx-self": "6.22.0", + "babel-plugin-transform-react-jsx-source": "6.22.0", + "babel-preset-flow": "6.23.0" + } + }, + "babel-preset-stage-2": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-preset-stage-2/-/babel-preset-stage-2-6.24.1.tgz", + "integrity": "sha1-2eKWD7PXEYfw5k7sYrwHdnIZvcE=", + "requires": { + "babel-plugin-syntax-dynamic-import": "6.18.0", + "babel-plugin-transform-class-properties": "6.24.1", + "babel-plugin-transform-decorators": "6.24.1", + "babel-preset-stage-3": "6.24.1" + } + }, + "babel-preset-stage-3": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-preset-stage-3/-/babel-preset-stage-3-6.24.1.tgz", + "integrity": "sha1-g2raCp56f6N8sTj7kyb4eTSkg5U=", + "requires": { + "babel-plugin-syntax-trailing-function-commas": "6.22.0", + "babel-plugin-transform-async-generator-functions": "6.24.1", + "babel-plugin-transform-async-to-generator": "6.24.1", + "babel-plugin-transform-exponentiation-operator": "6.24.1", + "babel-plugin-transform-object-rest-spread": "6.26.0" + } + }, + "babel-register": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-register/-/babel-register-6.26.0.tgz", + "integrity": "sha1-btAhFz4vy0htestFxgCahW9kcHE=", + "requires": { + "babel-core": "6.26.0", + "babel-runtime": "6.26.0", + "core-js": "2.5.1", + "home-or-tmp": "2.0.0", + "lodash": "4.17.4", + "mkdirp": "0.5.1", + "source-map-support": "0.4.18" + } + }, + "babel-runtime": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz", + "integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=", + "requires": { + "core-js": "2.5.1", + "regenerator-runtime": "0.11.0" + } + }, + "babel-template": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-template/-/babel-template-6.26.0.tgz", + "integrity": "sha1-3gPi0WOWsGn0bdn/+FIfsaDjXgI=", + "requires": { + "babel-runtime": "6.26.0", + "babel-traverse": "6.26.0", + "babel-types": "6.26.0", + "babylon": "6.18.0", + "lodash": "4.17.4" + } + }, + "babel-traverse": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-traverse/-/babel-traverse-6.26.0.tgz", + "integrity": "sha1-RqnL1+3MYsjlwGTi0tjQ9ANXZu4=", + "requires": { + "babel-code-frame": "6.26.0", + "babel-messages": "6.23.0", + "babel-runtime": "6.26.0", + "babel-types": "6.26.0", + "babylon": "6.18.0", + "debug": "2.6.9", + "globals": "9.18.0", + "invariant": "2.2.2", + "lodash": "4.17.4" + } + }, + "babel-types": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-types/-/babel-types-6.26.0.tgz", + "integrity": "sha1-o7Bz+Uq0nrb6Vc1lInozQ4BjJJc=", + "requires": { + "babel-runtime": "6.26.0", + "esutils": "2.0.2", + "lodash": "4.17.4", + "to-fast-properties": "1.0.3" + } + }, + "babylon": { + "version": "6.18.0", + "resolved": "https://registry.npmjs.org/babylon/-/babylon-6.18.0.tgz", + "integrity": "sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ==" + }, + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" + }, + "base64-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.2.1.tgz", + "integrity": "sha512-dwVUVIXsBZXwTuwnXI9RK8sBmgq09NDHzyR9SAph9eqk76gKK2JSQmZARC2zRC81JC2QTtxD0ARU5qTS25gIGw==" + }, + "beeper": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/beeper/-/beeper-1.1.1.tgz", + "integrity": "sha1-5tXqjF2tABMEpwsiY4RH9pyy+Ak=" + }, + "big.js": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-3.2.0.tgz", + "integrity": "sha512-+hN/Zh2D08Mx65pZ/4g5bsmNiZUuChDiQfTUQ7qJr4/kuopCr88xZsAXv6mBoZEsUI4OuGHlX59qE94K2mMW8Q==" + }, + "binary-extensions": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.10.0.tgz", + "integrity": "sha1-muuabF6IY4qtFx4Wf1kAq+JINdA=" + }, + "bl": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/bl/-/bl-1.2.1.tgz", + "integrity": "sha1-ysMo977kVzDUBLaSID/LWQ4XLV4=", + "requires": { + "readable-stream": "2.3.3" + } + }, + "block-stream": { + "version": "0.0.9", + "resolved": "https://registry.npmjs.org/block-stream/-/block-stream-0.0.9.tgz", + "integrity": "sha1-E+v+d4oDIFz+A3UUgeu0szAMEmo=", + "requires": { + "inherits": "2.0.3" + } + }, + "bluebird": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.1.tgz", + "integrity": "sha512-MKiLiV+I1AA596t9w1sQJ8jkiSr5+ZKi0WKrYGUn6d1Fx+Ij4tIj+m2WMQSGczs5jZVxV339chE8iwk6F64wjA==" + }, + "bn.js": { + "version": "4.11.8", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.8.tgz", + "integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA==" + }, + "body-parser": { + "version": "1.14.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.14.2.tgz", + "integrity": "sha1-EBXLH+LEQ4WCWVgdtTMy+NDPUPk=", + "requires": { + "bytes": "2.2.0", + "content-type": "1.0.4", + "debug": "2.2.0", + "depd": "1.1.1", + "http-errors": "1.3.1", + "iconv-lite": "0.4.13", + "on-finished": "2.3.0", + "qs": "5.2.0", + "raw-body": "2.1.7", + "type-is": "1.6.15" + }, + "dependencies": { + "debug": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.2.0.tgz", + "integrity": "sha1-+HBX6ZWxofauaklgZkE3vFbwOdo=", + "requires": { + "ms": "0.7.1" + } + }, + "iconv-lite": { + "version": "0.4.13", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.13.tgz", + "integrity": "sha1-H4irpKsLFQjoMSrMOTRfNumS4vI=" + }, + "ms": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.1.tgz", + "integrity": "sha1-nNE8A62/8ltl7/3nzoZO6VIBcJg=" + }, + "qs": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-5.2.0.tgz", + "integrity": "sha1-qfMRQq9GjLcrJbMBNrokVoNJFr4=" + } + } + }, + "brace-expansion": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.8.tgz", + "integrity": "sha1-wHshHHyVLsH479Uad+8NHTmQopI=", + "requires": { + "balanced-match": "1.0.0", + "concat-map": "0.0.1" + } + }, + "braces": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/braces/-/braces-1.8.5.tgz", + "integrity": "sha1-uneWLhLf+WnWt2cR6RS3N4V79qc=", + "requires": { + "expand-range": "1.8.2", + "preserve": "0.2.0", + "repeat-element": "1.1.2" + } + }, + "brorand": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", + "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=" + }, + "browserify-aes": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.1.1.tgz", + "integrity": "sha512-UGnTYAnB2a3YuYKIRy1/4FB2HdM866E0qC46JXvVTYKlBlZlnvfpSfY6OKfXZAkv70eJ2a1SqzpAo5CRhZGDFg==", + "requires": { + "buffer-xor": "1.0.3", + "cipher-base": "1.0.4", + "create-hash": "1.1.3", + "evp_bytestokey": "1.0.3", + "inherits": "2.0.3", + "safe-buffer": "5.1.1" + } + }, + "browserify-cipher": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/browserify-cipher/-/browserify-cipher-1.0.0.tgz", + "integrity": "sha1-mYgkSHS/XtTijalWZtzWasj8Njo=", + "requires": { + "browserify-aes": "1.1.1", + "browserify-des": "1.0.0", + "evp_bytestokey": "1.0.3" + } + }, + "browserify-des": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/browserify-des/-/browserify-des-1.0.0.tgz", + "integrity": "sha1-2qJ3cXRwki7S/hhZQRihdUOXId0=", + "requires": { + "cipher-base": "1.0.4", + "des.js": "1.0.0", + "inherits": "2.0.3" + } + }, + "browserify-rsa": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.0.1.tgz", + "integrity": "sha1-IeCr+vbyApzy+vsTNWenAdQTVSQ=", + "requires": { + "bn.js": "4.11.8", + "randombytes": "2.0.5" + } + }, + "browserify-sign": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.0.4.tgz", + "integrity": "sha1-qk62jl17ZYuqa/alfmMMvXqT0pg=", + "requires": { + "bn.js": "4.11.8", + "browserify-rsa": "4.0.1", + "create-hash": "1.1.3", + "create-hmac": "1.1.6", + "elliptic": "6.4.0", + "inherits": "2.0.3", + "parse-asn1": "5.1.0" + } + }, + "browserify-zlib": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.1.4.tgz", + "integrity": "sha1-uzX4pRn2AOD6a4SFJByXnQFB+y0=", + "requires": { + "pako": "0.2.9" + } + }, + "browserslist": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-2.5.1.tgz", + "integrity": "sha512-jAvM2ku7YDJ+leAq3bFH1DE0Ylw+F+EQDq4GkqZfgPEqpWYw9ofQH85uKSB9r3Tv7XDbfqVtE+sdvKJW7IlPJA==", + "requires": { + "caniuse-lite": "1.0.30000748", + "electron-to-chromium": "1.3.27" + } + }, + "bser": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.0.0.tgz", + "integrity": "sha1-mseNPtXZFYBP2HrLFYvHlxR6Fxk=", + "requires": { + "node-int64": "0.4.0" + } + }, + "buffer": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.1.tgz", + "integrity": "sha1-bRu2AbB6TvztlwlBMgkwJ8lbwpg=", + "requires": { + "base64-js": "1.2.1", + "ieee754": "1.1.8", + "isarray": "1.0.0" + } + }, + "buffer-xor": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz", + "integrity": "sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk=" + }, + "bufferstreams": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/bufferstreams/-/bufferstreams-1.0.1.tgz", + "integrity": "sha1-z7GtlWjTujz+k1upq92VLeiKqyo=", + "requires": { + "readable-stream": "1.1.14" + }, + "dependencies": { + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" + }, + "readable-stream": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", + "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", + "requires": { + "core-util-is": "1.0.2", + "inherits": "2.0.3", + "isarray": "0.0.1", + "string_decoder": "0.10.31" + } + }, + "string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" + } + } + }, + "builtin-modules": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz", + "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=" + }, + "builtin-status-codes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz", + "integrity": "sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug=" + }, + "bytes": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-2.2.0.tgz", + "integrity": "sha1-/TVGSkA/b5EXwt42Cez/nK4ABYg=" + }, + "caller-path": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/caller-path/-/caller-path-0.1.0.tgz", + "integrity": "sha1-lAhe9jWB7NPaqSREqP6U6CV3dR8=", + "requires": { + "callsites": "0.2.0" + } + }, + "callsites": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-0.2.0.tgz", + "integrity": "sha1-r6uWJikQp/M8GaV3WCXGnzTjUMo=" + }, + "camelcase": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", + "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=" + }, + "camelcase-css": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-1.0.1.tgz", + "integrity": "sha1-FXxCOCZfXPlKHf/ehkRlUsvz9wU=" + }, + "camelcase-keys": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-2.1.0.tgz", + "integrity": "sha1-MIvur/3ygRkFHvodkyITyRuPkuc=", + "requires": { + "camelcase": "2.1.1", + "map-obj": "1.0.1" + }, + "dependencies": { + "camelcase": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-2.1.1.tgz", + "integrity": "sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8=" + } + } + }, + "caniuse-api": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/caniuse-api/-/caniuse-api-1.6.1.tgz", + "integrity": "sha1-tTTnxzTE+B7F++isoq0kNUuWLGw=", + "requires": { + "browserslist": "1.7.7", + "caniuse-db": "1.0.30000748", + "lodash.memoize": "4.1.2", + "lodash.uniq": "4.5.0" + }, + "dependencies": { + "browserslist": { + "version": "1.7.7", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-1.7.7.tgz", + "integrity": "sha1-C9dnBCWL6CmyOYu1Dkti0aFmsLk=", + "requires": { + "caniuse-db": "1.0.30000748", + "electron-to-chromium": "1.3.27" + } + } + } + }, + "caniuse-db": { + "version": "1.0.30000748", + "resolved": "https://registry.npmjs.org/caniuse-db/-/caniuse-db-1.0.30000748.tgz", + "integrity": "sha1-eF2e381kW/eVxv887TPEXVgMSKA=" + }, + "caniuse-lite": { + "version": "1.0.30000748", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30000748.tgz", + "integrity": "sha1-RMjW2lKtZaXXudyk7+vQvdmCugk=" + }, + "center-align": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/center-align/-/center-align-0.1.3.tgz", + "integrity": "sha1-qg0yYptu6XIgBBHL1EYckHvCt60=", + "requires": { + "align-text": "0.1.4", + "lazy-cache": "1.0.4" + } + }, + "chalk": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.2.0.tgz", + "integrity": "sha512-0BMM/2hG3ZaoPfR6F+h/oWpZtsh3b/s62TjSM6MGCJWEbJDN1acqCXvyhhZsDSVFklpebUoQ5O1kKC7lOzrn9g==", + "requires": { + "ansi-styles": "3.2.0", + "escape-string-regexp": "1.0.5", + "supports-color": "4.5.0" + } + }, + "chokidar": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-1.7.0.tgz", + "integrity": "sha1-eY5ol3gVHIB2tLNg5e3SjNortGg=", + "requires": { + "anymatch": "1.3.2", + "async-each": "1.0.1", + "glob-parent": "2.0.0", + "inherits": "2.0.3", + "is-binary-path": "1.0.1", + "is-glob": "2.0.1", + "path-is-absolute": "1.0.1", + "readdirp": "2.1.0" + } + }, + "cipher-base": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz", + "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==", + "requires": { + "inherits": "2.0.3", + "safe-buffer": "5.1.1" + } + }, + "circular-json": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/circular-json/-/circular-json-0.3.3.tgz", + "integrity": "sha512-UZK3NBx2Mca+b5LsG7bY183pHWt5Y1xts4P3Pz7ENTwGVnJOUWbRb3ocjvX7hx9tq/yTAdclXm9sZ38gNuem4A==" + }, + "clap": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/clap/-/clap-1.2.3.tgz", + "integrity": "sha512-4CoL/A3hf90V3VIEjeuhSvlGFEHKzOz+Wfc2IVZc+FaUgU0ZQafJTP49fvnULipOPcAfqhyI2duwQyns6xqjYA==", + "requires": { + "chalk": "1.1.3" + }, + "dependencies": { + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=" + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "requires": { + "ansi-styles": "2.2.1", + "escape-string-regexp": "1.0.5", + "has-ansi": "2.0.0", + "strip-ansi": "3.0.1", + "supports-color": "2.0.0" + } + }, + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" + } + } + }, + "classnames": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.2.5.tgz", + "integrity": "sha1-+zgB1FNGdknvNgPH1hoCvRKb3m0=" + }, + "clean-css": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-4.1.9.tgz", + "integrity": "sha1-Nc7ornaHpJuYA09w3gDE7dOCYwE=", + "requires": { + "source-map": "0.5.7" + }, + "dependencies": { + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" + } + } + }, + "cli-cursor": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz", + "integrity": "sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU=", + "requires": { + "restore-cursor": "2.0.0" + } + }, + "cli-width": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.0.tgz", + "integrity": "sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk=" + }, + "clipboard": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/clipboard/-/clipboard-1.7.1.tgz", + "integrity": "sha1-Ng1taUbpmnof7zleQrqStem1oWs=", + "requires": { + "good-listener": "1.2.2", + "select": "1.1.2", + "tiny-emitter": "2.0.2" + } + }, + "cliui": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-3.2.0.tgz", + "integrity": "sha1-EgYBU3qRbSmUD5NNo7SNWFo5IT0=", + "requires": { + "string-width": "1.0.2", + "strip-ansi": "3.0.1", + "wrap-ansi": "2.1.0" + }, + "dependencies": { + "is-fullwidth-code-point": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", + "requires": { + "number-is-nan": "1.0.1" + } + }, + "string-width": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "requires": { + "code-point-at": "1.1.0", + "is-fullwidth-code-point": "1.0.0", + "strip-ansi": "3.0.1" + } + } + } + }, + "clone": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.2.tgz", + "integrity": "sha1-Jgt6meux7f4kdTgXX3gyQ8sZ0Uk=" + }, + "clone-buffer": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/clone-buffer/-/clone-buffer-1.0.0.tgz", + "integrity": "sha1-4+JbIHrE5wGvch4staFnksrD3Fg=" + }, + "clone-regexp": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/clone-regexp/-/clone-regexp-1.0.0.tgz", + "integrity": "sha1-6uCiQT9VwJQvgYwin+/OhF1/Oxw=", + "requires": { + "is-regexp": "1.0.0", + "is-supported-regexp-flag": "1.0.0" + } + }, + "clone-stats": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/clone-stats/-/clone-stats-0.0.1.tgz", + "integrity": "sha1-uI+UqCzzi4eR1YBG6kAprYjKmdE=" + }, + "cloneable-readable": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/cloneable-readable/-/cloneable-readable-1.0.0.tgz", + "integrity": "sha1-pikNQT8hemEjL5XkWP84QYz7ARc=", + "requires": { + "inherits": "2.0.3", + "process-nextick-args": "1.0.7", + "through2": "2.0.3" + } + }, + "co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=" + }, + "coa": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/coa/-/coa-1.0.4.tgz", + "integrity": "sha1-qe8VNmDWqGqL3sAomlxoTSF0Mv0=", + "requires": { + "q": "1.5.1" + } + }, + "code-point-at": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", + "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=" + }, + "color": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/color/-/color-0.11.4.tgz", + "integrity": "sha1-bXtcdPtl6EHNSHkq0e1eB7kE12Q=", + "requires": { + "clone": "1.0.2", + "color-convert": "1.9.0", + "color-string": "0.3.0" + } + }, + "color-convert": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.0.tgz", + "integrity": "sha1-Gsz5fdc5uYO/mU1W/sj5WFNkG3o=", + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" + }, + "color-string": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-0.3.0.tgz", + "integrity": "sha1-J9RvtnAlxcL6JZk7+/V55HhBuZE=", + "requires": { + "color-name": "1.1.3" + } + }, + "colormin": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/colormin/-/colormin-1.1.2.tgz", + "integrity": "sha1-6i90IKcrlogaOKrlnsEkpvcpgTM=", + "requires": { + "color": "0.11.4", + "css-color-names": "0.0.4", + "has": "1.0.1" + } + }, + "colors": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.1.2.tgz", + "integrity": "sha1-FopHAXVran9RoSzgyXv6KMCE7WM=" + }, + "commander": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.11.0.tgz", + "integrity": "sha512-b0553uYA5YAEGgyYIGYROzKQ7X5RAqedkfjiZxwi0kL1g3bOaBNNZfYkzt/CL0umgD5wc9Jec2FbB98CjkMRvQ==" + }, + "commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=" + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + }, + "concat-stream": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.0.tgz", + "integrity": "sha1-CqxmL9Ur54lk1VMvaUeE5wEQrPc=", + "requires": { + "inherits": "2.0.3", + "readable-stream": "2.3.3", + "typedarray": "0.0.6" + } + }, + "concat-with-sourcemaps": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/concat-with-sourcemaps/-/concat-with-sourcemaps-1.0.4.tgz", + "integrity": "sha1-9Vs74q60dgGxCi1SWcz7cP0vHdY=", + "requires": { + "source-map": "0.5.7" + }, + "dependencies": { + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" + } + } + }, + "console-browserify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.1.0.tgz", + "integrity": "sha1-8CQcRXMKn8YyOyBtvzjtx0HQuxA=", + "requires": { + "date-now": "0.1.4" + } + }, + "consolidate": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/consolidate/-/consolidate-0.14.5.tgz", + "integrity": "sha1-WiUEe8dvcwcmZ8jLUsmJiI9JTGM=", + "requires": { + "bluebird": "3.5.1" + } + }, + "constants-browserify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/constants-browserify/-/constants-browserify-1.0.0.tgz", + "integrity": "sha1-wguW2MYXdIqvHBYCF2DNJ/y4y3U=" + }, + "content-type": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", + "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" + }, + "convert-source-map": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.5.0.tgz", + "integrity": "sha1-ms1whRxtXf3ZPZKC5e35SgP/RrU=" + }, + "core-js": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.5.1.tgz", + "integrity": "sha1-rmh03GaTd4m4B1T/VCjfZoGcpQs=" + }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" + }, + "cosmiconfig": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-2.2.2.tgz", + "integrity": "sha512-GiNXLwAFPYHy25XmTPpafYvn3CLAkJ8FLsscq78MQd1Kh0OU6Yzhn4eV2MVF4G9WEQZoWEGltatdR+ntGPMl5A==", + "requires": { + "is-directory": "0.3.1", + "js-yaml": "3.7.0", + "minimist": "1.2.0", + "object-assign": "4.1.1", + "os-homedir": "1.0.2", + "parse-json": "2.2.0", + "require-from-string": "1.2.1" + }, + "dependencies": { + "minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" + } + } + }, + "create-ecdh": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.0.tgz", + "integrity": "sha1-iIxyNZbN92EvZJgjPuvXo1MBc30=", + "requires": { + "bn.js": "4.11.8", + "elliptic": "6.4.0" + } + }, + "create-hash": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.1.3.tgz", + "integrity": "sha1-YGBCrIuSYnUPSDyt2rD1gZFy2P0=", + "requires": { + "cipher-base": "1.0.4", + "inherits": "2.0.3", + "ripemd160": "2.0.1", + "sha.js": "2.4.9" + } + }, + "create-hmac": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.6.tgz", + "integrity": "sha1-rLniIaThe9sHbpBlfEK5PjcmzwY=", + "requires": { + "cipher-base": "1.0.4", + "create-hash": "1.1.3", + "inherits": "2.0.3", + "ripemd160": "2.0.1", + "safe-buffer": "5.1.1", + "sha.js": "2.4.9" + } + }, + "create-react-class": { + "version": "15.6.2", + "resolved": "https://registry.npmjs.org/create-react-class/-/create-react-class-15.6.2.tgz", + "integrity": "sha1-zx7RXxKq1/FO9fLf4F5sQvke8Co=", + "requires": { + "fbjs": "0.8.16", + "loose-envify": "1.3.1", + "object-assign": "4.1.1" + } + }, + "cross-spawn": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz", + "integrity": "sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk=", + "requires": { + "lru-cache": "4.1.1", + "shebang-command": "1.2.0", + "which": "1.3.0" + } + }, + "crypto-browserify": { + "version": "3.11.1", + "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.11.1.tgz", + "integrity": "sha512-Na7ZlwCOqoaW5RwUK1WpXws2kv8mNhWdTlzob0UXulk6G9BDbyiJaGTYBIX61Ozn9l1EPPJpICZb4DaOpT9NlQ==", + "requires": { + "browserify-cipher": "1.0.0", + "browserify-sign": "4.0.4", + "create-ecdh": "4.0.0", + "create-hash": "1.1.3", + "create-hmac": "1.1.6", + "diffie-hellman": "5.0.2", + "inherits": "2.0.3", + "pbkdf2": "3.0.14", + "public-encrypt": "4.0.0", + "randombytes": "2.0.5" + } + }, + "css": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/css/-/css-2.2.1.tgz", + "integrity": "sha1-c6TIHehdtmTU7mdPfUcIXjstVdw=", + "requires": { + "inherits": "2.0.3", + "source-map": "0.1.43", + "source-map-resolve": "0.3.1", + "urix": "0.1.0" + }, + "dependencies": { + "source-map": { + "version": "0.1.43", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.1.43.tgz", + "integrity": "sha1-wkvBRspRfBRx9drL4lcbK3+eM0Y=", + "requires": { + "amdefine": "1.0.1" + } + } + } + }, + "css-color-names": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/css-color-names/-/css-color-names-0.0.4.tgz", + "integrity": "sha1-gIrcLnnPhHOAabZGyyDsJ762KeA=" + }, + "css-loader": { + "version": "0.28.7", + "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-0.28.7.tgz", + "integrity": "sha512-GxMpax8a/VgcfRrVy0gXD6yLd5ePYbXX/5zGgTVYp4wXtJklS8Z2VaUArJgc//f6/Dzil7BaJObdSv8eKKCPgg==", + "requires": { + "babel-code-frame": "6.26.0", + "css-selector-tokenizer": "0.7.0", + "cssnano": "3.10.0", + "icss-utils": "2.1.0", + "loader-utils": "1.1.0", + "lodash.camelcase": "4.3.0", + "object-assign": "4.1.1", + "postcss": "5.2.18", + "postcss-modules-extract-imports": "1.1.0", + "postcss-modules-local-by-default": "1.2.0", + "postcss-modules-scope": "1.1.0", + "postcss-modules-values": "1.3.0", + "postcss-value-parser": "3.3.0", + "source-list-map": "2.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=" + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "requires": { + "ansi-styles": "2.2.1", + "escape-string-regexp": "1.0.5", + "has-ansi": "2.0.0", + "strip-ansi": "3.0.1", + "supports-color": "2.0.0" + }, + "dependencies": { + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" + } + } + }, + "has-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", + "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=" + }, + "postcss": { + "version": "5.2.18", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-5.2.18.tgz", + "integrity": "sha512-zrUjRRe1bpXKsX1qAJNJjqZViErVuyEkMTRrwu4ud4sbTtIBRmtaYDrHmcGgmrbsW3MHfmtIf+vJumgQn+PrXg==", + "requires": { + "chalk": "1.1.3", + "js-base64": "2.3.2", + "source-map": "0.5.7", + "supports-color": "3.2.3" + } + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" + }, + "supports-color": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", + "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=", + "requires": { + "has-flag": "1.0.0" + } + } + } + }, + "css-selector-tokenizer": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/css-selector-tokenizer/-/css-selector-tokenizer-0.7.0.tgz", + "integrity": "sha1-5piEdK6MlTR3v15+/s/OzNnPTIY=", + "requires": { + "cssesc": "0.1.0", + "fastparse": "1.1.1", + "regexpu-core": "1.0.0" + }, + "dependencies": { + "regexpu-core": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-1.0.0.tgz", + "integrity": "sha1-hqdj9Y7k18L2sQLkdkBQ3n7ZDGs=", + "requires": { + "regenerate": "1.3.3", + "regjsgen": "0.2.0", + "regjsparser": "0.1.5" + } + } + } + }, + "cssesc": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-0.1.0.tgz", + "integrity": "sha1-yBSQPkViM3GgR3tAEJqq++6t27Q=" + }, + "cssnano": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/cssnano/-/cssnano-3.10.0.tgz", + "integrity": "sha1-Tzj2zqK5sX+gFJDyPx3GjqZcHDg=", + "requires": { + "autoprefixer": "6.7.7", + "decamelize": "1.2.0", + "defined": "1.0.0", + "has": "1.0.1", + "object-assign": "4.1.1", + "postcss": "5.2.18", + "postcss-calc": "5.3.1", + "postcss-colormin": "2.2.2", + "postcss-convert-values": "2.6.1", + "postcss-discard-comments": "2.0.4", + "postcss-discard-duplicates": "2.1.0", + "postcss-discard-empty": "2.1.0", + "postcss-discard-overridden": "0.1.1", + "postcss-discard-unused": "2.2.3", + "postcss-filter-plugins": "2.0.2", + "postcss-merge-idents": "2.1.7", + "postcss-merge-longhand": "2.0.2", + "postcss-merge-rules": "2.1.2", + "postcss-minify-font-values": "1.0.5", + "postcss-minify-gradients": "1.0.5", + "postcss-minify-params": "1.2.2", + "postcss-minify-selectors": "2.1.1", + "postcss-normalize-charset": "1.1.1", + "postcss-normalize-url": "3.0.8", + "postcss-ordered-values": "2.2.3", + "postcss-reduce-idents": "2.4.0", + "postcss-reduce-initial": "1.0.1", + "postcss-reduce-transforms": "1.0.4", + "postcss-svgo": "2.1.6", + "postcss-unique-selectors": "2.0.2", + "postcss-value-parser": "3.3.0", + "postcss-zindex": "2.2.0" + }, + "dependencies": { + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=" + }, + "autoprefixer": { + "version": "6.7.7", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-6.7.7.tgz", + "integrity": "sha1-Hb0cg1ZY41zj+ZhAmdsAWFx4IBQ=", + "requires": { + "browserslist": "1.7.7", + "caniuse-db": "1.0.30000748", + "normalize-range": "0.1.2", + "num2fraction": "1.2.2", + "postcss": "5.2.18", + "postcss-value-parser": "3.3.0" + } + }, + "browserslist": { + "version": "1.7.7", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-1.7.7.tgz", + "integrity": "sha1-C9dnBCWL6CmyOYu1Dkti0aFmsLk=", + "requires": { + "caniuse-db": "1.0.30000748", + "electron-to-chromium": "1.3.27" + } + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "requires": { + "ansi-styles": "2.2.1", + "escape-string-regexp": "1.0.5", + "has-ansi": "2.0.0", + "strip-ansi": "3.0.1", + "supports-color": "2.0.0" + }, + "dependencies": { + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" + } + } + }, + "has-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", + "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=" + }, + "postcss": { + "version": "5.2.18", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-5.2.18.tgz", + "integrity": "sha512-zrUjRRe1bpXKsX1qAJNJjqZViErVuyEkMTRrwu4ud4sbTtIBRmtaYDrHmcGgmrbsW3MHfmtIf+vJumgQn+PrXg==", + "requires": { + "chalk": "1.1.3", + "js-base64": "2.3.2", + "source-map": "0.5.7", + "supports-color": "3.2.3" + } + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" + }, + "supports-color": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", + "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=", + "requires": { + "has-flag": "1.0.0" + } + } + } + }, + "csso": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/csso/-/csso-2.3.2.tgz", + "integrity": "sha1-3dUsWHAz9J6Utx/FVWnyUuj/X4U=", + "requires": { + "clap": "1.2.3", + "source-map": "0.5.7" + }, + "dependencies": { + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" + } + } + }, + "currently-unhandled": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/currently-unhandled/-/currently-unhandled-0.4.1.tgz", + "integrity": "sha1-mI3zP+qxke95mmE2nddsF635V+o=", + "requires": { + "array-find-index": "1.0.2" + } + }, + "d": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/d/-/d-1.0.0.tgz", + "integrity": "sha1-dUu1v+VUUdpppYuU1F9MWwRi1Y8=", + "requires": { + "es5-ext": "0.10.35" + } + }, + "date-now": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/date-now/-/date-now-0.1.4.tgz", + "integrity": "sha1-6vQ5/U1ISK105cx9vvIAZyueNFs=" + }, + "dateformat": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-2.2.0.tgz", + "integrity": "sha1-QGXiATz5+5Ft39gu+1Bq1MZ2kGI=" + }, + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "debug-fabulous": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/debug-fabulous/-/debug-fabulous-0.2.1.tgz", + "integrity": "sha512-u0TV6HcfLsZ03xLBhdhSViQMldaiQ2o+8/nSILaXkuNSWvxkx66vYJUAam0Eu7gAilJRX/69J4kKdqajQPaPyw==", + "requires": { + "debug": "3.1.0", + "memoizee": "0.4.11", + "object-assign": "4.1.1" + }, + "dependencies": { + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "requires": { + "ms": "2.0.0" + } + } + } + }, + "decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=" + }, + "decode-uri-component": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", + "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=" + }, + "deep-is": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", + "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=" + }, + "defaults": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.3.tgz", + "integrity": "sha1-xlYFHpgX2f8I7YgUd/P+QBnz730=", + "requires": { + "clone": "1.0.2" + } + }, + "define-properties": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.2.tgz", + "integrity": "sha1-g6c/L+pWmJj7c3GTyPhzyvbUXJQ=", + "requires": { + "foreach": "2.0.5", + "object-keys": "1.0.11" + } + }, + "defined": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/defined/-/defined-1.0.0.tgz", + "integrity": "sha1-yY2bzvdWdBiOEQlpFRGZ45sfppM=" + }, + "del": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/del/-/del-3.0.0.tgz", + "integrity": "sha1-U+z2mf/LyzljdpGrE7rxYIGXZuU=", + "requires": { + "globby": "6.1.0", + "is-path-cwd": "1.0.0", + "is-path-in-cwd": "1.0.0", + "p-map": "1.2.0", + "pify": "3.0.0", + "rimraf": "2.6.2" + }, + "dependencies": { + "pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=" + } + } + }, + "delegate": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/delegate/-/delegate-3.1.3.tgz", + "integrity": "sha1-moJRp3fXAl+qVXN7w7BxdCEnqf0=" + }, + "depd": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.1.tgz", + "integrity": "sha1-V4O04cRZ8G+lyif5kfPQbnoxA1k=" + }, + "deprecated": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/deprecated/-/deprecated-0.0.1.tgz", + "integrity": "sha1-+cmvVGSvoeepcUWKi97yqpTVuxk=" + }, + "des.js": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.0.0.tgz", + "integrity": "sha1-wHTS4qpqipoH29YfmhXCzYPsjsw=", + "requires": { + "inherits": "2.0.3", + "minimalistic-assert": "1.0.0" + } + }, + "detect-file": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/detect-file/-/detect-file-0.1.0.tgz", + "integrity": "sha1-STXe39lIhkjgBrASlWbpOGcR6mM=", + "requires": { + "fs-exists-sync": "0.1.0" + } + }, + "detect-indent": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-4.0.0.tgz", + "integrity": "sha1-920GQ1LN9Docts5hnE7jqUdd4gg=", + "requires": { + "repeating": "2.0.1" + } + }, + "detect-newline": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-2.1.0.tgz", + "integrity": "sha1-9B8cEL5LAOh7XxPaaAdZ8sW/0+I=" + }, + "diff": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-1.4.0.tgz", + "integrity": "sha1-fyjS657nsVqX79ic5j3P2qPMur8=" + }, + "diffie-hellman": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.2.tgz", + "integrity": "sha1-tYNXOScM/ias9jIJn97SoH8gnl4=", + "requires": { + "bn.js": "4.11.8", + "miller-rabin": "4.0.1", + "randombytes": "2.0.5" + } + }, + "disparity": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/disparity/-/disparity-2.0.0.tgz", + "integrity": "sha1-V92stHMkrl9Y0swNqIbbTOnutxg=", + "requires": { + "ansi-styles": "2.2.1", + "diff": "1.4.0" + }, + "dependencies": { + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=" + } + } + }, + "disposables": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/disposables/-/disposables-1.0.1.tgz", + "integrity": "sha1-BkcnoltU9QK9griaot+4358bOeM=" + }, + "dnd-core": { + "version": "2.5.4", + "resolved": "https://registry.npmjs.org/dnd-core/-/dnd-core-2.5.4.tgz", + "integrity": "sha512-BcI782MfTm3wCxeIS5c7tAutyTwEIANtuu3W6/xkoJRwiqhRXKX3BbGlycUxxyzMsKdvvoavxgrC3EMPFNYL9A==", + "requires": { + "asap": "2.0.6", + "invariant": "2.2.2", + "lodash": "4.17.4", + "redux": "3.7.2" + } + }, + "dnode": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/dnode/-/dnode-1.2.2.tgz", + "integrity": "sha1-SsPP4m4pKzs5uCWK59lO3FgTLvo=", + "requires": { + "dnode-protocol": "0.2.2", + "jsonify": "0.0.0" + } + }, + "dnode-protocol": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/dnode-protocol/-/dnode-protocol-0.2.2.tgz", + "integrity": "sha1-URUdFvw7X4SBXuC5SXoQYdDRlJ0=", + "requires": { + "jsonify": "0.0.0", + "traverse": "0.6.6" + } + }, + "doctrine": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.0.0.tgz", + "integrity": "sha1-xz2NKQnSIpHhoAejlYBNqLZl/mM=", + "requires": { + "esutils": "2.0.2", + "isarray": "1.0.0" + } + }, + "dom-css": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/dom-css/-/dom-css-2.1.0.tgz", + "integrity": "sha1-/bwtWgFdCj4YcuEUcrvQ57nmogI=", + "requires": { + "add-px-to-style": "1.0.0", + "prefix-style": "2.0.1", + "to-camel-case": "1.0.0" + } + }, + "dom-helpers": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-3.2.1.tgz", + "integrity": "sha1-MgPgf+0he9H0JLAZc1WC/Deyglo=" + }, + "domain-browser": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-1.1.7.tgz", + "integrity": "sha1-hnqksJP6oF8d4IwG9NeyH9+GmLw=" + }, + "duplexer": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.1.tgz", + "integrity": "sha1-rOb/gIwc5mtX0ev5eXessCM0z8E=" + }, + "duplexer2": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.0.2.tgz", + "integrity": "sha1-xhTc9n4vsUmVqRcR5aYX6KYKMds=", + "requires": { + "readable-stream": "1.1.14" + }, + "dependencies": { + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" + }, + "readable-stream": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", + "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", + "requires": { + "core-util-is": "1.0.2", + "inherits": "2.0.3", + "isarray": "0.0.1", + "string_decoder": "0.10.31" + } + }, + "string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" + } + } + }, + "ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" + }, + "electron-to-chromium": { + "version": "1.3.27", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.27.tgz", + "integrity": "sha1-eOy4o5kGYYe7N07t412ccFZagD0=" + }, + "element-class": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/element-class/-/element-class-0.2.2.tgz", + "integrity": "sha1-nTu9B2f5AT744cjr5yLBQCpgBQ4=" + }, + "elliptic": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.4.0.tgz", + "integrity": "sha1-ysmvh2LIWDYYcAPI3+GT5eLq5d8=", + "requires": { + "bn.js": "4.11.8", + "brorand": "1.1.0", + "hash.js": "1.1.3", + "hmac-drbg": "1.0.1", + "inherits": "2.0.3", + "minimalistic-assert": "1.0.0", + "minimalistic-crypto-utils": "1.0.1" + } + }, + "emojis-list": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-2.1.0.tgz", + "integrity": "sha1-TapNnbAPmBmIDHn6RXrlsJof04k=" + }, + "encoding": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.12.tgz", + "integrity": "sha1-U4tm8+5izRq1HsMjgp0flIDHS+s=", + "requires": { + "iconv-lite": "0.4.19" + } + }, + "end-of-stream": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-0.1.5.tgz", + "integrity": "sha1-jhdyBsPICDfYVjLouTWd/osvbq8=", + "requires": { + "once": "1.3.3" + }, + "dependencies": { + "once": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/once/-/once-1.3.3.tgz", + "integrity": "sha1-suJhVXzkwxTsgwTz+oJmPkKXyiA=", + "requires": { + "wrappy": "1.0.2" + } + } + } + }, + "enhanced-resolve": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-3.4.1.tgz", + "integrity": "sha1-BCHjOf1xQZs9oT0Smzl5BAIwR24=", + "requires": { + "graceful-fs": "4.1.11", + "memory-fs": "0.4.1", + "object-assign": "4.1.1", + "tapable": "0.2.8" + } + }, + "errno": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.4.tgz", + "integrity": "sha1-uJbiOp5ei6M4cfyZar02NfyaHH0=", + "requires": { + "prr": "0.0.0" + } + }, + "error-ex": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.1.tgz", + "integrity": "sha1-+FWobOYa3E6GIcPNoh56dhLDqNw=", + "requires": { + "is-arrayish": "0.2.1" + } + }, + "es-abstract": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.9.0.tgz", + "integrity": "sha512-kk3IJoKo7A3pWJc0OV8yZ/VEX2oSUytfekrJiqoxBlKJMFAJVJVpGdHClCCTdv+Fn2zHfpDHHIelMFhZVfef3Q==", + "requires": { + "es-to-primitive": "1.1.1", + "function-bind": "1.1.1", + "has": "1.0.1", + "is-callable": "1.1.3", + "is-regex": "1.0.4" + } + }, + "es-to-primitive": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.1.1.tgz", + "integrity": "sha1-RTVSSKiJeQNLZ5Lhm7gfK3l13Q0=", + "requires": { + "is-callable": "1.1.3", + "is-date-object": "1.0.1", + "is-symbol": "1.0.1" + } + }, + "es5-ext": { + "version": "0.10.35", + "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.35.tgz", + "integrity": "sha1-GO6FjOajxFx9eekcFfzKnsVoSU8=", + "requires": { + "es6-iterator": "2.0.3", + "es6-symbol": "3.1.1" + } + }, + "es6-iterator": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz", + "integrity": "sha1-p96IkUGgWpSwhUQDstCg+/qY87c=", + "requires": { + "d": "1.0.0", + "es5-ext": "0.10.35", + "es6-symbol": "3.1.1" + } + }, + "es6-map": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/es6-map/-/es6-map-0.1.5.tgz", + "integrity": "sha1-kTbgUD3MBqMBaQ8LsU/042TpSfA=", + "requires": { + "d": "1.0.0", + "es5-ext": "0.10.35", + "es6-iterator": "2.0.3", + "es6-set": "0.1.5", + "es6-symbol": "3.1.1", + "event-emitter": "0.3.5" + } + }, + "es6-promise": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-3.3.1.tgz", + "integrity": "sha1-oIzd6EzNvzTQJ6FFG8kdS80ophM=" + }, + "es6-set": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/es6-set/-/es6-set-0.1.5.tgz", + "integrity": "sha1-0rPsXU2ADO2BjbU40ol02wpzzLE=", + "requires": { + "d": "1.0.0", + "es5-ext": "0.10.35", + "es6-iterator": "2.0.3", + "es6-symbol": "3.1.1", + "event-emitter": "0.3.5" + } + }, + "es6-symbol": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.1.tgz", + "integrity": "sha1-vwDvT9q2uhtG7Le2KbTH7VcVzHc=", + "requires": { + "d": "1.0.0", + "es5-ext": "0.10.35" + } + }, + "es6-weak-map": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/es6-weak-map/-/es6-weak-map-2.0.2.tgz", + "integrity": "sha1-XjqzIlH/0VOKH45f+hNXdy+S2W8=", + "requires": { + "d": "1.0.0", + "es5-ext": "0.10.35", + "es6-iterator": "2.0.3", + "es6-symbol": "3.1.1" + } + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" + }, + "escope": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/escope/-/escope-3.6.0.tgz", + "integrity": "sha1-4Bl16BJ4GhY6ba392AOY3GTIicM=", + "requires": { + "es6-map": "0.1.5", + "es6-weak-map": "2.0.2", + "esrecurse": "4.2.0", + "estraverse": "4.2.0" + } + }, + "esformatter": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/esformatter/-/esformatter-0.10.0.tgz", + "integrity": "sha1-4yHsw9lAgzcs389cb5Qs72/sWdM=", + "requires": { + "acorn-to-esprima": "2.0.8", + "babel-traverse": "6.26.0", + "debug": "0.7.4", + "disparity": "2.0.0", + "esformatter-parser": "1.0.0", + "glob": "7.1.2", + "minimatch": "3.0.4", + "minimist": "1.2.0", + "mout": "1.1.0", + "npm-run": "3.0.0", + "resolve": "1.4.0", + "rocambole": "0.7.0", + "rocambole-indent": "2.0.4", + "rocambole-linebreak": "1.0.2", + "rocambole-node": "1.0.0", + "rocambole-token": "1.2.1", + "rocambole-whitespace": "1.0.0", + "stdin": "0.0.1", + "strip-json-comments": "0.1.3", + "supports-color": "1.3.1", + "user-home": "2.0.0" + }, + "dependencies": { + "debug": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-0.7.4.tgz", + "integrity": "sha1-BuHqgILCyxTjmAbiLi9vdX+Srzk=" + }, + "minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" + }, + "supports-color": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-1.3.1.tgz", + "integrity": "sha1-FXWN8J2P87SswwdTn6vicJXhBC0=" + } + } + }, + "esformatter-parser": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/esformatter-parser/-/esformatter-parser-1.0.0.tgz", + "integrity": "sha1-CFQHLQSHU57TnK442KVDLBfsEdM=", + "requires": { + "acorn-to-esprima": "2.0.8", + "babel-traverse": "6.26.0", + "babylon": "6.18.0", + "rocambole": "0.7.0" + } + }, + "eslint": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-4.8.0.tgz", + "integrity": "sha1-Ip7w41Tg5h2DfHqA/fuoJeGZgV4=", + "requires": { + "ajv": "5.2.3", + "babel-code-frame": "6.26.0", + "chalk": "2.2.0", + "concat-stream": "1.6.0", + "cross-spawn": "5.1.0", + "debug": "3.1.0", + "doctrine": "2.0.0", + "eslint-scope": "3.7.1", + "espree": "3.5.1", + "esquery": "1.0.0", + "estraverse": "4.2.0", + "esutils": "2.0.2", + "file-entry-cache": "2.0.0", + "functional-red-black-tree": "1.0.1", + "glob": "7.1.2", + "globals": "9.18.0", + "ignore": "3.3.6", + "imurmurhash": "0.1.4", + "inquirer": "3.3.0", + "is-resolvable": "1.0.0", + "js-yaml": "3.10.0", + "json-stable-stringify": "1.0.1", + "levn": "0.3.0", + "lodash": "4.17.4", + "minimatch": "3.0.4", + "mkdirp": "0.5.1", + "natural-compare": "1.4.0", + "optionator": "0.8.2", + "path-is-inside": "1.0.2", + "pluralize": "7.0.0", + "progress": "2.0.0", + "require-uncached": "1.0.3", + "semver": "5.4.1", + "strip-ansi": "4.0.0", + "strip-json-comments": "2.0.1", + "table": "4.0.2", + "text-table": "0.2.0" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=" + }, + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "requires": { + "ms": "2.0.0" + } + }, + "esprima": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.0.tgz", + "integrity": "sha512-oftTcaMu/EGrEIu904mWteKIv8vMuOgGYo7EhVJJN00R/EED9DCua/xxHRdYnKtcECzVg7xOWhflvJMnqcFZjw==" + }, + "js-yaml": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.10.0.tgz", + "integrity": "sha512-O2v52ffjLa9VeM43J4XocZE//WT9N0IiwDa3KSHH7Tu8CtH+1qM8SIZvnsTh6v+4yFy5KUY3BHUVwjpfAWsjIA==", + "requires": { + "argparse": "1.0.9", + "esprima": "4.0.0" + } + }, + "semver": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.4.1.tgz", + "integrity": "sha512-WfG/X9+oATh81XtllIo/I8gOiY9EXRdv1cQdyykeXK17YcUW3EXUAi2To4pcH6nZtJPr7ZOpM5OMyWJZm+8Rsg==" + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "requires": { + "ansi-regex": "3.0.0" + } + }, + "strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=" + } + } + }, + "eslint-loader": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/eslint-loader/-/eslint-loader-1.9.0.tgz", + "integrity": "sha512-40aN976qSNPyb9ejTqjEthZITpls1SVKtwguahmH1dzGCwQU/vySE+xX33VZmD8csU0ahVNCtFlsPgKqRBiqgg==", + "requires": { + "loader-fs-cache": "1.0.1", + "loader-utils": "1.1.0", + "object-assign": "4.1.1", + "object-hash": "1.2.0", + "rimraf": "2.6.2" + } + }, + "eslint-plugin-filenames": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-filenames/-/eslint-plugin-filenames-1.2.0.tgz", + "integrity": "sha1-runByQGJyV0uSZAsFg7O7+zZn1M=", + "requires": { + "lodash.camelcase": "4.3.0", + "lodash.kebabcase": "4.1.1", + "lodash.snakecase": "4.1.1", + "lodash.upperfirst": "4.3.1" + } + }, + "eslint-plugin-react": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.4.0.tgz", + "integrity": "sha512-tvjU9u3VqmW2vVuYnE8Qptq+6ji4JltjOjJ9u7VAOxVYkUkyBZWRvNYKbDv5fN+L6wiA+4we9+qQahZ0m63XEA==", + "requires": { + "doctrine": "2.0.0", + "has": "1.0.1", + "jsx-ast-utils": "2.0.1", + "prop-types": "15.6.0" + } + }, + "eslint-scope": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-3.7.1.tgz", + "integrity": "sha1-PWPD7f2gLgbgGkUq2IyqzHzctug=", + "requires": { + "esrecurse": "4.2.0", + "estraverse": "4.2.0" + } + }, + "espree": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-3.5.1.tgz", + "integrity": "sha1-DJiLirRttTEAoZVK5LqZXd0n2H4=", + "requires": { + "acorn": "5.1.2", + "acorn-jsx": "3.0.1" + } + }, + "esprima": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-2.7.3.tgz", + "integrity": "sha1-luO3DVd59q1JzQMmc9HDEnZ7pYE=" + }, + "esprint": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/esprint/-/esprint-0.4.0.tgz", + "integrity": "sha1-+JybrONtkEB5aKj5zrCAD/eGqrA=", + "requires": { + "dnode": "1.2.2", + "fb-watchman": "2.0.0", + "glob": "7.1.2", + "sane": "1.7.0", + "worker-farm": "1.5.0", + "yargs": "8.0.2" + } + }, + "esquery": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.0.0.tgz", + "integrity": "sha1-z7qLV9f7qT8XKYqKAGoEzaE9gPo=", + "requires": { + "estraverse": "4.2.0" + } + }, + "esrecurse": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.2.0.tgz", + "integrity": "sha1-+pVo2Y04I/mkHZHpAtyrnqblsWM=", + "requires": { + "estraverse": "4.2.0", + "object-assign": "4.1.1" + } + }, + "estraverse": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.2.0.tgz", + "integrity": "sha1-De4/7TH81GlhjOc0IJn8GvoL2xM=" + }, + "esutils": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", + "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=" + }, + "event-emitter": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz", + "integrity": "sha1-34xp7vFkeSPHFXuc6DhAYQsCzDk=", + "requires": { + "d": "1.0.0", + "es5-ext": "0.10.35" + } + }, + "event-stream": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/event-stream/-/event-stream-3.3.4.tgz", + "integrity": "sha1-SrTJoPWlTbkzi0w02Gv86PSzVXE=", + "requires": { + "duplexer": "0.1.1", + "from": "0.1.7", + "map-stream": "0.1.0", + "pause-stream": "0.0.11", + "split": "0.3.3", + "stream-combiner": "0.0.4", + "through": "2.3.8" + } + }, + "events": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz", + "integrity": "sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ=" + }, + "evp_bytestokey": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz", + "integrity": "sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==", + "requires": { + "md5.js": "1.3.4", + "safe-buffer": "5.1.1" + } + }, + "exec-sh": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/exec-sh/-/exec-sh-0.2.1.tgz", + "integrity": "sha512-aLt95pexaugVtQerpmE51+4QfWrNc304uez7jvj6fWnN8GeEHpttB8F36n8N7uVhUMbH/1enbxQ9HImZ4w/9qg==", + "requires": { + "merge": "1.2.0" + } + }, + "execa": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-0.7.0.tgz", + "integrity": "sha1-lEvs00zEHuMqY6n68nrVpl/Fl3c=", + "requires": { + "cross-spawn": "5.1.0", + "get-stream": "3.0.0", + "is-stream": "1.1.0", + "npm-run-path": "2.0.2", + "p-finally": "1.0.0", + "signal-exit": "3.0.2", + "strip-eof": "1.0.0" + } + }, + "execall": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/execall/-/execall-1.0.0.tgz", + "integrity": "sha1-c9CQTjlbPKsGWLCNCewlMH8pu3M=", + "requires": { + "clone-regexp": "1.0.0" + } + }, + "exenv": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/exenv/-/exenv-1.2.2.tgz", + "integrity": "sha1-KueOhdmJQVhnCwPUe+wfA72Ru50=" + }, + "expand-brackets": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-0.1.5.tgz", + "integrity": "sha1-3wcoTjQqgHzXM6xa9yQR5YHRF3s=", + "requires": { + "is-posix-bracket": "0.1.1" + } + }, + "expand-range": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/expand-range/-/expand-range-1.8.2.tgz", + "integrity": "sha1-opnv/TNf4nIeuujiV+x5ZE/IUzc=", + "requires": { + "fill-range": "2.2.3" + } + }, + "expand-tilde": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/expand-tilde/-/expand-tilde-1.2.2.tgz", + "integrity": "sha1-C4HrqJflo9MdHD0QL48BRB5VlEk=", + "requires": { + "os-homedir": "1.0.2" + } + }, + "extend": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.1.tgz", + "integrity": "sha1-p1Xqe8Gt/MWjHOfnYtuq3F5jZEQ=" + }, + "external-editor": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-2.0.5.tgz", + "integrity": "sha512-Msjo64WT5W+NhOpQXh0nOHm+n0RfU1QUwDnKYvJ8dEJ8zlwLrqXNTv5mSUTJpepf41PDJGyhueTw2vNZW+Fr/w==", + "requires": { + "iconv-lite": "0.4.19", + "jschardet": "1.5.1", + "tmp": "0.0.33" + } + }, + "extglob": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/extglob/-/extglob-0.3.2.tgz", + "integrity": "sha1-Lhj/PS9JqydlzskCPwEdqo2DSaE=", + "requires": { + "is-extglob": "1.0.0" + } + }, + "extract-text-webpack-plugin": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/extract-text-webpack-plugin/-/extract-text-webpack-plugin-3.0.1.tgz", + "integrity": "sha512-zv0/Cg2mU8uMzeQQ3oyfJvZU4Iv/GbQYUIr/HU+8pZetT/0W3xj6XAbxoG4gsp8SbnYcFd4BOsCAZPl9NvplPw==", + "requires": { + "async": "2.5.0", + "loader-utils": "1.1.0", + "schema-utils": "0.3.0", + "webpack-sources": "1.0.1" + } + }, + "fancy-log": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fancy-log/-/fancy-log-1.3.0.tgz", + "integrity": "sha1-Rb4X0Cu5kX1gzP/UmVyZnmyMmUg=", + "requires": { + "chalk": "1.1.3", + "time-stamp": "1.1.0" + }, + "dependencies": { + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=" + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "requires": { + "ansi-styles": "2.2.1", + "escape-string-regexp": "1.0.5", + "has-ansi": "2.0.0", + "strip-ansi": "3.0.1", + "supports-color": "2.0.0" + } + }, + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" + } + } + }, + "fast-deep-equal": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.0.0.tgz", + "integrity": "sha1-liVqO8l1WV6zbYLpkp0GDYk0Of8=" + }, + "fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=" + }, + "fastparse": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/fastparse/-/fastparse-1.1.1.tgz", + "integrity": "sha1-0eJkOzipTXWDtHkGDmxK/8lAcfg=" + }, + "faye-websocket": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.7.3.tgz", + "integrity": "sha1-zEB0x/Sk39A69U3WXDVLE1EyzhE=", + "requires": { + "websocket-driver": "0.7.0" + } + }, + "fb-watchman": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.0.tgz", + "integrity": "sha1-VOmr99+i8mzZsWNsWIwa/AXeXVg=", + "requires": { + "bser": "2.0.0" + } + }, + "fbjs": { + "version": "0.8.16", + "resolved": "https://registry.npmjs.org/fbjs/-/fbjs-0.8.16.tgz", + "integrity": "sha1-XmdDL1UNxBtXK/VYR7ispk5TN9s=", + "requires": { + "core-js": "1.2.7", + "isomorphic-fetch": "2.2.1", + "loose-envify": "1.3.1", + "object-assign": "4.1.1", + "promise": "7.3.1", + "setimmediate": "1.0.5", + "ua-parser-js": "0.7.17" + }, + "dependencies": { + "core-js": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-1.2.7.tgz", + "integrity": "sha1-ZSKUwUZR2yj6k70tX/KYOk8IxjY=" + } + } + }, + "figures": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz", + "integrity": "sha1-OrGi0qYsi/tDGgyUy3l6L84nyWI=", + "requires": { + "escape-string-regexp": "1.0.5" + } + }, + "file-entry-cache": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-2.0.0.tgz", + "integrity": "sha1-w5KZDD5oR4PYOLjISkXYoEhFg2E=", + "requires": { + "flat-cache": "1.3.0", + "object-assign": "4.1.1" + } + }, + "file-loader": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/file-loader/-/file-loader-1.1.5.tgz", + "integrity": "sha512-RzGHDatcVNpGISTvCpfUfOGpYuSR7HSsSg87ki+wF6rw1Hm0RALPTiAdsxAq1UwLf0RRhbe22/eHK6nhXspiOQ==", + "requires": { + "loader-utils": "1.1.0", + "schema-utils": "0.3.0" + } + }, + "filename-regex": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/filename-regex/-/filename-regex-2.0.1.tgz", + "integrity": "sha1-wcS5vuPglyXdsQa3XB4wH+LxiyY=" + }, + "filesize": { + "version": "3.5.10", + "resolved": "https://registry.npmjs.org/filesize/-/filesize-3.5.10.tgz", + "integrity": "sha1-/I+iPdtO+eXgq24eZPZ5okpWdh8=" + }, + "fill-range": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-2.2.3.tgz", + "integrity": "sha1-ULd9/X5Gm8dJJHCWNpn+eoSFpyM=", + "requires": { + "is-number": "2.1.0", + "isobject": "2.1.0", + "randomatic": "1.1.7", + "repeat-element": "1.1.2", + "repeat-string": "1.6.1" + } + }, + "find-cache-dir": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-1.0.0.tgz", + "integrity": "sha1-kojj6ePMN0hxfTnq3hfPcfww7m8=", + "requires": { + "commondir": "1.0.1", + "make-dir": "1.0.0", + "pkg-dir": "2.0.0" + } + }, + "find-index": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/find-index/-/find-index-0.1.1.tgz", + "integrity": "sha1-Z101iyyjiS15Whq0cjL4tuLg3eQ=" + }, + "find-up": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", + "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", + "requires": { + "locate-path": "2.0.0" + } + }, + "findup-sync": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-0.4.3.tgz", + "integrity": "sha1-QAQ5Kee8YK3wt/SCfExudaDeyhI=", + "requires": { + "detect-file": "0.1.0", + "is-glob": "2.0.1", + "micromatch": "2.3.11", + "resolve-dir": "0.1.1" + } + }, + "fined": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fined/-/fined-1.1.0.tgz", + "integrity": "sha1-s33IRLdqL15wgeiE98CuNE8VNHY=", + "requires": { + "expand-tilde": "2.0.2", + "is-plain-object": "2.0.4", + "object.defaults": "1.1.0", + "object.pick": "1.3.0", + "parse-filepath": "1.0.1" + }, + "dependencies": { + "expand-tilde": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/expand-tilde/-/expand-tilde-2.0.2.tgz", + "integrity": "sha1-l+gBqgUt8CRU3kawK/YhZCzchQI=", + "requires": { + "homedir-polyfill": "1.0.1" + } + } + } + }, + "first-chunk-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/first-chunk-stream/-/first-chunk-stream-1.0.0.tgz", + "integrity": "sha1-Wb+1DNkF9g18OUzT2ayqtOatk04=" + }, + "flagged-respawn": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/flagged-respawn/-/flagged-respawn-0.3.2.tgz", + "integrity": "sha1-/xke3c1wiKZ1smEP/8l2vpuAdLU=" + }, + "flat-cache": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-1.3.0.tgz", + "integrity": "sha1-0wMLMrOBVPTjt+nHCfSQ9++XxIE=", + "requires": { + "circular-json": "0.3.3", + "del": "2.2.2", + "graceful-fs": "4.1.11", + "write": "0.2.1" + }, + "dependencies": { + "del": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/del/-/del-2.2.2.tgz", + "integrity": "sha1-wSyYHQZ4RshLyvhiz/kw2Qf/0ag=", + "requires": { + "globby": "5.0.0", + "is-path-cwd": "1.0.0", + "is-path-in-cwd": "1.0.0", + "object-assign": "4.1.1", + "pify": "2.3.0", + "pinkie-promise": "2.0.1", + "rimraf": "2.6.2" + } + }, + "globby": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-5.0.0.tgz", + "integrity": "sha1-69hGZ8oNuzMLmbz8aOrCvFQ3Dg0=", + "requires": { + "array-union": "1.0.2", + "arrify": "1.0.1", + "glob": "7.1.2", + "object-assign": "4.1.1", + "pify": "2.3.0", + "pinkie-promise": "2.0.1" + } + } + } + }, + "flatten": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/flatten/-/flatten-1.0.2.tgz", + "integrity": "sha1-2uRqnXj74lKSJYzB54CkHZXAN4I=" + }, + "for-in": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", + "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=" + }, + "for-own": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/for-own/-/for-own-0.1.5.tgz", + "integrity": "sha1-UmXGgaTylNq78XyVCbZ2OqhFEM4=", + "requires": { + "for-in": "1.0.2" + } + }, + "foreach": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/foreach/-/foreach-2.0.5.tgz", + "integrity": "sha1-C+4AUBiusmDQo6865ljdATbsG5k=" + }, + "from": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/from/-/from-0.1.7.tgz", + "integrity": "sha1-g8YK/Fi5xWmXAH7Rp2izqzA6RP4=" + }, + "fs-exists-sync": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/fs-exists-sync/-/fs-exists-sync-0.1.0.tgz", + "integrity": "sha1-mC1ok6+RjnLQjeyehnP/K1qNat0=" + }, + "fs-readfile-promise": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fs-readfile-promise/-/fs-readfile-promise-2.0.1.tgz", + "integrity": "sha1-gAI4I5gfn//+AWCei+Zo9prknnA=", + "requires": { + "graceful-fs": "4.1.11" + } + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" + }, + "fstream": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.11.tgz", + "integrity": "sha1-XB+x8RdHcRTwYyoOtLcbPLD9MXE=", + "requires": { + "graceful-fs": "4.1.11", + "inherits": "2.0.3", + "mkdirp": "0.5.1", + "rimraf": "2.6.2" + } + }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + }, + "functional-red-black-tree": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", + "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=" + }, + "gaze": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/gaze/-/gaze-0.5.2.tgz", + "integrity": "sha1-QLcJU30k0dRXZ9takIaJ3+aaxE8=", + "requires": { + "globule": "0.1.0" + } + }, + "get-caller-file": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.2.tgz", + "integrity": "sha1-9wLmMSfn4jHBYKgMFVSstw1QR+U=" + }, + "get-node-dimensions": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/get-node-dimensions/-/get-node-dimensions-1.2.0.tgz", + "integrity": "sha1-lSmOMqdSoVXynrcQ4GlVe0p/6Yw=" + }, + "get-stdin": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-5.0.1.tgz", + "integrity": "sha1-Ei4WFZHiH/TFJTAwVpPyDmOTo5g=" + }, + "get-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", + "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=" + }, + "glob": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", + "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", + "requires": { + "fs.realpath": "1.0.0", + "inflight": "1.0.6", + "inherits": "2.0.3", + "minimatch": "3.0.4", + "once": "1.4.0", + "path-is-absolute": "1.0.1" + } + }, + "glob-base": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/glob-base/-/glob-base-0.3.0.tgz", + "integrity": "sha1-27Fk9iIbHAscz4Kuoyi0l98Oo8Q=", + "requires": { + "glob-parent": "2.0.0", + "is-glob": "2.0.1" + } + }, + "glob-parent": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-2.0.0.tgz", + "integrity": "sha1-gTg9ctsFT8zPUzbaqQLxgvbtuyg=", + "requires": { + "is-glob": "2.0.1" + } + }, + "glob-stream": { + "version": "3.1.18", + "resolved": "https://registry.npmjs.org/glob-stream/-/glob-stream-3.1.18.tgz", + "integrity": "sha1-kXCl8St5Awb9/lmPMT+PeVT9FDs=", + "requires": { + "glob": "4.5.3", + "glob2base": "0.0.12", + "minimatch": "2.0.10", + "ordered-read-streams": "0.1.0", + "through2": "0.6.5", + "unique-stream": "1.0.0" + }, + "dependencies": { + "glob": { + "version": "4.5.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-4.5.3.tgz", + "integrity": "sha1-xstz0yJsHv7wTePFbQEvAzd+4V8=", + "requires": { + "inflight": "1.0.6", + "inherits": "2.0.3", + "minimatch": "2.0.10", + "once": "1.4.0" + } + }, + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" + }, + "minimatch": { + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-2.0.10.tgz", + "integrity": "sha1-jQh8OcazjAAbl/ynzm0OHoCvusc=", + "requires": { + "brace-expansion": "1.1.8" + } + }, + "readable-stream": { + "version": "1.0.34", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", + "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", + "requires": { + "core-util-is": "1.0.2", + "inherits": "2.0.3", + "isarray": "0.0.1", + "string_decoder": "0.10.31" + } + }, + "string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" + }, + "through2": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-0.6.5.tgz", + "integrity": "sha1-QaucZ7KdVyCQcUEOHXp6lozTrUg=", + "requires": { + "readable-stream": "1.0.34", + "xtend": "4.0.1" + } + } + } + }, + "glob-watcher": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/glob-watcher/-/glob-watcher-0.0.6.tgz", + "integrity": "sha1-uVtKjfdLOcgymLDAXJeLTZo7cQs=", + "requires": { + "gaze": "0.5.2" + } + }, + "glob2base": { + "version": "0.0.12", + "resolved": "https://registry.npmjs.org/glob2base/-/glob2base-0.0.12.tgz", + "integrity": "sha1-nUGbPijxLoOjYhZKJ3BVkiycDVY=", + "requires": { + "find-index": "0.1.1" + } + }, + "global-modules": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-0.2.3.tgz", + "integrity": "sha1-6lo77ULG1s6ZWk+KEmm12uIjgo0=", + "requires": { + "global-prefix": "0.1.5", + "is-windows": "0.2.0" + } + }, + "global-prefix": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-0.1.5.tgz", + "integrity": "sha1-jTvGuNo8qBEqFg2NSW/wRiv+948=", + "requires": { + "homedir-polyfill": "1.0.1", + "ini": "1.3.4", + "is-windows": "0.2.0", + "which": "1.3.0" + } + }, + "globals": { + "version": "9.18.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-9.18.0.tgz", + "integrity": "sha512-S0nG3CLEQiY/ILxqtztTWH/3iRRdyBLw6KMDxnKMchrtbj2OFmehVh0WUCfW3DUrIgx/qFrJPICrq4Z4sTR9UQ==" + }, + "globby": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-6.1.0.tgz", + "integrity": "sha1-9abXDoOV4hyFj7BInWTfAkJNUGw=", + "requires": { + "array-union": "1.0.2", + "glob": "7.1.2", + "object-assign": "4.1.1", + "pify": "2.3.0", + "pinkie-promise": "2.0.1" + } + }, + "globjoin": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/globjoin/-/globjoin-0.1.4.tgz", + "integrity": "sha1-L0SUrIkZ43Z8XLtpHp9GMyQoXUM=" + }, + "globule": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/globule/-/globule-0.1.0.tgz", + "integrity": "sha1-2cjt3h2nnRJaFRt5UzuXhnY0auU=", + "requires": { + "glob": "3.1.21", + "lodash": "1.0.2", + "minimatch": "0.2.14" + }, + "dependencies": { + "glob": { + "version": "3.1.21", + "resolved": "https://registry.npmjs.org/glob/-/glob-3.1.21.tgz", + "integrity": "sha1-0p4KBV3qUTj00H7UDomC6DwgZs0=", + "requires": { + "graceful-fs": "1.2.3", + "inherits": "1.0.2", + "minimatch": "0.2.14" + } + }, + "graceful-fs": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-1.2.3.tgz", + "integrity": "sha1-FaSAaldUfLLS2/J/QuiajDRRs2Q=" + }, + "inherits": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-1.0.2.tgz", + "integrity": "sha1-ykMJ2t7mtUzAuNJH6NfHoJdb3Js=" + }, + "lodash": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-1.0.2.tgz", + "integrity": "sha1-j1dWDIO1n8JwvT1WG2kAQ0MOJVE=" + }, + "lru-cache": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-2.7.3.tgz", + "integrity": "sha1-bUUk6LlV+V1PW1iFHOId1y+06VI=" + }, + "minimatch": { + "version": "0.2.14", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-0.2.14.tgz", + "integrity": "sha1-x054BXT2PG+aCQ6Q775u9TpqdWo=", + "requires": { + "lru-cache": "2.7.3", + "sigmund": "1.0.1" + } + } + } + }, + "glogg": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/glogg/-/glogg-1.0.0.tgz", + "integrity": "sha1-f+DxmfV6yQbPUS/urY+Q7kooT8U=", + "requires": { + "sparkles": "1.0.0" + } + }, + "good-listener": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/good-listener/-/good-listener-1.2.2.tgz", + "integrity": "sha1-1TswzfkxPf+33JoNR3CWqm0UXFA=", + "requires": { + "delegate": "3.1.3" + } + }, + "graceful-fs": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", + "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=" + }, + "gulp": { + "version": "3.9.1", + "resolved": "https://registry.npmjs.org/gulp/-/gulp-3.9.1.tgz", + "integrity": "sha1-VxzkWSjdQK9lFPxAEYZgFsE4RbQ=", + "requires": { + "archy": "1.0.0", + "chalk": "1.1.3", + "deprecated": "0.0.1", + "gulp-util": "3.0.8", + "interpret": "1.0.4", + "liftoff": "2.3.0", + "minimist": "1.2.0", + "orchestrator": "0.3.8", + "pretty-hrtime": "1.0.3", + "semver": "4.3.6", + "tildify": "1.2.0", + "v8flags": "2.1.1", + "vinyl-fs": "0.3.14" + }, + "dependencies": { + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=" + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "requires": { + "ansi-styles": "2.2.1", + "escape-string-regexp": "1.0.5", + "has-ansi": "2.0.0", + "strip-ansi": "3.0.1", + "supports-color": "2.0.0" + } + }, + "minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" + }, + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" + } + } + }, + "gulp-cached": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/gulp-cached/-/gulp-cached-1.1.1.tgz", + "integrity": "sha1-/nzU+H83YB5gc8/t7lwr2vi2rM4=", + "requires": { + "lodash.defaults": "4.2.0", + "through2": "2.0.3" + } + }, + "gulp-clean-css": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/gulp-clean-css/-/gulp-clean-css-3.9.0.tgz", + "integrity": "sha512-CsqaSO2ZTMQI/WwbWloZWBudhsRMKgxBthzxt4bbcbWrjOY4pRFziyK9IH6YbTpaWAPKEwWpopPkpiAEoDofxw==", + "requires": { + "clean-css": "4.1.9", + "gulp-util": "3.0.8", + "through2": "2.0.3", + "vinyl-sourcemaps-apply": "0.2.1" + } + }, + "gulp-concat": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/gulp-concat/-/gulp-concat-2.6.1.tgz", + "integrity": "sha1-Yz0WyV2IUEYorQJmVmPO5aR5M1M=", + "requires": { + "concat-with-sourcemaps": "1.0.4", + "through2": "2.0.3", + "vinyl": "2.1.0" + }, + "dependencies": { + "clone": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.1.tgz", + "integrity": "sha1-0hfR6WERjjrJpLi7oyhVU79kfNs=" + }, + "clone-stats": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/clone-stats/-/clone-stats-1.0.0.tgz", + "integrity": "sha1-s3gt/4u1R04Yuba/D9/ngvh3doA=" + }, + "replace-ext": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-1.0.0.tgz", + "integrity": "sha1-3mMSg3P8v3w8z6TeWkgMRaZ5WOs=" + }, + "vinyl": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-2.1.0.tgz", + "integrity": "sha1-Ah+cLPlR1rk5lDyJ617lrdT9kkw=", + "requires": { + "clone": "2.1.1", + "clone-buffer": "1.0.0", + "clone-stats": "1.0.0", + "cloneable-readable": "1.0.0", + "remove-trailing-separator": "1.1.0", + "replace-ext": "1.0.0" + } + } + } + }, + "gulp-declare": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/gulp-declare/-/gulp-declare-0.3.0.tgz", + "integrity": "sha1-hoMPxvqojgY4IWLIZkuOlJV6/Nk=", + "requires": { + "nsdeclare": "0.1.0", + "vinyl-map": "1.0.2", + "xtend": "4.0.1" + } + }, + "gulp-livereload": { + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/gulp-livereload/-/gulp-livereload-3.8.1.tgz", + "integrity": "sha1-APdEstdJ0+njdGWJyKRKysd5tQ8=", + "requires": { + "chalk": "0.5.1", + "debug": "2.6.9", + "event-stream": "3.3.4", + "gulp-util": "3.0.8", + "lodash.assign": "3.2.0", + "mini-lr": "0.1.9" + }, + "dependencies": { + "ansi-regex": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-0.2.1.tgz", + "integrity": "sha1-DY6UaWej2BQ/k+JOKYUl/BsiNfk=" + }, + "ansi-styles": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-1.1.0.tgz", + "integrity": "sha1-6uy/Zs1waIJ2Cy9GkVgrj1XXp94=" + }, + "chalk": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-0.5.1.tgz", + "integrity": "sha1-Zjs6ZItotV0EaQ1JFnqoN4WPIXQ=", + "requires": { + "ansi-styles": "1.1.0", + "escape-string-regexp": "1.0.5", + "has-ansi": "0.1.0", + "strip-ansi": "0.3.0", + "supports-color": "0.2.0" + } + }, + "has-ansi": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-0.1.0.tgz", + "integrity": "sha1-hPJlqujA5qiKEtcCKJS3VoiUxi4=", + "requires": { + "ansi-regex": "0.2.1" + } + }, + "strip-ansi": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-0.3.0.tgz", + "integrity": "sha1-JfSOoiynkYfzF0pNuHWTR7sSYiA=", + "requires": { + "ansi-regex": "0.2.1" + } + }, + "supports-color": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-0.2.0.tgz", + "integrity": "sha1-2S3iaU6z9nMjlz1649i1W0wiGQo=" + } + } + }, + "gulp-postcss": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/gulp-postcss/-/gulp-postcss-7.0.0.tgz", + "integrity": "sha1-z7YqGfqUf4vmfOnsronOuVnwz5M=", + "requires": { + "gulp-util": "3.0.8", + "postcss": "6.0.13", + "postcss-load-config": "1.2.0", + "vinyl-sourcemaps-apply": "0.2.1" + } + }, + "gulp-print": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/gulp-print/-/gulp-print-2.0.1.tgz", + "integrity": "sha1-Gs7ljqyK8tPErTMp2+RldYOTxBQ=", + "requires": { + "gulp-util": "3.0.8", + "map-stream": "0.0.7" + }, + "dependencies": { + "map-stream": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/map-stream/-/map-stream-0.0.7.tgz", + "integrity": "sha1-ih8HiW2CsQkmvTdEokIACfiJdKg=" + } + } + }, + "gulp-sourcemaps": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/gulp-sourcemaps/-/gulp-sourcemaps-2.6.1.tgz", + "integrity": "sha512-1qHCI3hdmsMdq/SUotxwUh/L8YzlI6J9zQ5ifNOtx4Y6KV5y5sGuORv1KZzWhuKtz/mXNh5xLESUtwC4EndCjA==", + "requires": { + "@gulp-sourcemaps/identity-map": "1.0.1", + "@gulp-sourcemaps/map-sources": "1.0.0", + "acorn": "4.0.13", + "convert-source-map": "1.5.0", + "css": "2.2.1", + "debug-fabulous": "0.2.1", + "detect-newline": "2.1.0", + "graceful-fs": "4.1.11", + "source-map": "0.6.1", + "strip-bom-string": "1.0.0", + "through2": "2.0.3", + "vinyl": "1.2.0" + }, + "dependencies": { + "acorn": { + "version": "4.0.13", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-4.0.13.tgz", + "integrity": "sha1-EFSVrlNh1pe9GVyCUZLhrX8lN4c=" + }, + "vinyl": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-1.2.0.tgz", + "integrity": "sha1-XIgDbPVl5d8FVYv8kR+GVt8hiIQ=", + "requires": { + "clone": "1.0.2", + "clone-stats": "0.0.1", + "replace-ext": "0.0.1" + } + } + } + }, + "gulp-stripbom": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/gulp-stripbom/-/gulp-stripbom-1.0.4.tgz", + "integrity": "sha1-WMHQPoXgCKeqtH2BsSl8jBvIKOs=", + "requires": { + "gulp-util": "3.0.8", + "log-symbols": "1.0.2", + "strip-bom": "1.0.0", + "through2": "0.5.1" + }, + "dependencies": { + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" + }, + "readable-stream": { + "version": "1.0.34", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", + "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", + "requires": { + "core-util-is": "1.0.2", + "inherits": "2.0.3", + "isarray": "0.0.1", + "string_decoder": "0.10.31" + } + }, + "string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" + }, + "strip-bom": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-1.0.0.tgz", + "integrity": "sha1-hbiGLzhEtabV7IRnqTWYFzo295Q=", + "requires": { + "first-chunk-stream": "1.0.0", + "is-utf8": "0.2.1" + } + }, + "through2": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/through2/-/through2-0.5.1.tgz", + "integrity": "sha1-390BLrnHAOIyP9M084rGIqs3Lac=", + "requires": { + "readable-stream": "1.0.34", + "xtend": "3.0.0" + } + }, + "xtend": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-3.0.0.tgz", + "integrity": "sha1-XM50B7r2Qsunvs2laBEcST9ZZlo=" + } + } + }, + "gulp-util": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/gulp-util/-/gulp-util-3.0.8.tgz", + "integrity": "sha1-AFTh50RQLifATBh8PsxQXdVLu08=", + "requires": { + "array-differ": "1.0.0", + "array-uniq": "1.0.3", + "beeper": "1.1.1", + "chalk": "1.1.3", + "dateformat": "2.2.0", + "fancy-log": "1.3.0", + "gulplog": "1.0.0", + "has-gulplog": "0.1.0", + "lodash._reescape": "3.0.0", + "lodash._reevaluate": "3.0.0", + "lodash._reinterpolate": "3.0.0", + "lodash.template": "3.6.2", + "minimist": "1.2.0", + "multipipe": "0.1.2", + "object-assign": "3.0.0", + "replace-ext": "0.0.1", + "through2": "2.0.3", + "vinyl": "0.5.3" + }, + "dependencies": { + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=" + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "requires": { + "ansi-styles": "2.2.1", + "escape-string-regexp": "1.0.5", + "has-ansi": "2.0.0", + "strip-ansi": "3.0.1", + "supports-color": "2.0.0" + } + }, + "minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" + }, + "object-assign": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-3.0.0.tgz", + "integrity": "sha1-m+3VygiXlJvKR+f/QIBi1Un1h/I=" + }, + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" + } + } + }, + "gulp-watch": { + "version": "4.3.11", + "resolved": "https://registry.npmjs.org/gulp-watch/-/gulp-watch-4.3.11.tgz", + "integrity": "sha1-Fi/FY96fx3DpH5p845VVE6mhGMA=", + "requires": { + "anymatch": "1.3.2", + "chokidar": "1.7.0", + "glob-parent": "3.1.0", + "gulp-util": "3.0.8", + "object-assign": "4.1.1", + "path-is-absolute": "1.0.1", + "readable-stream": "2.3.3", + "slash": "1.0.0", + "vinyl": "1.2.0", + "vinyl-file": "2.0.0" + }, + "dependencies": { + "glob-parent": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", + "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=", + "requires": { + "is-glob": "3.1.0", + "path-dirname": "1.0.2" + } + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=" + }, + "is-glob": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", + "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", + "requires": { + "is-extglob": "2.1.1" + } + }, + "vinyl": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-1.2.0.tgz", + "integrity": "sha1-XIgDbPVl5d8FVYv8kR+GVt8hiIQ=", + "requires": { + "clone": "1.0.2", + "clone-stats": "0.0.1", + "replace-ext": "0.0.1" + } + } + } + }, + "gulp-wrap": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/gulp-wrap/-/gulp-wrap-0.13.0.tgz", + "integrity": "sha1-kPsLSieiZkM4Mv98YSLbXB7olMY=", + "requires": { + "consolidate": "0.14.5", + "es6-promise": "3.3.1", + "fs-readfile-promise": "2.0.1", + "gulp-util": "3.0.8", + "js-yaml": "3.7.0", + "lodash": "4.17.4", + "node.extend": "1.1.6", + "through2": "2.0.3", + "tryit": "1.0.3", + "vinyl-bufferstream": "1.0.1" + } + }, + "gulplog": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/gulplog/-/gulplog-1.0.0.tgz", + "integrity": "sha1-4oxNRdBey77YGDY86PnFkmIp/+U=", + "requires": { + "glogg": "1.0.0" + } + }, + "has": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.1.tgz", + "integrity": "sha1-hGFzP1OLCDfJNh45qauelwTcLyg=", + "requires": { + "function-bind": "1.1.1" + } + }, + "has-ansi": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", + "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", + "requires": { + "ansi-regex": "2.1.1" + } + }, + "has-flag": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-2.0.0.tgz", + "integrity": "sha1-6CB68cx7MNRGzHC3NLXovhj4jVE=" + }, + "has-gulplog": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/has-gulplog/-/has-gulplog-0.1.0.tgz", + "integrity": "sha1-ZBTIKRNpfaUVkDl9r7EvIpZ4Ec4=", + "requires": { + "sparkles": "1.0.0" + } + }, + "hash-base": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-2.0.2.tgz", + "integrity": "sha1-ZuodhW206KVHDK32/OI65SRO8uE=", + "requires": { + "inherits": "2.0.3" + } + }, + "hash.js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.3.tgz", + "integrity": "sha512-/UETyP0W22QILqS+6HowevwhEFJ3MBJnwTf75Qob9Wz9t0DPuisL8kW8YZMK62dHAKE1c1p+gY1TtOLY+USEHA==", + "requires": { + "inherits": "2.0.3", + "minimalistic-assert": "1.0.0" + } + }, + "history": { + "version": "4.7.2", + "resolved": "https://registry.npmjs.org/history/-/history-4.7.2.tgz", + "integrity": "sha512-1zkBRWW6XweO0NBcjiphtVJVsIQ+SXF29z9DVkceeaSLVMFXHool+fdCZD4spDCfZJCILPILc3bm7Bc+HRi0nA==", + "requires": { + "invariant": "2.2.2", + "loose-envify": "1.3.1", + "resolve-pathname": "2.2.0", + "value-equal": "0.4.0", + "warning": "3.0.0" + } + }, + "hmac-drbg": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", + "integrity": "sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=", + "requires": { + "hash.js": "1.1.3", + "minimalistic-assert": "1.0.0", + "minimalistic-crypto-utils": "1.0.1" + } + }, + "hoist-non-react-statics": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-2.3.1.tgz", + "integrity": "sha1-ND24TGAYxlB3iJgkATWhQg7iLOA=" + }, + "home-or-tmp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/home-or-tmp/-/home-or-tmp-2.0.0.tgz", + "integrity": "sha1-42w/LSyufXRqhX440Y1fMqeILbg=", + "requires": { + "os-homedir": "1.0.2", + "os-tmpdir": "1.0.2" + } + }, + "homedir-polyfill": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.1.tgz", + "integrity": "sha1-TCu8inWJmP7r9e1oWA921GdotLw=", + "requires": { + "parse-passwd": "1.0.0" + } + }, + "hosted-git-info": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.5.0.tgz", + "integrity": "sha512-pNgbURSuab90KbTqvRPsseaTxOJCZBD0a7t+haSN33piP9cCM4l0CqdzAif2hUqm716UovKB2ROmiabGAKVXyg==" + }, + "html-comment-regex": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/html-comment-regex/-/html-comment-regex-1.1.1.tgz", + "integrity": "sha1-ZouTd26q5V696POtRkswekljYl4=" + }, + "html-tags": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/html-tags/-/html-tags-2.0.0.tgz", + "integrity": "sha1-ELMKOGCF9Dzt41PMj6fLDe7qZos=" + }, + "http-errors": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.3.1.tgz", + "integrity": "sha1-GX4izevUGYWF6GlO9nhhl7ke2UI=", + "requires": { + "inherits": "2.0.3", + "statuses": "1.4.0" + } + }, + "http-parser-js": { + "version": "0.4.9", + "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.4.9.tgz", + "integrity": "sha1-6hoE+2St/wJC6ZdPKX3Uw8rSceE=" + }, + "https-browserify": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-0.0.1.tgz", + "integrity": "sha1-P5E2XKvmC3ftDruiS0VOPgnZWoI=" + }, + "iconv-lite": { + "version": "0.4.19", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.19.tgz", + "integrity": "sha512-oTZqweIP51xaGPI4uPa56/Pri/480R+mo7SeU+YETByQNhDG55ycFyNLIgta9vXhILrxXDmF7ZGhqZIcuN0gJQ==" + }, + "icss-replace-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/icss-replace-symbols/-/icss-replace-symbols-1.1.0.tgz", + "integrity": "sha1-Bupvg2ead0njhs/h/oEq5dsiPe0=" + }, + "icss-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-2.1.0.tgz", + "integrity": "sha1-g/Cg7DeL8yRheLbCrZE28TWxyWI=", + "requires": { + "postcss": "6.0.13" + } + }, + "ieee754": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.8.tgz", + "integrity": "sha1-vjPUCsEO8ZJnAfbwii2G+/0a0+Q=" + }, + "ignore": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-3.3.6.tgz", + "integrity": "sha512-HrxmNxKTGZ9a3uAl/FNG66Sdt0G9L4TtMbbUQjP1WhGmSj0FOyHvSgx7623aGJvXfPOur8MwmarlHT+37jmzlw==" + }, + "imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=" + }, + "indent-string": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-2.1.0.tgz", + "integrity": "sha1-ji1INIdCEhtKghi3oTfppSBJ3IA=", + "requires": { + "repeating": "2.0.1" + } + }, + "indexes-of": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/indexes-of/-/indexes-of-1.0.1.tgz", + "integrity": "sha1-8w9xbI4r00bHtn0985FVZqfAVgc=" + }, + "indexof": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/indexof/-/indexof-0.0.1.tgz", + "integrity": "sha1-gtwzbSMrkGIXnQWrMpOmYFn9Q10=" + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "requires": { + "once": "1.4.0", + "wrappy": "1.0.2" + } + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + }, + "ini": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.4.tgz", + "integrity": "sha1-BTfLedr1m1mhpRff9wbIbsA5Fi4=" + }, + "inquirer": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-3.3.0.tgz", + "integrity": "sha512-h+xtnyk4EwKvFWHrUYsWErEVR+igKtLdchu+o0Z1RL7VU/jVMFbYir2bp6bAj8efFNxWqHX0dIss6fJQ+/+qeQ==", + "requires": { + "ansi-escapes": "3.0.0", + "chalk": "2.2.0", + "cli-cursor": "2.1.0", + "cli-width": "2.2.0", + "external-editor": "2.0.5", + "figures": "2.0.0", + "lodash": "4.17.4", + "mute-stream": "0.0.7", + "run-async": "2.3.0", + "rx-lite": "4.0.8", + "rx-lite-aggregates": "4.0.8", + "string-width": "2.1.1", + "strip-ansi": "4.0.0", + "through": "2.3.8" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=" + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "requires": { + "ansi-regex": "3.0.0" + } + } + } + }, + "interpret": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.0.4.tgz", + "integrity": "sha1-ggzdWIuGj/sZGoCVBtbJyPISsbA=" + }, + "invariant": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.2.tgz", + "integrity": "sha1-nh9WrArNtr8wMwbzOL47IErmA2A=", + "requires": { + "loose-envify": "1.3.1" + } + }, + "invert-kv": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-1.0.0.tgz", + "integrity": "sha1-EEqOSqym09jNFXqO+L+rLXo//bY=" + }, + "is": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/is/-/is-3.2.1.tgz", + "integrity": "sha1-0Kwq1V63sL7JJqUmb2xmKqqD3KU=" + }, + "is-absolute": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/is-absolute/-/is-absolute-0.2.6.tgz", + "integrity": "sha1-IN5p89uULvLYe5wto28XIjWxtes=", + "requires": { + "is-relative": "0.2.1", + "is-windows": "0.2.0" + } + }, + "is-absolute-url": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-absolute-url/-/is-absolute-url-2.1.0.tgz", + "integrity": "sha1-UFMN+4T8yap9vnhS6Do3uTufKqY=" + }, + "is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=" + }, + "is-binary-path": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-1.0.1.tgz", + "integrity": "sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg=", + "requires": { + "binary-extensions": "1.10.0" + } + }, + "is-buffer": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.5.tgz", + "integrity": "sha1-Hzsm72E7IUuIy8ojzGwB2Hlh7sw=" + }, + "is-builtin-module": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-1.0.0.tgz", + "integrity": "sha1-VAVy0096wxGfj3bDDLwbHgN6/74=", + "requires": { + "builtin-modules": "1.1.1" + } + }, + "is-callable": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.3.tgz", + "integrity": "sha1-hut1OSgF3cM69xySoO7fdO52BLI=" + }, + "is-date-object": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.1.tgz", + "integrity": "sha1-mqIOtq7rv/d/vTPnTKAbM1gdOhY=" + }, + "is-directory": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/is-directory/-/is-directory-0.3.1.tgz", + "integrity": "sha1-YTObbyR1/Hcv2cnYP1yFddwVSuE=" + }, + "is-dotfile": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-dotfile/-/is-dotfile-1.0.3.tgz", + "integrity": "sha1-pqLzL/0t+wT1yiXs0Pa4PPeYoeE=" + }, + "is-equal-shallow": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/is-equal-shallow/-/is-equal-shallow-0.1.3.tgz", + "integrity": "sha1-IjgJj8Ih3gvPpdnqxMRdY4qhxTQ=", + "requires": { + "is-primitive": "2.0.0" + } + }, + "is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=" + }, + "is-extglob": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz", + "integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=" + }, + "is-finite": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-finite/-/is-finite-1.0.2.tgz", + "integrity": "sha1-zGZ3aVYCvlUO8R6LSqYwU0K20Ko=", + "requires": { + "number-is-nan": "1.0.1" + } + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=" + }, + "is-glob": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz", + "integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=", + "requires": { + "is-extglob": "1.0.0" + } + }, + "is-number": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-2.1.0.tgz", + "integrity": "sha1-Afy7s5NGOlSPL0ZszhbezknbkI8=", + "requires": { + "kind-of": "3.2.2" + } + }, + "is-path-cwd": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-1.0.0.tgz", + "integrity": "sha1-0iXsIxMuie3Tj9p2dHLmLmXxEG0=" + }, + "is-path-in-cwd": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-path-in-cwd/-/is-path-in-cwd-1.0.0.tgz", + "integrity": "sha1-ZHdYK4IU1gI0YJRWcAO+ip6sBNw=", + "requires": { + "is-path-inside": "1.0.0" + } + }, + "is-path-inside": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-1.0.0.tgz", + "integrity": "sha1-/AbloWg/vaE95mev9xe7wQpI838=", + "requires": { + "path-is-inside": "1.0.2" + } + }, + "is-plain-obj": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", + "integrity": "sha1-caUMhCnfync8kqOQpKA7OfzVHT4=" + }, + "is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "requires": { + "isobject": "3.0.1" + }, + "dependencies": { + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" + } + } + }, + "is-posix-bracket": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-posix-bracket/-/is-posix-bracket-0.1.1.tgz", + "integrity": "sha1-MzTceXdDaOkvAW5vvAqI9c1ua8Q=" + }, + "is-primitive": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-primitive/-/is-primitive-2.0.0.tgz", + "integrity": "sha1-IHurkWOEmcB7Kt8kCkGochADRXU=" + }, + "is-promise": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.1.0.tgz", + "integrity": "sha1-eaKp7OfwlugPNtKy87wWwf9L8/o=" + }, + "is-regex": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.4.tgz", + "integrity": "sha1-VRdIm1RwkbCTDglWVM7SXul+lJE=", + "requires": { + "has": "1.0.1" + } + }, + "is-regexp": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-regexp/-/is-regexp-1.0.0.tgz", + "integrity": "sha1-/S2INUXEa6xaYz57mgnof6LLUGk=" + }, + "is-relative": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-relative/-/is-relative-0.2.1.tgz", + "integrity": "sha1-0n9MfVFtF1+2ENuEu+7yPDvJeqU=", + "requires": { + "is-unc-path": "0.1.2" + } + }, + "is-resolvable": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-resolvable/-/is-resolvable-1.0.0.tgz", + "integrity": "sha1-jfV8YeouPFAUCNEA+wE8+NbgzGI=", + "requires": { + "tryit": "1.0.3" + } + }, + "is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=" + }, + "is-supported-regexp-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-supported-regexp-flag/-/is-supported-regexp-flag-1.0.0.tgz", + "integrity": "sha1-i1IMhfrnolM4LUsCZS4EVXbhO7g=" + }, + "is-svg": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-svg/-/is-svg-2.1.0.tgz", + "integrity": "sha1-z2EJDaDZ77yrhyLeum8DIgjbsOk=", + "requires": { + "html-comment-regex": "1.1.1" + } + }, + "is-symbol": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.1.tgz", + "integrity": "sha1-PMWfAAJRlLarLjjbrmaJJWtmBXI=" + }, + "is-unc-path": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/is-unc-path/-/is-unc-path-0.1.2.tgz", + "integrity": "sha1-arBTpyVzwQJQ/0FqOBTDUXivObk=", + "requires": { + "unc-path-regex": "0.1.2" + } + }, + "is-utf8": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz", + "integrity": "sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI=" + }, + "is-windows": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-0.2.0.tgz", + "integrity": "sha1-3hqm1j6indJIc3tp8f+LgALSEIw=" + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" + }, + "isobject": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", + "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", + "requires": { + "isarray": "1.0.0" + } + }, + "isomorphic-fetch": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-2.2.1.tgz", + "integrity": "sha1-YRrhrPFPXoH3KVB0coGf6XM1WKk=", + "requires": { + "node-fetch": "1.7.3", + "whatwg-fetch": "2.0.3" + } + }, + "isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" + }, + "jdu": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/jdu/-/jdu-1.0.0.tgz", + "integrity": "sha1-KPHjiFAXha4KHZPpPtCxTdQeUc4=" + }, + "jquery": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.2.1.tgz", + "integrity": "sha1-XE2d5lKvbNCncBVKYxu6ErAVx4c=" + }, + "js-base64": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.3.2.tgz", + "integrity": "sha512-Y2/+DnfJJXT1/FCwUebUhLWb3QihxiSC42+ctHLGogmW2jPY6LCapMdFZXRvVP2z6qyKW7s6qncE/9gSqZiArw==" + }, + "js-tokens": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz", + "integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls=" + }, + "js-yaml": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.7.0.tgz", + "integrity": "sha1-XJZ93YN6m/3KXy3oQlOr6KHAO4A=", + "requires": { + "argparse": "1.0.9", + "esprima": "2.7.3" + } + }, + "jschardet": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/jschardet/-/jschardet-1.5.1.tgz", + "integrity": "sha512-vE2hT1D0HLZCLLclfBSfkfTTedhVj0fubHpJBHKwwUWX0nSbhPAfk+SG9rTX95BYNmau8rGFfCeaT6T5OW1C2A==" + }, + "jsesc": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-1.3.0.tgz", + "integrity": "sha1-RsP+yMGJKxKwgz25vHYiF226s0s=" + }, + "json-loader": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/json-loader/-/json-loader-0.5.7.tgz", + "integrity": "sha512-QLPs8Dj7lnf3e3QYS1zkCo+4ZwqOiF9d/nZnYozTISxXWCfNs9yuky5rJw4/W34s7POaNlbZmQGaB5NiXCbP4w==" + }, + "json-schema-traverse": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz", + "integrity": "sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A=" + }, + "json-stable-stringify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz", + "integrity": "sha1-mnWdOcXy/1A/1TAGRu1EX4jE+a8=", + "requires": { + "jsonify": "0.0.0" + } + }, + "json5": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-0.5.1.tgz", + "integrity": "sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE=" + }, + "jsonify": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz", + "integrity": "sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM=" + }, + "jsx-ast-utils": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-2.0.1.tgz", + "integrity": "sha1-6AGxs5mF4g//yHtA43SAgOLcrH8=", + "requires": { + "array-includes": "3.0.3" + } + }, + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "1.1.5" + } + }, + "known-css-properties": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/known-css-properties/-/known-css-properties-0.4.1.tgz", + "integrity": "sha512-n+ThoCKhyMFKkMfksdLMP5ndp+VzwDRzQdH6JlmZ2GTpUenYB2EeEKjOue2SErAAG/MmBSUISpwvawDhydWQdQ==" + }, + "lazy-cache": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-1.0.4.tgz", + "integrity": "sha1-odePw6UEdMuAhF07O24dpJpEbo4=" + }, + "lcid": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/lcid/-/lcid-1.0.0.tgz", + "integrity": "sha1-MIrMr6C8SDo4Z7S28rlQYlHRuDU=", + "requires": { + "invert-kv": "1.0.0" + } + }, + "levn": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", + "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", + "requires": { + "prelude-ls": "1.1.2", + "type-check": "0.3.2" + } + }, + "liftoff": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/liftoff/-/liftoff-2.3.0.tgz", + "integrity": "sha1-qY8v9nGD2Lp8+soQVIvX/wVQs4U=", + "requires": { + "extend": "3.0.1", + "findup-sync": "0.4.3", + "fined": "1.1.0", + "flagged-respawn": "0.3.2", + "lodash.isplainobject": "4.0.6", + "lodash.isstring": "4.0.1", + "lodash.mapvalues": "4.6.0", + "rechoir": "0.6.2", + "resolve": "1.4.0" + } + }, + "livereload-js": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/livereload-js/-/livereload-js-2.2.2.tgz", + "integrity": "sha1-bIclfmSKtHW8JOoldFftzB+NC8I=" + }, + "load-json-file": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-2.0.0.tgz", + "integrity": "sha1-eUfkIUmvgNaWy/eXvKq8/h/inKg=", + "requires": { + "graceful-fs": "4.1.11", + "parse-json": "2.2.0", + "pify": "2.3.0", + "strip-bom": "3.0.0" + } + }, + "loader-fs-cache": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/loader-fs-cache/-/loader-fs-cache-1.0.1.tgz", + "integrity": "sha1-VuC/CL2XCLJqdltoUJhAyN7J/bw=", + "requires": { + "find-cache-dir": "0.1.1", + "mkdirp": "0.5.1" + }, + "dependencies": { + "find-cache-dir": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-0.1.1.tgz", + "integrity": "sha1-yN765XyKUqinhPnjHFfHQumToLk=", + "requires": { + "commondir": "1.0.1", + "mkdirp": "0.5.1", + "pkg-dir": "1.0.0" + } + }, + "find-up": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz", + "integrity": "sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=", + "requires": { + "path-exists": "2.1.0", + "pinkie-promise": "2.0.1" + } + }, + "path-exists": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz", + "integrity": "sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=", + "requires": { + "pinkie-promise": "2.0.1" + } + }, + "pkg-dir": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-1.0.0.tgz", + "integrity": "sha1-ektQio1bstYp1EcFb/TpyTFM89Q=", + "requires": { + "find-up": "1.1.2" + } + } + } + }, + "loader-runner": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-2.3.0.tgz", + "integrity": "sha1-9IKuqC1UPgeSFwDVpG7yb9rGuKI=" + }, + "loader-utils": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.1.0.tgz", + "integrity": "sha1-yYrvSIvM7aL/teLeZG1qdUQp9c0=", + "requires": { + "big.js": "3.2.0", + "emojis-list": "2.1.0", + "json5": "0.5.1" + } + }, + "locate-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", + "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", + "requires": { + "p-locate": "2.0.0", + "path-exists": "3.0.0" + } + }, + "lodash": { + "version": "4.17.4", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.4.tgz", + "integrity": "sha1-eCA6TRwyiuHYbcpkYONptX9AVa4=" + }, + "lodash-es": { + "version": "4.17.4", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.4.tgz", + "integrity": "sha1-3MHXVS4VCgZABzupyzHXDwMpUOc=" + }, + "lodash._baseassign": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/lodash._baseassign/-/lodash._baseassign-3.2.0.tgz", + "integrity": "sha1-jDigmVAPIVrQnlnxci/QxSv+Ck4=", + "requires": { + "lodash._basecopy": "3.0.1", + "lodash.keys": "3.1.2" + } + }, + "lodash._basecopy": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/lodash._basecopy/-/lodash._basecopy-3.0.1.tgz", + "integrity": "sha1-jaDmqHbPNEwK2KVIghEd08XHyjY=" + }, + "lodash._basetostring": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/lodash._basetostring/-/lodash._basetostring-3.0.1.tgz", + "integrity": "sha1-0YYdh3+CSlL2aYMtyvPuFVZqB9U=" + }, + "lodash._basevalues": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/lodash._basevalues/-/lodash._basevalues-3.0.0.tgz", + "integrity": "sha1-W3dXYoAr3j0yl1A+JjAIIP32Ybc=" + }, + "lodash._bindcallback": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/lodash._bindcallback/-/lodash._bindcallback-3.0.1.tgz", + "integrity": "sha1-5THCdkTPi1epnhftlbNcdIeJOS4=" + }, + "lodash._createassigner": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/lodash._createassigner/-/lodash._createassigner-3.1.1.tgz", + "integrity": "sha1-g4pbri/aymOsIt7o4Z+k5taXCxE=", + "requires": { + "lodash._bindcallback": "3.0.1", + "lodash._isiterateecall": "3.0.9", + "lodash.restparam": "3.6.1" + } + }, + "lodash._getnative": { + "version": "3.9.1", + "resolved": "https://registry.npmjs.org/lodash._getnative/-/lodash._getnative-3.9.1.tgz", + "integrity": "sha1-VwvH3t5G1hzc3mh9ZdPuy6o6r/U=" + }, + "lodash._isiterateecall": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/lodash._isiterateecall/-/lodash._isiterateecall-3.0.9.tgz", + "integrity": "sha1-UgOte6Ql+uhCRg5pbbnPPmqsBXw=" + }, + "lodash._reescape": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/lodash._reescape/-/lodash._reescape-3.0.0.tgz", + "integrity": "sha1-Kx1vXf4HyKNVdT5fJ/rH8c3hYWo=" + }, + "lodash._reevaluate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/lodash._reevaluate/-/lodash._reevaluate-3.0.0.tgz", + "integrity": "sha1-WLx0xAZklTrgsSTYBpltrKQx4u0=" + }, + "lodash._reinterpolate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz", + "integrity": "sha1-DM8tiRZq8Ds2Y8eWU4t1rG4RTZ0=" + }, + "lodash._root": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/lodash._root/-/lodash._root-3.0.1.tgz", + "integrity": "sha1-+6HEUkwZ7ppfgTa0YJ8BfPTe1pI=" + }, + "lodash.assign": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/lodash.assign/-/lodash.assign-3.2.0.tgz", + "integrity": "sha1-POnwI0tLIiPilrj6CsH+6OvKZPo=", + "requires": { + "lodash._baseassign": "3.2.0", + "lodash._createassigner": "3.1.1", + "lodash.keys": "3.1.2" + } + }, + "lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha1-soqmKIorn8ZRA1x3EfZathkDMaY=" + }, + "lodash.clone": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.clone/-/lodash.clone-4.5.0.tgz", + "integrity": "sha1-GVhwRQ9aExkkeN9Lw9I9LeoZB7Y=" + }, + "lodash.defaults": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", + "integrity": "sha1-0JF4cW/+pN3p5ft7N/bwgCJ0WAw=" + }, + "lodash.escape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/lodash.escape/-/lodash.escape-3.2.0.tgz", + "integrity": "sha1-mV7g3BjBtIzJLv+ucaEKq1tIdpg=", + "requires": { + "lodash._root": "3.0.1" + } + }, + "lodash.isarguments": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", + "integrity": "sha1-L1c9hcaiQon/AGY7SRwdM4/zRYo=" + }, + "lodash.isarray": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/lodash.isarray/-/lodash.isarray-3.0.4.tgz", + "integrity": "sha1-eeTriMNqgSKvhvhEqpvNhRtfu1U=" + }, + "lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs=" + }, + "lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha1-1SfftUVuynzJu5XV2ur4i6VKVFE=" + }, + "lodash.kebabcase": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.kebabcase/-/lodash.kebabcase-4.1.1.tgz", + "integrity": "sha1-hImxyw0p/4gZXM7KRI/21swpXDY=" + }, + "lodash.keys": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/lodash.keys/-/lodash.keys-3.1.2.tgz", + "integrity": "sha1-TbwEcrFWvlCgsoaFXRvQsMZWCYo=", + "requires": { + "lodash._getnative": "3.9.1", + "lodash.isarguments": "3.1.0", + "lodash.isarray": "3.0.4" + } + }, + "lodash.mapvalues": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.mapvalues/-/lodash.mapvalues-4.6.0.tgz", + "integrity": "sha1-G6+lAF3p3W9PJmaMMMo3IwzJaJw=" + }, + "lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4=" + }, + "lodash.restparam": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/lodash.restparam/-/lodash.restparam-3.6.1.tgz", + "integrity": "sha1-k2pOMJ7zMKdkXtQUWYbIWuWyCAU=" + }, + "lodash.snakecase": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.snakecase/-/lodash.snakecase-4.1.1.tgz", + "integrity": "sha1-OdcUo1NXFHg3rv1ktdy7Fr7Nj40=" + }, + "lodash.some": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.some/-/lodash.some-4.6.0.tgz", + "integrity": "sha1-G7nzFO9ri63tE7VJFpsqlF62jk0=" + }, + "lodash.template": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/lodash.template/-/lodash.template-3.6.2.tgz", + "integrity": "sha1-+M3sxhaaJVvpCYrosMU9N4kx0U8=", + "requires": { + "lodash._basecopy": "3.0.1", + "lodash._basetostring": "3.0.1", + "lodash._basevalues": "3.0.0", + "lodash._isiterateecall": "3.0.9", + "lodash._reinterpolate": "3.0.0", + "lodash.escape": "3.2.0", + "lodash.keys": "3.1.2", + "lodash.restparam": "3.6.1", + "lodash.templatesettings": "3.1.1" + } + }, + "lodash.templatesettings": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/lodash.templatesettings/-/lodash.templatesettings-3.1.1.tgz", + "integrity": "sha1-+zB4RHU7Zrnxr6VOJix0UwfbqOU=", + "requires": { + "lodash._reinterpolate": "3.0.0", + "lodash.escape": "3.2.0" + } + }, + "lodash.uniq": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", + "integrity": "sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=" + }, + "lodash.upperfirst": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/lodash.upperfirst/-/lodash.upperfirst-4.3.1.tgz", + "integrity": "sha1-E2Xt9DFIBIHvDRxolXpe2Z1J984=" + }, + "log-symbols": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-1.0.2.tgz", + "integrity": "sha1-N2/3tY6jCGoPCfrMdGF+ylAeGhg=", + "requires": { + "chalk": "1.1.3" + }, + "dependencies": { + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=" + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "requires": { + "ansi-styles": "2.2.1", + "escape-string-regexp": "1.0.5", + "has-ansi": "2.0.0", + "strip-ansi": "3.0.1", + "supports-color": "2.0.0" + } + }, + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" + } + } + }, + "longest": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/longest/-/longest-1.0.1.tgz", + "integrity": "sha1-MKCy2jj3N3DoKUoNIuZiXtd9AJc=" + }, + "loose-envify": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.3.1.tgz", + "integrity": "sha1-0aitM/qc4OcT1l/dCsi3SNR4yEg=", + "requires": { + "js-tokens": "3.0.2" + } + }, + "loud-rejection": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/loud-rejection/-/loud-rejection-1.6.0.tgz", + "integrity": "sha1-W0b4AUft7leIcPCG0Eghz5mOVR8=", + "requires": { + "currently-unhandled": "0.4.1", + "signal-exit": "3.0.2" + } + }, + "lru-cache": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.1.tgz", + "integrity": "sha512-q4spe4KTfsAS1SUHLO0wz8Qiyf1+vMIAgpRYioFYDMNqKfHQbg+AVDH3i4fvpl71/P1L0dBl+fQi+P37UYf0ew==", + "requires": { + "pseudomap": "1.0.2", + "yallist": "2.1.2" + } + }, + "lru-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/lru-queue/-/lru-queue-0.1.0.tgz", + "integrity": "sha1-Jzi9nw089PhEkMVzbEhpmsYyzaM=", + "requires": { + "es5-ext": "0.10.35" + } + }, + "macaddress": { + "version": "0.2.8", + "resolved": "https://registry.npmjs.org/macaddress/-/macaddress-0.2.8.tgz", + "integrity": "sha1-WQTcU3w57G2+/q6QIycTX6hRHxI=" + }, + "make-dir": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.0.0.tgz", + "integrity": "sha1-l6ARdR6R3YfPre9Ygy67BJNt6Xg=", + "requires": { + "pify": "2.3.0" + } + }, + "makeerror": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.11.tgz", + "integrity": "sha1-4BpckQnyr3lmDk6LlYd5AYT1qWw=", + "requires": { + "tmpl": "1.0.4" + } + }, + "map-cache": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", + "integrity": "sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=" + }, + "map-obj": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz", + "integrity": "sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0=" + }, + "map-stream": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/map-stream/-/map-stream-0.1.0.tgz", + "integrity": "sha1-5WqpTEyAVaFkBKBnS3jyFffI4ZQ=" + }, + "math-expression-evaluator": { + "version": "1.2.17", + "resolved": "https://registry.npmjs.org/math-expression-evaluator/-/math-expression-evaluator-1.2.17.tgz", + "integrity": "sha1-3oGf282E3M2PrlnGrreWFbnSZqw=" + }, + "mathml-tag-names": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mathml-tag-names/-/mathml-tag-names-2.0.1.tgz", + "integrity": "sha1-jUEmgWi/htEQK5gQnijlMeejRXg=" + }, + "md5.js": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.4.tgz", + "integrity": "sha1-6b296UogpawYsENA/Fdk1bCdkB0=", + "requires": { + "hash-base": "3.0.4", + "inherits": "2.0.3" + }, + "dependencies": { + "hash-base": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.0.4.tgz", + "integrity": "sha1-X8hoaEfs1zSZQDMZprCj8/auSRg=", + "requires": { + "inherits": "2.0.3", + "safe-buffer": "5.1.1" + } + } + } + }, + "media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" + }, + "mem": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/mem/-/mem-1.1.0.tgz", + "integrity": "sha1-Xt1StIXKHZAP5kiVUFOZoN+kX3Y=", + "requires": { + "mimic-fn": "1.1.0" + } + }, + "memoizee": { + "version": "0.4.11", + "resolved": "https://registry.npmjs.org/memoizee/-/memoizee-0.4.11.tgz", + "integrity": "sha1-vemBdmPJ5A/bKk6hw2cpYIeujI8=", + "requires": { + "d": "1.0.0", + "es5-ext": "0.10.35", + "es6-weak-map": "2.0.2", + "event-emitter": "0.3.5", + "is-promise": "2.1.0", + "lru-queue": "0.1.0", + "next-tick": "1.0.0", + "timers-ext": "0.1.2" + } + }, + "memory-fs": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.4.1.tgz", + "integrity": "sha1-OpoguEYlI+RHz7x+i7gO1me/xVI=", + "requires": { + "errno": "0.1.4", + "readable-stream": "2.3.3" + } + }, + "meow": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/meow/-/meow-3.7.0.tgz", + "integrity": "sha1-cstmi0JSKCkKu/qFaJJYcwioAfs=", + "requires": { + "camelcase-keys": "2.1.0", + "decamelize": "1.2.0", + "loud-rejection": "1.6.0", + "map-obj": "1.0.1", + "minimist": "1.2.0", + "normalize-package-data": "2.4.0", + "object-assign": "4.1.1", + "read-pkg-up": "1.0.1", + "redent": "1.0.0", + "trim-newlines": "1.0.0" + }, + "dependencies": { + "find-up": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz", + "integrity": "sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=", + "requires": { + "path-exists": "2.1.0", + "pinkie-promise": "2.0.1" + } + }, + "load-json-file": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", + "integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=", + "requires": { + "graceful-fs": "4.1.11", + "parse-json": "2.2.0", + "pify": "2.3.0", + "pinkie-promise": "2.0.1", + "strip-bom": "2.0.0" + } + }, + "minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" + }, + "path-exists": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz", + "integrity": "sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=", + "requires": { + "pinkie-promise": "2.0.1" + } + }, + "path-type": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz", + "integrity": "sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE=", + "requires": { + "graceful-fs": "4.1.11", + "pify": "2.3.0", + "pinkie-promise": "2.0.1" + } + }, + "read-pkg": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz", + "integrity": "sha1-9f+qXs0pyzHAR0vKfXVra7KePyg=", + "requires": { + "load-json-file": "1.1.0", + "normalize-package-data": "2.4.0", + "path-type": "1.1.0" + } + }, + "read-pkg-up": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz", + "integrity": "sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI=", + "requires": { + "find-up": "1.1.2", + "read-pkg": "1.1.0" + } + }, + "strip-bom": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", + "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=", + "requires": { + "is-utf8": "0.2.1" + } + } + } + }, + "merge": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/merge/-/merge-1.2.0.tgz", + "integrity": "sha1-dTHjnUlJwoGma4xabgJl6LBYlNo=" + }, + "micromatch": { + "version": "2.3.11", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-2.3.11.tgz", + "integrity": "sha1-hmd8l9FyCzY0MdBNDRUpO9OMFWU=", + "requires": { + "arr-diff": "2.0.0", + "array-unique": "0.2.1", + "braces": "1.8.5", + "expand-brackets": "0.1.5", + "extglob": "0.3.2", + "filename-regex": "2.0.1", + "is-extglob": "1.0.0", + "is-glob": "2.0.1", + "kind-of": "3.2.2", + "normalize-path": "2.1.1", + "object.omit": "2.0.1", + "parse-glob": "3.0.4", + "regex-cache": "0.4.4" + } + }, + "miller-rabin": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/miller-rabin/-/miller-rabin-4.0.1.tgz", + "integrity": "sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA==", + "requires": { + "bn.js": "4.11.8", + "brorand": "1.1.0" + } + }, + "mime": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.4.1.tgz", + "integrity": "sha512-KI1+qOZu5DcW6wayYHSzR/tXKCDC5Om4s1z2QJjDULzLcmf3DvzS7oluY4HCTrc+9FiKmWUgeNLg7W3uIQvxtQ==" + }, + "mime-db": { + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.30.0.tgz", + "integrity": "sha1-dMZD2i3Z1qRTmZY0ZbJtXKfXHwE=" + }, + "mime-types": { + "version": "2.1.17", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.17.tgz", + "integrity": "sha1-Cdejk/A+mVp5+K+Fe3Cp4KsWVXo=", + "requires": { + "mime-db": "1.30.0" + } + }, + "mimic-fn": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.1.0.tgz", + "integrity": "sha1-5md4PZLonb00KBi1IwudYqZyrRg=" + }, + "mini-lr": { + "version": "0.1.9", + "resolved": "https://registry.npmjs.org/mini-lr/-/mini-lr-0.1.9.tgz", + "integrity": "sha1-AhmdJzR5U9H9HW297UJh8Yey0PY=", + "requires": { + "body-parser": "1.14.2", + "debug": "2.6.9", + "faye-websocket": "0.7.3", + "livereload-js": "2.2.2", + "parseurl": "1.3.2", + "qs": "2.2.5" + }, + "dependencies": { + "qs": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/qs/-/qs-2.2.5.tgz", + "integrity": "sha1-EIirr53MCuWuRbcJ5sa1iIsjkjw=" + } + } + }, + "minimalistic-assert": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.0.tgz", + "integrity": "sha1-cCvi3aazf0g2vLP121ZkG2Sh09M=" + }, + "minimalistic-crypto-utils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", + "integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=" + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "requires": { + "brace-expansion": "1.1.8" + } + }, + "minimist": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" + }, + "mkdirp": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "requires": { + "minimist": "0.0.8" + } + }, + "mobile-detect": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/mobile-detect/-/mobile-detect-1.3.7.tgz", + "integrity": "sha512-thfQQA1gz4mezj6x3Rr55uEqcyELvgCYQLQEHHgQDY6V0uTh3GTgPJGFd5rB+n085fv7TDJSTqYAaKa9+B1wrg==" + }, + "moment": { + "version": "2.18.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.18.1.tgz", + "integrity": "sha1-w2GT3Tzhwu7SrbfIAtu8d6gbHA8=" + }, + "mousetrap": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/mousetrap/-/mousetrap-1.6.1.tgz", + "integrity": "sha1-KghfXHUSlMdefoH27CVFspy/Qtk=" + }, + "mout": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/mout/-/mout-1.1.0.tgz", + "integrity": "sha512-XsP0vf4As6BfqglxZqbqQ8SR6KQot2AgxvR0gG+WtUkf90vUXchMOZQtPf/Hml1rEffJupqL/tIrU6EYhsUQjw==" + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + }, + "multipipe": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/multipipe/-/multipipe-0.1.2.tgz", + "integrity": "sha1-Ko8t33Du1WTf8tV/HhoTfZ8FB4s=", + "requires": { + "duplexer2": "0.0.2" + } + }, + "mute-stream": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.7.tgz", + "integrity": "sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s=" + }, + "natives": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/natives/-/natives-1.1.0.tgz", + "integrity": "sha1-6f+EFBimsux6SV6TmYT3jxY+bjE=" + }, + "natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=" + }, + "new-from": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/new-from/-/new-from-0.0.3.tgz", + "integrity": "sha1-HErRNhPePhXWMhtw7Vwjk36iXmc=", + "requires": { + "readable-stream": "1.1.14" + }, + "dependencies": { + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" + }, + "readable-stream": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", + "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", + "requires": { + "core-util-is": "1.0.2", + "inherits": "2.0.3", + "isarray": "0.0.1", + "string_decoder": "0.10.31" + } + }, + "string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" + } + } + }, + "next-tick": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.0.0.tgz", + "integrity": "sha1-yobR/ogoFpsBICCOPchCS524NCw=" + }, + "node-fetch": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-1.7.3.tgz", + "integrity": "sha512-NhZ4CsKx7cYm2vSrBAr2PvFOe6sWDf0UYLRqA6svUYg7+/TSfVAu49jYC4BvQ4Sms9SZgdqGBgroqfDhJdTyKQ==", + "requires": { + "encoding": "0.1.12", + "is-stream": "1.1.0" + } + }, + "node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha1-h6kGXNs1XTGC2PlM4RGIuCXGijs=" + }, + "node-libs-browser": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/node-libs-browser/-/node-libs-browser-2.0.0.tgz", + "integrity": "sha1-o6WeyXAkmFtG6Vg3lkb5bEthZkY=", + "requires": { + "assert": "1.4.1", + "browserify-zlib": "0.1.4", + "buffer": "4.9.1", + "console-browserify": "1.1.0", + "constants-browserify": "1.0.0", + "crypto-browserify": "3.11.1", + "domain-browser": "1.1.7", + "events": "1.1.1", + "https-browserify": "0.0.1", + "os-browserify": "0.2.1", + "path-browserify": "0.0.0", + "process": "0.11.10", + "punycode": "1.4.1", + "querystring-es3": "0.2.1", + "readable-stream": "2.3.3", + "stream-browserify": "2.0.1", + "stream-http": "2.7.2", + "string_decoder": "0.10.31", + "timers-browserify": "2.0.4", + "tty-browserify": "0.0.0", + "url": "0.11.0", + "util": "0.10.3", + "vm-browserify": "0.0.4" + }, + "dependencies": { + "string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" + } + } + }, + "node.extend": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/node.extend/-/node.extend-1.1.6.tgz", + "integrity": "sha1-p7iCyC1sk6SGOlUEvV3o7IYli5Y=", + "requires": { + "is": "3.2.1" + } + }, + "normalize-package-data": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.4.0.tgz", + "integrity": "sha512-9jjUFbTPfEy3R/ad/2oNbKtW9Hgovl5O1FvFWKkKblNXoN/Oou6+9+KKohPK13Yc3/TyunyWhJp6gvRNR/PPAw==", + "requires": { + "hosted-git-info": "2.5.0", + "is-builtin-module": "1.0.0", + "semver": "4.3.6", + "validate-npm-package-license": "3.0.1" + } + }, + "normalize-path": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", + "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", + "requires": { + "remove-trailing-separator": "1.1.0" + } + }, + "normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha1-LRDAa9/TEuqXd2laTShDlFa3WUI=" + }, + "normalize-selector": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/normalize-selector/-/normalize-selector-0.2.0.tgz", + "integrity": "sha1-0LFF62kRicY6eNIB3E/bEpPvDAM=" + }, + "normalize-url": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-1.9.1.tgz", + "integrity": "sha1-LMDWazHqIwNkWENuNiDYWVTGbDw=", + "requires": { + "object-assign": "4.1.1", + "prepend-http": "1.0.4", + "query-string": "4.3.4", + "sort-keys": "1.1.2" + }, + "dependencies": { + "query-string": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/query-string/-/query-string-4.3.4.tgz", + "integrity": "sha1-u7aTucqRXCMlFbIosaArYJBD2+s=", + "requires": { + "object-assign": "4.1.1", + "strict-uri-encode": "1.1.0" + } + } + } + }, + "normalize.css": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/normalize.css/-/normalize.css-7.0.0.tgz", + "integrity": "sha1-q/sd2CRwZ04DIrU86xqvQSk45L8=" + }, + "npm-path": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/npm-path/-/npm-path-1.1.0.tgz", + "integrity": "sha1-BHSuAEGcMn1UcBt88s0F3Ii+EUA=", + "requires": { + "which": "1.3.0" + } + }, + "npm-run": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/npm-run/-/npm-run-3.0.0.tgz", + "integrity": "sha1-Vokg+ECpj9jiKZ22ayYW4kdsr2k=", + "requires": { + "minimist": "1.2.0", + "npm-path": "1.1.0", + "npm-which": "2.0.0", + "serializerr": "1.0.3", + "sync-exec": "0.6.2" + }, + "dependencies": { + "minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" + } + } + }, + "npm-run-path": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", + "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", + "requires": { + "path-key": "2.0.1" + } + }, + "npm-which": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/npm-which/-/npm-which-2.0.0.tgz", + "integrity": "sha1-DEaYIWC3gwk2YdHQG9RJbS/qu6w=", + "requires": { + "commander": "2.11.0", + "npm-path": "1.1.0", + "which": "1.3.0" + } + }, + "nsdeclare": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/nsdeclare/-/nsdeclare-0.1.0.tgz", + "integrity": "sha1-ENqhU2QjgtPPLAGpFvTrIKEosZ8=" + }, + "num2fraction": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/num2fraction/-/num2fraction-1.2.2.tgz", + "integrity": "sha1-b2gragJ6Tp3fpFZM0lidHU5mnt4=" + }, + "number-is-nan": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=" + }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" + }, + "object-hash": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-1.2.0.tgz", + "integrity": "sha512-smRWXzkvxw72VquyZ0wggySl7PFUtoDhvhpdwgESXxUrH7vVhhp9asfup1+rVLrhsl7L45Ee1Q/l5R2Ul4MwUg==" + }, + "object-keys": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.0.11.tgz", + "integrity": "sha1-xUYBd4rVYPEULODgG8yotW0TQm0=" + }, + "object.defaults": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/object.defaults/-/object.defaults-1.1.0.tgz", + "integrity": "sha1-On+GgzS0B96gbaFtiNXNKeQ1/s8=", + "requires": { + "array-each": "1.0.1", + "array-slice": "1.0.0", + "for-own": "1.0.0", + "isobject": "3.0.1" + }, + "dependencies": { + "for-own": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/for-own/-/for-own-1.0.0.tgz", + "integrity": "sha1-xjMy9BXO3EsE2/5wz4NklMU8tEs=", + "requires": { + "for-in": "1.0.2" + } + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" + } + } + }, + "object.omit": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/object.omit/-/object.omit-2.0.1.tgz", + "integrity": "sha1-Gpx0SCnznbuFjHbKNXmuKlTr0fo=", + "requires": { + "for-own": "0.1.5", + "is-extendable": "0.1.1" + } + }, + "object.pick": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", + "integrity": "sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c=", + "requires": { + "isobject": "3.0.1" + }, + "dependencies": { + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" + } + } + }, + "on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", + "requires": { + "ee-first": "1.1.1" + } + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "requires": { + "wrappy": "1.0.2" + } + }, + "onetime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz", + "integrity": "sha1-BnQoIw/WdEOyeUsiu6UotoZ5YtQ=", + "requires": { + "mimic-fn": "1.1.0" + } + }, + "optionator": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.2.tgz", + "integrity": "sha1-NkxeQJ0/TWMB1sC0wFu6UBgK62Q=", + "requires": { + "deep-is": "0.1.3", + "fast-levenshtein": "2.0.6", + "levn": "0.3.0", + "prelude-ls": "1.1.2", + "type-check": "0.3.2", + "wordwrap": "1.0.0" + } + }, + "orchestrator": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/orchestrator/-/orchestrator-0.3.8.tgz", + "integrity": "sha1-FOfp4nZPcxX7rBhOUGx6pt+UrX4=", + "requires": { + "end-of-stream": "0.1.5", + "sequencify": "0.0.7", + "stream-consume": "0.1.0" + } + }, + "ordered-read-streams": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/ordered-read-streams/-/ordered-read-streams-0.1.0.tgz", + "integrity": "sha1-/VZamvjrRHO6abbtijQ1LLVS8SY=" + }, + "os-browserify": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/os-browserify/-/os-browserify-0.2.1.tgz", + "integrity": "sha1-Y/xMzuXS13Y9Jrv4YBB45sLgBE8=" + }, + "os-homedir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", + "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=" + }, + "os-locale": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-2.1.0.tgz", + "integrity": "sha512-3sslG3zJbEYcaC4YVAvDorjGxc7tv6KVATnLPZONiljsUncvihe9BQoVCEs0RZ1kmf4Hk9OBqlZfJZWI4GanKA==", + "requires": { + "execa": "0.7.0", + "lcid": "1.0.0", + "mem": "1.1.0" + } + }, + "os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=" + }, + "p-finally": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", + "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=" + }, + "p-limit": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.1.0.tgz", + "integrity": "sha1-sH/y2aXYi+yAYDWJWiurZqJ5iLw=" + }, + "p-locate": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", + "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", + "requires": { + "p-limit": "1.1.0" + } + }, + "p-map": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-1.2.0.tgz", + "integrity": "sha512-r6zKACMNhjPJMTl8KcFH4li//gkrXWfbD6feV8l6doRHlzljFWGJ2AP6iKaCJXyZmAUMOPtvbW7EXkbWO/pLEA==" + }, + "pako": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz", + "integrity": "sha1-8/dSL073gjSNqBYbrZ7P1Rv4OnU=" + }, + "parse-asn1": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.0.tgz", + "integrity": "sha1-N8T5t+06tlx0gXtfJICTf7+XxxI=", + "requires": { + "asn1.js": "4.9.1", + "browserify-aes": "1.1.1", + "create-hash": "1.1.3", + "evp_bytestokey": "1.0.3", + "pbkdf2": "3.0.14" + } + }, + "parse-filepath": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parse-filepath/-/parse-filepath-1.0.1.tgz", + "integrity": "sha1-FZ1hVdQ5BNFsEO9piRHaHpGWm3M=", + "requires": { + "is-absolute": "0.2.6", + "map-cache": "0.2.2", + "path-root": "0.1.1" + } + }, + "parse-glob": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/parse-glob/-/parse-glob-3.0.4.tgz", + "integrity": "sha1-ssN2z7EfNVE7rdFz7wu246OIORw=", + "requires": { + "glob-base": "0.3.0", + "is-dotfile": "1.0.3", + "is-extglob": "1.0.0", + "is-glob": "2.0.1" + } + }, + "parse-json": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", + "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=", + "requires": { + "error-ex": "1.3.1" + } + }, + "parse-passwd": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/parse-passwd/-/parse-passwd-1.0.0.tgz", + "integrity": "sha1-bVuTSkVpk7I9N/QKOC1vFmao5cY=" + }, + "parseurl": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.2.tgz", + "integrity": "sha1-/CidTtiZMRlGDBViUyYs3I3mW/M=" + }, + "path-browserify": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-0.0.0.tgz", + "integrity": "sha1-oLhwcpquIUAFt9UDLsLLuw+0RRo=" + }, + "path-dirname": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/path-dirname/-/path-dirname-1.0.2.tgz", + "integrity": "sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA=" + }, + "path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=" + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" + }, + "path-is-inside": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz", + "integrity": "sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=" + }, + "path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=" + }, + "path-parse": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.5.tgz", + "integrity": "sha1-PBrfhx6pzWyUMbbqK9dKD/BVxME=" + }, + "path-root": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/path-root/-/path-root-0.1.1.tgz", + "integrity": "sha1-mkpoFMrBwM1zNgqV8yCDyOpHRbc=", + "requires": { + "path-root-regex": "0.1.2" + } + }, + "path-root-regex": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/path-root-regex/-/path-root-regex-0.1.2.tgz", + "integrity": "sha1-v8zcjfWxLcUsi0PsONGNcsBLqW0=" + }, + "path-to-regexp": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.7.0.tgz", + "integrity": "sha1-Wf3g9DW62suhA6hOnTvGTpa5k30=", + "requires": { + "isarray": "0.0.1" + }, + "dependencies": { + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" + } + } + }, + "path-type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-2.0.0.tgz", + "integrity": "sha1-8BLMuEFbcJb8LaoQVMPXI4lZTHM=", + "requires": { + "pify": "2.3.0" + } + }, + "pause-stream": { + "version": "0.0.11", + "resolved": "https://registry.npmjs.org/pause-stream/-/pause-stream-0.0.11.tgz", + "integrity": "sha1-/lo0sMvOErWqaitAPuLnO2AvFEU=", + "requires": { + "through": "2.3.8" + } + }, + "pbkdf2": { + "version": "3.0.14", + "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.0.14.tgz", + "integrity": "sha512-gjsZW9O34fm0R7PaLHRJmLLVfSoesxztjPjE9o6R+qtVJij90ltg1joIovN9GKrRW3t1PzhDDG3UMEMFfZ+1wA==", + "requires": { + "create-hash": "1.1.3", + "create-hmac": "1.1.6", + "ripemd160": "2.0.1", + "safe-buffer": "5.1.1", + "sha.js": "2.4.9" + } + }, + "performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" + }, + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=" + }, + "pinkie": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", + "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=" + }, + "pinkie-promise": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", + "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=", + "requires": { + "pinkie": "2.0.4" + } + }, + "pkg-dir": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-2.0.0.tgz", + "integrity": "sha1-9tXREJ4Z1j7fQo4L1X4Sd3YVM0s=", + "requires": { + "find-up": "2.1.0" + } + }, + "pluralize": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-7.0.0.tgz", + "integrity": "sha512-ARhBOdzS3e41FbkW/XWrTEtukqqLoK5+Z/4UeDaLuSW+39JPeFgs4gCGqsrJHVZX0fUrx//4OF0K1CUGwlIFow==" + }, + "postcss": { + "version": "6.0.13", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.13.tgz", + "integrity": "sha512-nHsrD1PPTMSJDfU+osVsLtPkSP9YGeoOz4FDLN4r1DW4N5vqL1J+gACzTQHsfwIiWG/0/nV4yCzjTMo1zD8U1g==", + "requires": { + "chalk": "2.2.0", + "source-map": "0.6.1", + "supports-color": "4.5.0" + } + }, + "postcss-calc": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/postcss-calc/-/postcss-calc-5.3.1.tgz", + "integrity": "sha1-d7rnypKK2FcW4v2kLyYb98HWW14=", + "requires": { + "postcss": "5.2.18", + "postcss-message-helpers": "2.0.0", + "reduce-css-calc": "1.3.0" + }, + "dependencies": { + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=" + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "requires": { + "ansi-styles": "2.2.1", + "escape-string-regexp": "1.0.5", + "has-ansi": "2.0.0", + "strip-ansi": "3.0.1", + "supports-color": "2.0.0" + }, + "dependencies": { + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" + } + } + }, + "has-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", + "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=" + }, + "postcss": { + "version": "5.2.18", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-5.2.18.tgz", + "integrity": "sha512-zrUjRRe1bpXKsX1qAJNJjqZViErVuyEkMTRrwu4ud4sbTtIBRmtaYDrHmcGgmrbsW3MHfmtIf+vJumgQn+PrXg==", + "requires": { + "chalk": "1.1.3", + "js-base64": "2.3.2", + "source-map": "0.5.7", + "supports-color": "3.2.3" + } + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" + }, + "supports-color": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", + "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=", + "requires": { + "has-flag": "1.0.0" + } + } + } + }, + "postcss-colormin": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/postcss-colormin/-/postcss-colormin-2.2.2.tgz", + "integrity": "sha1-ZjFBfV8OkJo9fsJrJMio0eT5bks=", + "requires": { + "colormin": "1.1.2", + "postcss": "5.2.18", + "postcss-value-parser": "3.3.0" + }, + "dependencies": { + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=" + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "requires": { + "ansi-styles": "2.2.1", + "escape-string-regexp": "1.0.5", + "has-ansi": "2.0.0", + "strip-ansi": "3.0.1", + "supports-color": "2.0.0" + }, + "dependencies": { + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" + } + } + }, + "has-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", + "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=" + }, + "postcss": { + "version": "5.2.18", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-5.2.18.tgz", + "integrity": "sha512-zrUjRRe1bpXKsX1qAJNJjqZViErVuyEkMTRrwu4ud4sbTtIBRmtaYDrHmcGgmrbsW3MHfmtIf+vJumgQn+PrXg==", + "requires": { + "chalk": "1.1.3", + "js-base64": "2.3.2", + "source-map": "0.5.7", + "supports-color": "3.2.3" + } + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" + }, + "supports-color": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", + "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=", + "requires": { + "has-flag": "1.0.0" + } + } + } + }, + "postcss-convert-values": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/postcss-convert-values/-/postcss-convert-values-2.6.1.tgz", + "integrity": "sha1-u9hZPFwf0uPRwyK7kl3K6Nrk1i0=", + "requires": { + "postcss": "5.2.18", + "postcss-value-parser": "3.3.0" + }, + "dependencies": { + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=" + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "requires": { + "ansi-styles": "2.2.1", + "escape-string-regexp": "1.0.5", + "has-ansi": "2.0.0", + "strip-ansi": "3.0.1", + "supports-color": "2.0.0" + }, + "dependencies": { + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" + } + } + }, + "has-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", + "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=" + }, + "postcss": { + "version": "5.2.18", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-5.2.18.tgz", + "integrity": "sha512-zrUjRRe1bpXKsX1qAJNJjqZViErVuyEkMTRrwu4ud4sbTtIBRmtaYDrHmcGgmrbsW3MHfmtIf+vJumgQn+PrXg==", + "requires": { + "chalk": "1.1.3", + "js-base64": "2.3.2", + "source-map": "0.5.7", + "supports-color": "3.2.3" + } + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" + }, + "supports-color": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", + "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=", + "requires": { + "has-flag": "1.0.0" + } + } + } + }, + "postcss-discard-comments": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/postcss-discard-comments/-/postcss-discard-comments-2.0.4.tgz", + "integrity": "sha1-vv6J+v1bPazlzM5Rt2uBUUvgDj0=", + "requires": { + "postcss": "5.2.18" + }, + "dependencies": { + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=" + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "requires": { + "ansi-styles": "2.2.1", + "escape-string-regexp": "1.0.5", + "has-ansi": "2.0.0", + "strip-ansi": "3.0.1", + "supports-color": "2.0.0" + }, + "dependencies": { + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" + } + } + }, + "has-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", + "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=" + }, + "postcss": { + "version": "5.2.18", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-5.2.18.tgz", + "integrity": "sha512-zrUjRRe1bpXKsX1qAJNJjqZViErVuyEkMTRrwu4ud4sbTtIBRmtaYDrHmcGgmrbsW3MHfmtIf+vJumgQn+PrXg==", + "requires": { + "chalk": "1.1.3", + "js-base64": "2.3.2", + "source-map": "0.5.7", + "supports-color": "3.2.3" + } + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" + }, + "supports-color": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", + "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=", + "requires": { + "has-flag": "1.0.0" + } + } + } + }, + "postcss-discard-duplicates": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/postcss-discard-duplicates/-/postcss-discard-duplicates-2.1.0.tgz", + "integrity": "sha1-uavye4isGIFYpesSq8riAmO5GTI=", + "requires": { + "postcss": "5.2.18" + }, + "dependencies": { + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=" + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "requires": { + "ansi-styles": "2.2.1", + "escape-string-regexp": "1.0.5", + "has-ansi": "2.0.0", + "strip-ansi": "3.0.1", + "supports-color": "2.0.0" + }, + "dependencies": { + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" + } + } + }, + "has-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", + "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=" + }, + "postcss": { + "version": "5.2.18", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-5.2.18.tgz", + "integrity": "sha512-zrUjRRe1bpXKsX1qAJNJjqZViErVuyEkMTRrwu4ud4sbTtIBRmtaYDrHmcGgmrbsW3MHfmtIf+vJumgQn+PrXg==", + "requires": { + "chalk": "1.1.3", + "js-base64": "2.3.2", + "source-map": "0.5.7", + "supports-color": "3.2.3" + } + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" + }, + "supports-color": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", + "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=", + "requires": { + "has-flag": "1.0.0" + } + } + } + }, + "postcss-discard-empty": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/postcss-discard-empty/-/postcss-discard-empty-2.1.0.tgz", + "integrity": "sha1-0rS9nVztXr2Nyt52QMfXzX9PkrU=", + "requires": { + "postcss": "5.2.18" + }, + "dependencies": { + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=" + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "requires": { + "ansi-styles": "2.2.1", + "escape-string-regexp": "1.0.5", + "has-ansi": "2.0.0", + "strip-ansi": "3.0.1", + "supports-color": "2.0.0" + }, + "dependencies": { + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" + } + } + }, + "has-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", + "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=" + }, + "postcss": { + "version": "5.2.18", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-5.2.18.tgz", + "integrity": "sha512-zrUjRRe1bpXKsX1qAJNJjqZViErVuyEkMTRrwu4ud4sbTtIBRmtaYDrHmcGgmrbsW3MHfmtIf+vJumgQn+PrXg==", + "requires": { + "chalk": "1.1.3", + "js-base64": "2.3.2", + "source-map": "0.5.7", + "supports-color": "3.2.3" + } + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" + }, + "supports-color": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", + "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=", + "requires": { + "has-flag": "1.0.0" + } + } + } + }, + "postcss-discard-overridden": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/postcss-discard-overridden/-/postcss-discard-overridden-0.1.1.tgz", + "integrity": "sha1-ix6vVU9ob7KIzYdMVWZ7CqNmjVg=", + "requires": { + "postcss": "5.2.18" + }, + "dependencies": { + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=" + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "requires": { + "ansi-styles": "2.2.1", + "escape-string-regexp": "1.0.5", + "has-ansi": "2.0.0", + "strip-ansi": "3.0.1", + "supports-color": "2.0.0" + }, + "dependencies": { + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" + } + } + }, + "has-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", + "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=" + }, + "postcss": { + "version": "5.2.18", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-5.2.18.tgz", + "integrity": "sha512-zrUjRRe1bpXKsX1qAJNJjqZViErVuyEkMTRrwu4ud4sbTtIBRmtaYDrHmcGgmrbsW3MHfmtIf+vJumgQn+PrXg==", + "requires": { + "chalk": "1.1.3", + "js-base64": "2.3.2", + "source-map": "0.5.7", + "supports-color": "3.2.3" + } + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" + }, + "supports-color": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", + "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=", + "requires": { + "has-flag": "1.0.0" + } + } + } + }, + "postcss-discard-unused": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/postcss-discard-unused/-/postcss-discard-unused-2.2.3.tgz", + "integrity": "sha1-vOMLLMWR/8Y0Mitfs0ZLbZNPRDM=", + "requires": { + "postcss": "5.2.18", + "uniqs": "2.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=" + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "requires": { + "ansi-styles": "2.2.1", + "escape-string-regexp": "1.0.5", + "has-ansi": "2.0.0", + "strip-ansi": "3.0.1", + "supports-color": "2.0.0" + }, + "dependencies": { + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" + } + } + }, + "has-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", + "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=" + }, + "postcss": { + "version": "5.2.18", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-5.2.18.tgz", + "integrity": "sha512-zrUjRRe1bpXKsX1qAJNJjqZViErVuyEkMTRrwu4ud4sbTtIBRmtaYDrHmcGgmrbsW3MHfmtIf+vJumgQn+PrXg==", + "requires": { + "chalk": "1.1.3", + "js-base64": "2.3.2", + "source-map": "0.5.7", + "supports-color": "3.2.3" + } + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" + }, + "supports-color": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", + "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=", + "requires": { + "has-flag": "1.0.0" + } + } + } + }, + "postcss-filter-plugins": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/postcss-filter-plugins/-/postcss-filter-plugins-2.0.2.tgz", + "integrity": "sha1-bYWGJTTXNaxCDkqFgG4fXUKG2Ew=", + "requires": { + "postcss": "5.2.18", + "uniqid": "4.1.1" + }, + "dependencies": { + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=" + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "requires": { + "ansi-styles": "2.2.1", + "escape-string-regexp": "1.0.5", + "has-ansi": "2.0.0", + "strip-ansi": "3.0.1", + "supports-color": "2.0.0" + }, + "dependencies": { + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" + } + } + }, + "has-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", + "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=" + }, + "postcss": { + "version": "5.2.18", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-5.2.18.tgz", + "integrity": "sha512-zrUjRRe1bpXKsX1qAJNJjqZViErVuyEkMTRrwu4ud4sbTtIBRmtaYDrHmcGgmrbsW3MHfmtIf+vJumgQn+PrXg==", + "requires": { + "chalk": "1.1.3", + "js-base64": "2.3.2", + "source-map": "0.5.7", + "supports-color": "3.2.3" + } + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" + }, + "supports-color": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", + "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=", + "requires": { + "has-flag": "1.0.0" + } + } + } + }, + "postcss-js": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-1.0.1.tgz", + "integrity": "sha512-smhUUMF5o5W1ZCQSyh5A3lNOXFLdNrxqyhWbLsGolZH2AgVmlyhxhYbIixfsdKE6r1vG5i7O40DPcvEvE1mvjw==", + "requires": { + "camelcase-css": "1.0.1", + "postcss": "6.0.13" + } + }, + "postcss-less": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/postcss-less/-/postcss-less-1.1.1.tgz", + "integrity": "sha512-zl0EEqq8Urh37Ppdv9zzhpZpLHrgkxmt6e3O4ftRa7/b8Uq2LV+/KBVM8/KuzmHNu+mthhOArg1lxbfqQ3NUdg==", + "requires": { + "postcss": "5.2.18" + }, + "dependencies": { + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=" + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "requires": { + "ansi-styles": "2.2.1", + "escape-string-regexp": "1.0.5", + "has-ansi": "2.0.0", + "strip-ansi": "3.0.1", + "supports-color": "2.0.0" + }, + "dependencies": { + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" + } + } + }, + "has-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", + "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=" + }, + "postcss": { + "version": "5.2.18", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-5.2.18.tgz", + "integrity": "sha512-zrUjRRe1bpXKsX1qAJNJjqZViErVuyEkMTRrwu4ud4sbTtIBRmtaYDrHmcGgmrbsW3MHfmtIf+vJumgQn+PrXg==", + "requires": { + "chalk": "1.1.3", + "js-base64": "2.3.2", + "source-map": "0.5.7", + "supports-color": "3.2.3" + } + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" + }, + "supports-color": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", + "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=", + "requires": { + "has-flag": "1.0.0" + } + } + } + }, + "postcss-load-config": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-1.2.0.tgz", + "integrity": "sha1-U56a/J3chiASHr+djDZz4M5Q0oo=", + "requires": { + "cosmiconfig": "2.2.2", + "object-assign": "4.1.1", + "postcss-load-options": "1.2.0", + "postcss-load-plugins": "2.3.0" + } + }, + "postcss-load-options": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postcss-load-options/-/postcss-load-options-1.2.0.tgz", + "integrity": "sha1-sJixVZ3awt8EvAuzdfmaXP4rbYw=", + "requires": { + "cosmiconfig": "2.2.2", + "object-assign": "4.1.1" + } + }, + "postcss-load-plugins": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/postcss-load-plugins/-/postcss-load-plugins-2.3.0.tgz", + "integrity": "sha1-dFdoEWWZrKLwCfrUJrABdQSdjZI=", + "requires": { + "cosmiconfig": "2.2.2", + "object-assign": "4.1.1" + } + }, + "postcss-loader": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-2.0.6.tgz", + "integrity": "sha512-HIq7yy1hh9KI472Y38iSRV4WupZUNy6zObkxQM/ZuInoaE2+PyX4NcO6jjP5HG5mXL7j5kcNEl0fAG4Kva7O9w==", + "requires": { + "loader-utils": "1.1.0", + "postcss": "6.0.13", + "postcss-load-config": "1.2.0", + "schema-utils": "0.3.0" + } + }, + "postcss-media-query-parser": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/postcss-media-query-parser/-/postcss-media-query-parser-0.2.3.tgz", + "integrity": "sha1-J7Ocb02U+Bsac7j3Y1HGCeXO8kQ=" + }, + "postcss-merge-idents": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/postcss-merge-idents/-/postcss-merge-idents-2.1.7.tgz", + "integrity": "sha1-TFUwMTwI4dWzu/PSu8dH4njuonA=", + "requires": { + "has": "1.0.1", + "postcss": "5.2.18", + "postcss-value-parser": "3.3.0" + }, + "dependencies": { + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=" + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "requires": { + "ansi-styles": "2.2.1", + "escape-string-regexp": "1.0.5", + "has-ansi": "2.0.0", + "strip-ansi": "3.0.1", + "supports-color": "2.0.0" + }, + "dependencies": { + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" + } + } + }, + "has-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", + "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=" + }, + "postcss": { + "version": "5.2.18", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-5.2.18.tgz", + "integrity": "sha512-zrUjRRe1bpXKsX1qAJNJjqZViErVuyEkMTRrwu4ud4sbTtIBRmtaYDrHmcGgmrbsW3MHfmtIf+vJumgQn+PrXg==", + "requires": { + "chalk": "1.1.3", + "js-base64": "2.3.2", + "source-map": "0.5.7", + "supports-color": "3.2.3" + } + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" + }, + "supports-color": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", + "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=", + "requires": { + "has-flag": "1.0.0" + } + } + } + }, + "postcss-merge-longhand": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/postcss-merge-longhand/-/postcss-merge-longhand-2.0.2.tgz", + "integrity": "sha1-I9kM0Sewp3mUkVMyc5A0oaTz1lg=", + "requires": { + "postcss": "5.2.18" + }, + "dependencies": { + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=" + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "requires": { + "ansi-styles": "2.2.1", + "escape-string-regexp": "1.0.5", + "has-ansi": "2.0.0", + "strip-ansi": "3.0.1", + "supports-color": "2.0.0" + }, + "dependencies": { + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" + } + } + }, + "has-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", + "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=" + }, + "postcss": { + "version": "5.2.18", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-5.2.18.tgz", + "integrity": "sha512-zrUjRRe1bpXKsX1qAJNJjqZViErVuyEkMTRrwu4ud4sbTtIBRmtaYDrHmcGgmrbsW3MHfmtIf+vJumgQn+PrXg==", + "requires": { + "chalk": "1.1.3", + "js-base64": "2.3.2", + "source-map": "0.5.7", + "supports-color": "3.2.3" + } + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" + }, + "supports-color": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", + "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=", + "requires": { + "has-flag": "1.0.0" + } + } + } + }, + "postcss-merge-rules": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/postcss-merge-rules/-/postcss-merge-rules-2.1.2.tgz", + "integrity": "sha1-0d9d+qexrMO+VT8OnhDofGG19yE=", + "requires": { + "browserslist": "1.7.7", + "caniuse-api": "1.6.1", + "postcss": "5.2.18", + "postcss-selector-parser": "2.2.3", + "vendors": "1.0.1" + }, + "dependencies": { + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=" + }, + "browserslist": { + "version": "1.7.7", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-1.7.7.tgz", + "integrity": "sha1-C9dnBCWL6CmyOYu1Dkti0aFmsLk=", + "requires": { + "caniuse-db": "1.0.30000748", + "electron-to-chromium": "1.3.27" + } + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "requires": { + "ansi-styles": "2.2.1", + "escape-string-regexp": "1.0.5", + "has-ansi": "2.0.0", + "strip-ansi": "3.0.1", + "supports-color": "2.0.0" + }, + "dependencies": { + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" + } + } + }, + "has-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", + "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=" + }, + "postcss": { + "version": "5.2.18", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-5.2.18.tgz", + "integrity": "sha512-zrUjRRe1bpXKsX1qAJNJjqZViErVuyEkMTRrwu4ud4sbTtIBRmtaYDrHmcGgmrbsW3MHfmtIf+vJumgQn+PrXg==", + "requires": { + "chalk": "1.1.3", + "js-base64": "2.3.2", + "source-map": "0.5.7", + "supports-color": "3.2.3" + } + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" + }, + "supports-color": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", + "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=", + "requires": { + "has-flag": "1.0.0" + } + } + } + }, + "postcss-message-helpers": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postcss-message-helpers/-/postcss-message-helpers-2.0.0.tgz", + "integrity": "sha1-pPL0+rbk/gAvCu0ABHjN9S+bpg4=" + }, + "postcss-minify-font-values": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/postcss-minify-font-values/-/postcss-minify-font-values-1.0.5.tgz", + "integrity": "sha1-S1jttWZB66fIR0qzUmyv17vey2k=", + "requires": { + "object-assign": "4.1.1", + "postcss": "5.2.18", + "postcss-value-parser": "3.3.0" + }, + "dependencies": { + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=" + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "requires": { + "ansi-styles": "2.2.1", + "escape-string-regexp": "1.0.5", + "has-ansi": "2.0.0", + "strip-ansi": "3.0.1", + "supports-color": "2.0.0" + }, + "dependencies": { + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" + } + } + }, + "has-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", + "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=" + }, + "postcss": { + "version": "5.2.18", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-5.2.18.tgz", + "integrity": "sha512-zrUjRRe1bpXKsX1qAJNJjqZViErVuyEkMTRrwu4ud4sbTtIBRmtaYDrHmcGgmrbsW3MHfmtIf+vJumgQn+PrXg==", + "requires": { + "chalk": "1.1.3", + "js-base64": "2.3.2", + "source-map": "0.5.7", + "supports-color": "3.2.3" + } + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" + }, + "supports-color": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", + "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=", + "requires": { + "has-flag": "1.0.0" + } + } + } + }, + "postcss-minify-gradients": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/postcss-minify-gradients/-/postcss-minify-gradients-1.0.5.tgz", + "integrity": "sha1-Xb2hE3NwP4PPtKPqOIHY11/15uE=", + "requires": { + "postcss": "5.2.18", + "postcss-value-parser": "3.3.0" + }, + "dependencies": { + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=" + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "requires": { + "ansi-styles": "2.2.1", + "escape-string-regexp": "1.0.5", + "has-ansi": "2.0.0", + "strip-ansi": "3.0.1", + "supports-color": "2.0.0" + }, + "dependencies": { + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" + } + } + }, + "has-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", + "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=" + }, + "postcss": { + "version": "5.2.18", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-5.2.18.tgz", + "integrity": "sha512-zrUjRRe1bpXKsX1qAJNJjqZViErVuyEkMTRrwu4ud4sbTtIBRmtaYDrHmcGgmrbsW3MHfmtIf+vJumgQn+PrXg==", + "requires": { + "chalk": "1.1.3", + "js-base64": "2.3.2", + "source-map": "0.5.7", + "supports-color": "3.2.3" + } + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" + }, + "supports-color": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", + "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=", + "requires": { + "has-flag": "1.0.0" + } + } + } + }, + "postcss-minify-params": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/postcss-minify-params/-/postcss-minify-params-1.2.2.tgz", + "integrity": "sha1-rSzgcTc7lDs9kwo/pZo1jCjW8fM=", + "requires": { + "alphanum-sort": "1.0.2", + "postcss": "5.2.18", + "postcss-value-parser": "3.3.0", + "uniqs": "2.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=" + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "requires": { + "ansi-styles": "2.2.1", + "escape-string-regexp": "1.0.5", + "has-ansi": "2.0.0", + "strip-ansi": "3.0.1", + "supports-color": "2.0.0" + }, + "dependencies": { + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" + } + } + }, + "has-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", + "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=" + }, + "postcss": { + "version": "5.2.18", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-5.2.18.tgz", + "integrity": "sha512-zrUjRRe1bpXKsX1qAJNJjqZViErVuyEkMTRrwu4ud4sbTtIBRmtaYDrHmcGgmrbsW3MHfmtIf+vJumgQn+PrXg==", + "requires": { + "chalk": "1.1.3", + "js-base64": "2.3.2", + "source-map": "0.5.7", + "supports-color": "3.2.3" + } + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" + }, + "supports-color": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", + "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=", + "requires": { + "has-flag": "1.0.0" + } + } + } + }, + "postcss-minify-selectors": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/postcss-minify-selectors/-/postcss-minify-selectors-2.1.1.tgz", + "integrity": "sha1-ssapjAByz5G5MtGkllCBFDEXNb8=", + "requires": { + "alphanum-sort": "1.0.2", + "has": "1.0.1", + "postcss": "5.2.18", + "postcss-selector-parser": "2.2.3" + }, + "dependencies": { + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=" + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "requires": { + "ansi-styles": "2.2.1", + "escape-string-regexp": "1.0.5", + "has-ansi": "2.0.0", + "strip-ansi": "3.0.1", + "supports-color": "2.0.0" + }, + "dependencies": { + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" + } + } + }, + "has-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", + "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=" + }, + "postcss": { + "version": "5.2.18", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-5.2.18.tgz", + "integrity": "sha512-zrUjRRe1bpXKsX1qAJNJjqZViErVuyEkMTRrwu4ud4sbTtIBRmtaYDrHmcGgmrbsW3MHfmtIf+vJumgQn+PrXg==", + "requires": { + "chalk": "1.1.3", + "js-base64": "2.3.2", + "source-map": "0.5.7", + "supports-color": "3.2.3" + } + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" + }, + "supports-color": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", + "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=", + "requires": { + "has-flag": "1.0.0" + } + } + } + }, + "postcss-mixins": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/postcss-mixins/-/postcss-mixins-6.1.1.tgz", + "integrity": "sha512-wjVUSlKczG4D5xlO5a8UTEJ2FPiuRwNc6qdXm3b4ZucCu2bYfihG2ELTfeDj14k1vBPxMP1mPWK65LeGkszQzw==", + "requires": { + "globby": "6.1.0", + "postcss": "6.0.13", + "postcss-js": "1.0.1", + "postcss-simple-vars": "4.1.0", + "sugarss": "1.0.0" + } + }, + "postcss-modules-extract-imports": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-1.1.0.tgz", + "integrity": "sha1-thTJcgvmgW6u41+zpfqh26agXds=", + "requires": { + "postcss": "6.0.13" + } + }, + "postcss-modules-local-by-default": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-1.2.0.tgz", + "integrity": "sha1-99gMOYxaOT+nlkRmvRlQCn1hwGk=", + "requires": { + "css-selector-tokenizer": "0.7.0", + "postcss": "6.0.13" + } + }, + "postcss-modules-scope": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-1.1.0.tgz", + "integrity": "sha1-1upkmUx5+XtipytCb75gVqGUu5A=", + "requires": { + "css-selector-tokenizer": "0.7.0", + "postcss": "6.0.13" + } + }, + "postcss-modules-values": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-1.3.0.tgz", + "integrity": "sha1-7P+p1+GSUYOJ9CrQ6D9yrsRW6iA=", + "requires": { + "icss-replace-symbols": "1.1.0", + "postcss": "6.0.13" + } + }, + "postcss-nested": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-2.1.2.tgz", + "integrity": "sha512-CU7KjbFOZSNrbFwrl8+KJHTj29GjCEhL86kCKyvf+k633fc+FQA6IuhGyPze5e+a4O5d2fP7hDlMOlVDXia1Xg==", + "requires": { + "postcss": "6.0.13", + "postcss-selector-parser": "2.2.3" + } + }, + "postcss-normalize-charset": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-charset/-/postcss-normalize-charset-1.1.1.tgz", + "integrity": "sha1-757nEhLX/nWceO0WL2HtYrXLk/E=", + "requires": { + "postcss": "5.2.18" + }, + "dependencies": { + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=" + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "requires": { + "ansi-styles": "2.2.1", + "escape-string-regexp": "1.0.5", + "has-ansi": "2.0.0", + "strip-ansi": "3.0.1", + "supports-color": "2.0.0" + }, + "dependencies": { + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" + } + } + }, + "has-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", + "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=" + }, + "postcss": { + "version": "5.2.18", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-5.2.18.tgz", + "integrity": "sha512-zrUjRRe1bpXKsX1qAJNJjqZViErVuyEkMTRrwu4ud4sbTtIBRmtaYDrHmcGgmrbsW3MHfmtIf+vJumgQn+PrXg==", + "requires": { + "chalk": "1.1.3", + "js-base64": "2.3.2", + "source-map": "0.5.7", + "supports-color": "3.2.3" + } + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" + }, + "supports-color": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", + "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=", + "requires": { + "has-flag": "1.0.0" + } + } + } + }, + "postcss-normalize-url": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/postcss-normalize-url/-/postcss-normalize-url-3.0.8.tgz", + "integrity": "sha1-EI90s/L82viRov+j6kWSJ5/HgiI=", + "requires": { + "is-absolute-url": "2.1.0", + "normalize-url": "1.9.1", + "postcss": "5.2.18", + "postcss-value-parser": "3.3.0" + }, + "dependencies": { + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=" + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "requires": { + "ansi-styles": "2.2.1", + "escape-string-regexp": "1.0.5", + "has-ansi": "2.0.0", + "strip-ansi": "3.0.1", + "supports-color": "2.0.0" + }, + "dependencies": { + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" + } + } + }, + "has-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", + "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=" + }, + "postcss": { + "version": "5.2.18", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-5.2.18.tgz", + "integrity": "sha512-zrUjRRe1bpXKsX1qAJNJjqZViErVuyEkMTRrwu4ud4sbTtIBRmtaYDrHmcGgmrbsW3MHfmtIf+vJumgQn+PrXg==", + "requires": { + "chalk": "1.1.3", + "js-base64": "2.3.2", + "source-map": "0.5.7", + "supports-color": "3.2.3" + } + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" + }, + "supports-color": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", + "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=", + "requires": { + "has-flag": "1.0.0" + } + } + } + }, + "postcss-ordered-values": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/postcss-ordered-values/-/postcss-ordered-values-2.2.3.tgz", + "integrity": "sha1-7sbCpntsQSqNsgQud/6NpD+VwR0=", + "requires": { + "postcss": "5.2.18", + "postcss-value-parser": "3.3.0" + }, + "dependencies": { + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=" + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "requires": { + "ansi-styles": "2.2.1", + "escape-string-regexp": "1.0.5", + "has-ansi": "2.0.0", + "strip-ansi": "3.0.1", + "supports-color": "2.0.0" + }, + "dependencies": { + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" + } + } + }, + "has-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", + "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=" + }, + "postcss": { + "version": "5.2.18", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-5.2.18.tgz", + "integrity": "sha512-zrUjRRe1bpXKsX1qAJNJjqZViErVuyEkMTRrwu4ud4sbTtIBRmtaYDrHmcGgmrbsW3MHfmtIf+vJumgQn+PrXg==", + "requires": { + "chalk": "1.1.3", + "js-base64": "2.3.2", + "source-map": "0.5.7", + "supports-color": "3.2.3" + } + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" + }, + "supports-color": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", + "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=", + "requires": { + "has-flag": "1.0.0" + } + } + } + }, + "postcss-reduce-idents": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/postcss-reduce-idents/-/postcss-reduce-idents-2.4.0.tgz", + "integrity": "sha1-wsbSDMlYKE9qv75j92Cb9AkFmtM=", + "requires": { + "postcss": "5.2.18", + "postcss-value-parser": "3.3.0" + }, + "dependencies": { + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=" + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "requires": { + "ansi-styles": "2.2.1", + "escape-string-regexp": "1.0.5", + "has-ansi": "2.0.0", + "strip-ansi": "3.0.1", + "supports-color": "2.0.0" + }, + "dependencies": { + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" + } + } + }, + "has-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", + "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=" + }, + "postcss": { + "version": "5.2.18", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-5.2.18.tgz", + "integrity": "sha512-zrUjRRe1bpXKsX1qAJNJjqZViErVuyEkMTRrwu4ud4sbTtIBRmtaYDrHmcGgmrbsW3MHfmtIf+vJumgQn+PrXg==", + "requires": { + "chalk": "1.1.3", + "js-base64": "2.3.2", + "source-map": "0.5.7", + "supports-color": "3.2.3" + } + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" + }, + "supports-color": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", + "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=", + "requires": { + "has-flag": "1.0.0" + } + } + } + }, + "postcss-reduce-initial": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/postcss-reduce-initial/-/postcss-reduce-initial-1.0.1.tgz", + "integrity": "sha1-aPgGlfBF0IJjqHmtJA343WT2ROo=", + "requires": { + "postcss": "5.2.18" + }, + "dependencies": { + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=" + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "requires": { + "ansi-styles": "2.2.1", + "escape-string-regexp": "1.0.5", + "has-ansi": "2.0.0", + "strip-ansi": "3.0.1", + "supports-color": "2.0.0" + }, + "dependencies": { + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" + } + } + }, + "has-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", + "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=" + }, + "postcss": { + "version": "5.2.18", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-5.2.18.tgz", + "integrity": "sha512-zrUjRRe1bpXKsX1qAJNJjqZViErVuyEkMTRrwu4ud4sbTtIBRmtaYDrHmcGgmrbsW3MHfmtIf+vJumgQn+PrXg==", + "requires": { + "chalk": "1.1.3", + "js-base64": "2.3.2", + "source-map": "0.5.7", + "supports-color": "3.2.3" + } + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" + }, + "supports-color": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", + "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=", + "requires": { + "has-flag": "1.0.0" + } + } + } + }, + "postcss-reduce-transforms": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/postcss-reduce-transforms/-/postcss-reduce-transforms-1.0.4.tgz", + "integrity": "sha1-/3b02CEkN7McKYpC0uFEQCV3GuE=", + "requires": { + "has": "1.0.1", + "postcss": "5.2.18", + "postcss-value-parser": "3.3.0" + }, + "dependencies": { + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=" + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "requires": { + "ansi-styles": "2.2.1", + "escape-string-regexp": "1.0.5", + "has-ansi": "2.0.0", + "strip-ansi": "3.0.1", + "supports-color": "2.0.0" + }, + "dependencies": { + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" + } + } + }, + "has-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", + "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=" + }, + "postcss": { + "version": "5.2.18", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-5.2.18.tgz", + "integrity": "sha512-zrUjRRe1bpXKsX1qAJNJjqZViErVuyEkMTRrwu4ud4sbTtIBRmtaYDrHmcGgmrbsW3MHfmtIf+vJumgQn+PrXg==", + "requires": { + "chalk": "1.1.3", + "js-base64": "2.3.2", + "source-map": "0.5.7", + "supports-color": "3.2.3" + } + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" + }, + "supports-color": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", + "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=", + "requires": { + "has-flag": "1.0.0" + } + } + } + }, + "postcss-reporter": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/postcss-reporter/-/postcss-reporter-5.0.0.tgz", + "integrity": "sha512-rBkDbaHAu5uywbCR2XE8a25tats3xSOsGNx6mppK6Q9kSFGKc/FyAzfci+fWM2l+K402p1D0pNcfDGxeje5IKg==", + "requires": { + "chalk": "2.2.0", + "lodash": "4.17.4", + "log-symbols": "2.1.0", + "postcss": "6.0.13" + }, + "dependencies": { + "log-symbols": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-2.1.0.tgz", + "integrity": "sha512-zLeLrzMA1A2vRF1e/0Mo+LNINzi6jzBylHj5WqvQ/WK/5WCZt8si9SyN4p9llr/HRYvVR1AoXHRHl4WTHyQAzQ==", + "requires": { + "chalk": "2.2.0" + } + } + } + }, + "postcss-resolve-nested-selector": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/postcss-resolve-nested-selector/-/postcss-resolve-nested-selector-0.1.1.tgz", + "integrity": "sha1-Kcy8fDfe36wwTp//C/FZaz9qDk4=" + }, + "postcss-safe-parser": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/postcss-safe-parser/-/postcss-safe-parser-3.0.1.tgz", + "integrity": "sha1-t1Pv9sfArqXoN1++TN6L+QY/8UI=", + "requires": { + "postcss": "6.0.13" + } + }, + "postcss-scss": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/postcss-scss/-/postcss-scss-1.0.2.tgz", + "integrity": "sha1-/0XPM1S4ee6JpOtoaA9GrJuxT5Q=", + "requires": { + "postcss": "6.0.13" + } + }, + "postcss-selector-parser": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-2.2.3.tgz", + "integrity": "sha1-+UN3iGBsPJrO4W/+jYsWKX8nu5A=", + "requires": { + "flatten": "1.0.2", + "indexes-of": "1.0.1", + "uniq": "1.0.1" + } + }, + "postcss-simple-vars": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/postcss-simple-vars/-/postcss-simple-vars-4.1.0.tgz", + "integrity": "sha512-J/TRomA8EqXhS4VjQJsPCYTFIa9FYN/dkJK/8oZ0BYeVIPx91goqM8T+ljsP57+4bwSEywFOuB7EZ8n1gjjxZw==", + "requires": { + "postcss": "6.0.13" + } + }, + "postcss-sorting": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/postcss-sorting/-/postcss-sorting-3.0.2.tgz", + "integrity": "sha1-c+aRO3FUJiAdIuihdpsFAio3qvw=", + "requires": { + "lodash": "4.17.4", + "postcss": "6.0.13" + } + }, + "postcss-svgo": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/postcss-svgo/-/postcss-svgo-2.1.6.tgz", + "integrity": "sha1-tt8YqmE7Zm4TPwittSGcJoSsEI0=", + "requires": { + "is-svg": "2.1.0", + "postcss": "5.2.18", + "postcss-value-parser": "3.3.0", + "svgo": "0.7.2" + }, + "dependencies": { + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=" + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "requires": { + "ansi-styles": "2.2.1", + "escape-string-regexp": "1.0.5", + "has-ansi": "2.0.0", + "strip-ansi": "3.0.1", + "supports-color": "2.0.0" + }, + "dependencies": { + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" + } + } + }, + "has-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", + "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=" + }, + "postcss": { + "version": "5.2.18", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-5.2.18.tgz", + "integrity": "sha512-zrUjRRe1bpXKsX1qAJNJjqZViErVuyEkMTRrwu4ud4sbTtIBRmtaYDrHmcGgmrbsW3MHfmtIf+vJumgQn+PrXg==", + "requires": { + "chalk": "1.1.3", + "js-base64": "2.3.2", + "source-map": "0.5.7", + "supports-color": "3.2.3" + } + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" + }, + "supports-color": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", + "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=", + "requires": { + "has-flag": "1.0.0" + } + } + } + }, + "postcss-unique-selectors": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/postcss-unique-selectors/-/postcss-unique-selectors-2.0.2.tgz", + "integrity": "sha1-mB1X0p3csz57Hf4f1DuGSfkzyh0=", + "requires": { + "alphanum-sort": "1.0.2", + "postcss": "5.2.18", + "uniqs": "2.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=" + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "requires": { + "ansi-styles": "2.2.1", + "escape-string-regexp": "1.0.5", + "has-ansi": "2.0.0", + "strip-ansi": "3.0.1", + "supports-color": "2.0.0" + }, + "dependencies": { + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" + } + } + }, + "has-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", + "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=" + }, + "postcss": { + "version": "5.2.18", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-5.2.18.tgz", + "integrity": "sha512-zrUjRRe1bpXKsX1qAJNJjqZViErVuyEkMTRrwu4ud4sbTtIBRmtaYDrHmcGgmrbsW3MHfmtIf+vJumgQn+PrXg==", + "requires": { + "chalk": "1.1.3", + "js-base64": "2.3.2", + "source-map": "0.5.7", + "supports-color": "3.2.3" + } + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" + }, + "supports-color": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", + "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=", + "requires": { + "has-flag": "1.0.0" + } + } + } + }, + "postcss-value-parser": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.0.tgz", + "integrity": "sha1-h/OPnxj3dKSrTIojL1xc6IcqnRU=" + }, + "postcss-zindex": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/postcss-zindex/-/postcss-zindex-2.2.0.tgz", + "integrity": "sha1-0hCd3AVbka9n/EyzsCWUZjnSryI=", + "requires": { + "has": "1.0.1", + "postcss": "5.2.18", + "uniqs": "2.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=" + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "requires": { + "ansi-styles": "2.2.1", + "escape-string-regexp": "1.0.5", + "has-ansi": "2.0.0", + "strip-ansi": "3.0.1", + "supports-color": "2.0.0" + }, + "dependencies": { + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" + } + } + }, + "has-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", + "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=" + }, + "postcss": { + "version": "5.2.18", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-5.2.18.tgz", + "integrity": "sha512-zrUjRRe1bpXKsX1qAJNJjqZViErVuyEkMTRrwu4ud4sbTtIBRmtaYDrHmcGgmrbsW3MHfmtIf+vJumgQn+PrXg==", + "requires": { + "chalk": "1.1.3", + "js-base64": "2.3.2", + "source-map": "0.5.7", + "supports-color": "3.2.3" + } + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" + }, + "supports-color": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", + "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=", + "requires": { + "has-flag": "1.0.0" + } + } + } + }, + "prefix-style": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/prefix-style/-/prefix-style-2.0.1.tgz", + "integrity": "sha1-ZrupqHDP2jCKXcIOhekSCTLJWgY=" + }, + "prelude-ls": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", + "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=" + }, + "prepend-http": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-1.0.4.tgz", + "integrity": "sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw=" + }, + "preserve": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/preserve/-/preserve-0.2.0.tgz", + "integrity": "sha1-gV7R9uvGWSb4ZbMQwHE7yzMVzks=" + }, + "pretty-hrtime": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz", + "integrity": "sha1-t+PqQkNaTJsnWdmeDyAesZWALuE=" + }, + "private": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/private/-/private-0.1.8.tgz", + "integrity": "sha512-VvivMrbvd2nKkiG38qjULzlc+4Vx4wm/whI9pQD35YrARNnhxeiRktSOhSukRLFNlzg6Br/cJPet5J/u19r/mg==" + }, + "process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha1-czIwDoQBYb2j5podHZGn1LwW8YI=" + }, + "process-nextick-args": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz", + "integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M=" + }, + "progress": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.0.tgz", + "integrity": "sha1-ihvjZr+Pwj2yvSPxDG/pILQ4nR8=" + }, + "promise": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz", + "integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==", + "requires": { + "asap": "2.0.6" + } + }, + "prop-types": { + "version": "15.6.0", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.6.0.tgz", + "integrity": "sha1-zq8IMCL8RrSjX2nhPvda7Q1jmFY=", + "requires": { + "fbjs": "0.8.16", + "loose-envify": "1.3.1", + "object-assign": "4.1.1" + } + }, + "protochain": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/protochain/-/protochain-1.0.5.tgz", + "integrity": "sha1-mRxAfpneJkqt+PgVBLXn+ve/omA=" + }, + "prr": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/prr/-/prr-0.0.0.tgz", + "integrity": "sha1-GoS4WQgyVQFBGFPQCB7j+obikmo=" + }, + "pseudomap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", + "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=" + }, + "public-encrypt": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.0.tgz", + "integrity": "sha1-OfaZ86RlYN1eusvKaTyvfGXBjMY=", + "requires": { + "bn.js": "4.11.8", + "browserify-rsa": "4.0.1", + "create-hash": "1.1.3", + "parse-asn1": "5.1.0", + "randombytes": "2.0.5" + } + }, + "punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" + }, + "q": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", + "integrity": "sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=" + }, + "qs": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.1.tgz", + "integrity": "sha512-eRzhrN1WSINYCDCbrz796z37LOe3m5tmW7RQf6oBntukAG1nmovJvhnwHHRMAfeoItc1m2Hk02WER2aQ/iqs+A==" + }, + "query-string": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/query-string/-/query-string-5.0.0.tgz", + "integrity": "sha1-+99wBLTSr/eS+YcZgbeieU9VWUc=", + "requires": { + "decode-uri-component": "0.2.0", + "object-assign": "4.1.1", + "strict-uri-encode": "1.1.0" + } + }, + "querystring": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", + "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=" + }, + "querystring-es3": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/querystring-es3/-/querystring-es3-0.2.1.tgz", + "integrity": "sha1-nsYfeQSYdXB9aUFFlv2Qek1xHnM=" + }, + "raf": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/raf/-/raf-3.4.0.tgz", + "integrity": "sha512-pDP/NMRAXoTfrhCfyfSEwJAKLaxBU9eApMeBPB1TkDouZmvPerIClV8lTAd+uF8ZiTaVl69e1FCxQrAd/VTjGw==", + "requires": { + "performance-now": "2.1.0" + } + }, + "randomatic": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/randomatic/-/randomatic-1.1.7.tgz", + "integrity": "sha512-D5JUjPyJbaJDkuAazpVnSfVkLlpeO3wDlPROTMLGKG1zMFNFRgrciKo1ltz/AzNTkqE0HzDx655QOL51N06how==", + "requires": { + "is-number": "3.0.0", + "kind-of": "4.0.0" + }, + "dependencies": { + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "requires": { + "kind-of": "3.2.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "1.1.5" + } + } + } + }, + "kind-of": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", + "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=", + "requires": { + "is-buffer": "1.1.5" + } + } + } + }, + "randombytes": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.0.5.tgz", + "integrity": "sha512-8T7Zn1AhMsQ/HI1SjcCfT/t4ii3eAqco3yOcSzS4mozsOz69lHLsoMXmF9nZgnFanYscnSlUSgs8uZyKzpE6kg==", + "requires": { + "safe-buffer": "5.1.1" + } + }, + "raven-for-redux": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/raven-for-redux/-/raven-for-redux-1.0.0.tgz", + "integrity": "sha1-NnHC/TmxVbkucBOJWAb/df8tXpY=" + }, + "raven-js": { + "version": "3.17.0", + "resolved": "https://registry.npmjs.org/raven-js/-/raven-js-3.17.0.tgz", + "integrity": "sha1-d5RXrHkQUSw8LMm7bQqe61mpaew=" + }, + "raw-body": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.1.7.tgz", + "integrity": "sha1-rf6s4uT7MJgFgBTQjActzFl1h3Q=", + "requires": { + "bytes": "2.4.0", + "iconv-lite": "0.4.13", + "unpipe": "1.0.0" + }, + "dependencies": { + "bytes": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-2.4.0.tgz", + "integrity": "sha1-fZcZb51br39pNeJZhVSe3SpsIzk=" + }, + "iconv-lite": { + "version": "0.4.13", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.13.tgz", + "integrity": "sha1-H4irpKsLFQjoMSrMOTRfNumS4vI=" + } + } + }, + "react": { + "version": "15.6.0", + "resolved": "https://registry.npmjs.org/react/-/react-15.6.0.tgz", + "integrity": "sha1-wjKZtI4w7TAlCM6J4aAskZ+Ca84=", + "requires": { + "create-react-class": "15.6.2", + "fbjs": "0.8.16", + "loose-envify": "1.3.1", + "object-assign": "4.1.1", + "prop-types": "15.6.0" + } + }, + "react-addons-shallow-compare": { + "version": "15.6.2", + "resolved": "https://registry.npmjs.org/react-addons-shallow-compare/-/react-addons-shallow-compare-15.6.2.tgz", + "integrity": "sha1-GYoAuR/DdiPbZKKP0XtZa6NicC8=", + "requires": { + "fbjs": "0.8.16", + "object-assign": "4.1.1" + } + }, + "react-async-script": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/react-async-script/-/react-async-script-0.9.1.tgz", + "integrity": "sha1-1KSWxyb6sMbvBZmKsW5Qx0KCC2A=", + "requires": { + "prop-types": "15.6.0" + } + }, + "react-autosuggest": { + "version": "9.3.2", + "resolved": "https://registry.npmjs.org/react-autosuggest/-/react-autosuggest-9.3.2.tgz", + "integrity": "sha512-/wY64zaFVny9OqcRvLwhcJz9SzpYK5qni+7sjrQOPKN1gFKi5cxy+cMOPEE7u5ZfXLj5mDxf+RNxNWM1wo39Xg==", + "requires": { + "prop-types": "15.6.0", + "react-autowhatever": "10.1.0", + "shallow-equal": "1.0.0" + } + }, + "react-autowhatever": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/react-autowhatever/-/react-autowhatever-10.1.0.tgz", + "integrity": "sha512-LMZggoRgcmldAMyABY3Dz/DRiTQViMsQllXtOsDrZeBRwPIfn0RAOySaQMUNyECrHaCB5pm66jgQvkyNSh/BjA==", + "requires": { + "prop-types": "15.6.0", + "react-themeable": "1.1.0", + "section-iterator": "2.0.0" + } + }, + "react-custom-scrollbars": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/react-custom-scrollbars/-/react-custom-scrollbars-4.1.2.tgz", + "integrity": "sha1-DmDEpGxKYfnkmUp2Y+K5y7xRh6M=", + "requires": { + "dom-css": "2.1.0", + "prop-types": "15.6.0", + "raf": "3.4.0" + } + }, + "react-dnd": { + "version": "2.5.4", + "resolved": "https://registry.npmjs.org/react-dnd/-/react-dnd-2.5.4.tgz", + "integrity": "sha512-y9YmnusURc+3KPgvhYKvZ9oCucj51MSZWODyaeV0KFU0cquzA7dCD1g/OIYUKtNoZ+MXtacDngkdud2TklMSjw==", + "requires": { + "disposables": "1.0.1", + "dnd-core": "2.5.4", + "hoist-non-react-statics": "2.3.1", + "invariant": "2.2.2", + "lodash": "4.17.4", + "prop-types": "15.6.0" + } + }, + "react-dnd-html5-backend": { + "version": "2.5.4", + "resolved": "https://registry.npmjs.org/react-dnd-html5-backend/-/react-dnd-html5-backend-2.5.4.tgz", + "integrity": "sha512-jDqAkm/hI8Tl4HcsbhkBgB6HgpJR1e+ML1SbfxaegXYiuMxEVQm0FOwEH5WxUoo6fmIG4N+H0rSm59POuZOCaA==", + "requires": { + "lodash": "4.17.4" + } + }, + "react-document-title": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/react-document-title/-/react-document-title-2.0.3.tgz", + "integrity": "sha1-u/kioNcUEvyUgkXkKDskEt9w8rk=", + "requires": { + "prop-types": "15.6.0", + "react-side-effect": "1.1.3" + } + }, + "react-dom": { + "version": "15.6.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-15.6.0.tgz", + "integrity": "sha1-i8I8sMgOcGNVt2yp+M5Hz3vfttE=", + "requires": { + "fbjs": "0.8.16", + "loose-envify": "1.3.1", + "object-assign": "4.1.1", + "prop-types": "15.5.10" + }, + "dependencies": { + "prop-types": { + "version": "15.5.10", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.5.10.tgz", + "integrity": "sha1-J5ffwxJhguOpXj37suiT3ddFYVQ=", + "requires": { + "fbjs": "0.8.16", + "loose-envify": "1.3.1" + } + } + } + }, + "react-google-recaptcha": { + "version": "0.9.7", + "resolved": "https://registry.npmjs.org/react-google-recaptcha/-/react-google-recaptcha-0.9.7.tgz", + "integrity": "sha1-E62UQBHQJWbovQbuhWes4hodDtY=", + "requires": { + "prop-types": "15.6.0" + } + }, + "react-lazyload": { + "version": "2.2.7", + "resolved": "https://registry.npmjs.org/react-lazyload/-/react-lazyload-2.2.7.tgz", + "integrity": "sha1-at8HE/MiQKIcYwq10ejPYtDCZK8=", + "requires": { + "prop-types": "15.6.0" + } + }, + "react-measure": { + "version": "1.4.7", + "resolved": "https://registry.npmjs.org/react-measure/-/react-measure-1.4.7.tgz", + "integrity": "sha1-odLKDc/vBJeLesJjp2XctqCTb9s=", + "requires": { + "get-node-dimensions": "1.2.0", + "prop-types": "15.6.0", + "resize-observer-polyfill": "1.4.2" + } + }, + "react-portal": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/react-portal/-/react-portal-3.1.0.tgz", + "integrity": "sha1-hlxE+3Kh2hBsZJIGk2VZzoke6Jk=", + "requires": { + "prop-types": "15.6.0" + } + }, + "react-redux": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-5.0.6.tgz", + "integrity": "sha512-8taaaGu+J7PMJQDJrk/xiWEYQmdo3mkXw6wPr3K3LxvXis3Fymiq7c13S+Tpls/AyNUAsoONkU81AP0RA6y6Vw==", + "requires": { + "hoist-non-react-statics": "2.3.1", + "invariant": "2.2.2", + "lodash": "4.17.4", + "lodash-es": "4.17.4", + "loose-envify": "1.3.1", + "prop-types": "15.6.0" + } + }, + "react-router": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-4.2.0.tgz", + "integrity": "sha512-DY6pjwRhdARE4TDw7XjxjZsbx9lKmIcyZoZ+SDO7SBJ1KUeWNxT22Kara2AC7u6/c2SYEHlEDLnzBCcNhLE8Vg==", + "requires": { + "history": "4.7.2", + "hoist-non-react-statics": "2.3.1", + "invariant": "2.2.2", + "loose-envify": "1.3.1", + "path-to-regexp": "1.7.0", + "prop-types": "15.6.0", + "warning": "3.0.0" + } + }, + "react-router-dom": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-4.2.2.tgz", + "integrity": "sha512-cHMFC1ZoLDfEaMFoKTjN7fry/oczMgRt5BKfMAkTu5zEuJvUiPp1J8d0eXSVTnBh6pxlbdqDhozunOOLtmKfPA==", + "requires": { + "history": "4.7.2", + "invariant": "2.2.2", + "loose-envify": "1.3.1", + "prop-types": "15.6.0", + "react-router": "4.2.0", + "warning": "3.0.0" + } + }, + "react-router-redux": { + "version": "5.0.0-alpha.6", + "resolved": "https://registry.npmjs.org/react-router-redux/-/react-router-redux-5.0.0-alpha.6.tgz", + "integrity": "sha1-dBhmPC7NPFG+hW/PKPPR3uzBpXY=", + "requires": { + "history": "4.7.2", + "prop-types": "15.6.0", + "react-router": "4.2.0" + } + }, + "react-side-effect": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/react-side-effect/-/react-side-effect-1.1.3.tgz", + "integrity": "sha1-USwlq+DewXKDTEAB7FxR4E1BvFw=", + "requires": { + "exenv": "1.2.2", + "shallowequal": "1.0.2" + } + }, + "react-slider": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/react-slider/-/react-slider-0.9.0.tgz", + "integrity": "sha512-xAcW33uW82317OcJw0vcpT2N949MqwNSy53CiuPwXOEx/g4BdFbOgfemlsueEd+3q7DvAAP/Kj8fiy+1rekfiA==" + }, + "react-tabs": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/react-tabs/-/react-tabs-2.1.0.tgz", + "integrity": "sha512-40jhOZ5ptJFRhO/6hxCesjv24f83HBWKjV2hJTIN5qZBKmZmTDMbIcgEjUUZ5fPSRhAvhv8rVrmZpIMIRnR9BQ==", + "requires": { + "classnames": "2.2.5", + "prop-types": "15.6.0" + } + }, + "react-tag-autocomplete": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/react-tag-autocomplete/-/react-tag-autocomplete-5.4.1.tgz", + "integrity": "sha1-lBJmxz8m4y8VHtlPyShSWP3/Dv8=" + }, + "react-tether": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/react-tether/-/react-tether-0.5.7.tgz", + "integrity": "sha1-QY6mEEG2W5WCcUeEibcaNXLwFCI=", + "requires": { + "prop-types": "15.6.0", + "tether": "1.4.0" + } + }, + "react-text-truncate": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/react-text-truncate/-/react-text-truncate-0.12.0.tgz", + "integrity": "sha1-SJsZnyGL5Iqz35vh31eEU0j3fiM=", + "requires": { + "prop-types": "15.6.0" + } + }, + "react-themeable": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/react-themeable/-/react-themeable-1.1.0.tgz", + "integrity": "sha1-fURm3ZsrX6dQWHJ4JenxUro3mg4=", + "requires": { + "object-assign": "3.0.0" + }, + "dependencies": { + "object-assign": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-3.0.0.tgz", + "integrity": "sha1-m+3VygiXlJvKR+f/QIBi1Un1h/I=" + } + } + }, + "react-truncate": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/react-truncate/-/react-truncate-2.2.2.tgz", + "integrity": "sha512-9WEnc2xopxCnCIipm4kvoV1KJof1lyrH17vq0qkCUrY7LRpL7baftX6es3WQfNGLsm74XKBMqQWTLdP1Rjek1w==" + }, + "react-virtualized": { + "version": "9.10.1", + "resolved": "https://registry.npmjs.org/react-virtualized/-/react-virtualized-9.10.1.tgz", + "integrity": "sha512-A58MfDt5hyy3FoIni+5HDZM1jihgvHG81Mzld34Auhi6X7tRYcN4OCYuRb9eH6w/6CAAKC2MHd+f9z7yeYDCqA==", + "requires": { + "babel-runtime": "6.26.0", + "classnames": "2.2.5", + "dom-helpers": "3.2.1", + "loose-envify": "1.3.1", + "prop-types": "15.6.0" + } + }, + "read-pkg": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-2.0.0.tgz", + "integrity": "sha1-jvHAYjxqbbDcZxPEv6xGMysjaPg=", + "requires": { + "load-json-file": "2.0.0", + "normalize-package-data": "2.4.0", + "path-type": "2.0.0" + } + }, + "read-pkg-up": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-2.0.0.tgz", + "integrity": "sha1-a3KoBImE4MQeeVEP1en6mbO1Sb4=", + "requires": { + "find-up": "2.1.0", + "read-pkg": "2.0.0" + } + }, + "readable-stream": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.3.tgz", + "integrity": "sha512-m+qzzcn7KUxEmd1gMbchF+Y2eIUbieUaxkWtptyHywrX0rE8QEYqPC07Vuy4Wm32/xE16NcdBctb8S0Xe/5IeQ==", + "requires": { + "core-util-is": "1.0.2", + "inherits": "2.0.3", + "isarray": "1.0.0", + "process-nextick-args": "1.0.7", + "safe-buffer": "5.1.1", + "string_decoder": "1.0.3", + "util-deprecate": "1.0.2" + } + }, + "readdirp": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.1.0.tgz", + "integrity": "sha1-TtCtBg3zBzMAxIRANz9y0cxkLXg=", + "requires": { + "graceful-fs": "4.1.11", + "minimatch": "3.0.4", + "readable-stream": "2.3.3", + "set-immediate-shim": "1.0.1" + } + }, + "rechoir": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz", + "integrity": "sha1-hSBLVNuoLVdC4oyWdW70OvUOM4Q=", + "requires": { + "resolve": "1.4.0" + } + }, + "redent": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-1.0.0.tgz", + "integrity": "sha1-z5Fqsf1fHxbfsggi3W7H9zDCr94=", + "requires": { + "indent-string": "2.1.0", + "strip-indent": "1.0.1" + } + }, + "reduce-css-calc": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/reduce-css-calc/-/reduce-css-calc-1.3.0.tgz", + "integrity": "sha1-dHyRTgSWFKTJz7umKYca0dKSdxY=", + "requires": { + "balanced-match": "0.4.2", + "math-expression-evaluator": "1.2.17", + "reduce-function-call": "1.0.2" + }, + "dependencies": { + "balanced-match": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-0.4.2.tgz", + "integrity": "sha1-yz8+PHMtwPAe5wtAPzAuYddwmDg=" + } + } + }, + "reduce-function-call": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/reduce-function-call/-/reduce-function-call-1.0.2.tgz", + "integrity": "sha1-WiAL+S4ON3UXUv5FsKszD9S2vpk=", + "requires": { + "balanced-match": "0.4.2" + }, + "dependencies": { + "balanced-match": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-0.4.2.tgz", + "integrity": "sha1-yz8+PHMtwPAe5wtAPzAuYddwmDg=" + } + } + }, + "reduce-reducers": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/reduce-reducers/-/reduce-reducers-0.1.2.tgz", + "integrity": "sha1-+htHGLxSkqcd3R5dg5yb6pdw8Us=" + }, + "redux": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/redux/-/redux-3.7.2.tgz", + "integrity": "sha512-pNqnf9q1hI5HHZRBkj3bAngGZW/JMCmexDlOxw4XagXY2o1327nHH54LoTjiPJ0gizoqPDRqWyX/00g0hD6w+A==", + "requires": { + "lodash": "4.17.4", + "lodash-es": "4.17.4", + "loose-envify": "1.3.1", + "symbol-observable": "1.0.4" + } + }, + "redux-actions": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/redux-actions/-/redux-actions-2.2.1.tgz", + "integrity": "sha1-1kGGslZJoTwFR4VH1811N7iSQQ0=", + "requires": { + "invariant": "2.2.2", + "lodash": "4.17.4", + "lodash-es": "4.17.4", + "reduce-reducers": "0.1.2" + } + }, + "redux-batched-actions": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/redux-batched-actions/-/redux-batched-actions-0.2.0.tgz", + "integrity": "sha1-2gAAyIKw5shhqW1YI702rfXZwN0=" + }, + "redux-localstorage": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/redux-localstorage/-/redux-localstorage-0.4.1.tgz", + "integrity": "sha1-+vbXGcWBOXKU2BFHP/zt7gZckzw=" + }, + "redux-thunk": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.2.0.tgz", + "integrity": "sha1-5hWhbha0ehmlFXZhM9Hj6Zt4UuU=" + }, + "regenerate": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.3.3.tgz", + "integrity": "sha512-jVpo1GadrDAK59t/0jRx5VxYWQEDkkEKi6+HjE3joFVLfDOh9Xrdh0dF1eSq+BI/SwvTQ44gSscJ8N5zYL61sg==" + }, + "regenerator-runtime": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.0.tgz", + "integrity": "sha512-/aA0kLeRb5N9K0d4fw7ooEbI+xDe+DKD499EQqygGqeS8N3xto15p09uY2xj7ixP81sNPXvRLnAQIqdVStgb1A==" + }, + "regenerator-transform": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.10.1.tgz", + "integrity": "sha512-PJepbvDbuK1xgIgnau7Y90cwaAmO/LCLMI2mPvaXq2heGMR3aWW5/BQvYrhJ8jgmQjXewXvBjzfqKcVOmhjZ6Q==", + "requires": { + "babel-runtime": "6.26.0", + "babel-types": "6.26.0", + "private": "0.1.8" + } + }, + "regex-cache": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/regex-cache/-/regex-cache-0.4.4.tgz", + "integrity": "sha512-nVIZwtCjkC9YgvWkpM55B5rBhBYRZhAaJbgcFYXXsHnbZ9UZI9nnVWYZpBlCqv9ho2eZryPnWrZGsOdPwVWXWQ==", + "requires": { + "is-equal-shallow": "0.1.3" + } + }, + "regexpu-core": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-2.0.0.tgz", + "integrity": "sha1-SdA4g3uNz4v6W5pCE5k45uoq4kA=", + "requires": { + "regenerate": "1.3.3", + "regjsgen": "0.2.0", + "regjsparser": "0.1.5" + } + }, + "regjsgen": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.2.0.tgz", + "integrity": "sha1-bAFq3qxVT3WCP+N6wFuS1aTtsfc=" + }, + "regjsparser": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.1.5.tgz", + "integrity": "sha1-fuj4Tcb6eS0/0K4ijSS9lJ6tIFw=", + "requires": { + "jsesc": "0.5.0" + }, + "dependencies": { + "jsesc": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", + "integrity": "sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0=" + } + } + }, + "remove-trailing-separator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", + "integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=" + }, + "repeat-element": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.2.tgz", + "integrity": "sha1-7wiaF40Ug7quTZPrmLT55OEdmQo=" + }, + "repeat-string": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", + "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=" + }, + "repeating": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/repeating/-/repeating-2.0.1.tgz", + "integrity": "sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo=", + "requires": { + "is-finite": "1.0.2" + } + }, + "replace-ext": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-0.0.1.tgz", + "integrity": "sha1-KbvZIHinOfC8zitO5B6DeVNSKSQ=" + }, + "require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=" + }, + "require-from-string": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-1.2.1.tgz", + "integrity": "sha1-UpyczvJzgK3+yaL5ZbZJu+5jZBg=" + }, + "require-main-filename": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz", + "integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=" + }, + "require-nocache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/require-nocache/-/require-nocache-1.0.0.tgz", + "integrity": "sha1-pmXQtgoH6CSYdXkKTTUCGdPIX6M=" + }, + "require-uncached": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/require-uncached/-/require-uncached-1.0.3.tgz", + "integrity": "sha1-Tg1W1slmL9MeQwEcS5WqSZVUIdM=", + "requires": { + "caller-path": "0.1.0", + "resolve-from": "1.0.1" + } + }, + "reselect": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-3.0.1.tgz", + "integrity": "sha1-79qpjqdFEyTQkrKyFjpqHXqaIUc=" + }, + "resize-observer-polyfill": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.4.2.tgz", + "integrity": "sha1-o3GY5iCeiIrLFTKplo4G04tniOU=" + }, + "resolve": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.4.0.tgz", + "integrity": "sha512-aW7sVKPufyHqOmyyLzg/J+8606v5nevBgaliIlV7nUpVMsDnoBGV/cbSLNjZAg9q0Cfd/+easKVKQ8vOu8fn1Q==", + "requires": { + "path-parse": "1.0.5" + } + }, + "resolve-dir": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/resolve-dir/-/resolve-dir-0.1.1.tgz", + "integrity": "sha1-shklmlYC+sXFxJatiUpujMQwJh4=", + "requires": { + "expand-tilde": "1.2.2", + "global-modules": "0.2.3" + } + }, + "resolve-from": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-1.0.1.tgz", + "integrity": "sha1-Jsv+k10a7uq7Kbw/5a6wHpPUQiY=" + }, + "resolve-pathname": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/resolve-pathname/-/resolve-pathname-2.2.0.tgz", + "integrity": "sha512-bAFz9ld18RzJfddgrO2e/0S2O81710++chRMUxHjXOYKF6jTAMrUNZrEZ1PvV0zlhfjidm08iRPdTLPno1FuRg==" + }, + "resolve-url": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", + "integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=" + }, + "restore-cursor": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz", + "integrity": "sha1-n37ih/gv0ybU/RYpI9YhKe7g368=", + "requires": { + "onetime": "2.0.1", + "signal-exit": "3.0.2" + } + }, + "right-align": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/right-align/-/right-align-0.1.3.tgz", + "integrity": "sha1-YTObci/mo1FWiSENJOFMlhSGE+8=", + "requires": { + "align-text": "0.1.4" + } + }, + "rimraf": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz", + "integrity": "sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w==", + "requires": { + "glob": "7.1.2" + } + }, + "ripemd160": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.1.tgz", + "integrity": "sha1-D0WEKVxTo2KK9+bXmsohzlfRxuc=", + "requires": { + "hash-base": "2.0.2", + "inherits": "2.0.3" + } + }, + "rocambole": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/rocambole/-/rocambole-0.7.0.tgz", + "integrity": "sha1-9seVBVF9xCtvuECEK4uVOw+WhYU=", + "requires": { + "esprima": "2.7.3" + } + }, + "rocambole-indent": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/rocambole-indent/-/rocambole-indent-2.0.4.tgz", + "integrity": "sha1-oYokl3ygQAuGHapGMehh3LUtCFw=", + "requires": { + "debug": "2.6.9", + "mout": "0.11.1", + "rocambole-token": "1.2.1" + }, + "dependencies": { + "mout": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/mout/-/mout-0.11.1.tgz", + "integrity": "sha1-ujYR318OWx/7/QEWa48C0fX6K5k=" + } + } + }, + "rocambole-linebreak": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/rocambole-linebreak/-/rocambole-linebreak-1.0.2.tgz", + "integrity": "sha1-A2IVFbQ7RyHJflocG8paA2Y2jy8=", + "requires": { + "debug": "2.6.9", + "rocambole-token": "1.2.1", + "semver": "4.3.6" + } + }, + "rocambole-node": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/rocambole-node/-/rocambole-node-1.0.0.tgz", + "integrity": "sha1-21tJ3nQHsAgN1RSHLyjjk9D3/z8=" + }, + "rocambole-token": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/rocambole-token/-/rocambole-token-1.2.1.tgz", + "integrity": "sha1-x4XfdCjcPLJ614lwR71SOMwHDTU=" + }, + "rocambole-whitespace": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/rocambole-whitespace/-/rocambole-whitespace-1.0.0.tgz", + "integrity": "sha1-YzMJSSVrKZQfWbGQRZ+ZnGsdO/k=", + "requires": { + "debug": "2.6.9", + "repeat-string": "1.6.1", + "rocambole-token": "1.2.1" + } + }, + "run-async": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.3.0.tgz", + "integrity": "sha1-A3GrSuC91yDUFm19/aZP96RFpsA=", + "requires": { + "is-promise": "2.1.0" + } + }, + "run-sequence": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/run-sequence/-/run-sequence-2.2.0.tgz", + "integrity": "sha512-xW5DmUwdvoyYQUMPKN8UW7TZSFs7AxtT59xo1m5y91jHbvwGlGgOmdV1Yw5P68fkjf3aHUZ4G1o1mZCtNe0qtw==", + "requires": { + "chalk": "1.1.3", + "gulp-util": "3.0.8" + }, + "dependencies": { + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=" + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "requires": { + "ansi-styles": "2.2.1", + "escape-string-regexp": "1.0.5", + "has-ansi": "2.0.0", + "strip-ansi": "3.0.1", + "supports-color": "2.0.0" + } + }, + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" + } + } + }, + "rx-lite": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/rx-lite/-/rx-lite-4.0.8.tgz", + "integrity": "sha1-Cx4Rr4vESDbwSmQH6S2kJGe3lEQ=" + }, + "rx-lite-aggregates": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/rx-lite-aggregates/-/rx-lite-aggregates-4.0.8.tgz", + "integrity": "sha1-dTuHqJoRyVRnxKwWJsTvxOBcZ74=", + "requires": { + "rx-lite": "4.0.8" + } + }, + "safe-buffer": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", + "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==" + }, + "sane": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/sane/-/sane-1.7.0.tgz", + "integrity": "sha1-s1ebzLRclM8gNVzIESSZDf00bjA=", + "requires": { + "anymatch": "1.3.2", + "exec-sh": "0.2.1", + "fb-watchman": "2.0.0", + "minimatch": "3.0.4", + "minimist": "1.2.0", + "walker": "1.0.7", + "watch": "0.10.0" + }, + "dependencies": { + "minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" + } + } + }, + "sax": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", + "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" + }, + "schema-utils": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-0.3.0.tgz", + "integrity": "sha1-9YdyIs4+kx7a4DnxfrNxbnE3+M8=", + "requires": { + "ajv": "5.2.3" + } + }, + "section-iterator": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/section-iterator/-/section-iterator-2.0.0.tgz", + "integrity": "sha1-v0RNev7rlK1Dw5rS+yYVFifMuio=" + }, + "select": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/select/-/select-1.1.2.tgz", + "integrity": "sha1-DnNQrN7ICxEIUoeG7B1EGNEbOW0=" + }, + "semver": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/semver/-/semver-4.3.6.tgz", + "integrity": "sha1-MAvG4OhjdPe6YQaLWx7NV/xlMto=" + }, + "sequencify": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/sequencify/-/sequencify-0.0.7.tgz", + "integrity": "sha1-kM/xnQLgcCf9dn9erT57ldHnOAw=" + }, + "serializerr": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/serializerr/-/serializerr-1.0.3.tgz", + "integrity": "sha1-EtTFqhw/+49tHcXzlaqUVVacP5E=", + "requires": { + "protochain": "1.0.5" + } + }, + "set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" + }, + "set-immediate-shim": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz", + "integrity": "sha1-SysbJ+uAip+NzEgaWOXlb1mfP2E=" + }, + "setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=" + }, + "sha.js": { + "version": "2.4.9", + "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.9.tgz", + "integrity": "sha512-G8zektVqbiPHrylgew9Zg1VRB1L/DtXNUVAM6q4QLy8NE3qtHlFXTf8VLL4k1Yl6c7NMjtZUTdXV+X44nFaT6A==", + "requires": { + "inherits": "2.0.3", + "safe-buffer": "5.1.1" + } + }, + "shallow-equal": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shallow-equal/-/shallow-equal-1.0.0.tgz", + "integrity": "sha1-UI0YOLPeWQq4dXsBGyXkMJAJRfc=" + }, + "shallowequal": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.0.2.tgz", + "integrity": "sha512-zlVXeVUKvo+HEv1e2KQF/csyeMKx2oHvatQ9l6XjCUj3agvC8XGf6R9HvIPDSmp8FNPvx7b5kaEJTRi7CqxtEw==" + }, + "shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", + "requires": { + "shebang-regex": "1.0.0" + } + }, + "shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=" + }, + "sigmund": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/sigmund/-/sigmund-1.0.1.tgz", + "integrity": "sha1-P/IfGYytIXX587eBhT/ZTQ0ZtZA=" + }, + "signal-exit": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", + "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=" + }, + "signalr": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/signalr/-/signalr-2.2.2.tgz", + "integrity": "sha1-ugeRXrXgjPvId2UEfsaXJpTQCOI=", + "requires": { + "jquery": "3.2.1" + } + }, + "slash": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-1.0.0.tgz", + "integrity": "sha1-xB8vbDn8FtHNF61LXYlhFK5HDVU=" + }, + "slice-ansi": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-1.0.0.tgz", + "integrity": "sha512-POqxBK6Lb3q6s047D/XsDVNPnF9Dl8JSaqe9h9lURl0OdNqy/ujDrOiIHtsqXMGbWWTIomRzAMaTyawAU//Reg==", + "requires": { + "is-fullwidth-code-point": "2.0.0" + } + }, + "sort-keys": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-1.1.2.tgz", + "integrity": "sha1-RBttTTRnmPG05J6JIK37oOVD+a0=", + "requires": { + "is-plain-obj": "1.1.0" + } + }, + "source-list-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.0.tgz", + "integrity": "sha512-I2UmuJSRr/T8jisiROLU3A3ltr+swpniSmNPI4Ml3ZCX6tVnDsuZzK7F2hl5jTqbZBWCEKlj5HRQiPExXLgE8A==" + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" + }, + "source-map-resolve": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.3.1.tgz", + "integrity": "sha1-YQ9hIqRFuN1RU1oqcbeD38Ekh2E=", + "requires": { + "atob": "1.1.3", + "resolve-url": "0.2.1", + "source-map-url": "0.3.0", + "urix": "0.1.0" + } + }, + "source-map-support": { + "version": "0.4.18", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.4.18.tgz", + "integrity": "sha512-try0/JqxPLF9nOjvSta7tVondkP5dwgyLDjVoyMDlmjugT2lRZ1OfsrYTkCd2hkDnJTKRbO/Rl3orm8vlsUzbA==", + "requires": { + "source-map": "0.5.7" + }, + "dependencies": { + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" + } + } + }, + "source-map-url": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.3.0.tgz", + "integrity": "sha1-fsrxO1e80J2opAxdJp2zN5nUqvk=" + }, + "sparkles": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/sparkles/-/sparkles-1.0.0.tgz", + "integrity": "sha1-Gsu/tZJDbRC76PeFt8xvgoFQEsM=" + }, + "spdx-correct": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-1.0.2.tgz", + "integrity": "sha1-SzBz2TP/UfORLwOsVRlJikFQ20A=", + "requires": { + "spdx-license-ids": "1.2.2" + } + }, + "spdx-expression-parse": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-1.0.4.tgz", + "integrity": "sha1-m98vIOH0DtRH++JzJmGR/O1RYmw=" + }, + "spdx-license-ids": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-1.2.2.tgz", + "integrity": "sha1-yd96NCRZSt5r0RkA1ZZpbcBrrFc=" + }, + "specificity": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/specificity/-/specificity-0.3.2.tgz", + "integrity": "sha512-Nc/QN/A425Qog7j9aHmwOrlwX2e7pNI47ciwxwy4jOlvbbMHkNNJchit+FX+UjF3IAdiaaV5BKeWuDUnws6G1A==" + }, + "split": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/split/-/split-0.3.3.tgz", + "integrity": "sha1-zQ7qXmOiEd//frDwkcQTPi0N0o8=", + "requires": { + "through": "2.3.8" + } + }, + "sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=" + }, + "statuses": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz", + "integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew==" + }, + "stdin": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/stdin/-/stdin-0.0.1.tgz", + "integrity": "sha1-0wQZgarsPf28d6GzjWNy449ftx4=" + }, + "stream-browserify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-2.0.1.tgz", + "integrity": "sha1-ZiZu5fm9uZQKTkUUyvtDu3Hlyds=", + "requires": { + "inherits": "2.0.3", + "readable-stream": "2.3.3" + } + }, + "stream-combiner": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/stream-combiner/-/stream-combiner-0.0.4.tgz", + "integrity": "sha1-TV5DPBhSYd3mI8o/RMWGvPXErRQ=", + "requires": { + "duplexer": "0.1.1" + } + }, + "stream-consume": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/stream-consume/-/stream-consume-0.1.0.tgz", + "integrity": "sha1-pB6tGm1ggc63n2WwYZAbbY89HQ8=" + }, + "stream-http": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/stream-http/-/stream-http-2.7.2.tgz", + "integrity": "sha512-c0yTD2rbQzXtSsFSVhtpvY/vS6u066PcXOX9kBB3mSO76RiUQzL340uJkGBWnlBg4/HZzqiUXtaVA7wcRcJgEw==", + "requires": { + "builtin-status-codes": "3.0.0", + "inherits": "2.0.3", + "readable-stream": "2.3.3", + "to-arraybuffer": "1.0.1", + "xtend": "4.0.1" + } + }, + "streamqueue": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/streamqueue/-/streamqueue-1.1.1.tgz", + "integrity": "sha1-0612aGvpJLv5yix0qBSiGCR11tc=", + "requires": { + "isstream": "0.1.2", + "readable-stream": "1.0.34" + }, + "dependencies": { + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" + }, + "readable-stream": { + "version": "1.0.34", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", + "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", + "requires": { + "core-util-is": "1.0.2", + "inherits": "2.0.3", + "isarray": "0.0.1", + "string_decoder": "0.10.31" + } + }, + "string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" + } + } + }, + "strict-uri-encode": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz", + "integrity": "sha1-J5siXfHVgrH1TmWt3UNS4Y+qBxM=" + }, + "string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "requires": { + "is-fullwidth-code-point": "2.0.0", + "strip-ansi": "4.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=" + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "requires": { + "ansi-regex": "3.0.0" + } + } + } + }, + "string_decoder": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz", + "integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==", + "requires": { + "safe-buffer": "5.1.1" + } + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "requires": { + "ansi-regex": "2.1.1" + } + }, + "strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=" + }, + "strip-bom-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-bom-stream/-/strip-bom-stream-2.0.0.tgz", + "integrity": "sha1-+H217yYT9paKpUWr/h7HKLaoKco=", + "requires": { + "first-chunk-stream": "2.0.0", + "strip-bom": "2.0.0" + }, + "dependencies": { + "first-chunk-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/first-chunk-stream/-/first-chunk-stream-2.0.0.tgz", + "integrity": "sha1-G97NuOCDwGZLkZRVgVd6Q6nzHXA=", + "requires": { + "readable-stream": "2.3.3" + } + }, + "strip-bom": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", + "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=", + "requires": { + "is-utf8": "0.2.1" + } + } + } + }, + "strip-bom-string": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-bom-string/-/strip-bom-string-1.0.0.tgz", + "integrity": "sha1-5SEekiQ2n7uB1jOi8ABE3IztrZI=" + }, + "strip-eof": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", + "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=" + }, + "strip-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-1.0.1.tgz", + "integrity": "sha1-DHlipq3vp7vUrDZkYKY4VSrhoKI=", + "requires": { + "get-stdin": "4.0.1" + }, + "dependencies": { + "get-stdin": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-4.0.1.tgz", + "integrity": "sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4=" + } + } + }, + "strip-json-comments": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-0.1.3.tgz", + "integrity": "sha1-Fkxk43Coo8wAyeAbU55WmCPw7lQ=" + }, + "style-loader": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-0.19.0.tgz", + "integrity": "sha512-9mx9sC9nX1dgP96MZOODpGC6l1RzQBITI2D5WJhu+wnbrSYVKLGuy14XJSLVQih/0GFrPpjelt+s//VcZQ2Evw==", + "requires": { + "loader-utils": "1.1.0", + "schema-utils": "0.3.0" + } + }, + "style-search": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/style-search/-/style-search-0.1.0.tgz", + "integrity": "sha1-eVjHk+R+MuB9K1yv5cC/jhLneQI=" + }, + "stylelint": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/stylelint/-/stylelint-8.2.0.tgz", + "integrity": "sha512-57JWIz/1Uh9ehZMZyAqlFC0EDfQrMXCH8yqt8ZuJQQvV3LBKgAM/JYd+CWi1hC4eJtRODSPbIIBYKdGjkPZdMg==", + "requires": { + "autoprefixer": "7.1.5", + "balanced-match": "1.0.0", + "chalk": "2.2.0", + "cosmiconfig": "3.1.0", + "debug": "3.1.0", + "execall": "1.0.0", + "file-entry-cache": "2.0.0", + "get-stdin": "5.0.1", + "globby": "6.1.0", + "globjoin": "0.1.4", + "html-tags": "2.0.0", + "ignore": "3.3.6", + "imurmurhash": "0.1.4", + "known-css-properties": "0.4.1", + "lodash": "4.17.4", + "log-symbols": "2.1.0", + "mathml-tag-names": "2.0.1", + "meow": "3.7.0", + "micromatch": "2.3.11", + "normalize-selector": "0.2.0", + "pify": "3.0.0", + "postcss": "6.0.13", + "postcss-less": "1.1.1", + "postcss-media-query-parser": "0.2.3", + "postcss-reporter": "5.0.0", + "postcss-resolve-nested-selector": "0.1.1", + "postcss-safe-parser": "3.0.1", + "postcss-scss": "1.0.2", + "postcss-selector-parser": "2.2.3", + "postcss-value-parser": "3.3.0", + "resolve-from": "4.0.0", + "specificity": "0.3.2", + "string-width": "2.1.1", + "style-search": "0.1.0", + "sugarss": "1.0.0", + "svg-tags": "1.0.0", + "table": "4.0.2" + }, + "dependencies": { + "cosmiconfig": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-3.1.0.tgz", + "integrity": "sha512-zedsBhLSbPBms+kE7AH4vHg6JsKDz6epSv2/+5XHs8ILHlgDciSJfSWf8sX9aQ52Jb7KI7VswUTsLpR/G0cr2Q==", + "requires": { + "is-directory": "0.3.1", + "js-yaml": "3.10.0", + "parse-json": "3.0.0", + "require-from-string": "2.0.1" + } + }, + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "requires": { + "ms": "2.0.0" + } + }, + "esprima": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.0.tgz", + "integrity": "sha512-oftTcaMu/EGrEIu904mWteKIv8vMuOgGYo7EhVJJN00R/EED9DCua/xxHRdYnKtcECzVg7xOWhflvJMnqcFZjw==" + }, + "js-yaml": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.10.0.tgz", + "integrity": "sha512-O2v52ffjLa9VeM43J4XocZE//WT9N0IiwDa3KSHH7Tu8CtH+1qM8SIZvnsTh6v+4yFy5KUY3BHUVwjpfAWsjIA==", + "requires": { + "argparse": "1.0.9", + "esprima": "4.0.0" + } + }, + "log-symbols": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-2.1.0.tgz", + "integrity": "sha512-zLeLrzMA1A2vRF1e/0Mo+LNINzi6jzBylHj5WqvQ/WK/5WCZt8si9SyN4p9llr/HRYvVR1AoXHRHl4WTHyQAzQ==", + "requires": { + "chalk": "2.2.0" + } + }, + "parse-json": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-3.0.0.tgz", + "integrity": "sha1-+m9HsY4jgm6tMvJj50TQ4ehH+xM=", + "requires": { + "error-ex": "1.3.1" + } + }, + "pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=" + }, + "require-from-string": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.1.tgz", + "integrity": "sha1-xUUjPp19pmFunVmt+zn8n1iGdv8=" + }, + "resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==" + } + } + }, + "stylelint-order": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/stylelint-order/-/stylelint-order-0.7.0.tgz", + "integrity": "sha1-zqtcviSqM/pjWQAkmVOV9u38mrc=", + "requires": { + "lodash": "4.17.4", + "postcss": "6.0.13", + "postcss-sorting": "3.0.2" + } + }, + "sugarss": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/sugarss/-/sugarss-1.0.0.tgz", + "integrity": "sha1-ZeUbOVhDL7cNVFGmi7M+MtDPHvc=", + "requires": { + "postcss": "6.0.13" + } + }, + "supports-color": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-4.5.0.tgz", + "integrity": "sha1-vnoN5ITexcXN34s9WRJQRJEvY1s=", + "requires": { + "has-flag": "2.0.0" + } + }, + "svg-tags": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/svg-tags/-/svg-tags-1.0.0.tgz", + "integrity": "sha1-WPcc7jvVGbWdSyqEO2x95krAR2Q=" + }, + "svgo": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/svgo/-/svgo-0.7.2.tgz", + "integrity": "sha1-n1dyQTlSE1xv779Ar+ak+qiLS7U=", + "requires": { + "coa": "1.0.4", + "colors": "1.1.2", + "csso": "2.3.2", + "js-yaml": "3.7.0", + "mkdirp": "0.5.1", + "sax": "1.2.4", + "whet.extend": "0.9.9" + } + }, + "symbol-observable": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.0.4.tgz", + "integrity": "sha1-Kb9hXUqnEhvdiYsi1LP5vE4qoD0=" + }, + "sync-exec": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/sync-exec/-/sync-exec-0.6.2.tgz", + "integrity": "sha1-cX0izFPwzh3vVZQ2LzqJouu5EQU=" + }, + "table": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/table/-/table-4.0.2.tgz", + "integrity": "sha512-UUkEAPdSGxtRpiV9ozJ5cMTtYiqz7Ni1OGqLXRCynrvzdtR1p+cfOWe2RJLwvUG8hNanaSRjecIqwOjqeatDsA==", + "requires": { + "ajv": "5.2.3", + "ajv-keywords": "2.1.0", + "chalk": "2.2.0", + "lodash": "4.17.4", + "slice-ansi": "1.0.0", + "string-width": "2.1.1" + } + }, + "tapable": { + "version": "0.2.8", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-0.2.8.tgz", + "integrity": "sha1-mTcqXJmb8t8WCvwNdL7U9HlIzSI=" + }, + "tar": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-2.2.1.tgz", + "integrity": "sha1-jk0qJWwOIYXGsYrWlK7JaLg8sdE=", + "requires": { + "block-stream": "0.0.9", + "fstream": "1.0.11", + "inherits": "2.0.3" + } + }, + "tar.gz": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tar.gz/-/tar.gz-1.0.5.tgz", + "integrity": "sha1-4a2n5F7yJBtLHuWBI8j0C108G8Q=", + "requires": { + "bluebird": "2.11.0", + "commander": "2.11.0", + "fstream": "1.0.11", + "mout": "0.11.1", + "tar": "2.2.1" + }, + "dependencies": { + "bluebird": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-2.11.0.tgz", + "integrity": "sha1-U0uQM8AiyVecVro7Plpcqvu2UOE=" + }, + "mout": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/mout/-/mout-0.11.1.tgz", + "integrity": "sha1-ujYR318OWx/7/QEWa48C0fX6K5k=" + } + } + }, + "tether": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/tether/-/tether-1.4.0.tgz", + "integrity": "sha1-D5+hcfdb9YSF2BSelHmdeudNHBo=" + }, + "text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=" + }, + "through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=" + }, + "through2": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.3.tgz", + "integrity": "sha1-AARWmzfHx0ujnEPzzteNGtlBQL4=", + "requires": { + "readable-stream": "2.3.3", + "xtend": "4.0.1" + } + }, + "tildify": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/tildify/-/tildify-1.2.0.tgz", + "integrity": "sha1-3OwD9V3Km3qj5bBPIYF+tW5jWIo=", + "requires": { + "os-homedir": "1.0.2" + } + }, + "time-stamp": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/time-stamp/-/time-stamp-1.1.0.tgz", + "integrity": "sha1-dkpaEa9QVhkhsTPztE5hhofg9cM=" + }, + "timers-browserify": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/timers-browserify/-/timers-browserify-2.0.4.tgz", + "integrity": "sha512-uZYhyU3EX8O7HQP+J9fTVYwsq90Vr68xPEFo7yrVImIxYvHgukBEgOB/SgGoorWVTzGM/3Z+wUNnboA4M8jWrg==", + "requires": { + "setimmediate": "1.0.5" + } + }, + "timers-ext": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/timers-ext/-/timers-ext-0.1.2.tgz", + "integrity": "sha1-YcxHp2wavTGV8UUn+XjViulMUgQ=", + "requires": { + "es5-ext": "0.10.35", + "next-tick": "1.0.0" + } + }, + "tiny-emitter": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-2.0.2.tgz", + "integrity": "sha512-2NM0auVBGft5tee/OxP4PI3d8WItkDM+fPnaRAVo6xTDI2knbz9eC5ArWGqtGlYqiH3RU5yMpdyTTO7MguC4ow==" + }, + "tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "requires": { + "os-tmpdir": "1.0.2" + } + }, + "tmpl": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.4.tgz", + "integrity": "sha1-I2QN17QtAEM5ERQIIOXPRA5SHdE=" + }, + "to-arraybuffer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz", + "integrity": "sha1-fSKbH8xjfkZsoIEYCDanqr/4P0M=" + }, + "to-camel-case": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/to-camel-case/-/to-camel-case-1.0.0.tgz", + "integrity": "sha1-GlYFSy+daWKYzmamCJcyK29CPkY=", + "requires": { + "to-space-case": "1.0.0" + } + }, + "to-fast-properties": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-1.0.3.tgz", + "integrity": "sha1-uDVx+k2MJbguIxsG46MFXeTKGkc=" + }, + "to-no-case": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/to-no-case/-/to-no-case-1.0.2.tgz", + "integrity": "sha1-xyKQcWTvaxeBMsjmmTAhLRtKoWo=" + }, + "to-space-case": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/to-space-case/-/to-space-case-1.0.0.tgz", + "integrity": "sha1-sFLar7Gysp3HcM6gFj5ewOvJ/Bc=", + "requires": { + "to-no-case": "1.0.2" + } + }, + "traverse": { + "version": "0.6.6", + "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.6.6.tgz", + "integrity": "sha1-y99WD9e5r2MlAv7UD5GMFX6pcTc=" + }, + "trim-newlines": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-1.0.0.tgz", + "integrity": "sha1-WIeWa7WCpFA6QetST301ARgVphM=" + }, + "trim-right": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/trim-right/-/trim-right-1.0.1.tgz", + "integrity": "sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM=" + }, + "tryit": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tryit/-/tryit-1.0.3.tgz", + "integrity": "sha1-OTvnMKlEb9Hq1tpZoBQwjzbCics=" + }, + "tty-browserify": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.0.tgz", + "integrity": "sha1-oVe6QC2iTpv5V/mqadUk7tQpAaY=" + }, + "type-check": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", + "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", + "requires": { + "prelude-ls": "1.1.2" + } + }, + "type-is": { + "version": "1.6.15", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.15.tgz", + "integrity": "sha1-yrEPtJCeRByChC6v4a1kbIGARBA=", + "requires": { + "media-typer": "0.3.0", + "mime-types": "2.1.17" + } + }, + "typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=" + }, + "ua-parser-js": { + "version": "0.7.17", + "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.17.tgz", + "integrity": "sha512-uRdSdu1oA1rncCQL7sCj8vSyZkgtL7faaw9Tc9rZ3mGgraQ7+Pdx7w5mnOSF3gw9ZNG6oc+KXfkon3bKuROm0g==" + }, + "uglify-js": { + "version": "2.8.29", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.8.29.tgz", + "integrity": "sha1-KcVzMUgFe7Th913zW3qcty5qWd0=", + "requires": { + "source-map": "0.5.7", + "uglify-to-browserify": "1.0.2", + "yargs": "3.10.0" + }, + "dependencies": { + "camelcase": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-1.2.1.tgz", + "integrity": "sha1-m7UwTS4LVmmLLHWLCKPqqdqlijk=" + }, + "cliui": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-2.1.0.tgz", + "integrity": "sha1-S0dXYP+AJkx2LDoXGQMukcf+oNE=", + "requires": { + "center-align": "0.1.3", + "right-align": "0.1.3", + "wordwrap": "0.0.2" + } + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" + }, + "wordwrap": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.2.tgz", + "integrity": "sha1-t5Zpu0LstAn4PVg8rVLKF+qhZD8=" + }, + "yargs": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-3.10.0.tgz", + "integrity": "sha1-9+572FfdfB0tOMDnTvvWgdFDH9E=", + "requires": { + "camelcase": "1.2.1", + "cliui": "2.1.0", + "decamelize": "1.2.0", + "window-size": "0.1.0" + } + } + } + }, + "uglify-to-browserify": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz", + "integrity": "sha1-bgkk1r2mta/jSeOabWMoUKD4grc=", + "optional": true + }, + "uglifyjs-webpack-plugin": { + "version": "0.4.6", + "resolved": "https://registry.npmjs.org/uglifyjs-webpack-plugin/-/uglifyjs-webpack-plugin-0.4.6.tgz", + "integrity": "sha1-uVH0q7a9YX5m9j64kUmOORdj4wk=", + "requires": { + "source-map": "0.5.7", + "uglify-js": "2.8.29", + "webpack-sources": "1.0.1" + }, + "dependencies": { + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" + } + } + }, + "unc-path-regex": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/unc-path-regex/-/unc-path-regex-0.1.2.tgz", + "integrity": "sha1-5z3T17DXxe2G+6xrCufYxqadUPo=" + }, + "uniq": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/uniq/-/uniq-1.0.1.tgz", + "integrity": "sha1-sxxa6CVIRKOoKBVBzisEuGWnNP8=" + }, + "uniqid": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/uniqid/-/uniqid-4.1.1.tgz", + "integrity": "sha1-iSIN32t1GuUrX3JISGNShZa7hME=", + "requires": { + "macaddress": "0.2.8" + } + }, + "uniqs": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/uniqs/-/uniqs-2.0.0.tgz", + "integrity": "sha1-/+3ks2slKQaW5uFl1KWe25mOawI=" + }, + "unique-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unique-stream/-/unique-stream-1.0.0.tgz", + "integrity": "sha1-1ZpKdUJ0R9mqbJHnAmP40mpLEEs=" + }, + "unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" + }, + "urix": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", + "integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=" + }, + "url": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/url/-/url-0.11.0.tgz", + "integrity": "sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE=", + "requires": { + "punycode": "1.3.2", + "querystring": "0.2.0" + }, + "dependencies": { + "punycode": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", + "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=" + } + } + }, + "url-loader": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/url-loader/-/url-loader-0.6.2.tgz", + "integrity": "sha512-h3qf9TNn53BpuXTTcpC+UehiRrl0Cv45Yr/xWayApjw6G8Bg2dGke7rIwDQ39piciWCWrC+WiqLjOh3SUp9n0Q==", + "requires": { + "loader-utils": "1.1.0", + "mime": "1.4.1", + "schema-utils": "0.3.0" + } + }, + "user-home": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/user-home/-/user-home-2.0.0.tgz", + "integrity": "sha1-nHC/2Babwdy/SGBODwS4tJzenp8=", + "requires": { + "os-homedir": "1.0.2" + } + }, + "util": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz", + "integrity": "sha1-evsa/lCAUkZInj23/g7TeTNqwPk=", + "requires": { + "inherits": "2.0.1" + }, + "dependencies": { + "inherits": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz", + "integrity": "sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE=" + } + } + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" + }, + "v8flags": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/v8flags/-/v8flags-2.1.1.tgz", + "integrity": "sha1-qrGh+jDUX4jdMhFIh1rALAtV5bQ=", + "requires": { + "user-home": "1.1.1" + }, + "dependencies": { + "user-home": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/user-home/-/user-home-1.1.1.tgz", + "integrity": "sha1-K1viOjK2Onyd640PKNSFcko98ZA=" + } + } + }, + "validate-npm-package-license": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.1.tgz", + "integrity": "sha1-KAS6vnEq0zeUWaz74kdGqywwP7w=", + "requires": { + "spdx-correct": "1.0.2", + "spdx-expression-parse": "1.0.4" + } + }, + "value-equal": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/value-equal/-/value-equal-0.4.0.tgz", + "integrity": "sha512-x+cYdNnaA3CxvMaTX0INdTCN8m8aF2uY9BvEqmxuYp8bL09cs/kWVQPVGcA35fMktdOsP69IgU7wFj/61dJHEw==" + }, + "vendors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/vendors/-/vendors-1.0.1.tgz", + "integrity": "sha1-N61zyO5Bf7PVgOeFMSMH0nSEfyI=" + }, + "vinyl": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-0.5.3.tgz", + "integrity": "sha1-sEVbOPxeDPMNQyUTLkYZcMIJHN4=", + "requires": { + "clone": "1.0.2", + "clone-stats": "0.0.1", + "replace-ext": "0.0.1" + } + }, + "vinyl-bufferstream": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/vinyl-bufferstream/-/vinyl-bufferstream-1.0.1.tgz", + "integrity": "sha1-BTeGn1gO/6TKRay0dXnkuf5jCBo=", + "requires": { + "bufferstreams": "1.0.1" + } + }, + "vinyl-file": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/vinyl-file/-/vinyl-file-2.0.0.tgz", + "integrity": "sha1-p+v1/779obfRjRQPyweyI++2dRo=", + "requires": { + "graceful-fs": "4.1.11", + "pify": "2.3.0", + "pinkie-promise": "2.0.1", + "strip-bom": "2.0.0", + "strip-bom-stream": "2.0.0", + "vinyl": "1.2.0" + }, + "dependencies": { + "strip-bom": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", + "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=", + "requires": { + "is-utf8": "0.2.1" + } + }, + "vinyl": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-1.2.0.tgz", + "integrity": "sha1-XIgDbPVl5d8FVYv8kR+GVt8hiIQ=", + "requires": { + "clone": "1.0.2", + "clone-stats": "0.0.1", + "replace-ext": "0.0.1" + } + } + } + }, + "vinyl-fs": { + "version": "0.3.14", + "resolved": "https://registry.npmjs.org/vinyl-fs/-/vinyl-fs-0.3.14.tgz", + "integrity": "sha1-mmhRzhysHBzqX+hsCTHWIMLPqeY=", + "requires": { + "defaults": "1.0.3", + "glob-stream": "3.1.18", + "glob-watcher": "0.0.6", + "graceful-fs": "3.0.11", + "mkdirp": "0.5.1", + "strip-bom": "1.0.0", + "through2": "0.6.5", + "vinyl": "0.4.6" + }, + "dependencies": { + "clone": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/clone/-/clone-0.2.0.tgz", + "integrity": "sha1-xhJqkK1Pctv1rNskPMN3JP6T/B8=" + }, + "graceful-fs": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-3.0.11.tgz", + "integrity": "sha1-dhPHeKGv6mLyXGMKCG1/Osu92Bg=", + "requires": { + "natives": "1.1.0" + } + }, + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" + }, + "readable-stream": { + "version": "1.0.34", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", + "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", + "requires": { + "core-util-is": "1.0.2", + "inherits": "2.0.3", + "isarray": "0.0.1", + "string_decoder": "0.10.31" + } + }, + "string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" + }, + "strip-bom": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-1.0.0.tgz", + "integrity": "sha1-hbiGLzhEtabV7IRnqTWYFzo295Q=", + "requires": { + "first-chunk-stream": "1.0.0", + "is-utf8": "0.2.1" + } + }, + "through2": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-0.6.5.tgz", + "integrity": "sha1-QaucZ7KdVyCQcUEOHXp6lozTrUg=", + "requires": { + "readable-stream": "1.0.34", + "xtend": "4.0.1" + } + }, + "vinyl": { + "version": "0.4.6", + "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-0.4.6.tgz", + "integrity": "sha1-LzVsh6VQolVGHza76ypbqL94SEc=", + "requires": { + "clone": "0.2.0", + "clone-stats": "0.0.1" + } + } + } + }, + "vinyl-map": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/vinyl-map/-/vinyl-map-1.0.2.tgz", + "integrity": "sha1-qLKWAl+XP6fK1igXlnpI8dF2v3w=", + "requires": { + "bl": "1.2.1", + "new-from": "0.0.3", + "through2": "0.4.2" + }, + "dependencies": { + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" + }, + "object-keys": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-0.4.0.tgz", + "integrity": "sha1-KKaq50KN0sOpLz2V8hM13SBOAzY=" + }, + "readable-stream": { + "version": "1.0.34", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", + "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", + "requires": { + "core-util-is": "1.0.2", + "inherits": "2.0.3", + "isarray": "0.0.1", + "string_decoder": "0.10.31" + } + }, + "string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" + }, + "through2": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/through2/-/through2-0.4.2.tgz", + "integrity": "sha1-2/WGYDEVHsg1K7bE22SiKSqEC5s=", + "requires": { + "readable-stream": "1.0.34", + "xtend": "2.1.2" + } + }, + "xtend": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-2.1.2.tgz", + "integrity": "sha1-bv7MKk2tjmlixJAbM3znuoe10os=", + "requires": { + "object-keys": "0.4.0" + } + } + } + }, + "vinyl-sourcemaps-apply": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/vinyl-sourcemaps-apply/-/vinyl-sourcemaps-apply-0.2.1.tgz", + "integrity": "sha1-q2VJ1h0XLCsbh75cUI0jnI74dwU=", + "requires": { + "source-map": "0.5.7" + }, + "dependencies": { + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" + } + } + }, + "vm-browserify": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-0.0.4.tgz", + "integrity": "sha1-XX6kW7755Kb/ZflUOOCofDV9WnM=", + "requires": { + "indexof": "0.0.1" + } + }, + "walker": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.7.tgz", + "integrity": "sha1-L3+bj9ENZ3JisYqITijRlhjgKPs=", + "requires": { + "makeerror": "1.0.11" + } + }, + "warning": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/warning/-/warning-3.0.0.tgz", + "integrity": "sha1-MuU3fLVy3kqwR1O9+IIcAe1gW3w=", + "requires": { + "loose-envify": "1.3.1" + } + }, + "watch": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/watch/-/watch-0.10.0.tgz", + "integrity": "sha1-d3mLLaD5kQ1ZXxrOWwwiWFIfIdw=" + }, + "watchpack": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-1.4.0.tgz", + "integrity": "sha1-ShRyvLuVK9Cpu0A2gB+VTfs5+qw=", + "requires": { + "async": "2.5.0", + "chokidar": "1.7.0", + "graceful-fs": "4.1.11" + } + }, + "webpack": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-3.6.0.tgz", + "integrity": "sha512-OsHT3D0W0KmPPh60tC7asNnOmST6bKTiR90UyEdT9QYoaJ4OYN4Gg7WK1k3VxHK07ZoiYWPsKvlS/gAjwL/vRA==", + "requires": { + "acorn": "5.1.2", + "acorn-dynamic-import": "2.0.2", + "ajv": "5.2.3", + "ajv-keywords": "2.1.0", + "async": "2.5.0", + "enhanced-resolve": "3.4.1", + "escope": "3.6.0", + "interpret": "1.0.4", + "json-loader": "0.5.7", + "json5": "0.5.1", + "loader-runner": "2.3.0", + "loader-utils": "1.1.0", + "memory-fs": "0.4.1", + "mkdirp": "0.5.1", + "node-libs-browser": "2.0.0", + "source-map": "0.5.7", + "supports-color": "4.5.0", + "tapable": "0.2.8", + "uglifyjs-webpack-plugin": "0.4.6", + "watchpack": "1.4.0", + "webpack-sources": "1.0.1", + "yargs": "8.0.2" + }, + "dependencies": { + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" + } + } + }, + "webpack-sources": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.0.1.tgz", + "integrity": "sha512-05tMxipUCwHqYaVS8xc7sYPTly8PzXayRCB4dTxLhWTqlKUiwH6ezmEe0OSreL1c30LAuA3Zqmc+uEBUGFJDjw==", + "requires": { + "source-list-map": "2.0.0", + "source-map": "0.5.7" + }, + "dependencies": { + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" + } + } + }, + "webpack-stream": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/webpack-stream/-/webpack-stream-4.0.0.tgz", + "integrity": "sha1-82c92QfW2bHqe/UfzR24W1/Z4PI=", + "requires": { + "gulp-util": "3.0.8", + "lodash.clone": "4.5.0", + "lodash.some": "4.6.0", + "memory-fs": "0.4.1", + "through": "2.3.8", + "vinyl": "2.1.0", + "webpack": "3.6.0" + }, + "dependencies": { + "clone": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.1.tgz", + "integrity": "sha1-0hfR6WERjjrJpLi7oyhVU79kfNs=" + }, + "clone-stats": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/clone-stats/-/clone-stats-1.0.0.tgz", + "integrity": "sha1-s3gt/4u1R04Yuba/D9/ngvh3doA=" + }, + "replace-ext": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-1.0.0.tgz", + "integrity": "sha1-3mMSg3P8v3w8z6TeWkgMRaZ5WOs=" + }, + "vinyl": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-2.1.0.tgz", + "integrity": "sha1-Ah+cLPlR1rk5lDyJ617lrdT9kkw=", + "requires": { + "clone": "2.1.1", + "clone-buffer": "1.0.0", + "clone-stats": "1.0.0", + "cloneable-readable": "1.0.0", + "remove-trailing-separator": "1.1.0", + "replace-ext": "1.0.0" + } + } + } + }, + "websocket-driver": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.0.tgz", + "integrity": "sha1-DK+dLXVdk67gSdS90NP+LMoqJOs=", + "requires": { + "http-parser-js": "0.4.9", + "websocket-extensions": "0.1.2" + } + }, + "websocket-extensions": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.2.tgz", + "integrity": "sha1-Dhh4HeYpoYMIzhSBZQ9n/6JpOl0=" + }, + "whatwg-fetch": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-2.0.3.tgz", + "integrity": "sha1-nITsLc9oGH/wC8ZOEnS0QhduHIQ=" + }, + "whet.extend": { + "version": "0.9.9", + "resolved": "https://registry.npmjs.org/whet.extend/-/whet.extend-0.9.9.tgz", + "integrity": "sha1-+HfVv2SMl+WqVC+twW1qJZucEaE=" + }, + "which": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.0.tgz", + "integrity": "sha512-xcJpopdamTuY5duC/KnTTNBraPK54YwpenP4lzxU8H91GudWpFv38u0CKjclE1Wi2EH2EDz5LRcHcKbCIzqGyg==", + "requires": { + "isexe": "2.0.0" + } + }, + "which-module": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", + "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=" + }, + "window-size": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/window-size/-/window-size-0.1.0.tgz", + "integrity": "sha1-VDjNLqk7IC76Ohn+iIeu58lPnJ0=" + }, + "wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=" + }, + "worker-farm": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/worker-farm/-/worker-farm-1.5.0.tgz", + "integrity": "sha512-DHRiUggxtbruaTwnLDm2/BRDKZIoOYvrgYUj5Bam4fU6Gtvc0FaEyoswFPBjMXAweGW2H4BDNIpy//1yXXuaqQ==", + "requires": { + "errno": "0.1.4", + "xtend": "4.0.1" + } + }, + "wrap-ansi": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", + "integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=", + "requires": { + "string-width": "1.0.2", + "strip-ansi": "3.0.1" + }, + "dependencies": { + "is-fullwidth-code-point": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", + "requires": { + "number-is-nan": "1.0.1" + } + }, + "string-width": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "requires": { + "code-point-at": "1.1.0", + "is-fullwidth-code-point": "1.0.0", + "strip-ansi": "3.0.1" + } + } + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + }, + "write": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/write/-/write-0.2.1.tgz", + "integrity": "sha1-X8A4KOJkzqP+kUVUdvejxWbLB1c=", + "requires": { + "mkdirp": "0.5.1" + } + }, + "xtend": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz", + "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=" + }, + "y18n": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.1.tgz", + "integrity": "sha1-bRX7qITAhnnA136I53WegR4H+kE=" + }, + "yallist": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", + "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=" + }, + "yargs": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-8.0.2.tgz", + "integrity": "sha1-YpmpBVsc78lp/355wdkY3Osiw2A=", + "requires": { + "camelcase": "4.1.0", + "cliui": "3.2.0", + "decamelize": "1.2.0", + "get-caller-file": "1.0.2", + "os-locale": "2.1.0", + "read-pkg-up": "2.0.0", + "require-directory": "2.1.1", + "require-main-filename": "1.0.1", + "set-blocking": "2.0.0", + "string-width": "2.1.1", + "which-module": "2.0.0", + "y18n": "3.2.1", + "yargs-parser": "7.0.0" + } + }, + "yargs-parser": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-7.0.0.tgz", + "integrity": "sha1-jQrELxbqVd69MyyvTEA4s+P139k=", + "requires": { + "camelcase": "4.1.0" + } + } + } +} diff --git a/package.json b/package.json index 12bc565c3..e682076ef 100644 --- a/package.json +++ b/package.json @@ -1,46 +1,117 @@ { - "name": "Lidarr", + "name": "lidarr", "version": "1.0.0", "description": "Lidarr", - "main": "main.js", "scripts": { "build": "gulp build", - "start": "gulp watch" + "start": "gulp watch", + "eslint": "esprint check", + "eslint-fix": "eslint start --fix", + "stylelint": "stylelint frontend/**/*.css --config frontend/.stylelintrc" }, "repository": { "type": "git", - "url": "git://github.com/lidarr/Lidarr.git" + "url": "git://github.com/Lidarr/Lidarr.git" }, - "author": "", + "author": "Team Lidarr", "license": "GPL-3.0", - "gitHead": "9ff7aa1bf7fe38c4c5bdb92f56c8ad556916ed67", "readmeFilename": "readme.md", "dependencies": { - "autoprefixer-core": "5.2.1", - "del": "1.2.0", - "gulp": "3.9.0", - "gulp-cached": "1.1.0", - "gulp-concat": "2.6.0", + "autoprefixer": "7.1.5", + "babel-core": "6.26.0", + "babel-eslint": "8.0.1", + "babel-loader": "7.1.2", + "babel-plugin-transform-class-properties": "6.24.1", + "babel-preset-decorators-legacy": "1.0.0", + "babel-preset-es2015": "6.24.1", + "babel-preset-react": "6.24.1", + "babel-preset-stage-2": "6.24.1", + "classnames": "2.2.5", + "clipboard": "1.7.1", + "create-react-class": "^15.6.2", + "css-loader": "0.28.7", + "del": "3.0.0", + "element-class": "0.2.2", + "esformatter": "0.10.0", + "eslint": "4.8.0", + "eslint-loader": "1.9.0", + "eslint-plugin-filenames": "1.2.0", + "eslint-plugin-react": "7.4.0", + "esprint": "0.4.0", + "extract-text-webpack-plugin": "3.0.1", + "file-loader": "1.1.5", + "filesize": "3.5.10", + "gulp": "3.9.1", + "gulp-cached": "1.1.1", + "gulp-clean-css": "3.9.0", + "gulp-concat": "2.6.1", "gulp-declare": "0.3.0", - "gulp-handlebars": "3.0.1", - "gulp-jshint": "1.11.2", - "gulp-less": "3.0.3", - "gulp-livereload": "3.8.0", - "gulp-postcss": "6.0.0", - "gulp-print": "1.1.0", - "gulp-replace": "0.5.3", - "gulp-run": "1.6.8", - "gulp-sourcemaps": "1.5.2", + "gulp-livereload": "3.8.1", + "gulp-postcss": "7.0.0", + "gulp-print": "2.0.1", + "gulp-sourcemaps": "2.6.1", "gulp-stripbom": "1.0.4", - "gulp-webpack": "1.5.0", - "gulp-wrap": "0.11.0", - "handlebars": "3.0.3", - "jshint-loader": "0.8.3", - "jshint-stylish": "2.0.1", - "run-sequence": "1.1.1", - "streamqueue": "1.1.0", - "tar.gz": "0.1.1", - "webpack": "1.12.0", - "webpack-stream": "2.1.0" - } + "gulp-util": "3.0.8", + "gulp-watch": "4.3.11", + "gulp-wrap": "0.13.0", + "history": "4.7.2", + "jdu": "1.0.0", + "jquery": "3.2.1", + "loader-utils": "^1.1.0", + "lodash": "4.17.4", + "mobile-detect": "1.3.7", + "moment": "2.18.1", + "mousetrap": "1.6.1", + "normalize.css": "7.0.0", + "postcss-loader": "2.0.6", + "postcss-mixins": "6.1.1", + "postcss-nested": "2.1.2", + "postcss-simple-vars": "4.1.0", + "prop-types": "15.6.0", + "qs": "6.5.1", + "query-string": "5.0.0", + "raven-for-redux": "1.0.0", + "raven-js": "3.17.0", + "react": "15.6.0", + "react-addons-shallow-compare": "15.6.2", + "react-async-script": "0.9.1", + "react-autosuggest": "9.3.2", + "react-custom-scrollbars": "4.1.2", + "react-dnd": "2.5.4", + "react-dnd-html5-backend": "2.5.4", + "react-document-title": "2.0.3", + "react-dom": "15.6.0", + "react-google-recaptcha": "0.9.7", + "react-lazyload": "2.2.7", + "react-measure": "1.4.7", + "react-portal": "3.1.0", + "react-redux": "5.0.6", + "react-router-dom": "4.2.2", + "react-router-redux": "5.0.0-alpha.6", + "react-slider": "0.9.0", + "react-tabs": "2.1.0", + "react-tag-autocomplete": "5.4.1", + "react-tether": "0.5.7", + "react-text-truncate": "0.12.0", + "react-truncate": "2.2.2", + "react-virtualized": "9.10.1", + "redux": "3.7.2", + "redux-actions": "2.2.1", + "redux-batched-actions": "0.2.0", + "redux-localstorage": "0.4.1", + "redux-thunk": "2.2.0", + "require-nocache": "1.0.0", + "reselect": "3.0.1", + "run-sequence": "2.2.0", + "signalr": "2.2.2", + "streamqueue": "1.1.1", + "style-loader": "0.19.0", + "stylelint": "8.2.0", + "stylelint-order": "0.7.0", + "tar.gz": "1.0.5", + "url-loader": "0.6.2", + "webpack": "3.6.0", + "webpack-stream": "^4.0.0" + }, + "main": "index.js" } diff --git a/setup/build.bat b/setup/build.bat index 1821e5844..faef79cb4 100644 --- a/setup/build.bat +++ b/setup/build.bat @@ -1,3 +1,3 @@ -#SET BUILD_NUMBER=1 -#SET branch=develop -inno\ISCC.exe nzbdrone.iss \ No newline at end of file +REM SET BUILD_NUMBER=1 +REM SET branch=develop +inno\ISCC.exe lidarr.iss \ No newline at end of file diff --git a/setup/lidarr.iss b/setup/lidarr.iss new file mode 100644 index 000000000..09c44e103 --- /dev/null +++ b/setup/lidarr.iss @@ -0,0 +1,65 @@ +; Script generated by the Inno Setup Script Wizard. +; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES! + +#define AppName "Lidarr" +#define AppPublisher "Team Lidarr" +#define AppURL "https://lidarr.audio/" +#define ForumsURL "https://forums.lidarr.audio/" +#define AppExeName "Lidarr.exe" +#define BuildNumber "0.3" +#define BuildNumber GetEnv('BUILD_NUMBER') +#define BranchName GetEnv('branch') + +[Setup] +; NOTE: The value of AppId uniquely identifies this application. +; Do not use the same AppId value in installers for other applications. +; (To generate a new GUID, click Tools | Generate GUID inside the IDE.) +AppId={{56C1065D-3523-4025-B76D-6F73F67F7F93} +AppName={#AppName} +AppVersion=2.0 +AppPublisher={#AppPublisher} +AppPublisherURL={#AppURL} +AppSupportURL={#ForumsURL} +AppUpdatesURL={#AppURL} +DefaultDirName={commonappdata}\Lidarr\bin +DisableDirPage=yes +DefaultGroupName={#AppName} +DisableProgramGroupPage=yes +OutputBaseFilename=Lidarr.{#BranchName}.{#BuildNumber}.windows +SolidCompression=yes +AppCopyright=Creative Commons 3.0 License +AllowUNCPath=False +UninstallDisplayIcon={app}\Lidarr.exe +DisableReadyPage=True +CompressionThreads=2 +Compression=lzma2/normal +AppContact={#ForumsURL} +VersionInfoVersion={#BuildNumber} + +[Languages] +Name: "english"; MessagesFile: "compiler:Default.isl" + +[Tasks] +Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}" +Name: "windowsService"; Description: "Install Windows Service (Starts when the computer starts)"; GroupDescription: "Start automatically"; Flags: exclusive +Name: "startupShortcut"; Description: "Create shortcut in Startup folder (Starts when you log into Windows)"; GroupDescription: "Start automatically"; Flags: exclusive unchecked +Name: "none"; Description: "Do not start automatically"; GroupDescription: "Start automatically"; Flags: exclusive unchecked + +[Files] +Source: "..\_output\Lidarr.exe"; DestDir: "{app}"; Flags: ignoreversion +Source: "..\_output\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs +; NOTE: Don't use "Flags: ignoreversion" on any shared system files + +[Icons] +Name: "{group}\{#AppName}"; Filename: "{app}\{#AppExeName}"; Parameters: "/icon" +Name: "{commondesktop}\{#AppName}"; Filename: "{app}\{#AppExeName}"; Parameters: "/icon" +Name: "{userstartup}\{#AppName}"; Filename: "{app}\Lidarr.exe"; WorkingDir: "{app}"; Tasks: startupShortcut + +[Run] +Filename: "{app}\Lidarr.Console.exe"; Parameters: "/u"; Flags: runhidden waituntilterminated; +Filename: "{app}\Lidarr.Console.exe"; Parameters: "/i"; Flags: runhidden waituntilterminated; Tasks: windowsService +Filename: "{app}\Lidarr.exe"; Description: "Open Lidarr"; Flags: postinstall skipifsilent nowait; Tasks: windowsService; +Filename: "{app}\Lidarr.exe"; Description: "Start Lidarr"; Flags: postinstall skipifsilent nowait; Tasks: startupShortcut none; + +[UninstallRun] +Filename: "{app}\lidarr.console.exe"; Parameters: "/u"; Flags: waituntilterminated skipifdoesntexist diff --git a/setup/nzbdrone.iss b/setup/nzbdrone.iss deleted file mode 100644 index 011781078..000000000 --- a/setup/nzbdrone.iss +++ /dev/null @@ -1,60 +0,0 @@ -; Script generated by the Inno Setup Script Wizard. -; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES! - -#define AppName "Lidarr" -#define AppPublisher "Team Lidarr" -#define AppURL "https://lidarr.audio/" -#define ForumsURL "https://forums.lidarr.audio/" -#define AppExeName "Lidarr.exe" -#define BuildNumber "2.0" -#define BuildNumber GetEnv('BUILD_NUMBER') -#define BranchName GetEnv('branch') - -[Setup] -; NOTE: The value of AppId uniquely identifies this application. -; Do not use the same AppId value in installers for other applications. -; (To generate a new GUID, click Tools | Generate GUID inside the IDE.) -AppId={{56C1065D-3523-4025-B76D-6F73F67F7F71} -AppName={#AppName} -AppVersion=2.0 -AppPublisher={#AppPublisher} -AppPublisherURL={#AppURL} -AppSupportURL={#ForumsURL} -AppUpdatesURL={#AppURL} -DefaultDirName={commonappdata}\Lidarr\bin -DisableDirPage=yes -DefaultGroupName={#AppName} -DisableProgramGroupPage=yes -OutputBaseFilename=Lidarr.{#BranchName}.{#BuildNumber} -SolidCompression=yes -AppCopyright=Creative Commons 3.0 License -AllowUNCPath=False -UninstallDisplayIcon={app}\Lidarr.exe -DisableReadyPage=True -CompressionThreads=2 -Compression=lzma2/normal -AppContact={#ForumsURL} -VersionInfoVersion={#BuildNumber} - -[Languages] -Name: "english"; MessagesFile: "compiler:Default.isl" - -[Tasks] -;Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked -Name: "windowsService"; Description: "Install as a Windows Service" - -[Files] -Source: "..\_output\Lidarr.exe"; DestDir: "{app}"; Flags: ignoreversion -Source: "..\_output\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs -; NOTE: Don't use "Flags: ignoreversion" on any shared system files - -[Icons] -Name: "{group}\{#AppName}"; Filename: "{app}\{#AppExeName}"; Parameters: "/icon" -Name: "{commondesktop}\{#AppName}"; Filename: "{app}\{#AppExeName}"; Parameters: "/icon" - -[Run] -Filename: "{app}\lidarr.console.exe"; Parameters: "/u"; Flags: waituntilterminated; -Filename: "{app}\lidarr.console.exe"; Parameters: "/i"; Flags: waituntilterminated; Tasks: windowsService - -[UninstallRun] -Filename: "{app}\lidarr.console.exe"; Parameters: "/u"; Flags: waituntilterminated skipifdoesntexist diff --git a/src/Lidarr.Api.V3/AlbumStudio/AlbumStudioArtistResource.cs b/src/Lidarr.Api.V3/AlbumStudio/AlbumStudioArtistResource.cs new file mode 100644 index 000000000..e1c43ebc5 --- /dev/null +++ b/src/Lidarr.Api.V3/AlbumStudio/AlbumStudioArtistResource.cs @@ -0,0 +1,12 @@ +using System.Collections.Generic; +using Lidarr.Api.V3.Albums; + +namespace Lidarr.Api.V3.AlbumStudio +{ + public class AlbumStudioArtistResource + { + public int Id { get; set; } + public bool? Monitored { get; set; } + public List Albums { get; set; } + } +} diff --git a/src/Lidarr.Api.V3/AlbumStudio/AlbumStudioModule.cs b/src/Lidarr.Api.V3/AlbumStudio/AlbumStudioModule.cs new file mode 100644 index 000000000..71dc76920 --- /dev/null +++ b/src/Lidarr.Api.V3/AlbumStudio/AlbumStudioModule.cs @@ -0,0 +1,55 @@ +using System.Linq; +using Nancy; +using NzbDrone.Core.Music; +using Lidarr.Http.Extensions; + +namespace Lidarr.Api.V3.AlbumStudio +{ + public class AlbumStudioModule : SonarrV3Module + { + private readonly IArtistService _artistService; + private readonly IAlbumMonitoredService _episodeMonitoredService; + + public AlbumStudioModule(IArtistService artistService, IAlbumMonitoredService episodeMonitoredService) + : base("/albumstudio") + { + _artistService = artistService; + _episodeMonitoredService = episodeMonitoredService; + Post["/"] = artist => UpdateAll(); + } + + private Response UpdateAll() + { + //Read from request + var request = Request.Body.FromJson(); + var artistToUpdate = _artistService.GetArtists(request.Artist.Select(s => s.Id)); + + foreach (var s in request.Artist) + { + var artist = artistToUpdate.Single(c => c.Id == s.Id); + + if (s.Monitored.HasValue) + { + artist.Monitored = s.Monitored.Value; + } + + if (s.Albums != null && s.Albums.Any()) + { + foreach (var artistAlbum in artist.Albums) + { + var album = s.Albums.FirstOrDefault(c => c.Id == artistAlbum.Id); + + if (album != null) + { + artistAlbum.Monitored = album.Monitored; + } + } + } + + _episodeMonitoredService.SetAlbumMonitoredStatus(artist, request.MonitoringOptions); + } + + return "ok".AsResponse(HttpStatusCode.Accepted); + } + } +} diff --git a/src/Lidarr.Api.V3/AlbumStudio/AlbumStudioResource.cs b/src/Lidarr.Api.V3/AlbumStudio/AlbumStudioResource.cs new file mode 100644 index 000000000..b03f0629e --- /dev/null +++ b/src/Lidarr.Api.V3/AlbumStudio/AlbumStudioResource.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; +using NzbDrone.Core.Music; + +namespace Lidarr.Api.V3.AlbumStudio +{ + public class AlbumStudioResource + { + public List Artist { get; set; } + public MonitoringOptions MonitoringOptions { get; set; } + } +} diff --git a/src/Lidarr.Api.V3/Albums/AlbumModule.cs b/src/Lidarr.Api.V3/Albums/AlbumModule.cs new file mode 100644 index 000000000..6b5e47d31 --- /dev/null +++ b/src/Lidarr.Api.V3/Albums/AlbumModule.cs @@ -0,0 +1,71 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Nancy; +using NzbDrone.Core.DecisionEngine; +using NzbDrone.Core.Music; +using NzbDrone.SignalR; +using Lidarr.Http.Extensions; +using Lidarr.Http.REST; +using NzbDrone.Core.ArtistStats; + +namespace Lidarr.Api.V3.Albums +{ + public class AlbumModule : AlbumModuleWithSignalR + { + public AlbumModule(IArtistService artistService, + IAlbumService albumService, + IArtistStatisticsService artistStatisticsService, + IUpgradableSpecification upgradableSpecification, + IBroadcastSignalRMessage signalRBroadcaster) + : base(albumService, artistStatisticsService, artistService, upgradableSpecification, signalRBroadcaster) + { + GetResourceAll = GetAlbums; + Put[@"/(?[\d]{1,10})"] = x => SetAlbumMonitored(x.Id); + Put["/monitor"] = x => SetAlbumsMonitored(); + } + + private List GetAlbums() + { + var artistIdQuery = Request.Query.ArtistId; + var albumIdsQuery = Request.Query.AlbumIds; + + if (!Request.Query.ArtistId.HasValue && !albumIdsQuery.HasValue) + { + return MapToResource(_albumService.GetAllAlbums(), false); + } + + if (artistIdQuery.HasValue) + { + int artistId = Convert.ToInt32(artistIdQuery.Value); + + return MapToResource(_albumService.GetAlbumsByArtist(artistId), false); + } + + string albumIdsValue = albumIdsQuery.Value.ToString(); + + var albumIds = albumIdsValue.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries) + .Select(e => Convert.ToInt32(e)) + .ToList(); + + return MapToResource(_albumService.GetAlbums(albumIds), false); + } + + private Response SetAlbumMonitored(int id) + { + var resource = Request.Body.FromJson(); + _albumService.SetAlbumMonitored(id, resource.Monitored); + + return MapToResource(_albumService.GetAlbum(id), false).AsResponse(HttpStatusCode.Accepted); + } + + private Response SetAlbumsMonitored() + { + var resource = Request.Body.FromJson(); + + _albumService.SetMonitored(resource.AlbumIds, resource.Monitored); + + return MapToResource(_albumService.GetAlbums(resource.AlbumIds), false).AsResponse(HttpStatusCode.Accepted); + } + } +} diff --git a/src/NzbDrone.Api/Albums/AlbumModuleWithSignalR.cs b/src/Lidarr.Api.V3/Albums/AlbumModuleWithSignalR.cs similarity index 80% rename from src/NzbDrone.Api/Albums/AlbumModuleWithSignalR.cs rename to src/Lidarr.Api.V3/Albums/AlbumModuleWithSignalR.cs index 59fe8fc2d..17c64b70c 100644 --- a/src/NzbDrone.Api/Albums/AlbumModuleWithSignalR.cs +++ b/src/Lidarr.Api.V3/Albums/AlbumModuleWithSignalR.cs @@ -1,32 +1,28 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using FluentValidation; using NzbDrone.Common.Extensions; -using NzbDrone.Api.TrackFiles; -using NzbDrone.Api.Music; -using NzbDrone.Core.Datastore.Events; +using Lidarr.Api.V3.Artist; using NzbDrone.Core.DecisionEngine; -using NzbDrone.Core.Download; -using NzbDrone.Core.MediaFiles.Events; -using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Music; using NzbDrone.Core.ArtistStats; using NzbDrone.SignalR; +using Lidarr.Http; -namespace NzbDrone.Api.Albums +namespace Lidarr.Api.V3.Albums { - public abstract class AlbumModuleWithSignalR : NzbDroneRestModuleWithSignalR + public abstract class AlbumModuleWithSignalR : LidarrRestModuleWithSignalR { protected readonly IAlbumService _albumService; protected readonly IArtistStatisticsService _artistStatisticsService; protected readonly IArtistService _artistService; - protected readonly IQualityUpgradableSpecification _qualityUpgradableSpecification; + protected readonly IUpgradableSpecification _qualityUpgradableSpecification; protected AlbumModuleWithSignalR(IAlbumService albumService, IArtistStatisticsService artistStatisticsService, IArtistService artistService, - IQualityUpgradableSpecification qualityUpgradableSpecification, + IUpgradableSpecification qualityUpgradableSpecification, IBroadcastSignalRMessage signalRBroadcaster) : base(signalRBroadcaster) { @@ -41,7 +37,7 @@ namespace NzbDrone.Api.Albums protected AlbumModuleWithSignalR(IAlbumService albumService, IArtistStatisticsService artistStatisticsService, IArtistService artistService, - IQualityUpgradableSpecification qualityUpgradableSpecification, + IUpgradableSpecification qualityUpgradableSpecification, IBroadcastSignalRMessage signalRBroadcaster, string resource) : base(signalRBroadcaster, resource) @@ -86,7 +82,7 @@ namespace NzbDrone.Api.Albums if (includeArtist) { - var artistDict = new Dictionary(); + var artistDict = new Dictionary(); for (var i = 0; i < albums.Count; i++) { var album = albums[i]; @@ -94,7 +90,7 @@ namespace NzbDrone.Api.Albums var artist = album.Artist ?? artistDict.GetValueOrDefault(albums[i].ArtistId) ?? _artistService.GetArtist(albums[i].ArtistId); artistDict[artist.Id] = artist; - + if (includeArtist) { resource.Artist = artist.ToResource(); @@ -102,12 +98,9 @@ namespace NzbDrone.Api.Albums } } - for (var i = 0; i < albums.Count; i++) - { - var resource = result[i]; - FetchAndLinkAlbumStatistics(resource); - } - + var artistList = albums.DistinctBy(a => a.ArtistId).ToList(); + var artistStats = _artistStatisticsService.ArtistStatistics(); + LinkArtistStatistics(result, artistStats); return result; } @@ -117,6 +110,15 @@ namespace NzbDrone.Api.Albums LinkArtistStatistics(resource, _artistStatisticsService.ArtistStatistics(resource.ArtistId)); } + private void LinkArtistStatistics(List resources, List artistStatistics) + { + foreach (var album in resources) + { + var stats = artistStatistics.SingleOrDefault(ss => ss.ArtistId == album.ArtistId); + LinkArtistStatistics(album, stats); + } + } + private void LinkArtistStatistics(AlbumResource resource, ArtistStatistics artistStatistics) { if (artistStatistics.AlbumStatistics != null) @@ -124,7 +126,7 @@ namespace NzbDrone.Api.Albums var dictAlbumStats = artistStatistics.AlbumStatistics.ToDictionary(v => v.AlbumId); resource.Statistics = dictAlbumStats.GetValueOrDefault(resource.Id).ToResource(); - + } } diff --git a/src/Lidarr.Api.V3/Albums/AlbumResource.cs b/src/Lidarr.Api.V3/Albums/AlbumResource.cs new file mode 100644 index 000000000..ddfc8c5b1 --- /dev/null +++ b/src/Lidarr.Api.V3/Albums/AlbumResource.cs @@ -0,0 +1,67 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Newtonsoft.Json; +using NzbDrone.Core.Music; +using Lidarr.Api.V3.Artist; +using Lidarr.Http.REST; +using NzbDrone.Core.MediaCover; + +namespace Lidarr.Api.V3.Albums +{ + public class AlbumResource : RestResource + { + public string Title { get; set; } + public int ArtistId { get; set; } + public List AlbumLabel { get; set; } + public string ForeignAlbumId { get; set; } + public bool Monitored { get; set; } + public string Path { get; set; } + public int ProfileId { get; set; } + public int Duration { get; set; } + public string AlbumType { get; set; } + public Ratings Ratings { get; set; } + public DateTime? ReleaseDate { get; set; } + public List Genres { get; set; } + public ArtistResource Artist { get; set; } + public List Images { get; set; } + public AlbumStatisticsResource Statistics { get; set; } + + //Hiding this so people don't think its usable (only used to set the initial state) + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] + public bool Grabbed { get; set; } + } + + public static class EpisodeResourceMapper + { + public static AlbumResource ToResource(this Album model) + { + if (model == null) return null; + + return new AlbumResource + { + Id = model.Id, + ArtistId = model.ArtistId, + AlbumLabel = model.Label, + ForeignAlbumId = model.ForeignAlbumId, + Path = model.Path, + ProfileId = model.ProfileId, + Monitored = model.Monitored, + ReleaseDate = model.ReleaseDate, + Genres = model.Genres, + Title = model.Title, + Images = model.Images, + Ratings = model.Ratings, + Duration = model.Duration, + AlbumType = model.AlbumType + }; + } + + public static List ToResource(this IEnumerable models) + { + if (models == null) return null; + + return models.Select(ToResource).ToList(); + } + } +} diff --git a/src/NzbDrone.Api/Albums/AlbumStatisticsResource.cs b/src/Lidarr.Api.V3/Albums/AlbumStatisticsResource.cs similarity index 75% rename from src/NzbDrone.Api/Albums/AlbumStatisticsResource.cs rename to src/Lidarr.Api.V3/Albums/AlbumStatisticsResource.cs index 13ffb2e4b..e53450ed5 100644 --- a/src/NzbDrone.Api/Albums/AlbumStatisticsResource.cs +++ b/src/Lidarr.Api.V3/Albums/AlbumStatisticsResource.cs @@ -1,7 +1,7 @@ -using System; +using System; using NzbDrone.Core.ArtistStats; -namespace NzbDrone.Api.Albums +namespace Lidarr.Api.V3.Albums { public class AlbumStatisticsResource { @@ -10,14 +10,11 @@ namespace NzbDrone.Api.Albums public int TotalTrackCount { get; set; } public long SizeOnDisk { get; set; } - public decimal PercentOfTracks + public decimal PercentOfEpisodes { get { - if (TrackCount == 0) - { - return 0; - } + if (TrackCount == 0) return 0; return (decimal)TrackFileCount / (decimal)TrackCount * 100; } @@ -28,10 +25,7 @@ namespace NzbDrone.Api.Albums { public static AlbumStatisticsResource ToResource(this AlbumStatistics model) { - if (model == null) - { - return null; - } + if (model == null) return null; return new AlbumStatisticsResource { diff --git a/src/Lidarr.Api.V3/Albums/AlbumsMonitoredResource.cs b/src/Lidarr.Api.V3/Albums/AlbumsMonitoredResource.cs new file mode 100644 index 000000000..9c0120eda --- /dev/null +++ b/src/Lidarr.Api.V3/Albums/AlbumsMonitoredResource.cs @@ -0,0 +1,11 @@ +using System; +using System.Collections.Generic; + +namespace Lidarr.Api.V3.Albums +{ + public class AlbumsMonitoredResource + { + public List AlbumIds { get; set; } + public bool Monitored { get; set; } + } +} diff --git a/src/NzbDrone.Api/Series/AlternateTitleResource.cs b/src/Lidarr.Api.V3/Artist/AlternateTitleResource.cs similarity index 85% rename from src/NzbDrone.Api/Series/AlternateTitleResource.cs rename to src/Lidarr.Api.V3/Artist/AlternateTitleResource.cs index b1d6cc22c..d3df621b9 100644 --- a/src/NzbDrone.Api/Series/AlternateTitleResource.cs +++ b/src/Lidarr.Api.V3/Artist/AlternateTitleResource.cs @@ -1,4 +1,4 @@ -namespace NzbDrone.Api.Series +namespace Lidarr.Api.V3.Series { public class AlternateTitleResource { diff --git a/src/Lidarr.Api.V3/Artist/ArtistEditorDeleteResource.cs b/src/Lidarr.Api.V3/Artist/ArtistEditorDeleteResource.cs new file mode 100644 index 000000000..09c5b892c --- /dev/null +++ b/src/Lidarr.Api.V3/Artist/ArtistEditorDeleteResource.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; + +namespace Lidarr.Api.V3.Artist +{ + public class ArtistEditorDeleteResource + { + public List ArtistIds { get; set; } + public bool DeleteFiles { get; set; } + } +} diff --git a/src/Lidarr.Api.V3/Artist/ArtistEditorModule.cs b/src/Lidarr.Api.V3/Artist/ArtistEditorModule.cs new file mode 100644 index 000000000..2cbdd8046 --- /dev/null +++ b/src/Lidarr.Api.V3/Artist/ArtistEditorModule.cs @@ -0,0 +1,85 @@ +using System.Collections.Generic; +using Nancy; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.Music; +using Lidarr.Http.Extensions; + +namespace Lidarr.Api.V3.Artist +{ + public class ArtistEditorModule : SonarrV3Module + { + private readonly IArtistService _artistService; + + public ArtistEditorModule(IArtistService artistService) + : base("/artist/editor") + { + _artistService = artistService; + Put["/"] = artist => SaveAll(); + Delete["/"] = artist => DeleteArtist(); + } + + private Response SaveAll() + { + var resource = Request.Body.FromJson(); + var artistToUpdate = _artistService.GetArtists(resource.ArtistIds); + + foreach (var artist in artistToUpdate) + { + if (resource.Monitored.HasValue) + { + artist.Monitored = resource.Monitored.Value; + } + + if (resource.QualityProfileId.HasValue) + { + artist.ProfileId = resource.QualityProfileId.Value; + } + + if (resource.AlbumFolder.HasValue) + { + artist.AlbumFolder = resource.AlbumFolder.Value; + } + + if (resource.RootFolderPath.IsNotNullOrWhiteSpace()) + { + artist.RootFolderPath = resource.RootFolderPath; + } + + if (resource.Tags != null) + { + var newTags = resource.Tags; + var applyTags = resource.ApplyTags; + + switch (applyTags) + { + case ApplyTags.Add: + newTags.ForEach(t => artist.Tags.Add(t)); + break; + case ApplyTags.Remove: + newTags.ForEach(t => artist.Tags.Remove(t)); + break; + case ApplyTags.Replace: + artist.Tags = new HashSet(newTags); + break; + } + } + } + + return _artistService.UpdateArtists(artistToUpdate) + .ToResource() + .AsResponse(HttpStatusCode.Accepted); + } + + private Response DeleteArtist() + { + var resource = Request.Body.FromJson(); + + foreach (var artistId in resource.ArtistIds) + { + _artistService.DeleteArtist(artistId, false); + } + + return new object().AsResponse(); + } + } +} diff --git a/src/Lidarr.Api.V3/Artist/ArtistEditorResource.cs b/src/Lidarr.Api.V3/Artist/ArtistEditorResource.cs new file mode 100644 index 000000000..649b9910a --- /dev/null +++ b/src/Lidarr.Api.V3/Artist/ArtistEditorResource.cs @@ -0,0 +1,25 @@ +using System.Collections.Generic; +using NzbDrone.Core.Music; + +namespace Lidarr.Api.V3.Artist +{ + public class ArtistEditorResource + { + public List ArtistIds { get; set; } + public bool? Monitored { get; set; } + public int? QualityProfileId { get; set; } + public int? LanguageProfileId { get; set; } + //public SeriesTypes? SeriesType { get; set; } + public bool? AlbumFolder { get; set; } + public string RootFolderPath { get; set; } + public List Tags { get; set; } + public ApplyTags ApplyTags { get; set; } + } + + public enum ApplyTags + { + Add, + Remove, + Replace + } +} diff --git a/src/Lidarr.Api.V3/Artist/ArtistImportModule.cs b/src/Lidarr.Api.V3/Artist/ArtistImportModule.cs new file mode 100644 index 000000000..2a3e1519e --- /dev/null +++ b/src/Lidarr.Api.V3/Artist/ArtistImportModule.cs @@ -0,0 +1,29 @@ +using System.Collections.Generic; +using Nancy; +using NzbDrone.Core.Music; +using Lidarr.Http; +using Lidarr.Http.Extensions; + +namespace Lidarr.Api.V3.Artist +{ + public class ArtistImportModule : LidarrRestModule + { + private readonly IAddArtistService _addArtistService; + + public ArtistImportModule(IAddArtistService addArtistService) + : base("/artist/import") + { + _addArtistService = addArtistService; + Post["/"] = x => Import(); + } + + + private Response Import() + { + var resource = Request.Body.FromJson>(); + var newArtist = resource.ToModel(); + + return _addArtistService.AddArtists(newArtist).ToResource().AsResponse(); + } + } +} diff --git a/src/Lidarr.Api.V3/Artist/ArtistLookupModule.cs b/src/Lidarr.Api.V3/Artist/ArtistLookupModule.cs new file mode 100644 index 000000000..d0cbc6349 --- /dev/null +++ b/src/Lidarr.Api.V3/Artist/ArtistLookupModule.cs @@ -0,0 +1,45 @@ +using System.Collections.Generic; +using System.Linq; +using Nancy; +using NzbDrone.Core.MediaCover; +using NzbDrone.Core.MetadataSource; +using Lidarr.Http; +using Lidarr.Http.Extensions; + +namespace Lidarr.Api.V3.Artist +{ + public class ArtistLookupModule : LidarrRestModule + { + private readonly ISearchForNewArtist _searchProxy; + + public ArtistLookupModule(ISearchForNewArtist searchProxy) + : base("/artist/lookup") + { + _searchProxy = searchProxy; + Get["/"] = x => Search(); + } + + + private Response Search() + { + var tvDbResults = _searchProxy.SearchForNewArtist((string)Request.Query.term); + return MapToResource(tvDbResults).AsResponse(); + } + + + private static IEnumerable MapToResource(IEnumerable artist) + { + foreach (var currentArtist in artist) + { + var resource = currentArtist.ToResource(); + var poster = currentArtist.Images.FirstOrDefault(c => c.CoverType == MediaCoverTypes.Poster); + if (poster != null) + { + resource.RemotePoster = poster.Url; + } + + yield return resource; + } + } + } +} diff --git a/src/Lidarr.Api.V3/Artist/ArtistModule.cs b/src/Lidarr.Api.V3/Artist/ArtistModule.cs new file mode 100644 index 000000000..0ead0137c --- /dev/null +++ b/src/Lidarr.Api.V3/Artist/ArtistModule.cs @@ -0,0 +1,247 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using FluentValidation; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.Datastore.Events; +using NzbDrone.Core.MediaCover; +using NzbDrone.Core.MediaFiles; +using NzbDrone.Core.MediaFiles.Events; +using NzbDrone.Core.Messaging.Events; +using NzbDrone.Core.ArtistStats; +using NzbDrone.Core.Music; +using NzbDrone.Core.Music.Events; +using NzbDrone.Core.Validation; +using NzbDrone.Core.Validation.Paths; +using Lidarr.Api.V3.Albums; +using NzbDrone.SignalR; +using Lidarr.Http; +using Lidarr.Http.Extensions; +using Lidarr.Http.Mapping; + +namespace Lidarr.Api.V3.Artist +{ + public class ArtistModule : LidarrRestModuleWithSignalR, + IHandle, + IHandle, + IHandle, + IHandle, + IHandle, + IHandle, + IHandle + + { + private readonly IArtistService _artistService; + private readonly IAddArtistService _addArtistService; + private readonly IArtistStatisticsService _artistStatisticsService; + private readonly IMapCoversToLocal _coverMapper; + private readonly IAlbumService _albumService; + + public ArtistModule(IBroadcastSignalRMessage signalRBroadcaster, + IArtistService artistService, + IAddArtistService addArtistService, + IArtistStatisticsService artistStatisticsService, + IMapCoversToLocal coverMapper, + IAlbumService albumService, + RootFolderValidator rootFolderValidator, + ArtistPathValidator artistPathValidator, + ArtistExistsValidator artistExistsValidator, + ArtistAncestorValidator artistAncestorValidator, + ProfileExistsValidator profileExistsValidator, + LanguageProfileExistsValidator languageProfileExistsValidator + ) + : base(signalRBroadcaster) + { + _artistService = artistService; + _addArtistService = addArtistService; + _artistStatisticsService = artistStatisticsService; + + _coverMapper = coverMapper; + _albumService = albumService; + + GetResourceAll = AllArtists; + GetResourceById = GetArtist; + CreateResource = AddArtist; + UpdateResource = UpdateArtist; + DeleteResource = DeleteArtist; + + Http.Validation.RuleBuilderExtensions.ValidId(SharedValidator.RuleFor(s => s.QualityProfileId)); + + SharedValidator.RuleFor(s => s.Path) + .Cascade(CascadeMode.StopOnFirstFailure) + .IsValidPath() + .SetValidator(rootFolderValidator) + .SetValidator(artistPathValidator) + .SetValidator(artistAncestorValidator) + .When(s => !s.Path.IsNullOrWhiteSpace()); + + SharedValidator.RuleFor(s => s.QualityProfileId).SetValidator(profileExistsValidator); + SharedValidator.RuleFor(s => s.LanguageProfileId).SetValidator(languageProfileExistsValidator); + + PostValidator.RuleFor(s => s.Path).IsValidPath().When(s => s.RootFolderPath.IsNullOrWhiteSpace()); + PostValidator.RuleFor(s => s.RootFolderPath).IsValidPath().When(s => s.Path.IsNullOrWhiteSpace()); + PostValidator.RuleFor(s => s.ArtistName).NotEmpty(); + PostValidator.RuleFor(s => s.ForeignArtistId).NotEmpty().SetValidator(artistExistsValidator); + + PutValidator.RuleFor(s => s.Path).IsValidPath(); + } + + private ArtistResource GetArtist(int id) + { + var artist = _artistService.GetArtist(id); + return GetArtistResource(artist); + } + + private ArtistResource GetArtistResource(NzbDrone.Core.Music.Artist artist) + { + if (artist == null) return null; + + var resource = artist.ToResource(); + MapCoversToLocal(resource); + //MapAlbums(resource); + FetchAndLinkArtistStatistics(resource); + //PopulateAlternateTitles(resource); + + return resource; + } + + private List AllArtists() + { + var artistStats = _artistStatisticsService.ArtistStatistics(); + var artistsResources = _artistService.GetAllArtists().ToResource(); + + MapCoversToLocal(artistsResources.ToArray()); + //MapAlbums(artistsResources.ToArray()); + LinkArtistStatistics(artistsResources, artistStats); + //PopulateAlternateTitles(seriesResources); + + return artistsResources; + } + + private int AddArtist(ArtistResource artistResource) + { + var artist = _addArtistService.AddArtist(artistResource.ToModel()); + + return artist.Id; + } + + private void UpdateArtist(ArtistResource artistResource) + { + var model = artistResource.ToModel(_artistService.GetArtist(artistResource.Id)); + + _artistService.UpdateArtist(model); + + BroadcastResourceChange(ModelAction.Updated, artistResource); + } + + private void DeleteArtist(int id) + { + var deleteFiles = Request.GetBooleanQueryParameter("deleteFiles"); + + _artistService.DeleteArtist(id, deleteFiles); + } + + private void MapCoversToLocal(params ArtistResource[] artists) + { + foreach (var artistResource in artists) + { + _coverMapper.ConvertToLocalUrls(artistResource.Id, artistResource.Images); + } + } + + private void MapAlbums(params ArtistResource[] artists) + { + foreach (var artistResource in artists) + { + artistResource.Albums = _albumService.GetAlbumsByArtist(artistResource.Id).ToResource(); + } + } + + private void FetchAndLinkArtistStatistics(ArtistResource resource) + { + LinkArtistStatistics(resource, _artistStatisticsService.ArtistStatistics(resource.Id)); + } + + private void LinkArtistStatistics(List resources, List artistStatistics) + { + foreach (var artist in resources) + { + var stats = artistStatistics.SingleOrDefault(ss => ss.ArtistId == artist.Id); + if (stats == null) continue; + + LinkArtistStatistics(artist, stats); + } + } + + private void LinkArtistStatistics(ArtistResource resource, ArtistStatistics artistStatistics) + { + resource.TotalTrackCount = artistStatistics.TotalTrackCount; + resource.TrackCount = artistStatistics.TrackCount; + resource.TrackFileCount = artistStatistics.TrackFileCount; + resource.SizeOnDisk = artistStatistics.SizeOnDisk; + resource.AlbumCount = artistStatistics.AlbumCount; + + if (artistStatistics.AlbumStatistics != null) + { + foreach (var album in resource.Albums) + { + album.Statistics = artistStatistics.AlbumStatistics.SingleOrDefault(s => s.AlbumId == album.Id).ToResource(); + } + } + } + + //private void PopulateAlternateTitles(List resources) + //{ + // foreach (var resource in resources) + // { + // PopulateAlternateTitles(resource); + // } + //} + + //private void PopulateAlternateTitles(ArtistResource resource) + //{ + // var mappings = _sceneMappingService.FindByTvdbId(resource.TvdbId); + + // if (mappings == null) return; + + // resource.AlternateTitles = mappings.Select(v => new AlternateTitleResource { Title = v.Title, SeasonNumber = v.SeasonNumber, SceneSeasonNumber = v.SceneSeasonNumber }).ToList(); + //} + + public void Handle(TrackImportedEvent message) + { + BroadcastResourceChange(ModelAction.Updated, message.ImportedTrack.ArtistId); + } + + public void Handle(TrackFileDeletedEvent message) + { + if (message.Reason == DeleteMediaFileReason.Upgrade) return; + + BroadcastResourceChange(ModelAction.Updated, message.TrackFile.ArtistId); + } + + public void Handle(ArtistUpdatedEvent message) + { + BroadcastResourceChange(ModelAction.Updated, message.Artist.Id); + } + + public void Handle(ArtistEditedEvent message) + { + BroadcastResourceChange(ModelAction.Updated, message.Artist.Id); + } + + public void Handle(ArtistDeletedEvent message) + { + BroadcastResourceChange(ModelAction.Deleted, message.Artist.ToResource()); + } + + public void Handle(ArtistRenamedEvent message) + { + BroadcastResourceChange(ModelAction.Updated, message.Artist.Id); + } + + public void Handle(MediaCoversUpdatedEvent message) + { + BroadcastResourceChange(ModelAction.Updated, message.Artist.Id); + } + } +} diff --git a/src/Lidarr.Api.V3/Artist/ArtistResource.cs b/src/Lidarr.Api.V3/Artist/ArtistResource.cs new file mode 100644 index 000000000..2ac1f42c4 --- /dev/null +++ b/src/Lidarr.Api.V3/Artist/ArtistResource.cs @@ -0,0 +1,190 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using NzbDrone.Core.MediaCover; +using NzbDrone.Core.Music; +using Lidarr.Api.V3.Albums; +using Lidarr.Http.REST; + +namespace Lidarr.Api.V3.Artist +{ + public class ArtistResource : RestResource + { + //Todo: Sorters should be done completely on the client + //Todo: Is there an easy way to keep IgnoreArticlesWhenSorting in sync between, Series, History, Missing? + //Todo: We should get the entire Profile instead of ID and Name separately + + public ArtistStatusType Status { get; set; } + + public bool Ended => Status == ArtistStatusType.Ended; + + public DateTime? LastInfoSync { get; set; } + + public string ArtistName { get; set; } + public string ForeignArtistId { get; set; } + public string MBId { get; set; } + public int TADBId { get; set; } + public int DiscogsId { get; set; } + public string AllMusicId { get; set; } + public string Overview { get; set; } + public string ArtistType { get; set; } + public string Disambiguation { get; set; } + public List PrimaryAlbumTypes { get; set; } + public List SecondaryAlbumTypes { get; set; } + public List Links { get; set; } + + public int? AlbumCount { get; set; } + public int? TotalTrackCount { get; set; } + public int? TrackCount { get; set; } + public int? TrackFileCount { get; set; } + public long? SizeOnDisk { get; set; } + //public SeriesStatusType Status { get; set; } + + public List Images { get; set; } + public List Members { get; set; } + + public string RemotePoster { get; set; } + public List Albums { get; set; } + + + //View & Edit + public string Path { get; set; } + public int QualityProfileId { get; set; } + public int LanguageProfileId { get; set; } + + //Editing Only + public bool AlbumFolder { get; set; } + public bool Monitored { get; set; } + + public string RootFolderPath { get; set; } + //public string Certification { get; set; } + public List Genres { get; set; } + public string CleanName { get; set; } + public string SortName { get; set; } + public HashSet Tags { get; set; } + public DateTime Added { get; set; } + public AddArtistOptions AddOptions { get; set; } + public Ratings Ratings { get; set; } + public string NameSlug { get; set; } + + //TODO: Add series statistics as a property of the series (instead of individual properties) + } + + public static class SeriesResourceMapper + { + public static ArtistResource ToResource(this NzbDrone.Core.Music.Artist model) + { + if (model == null) return null; + + return new ArtistResource + { + Id = model.Id, + + ArtistName = model.Name, + //AlternateTitles + SortName = model.SortName, + + Status = model.Status, + Overview = model.Overview, + ArtistType = model.ArtistType, + Disambiguation = model.Disambiguation, + + PrimaryAlbumTypes = model.PrimaryAlbumTypes, + SecondaryAlbumTypes = model.SecondaryAlbumTypes, + + Images = model.Images, + + Albums = model.Albums.ToResource(), + //Year = model.Year, + + Path = model.Path, + QualityProfileId = model.ProfileId, + LanguageProfileId = model.LanguageProfileId, + Links = model.Links, + + AlbumFolder = model.AlbumFolder, + Monitored = model.Monitored, + + LastInfoSync = model.LastInfoSync, + //SeriesType = model.SeriesType, + CleanName = model.CleanName, + ForeignArtistId = model.ForeignArtistId, + NameSlug = model.NameSlug, + RootFolderPath = model.RootFolderPath, + //Certification = model.Certification, + Genres = model.Genres, + Tags = model.Tags, + Added = model.Added, + AddOptions = model.AddOptions, + Ratings = model.Ratings + }; + } + + public static NzbDrone.Core.Music.Artist ToModel(this ArtistResource resource) + { + if (resource == null) return null; + + return new NzbDrone.Core.Music.Artist + { + Id = resource.Id, + + Name = resource.ArtistName, + //AlternateTitles + SortName = resource.SortName, + + Status = resource.Status, + Overview = resource.Overview, + //NextAiring + //PreviousAiring + // Network = resource.Network, + //AirTime = resource.AirTime, + Images = resource.Images, + + //Albums = resource.Albums.ToModel(), + //Year = resource.Year, + + Path = resource.Path, + ProfileId = resource.QualityProfileId, + LanguageProfileId = resource.LanguageProfileId, + Links = resource.Links, + PrimaryAlbumTypes = resource.PrimaryAlbumTypes, + SecondaryAlbumTypes = resource.SecondaryAlbumTypes, + + AlbumFolder = resource.AlbumFolder, + Monitored = resource.Monitored, + + LastInfoSync = resource.LastInfoSync, + //SeriesType = resource.SeriesType, + CleanName = resource.CleanName, + ForeignArtistId = resource.ForeignArtistId, + NameSlug = resource.NameSlug, + RootFolderPath = resource.RootFolderPath, + //Certification = resource.Certification, + Genres = resource.Genres, + Tags = resource.Tags, + Added = resource.Added, + AddOptions = resource.AddOptions, + Ratings = resource.Ratings + }; + } + + public static NzbDrone.Core.Music.Artist ToModel(this ArtistResource resource, NzbDrone.Core.Music.Artist artist) + { + var updatedArtist = resource.ToModel(); + + artist.ApplyChanges(updatedArtist); + + return artist; + } + + public static List ToResource(this IEnumerable artist) + { + return artist.Select(ToResource).ToList(); + } + + public static List ToModel(this IEnumerable resources) + { + return resources.Select(ToModel).ToList(); + } + } +} diff --git a/src/Lidarr.Api.V3/Blacklist/BlacklistModule.cs b/src/Lidarr.Api.V3/Blacklist/BlacklistModule.cs new file mode 100644 index 000000000..7fecda1ca --- /dev/null +++ b/src/Lidarr.Api.V3/Blacklist/BlacklistModule.cs @@ -0,0 +1,36 @@ +using NzbDrone.Core.Blacklisting; +using NzbDrone.Core.Datastore; +using Lidarr.Http; + +namespace Lidarr.Api.V3.Blacklist +{ + public class BlacklistModule : LidarrRestModule + { + private readonly IBlacklistService _blacklistService; + + public BlacklistModule(IBlacklistService blacklistService) + { + _blacklistService = blacklistService; + GetResourcePaged = GetBlacklist; + DeleteResource = DeleteBlacklist; + } + + private PagingResource GetBlacklist(PagingResource pagingResource) + { + var pagingSpec = new PagingSpec + { + Page = pagingResource.Page, + PageSize = pagingResource.PageSize, + SortKey = pagingResource.SortKey, + SortDirection = pagingResource.SortDirection + }; + + return ApplyToPage(_blacklistService.Paged, pagingSpec, BlacklistResourceMapper.MapToResource); + } + + private void DeleteBlacklist(int id) + { + _blacklistService.Delete(id); + } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Api/Blacklist/BlacklistResource.cs b/src/Lidarr.Api.V3/Blacklist/BlacklistResource.cs similarity index 78% rename from src/NzbDrone.Api/Blacklist/BlacklistResource.cs rename to src/Lidarr.Api.V3/Blacklist/BlacklistResource.cs index d534e720f..d47926b97 100644 --- a/src/NzbDrone.Api/Blacklist/BlacklistResource.cs +++ b/src/Lidarr.Api.V3/Blacklist/BlacklistResource.cs @@ -1,17 +1,19 @@ -using System; +using System; using System.Collections.Generic; -using NzbDrone.Api.REST; -using NzbDrone.Core.Qualities; -using NzbDrone.Api.Music; using NzbDrone.Core.Indexers; +using NzbDrone.Core.Languages; +using NzbDrone.Core.Qualities; +using Lidarr.Api.V3.Artist; +using Lidarr.Http.REST; -namespace NzbDrone.Api.Blacklist +namespace Lidarr.Api.V3.Blacklist { public class BlacklistResource : RestResource { public int ArtistId { get; set; } public List AlbumIds { get; set; } public string SourceTitle { get; set; } + public Language Language { get; set; } public QualityModel Quality { get; set; } public DateTime Date { get; set; } public DownloadProtocol Protocol { get; set; } @@ -23,7 +25,7 @@ namespace NzbDrone.Api.Blacklist public static class BlacklistResourceMapper { - public static BlacklistResource MapToResource(this Core.Blacklisting.Blacklist model) + public static BlacklistResource MapToResource(this NzbDrone.Core.Blacklisting.Blacklist model) { if (model == null) return null; @@ -34,6 +36,7 @@ namespace NzbDrone.Api.Blacklist ArtistId = model.ArtistId, AlbumIds = model.AlbumIds, SourceTitle = model.SourceTitle, + Language = model.Language, Quality = model.Quality, Date = model.Date, Protocol = model.Protocol, diff --git a/src/Lidarr.Api.V3/Calendar/CalendarFeedModule.cs b/src/Lidarr.Api.V3/Calendar/CalendarFeedModule.cs new file mode 100644 index 000000000..86a467be7 --- /dev/null +++ b/src/Lidarr.Api.V3/Calendar/CalendarFeedModule.cs @@ -0,0 +1,99 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Ical.Net; +using Ical.Net.DataTypes; +using Ical.Net.General; +using Ical.Net.Interfaces.Serialization; +using Ical.Net.Serialization; +using Ical.Net.Serialization.iCalendar.Factory; +using Nancy; +using Nancy.Responses; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.Tags; +using NzbDrone.Core.Music; +using Lidarr.Http.Extensions; + +namespace Lidarr.Api.V3.Calendar +{ + public class CalendarFeedModule : SonarrV3FeedModule + { + private readonly IAlbumService _albumService; + private readonly ITagService _tagService; + + public CalendarFeedModule(IAlbumService albumService, ITagService tagService) + : base("calendar") + { + _albumService = albumService; + _tagService = tagService; + + Get["/Lidarr.ics"] = options => GetCalendarFeed(); + } + + private Response GetCalendarFeed() + { + var pastDays = 7; + var futureDays = 28; + var start = DateTime.Today.AddDays(-pastDays); + var end = DateTime.Today.AddDays(futureDays); + var unmonitored = Request.GetBooleanQueryParameter("unmonitored"); + var asAllDay = Request.GetBooleanQueryParameter("asAllDay"); + var tags = new List(); + + var queryPastDays = Request.Query.PastDays; + var queryFutureDays = Request.Query.FutureDays; + var queryTags = Request.Query.Tags; + + if (queryPastDays.HasValue) + { + pastDays = int.Parse(queryPastDays.Value); + start = DateTime.Today.AddDays(-pastDays); + } + + if (queryFutureDays.HasValue) + { + futureDays = int.Parse(queryFutureDays.Value); + end = DateTime.Today.AddDays(futureDays); + } + + if (queryTags.HasValue) + { + var tagInput = (string)queryTags.Value.ToString(); + tags.AddRange(tagInput.Split(',').Select(_tagService.GetTag).Select(t => t.Id)); + } + + var albums = _albumService.AlbumsBetweenDates(start, end, unmonitored); + var calendar = new Ical.Net.Calendar + { + ProductId = "-//lidarr.audio//Lidarr//EN" + }; + + var calendarName = "Lidarr Music Schedule"; + calendar.AddProperty(new CalendarProperty("NAME", calendarName)); + calendar.AddProperty(new CalendarProperty("X-WR-CALNAME", calendarName)); + + foreach (var album in albums.OrderBy(v => v.ReleaseDate.Value)) + { + if (tags.Any() && tags.None(album.Artist.Tags.Contains)) + { + continue; + } + + var occurrence = calendar.Create(); + occurrence.Uid = "NzbDrone_album_" + album.Id; + //occurrence.Status = album.HasFile ? EventStatus.Confirmed : EventStatus.Tentative; + //occurrence.Description = album.Overview; + //occurrence.Categories = new List() { album.Series.Network }; + + occurrence.Start = new CalDateTime(album.ReleaseDate.Value.ToLocalTime()) { HasTime = false }; + + occurrence.Summary = $"{album.Artist.Name} - {album.Title}"; + } + + var serializer = (IStringSerializer)new SerializerFactory().Build(calendar.GetType(), new SerializationContext()); + var icalendar = serializer.SerializeToString(calendar); + + return new TextResponse(icalendar, "text/calendar"); + } + } +} diff --git a/src/Lidarr.Api.V3/Calendar/CalendarModule.cs b/src/Lidarr.Api.V3/Calendar/CalendarModule.cs new file mode 100644 index 000000000..db4e78722 --- /dev/null +++ b/src/Lidarr.Api.V3/Calendar/CalendarModule.cs @@ -0,0 +1,44 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using NzbDrone.Core.DecisionEngine; +using NzbDrone.Core.Music; +using NzbDrone.Core.ArtistStats; +using NzbDrone.SignalR; +using Lidarr.Api.V3.Albums; +using Lidarr.Http.Extensions; + +namespace Lidarr.Api.V3.Calendar +{ + public class CalendarModule : AlbumModuleWithSignalR + { + public CalendarModule(IAlbumService albumService, + IArtistStatisticsService artistStatisticsService, + IArtistService artistService, + IUpgradableSpecification ugradableSpecification, + IBroadcastSignalRMessage signalRBroadcaster) + : base(albumService, artistStatisticsService, artistService, ugradableSpecification, signalRBroadcaster, "calendar") + { + GetResourceAll = GetCalendar; + } + + private List GetCalendar() + { + var start = DateTime.Today; + var end = DateTime.Today.AddDays(2); + var includeUnmonitored = Request.GetBooleanQueryParameter("unmonitored"); + var includeArtist = Request.GetBooleanQueryParameter("includeArtist"); + var includeEpisodeFile = Request.GetBooleanQueryParameter("includeEpisodeFile"); + + var queryStart = Request.Query.Start; + var queryEnd = Request.Query.End; + + if (queryStart.HasValue) start = DateTime.Parse(queryStart.Value); + if (queryEnd.HasValue) end = DateTime.Parse(queryEnd.Value); + + var resources = MapToResource(_albumService.AlbumsBetweenDates(start, end, includeUnmonitored), includeArtist); + + return resources.OrderBy(e => e.ReleaseDate).ToList(); + } + } +} diff --git a/src/Lidarr.Api.V3/Commands/CommandModule.cs b/src/Lidarr.Api.V3/Commands/CommandModule.cs new file mode 100644 index 000000000..c4ceba3ed --- /dev/null +++ b/src/Lidarr.Api.V3/Commands/CommandModule.cs @@ -0,0 +1,70 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using NzbDrone.Common; +using NzbDrone.Core.Datastore.Events; +using NzbDrone.Core.Messaging.Commands; +using NzbDrone.Core.Messaging.Events; +using NzbDrone.Core.ProgressMessaging; +using NzbDrone.SignalR; +using Lidarr.Http; +using Lidarr.Http.Extensions; +using Lidarr.Http.Validation; + +namespace Lidarr.Api.V3.Commands +{ + public class CommandModule : LidarrRestModuleWithSignalR, IHandle + { + private readonly IManageCommandQueue _commandQueueManager; + private readonly IServiceFactory _serviceFactory; + + public CommandModule(IManageCommandQueue commandQueueManager, + IBroadcastSignalRMessage signalRBroadcaster, + IServiceFactory serviceFactory) + : base(signalRBroadcaster) + { + _commandQueueManager = commandQueueManager; + _serviceFactory = serviceFactory; + + GetResourceById = GetCommand; + CreateResource = StartCommand; + GetResourceAll = GetStartedCommands; + + PostValidator.RuleFor(c => c.Name).NotBlank(); + } + + private CommandResource GetCommand(int id) + { + return _commandQueueManager.Get(id).ToResource(); + } + + private int StartCommand(CommandResource commandResource) + { + var commandType = + _serviceFactory.GetImplementations(typeof (Command)) + .Single(c => c.Name.Replace("Command", "") + .Equals(commandResource.Name, StringComparison.InvariantCultureIgnoreCase)); + + dynamic command = Request.Body.FromJson(commandType); + command.Trigger = CommandTrigger.Manual; + command.SuppressMessages = !command.SendUpdatesToClient; + command.SendUpdatesToClient = true; + + var trackedCommand = _commandQueueManager.Push(command, CommandPriority.Normal, CommandTrigger.Manual); + return trackedCommand.Id; + } + + private List GetStartedCommands() + { + return _commandQueueManager.GetStarted().ToResource(); + } + + public void Handle(CommandUpdatedEvent message) + { + if (message.Command.Body.SendUpdatesToClient) + { + BroadcastResourceChange(ModelAction.Updated, message.Command.ToResource()); + } + } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Api/Commands/CommandResource.cs b/src/Lidarr.Api.V3/Commands/CommandResource.cs similarity index 91% rename from src/NzbDrone.Api/Commands/CommandResource.cs rename to src/Lidarr.Api.V3/Commands/CommandResource.cs index cf09f12ac..743db720c 100644 --- a/src/NzbDrone.Api/Commands/CommandResource.cs +++ b/src/Lidarr.Api.V3/Commands/CommandResource.cs @@ -2,16 +2,16 @@ using System.Collections.Generic; using System.Linq; using Newtonsoft.Json; -using NzbDrone.Api.REST; using NzbDrone.Core.Messaging.Commands; +using Lidarr.Http.REST; -namespace NzbDrone.Api.Commands +namespace Lidarr.Api.V3.Commands { public class CommandResource : RestResource { public string Name { get; set; } public string Message { get; set; } - public object Body { get; set; } + public Command Body { get; set; } public CommandPriority Priority { get; set; } public CommandStatus Status { get; set; } public DateTime Queued { get; set; } @@ -72,7 +72,7 @@ namespace NzbDrone.Api.Commands { get { - if (Body != null) return (Body as Command).SendUpdatesToClient; + if (Body != null) return Body.SendUpdatesToClient; return false; } @@ -84,7 +84,7 @@ namespace NzbDrone.Api.Commands { get { - if (Body != null) return (Body as Command).UpdateScheduledTask; + if (Body != null) return Body.UpdateScheduledTask; return false; } diff --git a/src/Lidarr.Api.V3/Config/DownloadClientConfigModule.cs b/src/Lidarr.Api.V3/Config/DownloadClientConfigModule.cs new file mode 100644 index 000000000..262b378c7 --- /dev/null +++ b/src/Lidarr.Api.V3/Config/DownloadClientConfigModule.cs @@ -0,0 +1,17 @@ +using NzbDrone.Core.Configuration; + +namespace Lidarr.Api.V3.Config +{ + public class DownloadClientConfigModule : SonarrConfigModule + { + public DownloadClientConfigModule(IConfigService configService) + : base(configService) + { + } + + protected override DownloadClientConfigResource ToResource(IConfigService model) + { + return DownloadClientConfigResourceMapper.ToResource(model); + } + } +} \ No newline at end of file diff --git a/src/Lidarr.Api.V3/Config/DownloadClientConfigResource.cs b/src/Lidarr.Api.V3/Config/DownloadClientConfigResource.cs new file mode 100644 index 000000000..49340c01b --- /dev/null +++ b/src/Lidarr.Api.V3/Config/DownloadClientConfigResource.cs @@ -0,0 +1,33 @@ +using NzbDrone.Core.Configuration; +using Lidarr.Http.REST; + +namespace Lidarr.Api.V3.Config +{ + public class DownloadClientConfigResource : RestResource + { + public string DownloadClientWorkingFolders { get; set; } + + public bool EnableCompletedDownloadHandling { get; set; } + public bool RemoveCompletedDownloads { get; set; } + + public bool AutoRedownloadFailed { get; set; } + public bool RemoveFailedDownloads { get; set; } + } + + public static class DownloadClientConfigResourceMapper + { + public static DownloadClientConfigResource ToResource(IConfigService model) + { + return new DownloadClientConfigResource + { + DownloadClientWorkingFolders = model.DownloadClientWorkingFolders, + + EnableCompletedDownloadHandling = model.EnableCompletedDownloadHandling, + RemoveCompletedDownloads = model.RemoveCompletedDownloads, + + AutoRedownloadFailed = model.AutoRedownloadFailed, + RemoveFailedDownloads = model.RemoveFailedDownloads + }; + } + } +} diff --git a/src/NzbDrone.Api/Config/HostConfigModule.cs b/src/Lidarr.Api.V3/Config/HostConfigModule.cs similarity index 95% rename from src/NzbDrone.Api/Config/HostConfigModule.cs rename to src/Lidarr.Api.V3/Config/HostConfigModule.cs index 367bf770d..081ef9f5c 100644 --- a/src/NzbDrone.Api/Config/HostConfigModule.cs +++ b/src/Lidarr.Api.V3/Config/HostConfigModule.cs @@ -8,10 +8,11 @@ using NzbDrone.Core.Configuration; using NzbDrone.Core.Update; using NzbDrone.Core.Validation; using NzbDrone.Core.Validation.Paths; +using Lidarr.Http; -namespace NzbDrone.Api.Config +namespace Lidarr.Api.V3.Config { - public class HostConfigModule : NzbDroneRestModule + public class HostConfigModule : LidarrRestModule { private readonly IConfigFileProvider _configFileProvider; private readonly IConfigService _configService; @@ -74,7 +75,6 @@ namespace NzbDrone.Api.Config .ToDictionary(prop => prop.Name, prop => prop.GetValue(resource, null)); _configFileProvider.SaveConfigDictionary(dictionary); - _configService.SaveConfigDictionary(dictionary); if (resource.Username.IsNotNullOrWhiteSpace() && resource.Password.IsNotNullOrWhiteSpace()) { diff --git a/src/NzbDrone.Api/Config/HostConfigResource.cs b/src/Lidarr.Api.V3/Config/HostConfigResource.cs similarity index 97% rename from src/NzbDrone.Api/Config/HostConfigResource.cs rename to src/Lidarr.Api.V3/Config/HostConfigResource.cs index 930e0301c..8d45f2a23 100644 --- a/src/NzbDrone.Api/Config/HostConfigResource.cs +++ b/src/Lidarr.Api.V3/Config/HostConfigResource.cs @@ -1,10 +1,10 @@ -using NzbDrone.Api.REST; +using NzbDrone.Common.Http.Proxy; using NzbDrone.Core.Authentication; using NzbDrone.Core.Configuration; using NzbDrone.Core.Update; -using NzbDrone.Common.Http.Proxy; +using Lidarr.Http.REST; -namespace NzbDrone.Api.Config +namespace Lidarr.Api.V3.Config { public class HostConfigResource : RestResource { diff --git a/src/NzbDrone.Api/Config/IndexerConfigModule.cs b/src/Lidarr.Api.V3/Config/IndexerConfigModule.cs similarity index 82% rename from src/NzbDrone.Api/Config/IndexerConfigModule.cs rename to src/Lidarr.Api.V3/Config/IndexerConfigModule.cs index 73c2442b8..b7a066539 100644 --- a/src/NzbDrone.Api/Config/IndexerConfigModule.cs +++ b/src/Lidarr.Api.V3/Config/IndexerConfigModule.cs @@ -1,10 +1,10 @@ using FluentValidation; -using NzbDrone.Api.Validation; using NzbDrone.Core.Configuration; +using Lidarr.Http.Validation; -namespace NzbDrone.Api.Config +namespace Lidarr.Api.V3.Config { - public class IndexerConfigModule : NzbDroneConfigModule + public class IndexerConfigModule : SonarrConfigModule { public IndexerConfigModule(IConfigService configService) diff --git a/src/NzbDrone.Api/Config/IndexerConfigResource.cs b/src/Lidarr.Api.V3/Config/IndexerConfigResource.cs similarity index 86% rename from src/NzbDrone.Api/Config/IndexerConfigResource.cs rename to src/Lidarr.Api.V3/Config/IndexerConfigResource.cs index 179e28c3f..ffeb6d76d 100644 --- a/src/NzbDrone.Api/Config/IndexerConfigResource.cs +++ b/src/Lidarr.Api.V3/Config/IndexerConfigResource.cs @@ -1,7 +1,7 @@ -using NzbDrone.Api.REST; -using NzbDrone.Core.Configuration; +using NzbDrone.Core.Configuration; +using Lidarr.Http.REST; -namespace NzbDrone.Api.Config +namespace Lidarr.Api.V3.Config { public class IndexerConfigResource : RestResource { diff --git a/src/NzbDrone.Api/Config/MediaManagementConfigModule.cs b/src/Lidarr.Api.V3/Config/MediaManagementConfigModule.cs similarity index 85% rename from src/NzbDrone.Api/Config/MediaManagementConfigModule.cs rename to src/Lidarr.Api.V3/Config/MediaManagementConfigModule.cs index 8b35e53ed..dbf8ff9a4 100644 --- a/src/NzbDrone.Api/Config/MediaManagementConfigModule.cs +++ b/src/Lidarr.Api.V3/Config/MediaManagementConfigModule.cs @@ -2,9 +2,9 @@ using NzbDrone.Core.Configuration; using NzbDrone.Core.Validation.Paths; -namespace NzbDrone.Api.Config +namespace Lidarr.Api.V3.Config { - public class MediaManagementConfigModule : NzbDroneConfigModule + public class MediaManagementConfigModule : SonarrConfigModule { public MediaManagementConfigModule(IConfigService configService, PathExistsValidator pathExistsValidator) : base(configService) diff --git a/src/NzbDrone.Api/Config/MediaManagementConfigResource.cs b/src/Lidarr.Api.V3/Config/MediaManagementConfigResource.cs similarity index 97% rename from src/NzbDrone.Api/Config/MediaManagementConfigResource.cs rename to src/Lidarr.Api.V3/Config/MediaManagementConfigResource.cs index b2a7c9b65..857e8baa0 100644 --- a/src/NzbDrone.Api/Config/MediaManagementConfigResource.cs +++ b/src/Lidarr.Api.V3/Config/MediaManagementConfigResource.cs @@ -1,8 +1,8 @@ -using NzbDrone.Api.REST; using NzbDrone.Core.Configuration; using NzbDrone.Core.MediaFiles; +using Lidarr.Http.REST; -namespace NzbDrone.Api.Config +namespace Lidarr.Api.V3.Config { public class MediaManagementConfigResource : RestResource { diff --git a/src/Lidarr.Api.V3/Config/MetadataProviderConfigModule.cs b/src/Lidarr.Api.V3/Config/MetadataProviderConfigModule.cs new file mode 100644 index 000000000..9292c4842 --- /dev/null +++ b/src/Lidarr.Api.V3/Config/MetadataProviderConfigModule.cs @@ -0,0 +1,22 @@ +using System.Linq; +using System.Reflection; +using NzbDrone.Core.Configuration; +using Lidarr.Http; +using NzbDrone.Core.Validation; + +namespace Lidarr.Api.V3.Config +{ + public class MetadataProviderConfigModule : SonarrConfigModule + { + public MetadataProviderConfigModule(IConfigService configService) + : base(configService) + { + SharedValidator.RuleFor(c => c.MetadataSource).IsValidUrl(); + } + + protected override MetadataProviderConfigResource ToResource(IConfigService model) + { + return MetadataProviderConfigResourceMapper.ToResource(model); + } + } +} diff --git a/src/Lidarr.Api.V3/Config/MetadataProviderConfigResource.cs b/src/Lidarr.Api.V3/Config/MetadataProviderConfigResource.cs new file mode 100644 index 000000000..f14ff6ce8 --- /dev/null +++ b/src/Lidarr.Api.V3/Config/MetadataProviderConfigResource.cs @@ -0,0 +1,24 @@ +using NzbDrone.Core.Configuration; +using Lidarr.Http.REST; + +namespace Lidarr.Api.V3.Config +{ + public class MetadataProviderConfigResource : RestResource + { + //Calendar + public string MetadataSource { get; set; } + + } + + public static class MetadataProviderConfigResourceMapper + { + public static MetadataProviderConfigResource ToResource(IConfigService model) + { + return new MetadataProviderConfigResource + { + MetadataSource = model.MetadataSource, + + }; + } + } +} diff --git a/src/NzbDrone.Api/Config/NamingConfigModule.cs b/src/Lidarr.Api.V3/Config/NamingConfigModule.cs similarity index 85% rename from src/NzbDrone.Api/Config/NamingConfigModule.cs rename to src/Lidarr.Api.V3/Config/NamingConfigModule.cs index abe7096c9..4484f0455 100644 --- a/src/NzbDrone.Api/Config/NamingConfigModule.cs +++ b/src/Lidarr.Api.V3/Config/NamingConfigModule.cs @@ -1,16 +1,18 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using FluentValidation; using FluentValidation.Results; +using Nancy.ModelBinding; using Nancy.Responses; using NzbDrone.Common.Extensions; using NzbDrone.Core.Organizer; -using Nancy.ModelBinding; -using NzbDrone.Api.Extensions; +using Lidarr.Http; +using Lidarr.Http.Extensions; +using Lidarr.Http.Mapping; -namespace NzbDrone.Api.Config +namespace Lidarr.Api.V3.Config { - public class NamingConfigModule : NzbDroneRestModule + public class NamingConfigModule : LidarrRestModule { private readonly INamingConfigService _namingConfigService; private readonly IFilenameSampleService _filenameSampleService; @@ -31,9 +33,9 @@ namespace NzbDrone.Api.Config GetResourceById = GetNamingConfig; UpdateResource = UpdateNamingConfig; - Get["/samples"] = x => GetExamples(this.Bind()); + Get["/examples"] = x => GetExamples(this.Bind()); + - SharedValidator.RuleFor(c => c.MultiEpisodeStyle).InclusiveBetween(0, 5); SharedValidator.RuleFor(c => c.StandardTrackFormat).ValidTrackFormat(); SharedValidator.RuleFor(c => c.ArtistFolderFormat).ValidArtistFolderFormat(); SharedValidator.RuleFor(c => c.AlbumFolderFormat).ValidAlbumFolderFormat(); @@ -66,24 +68,28 @@ namespace NzbDrone.Api.Config return GetNamingConfig(); } - private JsonResponse GetExamples(NamingConfigResource config) + private JsonResponse GetExamples(NamingConfigResource config) { + if (config.Id == 0) + { + config = GetNamingConfig(); + } + var nameSpec = config.ToModel(); - var sampleResource = new NamingSampleResource(); + var sampleResource = new NamingExampleResource(); - var singleTrackSampleResult = _filenameSampleService.GetStandardTrackSample(nameSpec); sampleResource.SingleTrackExample = _filenameValidationService.ValidateTrackFilename(singleTrackSampleResult) != null - ? "Invalid format" + ? null : singleTrackSampleResult.FileName; sampleResource.ArtistFolderExample = nameSpec.ArtistFolderFormat.IsNullOrWhiteSpace() - ? "Invalid format" + ? null : _filenameSampleService.GetArtistFolderSample(nameSpec); sampleResource.AlbumFolderExample = nameSpec.AlbumFolderFormat.IsNullOrWhiteSpace() - ? "Invalid format" + ? null : _filenameSampleService.GetAlbumFolderSample(nameSpec); return sampleResource.AsResponse(); @@ -91,16 +97,14 @@ namespace NzbDrone.Api.Config private void ValidateFormatResult(NamingConfig nameSpec) { - var singleTrackSampleResult = _filenameSampleService.GetStandardTrackSample(nameSpec); - + var singleTrackValidationResult = _filenameValidationService.ValidateTrackFilename(singleTrackSampleResult); var validationFailures = new List(); validationFailures.AddIfNotNull(singleTrackValidationResult); - if (validationFailures.Any()) { throw new ValidationException(validationFailures.DistinctBy(v => v.PropertyName).ToArray()); diff --git a/src/Lidarr.Api.V3/Config/NamingConfigResource.cs b/src/Lidarr.Api.V3/Config/NamingConfigResource.cs new file mode 100644 index 000000000..f081d65a6 --- /dev/null +++ b/src/Lidarr.Api.V3/Config/NamingConfigResource.cs @@ -0,0 +1,19 @@ +using Lidarr.Http.REST; + +namespace Lidarr.Api.V3.Config +{ + public class NamingConfigResource : RestResource + { + public bool RenameTracks { get; set; } + public bool ReplaceIllegalCharacters { get; set; } + public string StandardTrackFormat { get; set; } + public string ArtistFolderFormat { get; set; } + public string AlbumFolderFormat { get; set; } + public bool IncludeArtistName { get; set; } + public bool IncludeAlbumTitle { get; set; } + public bool IncludeQuality { get; set; } + public bool ReplaceSpaces { get; set; } + public string Separator { get; set; } + public string NumberStyle { get; set; } + } +} diff --git a/src/Lidarr.Api.V3/Config/NamingExampleResource.cs b/src/Lidarr.Api.V3/Config/NamingExampleResource.cs new file mode 100644 index 000000000..d516dc8e1 --- /dev/null +++ b/src/Lidarr.Api.V3/Config/NamingExampleResource.cs @@ -0,0 +1,59 @@ +using NzbDrone.Core.Organizer; + +namespace Lidarr.Api.V3.Config +{ + public class NamingExampleResource + { + public string SingleTrackExample { get; set; } + public string ArtistFolderExample { get; set; } + public string AlbumFolderExample { get; set; } + } + + public static class NamingConfigResourceMapper + { + public static NamingConfigResource ToResource(this NamingConfig model) + { + return new NamingConfigResource + { + Id = model.Id, + + RenameTracks = model.RenameTracks, + ReplaceIllegalCharacters = model.ReplaceIllegalCharacters, + StandardTrackFormat = model.StandardTrackFormat, + ArtistFolderFormat = model.ArtistFolderFormat, + AlbumFolderFormat = model.AlbumFolderFormat + //IncludeSeriesTitle + //IncludeEpisodeTitle + //IncludeQuality + //ReplaceSpaces + //Separator + //NumberStyle + }; + } + + public static void AddToResource(this BasicNamingConfig basicNamingConfig, NamingConfigResource resource) + { + resource.IncludeArtistName = basicNamingConfig.IncludeArtistName; + resource.IncludeAlbumTitle = basicNamingConfig.IncludeAlbumTitle; + resource.IncludeQuality = basicNamingConfig.IncludeQuality; + resource.ReplaceSpaces = basicNamingConfig.ReplaceSpaces; + resource.Separator = basicNamingConfig.Separator; + resource.NumberStyle = basicNamingConfig.NumberStyle; + } + + public static NamingConfig ToModel(this NamingConfigResource resource) + { + return new NamingConfig + { + Id = resource.Id, + + RenameTracks = resource.RenameTracks, + ReplaceIllegalCharacters = resource.ReplaceIllegalCharacters, + StandardTrackFormat = resource.StandardTrackFormat, + + ArtistFolderFormat = resource.ArtistFolderFormat, + AlbumFolderFormat = resource.AlbumFolderFormat + }; + } + } +} diff --git a/src/Lidarr.Api.V3/Config/SonarrConfigModule.cs b/src/Lidarr.Api.V3/Config/SonarrConfigModule.cs new file mode 100644 index 000000000..b294ef7bb --- /dev/null +++ b/src/Lidarr.Api.V3/Config/SonarrConfigModule.cs @@ -0,0 +1,52 @@ +using System.Linq; +using System.Reflection; +using NzbDrone.Core.Configuration; +using Lidarr.Http; +using Lidarr.Http.REST; + +namespace Lidarr.Api.V3.Config +{ + public abstract class SonarrConfigModule : LidarrRestModule where TResource : RestResource, new() + { + private readonly IConfigService _configService; + + protected SonarrConfigModule(IConfigService configService) + : this(new TResource().ResourceName.Replace("config", ""), configService) + { + } + + protected SonarrConfigModule(string resource, IConfigService configService) : + base("config/" + resource.Trim('/')) + { + _configService = configService; + + GetResourceSingle = GetConfig; + GetResourceById = GetConfig; + UpdateResource = SaveConfig; + } + + private TResource GetConfig() + { + var resource = ToResource(_configService); + resource.Id = 1; + + return resource; + } + + protected abstract TResource ToResource(IConfigService model); + + private TResource GetConfig(int id) + { + return GetConfig(); + } + + private void SaveConfig(TResource resource) + { + var dictionary = resource.GetType() + .GetProperties(BindingFlags.Instance | BindingFlags.Public) + .ToDictionary(prop => prop.Name, prop => prop.GetValue(resource, null)); + + _configService.SaveConfigDictionary(dictionary); + } + } +} diff --git a/src/Lidarr.Api.V3/Config/UiConfigModule.cs b/src/Lidarr.Api.V3/Config/UiConfigModule.cs new file mode 100644 index 000000000..2bc1fd4f9 --- /dev/null +++ b/src/Lidarr.Api.V3/Config/UiConfigModule.cs @@ -0,0 +1,21 @@ +using System.Linq; +using System.Reflection; +using NzbDrone.Core.Configuration; +using Lidarr.Http; + +namespace Lidarr.Api.V3.Config +{ + public class UiConfigModule : SonarrConfigModule + { + public UiConfigModule(IConfigService configService) + : base(configService) + { + + } + + protected override UiConfigResource ToResource(IConfigService model) + { + return UiConfigResourceMapper.ToResource(model); + } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Api/Config/UiConfigResource.cs b/src/Lidarr.Api.V3/Config/UiConfigResource.cs similarity index 91% rename from src/NzbDrone.Api/Config/UiConfigResource.cs rename to src/Lidarr.Api.V3/Config/UiConfigResource.cs index 7c7d27b67..28ed7ce28 100644 --- a/src/NzbDrone.Api/Config/UiConfigResource.cs +++ b/src/Lidarr.Api.V3/Config/UiConfigResource.cs @@ -1,7 +1,7 @@ -using NzbDrone.Api.REST; -using NzbDrone.Core.Configuration; +using NzbDrone.Core.Configuration; +using Lidarr.Http.REST; -namespace NzbDrone.Api.Config +namespace Lidarr.Api.V3.Config { public class UiConfigResource : RestResource { @@ -26,7 +26,7 @@ namespace NzbDrone.Api.Config { FirstDayOfWeek = model.FirstDayOfWeek, CalendarWeekColumnHeader = model.CalendarWeekColumnHeader, - + ShortDateFormat = model.ShortDateFormat, LongDateFormat = model.LongDateFormat, TimeFormat = model.TimeFormat, diff --git a/src/NzbDrone.Api/DiskSpace/DiskSpaceModule.cs b/src/Lidarr.Api.V3/DiskSpace/DiskSpaceModule.cs similarity index 76% rename from src/NzbDrone.Api/DiskSpace/DiskSpaceModule.cs rename to src/Lidarr.Api.V3/DiskSpace/DiskSpaceModule.cs index f6d8354b4..15806fb59 100644 --- a/src/NzbDrone.Api/DiskSpace/DiskSpaceModule.cs +++ b/src/Lidarr.Api.V3/DiskSpace/DiskSpaceModule.cs @@ -1,20 +1,20 @@ using System.Collections.Generic; using NzbDrone.Core.DiskSpace; +using Lidarr.Http; -namespace NzbDrone.Api.DiskSpace +namespace Lidarr.Api.V3.DiskSpace { - public class DiskSpaceModule :NzbDroneRestModule + public class DiskSpaceModule :LidarrRestModule { private readonly IDiskSpaceService _diskSpaceService; public DiskSpaceModule(IDiskSpaceService diskSpaceService) - : base("diskspace") + :base("diskspace") { _diskSpaceService = diskSpaceService; GetResourceAll = GetFreeSpace; } - public List GetFreeSpace() { return _diskSpaceService.GetFreeSpace().ConvertAll(DiskSpaceResourceMapper.MapToResource); diff --git a/src/NzbDrone.Api/DiskSpace/DiskSpaceResource.cs b/src/Lidarr.Api.V3/DiskSpace/DiskSpaceResource.cs similarity index 78% rename from src/NzbDrone.Api/DiskSpace/DiskSpaceResource.cs rename to src/Lidarr.Api.V3/DiskSpace/DiskSpaceResource.cs index fc36f9d5c..fc236f1c7 100644 --- a/src/NzbDrone.Api/DiskSpace/DiskSpaceResource.cs +++ b/src/Lidarr.Api.V3/DiskSpace/DiskSpaceResource.cs @@ -1,6 +1,6 @@ -using NzbDrone.Api.REST; +using Lidarr.Http.REST; -namespace NzbDrone.Api.DiskSpace +namespace Lidarr.Api.V3.DiskSpace { public class DiskSpaceResource : RestResource { @@ -12,7 +12,7 @@ namespace NzbDrone.Api.DiskSpace public static class DiskSpaceResourceMapper { - public static DiskSpaceResource MapToResource(this Core.DiskSpace.DiskSpace model) + public static DiskSpaceResource MapToResource(this NzbDrone.Core.DiskSpace.DiskSpace model) { if (model == null) return null; diff --git a/src/Lidarr.Api.V3/DownloadClient/DownloadClientModule.cs b/src/Lidarr.Api.V3/DownloadClient/DownloadClientModule.cs new file mode 100644 index 000000000..af12eb21d --- /dev/null +++ b/src/Lidarr.Api.V3/DownloadClient/DownloadClientModule.cs @@ -0,0 +1,20 @@ +using NzbDrone.Core.Download; + +namespace Lidarr.Api.V3.DownloadClient +{ + public class DownloadClientModule : ProviderModuleBase + { + public static readonly DownloadClientResourceMapper ResourceMapper = new DownloadClientResourceMapper(); + + public DownloadClientModule(IDownloadClientFactory downloadClientFactory) + : base(downloadClientFactory, "downloadclient", ResourceMapper) + { + } + + protected override void Validate(DownloadClientDefinition definition, bool includeWarnings) + { + if (!definition.Enable) return; + base.Validate(definition, includeWarnings); + } + } +} \ No newline at end of file diff --git a/src/Lidarr.Api.V3/DownloadClient/DownloadClientResource.cs b/src/Lidarr.Api.V3/DownloadClient/DownloadClientResource.cs new file mode 100644 index 000000000..6205f8813 --- /dev/null +++ b/src/Lidarr.Api.V3/DownloadClient/DownloadClientResource.cs @@ -0,0 +1,38 @@ +using NzbDrone.Core.Download; +using NzbDrone.Core.Indexers; + +namespace Lidarr.Api.V3.DownloadClient +{ + public class DownloadClientResource : ProviderResource + { + public bool Enable { get; set; } + public DownloadProtocol Protocol { get; set; } + } + + public class DownloadClientResourceMapper : ProviderResourceMapper + { + public override DownloadClientResource ToResource(DownloadClientDefinition definition) + { + if (definition == null) return null; + + var resource = base.ToResource(definition); + + resource.Enable = definition.Enable; + resource.Protocol = definition.Protocol; + + return resource; + } + + public override DownloadClientDefinition ToModel(DownloadClientResource resource) + { + if (resource == null) return null; + + var definition = base.ToModel(resource); + + definition.Enable = resource.Enable; + definition.Protocol = resource.Protocol; + + return definition; + } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Api/FileSystem/FileSystemModule.cs b/src/Lidarr.Api.V3/FileSystem/FileSystemModule.cs similarity index 84% rename from src/NzbDrone.Api/FileSystem/FileSystemModule.cs rename to src/Lidarr.Api.V3/FileSystem/FileSystemModule.cs index 392c3c0c5..f6a9bb24a 100644 --- a/src/NzbDrone.Api/FileSystem/FileSystemModule.cs +++ b/src/Lidarr.Api.V3/FileSystem/FileSystemModule.cs @@ -1,15 +1,15 @@ -using System; +using System; using System.IO; using System.Linq; using Nancy; -using NzbDrone.Api.Extensions; using NzbDrone.Common.Disk; using NzbDrone.Common.Extensions; using NzbDrone.Core.MediaFiles; +using Lidarr.Http.Extensions; -namespace NzbDrone.Api.FileSystem +namespace Lidarr.Api.V3.FileSystem { - public class FileSystemModule : NzbDroneApiModule + public class FileSystemModule : SonarrV3Module { private readonly IFileSystemLookupService _fileSystemLookupService; private readonly IDiskProvider _diskProvider; @@ -31,13 +31,8 @@ namespace NzbDrone.Api.FileSystem private Response GetContents() { var pathQuery = Request.Query.path; - var includeFilesQuery = Request.Query.includeFiles; - bool includeFiles = false; + var includeFiles = Request.GetBooleanQueryParameter("includeFiles"); - if (includeFilesQuery.HasValue) - { - includeFiles = Convert.ToBoolean(includeFilesQuery.Value); - } return _fileSystemLookupService.LookupContents((string)pathQuery.Value, includeFiles).AsResponse(); } @@ -73,4 +68,4 @@ namespace NzbDrone.Api.FileSystem }).AsResponse(); } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Api/Health/HealthModule.cs b/src/Lidarr.Api.V3/Health/HealthModule.cs similarity index 85% rename from src/NzbDrone.Api/Health/HealthModule.cs rename to src/Lidarr.Api.V3/Health/HealthModule.cs index 2699fa7d6..d299b40f7 100644 --- a/src/NzbDrone.Api/Health/HealthModule.cs +++ b/src/Lidarr.Api.V3/Health/HealthModule.cs @@ -3,10 +3,11 @@ using NzbDrone.Core.Datastore.Events; using NzbDrone.Core.HealthCheck; using NzbDrone.Core.Messaging.Events; using NzbDrone.SignalR; +using Lidarr.Http; -namespace NzbDrone.Api.Health +namespace Lidarr.Api.V3.Health { - public class HealthModule : NzbDroneRestModuleWithSignalR, + public class HealthModule : LidarrRestModuleWithSignalR, IHandle { private readonly IHealthCheckService _healthCheckService; diff --git a/src/NzbDrone.Api/Health/HealthResource.cs b/src/Lidarr.Api.V3/Health/HealthResource.cs similarity index 86% rename from src/NzbDrone.Api/Health/HealthResource.cs rename to src/Lidarr.Api.V3/Health/HealthResource.cs index e860cb778..feebcb230 100644 --- a/src/NzbDrone.Api/Health/HealthResource.cs +++ b/src/Lidarr.Api.V3/Health/HealthResource.cs @@ -1,13 +1,14 @@ using System.Collections.Generic; using System.Linq; -using NzbDrone.Api.REST; using NzbDrone.Common.Http; using NzbDrone.Core.HealthCheck; +using Lidarr.Http.REST; -namespace NzbDrone.Api.Health +namespace Lidarr.Api.V3.Health { public class HealthResource : RestResource { + public string Source { get; set; } public HealthCheckResult Type { get; set; } public string Message { get; set; } public HttpUri WikiUrl { get; set; } @@ -22,7 +23,7 @@ namespace NzbDrone.Api.Health return new HealthResource { Id = model.Id, - + Source = model.Source.Name, Type = model.Type, Message = model.Message, WikiUrl = model.WikiUrl diff --git a/src/Lidarr.Api.V3/History/HistoryModule.cs b/src/Lidarr.Api.V3/History/HistoryModule.cs new file mode 100644 index 000000000..9b1c73799 --- /dev/null +++ b/src/Lidarr.Api.V3/History/HistoryModule.cs @@ -0,0 +1,91 @@ +using System; +using Nancy; +using NzbDrone.Core.Datastore; +using NzbDrone.Core.DecisionEngine; +using NzbDrone.Core.Download; +using NzbDrone.Core.History; +using Lidarr.Api.V3.Albums; +using Lidarr.Api.V3.Artist; +using Lidarr.Api.V3.Tracks; +using Lidarr.Http; +using Lidarr.Http.Extensions; + +namespace Lidarr.Api.V3.History +{ + public class HistoryModule : LidarrRestModule + { + private readonly IHistoryService _historyService; + private readonly IUpgradableSpecification _upgradableSpecification; + private readonly IFailedDownloadService _failedDownloadService; + + public HistoryModule(IHistoryService historyService, + IUpgradableSpecification upgradableSpecification, + IFailedDownloadService failedDownloadService) + { + _historyService = historyService; + _upgradableSpecification = upgradableSpecification; + _failedDownloadService = failedDownloadService; + GetResourcePaged = GetHistory; + + Post["/failed"] = x => MarkAsFailed(); + } + + protected HistoryResource MapToResource(NzbDrone.Core.History.History model, bool includeArtist, bool includeAlbum, bool includeTrack) + { + var resource = model.ToResource(); + + if (includeArtist) + { + resource.Artist = model.Artist.ToResource(); + } + if (includeAlbum) + { + resource.Album = model.Album.ToResource(); + } + if (includeTrack) + { + resource.Track = model.Track.ToResource(); + } + + + if (model.Artist != null) + { + resource.QualityCutoffNotMet = _upgradableSpecification.CutoffNotMet(model.Artist.Profile.Value, + model.Artist.LanguageProfile, + model.Quality, + model.Language); + } + + return resource; + } + + private PagingResource GetHistory(PagingResource pagingResource) + { + var pagingSpec = pagingResource.MapToPagingSpec("date", SortDirection.Descending); + var includeArtist = Request.GetBooleanQueryParameter("includeArtist"); + var includeAlbum = Request.GetBooleanQueryParameter("includeAlbum"); + var includeTrack = Request.GetBooleanQueryParameter("includeTrack"); + + if (pagingResource.FilterKey == "eventType") + { + var filterValue = (HistoryEventType)Convert.ToInt32(pagingResource.FilterValue); + pagingSpec.FilterExpression = v => v.EventType == filterValue; + } + + if (pagingResource.FilterKey == "albumId") + { + int albumId = Convert.ToInt32(pagingResource.FilterValue); + pagingSpec.FilterExpression = h => h.AlbumId == albumId; + } + + return ApplyToPage(_historyService.Paged, pagingSpec, h => MapToResource(h, includeArtist, includeAlbum, includeTrack)); + } + + private Response MarkAsFailed() + { + var id = (int)Request.Form.Id; + _failedDownloadService.MarkAsFailed(id); + return new object().AsResponse(); + } + } +} diff --git a/src/Lidarr.Api.V3/History/HistoryResource.cs b/src/Lidarr.Api.V3/History/HistoryResource.cs new file mode 100644 index 000000000..86d8a9964 --- /dev/null +++ b/src/Lidarr.Api.V3/History/HistoryResource.cs @@ -0,0 +1,62 @@ +using System; +using System.Collections.Generic; +using NzbDrone.Core.History; +using NzbDrone.Core.Languages; +using NzbDrone.Core.Qualities; +using Lidarr.Api.V3.Albums; +using Lidarr.Api.V3.Artist; +using Lidarr.Api.V3.Tracks; +using Lidarr.Http.REST; + +namespace Lidarr.Api.V3.History +{ + public class HistoryResource : RestResource + { + public int AlbumId { get; set; } + public int ArtistId { get; set; } + public int TrackId { get; set; } + public string SourceTitle { get; set; } + public Language Language { get; set; } + public QualityModel Quality { get; set; } + public bool QualityCutoffNotMet { get; set; } + public DateTime Date { get; set; } + public string DownloadId { get; set; } + + public HistoryEventType EventType { get; set; } + + public Dictionary Data { get; set; } + + public AlbumResource Album { get; set; } + public ArtistResource Artist { get; set; } + public TrackResource Track { get; set; } + } + + public static class HistoryResourceMapper + { + public static HistoryResource ToResource(this NzbDrone.Core.History.History model) + { + if (model == null) return null; + + return new HistoryResource + { + Id = model.Id, + + AlbumId = model.AlbumId, + ArtistId = model.ArtistId, + TrackId = model.TrackId, + SourceTitle = model.SourceTitle, + Language = model.Language, + Quality = model.Quality, + //QualityCutoffNotMet + Date = model.Date, + DownloadId = model.DownloadId, + + EventType = model.EventType, + + Data = model.Data + //Episode + //Series + }; + } + } +} diff --git a/src/Lidarr.Api.V3/Indexers/IndexerModule.cs b/src/Lidarr.Api.V3/Indexers/IndexerModule.cs new file mode 100644 index 000000000..c3451b61b --- /dev/null +++ b/src/Lidarr.Api.V3/Indexers/IndexerModule.cs @@ -0,0 +1,20 @@ +using NzbDrone.Core.Indexers; + +namespace Lidarr.Api.V3.Indexers +{ + public class IndexerModule : ProviderModuleBase + { + public static readonly IndexerResourceMapper ResourceMapper = new IndexerResourceMapper(); + + public IndexerModule(IndexerFactory indexerFactory) + : base(indexerFactory, "indexer", ResourceMapper) + { + } + + protected override void Validate(IndexerDefinition definition, bool includeWarnings) + { + if (!definition.Enable) return; + base.Validate(definition, includeWarnings); + } + } +} \ No newline at end of file diff --git a/src/Lidarr.Api.V3/Indexers/IndexerResource.cs b/src/Lidarr.Api.V3/Indexers/IndexerResource.cs new file mode 100644 index 000000000..cfe23ea61 --- /dev/null +++ b/src/Lidarr.Api.V3/Indexers/IndexerResource.cs @@ -0,0 +1,43 @@ +using NzbDrone.Core.Indexers; + +namespace Lidarr.Api.V3.Indexers +{ + public class IndexerResource : ProviderResource + { + public bool EnableRss { get; set; } + public bool EnableSearch { get; set; } + public bool SupportsRss { get; set; } + public bool SupportsSearch { get; set; } + public DownloadProtocol Protocol { get; set; } + } + + public class IndexerResourceMapper : ProviderResourceMapper + { + public override IndexerResource ToResource(IndexerDefinition definition) + { + if (definition == null) return null; + + var resource = base.ToResource(definition); + + resource.EnableRss = definition.EnableRss; + resource.EnableSearch = definition.EnableSearch; + resource.SupportsRss = definition.SupportsRss; + resource.SupportsSearch = definition.SupportsSearch; + resource.Protocol = definition.Protocol; + + return resource; + } + + public override IndexerDefinition ToModel(IndexerResource resource) + { + if (resource == null) return null; + + var definition = base.ToModel(resource); + + definition.EnableRss = resource.EnableRss; + definition.EnableSearch = resource.EnableSearch; + + return definition; + } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Api/Indexers/ReleaseModule.cs b/src/Lidarr.Api.V3/Indexers/ReleaseModule.cs similarity index 82% rename from src/NzbDrone.Api/Indexers/ReleaseModule.cs rename to src/Lidarr.Api.V3/Indexers/ReleaseModule.cs index 858263b4e..07d7a6de6 100644 --- a/src/NzbDrone.Api/Indexers/ReleaseModule.cs +++ b/src/Lidarr.Api.V3/Indexers/ReleaseModule.cs @@ -1,20 +1,20 @@ -using System; +using System; using System.Collections.Generic; using FluentValidation; using Nancy; +using Nancy.ModelBinding; using NLog; +using NzbDrone.Common.Cache; using NzbDrone.Core.DecisionEngine; using NzbDrone.Core.Download; using NzbDrone.Core.Exceptions; -using NzbDrone.Core.IndexerSearch; using NzbDrone.Core.Indexers; +using NzbDrone.Core.IndexerSearch; using NzbDrone.Core.Parser.Model; -using Nancy.ModelBinding; -using NzbDrone.Api.Extensions; -using NzbDrone.Common.Cache; +using Lidarr.Http.Extensions; using HttpStatusCode = System.Net.HttpStatusCode; -namespace NzbDrone.Api.Indexers +namespace Lidarr.Api.V3.Indexers { public class ReleaseModule : ReleaseModuleBase { @@ -53,13 +53,13 @@ namespace NzbDrone.Api.Indexers private Response DownloadRelease(ReleaseResource release) { - var remoteAlbum = _remoteAlbumCache.Find(release.Guid); + var remoteAlbum = _remoteAlbumCache.Find(GetCacheKey(release)); if (remoteAlbum == null) { _logger.Debug("Couldn't find requested release in cache, cache timeout probably expired."); - return new NotFoundResponse(); + throw new NzbDroneClientException(HttpStatusCode.NotFound, "Couldn't find requested release in cache, try searching again"); } try @@ -68,7 +68,7 @@ namespace NzbDrone.Api.Indexers } catch (ReleaseDownloadException ex) { - _logger.Error(ex); + _logger.Error(ex, ex.Message); throw new NzbDroneClientException(HttpStatusCode.Conflict, "Getting release from indexer failed"); } @@ -77,7 +77,7 @@ namespace NzbDrone.Api.Indexers private List GetReleases() { - if (Request.Query.albumId != null) + if (Request.Query.albumId.HasValue) { return GetAlbumReleases(Request.Query.albumId); } @@ -96,7 +96,7 @@ namespace NzbDrone.Api.Indexers } catch (Exception ex) { - _logger.Error(ex, "Album search failed"); + _logger.Error(ex, "Album search failed: " + ex.Message); } return new List(); @@ -113,8 +113,14 @@ namespace NzbDrone.Api.Indexers protected override ReleaseResource MapDecision(DownloadDecision decision, int initialWeight) { - _remoteAlbumCache.Set(decision.RemoteAlbum.Release.Guid, decision.RemoteAlbum, TimeSpan.FromMinutes(30)); - return base.MapDecision(decision, initialWeight); + var resource = base.MapDecision(decision, initialWeight); + _remoteAlbumCache.Set(GetCacheKey(resource), decision.RemoteAlbum, TimeSpan.FromMinutes(30)); + return resource; + } + + private string GetCacheKey(ReleaseResource resource) + { + return string.Concat(resource.IndexerId, "_", resource.Guid); } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Api/Indexers/ReleaseModuleBase.cs b/src/Lidarr.Api.V3/Indexers/ReleaseModuleBase.cs similarity index 87% rename from src/NzbDrone.Api/Indexers/ReleaseModuleBase.cs rename to src/Lidarr.Api.V3/Indexers/ReleaseModuleBase.cs index 32344ef34..5d0e79ec5 100644 --- a/src/NzbDrone.Api/Indexers/ReleaseModuleBase.cs +++ b/src/Lidarr.Api.V3/Indexers/ReleaseModuleBase.cs @@ -1,9 +1,10 @@ -using System.Collections.Generic; +using System.Collections.Generic; using NzbDrone.Core.DecisionEngine; +using Lidarr.Http; -namespace NzbDrone.Api.Indexers +namespace Lidarr.Api.V3.Indexers { - public abstract class ReleaseModuleBase : NzbDroneRestModule + public abstract class ReleaseModuleBase : LidarrRestModule { protected virtual List MapDecisions(IEnumerable decisions) { diff --git a/src/NzbDrone.Api/Indexers/ReleasePushModule.cs b/src/Lidarr.Api.V3/Indexers/ReleasePushModule.cs similarity index 89% rename from src/NzbDrone.Api/Indexers/ReleasePushModule.cs rename to src/Lidarr.Api.V3/Indexers/ReleasePushModule.cs index c25e45726..817602f12 100644 --- a/src/NzbDrone.Api/Indexers/ReleasePushModule.cs +++ b/src/Lidarr.Api.V3/Indexers/ReleasePushModule.cs @@ -1,15 +1,15 @@ -using Nancy; -using Nancy.ModelBinding; +using System.Collections.Generic; +using System.Linq; using FluentValidation; +using Nancy; +using Nancy.ModelBinding; +using NLog; using NzbDrone.Core.DecisionEngine; using NzbDrone.Core.Download; -using System.Collections.Generic; -using System.Linq; using NzbDrone.Core.Parser.Model; -using NzbDrone.Api.Extensions; -using NLog; +using Lidarr.Http.Extensions; -namespace NzbDrone.Api.Indexers +namespace Lidarr.Api.V3.Indexers { class ReleasePushModule : ReleaseModuleBase { @@ -29,7 +29,7 @@ namespace NzbDrone.Api.Indexers PostValidator.RuleFor(s => s.Title).NotEmpty(); PostValidator.RuleFor(s => s.DownloadUrl).NotEmpty(); - PostValidator.RuleFor(s => s.Protocol).NotEmpty(); + PostValidator.RuleFor(s => s.DownloadProtocol).NotEmpty(); PostValidator.RuleFor(s => s.PublishDate).NotEmpty(); } diff --git a/src/NzbDrone.Api/Indexers/ReleaseResource.cs b/src/Lidarr.Api.V3/Indexers/ReleaseResource.cs similarity index 83% rename from src/NzbDrone.Api/Indexers/ReleaseResource.cs rename to src/Lidarr.Api.V3/Indexers/ReleaseResource.cs index a8f8cdfab..eb44fdaf6 100644 --- a/src/NzbDrone.Api/Indexers/ReleaseResource.cs +++ b/src/Lidarr.Api.V3/Indexers/ReleaseResource.cs @@ -1,15 +1,14 @@ -using System; +using System; using System.Collections.Generic; -using Newtonsoft.Json; -using NzbDrone.Api.REST; -using NzbDrone.Core.Parser; -using NzbDrone.Core.Qualities; +using System.Linq; +using NzbDrone.Core.DecisionEngine; using NzbDrone.Core.Indexers; +using NzbDrone.Core.Languages; using NzbDrone.Core.Parser.Model; -using NzbDrone.Core.DecisionEngine; -using System.Linq; +using NzbDrone.Core.Qualities; +using Lidarr.Http.REST; -namespace NzbDrone.Api.Indexers +namespace Lidarr.Api.V3.Indexers { public class ReleaseResource : RestResource { @@ -23,10 +22,13 @@ namespace NzbDrone.Api.Indexers public int IndexerId { get; set; } public string Indexer { get; set; } public string ReleaseGroup { get; set; } + public string SubGroup { get; set; } public string ReleaseHash { get; set; } public string Title { get; set; } + public bool FullSeason { get; set; } + public bool SceneSource { get; set; } public Language Language { get; set; } - public string ReleaseDate { get; set; } + public string AirDate { get; set; } public string ArtistName { get; set; } public string AlbumTitle { get; set; } public bool Approved { get; set; } @@ -40,37 +42,19 @@ namespace NzbDrone.Api.Indexers public bool DownloadAllowed { get; set; } public int ReleaseWeight { get; set; } - public string MagnetUrl { get; set; } public string InfoHash { get; set; } public int? Seeders { get; set; } public int? Leechers { get; set; } public DownloadProtocol Protocol { get; set; } + //TODO: besides a test I don't think this is used... + public DownloadProtocol DownloadProtocol { get; set; } - // TODO: Remove in v3 - // Used to support the original Release Push implementation - // JsonIgnore so we don't serialize it, but can still parse it - [JsonIgnore] - public DownloadProtocol DownloadProtocol - { - get - { - return Protocol; - } - set - { - if (value > 0 && Protocol == 0) - { - Protocol = value; - } - } - } - - public bool IsDaily { get; set; } - public bool IsAbsoluteNumbering { get; set; } - public bool IsPossibleSpecialEpisode { get; set; } - public bool Special { get; set; } + //public bool IsDaily { get; set; } + //public bool IsAbsoluteNumbering { get; set; } + //public bool IsPossibleSpecialEpisode { get; set; } + //public bool Special { get; set; } } public static class ReleaseResourceMapper @@ -98,9 +82,9 @@ namespace NzbDrone.Api.Indexers ReleaseHash = parsedAlbumInfo.ReleaseHash, Title = releaseInfo.Title, Language = parsedAlbumInfo.Language, - ReleaseDate = parsedAlbumInfo.ReleaseDate, ArtistName = parsedAlbumInfo.ArtistName, AlbumTitle = parsedAlbumInfo.AlbumTitle, + Approved = model.Approved, TemporarilyRejected = model.TemporarilyRejected, Rejected = model.Rejected, @@ -112,11 +96,17 @@ namespace NzbDrone.Api.Indexers DownloadAllowed = remoteAlbum.DownloadAllowed, //ReleaseWeight + MagnetUrl = torrentInfo.MagnetUrl, InfoHash = torrentInfo.InfoHash, Seeders = torrentInfo.Seeders, Leechers = (torrentInfo.Peers.HasValue && torrentInfo.Seeders.HasValue) ? (torrentInfo.Peers.Value - torrentInfo.Seeders.Value) : (int?)null, Protocol = releaseInfo.DownloadProtocol, + + //IsDaily = parsedEpisodeInfo.IsDaily, + //IsAbsoluteNumbering = parsedEpisodeInfo.IsAbsoluteNumbering, + //IsPossibleSpecialEpisode = parsedEpisodeInfo.IsPossibleSpecialEpisode, + //Special = parsedEpisodeInfo.Special, }; } @@ -149,9 +139,9 @@ namespace NzbDrone.Api.Indexers model.IndexerId = resource.IndexerId; model.Indexer = resource.Indexer; model.DownloadProtocol = resource.DownloadProtocol; - model.PublishDate = resource.PublishDate; + model.PublishDate = resource.PublishDate.ToUniversalTime(); return model; } } -} \ No newline at end of file +} diff --git a/src/Lidarr.Api.V3/Lidarr.Api.V3.csproj b/src/Lidarr.Api.V3/Lidarr.Api.V3.csproj new file mode 100644 index 000000000..6e3aed69d --- /dev/null +++ b/src/Lidarr.Api.V3/Lidarr.Api.V3.csproj @@ -0,0 +1,245 @@ + + + + + Debug + x86 + {7140FF1F-79BE-492F-9188-B21A050BF708} + Library + Properties + Lidarr.Api.V3 + Lidarr.Api.V3 + v4.6.1 + 512 + ..\ + true + + + 12.0.0 + 2.0 + + + true + ..\..\_output\ + DEBUG;TRACE + full + x86 + prompt + MinimumRecommendedRules.ruleset + 4 + false + false + + + ..\..\_output\ + TRACE + true + pdbonly + x86 + prompt + MinimumRecommendedRules.ruleset + 4 + false + + + + ..\packages\Ical.Net.2.2.32\lib\net40\antlr.runtime.dll + + + ..\packages\FluentValidation.6.2.1.0\lib\portable-net40+sl50+wp80+win8+wpa81\FluentValidation.dll + True + + + ..\packages\Ical.Net.2.2.32\lib\net40\Ical.Net.dll + + + ..\packages\Ical.Net.2.2.32\lib\net40\Ical.Net.Collections.dll + + + ..\packages\Nancy.1.4.4\lib\net40\Nancy.dll + + + ..\packages\Nancy.Authentication.Basic.1.4.1\lib\net40\Nancy.Authentication.Basic.dll + True + + + ..\packages\Nancy.Authentication.Forms.1.4.1\lib\net40\Nancy.Authentication.Forms.dll + True + + + ..\packages\Newtonsoft.Json.9.0.1\lib\net40\Newtonsoft.Json.dll + True + + + ..\packages\NLog.4.4.12\lib\net45\NLog.dll + + + ..\packages\Ical.Net.2.2.32\lib\net40\NodaTime.dll + + + + + + + + Properties\SharedAssemblyInfo.cs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Designer + + + + + {F6FC6BE7-0847-4817-A1ED-223DC647C3D7} + Marr.Data + + + {5370BFF7-1BD7-46BC-AF06-7D9EA5CDA1D6} + Lidarr.Http + + + {f2be0fdf-6e47-4827-a420-dd4ef82407f8} + NzbDrone.Common + + + {ff5ee3b6-913b-47ce-9ceb-11c51b4e1205} + NzbDrone.Core + + + {7c2cc69f-5ca0-4e5c-85cb-983f9f6c3b36} + NzbDrone.SignalR + + + + + + \ No newline at end of file diff --git a/src/NzbDrone.Api/Logs/LogFileModule.cs b/src/Lidarr.Api.V3/Logs/LogFileModule.cs similarity index 85% rename from src/NzbDrone.Api/Logs/LogFileModule.cs rename to src/Lidarr.Api.V3/Logs/LogFileModule.cs index bed6f7de2..19676d523 100644 --- a/src/NzbDrone.Api/Logs/LogFileModule.cs +++ b/src/Lidarr.Api.V3/Logs/LogFileModule.cs @@ -5,7 +5,7 @@ using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.Extensions; using NzbDrone.Core.Configuration; -namespace NzbDrone.Api.Logs +namespace Lidarr.Api.V3.Logs { public class LogFileModule : LogFileModuleBase { @@ -31,6 +31,13 @@ namespace NzbDrone.Api.Logs return Path.Combine(_appFolderInfo.GetLogFolder(), filename); } - protected override string DownloadUrlRoot => "logfile"; + protected override string DownloadUrlRoot + { + get + { + return "logfile"; + } + } + } } \ No newline at end of file diff --git a/src/NzbDrone.Api/Logs/LogFileModuleBase.cs b/src/Lidarr.Api.V3/Logs/LogFileModuleBase.cs similarity index 89% rename from src/NzbDrone.Api/Logs/LogFileModuleBase.cs rename to src/Lidarr.Api.V3/Logs/LogFileModuleBase.cs index d8a12d1bf..47a8eab6d 100644 --- a/src/NzbDrone.Api/Logs/LogFileModuleBase.cs +++ b/src/Lidarr.Api.V3/Logs/LogFileModuleBase.cs @@ -1,14 +1,15 @@ using System.Collections.Generic; using System.IO; using System.Linq; -using NzbDrone.Common.Disk; using Nancy; using Nancy.Responses; +using NzbDrone.Common.Disk; using NzbDrone.Core.Configuration; +using Lidarr.Http; -namespace NzbDrone.Api.Logs +namespace Lidarr.Api.V3.Logs { - public abstract class LogFileModuleBase : NzbDroneRestModule + public abstract class LogFileModuleBase : LidarrRestModule { protected const string LOGFILE_ROUTE = @"/(?[-.a-zA-Z0-9]+?\.txt)"; @@ -43,7 +44,7 @@ namespace NzbDrone.Api.Logs Id = i + 1, Filename = filename, LastWriteTime = _diskProvider.FileGetLastWrite(file), - ContentsUrl = string.Format("{0}/api/{1}/{2}", _configFileProvider.UrlBase, Resource, filename), + ContentsUrl = string.Format("{0}/api/v3/{1}/{2}", _configFileProvider.UrlBase, Resource, filename), DownloadUrl = string.Format("{0}/{1}/{2}", _configFileProvider.UrlBase, DownloadUrlRoot, filename) }); } diff --git a/src/NzbDrone.Api/Logs/LogFileResource.cs b/src/Lidarr.Api.V3/Logs/LogFileResource.cs similarity index 83% rename from src/NzbDrone.Api/Logs/LogFileResource.cs rename to src/Lidarr.Api.V3/Logs/LogFileResource.cs index 9f67c8af7..2bfb7da7c 100644 --- a/src/NzbDrone.Api/Logs/LogFileResource.cs +++ b/src/Lidarr.Api.V3/Logs/LogFileResource.cs @@ -1,7 +1,7 @@ using System; -using NzbDrone.Api.REST; +using Lidarr.Http.REST; -namespace NzbDrone.Api.Logs +namespace Lidarr.Api.V3.Logs { public class LogFileResource : RestResource { diff --git a/src/NzbDrone.Api/Logs/LogModule.cs b/src/Lidarr.Api.V3/Logs/LogModule.cs similarity index 84% rename from src/NzbDrone.Api/Logs/LogModule.cs rename to src/Lidarr.Api.V3/Logs/LogModule.cs index 88ead3ec0..a1b9b79e3 100644 --- a/src/NzbDrone.Api/Logs/LogModule.cs +++ b/src/Lidarr.Api.V3/Logs/LogModule.cs @@ -1,8 +1,9 @@ using NzbDrone.Core.Instrumentation; +using Lidarr.Http; -namespace NzbDrone.Api.Logs +namespace Lidarr.Api.V3.Logs { - public class LogModule : NzbDroneRestModule + public class LogModule : LidarrRestModule { private readonly ILogService _logService; @@ -46,7 +47,14 @@ namespace NzbDrone.Api.Logs } } - return ApplyToPage(_logService.Paged, pageSpec, LogResourceMapper.ToResource); + var response = ApplyToPage(_logService.Paged, pageSpec, LogResourceMapper.ToResource); + + if (pageSpec.SortKey == "id") + { + response.SortKey = "time"; + } + + return response; } } } \ No newline at end of file diff --git a/src/NzbDrone.Api/Logs/LogResource.cs b/src/Lidarr.Api.V3/Logs/LogResource.cs similarity index 80% rename from src/NzbDrone.Api/Logs/LogResource.cs rename to src/Lidarr.Api.V3/Logs/LogResource.cs index 504a45839..e3eef3836 100644 --- a/src/NzbDrone.Api/Logs/LogResource.cs +++ b/src/Lidarr.Api.V3/Logs/LogResource.cs @@ -1,7 +1,8 @@ using System; -using NzbDrone.Api.REST; +using NzbDrone.Core.Instrumentation; +using Lidarr.Http.REST; -namespace NzbDrone.Api.Logs +namespace Lidarr.Api.V3.Logs { public class LogResource : RestResource { @@ -11,11 +12,12 @@ namespace NzbDrone.Api.Logs public string Level { get; set; } public string Logger { get; set; } public string Message { get; set; } + public string Method { get; set; } } public static class LogResourceMapper { - public static LogResource ToResource(this Core.Instrumentation.Log model) + public static LogResource ToResource(this Log model) { if (model == null) return null; diff --git a/src/NzbDrone.Api/Logs/UpdateLogFileModule.cs b/src/Lidarr.Api.V3/Logs/UpdateLogFileModule.cs similarity index 88% rename from src/NzbDrone.Api/Logs/UpdateLogFileModule.cs rename to src/Lidarr.Api.V3/Logs/UpdateLogFileModule.cs index 5c4f81f02..f9ac8311c 100644 --- a/src/NzbDrone.Api/Logs/UpdateLogFileModule.cs +++ b/src/Lidarr.Api.V3/Logs/UpdateLogFileModule.cs @@ -7,7 +7,7 @@ using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.Extensions; using NzbDrone.Core.Configuration; -namespace NzbDrone.Api.Logs +namespace Lidarr.Api.V3.Logs { public class UpdateLogFileModule : LogFileModuleBase { @@ -37,6 +37,12 @@ namespace NzbDrone.Api.Logs return Path.Combine(_appFolderInfo.GetUpdateLogFolder(), filename); } - protected override string DownloadUrlRoot => "updatelogfile"; + protected override string DownloadUrlRoot + { + get + { + return "updatelogfile"; + } + } } } \ No newline at end of file diff --git a/src/NzbDrone.Api/ManualImport/ManualImportModule.cs b/src/Lidarr.Api.V3/ManualImport/ManualImportModule.cs similarity index 76% rename from src/NzbDrone.Api/ManualImport/ManualImportModule.cs rename to src/Lidarr.Api.V3/ManualImport/ManualImportModule.cs index bcd99de1b..11a3e4924 100644 --- a/src/NzbDrone.Api/ManualImport/ManualImportModule.cs +++ b/src/Lidarr.Api.V3/ManualImport/ManualImportModule.cs @@ -2,10 +2,11 @@ using System.Collections.Generic; using System.Linq; using NzbDrone.Core.MediaFiles.TrackImport.Manual; using NzbDrone.Core.Qualities; +using Lidarr.Http; -namespace NzbDrone.Api.ManualImport +namespace Lidarr.Api.V3.ManualImport { - public class ManualImportModule : NzbDroneRestModule + public class ManualImportModule : LidarrRestModule { private readonly IManualImportService _manualImportService; @@ -19,11 +20,8 @@ namespace NzbDrone.Api.ManualImport private List GetMediaFiles() { - var folderQuery = Request.Query.folder; - var folder = (string)folderQuery.Value; - - var downloadIdQuery = Request.Query.downloadId; - var downloadId = (string)downloadIdQuery.Value; + var folder = (string)Request.Query.folder; + var downloadId = (string)Request.Query.downloadId; return _manualImportService.GetMediaFiles(folder, downloadId).ToResource().Select(AddQualityWeight).ToList(); } diff --git a/src/Lidarr.Api.V3/ManualImport/ManualImportResource.cs b/src/Lidarr.Api.V3/ManualImport/ManualImportResource.cs new file mode 100644 index 000000000..40d77cadb --- /dev/null +++ b/src/Lidarr.Api.V3/ManualImport/ManualImportResource.cs @@ -0,0 +1,60 @@ +using NzbDrone.Common.Crypto; +using NzbDrone.Core.DecisionEngine; +using NzbDrone.Core.MediaFiles.TrackImport.Manual; +using NzbDrone.Core.Qualities; +using NzbDrone.Core.Languages; +using Lidarr.Api.V3.Artist; +using Lidarr.Api.V3.Albums; +using Lidarr.Api.V3.Tracks; +using Lidarr.Http.REST; +using System.Collections.Generic; +using System.Linq; + +namespace Lidarr.Api.V3.ManualImport +{ + public class ManualImportResource : RestResource + { + public string Path { get; set; } + public string RelativePath { get; set; } + public string Name { get; set; } + public long Size { get; set; } + public ArtistResource Artist { get; set; } + public AlbumResource Album { get; set; } + public List Tracks { get; set; } + public QualityModel Quality { get; set; } + public Language Language { get; set; } + public int QualityWeight { get; set; } + public string DownloadId { get; set; } + public IEnumerable Rejections { get; set; } + } + + public static class ManualImportResourceMapper + { + public static ManualImportResource ToResource(this ManualImportItem model) + { + if (model == null) return null; + + return new ManualImportResource + { + Id = HashConverter.GetHashInt31(model.Path), + Path = model.Path, + RelativePath = model.RelativePath, + Name = model.Name, + Size = model.Size, + Artist = model.Artist.ToResource(), + Album = model.Album.ToResource(), + Tracks = model.Tracks.ToResource(), + Quality = model.Quality, + Language = model.Language, + //QualityWeight + DownloadId = model.DownloadId, + Rejections = model.Rejections + }; + } + + public static List ToResource(this IEnumerable models) + { + return models.Select(ToResource).ToList(); + } + } +} diff --git a/src/NzbDrone.Api/MediaCovers/MediaCoverModule.cs b/src/Lidarr.Api.V3/MediaCovers/MediaCoverModule.cs similarity index 95% rename from src/NzbDrone.Api/MediaCovers/MediaCoverModule.cs rename to src/Lidarr.Api.V3/MediaCovers/MediaCoverModule.cs index a4ad78ef4..922cf54a4 100644 --- a/src/NzbDrone.Api/MediaCovers/MediaCoverModule.cs +++ b/src/Lidarr.Api.V3/MediaCovers/MediaCoverModule.cs @@ -6,9 +6,9 @@ using NzbDrone.Common.Disk; using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.Extensions; -namespace NzbDrone.Api.MediaCovers +namespace Lidarr.Api.V3.MediaCovers { - public class MediaCoverModule : NzbDroneApiModule + public class MediaCoverModule : SonarrV3Module { private static readonly Regex RegexResizedImage = new Regex(@"-\d+\.jpg$", RegexOptions.Compiled | RegexOptions.IgnoreCase); diff --git a/src/Lidarr.Api.V3/Metadata/MetadataModule.cs b/src/Lidarr.Api.V3/Metadata/MetadataModule.cs new file mode 100644 index 000000000..c8de3fc02 --- /dev/null +++ b/src/Lidarr.Api.V3/Metadata/MetadataModule.cs @@ -0,0 +1,20 @@ +using NzbDrone.Core.Extras.Metadata; + +namespace Lidarr.Api.V3.Metadata +{ + public class MetadataModule : ProviderModuleBase + { + public static readonly MetadataResourceMapper ResourceMapper = new MetadataResourceMapper(); + + public MetadataModule(IMetadataFactory metadataFactory) + : base(metadataFactory, "metadata", ResourceMapper) + { + } + + protected override void Validate(MetadataDefinition definition, bool includeWarnings) + { + if (!definition.Enable) return; + base.Validate(definition, includeWarnings); + } + } +} \ No newline at end of file diff --git a/src/Lidarr.Api.V3/Metadata/MetadataResource.cs b/src/Lidarr.Api.V3/Metadata/MetadataResource.cs new file mode 100644 index 000000000..3920b46f9 --- /dev/null +++ b/src/Lidarr.Api.V3/Metadata/MetadataResource.cs @@ -0,0 +1,34 @@ +using NzbDrone.Core.Extras.Metadata; + +namespace Lidarr.Api.V3.Metadata +{ + public class MetadataResource : ProviderResource + { + public bool Enable { get; set; } + } + + public class MetadataResourceMapper : ProviderResourceMapper + { + public override MetadataResource ToResource(MetadataDefinition definition) + { + if (definition == null) return null; + + var resource = base.ToResource(definition); + + resource.Enable = definition.Enable; + + return resource; + } + + public override MetadataDefinition ToModel(MetadataResource resource) + { + if (resource == null) return null; + + var definition = base.ToModel(resource); + + definition.Enable = resource.Enable; + + return definition; + } + } +} \ No newline at end of file diff --git a/src/Lidarr.Api.V3/Notifications/NotificationModule.cs b/src/Lidarr.Api.V3/Notifications/NotificationModule.cs new file mode 100644 index 000000000..edf57eaa8 --- /dev/null +++ b/src/Lidarr.Api.V3/Notifications/NotificationModule.cs @@ -0,0 +1,20 @@ +using NzbDrone.Core.Notifications; + +namespace Lidarr.Api.V3.Notifications +{ + public class NotificationModule : ProviderModuleBase + { + public static readonly NotificationResourceMapper ResourceMapper = new NotificationResourceMapper(); + + public NotificationModule(NotificationFactory notificationFactory) + : base(notificationFactory, "notification", ResourceMapper) + { + } + + protected override void Validate(NotificationDefinition definition, bool includeWarnings) + { + if (!definition.OnGrab && !definition.OnDownload) return; + base.Validate(definition, includeWarnings); + } + } +} \ No newline at end of file diff --git a/src/Lidarr.Api.V3/Notifications/NotificationResource.cs b/src/Lidarr.Api.V3/Notifications/NotificationResource.cs new file mode 100644 index 000000000..1cdb4e176 --- /dev/null +++ b/src/Lidarr.Api.V3/Notifications/NotificationResource.cs @@ -0,0 +1,57 @@ +using NzbDrone.Core.Notifications; + +namespace Lidarr.Api.V3.Notifications +{ + public class NotificationResource : ProviderResource + { + public string Link { get; set; } + public bool OnGrab { get; set; } + public bool OnDownload { get; set; } + public bool OnUpgrade { get; set; } + public bool OnRename { get; set; } + public bool SupportsOnGrab { get; set; } + public bool SupportsOnDownload { get; set; } + public bool SupportsOnUpgrade { get; set; } + public bool SupportsOnRename { get; set; } + public string TestCommand { get; set; } + } + + public class NotificationResourceMapper : ProviderResourceMapper + { + public override NotificationResource ToResource(NotificationDefinition definition) + { + if (definition == null) return default(NotificationResource); + + var resource = base.ToResource(definition); + + resource.OnGrab = definition.OnGrab; + resource.OnDownload = definition.OnDownload; + resource.OnUpgrade = definition.OnUpgrade; + resource.OnRename = definition.OnRename; + resource.SupportsOnGrab = definition.SupportsOnGrab; + resource.SupportsOnDownload = definition.SupportsOnDownload; + resource.SupportsOnUpgrade = definition.SupportsOnUpgrade; + resource.SupportsOnRename = definition.SupportsOnRename; + + return resource; + } + + public override NotificationDefinition ToModel(NotificationResource resource) + { + if (resource == null) return default(NotificationDefinition); + + var definition = base.ToModel(resource); + + definition.OnGrab = resource.OnGrab; + definition.OnDownload = resource.OnDownload; + definition.OnUpgrade = resource.OnUpgrade; + definition.OnRename = resource.OnRename; + definition.SupportsOnGrab = resource.SupportsOnGrab; + definition.SupportsOnDownload = resource.SupportsOnDownload; + definition.SupportsOnUpgrade = resource.SupportsOnUpgrade; + definition.SupportsOnRename = resource.SupportsOnRename; + + return definition; + } + } +} \ No newline at end of file diff --git a/src/Lidarr.Api.V3/Parse/ParseModule.cs b/src/Lidarr.Api.V3/Parse/ParseModule.cs new file mode 100644 index 000000000..e6f3b15b6 --- /dev/null +++ b/src/Lidarr.Api.V3/Parse/ParseModule.cs @@ -0,0 +1,59 @@ +using NzbDrone.Common.Extensions; +using NzbDrone.Core.Parser; +using Lidarr.Api.V3.Albums; +using Lidarr.Api.V3.Artist; +using Lidarr.Http; + +namespace Lidarr.Api.V3.Parse +{ + public class ParseModule : LidarrRestModule + { + private readonly IParsingService _parsingService; + + public ParseModule(IParsingService parsingService) + { + _parsingService = parsingService; + + GetResourceSingle = Parse; + } + + private ParseResource Parse() + { + var title = Request.Query.Title.Value as string; + var path = Request.Query.Path.Value as string; + var parsedEpisodeInfo = path.IsNotNullOrWhiteSpace() ? Parser.ParseMusicPath(path) : Parser.ParseMusicTitle(title); + + if (parsedEpisodeInfo == null) + { + return null; + } + + return new ParseResource + { + Title = title, + ParsedAlbumInfo = parsedEpisodeInfo + }; + + //var remoteEpisode = null //_parsingService.Map(parsedEpisodeInfo, 0, 0); + + //if (remoteEpisode != null) + //{ + // return new ParseResource + // { + // Title = title, + // ParsedAlbumInfo = remoteEpisode.ParsedEpisodeInfo, + // Artist = remoteEpisode.Series.ToResource(), + // Albums = remoteEpisode.Episodes.ToResource() + // }; + //} + //else + //{ + // return new ParseResource + // { + // Title = title, + // ParsedAlbumInfo = parsedEpisodeInfo + // }; + //} + } + } +} diff --git a/src/Lidarr.Api.V3/Parse/ParseResource.cs b/src/Lidarr.Api.V3/Parse/ParseResource.cs new file mode 100644 index 000000000..85a335186 --- /dev/null +++ b/src/Lidarr.Api.V3/Parse/ParseResource.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; +using NzbDrone.Core.Parser.Model; +using Lidarr.Api.V3.Albums; +using Lidarr.Api.V3.Artist; +using Lidarr.Http.REST; + +namespace Lidarr.Api.V3.Parse +{ + public class ParseResource : RestResource + { + public string Title { get; set; } + public ParsedTrackInfo ParsedAlbumInfo { get; set; } + public ArtistResource Artist { get; set; } + public List Albums { get; set; } + } +} diff --git a/src/NzbDrone.Api/Profiles/Delay/DelayProfileModule.cs b/src/Lidarr.Api.V3/Profiles/Delay/DelayProfileModule.cs similarity index 76% rename from src/NzbDrone.Api/Profiles/Delay/DelayProfileModule.cs rename to src/Lidarr.Api.V3/Profiles/Delay/DelayProfileModule.cs index e7975b661..217a50bc6 100644 --- a/src/NzbDrone.Api/Profiles/Delay/DelayProfileModule.cs +++ b/src/Lidarr.Api.V3/Profiles/Delay/DelayProfileModule.cs @@ -1,13 +1,17 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using FluentValidation; using FluentValidation.Results; -using NzbDrone.Api.REST; -using NzbDrone.Api.Validation; +using Nancy; using NzbDrone.Core.Profiles.Delay; +using Lidarr.Http; +using Lidarr.Http.Extensions; +using Lidarr.Http.REST; +using Lidarr.Http.Validation; -namespace NzbDrone.Api.Profiles.Delay +namespace Lidarr.Api.V3.Profiles.Delay { - public class DelayProfileModule : NzbDroneRestModule + public class DelayProfileModule : LidarrRestModule { private readonly IDelayProfileService _delayProfileService; @@ -20,6 +24,7 @@ namespace NzbDrone.Api.Profiles.Delay UpdateResource = Update; CreateResource = Create; DeleteResource = DeleteProfile; + Put[@"/reorder/(?[\d]{1,10})"] = options => Reorder(options.Id); SharedValidator.RuleFor(d => d.Tags).NotEmpty().When(d => d.Id != 1); SharedValidator.RuleFor(d => d.Tags).EmptyCollection().When(d => d.Id == 1); @@ -71,5 +76,15 @@ namespace NzbDrone.Api.Profiles.Delay { return _delayProfileService.All().ToResource(); } + + private Response Reorder(int id) + { + ValidateId(id); + + var afterIdQuery = Request.Query.After; + int? afterId = afterIdQuery.HasValue ? Convert.ToInt32(afterIdQuery.Value) : null; + + return _delayProfileService.Reorder(id, afterId).ToResource().AsResponse(); + } } } \ No newline at end of file diff --git a/src/NzbDrone.Api/Profiles/Delay/DelayProfileResource.cs b/src/Lidarr.Api.V3/Profiles/Delay/DelayProfileResource.cs similarity index 96% rename from src/NzbDrone.Api/Profiles/Delay/DelayProfileResource.cs rename to src/Lidarr.Api.V3/Profiles/Delay/DelayProfileResource.cs index e35df9043..6ec92a6cd 100644 --- a/src/NzbDrone.Api/Profiles/Delay/DelayProfileResource.cs +++ b/src/Lidarr.Api.V3/Profiles/Delay/DelayProfileResource.cs @@ -1,10 +1,10 @@ using System.Collections.Generic; using System.Linq; -using NzbDrone.Api.REST; using NzbDrone.Core.Indexers; using NzbDrone.Core.Profiles.Delay; +using Lidarr.Http.REST; -namespace NzbDrone.Api.Profiles.Delay +namespace Lidarr.Api.V3.Profiles.Delay { public class DelayProfileResource : RestResource { diff --git a/src/Lidarr.Api.V3/Profiles/Language/LanguageProfileModule.cs b/src/Lidarr.Api.V3/Profiles/Language/LanguageProfileModule.cs new file mode 100644 index 000000000..458eede53 --- /dev/null +++ b/src/Lidarr.Api.V3/Profiles/Language/LanguageProfileModule.cs @@ -0,0 +1,57 @@ +using System.Collections.Generic; +using FluentValidation; +using NzbDrone.Core.Profiles.Languages; +using Lidarr.Http; + +namespace Lidarr.Api.V3.Profiles.Language +{ + public class LanguageProfileModule : LidarrRestModule + { + private readonly ILanguageProfileService _profileService; + + public LanguageProfileModule(ILanguageProfileService profileService) + { + _profileService = profileService; + SharedValidator.RuleFor(c => c.Name).NotEmpty(); + SharedValidator.RuleFor(c => c.Cutoff).NotNull(); + SharedValidator.RuleFor(c => c.Languages).MustHaveAllowedLanguage(); + + GetResourceAll = GetAll; + GetResourceById = GetById; + UpdateResource = Update; + CreateResource = Create; + DeleteResource = DeleteProfile; + } + + private int Create(LanguageProfileResource resource) + { + var model = resource.ToModel(); + model = _profileService.Add(model); + return model.Id; + } + + private void DeleteProfile(int id) + { + _profileService.Delete(id); + } + + private void Update(LanguageProfileResource resource) + { + var model = resource.ToModel(); + + _profileService.Update(model); + } + + private LanguageProfileResource GetById(int id) + { + return _profileService.Get(id).ToResource(); + } + + private List GetAll() + { + var profiles = _profileService.All().ToResource(); + + return profiles; + } + } +} \ No newline at end of file diff --git a/src/Lidarr.Api.V3/Profiles/Language/LanguageProfileResource.cs b/src/Lidarr.Api.V3/Profiles/Language/LanguageProfileResource.cs new file mode 100644 index 000000000..fabfc40f7 --- /dev/null +++ b/src/Lidarr.Api.V3/Profiles/Language/LanguageProfileResource.cs @@ -0,0 +1,76 @@ +using System.Collections.Generic; +using System.Linq; +using NzbDrone.Core.Profiles.Languages; +using Lidarr.Http.REST; + +namespace Lidarr.Api.V3.Profiles.Language +{ + public class LanguageProfileResource : RestResource + { + public string Name { get; set; } + public NzbDrone.Core.Languages.Language Cutoff { get; set; } + public List Languages { get; set; } + } + + public class ProfileLanguageItemResource : RestResource + { + public NzbDrone.Core.Languages.Language Language { get; set; } + public bool Allowed { get; set; } + } + + public static class LanguageProfileResourceMapper + { + public static LanguageProfileResource ToResource(this LanguageProfile model) + { + if (model == null) return null; + + return new LanguageProfileResource + { + Id = model.Id, + Name = model.Name, + Cutoff = model.Cutoff, + Languages = model.Languages.ConvertAll(ToResource) + }; + } + + public static ProfileLanguageItemResource ToResource(this ProfileLanguageItem model) + { + if (model == null) return null; + + return new ProfileLanguageItemResource + { + Language = model.Language, + Allowed = model.Allowed + }; + } + + public static LanguageProfile ToModel(this LanguageProfileResource resource) + { + if (resource == null) return null; + + return new LanguageProfile + { + Id = resource.Id, + Name = resource.Name, + Cutoff = (NzbDrone.Core.Languages.Language)resource.Cutoff.Id, + Languages = resource.Languages.ConvertAll(ToModel) + }; + } + + public static ProfileLanguageItem ToModel(this ProfileLanguageItemResource resource) + { + if (resource == null) return null; + + return new ProfileLanguageItem + { + Language = (NzbDrone.Core.Languages.Language)resource.Language.Id, + Allowed = resource.Allowed + }; + } + + public static List ToResource(this IEnumerable models) + { + return models.Select(ToResource).ToList(); + } + } +} diff --git a/src/Lidarr.Api.V3/Profiles/Language/LanguageProfileSchemaModule.cs b/src/Lidarr.Api.V3/Profiles/Language/LanguageProfileSchemaModule.cs new file mode 100644 index 000000000..899658441 --- /dev/null +++ b/src/Lidarr.Api.V3/Profiles/Language/LanguageProfileSchemaModule.cs @@ -0,0 +1,37 @@ +using System.Linq; +using NzbDrone.Core.Profiles.Languages; +using Lidarr.Http; + +namespace Lidarr.Api.V3.Profiles.Language +{ + public class LanguageProfileSchemaModule : LidarrRestModule + { + + public LanguageProfileSchemaModule() + : base("/languageprofile/schema") + { + GetResourceSingle = GetAll; + } + + private LanguageProfileResource GetAll() + { + var orderedLanguages = NzbDrone.Core.Languages.Language.All + .Where(l => l != NzbDrone.Core.Languages.Language.Unknown) + .OrderByDescending(l => l.Name) + .ToList(); + + orderedLanguages.Insert(0, NzbDrone.Core.Languages.Language.Unknown); + + var languages = orderedLanguages.Select(v => new ProfileLanguageItem {Language = v, Allowed = false}) + .ToList(); + + var profile = new LanguageProfile + { + Cutoff = NzbDrone.Core.Languages.Language.Unknown, + Languages = languages + }; + + return profile.ToResource(); + } + } +} \ No newline at end of file diff --git a/src/Lidarr.Api.V3/Profiles/Language/LanguageValidator.cs b/src/Lidarr.Api.V3/Profiles/Language/LanguageValidator.cs new file mode 100644 index 000000000..6aa5de220 --- /dev/null +++ b/src/Lidarr.Api.V3/Profiles/Language/LanguageValidator.cs @@ -0,0 +1,43 @@ +using System.Collections.Generic; +using System.Linq; +using FluentValidation; +using FluentValidation.Validators; + +namespace Lidarr.Api.V3.Profiles.Language +{ + public static class LanguageValidation + { + public static IRuleBuilderOptions> MustHaveAllowedLanguage(this IRuleBuilder> ruleBuilder) + { + ruleBuilder.SetValidator(new NotEmptyValidator(null)); + + return ruleBuilder.SetValidator(new LanguageValidator()); + } + } + + + public class LanguageValidator : PropertyValidator + { + public LanguageValidator() + : base("Must have at least one allowed language") + { + } + + protected override bool IsValid(PropertyValidatorContext context) + { + var list = context.PropertyValue as IList; + + if (list == null) + { + return false; + } + + if (!list.Any(c => c.Allowed)) + { + return false; + } + + return true; + } + } +} diff --git a/src/Lidarr.Api.V3/Profiles/Quality/QualityProfileModule.cs b/src/Lidarr.Api.V3/Profiles/Quality/QualityProfileModule.cs new file mode 100644 index 000000000..dbc7d8a1d --- /dev/null +++ b/src/Lidarr.Api.V3/Profiles/Quality/QualityProfileModule.cs @@ -0,0 +1,55 @@ +using System.Collections.Generic; +using FluentValidation; +using NzbDrone.Core.Profiles.Qualities; +using Lidarr.Http; + +namespace Lidarr.Api.V3.Profiles.Quality +{ + public class ProfileModule : LidarrRestModule + { + private readonly IProfileService _profileService; + + public ProfileModule(IProfileService profileService) + { + _profileService = profileService; + SharedValidator.RuleFor(c => c.Name).NotEmpty(); + SharedValidator.RuleFor(c => c.Cutoff).NotNull(); + SharedValidator.RuleFor(c => c.Items).MustHaveAllowedQuality(); + + GetResourceAll = GetAll; + GetResourceById = GetById; + UpdateResource = Update; + CreateResource = Create; + DeleteResource = DeleteProfile; + } + + private int Create(QualityProfileResource resource) + { + var model = resource.ToModel(); + model = _profileService.Add(model); + return model.Id; + } + + private void DeleteProfile(int id) + { + _profileService.Delete(id); + } + + private void Update(QualityProfileResource resource) + { + var model = resource.ToModel(); + + _profileService.Update(model); + } + + private QualityProfileResource GetById(int id) + { + return _profileService.Get(id).ToResource(); + } + + private List GetAll() + { + return _profileService.All().ToResource(); + } + } +} \ No newline at end of file diff --git a/src/Lidarr.Api.V3/Profiles/Quality/QualityProfileResource.cs b/src/Lidarr.Api.V3/Profiles/Quality/QualityProfileResource.cs new file mode 100644 index 000000000..111fbec79 --- /dev/null +++ b/src/Lidarr.Api.V3/Profiles/Quality/QualityProfileResource.cs @@ -0,0 +1,78 @@ +using System.Collections.Generic; +using System.Linq; +using NzbDrone.Core.Profiles.Qualities; +using Lidarr.Http.REST; + +namespace Lidarr.Api.V3.Profiles.Quality +{ + public class QualityProfileResource : RestResource + { + public string Name { get; set; } + public NzbDrone.Core.Qualities.Quality Cutoff { get; set; } + public List Items { get; set; } + } + + public class QualityProfileQualityItemResource : RestResource + { + public NzbDrone.Core.Qualities.Quality Quality { get; set; } + public bool Allowed { get; set; } + } + + public static class ProfileResourceMapper + { + public static QualityProfileResource ToResource(this Profile model) + { + if (model == null) return null; + + return new QualityProfileResource + { + Id = model.Id, + + Name = model.Name, + Cutoff = model.Cutoff, + Items = model.Items.ConvertAll(ToResource), + }; + } + + public static QualityProfileQualityItemResource ToResource(this ProfileQualityItem model) + { + if (model == null) return null; + + return new QualityProfileQualityItemResource + { + Quality = model.Quality, + Allowed = model.Allowed + }; + } + + public static Profile ToModel(this QualityProfileResource resource) + { + if (resource == null) return null; + + return new Profile + { + Id = resource.Id, + + Name = resource.Name, + Cutoff = (NzbDrone.Core.Qualities.Quality)resource.Cutoff.Id, + Items = resource.Items.ConvertAll(ToModel) + }; + } + + public static ProfileQualityItem ToModel(this QualityProfileQualityItemResource resource) + { + if (resource == null) return null; + + return new ProfileQualityItem + { + Quality = (NzbDrone.Core.Qualities.Quality)resource.Quality.Id, + Allowed = resource.Allowed + }; + } + + public static List ToResource(this IEnumerable models) + { + return models.Select(ToResource).ToList(); + } + } +} \ No newline at end of file diff --git a/src/Lidarr.Api.V3/Profiles/Quality/QualityProfileSchemaModule.cs b/src/Lidarr.Api.V3/Profiles/Quality/QualityProfileSchemaModule.cs new file mode 100644 index 000000000..4aebb9fb2 --- /dev/null +++ b/src/Lidarr.Api.V3/Profiles/Quality/QualityProfileSchemaModule.cs @@ -0,0 +1,34 @@ +using System.Linq; +using NzbDrone.Core.Profiles.Qualities; +using NzbDrone.Core.Qualities; +using Lidarr.Http; + +namespace Lidarr.Api.V3.Profiles.Quality +{ + public class QualityProfileSchemaModule : LidarrRestModule + { + private readonly IQualityDefinitionService _qualityDefinitionService; + + public QualityProfileSchemaModule(IQualityDefinitionService qualityDefinitionService) + : base("/qualityprofile/schema") + { + _qualityDefinitionService = qualityDefinitionService; + + GetResourceSingle = GetSchema; + } + + private QualityProfileResource GetSchema() + { + var items = _qualityDefinitionService.All() + .OrderBy(v => v.Weight) + .Select(v => new ProfileQualityItem { Quality = v.Quality, Allowed = false }) + .ToList(); + + var qualityProfile = new Profile(); + qualityProfile.Cutoff = NzbDrone.Core.Qualities.Quality.Unknown; + qualityProfile.Items = items; + + return qualityProfile.ToResource(); + } + } +} \ No newline at end of file diff --git a/src/Lidarr.Api.V3/Profiles/Quality/QualityProfileValidation.cs b/src/Lidarr.Api.V3/Profiles/Quality/QualityProfileValidation.cs new file mode 100644 index 000000000..7382dbefb --- /dev/null +++ b/src/Lidarr.Api.V3/Profiles/Quality/QualityProfileValidation.cs @@ -0,0 +1,43 @@ +using System.Collections.Generic; +using System.Linq; +using FluentValidation; +using FluentValidation.Validators; + +namespace Lidarr.Api.V3.Profiles.Quality +{ + public static class QualityProfileValidation + { + public static IRuleBuilderOptions> MustHaveAllowedQuality(this IRuleBuilder> ruleBuilder) + { + ruleBuilder.SetValidator(new NotEmptyValidator(null)); + + return ruleBuilder.SetValidator(new AllowedValidator()); + } + } + + public class AllowedValidator : PropertyValidator + { + public AllowedValidator() + : base("Must contain at least one allowed quality") + { + + } + + protected override bool IsValid(PropertyValidatorContext context) + { + var list = context.PropertyValue as IList; + + if (list == null) + { + return false; + } + + if (!list.Any(c => c.Allowed)) + { + return false; + } + + return true; + } + } +} diff --git a/src/Lidarr.Api.V3/Properties/AssemblyInfo.cs b/src/Lidarr.Api.V3/Properties/AssemblyInfo.cs new file mode 100644 index 000000000..8cfbdf950 --- /dev/null +++ b/src/Lidarr.Api.V3/Properties/AssemblyInfo.cs @@ -0,0 +1,9 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +[assembly: AssemblyTitle("Lidarr.Api")] + +[assembly: Guid("4c0922d7-979e-4ff7-b44b-b8ac2100eeb5")] + +[assembly: InternalsVisibleTo("Lidarr.Core")] diff --git a/src/Lidarr.Api.V3/ProviderModuleBase.cs b/src/Lidarr.Api.V3/ProviderModuleBase.cs new file mode 100644 index 000000000..43d7228a0 --- /dev/null +++ b/src/Lidarr.Api.V3/ProviderModuleBase.cs @@ -0,0 +1,187 @@ +using System.Collections.Generic; +using System.Linq; +using FluentValidation; +using FluentValidation.Results; +using Nancy; +using Newtonsoft.Json; +using NzbDrone.Core.ThingiProvider; +using NzbDrone.Core.Validation; +using Lidarr.Http; +using Lidarr.Http.Extensions; + +namespace Lidarr.Api.V3 +{ + public abstract class ProviderModuleBase : LidarrRestModule + where TProviderDefinition : ProviderDefinition, new() + where TProvider : IProvider + where TProviderResource : ProviderResource, new() + { + private readonly IProviderFactory _providerFactory; + private readonly ProviderResourceMapper _resourceMapper; + + protected ProviderModuleBase(IProviderFactory providerFactory, string resource, ProviderResourceMapper resourceMapper) + : base(resource) + { + _providerFactory = providerFactory; + _resourceMapper = resourceMapper; + + Get["schema"] = x => GetTemplates(); + Post["test"] = x => Test(ReadResourceFromRequest(true)); + Post["action/{action}"] = x => RequestAction(x.action, ReadResourceFromRequest(true)); + + GetResourceAll = GetAll; + GetResourceById = GetProviderById; + CreateResource = CreateProvider; + UpdateResource = UpdateProvider; + DeleteResource = DeleteProvider; + + SharedValidator.RuleFor(c => c.Name).NotEmpty(); + SharedValidator.RuleFor(c => c.Name).Must((v,c) => !_providerFactory.All().Any(p => p.Name == c && p.Id != v.Id)).WithMessage("Should be unique"); + SharedValidator.RuleFor(c => c.Implementation).NotEmpty(); + SharedValidator.RuleFor(c => c.ConfigContract).NotEmpty(); + + PostValidator.RuleFor(c => c.Fields).NotNull(); + } + + private TProviderResource GetProviderById(int id) + { + var definition = _providerFactory.Get(id); + _providerFactory.SetProviderCharacteristics(definition); + + return _resourceMapper.ToResource(definition); + } + + private List GetAll() + { + var providerDefinitions = _providerFactory.All().OrderBy(p => p.ImplementationName); + + var result = new List(providerDefinitions.Count()); + + foreach (var definition in providerDefinitions) + { + _providerFactory.SetProviderCharacteristics(definition); + + result.Add(_resourceMapper.ToResource(definition)); + } + + return result.OrderBy(p => p.Name).ToList(); + } + + private int CreateProvider(TProviderResource providerResource) + { + var providerDefinition = GetDefinition(providerResource, false); + + if (providerDefinition.Enable) + { + Test(providerDefinition, false); + } + + providerDefinition = _providerFactory.Create(providerDefinition); + + return providerDefinition.Id; + } + + private void UpdateProvider(TProviderResource providerResource) + { + var providerDefinition = GetDefinition(providerResource, false); + + if (providerDefinition.Enable) + { + Test(providerDefinition, false); + } + + _providerFactory.Update(providerDefinition); + } + + private TProviderDefinition GetDefinition(TProviderResource providerResource, bool includeWarnings = false, bool validate = true) + { + var definition = _resourceMapper.ToModel(providerResource); + + if (validate) + { + Validate(definition, includeWarnings); + } + + return definition; + } + + private void DeleteProvider(int id) + { + _providerFactory.Delete(id); + } + + private Response GetTemplates() + { + var defaultDefinitions = _providerFactory.GetDefaultDefinitions().OrderBy(p => p.ImplementationName).ToList(); + + var result = new List(defaultDefinitions.Count()); + + foreach (var providerDefinition in defaultDefinitions) + { + var providerResource = _resourceMapper.ToResource(providerDefinition); + var presetDefinitions = _providerFactory.GetPresetDefinitions(providerDefinition); + + providerResource.Presets = presetDefinitions.Select(v => + { + var presetResource = _resourceMapper.ToResource(v); + + return presetResource as ProviderResource; + }).ToList(); + + result.Add(providerResource); + } + + return result.AsResponse(); + } + + private Response Test(TProviderResource providerResource) + { + var providerDefinition = GetDefinition(providerResource, true); + + Test(providerDefinition, true); + + return "{}"; + } + + private Response RequestAction(string action, TProviderResource providerResource) + { + var providerDefinition = GetDefinition(providerResource, true, false); + + var query = ((IDictionary)Request.Query.ToDictionary()).ToDictionary(k => k.Key, k => k.Value.ToString()); + + var data = _providerFactory.RequestAction(providerDefinition, action, query); + Response resp = JsonConvert.SerializeObject(data); + resp.ContentType = "application/json"; + return resp; + } + + protected virtual void Validate(TProviderDefinition definition, bool includeWarnings) + { + var validationResult = definition.Settings.Validate(); + + VerifyValidationResult(validationResult, includeWarnings); + } + + protected virtual void Test(TProviderDefinition definition, bool includeWarnings) + { + var validationResult = _providerFactory.Test(definition); + + VerifyValidationResult(validationResult, includeWarnings); + } + + protected void VerifyValidationResult(ValidationResult validationResult, bool includeWarnings) + { + var result = new NzbDroneValidationResult(validationResult.Errors); + + if (includeWarnings && (!result.IsValid || result.HasWarnings)) + { + throw new ValidationException(result.Failures); + } + + if (!result.IsValid) + { + throw new ValidationException(result.Errors); + } + } + } +} diff --git a/src/Lidarr.Api.V3/ProviderResource.cs b/src/Lidarr.Api.V3/ProviderResource.cs new file mode 100644 index 000000000..e9aa78c80 --- /dev/null +++ b/src/Lidarr.Api.V3/ProviderResource.cs @@ -0,0 +1,70 @@ +using System.Collections.Generic; +using NzbDrone.Common.Reflection; +using NzbDrone.Core.ThingiProvider; +using Lidarr.Http.ClientSchema; +using Lidarr.Http.REST; + +namespace Lidarr.Api.V3 +{ + public class ProviderResource : RestResource + { + public string Name { get; set; } + public List Fields { get; set; } + public string ImplementationName { get; set; } + public string Implementation { get; set; } + public string ConfigContract { get; set; } + public string InfoLink { get; set; } + public ProviderMessage Message { get; set; } + public HashSet Tags { get; set; } + + public List Presets { get; set; } + } + + public class ProviderResourceMapper + where TProviderResource : ProviderResource, new() + where TProviderDefinition : ProviderDefinition, new() + { + public virtual TProviderResource ToResource(TProviderDefinition definition) + + { + return new TProviderResource + { + Id = definition.Id, + + Name = definition.Name, + ImplementationName = definition.ImplementationName, + Implementation = definition.Implementation, + ConfigContract = definition.ConfigContract, + Message = definition.Message, + Tags = definition.Tags, + Fields = SchemaBuilder.ToSchema(definition.Settings), + + InfoLink = string.Format("https://github.com/Sonarr/Sonarr/wiki/Supported-{0}#{1}", + typeof(TProviderResource).Name.Replace("Resource", "s"), + definition.Implementation.ToLower()) + }; + } + + public virtual TProviderDefinition ToModel(TProviderResource resource) + { + if (resource == null) return default(TProviderDefinition); + + var definition = new TProviderDefinition + { + Id = resource.Id, + + Name = resource.Name, + ImplementationName = resource.ImplementationName, + Implementation = resource.Implementation, + ConfigContract = resource.ConfigContract, + Message = resource.Message, + Tags = resource.Tags + }; + + var configContract = ReflectionExtensions.CoreAssembly.FindTypeByName(definition.ConfigContract); + definition.Settings = (IProviderConfig)SchemaBuilder.ReadFromSchema(resource.Fields, configContract); + + return definition; + } + } +} \ No newline at end of file diff --git a/src/Lidarr.Api.V3/Qualities/QualityDefinitionModule.cs b/src/Lidarr.Api.V3/Qualities/QualityDefinitionModule.cs new file mode 100644 index 000000000..1da32a774 --- /dev/null +++ b/src/Lidarr.Api.V3/Qualities/QualityDefinitionModule.cs @@ -0,0 +1,54 @@ +using System.Collections.Generic; +using System.Linq; +using Nancy; +using NzbDrone.Core.Qualities; +using Lidarr.Http; +using Lidarr.Http.Extensions; + +namespace Lidarr.Api.V3.Qualities +{ + public class QualityDefinitionModule : LidarrRestModule + { + private readonly IQualityDefinitionService _qualityDefinitionService; + + public QualityDefinitionModule(IQualityDefinitionService qualityDefinitionService) + { + _qualityDefinitionService = qualityDefinitionService; + + GetResourceAll = GetAll; + GetResourceById = GetById; + UpdateResource = Update; + Put["/update"] = d => UpdateMany(); + } + + private void Update(QualityDefinitionResource resource) + { + var model = resource.ToModel(); + _qualityDefinitionService.Update(model); + } + + private QualityDefinitionResource GetById(int id) + { + return _qualityDefinitionService.GetById(id).ToResource(); + } + + private List GetAll() + { + return _qualityDefinitionService.All().ToResource(); + } + + private Response UpdateMany() + { + //Read from request + var qualityDefinitions = Request.Body.FromJson>() + .ToModel() + .ToList(); + + _qualityDefinitionService.UpdateMany(qualityDefinitions); + + return _qualityDefinitionService.All() + .ToResource() + .AsResponse(HttpStatusCode.Accepted); + } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Api/Qualities/QualityDefinitionResource.cs b/src/Lidarr.Api.V3/Qualities/QualityDefinitionResource.cs similarity index 86% rename from src/NzbDrone.Api/Qualities/QualityDefinitionResource.cs rename to src/Lidarr.Api.V3/Qualities/QualityDefinitionResource.cs index ea0edc0ab..7d2000c79 100644 --- a/src/NzbDrone.Api/Qualities/QualityDefinitionResource.cs +++ b/src/Lidarr.Api.V3/Qualities/QualityDefinitionResource.cs @@ -1,9 +1,9 @@ using System.Collections.Generic; using System.Linq; -using NzbDrone.Api.REST; using NzbDrone.Core.Qualities; +using Lidarr.Http.REST; -namespace NzbDrone.Api.Qualities +namespace Lidarr.Api.V3.Qualities { public class QualityDefinitionResource : RestResource { @@ -26,13 +26,9 @@ namespace NzbDrone.Api.Qualities return new QualityDefinitionResource { Id = model.Id, - Quality = model.Quality, - Title = model.Title, - Weight = model.Weight, - MinSize = model.MinSize, MaxSize = model.MaxSize }; @@ -45,13 +41,9 @@ namespace NzbDrone.Api.Qualities return new QualityDefinition { Id = resource.Id, - Quality = resource.Quality, - Title = resource.Title, - Weight = resource.Weight, - MinSize = resource.MinSize, MaxSize = resource.MaxSize }; @@ -61,5 +53,10 @@ namespace NzbDrone.Api.Qualities { return models.Select(ToResource).ToList(); } + + public static List ToModel(this IEnumerable resources) + { + return resources.Select(ToModel).ToList(); + } } } \ No newline at end of file diff --git a/src/Lidarr.Api.V3/Queue/QueueActionModule.cs b/src/Lidarr.Api.V3/Queue/QueueActionModule.cs new file mode 100644 index 000000000..d0ff7e818 --- /dev/null +++ b/src/Lidarr.Api.V3/Queue/QueueActionModule.cs @@ -0,0 +1,167 @@ +using System; +using System.Collections.Generic; +using Nancy; +using NzbDrone.Core.Download; +using NzbDrone.Core.Download.Pending; +using NzbDrone.Core.Download.TrackedDownloads; +using NzbDrone.Core.Queue; +using Lidarr.Http; +using Lidarr.Http.Extensions; +using Lidarr.Http.REST; + +namespace Lidarr.Api.V3.Queue +{ + public class QueueActionModule : LidarrRestModule + { + private readonly IQueueService _queueService; + private readonly ITrackedDownloadService _trackedDownloadService; + private readonly IFailedDownloadService _failedDownloadService; + private readonly IProvideDownloadClient _downloadClientProvider; + private readonly IPendingReleaseService _pendingReleaseService; + private readonly IDownloadService _downloadService; + + public QueueActionModule(IQueueService queueService, + ITrackedDownloadService trackedDownloadService, + IFailedDownloadService failedDownloadService, + IProvideDownloadClient downloadClientProvider, + IPendingReleaseService pendingReleaseService, + IDownloadService downloadService) + { + _queueService = queueService; + _trackedDownloadService = trackedDownloadService; + _failedDownloadService = failedDownloadService; + _downloadClientProvider = downloadClientProvider; + _pendingReleaseService = pendingReleaseService; + _downloadService = downloadService; + + Post[@"/grab/(?[\d]{1,10})"] = x => Grab((int)x.Id); + Post["/grab/bulk"] = x => Grab(); + + Delete[@"/(?[\d]{1,10})"] = x => Remove((int)x.Id); + Delete["/bulk"] = x => Remove(); + } + + private Response Grab(int id) + { + var pendingRelease = _pendingReleaseService.FindPendingQueueItem(id); + + if (pendingRelease == null) + { + throw new NotFoundException(); + } + + _downloadService.DownloadReport(pendingRelease.RemoteAlbum); + + return new object().AsResponse(); + } + + private Response Grab() + { + var resource = Request.Body.FromJson(); + + foreach (var id in resource.Ids) + { + var pendingRelease = _pendingReleaseService.FindPendingQueueItem(id); + + if (pendingRelease == null) + { + throw new NotFoundException(); + } + + _downloadService.DownloadReport(pendingRelease.RemoteAlbum); + } + + return new object().AsResponse(); + } + + private Response Remove(int id) + { + var blacklist = Request.GetBooleanQueryParameter("blacklist"); + + var trackedDownload = Remove(id, blacklist); + + if (trackedDownload != null) + { + _trackedDownloadService.StopTracking(trackedDownload.DownloadItem.DownloadId); + } + + return new object().AsResponse(); + } + + private Response Remove() + { + var blacklist = Request.GetBooleanQueryParameter("blacklist"); + + var resource = Request.Body.FromJson(); + var trackedDownloadIds = new List(); + + foreach (var id in resource.Ids) + { + var trackedDownload = Remove(id, blacklist); + + if (trackedDownload != null) + { + trackedDownloadIds.Add(trackedDownload.DownloadItem.DownloadId); + } + } + + _trackedDownloadService.StopTracking(trackedDownloadIds); + + return new object().AsResponse(); + } + + private TrackedDownload Remove(int id, bool blacklist) + { + var pendingRelease = _pendingReleaseService.FindPendingQueueItem(id); + + if (pendingRelease != null) + { + _pendingReleaseService.RemovePendingQueueItems(pendingRelease.Id); + + return null; + } + + var trackedDownload = GetTrackedDownload(id); + + if (trackedDownload == null) + { + throw new NotFoundException(); + } + + var downloadClient = _downloadClientProvider.Get(trackedDownload.DownloadClient); + + if (downloadClient == null) + { + throw new BadRequestException(); + } + + downloadClient.RemoveItem(trackedDownload.DownloadItem.DownloadId, true); + + if (blacklist) + { + _failedDownloadService.MarkAsFailed(trackedDownload.DownloadItem.DownloadId); + } + + return trackedDownload; + } + + private TrackedDownload GetTrackedDownload(int queueId) + { + var queueItem = _queueService.Find(queueId); + + if (queueItem == null) + { + throw new NotFoundException(); + } + + var trackedDownload = _trackedDownloadService.Find(queueItem.DownloadId); + + if (trackedDownload == null) + { + throw new NotFoundException(); + } + + return trackedDownload; + } + } +} diff --git a/src/Lidarr.Api.V3/Queue/QueueBulkResource.cs b/src/Lidarr.Api.V3/Queue/QueueBulkResource.cs new file mode 100644 index 000000000..b1024846e --- /dev/null +++ b/src/Lidarr.Api.V3/Queue/QueueBulkResource.cs @@ -0,0 +1,9 @@ +using System.Collections.Generic; + +namespace Lidarr.Api.V3.Queue +{ + public class QueueBulkResource + { + public List Ids { get; set; } + } +} diff --git a/src/Lidarr.Api.V3/Queue/QueueDetailsModule.cs b/src/Lidarr.Api.V3/Queue/QueueDetailsModule.cs new file mode 100644 index 000000000..4adda5669 --- /dev/null +++ b/src/Lidarr.Api.V3/Queue/QueueDetailsModule.cs @@ -0,0 +1,68 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using NzbDrone.Core.Datastore.Events; +using NzbDrone.Core.Download.Pending; +using NzbDrone.Core.Messaging.Events; +using NzbDrone.Core.Queue; +using NzbDrone.SignalR; +using Lidarr.Http; +using Lidarr.Http.Extensions; + +namespace Lidarr.Api.V3.Queue +{ + public class QueueDetailsModule : LidarrRestModuleWithSignalR, + IHandle, IHandle + { + private readonly IQueueService _queueService; + private readonly IPendingReleaseService _pendingReleaseService; + + public QueueDetailsModule(IBroadcastSignalRMessage broadcastSignalRMessage, IQueueService queueService, IPendingReleaseService pendingReleaseService) + : base(broadcastSignalRMessage, "queue/details") + { + _queueService = queueService; + _pendingReleaseService = pendingReleaseService; + GetResourceAll = GetQueue; + } + + private List GetQueue() + { + var includeSeries = Request.GetBooleanQueryParameter("includeSeries"); + var includeEpisode = Request.GetBooleanQueryParameter("includeEpisode", true); + var queue = _queueService.GetQueue(); + var pending = _pendingReleaseService.GetPendingQueue(); + var fullQueue = queue.Concat(pending); + + var artistIdQuery = Request.Query.ArtistId; + var albumIdsQuery = Request.Query.AlbumIds; + + if (artistIdQuery.HasValue) + { + return fullQueue.Where(q => q.Artist.Id == (int)artistIdQuery).ToResource(includeSeries, includeEpisode); + } + + if (albumIdsQuery.HasValue) + { + string albumIdsValue = albumIdsQuery.Value.ToString(); + + var albumIds = albumIdsValue.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries) + .Select(e => Convert.ToInt32(e)) + .ToList(); + + return fullQueue.Where(q => albumIds.Contains(q.Album.Id)).ToResource(includeSeries, includeEpisode); + } + + return fullQueue.ToResource(includeSeries, includeEpisode); + } + + public void Handle(QueueUpdatedEvent message) + { + BroadcastResourceChange(ModelAction.Sync); + } + + public void Handle(PendingReleasesUpdatedEvent message) + { + BroadcastResourceChange(ModelAction.Sync); + } + } +} diff --git a/src/Lidarr.Api.V3/Queue/QueueModule.cs b/src/Lidarr.Api.V3/Queue/QueueModule.cs new file mode 100644 index 000000000..8ecfdccfc --- /dev/null +++ b/src/Lidarr.Api.V3/Queue/QueueModule.cs @@ -0,0 +1,113 @@ +using System; +using System.Linq; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.Datastore; +using NzbDrone.Core.Datastore.Events; +using NzbDrone.Core.Download.Pending; +using NzbDrone.Core.Messaging.Events; +using NzbDrone.Core.Queue; +using NzbDrone.SignalR; +using Lidarr.Http; +using Lidarr.Http.Extensions; + +namespace Lidarr.Api.V3.Queue +{ + public class QueueModule : LidarrRestModuleWithSignalR, + IHandle, IHandle + { + private readonly IQueueService _queueService; + private readonly IPendingReleaseService _pendingReleaseService; + + public QueueModule(IBroadcastSignalRMessage broadcastSignalRMessage, IQueueService queueService, IPendingReleaseService pendingReleaseService) + : base(broadcastSignalRMessage) + { + _queueService = queueService; + _pendingReleaseService = pendingReleaseService; + GetResourcePaged = GetQueue; + } + + private PagingResource GetQueue(PagingResource pagingResource) + { + var pagingSpec = pagingResource.MapToPagingSpec("timeleft", SortDirection.Ascending); + var includeArtist = Request.GetBooleanQueryParameter("includeArtist"); + var includeAlbum = Request.GetBooleanQueryParameter("includeAlbum"); + + return ApplyToPage(GetQueue, pagingSpec, (q) => MapToResource(q, includeArtist, includeAlbum)); + } + + private PagingSpec GetQueue(PagingSpec pagingSpec) + { + var ascending = pagingSpec.SortDirection == SortDirection.Ascending; + var orderByFunc = GetOrderByFunc(pagingSpec); + + var queue = _queueService.GetQueue(); + var pending = _pendingReleaseService.GetPendingQueue(); + var fullQueue = queue.Concat(pending).ToList(); + IOrderedEnumerable ordered; + + if (pagingSpec.SortKey == "timeleft") + { + ordered = ascending ? fullQueue.OrderBy(q => q.Timeleft, new TimeleftComparer()) : + fullQueue.OrderByDescending(q => q.Timeleft, new TimeleftComparer()); + } + + else if (pagingSpec.SortKey == "estimatedCompletionTime") + { + ordered = ascending ? fullQueue.OrderBy(q => q.EstimatedCompletionTime, new EstimatedCompletionTimeComparer()) : + fullQueue.OrderByDescending(q => q.EstimatedCompletionTime, new EstimatedCompletionTimeComparer()); + } + + else + { + ordered = ascending ? fullQueue.OrderBy(orderByFunc) : fullQueue.OrderByDescending(orderByFunc); + } + + ordered = ordered.ThenByDescending(q => 100 - q.Sizeleft / q.Size * 100); + + pagingSpec.Records = ordered.Skip((pagingSpec.Page - 1) * pagingSpec.PageSize).Take(pagingSpec.PageSize).ToList(); + pagingSpec.TotalRecords = fullQueue.Count; + + if (pagingSpec.Records.Empty() && pagingSpec.Page > 1) + { + pagingSpec.Page = (int)Math.Max(Math.Ceiling((decimal)(pagingSpec.TotalRecords / pagingSpec.PageSize)), 1); + pagingSpec.Records = ordered.Skip((pagingSpec.Page - 1) * pagingSpec.PageSize).Take(pagingSpec.PageSize).ToList(); + } + + return pagingSpec; + } + + private Func GetOrderByFunc(PagingSpec pagingSpec) + { + switch (pagingSpec.SortKey) + { + case "series.sortTitle": + return q => q.Artist.SortName; + case "episode": + return q => q.Album; + case "episode.title": + return q => q.Album.Title; + case "quality": + return q => q.Quality; + case "progress": + return q => 100 - q.Sizeleft / q.Size * 100; + default: + return q => q.Timeleft; + } + } + + private QueueResource MapToResource(NzbDrone.Core.Queue.Queue queueItem, bool includeSeries, bool includeEpisode) + { + return queueItem.ToResource(includeSeries, includeEpisode); + } + + public void Handle(QueueUpdatedEvent message) + { + BroadcastResourceChange(ModelAction.Sync); + } + + public void Handle(PendingReleasesUpdatedEvent message) + { + BroadcastResourceChange(ModelAction.Sync); + } + } +} diff --git a/src/Lidarr.Api.V3/Queue/QueueResource.cs b/src/Lidarr.Api.V3/Queue/QueueResource.cs new file mode 100644 index 000000000..e37a0155b --- /dev/null +++ b/src/Lidarr.Api.V3/Queue/QueueResource.cs @@ -0,0 +1,70 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using NzbDrone.Core.Download.TrackedDownloads; +using NzbDrone.Core.Indexers; +using NzbDrone.Core.Qualities; +using Lidarr.Api.V3.Albums; +using Lidarr.Api.V3.Artist; +using Lidarr.Http.REST; + +namespace Lidarr.Api.V3.Queue +{ + public class QueueResource : RestResource + { + public int ArtistId { get; set; } + public int AlbumId { get; set; } + public ArtistResource Artist { get; set; } + public AlbumResource Album { get; set; } + public QualityModel Quality { get; set; } + public decimal Size { get; set; } + public string Title { get; set; } + public decimal Sizeleft { get; set; } + public TimeSpan? Timeleft { get; set; } + public DateTime? EstimatedCompletionTime { get; set; } + public string Status { get; set; } + public string TrackedDownloadStatus { get; set; } + public List StatusMessages { get; set; } + public string ErrorMessage { get; set; } + public string DownloadId { get; set; } + public DownloadProtocol Protocol { get; set; } + public string DownloadClient { get; set; } + public string Indexer { get; set; } + } + + public static class QueueResourceMapper + { + public static QueueResource ToResource(this NzbDrone.Core.Queue.Queue model, bool includeSeries, bool includeEpisode) + { + if (model == null) return null; + + return new QueueResource + { + Id = model.Id, + ArtistId = model.Artist.Id, + AlbumId = model.Album.Id, + Artist = includeSeries ? model.Artist.ToResource() : null, + Album = includeEpisode ? model.Album.ToResource() : null, + Quality = model.Quality, + Size = model.Size, + Title = model.Title, + Sizeleft = model.Sizeleft, + Timeleft = model.Timeleft, + EstimatedCompletionTime = model.EstimatedCompletionTime, + Status = model.Status, + TrackedDownloadStatus = model.TrackedDownloadStatus, + StatusMessages = model.StatusMessages, + ErrorMessage = model.ErrorMessage, + DownloadId = model.DownloadId, + Protocol = model.Protocol, + DownloadClient = model.DownloadClient, + Indexer = model.Indexer + }; + } + + public static List ToResource(this IEnumerable models, bool includeSeries, bool includeEpisode) + { + return models.Select((m) => ToResource(m, includeSeries, includeEpisode)).ToList(); + } + } +} diff --git a/src/Lidarr.Api.V3/Queue/QueueStatusModule.cs b/src/Lidarr.Api.V3/Queue/QueueStatusModule.cs new file mode 100644 index 000000000..09b59fc33 --- /dev/null +++ b/src/Lidarr.Api.V3/Queue/QueueStatusModule.cs @@ -0,0 +1,56 @@ +using System; +using System.Linq; +using Nancy.Responses; +using NzbDrone.Core.Datastore.Events; +using NzbDrone.Core.Download.Pending; +using NzbDrone.Core.Messaging.Events; +using NzbDrone.Core.Queue; +using NzbDrone.SignalR; +using Lidarr.Http; +using Lidarr.Http.Extensions; + +namespace Lidarr.Api.V3.Queue +{ + public class QueueStatusModule : LidarrRestModuleWithSignalR, + IHandle, IHandle + { + private readonly IQueueService _queueService; + private readonly IPendingReleaseService _pendingReleaseService; + + public QueueStatusModule(IBroadcastSignalRMessage broadcastSignalRMessage, IQueueService queueService, IPendingReleaseService pendingReleaseService) + : base(broadcastSignalRMessage, "queue/status") + { + _queueService = queueService; + _pendingReleaseService = pendingReleaseService; + Get["/"] = x => GetQueueStatusResponse(); + } + + private JsonResponse GetQueueStatusResponse() + { + return GetQueueStatus().AsResponse(); + } + + private QueueStatusResource GetQueueStatus() + { + var queue = _queueService.GetQueue(); + var pending = _pendingReleaseService.GetPendingQueue(); + + return new QueueStatusResource + { + Count = queue.Count + pending.Count, + Errors = queue.Any(q => q.TrackedDownloadStatus.Equals("Error", StringComparison.InvariantCultureIgnoreCase)), + Warnings = queue.Any(q => q.TrackedDownloadStatus.Equals("Warning", StringComparison.InvariantCultureIgnoreCase)) + }; + } + + public void Handle(QueueUpdatedEvent message) + { + BroadcastResourceChange(ModelAction.Updated, GetQueueStatus()); + } + + public void Handle(PendingReleasesUpdatedEvent message) + { + BroadcastResourceChange(ModelAction.Updated, GetQueueStatus()); + } + } +} \ No newline at end of file diff --git a/src/Lidarr.Api.V3/Queue/QueueStatusResource.cs b/src/Lidarr.Api.V3/Queue/QueueStatusResource.cs new file mode 100644 index 000000000..82261abc3 --- /dev/null +++ b/src/Lidarr.Api.V3/Queue/QueueStatusResource.cs @@ -0,0 +1,11 @@ +using Lidarr.Http.REST; + +namespace Lidarr.Api.V3.Queue +{ + public class QueueStatusResource : RestResource + { + public int Count { get; set; } + public bool Errors { get; set; } + public bool Warnings { get; set; } + } +} diff --git a/src/NzbDrone.Api/RemotePathMappings/RemotePathMappingModule.cs b/src/Lidarr.Api.V3/RemotePathMappings/RemotePathMappingModule.cs similarity index 93% rename from src/NzbDrone.Api/RemotePathMappings/RemotePathMappingModule.cs rename to src/Lidarr.Api.V3/RemotePathMappings/RemotePathMappingModule.cs index a61b5f7b3..cbdffa155 100644 --- a/src/NzbDrone.Api/RemotePathMappings/RemotePathMappingModule.cs +++ b/src/Lidarr.Api.V3/RemotePathMappings/RemotePathMappingModule.cs @@ -2,10 +2,11 @@ using FluentValidation; using NzbDrone.Core.RemotePathMappings; using NzbDrone.Core.Validation.Paths; +using Lidarr.Http; -namespace NzbDrone.Api.RemotePathMappings +namespace Lidarr.Api.V3.RemotePathMappings { - public class RemotePathMappingModule : NzbDroneRestModule + public class RemotePathMappingModule : LidarrRestModule { private readonly IRemotePathMappingService _remotePathMappingService; diff --git a/src/NzbDrone.Api/RemotePathMappings/RemotePathMappingResource.cs b/src/Lidarr.Api.V3/RemotePathMappings/RemotePathMappingResource.cs similarity index 95% rename from src/NzbDrone.Api/RemotePathMappings/RemotePathMappingResource.cs rename to src/Lidarr.Api.V3/RemotePathMappings/RemotePathMappingResource.cs index 60c01b682..e1b716ba6 100644 --- a/src/NzbDrone.Api/RemotePathMappings/RemotePathMappingResource.cs +++ b/src/Lidarr.Api.V3/RemotePathMappings/RemotePathMappingResource.cs @@ -1,9 +1,9 @@ using System.Collections.Generic; using System.Linq; -using NzbDrone.Api.REST; using NzbDrone.Core.RemotePathMappings; +using Lidarr.Http.REST; -namespace NzbDrone.Api.RemotePathMappings +namespace Lidarr.Api.V3.RemotePathMappings { public class RemotePathMappingResource : RestResource { diff --git a/src/Lidarr.Api.V3/Restrictions/RestrictionModule.cs b/src/Lidarr.Api.V3/Restrictions/RestrictionModule.cs new file mode 100644 index 000000000..810eb821a --- /dev/null +++ b/src/Lidarr.Api.V3/Restrictions/RestrictionModule.cs @@ -0,0 +1,60 @@ +using System.Collections.Generic; +using FluentValidation.Results; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.Restrictions; +using Lidarr.Http; + +namespace Lidarr.Api.V3.Restrictions +{ + public class RestrictionModule : LidarrRestModule + { + private readonly IRestrictionService _restrictionService; + + + public RestrictionModule(IRestrictionService restrictionService) + { + _restrictionService = restrictionService; + + GetResourceById = Get; + GetResourceAll = GetAll; + CreateResource = Create; + UpdateResource = Update; + DeleteResource = Delete; + + SharedValidator.Custom(restriction => + { + if (restriction.Ignored.IsNullOrWhiteSpace() && restriction.Required.IsNullOrWhiteSpace()) + { + return new ValidationFailure("", "Either 'Must contain' or 'Must not contain' is required"); + } + + return null; + }); + } + + private RestrictionResource Get(int id) + { + return _restrictionService.Get(id).ToResource(); + } + + private List GetAll() + { + return _restrictionService.All().ToResource(); + } + + private int Create(RestrictionResource resource) + { + return _restrictionService.Add(resource.ToModel()).Id; + } + + private void Update(RestrictionResource resource) + { + _restrictionService.Update(resource.ToModel()); + } + + private void Delete(int id) + { + _restrictionService.Delete(id); + } + } +} diff --git a/src/NzbDrone.Api/Restrictions/RestrictionResource.cs b/src/Lidarr.Api.V3/Restrictions/RestrictionResource.cs similarity index 96% rename from src/NzbDrone.Api/Restrictions/RestrictionResource.cs rename to src/Lidarr.Api.V3/Restrictions/RestrictionResource.cs index 14085e820..8c8689cd6 100644 --- a/src/NzbDrone.Api/Restrictions/RestrictionResource.cs +++ b/src/Lidarr.Api.V3/Restrictions/RestrictionResource.cs @@ -1,9 +1,9 @@ using System.Collections.Generic; using System.Linq; -using NzbDrone.Api.REST; using NzbDrone.Core.Restrictions; +using Lidarr.Http.REST; -namespace NzbDrone.Api.Restrictions +namespace Lidarr.Api.V3.Restrictions { public class RestrictionResource : RestResource { diff --git a/src/Lidarr.Api.V3/RootFolders/RootFolderModule.cs b/src/Lidarr.Api.V3/RootFolders/RootFolderModule.cs new file mode 100644 index 000000000..9258db3f6 --- /dev/null +++ b/src/Lidarr.Api.V3/RootFolders/RootFolderModule.cs @@ -0,0 +1,58 @@ +using System.Collections.Generic; +using FluentValidation; +using NzbDrone.Core.RootFolders; +using NzbDrone.Core.Validation.Paths; +using NzbDrone.SignalR; +using Lidarr.Http; + +namespace Lidarr.Api.V3.RootFolders +{ + public class RootFolderModule : LidarrRestModuleWithSignalR + { + private readonly IRootFolderService _rootFolderService; + + public RootFolderModule(IRootFolderService rootFolderService, + IBroadcastSignalRMessage signalRBroadcaster, + RootFolderValidator rootFolderValidator, + PathExistsValidator pathExistsValidator, + MappedNetworkDriveValidator mappedNetworkDriveValidator) + : base(signalRBroadcaster) + { + _rootFolderService = rootFolderService; + + GetResourceAll = GetRootFolders; + GetResourceById = GetRootFolder; + CreateResource = CreateRootFolder; + DeleteResource = DeleteFolder; + + SharedValidator.RuleFor(c => c.Path) + .Cascade(CascadeMode.StopOnFirstFailure) + .IsValidPath() + .SetValidator(rootFolderValidator) + .SetValidator(mappedNetworkDriveValidator) + .SetValidator(pathExistsValidator); + } + + private RootFolderResource GetRootFolder(int id) + { + return _rootFolderService.Get(id).ToResource(); + } + + private int CreateRootFolder(RootFolderResource rootFolderResource) + { + var model = rootFolderResource.ToModel(); + + return _rootFolderService.Add(model).Id; + } + + private List GetRootFolders() + { + return _rootFolderService.AllWithUnmappedFolders().ToResource(); + } + + private void DeleteFolder(int id) + { + _rootFolderService.Remove(id); + } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Api/RootFolders/RootFolderResource.cs b/src/Lidarr.Api.V3/RootFolders/RootFolderResource.cs similarity index 95% rename from src/NzbDrone.Api/RootFolders/RootFolderResource.cs rename to src/Lidarr.Api.V3/RootFolders/RootFolderResource.cs index 86efef529..992f112be 100644 --- a/src/NzbDrone.Api/RootFolders/RootFolderResource.cs +++ b/src/Lidarr.Api.V3/RootFolders/RootFolderResource.cs @@ -1,9 +1,9 @@ using System.Collections.Generic; using System.Linq; -using NzbDrone.Api.REST; using NzbDrone.Core.RootFolders; +using Lidarr.Http.REST; -namespace NzbDrone.Api.RootFolders +namespace Lidarr.Api.V3.RootFolders { public class RootFolderResource : RestResource { diff --git a/src/Lidarr.Api.V3/SonarrV3FeedModule.cs b/src/Lidarr.Api.V3/SonarrV3FeedModule.cs new file mode 100644 index 000000000..0c6e838b2 --- /dev/null +++ b/src/Lidarr.Api.V3/SonarrV3FeedModule.cs @@ -0,0 +1,12 @@ +using Nancy; + +namespace Lidarr.Api.V3 +{ + public abstract class SonarrV3FeedModule : NancyModule + { + protected SonarrV3FeedModule(string resource) + : base("/feed/v3/" + resource.Trim('/')) + { + } + } +} \ No newline at end of file diff --git a/src/Lidarr.Api.V3/SonarrV3Module.cs b/src/Lidarr.Api.V3/SonarrV3Module.cs new file mode 100644 index 000000000..112368f9f --- /dev/null +++ b/src/Lidarr.Api.V3/SonarrV3Module.cs @@ -0,0 +1,12 @@ +using Nancy; + +namespace Lidarr.Api.V3 +{ + public abstract class SonarrV3Module : NancyModule + { + protected SonarrV3Module(string resource) + : base("/api/v3/" + resource.Trim('/')) + { + } + } +} \ No newline at end of file diff --git a/src/Lidarr.Api.V3/System/Backup/BackupModule.cs b/src/Lidarr.Api.V3/System/Backup/BackupModule.cs new file mode 100644 index 000000000..9bf93146d --- /dev/null +++ b/src/Lidarr.Api.V3/System/Backup/BackupModule.cs @@ -0,0 +1,35 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; +using NzbDrone.Core.Backup; +using Lidarr.Http; + +namespace Lidarr.Api.V3.System.Backup +{ + public class BackupModule : LidarrRestModule + { + private readonly IBackupService _backupService; + + public BackupModule(IBackupService backupService) : base("system/backup") + { + _backupService = backupService; + GetResourceAll = GetBackupFiles; + } + + public List GetBackupFiles() + { + var backups = _backupService.GetBackups(); + + return backups.Select(b => new BackupResource + { + Id = b.Name.GetHashCode(), + Name = b.Name, + Path = $"/backup/{b.Type.ToString().ToLower()}/{b.Name}", + Type = b.Type, + Time = b.Time + }) + .OrderByDescending(b => b.Time) + .ToList(); + } + } +} diff --git a/src/NzbDrone.Api/System/Backup/BackupResource.cs b/src/Lidarr.Api.V3/System/Backup/BackupResource.cs similarity index 81% rename from src/NzbDrone.Api/System/Backup/BackupResource.cs rename to src/Lidarr.Api.V3/System/Backup/BackupResource.cs index 7eac82838..243ccd5bf 100644 --- a/src/NzbDrone.Api/System/Backup/BackupResource.cs +++ b/src/Lidarr.Api.V3/System/Backup/BackupResource.cs @@ -1,8 +1,8 @@ using System; -using NzbDrone.Api.REST; using NzbDrone.Core.Backup; +using Lidarr.Http.REST; -namespace NzbDrone.Api.System.Backup +namespace Lidarr.Api.V3.System.Backup { public class BackupResource : RestResource { diff --git a/src/NzbDrone.Api/System/SystemModule.cs b/src/Lidarr.Api.V3/System/SystemModule.cs similarity index 92% rename from src/NzbDrone.Api/System/SystemModule.cs rename to src/Lidarr.Api.V3/System/SystemModule.cs index c62ed3b9e..5017c5ed4 100644 --- a/src/NzbDrone.Api/System/SystemModule.cs +++ b/src/Lidarr.Api.V3/System/SystemModule.cs @@ -1,15 +1,15 @@ using Nancy; using Nancy.Routing; -using NzbDrone.Api.Extensions; using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.Extensions; using NzbDrone.Core.Configuration; using NzbDrone.Core.Datastore; using NzbDrone.Core.Lifecycle; +using Lidarr.Http.Extensions; -namespace NzbDrone.Api.System +namespace Lidarr.Api.V3.System { - public class SystemModule : NzbDroneApiModule + public class SystemModule : SonarrV3Module { private readonly IAppFolderInfo _appFolderInfo; private readonly IRuntimeInfo _runtimeInfo; @@ -27,7 +27,8 @@ namespace NzbDrone.Api.System IRouteCacheProvider routeCacheProvider, IConfigFileProvider configFileProvider, IMainDatabase database, - ILifecycleService lifecycleService) : base("system") + ILifecycleService lifecycleService) + : base("system") { _appFolderInfo = appFolderInfo; _runtimeInfo = runtimeInfo; @@ -46,7 +47,7 @@ namespace NzbDrone.Api.System private Response GetStatus() { return new - { + { Version = BuildInfo.Version.ToString(), BuildTime = BuildInfo.BuildDateTime, IsDebug = BuildInfo.IsDebug, @@ -62,6 +63,7 @@ namespace NzbDrone.Api.System IsLinux = OsInfo.IsLinux, IsOsx = OsInfo.IsOsx, IsWindows = OsInfo.IsWindows, + Mode = _runtimeInfo.Mode, Branch = _configFileProvider.Branch, Authentication = _configFileProvider.AuthenticationMethod, SqliteVersion = _database.Version, diff --git a/src/Lidarr.Api.V3/System/Tasks/TaskModule.cs b/src/Lidarr.Api.V3/System/Tasks/TaskModule.cs new file mode 100644 index 000000000..e23a8567e --- /dev/null +++ b/src/Lidarr.Api.V3/System/Tasks/TaskModule.cs @@ -0,0 +1,67 @@ +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; +using NzbDrone.Core.Datastore.Events; +using NzbDrone.Core.Jobs; +using NzbDrone.Core.Messaging.Events; +using NzbDrone.SignalR; +using Lidarr.Http; + +namespace Lidarr.Api.V3.System.Tasks +{ + public class TaskModule : LidarrRestModuleWithSignalR, IHandle + { + private readonly ITaskManager _taskManager; + + private static readonly Regex NameRegex = new Regex("(? GetAll() + { + return _taskManager.GetAll() + .Select(ConvertToResource) + .OrderBy(t => t.Name) + .ToList(); + } + + private TaskResource GetTask(int id) + { + var task = _taskManager.GetAll() + .SingleOrDefault(t => t.Id == id); + + if (task == null) + { + return null; + } + + return ConvertToResource(task); + } + + private static TaskResource ConvertToResource(ScheduledTask scheduledTask) + { + var taskName = scheduledTask.TypeName.Split('.').Last().Replace("Command", ""); + + return new TaskResource + { + Id = scheduledTask.Id, + Name = NameRegex.Replace(taskName, match => " " + match.Value), + TaskName = taskName, + Interval = scheduledTask.Interval, + LastExecution = scheduledTask.LastExecution, + NextExecution = scheduledTask.LastExecution.AddMinutes(scheduledTask.Interval) + }; + } + + public void Handle(CommandExecutedEvent message) + { + BroadcastResourceChange(ModelAction.Sync); + } + } +} diff --git a/src/NzbDrone.Api/System/Tasks/TaskResource.cs b/src/Lidarr.Api.V3/System/Tasks/TaskResource.cs similarity index 83% rename from src/NzbDrone.Api/System/Tasks/TaskResource.cs rename to src/Lidarr.Api.V3/System/Tasks/TaskResource.cs index fda392cae..9c6d6385b 100644 --- a/src/NzbDrone.Api/System/Tasks/TaskResource.cs +++ b/src/Lidarr.Api.V3/System/Tasks/TaskResource.cs @@ -1,7 +1,7 @@ using System; -using NzbDrone.Api.REST; +using Lidarr.Http.REST; -namespace NzbDrone.Api.System.Tasks +namespace Lidarr.Api.V3.System.Tasks { public class TaskResource : RestResource { diff --git a/src/Lidarr.Api.V3/Tags/TagDetailsModule.cs b/src/Lidarr.Api.V3/Tags/TagDetailsModule.cs new file mode 100644 index 000000000..a8a4f173c --- /dev/null +++ b/src/Lidarr.Api.V3/Tags/TagDetailsModule.cs @@ -0,0 +1,23 @@ +using NzbDrone.Core.Tags; +using Lidarr.Http; + +namespace Lidarr.Api.V3.Tags +{ + public class TagDetailsModule : LidarrRestModule + { + private readonly ITagService _tagService; + + public TagDetailsModule(ITagService tagService) + : base("/tag/details") + { + _tagService = tagService; + + GetResourceById = Get; + } + + private TagDetailsResource Get(int id) + { + return _tagService.Details(id).ToResource(); + } + } +} diff --git a/src/Lidarr.Api.V3/Tags/TagDetailsResource.cs b/src/Lidarr.Api.V3/Tags/TagDetailsResource.cs new file mode 100644 index 000000000..10675b3c1 --- /dev/null +++ b/src/Lidarr.Api.V3/Tags/TagDetailsResource.cs @@ -0,0 +1,44 @@ +using System.Collections.Generic; +using System.Linq; +using NzbDrone.Core.Tags; +using Lidarr.Api.V3.Notifications; +using Lidarr.Api.V3.Profiles.Delay; +using Lidarr.Api.V3.Restrictions; +using Lidarr.Http.REST; + +namespace Lidarr.Api.V3.Tags +{ + public class TagDetailsResource : RestResource + { + public string Label { get; set; } + public List DelayProfiles { get; set; } + public List Notifications { get; set; } + public List Restrictions { get; set; } + public List ArtistIds { get; set; } + } + + public static class TagDetailsResourceMapper + { + private static readonly NotificationResourceMapper NotificationResourceMapper = new NotificationResourceMapper(); + + public static TagDetailsResource ToResource(this TagDetails model) + { + if (model == null) return null; + + return new TagDetailsResource + { + Id = model.Id, + Label = model.Label, + DelayProfiles = model.DelayProfiles.ToResource(), + Notifications = model.Notifications.Select(NotificationResourceMapper.ToResource).ToList(), + Restrictions = model.Restrictions.ToResource(), + ArtistIds = model.Artist.Select(s => s.Id).ToList() + }; + } + + public static List ToResource(this IEnumerable models) + { + return models.Select(ToResource).ToList(); + } + } +} diff --git a/src/Lidarr.Api.V3/Tags/TagModule.cs b/src/Lidarr.Api.V3/Tags/TagModule.cs new file mode 100644 index 000000000..045fe723d --- /dev/null +++ b/src/Lidarr.Api.V3/Tags/TagModule.cs @@ -0,0 +1,57 @@ +using System.Collections.Generic; +using NzbDrone.Core.Datastore.Events; +using NzbDrone.Core.Messaging.Events; +using NzbDrone.Core.Tags; +using NzbDrone.SignalR; +using Lidarr.Http; + +namespace Lidarr.Api.V3.Tags +{ + public class TagModule : LidarrRestModuleWithSignalR, IHandle + { + private readonly ITagService _tagService; + + public TagModule(IBroadcastSignalRMessage signalRBroadcaster, + ITagService tagService) + : base(signalRBroadcaster) + { + _tagService = tagService; + + GetResourceById = Get; + GetResourceAll = GetAll; + CreateResource = Create; + UpdateResource = Update; + DeleteResource = Delete; + } + + private TagResource Get(int id) + { + return _tagService.GetTag(id).ToResource(); + } + + private List GetAll() + { + return _tagService.All().ToResource(); + } + + private int Create(TagResource resource) + { + return _tagService.Add(resource.ToModel()).Id; + } + + private void Update(TagResource resource) + { + _tagService.Update(resource.ToModel()); + } + + private void Delete(int id) + { + _tagService.Delete(id); + } + + public void Handle(TagsUpdatedEvent message) + { + BroadcastResourceChange(ModelAction.Sync); + } + } +} diff --git a/src/NzbDrone.Api/Tags/TagResource.cs b/src/Lidarr.Api.V3/Tags/TagResource.cs similarity index 94% rename from src/NzbDrone.Api/Tags/TagResource.cs rename to src/Lidarr.Api.V3/Tags/TagResource.cs index 678107bf5..1c95024f2 100644 --- a/src/NzbDrone.Api/Tags/TagResource.cs +++ b/src/Lidarr.Api.V3/Tags/TagResource.cs @@ -1,9 +1,9 @@ using System.Collections.Generic; using System.Linq; -using NzbDrone.Api.REST; using NzbDrone.Core.Tags; +using Lidarr.Http.REST; -namespace NzbDrone.Api.Tags +namespace Lidarr.Api.V3.Tags { public class TagResource : RestResource { @@ -19,7 +19,6 @@ namespace NzbDrone.Api.Tags return new TagResource { Id = model.Id, - Label = model.Label }; } @@ -31,7 +30,6 @@ namespace NzbDrone.Api.Tags return new Tag { Id = resource.Id, - Label = resource.Label }; } diff --git a/src/Lidarr.Api.V3/TrackFiles/MediaInfoResource.cs b/src/Lidarr.Api.V3/TrackFiles/MediaInfoResource.cs new file mode 100644 index 000000000..7ebdd974b --- /dev/null +++ b/src/Lidarr.Api.V3/TrackFiles/MediaInfoResource.cs @@ -0,0 +1,28 @@ +using NzbDrone.Core.MediaFiles.MediaInfo; +using Lidarr.Http.REST; + +namespace Lidarr.Api.V3.TrackFiles +{ + public class MediaInfoResource : RestResource + { + public decimal AudioChannels { get; set; } + public string AudioCodec { get; set; } + } + + public static class MediaInfoResourceMapper + { + public static MediaInfoResource ToResource(this MediaInfoModel model, string sceneName) + { + if (model == null) + { + return null; + } + + return new MediaInfoResource + { + AudioChannels = MediaInfoFormatter.FormatAudioChannels(model), + AudioCodec = MediaInfoFormatter.FormatAudioCodec(model) + }; + } + } +} diff --git a/src/Lidarr.Api.V3/TrackFiles/TrackFileListResource.cs b/src/Lidarr.Api.V3/TrackFiles/TrackFileListResource.cs new file mode 100644 index 000000000..d8bf09018 --- /dev/null +++ b/src/Lidarr.Api.V3/TrackFiles/TrackFileListResource.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; +using NzbDrone.Core.Languages; +using NzbDrone.Core.Qualities; + +namespace Lidarr.Api.V3.TrackFiles +{ + public class TrackFileListResource + { + public List TrackFileIds { get; set; } + public Language Language { get; set; } + public QualityModel Quality { get; set; } + } +} diff --git a/src/Lidarr.Api.V3/TrackFiles/TrackFileModule.cs b/src/Lidarr.Api.V3/TrackFiles/TrackFileModule.cs new file mode 100644 index 000000000..753820b29 --- /dev/null +++ b/src/Lidarr.Api.V3/TrackFiles/TrackFileModule.cs @@ -0,0 +1,173 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Nancy; +using NzbDrone.Core.Datastore.Events; +using NzbDrone.Core.DecisionEngine; +using NzbDrone.Core.MediaFiles; +using NzbDrone.Core.MediaFiles.Events; +using NzbDrone.Core.Messaging.Events; +using NzbDrone.Core.Music; +using NzbDrone.SignalR; +using Lidarr.Http; +using Lidarr.Http.Extensions; +using NzbDrone.Core.Exceptions; +using HttpStatusCode = System.Net.HttpStatusCode; + +namespace Lidarr.Api.V3.TrackFiles +{ + public class TrackModule : LidarrRestModuleWithSignalR, + IHandle + { + private readonly IMediaFileService _mediaFileService; + private readonly IDeleteMediaFiles _mediaFileDeletionService; + private readonly IArtistService _artistService; + private readonly IAlbumService _albumService; + private readonly IUpgradableSpecification _upgradableSpecification; + + public TrackModule(IBroadcastSignalRMessage signalRBroadcaster, + IMediaFileService mediaFileService, + IDeleteMediaFiles mediaFileDeletionService, + IArtistService artistService, + IAlbumService albumService, + IUpgradableSpecification upgradableSpecification) + : base(signalRBroadcaster) + { + _mediaFileService = mediaFileService; + _mediaFileDeletionService = mediaFileDeletionService; + _artistService = artistService; + _albumService = albumService; + _upgradableSpecification = upgradableSpecification; + + GetResourceById = GetTrackFile; + GetResourceAll = GetTrackFiles; + UpdateResource = SetQuality; + DeleteResource = DeleteTrackFile; + + Put["/editor"] = trackFiles => SetQuality(); + Delete["/bulk"] = trackFiles => DeleteTrackFiles(); + } + + private TrackFileResource GetTrackFile(int id) + { + var trackFile = _mediaFileService.Get(id); + var artist = _artistService.GetArtist(trackFile.ArtistId); + + return trackFile.ToResource(artist, _upgradableSpecification); + } + + private List GetTrackFiles() + { + var artistIdQuery = Request.Query.ArtistId; + var trackFileIdsQuery = Request.Query.TrackFileIds; + var albumIdQuery = Request.Query.AlbumId; + + if (!artistIdQuery.HasValue && !trackFileIdsQuery.HasValue && !albumIdQuery.HasValue) + { + throw new Lidarr.Http.REST.BadRequestException("artistId, albumId, or trackFileIds must be provided"); + } + + if (artistIdQuery.HasValue && !albumIdQuery.HasValue) + { + int artistId = Convert.ToInt32(artistIdQuery.Value); + var artist = _artistService.GetArtist(artistId); + + return _mediaFileService.GetFilesByArtist(artistId).ConvertAll(f => f.ToResource(artist, _upgradableSpecification)); + } + + if (albumIdQuery.HasValue) + { + int albumId = Convert.ToInt32(albumIdQuery.Value); + var album = _albumService.GetAlbum(albumId); + + return _mediaFileService.GetFilesByAlbum(album.ArtistId, album.Id).ConvertAll(f => f.ToResource(album.Artist, _upgradableSpecification)); + } + + else + { + string trackFileIdsValue = trackFileIdsQuery.Value.ToString(); + + var trackFileIds = trackFileIdsValue.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries) + .Select(e => Convert.ToInt32(e)) + .ToList(); + + var trackFiles = _mediaFileService.Get(trackFileIds); + + return trackFiles.GroupBy(e => e.ArtistId) + .SelectMany(f => f.ToList() + .ConvertAll(e => e.ToResource(_artistService.GetArtist(f.Key), _upgradableSpecification))) + .ToList(); + } + } + + private void SetQuality(TrackFileResource trackFileResource) + { + var trackFile = _mediaFileService.Get(trackFileResource.Id); + trackFile.Quality = trackFileResource.Quality; + _mediaFileService.Update(trackFile); + } + + private Response SetQuality() + { + var resource = Request.Body.FromJson(); + var trackFiles = _mediaFileService.GetFiles(resource.TrackFileIds); + + foreach (var trackFile in trackFiles) + { + if (resource.Language != null) + { + trackFile.Language = resource.Language; + } + + if (resource.Quality != null) + { + trackFile.Quality = resource.Quality; + } + } + + _mediaFileService.Update(trackFiles); + + var artist = _artistService.GetArtist(trackFiles.First().ArtistId); + + return trackFiles.ConvertAll(f => f.ToResource(artist, _upgradableSpecification)) + .AsResponse(Nancy.HttpStatusCode.Accepted); + } + + private void DeleteTrackFile(int id) + { + var trackFile = _mediaFileService.Get(id); + + if (trackFile == null) + { + throw new NzbDroneClientException(HttpStatusCode.NotFound, "Track file not found"); + } + + var artist = _artistService.GetArtist(trackFile.ArtistId); + var fullPath = Path.Combine(artist.Path, trackFile.RelativePath); + + _mediaFileDeletionService.DeleteTrackFile(artist, trackFile); + } + + private Response DeleteTrackFiles() + { + var resource = Request.Body.FromJson(); + var trackFiles = _mediaFileService.GetFiles(resource.TrackFileIds); + var artist = _artistService.GetArtist(trackFiles.First().ArtistId); + + foreach (var trackFile in trackFiles) + { + var fullPath = Path.Combine(artist.Path, trackFile.RelativePath); + + _mediaFileDeletionService.DeleteTrackFile(artist, trackFile); + } + + return new object().AsResponse(); + } + + public void Handle(TrackFileAddedEvent message) + { + BroadcastResourceChange(ModelAction.Updated, message.TrackFile.Id); + } + } +} diff --git a/src/Lidarr.Api.V3/TrackFiles/TrackFileResource.cs b/src/Lidarr.Api.V3/TrackFiles/TrackFileResource.cs new file mode 100644 index 000000000..be6deaabe --- /dev/null +++ b/src/Lidarr.Api.V3/TrackFiles/TrackFileResource.cs @@ -0,0 +1,78 @@ +using System; +using System.IO; +using NzbDrone.Core.DecisionEngine; +using NzbDrone.Core.Languages; +using NzbDrone.Core.MediaFiles; +using NzbDrone.Core.Qualities; +using Lidarr.Http.REST; + +namespace Lidarr.Api.V3.TrackFiles +{ + public class TrackFileResource : RestResource + { + public int ArtistId { get; set; } + public int AlbumId { get; set; } + public string RelativePath { get; set; } + public string Path { get; set; } + public long Size { get; set; } + public DateTime DateAdded { get; set; } + //public string SceneName { get; set; } + public Language Language { get; set; } + public QualityModel Quality { get; set; } + public MediaInfoResource MediaInfo { get; set; } + + public bool QualityCutoffNotMet { get; set; } + } + + public static class TrackFileResourceMapper + { + private static TrackFileResource ToResource(this TrackFile model) + { + if (model == null) return null; + + return new TrackFileResource + { + Id = model.Id, + + ArtistId = model.ArtistId, + AlbumId = model.AlbumId, + RelativePath = model.RelativePath, + //Path + Size = model.Size, + DateAdded = model.DateAdded, + // SceneName = model.SceneName, + Language = model.Language, + Quality = model.Quality, + MediaInfo = model.MediaInfo.ToResource(model.SceneName) + //QualityCutoffNotMet + }; + + } + + public static TrackFileResource ToResource(this TrackFile model, NzbDrone.Core.Music.Artist artist, IUpgradableSpecification upgradableSpecification) + { + if (model == null) return null; + + return new TrackFileResource + { + Id = model.Id, + + ArtistId = model.ArtistId, + AlbumId = model.AlbumId, + RelativePath = model.RelativePath, + Path = Path.Combine(artist.Path, model.RelativePath), + Size = model.Size, + DateAdded = model.DateAdded, + //SceneName = model.SceneName, + Language = model.Language, + Quality = model.Quality, + MediaInfo = model.MediaInfo.ToResource(model.SceneName), + + QualityCutoffNotMet = upgradableSpecification.CutoffNotMet(artist.Profile.Value, + artist.LanguageProfile.Value, + model.Quality, + model.Language) + }; + } + } +} diff --git a/src/Lidarr.Api.V3/Tracks/RenameTrackModule.cs b/src/Lidarr.Api.V3/Tracks/RenameTrackModule.cs new file mode 100644 index 000000000..073acdff4 --- /dev/null +++ b/src/Lidarr.Api.V3/Tracks/RenameTrackModule.cs @@ -0,0 +1,43 @@ +using System.Collections.Generic; +using NzbDrone.Core.MediaFiles; +using Lidarr.Http; +using Lidarr.Http.REST; + +namespace Lidarr.Api.V3.Tracks +{ + public class RenameTrackModule : LidarrRestModule + { + private readonly IRenameTrackFileService _renameTrackFileService; + + public RenameTrackModule(IRenameTrackFileService renameTrackFileService) + : base("rename") + { + _renameTrackFileService = renameTrackFileService; + + GetResourceAll = GetTracks; + } + + private List GetTracks() + { + int artistId; + + if (Request.Query.ArtistId.HasValue) + { + artistId = (int)Request.Query.ArtistId; + } + + else + { + throw new BadRequestException("artistId is missing"); + } + + if (Request.Query.albumId.HasValue) + { + var albumId = (int)Request.Query.albumId; + return _renameTrackFileService.GetRenamePreviews(artistId, albumId).ToResource(); + } + + return _renameTrackFileService.GetRenamePreviews(artistId).ToResource(); + } + } +} diff --git a/src/NzbDrone.Api/Tracks/RenameTrackResource.cs b/src/Lidarr.Api.V3/Tracks/RenameTrackResource.cs similarity index 78% rename from src/NzbDrone.Api/Tracks/RenameTrackResource.cs rename to src/Lidarr.Api.V3/Tracks/RenameTrackResource.cs index 12f67bb60..9bfe676cb 100644 --- a/src/NzbDrone.Api/Tracks/RenameTrackResource.cs +++ b/src/Lidarr.Api.V3/Tracks/RenameTrackResource.cs @@ -1,8 +1,8 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; -using NzbDrone.Api.REST; +using Lidarr.Http.REST; -namespace NzbDrone.Api.Tracks +namespace Lidarr.Api.V3.Tracks { public class RenameTrackResource : RestResource { @@ -16,7 +16,7 @@ namespace NzbDrone.Api.Tracks public static class RenameTrackResourceMapper { - public static RenameTrackResource ToResource(this Core.MediaFiles.RenameTrackFilePreview model) + public static RenameTrackResource ToResource(this NzbDrone.Core.MediaFiles.RenameTrackFilePreview model) { if (model == null) return null; @@ -31,7 +31,7 @@ namespace NzbDrone.Api.Tracks }; } - public static List ToResource(this IEnumerable models) + public static List ToResource(this IEnumerable models) { return models.Select(ToResource).ToList(); } diff --git a/src/Lidarr.Api.V3/Tracks/TrackModule.cs b/src/Lidarr.Api.V3/Tracks/TrackModule.cs new file mode 100644 index 000000000..32d761d9d --- /dev/null +++ b/src/Lidarr.Api.V3/Tracks/TrackModule.cs @@ -0,0 +1,58 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Nancy; +using NzbDrone.Core.DecisionEngine; +using NzbDrone.Core.Music; +using NzbDrone.SignalR; +using Lidarr.Http.Extensions; +using Lidarr.Http.REST; + +namespace Lidarr.Api.V3.Tracks +{ + public class TrackModule : TrackModuleWithSignalR + { + public TrackModule(IArtistService artistService, + ITrackService trackService, + IUpgradableSpecification upgradableSpecification, + IBroadcastSignalRMessage signalRBroadcaster) + : base(trackService, artistService, upgradableSpecification, signalRBroadcaster) + { + GetResourceAll = GetTracks; + } + + private List GetTracks() + { + var artistIdQuery = Request.Query.ArtistId; + var albumIdQuery = Request.Query.AlbumId; + var trackIdsQuery = Request.Query.TrackIds; + + if (!artistIdQuery.HasValue && !trackIdsQuery.HasValue && !albumIdQuery.HasValue) + { + throw new BadRequestException("artistId or trackIds must be provided"); + } + + if (artistIdQuery.HasValue && !albumIdQuery.HasValue) + { + int artistId = Convert.ToInt32(artistIdQuery.Value); + + return MapToResource(_trackService.GetTracksByArtist(artistId), false, false); + } + + if (albumIdQuery.HasValue) + { + int albumId = Convert.ToInt32(albumIdQuery.Value); + + return MapToResource(_trackService.GetTracksByAlbum(albumId), false, false); + } + + string trackIdsValue = trackIdsQuery.Value.ToString(); + + var trackIds = trackIdsValue.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries) + .Select(e => Convert.ToInt32(e)) + .ToList(); + + return MapToResource(_trackService.GetTracks(trackIds), false, false); + } + } +} diff --git a/src/Lidarr.Api.V3/Tracks/TrackModuleWithSignalR.cs b/src/Lidarr.Api.V3/Tracks/TrackModuleWithSignalR.cs new file mode 100644 index 000000000..bd4f3f654 --- /dev/null +++ b/src/Lidarr.Api.V3/Tracks/TrackModuleWithSignalR.cs @@ -0,0 +1,132 @@ +using System.Collections.Generic; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.Datastore.Events; +using NzbDrone.Core.DecisionEngine; +using NzbDrone.Core.Download; +using NzbDrone.Core.MediaFiles.Events; +using NzbDrone.Core.Messaging.Events; +using NzbDrone.Core.Music; +using NzbDrone.SignalR; +using Lidarr.Api.V3.TrackFiles; +using Lidarr.Api.V3.Artist; +using Lidarr.Http; + +namespace Lidarr.Api.V3.Tracks +{ + public abstract class TrackModuleWithSignalR : LidarrRestModuleWithSignalR + //IHandle, + //IHandle + { + protected readonly ITrackService _trackService; + protected readonly IArtistService _artistService; + protected readonly IUpgradableSpecification _upgradableSpecification; + + protected TrackModuleWithSignalR(ITrackService trackService, + IArtistService artistService, + IUpgradableSpecification upgradableSpecification, + IBroadcastSignalRMessage signalRBroadcaster) + : base(signalRBroadcaster) + { + _trackService = trackService; + _artistService = artistService; + _upgradableSpecification = upgradableSpecification; + + GetResourceById = GetEpisode; + } + + protected TrackModuleWithSignalR(ITrackService episodeService, + IArtistService seriesService, + IUpgradableSpecification upgradableSpecification, + IBroadcastSignalRMessage signalRBroadcaster, + string resource) + : base(signalRBroadcaster, resource) + { + _trackService = episodeService; + _artistService = seriesService; + _upgradableSpecification = upgradableSpecification; + + GetResourceById = GetEpisode; + } + + protected TrackResource GetEpisode(int id) + { + var episode = _trackService.GetTrack(id); + var resource = MapToResource(episode, true, true); + return resource; + } + + protected TrackResource MapToResource(Track track, bool includeArtist, bool includeTrackFile) + { + var resource = track.ToResource(); + + if (includeArtist || includeTrackFile) + { + var artist = track.Artist ?? _artistService.GetArtist(track.ArtistId); + + if (includeArtist) + { + resource.Artist = artist.ToResource(); + } + if (includeTrackFile && track.TrackFileId != 0) + { + resource.TrackFile = track.TrackFile.Value.ToResource(artist, _upgradableSpecification); + } + } + + return resource; + } + + protected List MapToResource(List episodes, bool includeSeries, bool includeEpisodeFile) + { + var result = episodes.ToResource(); + + if (includeSeries || includeEpisodeFile) + { + var seriesDict = new Dictionary(); + for (var i = 0; i < episodes.Count; i++) + { + var episode = episodes[i]; + var resource = result[i]; + + var series = episode.Artist ?? seriesDict.GetValueOrDefault(episodes[i].ArtistId) ?? _artistService.GetArtist(episodes[i].ArtistId); + seriesDict[series.Id] = series; + + if (includeSeries) + { + resource.Artist = series.ToResource(); + } + if (includeEpisodeFile && episodes[i].TrackFileId != 0) + { + resource.TrackFile = episodes[i].TrackFile.Value.ToResource(series, _upgradableSpecification); + } + } + } + + return result; + } + + //public void Handle(EpisodeGrabbedEvent message) + //{ + // foreach (var episode in message.Episode.Episodes) + // { + // var resource = episode.ToResource(); + // resource.Grabbed = true; + + // BroadcastResourceChange(ModelAction.Updated, resource); + // } + //} + + //public void Handle(EpisodeImportedEvent message) + //{ + // if (!message.NewDownload) + // { + // return; + // } + + // foreach (var episode in message.EpisodeInfo.Episodes) + // { + // BroadcastResourceChange(ModelAction.Updated, episode.Id); + // } + //} + } +} diff --git a/src/NzbDrone.Api/Tracks/TrackResource.cs b/src/Lidarr.Api.V3/Tracks/TrackResource.cs similarity index 94% rename from src/NzbDrone.Api/Tracks/TrackResource.cs rename to src/Lidarr.Api.V3/Tracks/TrackResource.cs index b8a007671..222394c52 100644 --- a/src/NzbDrone.Api/Tracks/TrackResource.cs +++ b/src/Lidarr.Api.V3/Tracks/TrackResource.cs @@ -1,13 +1,13 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using Newtonsoft.Json; -using NzbDrone.Api.TrackFiles; -using NzbDrone.Api.REST; -using NzbDrone.Api.Music; using NzbDrone.Core.Music; +using Lidarr.Api.V3.TrackFiles; +using Lidarr.Api.V3.Artist; +using Lidarr.Http.REST; -namespace NzbDrone.Api.Tracks +namespace Lidarr.Api.V3.Tracks { public class TrackResource : RestResource { diff --git a/src/NzbDrone.Api/Update/UpdateModule.cs b/src/Lidarr.Api.V3/Update/UpdateModule.cs similarity index 91% rename from src/NzbDrone.Api/Update/UpdateModule.cs rename to src/Lidarr.Api.V3/Update/UpdateModule.cs index 2104f23ea..9aa4c2c94 100644 --- a/src/NzbDrone.Api/Update/UpdateModule.cs +++ b/src/Lidarr.Api.V3/Update/UpdateModule.cs @@ -2,10 +2,11 @@ using System.Linq; using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Core.Update; +using Lidarr.Http; -namespace NzbDrone.Api.Update +namespace Lidarr.Api.V3.Update { - public class UpdateModule : NzbDroneRestModule + public class UpdateModule : LidarrRestModule { private readonly IRecentUpdateProvider _recentUpdateProvider; diff --git a/src/NzbDrone.Api/Update/UpdateResource.cs b/src/Lidarr.Api.V3/Update/UpdateResource.cs similarity index 96% rename from src/NzbDrone.Api/Update/UpdateResource.cs rename to src/Lidarr.Api.V3/Update/UpdateResource.cs index dca6f6725..2ed916cdc 100644 --- a/src/NzbDrone.Api/Update/UpdateResource.cs +++ b/src/Lidarr.Api.V3/Update/UpdateResource.cs @@ -2,10 +2,10 @@ using System.Collections.Generic; using System.Linq; using Newtonsoft.Json; -using NzbDrone.Api.REST; using NzbDrone.Core.Update; +using Lidarr.Http.REST; -namespace NzbDrone.Api.Update +namespace Lidarr.Api.V3.Update { public class UpdateResource : RestResource { diff --git a/src/Lidarr.Api.V3/Wanted/CutoffModule.cs b/src/Lidarr.Api.V3/Wanted/CutoffModule.cs new file mode 100644 index 000000000..e104269fe --- /dev/null +++ b/src/Lidarr.Api.V3/Wanted/CutoffModule.cs @@ -0,0 +1,54 @@ +using NzbDrone.Core.Datastore; +using NzbDrone.Core.DecisionEngine; +using NzbDrone.Core.Music; +using NzbDrone.Core.ArtistStats; +using NzbDrone.SignalR; +using Lidarr.Api.V3.Albums; +using Lidarr.Http; +using Lidarr.Http.Extensions; + +namespace Lidarr.Api.V3.Wanted +{ + public class CutoffModule : AlbumModuleWithSignalR + { + private readonly IAlbumCutoffService _albumCutoffService; + + public CutoffModule(IAlbumCutoffService albumCutoffService, + IAlbumService albumService, + IArtistStatisticsService artistStatisticsService, + IArtistService artistService, + IUpgradableSpecification upgradableSpecification, + IBroadcastSignalRMessage signalRBroadcaster) + : base(albumService, artistStatisticsService, artistService, upgradableSpecification, signalRBroadcaster, "wanted/cutoff") + { + _albumCutoffService = albumCutoffService; + GetResourcePaged = GetCutoffUnmetAlbums; + } + + private PagingResource GetCutoffUnmetAlbums(PagingResource pagingResource) + { + var pagingSpec = new PagingSpec + { + Page = pagingResource.Page, + PageSize = pagingResource.PageSize, + SortKey = pagingResource.SortKey, + SortDirection = pagingResource.SortDirection + }; + + var includeArtist = Request.GetBooleanQueryParameter("includeArtist"); + + if (pagingResource.FilterKey == "monitored" && pagingResource.FilterValue == "false") + { + pagingSpec.FilterExpression = v => v.Monitored == false || v.Artist.Monitored == false; + } + else + { + pagingSpec.FilterExpression = v => v.Monitored == true && v.Artist.Monitored == true; + } + + var resource = ApplyToPage(_albumCutoffService.AlbumsWhereCutoffUnmet, pagingSpec, v => MapToResource(v, includeArtist)); + + return resource; + } + } +} diff --git a/src/Lidarr.Api.V3/Wanted/MissingModule.cs b/src/Lidarr.Api.V3/Wanted/MissingModule.cs new file mode 100644 index 000000000..05eb9b8a5 --- /dev/null +++ b/src/Lidarr.Api.V3/Wanted/MissingModule.cs @@ -0,0 +1,50 @@ +using NzbDrone.Core.Datastore; +using NzbDrone.Core.DecisionEngine; +using NzbDrone.Core.Music; +using NzbDrone.Core.ArtistStats; +using NzbDrone.SignalR; +using Lidarr.Api.V3.Albums; +using Lidarr.Http; +using Lidarr.Http.Extensions; + +namespace Lidarr.Api.V3.Wanted +{ + public class MissingModule : AlbumModuleWithSignalR + { + public MissingModule(IAlbumService albumService, + IArtistStatisticsService artistStatisticsService, + IArtistService artistService, + IUpgradableSpecification upgradableSpecification, + IBroadcastSignalRMessage signalRBroadcaster) + : base(albumService, artistStatisticsService, artistService, upgradableSpecification, signalRBroadcaster, "wanted/missing") + { + GetResourcePaged = GetMissingAlbums; + } + + private PagingResource GetMissingAlbums(PagingResource pagingResource) + { + var pagingSpec = new PagingSpec + { + Page = pagingResource.Page, + PageSize = pagingResource.PageSize, + SortKey = pagingResource.SortKey, + SortDirection = pagingResource.SortDirection + }; + + var includeArtist = Request.GetBooleanQueryParameter("includeArtist"); + + if (pagingResource.FilterKey == "monitored" && pagingResource.FilterValue == "false") + { + pagingSpec.FilterExpression = v => v.Monitored == false || v.Artist.Monitored == false; + } + else + { + pagingSpec.FilterExpression = v => v.Monitored == true && v.Artist.Monitored == true; + } + + var resource = ApplyToPage(_albumService.AlbumsWithoutFiles, pagingSpec, v => MapToResource(v, includeArtist)); + + return resource; + } + } +} diff --git a/src/Lidarr.Api.V3/app.config b/src/Lidarr.Api.V3/app.config new file mode 100644 index 000000000..d4d6857aa --- /dev/null +++ b/src/Lidarr.Api.V3/app.config @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Lidarr.Api.V3/packages.config b/src/Lidarr.Api.V3/packages.config new file mode 100644 index 000000000..84bf483de --- /dev/null +++ b/src/Lidarr.Api.V3/packages.config @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/src/NzbDrone.Api/Authentication/1tews5g3.gd1~ b/src/Lidarr.Http/Authentication/1tews5g3.gd1~ similarity index 100% rename from src/NzbDrone.Api/Authentication/1tews5g3.gd1~ rename to src/Lidarr.Http/Authentication/1tews5g3.gd1~ diff --git a/src/NzbDrone.Api/Authentication/AuthenticationModule.cs b/src/Lidarr.Http/Authentication/AuthenticationModule.cs similarity index 89% rename from src/NzbDrone.Api/Authentication/AuthenticationModule.cs rename to src/Lidarr.Http/Authentication/AuthenticationModule.cs index df940a947..8feee4589 100644 --- a/src/NzbDrone.Api/Authentication/AuthenticationModule.cs +++ b/src/Lidarr.Http/Authentication/AuthenticationModule.cs @@ -7,7 +7,7 @@ using NzbDrone.Common.EnsureThat; using NzbDrone.Core.Authentication; using NzbDrone.Core.Configuration; -namespace NzbDrone.Api.Authentication +namespace Lidarr.Http.Authentication { public class AuthenticationModule : NancyModule { @@ -33,7 +33,8 @@ namespace NzbDrone.Api.Authentication if (user == null) { - return Context.GetRedirect("~/login?returnUrl=" + (string)Request.Query.returnUrl); + var returnUrl = (string)Request.Query.returnUrl; + return Context.GetRedirect($"~/login?returnUrl={returnUrl}&loginFailed=true"); } DateTime? expiry = null; diff --git a/src/NzbDrone.Api/Authentication/AuthenticationService.cs b/src/Lidarr.Http/Authentication/AuthenticationService.cs similarity index 95% rename from src/NzbDrone.Api/Authentication/AuthenticationService.cs rename to src/Lidarr.Http/Authentication/AuthenticationService.cs index beb908b11..97436e979 100644 --- a/src/NzbDrone.Api/Authentication/AuthenticationService.cs +++ b/src/Lidarr.Http/Authentication/AuthenticationService.cs @@ -4,12 +4,12 @@ using Nancy; using Nancy.Authentication.Basic; using Nancy.Authentication.Forms; using Nancy.Security; -using NzbDrone.Api.Extensions; using NzbDrone.Common.Extensions; using NzbDrone.Core.Authentication; using NzbDrone.Core.Configuration; +using Lidarr.Http.Extensions; -namespace NzbDrone.Api.Authentication +namespace Lidarr.Http.Authentication { public interface IAuthenticationService : IUserValidator, IUserMapper { @@ -18,7 +18,6 @@ namespace NzbDrone.Api.Authentication public class AuthenticationService : IAuthenticationService { - private readonly IConfigFileProvider _configFileProvider; private readonly IUserService _userService; private static readonly NzbDroneUser AnonymousUser = new NzbDroneUser { UserName = "Anonymous" }; @@ -27,7 +26,6 @@ namespace NzbDrone.Api.Authentication public AuthenticationService(IConfigFileProvider configFileProvider, IUserService userService) { - _configFileProvider = configFileProvider; _userService = userService; API_KEY = configFileProvider.ApiKey; AUTH_METHOD = configFileProvider.AuthenticationMethod; diff --git a/src/Lidarr.Http/Authentication/EnableAuthInNancy.cs b/src/Lidarr.Http/Authentication/EnableAuthInNancy.cs new file mode 100644 index 000000000..1d619583d --- /dev/null +++ b/src/Lidarr.Http/Authentication/EnableAuthInNancy.cs @@ -0,0 +1,136 @@ +using System; +using System.Text; +using Nancy; +using Nancy.Authentication.Basic; +using Nancy.Authentication.Forms; +using Nancy.Bootstrapper; +using Nancy.Cookies; +using Nancy.Cryptography; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.Authentication; +using NzbDrone.Core.Configuration; +using Lidarr.Http.Extensions; +using Lidarr.Http.Extensions.Pipelines; + +namespace Lidarr.Http.Authentication +{ + public class EnableAuthInNancy : IRegisterNancyPipeline + { + private readonly IAuthenticationService _authenticationService; + private readonly IConfigService _configService; + private readonly IConfigFileProvider _configFileProvider; + private FormsAuthenticationConfiguration FormsAuthConfig; + + public EnableAuthInNancy(IAuthenticationService authenticationService, + IConfigService configService, + IConfigFileProvider configFileProvider) + { + _authenticationService = authenticationService; + _configService = configService; + _configFileProvider = configFileProvider; + } + + public int Order => 10; + + public void Register(IPipelines pipelines) + { + if (_configFileProvider.AuthenticationMethod == AuthenticationType.Forms) + { + RegisterFormsAuth(pipelines); + pipelines.AfterRequest.AddItemToEndOfPipeline((Action)SlidingAuthenticationForFormsAuth); + } + + else if (_configFileProvider.AuthenticationMethod == AuthenticationType.Basic) + { + pipelines.EnableBasicAuthentication(new BasicAuthenticationConfiguration(_authenticationService, "Lidarr")); + } + + pipelines.BeforeRequest.AddItemToEndOfPipeline((Func)RequiresAuthentication); + pipelines.AfterRequest.AddItemToEndOfPipeline((Action)RemoveLoginHooksForApiCalls); + } + + private Response RequiresAuthentication(NancyContext context) + { + Response response = null; + + if (!_authenticationService.IsAuthenticated(context)) + { + response = new Response { StatusCode = HttpStatusCode.Unauthorized }; + } + + return response; + } + + private void RegisterFormsAuth(IPipelines pipelines) + { + FormsAuthentication.FormsAuthenticationCookieName = "LidarrAuth"; + + var cryptographyConfiguration = new CryptographyConfiguration( + new RijndaelEncryptionProvider(new PassphraseKeyGenerator(_configService.RijndaelPassphrase, Encoding.ASCII.GetBytes(_configService.RijndaelSalt))), + new DefaultHmacProvider(new PassphraseKeyGenerator(_configService.HmacPassphrase, Encoding.ASCII.GetBytes(_configService.HmacSalt))) + ); + + FormsAuthConfig = new FormsAuthenticationConfiguration + + { + RedirectUrl = _configFileProvider.UrlBase + "/login", + UserMapper = _authenticationService, + Path = GetCookiePath(), + CryptographyConfiguration = cryptographyConfiguration + }; + + FormsAuthentication.Enable(pipelines, FormsAuthConfig); + } + + private void RemoveLoginHooksForApiCalls(NancyContext context) + { + if (context.Request.IsApiRequest()) + { + if ((context.Response.StatusCode == HttpStatusCode.SeeOther && + context.Response.Headers["Location"].StartsWith($"{_configFileProvider.UrlBase}/login", StringComparison.InvariantCultureIgnoreCase)) || + context.Response.StatusCode == HttpStatusCode.Unauthorized) + { + context.Response = new { Error = "Unauthorized" }.AsResponse(HttpStatusCode.Unauthorized); + } + } + } + + private void SlidingAuthenticationForFormsAuth(NancyContext context) + { + if (context.CurrentUser == null) + { + return; + } + + var formsAuthCookieName = FormsAuthentication.FormsAuthenticationCookieName; + + if (!context.Request.Path.Equals("/logout") && + context.Request.Cookies.ContainsKey(formsAuthCookieName)) + { + var formsAuthCookieValue = context.Request.Cookies[formsAuthCookieName]; + + if (FormsAuthentication.DecryptAndValidateAuthenticationCookie(formsAuthCookieValue, FormsAuthConfig).IsNotNullOrWhiteSpace()) + { + var formsAuthCookie = new NancyCookie(formsAuthCookieName, formsAuthCookieValue, true, false, DateTime.UtcNow.AddDays(7)) + { + Path = GetCookiePath() + }; + + context.Response.WithCookie(formsAuthCookie); + } + } + } + + private string GetCookiePath() + { + var urlBase = _configFileProvider.UrlBase; + + if (urlBase.IsNullOrWhiteSpace()) + { + return "/"; + } + + return urlBase; + } + } +} diff --git a/src/NzbDrone.Api/Authentication/LoginResource.cs b/src/Lidarr.Http/Authentication/LoginResource.cs similarity index 81% rename from src/NzbDrone.Api/Authentication/LoginResource.cs rename to src/Lidarr.Http/Authentication/LoginResource.cs index 5d6a5c9f5..db1b94513 100644 --- a/src/NzbDrone.Api/Authentication/LoginResource.cs +++ b/src/Lidarr.Http/Authentication/LoginResource.cs @@ -1,4 +1,4 @@ -namespace NzbDrone.Api.Authentication +namespace Lidarr.Http.Authentication { public class LoginResource { diff --git a/src/NzbDrone.Api/Authentication/NzbDroneUser.cs b/src/Lidarr.Http/Authentication/NzbDroneUser.cs similarity index 85% rename from src/NzbDrone.Api/Authentication/NzbDroneUser.cs rename to src/Lidarr.Http/Authentication/NzbDroneUser.cs index c8fce02fd..a83a0fda5 100644 --- a/src/NzbDrone.Api/Authentication/NzbDroneUser.cs +++ b/src/Lidarr.Http/Authentication/NzbDroneUser.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; using Nancy.Security; -namespace NzbDrone.Api.Authentication +namespace Lidarr.Http.Authentication { public class NzbDroneUser : IUserIdentity { diff --git a/src/NzbDrone.Api/ClientSchema/Field.cs b/src/Lidarr.Http/ClientSchema/Field.cs similarity index 92% rename from src/NzbDrone.Api/ClientSchema/Field.cs rename to src/Lidarr.Http/ClientSchema/Field.cs index ec611e8d6..332ed871d 100644 --- a/src/NzbDrone.Api/ClientSchema/Field.cs +++ b/src/Lidarr.Http/ClientSchema/Field.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; -namespace NzbDrone.Api.ClientSchema +namespace Lidarr.Http.ClientSchema { public class Field { diff --git a/src/NzbDrone.Api/ClientSchema/SchemaBuilder.cs b/src/Lidarr.Http/ClientSchema/SchemaBuilder.cs similarity index 99% rename from src/NzbDrone.Api/ClientSchema/SchemaBuilder.cs rename to src/Lidarr.Http/ClientSchema/SchemaBuilder.cs index 0a7acb9e1..c6b8240d5 100644 --- a/src/NzbDrone.Api/ClientSchema/SchemaBuilder.cs +++ b/src/Lidarr.Http/ClientSchema/SchemaBuilder.cs @@ -7,7 +7,7 @@ using NzbDrone.Common.Extensions; using NzbDrone.Common.Reflection; using NzbDrone.Core.Annotations; -namespace NzbDrone.Api.ClientSchema +namespace Lidarr.Http.ClientSchema { public static class SchemaBuilder { diff --git a/src/NzbDrone.Api/ClientSchema/SelectOption.cs b/src/Lidarr.Http/ClientSchema/SelectOption.cs similarity index 76% rename from src/NzbDrone.Api/ClientSchema/SelectOption.cs rename to src/Lidarr.Http/ClientSchema/SelectOption.cs index fe42f46dd..58029e8fa 100644 --- a/src/NzbDrone.Api/ClientSchema/SelectOption.cs +++ b/src/Lidarr.Http/ClientSchema/SelectOption.cs @@ -1,4 +1,4 @@ -namespace NzbDrone.Api.ClientSchema +namespace Lidarr.Http.ClientSchema { public class SelectOption { diff --git a/src/NzbDrone.Api/ErrorManagement/ErrorHandler.cs b/src/Lidarr.Http/ErrorManagement/ErrorHandler.cs similarity index 93% rename from src/NzbDrone.Api/ErrorManagement/ErrorHandler.cs rename to src/Lidarr.Http/ErrorManagement/ErrorHandler.cs index 6ba25d741..4307f9d53 100644 --- a/src/NzbDrone.Api/ErrorManagement/ErrorHandler.cs +++ b/src/Lidarr.Http/ErrorManagement/ErrorHandler.cs @@ -1,8 +1,8 @@ using Nancy; using Nancy.ErrorHandling; -using NzbDrone.Api.Extensions; +using Lidarr.Http.Extensions; -namespace NzbDrone.Api.ErrorManagement +namespace Lidarr.Http.ErrorManagement { public class ErrorHandler : IStatusCodeHandler { diff --git a/src/NzbDrone.Api/ErrorManagement/ErrorModel.cs b/src/Lidarr.Http/ErrorManagement/ErrorModel.cs similarity index 83% rename from src/NzbDrone.Api/ErrorManagement/ErrorModel.cs rename to src/Lidarr.Http/ErrorManagement/ErrorModel.cs index b88600717..cc2b95562 100644 --- a/src/NzbDrone.Api/ErrorManagement/ErrorModel.cs +++ b/src/Lidarr.Http/ErrorManagement/ErrorModel.cs @@ -1,4 +1,6 @@ -namespace NzbDrone.Api.ErrorManagement +using Lidarr.Http.Exceptions; + +namespace Lidarr.Http.ErrorManagement { public class ErrorModel { diff --git a/src/Lidarr.Http/ErrorManagement/LidarrErrorPipeline.cs b/src/Lidarr.Http/ErrorManagement/LidarrErrorPipeline.cs new file mode 100644 index 000000000..dc36c83e3 --- /dev/null +++ b/src/Lidarr.Http/ErrorManagement/LidarrErrorPipeline.cs @@ -0,0 +1,79 @@ +using System; +using System.Data.SQLite; +using FluentValidation; +using Nancy; +using NLog; +using NzbDrone.Core.Exceptions; +using Lidarr.Http.Exceptions; +using Lidarr.Http.Extensions; +using HttpStatusCode = Nancy.HttpStatusCode; + +namespace Lidarr.Http.ErrorManagement +{ + public class LidarrErrorPipeline + { + private readonly Logger _logger; + + public LidarrErrorPipeline(Logger logger) + { + _logger = logger; + } + + public Response HandleException(NancyContext context, Exception exception) + { + _logger.Trace("Handling Exception"); + + var apiException = exception as ApiException; + + if (apiException != null) + { + _logger.Warn(apiException, "API Error"); + return apiException.ToErrorResponse(); + } + + var validationException = exception as ValidationException; + + if (validationException != null) + { + _logger.Warn("Invalid request {0}", validationException.Message); + + return validationException.Errors.AsResponse(HttpStatusCode.BadRequest); + } + + var clientException = exception as NzbDroneClientException; + + if (clientException != null) + { + return new ErrorModel + { + Message = exception.Message, + Description = exception.ToString() + }.AsResponse((HttpStatusCode)clientException.StatusCode); + } + + var sqLiteException = exception as SQLiteException; + + if (sqLiteException != null) + { + if (context.Request.Method == "PUT" || context.Request.Method == "POST") + { + if (sqLiteException.Message.Contains("constraint failed")) + return new ErrorModel + { + Message = exception.Message, + }.AsResponse(HttpStatusCode.Conflict); + } + + _logger.Error(sqLiteException, "[{0} {1}]", context.Request.Method, context.Request.Path); + } + + _logger.Fatal(exception, "Request Failed. {0} {1}", context.Request.Method, context.Request.Path); + + return new ErrorModel + { + Message = exception.Message, + Description = exception.ToString() + }.AsResponse(HttpStatusCode.InternalServerError); + } + } +} diff --git a/src/NzbDrone.Api/ErrorManagement/ApiException.cs b/src/Lidarr.Http/Exceptions/ApiException.cs similarity index 90% rename from src/NzbDrone.Api/ErrorManagement/ApiException.cs rename to src/Lidarr.Http/Exceptions/ApiException.cs index 2a9f2678f..7823c552f 100644 --- a/src/NzbDrone.Api/ErrorManagement/ApiException.cs +++ b/src/Lidarr.Http/Exceptions/ApiException.cs @@ -1,9 +1,10 @@ using System; using Nancy; using Nancy.Responses; -using NzbDrone.Api.Extensions; +using Lidarr.Http.ErrorManagement; +using Lidarr.Http.Extensions; -namespace NzbDrone.Api.ErrorManagement +namespace Lidarr.Http.Exceptions { public abstract class ApiException : Exception { diff --git a/src/NzbDrone.Api/Exceptions/InvalidApiKeyException.cs b/src/Lidarr.Http/Exceptions/InvalidApiKeyException.cs similarity index 87% rename from src/NzbDrone.Api/Exceptions/InvalidApiKeyException.cs rename to src/Lidarr.Http/Exceptions/InvalidApiKeyException.cs index 8c16e8133..067f0ccc4 100644 --- a/src/NzbDrone.Api/Exceptions/InvalidApiKeyException.cs +++ b/src/Lidarr.Http/Exceptions/InvalidApiKeyException.cs @@ -1,6 +1,6 @@ using System; -namespace NzbDrone.Api.Exceptions +namespace Lidarr.Http.Exceptions { public class InvalidApiKeyException : Exception { diff --git a/src/NzbDrone.Api/Extensions/AccessControlHeaders.cs b/src/Lidarr.Http/Extensions/AccessControlHeaders.cs similarity index 92% rename from src/NzbDrone.Api/Extensions/AccessControlHeaders.cs rename to src/Lidarr.Http/Extensions/AccessControlHeaders.cs index 5a32395cb..5ffc632d7 100644 --- a/src/NzbDrone.Api/Extensions/AccessControlHeaders.cs +++ b/src/Lidarr.Http/Extensions/AccessControlHeaders.cs @@ -1,4 +1,4 @@ -namespace NzbDrone.Api.Extensions +namespace Lidarr.Http.Extensions { public static class AccessControlHeaders { diff --git a/src/NzbDrone.Api/Extensions/NancyJsonSerializer.cs b/src/Lidarr.Http/Extensions/NancyJsonSerializer.cs similarity index 93% rename from src/NzbDrone.Api/Extensions/NancyJsonSerializer.cs rename to src/Lidarr.Http/Extensions/NancyJsonSerializer.cs index 00b3c3b2c..ff3459de9 100644 --- a/src/NzbDrone.Api/Extensions/NancyJsonSerializer.cs +++ b/src/Lidarr.Http/Extensions/NancyJsonSerializer.cs @@ -3,7 +3,7 @@ using System.IO; using Nancy; using NzbDrone.Common.Serializer; -namespace NzbDrone.Api.Extensions +namespace Lidarr.Http.Extensions { public class NancyJsonSerializer : ISerializer { diff --git a/src/NzbDrone.Api/Extensions/Pipelines/CacheHeaderPipeline.cs b/src/Lidarr.Http/Extensions/Pipelines/CacheHeaderPipeline.cs similarity index 92% rename from src/NzbDrone.Api/Extensions/Pipelines/CacheHeaderPipeline.cs rename to src/Lidarr.Http/Extensions/Pipelines/CacheHeaderPipeline.cs index 94c738d9b..febfc17c3 100644 --- a/src/NzbDrone.Api/Extensions/Pipelines/CacheHeaderPipeline.cs +++ b/src/Lidarr.Http/Extensions/Pipelines/CacheHeaderPipeline.cs @@ -1,9 +1,9 @@ using System; using Nancy; using Nancy.Bootstrapper; -using NzbDrone.Api.Frontend; +using Lidarr.Http.Frontend; -namespace NzbDrone.Api.Extensions.Pipelines +namespace Lidarr.Http.Extensions.Pipelines { public class CacheHeaderPipeline : IRegisterNancyPipeline { diff --git a/src/NzbDrone.Api/Extensions/Pipelines/CorsPipeline.cs b/src/Lidarr.Http/Extensions/Pipelines/CorsPipeline.cs similarity index 97% rename from src/NzbDrone.Api/Extensions/Pipelines/CorsPipeline.cs rename to src/Lidarr.Http/Extensions/Pipelines/CorsPipeline.cs index b8c83298a..3f980524f 100644 --- a/src/NzbDrone.Api/Extensions/Pipelines/CorsPipeline.cs +++ b/src/Lidarr.Http/Extensions/Pipelines/CorsPipeline.cs @@ -3,7 +3,7 @@ using System.Linq; using Nancy; using Nancy.Bootstrapper; -namespace NzbDrone.Api.Extensions.Pipelines +namespace Lidarr.Http.Extensions.Pipelines { public class CorsPipeline : IRegisterNancyPipeline { diff --git a/src/NzbDrone.Api/Extensions/Pipelines/GZipPipeline.cs b/src/Lidarr.Http/Extensions/Pipelines/GZipPipeline.cs similarity index 98% rename from src/NzbDrone.Api/Extensions/Pipelines/GZipPipeline.cs rename to src/Lidarr.Http/Extensions/Pipelines/GZipPipeline.cs index 12293f23c..782dd5b15 100644 --- a/src/NzbDrone.Api/Extensions/Pipelines/GZipPipeline.cs +++ b/src/Lidarr.Http/Extensions/Pipelines/GZipPipeline.cs @@ -7,7 +7,7 @@ using Nancy.Bootstrapper; using NLog; using NzbDrone.Common.Extensions; -namespace NzbDrone.Api.Extensions.Pipelines +namespace Lidarr.Http.Extensions.Pipelines { public class GzipCompressionPipeline : IRegisterNancyPipeline { @@ -63,20 +63,24 @@ namespace NzbDrone.Api.Extensions.Pipelines private static bool ContentLengthIsTooSmall(Response response) { var contentLength = response.Headers.GetValueOrDefault("Content-Length"); + if (contentLength != null && long.Parse(contentLength) < 1024) { return true; } + return false; } private static bool AlreadyGzipEncoded(Response response) { var contentEncoding = response.Headers.GetValueOrDefault("Content-Encoding"); + if (contentEncoding == "gzip") { return true; } + return false; } } diff --git a/src/NzbDrone.Api/Extensions/Pipelines/IRegisterNancyPipeline.cs b/src/Lidarr.Http/Extensions/Pipelines/IRegisterNancyPipeline.cs similarity index 78% rename from src/NzbDrone.Api/Extensions/Pipelines/IRegisterNancyPipeline.cs rename to src/Lidarr.Http/Extensions/Pipelines/IRegisterNancyPipeline.cs index 0376ccc70..84105c46b 100644 --- a/src/NzbDrone.Api/Extensions/Pipelines/IRegisterNancyPipeline.cs +++ b/src/Lidarr.Http/Extensions/Pipelines/IRegisterNancyPipeline.cs @@ -1,6 +1,6 @@ using Nancy.Bootstrapper; -namespace NzbDrone.Api.Extensions.Pipelines +namespace Lidarr.Http.Extensions.Pipelines { public interface IRegisterNancyPipeline { diff --git a/src/NzbDrone.Api/Extensions/Pipelines/IfModifiedPipeline.cs b/src/Lidarr.Http/Extensions/Pipelines/IfModifiedPipeline.cs similarity index 93% rename from src/NzbDrone.Api/Extensions/Pipelines/IfModifiedPipeline.cs rename to src/Lidarr.Http/Extensions/Pipelines/IfModifiedPipeline.cs index 68abf4ade..cf619745d 100644 --- a/src/NzbDrone.Api/Extensions/Pipelines/IfModifiedPipeline.cs +++ b/src/Lidarr.Http/Extensions/Pipelines/IfModifiedPipeline.cs @@ -1,9 +1,9 @@ using System; using Nancy; using Nancy.Bootstrapper; -using NzbDrone.Api.Frontend; +using Lidarr.Http.Frontend; -namespace NzbDrone.Api.Extensions.Pipelines +namespace Lidarr.Http.Extensions.Pipelines { public class IfModifiedPipeline : IRegisterNancyPipeline { diff --git a/src/Lidarr.Http/Extensions/Pipelines/LidarrVersionPipeline.cs b/src/Lidarr.Http/Extensions/Pipelines/LidarrVersionPipeline.cs new file mode 100644 index 000000000..1a425031d --- /dev/null +++ b/src/Lidarr.Http/Extensions/Pipelines/LidarrVersionPipeline.cs @@ -0,0 +1,25 @@ +using System; +using Nancy; +using Nancy.Bootstrapper; +using NzbDrone.Common.EnvironmentInfo; + +namespace Lidarr.Http.Extensions.Pipelines +{ + public class LidarrVersionPipeline : IRegisterNancyPipeline + { + public int Order => 0; + + public void Register(IPipelines pipelines) + { + pipelines.AfterRequest.AddItemToStartOfPipeline((Action) Handle); + } + + private void Handle(NancyContext context) + { + if (!context.Response.Headers.ContainsKey("X-ApplicationVersion")) + { + context.Response.Headers.Add("X-ApplicationVersion", BuildInfo.Version.ToString()); + } + } + } +} diff --git a/src/NzbDrone.Api/Extensions/Pipelines/RequestLoggingPipeline.cs b/src/Lidarr.Http/Extensions/Pipelines/RequestLoggingPipeline.cs similarity index 91% rename from src/NzbDrone.Api/Extensions/Pipelines/RequestLoggingPipeline.cs rename to src/Lidarr.Http/Extensions/Pipelines/RequestLoggingPipeline.cs index 73668bc81..34d40cd5a 100644 --- a/src/NzbDrone.Api/Extensions/Pipelines/RequestLoggingPipeline.cs +++ b/src/Lidarr.Http/Extensions/Pipelines/RequestLoggingPipeline.cs @@ -1,91 +1,93 @@ -using System; -using System.Threading; -using Nancy; -using Nancy.Bootstrapper; -using NLog; -using NzbDrone.Api.ErrorManagement; -using NzbDrone.Common.Extensions; - -namespace NzbDrone.Api.Extensions.Pipelines -{ - public class RequestLoggingPipeline : IRegisterNancyPipeline - { - private static readonly Logger _loggerHttp = LogManager.GetLogger("Http"); - private static readonly Logger _loggerApi = LogManager.GetLogger("Api"); - - private static int _requestSequenceID; - - private readonly NzbDroneErrorPipeline _errorPipeline; - - public RequestLoggingPipeline(NzbDroneErrorPipeline errorPipeline) - { - _errorPipeline = errorPipeline; - } - - public int Order => 100; - - public void Register(IPipelines pipelines) - { - pipelines.BeforeRequest.AddItemToStartOfPipeline(LogStart); - pipelines.AfterRequest.AddItemToEndOfPipeline(LogEnd); - pipelines.OnError.AddItemToEndOfPipeline(LogError); - } - - private Response LogStart(NancyContext context) - { - var id = Interlocked.Increment(ref _requestSequenceID); - - context.Items["ApiRequestSequenceID"] = id; - context.Items["ApiRequestStartTime"] = DateTime.UtcNow; - - var reqPath = GetRequestPathAndQuery(context.Request); - - _loggerHttp.Trace("Req: {0} [{1}] {2}", id, context.Request.Method, reqPath); - - return null; - } - - private void LogEnd(NancyContext context) - { - var id = (int)context.Items["ApiRequestSequenceID"]; - var startTime = (DateTime)context.Items["ApiRequestStartTime"]; - - var endTime = DateTime.UtcNow; - var duration = endTime - startTime; - - var reqPath = GetRequestPathAndQuery(context.Request); - - _loggerHttp.Trace("Res: {0} [{1}] {2}: {3}.{4} ({5} ms)", id, context.Request.Method, reqPath, (int)context.Response.StatusCode, context.Response.StatusCode, (int)duration.TotalMilliseconds); - - if (context.Request.IsApiRequest()) - { - _loggerApi.Debug("[{0}] {1}: {2}.{3} ({4} ms)", context.Request.Method, reqPath, (int)context.Response.StatusCode, context.Response.StatusCode, (int)duration.TotalMilliseconds); - } - } - - private Response LogError(NancyContext context, Exception exception) - { - var response = _errorPipeline.HandleException(context, exception); - - context.Response = response; - - LogEnd(context); - - context.Response = null; - - return response; - } - - private static string GetRequestPathAndQuery(Request request) - { - if (request.Url.Query.IsNotNullOrWhiteSpace()) - { - return string.Concat(request.Url.Path, request.Url.Query); - } - else - { - return request.Url.Path; - } - } - } -} +using System; +using System.Threading; +using Nancy; +using Nancy.Bootstrapper; +using NLog; +using NzbDrone.Common.Extensions; +using Lidarr.Http.ErrorManagement; +using Lidarr.Http.Extensions; +using Lidarr.Http.Extensions.Pipelines; + +namespace NzbDrone.Api.Extensions.Pipelines +{ + public class RequestLoggingPipeline : IRegisterNancyPipeline + { + private static readonly Logger _loggerHttp = LogManager.GetLogger("Http"); + private static readonly Logger _loggerApi = LogManager.GetLogger("Api"); + + private static int _requestSequenceID; + + private readonly LidarrErrorPipeline _errorPipeline; + + public RequestLoggingPipeline(LidarrErrorPipeline errorPipeline) + { + _errorPipeline = errorPipeline; + } + + public int Order => 100; + + public void Register(IPipelines pipelines) + { + pipelines.BeforeRequest.AddItemToStartOfPipeline(LogStart); + pipelines.AfterRequest.AddItemToEndOfPipeline(LogEnd); + pipelines.OnError.AddItemToEndOfPipeline(LogError); + } + + private Response LogStart(NancyContext context) + { + var id = Interlocked.Increment(ref _requestSequenceID); + + context.Items["ApiRequestSequenceID"] = id; + context.Items["ApiRequestStartTime"] = DateTime.UtcNow; + + var reqPath = GetRequestPathAndQuery(context.Request); + + _loggerHttp.Trace("Req: {0} [{1}] {2}", id, context.Request.Method, reqPath); + + return null; + } + + private void LogEnd(NancyContext context) + { + var id = (int)context.Items["ApiRequestSequenceID"]; + var startTime = (DateTime)context.Items["ApiRequestStartTime"]; + + var endTime = DateTime.UtcNow; + var duration = endTime - startTime; + + var reqPath = GetRequestPathAndQuery(context.Request); + + _loggerHttp.Trace("Res: {0} [{1}] {2}: {3}.{4} ({5} ms)", id, context.Request.Method, reqPath, (int)context.Response.StatusCode, context.Response.StatusCode, (int)duration.TotalMilliseconds); + + if (context.Request.IsApiRequest()) + { + _loggerApi.Debug("[{0}] {1}: {2}.{3} ({4} ms)", context.Request.Method, reqPath, (int)context.Response.StatusCode, context.Response.StatusCode, (int)duration.TotalMilliseconds); + } + } + + private Response LogError(NancyContext context, Exception exception) + { + var response = _errorPipeline.HandleException(context, exception); + + context.Response = response; + + LogEnd(context); + + context.Response = null; + + return response; + } + + private static string GetRequestPathAndQuery(Request request) + { + if (request.Url.Query.IsNotNullOrWhiteSpace()) + { + return string.Concat(request.Url.Path, request.Url.Query); + } + else + { + return request.Url.Path; + } + } + } +} diff --git a/src/NzbDrone.Api/Extensions/Pipelines/UrlBasePipeline.cs b/src/Lidarr.Http/Extensions/Pipelines/UrlBasePipeline.cs similarity index 100% rename from src/NzbDrone.Api/Extensions/Pipelines/UrlBasePipeline.cs rename to src/Lidarr.Http/Extensions/Pipelines/UrlBasePipeline.cs diff --git a/src/NzbDrone.Api/Extensions/ReqResExtensions.cs b/src/Lidarr.Http/Extensions/ReqResExtensions.cs similarity index 98% rename from src/NzbDrone.Api/Extensions/ReqResExtensions.cs rename to src/Lidarr.Http/Extensions/ReqResExtensions.cs index 1f1d89180..78a3d911a 100644 --- a/src/NzbDrone.Api/Extensions/ReqResExtensions.cs +++ b/src/Lidarr.Http/Extensions/ReqResExtensions.cs @@ -6,7 +6,7 @@ using Nancy.Responses; using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.Serializer; -namespace NzbDrone.Api.Extensions +namespace Lidarr.Http.Extensions { public static class ReqResExtensions { diff --git a/src/Lidarr.Http/Extensions/RequestExtensions.cs b/src/Lidarr.Http/Extensions/RequestExtensions.cs new file mode 100644 index 000000000..d9dc691a1 --- /dev/null +++ b/src/Lidarr.Http/Extensions/RequestExtensions.cs @@ -0,0 +1,52 @@ +using System; +using Nancy; + +namespace Lidarr.Http.Extensions +{ + public static class RequestExtensions + { + public static bool IsApiRequest(this Request request) + { + return request.Path.StartsWith("/api/", StringComparison.InvariantCultureIgnoreCase); + } + + public static bool IsFeedRequest(this Request request) + { + return request.Path.StartsWith("/feed/", StringComparison.InvariantCultureIgnoreCase); + } + + public static bool IsSignalRRequest(this Request request) + { + return request.Path.StartsWith("/signalr/", StringComparison.InvariantCultureIgnoreCase); + } + + public static bool IsLocalRequest(this Request request) + { + return (request.UserHostAddress.Equals("localhost") || + request.UserHostAddress.Equals("127.0.0.1") || + request.UserHostAddress.Equals("::1")); + } + + public static bool IsLoginRequest(this Request request) + { + return request.Path.Equals("/login", StringComparison.InvariantCultureIgnoreCase); + } + + public static bool IsContentRequest(this Request request) + { + return request.Path.StartsWith("/Content/", StringComparison.InvariantCultureIgnoreCase); + } + + public static bool GetBooleanQueryParameter(this Request request, string parameter, bool defaultValue = false) + { + var parameterValue = request.Query[parameter]; + + if (parameterValue.HasValue) + { + return bool.Parse(parameterValue.Value); + } + + return defaultValue; + } + } +} diff --git a/src/NzbDrone.Api/Frontend/CacheableSpecification.cs b/src/Lidarr.Http/Frontend/CacheableSpecification.cs similarity index 93% rename from src/NzbDrone.Api/Frontend/CacheableSpecification.cs rename to src/Lidarr.Http/Frontend/CacheableSpecification.cs index 7995c7da1..7d934530f 100644 --- a/src/NzbDrone.Api/Frontend/CacheableSpecification.cs +++ b/src/Lidarr.Http/Frontend/CacheableSpecification.cs @@ -3,7 +3,7 @@ using Nancy; using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.Extensions; -namespace NzbDrone.Api.Frontend +namespace Lidarr.Http.Frontend { public interface ICacheableSpecification { @@ -29,7 +29,7 @@ namespace NzbDrone.Api.Frontend } if (context.Request.Path.StartsWith("/signalr", StringComparison.CurrentCultureIgnoreCase)) return false; - if (context.Request.Path.EndsWith("main.js")) return false; + if (context.Request.Path.EndsWith("index.js")) return false; if (context.Request.Path.StartsWith("/feed", StringComparison.CurrentCultureIgnoreCase)) return false; if (context.Request.Path.StartsWith("/log", StringComparison.CurrentCultureIgnoreCase) && diff --git a/src/NzbDrone.Api/Frontend/Mappers/BackupFileMapper.cs b/src/Lidarr.Http/Frontend/Mappers/BackupFileMapper.cs similarity index 95% rename from src/NzbDrone.Api/Frontend/Mappers/BackupFileMapper.cs rename to src/Lidarr.Http/Frontend/Mappers/BackupFileMapper.cs index 9e4912524..bc79a53fb 100644 --- a/src/NzbDrone.Api/Frontend/Mappers/BackupFileMapper.cs +++ b/src/Lidarr.Http/Frontend/Mappers/BackupFileMapper.cs @@ -4,7 +4,7 @@ using NzbDrone.Common.Disk; using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.Extensions; -namespace NzbDrone.Api.Frontend.Mappers +namespace Lidarr.Http.Frontend.Mappers { public class BackupFileMapper : StaticResourceMapperBase { diff --git a/src/Lidarr.Http/Frontend/Mappers/BrowserConfig.cs b/src/Lidarr.Http/Frontend/Mappers/BrowserConfig.cs new file mode 100644 index 000000000..4d7bb0808 --- /dev/null +++ b/src/Lidarr.Http/Frontend/Mappers/BrowserConfig.cs @@ -0,0 +1,34 @@ +using System.IO; +using NLog; +using NzbDrone.Common.Disk; +using NzbDrone.Common.EnvironmentInfo; +using NzbDrone.Core.Configuration; + +namespace Lidarr.Http.Frontend.Mappers +{ + public class BrowserConfig : StaticResourceMapperBase + { + private readonly IAppFolderInfo _appFolderInfo; + private readonly IConfigFileProvider _configFileProvider; + + public BrowserConfig(IAppFolderInfo appFolderInfo, IDiskProvider diskProvider, IConfigFileProvider configFileProvider, Logger logger) + : base(diskProvider, logger) + { + _appFolderInfo = appFolderInfo; + _configFileProvider = configFileProvider; + } + + public override string Map(string resourceUrl) + { + var path = resourceUrl.Replace('/', Path.DirectorySeparatorChar); + path = path.Trim(Path.DirectorySeparatorChar); + + return Path.ChangeExtension(Path.Combine(_appFolderInfo.StartUpFolder, _configFileProvider.UiFolder, path), "xml"); + } + + public override bool CanHandle(string resourceUrl) + { + return resourceUrl.StartsWith("/Content/Images/Icons/browserconfig"); + } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Api/Frontend/Mappers/CacheBreakerProvider.cs b/src/Lidarr.Http/Frontend/Mappers/CacheBreakerProvider.cs similarity index 96% rename from src/NzbDrone.Api/Frontend/Mappers/CacheBreakerProvider.cs rename to src/Lidarr.Http/Frontend/Mappers/CacheBreakerProvider.cs index 53ebb2986..f165594a8 100644 --- a/src/NzbDrone.Api/Frontend/Mappers/CacheBreakerProvider.cs +++ b/src/Lidarr.Http/Frontend/Mappers/CacheBreakerProvider.cs @@ -3,7 +3,7 @@ using System.Linq; using NzbDrone.Common.Crypto; using NzbDrone.Common.Extensions; -namespace NzbDrone.Api.Frontend.Mappers +namespace Lidarr.Http.Frontend.Mappers { public interface ICacheBreakerProvider { diff --git a/src/NzbDrone.Api/Frontend/Mappers/FaviconMapper.cs b/src/Lidarr.Http/Frontend/Mappers/FaviconMapper.cs similarity index 88% rename from src/NzbDrone.Api/Frontend/Mappers/FaviconMapper.cs rename to src/Lidarr.Http/Frontend/Mappers/FaviconMapper.cs index 002ffa7ce..c269fa1b3 100644 --- a/src/NzbDrone.Api/Frontend/Mappers/FaviconMapper.cs +++ b/src/Lidarr.Http/Frontend/Mappers/FaviconMapper.cs @@ -1,10 +1,10 @@ -using System.IO; +using System.IO; using NLog; using NzbDrone.Common.Disk; using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Core.Configuration; -namespace NzbDrone.Api.Frontend.Mappers +namespace Lidarr.Http.Frontend.Mappers { public class FaviconMapper : StaticResourceMapperBase { @@ -27,7 +27,7 @@ namespace NzbDrone.Api.Frontend.Mappers fileName = "favicon-debug.ico"; } - var path = Path.Combine("Content", "Images", fileName); + var path = Path.Combine("Content", "Images", "Icons", fileName); return Path.Combine(_appFolderInfo.StartUpFolder, _configFileProvider.UiFolder, path); } diff --git a/src/NzbDrone.Api/Frontend/Mappers/IMapHttpRequestsToDisk.cs b/src/Lidarr.Http/Frontend/Mappers/IMapHttpRequestsToDisk.cs similarity index 84% rename from src/NzbDrone.Api/Frontend/Mappers/IMapHttpRequestsToDisk.cs rename to src/Lidarr.Http/Frontend/Mappers/IMapHttpRequestsToDisk.cs index 6390a2545..1bfd458ad 100644 --- a/src/NzbDrone.Api/Frontend/Mappers/IMapHttpRequestsToDisk.cs +++ b/src/Lidarr.Http/Frontend/Mappers/IMapHttpRequestsToDisk.cs @@ -1,7 +1,7 @@  using Nancy; -namespace NzbDrone.Api.Frontend.Mappers +namespace Lidarr.Http.Frontend.Mappers { public interface IMapHttpRequestsToDisk { diff --git a/src/NzbDrone.Api/Frontend/Mappers/IndexHtmlMapper.cs b/src/Lidarr.Http/Frontend/Mappers/IndexHtmlMapper.cs similarity index 87% rename from src/NzbDrone.Api/Frontend/Mappers/IndexHtmlMapper.cs rename to src/Lidarr.Http/Frontend/Mappers/IndexHtmlMapper.cs index ae66b2aa2..aefe17fb6 100644 --- a/src/NzbDrone.Api/Frontend/Mappers/IndexHtmlMapper.cs +++ b/src/Lidarr.Http/Frontend/Mappers/IndexHtmlMapper.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.IO; using System.Text.RegularExpressions; using Nancy; @@ -8,7 +8,7 @@ using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Core.Analytics; using NzbDrone.Core.Configuration; -namespace NzbDrone.Api.Frontend.Mappers +namespace Lidarr.Http.Frontend.Mappers { public class IndexHtmlMapper : StaticResourceMapperBase { @@ -17,7 +17,7 @@ namespace NzbDrone.Api.Frontend.Mappers private readonly IAnalyticsService _analyticsService; private readonly Func _cacheBreakProviderFactory; private readonly string _indexPath; - private static readonly Regex ReplaceRegex = new Regex(@"(?:(?href|src)=\"")(?.*?(?css|js|png|ico|ics))(?:\"")(?:\s(?data-no-hash))?", RegexOptions.Compiled | RegexOptions.IgnoreCase); + private static readonly Regex ReplaceRegex = new Regex(@"(?:(?href|src)=\"")(?.*?(?css|js|png|ico|ics|svg))(?:\"")(?:\s(?data-no-hash))?", RegexOptions.Compiled | RegexOptions.IgnoreCase); private static string API_KEY; private static string URL_BASE; @@ -49,7 +49,9 @@ namespace NzbDrone.Api.Frontend.Mappers public override bool CanHandle(string resourceUrl) { - return !resourceUrl.Contains(".") && !resourceUrl.StartsWith("/login"); + return !resourceUrl.Contains(".") && + !resourceUrl.StartsWith("/login") && + !resourceUrl.StartsWith("/Content"); } public override Response GetResponse(string resourceUrl) @@ -100,13 +102,14 @@ namespace NzbDrone.Api.Frontend.Mappers return string.Format("{0}=\"{1}{2}\"", match.Groups["attribute"].Value, URL_BASE, url); }); - text = text.Replace("API_ROOT", URL_BASE + "/api"); + text = text.Replace("API_ROOT", URL_BASE + "/api/v3"); text = text.Replace("API_KEY", API_KEY); + text = text.Replace("RELEASE", BuildInfo.Release); text = text.Replace("APP_VERSION", BuildInfo.Version.ToString()); text = text.Replace("APP_BRANCH", _configFileProvider.Branch.ToLower()); text = text.Replace("APP_ANALYTICS", _analyticsService.IsEnabled.ToString().ToLowerInvariant()); text = text.Replace("URL_BASE", URL_BASE); - text = text.Replace("PRODUCTION", RuntimeInfo.IsProduction.ToString().ToLowerInvariant()); + text = text.Replace("IS_PRODUCTION", RuntimeInfo.IsProduction.ToString().ToLowerInvariant()); _generatedContent = text; diff --git a/src/NzbDrone.Api/Frontend/Mappers/LogFileMapper.cs b/src/Lidarr.Http/Frontend/Mappers/LogFileMapper.cs similarity index 95% rename from src/NzbDrone.Api/Frontend/Mappers/LogFileMapper.cs rename to src/Lidarr.Http/Frontend/Mappers/LogFileMapper.cs index 5fda7d483..c6f73732f 100644 --- a/src/NzbDrone.Api/Frontend/Mappers/LogFileMapper.cs +++ b/src/Lidarr.Http/Frontend/Mappers/LogFileMapper.cs @@ -4,7 +4,7 @@ using NzbDrone.Common.Disk; using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.Extensions; -namespace NzbDrone.Api.Frontend.Mappers +namespace Lidarr.Http.Frontend.Mappers { public class UpdateLogFileMapper : StaticResourceMapperBase { diff --git a/src/Lidarr.Http/Frontend/Mappers/LoginHtmlMapper.cs b/src/Lidarr.Http/Frontend/Mappers/LoginHtmlMapper.cs new file mode 100644 index 000000000..22b4071da --- /dev/null +++ b/src/Lidarr.Http/Frontend/Mappers/LoginHtmlMapper.cs @@ -0,0 +1,71 @@ +using System.IO; +using Nancy; +using NLog; +using NzbDrone.Common.Disk; +using NzbDrone.Common.EnvironmentInfo; +using NzbDrone.Core.Configuration; + +namespace Lidarr.Http.Frontend.Mappers +{ + public class LoginHtmlMapper : StaticResourceMapperBase + { + private readonly IDiskProvider _diskProvider; + private readonly string _indexPath; + + private string _generatedContent; + + public LoginHtmlMapper(IAppFolderInfo appFolderInfo, + IDiskProvider diskProvider, + IConfigFileProvider configFileProvider, + Logger logger) + : base(diskProvider, logger) + { + _diskProvider = diskProvider; + _indexPath = Path.Combine(appFolderInfo.StartUpFolder, configFileProvider.UiFolder, "login.html"); + } + + public override string Map(string resourceUrl) + { + return _indexPath; + } + + public override bool CanHandle(string resourceUrl) + { + return resourceUrl.StartsWith("/login"); + } + + public override Response GetResponse(string resourceUrl) + { + var response = base.GetResponse(resourceUrl); + response.Headers["X-UA-Compatible"] = "IE=edge"; + + return response; + } + + protected override Stream GetContentStream(string filePath) + { + var text = GetLoginText(); + + var stream = new MemoryStream(); + var writer = new StreamWriter(stream); + writer.Write(text); + writer.Flush(); + stream.Position = 0; + return stream; + } + + private string GetLoginText() + { + if (RuntimeInfo.IsProduction && _generatedContent != null) + { + return _generatedContent; + } + + var text = _diskProvider.ReadAllText(_indexPath); + + _generatedContent = text; + + return _generatedContent; + } + } +} diff --git a/src/Lidarr.Http/Frontend/Mappers/ManifestMapper.cs b/src/Lidarr.Http/Frontend/Mappers/ManifestMapper.cs new file mode 100644 index 000000000..92afc08ca --- /dev/null +++ b/src/Lidarr.Http/Frontend/Mappers/ManifestMapper.cs @@ -0,0 +1,34 @@ +using System.IO; +using NLog; +using NzbDrone.Common.Disk; +using NzbDrone.Common.EnvironmentInfo; +using NzbDrone.Core.Configuration; + +namespace Lidarr.Http.Frontend.Mappers +{ + public class ManifestMapper : StaticResourceMapperBase + { + private readonly IAppFolderInfo _appFolderInfo; + private readonly IConfigFileProvider _configFileProvider; + + public ManifestMapper(IAppFolderInfo appFolderInfo, IDiskProvider diskProvider, IConfigFileProvider configFileProvider, Logger logger) + : base(diskProvider, logger) + { + _appFolderInfo = appFolderInfo; + _configFileProvider = configFileProvider; + } + + public override string Map(string resourceUrl) + { + var path = resourceUrl.Replace('/', Path.DirectorySeparatorChar); + path = path.Trim(Path.DirectorySeparatorChar); + + return Path.ChangeExtension(Path.Combine(_appFolderInfo.StartUpFolder, _configFileProvider.UiFolder, path), "json"); + } + + public override bool CanHandle(string resourceUrl) + { + return resourceUrl.StartsWith("/Content/Images/Icons/manifest"); + } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Api/Frontend/Mappers/MediaCoverMapper.cs b/src/Lidarr.Http/Frontend/Mappers/MediaCoverMapper.cs similarity index 97% rename from src/NzbDrone.Api/Frontend/Mappers/MediaCoverMapper.cs rename to src/Lidarr.Http/Frontend/Mappers/MediaCoverMapper.cs index a4e5fb8f2..9aa9dfc49 100644 --- a/src/NzbDrone.Api/Frontend/Mappers/MediaCoverMapper.cs +++ b/src/Lidarr.Http/Frontend/Mappers/MediaCoverMapper.cs @@ -5,7 +5,7 @@ using NzbDrone.Common.Disk; using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.Extensions; -namespace NzbDrone.Api.Frontend.Mappers +namespace Lidarr.Http.Frontend.Mappers { public class MediaCoverMapper : StaticResourceMapperBase { diff --git a/src/NzbDrone.Api/Frontend/Mappers/RobotsTxtMapper.cs b/src/Lidarr.Http/Frontend/Mappers/RobotsTxtMapper.cs similarity index 96% rename from src/NzbDrone.Api/Frontend/Mappers/RobotsTxtMapper.cs rename to src/Lidarr.Http/Frontend/Mappers/RobotsTxtMapper.cs index 60b3131c6..d6bfedb5f 100644 --- a/src/NzbDrone.Api/Frontend/Mappers/RobotsTxtMapper.cs +++ b/src/Lidarr.Http/Frontend/Mappers/RobotsTxtMapper.cs @@ -4,7 +4,7 @@ using NzbDrone.Common.Disk; using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Core.Configuration; -namespace NzbDrone.Api.Frontend.Mappers +namespace Lidarr.Http.Frontend.Mappers { public class RobotsTxtMapper : StaticResourceMapperBase { diff --git a/src/NzbDrone.Api/Frontend/Mappers/StaticResourceMapper.cs b/src/Lidarr.Http/Frontend/Mappers/StaticResourceMapper.cs similarity index 83% rename from src/NzbDrone.Api/Frontend/Mappers/StaticResourceMapper.cs rename to src/Lidarr.Http/Frontend/Mappers/StaticResourceMapper.cs index 61ed14e9b..32518a935 100644 --- a/src/NzbDrone.Api/Frontend/Mappers/StaticResourceMapper.cs +++ b/src/Lidarr.Http/Frontend/Mappers/StaticResourceMapper.cs @@ -1,10 +1,10 @@ -using System.IO; +using System.IO; using NLog; using NzbDrone.Common.Disk; using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Core.Configuration; -namespace NzbDrone.Api.Frontend.Mappers +namespace Lidarr.Http.Frontend.Mappers { public class StaticResourceMapper : StaticResourceMapperBase { @@ -28,6 +28,12 @@ namespace NzbDrone.Api.Frontend.Mappers public override bool CanHandle(string resourceUrl) { + if (resourceUrl.StartsWith("/Content/Images/Icons/manifest") || + resourceUrl.StartsWith("/Content/Images/Icons/browserconfig")) + { + return false; + } + return resourceUrl.StartsWith("/Content") || resourceUrl.EndsWith(".js") || resourceUrl.EndsWith(".map") || diff --git a/src/NzbDrone.Api/Frontend/Mappers/StaticResourceMapperBase.cs b/src/Lidarr.Http/Frontend/Mappers/StaticResourceMapperBase.cs similarity index 87% rename from src/NzbDrone.Api/Frontend/Mappers/StaticResourceMapperBase.cs rename to src/Lidarr.Http/Frontend/Mappers/StaticResourceMapperBase.cs index 5d4b57d50..96307eae7 100644 --- a/src/NzbDrone.Api/Frontend/Mappers/StaticResourceMapperBase.cs +++ b/src/Lidarr.Http/Frontend/Mappers/StaticResourceMapperBase.cs @@ -1,12 +1,12 @@ -using System; +using System; using System.IO; -using NLog; using Nancy; using Nancy.Responses; +using NLog; using NzbDrone.Common.Disk; using NzbDrone.Common.EnvironmentInfo; -namespace NzbDrone.Api.Frontend.Mappers +namespace Lidarr.Http.Frontend.Mappers { public abstract class StaticResourceMapperBase : IMapHttpRequestsToDisk { @@ -21,10 +21,7 @@ namespace NzbDrone.Api.Frontend.Mappers _diskProvider = diskProvider; _logger = logger; - if (!RuntimeInfo.IsProduction) - { - _caseSensitive = StringComparison.OrdinalIgnoreCase; - } + _caseSensitive = RuntimeInfo.IsProduction ? DiskProviderBase.PathStringComparison : StringComparison.OrdinalIgnoreCase; } public abstract string Map(string resourceUrl); @@ -52,4 +49,4 @@ namespace NzbDrone.Api.Frontend.Mappers } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Api/Frontend/Mappers/UpdateLogFileMapper.cs b/src/Lidarr.Http/Frontend/Mappers/UpdateLogFileMapper.cs similarity index 95% rename from src/NzbDrone.Api/Frontend/Mappers/UpdateLogFileMapper.cs rename to src/Lidarr.Http/Frontend/Mappers/UpdateLogFileMapper.cs index 021bdba58..13fe24b58 100644 --- a/src/NzbDrone.Api/Frontend/Mappers/UpdateLogFileMapper.cs +++ b/src/Lidarr.Http/Frontend/Mappers/UpdateLogFileMapper.cs @@ -4,7 +4,7 @@ using NzbDrone.Common.Disk; using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.Extensions; -namespace NzbDrone.Api.Frontend.Mappers +namespace Lidarr.Http.Frontend.Mappers { public class LogFileMapper : StaticResourceMapperBase { diff --git a/src/NzbDrone.Api/Frontend/StaticResourceModule.cs b/src/Lidarr.Http/Frontend/StaticResourceModule.cs similarity index 95% rename from src/NzbDrone.Api/Frontend/StaticResourceModule.cs rename to src/Lidarr.Http/Frontend/StaticResourceModule.cs index f58667c6c..627ec28b5 100644 --- a/src/NzbDrone.Api/Frontend/StaticResourceModule.cs +++ b/src/Lidarr.Http/Frontend/StaticResourceModule.cs @@ -1,13 +1,13 @@ using System; using System.Collections.Generic; using System.Linq; +using Nancy; using Nancy.Responses; using NLog; -using Nancy; -using NzbDrone.Api.Frontend.Mappers; using NzbDrone.Core.Configuration; +using Lidarr.Http.Frontend.Mappers; -namespace NzbDrone.Api.Frontend +namespace Lidarr.Http.Frontend { public class StaticResourceModule : NancyModule { diff --git a/src/Lidarr.Http/LIdarrRestModuleWithSignalR.cs b/src/Lidarr.Http/LIdarrRestModuleWithSignalR.cs new file mode 100644 index 000000000..dd5efe0ee --- /dev/null +++ b/src/Lidarr.Http/LIdarrRestModuleWithSignalR.cs @@ -0,0 +1,71 @@ +using NzbDrone.Core.Datastore; +using NzbDrone.Core.Datastore.Events; +using NzbDrone.Core.Messaging.Events; +using NzbDrone.SignalR; +using Lidarr.Http.REST; + +namespace Lidarr.Http +{ + public abstract class LidarrRestModuleWithSignalR : LidarrRestModule, IHandle> + where TResource : RestResource, new() + where TModel : ModelBase, new() + { + private readonly IBroadcastSignalRMessage _signalRBroadcaster; + + protected LidarrRestModuleWithSignalR(IBroadcastSignalRMessage signalRBroadcaster) + { + _signalRBroadcaster = signalRBroadcaster; + } + + protected LidarrRestModuleWithSignalR(IBroadcastSignalRMessage signalRBroadcaster, string resource) + : base(resource) + { + _signalRBroadcaster = signalRBroadcaster; + } + + public void Handle(ModelEvent message) + { + if (message.Action == ModelAction.Deleted || message.Action == ModelAction.Sync) + { + BroadcastResourceChange(message.Action); + } + + BroadcastResourceChange(message.Action, message.Model.Id); + } + + protected void BroadcastResourceChange(ModelAction action, int id) + { + var resource = GetResourceById(id); + BroadcastResourceChange(action, resource); + } + + + protected void BroadcastResourceChange(ModelAction action, TResource resource) + { + var signalRMessage = new SignalRMessage + { + Name = Resource, + Body = new ResourceChangeMessage(resource, action), + Action = action + }; + + _signalRBroadcaster.BroadcastMessage(signalRMessage); + } + + + protected void BroadcastResourceChange(ModelAction action) + { + if (GetType().Namespace.Contains("V3")) + { + var signalRMessage = new SignalRMessage + { + Name = Resource, + Body = new ResourceChangeMessage(action), + Action = action + }; + + _signalRBroadcaster.BroadcastMessage(signalRMessage); + } + } + } +} diff --git a/src/Lidarr.Http/Lidarr.Http.csproj b/src/Lidarr.Http/Lidarr.Http.csproj new file mode 100644 index 000000000..493e163b8 --- /dev/null +++ b/src/Lidarr.Http/Lidarr.Http.csproj @@ -0,0 +1,157 @@ + + + + + Debug + AnyCPU + {5370BFF7-1BD7-46BC-AF06-7D9EA5CDA1D6} + Library + Properties + Lidarr.Http + Lidarr.Http + v4.6.1 + 512 + + + + true + bin\x86\Debug\ + DEBUG;TRACE + full + x86 + prompt + MinimumRecommendedRules.ruleset + false + + + bin\x86\Release\ + TRACE + true + pdbonly + x86 + prompt + MinimumRecommendedRules.ruleset + false + + + + ..\packages\FluentValidation.6.2.1.0\lib\portable-net40+sl50+wp80+win8+wpa81\FluentValidation.dll + True + + + ..\packages\Nancy.1.4.4\lib\net40\Nancy.dll + + + ..\packages\Nancy.Authentication.Basic.1.4.1\lib\net40\Nancy.Authentication.Basic.dll + True + + + ..\packages\Nancy.Authentication.Forms.1.4.1\lib\net40\Nancy.Authentication.Forms.dll + True + + + ..\packages\Newtonsoft.Json.9.0.1\lib\net40\Newtonsoft.Json.dll + True + + + ..\packages\NLog.4.4.12\lib\net45\NLog.dll + + + + + ..\Libraries\Sqlite\System.Data.SQLite.dll + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {F6FC6BE7-0847-4817-A1ED-223DC647C3D7} + Marr.Data + + + {F2BE0FDF-6E47-4827-A420-DD4EF82407F8} + NzbDrone.Common + + + {FF5EE3B6-913B-47CE-9CEB-11C51B4E1205} + NzbDrone.Core + + + {7C2CC69F-5CA0-4E5C-85CB-983F9F6C3B36} + NzbDrone.SignalR + + + + + + + + + \ No newline at end of file diff --git a/src/Lidarr.Http/LidarrBootstrapper.cs b/src/Lidarr.Http/LidarrBootstrapper.cs new file mode 100644 index 000000000..65c528f56 --- /dev/null +++ b/src/Lidarr.Http/LidarrBootstrapper.cs @@ -0,0 +1,59 @@ +using System.Linq; +using Nancy.Bootstrapper; +using Nancy.Diagnostics; +using NLog; +using NzbDrone.Common.EnvironmentInfo; +using NzbDrone.Common.Instrumentation; +using NzbDrone.Core.Instrumentation; +using NzbDrone.Core.Lifecycle; +using NzbDrone.Core.Messaging.Events; +using Lidarr.Http.Extensions.Pipelines; +using TinyIoC; + +namespace Lidarr.Http +{ + public class LidarrBootstrapper : TinyIoCNancyBootstrapper + { + private readonly TinyIoCContainer _tinyIoCContainer; + private static readonly Logger Logger = NzbDroneLogger.GetLogger(typeof(LidarrBootstrapper)); + + public LidarrBootstrapper(TinyIoCContainer tinyIoCContainer) + { + _tinyIoCContainer = tinyIoCContainer; + } + + protected override void ApplicationStartup(TinyIoCContainer container, IPipelines pipelines) + { + Logger.Info("Starting Web Server"); + + if (RuntimeInfo.IsProduction) + { + DiagnosticsHook.Disable(pipelines); + } + + RegisterPipelines(pipelines); + + container.Resolve().Register(); + container.Resolve().PublishEvent(new ApplicationStartedEvent()); + } + + private void RegisterPipelines(IPipelines pipelines) + { + var pipelineRegistrars = _tinyIoCContainer.ResolveAll().OrderBy(v => v.Order).ToList(); + + foreach (var registerNancyPipeline in pipelineRegistrars) + { + registerNancyPipeline.Register(pipelines); + } + } + + protected override TinyIoCContainer GetApplicationContainer() + { + return _tinyIoCContainer; + } + + protected override DiagnosticsConfiguration DiagnosticsConfiguration => new DiagnosticsConfiguration { Password = @"password" }; + + protected override byte[] FavIcon => null; + } +} diff --git a/src/Lidarr.Http/LidarrRestModule.cs b/src/Lidarr.Http/LidarrRestModule.cs new file mode 100644 index 000000000..d5f148a06 --- /dev/null +++ b/src/Lidarr.Http/LidarrRestModule.cs @@ -0,0 +1,56 @@ +using System; +using NzbDrone.Core.Datastore; +using Lidarr.Http.REST; +using Lidarr.Http.Validation; + +namespace Lidarr.Http +{ + public abstract class LidarrRestModule : RestModule where TResource : RestResource, new() + { + protected string Resource { get; private set; } + + + private static string BaseUrl() + { + var isV3 = typeof(TResource).Namespace.Contains(".V3."); + if (isV3) + { + return "/api/v3/"; + } + return "/api/"; + } + + private static string ResourceName() + { + return new TResource().ResourceName.Trim('/').ToLower(); + } + + protected LidarrRestModule() + : this(ResourceName()) + { + } + + protected LidarrRestModule(string resource) + : base(BaseUrl() + resource.Trim('/').ToLower()) + { + Resource = resource; + PostValidator.RuleFor(r => r.Id).IsZero(); + PutValidator.RuleFor(r => r.Id).ValidId(); + } + + protected PagingResource ApplyToPage(Func, PagingSpec> function, PagingSpec pagingSpec, Converter mapper) + { + pagingSpec = function(pagingSpec); + + return new PagingResource + { + Page = pagingSpec.Page, + PageSize = pagingSpec.PageSize, + SortDirection = pagingSpec.SortDirection, + SortKey = pagingSpec.SortKey, + TotalRecords = pagingSpec.TotalRecords, + Records = pagingSpec.Records.ConvertAll(mapper) + }; + } + } +} diff --git a/src/Lidarr.Http/Mapping/MappingValidation.cs b/src/Lidarr.Http/Mapping/MappingValidation.cs new file mode 100644 index 000000000..e4f056520 --- /dev/null +++ b/src/Lidarr.Http/Mapping/MappingValidation.cs @@ -0,0 +1,54 @@ +using System; +using System.Linq; +using System.Reflection; +using NzbDrone.Common.Reflection; +using Lidarr.Http.REST; + +namespace Lidarr.Http.Mapping +{ + public static class MappingValidation + { + public static void ValidateMapping(Type modelType, Type resourceType) + { + var errors = modelType.GetSimpleProperties().Where(c=>!c.GetGetMethod().IsStatic).Select(p => GetError(resourceType, p)).Where(c => c != null).ToList(); + + if (errors.Any()) + { + throw new ResourceMappingException(errors); + } + + PrintExtraProperties(modelType, resourceType); + } + + private static void PrintExtraProperties(Type modelType, Type resourceType) + { + var resourceBaseProperties = typeof(RestResource).GetProperties().Select(c => c.Name); + var resourceProperties = resourceType.GetProperties().Select(c => c.Name).Except(resourceBaseProperties); + var modelProperties = modelType.GetProperties().Select(c => c.Name); + + var extra = resourceProperties.Except(modelProperties); + + foreach (var extraProp in extra) + { + Console.WriteLine("Extra: [{0}]", extraProp); + } + } + + private static string GetError(Type resourceType, PropertyInfo modelProperty) + { + var resourceProperty = resourceType.GetProperties().FirstOrDefault(c => c.Name == modelProperty.Name); + + if (resourceProperty == null) + { + return string.Format("public {0} {1} {{ get; set; }}", modelProperty.PropertyType.Name, modelProperty.Name); + } + + if (resourceProperty.PropertyType != modelProperty.PropertyType && !typeof(RestResource).IsAssignableFrom(resourceProperty.PropertyType)) + { + return string.Format("Expected {0}.{1} to have type of {2} but found {3}", resourceType.Name, resourceProperty.Name, modelProperty.PropertyType, resourceProperty.PropertyType); + } + + return null; + } + } +} \ No newline at end of file diff --git a/src/Lidarr.Http/Mapping/ResourceMappingException.cs b/src/Lidarr.Http/Mapping/ResourceMappingException.cs new file mode 100644 index 000000000..3744d3d5e --- /dev/null +++ b/src/Lidarr.Http/Mapping/ResourceMappingException.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Lidarr.Http.Mapping +{ + public class ResourceMappingException : ApplicationException + { + public ResourceMappingException(IEnumerable error) + : base(Environment.NewLine + string.Join(Environment.NewLine, error.OrderBy(c => c))) + { + + } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Api/PagingResource.cs b/src/Lidarr.Http/PagingResource.cs similarity index 95% rename from src/NzbDrone.Api/PagingResource.cs rename to src/Lidarr.Http/PagingResource.cs index b8025efc4..8b648fa4f 100644 --- a/src/NzbDrone.Api/PagingResource.cs +++ b/src/Lidarr.Http/PagingResource.cs @@ -1,7 +1,7 @@ -using System.Collections.Generic; +using System.Collections.Generic; using NzbDrone.Core.Datastore; -namespace NzbDrone.Api +namespace Lidarr.Http { public class PagingResource { diff --git a/src/Lidarr.Http/Properties/AssemblyInfo.cs b/src/Lidarr.Http/Properties/AssemblyInfo.cs new file mode 100644 index 000000000..9d340b973 --- /dev/null +++ b/src/Lidarr.Http/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("Sonarr.Nancy")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("Sonarr.Nancy")] +[assembly: AssemblyCopyright("Copyright © 2016")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("5370bff7-1bd7-46bc-af06-7d9ea5cda1d6")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/src/NzbDrone.Api/REST/BadRequestException.cs b/src/Lidarr.Http/REST/BadRequestException.cs similarity index 76% rename from src/NzbDrone.Api/REST/BadRequestException.cs rename to src/Lidarr.Http/REST/BadRequestException.cs index 450f484e5..5f61a1d49 100644 --- a/src/NzbDrone.Api/REST/BadRequestException.cs +++ b/src/Lidarr.Http/REST/BadRequestException.cs @@ -1,7 +1,7 @@ using Nancy; -using NzbDrone.Api.ErrorManagement; +using Lidarr.Http.Exceptions; -namespace NzbDrone.Api.REST +namespace Lidarr.Http.REST { public class BadRequestException : ApiException { diff --git a/src/NzbDrone.Api/REST/MethodNotAllowedException.cs b/src/Lidarr.Http/REST/MethodNotAllowedException.cs similarity index 78% rename from src/NzbDrone.Api/REST/MethodNotAllowedException.cs rename to src/Lidarr.Http/REST/MethodNotAllowedException.cs index 44d2065c6..e9dc7b74c 100644 --- a/src/NzbDrone.Api/REST/MethodNotAllowedException.cs +++ b/src/Lidarr.Http/REST/MethodNotAllowedException.cs @@ -1,7 +1,7 @@ using Nancy; -using NzbDrone.Api.ErrorManagement; +using Lidarr.Http.Exceptions; -namespace NzbDrone.Api.REST +namespace Lidarr.Http.REST { public class MethodNotAllowedException : ApiException { diff --git a/src/NzbDrone.Api/REST/NotFoundException.cs b/src/Lidarr.Http/REST/NotFoundException.cs similarity index 76% rename from src/NzbDrone.Api/REST/NotFoundException.cs rename to src/Lidarr.Http/REST/NotFoundException.cs index 92b4016a9..e8377ced4 100644 --- a/src/NzbDrone.Api/REST/NotFoundException.cs +++ b/src/Lidarr.Http/REST/NotFoundException.cs @@ -1,7 +1,7 @@ using Nancy; -using NzbDrone.Api.ErrorManagement; +using Lidarr.Http.Exceptions; -namespace NzbDrone.Api.REST +namespace Lidarr.Http.REST { public class NotFoundException : ApiException { diff --git a/src/NzbDrone.Api/REST/ResourceValidator.cs b/src/Lidarr.Http/REST/ResourceValidator.cs similarity index 95% rename from src/NzbDrone.Api/REST/ResourceValidator.cs rename to src/Lidarr.Http/REST/ResourceValidator.cs index 8062e6fd0..e052470d1 100644 --- a/src/NzbDrone.Api/REST/ResourceValidator.cs +++ b/src/Lidarr.Http/REST/ResourceValidator.cs @@ -1,13 +1,13 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Linq.Expressions; using FluentValidation; using FluentValidation.Internal; using FluentValidation.Resources; -using NzbDrone.Api.ClientSchema; -using System.Linq; +using Lidarr.Http.ClientSchema; -namespace NzbDrone.Api.REST +namespace Lidarr.Http.REST { public class ResourceValidator : AbstractValidator { diff --git a/src/NzbDrone.Api/REST/RestModule.cs b/src/Lidarr.Http/REST/RestModule.cs similarity index 92% rename from src/NzbDrone.Api/REST/RestModule.cs rename to src/Lidarr.Http/REST/RestModule.cs index 7c6ba37a4..040658aa9 100644 --- a/src/NzbDrone.Api/REST/RestModule.cs +++ b/src/Lidarr.Http/REST/RestModule.cs @@ -1,12 +1,12 @@ using System; using System.Collections.Generic; +using System.Linq; using FluentValidation; using Nancy; -using NzbDrone.Api.Extensions; -using System.Linq; using NzbDrone.Core.Datastore; +using Lidarr.Http.Extensions; -namespace NzbDrone.Api.REST +namespace Lidarr.Http.REST { public abstract class RestModule : NancyModule where TResource : RestResource, new() @@ -232,6 +232,7 @@ namespace NzbDrone.Api.REST { pagingResource.SortKey = Request.Query.SortKey.ToString(); + // For backwards compatibility with v2 if (Request.Query.SortDir != null) { pagingResource.SortDirection = Request.Query.SortDir.ToString() @@ -239,6 +240,15 @@ namespace NzbDrone.Api.REST ? SortDirection.Ascending : SortDirection.Descending; } + + // v3 uses SortDirection instead of SortDir to be consistent with every other use of it + if (Request.Query.SortDirection != null) + { + pagingResource.SortDirection = Request.Query.SortDirection.ToString() + .Equals("ascending", StringComparison.InvariantCultureIgnoreCase) + ? SortDirection.Ascending + : SortDirection.Descending; + } } if (Request.Query.FilterKey != null) diff --git a/src/NzbDrone.Api/REST/RestResource.cs b/src/Lidarr.Http/REST/RestResource.cs similarity index 91% rename from src/NzbDrone.Api/REST/RestResource.cs rename to src/Lidarr.Http/REST/RestResource.cs index ec9f195c6..b08aa6eef 100644 --- a/src/NzbDrone.Api/REST/RestResource.cs +++ b/src/Lidarr.Http/REST/RestResource.cs @@ -1,6 +1,6 @@ using Newtonsoft.Json; -namespace NzbDrone.Api.REST +namespace Lidarr.Http.REST { public abstract class RestResource { diff --git a/src/NzbDrone.Api/ResourceChangeMessage.cs b/src/Lidarr.Http/ResourceChangeMessage.cs similarity index 93% rename from src/NzbDrone.Api/ResourceChangeMessage.cs rename to src/Lidarr.Http/ResourceChangeMessage.cs index 6319dcc39..3d2e67c78 100644 --- a/src/NzbDrone.Api/ResourceChangeMessage.cs +++ b/src/Lidarr.Http/ResourceChangeMessage.cs @@ -1,8 +1,8 @@ using System; -using NzbDrone.Api.REST; using NzbDrone.Core.Datastore.Events; +using Lidarr.Http.REST; -namespace NzbDrone.Api +namespace Lidarr.Http { public class ResourceChangeMessage where TResource : RestResource { diff --git a/src/NzbDrone.Api/TinyIoCNancyBootstrapper.cs b/src/Lidarr.Http/TinyIoCNancyBootstrapper.cs similarity index 99% rename from src/NzbDrone.Api/TinyIoCNancyBootstrapper.cs rename to src/Lidarr.Http/TinyIoCNancyBootstrapper.cs index d938b0c6e..c4b3f5339 100644 --- a/src/NzbDrone.Api/TinyIoCNancyBootstrapper.cs +++ b/src/Lidarr.Http/TinyIoCNancyBootstrapper.cs @@ -7,7 +7,7 @@ using Nancy; using Nancy.Diagnostics; using Nancy.Bootstrapper; -namespace NzbDrone.Api +namespace Lidarr.Http { diff --git a/src/NzbDrone.Api/Validation/EmptyCollectionValidator.cs b/src/Lidarr.Http/Validation/EmptyCollectionValidator.cs similarity index 94% rename from src/NzbDrone.Api/Validation/EmptyCollectionValidator.cs rename to src/Lidarr.Http/Validation/EmptyCollectionValidator.cs index 432eb1ed9..1ad5e20e2 100644 --- a/src/NzbDrone.Api/Validation/EmptyCollectionValidator.cs +++ b/src/Lidarr.Http/Validation/EmptyCollectionValidator.cs @@ -2,7 +2,7 @@ using FluentValidation.Validators; using NzbDrone.Common.Extensions; -namespace NzbDrone.Api.Validation +namespace Lidarr.Http.Validation { public class EmptyCollectionValidator : PropertyValidator { diff --git a/src/NzbDrone.Api/Validation/RssSyncIntervalValidator.cs b/src/Lidarr.Http/Validation/RssSyncIntervalValidator.cs similarity index 95% rename from src/NzbDrone.Api/Validation/RssSyncIntervalValidator.cs rename to src/Lidarr.Http/Validation/RssSyncIntervalValidator.cs index 8a3f2d54c..797103b2b 100644 --- a/src/NzbDrone.Api/Validation/RssSyncIntervalValidator.cs +++ b/src/Lidarr.Http/Validation/RssSyncIntervalValidator.cs @@ -1,6 +1,6 @@ using FluentValidation.Validators; -namespace NzbDrone.Api.Validation +namespace Lidarr.Http.Validation { public class RssSyncIntervalValidator : PropertyValidator { diff --git a/src/NzbDrone.Api/Validation/RuleBuilderExtensions.cs b/src/Lidarr.Http/Validation/RuleBuilderExtensions.cs similarity index 97% rename from src/NzbDrone.Api/Validation/RuleBuilderExtensions.cs rename to src/Lidarr.Http/Validation/RuleBuilderExtensions.cs index 01a3a4f75..01f1608e2 100644 --- a/src/NzbDrone.Api/Validation/RuleBuilderExtensions.cs +++ b/src/Lidarr.Http/Validation/RuleBuilderExtensions.cs @@ -3,7 +3,7 @@ using System.Text.RegularExpressions; using FluentValidation; using FluentValidation.Validators; -namespace NzbDrone.Api.Validation +namespace Lidarr.Http.Validation { public static class RuleBuilderExtensions { diff --git a/src/Lidarr.Http/app.config b/src/Lidarr.Http/app.config new file mode 100644 index 000000000..c6693bd88 --- /dev/null +++ b/src/Lidarr.Http/app.config @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/src/Lidarr.Http/packages.config b/src/Lidarr.Http/packages.config new file mode 100644 index 000000000..4260a6994 --- /dev/null +++ b/src/Lidarr.Http/packages.config @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/src/Lidarr.sln b/src/Lidarr.sln new file mode 100644 index 000000000..fc5dfaa83 --- /dev/null +++ b/src/Lidarr.sln @@ -0,0 +1,313 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 15 +VisualStudioVersion = 15.0.27004.2002 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{57A04B72-8088-4F75-A582-1158CF8291F7}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Test.Common", "Test.Common", "{47697CDB-27B6-4B05-B4F8-0CBE6F6EDF97}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NzbDrone.Test.Dummy", "NzbDrone.Test.Dummy\NzbDrone.Test.Dummy.csproj", "{FAFB5948-A222-4CF6-AD14-026BE7564802}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NzbDrone.Test.Common", "NzbDrone.Test.Common\NzbDrone.Test.Common.csproj", "{CADDFCE0-7509-4430-8364-2074E1EEFCA2}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NzbDrone.Core.Test", "NzbDrone.Core.Test\NzbDrone.Core.Test.csproj", "{193ADD3B-792B-4173-8E4C-5A3F8F0237F0}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NzbDrone.Host.Test", "NzbDrone.App.Test\NzbDrone.Host.Test.csproj", "{C0EA1A40-91AD-4EEB-BD16-2DDDEBD20AE5}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NzbDrone.Update.Test", "NzbDrone.Update.Test\NzbDrone.Update.Test.csproj", "{35388E8E-0CDB-4A84-AD16-E4B6EFDA5D97}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NzbDrone.Common.Test", "NzbDrone.Common.Test\NzbDrone.Common.Test.csproj", "{BEC74619-DDBB-4FBA-B517-D3E20AFC9997}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NzbDrone.Api.Test", "NzbDrone.Api.Test\NzbDrone.Api.Test.csproj", "{D18A5DEB-5102-4775-A1AF-B75DAAA8907B}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NzbDrone.Libraries.Test", "NzbDrone.Libraries.Test\NzbDrone.Libraries.Test.csproj", "{CBF6B8B0-A015-413A-8C86-01238BB45770}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NzbDrone.Integration.Test", "NzbDrone.Integration.Test\NzbDrone.Integration.Test.csproj", "{8CEFECD0-A6C2-498F-98B1-3FBE5820F9AB}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NzbDrone.Automation.Test", "NzbDrone.Automation.Test\NzbDrone.Automation.Test.csproj", "{CC26800D-F67E-464B-88DE-8EB1A0C227A3}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "WindowsServiceHelpers", "WindowsServiceHelpers", "{F9E67978-5CD6-4A5F-827B-4249711C0B02}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ServiceInstall", "ServiceHelpers\ServiceInstall\ServiceInstall.csproj", "{6BCE712F-846D-4846-9D1B-A66B858DA755}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ServiceUninstall", "ServiceHelpers\ServiceUninstall\ServiceUninstall.csproj", "{700D0B95-95CD-43F3-B6C9-FAA0FC1358D4}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NzbDrone.Core", "NzbDrone.Core\NzbDrone.Core.csproj", "{FF5EE3B6-913B-47CE-9CEB-11C51B4E1205}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NzbDrone.Update", "NzbDrone.Update\NzbDrone.Update.csproj", "{4CCC53CD-8D5E-4CC4-97D2-5C9312AC2BD7}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NzbDrone.Common", "NzbDrone.Common\NzbDrone.Common.csproj", "{F2BE0FDF-6E47-4827-A420-DD4EF82407F8}" + ProjectSection(ProjectDependencies) = postProject + {9DC31DE3-79FF-47A8-96B4-6BA18F6BB1CB} = {9DC31DE3-79FF-47A8-96B4-6BA18F6BB1CB} + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".nuget", ".nuget", "{1E6B3CBE-1578-41C1-9BF9-78D818740BE9}" + ProjectSection(SolutionItems) = preProject + .nuget\NuGet.exe = .nuget\NuGet.exe + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Host", "Host", "{486ADF86-DD89-4E19-B805-9D94F19800D9}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NzbDrone.Console", "NzbDrone.Console\NzbDrone.Console.csproj", "{3DCA7B58-B8B3-49AC-9D9E-56F4A0460976}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NzbDrone.Host", "NzbDrone.Host\NzbDrone.Host.csproj", "{95C11A9E-56ED-456A-8447-2C89C1139266}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NzbDrone", "NzbDrone\NzbDrone.csproj", "{D12F7F2F-8A3C-415F-88FA-6DD061A84869}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NzbDrone.SignalR", "NzbDrone.SignalR\NzbDrone.SignalR.csproj", "{7C2CC69F-5CA0-4E5C-85CB-983F9F6C3B36}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "External", "External", "{F6E3A728-AE77-4D02-BAC8-82FBC1402DDA}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Marr.Data", "Marr.Data\Marr.Data.csproj", "{F6FC6BE7-0847-4817-A1ED-223DC647C3D7}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NzbDrone.Mono", "NzbDrone.Mono\NzbDrone.Mono.csproj", "{15AD7579-A314-4626-B556-663F51D97CD1}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NzbDrone.Windows", "NzbDrone.Windows\NzbDrone.Windows.csproj", "{911284D3-F130-459E-836C-2430B6FBF21D}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Platform", "Platform", "{0F0D4998-8F5D-4467-A909-BB192C4B3B4B}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Platform", "Platform", "{4EACDBBC-BCD7-4765-A57B-3E08331E4749}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NzbDrone.Windows.Test", "NzbDrone.Windows.Test\NzbDrone.Windows.Test.csproj", "{80B51429-7A0E-46D6-BEE3-C80DCB1C4EAA}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NzbDrone.Mono.Test", "NzbDrone.Mono.Test\NzbDrone.Mono.Test.csproj", "{40D72824-7D02-4A77-9106-8FE0EEA2B997}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MonoTorrent", "MonoTorrent\MonoTorrent.csproj", "{411A9E0E-FDC6-4E25-828A-0C2CD1CD96F8}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LogentriesCore", "LogentriesCore\LogentriesCore.csproj", "{90D6E9FC-7B88-4E1B-B018-8FA742274558}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LogentriesNLog", "LogentriesNLog\LogentriesNLog.csproj", "{9DC31DE3-79FF-47A8-96B4-6BA18F6BB1CB}" + ProjectSection(ProjectDependencies) = postProject + {90D6E9FC-7B88-4E1B-B018-8FA742274558} = {90D6E9FC-7B88-4E1B-B018-8FA742274558} + EndProjectSection +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CurlSharp", "ExternalModules\CurlSharp\CurlSharp\CurlSharp.csproj", "{74420A79-CC16-442C-8B1E-7C1B913844F0}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lidarr.Api.V3", "Lidarr.Api.V3\Lidarr.Api.V3.csproj", "{7140FF1F-79BE-492F-9188-B21A050BF708}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lidarr.Http", "Lidarr.Http\Lidarr.Http.csproj", "{5370BFF7-1BD7-46BC-AF06-7D9EA5CDA1D6}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|x86 = Debug|x86 + Mono|x86 = Mono|x86 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {FAFB5948-A222-4CF6-AD14-026BE7564802}.Debug|x86.ActiveCfg = Debug|x86 + {FAFB5948-A222-4CF6-AD14-026BE7564802}.Debug|x86.Build.0 = Debug|x86 + {FAFB5948-A222-4CF6-AD14-026BE7564802}.Mono|x86.ActiveCfg = Release|x86 + {FAFB5948-A222-4CF6-AD14-026BE7564802}.Mono|x86.Build.0 = Release|x86 + {FAFB5948-A222-4CF6-AD14-026BE7564802}.Release|x86.ActiveCfg = Release|x86 + {FAFB5948-A222-4CF6-AD14-026BE7564802}.Release|x86.Build.0 = Release|x86 + {CADDFCE0-7509-4430-8364-2074E1EEFCA2}.Debug|x86.ActiveCfg = Debug|x86 + {CADDFCE0-7509-4430-8364-2074E1EEFCA2}.Debug|x86.Build.0 = Debug|x86 + {CADDFCE0-7509-4430-8364-2074E1EEFCA2}.Mono|x86.ActiveCfg = Debug|x86 + {CADDFCE0-7509-4430-8364-2074E1EEFCA2}.Mono|x86.Build.0 = Debug|x86 + {CADDFCE0-7509-4430-8364-2074E1EEFCA2}.Release|x86.ActiveCfg = Release|x86 + {CADDFCE0-7509-4430-8364-2074E1EEFCA2}.Release|x86.Build.0 = Release|x86 + {193ADD3B-792B-4173-8E4C-5A3F8F0237F0}.Debug|x86.ActiveCfg = Debug|x86 + {193ADD3B-792B-4173-8E4C-5A3F8F0237F0}.Debug|x86.Build.0 = Debug|x86 + {193ADD3B-792B-4173-8E4C-5A3F8F0237F0}.Mono|x86.ActiveCfg = Debug|x86 + {193ADD3B-792B-4173-8E4C-5A3F8F0237F0}.Mono|x86.Build.0 = Debug|x86 + {193ADD3B-792B-4173-8E4C-5A3F8F0237F0}.Release|x86.ActiveCfg = Release|x86 + {193ADD3B-792B-4173-8E4C-5A3F8F0237F0}.Release|x86.Build.0 = Release|x86 + {C0EA1A40-91AD-4EEB-BD16-2DDDEBD20AE5}.Debug|x86.ActiveCfg = Debug|x86 + {C0EA1A40-91AD-4EEB-BD16-2DDDEBD20AE5}.Debug|x86.Build.0 = Debug|x86 + {C0EA1A40-91AD-4EEB-BD16-2DDDEBD20AE5}.Mono|x86.ActiveCfg = Debug|x86 + {C0EA1A40-91AD-4EEB-BD16-2DDDEBD20AE5}.Mono|x86.Build.0 = Debug|x86 + {C0EA1A40-91AD-4EEB-BD16-2DDDEBD20AE5}.Release|x86.ActiveCfg = Release|x86 + {C0EA1A40-91AD-4EEB-BD16-2DDDEBD20AE5}.Release|x86.Build.0 = Release|x86 + {35388E8E-0CDB-4A84-AD16-E4B6EFDA5D97}.Debug|x86.ActiveCfg = Debug|x86 + {35388E8E-0CDB-4A84-AD16-E4B6EFDA5D97}.Debug|x86.Build.0 = Debug|x86 + {35388E8E-0CDB-4A84-AD16-E4B6EFDA5D97}.Mono|x86.ActiveCfg = Debug|x86 + {35388E8E-0CDB-4A84-AD16-E4B6EFDA5D97}.Mono|x86.Build.0 = Debug|x86 + {35388E8E-0CDB-4A84-AD16-E4B6EFDA5D97}.Release|x86.ActiveCfg = Release|x86 + {35388E8E-0CDB-4A84-AD16-E4B6EFDA5D97}.Release|x86.Build.0 = Release|x86 + {BEC74619-DDBB-4FBA-B517-D3E20AFC9997}.Debug|x86.ActiveCfg = Debug|x86 + {BEC74619-DDBB-4FBA-B517-D3E20AFC9997}.Debug|x86.Build.0 = Debug|x86 + {BEC74619-DDBB-4FBA-B517-D3E20AFC9997}.Mono|x86.ActiveCfg = Debug|x86 + {BEC74619-DDBB-4FBA-B517-D3E20AFC9997}.Mono|x86.Build.0 = Debug|x86 + {BEC74619-DDBB-4FBA-B517-D3E20AFC9997}.Release|x86.ActiveCfg = Release|x86 + {BEC74619-DDBB-4FBA-B517-D3E20AFC9997}.Release|x86.Build.0 = Release|x86 + {D18A5DEB-5102-4775-A1AF-B75DAAA8907B}.Debug|x86.ActiveCfg = Debug|x86 + {D18A5DEB-5102-4775-A1AF-B75DAAA8907B}.Debug|x86.Build.0 = Debug|x86 + {D18A5DEB-5102-4775-A1AF-B75DAAA8907B}.Mono|x86.ActiveCfg = Release|x86 + {D18A5DEB-5102-4775-A1AF-B75DAAA8907B}.Mono|x86.Build.0 = Release|x86 + {D18A5DEB-5102-4775-A1AF-B75DAAA8907B}.Release|x86.ActiveCfg = Release|x86 + {D18A5DEB-5102-4775-A1AF-B75DAAA8907B}.Release|x86.Build.0 = Release|x86 + {CBF6B8B0-A015-413A-8C86-01238BB45770}.Debug|x86.ActiveCfg = Debug|x86 + {CBF6B8B0-A015-413A-8C86-01238BB45770}.Debug|x86.Build.0 = Debug|x86 + {CBF6B8B0-A015-413A-8C86-01238BB45770}.Mono|x86.ActiveCfg = Debug|x86 + {CBF6B8B0-A015-413A-8C86-01238BB45770}.Mono|x86.Build.0 = Debug|x86 + {CBF6B8B0-A015-413A-8C86-01238BB45770}.Release|x86.ActiveCfg = Release|x86 + {CBF6B8B0-A015-413A-8C86-01238BB45770}.Release|x86.Build.0 = Release|x86 + {8CEFECD0-A6C2-498F-98B1-3FBE5820F9AB}.Debug|x86.ActiveCfg = Debug|x86 + {8CEFECD0-A6C2-498F-98B1-3FBE5820F9AB}.Debug|x86.Build.0 = Debug|x86 + {8CEFECD0-A6C2-498F-98B1-3FBE5820F9AB}.Mono|x86.ActiveCfg = Debug|x86 + {8CEFECD0-A6C2-498F-98B1-3FBE5820F9AB}.Mono|x86.Build.0 = Debug|x86 + {8CEFECD0-A6C2-498F-98B1-3FBE5820F9AB}.Release|x86.ActiveCfg = Release|x86 + {8CEFECD0-A6C2-498F-98B1-3FBE5820F9AB}.Release|x86.Build.0 = Release|x86 + {CC26800D-F67E-464B-88DE-8EB1A0C227A3}.Debug|x86.ActiveCfg = Debug|x86 + {CC26800D-F67E-464B-88DE-8EB1A0C227A3}.Debug|x86.Build.0 = Debug|x86 + {CC26800D-F67E-464B-88DE-8EB1A0C227A3}.Mono|x86.ActiveCfg = Debug|x86 + {CC26800D-F67E-464B-88DE-8EB1A0C227A3}.Mono|x86.Build.0 = Debug|x86 + {CC26800D-F67E-464B-88DE-8EB1A0C227A3}.Release|x86.ActiveCfg = Release|x86 + {CC26800D-F67E-464B-88DE-8EB1A0C227A3}.Release|x86.Build.0 = Release|x86 + {6BCE712F-846D-4846-9D1B-A66B858DA755}.Debug|x86.ActiveCfg = Debug|x86 + {6BCE712F-846D-4846-9D1B-A66B858DA755}.Debug|x86.Build.0 = Debug|x86 + {6BCE712F-846D-4846-9D1B-A66B858DA755}.Mono|x86.ActiveCfg = Debug|x86 + {6BCE712F-846D-4846-9D1B-A66B858DA755}.Release|x86.ActiveCfg = Release|x86 + {6BCE712F-846D-4846-9D1B-A66B858DA755}.Release|x86.Build.0 = Release|x86 + {700D0B95-95CD-43F3-B6C9-FAA0FC1358D4}.Debug|x86.ActiveCfg = Debug|x86 + {700D0B95-95CD-43F3-B6C9-FAA0FC1358D4}.Debug|x86.Build.0 = Debug|x86 + {700D0B95-95CD-43F3-B6C9-FAA0FC1358D4}.Mono|x86.ActiveCfg = Debug|x86 + {700D0B95-95CD-43F3-B6C9-FAA0FC1358D4}.Release|x86.ActiveCfg = Release|x86 + {700D0B95-95CD-43F3-B6C9-FAA0FC1358D4}.Release|x86.Build.0 = Release|x86 + {FF5EE3B6-913B-47CE-9CEB-11C51B4E1205}.Debug|x86.ActiveCfg = Debug|x86 + {FF5EE3B6-913B-47CE-9CEB-11C51B4E1205}.Debug|x86.Build.0 = Debug|x86 + {FF5EE3B6-913B-47CE-9CEB-11C51B4E1205}.Mono|x86.ActiveCfg = Release|x86 + {FF5EE3B6-913B-47CE-9CEB-11C51B4E1205}.Mono|x86.Build.0 = Release|x86 + {FF5EE3B6-913B-47CE-9CEB-11C51B4E1205}.Release|x86.ActiveCfg = Release|x86 + {FF5EE3B6-913B-47CE-9CEB-11C51B4E1205}.Release|x86.Build.0 = Release|x86 + {4CCC53CD-8D5E-4CC4-97D2-5C9312AC2BD7}.Debug|x86.ActiveCfg = Debug|x86 + {4CCC53CD-8D5E-4CC4-97D2-5C9312AC2BD7}.Debug|x86.Build.0 = Debug|x86 + {4CCC53CD-8D5E-4CC4-97D2-5C9312AC2BD7}.Mono|x86.ActiveCfg = Debug|x86 + {4CCC53CD-8D5E-4CC4-97D2-5C9312AC2BD7}.Mono|x86.Build.0 = Debug|x86 + {4CCC53CD-8D5E-4CC4-97D2-5C9312AC2BD7}.Release|x86.ActiveCfg = Release|x86 + {4CCC53CD-8D5E-4CC4-97D2-5C9312AC2BD7}.Release|x86.Build.0 = Release|x86 + {F2BE0FDF-6E47-4827-A420-DD4EF82407F8}.Debug|x86.ActiveCfg = Debug|x86 + {F2BE0FDF-6E47-4827-A420-DD4EF82407F8}.Debug|x86.Build.0 = Debug|x86 + {F2BE0FDF-6E47-4827-A420-DD4EF82407F8}.Mono|x86.ActiveCfg = Release|x86 + {F2BE0FDF-6E47-4827-A420-DD4EF82407F8}.Mono|x86.Build.0 = Release|x86 + {F2BE0FDF-6E47-4827-A420-DD4EF82407F8}.Release|x86.ActiveCfg = Release|x86 + {F2BE0FDF-6E47-4827-A420-DD4EF82407F8}.Release|x86.Build.0 = Release|x86 + {3DCA7B58-B8B3-49AC-9D9E-56F4A0460976}.Debug|x86.ActiveCfg = Debug|x86 + {3DCA7B58-B8B3-49AC-9D9E-56F4A0460976}.Debug|x86.Build.0 = Debug|x86 + {3DCA7B58-B8B3-49AC-9D9E-56F4A0460976}.Mono|x86.ActiveCfg = Debug|x86 + {3DCA7B58-B8B3-49AC-9D9E-56F4A0460976}.Mono|x86.Build.0 = Debug|x86 + {3DCA7B58-B8B3-49AC-9D9E-56F4A0460976}.Release|x86.ActiveCfg = Release|x86 + {3DCA7B58-B8B3-49AC-9D9E-56F4A0460976}.Release|x86.Build.0 = Release|x86 + {95C11A9E-56ED-456A-8447-2C89C1139266}.Debug|x86.ActiveCfg = Debug|x86 + {95C11A9E-56ED-456A-8447-2C89C1139266}.Debug|x86.Build.0 = Debug|x86 + {95C11A9E-56ED-456A-8447-2C89C1139266}.Mono|x86.ActiveCfg = Debug|x86 + {95C11A9E-56ED-456A-8447-2C89C1139266}.Mono|x86.Build.0 = Debug|x86 + {95C11A9E-56ED-456A-8447-2C89C1139266}.Release|x86.ActiveCfg = Release|x86 + {95C11A9E-56ED-456A-8447-2C89C1139266}.Release|x86.Build.0 = Release|x86 + {D12F7F2F-8A3C-415F-88FA-6DD061A84869}.Debug|x86.ActiveCfg = Debug|x86 + {D12F7F2F-8A3C-415F-88FA-6DD061A84869}.Debug|x86.Build.0 = Debug|x86 + {D12F7F2F-8A3C-415F-88FA-6DD061A84869}.Mono|x86.ActiveCfg = Release|x86 + {D12F7F2F-8A3C-415F-88FA-6DD061A84869}.Release|x86.ActiveCfg = Release|x86 + {D12F7F2F-8A3C-415F-88FA-6DD061A84869}.Release|x86.Build.0 = Release|x86 + {7C2CC69F-5CA0-4E5C-85CB-983F9F6C3B36}.Debug|x86.ActiveCfg = Debug|x86 + {7C2CC69F-5CA0-4E5C-85CB-983F9F6C3B36}.Debug|x86.Build.0 = Debug|x86 + {7C2CC69F-5CA0-4E5C-85CB-983F9F6C3B36}.Mono|x86.ActiveCfg = Debug|x86 + {7C2CC69F-5CA0-4E5C-85CB-983F9F6C3B36}.Mono|x86.Build.0 = Debug|x86 + {7C2CC69F-5CA0-4E5C-85CB-983F9F6C3B36}.Release|x86.ActiveCfg = Release|x86 + {7C2CC69F-5CA0-4E5C-85CB-983F9F6C3B36}.Release|x86.Build.0 = Release|x86 + {F6FC6BE7-0847-4817-A1ED-223DC647C3D7}.Debug|x86.ActiveCfg = Debug|x86 + {F6FC6BE7-0847-4817-A1ED-223DC647C3D7}.Debug|x86.Build.0 = Debug|x86 + {F6FC6BE7-0847-4817-A1ED-223DC647C3D7}.Mono|x86.ActiveCfg = Release|x86 + {F6FC6BE7-0847-4817-A1ED-223DC647C3D7}.Mono|x86.Build.0 = Release|x86 + {F6FC6BE7-0847-4817-A1ED-223DC647C3D7}.Release|x86.ActiveCfg = Release|x86 + {F6FC6BE7-0847-4817-A1ED-223DC647C3D7}.Release|x86.Build.0 = Release|x86 + {15AD7579-A314-4626-B556-663F51D97CD1}.Debug|x86.ActiveCfg = Debug|x86 + {15AD7579-A314-4626-B556-663F51D97CD1}.Debug|x86.Build.0 = Debug|x86 + {15AD7579-A314-4626-B556-663F51D97CD1}.Mono|x86.ActiveCfg = Release|x86 + {15AD7579-A314-4626-B556-663F51D97CD1}.Release|x86.ActiveCfg = Release|x86 + {15AD7579-A314-4626-B556-663F51D97CD1}.Release|x86.Build.0 = Release|x86 + {911284D3-F130-459E-836C-2430B6FBF21D}.Debug|x86.ActiveCfg = Debug|x86 + {911284D3-F130-459E-836C-2430B6FBF21D}.Debug|x86.Build.0 = Debug|x86 + {911284D3-F130-459E-836C-2430B6FBF21D}.Mono|x86.ActiveCfg = Release|x86 + {911284D3-F130-459E-836C-2430B6FBF21D}.Release|x86.ActiveCfg = Release|x86 + {911284D3-F130-459E-836C-2430B6FBF21D}.Release|x86.Build.0 = Release|x86 + {80B51429-7A0E-46D6-BEE3-C80DCB1C4EAA}.Debug|x86.ActiveCfg = Debug|x86 + {80B51429-7A0E-46D6-BEE3-C80DCB1C4EAA}.Debug|x86.Build.0 = Debug|x86 + {80B51429-7A0E-46D6-BEE3-C80DCB1C4EAA}.Mono|x86.ActiveCfg = Release|x86 + {80B51429-7A0E-46D6-BEE3-C80DCB1C4EAA}.Release|x86.ActiveCfg = Release|x86 + {80B51429-7A0E-46D6-BEE3-C80DCB1C4EAA}.Release|x86.Build.0 = Release|x86 + {40D72824-7D02-4A77-9106-8FE0EEA2B997}.Debug|x86.ActiveCfg = Debug|x86 + {40D72824-7D02-4A77-9106-8FE0EEA2B997}.Debug|x86.Build.0 = Debug|x86 + {40D72824-7D02-4A77-9106-8FE0EEA2B997}.Mono|x86.ActiveCfg = Release|x86 + {40D72824-7D02-4A77-9106-8FE0EEA2B997}.Release|x86.ActiveCfg = Release|x86 + {40D72824-7D02-4A77-9106-8FE0EEA2B997}.Release|x86.Build.0 = Release|x86 + {411A9E0E-FDC6-4E25-828A-0C2CD1CD96F8}.Debug|x86.ActiveCfg = Debug|x86 + {411A9E0E-FDC6-4E25-828A-0C2CD1CD96F8}.Debug|x86.Build.0 = Debug|x86 + {411A9E0E-FDC6-4E25-828A-0C2CD1CD96F8}.Mono|x86.ActiveCfg = Release|x86 + {411A9E0E-FDC6-4E25-828A-0C2CD1CD96F8}.Mono|x86.Build.0 = Release|x86 + {411A9E0E-FDC6-4E25-828A-0C2CD1CD96F8}.Release|x86.ActiveCfg = Release|x86 + {411A9E0E-FDC6-4E25-828A-0C2CD1CD96F8}.Release|x86.Build.0 = Release|x86 + {90D6E9FC-7B88-4E1B-B018-8FA742274558}.Debug|x86.ActiveCfg = Debug|x86 + {90D6E9FC-7B88-4E1B-B018-8FA742274558}.Debug|x86.Build.0 = Debug|x86 + {90D6E9FC-7B88-4E1B-B018-8FA742274558}.Mono|x86.ActiveCfg = Release|x86 + {90D6E9FC-7B88-4E1B-B018-8FA742274558}.Mono|x86.Build.0 = Release|x86 + {90D6E9FC-7B88-4E1B-B018-8FA742274558}.Release|x86.ActiveCfg = Release|x86 + {90D6E9FC-7B88-4E1B-B018-8FA742274558}.Release|x86.Build.0 = Release|x86 + {9DC31DE3-79FF-47A8-96B4-6BA18F6BB1CB}.Debug|x86.ActiveCfg = Debug|x86 + {9DC31DE3-79FF-47A8-96B4-6BA18F6BB1CB}.Debug|x86.Build.0 = Debug|x86 + {9DC31DE3-79FF-47A8-96B4-6BA18F6BB1CB}.Mono|x86.ActiveCfg = Release|x86 + {9DC31DE3-79FF-47A8-96B4-6BA18F6BB1CB}.Mono|x86.Build.0 = Release|x86 + {9DC31DE3-79FF-47A8-96B4-6BA18F6BB1CB}.Release|x86.ActiveCfg = Release|x86 + {9DC31DE3-79FF-47A8-96B4-6BA18F6BB1CB}.Release|x86.Build.0 = Release|x86 + {74420A79-CC16-442C-8B1E-7C1B913844F0}.Debug|x86.ActiveCfg = Debug|Any CPU + {74420A79-CC16-442C-8B1E-7C1B913844F0}.Debug|x86.Build.0 = Debug|Any CPU + {74420A79-CC16-442C-8B1E-7C1B913844F0}.Mono|x86.ActiveCfg = Release|Any CPU + {74420A79-CC16-442C-8B1E-7C1B913844F0}.Mono|x86.Build.0 = Release|Any CPU + {74420A79-CC16-442C-8B1E-7C1B913844F0}.Release|x86.ActiveCfg = Release|Any CPU + {74420A79-CC16-442C-8B1E-7C1B913844F0}.Release|x86.Build.0 = Release|Any CPU + {7140FF1F-79BE-492F-9188-B21A050BF708}.Debug|x86.ActiveCfg = Debug|x86 + {7140FF1F-79BE-492F-9188-B21A050BF708}.Debug|x86.Build.0 = Debug|x86 + {7140FF1F-79BE-492F-9188-B21A050BF708}.Mono|x86.ActiveCfg = Release|x86 + {7140FF1F-79BE-492F-9188-B21A050BF708}.Mono|x86.Build.0 = Release|x86 + {7140FF1F-79BE-492F-9188-B21A050BF708}.Release|x86.ActiveCfg = Release|x86 + {7140FF1F-79BE-492F-9188-B21A050BF708}.Release|x86.Build.0 = Release|x86 + {5370BFF7-1BD7-46BC-AF06-7D9EA5CDA1D6}.Debug|x86.ActiveCfg = Debug|x86 + {5370BFF7-1BD7-46BC-AF06-7D9EA5CDA1D6}.Debug|x86.Build.0 = Debug|x86 + {5370BFF7-1BD7-46BC-AF06-7D9EA5CDA1D6}.Mono|x86.ActiveCfg = Release|x86 + {5370BFF7-1BD7-46BC-AF06-7D9EA5CDA1D6}.Mono|x86.Build.0 = Release|x86 + {5370BFF7-1BD7-46BC-AF06-7D9EA5CDA1D6}.Release|x86.ActiveCfg = Release|x86 + {5370BFF7-1BD7-46BC-AF06-7D9EA5CDA1D6}.Release|x86.Build.0 = Release|x86 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {47697CDB-27B6-4B05-B4F8-0CBE6F6EDF97} = {57A04B72-8088-4F75-A582-1158CF8291F7} + {FAFB5948-A222-4CF6-AD14-026BE7564802} = {47697CDB-27B6-4B05-B4F8-0CBE6F6EDF97} + {CADDFCE0-7509-4430-8364-2074E1EEFCA2} = {47697CDB-27B6-4B05-B4F8-0CBE6F6EDF97} + {193ADD3B-792B-4173-8E4C-5A3F8F0237F0} = {57A04B72-8088-4F75-A582-1158CF8291F7} + {C0EA1A40-91AD-4EEB-BD16-2DDDEBD20AE5} = {57A04B72-8088-4F75-A582-1158CF8291F7} + {35388E8E-0CDB-4A84-AD16-E4B6EFDA5D97} = {57A04B72-8088-4F75-A582-1158CF8291F7} + {BEC74619-DDBB-4FBA-B517-D3E20AFC9997} = {57A04B72-8088-4F75-A582-1158CF8291F7} + {D18A5DEB-5102-4775-A1AF-B75DAAA8907B} = {57A04B72-8088-4F75-A582-1158CF8291F7} + {CBF6B8B0-A015-413A-8C86-01238BB45770} = {57A04B72-8088-4F75-A582-1158CF8291F7} + {8CEFECD0-A6C2-498F-98B1-3FBE5820F9AB} = {57A04B72-8088-4F75-A582-1158CF8291F7} + {CC26800D-F67E-464B-88DE-8EB1A0C227A3} = {57A04B72-8088-4F75-A582-1158CF8291F7} + {6BCE712F-846D-4846-9D1B-A66B858DA755} = {F9E67978-5CD6-4A5F-827B-4249711C0B02} + {700D0B95-95CD-43F3-B6C9-FAA0FC1358D4} = {F9E67978-5CD6-4A5F-827B-4249711C0B02} + {3DCA7B58-B8B3-49AC-9D9E-56F4A0460976} = {486ADF86-DD89-4E19-B805-9D94F19800D9} + {95C11A9E-56ED-456A-8447-2C89C1139266} = {486ADF86-DD89-4E19-B805-9D94F19800D9} + {D12F7F2F-8A3C-415F-88FA-6DD061A84869} = {486ADF86-DD89-4E19-B805-9D94F19800D9} + {F6FC6BE7-0847-4817-A1ED-223DC647C3D7} = {F6E3A728-AE77-4D02-BAC8-82FBC1402DDA} + {15AD7579-A314-4626-B556-663F51D97CD1} = {0F0D4998-8F5D-4467-A909-BB192C4B3B4B} + {911284D3-F130-459E-836C-2430B6FBF21D} = {0F0D4998-8F5D-4467-A909-BB192C4B3B4B} + {4EACDBBC-BCD7-4765-A57B-3E08331E4749} = {57A04B72-8088-4F75-A582-1158CF8291F7} + {80B51429-7A0E-46D6-BEE3-C80DCB1C4EAA} = {4EACDBBC-BCD7-4765-A57B-3E08331E4749} + {40D72824-7D02-4A77-9106-8FE0EEA2B997} = {4EACDBBC-BCD7-4765-A57B-3E08331E4749} + {411A9E0E-FDC6-4E25-828A-0C2CD1CD96F8} = {F6E3A728-AE77-4D02-BAC8-82FBC1402DDA} + {90D6E9FC-7B88-4E1B-B018-8FA742274558} = {F6E3A728-AE77-4D02-BAC8-82FBC1402DDA} + {9DC31DE3-79FF-47A8-96B4-6BA18F6BB1CB} = {F6E3A728-AE77-4D02-BAC8-82FBC1402DDA} + {74420A79-CC16-442C-8B1E-7C1B913844F0} = {F6E3A728-AE77-4D02-BAC8-82FBC1402DDA} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + EnterpriseLibraryConfigurationToolBinariesPath = packages\Unity.2.1.505.0\lib\NET35;packages\Unity.2.1.505.2\lib\NET35 + SolutionGuid = {2C047BC5-490F-4DCE-962F-141370D23765} + EndGlobalSection + GlobalSection(MonoDevelopProperties) = preSolution + StartupItem = NzbDrone.Console\NzbDrone.Console.csproj + EndGlobalSection + GlobalSection(JSLint) = preSolution + SolutionConfigurationLocation = JSLintOptions.xml + EndGlobalSection +EndGlobal diff --git a/src/LogentriesCore/LogentriesCore.csproj b/src/LogentriesCore/LogentriesCore.csproj index 4f6c66677..eb2d0c560 100644 --- a/src/LogentriesCore/LogentriesCore.csproj +++ b/src/LogentriesCore/LogentriesCore.csproj @@ -51,6 +51,9 @@ MinimumRecommendedRules.ruleset + + ..\packages\Microsoft.WindowsAzure.ConfigurationManager.2.0.1.0\lib\net40\Microsoft.WindowsAzure.Configuration.dll + diff --git a/src/LogentriesNLog/LogentriesNLog.csproj b/src/LogentriesNLog/LogentriesNLog.csproj index 54bf715e7..0b6ea6670 100644 --- a/src/LogentriesNLog/LogentriesNLog.csproj +++ b/src/LogentriesNLog/LogentriesNLog.csproj @@ -52,7 +52,7 @@ - ..\packages\NLog.4.4.3\lib\net40\NLog.dll + ..\packages\NLog.4.4.12\lib\net40\NLog.dll diff --git a/src/LogentriesNLog/packages.config b/src/LogentriesNLog/packages.config index a14101dce..44135561f 100644 --- a/src/LogentriesNLog/packages.config +++ b/src/LogentriesNLog/packages.config @@ -1,4 +1,4 @@  - + \ No newline at end of file diff --git a/src/Microsoft.AspNet.SignalR.Core/AuthorizeAttribute.cs b/src/Microsoft.AspNet.SignalR.Core/AuthorizeAttribute.cs deleted file mode 100644 index 05caef0a0..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/AuthorizeAttribute.cs +++ /dev/null @@ -1,160 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -using System; -using System.Diagnostics.CodeAnalysis; -using System.Linq; -using System.Security.Principal; -using Microsoft.AspNet.SignalR.Hubs; - -namespace Microsoft.AspNet.SignalR -{ - /// - /// Apply to Hubs and Hub methods to authorize client connections to Hubs and authorize client invocations of Hub methods. - /// - [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = true, AllowMultiple = true)] - [SuppressMessage("Microsoft.Performance", "CA1813:AvoidUnsealedAttributes", Justification = "MVC and WebAPI don't seal their AuthorizeAttributes")] - public class AuthorizeAttribute : Attribute, IAuthorizeHubConnection, IAuthorizeHubMethodInvocation - { - private string _roles; - private string[] _rolesSplit = new string[0]; - private string _users; - private string[] _usersSplit = new string[0]; - - [SuppressMessage("Microsoft.Design", "CA1051:DoNotDeclareVisibleInstanceFields", Justification = "Already somewhat represented by set-only RequiredOutgoing property.")] - protected bool? _requireOutgoing; - - /// - /// Set to false to apply authorization only to the invocations of any of the Hub's server-side methods. - /// This property only affects attributes applied to the Hub class. - /// This property cannot be read. - /// - [SuppressMessage("Microsoft.Design", "CA1065:DoNotRaiseExceptionsInUnexpectedLocations", Justification = "Must be property because this is an attribute parameter.")] - public bool RequireOutgoing - { - // It is impossible to tell here whether the attribute is being applied to a method or class. This makes - // it impossible to determine whether the value should be true or false when _requireOutgoing is null. - // It is also impossible to have a Nullable attribute parameter type. - get { throw new NotImplementedException(Resources.Error_DoNotReadRequireOutgoing); } - set { _requireOutgoing = value; } - } - - /// - /// Gets or sets the user roles. - /// - public string Roles - { - get { return _roles ?? String.Empty; } - set - { - _roles = value; - _rolesSplit = SplitString(value); - } - } - - /// - /// Gets or sets the authorized users. - /// - public string Users - { - get { return _users ?? String.Empty; } - set - { - _users = value; - _usersSplit = SplitString(value); - } - } - - /// - /// Determines whether client is authorized to connect to . - /// - /// Description of the hub client is attempting to connect to. - /// The (re)connect request from the client. - /// true if the caller is authorized to connect to the hub; otherwise, false. - public virtual bool AuthorizeHubConnection(HubDescriptor hubDescriptor, IRequest request) - { - if (request == null) - { - throw new ArgumentNullException("request"); - } - - // If RequireOutgoing is explicitly set to false, authorize all connections. - if (_requireOutgoing.HasValue && !_requireOutgoing.Value) - { - return true; - } - - return UserAuthorized(request.User); - } - - /// - /// Determines whether client is authorized to invoke the method. - /// - /// An providing details regarding the method invocation. - /// Indicates whether the interface instance is an attribute applied directly to a method. - /// true if the caller is authorized to invoke the method; otherwise, false. - public virtual bool AuthorizeHubMethodInvocation(IHubIncomingInvokerContext hubIncomingInvokerContext, bool appliesToMethod) - { - if (hubIncomingInvokerContext == null) - { - throw new ArgumentNullException("hubIncomingInvokerContext"); - } - - // It is impossible to require outgoing auth at the method level with SignalR's current design. - // Even though this isn't the stage at which outgoing auth would be applied, we want to throw a runtime error - // to indicate when the attribute is being used with obviously incorrect expectations. - - // We must explicitly check if _requireOutgoing is true since it is a Nullable type. - if (appliesToMethod && (_requireOutgoing == true)) - { - throw new ArgumentException(Resources.Error_MethodLevelOutgoingAuthorization); - } - - return UserAuthorized(hubIncomingInvokerContext.Hub.Context.User); - } - - /// - /// When overridden, provides an entry point for custom authorization checks. - /// Called by and . - /// - /// The for the client being authorize - /// true if the user is authorized, otherwise, false - protected virtual bool UserAuthorized(IPrincipal user) - { - if (user == null) - { - return false; - } - - if (!user.Identity.IsAuthenticated) - { - return false; - } - - if (_usersSplit.Length > 0 && !_usersSplit.Contains(user.Identity.Name, StringComparer.OrdinalIgnoreCase)) - { - return false; - } - - if (_rolesSplit.Length > 0 && !_rolesSplit.Any(user.IsInRole)) - { - return false; - } - - return true; - } - - private static string[] SplitString(string original) - { - if (String.IsNullOrEmpty(original)) - { - return new string[0]; - } - - var split = from piece in original.Split(',') - let trimmed = piece.Trim() - where !String.IsNullOrEmpty(trimmed) - select trimmed; - return split.ToArray(); - } - } -} diff --git a/src/Microsoft.AspNet.SignalR.Core/Configuration/ConfigurationExtensions.cs b/src/Microsoft.AspNet.SignalR.Core/Configuration/ConfigurationExtensions.cs deleted file mode 100644 index 630fad897..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/Configuration/ConfigurationExtensions.cs +++ /dev/null @@ -1,61 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -using System; - -namespace Microsoft.AspNet.SignalR.Configuration -{ - internal static class ConfigurationExtensions - { - public const int MissedTimeoutsBeforeClientReconnect = 2; - public const int HeartBeatsPerKeepAlive = 2; - public const int HeartBeatsPerDisconnectTimeout = 6; - - /// - /// The amount of time the client should wait without seeing a keep alive before trying to reconnect. - /// - public static TimeSpan? KeepAliveTimeout(this IConfigurationManager config) - { - if (config.KeepAlive != null) - { - return TimeSpan.FromTicks(config.KeepAlive.Value.Ticks * MissedTimeoutsBeforeClientReconnect); - } - else - { - return null; - } - } - - /// - /// The interval between successively checking connection states. - /// - public static TimeSpan HeartbeatInterval(this IConfigurationManager config) - { - if (config.KeepAlive != null) - { - return TimeSpan.FromTicks(config.KeepAlive.Value.Ticks / HeartBeatsPerKeepAlive); - } - else - { - // If KeepAlives are disabled, have the heartbeat run at the same rate it would if the KeepAlive was - // kept at the default value. - return TimeSpan.FromTicks(config.DisconnectTimeout.Ticks / HeartBeatsPerDisconnectTimeout); - } - } - - /// - /// The amount of time a Topic should stay in memory after its last subscriber is removed. - /// - /// - /// - public static TimeSpan TopicTtl(this IConfigurationManager config) - { - // If the deep-alive is disabled, don't take it into account when calculating the topic TTL. - var keepAliveTimeout = config.KeepAliveTimeout() ?? TimeSpan.Zero; - - // Keep topics alive for twice as long as we let connections to reconnect. (The DisconnectTimeout) - // Also add twice the keep-alive timeout since clients might take a while to notice they are disconnected. - // This should be a very conservative estimate for how long we must wait before considering a topic dead. - return TimeSpan.FromTicks((config.DisconnectTimeout.Ticks + keepAliveTimeout.Ticks) * 2); - } - } -} diff --git a/src/Microsoft.AspNet.SignalR.Core/Configuration/DefaultConfigurationManager.cs b/src/Microsoft.AspNet.SignalR.Core/Configuration/DefaultConfigurationManager.cs deleted file mode 100644 index af49f1978..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/Configuration/DefaultConfigurationManager.cs +++ /dev/null @@ -1,89 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -using System; - -namespace Microsoft.AspNet.SignalR.Configuration -{ - public class DefaultConfigurationManager : IConfigurationManager - { - // The below effectively sets the minimum heartbeat to once per second. - // if _minimumKeepAlive != 2 seconds, update the ArguementOutOfRanceExceptionMessage below - private static readonly TimeSpan _minimumKeepAlive = TimeSpan.FromSeconds(2); - - // if _minimumKeepAlivesPerDisconnectTimeout != 3, update the ArguementOutOfRanceExceptionMessage below - private const int _minimumKeepAlivesPerDisconnectTimeout = 3; - - // if _minimumDisconnectTimeout != 6 seconds, update the ArguementOutOfRanceExceptionMessage below - private static readonly TimeSpan _minimumDisconnectTimeout = TimeSpan.FromTicks(_minimumKeepAlive.Ticks * _minimumKeepAlivesPerDisconnectTimeout); - - private bool _keepAliveConfigured; - private TimeSpan? _keepAlive; - private TimeSpan _disconnectTimeout; - - public DefaultConfigurationManager() - { - ConnectionTimeout = TimeSpan.FromSeconds(110); - DisconnectTimeout = TimeSpan.FromSeconds(30); - DefaultMessageBufferSize = 1000; - } - - // TODO: Should we guard against negative TimeSpans here like everywhere else? - public TimeSpan ConnectionTimeout - { - get; - set; - } - - public TimeSpan DisconnectTimeout - { - get - { - return _disconnectTimeout; - } - set - { - if (value < _minimumDisconnectTimeout) - { - throw new ArgumentOutOfRangeException("value", Resources.Error_DisconnectTimeoutMustBeAtLeastSixSeconds); - } - - if (_keepAliveConfigured) - { - throw new InvalidOperationException(Resources.Error_DisconnectTimeoutCannotBeConfiguredAfterKeepAlive); - } - - _disconnectTimeout = value; - _keepAlive = TimeSpan.FromTicks(_disconnectTimeout.Ticks / _minimumKeepAlivesPerDisconnectTimeout); - } - } - - public TimeSpan? KeepAlive - { - get - { - return _keepAlive; - } - set - { - if (value < _minimumKeepAlive) - { - throw new ArgumentOutOfRangeException("value", Resources.Error_KeepAliveMustBeGreaterThanTwoSeconds); - } - - if (value > TimeSpan.FromTicks(_disconnectTimeout.Ticks / _minimumKeepAlivesPerDisconnectTimeout)) - { - throw new ArgumentOutOfRangeException("value", Resources.Error_KeepAliveMustBeNoMoreThanAThirdOfTheDisconnectTimeout); - } - - _keepAlive = value; - _keepAliveConfigured = true; - } - } - - public int DefaultMessageBufferSize - { - get; - set; - } - } -} diff --git a/src/Microsoft.AspNet.SignalR.Core/Configuration/IConfigurationManager.cs b/src/Microsoft.AspNet.SignalR.Core/Configuration/IConfigurationManager.cs deleted file mode 100644 index 9dd79a241..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/Configuration/IConfigurationManager.cs +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -using System; - -namespace Microsoft.AspNet.SignalR.Configuration -{ - /// - /// Provides access to server configuration. - /// - public interface IConfigurationManager - { - /// - /// Gets or sets a representing the amount of time to leave a connection open before timing out. - /// - TimeSpan ConnectionTimeout { get; set; } - - /// - /// Gets or sets a representing the amount of time to wait after a connection goes away before raising the disconnect event. - /// - TimeSpan DisconnectTimeout { get; set; } - - /// - /// Gets or sets a representing the amount of time between send keep alive messages. - /// If enabled, this value must be at least two seconds. Set to null to disable. - /// - TimeSpan? KeepAlive { get; set; } - - /// - /// Gets of sets the number of messages to buffer for a specific signal. - /// - int DefaultMessageBufferSize { get; set; } - } -} diff --git a/src/Microsoft.AspNet.SignalR.Core/ConnectionConfiguration.cs b/src/Microsoft.AspNet.SignalR.Core/ConnectionConfiguration.cs deleted file mode 100644 index eef56241e..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/ConnectionConfiguration.cs +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -namespace Microsoft.AspNet.SignalR -{ - public class ConnectionConfiguration - { - // Resolver isn't set to GlobalHost.DependencyResolver in the ctor because it is lazily created. - private IDependencyResolver _resolver; - - /// - /// The dependency resolver to use for the hub connection. - /// - public IDependencyResolver Resolver - { - get { return _resolver ?? GlobalHost.DependencyResolver; } - set { _resolver = value; } - } - - /// - /// Determines if browsers can make cross domain requests to SignalR endpoints. - /// - public bool EnableCrossDomain { get; set; } - } -} diff --git a/src/Microsoft.AspNet.SignalR.Core/ConnectionExtensions.cs b/src/Microsoft.AspNet.SignalR.Core/ConnectionExtensions.cs deleted file mode 100644 index 07cc587e1..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/ConnectionExtensions.cs +++ /dev/null @@ -1,60 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -using System; -using System.Threading.Tasks; -using Microsoft.AspNet.SignalR.Infrastructure; - -namespace Microsoft.AspNet.SignalR -{ - public static class ConnectionExtensions - { - /// - /// Sends a message to all connections subscribed to the specified signal. An example of signal may be a - /// specific connection id. - /// - /// The connection - /// The connectionId to send to. - /// The value to publish. - /// The list of connection ids to exclude - /// A task that represents when the broadcast is complete. - public static Task Send(this IConnection connection, string connectionId, object value, params string[] excludeConnectionIds) - { - if (connection == null) - { - throw new ArgumentNullException("connection"); - } - - if (string.IsNullOrEmpty(connectionId)) - { - throw new ArgumentException(Resources.Error_ArgumentNullOrEmpty, "connectionId"); - } - - var message = new ConnectionMessage(PrefixHelper.GetConnectionId(connectionId), - value, - PrefixHelper.GetPrefixedConnectionIds(excludeConnectionIds)); - - return connection.Send(message); - } - - /// - /// Broadcasts a value to all connections, excluding the connection ids specified. - /// - /// The connection - /// The value to broadcast. - /// The list of connection ids to exclude - /// A task that represents when the broadcast is complete. - public static Task Broadcast(this IConnection connection, object value, params string[] excludeConnectionIds) - { - if (connection == null) - { - throw new ArgumentNullException("connection"); - } - - var message = new ConnectionMessage(connection.DefaultSignal, - value, - PrefixHelper.GetPrefixedConnectionIds(excludeConnectionIds)); - - return connection.Send(message); - } - } -} diff --git a/src/Microsoft.AspNet.SignalR.Core/ConnectionMessage.cs b/src/Microsoft.AspNet.SignalR.Core/ConnectionMessage.cs deleted file mode 100644 index 6c632c9f6..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/ConnectionMessage.cs +++ /dev/null @@ -1,51 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using Microsoft.AspNet.SignalR.Infrastructure; - -namespace Microsoft.AspNet.SignalR -{ - /// - /// A message sent to one more connections. - /// - [SuppressMessage("Microsoft.Performance", "CA1815:OverrideEqualsAndOperatorEqualsOnValueTypes", Justification = "Messags are never compared, just used as data.")] - public struct ConnectionMessage - { - /// - /// The signal to this message should be sent to. Connections subscribed to this signal - /// will receive the message payload. - /// - public string Signal { get; private set; } - - /// - /// The payload of the message. - /// - public object Value { get; private set; } - - /// - /// Represents a list of signals that should be used to filter what connections - /// receive this message. - /// - public IList ExcludedSignals { get; private set; } - - public ConnectionMessage(string signal, object value) - : this(signal, value, ListHelper.Empty) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// The signal - /// The payload of the message - /// The signals to exclude. - public ConnectionMessage(string signal, object value, IList excludedSignals) - : this() - { - Signal = signal; - Value = value; - ExcludedSignals = excludedSignals; - } - } -} diff --git a/src/Microsoft.AspNet.SignalR.Core/Cookie.cs b/src/Microsoft.AspNet.SignalR.Core/Cookie.cs deleted file mode 100644 index b251da93d..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/Cookie.cs +++ /dev/null @@ -1,28 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -using System; - -namespace Microsoft.AspNet.SignalR -{ - public class Cookie - { - public Cookie(string name, string value) - : this(name, value, String.Empty, String.Empty) - { - - } - - public Cookie(string name, string value, string domain, string path) - { - Name = name; - Value = value; - Domain = domain; - Path = path; - } - - public string Name { get; private set; } - public string Domain { get; private set; } - public string Path { get; private set; } - public string Value { get; private set; } - } -} diff --git a/src/Microsoft.AspNet.SignalR.Core/DefaultDependencyResolver.cs b/src/Microsoft.AspNet.SignalR.Core/DefaultDependencyResolver.cs deleted file mode 100644 index da8cb14fc..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/DefaultDependencyResolver.cs +++ /dev/null @@ -1,231 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Globalization; -using System.Linq; -using System.Threading; -using Microsoft.AspNet.SignalR.Configuration; -using Microsoft.AspNet.SignalR.Hubs; -using Microsoft.AspNet.SignalR.Infrastructure; -using Microsoft.AspNet.SignalR.Json; -using Microsoft.AspNet.SignalR.Messaging; -using Microsoft.AspNet.SignalR.Tracing; -using Microsoft.AspNet.SignalR.Transports; - -namespace Microsoft.AspNet.SignalR -{ - public class DefaultDependencyResolver : IDependencyResolver - { - private readonly Dictionary>> _resolvers = new Dictionary>>(); - private readonly HashSet _trackedDisposables = new HashSet(); - private int _disposed; - - [SuppressMessage("Microsoft.Usage", "CA2214:DoNotCallOverridableMethodsInConstructors", Justification = "It's easiest")] - public DefaultDependencyResolver() - { - RegisterDefaultServices(); - - // Hubs - RegisterHubExtensions(); - } - - [SuppressMessage("Microsoft.Maintainability", "CA1506:AvoidExcessiveClassCoupling", Justification = "The resolver is the class that does the most coupling by design.")] - [SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "The resolver disposes dependencies on Dispose.")] - private void RegisterDefaultServices() - { - var traceManager = new Lazy(() => new TraceManager()); - Register(typeof(ITraceManager), () => traceManager.Value); - - var serverIdManager = new ServerIdManager(); - Register(typeof(IServerIdManager), () => serverIdManager); - - var serverMessageHandler = new Lazy(() => new ServerCommandHandler(this)); - Register(typeof(IServerCommandHandler), () => serverMessageHandler.Value); - - var newMessageBus = new Lazy(() => new MessageBus(this)); - Register(typeof(IMessageBus), () => newMessageBus.Value); - - var stringMinifier = new Lazy(() => new StringMinifier()); - Register(typeof(IStringMinifier), () => stringMinifier.Value); - - var serializer = new Lazy(); - Register(typeof(IJsonSerializer), () => serializer.Value); - - var transportManager = new Lazy(() => new TransportManager(this)); - Register(typeof(ITransportManager), () => transportManager.Value); - - var configurationManager = new DefaultConfigurationManager(); - Register(typeof(IConfigurationManager), () => configurationManager); - - var transportHeartbeat = new Lazy(() => new TransportHeartbeat(this)); - Register(typeof(ITransportHeartbeat), () => transportHeartbeat.Value); - - var connectionManager = new Lazy(() => new ConnectionManager(this)); - Register(typeof(IConnectionManager), () => connectionManager.Value); - - var ackHandler = new Lazy(); - Register(typeof(IAckHandler), () => ackHandler.Value); - - var perfCounterWriter = new Lazy(() => new PerformanceCounterManager(this)); - Register(typeof(IPerformanceCounterManager), () => perfCounterWriter.Value); - - var protectedData = new DefaultProtectedData(); - Register(typeof(IProtectedData), () => protectedData); - } - - private void RegisterHubExtensions() - { - var methodDescriptorProvider = new Lazy(); - Register(typeof(IMethodDescriptorProvider), () => methodDescriptorProvider.Value); - - var hubDescriptorProvider = new Lazy(() => new ReflectedHubDescriptorProvider(this)); - Register(typeof(IHubDescriptorProvider), () => hubDescriptorProvider.Value); - - var parameterBinder = new Lazy(); - Register(typeof(IParameterResolver), () => parameterBinder.Value); - - var activator = new Lazy(() => new DefaultHubActivator(this)); - Register(typeof(IHubActivator), () => activator.Value); - - var hubManager = new Lazy(() => new DefaultHubManager(this)); - Register(typeof(IHubManager), () => hubManager.Value); - - var proxyGenerator = new Lazy(() => new DefaultJavaScriptProxyGenerator(this)); - Register(typeof(IJavaScriptProxyGenerator), () => proxyGenerator.Value); - - var requestParser = new Lazy(); - Register(typeof(IHubRequestParser), () => requestParser.Value); - - var assemblyLocator = new Lazy(() => new DefaultAssemblyLocator()); - Register(typeof(IAssemblyLocator), () => assemblyLocator.Value); - - // Setup the default hub pipeline - var dispatcher = new Lazy(() => new HubPipeline().AddModule(new AuthorizeModule())); - Register(typeof(IHubPipeline), () => dispatcher.Value); - Register(typeof(IHubPipelineInvoker), () => dispatcher.Value); - } - - public virtual object GetService(Type serviceType) - { - if (serviceType == null) - { - throw new ArgumentNullException("serviceType"); - } - - IList> activators; - if (_resolvers.TryGetValue(serviceType, out activators)) - { - if (activators.Count == 0) - { - return null; - } - if (activators.Count > 1) - { - throw new InvalidOperationException(String.Format(CultureInfo.CurrentCulture, Resources.Error_MultipleActivatorsAreaRegisteredCallGetServices, serviceType.FullName)); - } - return Track(activators[0]); - } - return null; - } - - public virtual IEnumerable GetServices(Type serviceType) - { - IList> activators; - if (_resolvers.TryGetValue(serviceType, out activators)) - { - if (activators.Count == 0) - { - return null; - } - return activators.Select(Track).ToList(); - } - return null; - } - - public virtual void Register(Type serviceType, Func activator) - { - IList> activators; - if (!_resolvers.TryGetValue(serviceType, out activators)) - { - activators = new List>(); - _resolvers.Add(serviceType, activators); - } - else - { - activators.Clear(); - } - activators.Add(activator); - } - - public virtual void Register(Type serviceType, IEnumerable> activators) - { - if (activators == null) - { - throw new ArgumentNullException("activators"); - } - - IList> list; - if (!_resolvers.TryGetValue(serviceType, out list)) - { - list = new List>(); - _resolvers.Add(serviceType, list); - } - else - { - list.Clear(); - } - foreach (var a in activators) - { - list.Add(a); - } - } - - private object Track(Func creator) - { - object obj = creator(); - - if (_disposed == 0) - { - var disposable = obj as IDisposable; - if (disposable != null) - { - lock (_trackedDisposables) - { - if (_disposed == 0) - { - _trackedDisposables.Add(disposable); - } - } - } - } - - return obj; - } - - protected virtual void Dispose(bool disposing) - { - if (disposing) - { - if (Interlocked.Exchange(ref _disposed, 1) == 0) - { - lock (_trackedDisposables) - { - foreach (var d in _trackedDisposables) - { - d.Dispose(); - } - - _trackedDisposables.Clear(); - } - } - } - } - - public void Dispose() - { - Dispose(true); - } - } -} diff --git a/src/Microsoft.AspNet.SignalR.Core/DependencyResolverExtensions.cs b/src/Microsoft.AspNet.SignalR.Core/DependencyResolverExtensions.cs deleted file mode 100644 index d579634b1..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/DependencyResolverExtensions.cs +++ /dev/null @@ -1,61 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -using System; -using System.Collections.Generic; -using System.Linq; - -namespace Microsoft.AspNet.SignalR -{ - public static class DependencyResolverExtensions - { - public static T Resolve(this IDependencyResolver resolver) - { - if (resolver == null) - { - throw new ArgumentNullException("resolver"); - } - - return (T)resolver.GetService(typeof(T)); - } - - public static object Resolve(this IDependencyResolver resolver, Type type) - { - if (resolver == null) - { - throw new ArgumentNullException("resolver"); - } - - if (type == null) - { - throw new ArgumentNullException("type"); - } - - return resolver.GetService(type); - } - - public static IEnumerable ResolveAll(this IDependencyResolver resolver) - { - if (resolver == null) - { - throw new ArgumentNullException("resolver"); - } - - return resolver.GetServices(typeof(T)).Cast(); - } - - public static IEnumerable ResolveAll(this IDependencyResolver resolver, Type type) - { - if (resolver == null) - { - throw new ArgumentNullException("resolver"); - } - - if (type == null) - { - throw new ArgumentNullException("type"); - } - - return resolver.GetServices(type); - } - } -} diff --git a/src/Microsoft.AspNet.SignalR.Core/GlobalHost.cs b/src/Microsoft.AspNet.SignalR.Core/GlobalHost.cs deleted file mode 100644 index 7571495fc..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/GlobalHost.cs +++ /dev/null @@ -1,66 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -using System; -using Microsoft.AspNet.SignalR.Configuration; -using Microsoft.AspNet.SignalR.Hubs; -using Microsoft.AspNet.SignalR.Infrastructure; - -namespace Microsoft.AspNet.SignalR -{ - /// - /// Provides access to default host information. - /// - public static class GlobalHost - { - private static readonly Lazy _defaultResolver = new Lazy(() => new DefaultDependencyResolver()); - private static IDependencyResolver _resolver; - - /// - /// Gets or sets the the default - /// - public static IDependencyResolver DependencyResolver - { - get - { - return _resolver ?? _defaultResolver.Value; - } - set - { - _resolver = value; - } - } - - /// - /// Gets the default - /// - public static IConfigurationManager Configuration - { - get - { - return DependencyResolver.Resolve(); - } - } - - /// - /// Gets the default - /// - public static IConnectionManager ConnectionManager - { - get - { - return DependencyResolver.Resolve(); - } - } - - /// - /// - /// - public static IHubPipeline HubPipeline - { - get - { - return DependencyResolver.Resolve(); - } - } - } -} diff --git a/src/Microsoft.AspNet.SignalR.Core/GroupManager.cs b/src/Microsoft.AspNet.SignalR.Core/GroupManager.cs deleted file mode 100644 index c57476f70..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/GroupManager.cs +++ /dev/null @@ -1,117 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -using System; -using System.Threading.Tasks; -using Microsoft.AspNet.SignalR.Infrastructure; -using Microsoft.AspNet.SignalR.Messaging; - -namespace Microsoft.AspNet.SignalR -{ - /// - /// The default implementation. - /// - public class GroupManager : IConnectionGroupManager - { - private readonly IConnection _connection; - private readonly string _groupPrefix; - - /// - /// Initializes a new instance of the class. - /// - /// The this group resides on. - /// The prefix for this group. Either a name or type name. - public GroupManager(IConnection connection, string groupPrefix) - { - if (connection == null) - { - throw new ArgumentNullException("connection"); - } - - _connection = connection; - _groupPrefix = groupPrefix; - } - - /// - /// Sends a value to the specified group. - /// - /// The name of the group. - /// The value to send. - /// The list of connection ids to exclude - /// A task that represents when send is complete. - public Task Send(string groupName, object value, params string[] excludeConnectionIds) - { - if (string.IsNullOrEmpty(groupName)) - { - throw new ArgumentException((Resources.Error_ArgumentNullOrEmpty), "groupName"); - } - - var qualifiedName = CreateQualifiedName(groupName); - var message = new ConnectionMessage(qualifiedName, - value, - PrefixHelper.GetPrefixedConnectionIds(excludeConnectionIds)); - - return _connection.Send(message); - } - - /// - /// Adds a connection to the specified group. - /// - /// The connection id to add to the group. - /// The name of the group - /// A task that represents the connection id being added to the group. - public Task Add(string connectionId, string groupName) - { - if (connectionId == null) - { - throw new ArgumentNullException("connectionId"); - } - - if (groupName == null) - { - throw new ArgumentNullException("groupName"); - } - - var command = new Command - { - CommandType = CommandType.AddToGroup, - Value = CreateQualifiedName(groupName), - WaitForAck = true - }; - - return _connection.Send(connectionId, command); - } - - /// - /// Removes a connection from the specified group. - /// - /// The connection id to remove from the group. - /// The name of the group - /// A task that represents the connection id being removed from the group. - public Task Remove(string connectionId, string groupName) - { - if (connectionId == null) - { - throw new ArgumentNullException("connectionId"); - } - - if (groupName == null) - { - throw new ArgumentNullException("groupName"); - } - - var command = new Command - { - CommandType = CommandType.RemoveFromGroup, - Value = CreateQualifiedName(groupName), - WaitForAck = true - }; - - return _connection.Send(connectionId, command); - } - - private string CreateQualifiedName(string groupName) - { - return _groupPrefix + "." + groupName; - } - } -} diff --git a/src/Microsoft.AspNet.SignalR.Core/Hosting/HostConstants.cs b/src/Microsoft.AspNet.SignalR.Core/Hosting/HostConstants.cs deleted file mode 100644 index 9cb8b132a..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/Hosting/HostConstants.cs +++ /dev/null @@ -1,26 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -namespace Microsoft.AspNet.SignalR.Hosting -{ - public static class HostConstants - { - /// - /// The host should set this if they need to enable debug mode - /// - public static readonly string DebugMode = "debugMode"; - - /// - /// The host should set this is web sockets can be supported - /// - public static readonly string SupportsWebSockets = "supportsWebSockets"; - - /// - /// The host should set this if the web socket url is different - /// - public static readonly string WebSocketServerUrl = "webSocketServerUrl"; - - public static readonly string ShutdownToken = "shutdownToken"; - - public static readonly string InstanceName = "instanceName"; - } -} diff --git a/src/Microsoft.AspNet.SignalR.Core/Hosting/HostContext.cs b/src/Microsoft.AspNet.SignalR.Core/Hosting/HostContext.cs deleted file mode 100644 index cd5324f90..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/Hosting/HostContext.cs +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -using System; -using System.Collections.Generic; - -namespace Microsoft.AspNet.SignalR.Hosting -{ - public class HostContext - { - public IRequest Request { get; private set; } - public IResponse Response { get; private set; } - public IDictionary Items { get; private set; } - - public HostContext(IRequest request, IResponse response) - { - Request = request; - Response = response; - Items = new Dictionary(StringComparer.OrdinalIgnoreCase); - } - } -} diff --git a/src/Microsoft.AspNet.SignalR.Core/Hosting/HostContextExtensions.cs b/src/Microsoft.AspNet.SignalR.Core/Hosting/HostContextExtensions.cs deleted file mode 100644 index d40d9e5ac..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/Hosting/HostContextExtensions.cs +++ /dev/null @@ -1,53 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -using System; -using System.Threading; - -namespace Microsoft.AspNet.SignalR.Hosting -{ - public static class HostContextExtensions - { - public static T GetValue(this HostContext context, string key) - { - if (context == null) - { - throw new ArgumentNullException("context"); - } - - object value; - if (context.Items.TryGetValue(key, out value)) - { - return (T)value; - } - return default(T); - } - - public static bool IsDebuggingEnabled(this HostContext context) - { - return context.GetValue(HostConstants.DebugMode); - } - - public static bool SupportsWebSockets(this HostContext context) - { - // The server needs to implement IWebSocketRequest for websockets to be supported. - // It also needs to set the flag in the items collection. - return context.GetValue(HostConstants.SupportsWebSockets) && - context.Request is IWebSocketRequest; - } - - public static string WebSocketServerUrl(this HostContext context) - { - return context.GetValue(HostConstants.WebSocketServerUrl); - } - - public static CancellationToken HostShutdownToken(this HostContext context) - { - return context.GetValue(HostConstants.ShutdownToken); - } - - public static string InstanceName(this HostContext context) - { - return context.GetValue(HostConstants.InstanceName); - } - } -} diff --git a/src/Microsoft.AspNet.SignalR.Core/Hosting/HostDependencyResolverExtensions.cs b/src/Microsoft.AspNet.SignalR.Core/Hosting/HostDependencyResolverExtensions.cs deleted file mode 100644 index c674b317b..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/Hosting/HostDependencyResolverExtensions.cs +++ /dev/null @@ -1,55 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -using System; -using System.Threading; -using Microsoft.AspNet.SignalR.Infrastructure; - -namespace Microsoft.AspNet.SignalR.Hosting -{ - public static class HostDependencyResolverExtensions - { - public static void InitializeHost(this IDependencyResolver resolver, string instanceName, CancellationToken hostShutdownToken) - { - if (resolver == null) - { - throw new ArgumentNullException("resolver"); - } - - if (String.IsNullOrEmpty(instanceName)) - { - throw new ArgumentNullException("instanceName"); - } - - // Performance counters are broken on mono so just skip this step - if (!MonoUtility.IsRunningMono) - { - // Initialize the performance counters - resolver.InitializePerformanceCounters(instanceName, hostShutdownToken); - } - - // Dispose the dependency resolver on host shut down (cleanly) - resolver.InitializeResolverDispose(hostShutdownToken); - } - - private static void InitializePerformanceCounters(this IDependencyResolver resolver, string instanceName, CancellationToken hostShutdownToken) - { - var counters = resolver.Resolve(); - if (counters != null) - { - counters.Initialize(instanceName, hostShutdownToken); - } - } - - private static void InitializeResolverDispose(this IDependencyResolver resolver, CancellationToken hostShutdownToken) - { - // TODO: Guard against multiple calls to this - - // When the host triggers the shutdown token, dispose the resolver - hostShutdownToken.SafeRegister(state => - { - ((IDependencyResolver)state).Dispose(); - }, - resolver); - } - } -} diff --git a/src/Microsoft.AspNet.SignalR.Core/Hosting/IResponse.cs b/src/Microsoft.AspNet.SignalR.Core/Hosting/IResponse.cs deleted file mode 100644 index 3d26f0e61..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/Hosting/IResponse.cs +++ /dev/null @@ -1,49 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -using System; -using System.Diagnostics.CodeAnalysis; -using System.Threading; -using System.Threading.Tasks; - -namespace Microsoft.AspNet.SignalR.Hosting -{ - /// - /// Represents a connection to the client. - /// - public interface IResponse - { - /// - /// Gets a cancellation token that represents the client's lifetime. - /// - CancellationToken CancellationToken { get; } - - /// - /// Gets or sets the status code of the response. - /// - int StatusCode { get; set; } - - /// - /// Gets or sets the content type of the response. - /// - string ContentType { get; set; } - - /// - /// Writes buffered data. - /// - /// The data to write to the buffer. - void Write(ArraySegment data); - - /// - /// Flushes the buffered response to the client. - /// - /// A task that represents when the data has been flushed. - Task Flush(); - - /// - /// Closes the connection to the client. - /// - /// A task that represents when the connection is closed. - [SuppressMessage("Microsoft.Naming", "CA1716:IdentifiersShouldNotMatchKeywords", MessageId = "End", Justification = "Ends the response thus the name is appropriate.")] - Task End(); - } -} diff --git a/src/Microsoft.AspNet.SignalR.Core/Hosting/IWebSocket.cs b/src/Microsoft.AspNet.SignalR.Core/Hosting/IWebSocket.cs deleted file mode 100644 index 60c448f2a..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/Hosting/IWebSocket.cs +++ /dev/null @@ -1,48 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -using System; -using System.Threading.Tasks; - -namespace Microsoft.AspNet.SignalR.Hosting -{ - /// - /// Represents a web socket. - /// - public interface IWebSocket - { - /// - /// Invoked when data is sent over the websocket - /// - Action OnMessage { get; set; } - - /// - /// Invoked when the websocket closes - /// - Action OnClose { get; set; } - - /// - /// Invoked when there is an error - /// - Action OnError { get; set; } - - /// - /// Sends data over the websocket. - /// - /// The value to send. - /// A that represents the send is complete. - Task Send(string value); - - /// - /// Sends a chunk of data over the websocket ("endOfMessage" flag set to false.) - /// - /// - /// A that represents the send is complete. - Task SendChunk(ArraySegment message); - - /// - /// Sends a zero byte data chunk with the "endOfMessage" flag set to true. - /// - /// A that represents the flush is complete. - Task Flush(); - } -} diff --git a/src/Microsoft.AspNet.SignalR.Core/Hosting/IWebSocketRequest.cs b/src/Microsoft.AspNet.SignalR.Core/Hosting/IWebSocketRequest.cs deleted file mode 100644 index 95f5438f2..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/Hosting/IWebSocketRequest.cs +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -using System; -using System.Threading.Tasks; - -namespace Microsoft.AspNet.SignalR.Hosting -{ - public interface IWebSocketRequest : IRequest - { - /// - /// Accepts an websocket request using the specified user function. - /// - /// The callback that fires when the websocket is ready. - /// The task that completes when the websocket transport is ready. - Task AcceptWebSocketRequest(Func callback, Task initTask); - } -} diff --git a/src/Microsoft.AspNet.SignalR.Core/Hosting/PersistentConnectionFactory.cs b/src/Microsoft.AspNet.SignalR.Core/Hosting/PersistentConnectionFactory.cs deleted file mode 100644 index 33d9aeee1..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/Hosting/PersistentConnectionFactory.cs +++ /dev/null @@ -1,52 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -using System; -using System.Globalization; - -namespace Microsoft.AspNet.SignalR.Hosting -{ - /// - /// Responsible for creating instances. - /// - public class PersistentConnectionFactory - { - private readonly IDependencyResolver _resolver; - - /// - /// Creates a new instance of the class. - /// - /// The dependency resolver to use for when creating the . - public PersistentConnectionFactory(IDependencyResolver resolver) - { - if (resolver == null) - { - throw new ArgumentNullException("resolver"); - } - - _resolver = resolver; - } - - /// - /// Creates an instance of the specified type using the dependency resolver or the type's default constructor. - /// - /// The type of to create. - /// An instance of a . - public PersistentConnection CreateInstance(Type connectionType) - { - if (connectionType == null) - { - throw new ArgumentNullException("connectionType"); - } - - var connection = (_resolver.Resolve(connectionType) ?? - Activator.CreateInstance(connectionType)) as PersistentConnection; - - if (connection == null) - { - throw new InvalidOperationException(String.Format(CultureInfo.CurrentCulture, Resources.Error_IsNotA, connectionType.FullName, typeof(PersistentConnection).FullName)); - } - - return connection; - } - } -} diff --git a/src/Microsoft.AspNet.SignalR.Core/Hosting/RequestExtensions.cs b/src/Microsoft.AspNet.SignalR.Core/Hosting/RequestExtensions.cs deleted file mode 100644 index 52cba0096..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/Hosting/RequestExtensions.cs +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -using System; - -namespace Microsoft.AspNet.SignalR.Hosting -{ - internal static class RequestExtensions - { - /// - /// Gets a value from the QueryString, and if it's null or empty, gets it from the Form instead. - /// - public static string QueryStringOrForm(this IRequest request, string key) - { - var value = request.QueryString[key]; - if (String.IsNullOrEmpty(value)) - { - value = request.Form[key]; - } - return value; - } - } -} diff --git a/src/Microsoft.AspNet.SignalR.Core/Hosting/ResponseExtensions.cs b/src/Microsoft.AspNet.SignalR.Core/Hosting/ResponseExtensions.cs deleted file mode 100644 index a8f79a922..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/Hosting/ResponseExtensions.cs +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -using System; -using System.Text; -using System.Threading.Tasks; - -namespace Microsoft.AspNet.SignalR.Hosting -{ - /// - /// Extension methods for . - /// - public static class ResponseExtensions - { - /// - /// Closes the connection to a client with optional data. - /// - /// The . - /// The data to write to the connection. - /// A task that represents when the connection is closed. - public static Task End(this IResponse response, string data) - { - if (response == null) - { - throw new ArgumentNullException("response"); - } - - var bytes = Encoding.UTF8.GetBytes(data); - response.Write(new ArraySegment(bytes, 0, bytes.Length)); - return response.End(); - } - } -} diff --git a/src/Microsoft.AspNet.SignalR.Core/Hub.cs b/src/Microsoft.AspNet.SignalR.Core/Hub.cs deleted file mode 100644 index ca86f39e9..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/Hub.cs +++ /dev/null @@ -1,72 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -using System.Threading.Tasks; -using Microsoft.AspNet.SignalR.Hubs; - -namespace Microsoft.AspNet.SignalR -{ - /// - /// Provides methods that communicate with SignalR connections that connected to a . - /// - public abstract class Hub : IHub - { - protected Hub() - { - Clients = new HubConnectionContext(); - Clients.All = new NullClientProxy(); - Clients.Others = new NullClientProxy(); - Clients.Caller = new NullClientProxy(); - } - - /// - /// - /// - public HubConnectionContext Clients { get; set; } - - /// - /// Provides information about the calling client. - /// - public HubCallerContext Context { get; set; } - - /// - /// The group manager for this hub instance. - /// - public IGroupManager Groups { get; set; } - - /// - /// Called when a connection disconnects from this hub instance. - /// - /// A - public virtual Task OnDisconnected() - { - return TaskAsyncHelper.Empty; - } - - /// - /// Called when the connection connects to this hub instance. - /// - /// A - public virtual Task OnConnected() - { - return TaskAsyncHelper.Empty; - } - - /// - /// Called when the connection reconnects to this hub instance. - /// - /// A - public virtual Task OnReconnected() - { - return TaskAsyncHelper.Empty; - } - - protected virtual void Dispose(bool disposing) - { - } - - public void Dispose() - { - Dispose(true); - } - } -} diff --git a/src/Microsoft.AspNet.SignalR.Core/HubConfiguration.cs b/src/Microsoft.AspNet.SignalR.Core/HubConfiguration.cs deleted file mode 100644 index 13abdc5bc..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/HubConfiguration.cs +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -namespace Microsoft.AspNet.SignalR -{ - public class HubConfiguration : ConnectionConfiguration - { - /// - /// Determines whether JavaScript proxies for the server-side hubs should be auto generated at {Path}/hubs. - /// Defaults to true. - /// - public bool EnableJavaScriptProxies { get; set; } - - /// - /// Determines whether detailed exceptions thrown in Hub methods get reported back the invoking client. - /// Defaults to false. - /// - public bool EnableDetailedErrors { get; set; } - - public HubConfiguration() - { - EnableJavaScriptProxies = true; - } - } -} diff --git a/src/Microsoft.AspNet.SignalR.Core/Hubs/ClientHubInvocation.cs b/src/Microsoft.AspNet.SignalR.Core/Hubs/ClientHubInvocation.cs deleted file mode 100644 index 37dd52233..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/Hubs/ClientHubInvocation.cs +++ /dev/null @@ -1,47 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using Newtonsoft.Json; - -namespace Microsoft.AspNet.SignalR.Hubs -{ - /// - /// A description of a client-side hub method invocation. - /// - public class ClientHubInvocation - { - /// - /// The signal that clients receiving this invocation are subscribed to. - /// - [JsonIgnore] - public string Target { get; set; } - - /// - /// The name of the hub that the method being invoked belongs to. - /// - [JsonProperty("H")] - public string Hub { get; set; } - - /// - /// The name of the client-side hub method be invoked. - /// - [JsonProperty("M")] - public string Method { get; set; } - - /// - /// The argument list the client-side hub method will be called with. - /// - [SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays", Justification = "Type is used for serialization.")] - [JsonProperty("A")] - public object[] Args { get; set; } - - /// - /// A key-value store representing the hub state on the server that has changed since the last time the hub - /// state was sent to the client. - /// - [SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Justification = "Type is used for serialization.")] - [JsonProperty("S", NullValueHandling = NullValueHandling.Ignore)] - public IDictionary State { get; set; } - } -} diff --git a/src/Microsoft.AspNet.SignalR.Core/Hubs/ClientProxy.cs b/src/Microsoft.AspNet.SignalR.Core/Hubs/ClientProxy.cs deleted file mode 100644 index df87a13f1..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/Hubs/ClientProxy.cs +++ /dev/null @@ -1,44 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Dynamic; -using System.Threading.Tasks; -using Microsoft.AspNet.SignalR.Infrastructure; - -namespace Microsoft.AspNet.SignalR.Hubs -{ - public class ClientProxy : DynamicObject, IClientProxy - { - private readonly Func, Task> _send; - private readonly string _hubName; - private readonly IList _exclude; - - public ClientProxy(Func, Task> send, string hubName, IList exclude) - { - _send = send; - _hubName = hubName; - _exclude = exclude; - } - - [SuppressMessage("Microsoft.Design", "CA1062:Validate arguments of public methods", MessageId = "0", Justification = "Binder is passed in by the DLR")] - public override bool TryInvokeMember(InvokeMemberBinder binder, object[] args, out object result) - { - result = Invoke(binder.Name, args); - return true; - } - - public Task Invoke(string method, params object[] args) - { - var invocation = new ClientHubInvocation - { - Hub = _hubName, - Method = method, - Args = args - }; - - return _send(PrefixHelper.GetHubName(_hubName), invocation, _exclude); - } - } -} diff --git a/src/Microsoft.AspNet.SignalR.Core/Hubs/ConnectionIdProxy.cs b/src/Microsoft.AspNet.SignalR.Core/Hubs/ConnectionIdProxy.cs deleted file mode 100644 index cf3520fb8..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/Hubs/ConnectionIdProxy.cs +++ /dev/null @@ -1,18 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using Microsoft.AspNet.SignalR.Infrastructure; - -namespace Microsoft.AspNet.SignalR.Hubs -{ - public class ConnectionIdProxy : SignalProxy - { - public ConnectionIdProxy(Func, Task> send, string signal, string hubName, params string[] exclude) : - base(send, signal, hubName, PrefixHelper.HubConnectionIdPrefix, exclude) - { - - } - } -} diff --git a/src/Microsoft.AspNet.SignalR.Core/Hubs/DefaultAssemblyLocator.cs b/src/Microsoft.AspNet.SignalR.Core/Hubs/DefaultAssemblyLocator.cs deleted file mode 100644 index b25b1985d..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/Hubs/DefaultAssemblyLocator.cs +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -using System; -using System.Collections.Generic; -using System.Reflection; - -namespace Microsoft.AspNet.SignalR.Hubs -{ - public class DefaultAssemblyLocator : IAssemblyLocator - { - public virtual IList GetAssemblies() - { - return AppDomain.CurrentDomain.GetAssemblies(); - } - } -} diff --git a/src/Microsoft.AspNet.SignalR.Core/Hubs/DefaultHubActivator.cs b/src/Microsoft.AspNet.SignalR.Core/Hubs/DefaultHubActivator.cs deleted file mode 100644 index 8f160d09a..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/Hubs/DefaultHubActivator.cs +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -using System; - -namespace Microsoft.AspNet.SignalR.Hubs -{ - public class DefaultHubActivator : IHubActivator - { - private readonly IDependencyResolver _resolver; - - public DefaultHubActivator(IDependencyResolver resolver) - { - _resolver = resolver; - } - - public IHub Create(HubDescriptor descriptor) - { - if (descriptor == null) - { - throw new ArgumentNullException("descriptor"); - } - - if(descriptor.HubType == null) - { - return null; - } - - object hub = _resolver.Resolve(descriptor.HubType) ?? Activator.CreateInstance(descriptor.HubType); - return hub as IHub; - } - } -} diff --git a/src/Microsoft.AspNet.SignalR.Core/Hubs/DefaultJavaScriptProxyGenerator.cs b/src/Microsoft.AspNet.SignalR.Core/Hubs/DefaultJavaScriptProxyGenerator.cs deleted file mode 100644 index 7e84b318e..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/Hubs/DefaultJavaScriptProxyGenerator.cs +++ /dev/null @@ -1,211 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -using System; -using System.Collections; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.Linq; -using System.Text; -using Microsoft.AspNet.SignalR.Json; -using Newtonsoft.Json; - -namespace Microsoft.AspNet.SignalR.Hubs -{ - public class DefaultJavaScriptProxyGenerator : IJavaScriptProxyGenerator - { - private static readonly Lazy _templateFromResource = new Lazy(GetTemplateFromResource); - - private static readonly Type[] _numberTypes = new[] { typeof(byte), typeof(short), typeof(int), typeof(long), typeof(float), typeof(decimal), typeof(double) }; - private static readonly Type[] _dateTypes = new[] { typeof(DateTime), typeof(DateTimeOffset) }; - - private const string ScriptResource = "Microsoft.AspNet.SignalR.Scripts.hubs.js"; - - private readonly IHubManager _manager; - private readonly IJavaScriptMinifier _javaScriptMinifier; - private readonly Lazy _generatedTemplate; - - public DefaultJavaScriptProxyGenerator(IDependencyResolver resolver) : - this(resolver.Resolve(), - resolver.Resolve()) - { - } - - public DefaultJavaScriptProxyGenerator(IHubManager manager, IJavaScriptMinifier javaScriptMinifier) - { - _manager = manager; - _javaScriptMinifier = javaScriptMinifier ?? NullJavaScriptMinifier.Instance; - _generatedTemplate = new Lazy(() => GenerateProxy(_manager, _javaScriptMinifier, includeDocComments: false)); - } - - public string GenerateProxy(string serviceUrl) - { - serviceUrl = JavaScriptEncode(serviceUrl); - - var generateProxy = _generatedTemplate.Value; - - return generateProxy.Replace("{serviceUrl}", serviceUrl); - } - - public string GenerateProxy(string serviceUrl, bool includeDocComments) - { - serviceUrl = JavaScriptEncode(serviceUrl); - - string generateProxy = GenerateProxy(_manager, _javaScriptMinifier, includeDocComments); - - return generateProxy.Replace("{serviceUrl}", serviceUrl); - } - - private static string GenerateProxy(IHubManager hubManager, IJavaScriptMinifier javaScriptMinifier, bool includeDocComments) - { - string script = _templateFromResource.Value; - - var hubs = new StringBuilder(); - var first = true; - foreach (var descriptor in hubManager.GetHubs().OrderBy(h => h.Name)) - { - if (!first) - { - hubs.AppendLine(";"); - hubs.AppendLine(); - hubs.Append(" "); - } - GenerateType(hubManager, hubs, descriptor, includeDocComments); - first = false; - } - - if (hubs.Length > 0) - { - hubs.Append(";"); - } - - script = script.Replace("/*hubs*/", hubs.ToString()); - - return javaScriptMinifier.Minify(script); - } - - private static void GenerateType(IHubManager hubManager, StringBuilder sb, HubDescriptor descriptor, bool includeDocComments) - { - // Get only actions with minimum number of parameters. - var methods = GetMethods(hubManager, descriptor); - var hubName = GetDescriptorName(descriptor); - - sb.AppendFormat(" proxies.{0} = this.createHubProxy('{1}'); ", hubName, hubName).AppendLine(); - sb.AppendFormat(" proxies.{0}.client = {{ }};", hubName).AppendLine(); - sb.AppendFormat(" proxies.{0}.server = {{", hubName); - - bool first = true; - - foreach (var method in methods) - { - if (!first) - { - sb.Append(",").AppendLine(); - } - GenerateMethod(sb, method, includeDocComments, hubName); - first = false; - } - sb.AppendLine(); - sb.Append(" }"); - } - - private static string GetDescriptorName(Descriptor descriptor) - { - if (descriptor == null) - { - throw new ArgumentNullException("descriptor"); - } - - string name = descriptor.Name; - - // If the name was not specified then do not camel case - if (!descriptor.NameSpecified) - { - name = JsonUtility.CamelCase(name); - } - - return name; - } - - private static IEnumerable GetMethods(IHubManager manager, HubDescriptor descriptor) - { - return from method in manager.GetHubMethods(descriptor.Name) - group method by method.Name into overloads - let oload = (from overload in overloads - orderby overload.Parameters.Count - select overload).FirstOrDefault() - orderby oload.Name - select oload; - } - - private static void GenerateMethod(StringBuilder sb, MethodDescriptor method, bool includeDocComments, string hubName) - { - var parameterNames = method.Parameters.Select(p => p.Name).ToList(); - sb.AppendLine(); - sb.AppendFormat(" {0}: function ({1}) {{", GetDescriptorName(method), Commas(parameterNames)).AppendLine(); - if (includeDocComments) - { - sb.AppendFormat(Resources.DynamicComment_CallsMethodOnServerSideDeferredPromise, method.Name, method.Hub.Name).AppendLine(); - var parameterDoc = method.Parameters.Select(p => String.Format(CultureInfo.CurrentCulture, Resources.DynamicComment_ServerSideTypeIs, p.Name, MapToJavaScriptType(p.ParameterType), p.ParameterType)).ToList(); - if (parameterDoc.Any()) - { - sb.AppendLine(String.Join(Environment.NewLine, parameterDoc)); - } - } - sb.AppendFormat(" return proxies.{0}.invoke.apply(proxies.{0}, $.merge([\"{1}\"], $.makeArray(arguments)));", hubName, method.Name).AppendLine(); - sb.Append(" }"); - } - - private static string MapToJavaScriptType(Type type) - { - if (!type.IsPrimitive && !(type == typeof(string))) - { - return "Object"; - } - if (type == typeof(string)) - { - return "String"; - } - if (_numberTypes.Contains(type)) - { - return "Number"; - } - if (typeof(IEnumerable).IsAssignableFrom(type)) - { - return "Array"; - } - if (_dateTypes.Contains(type)) - { - return "Date"; - } - return String.Empty; - } - - private static string Commas(IEnumerable values) - { - return Commas(values, v => v); - } - - private static string Commas(IEnumerable values, Func selector) - { - return String.Join(", ", values.Select(selector)); - } - - private static string GetTemplateFromResource() - { - using (Stream resourceStream = typeof(DefaultJavaScriptProxyGenerator).Assembly.GetManifestResourceStream(ScriptResource)) - { - var reader = new StreamReader(resourceStream); - return reader.ReadToEnd(); - } - } - - private static string JavaScriptEncode(string value) - { - value = JsonConvert.SerializeObject(value); - - // Remove the quotes - return value.Substring(1, value.Length - 2); - } - } -} diff --git a/src/Microsoft.AspNet.SignalR.Core/Hubs/DynamicDictionary.cs b/src/Microsoft.AspNet.SignalR.Core/Hubs/DynamicDictionary.cs deleted file mode 100644 index 0a9963108..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/Hubs/DynamicDictionary.cs +++ /dev/null @@ -1,144 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -using System.Collections; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Dynamic; - -namespace Microsoft.AspNet.SignalR.Hubs -{ - public class DynamicDictionary : DynamicObject, IDictionary - { - private readonly IDictionary _obj; - - public DynamicDictionary(IDictionary obj) - { - _obj = obj; - } - - public object this[string key] - { - get - { - object result; - _obj.TryGetValue(key, out result); - return Wrap(result); - } - set - { - _obj[key] = Unwrap(value); - } - } - - [SuppressMessage("Microsoft.Design", "CA1062:Validate arguments of public methods", MessageId = "0", Justification = "The compiler generates calls to invoke this")] - public override bool TryGetMember(GetMemberBinder binder, out object result) - { - result = this[binder.Name]; - return true; - } - - [SuppressMessage("Microsoft.Design", "CA1062:Validate arguments of public methods", MessageId = "0", Justification = "The compiler generates calls to invoke this")] - public override bool TrySetMember(SetMemberBinder binder, object value) - { - this[binder.Name] = value; - return true; - } - - public static object Wrap(object value) - { - var obj = value as IDictionary; - if (obj != null) - { - return new DynamicDictionary(obj); - } - - return value; - } - - public static object Unwrap(object value) - { - var dictWrapper = value as DynamicDictionary; - if (dictWrapper != null) - { - return dictWrapper._obj; - } - - return value; - } - - public void Add(string key, object value) - { - _obj.Add(key, value); - } - - public bool ContainsKey(string key) - { - return _obj.ContainsKey(key); - } - - public ICollection Keys - { - get { return _obj.Keys; } - } - - public bool Remove(string key) - { - return _obj.Remove(key); - } - - public bool TryGetValue(string key, out object value) - { - return _obj.TryGetValue(key, out value); - } - - public ICollection Values - { - get { return _obj.Values; } - } - - public void Add(KeyValuePair item) - { - _obj.Add(item); - } - - public void Clear() - { - _obj.Clear(); - } - - public bool Contains(KeyValuePair item) - { - return _obj.Contains(item); - } - - public void CopyTo(KeyValuePair[] array, int arrayIndex) - { - _obj.CopyTo(array, arrayIndex); - } - - public int Count - { - get { return _obj.Count; } - } - - public bool IsReadOnly - { - get { return _obj.IsReadOnly; } - } - - public bool Remove(KeyValuePair item) - { - return _obj.Remove(item); - } - - public IEnumerator> GetEnumerator() - { - return _obj.GetEnumerator(); - } - - IEnumerator IEnumerable.GetEnumerator() - { - return GetEnumerator(); - } - } -} diff --git a/src/Microsoft.AspNet.SignalR.Core/Hubs/EmptyJavaScriptProxyGenerator.cs b/src/Microsoft.AspNet.SignalR.Core/Hubs/EmptyJavaScriptProxyGenerator.cs deleted file mode 100644 index d0ed9055e..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/Hubs/EmptyJavaScriptProxyGenerator.cs +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -using System; -using System.Globalization; - -namespace Microsoft.AspNet.SignalR.Hubs -{ - public class EmptyJavaScriptProxyGenerator : IJavaScriptProxyGenerator - { - public string GenerateProxy(string serviceUrl) - { - return String.Format(CultureInfo.InvariantCulture, "throw new Error('{0}');", Resources.Error_JavaScriptProxyDisabled); - } - } -} diff --git a/src/Microsoft.AspNet.SignalR.Core/Hubs/Extensions/HubManagerExtensions.cs b/src/Microsoft.AspNet.SignalR.Core/Hubs/Extensions/HubManagerExtensions.cs deleted file mode 100644 index bc5455b59..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/Hubs/Extensions/HubManagerExtensions.cs +++ /dev/null @@ -1,63 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -using System; -using System.Collections.Generic; -using System.Globalization; -using Microsoft.AspNet.SignalR.Infrastructure; - -namespace Microsoft.AspNet.SignalR.Hubs -{ - public static class HubManagerExtensions - { - public static HubDescriptor EnsureHub(this IHubManager hubManager, string hubName, params IPerformanceCounter[] counters) - { - if (hubManager == null) - { - throw new ArgumentNullException("hubManager"); - } - - if (String.IsNullOrEmpty(hubName)) - { - throw new ArgumentNullException("hubName"); - } - - if (counters == null) - { - throw new ArgumentNullException("counters"); - } - - var descriptor = hubManager.GetHub(hubName); - - if (descriptor == null) - { - for (var i = 0; i < counters.Length; i++) - { - counters[i].Increment(); - } - throw new InvalidOperationException(String.Format(CultureInfo.CurrentCulture, Resources.Error_HubCouldNotBeResolved, hubName)); - } - - return descriptor; - } - - public static IEnumerable GetHubs(this IHubManager hubManager) - { - if (hubManager == null) - { - throw new ArgumentNullException("hubManager"); - } - - return hubManager.GetHubs(d => true); - } - - public static IEnumerable GetHubMethods(this IHubManager hubManager, string hubName) - { - if (hubManager == null) - { - throw new ArgumentNullException("hubManager"); - } - - return hubManager.GetHubMethods(hubName, m => true); - } - } -} diff --git a/src/Microsoft.AspNet.SignalR.Core/Hubs/Extensions/HubTypeExtensions.cs b/src/Microsoft.AspNet.SignalR.Core/Hubs/Extensions/HubTypeExtensions.cs deleted file mode 100644 index 1bf73da42..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/Hubs/Extensions/HubTypeExtensions.cs +++ /dev/null @@ -1,30 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -using System; - -namespace Microsoft.AspNet.SignalR.Hubs -{ - internal static class HubTypeExtensions - { - internal static string GetHubName(this Type type) - { - if (!typeof(IHub).IsAssignableFrom(type)) - { - return null; - } - - return GetHubAttributeName(type) ?? type.Name; - } - - internal static string GetHubAttributeName(this Type type) - { - if (!typeof(IHub).IsAssignableFrom(type)) - { - return null; - } - - // We can still return null if there is no attribute name - return ReflectionHelper.GetAttributeValue(type, attr => attr.HubName); - } - } -} diff --git a/src/Microsoft.AspNet.SignalR.Core/Hubs/Extensions/MethodExtensions.cs b/src/Microsoft.AspNet.SignalR.Core/Hubs/Extensions/MethodExtensions.cs deleted file mode 100644 index 3418e8566..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/Hubs/Extensions/MethodExtensions.cs +++ /dev/null @@ -1,29 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using Microsoft.AspNet.SignalR.Json; - -namespace Microsoft.AspNet.SignalR.Hubs -{ - public static class MethodExtensions - { - [SuppressMessage("Microsoft.Design", "CA1062:Validate arguments of public methods", MessageId = "1", Justification = "The condition checks for null parameters")] - public static bool Matches(this MethodDescriptor methodDescriptor, IList parameters) - { - if (methodDescriptor == null) - { - throw new ArgumentNullException("methodDescriptor"); - } - - if ((methodDescriptor.Parameters.Count > 0 && parameters == null) - || methodDescriptor.Parameters.Count != parameters.Count) - { - return false; - } - - return true; - } - } -} diff --git a/src/Microsoft.AspNet.SignalR.Core/Hubs/GroupProxy.cs b/src/Microsoft.AspNet.SignalR.Core/Hubs/GroupProxy.cs deleted file mode 100644 index 9ca86d5ce..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/Hubs/GroupProxy.cs +++ /dev/null @@ -1,18 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using Microsoft.AspNet.SignalR.Infrastructure; - -namespace Microsoft.AspNet.SignalR.Hubs -{ - public class GroupProxy : SignalProxy - { - public GroupProxy(Func, Task> send, string signal, string hubName, IList exclude) : - base(send, signal, hubName, PrefixHelper.HubGroupPrefix, exclude) - { - - } - } -} diff --git a/src/Microsoft.AspNet.SignalR.Core/Hubs/HubCallerContext.cs b/src/Microsoft.AspNet.SignalR.Core/Hubs/HubCallerContext.cs deleted file mode 100644 index 2413da7da..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/Hubs/HubCallerContext.cs +++ /dev/null @@ -1,71 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -using System.Collections.Generic; -using System.Collections.Specialized; -using System.Security.Principal; - -namespace Microsoft.AspNet.SignalR.Hubs -{ - public class HubCallerContext - { - /// - /// Gets the connection id of the calling client. - /// - public string ConnectionId { get; private set; } - - /// - /// Gets the cookies for the request. - /// - public IDictionary RequestCookies - { - get - { - return Request.Cookies; - } - } - - /// - /// Gets the headers for the request. - /// - public NameValueCollection Headers - { - get - { - return Request.Headers; - } - } - - /// - /// Gets the querystring for the request. - /// - public NameValueCollection QueryString - { - get - { - return Request.QueryString; - } - } - - /// - /// Gets the for the request. - /// - public IPrincipal User - { - get - { - return Request.User; - } - } - - /// - /// Gets the for the current HTTP request. - /// - public IRequest Request { get; private set; } - - public HubCallerContext(IRequest request, string connectionId) - { - ConnectionId = connectionId; - Request = request; - } - } -} diff --git a/src/Microsoft.AspNet.SignalR.Core/Hubs/HubConnectionContext.cs b/src/Microsoft.AspNet.SignalR.Core/Hubs/HubConnectionContext.cs deleted file mode 100644 index fded4874b..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/Hubs/HubConnectionContext.cs +++ /dev/null @@ -1,111 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using Microsoft.AspNet.SignalR.Infrastructure; - -namespace Microsoft.AspNet.SignalR.Hubs -{ - /// - /// Encapsulates all information about an individual SignalR connection for an . - /// - public class HubConnectionContext : IHubConnectionContext - { - private readonly string _hubName; - private readonly string _connectionId; - private readonly Func, Task> _send; - - /// - /// Initializes a new instance of the . - /// - public HubConnectionContext() - { - } - - /// - /// Initializes a new instance of the . - /// - /// The pipeline invoker. - /// The connection. - /// The hub name. - /// The connection id. - /// The connection hub state. - public HubConnectionContext(IHubPipelineInvoker pipelineInvoker, IConnection connection, string hubName, string connectionId, StateChangeTracker tracker) - { - _send = (signal, invocation, exclude) => pipelineInvoker.Send(new HubOutgoingInvokerContext(connection, signal, invocation, exclude)); - _connectionId = connectionId; - _hubName = hubName; - - Caller = new StatefulSignalProxy(_send, connectionId, PrefixHelper.HubConnectionIdPrefix, hubName, tracker); - All = AllExcept(); - Others = AllExcept(connectionId); - } - - /// - /// All connected clients. - /// - public dynamic All { get; set; } - - /// - /// All connected clients except the calling client. - /// - public dynamic Others { get; set; } - - /// - /// Represents the calling client. - /// - public dynamic Caller { get; set; } - - /// - /// Returns a dynamic representation of all clients except the calling client ones specified. - /// - /// The list of connection ids to exclude - /// A dynamic representation of all clients except the calling client ones specified. - public dynamic AllExcept(params string[] excludeConnectionIds) - { - return new ClientProxy(_send, _hubName, PrefixHelper.GetPrefixedConnectionIds(excludeConnectionIds)); - } - - /// - /// Returns a dynamic representation of all clients in a group except the calling client. - /// - /// The name of the group - /// A dynamic representation of all clients in a group except the calling client. - public dynamic OthersInGroup(string groupName) - { - return Group(groupName, _connectionId); - } - - /// - /// Returns a dynamic representation of the specified group. - /// - /// The name of the group - /// The list of connection ids to exclude - /// A dynamic representation of the specified group. - public dynamic Group(string groupName, params string[] excludeConnectionIds) - { - if (string.IsNullOrEmpty(groupName)) - { - throw new ArgumentException(Resources.Error_ArgumentNullOrEmpty, "groupName"); - } - - return new GroupProxy(_send, groupName, _hubName, PrefixHelper.GetPrefixedConnectionIds(excludeConnectionIds)); - } - - /// - /// Returns a dynamic representation of the connection with the specified connectionid. - /// - /// The connection id - /// A dynamic representation of the specified client. - public dynamic Client(string connectionId) - { - if (string.IsNullOrEmpty(connectionId)) - { - throw new ArgumentException(Resources.Error_ArgumentNullOrEmpty, "connectionId"); - } - - return new ConnectionIdProxy(_send, connectionId, _hubName); - } - } -} diff --git a/src/Microsoft.AspNet.SignalR.Core/Hubs/HubContext.cs b/src/Microsoft.AspNet.SignalR.Core/Hubs/HubContext.cs deleted file mode 100644 index 37a295dcf..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/Hubs/HubContext.cs +++ /dev/null @@ -1,66 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using Microsoft.AspNet.SignalR.Infrastructure; - -namespace Microsoft.AspNet.SignalR.Hubs -{ - internal class HubContext : IHubContext - { - public HubContext(Func, Task> send, string hubName, IConnection connection) - { - Clients = new ExternalHubConnectionContext(send, hubName); - Groups = new GroupManager(connection, PrefixHelper.GetHubGroupName(hubName)); - } - - public IHubConnectionContext Clients { get; private set; } - - public IGroupManager Groups { get; private set; } - - private class ExternalHubConnectionContext : IHubConnectionContext - { - private readonly Func, Task> _send; - private readonly string _hubName; - - public ExternalHubConnectionContext(Func, Task> send, string hubName) - { - _send = send; - _hubName = hubName; - All = AllExcept(); - } - - public dynamic All - { - get; - private set; - } - - public dynamic AllExcept(params string[] exclude) - { - return new ClientProxy(_send, _hubName, exclude); - } - - public dynamic Group(string groupName, params string[] exclude) - { - if (string.IsNullOrEmpty(groupName)) - { - throw new ArgumentException(Resources.Error_ArgumentNullOrEmpty, "groupName"); - } - - return new GroupProxy(_send, groupName, _hubName, exclude); - } - - public dynamic Client(string connectionId) - { - if (string.IsNullOrEmpty(connectionId)) - { - throw new ArgumentException(Resources.Error_ArgumentNullOrEmpty, "connectionId"); - } - - return new ConnectionIdProxy(_send, connectionId, _hubName); - } - } - } -} diff --git a/src/Microsoft.AspNet.SignalR.Core/Hubs/HubDispatcher.cs b/src/Microsoft.AspNet.SignalR.Core/Hubs/HubDispatcher.cs deleted file mode 100644 index b97aa74c5..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/Hubs/HubDispatcher.cs +++ /dev/null @@ -1,522 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; -using System.Globalization; -using System.Linq; -using System.Linq.Expressions; -using System.Reflection; -using System.Threading.Tasks; -using Microsoft.AspNet.SignalR.Hosting; -using Microsoft.AspNet.SignalR.Infrastructure; -using Microsoft.AspNet.SignalR.Json; - -namespace Microsoft.AspNet.SignalR.Hubs -{ - /// - /// Handles all communication over the hubs persistent connection. - /// - [SuppressMessage("Microsoft.Maintainability", "CA1506:AvoidExcessiveClassCoupling", Justification = "This dispatcher makes use of many interfaces.")] - public class HubDispatcher : PersistentConnection - { - private const string HubsSuffix = "/hubs"; - - private readonly List _hubs = new List(); - private readonly bool _enableJavaScriptProxies; - private readonly bool _enableDetailedErrors; - - private IJavaScriptProxyGenerator _proxyGenerator; - private IHubManager _manager; - private IHubRequestParser _requestParser; - private IParameterResolver _binder; - private IHubPipelineInvoker _pipelineInvoker; - private IPerformanceCounterManager _counters; - private bool _isDebuggingEnabled; - - private static readonly MethodInfo _continueWithMethod = typeof(HubDispatcher).GetMethod("ContinueWith", BindingFlags.NonPublic | BindingFlags.Static); - - /// - /// Initializes an instance of the class. - /// - /// Configuration settings determining whether to enable JS proxies and provide clients with detailed hub errors. - public HubDispatcher(HubConfiguration configuration) - { - if (configuration == null) - { - throw new ArgumentNullException("configuration"); - } - - _enableJavaScriptProxies = configuration.EnableJavaScriptProxies; - _enableDetailedErrors = configuration.EnableDetailedErrors; - } - - protected override TraceSource Trace - { - get - { - return TraceManager["SignalR.HubDispatcher"]; - } - } - - internal override string GroupPrefix - { - get - { - return PrefixHelper.HubGroupPrefix; - } - } - - public override void Initialize(IDependencyResolver resolver, HostContext context) - { - if (resolver == null) - { - throw new ArgumentNullException("resolver"); - } - - if (context == null) - { - throw new ArgumentNullException("context"); - } - - _proxyGenerator = _enableJavaScriptProxies ? resolver.Resolve() - : new EmptyJavaScriptProxyGenerator(); - - _manager = resolver.Resolve(); - _binder = resolver.Resolve(); - _requestParser = resolver.Resolve(); - _pipelineInvoker = resolver.Resolve(); - _counters = resolver.Resolve(); - - base.Initialize(resolver, context); - } - - protected override bool AuthorizeRequest(IRequest request) - { - // Populate _hubs - string data = request.QueryStringOrForm("connectionData"); - - if (!String.IsNullOrEmpty(data)) - { - var clientHubInfo = JsonSerializer.Parse>(data); - - // If there's any hubs then perform the auth check - if (clientHubInfo != null && clientHubInfo.Any()) - { - var hubCache = new Dictionary(StringComparer.OrdinalIgnoreCase); - - foreach (var hubInfo in clientHubInfo) - { - if (hubCache.ContainsKey(hubInfo.Name)) - { - throw new InvalidOperationException(Resources.Error_DuplicateHubs); - } - - // Try to find the associated hub type - HubDescriptor hubDescriptor = _manager.EnsureHub(hubInfo.Name, - _counters.ErrorsHubResolutionTotal, - _counters.ErrorsHubResolutionPerSec, - _counters.ErrorsAllTotal, - _counters.ErrorsAllPerSec); - - if (_pipelineInvoker.AuthorizeConnect(hubDescriptor, request)) - { - // Add this to the list of hub descriptors this connection is interested in - hubCache.Add(hubDescriptor.Name, hubDescriptor); - } - } - - _hubs.AddRange(hubCache.Values); - - // If we have any hubs in the list then we're authorized - return _hubs.Count > 0; - } - } - - return base.AuthorizeRequest(request); - } - - /// - /// Processes the hub's incoming method calls. - /// - protected override Task OnReceived(IRequest request, string connectionId, string data) - { - HubRequest hubRequest = _requestParser.Parse(data); - - // Create the hub - HubDescriptor descriptor = _manager.EnsureHub(hubRequest.Hub, - _counters.ErrorsHubInvocationTotal, - _counters.ErrorsHubInvocationPerSec, - _counters.ErrorsAllTotal, - _counters.ErrorsAllPerSec); - - IJsonValue[] parameterValues = hubRequest.ParameterValues; - - // Resolve the method - MethodDescriptor methodDescriptor = _manager.GetHubMethod(descriptor.Name, hubRequest.Method, parameterValues); - - if (methodDescriptor == null) - { - _counters.ErrorsHubInvocationTotal.Increment(); - _counters.ErrorsHubInvocationPerSec.Increment(); - - // Empty (noop) method descriptor - // Use: Forces the hub pipeline module to throw an error. This error is encapsulated in the HubDispatcher. - // Encapsulating it in the HubDispatcher prevents the error from bubbling up to the transport level. - // Specifically this allows us to return a faulted task (call .fail on client) and to not cause the - // transport to unintentionally fail. - methodDescriptor = new NullMethodDescriptor(hubRequest.Method); - } - - // Resolving the actual state object - var tracker = new StateChangeTracker(hubRequest.State); - var hub = CreateHub(request, descriptor, connectionId, tracker, throwIfFailedToCreate: true); - - return InvokeHubPipeline(hub, parameterValues, methodDescriptor, hubRequest, tracker) - .ContinueWith(task => hub.Dispose(), TaskContinuationOptions.ExecuteSynchronously); - } - - [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Exceptions are flown to the caller.")] - private Task InvokeHubPipeline(IHub hub, - IJsonValue[] parameterValues, - MethodDescriptor methodDescriptor, - HubRequest hubRequest, - StateChangeTracker tracker) - { - Task piplineInvocation; - - try - { - var args = _binder.ResolveMethodParameters(methodDescriptor, parameterValues); - var context = new HubInvokerContext(hub, tracker, methodDescriptor, args); - - // Invoke the pipeline and save the task - piplineInvocation = _pipelineInvoker.Invoke(context); - } - catch (Exception ex) - { - piplineInvocation = TaskAsyncHelper.FromError(ex); - } - - // Determine if we have a faulted task or not and handle it appropriately. - return piplineInvocation.ContinueWith(task => - { - if (task.IsFaulted) - { - return ProcessResponse(tracker, result: null, request: hubRequest, error: task.Exception); - } - else if (task.IsCanceled) - { - return ProcessResponse(tracker, result: null, request: hubRequest, error: new OperationCanceledException()); - } - else - { - return ProcessResponse(tracker, task.Result, hubRequest, error: null); - } - }) - .FastUnwrap(); - } - - public override Task ProcessRequest(HostContext context) - { - if (context == null) - { - throw new ArgumentNullException("context"); - } - - // Trim any trailing slashes - string normalized = context.Request.Url.LocalPath.TrimEnd('/'); - - if (normalized.EndsWith(HubsSuffix, StringComparison.OrdinalIgnoreCase)) - { - // Generate the proper hub url - string hubUrl = normalized.Substring(0, normalized.Length - HubsSuffix.Length); - - // Generate the proxy - context.Response.ContentType = JsonUtility.JavaScriptMimeType; - return context.Response.End(_proxyGenerator.GenerateProxy(hubUrl)); - } - - _isDebuggingEnabled = context.IsDebuggingEnabled(); - - return base.ProcessRequest(context); - } - - internal static Task Connect(IHub hub) - { - return hub.OnConnected(); - } - - internal static Task Reconnect(IHub hub) - { - return hub.OnReconnected(); - } - - internal static Task Disconnect(IHub hub) - { - return hub.OnDisconnected(); - } - - [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "A faulted task is returned.")] - internal static Task Incoming(IHubIncomingInvokerContext context) - { - var tcs = new TaskCompletionSource(); - - try - { - object result = context.MethodDescriptor.Invoker(context.Hub, context.Args.ToArray()); - Type returnType = context.MethodDescriptor.ReturnType; - - if (typeof(Task).IsAssignableFrom(returnType)) - { - var task = (Task)result; - if (!returnType.IsGenericType) - { - task.ContinueWith(tcs); - } - else - { - // Get the in Task - Type resultType = returnType.GetGenericArguments().Single(); - - Type genericTaskType = typeof(Task<>).MakeGenericType(resultType); - - // Get the correct ContinueWith overload - var parameter = Expression.Parameter(typeof(object)); - - // TODO: Cache this whole thing - // Action callback = result => ContinueWith((Task)result, tcs); - MethodInfo continueWithMethod = _continueWithMethod.MakeGenericMethod(resultType); - - Expression body = Expression.Call(continueWithMethod, - Expression.Convert(parameter, genericTaskType), - Expression.Constant(tcs)); - - var continueWithInvoker = Expression.Lambda>(body, parameter).Compile(); - continueWithInvoker.Invoke(result); - } - } - else - { - tcs.TrySetResult(result); - } - } - catch (Exception ex) - { - tcs.TrySetUnwrappedException(ex); - } - - return tcs.Task; - } - - internal static Task Outgoing(IHubOutgoingInvokerContext context) - { - var message = new ConnectionMessage(context.Signal, context.Invocation, context.ExcludedSignals); - - return context.Connection.Send(message); - } - - protected override Task OnConnected(IRequest request, string connectionId) - { - return ExecuteHubEvent(request, connectionId, hub => _pipelineInvoker.Connect(hub)); - } - - protected override Task OnReconnected(IRequest request, string connectionId) - { - return ExecuteHubEvent(request, connectionId, hub => _pipelineInvoker.Reconnect(hub)); - } - - protected override IList OnRejoiningGroups(IRequest request, IList groups, string connectionId) - { - return _hubs.Select(hubDescriptor => - { - string groupPrefix = hubDescriptor.Name + "."; - - var hubGroups = groups.Where(g => g.StartsWith(groupPrefix, StringComparison.OrdinalIgnoreCase)) - .Select(g => g.Substring(groupPrefix.Length)) - .ToList(); - - return _pipelineInvoker.RejoiningGroups(hubDescriptor, request, hubGroups) - .Select(g => groupPrefix + g); - - }).SelectMany(groupsToRejoin => groupsToRejoin).ToList(); - } - - protected override Task OnDisconnected(IRequest request, string connectionId) - { - return ExecuteHubEvent(request, connectionId, hub => _pipelineInvoker.Disconnect(hub)); - } - - protected override IList GetSignals(string connectionId) - { - return _hubs.SelectMany(info => new[] { PrefixHelper.GetHubName(info.Name), PrefixHelper.GetHubConnectionId(info.CreateQualifiedName(connectionId)) }) - .Concat(new[] { PrefixHelper.GetConnectionId(connectionId), PrefixHelper.GetAck(connectionId) }) - .ToList(); - } - - private Task ExecuteHubEvent(IRequest request, string connectionId, Func action) - { - var hubs = GetHubs(request, connectionId).ToList(); - var operations = hubs.Select(instance => action(instance).OrEmpty().Catch()).ToArray(); - - if (operations.Length == 0) - { - DisposeHubs(hubs); - return TaskAsyncHelper.Empty; - } - - var tcs = new TaskCompletionSource(); - Task.Factory.ContinueWhenAll(operations, tasks => - { - DisposeHubs(hubs); - var faulted = tasks.FirstOrDefault(t => t.IsFaulted); - if (faulted != null) - { - tcs.SetUnwrappedException(faulted.Exception); - } - else if (tasks.Any(t => t.IsCanceled)) - { - tcs.SetCanceled(); - } - else - { - tcs.SetResult(null); - } - }); - - return tcs.Task; - } - - private IHub CreateHub(IRequest request, HubDescriptor descriptor, string connectionId, StateChangeTracker tracker = null, bool throwIfFailedToCreate = false) - { - try - { - var hub = _manager.ResolveHub(descriptor.Name); - - if (hub != null) - { - tracker = tracker ?? new StateChangeTracker(); - - hub.Context = new HubCallerContext(request, connectionId); - hub.Clients = new HubConnectionContext(_pipelineInvoker, Connection, descriptor.Name, connectionId, tracker); - hub.Groups = new GroupManager(Connection, PrefixHelper.GetHubGroupName(descriptor.Name)); - } - - return hub; - } - catch (Exception ex) - { - Trace.TraceInformation(String.Format(CultureInfo.CurrentCulture, Resources.Error_ErrorCreatingHub + ex.Message, descriptor.Name)); - - if (throwIfFailedToCreate) - { - throw; - } - - return null; - } - } - - private IEnumerable GetHubs(IRequest request, string connectionId) - { - return from descriptor in _hubs - select CreateHub(request, descriptor, connectionId) into hub - where hub != null - select hub; - } - - private static void DisposeHubs(IEnumerable hubs) - { - foreach (var hub in hubs) - { - hub.Dispose(); - } - } - - private Task ProcessResponse(StateChangeTracker tracker, object result, HubRequest request, Exception error) - { - var hubResult = new HubResponse - { - State = tracker.GetChanges(), - Result = result, - Id = request.Id, - }; - - if (error != null) - { - _counters.ErrorsHubInvocationTotal.Increment(); - _counters.ErrorsHubInvocationPerSec.Increment(); - _counters.ErrorsAllTotal.Increment(); - _counters.ErrorsAllPerSec.Increment(); - - if (_enableDetailedErrors) - { - var exception = error.InnerException ?? error; - hubResult.StackTrace = _isDebuggingEnabled ? exception.StackTrace : null; - hubResult.Error = exception.Message; - } - else - { - hubResult.Error = String.Format(CultureInfo.CurrentCulture, Resources.Error_HubInvocationFailed, request.Hub, request.Method); - } - } - - return Transport.Send(hubResult); - } - - private static void ContinueWith(Task task, TaskCompletionSource tcs) - { - if (task.IsCompleted) - { - // Fast path for tasks that completed synchronously - ContinueSync(task, tcs); - } - else - { - ContinueAsync(task, tcs); - } - } - - private static void ContinueSync(Task task, TaskCompletionSource tcs) - { - if (task.IsFaulted) - { - tcs.TrySetUnwrappedException(task.Exception); - } - else if (task.IsCanceled) - { - tcs.TrySetCanceled(); - } - else - { - tcs.TrySetResult(task.Result); - } - } - - private static void ContinueAsync(Task task, TaskCompletionSource tcs) - { - task.ContinueWith(t => - { - if (t.IsFaulted) - { - tcs.TrySetUnwrappedException(t.Exception); - } - else if (t.IsCanceled) - { - tcs.TrySetCanceled(); - } - else - { - tcs.TrySetResult(t.Result); - } - }); - } - - [SuppressMessage("Microsoft.Performance", "CA1812:AvoidUninstantiatedInternalClasses", Justification = "It is instantiated through JSON deserialization.")] - private class ClientHubInfo - { - public string Name { get; set; } - } - } -} diff --git a/src/Microsoft.AspNet.SignalR.Core/Hubs/HubMethodNameAttribute.cs b/src/Microsoft.AspNet.SignalR.Core/Hubs/HubMethodNameAttribute.cs deleted file mode 100644 index 49166ce07..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/Hubs/HubMethodNameAttribute.cs +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -using System; - -namespace Microsoft.AspNet.SignalR.Hubs -{ - [AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = false)] - public sealed class HubMethodNameAttribute : Attribute - { - public HubMethodNameAttribute(string methodName) - { - if (String.IsNullOrEmpty(methodName)) - { - throw new ArgumentNullException("methodName"); - } - MethodName = methodName; - } - - public string MethodName - { - get; - private set; - } - } -} diff --git a/src/Microsoft.AspNet.SignalR.Core/Hubs/HubNameAttribute.cs b/src/Microsoft.AspNet.SignalR.Core/Hubs/HubNameAttribute.cs deleted file mode 100644 index 942c88706..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/Hubs/HubNameAttribute.cs +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -using System; - -namespace Microsoft.AspNet.SignalR.Hubs -{ - [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] - public sealed class HubNameAttribute : Attribute - { - public HubNameAttribute(string hubName) - { - if (String.IsNullOrEmpty(hubName)) - { - throw new ArgumentNullException("hubName"); - } - HubName = hubName; - } - - public string HubName - { - get; - private set; - } - } -} diff --git a/src/Microsoft.AspNet.SignalR.Core/Hubs/HubRequest.cs b/src/Microsoft.AspNet.SignalR.Core/Hubs/HubRequest.cs deleted file mode 100644 index 083d855cb..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/Hubs/HubRequest.cs +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using Microsoft.AspNet.SignalR.Json; - -namespace Microsoft.AspNet.SignalR.Hubs -{ - public class HubRequest - { - public string Hub { get; set; } - public string Method { get; set; } - [SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays", Justification = "This type is used for de-serialization.")] - public IJsonValue[] ParameterValues { get; set; } - [SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Justification = "This type is used for de-serialization.")] - [SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays", Justification = "This type is used for de-serialization.")] - public IDictionary State { get; set; } - public string Id { get; set; } - } -} diff --git a/src/Microsoft.AspNet.SignalR.Core/Hubs/HubRequestParser.cs b/src/Microsoft.AspNet.SignalR.Core/Hubs/HubRequestParser.cs deleted file mode 100644 index f5f361f41..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/Hubs/HubRequestParser.cs +++ /dev/null @@ -1,69 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Linq; -using Microsoft.AspNet.SignalR.Json; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; - -namespace Microsoft.AspNet.SignalR.Hubs -{ - internal class HubRequestParser : IHubRequestParser - { - private static readonly IJsonValue[] _emptyArgs = new IJsonValue[0]; - - public HubRequest Parse(string data) - { - var serializer = new JsonNetSerializer(); - var deserializedData = serializer.Parse(data); - - var request = new HubRequest(); - - request.Hub = deserializedData.Hub; - request.Method = deserializedData.Method; - request.Id = deserializedData.Id; - request.State = GetState(deserializedData); - request.ParameterValues = (deserializedData.Args != null) ? deserializedData.Args.Select(value => new JRawValue(value)).ToArray() : _emptyArgs; - - return request; - } - - [SuppressMessage("Microsoft.Performance", "CA1812:AvoidUninstantiatedInternalClasses", Justification = "This type is used for deserialzation")] - private class HubInvocation - { - [JsonProperty("H")] - public string Hub { get; set; } - [JsonProperty("M")] - public string Method { get; set; } - [JsonProperty("I")] - public string Id { get; set; } - [JsonProperty("S")] - public JRaw State { get; set; } - [JsonProperty("A")] - public JRaw[] Args { get; set; } - } - - private static IDictionary GetState(HubInvocation deserializedData) - { - if (deserializedData.State == null) - { - return new Dictionary(); - } - - // Get the raw JSON string and check if it's over 4K - string json = deserializedData.State.ToString(); - - if (json.Length > 4096) - { - throw new InvalidOperationException(Resources.Error_StateExceededMaximumLength); - } - - var settings = new JsonSerializerSettings(); - settings.Converters.Add(new SipHashBasedDictionaryConverter()); - var serializer = new JsonNetSerializer(settings); - return serializer.Parse>(json); - } - } -} diff --git a/src/Microsoft.AspNet.SignalR.Core/Hubs/HubResponse.cs b/src/Microsoft.AspNet.SignalR.Core/Hubs/HubResponse.cs deleted file mode 100644 index a1305eb9a..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/Hubs/HubResponse.cs +++ /dev/null @@ -1,45 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using Newtonsoft.Json; - -namespace Microsoft.AspNet.SignalR.Hubs -{ - /// - /// The response returned from an incoming hub request. - /// - public class HubResponse - { - /// - /// The changes made the the round tripped state. - /// - [SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Justification = "Type is used for serialization")] - [JsonProperty("S", NullValueHandling = NullValueHandling.Ignore)] - public IDictionary State { get; set; } - - /// - /// The result of the invocation. - /// - [JsonProperty("R", NullValueHandling = NullValueHandling.Ignore)] - public object Result { get; set; } - - /// - /// The id of the operation. - /// - [JsonProperty("I")] - public string Id { get; set; } - - /// - /// The exception that occurs as a result of invoking the hub method. - /// - [JsonProperty("E", NullValueHandling = NullValueHandling.Ignore)] - public string Error { get; set; } - - /// - /// The stack trace of the exception that occurs as a result of invoking the hub method. - /// - [JsonProperty("T", NullValueHandling = NullValueHandling.Ignore)] - public string StackTrace { get; set; } - } -} diff --git a/src/Microsoft.AspNet.SignalR.Core/Hubs/IAssemblyLocator.cs b/src/Microsoft.AspNet.SignalR.Core/Hubs/IAssemblyLocator.cs deleted file mode 100644 index 5e98096fb..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/Hubs/IAssemblyLocator.cs +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Reflection; - -namespace Microsoft.AspNet.SignalR.Hubs -{ - public interface IAssemblyLocator - { - [SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate", Justification = "Might be expensive.")] - IList GetAssemblies(); - } -} diff --git a/src/Microsoft.AspNet.SignalR.Core/Hubs/IClientProxy.cs b/src/Microsoft.AspNet.SignalR.Core/Hubs/IClientProxy.cs deleted file mode 100644 index 71c9bc69c..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/Hubs/IClientProxy.cs +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -using System.Threading.Tasks; - -namespace Microsoft.AspNet.SignalR.Hubs -{ - /// - /// A server side proxy for the client side hub. - /// - public interface IClientProxy - { - /// - /// Invokes a method on the connection(s) represented by the instance. - /// - /// name of the method to invoke - /// argumetns to pass to the client - /// A task that represents when the data has been sent to the client. - Task Invoke(string method, params object[] args); - } -} diff --git a/src/Microsoft.AspNet.SignalR.Core/Hubs/IHub.cs b/src/Microsoft.AspNet.SignalR.Core/Hubs/IHub.cs deleted file mode 100644 index 1b7ef0ff8..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/Hubs/IHub.cs +++ /dev/null @@ -1,41 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -using System; -using System.Threading.Tasks; - -namespace Microsoft.AspNet.SignalR.Hubs -{ - public interface IHub : IDisposable - { - /// - /// Gets a . Which contains information about the calling client. - /// - HubCallerContext Context { get; set; } - - /// - /// Gets a dynamic object that represents all clients connected to this hub (not hub instance). - /// - HubConnectionContext Clients { get; set; } - - /// - /// Gets the the hub instance. - /// - IGroupManager Groups { get; set; } - - /// - /// Called when a new connection is made to the . - /// - Task OnConnected(); - - /// - /// Called when a connection reconnects to the after a timeout. - /// - Task OnReconnected(); - - /// - /// Called when a connection is disconnected from the . - /// - Task OnDisconnected(); - } -} - diff --git a/src/Microsoft.AspNet.SignalR.Core/Hubs/IHubActivator.cs b/src/Microsoft.AspNet.SignalR.Core/Hubs/IHubActivator.cs deleted file mode 100644 index 9a606a8a5..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/Hubs/IHubActivator.cs +++ /dev/null @@ -1,9 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -namespace Microsoft.AspNet.SignalR.Hubs -{ - public interface IHubActivator - { - IHub Create(HubDescriptor descriptor); - } -} diff --git a/src/Microsoft.AspNet.SignalR.Core/Hubs/IHubConnectionContext.cs b/src/Microsoft.AspNet.SignalR.Core/Hubs/IHubConnectionContext.cs deleted file mode 100644 index fa468dd47..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/Hubs/IHubConnectionContext.cs +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -namespace Microsoft.AspNet.SignalR.Hubs -{ - /// - /// Encapsulates all information about a SignalR connection for an . - /// - public interface IHubConnectionContext - { - dynamic All { get; } - dynamic AllExcept(params string[] excludeConnectionIds); - dynamic Client(string connectionId); - dynamic Group(string groupName, params string[] excludeConnectionIds); - } -} diff --git a/src/Microsoft.AspNet.SignalR.Core/Hubs/IHubRequestParser.cs b/src/Microsoft.AspNet.SignalR.Core/Hubs/IHubRequestParser.cs deleted file mode 100644 index dd798dd5f..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/Hubs/IHubRequestParser.cs +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -namespace Microsoft.AspNet.SignalR.Hubs -{ - /// - /// Handles parsing incoming requests through the . - /// - public interface IHubRequestParser - { - /// - /// Parses the incoming hub payload into a . - /// - /// The raw hub payload. - /// The resulting . - HubRequest Parse(string data); - } -} diff --git a/src/Microsoft.AspNet.SignalR.Core/Hubs/IJavaScriptMinifier.cs b/src/Microsoft.AspNet.SignalR.Core/Hubs/IJavaScriptMinifier.cs deleted file mode 100644 index 05690b5d7..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/Hubs/IJavaScriptMinifier.cs +++ /dev/null @@ -1,9 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -namespace Microsoft.AspNet.SignalR.Hubs -{ - public interface IJavaScriptMinifier - { - string Minify(string source); - } -} diff --git a/src/Microsoft.AspNet.SignalR.Core/Hubs/IJavaScriptProxyGenerator.cs b/src/Microsoft.AspNet.SignalR.Core/Hubs/IJavaScriptProxyGenerator.cs deleted file mode 100644 index 49abbe3ea..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/Hubs/IJavaScriptProxyGenerator.cs +++ /dev/null @@ -1,9 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -namespace Microsoft.AspNet.SignalR.Hubs -{ - public interface IJavaScriptProxyGenerator - { - string GenerateProxy(string serviceUrl); - } -} diff --git a/src/Microsoft.AspNet.SignalR.Core/Hubs/Lookup/DefaultHubManager.cs b/src/Microsoft.AspNet.SignalR.Core/Hubs/Lookup/DefaultHubManager.cs deleted file mode 100644 index f572b91e3..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/Hubs/Lookup/DefaultHubManager.cs +++ /dev/null @@ -1,95 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -using System; -using System.Collections.Generic; -using System.Linq; -using Microsoft.AspNet.SignalR.Json; - -namespace Microsoft.AspNet.SignalR.Hubs -{ - public class DefaultHubManager : IHubManager - { - private readonly IEnumerable _methodProviders; - private readonly IHubActivator _activator; - private readonly IEnumerable _hubProviders; - - public DefaultHubManager(IDependencyResolver resolver) - { - _hubProviders = resolver.ResolveAll(); - _methodProviders = resolver.ResolveAll(); - _activator = resolver.Resolve(); - } - - public HubDescriptor GetHub(string hubName) - { - HubDescriptor descriptor = null; - if (_hubProviders.FirstOrDefault(p => p.TryGetHub(hubName, out descriptor)) != null) - { - return descriptor; - } - - return null; - } - - public IEnumerable GetHubs(Func predicate) - { - var hubs = _hubProviders.SelectMany(p => p.GetHubs()); - - if (predicate != null) - { - return hubs.Where(predicate); - } - - return hubs; - } - - public MethodDescriptor GetHubMethod(string hubName, string method, IList parameters) - { - HubDescriptor hub = GetHub(hubName); - - if (hub == null) - { - return null; - } - - MethodDescriptor descriptor = null; - if (_methodProviders.FirstOrDefault(p => p.TryGetMethod(hub, method, out descriptor, parameters)) != null) - { - return descriptor; - } - - return null; - } - - public IEnumerable GetHubMethods(string hubName, Func predicate) - { - HubDescriptor hub = GetHub(hubName); - - if (hub == null) - { - return null; - } - - var methods = _methodProviders.SelectMany(p => p.GetMethods(hub)); - - if (predicate != null) - { - return methods.Where(predicate); - } - - return methods; - - } - - public IHub ResolveHub(string hubName) - { - HubDescriptor hub = GetHub(hubName); - return hub == null ? null : _activator.Create(hub); - } - - public IEnumerable ResolveHubs() - { - return GetHubs(predicate: null).Select(hub => _activator.Create(hub)); - } - } -} diff --git a/src/Microsoft.AspNet.SignalR.Core/Hubs/Lookup/DefaultParameterResolver.cs b/src/Microsoft.AspNet.SignalR.Core/Hubs/Lookup/DefaultParameterResolver.cs deleted file mode 100644 index 5b31a57f9..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/Hubs/Lookup/DefaultParameterResolver.cs +++ /dev/null @@ -1,54 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -using System; -using System.Collections.Generic; -using System.Linq; -using Microsoft.AspNet.SignalR.Json; - -namespace Microsoft.AspNet.SignalR.Hubs -{ - public class DefaultParameterResolver : IParameterResolver - { - /// - /// Resolves a parameter value based on the provided object. - /// - /// Parameter descriptor. - /// Value to resolve the parameter value from. - /// The parameter value. - public virtual object ResolveParameter(ParameterDescriptor descriptor, IJsonValue value) - { - if (descriptor == null) - { - throw new ArgumentNullException("descriptor"); - } - - if (value == null) - { - throw new ArgumentNullException("value"); - } - - if (value.GetType() == descriptor.ParameterType) - { - return value; - } - - return value.ConvertTo(descriptor.ParameterType); - } - - /// - /// Resolves method parameter values based on provided objects. - /// - /// Method descriptor. - /// List of values to resolve parameter values from. - /// Array of parameter values. - public virtual IList ResolveMethodParameters(MethodDescriptor method, IList values) - { - if (method == null) - { - throw new ArgumentNullException("method"); - } - - return method.Parameters.Zip(values, ResolveParameter).ToArray(); - } - } -} diff --git a/src/Microsoft.AspNet.SignalR.Core/Hubs/Lookup/Descriptors/Descriptor.cs b/src/Microsoft.AspNet.SignalR.Core/Hubs/Lookup/Descriptors/Descriptor.cs deleted file mode 100644 index 750a70965..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/Hubs/Lookup/Descriptors/Descriptor.cs +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -namespace Microsoft.AspNet.SignalR.Hubs -{ - public abstract class Descriptor - { - /// - /// Name of Descriptor. - /// - public virtual string Name { get; set; } - - /// - /// Flags whether the name was specified. - /// - public virtual bool NameSpecified { get; set; } - } -} diff --git a/src/Microsoft.AspNet.SignalR.Core/Hubs/Lookup/Descriptors/HubDescriptor.cs b/src/Microsoft.AspNet.SignalR.Core/Hubs/Lookup/Descriptors/HubDescriptor.cs deleted file mode 100644 index c4271eaef..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/Hubs/Lookup/Descriptors/HubDescriptor.cs +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -using System; - -namespace Microsoft.AspNet.SignalR.Hubs -{ - /// - /// Holds information about a single hub. - /// - public class HubDescriptor : Descriptor - { - /// - /// Hub type. - /// - public virtual Type HubType { get; set; } - - public string CreateQualifiedName(string unqualifiedName) - { - return Name + "." + unqualifiedName; - } - } -} diff --git a/src/Microsoft.AspNet.SignalR.Core/Hubs/Lookup/Descriptors/MethodDescriptor.cs b/src/Microsoft.AspNet.SignalR.Core/Hubs/Lookup/Descriptors/MethodDescriptor.cs deleted file mode 100644 index 971f54f01..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/Hubs/Lookup/Descriptors/MethodDescriptor.cs +++ /dev/null @@ -1,42 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; - -namespace Microsoft.AspNet.SignalR.Hubs -{ - /// - /// Holds information about a single hub method. - /// - public class MethodDescriptor : Descriptor - { - /// - /// The return type of this method. - /// - public virtual Type ReturnType { get; set; } - - /// - /// Hub descriptor object, target to this method. - /// - public virtual HubDescriptor Hub { get; set; } - - /// - /// Available method parameters. - /// - [SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Justification = "This is supposed to be mutable")] - public virtual IList Parameters { get; set; } - - /// - /// Method invocation delegate. - /// Takes a target hub and an array of invocation arguments as it's arguments. - /// - public virtual Func Invoker { get; set; } - - /// - /// Attributes attached to this method. - /// - public virtual IEnumerable Attributes { get; set; } - } -} - diff --git a/src/Microsoft.AspNet.SignalR.Core/Hubs/Lookup/Descriptors/NullMethodDescriptor.cs b/src/Microsoft.AspNet.SignalR.Core/Hubs/Lookup/Descriptors/NullMethodDescriptor.cs deleted file mode 100644 index d1617dbfb..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/Hubs/Lookup/Descriptors/NullMethodDescriptor.cs +++ /dev/null @@ -1,40 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; - -namespace Microsoft.AspNet.SignalR.Hubs -{ - public class NullMethodDescriptor : MethodDescriptor - { - private static readonly IEnumerable _attributes = new List(); - private static readonly IList _parameters = new List(); - - private string _methodName; - - public NullMethodDescriptor(string methodName) - { - _methodName = methodName; - } - - public override Func Invoker - { - get - { - return (emptyHub, emptyParameters) => - { - throw new InvalidOperationException(String.Format(CultureInfo.CurrentCulture, Resources.Error_MethodCouldNotBeResolved, _methodName)); - }; - } - } - - public override IList Parameters - { - get { return _parameters; } - } - - public override IEnumerable Attributes - { - get { return _attributes; } - } - } -} diff --git a/src/Microsoft.AspNet.SignalR.Core/Hubs/Lookup/Descriptors/ParameterDescriptor.cs b/src/Microsoft.AspNet.SignalR.Core/Hubs/Lookup/Descriptors/ParameterDescriptor.cs deleted file mode 100644 index 4e32d3a8b..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/Hubs/Lookup/Descriptors/ParameterDescriptor.cs +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -using System; - -namespace Microsoft.AspNet.SignalR.Hubs -{ - /// - /// Holds information about a single hub method parameter. - /// - public class ParameterDescriptor - { - /// - /// Parameter name. - /// - public virtual string Name { get; set; } - - /// - /// Parameter type. - /// - public virtual Type ParameterType { get; set; } - } -} - diff --git a/src/Microsoft.AspNet.SignalR.Core/Hubs/Lookup/HubMethodDispatcher.cs b/src/Microsoft.AspNet.SignalR.Core/Hubs/Lookup/HubMethodDispatcher.cs deleted file mode 100644 index c52b5e760..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/Hubs/Lookup/HubMethodDispatcher.cs +++ /dev/null @@ -1,79 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -using System.Collections.Generic; -using System.Linq.Expressions; -using System.Reflection; - -namespace Microsoft.AspNet.SignalR.Hubs -{ - internal class HubMethodDispatcher - { - private HubMethodExecutor _executor; - - public HubMethodDispatcher(MethodInfo methodInfo) - { - _executor = GetExecutor(methodInfo); - MethodInfo = methodInfo; - } - - private delegate object HubMethodExecutor(IHub hub, object[] parameters); - - private delegate void VoidHubMethodExecutor(IHub hub, object[] parameters); - - public MethodInfo MethodInfo { get; private set; } - - public object Execute(IHub hub, object[] parameters) - { - return _executor(hub, parameters); - } - - private static HubMethodExecutor GetExecutor(MethodInfo methodInfo) - { - // Parameters to executor - ParameterExpression hubParameter = Expression.Parameter(typeof(IHub), "hub"); - ParameterExpression parametersParameter = Expression.Parameter(typeof(object[]), "parameters"); - - // Build parameter list - List parameters = new List(); - ParameterInfo[] paramInfos = methodInfo.GetParameters(); - for (int i = 0; i < paramInfos.Length; i++) - { - ParameterInfo paramInfo = paramInfos[i]; - BinaryExpression valueObj = Expression.ArrayIndex(parametersParameter, Expression.Constant(i)); - UnaryExpression valueCast = Expression.Convert(valueObj, paramInfo.ParameterType); - - // valueCast is "(Ti) parameters[i]" - parameters.Add(valueCast); - } - - // Call method - UnaryExpression instanceCast = (!methodInfo.IsStatic) ? Expression.Convert(hubParameter, methodInfo.ReflectedType) : null; - MethodCallExpression methodCall = Expression.Call(instanceCast, methodInfo, parameters); - - // methodCall is "((TController) hub) method((T0) parameters[0], (T1) parameters[1], ...)" - // Create function - if (methodCall.Type == typeof(void)) - { - Expression lambda = Expression.Lambda(methodCall, hubParameter, parametersParameter); - VoidHubMethodExecutor voidExecutor = lambda.Compile(); - return WrapVoidAction(voidExecutor); - } - else - { - // must coerce methodCall to match HubMethodExecutor signature - UnaryExpression castMethodCall = Expression.Convert(methodCall, typeof(object)); - Expression lambda = Expression.Lambda(castMethodCall, hubParameter, parametersParameter); - return lambda.Compile(); - } - } - - private static HubMethodExecutor WrapVoidAction(VoidHubMethodExecutor executor) - { - return delegate(IHub hub, object[] parameters) - { - executor(hub, parameters); - return null; - }; - } - } -} diff --git a/src/Microsoft.AspNet.SignalR.Core/Hubs/Lookup/IHubDescriptorProvider.cs b/src/Microsoft.AspNet.SignalR.Core/Hubs/Lookup/IHubDescriptorProvider.cs deleted file mode 100644 index e439d0b92..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/Hubs/Lookup/IHubDescriptorProvider.cs +++ /dev/null @@ -1,28 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; - -namespace Microsoft.AspNet.SignalR.Hubs -{ - /// - /// Describes hub descriptor provider, which provides information about available hubs. - /// - public interface IHubDescriptorProvider - { - /// - /// Retrieve all avaiable hubs. - /// - /// Collection of hub descriptors. - [SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate", Justification = "This call might be expensive")] - IList GetHubs(); - - /// - /// Tries to retrieve hub with a given name. - /// - /// Name of the hub. - /// Retrieved descriptor object. - /// True, if hub has been found - bool TryGetHub(string hubName, out HubDescriptor descriptor); - } -} diff --git a/src/Microsoft.AspNet.SignalR.Core/Hubs/Lookup/IHubManager.cs b/src/Microsoft.AspNet.SignalR.Core/Hubs/Lookup/IHubManager.cs deleted file mode 100644 index 99da9b351..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/Hubs/Lookup/IHubManager.cs +++ /dev/null @@ -1,57 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -using System; -using System.Collections.Generic; -using Microsoft.AspNet.SignalR.Json; - -namespace Microsoft.AspNet.SignalR.Hubs -{ - /// - /// Describes a hub manager - main point in the whole hub and method lookup process. - /// - public interface IHubManager - { - /// - /// Retrieves a single hub descriptor. - /// - /// Name of the hub. - /// Hub descriptor, if found. Null, otherwise. - HubDescriptor GetHub(string hubName); - - /// - /// Retrieves all available hubs matching the given predicate. - /// - /// List of hub descriptors. - IEnumerable GetHubs(Func predicate); - - /// - /// Resolves a given hub name to a concrete object. - /// - /// Name of the hub. - /// Hub implementation instance, if found. Null otherwise. - IHub ResolveHub(string hubName); - - /// - /// Resolves all available hubs to their concrete objects. - /// - /// List of hub instances. - IEnumerable ResolveHubs(); - - /// - /// Retrieves a method with a given name on a given hub. - /// - /// Name of the hub. - /// Name of the method to find. - /// Method parameters to match. - /// Descriptor of the method, if found. Null otherwise. - MethodDescriptor GetHubMethod(string hubName, string method, IList parameters); - - /// - /// Gets all methods available to call on a given hub. - /// - /// Name of the hub, - /// Optional predicate for filtering results. - /// List of available methods. - IEnumerable GetHubMethods(string hubName, Func predicate); - } -} diff --git a/src/Microsoft.AspNet.SignalR.Core/Hubs/Lookup/IMethodDescriptorProvider.cs b/src/Microsoft.AspNet.SignalR.Core/Hubs/Lookup/IMethodDescriptorProvider.cs deleted file mode 100644 index ae0700f36..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/Hubs/Lookup/IMethodDescriptorProvider.cs +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using Microsoft.AspNet.SignalR.Json; - -namespace Microsoft.AspNet.SignalR.Hubs -{ - /// - /// Describes a hub method provider that builds a collection of available methods on a given hub. - /// - public interface IMethodDescriptorProvider - { - /// - /// Retrieve all methods on a given hub. - /// - /// Hub descriptor object. - /// Available methods. - IEnumerable GetMethods(HubDescriptor hub); - - /// - /// Tries to retrieve a method. - /// - /// Hub descriptor object - /// Name of the method. - /// Descriptor of the method, if found. Null otherwise. - /// Method parameters to match. - /// True, if a method has been found. - [SuppressMessage("Microsoft.Design", "CA1021:AvoidOutParameters", MessageId = "2#", Justification = "This is a well known pattern for efficient lookup")] - bool TryGetMethod(HubDescriptor hub, string method, out MethodDescriptor descriptor, IList parameters); - } -} diff --git a/src/Microsoft.AspNet.SignalR.Core/Hubs/Lookup/IParameterResolver.cs b/src/Microsoft.AspNet.SignalR.Core/Hubs/Lookup/IParameterResolver.cs deleted file mode 100644 index e1ed36d61..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/Hubs/Lookup/IParameterResolver.cs +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -using System.Collections.Generic; -using Microsoft.AspNet.SignalR.Json; - -namespace Microsoft.AspNet.SignalR.Hubs -{ - /// - /// Describes a parameter resolver for resolving parameter-matching values based on provided information. - /// - public interface IParameterResolver - { - /// - /// Resolves method parameter values based on provided objects. - /// - /// Method descriptor. - /// List of values to resolve parameter values from. - /// Array of parameter values. - IList ResolveMethodParameters(MethodDescriptor method, IList values); - } -} diff --git a/src/Microsoft.AspNet.SignalR.Core/Hubs/Lookup/ReflectedHubDescriptorProvider.cs b/src/Microsoft.AspNet.SignalR.Core/Hubs/Lookup/ReflectedHubDescriptorProvider.cs deleted file mode 100644 index 49b1e1d44..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/Hubs/Lookup/ReflectedHubDescriptorProvider.cs +++ /dev/null @@ -1,87 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; - -namespace Microsoft.AspNet.SignalR.Hubs -{ - public class ReflectedHubDescriptorProvider : IHubDescriptorProvider - { - private readonly Lazy> _hubs; - private readonly Lazy _locator; - - public ReflectedHubDescriptorProvider(IDependencyResolver resolver) - { - _locator = new Lazy(resolver.Resolve); - _hubs = new Lazy>(BuildHubsCache); - } - - public IList GetHubs() - { - return _hubs.Value - .Select(kv => kv.Value) - .Distinct() - .ToList(); - } - - public bool TryGetHub(string hubName, out HubDescriptor descriptor) - { - return _hubs.Value.TryGetValue(hubName, out descriptor); - } - - protected IDictionary BuildHubsCache() - { - // Getting all IHub-implementing types that apply - var types = _locator.Value.GetAssemblies() - .SelectMany(GetTypesSafe) - .Where(IsHubType); - - // Building cache entries for each descriptor - // Each descriptor is stored in dictionary under a key - // that is it's name or the name provided by an attribute - var cacheEntries = types - .Select(type => new HubDescriptor - { - NameSpecified = (type.GetHubAttributeName() != null), - Name = type.GetHubName(), - HubType = type - }) - .ToDictionary(hub => hub.Name, - hub => hub, - StringComparer.OrdinalIgnoreCase); - - return cacheEntries; - } - - [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "If we throw then it's not a hub type")] - private static bool IsHubType(Type type) - { - try - { - return typeof(IHub).IsAssignableFrom(type) && - !type.IsAbstract && - (type.Attributes.HasFlag(TypeAttributes.Public) || - type.Attributes.HasFlag(TypeAttributes.NestedPublic)); - } - catch - { - return false; - } - } - - [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "If we throw then we have an empty type")] - private static IEnumerable GetTypesSafe(Assembly a) - { - try - { - return a.GetTypes(); - } - catch - { - return Enumerable.Empty(); - } - } - } -} diff --git a/src/Microsoft.AspNet.SignalR.Core/Hubs/Lookup/ReflectedMethodDescriptorProvider.cs b/src/Microsoft.AspNet.SignalR.Core/Hubs/Lookup/ReflectedMethodDescriptorProvider.cs deleted file mode 100644 index db6b95756..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/Hubs/Lookup/ReflectedMethodDescriptorProvider.cs +++ /dev/null @@ -1,150 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using System.Reflection; -using Microsoft.AspNet.SignalR.Json; - -namespace Microsoft.AspNet.SignalR.Hubs -{ - public class ReflectedMethodDescriptorProvider : IMethodDescriptorProvider - { - private readonly ConcurrentDictionary>> _methods; - private readonly ConcurrentDictionary _executableMethods; - - public ReflectedMethodDescriptorProvider() - { - _methods = new ConcurrentDictionary>>(StringComparer.OrdinalIgnoreCase); - _executableMethods = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); - } - - public IEnumerable GetMethods(HubDescriptor hub) - { - return FetchMethodsFor(hub) - .SelectMany(kv => kv.Value) - .ToList(); - } - - /// - /// Retrieves an existing dictionary of all available methods for a given hub from cache. - /// If cache entry does not exist - it is created automatically by BuildMethodCacheFor. - /// - /// - /// - private IDictionary> FetchMethodsFor(HubDescriptor hub) - { - return _methods.GetOrAdd( - hub.Name, - key => BuildMethodCacheFor(hub)); - } - - /// - /// Builds a dictionary of all possible methods on a given hub. - /// Single entry contains a collection of available overloads for a given method name (key). - /// This dictionary is being cached afterwards. - /// - /// Hub to build cache for - /// Dictionary of available methods - private static IDictionary> BuildMethodCacheFor(HubDescriptor hub) - { - return ReflectionHelper.GetExportedHubMethods(hub.HubType) - .GroupBy(GetMethodName, StringComparer.OrdinalIgnoreCase) - .ToDictionary(group => group.Key, - group => group.Select(oload => - new MethodDescriptor - { - ReturnType = oload.ReturnType, - Name = group.Key, - NameSpecified = (GetMethodAttributeName(oload) != null), - Invoker = new HubMethodDispatcher(oload).Execute, - Hub = hub, - Attributes = oload.GetCustomAttributes(typeof(Attribute), inherit: true).Cast(), - Parameters = oload.GetParameters() - .Select(p => new ParameterDescriptor - { - Name = p.Name, - ParameterType = p.ParameterType, - }) - .ToList() - }), - StringComparer.OrdinalIgnoreCase); - } - - /// - /// Searches the specified Hub for the specified . - /// - /// - /// In the case that there are multiple overloads of the specified , the parameter set helps determine exactly which instance of the overload should be resolved. - /// If there are multiple overloads found with the same number of matching parameters, none of the methods will be returned because it is not possible to determine which overload of the method was intended to be resolved. - /// - /// Hub to search for the specified on. - /// The method name to search for. - /// If successful, the that was resolved. - /// The set of parameters that will be used to help locate a specific overload of the specified . - /// True if the method matching the name/parameter set is found on the hub, otherwise false. - public bool TryGetMethod(HubDescriptor hub, string method, out MethodDescriptor descriptor, IList parameters) - { - string hubMethodKey = BuildHubExecutableMethodCacheKey(hub, method, parameters); - - if (!_executableMethods.TryGetValue(hubMethodKey, out descriptor)) - { - IEnumerable overloads; - - if (FetchMethodsFor(hub).TryGetValue(method, out overloads)) - { - var matches = overloads.Where(o => o.Matches(parameters)).ToList(); - - // If only one match is found, that is the "executable" version, otherwise none of the methods can be returned because we don't know which one was actually being targeted - descriptor = matches.Count == 1 ? matches[0] : null; - } - else - { - descriptor = null; - } - - // If an executable method was found, cache it for future lookups (NOTE: we don't cache null instances because it could be a surface area for DoS attack by supplying random method names to flood the cache) - if (descriptor != null) - { - _executableMethods.TryAdd(hubMethodKey, descriptor); - } - } - - return descriptor != null; - } - - private static string BuildHubExecutableMethodCacheKey(HubDescriptor hub, string method, IList parameters) - { - string normalizedParameterCountKeyPart; - - if (parameters != null) - { - normalizedParameterCountKeyPart = parameters.Count.ToString(CultureInfo.InvariantCulture); - } - else - { - // NOTE: we normalize a null parameter array to be the same as an empty (i.e. Length == 0) parameter array - normalizedParameterCountKeyPart = "0"; - } - - // NOTE: we always normalize to all uppercase since method names are case insensitive and could theoretically come in diff. variations per call - string normalizedMethodName = method.ToUpperInvariant(); - - string methodKey = hub.Name + "::" + normalizedMethodName + "(" + normalizedParameterCountKeyPart + ")"; - - return methodKey; - } - - private static string GetMethodName(MethodInfo method) - { - return GetMethodAttributeName(method) ?? method.Name; - } - - private static string GetMethodAttributeName(MethodInfo method) - { - return ReflectionHelper.GetAttributeValue(method, a => a.MethodName); - } - } -} diff --git a/src/Microsoft.AspNet.SignalR.Core/Hubs/NullClientProxy.cs b/src/Microsoft.AspNet.SignalR.Core/Hubs/NullClientProxy.cs deleted file mode 100644 index 4d944017e..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/Hubs/NullClientProxy.cs +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -using System; -using System.Dynamic; -using System.Globalization; - -namespace Microsoft.AspNet.SignalR.Hubs -{ - internal class NullClientProxy : DynamicObject - { - public override bool TryGetMember(GetMemberBinder binder, out object result) - { - throw new InvalidOperationException(String.Format(CultureInfo.CurrentCulture, Resources.Error_UsingHubInstanceNotCreatedUnsupported)); - } - - public override bool TryInvokeMember(InvokeMemberBinder binder, object[] args, out object result) - { - throw new InvalidOperationException(String.Format(CultureInfo.CurrentCulture, Resources.Error_UsingHubInstanceNotCreatedUnsupported)); - } - } -} diff --git a/src/Microsoft.AspNet.SignalR.Core/Hubs/NullJavaScriptMinifier.cs b/src/Microsoft.AspNet.SignalR.Core/Hubs/NullJavaScriptMinifier.cs deleted file mode 100644 index 932d38d0a..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/Hubs/NullJavaScriptMinifier.cs +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -using System.Diagnostics.CodeAnalysis; - -namespace Microsoft.AspNet.SignalR.Hubs -{ - public class NullJavaScriptMinifier : IJavaScriptMinifier - { - [SuppressMessage("Microsoft.Security", "CA2104:DoNotDeclareReadOnlyMutableReferenceTypes", Justification = "This is a singleton")] - public static readonly NullJavaScriptMinifier Instance = new NullJavaScriptMinifier(); - - public string Minify(string source) - { - return source; - } - } -} diff --git a/src/Microsoft.AspNet.SignalR.Core/Hubs/Pipeline/Auth/AuthorizeModule.cs b/src/Microsoft.AspNet.SignalR.Core/Hubs/Pipeline/Auth/AuthorizeModule.cs deleted file mode 100644 index 046e45cba..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/Hubs/Pipeline/Auth/AuthorizeModule.cs +++ /dev/null @@ -1,125 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using System.Threading.Tasks; - -namespace Microsoft.AspNet.SignalR.Hubs -{ - /// - /// This module is added the the HubPipeline by default. - /// - /// Hub level attributes that implement such as are applied to determine - /// whether to allow potential clients to receive messages sent from that hub using a or a - /// All applicable hub attributes must allow hub connection for the connection to be authorized. - /// - /// Hub and method level attributes that implement such as are applied - /// to determine whether to allow callers to invoke hub methods. - /// All applicable hub level AND method level attributes must allow hub method invocation for the invocation to be authorized. - /// - /// Optionally, this module may be instantiated with and - /// authorizers that will be applied globally to all hubs and hub methods. - /// - public class AuthorizeModule : HubPipelineModule - { - // Global authorizers - private readonly IAuthorizeHubConnection _globalConnectionAuthorizer; - private readonly IAuthorizeHubMethodInvocation _globalInvocationAuthorizer; - - // Attribute authorizer caches - private readonly ConcurrentDictionary> _connectionAuthorizersCache; - private readonly ConcurrentDictionary> _classInvocationAuthorizersCache; - private readonly ConcurrentDictionary> _methodInvocationAuthorizersCache; - - // By default, this module does not include any authorizers that are applied globally. - // This module will always apply authorizers attached to hubs or hub methods - public AuthorizeModule() - : this(globalConnectionAuthorizer: null, globalInvocationAuthorizer: null) - { - } - - public AuthorizeModule(IAuthorizeHubConnection globalConnectionAuthorizer, IAuthorizeHubMethodInvocation globalInvocationAuthorizer) - { - // Set global authorizers - _globalConnectionAuthorizer = globalConnectionAuthorizer; - _globalInvocationAuthorizer = globalInvocationAuthorizer; - - // Initialize attribute authorizer caches - _connectionAuthorizersCache = new ConcurrentDictionary>(); - _classInvocationAuthorizersCache = new ConcurrentDictionary>(); - _methodInvocationAuthorizersCache = new ConcurrentDictionary>(); - } - - public override Func BuildAuthorizeConnect(Func authorizeConnect) - { - return base.BuildAuthorizeConnect((hubDescriptor, request) => - { - // Execute custom modules first and short circuit if any deny authorization. - if (!authorizeConnect(hubDescriptor, request)) - { - return false; - } - - // Execute the global hub connection authorizer if there is one next and short circuit if it denies authorization. - if (_globalConnectionAuthorizer != null && !_globalConnectionAuthorizer.AuthorizeHubConnection(hubDescriptor, request)) - { - return false; - } - - // Get hub attributes implementing IAuthorizeHubConnection from the cache - // If the attributes do not exist in the cache, retrieve them using reflection and add them to the cache - var attributeAuthorizers = _connectionAuthorizersCache.GetOrAdd(hubDescriptor.HubType, - hubType => hubType.GetCustomAttributes(typeof(IAuthorizeHubConnection), inherit: true).Cast()); - - // Every attribute (if any) implementing IAuthorizeHubConnection attached to the relevant hub MUST allow the connection - return attributeAuthorizers.All(a => a.AuthorizeHubConnection(hubDescriptor, request)); - }); - } - - public override Func> BuildIncoming(Func> invoke) - { - return base.BuildIncoming(context => - { - // Execute the global method invocation authorizer if there is one and short circuit if it denies authorization. - if (_globalInvocationAuthorizer == null || _globalInvocationAuthorizer.AuthorizeHubMethodInvocation(context, appliesToMethod: false)) - { - // Get hub attributes implementing IAuthorizeHubMethodInvocation from the cache - // If the attributes do not exist in the cache, retrieve them using reflection and add them to the cache - var classLevelAuthorizers = _classInvocationAuthorizersCache.GetOrAdd(context.Hub.GetType(), - hubType => hubType.GetCustomAttributes(typeof(IAuthorizeHubMethodInvocation), inherit: true).Cast()); - - // Execute all hub level authorizers and short circuit if ANY deny authorization. - if (classLevelAuthorizers.All(a => a.AuthorizeHubMethodInvocation(context, appliesToMethod: false))) - { - // If the MethodDescriptor is a NullMethodDescriptor, we don't want to cache it since a new one is created - // for each invocation with an invalid method name. #1801 - if (context.MethodDescriptor is NullMethodDescriptor) - { - return invoke(context); - } - - // Get method attributes implementing IAuthorizeHubMethodInvocation from the cache - // If the attributes do not exist in the cache, retrieve them from the MethodDescriptor and add them to the cache - var methodLevelAuthorizers = _methodInvocationAuthorizersCache.GetOrAdd(context.MethodDescriptor, - methodDescriptor => methodDescriptor.Attributes.OfType()); - - // Execute all method level authorizers. If ALL provide authorization, continue executing the invocation pipeline. - if (methodLevelAuthorizers.All(a => a.AuthorizeHubMethodInvocation(context, appliesToMethod: true))) - { - return invoke(context); - } - } - } - - // Send error back to the client - return TaskAsyncHelper.FromError( - new NotAuthorizedException(String.Format(CultureInfo.CurrentCulture, Resources.Error_CallerNotAuthorizedToInvokeMethodOn, - context.MethodDescriptor.Name, - context.MethodDescriptor.Hub.Name))); - }); - } - } -} diff --git a/src/Microsoft.AspNet.SignalR.Core/Hubs/Pipeline/Auth/IAuthorizeHubConnection.cs b/src/Microsoft.AspNet.SignalR.Core/Hubs/Pipeline/Auth/IAuthorizeHubConnection.cs deleted file mode 100644 index c89e60014..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/Hubs/Pipeline/Auth/IAuthorizeHubConnection.cs +++ /dev/null @@ -1,18 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -namespace Microsoft.AspNet.SignalR.Hubs -{ - /// - /// Interface to be implemented by s that can authorize client to connect to a . - /// - public interface IAuthorizeHubConnection - { - /// - /// Given a , determine whether client is authorized to connect to . - /// - /// Description of the hub client is attempting to connect to. - /// The connection request from the client. - /// true if the caller is authorized to connect to the hub; otherwise, false. - bool AuthorizeHubConnection(HubDescriptor hubDescriptor, IRequest request); - } -} diff --git a/src/Microsoft.AspNet.SignalR.Core/Hubs/Pipeline/Auth/IAuthorizeHubMethodInvocation.cs b/src/Microsoft.AspNet.SignalR.Core/Hubs/Pipeline/Auth/IAuthorizeHubMethodInvocation.cs deleted file mode 100644 index 4d1eee752..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/Hubs/Pipeline/Auth/IAuthorizeHubMethodInvocation.cs +++ /dev/null @@ -1,18 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -namespace Microsoft.AspNet.SignalR.Hubs -{ - /// - /// Interface to be implemented by s that can authorize the invocation of methods. - /// - public interface IAuthorizeHubMethodInvocation - { - /// - /// Given a , determine whether client is authorized to invoke the method. - /// - /// An providing details regarding the method invocation. - /// Indicates whether the interface instance is an attribute applied directly to a method. - /// true if the caller is authorized to invoke the method; otherwise, false. - bool AuthorizeHubMethodInvocation(IHubIncomingInvokerContext hubIncomingInvokerContext, bool appliesToMethod); - } -} diff --git a/src/Microsoft.AspNet.SignalR.Core/Hubs/Pipeline/Auth/NotAuthorizedException.cs b/src/Microsoft.AspNet.SignalR.Core/Hubs/Pipeline/Auth/NotAuthorizedException.cs deleted file mode 100644 index 10d32b649..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/Hubs/Pipeline/Auth/NotAuthorizedException.cs +++ /dev/null @@ -1,18 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -using System; - -namespace Microsoft.AspNet.SignalR.Hubs -{ - [Serializable] - public class NotAuthorizedException : Exception - { - public NotAuthorizedException() { } - public NotAuthorizedException(string message) : base(message) { } - public NotAuthorizedException(string message, Exception inner) : base(message, inner) { } - protected NotAuthorizedException( - System.Runtime.Serialization.SerializationInfo info, - System.Runtime.Serialization.StreamingContext context) - : base(info, context) { } - } -} diff --git a/src/Microsoft.AspNet.SignalR.Core/Hubs/Pipeline/HubInvokerContext.cs b/src/Microsoft.AspNet.SignalR.Core/Hubs/Pipeline/HubInvokerContext.cs deleted file mode 100644 index eea40b052..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/Hubs/Pipeline/HubInvokerContext.cs +++ /dev/null @@ -1,42 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -using System.Collections.Generic; - -namespace Microsoft.AspNet.SignalR.Hubs -{ - internal class HubInvokerContext : IHubIncomingInvokerContext - { - public HubInvokerContext(IHub hub, StateChangeTracker tracker, MethodDescriptor methodDescriptor, IList args) - { - Hub = hub; - MethodDescriptor = methodDescriptor; - Args = args; - StateTracker = tracker; - } - - public IHub Hub - { - get; - private set; - } - - public MethodDescriptor MethodDescriptor - { - get; - private set; - } - - public IList Args - { - get; - private set; - } - - - public StateChangeTracker StateTracker - { - get; - private set; - } - } -} diff --git a/src/Microsoft.AspNet.SignalR.Core/Hubs/Pipeline/HubOutgoingInvokerContext.cs b/src/Microsoft.AspNet.SignalR.Core/Hubs/Pipeline/HubOutgoingInvokerContext.cs deleted file mode 100644 index 8de9851f7..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/Hubs/Pipeline/HubOutgoingInvokerContext.cs +++ /dev/null @@ -1,41 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -using System.Collections.Generic; - -namespace Microsoft.AspNet.SignalR.Hubs -{ - internal class HubOutgoingInvokerContext : IHubOutgoingInvokerContext - { - public HubOutgoingInvokerContext(IConnection connection, string signal, ClientHubInvocation invocation, IList excludedSignals) - { - Connection = connection; - Signal = signal; - Invocation = invocation; - ExcludedSignals = excludedSignals; - } - - public IConnection Connection - { - get; - private set; - } - - public ClientHubInvocation Invocation - { - get; - private set; - } - - public string Signal - { - get; - private set; - } - - public IList ExcludedSignals - { - get; - private set; - } - } -} diff --git a/src/Microsoft.AspNet.SignalR.Core/Hubs/Pipeline/HubPipeline.cs b/src/Microsoft.AspNet.SignalR.Core/Hubs/Pipeline/HubPipeline.cs deleted file mode 100644 index 9cf6eccd4..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/Hubs/Pipeline/HubPipeline.cs +++ /dev/null @@ -1,103 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using System.Threading.Tasks; - -namespace Microsoft.AspNet.SignalR.Hubs -{ - internal class HubPipeline : IHubPipeline, IHubPipelineInvoker - { - private readonly Stack _modules; - private readonly Lazy _pipeline; - - public HubPipeline() - { - _modules = new Stack(); - _pipeline = new Lazy(() => new ComposedPipeline(_modules)); - } - - public IHubPipeline AddModule(IHubPipelineModule pipelineModule) - { - if (_pipeline.IsValueCreated) - { - throw new InvalidOperationException(String.Format(CultureInfo.CurrentCulture, Resources.Error_UnableToAddModulePiplineAlreadyInvoked)); - } - _modules.Push(pipelineModule); - return this; - } - - private ComposedPipeline Pipeline - { - get { return _pipeline.Value; } - } - - public Task Invoke(IHubIncomingInvokerContext context) - { - return Pipeline.Invoke(context); - } - - public Task Connect(IHub hub) - { - return Pipeline.Connect(hub); - } - - public Task Reconnect(IHub hub) - { - return Pipeline.Reconnect(hub); - } - - public Task Disconnect(IHub hub) - { - return Pipeline.Disconnect(hub); - } - - public bool AuthorizeConnect(HubDescriptor hubDescriptor, IRequest request) - { - return Pipeline.AuthorizeConnect(hubDescriptor, request); - } - - public IList RejoiningGroups(HubDescriptor hubDescriptor, IRequest request, IList groups) - { - return Pipeline.RejoiningGroups(hubDescriptor, request, groups); - } - - public Task Send(IHubOutgoingInvokerContext context) - { - return Pipeline.Send(context); - } - - private class ComposedPipeline - { - - public Func> Invoke; - public Func Connect; - public Func Reconnect; - public Func Disconnect; - public Func AuthorizeConnect; - public Func, IList> RejoiningGroups; - public Func Send; - - public ComposedPipeline(Stack modules) - { - // This wouldn't look nearly as gnarly if C# had better type inference, but now we don't need the ComposedModule or PassThroughModule. - Invoke = Compose>>(modules, (m, f) => m.BuildIncoming(f))(HubDispatcher.Incoming); - Connect = Compose>(modules, (m, f) => m.BuildConnect(f))(HubDispatcher.Connect); - Reconnect = Compose>(modules, (m, f) => m.BuildReconnect(f))(HubDispatcher.Reconnect); - Disconnect = Compose>(modules, (m, f) => m.BuildDisconnect(f))(HubDispatcher.Disconnect); - AuthorizeConnect = Compose>(modules, (m, f) => m.BuildAuthorizeConnect(f))((h, r) => true); - RejoiningGroups = Compose, IList>>(modules, (m, f) => m.BuildRejoiningGroups(f))((h, r, g) => g); - Send = Compose>(modules, (m, f) => m.BuildOutgoing(f))(HubDispatcher.Outgoing); - } - - // IHubPipelineModule could be turned into a second generic parameter, but it would make the above invocations even longer than they currently are. - private static Func Compose(IEnumerable modules, Func method) - { - // Notice we are reversing and aggregating in one step. (Function composition is associative) - return modules.Aggregate>(x => x, (a, b) => (x => method(b, a(x)))); - } - } - } -} diff --git a/src/Microsoft.AspNet.SignalR.Core/Hubs/Pipeline/HubPipelineExtensions.cs b/src/Microsoft.AspNet.SignalR.Core/Hubs/Pipeline/HubPipelineExtensions.cs deleted file mode 100644 index 0eaac77b8..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/Hubs/Pipeline/HubPipelineExtensions.cs +++ /dev/null @@ -1,28 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -using System; -using Microsoft.AspNet.SignalR.Hubs; - -namespace Microsoft.AspNet.SignalR -{ - public static class HubPipelineExtensions - { - /// - /// Requiring Authentication adds an to the with - /// and authorizers that will be applied globally to all hubs and hub methods. - /// These authorizers require that the 's - /// IsAuthenticated for any clients that invoke server-side hub methods or receive client-side hub method invocations. - /// - /// The to which the will be added. - public static void RequireAuthentication(this IHubPipeline pipeline) - { - if (pipeline == null) - { - throw new ArgumentNullException("pipeline"); - } - - var authorizer = new AuthorizeAttribute(); - pipeline.AddModule(new AuthorizeModule(globalConnectionAuthorizer: authorizer, globalInvocationAuthorizer: authorizer)); - } - } -} diff --git a/src/Microsoft.AspNet.SignalR.Core/Hubs/Pipeline/HubPipelineModule.cs b/src/Microsoft.AspNet.SignalR.Core/Hubs/Pipeline/HubPipelineModule.cs deleted file mode 100644 index d914c34f0..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/Hubs/Pipeline/HubPipelineModule.cs +++ /dev/null @@ -1,311 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -using System; -using System.Collections.Generic; -using System.Threading.Tasks; - -namespace Microsoft.AspNet.SignalR.Hubs -{ - /// - /// Common base class to simplify the implementation of IHubPipelineModules. - /// A module can intercept and customize various stages of hub processing such as connecting, reconnecting, disconnecting, - /// invoking server-side hub methods, invoking client-side hub methods, authorizing hub clients and rejoining hub groups. - /// A module can be activated by calling . - /// The combined modules added to the are invoked via the - /// interface. - /// - public abstract class HubPipelineModule : IHubPipelineModule - { - /// - /// Wraps a function that invokes a server-side hub method. Even if a client has not been authorized to connect - /// to a hub, it will still be authorized to invoke server-side methods on that hub unless it is prevented in - /// by not executing the invoke parameter. - /// - /// A function that invokes a server-side hub method. - /// A wrapped function that invokes a server-side hub method. - public virtual Func> BuildIncoming(Func> invoke) - { - return context => - { - if (OnBeforeIncoming(context)) - { - return invoke(context).OrEmpty() - .Then(result => OnAfterIncoming(result, context)) - .Catch(ex => OnIncomingError(ex, context)); - } - - return TaskAsyncHelper.FromResult(null); - }; - } - - /// - /// Wraps a function that is called when a client connects to the for each - /// the client connects to. By default, this results in the 's - /// OnConnected method being invoked. - /// - /// A function to be called when a client connects to a hub. - /// A wrapped function to be called when a client connects to a hub. - public virtual Func BuildConnect(Func connect) - { - return hub => - { - if (OnBeforeConnect(hub)) - { - return connect(hub).OrEmpty().Then(h => OnAfterConnect(h), hub); - } - - return TaskAsyncHelper.Empty; - }; - } - - /// - /// Wraps a function that is called when a client reconnects to the for each - /// the client connects to. By default, this results in the 's - /// OnReconnected method being invoked. - /// - /// A function to be called when a client reconnects to a hub. - /// A wrapped function to be called when a client reconnects to a hub. - public virtual Func BuildReconnect(Func reconnect) - { - return (hub) => - { - if (OnBeforeReconnect(hub)) - { - return reconnect(hub).OrEmpty().Then(h => OnAfterReconnect(h), hub); - } - return TaskAsyncHelper.Empty; - }; - } - - /// - /// Wraps a function that is called when a client disconnects from the for each - /// the client was connected to. By default, this results in the 's - /// OnDisconnected method being invoked. - /// - /// A function to be called when a client disconnects from a hub. - /// A wrapped function to be called when a client disconnects from a hub. - public virtual Func BuildDisconnect(Func disconnect) - { - return hub => - { - if (OnBeforeDisconnect(hub)) - { - return disconnect(hub).OrEmpty().Then(h => OnAfterDisconnect(h), hub); - } - - return TaskAsyncHelper.Empty; - }; - } - - /// - /// Wraps a function to be called before a client subscribes to signals belonging to the hub described by the - /// . By default, the will look for attributes on the - /// to help determine if the client is authorized to subscribe to method invocations for the - /// described hub. - /// The function returns true if the client is authorized to subscribe to client-side hub method - /// invocations; false, otherwise. - /// - /// - /// A function that dictates whether or not the client is authorized to connect to the described Hub. - /// - /// - /// A wrapped function that dictates whether or not the client is authorized to connect to the described Hub. - /// - public virtual Func BuildAuthorizeConnect(Func authorizeConnect) - { - return (hubDescriptor, request) => - { - if (OnBeforeAuthorizeConnect(hubDescriptor, request)) - { - return authorizeConnect(hubDescriptor, request); - } - return false; - }; - } - - /// - /// Wraps a function that determines which of the groups belonging to the hub described by the - /// the client should be allowed to rejoin. - /// By default, clients will rejoin all the groups they were in prior to reconnecting. - /// - /// A function that determines which groups the client should be allowed to rejoin. - /// A wrapped function that determines which groups the client should be allowed to rejoin. - public virtual Func, IList> BuildRejoiningGroups(Func, IList> rejoiningGroups) - { - return rejoiningGroups; - } - - /// - /// Wraps a function that invokes a client-side hub method. - /// - /// A function that invokes a client-side hub method. - /// A wrapped function that invokes a client-side hub method. - public virtual Func BuildOutgoing(Func send) - { - return context => - { - if (OnBeforeOutgoing(context)) - { - return send(context).OrEmpty().Then(ctx => OnAfterOutgoing(ctx), context); - } - - return TaskAsyncHelper.Empty; - }; - } - - /// - /// This method is called before the AuthorizeConnect components of any modules added later to the - /// are executed. If this returns false, then those later-added modules will not run and the client will not be allowed - /// to subscribe to client-side invocations of methods belonging to the hub defined by the . - /// - /// A description of the hub the client is trying to subscribe to. - /// The connect request of the client trying to subscribe to the hub. - /// true, if the client is authorized to connect to the hub, false otherwise. - protected virtual bool OnBeforeAuthorizeConnect(HubDescriptor hubDescriptor, IRequest request) - { - return true; - } - - /// - /// This method is called before the connect components of any modules added later to the are - /// executed. If this returns false, then those later-added modules and the method will - /// not be run. - /// - /// The hub the client has connected to. - /// - /// true, if the connect components of later added modules and the method should be executed; - /// false, otherwise. - /// - protected virtual bool OnBeforeConnect(IHub hub) - { - return true; - } - - /// - /// This method is called after the connect components of any modules added later to the are - /// executed and after is executed, if at all. - /// - /// The hub the client has connected to. - protected virtual void OnAfterConnect(IHub hub) - { - - } - - /// - /// This method is called before the reconnect components of any modules added later to the are - /// executed. If this returns false, then those later-added modules and the method will - /// not be run. - /// - /// The hub the client has reconnected to. - /// - /// true, if the reconnect components of later added modules and the method should be executed; - /// false, otherwise. - /// - protected virtual bool OnBeforeReconnect(IHub hub) - { - return true; - } - - /// - /// This method is called after the reconnect components of any modules added later to the are - /// executed and after is executed, if at all. - /// - /// The hub the client has reconnected to. - protected virtual void OnAfterReconnect(IHub hub) - { - - } - - /// - /// This method is called before the outgoing components of any modules added later to the are - /// executed. If this returns false, then those later-added modules and the client-side hub method invocation(s) will not - /// be executed. - /// - /// A description of the client-side hub method invocation. - /// - /// true, if the outgoing components of later added modules and the client-side hub method invocation(s) should be executed; - /// false, otherwise. - /// - protected virtual bool OnBeforeOutgoing(IHubOutgoingInvokerContext context) - { - return true; - } - - /// - /// This method is called after the outgoing components of any modules added later to the are - /// executed. This does not mean that all the clients have received the hub method invocation, but it does indicate indicate - /// a hub invocation message has successfully been published to a message bus. - /// - /// A description of the client-side hub method invocation. - protected virtual void OnAfterOutgoing(IHubOutgoingInvokerContext context) - { - - } - - /// - /// This method is called before the disconnect components of any modules added later to the are - /// executed. If this returns false, then those later-added modules and the method will - /// not be run. - /// - /// The hub the client has disconnected from. - /// - /// true, if the disconnect components of later added modules and the method should be executed; - /// false, otherwise. - /// - protected virtual bool OnBeforeDisconnect(IHub hub) - { - return true; - } - - /// - /// This method is called after the disconnect components of any modules added later to the are - /// executed and after is executed, if at all. - /// - /// The hub the client has disconnected from. - protected virtual void OnAfterDisconnect(IHub hub) - { - - } - - /// - /// This method is called before the incoming components of any modules added later to the are - /// executed. If this returns false, then those later-added modules and the server-side hub method invocation will not - /// be executed. Even if a client has not been authorized to connect to a hub, it will still be authorized to invoke - /// server-side methods on that hub unless it is prevented in by not - /// executing the invoke parameter or prevented in by returning false. - /// - /// A description of the server-side hub method invocation. - /// - /// true, if the incoming components of later added modules and the server-side hub method invocation should be executed; - /// false, otherwise. - /// - protected virtual bool OnBeforeIncoming(IHubIncomingInvokerContext context) - { - return true; - } - - /// - /// This method is called after the incoming components of any modules added later to the - /// and the server-side hub method have completed execution. - /// - /// The return value of the server-side hub method - /// A description of the server-side hub method invocation. - /// The possibly new or updated return value of the server-side hub method - protected virtual object OnAfterIncoming(object result, IHubIncomingInvokerContext context) - { - return result; - } - - /// - /// This is called when an uncaught exception is thrown by a server-side hub method or the incoming component of a - /// module added later to the . Observing the exception using this method will not prevent - /// it from bubbling up to other modules. - /// - /// The exception that was thrown during the server-side invocation. - /// A description of the server-side hub method invocation. - protected virtual void OnIncomingError(Exception ex, IHubIncomingInvokerContext context) - { - - } - } -} diff --git a/src/Microsoft.AspNet.SignalR.Core/Hubs/Pipeline/IHubIncomingInvokerContext.cs b/src/Microsoft.AspNet.SignalR.Core/Hubs/Pipeline/IHubIncomingInvokerContext.cs deleted file mode 100644 index a57d92b36..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/Hubs/Pipeline/IHubIncomingInvokerContext.cs +++ /dev/null @@ -1,34 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; - -namespace Microsoft.AspNet.SignalR.Hubs -{ - /// - /// A description of a server-side hub method invocation originating from a client. - /// - public interface IHubIncomingInvokerContext - { - /// - /// A hub instance that contains the invoked method as a member. - /// - IHub Hub { get; } - - /// - /// A description of the method being invoked by the client. - /// - MethodDescriptor MethodDescriptor { get; } - - /// - /// The arguments to be passed to the invoked method. - /// - [SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays", Justification = "This represents an ordered list of parameter values")] - IList Args { get; } - - /// - /// A key-value store representing the hub state on the client at the time of the invocation. - /// - StateChangeTracker StateTracker { get; } - } -} diff --git a/src/Microsoft.AspNet.SignalR.Core/Hubs/Pipeline/IHubOutgoingInvokerContext.cs b/src/Microsoft.AspNet.SignalR.Core/Hubs/Pipeline/IHubOutgoingInvokerContext.cs deleted file mode 100644 index 9f69af582..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/Hubs/Pipeline/IHubOutgoingInvokerContext.cs +++ /dev/null @@ -1,34 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -using System.Collections.Generic; -namespace Microsoft.AspNet.SignalR.Hubs -{ - /// - /// A description of a client-side hub method invocation originating from the server. - /// - public interface IHubOutgoingInvokerContext - { - /// - /// The , if any, corresponding to the client that invoked the server-side hub method - /// that is invoking the client-side hub method. - /// - IConnection Connection { get; } - - /// - /// A description of the method call to be made on the client. - /// - ClientHubInvocation Invocation { get; } - - /// - /// The signal (ConnectionId, hub type name or hub type name + "." + group name) belonging to clients that - /// receive the method invocation. - /// - string Signal { get; } - - /// - /// The signals (ConnectionId, hub type name or hub type name + "." + group name) belonging to clients that should - /// not receive the method invocation regardless of the . - /// - IList ExcludedSignals { get; } - } -} diff --git a/src/Microsoft.AspNet.SignalR.Core/Hubs/Pipeline/IHubPipeline.cs b/src/Microsoft.AspNet.SignalR.Core/Hubs/Pipeline/IHubPipeline.cs deleted file mode 100644 index d0f4c58eb..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/Hubs/Pipeline/IHubPipeline.cs +++ /dev/null @@ -1,29 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -namespace Microsoft.AspNet.SignalR.Hubs -{ - /// - /// A collection of modules that can intercept and customize various stages of hub processing such as connecting, - /// reconnecting, disconnecting, invoking server-side hub methods, invoking client-side hub methods, authorizing - /// hub clients and rejoining hub groups. - /// - public interface IHubPipeline - { - /// - /// Adds an to the hub pipeline. Modules added to the pipeline first will wrap - /// modules that are added to the pipeline later. All modules must be added to the pipeline before any methods - /// on the are invoked. - /// - /// - /// A module that may intercept and customize various stages of hub processing such as connecting, - /// reconnecting, disconnecting, invoking server-side hub methods, invoking client-side hub methods, authorizing - /// hub clients and rejoining hub groups. - /// - /// - /// The itself with the newly added module allowing - /// calls to be chained. - /// This method mutates the pipeline it is invoked on so it is not necessary to store its result. - /// - IHubPipeline AddModule(IHubPipelineModule pipelineModule); - } -} diff --git a/src/Microsoft.AspNet.SignalR.Core/Hubs/Pipeline/IHubPipelineInvoker.cs b/src/Microsoft.AspNet.SignalR.Core/Hubs/Pipeline/IHubPipelineInvoker.cs deleted file mode 100644 index 499e9c972..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/Hubs/Pipeline/IHubPipelineInvoker.cs +++ /dev/null @@ -1,76 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -using System.Collections.Generic; -using System.Threading.Tasks; - -namespace Microsoft.AspNet.SignalR.Hubs -{ - /// - /// Implementations of this interface are responsible for executing operation required to complete various stages - /// hub processing such as connecting, reconnecting, disconnecting, invoking server-side hub methods, invoking - /// client-side hub methods, authorizing hub clients and rejoining hub groups. - /// - public interface IHubPipelineInvoker - { - /// - /// Invokes a server-side hub method. - /// - /// A description of the server-side hub method invocation. - /// An asynchronous operation giving the return value of the server-side hub method invocation. - Task Invoke(IHubIncomingInvokerContext context); - - /// - /// Invokes a client-side hub method. - /// - /// A description of the client-side hub method invocation. - Task Send(IHubOutgoingInvokerContext context); - - /// - /// To be called when a client connects to the for each the client - /// connects to. By default, this results in the 's OnConnected method being invoked. - /// - /// A the client is connected to. - Task Connect(IHub hub); - - /// - /// To be called when a client reconnects to the for each the client - /// connects to. By default, this results in the 's OnReconnected method being invoked. - /// - /// A the client is reconnected to. - Task Reconnect(IHub hub); - - /// - /// To be called when a client disconnects from the for each the client - /// was connected to. By default, this results in the 's OnDisconnected method being invoked. - /// - /// A the client was disconnected from. - Task Disconnect(IHub hub); - - /// - /// To be called before a client subscribes to signals belonging to the hub described by the . - /// By default, the will look for attributes on the to help determine if - /// the client is authorized to subscribe to method invocations for the described hub. - /// - /// A description of the hub the client is attempting to connect to. - /// - /// The connect request being made by the client which should include the client's - /// User. - /// - /// true, if the client is authorized to subscribe to client-side hub method invocations; false, otherwise. - bool AuthorizeConnect(HubDescriptor hubDescriptor, IRequest request); - - /// - /// This method determines which of the groups belonging to the hub described by the the client should be - /// allowed to rejoin. - /// By default, clients that are reconnecting to the server will be removed from all groups they may have previously been a member of, - /// because untrusted clients may claim to be a member of groups they were never authorized to join. - /// - /// A description of the hub for which the client is attempting to rejoin groups. - /// The reconnect request being made by the client that is attempting to rejoin groups. - /// - /// The list of groups belonging to the relevant hub that the client claims to have been a member of before the reconnect. - /// - /// A list of groups the client is allowed to rejoin. - IList RejoiningGroups(HubDescriptor hubDescriptor, IRequest request, IList groups); - } -} diff --git a/src/Microsoft.AspNet.SignalR.Core/Hubs/Pipeline/IHubPipelineModule.cs b/src/Microsoft.AspNet.SignalR.Core/Hubs/Pipeline/IHubPipelineModule.cs deleted file mode 100644 index 79a01717c..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/Hubs/Pipeline/IHubPipelineModule.cs +++ /dev/null @@ -1,87 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -using System; -using System.Collections.Generic; -using System.Threading.Tasks; - -namespace Microsoft.AspNet.SignalR.Hubs -{ - /// - /// An can intercept and customize various stages of hub processing such as connecting, - /// reconnecting, disconnecting, invoking server-side hub methods, invoking client-side hub methods, authorizing hub - /// clients and rejoining hub groups. - /// Modules can be be activated by calling . - /// The combined modules added to the are invoked via the - /// interface. - /// - public interface IHubPipelineModule - { - /// - /// Wraps a function that invokes a server-side hub method. Even if a client has not been authorized to connect - /// to a hub, it will still be authorized to invoke server-side methods on that hub unless it is prevented in - /// by not executing the invoke parameter. - /// - /// A function that invokes a server-side hub method. - /// A wrapped function that invokes a server-side hub method. - Func> BuildIncoming(Func> invoke); - - /// - /// Wraps a function that invokes a client-side hub method. - /// - /// A function that invokes a client-side hub method. - /// A wrapped function that invokes a client-side hub method. - Func BuildOutgoing(Func send); - - /// - /// Wraps a function that is called when a client connects to the for each - /// the client connects to. By default, this results in the 's - /// OnConnected method being invoked. - /// - /// A function to be called when a client connects to a hub. - /// A wrapped function to be called when a client connects to a hub. - Func BuildConnect(Func connect); - - /// - /// Wraps a function that is called when a client reconnects to the for each - /// the client connects to. By default, this results in the 's - /// OnReconnected method being invoked. - /// - /// A function to be called when a client reconnects to a hub. - /// A wrapped function to be called when a client reconnects to a hub. - Func BuildReconnect(Func reconnect); - - /// - /// Wraps a function that is called when a client disconnects from the for each - /// the client was connected to. By default, this results in the 's - /// OnDisconnected method being invoked. - /// - /// A function to be called when a client disconnects from a hub. - /// A wrapped function to be called when a client disconnects from a hub. - Func BuildDisconnect(Func disconnect); - - /// - /// Wraps a function to be called before a client subscribes to signals belonging to the hub described by the - /// . By default, the will look for attributes on the - /// to help determine if the client is authorized to subscribe to method invocations for the - /// described hub. - /// The function returns true if the client is authorized to subscribe to client-side hub method - /// invocations; false, otherwise. - /// - /// - /// A function that dictates whether or not the client is authorized to connect to the described Hub. - /// - /// - /// A wrapped function that dictates whether or not the client is authorized to connect to the described Hub. - /// - Func BuildAuthorizeConnect(Func authorizeConnect); - - /// - /// Wraps a function that determines which of the groups belonging to the hub described by the - /// the client should be allowed to rejoin. - /// By default, clients will rejoin all the groups they were in prior to reconnecting. - /// - /// A function that determines which groups the client should be allowed to rejoin. - /// A wrapped function that determines which groups the client should be allowed to rejoin. - Func, IList> BuildRejoiningGroups(Func, IList> rejoiningGroups); - } -} diff --git a/src/Microsoft.AspNet.SignalR.Core/Hubs/ReflectionHelper.cs b/src/Microsoft.AspNet.SignalR.Core/Hubs/ReflectionHelper.cs deleted file mode 100644 index 6819ed97a..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/Hubs/ReflectionHelper.cs +++ /dev/null @@ -1,69 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; - -namespace Microsoft.AspNet.SignalR.Hubs -{ - public static class ReflectionHelper - { - private static readonly Type[] _excludeTypes = new[] { typeof(Hub), typeof(object) }; - private static readonly Type[] _excludeInterfaces = new[] { typeof(IHub), typeof(IDisposable) }; - - public static IEnumerable GetExportedHubMethods(Type type) - { - if (!typeof(IHub).IsAssignableFrom(type)) - { - return Enumerable.Empty(); - } - - var methods = type.GetMethods(BindingFlags.Public | BindingFlags.Instance); - var allInterfaceMethods = _excludeInterfaces.SelectMany(i => GetInterfaceMethods(type, i)); - - return methods.Except(allInterfaceMethods).Where(IsValidHubMethod); - - } - - private static bool IsValidHubMethod(MethodInfo methodInfo) - { - return !(_excludeTypes.Contains(methodInfo.GetBaseDefinition().DeclaringType) || - methodInfo.IsSpecialName); - } - - private static IEnumerable GetInterfaceMethods(Type type, Type iface) - { - if (!iface.IsAssignableFrom(type)) - { - return Enumerable.Empty(); - } - - return type.GetInterfaceMap(iface).TargetMethods; - } - - public static TResult GetAttributeValue(ICustomAttributeProvider source, Func valueGetter) - where TAttribute : Attribute - { - if (source == null) - { - throw new ArgumentNullException("source"); - } - - if (valueGetter == null) - { - throw new ArgumentNullException("valueGetter"); - } - - var attributes = source.GetCustomAttributes(typeof(TAttribute), false) - .Cast() - .ToList(); - if (attributes.Any()) - { - return valueGetter(attributes[0]); - } - return default(TResult); - } - - } -} diff --git a/src/Microsoft.AspNet.SignalR.Core/Hubs/SignalProxy.cs b/src/Microsoft.AspNet.SignalR.Core/Hubs/SignalProxy.cs deleted file mode 100644 index 1a54997ef..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/Hubs/SignalProxy.cs +++ /dev/null @@ -1,62 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Dynamic; -using System.Threading.Tasks; - -namespace Microsoft.AspNet.SignalR.Hubs -{ - public abstract class SignalProxy : DynamicObject, IClientProxy - { - private readonly IList _exclude; - private readonly string _prefix; - - protected SignalProxy(Func, Task> send, string signal, string hubName, string prefix, IList exclude) - { - Send = send; - Signal = signal; - HubName = hubName; - _prefix = prefix; - _exclude = exclude; - } - - protected Func, Task> Send { get; private set; } - protected string Signal { get; private set; } - protected string HubName { get; private set; } - - public override bool TryGetMember(GetMemberBinder binder, out object result) - { - result = null; - return false; - } - - [SuppressMessage("Microsoft.Design", "CA1062:Validate arguments of public methods", MessageId = "0", Justification = "The compiler generates calls to invoke this")] - public override bool TryInvokeMember(InvokeMemberBinder binder, object[] args, out object result) - { - result = Invoke(binder.Name, args); - return true; - } - - public Task Invoke(string method, params object[] args) - { - var invocation = GetInvocationData(method, args); - - string signal = _prefix + HubName + "." + Signal; - - return Send(signal, invocation, _exclude); - } - - protected virtual ClientHubInvocation GetInvocationData(string method, object[] args) - { - return new ClientHubInvocation - { - Hub = HubName, - Method = method, - Args = args, - Target = Signal - }; - } - } -} diff --git a/src/Microsoft.AspNet.SignalR.Core/Hubs/StateChangeTracker.cs b/src/Microsoft.AspNet.SignalR.Core/Hubs/StateChangeTracker.cs deleted file mode 100644 index 5c7075e31..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/Hubs/StateChangeTracker.cs +++ /dev/null @@ -1,66 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Linq; - -namespace Microsoft.AspNet.SignalR.Hubs -{ - /// - /// A change tracking dictionary. - /// - public class StateChangeTracker - { - private readonly IDictionary _values; - // Keep track of everyting that changed since creation - private readonly IDictionary _oldValues = new Dictionary(StringComparer.OrdinalIgnoreCase); - - public StateChangeTracker() - { - _values = new Dictionary(StringComparer.OrdinalIgnoreCase); - } - - public StateChangeTracker(IDictionary values) - { - _values = values; - } - - public object this[string key] - { - get - { - object result; - _values.TryGetValue(key, out result); - return DynamicDictionary.Wrap(result); - } - set - { - if (!_oldValues.ContainsKey(key)) - { - object oldValue; - _values.TryGetValue(key, out oldValue); - _oldValues[key] = oldValue; - } - - _values[key] = value; - } - } - - [SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate", Justification = "This might be expensive")] - public IDictionary GetChanges() - { - var changes = (from key in _oldValues.Keys - let oldValue = _oldValues[key] - let newValue = _values[key] - where !Object.Equals(oldValue, newValue) - select new - { - Key = key, - Value = newValue - }).ToDictionary(p => p.Key, p => p.Value); - - return changes.Count > 0 ? changes : null; - } - } -} diff --git a/src/Microsoft.AspNet.SignalR.Core/Hubs/StatefulSignalProxy.cs b/src/Microsoft.AspNet.SignalR.Core/Hubs/StatefulSignalProxy.cs deleted file mode 100644 index 8fbac0f32..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/Hubs/StatefulSignalProxy.cs +++ /dev/null @@ -1,48 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Dynamic; -using System.Threading.Tasks; -using Microsoft.AspNet.SignalR.Infrastructure; - -namespace Microsoft.AspNet.SignalR.Hubs -{ - public class StatefulSignalProxy : SignalProxy - { - private readonly StateChangeTracker _tracker; - - public StatefulSignalProxy(Func, Task> send, string signal, string hubName, string prefix, StateChangeTracker tracker) - : base(send, signal, prefix, hubName, ListHelper.Empty) - { - _tracker = tracker; - } - - [SuppressMessage("Microsoft.Design", "CA1062:Validate arguments of public methods", MessageId = "0", Justification = "The compiler generates calls to invoke this")] - public override bool TrySetMember(SetMemberBinder binder, object value) - { - _tracker[binder.Name] = value; - return true; - } - - [SuppressMessage("Microsoft.Design", "CA1062:Validate arguments of public methods", MessageId = "0", Justification = "The compiler generates calls to invoke this")] - public override bool TryGetMember(GetMemberBinder binder, out object result) - { - result = _tracker[binder.Name]; - return true; - } - - protected override ClientHubInvocation GetInvocationData(string method, object[] args) - { - return new ClientHubInvocation - { - Hub = HubName, - Method = method, - Args = args, - Target = Signal, - State = _tracker.GetChanges() - }; - } - } -} diff --git a/src/Microsoft.AspNet.SignalR.Core/IConnection.cs b/src/Microsoft.AspNet.SignalR.Core/IConnection.cs deleted file mode 100644 index a6283c259..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/IConnection.cs +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -using System.Threading.Tasks; - -namespace Microsoft.AspNet.SignalR -{ - /// - /// A communication channel for a and its connections. - /// - public interface IConnection - { - /// - /// The main signal for this connection. This is the main signalr for a . - /// - string DefaultSignal { get; } - - /// - /// Sends a message to connections subscribed to the signal. - /// - /// The message to send. - /// A task that returns when the message has be sent. - Task Send(ConnectionMessage message); - } -} diff --git a/src/Microsoft.AspNet.SignalR.Core/IConnectionGroupManager.cs b/src/Microsoft.AspNet.SignalR.Core/IConnectionGroupManager.cs deleted file mode 100644 index 93688901f..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/IConnectionGroupManager.cs +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -using System.Threading.Tasks; - -namespace Microsoft.AspNet.SignalR -{ - /// - /// Manages groups for a connection and allows sending messages to the group. - /// - public interface IConnectionGroupManager : IGroupManager - { - /// - /// Sends a value to the specified group. - /// - /// The name of the group. - /// The value to send. - /// The list of connection ids to exclude - /// A task that represents when send is complete. - Task Send(string groupName, object value, params string[] excludeConnectionIds); - } -} diff --git a/src/Microsoft.AspNet.SignalR.Core/IDependencyResolver.cs b/src/Microsoft.AspNet.SignalR.Core/IDependencyResolver.cs deleted file mode 100644 index a897d2139..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/IDependencyResolver.cs +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -using System; -using System.Collections.Generic; - -namespace Microsoft.AspNet.SignalR -{ - public interface IDependencyResolver : IDisposable - { - object GetService(Type serviceType); - IEnumerable GetServices(Type serviceType); - void Register(Type serviceType, Func activator); - void Register(Type serviceType, IEnumerable> activators); - } -} diff --git a/src/Microsoft.AspNet.SignalR.Core/IGroupManager.cs b/src/Microsoft.AspNet.SignalR.Core/IGroupManager.cs deleted file mode 100644 index 3b23e1bb6..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/IGroupManager.cs +++ /dev/null @@ -1,28 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -using System.Threading.Tasks; - -namespace Microsoft.AspNet.SignalR -{ - /// - /// Manages groups for a connection. - /// - public interface IGroupManager - { - /// - /// Adds a connection to the specified group. - /// - /// The connection id to add to the group. - /// The name of the group - /// A task that represents the connection id being added to the group. - Task Add(string connectionId, string groupName); - - /// - /// Removes a connection from the specified group. - /// - /// The connection id to remove from the group. - /// The name of the group - /// A task that represents the connection id being removed from the group. - Task Remove(string connectionId, string groupName); - } -} diff --git a/src/Microsoft.AspNet.SignalR.Core/IHubContext.cs b/src/Microsoft.AspNet.SignalR.Core/IHubContext.cs deleted file mode 100644 index db33cc64d..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/IHubContext.cs +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -using Microsoft.AspNet.SignalR.Hubs; - -namespace Microsoft.AspNet.SignalR -{ - /// - /// Provides access to information about a . - /// - public interface IHubContext - { - /// - /// Encapsulates all information about a SignalR connection for an . - /// - IHubConnectionContext Clients { get; } - - /// - /// Gets the the hub. - /// - IGroupManager Groups { get; } - } -} diff --git a/src/Microsoft.AspNet.SignalR.Core/IPersistentConnectionContext.cs b/src/Microsoft.AspNet.SignalR.Core/IPersistentConnectionContext.cs deleted file mode 100644 index da291fa98..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/IPersistentConnectionContext.cs +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -namespace Microsoft.AspNet.SignalR -{ - /// - /// Provides access to information about a . - /// - public interface IPersistentConnectionContext - { - /// - /// Gets the for the . - /// - IConnection Connection { get; } - - /// - /// Gets the for the . - /// - IConnectionGroupManager Groups { get; } - } -} diff --git a/src/Microsoft.AspNet.SignalR.Core/IRequest.cs b/src/Microsoft.AspNet.SignalR.Core/IRequest.cs deleted file mode 100644 index da5859f51..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/IRequest.cs +++ /dev/null @@ -1,50 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -using System; -using System.Collections.Generic; -using System.Collections.Specialized; -using System.Security.Principal; - -namespace Microsoft.AspNet.SignalR -{ - /// - /// Represents a SignalR request - /// - public interface IRequest - { - /// - /// Gets the url for this request. - /// - Uri Url { get; } - - /// - /// Gets the querystring for this request. - /// - NameValueCollection QueryString { get; } - - /// - /// Gets the headers for this request. - /// - NameValueCollection Headers { get; } - - /// - /// Gets the form for this request. - /// - NameValueCollection Form { get; } - - /// - /// Gets the cookies for this request. - /// - IDictionary Cookies { get; } - - /// - /// Gets security information for the current HTTP request. - /// - IPrincipal User { get; } - - /// - /// Gets state for the current HTTP request. - /// - IDictionary Items { get; } - } -} diff --git a/src/Microsoft.AspNet.SignalR.Core/Infrastructure/AckHandler.cs b/src/Microsoft.AspNet.SignalR.Core/Infrastructure/AckHandler.cs deleted file mode 100644 index 18479ec4c..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/Infrastructure/AckHandler.cs +++ /dev/null @@ -1,112 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -using System; -using System.Collections.Concurrent; -using System.Diagnostics.CodeAnalysis; -using System.Threading; -using System.Threading.Tasks; - -namespace Microsoft.AspNet.SignalR.Infrastructure -{ - public class AckHandler : IAckHandler, IDisposable - { - private readonly ConcurrentDictionary _acks = new ConcurrentDictionary(); - - // REVIEW: Consider making this pluggable - private readonly TimeSpan _ackThreshold; - - // REVIEW: Consider moving this logic to the transport heartbeat - private Timer _timer; - - public AckHandler() - : this(completeAcksOnTimeout: true, - ackThreshold: TimeSpan.FromSeconds(30), - ackInterval: TimeSpan.FromSeconds(5)) - { - } - - [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Acks", Justification = "Ack is a well known term")] - public AckHandler(bool completeAcksOnTimeout, TimeSpan ackThreshold, TimeSpan ackInterval) - { - if (completeAcksOnTimeout) - { - _timer = new Timer(_ => CheckAcks(), state: null, dueTime: ackInterval, period: ackInterval); - } - - _ackThreshold = ackThreshold; - } - - public Task CreateAck(string id) - { - return _acks.GetOrAdd(id, _ => new AckInfo()).Tcs.Task; - } - - public bool TriggerAck(string id) - { - AckInfo info; - if (_acks.TryRemove(id, out info)) - { - info.Tcs.TrySetResult(null); - return true; - } - - return false; - } - - private void CheckAcks() - { - foreach (var pair in _acks) - { - TimeSpan elapsed = DateTime.UtcNow - pair.Value.Created; - if (elapsed > _ackThreshold) - { - AckInfo info; - if (_acks.TryRemove(pair.Key, out info)) - { - // If we have a pending ack for longer than the threshold - // cancel it. - info.Tcs.TrySetCanceled(); - } - } - } - } - - protected virtual void Dispose(bool disposing) - { - if (disposing) - { - if (_timer != null) - { - _timer.Dispose(); - } - - // Trip all pending acks - foreach (var pair in _acks) - { - AckInfo info; - if (_acks.TryRemove(pair.Key, out info)) - { - info.Tcs.TrySetCanceled(); - } - } - } - } - - public void Dispose() - { - Dispose(true); - } - - private class AckInfo - { - public TaskCompletionSource Tcs { get; private set; } - public DateTime Created { get; private set; } - - public AckInfo() - { - Tcs = new TaskCompletionSource(); - Created = DateTime.UtcNow; - } - } - } -} diff --git a/src/Microsoft.AspNet.SignalR.Core/Infrastructure/ArraySegmentTextReader.cs b/src/Microsoft.AspNet.SignalR.Core/Infrastructure/ArraySegmentTextReader.cs deleted file mode 100644 index 08dfb1f97..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/Infrastructure/ArraySegmentTextReader.cs +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -using System; -using System.IO; -using System.Text; - -namespace Microsoft.AspNet.SignalR.Infrastructure -{ - public class ArraySegmentTextReader : TextReader - { - private readonly ArraySegment _buffer; - private readonly Encoding _encoding; - private int _offset; - - public ArraySegmentTextReader(ArraySegment buffer, Encoding encoding) - { - _buffer = buffer; - _encoding = encoding; - _offset = _buffer.Offset; - } - - public override int Read(char[] buffer, int index, int count) - { - int bytesCount = _encoding.GetByteCount(buffer, index, count); - int bytesToRead = Math.Min(_buffer.Count - _offset, bytesCount); - - int read = _encoding.GetChars(_buffer.Array, _offset, bytesToRead, buffer, index); - _offset += bytesToRead; - - return read; - } - } -} diff --git a/src/Microsoft.AspNet.SignalR.Core/Infrastructure/BinaryTextWriter.cs b/src/Microsoft.AspNet.SignalR.Core/Infrastructure/BinaryTextWriter.cs deleted file mode 100644 index a5f958b47..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/Infrastructure/BinaryTextWriter.cs +++ /dev/null @@ -1,34 +0,0 @@ -using System; -using Microsoft.AspNet.SignalR.Hosting; - -namespace Microsoft.AspNet.SignalR.Infrastructure -{ - /// - /// A buffering text writer that supports writing binary directly as well - /// - internal unsafe class BinaryTextWriter : BufferTextWriter, IBinaryWriter - { - public BinaryTextWriter(IResponse response) : - base((data, state) => ((IResponse)state).Write(data), response, reuseBuffers: true, bufferSize: 128) - { - - } - - public BinaryTextWriter(IWebSocket socket) : - base((data, state) => ((IWebSocket)state).SendChunk(data), socket, reuseBuffers: false, bufferSize: 1024) - { - - } - - - public BinaryTextWriter(Action, object> write, object state, bool reuseBuffers, int bufferSize) : - base(write, state, reuseBuffers, bufferSize) - { - } - - public void Write(ArraySegment data) - { - Writer.Write(data); - } - } -} diff --git a/src/Microsoft.AspNet.SignalR.Core/Infrastructure/BufferTextWriter.cs b/src/Microsoft.AspNet.SignalR.Core/Infrastructure/BufferTextWriter.cs deleted file mode 100644 index 6334f3533..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/Infrastructure/BufferTextWriter.cs +++ /dev/null @@ -1,187 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -using System; -using System.Diagnostics.CodeAnalysis; -using System.IO; -using System.Text; -using Microsoft.AspNet.SignalR.Hosting; - -namespace Microsoft.AspNet.SignalR.Infrastructure -{ - /// - /// TextWriter implementation over a write delegate optimized for writing in small chunks - /// we don't need to write to a long lived buffer. This saves massive amounts of memory - /// as the number of connections grows. - /// - internal abstract unsafe class BufferTextWriter : TextWriter - { - private readonly Encoding _encoding; - - private readonly Action, object> _write; - private readonly object _writeState; - private readonly bool _reuseBuffers; - - private ChunkedWriter _writer; - private int _bufferSize; - - public BufferTextWriter(IResponse response) : - this((data, state) => ((IResponse)state).Write(data), response, reuseBuffers: true, bufferSize: 128) - { - - } - - public BufferTextWriter(IWebSocket socket) : - this((data, state) => ((IWebSocket)state).SendChunk(data), socket, reuseBuffers: false, bufferSize: 1024 * 4) - { - - } - - [SuppressMessage("Microsoft.Globalization", "CA1305:SpecifyIFormatProvider", MessageId = "System.IO.TextWriter.#ctor", Justification = "It won't be used")] - protected BufferTextWriter(Action, object> write, object state, bool reuseBuffers, int bufferSize) - { - _write = write; - _writeState = state; - _encoding = new UTF8Encoding(); - _reuseBuffers = reuseBuffers; - _bufferSize = bufferSize; - } - - protected internal ChunkedWriter Writer - { - get - { - if (_writer == null) - { - _writer = new ChunkedWriter(_write, _writeState, _bufferSize, _encoding, _reuseBuffers); - } - - return _writer; - } - } - - public override Encoding Encoding - { - get { return _encoding; } - } - - public override void Write(string value) - { - Writer.Write(value); - } - - public override void WriteLine(string value) - { - Writer.Write(value); - } - - public override void Write(char value) - { - Writer.Write(value); - } - - public override void Flush() - { - Writer.Flush(); - } - - internal class ChunkedWriter - { - private int _charPos; - private int _charLen; - - private readonly Encoder _encoder; - private readonly char[] _charBuffer; - private readonly byte[] _byteBuffer; - private readonly Action, object> _write; - private readonly object _writeState; - - public ChunkedWriter(Action, object> write, object state, int chunkSize, Encoding encoding, bool reuseBuffers) - { - _charLen = chunkSize; - _charBuffer = new char[chunkSize]; - _write = write; - _writeState = state; - _encoder = encoding.GetEncoder(); - - if (reuseBuffers) - { - _byteBuffer = new byte[encoding.GetMaxByteCount(chunkSize)]; - } - } - - public void Write(char value) - { - if (_charPos == _charLen) - { - Flush(flushEncoder: false); - } - - _charBuffer[_charPos++] = value; - } - - public void Write(string value) - { - int length = value.Length; - int sourceIndex = 0; - - while (length > 0) - { - if (_charPos == _charLen) - { - Flush(flushEncoder: false); - } - - int count = _charLen - _charPos; - if (count > length) - { - count = length; - } - - value.CopyTo(sourceIndex, _charBuffer, _charPos, count); - _charPos += count; - sourceIndex += count; - length -= count; - } - } - - public void Write(ArraySegment data) - { - Flush(); - _write(data, _writeState); - } - - public void Flush() - { - Flush(flushEncoder: true); - } - - private void Flush(bool flushEncoder) - { - // If it's safe to reuse the buffer then do so - if (_byteBuffer != null) - { - Flush(_byteBuffer, flushEncoder); - } - else - { - // Allocate a byte array of the right size for this char buffer - int byteCount = _encoder.GetByteCount(_charBuffer, 0, _charPos, flush: false); - var byteBuffer = new byte[byteCount]; - Flush(byteBuffer, flushEncoder); - } - } - - private void Flush(byte[] byteBuffer, bool flushEncoder) - { - int count = _encoder.GetBytes(_charBuffer, 0, _charPos, byteBuffer, 0, flush: flushEncoder); - - _charPos = 0; - - if (count > 0) - { - _write(new ArraySegment(byteBuffer, 0, count), _writeState); - } - } - } - } -} diff --git a/src/Microsoft.AspNet.SignalR.Core/Infrastructure/CancellationTokenExtensions.cs b/src/Microsoft.AspNet.SignalR.Core/Infrastructure/CancellationTokenExtensions.cs deleted file mode 100644 index dff2ecc46..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/Infrastructure/CancellationTokenExtensions.cs +++ /dev/null @@ -1,89 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -using System; -using System.Threading; - -namespace Microsoft.AspNet.SignalR.Infrastructure -{ - internal static class CancellationTokenExtensions - { - public static IDisposable SafeRegister(this CancellationToken cancellationToken, Action callback, object state) - { - var callbackWrapper = new CancellationCallbackWrapper(callback, state); - - // Ensure delegate continues to use the C# Compiler static delegate caching optimization. - CancellationTokenRegistration registration = cancellationToken.Register(s => Cancel(s), - callbackWrapper, - useSynchronizationContext: false); - - var disposeCancellationState = new DiposeCancellationState(callbackWrapper, registration); - - // Ensure delegate continues to use the C# Compiler static delegate caching optimization. - return new DisposableAction(s => Dispose(s), disposeCancellationState); - } - - private static void Cancel(object state) - { - ((CancellationCallbackWrapper)state).TryInvoke(); - } - - private static void Dispose(object state) - { - ((DiposeCancellationState)state).TryDispose(); - } - - private class DiposeCancellationState - { - private readonly CancellationCallbackWrapper _callbackWrapper; - private readonly CancellationTokenRegistration _registration; - - public DiposeCancellationState(CancellationCallbackWrapper callbackWrapper, CancellationTokenRegistration registration) - { - _callbackWrapper = callbackWrapper; - _registration = registration; - } - - public void TryDispose() - { - // This normally waits until the callback is finished invoked but we don't care - if (_callbackWrapper.TrySetInvoked()) - { - try - { - _registration.Dispose(); - } - catch (ObjectDisposedException) - { - // Bug #1549, .NET 4.0 has a bug where this throws if the CTS is disposed. - } - } - } - } - - private class CancellationCallbackWrapper - { - private readonly Action _callback; - private readonly object _state; - private int _callbackInvoked; - - public CancellationCallbackWrapper(Action callback, object state) - { - _callback = callback; - _state = state; - } - - public bool TrySetInvoked() - { - return Interlocked.Exchange(ref _callbackInvoked, 1) == 0; - } - - public void TryInvoke() - { - if (TrySetInvoked()) - { - _callback(_state); - } - } - } - } -} diff --git a/src/Microsoft.AspNet.SignalR.Core/Infrastructure/Connection.cs b/src/Microsoft.AspNet.SignalR.Core/Infrastructure/Connection.cs deleted file mode 100644 index a938c87f0..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/Infrastructure/Connection.cs +++ /dev/null @@ -1,345 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; -using System.IO; -using System.Linq; -using System.Runtime.Serialization; -using System.Threading.Tasks; -using Microsoft.AspNet.SignalR.Json; -using Microsoft.AspNet.SignalR.Messaging; -using Microsoft.AspNet.SignalR.Tracing; -using Microsoft.AspNet.SignalR.Transports; - -namespace Microsoft.AspNet.SignalR.Infrastructure -{ - public class Connection : IConnection, ITransportConnection, ISubscriber - { - private readonly IMessageBus _bus; - private readonly IJsonSerializer _serializer; - private readonly string _baseSignal; - private readonly string _connectionId; - private readonly IList _signals; - private readonly DiffSet _groups; - private readonly IPerformanceCounterManager _counters; - - private bool _disconnected; - private bool _aborted; - private readonly TraceSource _traceSource; - private readonly IAckHandler _ackHandler; - private readonly IProtectedData _protectedData; - - public Connection(IMessageBus newMessageBus, - IJsonSerializer jsonSerializer, - string baseSignal, - string connectionId, - IList signals, - IList groups, - ITraceManager traceManager, - IAckHandler ackHandler, - IPerformanceCounterManager performanceCounterManager, - IProtectedData protectedData) - { - if (traceManager == null) - { - throw new ArgumentNullException("traceManager"); - } - - _bus = newMessageBus; - _serializer = jsonSerializer; - _baseSignal = baseSignal; - _connectionId = connectionId; - _signals = new List(signals.Concat(groups)); - _groups = new DiffSet(groups); - _traceSource = traceManager["SignalR.Connection"]; - _ackHandler = ackHandler; - _counters = performanceCounterManager; - _protectedData = protectedData; - } - - public string DefaultSignal - { - get - { - return _baseSignal; - } - } - - IList ISubscriber.EventKeys - { - get - { - return _signals; - } - } - - public event Action EventKeyAdded; - - public event Action EventKeyRemoved; - - public Action WriteCursor { get; set; } - - public string Identity - { - get - { - return _connectionId; - } - } - - [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "Used for debugging purposes.")] - private TraceSource Trace - { - get - { - return _traceSource; - } - } - - public Subscription Subscription - { - get; - set; - } - - public Task Send(ConnectionMessage message) - { - Message busMessage = CreateMessage(message.Signal, message.Value); - - if (message.ExcludedSignals != null) - { - busMessage.Filter = String.Join("|", message.ExcludedSignals); - } - - if (busMessage.WaitForAck) - { - Task ackTask = _ackHandler.CreateAck(busMessage.CommandId); - return _bus.Publish(busMessage).Then(task => task, ackTask); - } - - return _bus.Publish(busMessage); - } - - private Message CreateMessage(string key, object value) - { - var command = value as Command; - - ArraySegment messageBuffer = GetMessageBuffer(value); - - var message = new Message(_connectionId, key, messageBuffer); - - if (command != null) - { - // Set the command id - message.CommandId = command.Id; - message.WaitForAck = command.WaitForAck; - } - - return message; - } - - private ArraySegment GetMessageBuffer(object value) - { - using (var stream = new MemoryStream(128)) - { - var bufferWriter = new BinaryTextWriter((buffer, state) => - { - ((MemoryStream)state).Write(buffer.Array, buffer.Offset, buffer.Count); - }, - stream, - reuseBuffers: true, - bufferSize: 1024); - - using (bufferWriter) - { - _serializer.Serialize(value, bufferWriter); - bufferWriter.Flush(); - - return new ArraySegment(stream.ToArray()); - } - } - } - - public IDisposable Receive(string messageId, Func> callback, int maxMessages, object state) - { - var receiveContext = new ReceiveContext(this, callback, state); - - return _bus.Subscribe(this, - messageId, - (result, s) => MessageBusCallback(result, s), - maxMessages, - receiveContext); - } - - private static Task MessageBusCallback(MessageResult result, object state) - { - var context = (ReceiveContext)state; - - return context.InvokeCallback(result); - } - - private PersistentResponse GetResponse(MessageResult result) - { - // Do a single sweep through the results to process commands and extract values - ProcessResults(result); - - Debug.Assert(WriteCursor != null, "Unable to resolve the cursor since the method is null"); - - var response = new PersistentResponse(ExcludeMessage, WriteCursor); - response.Terminal = result.Terminal; - - if (!result.Terminal) - { - // Only set these properties if the message isn't terminal - response.Messages = result.Messages; - response.Disconnect = _disconnected; - response.Aborted = _aborted; - response.TotalCount = result.TotalCount; - } - - PopulateResponseState(response); - - _counters.ConnectionMessagesReceivedTotal.IncrementBy(result.TotalCount); - _counters.ConnectionMessagesReceivedPerSec.IncrementBy(result.TotalCount); - - return response; - } - - private bool ExcludeMessage(Message message) - { - if (String.IsNullOrEmpty(message.Filter)) - { - return false; - } - - string[] exclude = message.Filter.Split('|'); - - return exclude.Any(signal => Identity.Equals(signal, StringComparison.OrdinalIgnoreCase) || - _signals.Contains(signal) || - _groups.Contains(signal)); - } - - private void ProcessResults(MessageResult result) - { - result.Messages.Enumerate(message => message.IsAck || message.IsCommand, - (state, message) => - { - if (message.IsAck) - { - _ackHandler.TriggerAck(message.CommandId); - } - else if (message.IsCommand) - { - var command = _serializer.Parse(message.Value, message.Encoding); - - if (command == null) - { - if (MonoUtility.IsRunningMono) - { - return; - } - - throw new SerializationException("Couldn't parse message " + message.Value); - } - - ProcessCommand(command); - - // Only send the ack if this command is waiting for it - if (message.WaitForAck) - { - // If we're on the same box and there's a pending ack for this command then - // just trip it - if (!_ackHandler.TriggerAck(message.CommandId)) - { - _bus.Ack(_connectionId, message.CommandId).Catch(); - } - } - } - }, null); - } - - private void ProcessCommand(Command command) - { - switch (command.CommandType) - { - case CommandType.AddToGroup: - { - var name = command.Value; - - if (EventKeyAdded != null) - { - _groups.Add(name); - EventKeyAdded(this, name); - } - } - break; - case CommandType.RemoveFromGroup: - { - var name = command.Value; - - if (EventKeyRemoved != null) - { - _groups.Remove(name); - EventKeyRemoved(this, name); - } - } - break; - case CommandType.Disconnect: - _disconnected = true; - break; - case CommandType.Abort: - _aborted = true; - break; - } - } - - private void PopulateResponseState(PersistentResponse response) - { - PopulateResponseState(response, _groups, _serializer, _protectedData, _connectionId); - } - - internal static void PopulateResponseState(PersistentResponse response, - DiffSet groupSet, - IJsonSerializer serializer, - IProtectedData protectedData, - string connectionId) - { - bool anyChanges = groupSet.DetectChanges(); - - if (anyChanges) - { - // Create a protected payload of the sorted list - IEnumerable groups = groupSet.GetSnapshot(); - - // Remove group prefixes before any thing goes over the wire - string groupsString = connectionId + ':' + serializer.Stringify(PrefixHelper.RemoveGroupPrefixes(groups)); ; - - // The groups token - response.GroupsToken = protectedData.Protect(groupsString, Purposes.Groups); - } - } - - private class ReceiveContext - { - private readonly Connection _connection; - private readonly Func> _callback; - private readonly object _callbackState; - - public ReceiveContext(Connection connection, Func> callback, object callbackState) - { - _connection = connection; - _callback = callback; - _callbackState = callbackState; - } - - public Task InvokeCallback(MessageResult result) - { - var response = _connection.GetResponse(result); - - return _callback(response, _callbackState); - } - } - } -} diff --git a/src/Microsoft.AspNet.SignalR.Core/Infrastructure/ConnectionManager.cs b/src/Microsoft.AspNet.SignalR.Core/Infrastructure/ConnectionManager.cs deleted file mode 100644 index 9f74ff10e..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/Infrastructure/ConnectionManager.cs +++ /dev/null @@ -1,110 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using Microsoft.AspNet.SignalR.Hubs; -using Microsoft.AspNet.SignalR.Json; -using Microsoft.AspNet.SignalR.Messaging; -using Microsoft.AspNet.SignalR.Tracing; - -namespace Microsoft.AspNet.SignalR.Infrastructure -{ - /// - /// Default implementation. - /// - public class ConnectionManager : IConnectionManager - { - private readonly IDependencyResolver _resolver; - private readonly IPerformanceCounterManager _counters; - - /// - /// Initializes a new instance of the class. - /// - /// The . - public ConnectionManager(IDependencyResolver resolver) - { - _resolver = resolver; - _counters = _resolver.Resolve(); - } - - /// - /// Returns a for the . - /// - /// Type of the - /// A for the . - public IPersistentConnectionContext GetConnectionContext() where T : PersistentConnection - { - return GetConnection(typeof(T)); - } - - /// - /// Returns a for the . - /// - /// Type of the - /// A for the . - public IPersistentConnectionContext GetConnection(Type type) - { - if (type == null) - { - throw new ArgumentNullException("type"); - } - - string rawConnectionName = type.FullName; - string connectionName = PrefixHelper.GetPersistentConnectionName(rawConnectionName); - IConnection connection = GetConnectionCore(connectionName); - - return new PersistentConnectionContext(connection, new GroupManager(connection, PrefixHelper.GetPersistentConnectionGroupName(rawConnectionName))); - } - - /// - /// Returns a for the specified . - /// - /// Type of the - /// a for the specified - public IHubContext GetHubContext() where T : IHub - { - return GetHubContext(typeof(T).GetHubName()); - } - - /// - /// Returns a for the specified hub. - /// - /// Name of the hub - /// a for the specified hub - public IHubContext GetHubContext(string hubName) - { - var connection = GetConnectionCore(connectionName: null); - var hubManager = _resolver.Resolve(); - var pipelineInvoker = _resolver.Resolve(); - - hubManager.EnsureHub(hubName, - _counters.ErrorsHubResolutionTotal, - _counters.ErrorsHubResolutionPerSec, - _counters.ErrorsAllTotal, - _counters.ErrorsAllPerSec); - - Func, Task> send = (signal, value, exclude) => pipelineInvoker.Send(new HubOutgoingInvokerContext(connection, signal, value, exclude)); - - return new HubContext(send, hubName, connection); - } - - internal Connection GetConnectionCore(string connectionName) - { - IList signals = connectionName == null ? ListHelper.Empty : new[] { connectionName }; - - // Give this a unique id - var connectionId = Guid.NewGuid().ToString(); - return new Connection(_resolver.Resolve(), - _resolver.Resolve(), - connectionName, - connectionId, - signals, - ListHelper.Empty, - _resolver.Resolve(), - _resolver.Resolve(), - _resolver.Resolve(), - _resolver.Resolve()); - } - } -} diff --git a/src/Microsoft.AspNet.SignalR.Core/Infrastructure/DefaultProtectedData.cs b/src/Microsoft.AspNet.SignalR.Core/Infrastructure/DefaultProtectedData.cs deleted file mode 100644 index 686f33abf..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/Infrastructure/DefaultProtectedData.cs +++ /dev/null @@ -1,35 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -using System; -using System.Security.Cryptography; -using System.Text; - -namespace Microsoft.AspNet.SignalR.Infrastructure -{ - public class DefaultProtectedData : IProtectedData - { - private static readonly UTF8Encoding _encoding = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true); - - public string Protect(string data, string purpose) - { - byte[] purposeBytes = _encoding.GetBytes(purpose); - - byte[] unprotectedBytes = _encoding.GetBytes(data); - - byte[] protectedBytes = ProtectedData.Protect(unprotectedBytes, purposeBytes, DataProtectionScope.CurrentUser); - - return Convert.ToBase64String(protectedBytes); - } - - public string Unprotect(string protectedValue, string purpose) - { - byte[] purposeBytes = _encoding.GetBytes(purpose); - - byte[] protectedBytes = Convert.FromBase64String(protectedValue); - - byte[] unprotectedBytes = ProtectedData.Unprotect(protectedBytes, purposeBytes, DataProtectionScope.CurrentUser); - - return _encoding.GetString(unprotectedBytes); - } - } -} diff --git a/src/Microsoft.AspNet.SignalR.Core/Infrastructure/DiffPair.cs b/src/Microsoft.AspNet.SignalR.Core/Infrastructure/DiffPair.cs deleted file mode 100644 index f7a92b81e..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/Infrastructure/DiffPair.cs +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -using System.Collections.Generic; - -namespace Microsoft.AspNet.SignalR.Infrastructure -{ - internal struct DiffPair - { - public ICollection Added; - public ICollection Removed; - - public bool AnyChanges - { - get - { - return Added.Count > 0 || Removed.Count > 0; - } - } - } -} diff --git a/src/Microsoft.AspNet.SignalR.Core/Infrastructure/DiffSet.cs b/src/Microsoft.AspNet.SignalR.Core/Infrastructure/DiffSet.cs deleted file mode 100644 index 0d322bf13..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/Infrastructure/DiffSet.cs +++ /dev/null @@ -1,65 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -using System.Collections.Generic; - -namespace Microsoft.AspNet.SignalR.Infrastructure -{ - internal class DiffSet - { - private readonly HashSet _items; - private readonly HashSet _addedItems; - private readonly HashSet _removedItems; - - public DiffSet(IEnumerable items) - { - _addedItems = new HashSet(); - _removedItems = new HashSet(); - - _items = new HashSet(items); - } - - public bool Add(T item) - { - if (_items.Add(item)) - { - if (!_removedItems.Remove(item)) - { - _addedItems.Add(item); - } - return true; - } - return false; - } - - public bool Remove(T item) - { - if (_items.Remove(item)) - { - if (!_addedItems.Remove(item)) - { - _removedItems.Add(item); - } - return true; - } - return false; - } - - public bool Contains(T item) - { - return _items.Contains(item); - } - - public ICollection GetSnapshot() - { - return _items; - } - - public bool DetectChanges() - { - bool anyChanges = _addedItems.Count > 0 || _removedItems.Count > 0; - _addedItems.Clear(); - _removedItems.Clear(); - return anyChanges; - } - } -} diff --git a/src/Microsoft.AspNet.SignalR.Core/Infrastructure/DisposableAction.cs b/src/Microsoft.AspNet.SignalR.Core/Infrastructure/DisposableAction.cs deleted file mode 100644 index ab336d604..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/Infrastructure/DisposableAction.cs +++ /dev/null @@ -1,43 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -using System; -using System.Threading; -using System.Diagnostics.CodeAnalysis; - -namespace Microsoft.AspNet.SignalR.Infrastructure -{ - internal class DisposableAction : IDisposable - { - [SuppressMessage("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields", Justification = "The client projects use this.")] - public static readonly DisposableAction Empty = new DisposableAction(() => { }); - - private Action _action; - private readonly object _state; - - public DisposableAction(Action action) - : this(state => ((Action)state).Invoke(), state: action) - { - - } - - public DisposableAction(Action action, object state) - { - _action = action; - _state = state; - } - - protected virtual void Dispose(bool disposing) - { - if (disposing) - { - Interlocked.Exchange(ref _action, (state) => { }).Invoke(_state); - } - } - - public void Dispose() - { - Dispose(true); - } - } - -} diff --git a/src/Microsoft.AspNet.SignalR.Core/Infrastructure/Disposer.cs b/src/Microsoft.AspNet.SignalR.Core/Infrastructure/Disposer.cs deleted file mode 100644 index ab0155acd..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/Infrastructure/Disposer.cs +++ /dev/null @@ -1,61 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -using System; -using System.Diagnostics; -using System.Threading; - -namespace Microsoft.AspNet.SignalR.Infrastructure -{ - /// - /// Helper class to manage disposing a resource at an arbirtary time - /// - internal class Disposer : IDisposable - { - private static readonly object _disposedSentinel = new object(); - - private object _disposable; - - public void Set(IDisposable disposable) - { - if (disposable == null) - { - throw new ArgumentNullException("disposable"); - } - - object originalFieldValue = Interlocked.CompareExchange(ref _disposable, disposable, null); - if (originalFieldValue == null) - { - // this is the first call to Set() and Dispose() hasn't yet been called; do nothing - } - else if (originalFieldValue == _disposedSentinel) - { - // Dispose() has already been called, so we need to dispose of the object that was just added - disposable.Dispose(); - } - else - { -#if !NET35 && !SILVERLIGHT && !NETFX_CORE - // Set has been called multiple times, fail - Debug.Fail("Multiple calls to Disposer.Set(IDisposable) without calling Disposer.Dispose()"); -#endif - } - } - - protected virtual void Dispose(bool disposing) - { - if (disposing) - { - var disposable = Interlocked.Exchange(ref _disposable, _disposedSentinel) as IDisposable; - if (disposable != null) - { - disposable.Dispose(); - } - } - } - - public void Dispose() - { - Dispose(true); - } - } -} diff --git a/src/Microsoft.AspNet.SignalR.Core/Infrastructure/ExceptionsExtensions.cs b/src/Microsoft.AspNet.SignalR.Core/Infrastructure/ExceptionsExtensions.cs deleted file mode 100644 index 5545f2f81..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/Infrastructure/ExceptionsExtensions.cs +++ /dev/null @@ -1,28 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -using System; - -namespace Microsoft.AspNet.SignalR.Infrastructure -{ - internal static class ExceptionsExtensions - { - internal static Exception Unwrap(this Exception ex) - { - if (ex == null) - { - return null; - } - - var next = ex.GetBaseException(); - while (next.InnerException != null) - { - // On mono GetBaseException() doesn't seem to do anything - // so just walk the inner exception chain. - next = next.InnerException; - } - - return next; - } - } -} - diff --git a/src/Microsoft.AspNet.SignalR.Core/Infrastructure/IAckHandler.cs b/src/Microsoft.AspNet.SignalR.Core/Infrastructure/IAckHandler.cs deleted file mode 100644 index 977b2385f..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/Infrastructure/IAckHandler.cs +++ /dev/null @@ -1,13 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -using System.Threading.Tasks; - -namespace Microsoft.AspNet.SignalR.Infrastructure -{ - public interface IAckHandler - { - Task CreateAck(string id); - - bool TriggerAck(string id); - } -} diff --git a/src/Microsoft.AspNet.SignalR.Core/Infrastructure/IBinaryWriter.cs b/src/Microsoft.AspNet.SignalR.Core/Infrastructure/IBinaryWriter.cs deleted file mode 100644 index 06be2531d..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/Infrastructure/IBinaryWriter.cs +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -using System; - -namespace Microsoft.AspNet.SignalR.Infrastructure -{ - /// - /// Implemented on anything that has the ability to write raw binary data - /// - public interface IBinaryWriter - { - void Write(ArraySegment data); - } -} diff --git a/src/Microsoft.AspNet.SignalR.Core/Infrastructure/IConnectionManager.cs b/src/Microsoft.AspNet.SignalR.Core/Infrastructure/IConnectionManager.cs deleted file mode 100644 index bd783d12e..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/Infrastructure/IConnectionManager.cs +++ /dev/null @@ -1,36 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -using System.Diagnostics.CodeAnalysis; -using Microsoft.AspNet.SignalR.Hubs; - -namespace Microsoft.AspNet.SignalR.Infrastructure -{ - /// - /// Provides access to hubs and persistent connections references. - /// - public interface IConnectionManager - { - /// - /// Returns a for the specified . - /// - /// Type of the - /// a for the specified - [SuppressMessage("Microsoft.Design", "CA1004:GenericMethodsShouldProvideTypeParameter", Justification = "The hub type needs to be specified")] - IHubContext GetHubContext() where T : IHub; - - /// - /// Returns a for the specified hub. - /// - /// Name of the hub - /// a for the specified hub - IHubContext GetHubContext(string hubName); - - /// - /// Returns a for the . - /// - /// Type of the - /// A for the . - [SuppressMessage("Microsoft.Design", "CA1004:GenericMethodsShouldProvideTypeParameter", Justification = "The connection type needs to be specified")] - IPersistentConnectionContext GetConnectionContext() where T : PersistentConnection; - } -} diff --git a/src/Microsoft.AspNet.SignalR.Core/Infrastructure/IPerformanceCounter.cs b/src/Microsoft.AspNet.SignalR.Core/Infrastructure/IPerformanceCounter.cs deleted file mode 100644 index 3f90659e4..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/Infrastructure/IPerformanceCounter.cs +++ /dev/null @@ -1,18 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -using System.Diagnostics; - -namespace Microsoft.AspNet.SignalR.Infrastructure -{ - public interface IPerformanceCounter - { - string CounterName { get; } - long Decrement(); - long Increment(); - long IncrementBy(long value); - CounterSample NextSample(); - long RawValue { get; set; } - void Close(); - void RemoveInstance(); - } -} diff --git a/src/Microsoft.AspNet.SignalR.Core/Infrastructure/IPerformanceCounterManager.cs b/src/Microsoft.AspNet.SignalR.Core/Infrastructure/IPerformanceCounterManager.cs deleted file mode 100644 index c2d6b73e9..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/Infrastructure/IPerformanceCounterManager.cs +++ /dev/null @@ -1,193 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -using System.Threading; - -namespace Microsoft.AspNet.SignalR.Infrastructure -{ - /// - /// Provides access to performance counters. - /// - public interface IPerformanceCounterManager - { - /// - /// Initializes the performance counters. - /// - /// The host instance name. - /// The CancellationToken representing the host shutdown. - void Initialize(string instanceName, CancellationToken hostShutdownToken); - - /// - /// Loads a performance counter. - /// - /// The category name. - /// The counter name. - /// The instance name. - /// Whether the counter is read-only. - IPerformanceCounter LoadCounter(string categoryName, string counterName, string instanceName, bool isReadOnly); - - /// - /// Gets the performance counter representing the total number of connection Connect events since the application was started. - /// - IPerformanceCounter ConnectionsConnected { get; } - - /// - /// Gets the performance counter representing the total number of connection Reconnect events since the application was started. - /// - IPerformanceCounter ConnectionsReconnected { get; } - - /// - /// Gets the performance counter representing the total number of connection Disconnect events since the application was started. - /// - IPerformanceCounter ConnectionsDisconnected { get; } - - /// - /// Gets the performance counter representing the number of connections currently connected. - /// - IPerformanceCounter ConnectionsCurrent { get; } - - /// - /// Gets the performance counter representing the total number of messages received by connections (server to client) since the application was started. - /// - IPerformanceCounter ConnectionMessagesReceivedTotal { get; } - - /// - /// Gets the performance counter representing the total number of messages received by connections (server to client) since the application was started. - /// - IPerformanceCounter ConnectionMessagesSentTotal { get; } - - /// - /// Gets the performance counter representing the number of messages received by connections (server to client) per second. - /// - IPerformanceCounter ConnectionMessagesReceivedPerSec { get; } - - /// - /// Gets the performance counter representing the number of messages sent by connections (client to server) per second. - /// - IPerformanceCounter ConnectionMessagesSentPerSec { get; } - - /// - /// Gets the performance counter representing the total number of messages received by subscribers since the application was started. - /// - IPerformanceCounter MessageBusMessagesReceivedTotal { get; } - - /// - /// Gets the performance counter representing the number of messages received by a subscribers per second. - /// - IPerformanceCounter MessageBusMessagesReceivedPerSec { get; } - - /// - /// Gets the performance counter representing the number of messages received by the scaleout message bus per second. - /// - IPerformanceCounter ScaleoutMessageBusMessagesReceivedPerSec { get; } - - /// - /// Gets the performance counter representing the total number of messages published to the message bus since the application was started. - /// - IPerformanceCounter MessageBusMessagesPublishedTotal { get; } - - /// - /// Gets the performance counter representing the number of messages published to the message bus per second. - /// - IPerformanceCounter MessageBusMessagesPublishedPerSec { get; } - - /// - /// Gets the performance counter representing the current number of subscribers to the message bus. - /// - IPerformanceCounter MessageBusSubscribersCurrent { get; } - - /// - /// Gets the performance counter representing the total number of subscribers to the message bus since the application was started. - /// - IPerformanceCounter MessageBusSubscribersTotal { get; } - - /// - /// Gets the performance counter representing the number of new subscribers to the message bus per second. - /// - IPerformanceCounter MessageBusSubscribersPerSec { get; } - - /// - /// Gets the performance counter representing the number of workers allocated to deliver messages in the message bus. - /// - IPerformanceCounter MessageBusAllocatedWorkers { get; } - - /// - /// Gets the performance counter representing the number of workers currently busy delivering messages in the message bus. - /// - IPerformanceCounter MessageBusBusyWorkers { get; } - - /// - /// Gets the performance counter representing representing the current number of topics in the message bus. - /// - IPerformanceCounter MessageBusTopicsCurrent { get; } - - /// - /// Gets the performance counter representing the total number of all errors processed since the application was started. - /// - IPerformanceCounter ErrorsAllTotal { get; } - - /// - /// Gets the performance counter representing the number of all errors processed per second. - /// - IPerformanceCounter ErrorsAllPerSec { get; } - - /// - /// Gets the performance counter representing the total number of hub resolution errors processed since the application was started. - /// - IPerformanceCounter ErrorsHubResolutionTotal { get; } - - /// - /// Gets the performance counter representing the number of hub resolution errors per second. - /// - IPerformanceCounter ErrorsHubResolutionPerSec { get; } - - /// - /// Gets the performance counter representing the total number of hub invocation errors processed since the application was started. - /// - IPerformanceCounter ErrorsHubInvocationTotal { get; } - - /// - /// Gets the performance counter representing the number of hub invocation errors per second. - /// - IPerformanceCounter ErrorsHubInvocationPerSec { get; } - - /// - /// Gets the performance counter representing the total number of transport errors processed since the application was started. - /// - IPerformanceCounter ErrorsTransportTotal { get; } - - /// - /// Gets the performance counter representing the number of transport errors per second. - /// - IPerformanceCounter ErrorsTransportPerSec { get; } - - /// - /// Gets the performance counter representing the number of logical streams in the currently configured scaleout message bus provider. - /// - IPerformanceCounter ScaleoutStreamCountTotal { get; } - - /// - /// Gets the performance counter representing the number of logical streams in the currently configured scaleout message bus provider that are in the open state. - /// - IPerformanceCounter ScaleoutStreamCountOpen { get; } - - /// - /// Gets the performance counter representing the number of logical streams in the currently configured scaleout message bus provider that are in the buffering state. - /// - IPerformanceCounter ScaleoutStreamCountBuffering { get; } - - /// - /// Gets the performance counter representing the total number of scaleout errors since the application was started. - /// - IPerformanceCounter ScaleoutErrorsTotal { get; } - - /// - /// Gets the performance counter representing the number of scaleout errors per second. - /// - IPerformanceCounter ScaleoutErrorsPerSec { get; } - - /// - /// Gets the performance counter representing the current scaleout send queue length. - /// - IPerformanceCounter ScaleoutSendQueueLength { get; } - } -} diff --git a/src/Microsoft.AspNet.SignalR.Core/Infrastructure/IProtectedData.cs b/src/Microsoft.AspNet.SignalR.Core/Infrastructure/IProtectedData.cs deleted file mode 100644 index 8f46d2145..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/Infrastructure/IProtectedData.cs +++ /dev/null @@ -1,10 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -namespace Microsoft.AspNet.SignalR.Infrastructure -{ - public interface IProtectedData - { - string Protect(string data, string purpose); - string Unprotect(string protectedValue, string purpose); - } -} diff --git a/src/Microsoft.AspNet.SignalR.Core/Infrastructure/IServerCommandHandler.cs b/src/Microsoft.AspNet.SignalR.Core/Infrastructure/IServerCommandHandler.cs deleted file mode 100644 index 9db7f16fc..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/Infrastructure/IServerCommandHandler.cs +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -using System; -using System.Threading.Tasks; - -namespace Microsoft.AspNet.SignalR.Infrastructure -{ - /// - /// Handles commands from server to server. - /// - internal interface IServerCommandHandler - { - /// - /// Sends a command to all connected servers. - /// - /// - /// - Task SendCommand(ServerCommand command); - - /// - /// Gets or sets a callback that is invoked when a command is received. - /// - Action Command { get; set; } - } -} diff --git a/src/Microsoft.AspNet.SignalR.Core/Infrastructure/IServerIdManager.cs b/src/Microsoft.AspNet.SignalR.Core/Infrastructure/IServerIdManager.cs deleted file mode 100644 index a7637b70a..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/Infrastructure/IServerIdManager.cs +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -namespace Microsoft.AspNet.SignalR.Infrastructure -{ - /// - /// Generates a server id - /// - public interface IServerIdManager - { - /// - /// The id of the server. - /// - string ServerId { get; } - } -} diff --git a/src/Microsoft.AspNet.SignalR.Core/Infrastructure/IStringMinifier.cs b/src/Microsoft.AspNet.SignalR.Core/Infrastructure/IStringMinifier.cs deleted file mode 100644 index 6ff5a7941..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/Infrastructure/IStringMinifier.cs +++ /dev/null @@ -1,39 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -namespace Microsoft.AspNet.SignalR.Infrastructure -{ - public interface IStringMinifier - { - /// - /// Minifies a string in a way that can be reversed by this instance of . - /// - /// The string to be minified - /// A minified representation of the without the following characters:,|\ - string Minify(string value); - - /// - /// Reverses a call that was executed at least once previously on this instance of - /// without any subsequent calls to sharing the - /// same argument as the call that returned . - /// - /// - /// A minified string that was returned by a previous call to . - /// - /// - /// The argument of all previous calls to that returned . - /// If every call to on this instance of has never - /// returned or if the most recent call to that did - /// return was followed by a call to sharing - /// the same argument, may return null but must not throw. - /// - string Unminify(string value); - - /// - /// A call to this function indicates that any future attempt to unminify strings that were previously minified - /// from may be met with a null return value. This provides an opportunity clean up - /// any internal data structures that reference . - /// - /// The string that may have previously have been minified. - void RemoveUnminified(string value); - } -} diff --git a/src/Microsoft.AspNet.SignalR.Core/Infrastructure/InterlockedHelper.cs b/src/Microsoft.AspNet.SignalR.Core/Infrastructure/InterlockedHelper.cs deleted file mode 100644 index 3f4044dd7..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/Infrastructure/InterlockedHelper.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System.Threading; - -namespace Microsoft.AspNet.SignalR.Infrastructure -{ - - public static class InterlockedHelper - { - [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1045:DoNotPassTypesByReference", MessageId = "0#", Justification="This is an interlocked helper...")] - public static bool CompareExchangeOr(ref int location, int value, int comparandA, int comparandB) - { - return Interlocked.CompareExchange(ref location, value, comparandA) == comparandA || - Interlocked.CompareExchange(ref location, value, comparandB) == comparandB; - } - } - -} \ No newline at end of file diff --git a/src/Microsoft.AspNet.SignalR.Core/Infrastructure/ListHelper.cs b/src/Microsoft.AspNet.SignalR.Core/Infrastructure/ListHelper.cs deleted file mode 100644 index 12e8b8dce..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/Infrastructure/ListHelper.cs +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -using System.Collections.Generic; -using System.Collections.ObjectModel; - -namespace Microsoft.AspNet.SignalR.Infrastructure -{ - internal class ListHelper - { - public static readonly IList Empty = new ReadOnlyCollection(new List()); - } -} diff --git a/src/Microsoft.AspNet.SignalR.Core/Infrastructure/MonoUtility.cs b/src/Microsoft.AspNet.SignalR.Core/Infrastructure/MonoUtility.cs deleted file mode 100644 index ea1d80127..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/Infrastructure/MonoUtility.cs +++ /dev/null @@ -1,31 +0,0 @@ -using System; -using System.Diagnostics.CodeAnalysis; - -namespace Microsoft.AspNet.SignalR.Infrastructure -{ - internal static class MonoUtility - { - private static readonly Lazy _isRunningMono = new Lazy(() => CheckRunningOnMono()); - - internal static bool IsRunningMono - { - get - { - return _isRunningMono.Value; - } - } - - [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "This should never fail")] - private static bool CheckRunningOnMono() - { - try - { - return Type.GetType("Mono.Runtime") != null; - } - catch - { - return false; - } - } - } -} diff --git a/src/Microsoft.AspNet.SignalR.Core/Infrastructure/PerformanceCounterAttribute.cs b/src/Microsoft.AspNet.SignalR.Core/Infrastructure/PerformanceCounterAttribute.cs deleted file mode 100644 index 7a2cb0682..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/Infrastructure/PerformanceCounterAttribute.cs +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -using System; -using System.Diagnostics; - -namespace Microsoft.AspNet.SignalR.Infrastructure -{ - [AttributeUsage(AttributeTargets.Property, AllowMultiple=false)] - internal sealed class PerformanceCounterAttribute : Attribute - { - public string Name { get; set; } - public string Description { get; set; } - public PerformanceCounterType CounterType { get; set; } - } -} diff --git a/src/Microsoft.AspNet.SignalR.Core/Infrastructure/PerformanceCounterManager.cs b/src/Microsoft.AspNet.SignalR.Core/Infrastructure/PerformanceCounterManager.cs deleted file mode 100644 index 11b9a3577..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/Infrastructure/PerformanceCounterManager.cs +++ /dev/null @@ -1,418 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -using System; -using System.ComponentModel; -using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; -using System.Linq; -using System.Reflection; -using System.Threading; -#if !UTILS -using Microsoft.AspNet.SignalR.Tracing; -#endif - -namespace Microsoft.AspNet.SignalR.Infrastructure -{ - /// - /// Manages performance counters using Windows performance counters. - /// - public class PerformanceCounterManager : IPerformanceCounterManager - { - /// - /// The performance counter category name for SignalR counters. - /// - public const string CategoryName = "SignalR"; - - private readonly static PropertyInfo[] _counterProperties = GetCounterPropertyInfo(); - private readonly static IPerformanceCounter _noOpCounter = new NoOpPerformanceCounter(); - private volatile bool _initialized; - private object _initLocker = new object(); - -#if !UTILS - private readonly TraceSource _trace; - - public PerformanceCounterManager(DefaultDependencyResolver resolver) - : this(resolver.Resolve()) - { - - } - - /// - /// Creates a new instance. - /// - public PerformanceCounterManager(ITraceManager traceManager) - : this() - { - if (traceManager == null) - { - throw new ArgumentNullException("traceManager"); - } - - _trace = traceManager["SignalR.PerformanceCounterManager"]; - } -#endif - - public PerformanceCounterManager() - { - InitNoOpCounters(); - } - - /// - /// Gets the performance counter representing the total number of connection Connect events since the application was started. - /// - [PerformanceCounter(Name = "Connections Connected", Description = "The total number of connection Connect events since the application was started.", CounterType = PerformanceCounterType.NumberOfItems32)] - public IPerformanceCounter ConnectionsConnected { get; private set; } - - /// - /// Gets the performance counter representing the total number of connection Reconnect events since the application was started. - /// - [PerformanceCounter(Name = "Connections Reconnected", Description = "The total number of connection Reconnect events since the application was started.", CounterType = PerformanceCounterType.NumberOfItems32)] - public IPerformanceCounter ConnectionsReconnected { get; private set; } - - /// - /// Gets the performance counter representing the total number of connection Disconnect events since the application was started. - /// - [PerformanceCounter(Name = "Connections Disconnected", Description = "The total number of connection Disconnect events since the application was started.", CounterType = PerformanceCounterType.NumberOfItems32)] - public IPerformanceCounter ConnectionsDisconnected { get; private set; } - - /// - /// Gets the performance counter representing the number of connections currently connected. - /// - [PerformanceCounter(Name = "Connections Current", Description = "The number of connections currently connected.", CounterType = PerformanceCounterType.NumberOfItems32)] - public IPerformanceCounter ConnectionsCurrent { get; private set; } - - /// - /// Gets the performance counter representing the toal number of messages received by connections (server to client) since the application was started. - /// - [PerformanceCounter(Name = "Connection Messages Received Total", Description = "The toal number of messages received by connections (server to client) since the application was started.", CounterType = PerformanceCounterType.NumberOfItems64)] - public IPerformanceCounter ConnectionMessagesReceivedTotal { get; private set; } - - /// - /// Gets the performance counter representing the total number of messages sent by connections (client to server) since the application was started. - /// - [PerformanceCounter(Name = "Connection Messages Sent Total", Description = "The total number of messages sent by connections (client to server) since the application was started.", CounterType = PerformanceCounterType.NumberOfItems64)] - public IPerformanceCounter ConnectionMessagesSentTotal { get; private set; } - - /// - /// Gets the performance counter representing the number of messages received by connections (server to client) per second. - /// - [PerformanceCounter(Name = "Connection Messages Received/Sec", Description = "The number of messages received by connections (server to client) per second.", CounterType = PerformanceCounterType.RateOfCountsPerSecond32)] - public IPerformanceCounter ConnectionMessagesReceivedPerSec { get; private set; } - - /// - /// Gets the performance counter representing the number of messages sent by connections (client to server) per second. - /// - [PerformanceCounter(Name = "Connection Messages Sent/Sec", Description = "The number of messages sent by connections (client to server) per second.", CounterType = PerformanceCounterType.RateOfCountsPerSecond32)] - public IPerformanceCounter ConnectionMessagesSentPerSec { get; private set; } - - /// - /// Gets the performance counter representing the total number of messages received by subscribers since the application was started. - /// - [PerformanceCounter(Name = "Message Bus Messages Received Total", Description = "The total number of messages received by subscribers since the application was started.", CounterType = PerformanceCounterType.NumberOfItems64)] - public IPerformanceCounter MessageBusMessagesReceivedTotal { get; private set; } - - /// - /// Gets the performance counter representing the number of messages received by a subscribers per second. - /// - [PerformanceCounter(Name = "Message Bus Messages Received/Sec", Description = "The number of messages received by subscribers per second.", CounterType = PerformanceCounterType.RateOfCountsPerSecond32)] - public IPerformanceCounter MessageBusMessagesReceivedPerSec { get; private set; } - - /// - /// Gets the performance counter representing the number of messages received by the scaleout message bus per second. - /// - [PerformanceCounter(Name = "Scaleout Message Bus Messages Received/Sec", Description = "The number of messages received by the scaleout message bus per second.", CounterType = PerformanceCounterType.RateOfCountsPerSecond32)] - public IPerformanceCounter ScaleoutMessageBusMessagesReceivedPerSec { get; private set; } - - - /// - /// Gets the performance counter representing the total number of messages published to the message bus since the application was started. - /// - [PerformanceCounter(Name = "Messages Bus Messages Published Total", Description = "The total number of messages published to the message bus since the application was started.", CounterType = PerformanceCounterType.NumberOfItems64)] - public IPerformanceCounter MessageBusMessagesPublishedTotal { get; private set; } - - /// - /// Gets the performance counter representing the number of messages published to the message bus per second. - /// - [PerformanceCounter(Name = "Messages Bus Messages Published/Sec", Description = "The number of messages published to the message bus per second.", CounterType = PerformanceCounterType.RateOfCountsPerSecond32)] - public IPerformanceCounter MessageBusMessagesPublishedPerSec { get; private set; } - - /// - /// Gets the performance counter representing the current number of subscribers to the message bus. - /// - [PerformanceCounter(Name = "Message Bus Subscribers Current", Description = "The current number of subscribers to the message bus.", CounterType = PerformanceCounterType.NumberOfItems32)] - public IPerformanceCounter MessageBusSubscribersCurrent { get; private set; } - - /// - /// Gets the performance counter representing the total number of subscribers to the message bus since the application was started. - /// - [PerformanceCounter(Name = "Message Bus Subscribers Total", Description = "The total number of subscribers to the message bus since the application was started.", CounterType = PerformanceCounterType.NumberOfItems32)] - public IPerformanceCounter MessageBusSubscribersTotal { get; private set; } - - /// - /// Gets the performance counter representing the number of new subscribers to the message bus per second. - /// - [PerformanceCounter(Name = "Message Bus Subscribers/Sec", Description = "The number of new subscribers to the message bus per second.", CounterType = PerformanceCounterType.RateOfCountsPerSecond32)] - public IPerformanceCounter MessageBusSubscribersPerSec { get; private set; } - - /// - /// Gets the performance counter representing the number of workers allocated to deliver messages in the message bus. - /// - [PerformanceCounter(Name = "Message Bus Allocated Workers", Description = "The number of workers allocated to deliver messages in the message bus.", CounterType = PerformanceCounterType.NumberOfItems32)] - public IPerformanceCounter MessageBusAllocatedWorkers { get; private set; } - - /// - /// Gets the performance counter representing the number of workers currently busy delivering messages in the message bus. - /// - [PerformanceCounter(Name = "Message Bus Busy Workers", Description = "The number of workers currently busy delivering messages in the message bus.", CounterType = PerformanceCounterType.NumberOfItems32)] - public IPerformanceCounter MessageBusBusyWorkers { get; private set; } - - /// - /// Gets the performance counter representing representing the current number of topics in the message bus. - /// - [PerformanceCounter(Name = "Message Bus Topics Current", Description = "The number of topics in the message bus.", CounterType = PerformanceCounterType.NumberOfItems32)] - public IPerformanceCounter MessageBusTopicsCurrent { get; private set; } - - /// - /// Gets the performance counter representing the total number of all errors processed since the application was started. - /// - [PerformanceCounter(Name = "Errors: All Total", Description = "The total number of all errors processed since the application was started.", CounterType = PerformanceCounterType.NumberOfItems32)] - public IPerformanceCounter ErrorsAllTotal { get; private set; } - - /// - /// Gets the performance counter representing the number of all errors processed per second. - /// - [PerformanceCounter(Name = "Errors: All/Sec", Description = "The number of all errors processed per second.", CounterType = PerformanceCounterType.RateOfCountsPerSecond32)] - public IPerformanceCounter ErrorsAllPerSec { get; private set; } - - /// - /// Gets the performance counter representing the total number of hub resolution errors processed since the application was started. - /// - [PerformanceCounter(Name = "Errors: Hub Resolution Total", Description = "The total number of hub resolution errors processed since the application was started.", CounterType = PerformanceCounterType.NumberOfItems32)] - public IPerformanceCounter ErrorsHubResolutionTotal { get; private set; } - - /// - /// Gets the performance counter representing the number of hub resolution errors per second. - /// - [PerformanceCounter(Name = "Errors: Hub Resolution/Sec", Description = "The number of hub resolution errors per second.", CounterType = PerformanceCounterType.RateOfCountsPerSecond32)] - public IPerformanceCounter ErrorsHubResolutionPerSec { get; private set; } - - /// - /// Gets the performance counter representing the total number of hub invocation errors processed since the application was started. - /// - [PerformanceCounter(Name = "Errors: Hub Invocation Total", Description = "The total number of hub invocation errors processed since the application was started.", CounterType = PerformanceCounterType.NumberOfItems32)] - public IPerformanceCounter ErrorsHubInvocationTotal { get; private set; } - - /// - /// Gets the performance counter representing the number of hub invocation errors per second. - /// - [PerformanceCounter(Name = "Errors: Hub Invocation/Sec", Description = "The number of hub invocation errors per second.", CounterType = PerformanceCounterType.RateOfCountsPerSecond32)] - public IPerformanceCounter ErrorsHubInvocationPerSec { get; private set; } - - /// - /// Gets the performance counter representing the total number of transport errors processed since the application was started. - /// - [PerformanceCounter(Name = "Errors: Tranport Total", Description = "The total number of transport errors processed since the application was started.", CounterType = PerformanceCounterType.NumberOfItems32)] - public IPerformanceCounter ErrorsTransportTotal { get; private set; } - - /// - /// Gets the performance counter representing the number of transport errors per second. - /// - [PerformanceCounter(Name = "Errors: Transport/Sec", Description = "The number of transport errors per second.", CounterType = PerformanceCounterType.RateOfCountsPerSecond32)] - public IPerformanceCounter ErrorsTransportPerSec { get; private set; } - - - /// - /// Gets the performance counter representing the number of logical streams in the currently configured scaleout message bus provider. - /// - [PerformanceCounter(Name = "Scaleout Streams Total", Description = "The number of logical streams in the currently configured scaleout message bus provider.", CounterType = PerformanceCounterType.NumberOfItems32)] - public IPerformanceCounter ScaleoutStreamCountTotal { get; private set; } - - /// - /// Gets the performance counter representing the number of logical streams in the currently configured scaleout message bus provider that are in the open state. - /// - [PerformanceCounter(Name = "Scaleout Streams Open", Description = "The number of logical streams in the currently configured scaleout message bus provider that are in the open state", CounterType = PerformanceCounterType.NumberOfItems32)] - public IPerformanceCounter ScaleoutStreamCountOpen { get; private set; } - - /// - /// Gets the performance counter representing the number of logical streams in the currently configured scaleout message bus provider that are in the buffering state. - /// - [PerformanceCounter(Name = "Scaleout Streams Buffering", Description = "The number of logical streams in the currently configured scaleout message bus provider that are in the buffering state", CounterType = PerformanceCounterType.NumberOfItems32)] - public IPerformanceCounter ScaleoutStreamCountBuffering { get; private set; } - - /// - /// Gets the performance counter representing the total number of scaleout errors since the application was started. - /// - [PerformanceCounter(Name = "Scaleout Errors Total", Description = "The total number of scaleout errors since the application was started.", CounterType = PerformanceCounterType.NumberOfItems32)] - public IPerformanceCounter ScaleoutErrorsTotal { get; private set; } - - /// - /// Gets the performance counter representing the number of scaleout errors per second. - /// - [PerformanceCounter(Name = "Scaleout Errors/Sec", Description = "The number of scaleout errors per second.", CounterType = PerformanceCounterType.RateOfCountsPerSecond32)] - public IPerformanceCounter ScaleoutErrorsPerSec { get; private set; } - - /// - /// Gets the performance counter representing the current scaleout send queue length. - /// - [PerformanceCounter(Name = "Scaleout Send Queue Length", Description = "The current scaleout send queue length.", CounterType = PerformanceCounterType.NumberOfItems32)] - public IPerformanceCounter ScaleoutSendQueueLength { get; private set; } - - /// - /// Initializes the performance counters. - /// - /// The host instance name. - /// The CancellationToken representing the host shutdown. - public void Initialize(string instanceName, CancellationToken hostShutdownToken) - { - if (_initialized) - { - return; - } - - var needToRegisterWithShutdownToken = false; - lock (_initLocker) - { - if (!_initialized) - { - instanceName = instanceName ?? Guid.NewGuid().ToString(); - SetCounterProperties(instanceName); - // The initializer ran, so let's register the shutdown cleanup - if (hostShutdownToken != CancellationToken.None) - { - needToRegisterWithShutdownToken = true; - } - _initialized = true; - } - } - - if (needToRegisterWithShutdownToken) - { - hostShutdownToken.Register(UnloadCounters); - } - } - - private void UnloadCounters() - { - lock (_initLocker) - { - if (!_initialized) - { - // We were never initalized - return; - } - } - - var counterProperties = this.GetType() - .GetProperties() - .Where(p => p.PropertyType == typeof(IPerformanceCounter)); - - foreach (var property in counterProperties) - { - var counter = property.GetValue(this, null) as IPerformanceCounter; - counter.Close(); - counter.RemoveInstance(); - } - } - - private void InitNoOpCounters() - { - // Set all the counter properties to no-op by default. - // These will get reset to real counters when/if the Initialize method is called. - foreach (var property in _counterProperties) - { - property.SetValue(this, new NoOpPerformanceCounter(), null); - } - } - - private void SetCounterProperties(string instanceName) - { - var loadCounters = true; - - foreach (var property in _counterProperties) - { - PerformanceCounterAttribute attribute = GetPerformanceCounterAttribute(property); - - if (attribute == null) - { - continue; - } - - IPerformanceCounter counter = null; - - if (loadCounters) - { - counter = LoadCounter(CategoryName, attribute.Name, instanceName, isReadOnly:false); - - if (counter == null) - { - // We failed to load the counter so skip the rest - loadCounters = false; - } - } - - counter = counter ?? _noOpCounter; - - property.SetValue(this, counter, null); - } - } - - internal static PropertyInfo[] GetCounterPropertyInfo() - { - return typeof(PerformanceCounterManager) - .GetProperties() - .Where(p => p.PropertyType == typeof(IPerformanceCounter)) - .ToArray(); - } - - internal static PerformanceCounterAttribute GetPerformanceCounterAttribute(PropertyInfo property) - { - return property.GetCustomAttributes(typeof(PerformanceCounterAttribute), false) - .Cast() - .SingleOrDefault(); - } - - [SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic", Justification = "This file is shared")] - [SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "Counters are disposed later")] - public IPerformanceCounter LoadCounter(string categoryName, string counterName, string instanceName, bool isReadOnly) - { - // See http://msdn.microsoft.com/en-us/library/356cx381.aspx for the list of exceptions - // and when they are thrown. - try - { - var counter = new PerformanceCounter(categoryName, counterName, instanceName, isReadOnly); - - // Initialize the counter sample - counter.NextSample(); - - return new PerformanceCounterWrapper(counter); - } -#if UTILS - catch (InvalidOperationException) { return null; } - catch (UnauthorizedAccessException) { return null; } - catch (Win32Exception) { return null; } - catch (PlatformNotSupportedException) { return null; } -#else - catch (InvalidOperationException ex) - { - _trace.TraceEvent(TraceEventType.Error, 0, "Performance counter failed to load: " + ex.GetBaseException()); - return null; - } - catch (UnauthorizedAccessException ex) - { - _trace.TraceEvent(TraceEventType.Error, 0, "Performance counter failed to load: " + ex.GetBaseException()); - return null; - } - catch (Win32Exception ex) - { - _trace.TraceEvent(TraceEventType.Error, 0, "Performance counter failed to load: " + ex.GetBaseException()); - return null; - } - catch (PlatformNotSupportedException ex) - { - _trace.TraceEvent(TraceEventType.Error, 0, "Performance counter failed to load: " + ex.GetBaseException()); - return null; - } -#endif - } - } -} diff --git a/src/Microsoft.AspNet.SignalR.Core/Infrastructure/PerformanceCounterWrapper.cs b/src/Microsoft.AspNet.SignalR.Core/Infrastructure/PerformanceCounterWrapper.cs deleted file mode 100644 index f455158bf..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/Infrastructure/PerformanceCounterWrapper.cs +++ /dev/null @@ -1,68 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -using System; -using System.Diagnostics; - -namespace Microsoft.AspNet.SignalR.Infrastructure -{ - internal class PerformanceCounterWrapper : IPerformanceCounter - { - private readonly PerformanceCounter _counter; - - public PerformanceCounterWrapper(PerformanceCounter counter) - { - _counter = counter; - } - - public string CounterName - { - get - { - return _counter.CounterName; - } - } - - public long RawValue - { - get { return _counter.RawValue; } - set { _counter.RawValue = value; } - } - - public long Decrement() - { - return _counter.Decrement(); - } - - public long Increment() - { - return _counter.Increment(); - } - - public long IncrementBy(long value) - { - return _counter.IncrementBy(value); - } - - public void Close() - { - _counter.Close(); - } - - public void RemoveInstance() - { - try - { - _counter.RemoveInstance(); - } - catch(NotImplementedException) - { - // This happens on mono - } - } - - public CounterSample NextSample() - { - return _counter.NextSample(); - } - } -} diff --git a/src/Microsoft.AspNet.SignalR.Core/Infrastructure/PersistentConnectionContext.cs b/src/Microsoft.AspNet.SignalR.Core/Infrastructure/PersistentConnectionContext.cs deleted file mode 100644 index b27334591..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/Infrastructure/PersistentConnectionContext.cs +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -namespace Microsoft.AspNet.SignalR.Infrastructure -{ - internal class PersistentConnectionContext : IPersistentConnectionContext - { - public PersistentConnectionContext(IConnection connection, IConnectionGroupManager groupManager) - { - Connection = connection; - Groups = groupManager; - } - - public IConnection Connection { get; private set; } - - public IConnectionGroupManager Groups { get; private set; } - } -} diff --git a/src/Microsoft.AspNet.SignalR.Core/Infrastructure/PrefixHelper.cs b/src/Microsoft.AspNet.SignalR.Core/Infrastructure/PrefixHelper.cs deleted file mode 100644 index b734952e5..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/Infrastructure/PrefixHelper.cs +++ /dev/null @@ -1,96 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - - -using System; -using System.Collections.Generic; -using System.Linq; - -namespace Microsoft.AspNet.SignalR.Infrastructure -{ - internal static class PrefixHelper - { - // Hubs - internal const string HubPrefix = "h-"; - internal const string HubGroupPrefix = "hg-"; - internal const string HubConnectionIdPrefix = "hc-"; - - // Persistent Connections - internal const string PersistentConnectionPrefix = "pc-"; - internal const string PersistentConnectionGroupPrefix = "pcg-"; - - // Both - internal const string ConnectionIdPrefix = "c-"; - internal const string AckPrefix = "ack-"; - - public static bool HasGroupPrefix(string value) - { - return value.StartsWith(HubGroupPrefix, StringComparison.Ordinal) || - value.StartsWith(PersistentConnectionGroupPrefix, StringComparison.Ordinal); - } - - public static string GetConnectionId(string connectionId) - { - return ConnectionIdPrefix + connectionId; - } - - public static string GetHubConnectionId(string connectionId) - { - return HubConnectionIdPrefix + connectionId; - } - - public static string GetHubName(string connectionId) - { - return HubPrefix + connectionId; - } - - public static string GetHubGroupName(string groupName) - { - return HubGroupPrefix + groupName; - } - - public static string GetPersistentConnectionGroupName(string groupName) - { - return PersistentConnectionGroupPrefix + groupName; - } - - public static string GetPersistentConnectionName(string connectionName) - { - return PersistentConnectionPrefix + connectionName; - } - - public static string GetAck(string connectionId) - { - return AckPrefix + connectionId; - } - - public static IList GetPrefixedConnectionIds(IList connectionIds) - { - if (connectionIds.Count == 0) - { - return ListHelper.Empty; - } - - return connectionIds.Select(PrefixHelper.GetConnectionId).ToList(); - } - - public static IEnumerable RemoveGroupPrefixes(IEnumerable groups) - { - return groups.Select(PrefixHelper.RemoveGroupPrefix); - } - - public static string RemoveGroupPrefix(string name) - { - if (name.StartsWith(HubGroupPrefix, StringComparison.Ordinal)) - { - return name.Substring(HubGroupPrefix.Length); - } - - if (name.StartsWith(PersistentConnectionGroupPrefix, StringComparison.Ordinal)) - { - return name.Substring(PersistentConnectionGroupPrefix.Length); - } - - return name; - } - } -} diff --git a/src/Microsoft.AspNet.SignalR.Core/Infrastructure/Purposes.cs b/src/Microsoft.AspNet.SignalR.Core/Infrastructure/Purposes.cs deleted file mode 100644 index c2d87415e..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/Infrastructure/Purposes.cs +++ /dev/null @@ -1,11 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -namespace Microsoft.AspNet.SignalR.Infrastructure -{ - // These need to change when the format changes - public static class Purposes - { - public const string ConnectionToken = "SignalR.ConnectionToken"; - public const string Groups = "SignalR.Groups.v1.1"; - } -} diff --git a/src/Microsoft.AspNet.SignalR.Core/Infrastructure/SafeCancellationTokenSource.cs b/src/Microsoft.AspNet.SignalR.Core/Infrastructure/SafeCancellationTokenSource.cs deleted file mode 100644 index e2506de52..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/Infrastructure/SafeCancellationTokenSource.cs +++ /dev/null @@ -1,103 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -using System; -using System.Threading; - -namespace Microsoft.AspNet.SignalR.Infrastructure -{ - /// - /// Thread safe cancellation token source. Allows the following: - /// - Cancel will no-op if the token is disposed. - /// - Dispose may be called after Cancel. - /// - internal class SafeCancellationTokenSource : IDisposable - { - private CancellationTokenSource _cts; - private int _state; - - public SafeCancellationTokenSource() - { - _cts = new CancellationTokenSource(); - Token = _cts.Token; - } - - public CancellationToken Token { get; private set; } - - public void Cancel() - { - var value = Interlocked.CompareExchange(ref _state, State.Cancelling, State.Initial); - - if (value == State.Initial) - { - // Because cancellation tokens are so poorly behaved, always invoke the cancellation token on - // another thread. Don't capture any of the context (execution context or sync context) - // while doing this. -#if WINDOWS_PHONE || SILVERLIGHT - ThreadPool.QueueUserWorkItem(_ => -#elif NETFX_CORE - Task.Run(() => -#else - ThreadPool.UnsafeQueueUserWorkItem(_ => -#endif - { - try - { - _cts.Cancel(); - } - finally - { - if (Interlocked.CompareExchange(ref _state, State.Cancelled, State.Cancelling) == State.Disposing) - { - _cts.Dispose(); - Interlocked.Exchange(ref _state, State.Disposed); - } - } - } -#if !NETFX_CORE - , state: null -#endif -); - } - } - - protected virtual void Dispose(bool disposing) - { - if (disposing) - { - var value = Interlocked.Exchange(ref _state, State.Disposing); - - switch (value) - { - case State.Initial: - case State.Cancelled: - _cts.Dispose(); - Interlocked.Exchange(ref _state, State.Disposed); - break; - case State.Cancelling: - case State.Disposing: - // No-op - break; - case State.Disposed: - Interlocked.Exchange(ref _state, State.Disposed); - break; - default: - break; - } - } - } - - public void Dispose() - { - Dispose(true); - } - - private static class State - { - public const int Initial = 0; - public const int Cancelling = 1; - public const int Cancelled = 2; - public const int Disposing = 3; - public const int Disposed = 4; - } - } -} diff --git a/src/Microsoft.AspNet.SignalR.Core/Infrastructure/SafeSet.cs b/src/Microsoft.AspNet.SignalR.Core/Infrastructure/SafeSet.cs deleted file mode 100644 index ae9827a27..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/Infrastructure/SafeSet.cs +++ /dev/null @@ -1,60 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Linq; - -namespace Microsoft.AspNet.SignalR.Infrastructure -{ - internal class SafeSet - { - private readonly ConcurrentDictionary _items; - - public SafeSet() - { - _items = new ConcurrentDictionary(); - } - - public SafeSet(IEqualityComparer comparer) - { - _items = new ConcurrentDictionary(comparer); - } - - public SafeSet(IEnumerable items) - { - _items = new ConcurrentDictionary(items.Select(x => new KeyValuePair(x, null))); - } - - public ICollection GetSnapshot() - { - // The Keys property locks, so Select instead - return _items.Keys; - } - - public bool Contains(T item) - { - return _items.ContainsKey(item); - } - - public bool Add(T item) - { - return _items.TryAdd(item, null); - } - - public bool Remove(T item) - { - object _; - return _items.TryRemove(item, out _); - } - - public bool Any() - { - return _items.Any(); - } - - public long Count - { - get { return _items.Count; } - } - } -} diff --git a/src/Microsoft.AspNet.SignalR.Core/Infrastructure/ServerCommand.cs b/src/Microsoft.AspNet.SignalR.Core/Infrastructure/ServerCommand.cs deleted file mode 100644 index 604144187..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/Infrastructure/ServerCommand.cs +++ /dev/null @@ -1,30 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -namespace Microsoft.AspNet.SignalR.Infrastructure -{ - /// - /// A server to server command. - /// - internal class ServerCommand - { - /// - /// Gets or sets the id of the command where this message originated from. - /// - public string ServerId { get; set; } - - /// - /// Gets of sets the command type. - /// - public ServerCommandType ServerCommandType { get; set; } - - /// - /// Gets or sets the value for this command. - /// - public object Value { get; set; } - - internal bool IsFromSelf(string serverId) - { - return serverId.Equals(ServerId); - } - } -} diff --git a/src/Microsoft.AspNet.SignalR.Core/Infrastructure/ServerCommandHandler.cs b/src/Microsoft.AspNet.SignalR.Core/Infrastructure/ServerCommandHandler.cs deleted file mode 100644 index aecdfa28a..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/Infrastructure/ServerCommandHandler.cs +++ /dev/null @@ -1,148 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -using System; -using System.Collections.Generic; -using System.IO; -using System.Threading.Tasks; -using Microsoft.AspNet.SignalR.Json; -using Microsoft.AspNet.SignalR.Messaging; - -namespace Microsoft.AspNet.SignalR.Infrastructure -{ - /// - /// Default implementation. - /// - internal class ServerCommandHandler : IServerCommandHandler, ISubscriber, IDisposable - { - private readonly IMessageBus _messageBus; - private readonly IServerIdManager _serverIdManager; - private readonly IJsonSerializer _serializer; - private IDisposable _subscription; - - private const int MaxMessages = 10; - - // The signal for all signalr servers - private const string ServerSignal = "__SIGNALR__SERVER__"; - private static readonly string[] ServerSignals = new[] { ServerSignal }; - - public ServerCommandHandler(IDependencyResolver resolver) : - this(resolver.Resolve(), - resolver.Resolve(), - resolver.Resolve()) - { - - } - - public ServerCommandHandler(IMessageBus messageBus, IServerIdManager serverIdManager, IJsonSerializer serializer) - { - _messageBus = messageBus; - _serverIdManager = serverIdManager; - _serializer = serializer; - - ProcessMessages(); - } - - public Action Command - { - get; - set; - } - - - public IList EventKeys - { - get - { - return ServerSignals; - } - } - - event Action ISubscriber.EventKeyAdded - { - add - { - } - remove - { - } - } - - event Action ISubscriber.EventKeyRemoved - { - add - { - } - remove - { - } - } - - public Action WriteCursor { get; set; } - - public string Identity - { - get - { - return _serverIdManager.ServerId; - } - } - - public Subscription Subscription - { - get; - set; - } - - public Task SendCommand(ServerCommand command) - { - // Store where the message originated from - command.ServerId = _serverIdManager.ServerId; - - // Send the command to the all servers - return _messageBus.Publish(_serverIdManager.ServerId, ServerSignal, _serializer.Stringify(command)); - } - - protected virtual void Dispose(bool disposing) - { - if (disposing) - { - if (_subscription != null) - { - _subscription.Dispose(); - } - } - } - - public void Dispose() - { - Dispose(true); - } - - private void ProcessMessages() - { - // Process messages that come from the bus for servers - _subscription = _messageBus.Subscribe(this, cursor: null, callback: HandleServerCommands, maxMessages: MaxMessages, state: null); - } - - private Task HandleServerCommands(MessageResult result, object state) - { - result.Messages.Enumerate(m => ServerSignal.Equals(m.Key), - (s, m) => - { - var command = _serializer.Parse(m.Value, m.Encoding); - OnCommand(command); - }, - state: null); - - return TaskAsyncHelper.True; - } - - private void OnCommand(ServerCommand command) - { - if (Command != null) - { - Command(command); - } - } - } -} diff --git a/src/Microsoft.AspNet.SignalR.Core/Infrastructure/ServerCommandType.cs b/src/Microsoft.AspNet.SignalR.Core/Infrastructure/ServerCommandType.cs deleted file mode 100644 index 058164117..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/Infrastructure/ServerCommandType.cs +++ /dev/null @@ -1,9 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -namespace Microsoft.AspNet.SignalR.Infrastructure -{ - public enum ServerCommandType - { - RemoveConnection - } -} diff --git a/src/Microsoft.AspNet.SignalR.Core/Infrastructure/ServerIdManager.cs b/src/Microsoft.AspNet.SignalR.Core/Infrastructure/ServerIdManager.cs deleted file mode 100644 index e80d5cae1..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/Infrastructure/ServerIdManager.cs +++ /dev/null @@ -1,26 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -using System; - -namespace Microsoft.AspNet.SignalR.Infrastructure -{ - /// - /// Default implementation. - /// - public class ServerIdManager : IServerIdManager - { - public ServerIdManager() - { - ServerId = Guid.NewGuid().ToString(); - } - - /// - /// The id of the server. - /// - public string ServerId - { - get; - private set; - } - } -} diff --git a/src/Microsoft.AspNet.SignalR.Core/Infrastructure/SipHashBasedStringEqualityComparer.cs b/src/Microsoft.AspNet.SignalR.Core/Infrastructure/SipHashBasedStringEqualityComparer.cs deleted file mode 100644 index bf86a93f3..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/Infrastructure/SipHashBasedStringEqualityComparer.cs +++ /dev/null @@ -1,242 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -using System; -using System.Collections.Generic; -using System.Security.Cryptography; - -namespace Microsoft.AspNet.SignalR.Infrastructure -{ - // A string equality comparer based on the SipHash-2-4 algorithm. Key differences: - // (a) we output 32-bit hashes instead of 64-bit hashes, and - // (b) we don't care about endianness since hashes are used only in hash tables - // and aren't returned to user code. - // - // Meant to serve as a replacement for StringComparer.Ordinal. - // Derivative work of https://github.com/tanglebones/ch-siphash. - internal unsafe sealed class SipHashBasedStringEqualityComparer : IEqualityComparer - { - private static readonly RNGCryptoServiceProvider _rng = new RNGCryptoServiceProvider(); - - // the 128-bit secret key - private readonly ulong _k0; - private readonly ulong _k1; - - public SipHashBasedStringEqualityComparer() - : this(GenerateRandomKeySegment(), GenerateRandomKeySegment()) - { - } - - // for unit testing - internal SipHashBasedStringEqualityComparer(ulong k0, ulong k1) - { - _k0 = k0; - _k1 = k1; - } - - public bool Equals(string x, string y) - { - return String.Equals(x, y); - } - - private static ulong GenerateRandomKeySegment() - { - byte[] bytes = new byte[sizeof(ulong)]; - _rng.GetBytes(bytes); - return (ulong)BitConverter.ToInt64(bytes, 0); - } - - public int GetHashCode(string obj) - { - if (obj == null) - { - return 0; - } - - fixed (char* pChars = obj) - { - // treat input as an opaque blob, convert char count to byte count - return GetHashCode((byte*)pChars, checked((uint)obj.Length * sizeof(char))); - } - } - - // for unit testing - internal int GetHashCode(byte* bytes, uint len) - { - // Assume SipHash-2-4 is a strong PRF, therefore truncation to 32 bits is acceptable. - return (int)SipHash_2_4_UlongCast_ForcedInline(bytes, len, _k0, _k1); - } - - private static unsafe ulong SipHash_2_4_UlongCast_ForcedInline(byte* finb, uint inlen, ulong k0, ulong k1) - { - var v0 = 0x736f6d6570736575 ^ k0; - var v1 = 0x646f72616e646f6d ^ k1; - var v2 = 0x6c7967656e657261 ^ k0; - var v3 = 0x7465646279746573 ^ k1; - - var b = ((ulong)inlen) << 56; - - if (inlen > 0) - { - var inb = finb; - var left = inlen & 7; - var end = inb + inlen - left; - var linb = (ulong*)finb; - var lend = (ulong*)end; - for (; linb < lend; ++linb) - { - v3 ^= *linb; - - v0 += v1; - v1 = (v1 << 13) | (v1 >> (64 - 13)); - v1 ^= v0; - v0 = (v0 << 32) | (v0 >> (64 - 32)); - - v2 += v3; - v3 = (v3 << 16) | (v3 >> (64 - 16)); - v3 ^= v2; - - v0 += v3; - v3 = (v3 << 21) | (v3 >> (64 - 21)); - v3 ^= v0; - - v2 += v1; - v1 = (v1 << 17) | (v1 >> (64 - 17)); - v1 ^= v2; - v2 = (v2 << 32) | (v2 >> (64 - 32)); - v0 += v1; - v1 = (v1 << 13) | (v1 >> (64 - 13)); - v1 ^= v0; - v0 = (v0 << 32) | (v0 >> (64 - 32)); - - v2 += v3; - v3 = (v3 << 16) | (v3 >> (64 - 16)); - v3 ^= v2; - - v0 += v3; - v3 = (v3 << 21) | (v3 >> (64 - 21)); - v3 ^= v0; - - v2 += v1; - v1 = (v1 << 17) | (v1 >> (64 - 17)); - v1 ^= v2; - v2 = (v2 << 32) | (v2 >> (64 - 32)); - - v0 ^= *linb; - } - for (var i = 0; i < left; ++i) - { - b |= ((ulong)end[i]) << (8 * i); - } - } - - v3 ^= b; - v0 += v1; - v1 = (v1 << 13) | (v1 >> (64 - 13)); - v1 ^= v0; - v0 = (v0 << 32) | (v0 >> (64 - 32)); - - v2 += v3; - v3 = (v3 << 16) | (v3 >> (64 - 16)); - v3 ^= v2; - - v0 += v3; - v3 = (v3 << 21) | (v3 >> (64 - 21)); - v3 ^= v0; - - v2 += v1; - v1 = (v1 << 17) | (v1 >> (64 - 17)); - v1 ^= v2; - v2 = (v2 << 32) | (v2 >> (64 - 32)); - v0 += v1; - v1 = (v1 << 13) | (v1 >> (64 - 13)); - v1 ^= v0; - v0 = (v0 << 32) | (v0 >> (64 - 32)); - - v2 += v3; - v3 = (v3 << 16) | (v3 >> (64 - 16)); - v3 ^= v2; - - v0 += v3; - v3 = (v3 << 21) | (v3 >> (64 - 21)); - v3 ^= v0; - - v2 += v1; - v1 = (v1 << 17) | (v1 >> (64 - 17)); - v1 ^= v2; - v2 = (v2 << 32) | (v2 >> (64 - 32)); - v0 ^= b; - v2 ^= 0xff; - - v0 += v1; - v1 = (v1 << 13) | (v1 >> (64 - 13)); - v1 ^= v0; - v0 = (v0 << 32) | (v0 >> (64 - 32)); - - v2 += v3; - v3 = (v3 << 16) | (v3 >> (64 - 16)); - v3 ^= v2; - - v0 += v3; - v3 = (v3 << 21) | (v3 >> (64 - 21)); - v3 ^= v0; - - v2 += v1; - v1 = (v1 << 17) | (v1 >> (64 - 17)); - v1 ^= v2; - v2 = (v2 << 32) | (v2 >> (64 - 32)); - v0 += v1; - v1 = (v1 << 13) | (v1 >> (64 - 13)); - v1 ^= v0; - v0 = (v0 << 32) | (v0 >> (64 - 32)); - - v2 += v3; - v3 = (v3 << 16) | (v3 >> (64 - 16)); - v3 ^= v2; - - v0 += v3; - v3 = (v3 << 21) | (v3 >> (64 - 21)); - v3 ^= v0; - - v2 += v1; - v1 = (v1 << 17) | (v1 >> (64 - 17)); - v1 ^= v2; - v2 = (v2 << 32) | (v2 >> (64 - 32)); - v0 += v1; - v1 = (v1 << 13) | (v1 >> (64 - 13)); - v1 ^= v0; - v0 = (v0 << 32) | (v0 >> (64 - 32)); - - v2 += v3; - v3 = (v3 << 16) | (v3 >> (64 - 16)); - v3 ^= v2; - - v0 += v3; - v3 = (v3 << 21) | (v3 >> (64 - 21)); - v3 ^= v0; - - v2 += v1; - v1 = (v1 << 17) | (v1 >> (64 - 17)); - v1 ^= v2; - v2 = (v2 << 32) | (v2 >> (64 - 32)); - v0 += v1; - v1 = (v1 << 13) | (v1 >> (64 - 13)); - v1 ^= v0; - v0 = (v0 << 32) | (v0 >> (64 - 32)); - - v2 += v3; - v3 = (v3 << 16) | (v3 >> (64 - 16)); - v3 ^= v2; - - v0 += v3; - v3 = (v3 << 21) | (v3 >> (64 - 21)); - v3 ^= v0; - - v2 += v1; - v1 = (v1 << 17) | (v1 >> (64 - 17)); - v1 ^= v2; - v2 = (v2 << 32) | (v2 >> (64 - 32)); - - return v0 ^ v1 ^ v2 ^ v3; - } - } -} diff --git a/src/Microsoft.AspNet.SignalR.Core/Infrastructure/StringMinifier.cs b/src/Microsoft.AspNet.SignalR.Core/Infrastructure/StringMinifier.cs deleted file mode 100644 index ecbb249ef..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/Infrastructure/StringMinifier.cs +++ /dev/null @@ -1,97 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -using System; -using System.Collections.Concurrent; -using System.Diagnostics.CodeAnalysis; -using System.Threading; - -namespace Microsoft.AspNet.SignalR.Infrastructure -{ - internal class StringMinifier : IStringMinifier - { - private readonly ConcurrentDictionary _stringMinifier = new ConcurrentDictionary(); - private readonly ConcurrentDictionary _stringMaximizer = new ConcurrentDictionary(); - private int _lastMinifiedKey = -1; - - private readonly Func _createMinifiedString; - - public StringMinifier() - { - _createMinifiedString = CreateMinifiedString; - } - - public string Minify(string fullString) - { - return _stringMinifier.GetOrAdd(fullString, _createMinifiedString); - } - - public string Unminify(string minifiedString) - { - string result; - _stringMaximizer.TryGetValue(minifiedString, out result); - return result; - } - - public void RemoveUnminified(string fullString) - { - string minifiedString; - if (_stringMinifier.TryRemove(fullString, out minifiedString)) - { - string value; - _stringMaximizer.TryRemove(minifiedString, out value); - } - } - - private string CreateMinifiedString(string fullString) - { - var minString = GetStringFromInt((uint)Interlocked.Increment(ref _lastMinifiedKey)); - _stringMaximizer.TryAdd(minString, fullString); - return minString; - } - - [SuppressMessage("Microsoft.Usage", "CA2201:DoNotRaiseReservedExceptionTypes", Justification = "This is a valid exception to throw.")] - private static char GetCharFromSixBitInt(uint num) - { - if (num < 26) - { - return (char)(num + 'A'); - } - if (num < 52) - { - return (char)(num - 26 + 'a'); - } - if (num < 62) - { - return (char)(num - 52 + '0'); - } - if (num == 62) - { - return '_'; - } - if (num == 63) - { - return ':'; - } - throw new IndexOutOfRangeException(); - } - - private static string GetStringFromInt(uint num) - { - const int maxSize = 6; - - // Buffer must be large enough to store any 32 bit uint at 6 bits per character - var buffer = new char[maxSize]; - var index = maxSize; - do - { - // Append next 6 bits of num - buffer[--index] = GetCharFromSixBitInt(num & 0x3f); - num >>= 6; - - // Don't pad output string, but ensure at least one character is written - } while (num != 0); - - return new string(buffer, index, maxSize - index); - } - } -} diff --git a/src/Microsoft.AspNet.SignalR.Core/Infrastructure/TaskQueue.cs b/src/Microsoft.AspNet.SignalR.Core/Infrastructure/TaskQueue.cs deleted file mode 100644 index 1c3d63489..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/Infrastructure/TaskQueue.cs +++ /dev/null @@ -1,128 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -using System; -using System.Diagnostics.CodeAnalysis; -using System.Threading; -using System.Threading.Tasks; - -namespace Microsoft.AspNet.SignalR.Infrastructure -{ - // Allows serial queuing of Task instances - // The tasks are not called on the current synchronization context - - internal sealed class TaskQueue - { - private readonly object _lockObj = new object(); - private Task _lastQueuedTask; - private volatile bool _drained; - private readonly int? _maxSize; - private long _size; - - public TaskQueue() - : this(TaskAsyncHelper.Empty) - { - } - - public TaskQueue(Task initialTask) - { - _lastQueuedTask = initialTask; - } - - [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "This is shared code")] - public TaskQueue(Task initialTask, int maxSize) - { - _lastQueuedTask = initialTask; - _maxSize = maxSize; - } - -#if !CLIENT_NET45 && !CLIENT_NET4 && !NETFX_CORE && !SILVERLIGHT - [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "This is shared code.")] - public IPerformanceCounter QueueSizeCounter { get; set; } -#endif - - [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "This is shared code")] - public bool IsDrained - { - get - { - return _drained; - } - } - - [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "This is shared code")] - public Task Enqueue(Func taskFunc, object state) - { - // Lock the object for as short amount of time as possible - lock (_lockObj) - { - if (_drained) - { - return _lastQueuedTask; - } - - if (_maxSize != null) - { - // Increment the size if the queue - if (Interlocked.Increment(ref _size) > _maxSize) - { - Interlocked.Decrement(ref _size); - - // We failed to enqueue because the size limit was reached - return null; - } - -#if !CLIENT_NET45 && !CLIENT_NET4 && !NETFX_CORE && !SILVERLIGHT - var counter = QueueSizeCounter; - if (counter != null) - { - counter.Increment(); - } -#endif - } - - Task newTask = _lastQueuedTask.Then((next, nextState) => - { - return next(nextState).Finally(s => - { - var queue = (TaskQueue)s; - if (queue._maxSize != null) - { - // Decrement the number of items left in the queue - Interlocked.Decrement(ref queue._size); - -#if !CLIENT_NET45 && !CLIENT_NET4 && !NETFX_CORE && !SILVERLIGHT - var counter = QueueSizeCounter; - if (counter != null) - { - counter.Decrement(); - } -#endif - } - }, - this); - }, - taskFunc, state); - - _lastQueuedTask = newTask; - return newTask; - } - } - - [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "This is shared code")] - public Task Enqueue(Func taskFunc) - { - return Enqueue(state => ((Func)state).Invoke(), taskFunc); - } - - [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "This is shared code")] - public Task Drain() - { - lock (_lockObj) - { - _drained = true; - - return _lastQueuedTask; - } - } - } -} diff --git a/src/Microsoft.AspNet.SignalR.Core/Json/IJsonSerializer.cs b/src/Microsoft.AspNet.SignalR.Core/Json/IJsonSerializer.cs deleted file mode 100644 index d2ee8705d..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/Json/IJsonSerializer.cs +++ /dev/null @@ -1,28 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -using System; -using System.IO; - -namespace Microsoft.AspNet.SignalR.Json -{ - /// - /// Used to serialize and deserialize outgoing/incoming data. - /// - public interface IJsonSerializer - { - /// - /// Serializes the specified object to a . - /// - /// The object to serialize - /// The to serialize the object to. - void Serialize(object value, TextWriter writer); - - /// - /// Deserializes the JSON to a .NET object. - /// - /// The to deserialize the object from. - /// The of object being deserialized. - /// The deserialized object from the JSON string. - object Parse(TextReader reader, Type targetType); - } -} diff --git a/src/Microsoft.AspNet.SignalR.Core/Json/IJsonValue.cs b/src/Microsoft.AspNet.SignalR.Core/Json/IJsonValue.cs deleted file mode 100644 index 4e462abb7..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/Json/IJsonValue.cs +++ /dev/null @@ -1,26 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -using System; - -namespace Microsoft.AspNet.SignalR.Json -{ - /// - /// Represents a JSON value. - /// - public interface IJsonValue - { - /// - /// Converts the parameter value to the specified . - /// - /// The to convert the parameter to. - /// The converted parameter value. - object ConvertTo(Type type); - - /// - /// Determines if the parameter can be converted to the specified . - /// - /// The to check. - /// True if the parameter can be converted to the specified , false otherwise. - bool CanConvertTo(Type type); - } -} diff --git a/src/Microsoft.AspNet.SignalR.Core/Json/IJsonWritable.cs b/src/Microsoft.AspNet.SignalR.Core/Json/IJsonWritable.cs deleted file mode 100644 index d1b73ab37..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/Json/IJsonWritable.cs +++ /dev/null @@ -1,18 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -using System.IO; - -namespace Microsoft.AspNet.SignalR.Json -{ - /// - /// Implementations handle their own serialization to JSON. - /// - public interface IJsonWritable - { - /// - /// Serializes itself to JSON via a . - /// - /// The that receives the JSON serialized object. - void WriteJson(TextWriter writer); - } -} diff --git a/src/Microsoft.AspNet.SignalR.Core/Json/JRawValue.cs b/src/Microsoft.AspNet.SignalR.Core/Json/JRawValue.cs deleted file mode 100644 index df22e7078..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/Json/JRawValue.cs +++ /dev/null @@ -1,42 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -using System; -using System.IO; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; - -namespace Microsoft.AspNet.SignalR.Json -{ - /// - /// An implementation of IJsonValue over JSON.NET - /// - internal class JRawValue : IJsonValue - { - private readonly string _value; - - public JRawValue(JRaw value) - { - _value = value.ToString(); - } - - public object ConvertTo(Type type) - { - // A non generic implementation of ToObject on JToken - using (var jsonReader = new StringReader(_value)) - { - var settings = new JsonSerializerSettings - { - MaxDepth = 20 - }; - var serializer = JsonSerializer.Create(settings); - return serializer.Deserialize(jsonReader, type); - } - } - - public bool CanConvertTo(Type type) - { - // TODO: Implement when we implement better method overload resolution - return true; - } - } -} diff --git a/src/Microsoft.AspNet.SignalR.Core/Json/JsonNetSerializer.cs b/src/Microsoft.AspNet.SignalR.Core/Json/JsonNetSerializer.cs deleted file mode 100644 index 4b0f53cf5..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/Json/JsonNetSerializer.cs +++ /dev/null @@ -1,69 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -using System; -using System.IO; -using Newtonsoft.Json; - -namespace Microsoft.AspNet.SignalR.Json -{ - /// - /// Default implementation over Json.NET. - /// - public class JsonNetSerializer : IJsonSerializer - { - private readonly JsonSerializer _serializer; - - /// - /// Initializes a new instance of the class. - /// - public JsonNetSerializer() - : this(new JsonSerializerSettings()) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// The to use when serializing and deserializing. - public JsonNetSerializer(JsonSerializerSettings settings) - { - if (settings == null) - { - throw new ArgumentNullException("settings"); - } - - // Just override it anyways (we're saving the user) - settings.MaxDepth = 20; - _serializer = JsonSerializer.Create(settings); - } - - /// - /// Deserializes the JSON to a .NET object. - /// - /// The JSON to deserialize. - /// The of object being deserialized. - /// The deserialized object from the JSON string. - public object Parse(TextReader reader, Type targetType) - { - return _serializer.Deserialize(reader, targetType); - } - - /// - /// Serializes the specified object to a . - /// - /// The object to serialize - /// The to serialize the object to. - public void Serialize(object value, TextWriter writer) - { - var selfSerializer = value as IJsonWritable; - if (selfSerializer != null) - { - selfSerializer.WriteJson(writer); - } - else - { - _serializer.Serialize(writer, value); - } - } - } -} diff --git a/src/Microsoft.AspNet.SignalR.Core/Json/JsonSerializerExtensions.cs b/src/Microsoft.AspNet.SignalR.Core/Json/JsonSerializerExtensions.cs deleted file mode 100644 index 2a34ad264..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/Json/JsonSerializerExtensions.cs +++ /dev/null @@ -1,77 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -using System; -using System.Globalization; -using System.IO; -using System.Text; -using Microsoft.AspNet.SignalR.Infrastructure; - -namespace Microsoft.AspNet.SignalR.Json -{ - /// - /// Extensions for . - /// - public static class JsonSerializerExtensions - { - /// - /// Deserializes the JSON to a .NET object. - /// - /// The serializer - /// The of object being deserialized. - /// The JSON to deserialize - /// The deserialized object from the JSON string. - public static T Parse(this IJsonSerializer serializer, string json) - { - if (serializer == null) - { - throw new ArgumentNullException("serializer"); - } - - using (var reader = new StringReader(json)) - { - return (T)serializer.Parse(reader, typeof(T)); - } - } - - /// - /// Deserializes the JSON to a .NET object. - /// - /// The serializer - /// The of object being deserialized. - /// The JSON buffer to deserialize - /// The encoding to use. - /// The deserialized object from the JSON string. - public static T Parse(this IJsonSerializer serializer, ArraySegment jsonBuffer, Encoding encoding) - { - if (serializer == null) - { - throw new ArgumentNullException("serializer"); - } - - using (var reader = new ArraySegmentTextReader(jsonBuffer, encoding)) - { - return (T)serializer.Parse(reader, typeof(T)); - } - } - - /// - /// Serializes the specified object to a JSON string. - /// - /// The serializer - /// The object to serailize. - /// A JSON string representation of the object. - public static string Stringify(this IJsonSerializer serializer, object value) - { - if (serializer == null) - { - throw new ArgumentNullException("serializer"); - } - - using (var writer = new StringWriter(CultureInfo.InvariantCulture)) - { - serializer.Serialize(value, writer); - return writer.ToString(); - } - } - } -} diff --git a/src/Microsoft.AspNet.SignalR.Core/Json/JsonUtility.cs b/src/Microsoft.AspNet.SignalR.Core/Json/JsonUtility.cs deleted file mode 100644 index 534b261ba..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/Json/JsonUtility.cs +++ /dev/null @@ -1,124 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -using System; -using System.Globalization; -using System.Linq; -using System.Text; - -namespace Microsoft.AspNet.SignalR.Json -{ - /// - /// Helper class for common JSON operations. - /// - public static class JsonUtility - { - // JavaScript keywords taken from http://www.ecma-international.org/publications/files/ECMA-ST/Ecma-262.pdf - // Sections: 7.6.1.1, 7.6.1.2 - // Plus the implicity globals "NaN", "undefined", "Infinity" - private static readonly string[] _jsKeywords = new[] { "break", "do", "instanceof", "typeof", "case", "else", "new", "var", "catch", "finally", "return", "void", "continue", "for", "switch", "while", "debugger", "function", "this", "with", "default", "if", "throw", "delete", "in", "try", "class", "enum", "extends", "super", "const", "export", "import", "implements", "let", "private", "public", "yield", "interface", "package", "protected", "static", "NaN", "undefined", "Infinity" }; - - /// - /// Converts the specified name to camel case. - /// - /// The name to convert. - /// A camel cased version of the specified name. - public static string CamelCase(string name) - { - if (name == null) - { - throw new ArgumentNullException("name"); - } - - return String.Join(".", name.Split('.').Select(n => Char.ToLower(n[0], CultureInfo.InvariantCulture) + n.Substring(1))); - } - - /// - /// Gets a string that returns JSON mime type "application/json; charset=UTF-8". - /// - public static string JsonMimeType - { - get { return "application/json; charset=UTF-8"; } - } - - /// - /// Gets a string that returns JSONP mime type "application/javascript; charset=UTF-8". - /// - public static string JavaScriptMimeType - { - get { return "application/javascript; charset=UTF-8"; } - } - - public static string CreateJsonpCallback(string callback, string payload) - { - var sb = new StringBuilder(); - if (!IsValidJavaScriptCallback(callback)) - { - throw new InvalidOperationException(); - } - sb.AppendFormat("{0}(", callback).Append(payload).Append(");"); - return sb.ToString(); - } - - internal static bool IsValidJavaScriptCallback(string callback) - { - if (String.IsNullOrWhiteSpace(callback)) - { - return false; - } - - var identifiers = callback.Split('.'); - - // Check each identifier to ensure it's a valid JS identifier - foreach (var identifier in identifiers) - { - if (!IsValidJavaScriptFunctionName(identifier)) - { - return false; - } - } - - return true; - } - - internal static bool IsValidJavaScriptFunctionName(string name) - { - if (String.IsNullOrWhiteSpace(name) || IsJavaScriptReservedWord(name)) - { - return false; - } - - // JavaScript identifier must start with a letter or a '$' or an '_' char - var firstChar = name[0]; - if (!IsValidJavaScriptIdentifierStartChar(firstChar)) - { - return false; - } - - for (var i = 1; i < name.Length; i++) - { - // Characters can be a letter, digit, '$' or '_' - if (!IsValidJavaScriptIdenfitierNonStartChar(name[i])) - { - return false; - } - } - - return true; - } - - private static bool IsValidJavaScriptIdentifierStartChar(char startChar) - { - return Char.IsLetter(startChar) || startChar == '$' || startChar == '_'; - } - - private static bool IsValidJavaScriptIdenfitierNonStartChar(char identifierChar) - { - return Char.IsLetterOrDigit(identifierChar) || identifierChar == '$' || identifierChar == '_'; - } - - private static bool IsJavaScriptReservedWord(string word) - { - return _jsKeywords.Contains(word); - } - } -} diff --git a/src/Microsoft.AspNet.SignalR.Core/Json/SipHashBasedDictionaryConverter.cs b/src/Microsoft.AspNet.SignalR.Core/Json/SipHashBasedDictionaryConverter.cs deleted file mode 100644 index 418d53cf2..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/Json/SipHashBasedDictionaryConverter.cs +++ /dev/null @@ -1,113 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -using System; -using System.Collections.Generic; -using Microsoft.AspNet.SignalR.Infrastructure; -using Newtonsoft.Json; - -namespace Microsoft.AspNet.SignalR.Json -{ - /// - /// A converter for dictionaries that uses a SipHash comparer - /// - internal class SipHashBasedDictionaryConverter : JsonConverter - { - public override bool CanWrite - { - get - { - return false; - } - } - - public override bool CanConvert(Type objectType) - { - return objectType == typeof(IDictionary); - } - - public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) - { - return ReadJsonObject(reader); - } - - private object ReadJsonObject(JsonReader reader) - { - switch (reader.TokenType) - { - case JsonToken.StartObject: - return ReadObject(reader); - case JsonToken.StartArray: - return ReadArray(reader); - case JsonToken.Integer: - case JsonToken.Float: - case JsonToken.String: - case JsonToken.Boolean: - case JsonToken.Undefined: - case JsonToken.Null: - case JsonToken.Date: - case JsonToken.Bytes: - return reader.Value; - default: - throw new NotSupportedException(); - - } - } - - private object ReadArray(JsonReader reader) - { - var array = new List(); - - while (reader.Read()) - { - switch (reader.TokenType) - { - default: - object value = ReadJsonObject(reader); - - array.Add(value); - break; - case JsonToken.EndArray: - return array; - } - } - - throw new JsonSerializationException(Resources.Error_ParseObjectFailed); - } - - private object ReadObject(JsonReader reader) - { - var obj = new Dictionary(new SipHashBasedStringEqualityComparer()); - - while (reader.Read()) - { - switch (reader.TokenType) - { - case JsonToken.PropertyName: - string propertyName = reader.Value.ToString(); - - if (!reader.Read()) - { - throw new JsonSerializationException(Resources.Error_ParseObjectFailed); - } - - object value = ReadJsonObject(reader); - - obj[propertyName] = value; - break; - case JsonToken.EndObject: - return obj; - default: - throw new JsonSerializationException(Resources.Error_ParseObjectFailed); - - } - } - - throw new JsonSerializationException(Resources.Error_ParseObjectFailed); - } - - public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) - { - throw new NotImplementedException(); - } - } -} diff --git a/src/Microsoft.AspNet.SignalR.Core/Messaging/Command.cs b/src/Microsoft.AspNet.SignalR.Core/Messaging/Command.cs deleted file mode 100644 index c14c97af9..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/Messaging/Command.cs +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -using System; - -namespace Microsoft.AspNet.SignalR.Messaging -{ - public class Command - { - public Command() - { - Id = Guid.NewGuid().ToString(); - } - - public bool WaitForAck { get; set; } - public string Id { get; private set; } - public CommandType CommandType { get; set; } - public string Value { get; set; } - } -} diff --git a/src/Microsoft.AspNet.SignalR.Core/Messaging/CommandType.cs b/src/Microsoft.AspNet.SignalR.Core/Messaging/CommandType.cs deleted file mode 100644 index 12bff330b..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/Messaging/CommandType.cs +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -namespace Microsoft.AspNet.SignalR.Messaging -{ - public enum CommandType - { - AddToGroup, - RemoveFromGroup, - Disconnect, - Abort - } -} diff --git a/src/Microsoft.AspNet.SignalR.Core/Messaging/Cursor.cs b/src/Microsoft.AspNet.SignalR.Core/Messaging/Cursor.cs deleted file mode 100644 index ad915b562..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/Messaging/Cursor.cs +++ /dev/null @@ -1,264 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -using System; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.Text; - -namespace Microsoft.AspNet.SignalR.Messaging -{ - internal unsafe class Cursor - { - private static char[] _escapeChars = new[] { '\\', '|', ',' }; - private string _escapedKey; - - public string Key { get; private set; } - public ulong Id { get; set; } - - public static Cursor Clone(Cursor cursor) - { - return new Cursor(cursor.Key, cursor.Id, cursor._escapedKey); - } - - public Cursor(string key, ulong id) - : this(key, id, Escape(key)) - { - } - - public Cursor(string key, ulong id, string minifiedKey) - { - Key = key; - Id = id; - _escapedKey = minifiedKey; - } - - public static void WriteCursors(TextWriter textWriter, IList cursors, string prefix) - { - textWriter.Write(prefix); - - for (int i = 0; i < cursors.Count; i++) - { - if (i > 0) - { - textWriter.Write('|'); - } - Cursor cursor = cursors[i]; - textWriter.Write(cursor._escapedKey); - textWriter.Write(','); - WriteUlongAsHexToBuffer(cursor.Id, textWriter); - } - } - - internal static void WriteUlongAsHexToBuffer(ulong value, TextWriter textWriter) - { - // This tracks the length of the output and serves as the index for the next character to be written into the pBuffer. - // The length could reach up to 16 characters, so at least that much space should remain in the pBuffer. - int length = 0; - - // Write the hex value from left to right into the buffer without zero padding. - for (int i = 0; i < 16; i++) - { - // Convert the first 4 bits of the value to a valid hex character. - char hexChar = Int32ToHex((int)(value >> 60)); - value <<= 4; - - // Don't increment length if it would just add zero padding - if (length != 0 || hexChar != '0') - { - textWriter.Write(hexChar); - length++; - } - } - - if (length == 0) - { - textWriter.Write('0'); - } - } - - private static char Int32ToHex(int value) - { - return (value < 10) ? (char)(value + '0') : (char)(value - 10 + 'A'); - } - - private static string Escape(string value) - { - // Nothing to do, so bail - if (value.IndexOfAny(_escapeChars) == -1) - { - return value; - } - - var sb = new StringBuilder(); - // \\ = \ - // \| = | - // \, = , - foreach (var ch in value) - { - switch (ch) - { - case '\\': - sb.Append('\\').Append(ch); - break; - case '|': - sb.Append('\\').Append(ch); - break; - case ',': - sb.Append('\\').Append(ch); - break; - default: - sb.Append(ch); - break; - } - } - - return sb.ToString(); - } - - public static List GetCursors(string cursor, string prefix) - { - return GetCursors(cursor, prefix, s => s); - } - - public static List GetCursors(string cursor, string prefix, Func keyMaximizer) - { - return GetCursors(cursor, prefix, (key, state) => ((Func)state).Invoke(key), keyMaximizer); - } - - public static List GetCursors(string cursor, string prefix, Func keyMaximizer, object state) - { - // Technically GetCursors should never be called with a null value, so this is extra cautious - if (String.IsNullOrEmpty(cursor)) - { - throw new FormatException(Resources.Error_InvalidCursorFormat); - } - - // If the cursor does not begin with the prefix stream, it isn't necessarily a formatting problem. - // The cursor with a different prefix might have had different, but also valid, formatting. - // Null should be returned so new cursors will be generated - if (!cursor.StartsWith(prefix, StringComparison.Ordinal)) - { - return null; - } - - var signals = new HashSet(); - var cursors = new List(); - string currentKey = null; - string currentEscapedKey = null; - ulong currentId; - bool escape = false; - bool consumingKey = true; - var sb = new StringBuilder(); - var sbEscaped = new StringBuilder(); - Cursor parsedCursor; - - for (int i = prefix.Length; i < cursor.Length; i++) - { - var ch = cursor[i]; - - // escape can only be true if we are consuming the key - if (escape) - { - if (ch != '\\' && ch != ',' && ch != '|') - { - throw new FormatException(Resources.Error_InvalidCursorFormat); - } - - sb.Append(ch); - sbEscaped.Append(ch); - escape = false; - } - else - { - if (ch == '\\') - { - if (!consumingKey) - { - throw new FormatException(Resources.Error_InvalidCursorFormat); - } - - sbEscaped.Append('\\'); - escape = true; - } - else if (ch == ',') - { - if (!consumingKey) - { - throw new FormatException(Resources.Error_InvalidCursorFormat); - } - - // For now String.Empty is an acceptable key, but this should change once we verify - // that empty keys cannot be created legitimately. - currentKey = keyMaximizer(sb.ToString(), state); - - // If the keyMap cannot find a key, we cannot create an array of cursors. - // This most likely means there was an AppDomain restart or a misbehaving client. - if (currentKey == null) - { - return null; - } - // Don't allow duplicate keys - if (!signals.Add(currentKey)) - { - throw new FormatException(Resources.Error_InvalidCursorFormat); - } - - currentEscapedKey = sbEscaped.ToString(); - - sb.Clear(); - sbEscaped.Clear(); - consumingKey = false; - } - else if (ch == '|') - { - if (consumingKey) - { - throw new FormatException(Resources.Error_InvalidCursorFormat); - } - - ParseCursorId(sb, out currentId); - - parsedCursor = new Cursor(currentKey, currentId, currentEscapedKey); - - cursors.Add(parsedCursor); - sb.Clear(); - consumingKey = true; - } - else - { - sb.Append(ch); - if (consumingKey) - { - sbEscaped.Append(ch); - } - } - } - } - - if (consumingKey) - { - throw new FormatException(Resources.Error_InvalidCursorFormat); - } - - ParseCursorId(sb, out currentId); - - parsedCursor = new Cursor(currentKey, currentId, currentEscapedKey); - - cursors.Add(parsedCursor); - - return cursors; - } - - private static void ParseCursorId(StringBuilder sb, out ulong id) - { - string value = sb.ToString(); - id = UInt64.Parse(value, NumberStyles.HexNumber, CultureInfo.InvariantCulture); - } - - public override string ToString() - { - return Key; - } - } -} diff --git a/src/Microsoft.AspNet.SignalR.Core/Messaging/DefaultSubscription.cs b/src/Microsoft.AspNet.SignalR.Core/Messaging/DefaultSubscription.cs deleted file mode 100644 index 08e4b8049..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/Messaging/DefaultSubscription.cs +++ /dev/null @@ -1,239 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Globalization; -using System.IO; -using System.Security.Cryptography; -using System.Threading.Tasks; -using Microsoft.AspNet.SignalR.Infrastructure; - -namespace Microsoft.AspNet.SignalR.Messaging -{ - internal class DefaultSubscription : Subscription - { - internal static string _defaultCursorPrefix = GetCursorPrefix(); - - private List _cursors; - private List _cursorTopics; - - private readonly IStringMinifier _stringMinifier; - - public DefaultSubscription(string identity, - IList eventKeys, - TopicLookup topics, - string cursor, - Func> callback, - int maxMessages, - IStringMinifier stringMinifier, - IPerformanceCounterManager counters, - object state) : - base(identity, eventKeys, callback, maxMessages, counters, state) - { - _stringMinifier = stringMinifier; - - if (String.IsNullOrEmpty(cursor)) - { - _cursors = GetCursorsFromEventKeys(EventKeys, topics); - } - else - { - // Ensure delegate continues to use the C# Compiler static delegate caching optimization. - _cursors = Cursor.GetCursors(cursor, _defaultCursorPrefix, (k, s) => UnminifyCursor(k, s), stringMinifier) ?? GetCursorsFromEventKeys(EventKeys, topics); - } - - _cursorTopics = new List(); - - if (!String.IsNullOrEmpty(cursor)) - { - // Update all of the cursors so we're within the range - for (int i = _cursors.Count - 1; i >= 0; i--) - { - Cursor c = _cursors[i]; - Topic topic; - if (!EventKeys.Contains(c.Key)) - { - _cursors.Remove(c); - } - else if (!topics.TryGetValue(_cursors[i].Key, out topic) || _cursors[i].Id > topic.Store.GetMessageCount()) - { - UpdateCursor(c.Key, 0); - } - } - } - - // Add dummy entries so they can be filled in - for (int i = 0; i < _cursors.Count; i++) - { - _cursorTopics.Add(null); - } - } - - private static string UnminifyCursor(string key, object state) - { - return ((IStringMinifier)state).Unminify(key); - } - - public override bool AddEvent(string eventKey, Topic topic) - { - base.AddEvent(eventKey, topic); - - lock (_cursors) - { - // O(n), but small n and it's not common - var index = _cursors.FindIndex(c => c.Key == eventKey); - if (index == -1) - { - _cursors.Add(new Cursor(eventKey, GetMessageId(topic), _stringMinifier.Minify(eventKey))); - - _cursorTopics.Add(topic); - - return true; - } - - return false; - } - } - - public override void RemoveEvent(string eventKey) - { - base.RemoveEvent(eventKey); - - lock (_cursors) - { - var index = _cursors.FindIndex(c => c.Key == eventKey); - if (index != -1) - { - _cursors.RemoveAt(index); - _cursorTopics.RemoveAt(index); - } - } - } - - public override void SetEventTopic(string eventKey, Topic topic) - { - base.SetEventTopic(eventKey, topic); - - lock (_cursors) - { - // O(n), but small n and it's not common - var index = _cursors.FindIndex(c => c.Key == eventKey); - if (index != -1) - { - _cursorTopics[index] = topic; - } - } - } - - public override void WriteCursor(TextWriter textWriter) - { - lock (_cursors) - { - Cursor.WriteCursors(textWriter, _cursors, _defaultCursorPrefix); - } - } - - [SuppressMessage("Microsoft.Design", "CA1062:Validate arguments of public methods", MessageId = "0", Justification = "It is called from the base class")] - protected override void PerformWork(IList> items, out int totalCount, out object state) - { - totalCount = 0; - - lock (_cursors) - { - var cursors = new ulong[_cursors.Count]; - for (int i = 0; i < _cursors.Count; i++) - { - MessageStoreResult storeResult = _cursorTopics[i].Store.GetMessages(_cursors[i].Id, MaxMessages); - cursors[i] = storeResult.FirstMessageId + (ulong)storeResult.Messages.Count; - - if (storeResult.Messages.Count > 0) - { - items.Add(storeResult.Messages); - totalCount += storeResult.Messages.Count; - } - } - - // Return the state as a list of cursors - state = cursors; - } - } - - protected override void BeforeInvoke(object state) - { - lock (_cursors) - { - // Update the list of cursors before invoking anything - var nextCursors = (ulong[])state; - for (int i = 0; i < _cursors.Count; i++) - { - _cursors[i].Id = nextCursors[i]; - } - } - } - - private bool UpdateCursor(string key, ulong id) - { - lock (_cursors) - { - // O(n), but small n and it's not common - var index = _cursors.FindIndex(c => c.Key == key); - if (index != -1) - { - _cursors[index].Id = id; - return true; - } - - return false; - } - } - - private List GetCursorsFromEventKeys(IList eventKeys, TopicLookup topics) - { - var list = new List(eventKeys.Count); - foreach (var eventKey in eventKeys) - { - var cursor = new Cursor(eventKey, GetMessageId(topics, eventKey), _stringMinifier.Minify(eventKey)); - list.Add(cursor); - } - - return list; - } - - private static string GetCursorPrefix() - { - using (var rng = new RNGCryptoServiceProvider()) - { - var data = new byte[4]; - rng.GetBytes(data); - - using (var writer = new StringWriter(CultureInfo.InvariantCulture)) - { - var randomValue = (ulong)BitConverter.ToUInt32(data, 0); - Cursor.WriteUlongAsHexToBuffer(randomValue, writer); - return "d-" + writer.ToString() + "-"; - } - } - } - - private static ulong GetMessageId(TopicLookup topics, string key) - { - Topic topic; - if (topics.TryGetValue(key, out topic)) - { - return GetMessageId(topic); - } - return 0; - } - - private static ulong GetMessageId(Topic topic) - { - if (topic == null) - { - return 0; - } - - return topic.Store.GetMessageCount(); - } - } -} diff --git a/src/Microsoft.AspNet.SignalR.Core/Messaging/IMessageBus.cs b/src/Microsoft.AspNet.SignalR.Core/Messaging/IMessageBus.cs deleted file mode 100644 index e229d739d..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/Messaging/IMessageBus.cs +++ /dev/null @@ -1,28 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -using System; -using System.Threading.Tasks; - -namespace Microsoft.AspNet.SignalR.Messaging -{ - public interface IMessageBus - { - /// - /// - /// - /// - /// - Task Publish(Message message); - - /// - /// - /// - /// - /// - /// - /// - /// - /// - IDisposable Subscribe(ISubscriber subscriber, string cursor, Func> callback, int maxMessages, object state); - } -} diff --git a/src/Microsoft.AspNet.SignalR.Core/Messaging/ISubscriber.cs b/src/Microsoft.AspNet.SignalR.Core/Messaging/ISubscriber.cs deleted file mode 100644 index 317ab4d5f..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/Messaging/ISubscriber.cs +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -using System; -using System.Collections.Generic; -using System.IO; - -namespace Microsoft.AspNet.SignalR.Messaging -{ - public interface ISubscriber - { - IList EventKeys { get; } - - Action WriteCursor { get; set; } - - string Identity { get; } - - event Action EventKeyAdded; - - event Action EventKeyRemoved; - - Subscription Subscription { get; set; } - } -} diff --git a/src/Microsoft.AspNet.SignalR.Core/Messaging/ISubscription.cs b/src/Microsoft.AspNet.SignalR.Core/Messaging/ISubscription.cs deleted file mode 100644 index fa941ff74..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/Messaging/ISubscription.cs +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -using System.Threading.Tasks; - -namespace Microsoft.AspNet.SignalR.Messaging -{ - public interface ISubscription - { - string Identity { get; } - - bool SetQueued(); - bool UnsetQueued(); - - Task Work(); - } -} diff --git a/src/Microsoft.AspNet.SignalR.Core/Messaging/LocalEventKeyInfo.cs b/src/Microsoft.AspNet.SignalR.Core/Messaging/LocalEventKeyInfo.cs deleted file mode 100644 index 81ef9c362..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/Messaging/LocalEventKeyInfo.cs +++ /dev/null @@ -1,29 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -using System; - -namespace Microsoft.AspNet.SignalR.Messaging -{ - public class LocalEventKeyInfo - { - private readonly WeakReference _storeReference; - - public LocalEventKeyInfo(string key, ulong id, MessageStore store) - { - // Don't hold onto MessageStores that would otherwise be GC'd - _storeReference = new WeakReference(store); - Key = key; - Id = id; - } - - public string Key { get; private set; } - public ulong Id { get; private set; } - public MessageStore MessageStore - { - get - { - return _storeReference.Target as MessageStore; - } - } - } -} diff --git a/src/Microsoft.AspNet.SignalR.Core/Messaging/Message.cs b/src/Microsoft.AspNet.SignalR.Core/Messaging/Message.cs deleted file mode 100644 index bb1dbffd0..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/Messaging/Message.cs +++ /dev/null @@ -1,155 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -using System; -using System.Diagnostics.CodeAnalysis; -using System.IO; -using System.Text; - -namespace Microsoft.AspNet.SignalR.Messaging -{ - public class Message - { - private static readonly byte[] _zeroByteBuffer = new byte[0]; - private static readonly UTF8Encoding _encoding = new UTF8Encoding(); - - public Message() - { - Encoding = _encoding; - } - - public Message(string source, string key, string value) - { - if (source == null) - { - throw new ArgumentNullException("source"); - } - - if (key == null) - { - throw new ArgumentNullException("key"); - } - - Source = source; - Key = key; - Encoding = _encoding; - Value = value == null ? new ArraySegment(_zeroByteBuffer) : new ArraySegment(Encoding.GetBytes(value)); - } - - public Message(string source, string key, ArraySegment value) - : this() - { - if (source == null) - { - throw new ArgumentNullException("source"); - } - - if (key == null) - { - throw new ArgumentNullException("key"); - } - - Source = source; - Key = key; - Value = value; - } - - /// - /// Which connection the message originated from - /// - public string Source { get; set; } - - /// - /// The signal for the message (connection id, group, etc) - /// - public string Key { get; set; } - - /// - /// The message payload - /// - public ArraySegment Value { get; set; } - - /// - /// The command id if this message is a command - /// - public string CommandId { get; set; } - - /// - /// Determines if the caller should wait for acknowledgement for this message - /// - public bool WaitForAck { get; set; } - - /// - /// Determines if this message is itself an ACK - /// - public bool IsAck { get; set; } - - /// - /// A list of connection ids to filter out - /// - public string Filter { get; set; } - - /// - /// The encoding of the message - /// - public Encoding Encoding { get; private set; } - - /// - /// The payload id. Only used in scaleout scenarios - /// - public ulong MappingId { get; set; } - - /// - /// The stream index this message came from. Only used the scaleout scenarios. - /// - public int StreamIndex { get; set; } - - public bool IsCommand - { - get - { - return !String.IsNullOrEmpty(CommandId); - } - } - - [SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate", Justification = "This may be expensive")] - public string GetString() - { - // If there's no encoding this is a raw binary payload - if (Encoding == null) - { - throw new NotSupportedException(); - } - - return Encoding.GetString(Value.Array, Value.Offset, Value.Count); - } - - public void WriteTo(Stream stream) - { - var binaryWriter = new BinaryWriter(stream); - binaryWriter.Write(Source); - binaryWriter.Write(Key); - binaryWriter.Write(Value.Count); - binaryWriter.Write(Value.Array, Value.Offset, Value.Count); - binaryWriter.Write(CommandId ?? String.Empty); - binaryWriter.Write(WaitForAck); - binaryWriter.Write(IsAck); - binaryWriter.Write(Filter ?? String.Empty); - } - - public static Message ReadFrom(Stream stream) - { - var message = new Message(); - var binaryReader = new BinaryReader(stream); - message.Source = binaryReader.ReadString(); - message.Key = binaryReader.ReadString(); - int bytes = binaryReader.ReadInt32(); - message.Value = new ArraySegment(binaryReader.ReadBytes(bytes)); - message.CommandId = binaryReader.ReadString(); - message.WaitForAck = binaryReader.ReadBoolean(); - message.IsAck = binaryReader.ReadBoolean(); - message.Filter = binaryReader.ReadString(); - - return message; - } - } -} diff --git a/src/Microsoft.AspNet.SignalR.Core/Messaging/MessageBroker.cs b/src/Microsoft.AspNet.SignalR.Core/Messaging/MessageBroker.cs deleted file mode 100644 index a87b90a33..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/Messaging/MessageBroker.cs +++ /dev/null @@ -1,330 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.AspNet.SignalR.Infrastructure; - -namespace Microsoft.AspNet.SignalR.Messaging -{ - /// - /// This class is the main coordinator. It schedules work to be done for a particular subscription - /// and has an algorithm for choosing a number of workers (thread pool threads), to handle - /// the scheduled work. - /// - public class MessageBroker : IDisposable - { - private readonly Queue _queue = new Queue(); - - private readonly IPerformanceCounterManager _counters; - - // The maximum number of workers (threads) allowed to process all incoming messages - private readonly int _maxWorkers; - - // The maximum number of workers that can be left to idle (not busy but allocated) - private readonly int _maxIdleWorkers; - - // The number of allocated workers (currently running) - private int _allocatedWorkers; - - // The number of workers that are *actually* doing work - private int _busyWorkers; - - // Determines if the broker was disposed and should stop doing all work. - private bool _disposed; - - public MessageBroker(IPerformanceCounterManager performanceCounterManager) - : this(performanceCounterManager, 3 * Environment.ProcessorCount, Environment.ProcessorCount) - { - } - - public MessageBroker(IPerformanceCounterManager performanceCounterManager, int maxWorkers, int maxIdleWorkers) - { - _counters = performanceCounterManager; - _maxWorkers = maxWorkers; - _maxIdleWorkers = maxIdleWorkers; - } - - public TraceSource Trace - { - get; - set; - } - - public int AllocatedWorkers - { - get - { - return _allocatedWorkers; - } - } - - public int BusyWorkers - { - get - { - return _busyWorkers; - } - } - - public void Schedule(ISubscription subscription) - { - if (subscription == null) - { - throw new ArgumentNullException("subscription"); - } - - if (_disposed) - { - // Don't queue up new work if we've disposed the broker - return; - } - - if (subscription.SetQueued()) - { - lock (_queue) - { - _queue.Enqueue(subscription); - Monitor.Pulse(_queue); - AddWorker(); - } - } - } - - private void AddWorker() - { - // Only create a new worker if everyone is busy (up to the max) - if (_allocatedWorkers < _maxWorkers) - { - if (_allocatedWorkers == _busyWorkers) - { - _counters.MessageBusAllocatedWorkers.RawValue = Interlocked.Increment(ref _allocatedWorkers); - - Trace.TraceEvent(TraceEventType.Verbose, 0, "Creating a worker, allocated={0}, busy={1}", _allocatedWorkers, _busyWorkers); - - ThreadPool.QueueUserWorkItem(ProcessWork); - } - else - { - Trace.TraceEvent(TraceEventType.Verbose, 0, "No need to add a worker because all allocated workers are not busy, allocated={0}, busy={1}", _allocatedWorkers, _busyWorkers); - } - } - else - { - Trace.TraceEvent(TraceEventType.Verbose, 0, "Already at max workers, allocated={0}, busy={1}", _allocatedWorkers, _busyWorkers); - } - } - - private void ProcessWork(object state) - { - Task pumpTask = PumpAsync(); - - if (pumpTask.IsCompleted) - { - ProcessWorkSync(pumpTask); - } - else - { - ProcessWorkAsync(pumpTask); - } - - } - - [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "We want to avoid user code taking the process down.")] - private void ProcessWorkSync(Task pumpTask) - { - try - { - pumpTask.Wait(); - } - catch (Exception ex) - { - Trace.TraceEvent(TraceEventType.Error, 0, "Failed to process work - " + ex.GetBaseException()); - } - finally - { - // After the pump runs decrement the number of workers in flight - _counters.MessageBusAllocatedWorkers.RawValue = Interlocked.Decrement(ref _allocatedWorkers); - } - } - - private void ProcessWorkAsync(Task pumpTask) - { - pumpTask.ContinueWith(task => - { - // After the pump runs decrement the number of workers in flight - _counters.MessageBusAllocatedWorkers.RawValue = Interlocked.Decrement(ref _allocatedWorkers); - - if (task.IsFaulted) - { - Trace.TraceEvent(TraceEventType.Error, 0, "Failed to process work - " + task.Exception.GetBaseException()); - } - }); - } - - private Task PumpAsync() - { - var tcs = new TaskCompletionSource(); - PumpImpl(tcs); - return tcs.Task; - } - - [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "We want to avoid user code taking the process down.")] - private void PumpImpl(TaskCompletionSource taskCompletionSource, ISubscription subscription = null) - { - - Process: - // If we were doing work before and now we've been disposed just kill this worker early - if (_disposed) - { - taskCompletionSource.TrySetResult(null); - return; - } - - Debug.Assert(_allocatedWorkers <= _maxWorkers, "How did we pass the max?"); - - // If we're withing the acceptable limit of idleness, just keep running - int idleWorkers = _allocatedWorkers - _busyWorkers; - - if (subscription != null || idleWorkers <= _maxIdleWorkers) - { - // We already have a subscription doing work so skip the queue - if (subscription == null) - { - lock (_queue) - { - while (_queue.Count == 0) - { - Monitor.Wait(_queue); - - // When disposing, all workers are pulsed so that they can quit - // if they're waiting for things to do (idle) - if (_disposed) - { - taskCompletionSource.TrySetResult(null); - return; - } - } - - subscription = _queue.Dequeue(); - } - } - - _counters.MessageBusBusyWorkers.RawValue = Interlocked.Increment(ref _busyWorkers); - - Task workTask = subscription.Work(); - - if (workTask.IsCompleted) - { - try - { - workTask.Wait(); - - goto Process; - } - catch (Exception ex) - { - Trace.TraceEvent(TraceEventType.Error, 0, "Work failed for " + subscription.Identity + ": " + ex.GetBaseException()); - - goto Process; - } - finally - { - if (!subscription.UnsetQueued() || workTask.IsFaulted || workTask.IsCanceled) - { - // If we don't have more work to do just make the subscription null - subscription = null; - } - - _counters.MessageBusBusyWorkers.RawValue = Interlocked.Decrement(ref _busyWorkers); - - Debug.Assert(_busyWorkers >= 0, "The number of busy workers has somehow gone negative"); - } - } - else - { - PumpImplAsync(workTask, subscription, taskCompletionSource); - } - } - else - { - taskCompletionSource.TrySetResult(null); - } - } - - private void PumpImplAsync(Task workTask, ISubscription subscription, TaskCompletionSource taskCompletionSource) - { - // Async path - workTask.ContinueWith(task => - { - bool moreWork = subscription.UnsetQueued(); - - _counters.MessageBusBusyWorkers.RawValue = Interlocked.Decrement(ref _busyWorkers); - - Debug.Assert(_busyWorkers >= 0, "The number of busy workers has somehow gone negative"); - - if (task.IsFaulted) - { - Trace.TraceEvent(TraceEventType.Error, 0, "Work failed for " + subscription.Identity + ": " + task.Exception.GetBaseException()); - } - - if (moreWork && !task.IsFaulted && !task.IsCanceled) - { - PumpImpl(taskCompletionSource, subscription); - } - else - { - // Don't reference the subscription anymore - subscription = null; - - PumpImpl(taskCompletionSource); - } - }); - } - - protected virtual void Dispose(bool disposing) - { - if (disposing) - { - if (!_disposed) - { - _disposed = true; - - Trace.TraceEvent(TraceEventType.Verbose, 0, "Dispoing the broker"); - - if (MonoUtility.IsRunningMono) - { - return; - } - - // Wait for all threads to stop working - WaitForDrain(); - - Trace.TraceEvent(TraceEventType.Verbose, 0, "Disposed the broker"); - } - } - } - - public void Dispose() - { - Dispose(true); - } - - private void WaitForDrain() - { - while (_allocatedWorkers > 0) - { - lock (_queue) - { - // Tell all workers we're done - Monitor.PulseAll(_queue); - } - - Thread.Sleep(250); - } - } - } -} diff --git a/src/Microsoft.AspNet.SignalR.Core/Messaging/MessageBus.cs b/src/Microsoft.AspNet.SignalR.Core/Messaging/MessageBus.cs deleted file mode 100644 index c9692b03e..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/Messaging/MessageBus.cs +++ /dev/null @@ -1,588 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.AspNet.SignalR.Configuration; -using Microsoft.AspNet.SignalR.Infrastructure; -using Microsoft.AspNet.SignalR.Tracing; - -namespace Microsoft.AspNet.SignalR.Messaging -{ - /// - /// - /// - public class MessageBus : IMessageBus, IDisposable - { - private readonly MessageBroker _broker; - - // The size of the messages store we allocate per topic. - private readonly uint _messageStoreSize; - - // By default, topics are cleaned up after having no subscribers and after - // an interval based on the disconnect timeout has passed. While this works in normal cases - // it's an issue when the rate of incoming connections is too high. - // This is the maximum number of un-expired topics with no subscribers - // we'll leave hanging around. The rest will be cleaned up on an the gc interval. - private readonly int _maxTopicsWithNoSubscriptions; - - private readonly IStringMinifier _stringMinifier; - - private readonly ITraceManager _traceManager; - private readonly TraceSource _trace; - - private Timer _gcTimer; - private int _gcRunning; - private static readonly TimeSpan _gcInterval = TimeSpan.FromSeconds(5); - - private readonly TimeSpan _topicTtl; - - // For unit testing - internal Action BeforeTopicGarbageCollected; - internal Action AfterTopicGarbageCollected; - internal Action BeforeTopicMarked; - internal Action BeforeTopicCreated; - internal Action AfterTopicMarkedSuccessfully; - internal Action AfterTopicMarked; - - private const int DefaultMaxTopicsWithNoSubscriptions = 1000; - - private readonly Func _createTopic; - private readonly Action _addEvent; - private readonly Action _removeEvent; - private readonly Action _disposeSubscription; - - /// - /// - /// - /// - public MessageBus(IDependencyResolver resolver) - : this(resolver.Resolve(), - resolver.Resolve(), - resolver.Resolve(), - resolver.Resolve(), - DefaultMaxTopicsWithNoSubscriptions) - { - } - - /// - /// - /// - /// - /// - /// - /// - /// - [SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "The message broker is disposed when the bus is disposed.")] - public MessageBus(IStringMinifier stringMinifier, - ITraceManager traceManager, - IPerformanceCounterManager performanceCounterManager, - IConfigurationManager configurationManager, - int maxTopicsWithNoSubscriptions) - { - if (stringMinifier == null) - { - throw new ArgumentNullException("stringMinifier"); - } - - if (traceManager == null) - { - throw new ArgumentNullException("traceManager"); - } - - if (performanceCounterManager == null) - { - throw new ArgumentNullException("performanceCounterManager"); - } - - if (configurationManager == null) - { - throw new ArgumentNullException("configurationManager"); - } - - if (configurationManager.DefaultMessageBufferSize < 0) - { - throw new ArgumentOutOfRangeException(Resources.Error_BufferSizeOutOfRange); - } - - _stringMinifier = stringMinifier; - _traceManager = traceManager; - Counters = performanceCounterManager; - _trace = _traceManager["SignalR." + typeof(MessageBus).Name]; - _maxTopicsWithNoSubscriptions = maxTopicsWithNoSubscriptions; - - _gcTimer = new Timer(_ => GarbageCollectTopics(), state: null, dueTime: _gcInterval, period: _gcInterval); - - _broker = new MessageBroker(Counters) - { - Trace = _trace - }; - - // The default message store size - _messageStoreSize = (uint)configurationManager.DefaultMessageBufferSize; - - _topicTtl = configurationManager.TopicTtl(); - _createTopic = CreateTopic; - _addEvent = AddEvent; - _removeEvent = RemoveEvent; - _disposeSubscription = DisposeSubscription; - - Topics = new TopicLookup(); - } - - protected virtual TraceSource Trace - { - get - { - return _trace; - } - } - - protected internal TopicLookup Topics { get; private set; } - protected IPerformanceCounterManager Counters { get; private set; } - - public int AllocatedWorkers - { - get - { - return _broker.AllocatedWorkers; - } - } - - public int BusyWorkers - { - get - { - return _broker.BusyWorkers; - } - } - - /// - /// Publishes a new message to the specified event on the bus. - /// - /// The message to publish. - public virtual Task Publish(Message message) - { - if (message == null) - { - throw new ArgumentNullException("message"); - } - - Topic topic; - if (Topics.TryGetValue(message.Key, out topic)) - { - topic.Store.Add(message); - ScheduleTopic(topic); - } - - Counters.MessageBusMessagesPublishedTotal.Increment(); - Counters.MessageBusMessagesPublishedPerSec.Increment(); - - - return TaskAsyncHelper.Empty; - } - - protected ulong Save(Message message) - { - if (message == null) - { - throw new ArgumentNullException("message"); - } - - // GetTopic will return a topic for the given key. If topic exists and is Dying, - // it will revive it and mark it as NoSubscriptions - Topic topic = GetTopic(message.Key); - // Mark the topic as used so it doesn't immediately expire (if it was in that state before). - topic.MarkUsed(); - - return topic.Store.Add(message); - } - - /// - /// - /// - /// - /// - /// - /// - /// - /// - [SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "The disposable object is returned to the caller")] - public virtual IDisposable Subscribe(ISubscriber subscriber, string cursor, Func> callback, int maxMessages, object state) - { - if (subscriber == null) - { - throw new ArgumentNullException("subscriber"); - } - - if (callback == null) - { - throw new ArgumentNullException("callback"); - } - - Subscription subscription = CreateSubscription(subscriber, cursor, callback, maxMessages, state); - - // Set the subscription for this subscriber - subscriber.Subscription = subscription; - - var topics = new HashSet(); - - foreach (var key in subscriber.EventKeys) - { - // Create or retrieve topic and set it as HasSubscriptions - Topic topic = SubscribeTopic(key); - - // Set the subscription for this topic - subscription.SetEventTopic(key, topic); - - topics.Add(topic); - } - - subscriber.EventKeyAdded += _addEvent; - subscriber.EventKeyRemoved += _removeEvent; - subscriber.WriteCursor = subscription.WriteCursor; - - var subscriptionState = new SubscriptionState(subscriber); - var disposable = new DisposableAction(_disposeSubscription, subscriptionState); - - // When the subscription itself is disposed then dispose it - subscription.Disposable = disposable; - - // Add the subscription when it's all set and can be scheduled - // for work. It's important to do this after everything is wired up for the - // subscription so that publishes can schedule work at the right time. - foreach (var topic in topics) - { - topic.AddSubscription(subscription); - } - - subscriptionState.Initialized.Set(); - - // If there's a cursor then schedule work for this subscription - if (!String.IsNullOrEmpty(cursor)) - { - _broker.Schedule(subscription); - } - - return disposable; - } - - [SuppressMessage("Microsoft.Design", "CA1062:Validate arguments of public methods", MessageId = "0", Justification = "Called from derived class")] - protected virtual Subscription CreateSubscription(ISubscriber subscriber, string cursor, Func> callback, int messageBufferSize, object state) - { - return new DefaultSubscription(subscriber.Identity, subscriber.EventKeys, Topics, cursor, callback, messageBufferSize, _stringMinifier, Counters, state); - } - - protected void ScheduleEvent(string eventKey) - { - Topic topic; - if (Topics.TryGetValue(eventKey, out topic)) - { - ScheduleTopic(topic); - } - } - - private void ScheduleTopic(Topic topic) - { - try - { - topic.SubscriptionLock.EnterReadLock(); - - for (int i = 0; i < topic.Subscriptions.Count; i++) - { - ISubscription subscription = topic.Subscriptions[i]; - _broker.Schedule(subscription); - } - } - finally - { - topic.SubscriptionLock.ExitReadLock(); - } - } - - /// - /// Creates a topic for the specified key. - /// - /// The key to create the topic for. - /// A for the specified key. - protected virtual Topic CreateTopic(string key) - { - // REVIEW: This can be called multiple times, should we guard against it? - Counters.MessageBusTopicsCurrent.Increment(); - - return new Topic(_messageStoreSize, _topicTtl); - } - - protected virtual void Dispose(bool disposing) - { - if (disposing) - { - // Stop the broker from doing any work - _broker.Dispose(); - - // Spin while we wait for the timer to finish if it's currently running - while (Interlocked.Exchange(ref _gcRunning, 1) == 1) - { - Thread.Sleep(250); - } - - // Remove all topics - Topics.Clear(); - - if (_gcTimer != null) - { - _gcTimer.Dispose(); - } - } - } - - public void Dispose() - { - Dispose(true); - } - - internal void GarbageCollectTopics() - { - if (Interlocked.Exchange(ref _gcRunning, 1) == 1) - { - return; - } - - int topicsWithNoSubs = 0; - - foreach (var pair in Topics) - { - if (pair.Value.IsExpired) - { - if (BeforeTopicGarbageCollected != null) - { - BeforeTopicGarbageCollected(pair.Key, pair.Value); - } - - // Mark the topic as dead - DestroyTopic(pair.Key, pair.Value); - } - else if (pair.Value.State == TopicState.NoSubscriptions) - { - // Keep track of the number of topics with no subscriptions - topicsWithNoSubs++; - } - } - - int overflow = topicsWithNoSubs - _maxTopicsWithNoSubscriptions; - if (overflow > 0) - { - // If we've overflowed the max the collect topics that don't have - // subscribers - var candidates = new List>(); - foreach (var pair in Topics) - { - if (pair.Value.State == TopicState.NoSubscriptions) - { - candidates.Add(pair); - } - } - - // We want to remove the overflow but oldest first - candidates.Sort((leftPair, rightPair) => leftPair.Value.LastUsed.CompareTo(rightPair.Value.LastUsed)); - - // Clear up to the overflow and stay within bounds - for (int i = 0; i < overflow && i < candidates.Count; i++) - { - var pair = candidates[i]; - // We only want to kill the topic if it's in the NoSubscriptions or Dying state. - if (InterlockedHelper.CompareExchangeOr(ref pair.Value.State, TopicState.Dead, TopicState.NoSubscriptions, TopicState.Dying)) - { - // Kill it - DestroyTopicCore(pair.Key, pair.Value); - } - } - } - - Interlocked.Exchange(ref _gcRunning, 0); - } - - private void DestroyTopic(string key, Topic topic) - { - // The goal of this function is to destroy topics after 2 garbage collect cycles - // This first if statement will transition a topic into the dying state on the first GC cycle - // but it will prevent the code path from hitting the second if statement - if (Interlocked.CompareExchange(ref topic.State, TopicState.Dying, TopicState.NoSubscriptions) == TopicState.Dying) - { - // If we've hit this if statement we're on the second GC cycle with this soon to be - // destroyed topic. At this point we move the Topic State into the Dead state as - // long as it has not been revived from the dying state. We check if the state is - // still dying again to ensure that the topic has not been transitioned into a new - // state since we've decided to destroy it. - if (Interlocked.CompareExchange(ref topic.State, TopicState.Dead, TopicState.Dying) == TopicState.Dying) - { - DestroyTopicCore(key, topic); - } - } - } - - private void DestroyTopicCore(string key, Topic topic) - { - Topics.TryRemove(key); - _stringMinifier.RemoveUnminified(key); - - Counters.MessageBusTopicsCurrent.Decrement(); - - Trace.TraceInformation("RemoveTopic(" + key + ")"); - - if (AfterTopicGarbageCollected != null) - { - AfterTopicGarbageCollected(key, topic); - } - } - - internal Topic GetTopic(string key) - { - Topic topic; - int oldState; - - do - { - if (BeforeTopicCreated != null) - { - BeforeTopicCreated(key); - } - - topic = Topics.GetOrAdd(key, _createTopic); - - if (BeforeTopicMarked != null) - { - BeforeTopicMarked(key, topic); - } - - // If the topic was dying revive it to the NoSubscriptions state. This is used to ensure - // that in the scaleout case that even if we're publishing to a topic with no subscriptions - // that we keep it around in case a user hops nodes. - oldState = Interlocked.CompareExchange(ref topic.State, TopicState.NoSubscriptions, TopicState.Dying); - - if (AfterTopicMarked != null) - { - AfterTopicMarked(key, topic, topic.State); - } - - // If the topic is currently dead then we're racing with the DestroyTopicCore function, therefore - // loop around until we're able to create a new topic - } while (oldState == TopicState.Dead); - - if (AfterTopicMarkedSuccessfully != null) - { - AfterTopicMarkedSuccessfully(key, topic); - } - - return topic; - } - - internal Topic SubscribeTopic(string key) - { - Topic topic; - - do - { - if (BeforeTopicCreated != null) - { - BeforeTopicCreated(key); - } - - topic = Topics.GetOrAdd(key, _createTopic); - - if (BeforeTopicMarked != null) - { - BeforeTopicMarked(key, topic); - } - - // Transition into the HasSubscriptions state as long as the topic is not dead - InterlockedHelper.CompareExchangeOr(ref topic.State, TopicState.HasSubscriptions, TopicState.NoSubscriptions, TopicState.Dying); - - if (AfterTopicMarked != null) - { - AfterTopicMarked(key, topic, topic.State); - } - - // If we were unable to transition into the HasSubscription state that means we're in the Dead state. - // Loop around until we're able to create the topic new - } while (topic.State != TopicState.HasSubscriptions); - - if (AfterTopicMarkedSuccessfully != null) - { - AfterTopicMarkedSuccessfully(key, topic); - } - - return topic; - } - - private void AddEvent(ISubscriber subscriber, string eventKey) - { - Topic topic = SubscribeTopic(eventKey); - - // Add or update the cursor (in case it already exists) - if (subscriber.Subscription.AddEvent(eventKey, topic)) - { - // Add it to the list of subs - topic.AddSubscription(subscriber.Subscription); - } - } - - private void RemoveEvent(ISubscriber subscriber, string eventKey) - { - Topic topic; - if (Topics.TryGetValue(eventKey, out topic)) - { - topic.RemoveSubscription(subscriber.Subscription); - subscriber.Subscription.RemoveEvent(eventKey); - } - } - - [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Failure to invoke the callback should be ignored")] - private void DisposeSubscription(object state) - { - var subscriptionState = (SubscriptionState)state; - var subscriber = subscriptionState.Subscriber; - - // This will stop work from continuting to happen - subscriber.Subscription.Dispose(); - - try - { - // Invoke the terminal callback - subscriber.Subscription.Invoke(MessageResult.TerminalMessage).Wait(); - } - catch - { - // We failed to talk to the subscriber because they are already gone - // so the terminal message isn't required. - } - - subscriptionState.Initialized.Wait(); - - subscriber.EventKeyAdded -= _addEvent; - subscriber.EventKeyRemoved -= _removeEvent; - subscriber.WriteCursor = null; - - for (int i = subscriber.EventKeys.Count - 1; i >= 0; i--) - { - string eventKey = subscriber.EventKeys[i]; - RemoveEvent(subscriber, eventKey); - } - } - - private class SubscriptionState - { - public ISubscriber Subscriber { get; private set; } - public ManualResetEventSlim Initialized { get; private set; } - - public SubscriptionState(ISubscriber subscriber) - { - Initialized = new ManualResetEventSlim(); - Subscriber = subscriber; - } - } - } -} diff --git a/src/Microsoft.AspNet.SignalR.Core/Messaging/MessageBusExtensions.cs b/src/Microsoft.AspNet.SignalR.Core/Messaging/MessageBusExtensions.cs deleted file mode 100644 index 6da8de78f..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/Messaging/MessageBusExtensions.cs +++ /dev/null @@ -1,88 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using Microsoft.AspNet.SignalR.Infrastructure; - -namespace Microsoft.AspNet.SignalR.Messaging -{ - public static class MessageBusExtensions - { - public static Task Publish(this IMessageBus bus, string source, string key, string value) - { - if (bus == null) - { - throw new ArgumentNullException("bus"); - } - - if (source == null) - { - throw new ArgumentNullException("source"); - } - - if (String.IsNullOrEmpty(key)) - { - throw new ArgumentNullException("key"); - } - - return bus.Publish(new Message(source, key, value)); - } - - internal static Task Ack(this IMessageBus bus, string connectionId, string commandId) - { - // Prepare the ack - var message = new Message(connectionId, PrefixHelper.GetAck(connectionId), null); - message.CommandId = commandId; - message.IsAck = true; - return bus.Publish(message); - } - - public static void Enumerate(this IList> messages, Action onMessage) - { - if (messages == null) - { - throw new ArgumentNullException("messages"); - } - - if (onMessage == null) - { - throw new ArgumentNullException("onMessage"); - } - - Enumerate(messages, message => true, (state, message) => onMessage(message), state: null); - } - - public static void Enumerate(this IList> messages, Func filter, Action onMessage, T state) - { - if (messages == null) - { - throw new ArgumentNullException("messages"); - } - - if (filter == null) - { - throw new ArgumentNullException("filter"); - } - - if (onMessage == null) - { - throw new ArgumentNullException("onMessage"); - } - - for (int i = 0; i < messages.Count; i++) - { - ArraySegment segment = messages[i]; - for (int j = segment.Offset; j < segment.Offset + segment.Count; j++) - { - Message message = segment.Array[j]; - - if (filter(message)) - { - onMessage(state, message); - } - } - } - } - } -} diff --git a/src/Microsoft.AspNet.SignalR.Core/Messaging/MessageResult.cs b/src/Microsoft.AspNet.SignalR.Core/Messaging/MessageResult.cs deleted file mode 100644 index f29f86097..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/Messaging/MessageResult.cs +++ /dev/null @@ -1,46 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; - -namespace Microsoft.AspNet.SignalR.Messaging -{ - /// - /// - /// - [SuppressMessage("Microsoft.Performance", "CA1815:OverrideEqualsAndOperatorEqualsOnValueTypes", Justification = "Messages are never compared")] - public struct MessageResult - { - private static readonly List> _emptyList = new List>(); - public readonly static MessageResult TerminalMessage = new MessageResult(terminal: true); - - /// - /// Gets an associated with the result. - /// - [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an optimization to avoid allocations.")] - public IList> Messages { get; private set; } - - public int TotalCount { get; private set; } - - public bool Terminal { get; set; } - - public MessageResult(bool terminal) - : this(_emptyList, 0) - { - Terminal = terminal; - } - - /// - /// Initializes a new instance of the struct. - /// - /// The array of messages associated with this . - /// The amount of messages populated in the messages array. - public MessageResult(IList> messages, int totalCount) - : this() - { - Messages = messages; - TotalCount = totalCount; - } - } -} diff --git a/src/Microsoft.AspNet.SignalR.Core/Messaging/MessageStore.cs b/src/Microsoft.AspNet.SignalR.Core/Messaging/MessageStore.cs deleted file mode 100644 index 565907f4a..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/Messaging/MessageStore.cs +++ /dev/null @@ -1,209 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -using System; -using System.Threading; - -namespace Microsoft.AspNet.SignalR.Messaging -{ - // Represents a message store that is backed by a ring buffer. - public sealed class MessageStore where T : class - { - private static readonly uint _minFragmentCount = 4; - private static readonly uint _maxFragmentSize = (IntPtr.Size == 4) ? (uint)16384 : (uint)8192; // guarantees that fragments never end up in the LOH - private static readonly ArraySegment _emptyArraySegment = new ArraySegment(new T[0]); - private readonly uint _offset; - - private Fragment[] _fragments; - private readonly uint _fragmentSize; - - private long _nextFreeMessageId; - - // Creates a message store with the specified capacity. The actual capacity will be *at least* the - // specified value. That is, GetMessages may return more data than 'capacity'. - public MessageStore(uint capacity, uint offset) - { - // set a minimum capacity - if (capacity < 32) - { - capacity = 32; - } - - _offset = offset; - - // Dynamically choose an appropriate number of fragments and the size of each fragment. - // This is chosen to avoid allocations on the large object heap and to minimize contention - // in the store. We allocate a small amount of additional space to act as an overflow - // buffer; this increases throughput of the data structure. - checked - { - uint fragmentCount = Math.Max(_minFragmentCount, capacity / _maxFragmentSize); - _fragmentSize = Math.Min((capacity + fragmentCount - 1) / fragmentCount, _maxFragmentSize); - _fragments = new Fragment[fragmentCount + 1]; // +1 for the overflow buffer - } - } - - public MessageStore(uint capacity) - : this(capacity, offset: 0) - { - } - - // only for testing purposes - [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate", Justification = "Only for testing")] - public ulong GetMessageCount() - { - return (ulong)Volatile.Read(ref _nextFreeMessageId); - } - - // Adds a message to the store. Returns the ID of the newly added message. - public ulong Add(T message) - { - // keep looping in TryAddImpl until it succeeds - ulong newMessageId; - while (!TryAddImpl(message, out newMessageId)) ; - - // When TryAddImpl succeeds, record the fact that a message was just added to the - // store. We increment the next free id rather than set it explicitly since - // multiple threads might be trying to write simultaneously. There is a nifty - // side effect to this: _nextFreeMessageId will *always* return the total number - // of messages that *all* threads agree have ever been added to the store. (The - // actual number may be higher, but this field will eventually catch up as threads - // flush data.) - Interlocked.Increment(ref _nextFreeMessageId); - return newMessageId; - } - - private void GetFragmentOffsets(ulong messageId, out ulong fragmentNum, out int idxIntoFragmentsArray, out int idxIntoFragment) - { - fragmentNum = messageId / _fragmentSize; - - // from the bucket number, we can figure out where in _fragments this data sits - idxIntoFragmentsArray = (int)(fragmentNum % (uint)_fragments.Length); - idxIntoFragment = (int)(messageId % _fragmentSize); - } - - private ulong GetMessageId(ulong fragmentNum, uint offset) - { - return fragmentNum * _fragmentSize + offset; - } - - // Gets the next batch of messages, beginning with the specified ID. - // This function may return an empty array or an array of length greater than the capacity - // specified in the ctor. The client may also miss messages. See MessageStoreResult. - public MessageStoreResult GetMessages(ulong firstMessageId, int maxMessages) - { - return GetMessagesImpl(firstMessageId, maxMessages); - } - - private MessageStoreResult GetMessagesImpl(ulong firstMessageIdRequestedByClient, int maxMessages) - { - ulong nextFreeMessageId = (ulong)Volatile.Read(ref _nextFreeMessageId); - - // Case 1: - // The client is already up-to-date with the message store, so we return no data. - if (nextFreeMessageId <= firstMessageIdRequestedByClient) - { - return new MessageStoreResult(firstMessageIdRequestedByClient, _emptyArraySegment, hasMoreData: false); - } - - // look for the fragment containing the start of the data requested by the client - ulong fragmentNum; - int idxIntoFragmentsArray, idxIntoFragment; - GetFragmentOffsets(firstMessageIdRequestedByClient, out fragmentNum, out idxIntoFragmentsArray, out idxIntoFragment); - Fragment thisFragment = _fragments[idxIntoFragmentsArray]; - ulong firstMessageIdInThisFragment = GetMessageId(thisFragment.FragmentNum, offset: _offset); - ulong firstMessageIdInNextFragment = firstMessageIdInThisFragment + _fragmentSize; - - // Case 2: - // This fragment contains the first part of the data the client requested. - if (firstMessageIdInThisFragment <= firstMessageIdRequestedByClient && firstMessageIdRequestedByClient < firstMessageIdInNextFragment) - { - int count = (int)(Math.Min(nextFreeMessageId, firstMessageIdInNextFragment) - firstMessageIdRequestedByClient); - - // Limit the number of messages the caller sees - count = Math.Min(count, maxMessages); - - ArraySegment retMessages = new ArraySegment(thisFragment.Data, idxIntoFragment, count); - - return new MessageStoreResult(firstMessageIdRequestedByClient, retMessages, hasMoreData: (nextFreeMessageId > firstMessageIdInNextFragment)); - } - - // Case 3: - // The client has missed messages, so we need to send him the earliest fragment we have. - while (true) - { - GetFragmentOffsets(nextFreeMessageId, out fragmentNum, out idxIntoFragmentsArray, out idxIntoFragment); - Fragment tailFragment = _fragments[(idxIntoFragmentsArray + 1) % _fragments.Length]; - if (tailFragment.FragmentNum < fragmentNum) - { - firstMessageIdInThisFragment = GetMessageId(tailFragment.FragmentNum, offset: _offset); - int count = Math.Min(maxMessages, tailFragment.Data.Length); - return new MessageStoreResult(firstMessageIdInThisFragment, new ArraySegment(tailFragment.Data, 0, count), hasMoreData: true); - } - nextFreeMessageId = (ulong)Volatile.Read(ref _nextFreeMessageId); - } - } - - private bool TryAddImpl(T message, out ulong newMessageId) - { - ulong nextFreeMessageId = (ulong)Volatile.Read(ref _nextFreeMessageId); - - // locate the fragment containing the next free id, which is where we should write - ulong fragmentNum; - int idxIntoFragmentsArray, idxIntoFragment; - GetFragmentOffsets(nextFreeMessageId, out fragmentNum, out idxIntoFragmentsArray, out idxIntoFragment); - Fragment fragment = _fragments[idxIntoFragmentsArray]; - - if (fragment == null || fragment.FragmentNum < fragmentNum) - { - // the fragment is outdated (or non-existent) and must be replaced - - if (idxIntoFragment == 0) - { - // this thread is responsible for creating the fragment - Fragment newFragment = new Fragment(fragmentNum, _fragmentSize); - newFragment.Data[0] = message; - Fragment existingFragment = Interlocked.CompareExchange(ref _fragments[idxIntoFragmentsArray], newFragment, fragment); - if (existingFragment == fragment) - { - newMessageId = GetMessageId(fragmentNum, offset: _offset); - return true; - } - } - - // another thread is responsible for updating the fragment, so fall to bottom of method - } - else if (fragment.FragmentNum == fragmentNum) - { - // the fragment is valid, and we can just try writing into it until we reach the end of the fragment - T[] fragmentData = fragment.Data; - for (int i = idxIntoFragment; i < fragmentData.Length; i++) - { - T originalMessage = Interlocked.CompareExchange(ref fragmentData[i], message, null); - if (originalMessage == null) - { - newMessageId = GetMessageId(fragmentNum, offset: (uint)i); - return true; - } - } - - // another thread used the last open space in this fragment, so fall to bottom of method - } - - // failure; caller will retry operation - newMessageId = 0; - return false; - } - - private sealed class Fragment - { - public readonly ulong FragmentNum; - public readonly T[] Data; - - public Fragment(ulong fragmentNum, uint fragmentSize) - { - FragmentNum = fragmentNum; - Data = new T[fragmentSize]; - } - } - } -} diff --git a/src/Microsoft.AspNet.SignalR.Core/Messaging/MessageStoreResult.cs b/src/Microsoft.AspNet.SignalR.Core/Messaging/MessageStoreResult.cs deleted file mode 100644 index 665c887d4..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/Messaging/MessageStoreResult.cs +++ /dev/null @@ -1,53 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -using System; -using System.Diagnostics.CodeAnalysis; - -namespace Microsoft.AspNet.SignalR.Messaging -{ - // Represents the result of a call to MessageStore.GetMessages. - [SuppressMessage("Microsoft.Performance", "CA1815:OverrideEqualsAndOperatorEqualsOnValueTypes", Justification = "This is never compared")] - public struct MessageStoreResult where T : class - { - // The first message ID in the result set. Messages in the result set have sequentually increasing IDs. - // If FirstMessageId = 20 and Messages.Length = 4, then the messages have IDs { 20, 21, 22, 23 }. - private readonly ulong _firstMessageId; - - // If this is true, the backing MessageStore contains more messages, and the client should call GetMessages again. - private readonly bool _hasMoreData; - - // The actual result set. May be empty. - private readonly ArraySegment _messages; - - public MessageStoreResult(ulong firstMessageId, ArraySegment messages, bool hasMoreData) - { - _firstMessageId = firstMessageId; - _messages = messages; - _hasMoreData = hasMoreData; - } - - public ulong FirstMessageId - { - get - { - return _firstMessageId; - } - } - - public bool HasMoreData - { - get - { - return _hasMoreData; - } - } - - public ArraySegment Messages - { - get - { - return _messages; - } - } - } -} diff --git a/src/Microsoft.AspNet.SignalR.Core/Messaging/ScaleoutConfiguration.cs b/src/Microsoft.AspNet.SignalR.Core/Messaging/ScaleoutConfiguration.cs deleted file mode 100644 index 1ac31f354..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/Messaging/ScaleoutConfiguration.cs +++ /dev/null @@ -1,39 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -using System; - -namespace Microsoft.AspNet.SignalR.Messaging -{ - /// - /// Common settings for scale-out message bus implementations. - /// - public class ScaleoutConfiguration - { - public static readonly int DisableQueuing = 0; - - private int _maxQueueLength; - - /// - /// The maximum length of the outgoing send queue. Messages being sent to the backplane are queued - /// up to this length. After the max length is reached, further sends will throw an InvalidOperationException. - /// Set to ScaleoutConfiguration.DisableQueuing to disable queing. - /// Defaults to disabled. - /// - public virtual int MaxQueueLength - { - get - { - return _maxQueueLength; - } - set - { - if (value < 0) - { - throw new ArgumentOutOfRangeException("value"); - } - - _maxQueueLength = value; - } - } - } -} diff --git a/src/Microsoft.AspNet.SignalR.Core/Messaging/ScaleoutMapping.cs b/src/Microsoft.AspNet.SignalR.Core/Messaging/ScaleoutMapping.cs deleted file mode 100644 index 15fefa51c..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/Messaging/ScaleoutMapping.cs +++ /dev/null @@ -1,37 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -using System; -using System.Collections.Generic; -using Microsoft.AspNet.SignalR.Infrastructure; - -namespace Microsoft.AspNet.SignalR.Messaging -{ - public class ScaleoutMapping - { - public ScaleoutMapping(ulong id, ScaleoutMessage message) - : this(id, message, ListHelper.Empty) - { - } - - public ScaleoutMapping(ulong id, ScaleoutMessage message, IList localKeyInfo) - { - if (message == null) - { - throw new ArgumentNullException("message"); - } - - if (localKeyInfo == null) - { - throw new ArgumentNullException("localKeyInfo"); - } - - Id = id; - LocalKeyInfo = localKeyInfo; - ServerCreationTime = message.ServerCreationTime; - } - - public ulong Id { get; private set; } - public IList LocalKeyInfo { get; private set; } - public DateTime ServerCreationTime { get; private set; } - } -} diff --git a/src/Microsoft.AspNet.SignalR.Core/Messaging/ScaleoutMappingStore.cs b/src/Microsoft.AspNet.SignalR.Core/Messaging/ScaleoutMappingStore.cs deleted file mode 100644 index 4c70376fb..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/Messaging/ScaleoutMappingStore.cs +++ /dev/null @@ -1,123 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -using System; -using System.Collections; -using System.Collections.Generic; - -namespace Microsoft.AspNet.SignalR.Messaging -{ - public class ScaleoutMappingStore - { - private const int MaxMessages = 1000000; - - private ScaleoutStore _store; - - public ScaleoutMappingStore() - { - _store = new ScaleoutStore(MaxMessages); - } - - public void Add(ulong id, ScaleoutMessage message, IList localKeyInfo) - { - if (MaxMapping != null && id < MaxMapping.Id) - { - _store = new ScaleoutStore(MaxMessages); - } - - _store.Add(new ScaleoutMapping(id, message, localKeyInfo)); - } - - public ScaleoutMapping MaxMapping - { - get - { - return _store.MaxMapping; - } - } - - public IEnumerator GetEnumerator(ulong id) - { - MessageStoreResult result = _store.GetMessagesByMappingId(id); - - return new ScaleoutStoreEnumerator(_store, result); - } - - private struct ScaleoutStoreEnumerator : IEnumerator, IEnumerator - { - private readonly WeakReference _storeReference; - private MessageStoreResult _result; - private int _offset; - private int _length; - private ulong _nextId; - - public ScaleoutStoreEnumerator(ScaleoutStore store, MessageStoreResult result) - : this() - { - _storeReference = new WeakReference(store); - Initialize(result); - } - - public ScaleoutMapping Current - { - get - { - return _result.Messages.Array[_offset]; - } - } - - public void Dispose() - { - - } - - object IEnumerator.Current - { - get { return Current; } - } - - public bool MoveNext() - { - _offset++; - - if (_offset < _length) - { - return true; - } - - if (!_result.HasMoreData) - { - return false; - } - - // If the store falls out of scope - var store = (ScaleoutStore)_storeReference.Target; - - if (store == null) - { - return false; - } - - // Get the next result - MessageStoreResult result = store.GetMessages(_nextId); - Initialize(result); - - _offset++; - - return _offset < _length; - } - - public void Reset() - { - throw new NotSupportedException(); - } - - private void Initialize(MessageStoreResult result) - { - _result = result; - _offset = _result.Messages.Offset - 1; - _length = _result.Messages.Offset + _result.Messages.Count; - _nextId = _result.FirstMessageId + (ulong)_result.Messages.Count; - } - } - } -} diff --git a/src/Microsoft.AspNet.SignalR.Core/Messaging/ScaleoutMessage.cs b/src/Microsoft.AspNet.SignalR.Core/Messaging/ScaleoutMessage.cs deleted file mode 100644 index c1aa3993a..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/Messaging/ScaleoutMessage.cs +++ /dev/null @@ -1,74 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.IO; - -namespace Microsoft.AspNet.SignalR.Messaging -{ - /// - /// Represents a message to the scaleout backplane - /// - public class ScaleoutMessage - { - public ScaleoutMessage(IList messages) - { - Messages = messages; - ServerCreationTime = DateTime.UtcNow; - } - - public ScaleoutMessage() - { - } - - /// - /// The messages from SignalR - /// - [SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Justification = "This type is used for serialization")] - public IList Messages { get; set; } - - /// - /// The time the message was created on the origin server - /// - public DateTime ServerCreationTime { get; set; } - - public byte[] ToBytes() - { - using (var ms = new MemoryStream()) - { - var binaryWriter = new BinaryWriter(ms); - - binaryWriter.Write(Messages.Count); - for (int i = 0; i < Messages.Count; i++) - { - Messages[i].WriteTo(ms); - } - binaryWriter.Write(ServerCreationTime.Ticks); - - return ms.ToArray(); - } - } - - public static ScaleoutMessage FromBytes(byte[] data) - { - if (data == null) - { - throw new ArgumentNullException("data"); - } - - using (var stream = new MemoryStream(data)) - { - var binaryReader = new BinaryReader(stream); - var message = new ScaleoutMessage(); - message.Messages = new List(); - int count = binaryReader.ReadInt32(); - for (int i = 0; i < count; i++) - { - message.Messages.Add(Message.ReadFrom(stream)); - } - message.ServerCreationTime = new DateTime(binaryReader.ReadInt64()); - - return message; - } - } - } -} diff --git a/src/Microsoft.AspNet.SignalR.Core/Messaging/ScaleoutMessageBus.cs b/src/Microsoft.AspNet.SignalR.Core/Messaging/ScaleoutMessageBus.cs deleted file mode 100644 index 9c5c5d944..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/Messaging/ScaleoutMessageBus.cs +++ /dev/null @@ -1,232 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; -using System.Linq; -using System.Threading.Tasks; -using Microsoft.AspNet.SignalR.Infrastructure; -using Microsoft.AspNet.SignalR.Tracing; - -namespace Microsoft.AspNet.SignalR.Messaging -{ - /// - /// Common base class for scaleout message bus implementations. - /// - public abstract class ScaleoutMessageBus : MessageBus - { - private readonly SipHashBasedStringEqualityComparer _sipHashBasedComparer = new SipHashBasedStringEqualityComparer(0, 0); - private readonly TraceSource _trace; - private readonly Lazy _streamManager; - private readonly IPerformanceCounterManager _perfCounters; - - protected ScaleoutMessageBus(IDependencyResolver resolver, ScaleoutConfiguration configuration) - : base(resolver) - { - if (configuration == null) - { - throw new ArgumentNullException("configuration"); - } - - var traceManager = resolver.Resolve(); - _trace = traceManager["SignalR." + typeof(ScaleoutMessageBus).Name]; - _perfCounters = resolver.Resolve(); - _streamManager = new Lazy(() => new ScaleoutStreamManager(Send, OnReceivedCore, StreamCount, _trace, _perfCounters, configuration)); - } - - /// - /// The number of streams can't change for the lifetime of this instance. - /// - protected virtual int StreamCount - { - get - { - return 1; - } - } - - private ScaleoutStreamManager StreamManager - { - get - { - return _streamManager.Value; - } - } - - /// - /// Opens the specified queue for sending messages. - /// The index of the stream to open. - /// - protected void Open(int streamIndex) - { - StreamManager.Open(streamIndex); - } - - /// - /// Closes the specified queue. - /// The index of the stream to close. - /// - protected void Close(int streamIndex) - { - StreamManager.Close(streamIndex); - } - - /// - /// Closes the specified queue for sending messages making all sends fail asynchronously. - /// - /// The index of the stream to close. - /// The error that occurred. - protected void OnError(int streamIndex, Exception exception) - { - StreamManager.OnError(streamIndex, exception); - } - - /// - /// Sends messages to the backplane - /// - /// The list of messages to send - /// - protected virtual Task Send(IList messages) - { - // If we're only using a single stream then just send - if (StreamCount == 1) - { - return StreamManager.Send(0, messages); - } - - var taskCompletionSource = new TaskCompletionSource(); - - // Group messages by source (connection id) - var messagesBySource = messages.GroupBy(m => m.Source); - - SendImpl(messagesBySource.GetEnumerator(), taskCompletionSource); - - return taskCompletionSource.Task; - } - - protected virtual Task Send(int streamIndex, IList messages) - { - throw new NotImplementedException(); - } - - [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "We return a faulted tcs")] - private void SendImpl(IEnumerator> enumerator, TaskCompletionSource taskCompletionSource) - { - send: - - if (!enumerator.MoveNext()) - { - taskCompletionSource.TrySetResult(null); - } - else - { - IGrouping group = enumerator.Current; - - // Get the channel index we're going to use for this message - int index = (int)((uint)_sipHashBasedComparer.GetHashCode(group.Key) % StreamCount); - - Debug.Assert(index >= 0, "Hash function resulted in an index < 0."); - - Task sendTask = StreamManager.Send(index, group.ToArray()).Catch(); - - if (sendTask.IsCompleted) - { - try - { - sendTask.Wait(); - - goto send; - - } - catch (Exception ex) - { - taskCompletionSource.SetUnwrappedException(ex); - } - } - else - { - sendTask.Then((enumer, tcs) => SendImpl(enumer, tcs), enumerator, taskCompletionSource) - .ContinueWithNotComplete(taskCompletionSource); - } - } - } - - /// - /// Invoked when a payload is received from the backplane. There should only be one active call at any time. - /// - /// id of the stream. - /// id of the payload within that stream. - /// The scaleout message. - /// - protected virtual void OnReceived(int streamIndex, ulong id, ScaleoutMessage message) - { - StreamManager.OnReceived(streamIndex, id, message); - } - - [SuppressMessage("Microsoft.Design", "CA1062:Validate arguments of public methods", MessageId = "2", Justification = "Called from derived class")] - [SuppressMessage("Microsoft.Design", "CA1062:Validate arguments of public methods", MessageId = "0", Justification = "Called from derived class")] - private void OnReceivedCore(int streamIndex, ulong id, ScaleoutMessage scaleoutMessage) - { - Counters.ScaleoutMessageBusMessagesReceivedPerSec.IncrementBy(scaleoutMessage.Messages.Count); - - _trace.TraceInformation("OnReceived({0}, {1}, {2})", streamIndex, id, scaleoutMessage.Messages.Count); - - var localMapping = new LocalEventKeyInfo[scaleoutMessage.Messages.Count]; - var keys = new HashSet(); - - for (var i = 0; i < scaleoutMessage.Messages.Count; ++i) - { - Message message = scaleoutMessage.Messages[i]; - - // Remember where this message came from - message.MappingId = id; - message.StreamIndex = streamIndex; - - keys.Add(message.Key); - ulong localId = Save(message); - MessageStore messageStore = Topics[message.Key].Store; - - localMapping[i] = new LocalEventKeyInfo(message.Key, localId, messageStore); - } - - // Get the stream for this payload - ScaleoutMappingStore store = StreamManager.Streams[streamIndex]; - - // Publish only after we've setup the mapping fully - store.Add(id, scaleoutMessage, localMapping); - - // Schedule after we're done - foreach (var eventKey in keys) - { - ScheduleEvent(eventKey); - } - } - - public override Task Publish(Message message) - { - Counters.MessageBusMessagesPublishedTotal.Increment(); - Counters.MessageBusMessagesPublishedPerSec.Increment(); - - // TODO: Implement message batching here - return Send(new[] { message }); - } - - protected override void Dispose(bool disposing) - { - // Close all streams - for (int i = 0; i < StreamCount; i++) - { - Close(i); - } - - base.Dispose(disposing); - } - - [SuppressMessage("Microsoft.Design", "CA1062:Validate arguments of public methods", MessageId = "0", Justification = "Called from derived class")] - protected override Subscription CreateSubscription(ISubscriber subscriber, string cursor, Func> callback, int messageBufferSize, object state) - { - return new ScaleoutSubscription(subscriber.Identity, subscriber.EventKeys, cursor, StreamManager.Streams, callback, messageBufferSize, Counters, state); - } - } -} diff --git a/src/Microsoft.AspNet.SignalR.Core/Messaging/ScaleoutStore.cs b/src/Microsoft.AspNet.SignalR.Core/Messaging/ScaleoutStore.cs deleted file mode 100644 index 605447d82..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/Messaging/ScaleoutStore.cs +++ /dev/null @@ -1,440 +0,0 @@ -using System; -using System.Diagnostics.CodeAnalysis; -using System.Threading; - -namespace Microsoft.AspNet.SignalR.Messaging -{ - // Represents a message store that is backed by a ring buffer. - [SuppressMessage("Microsoft.Design", "CA1001:TypesThatOwnDisposableFieldsShouldBeDisposable", Justification = "The rate sampler doesn't need to be disposed")] - public sealed class ScaleoutStore - { - private const uint _minFragmentCount = 4; - - [SuppressMessage("Microsoft.Performance", "CA1802:UseLiteralsWhereAppropriate", Justification = "It's conditional based on architecture")] - private static readonly uint _maxFragmentSize = (IntPtr.Size == 4) ? (uint)16384 : (uint)8192; // guarantees that fragments never end up in the LOH - - private static readonly ArraySegment _emptyArraySegment = new ArraySegment(new ScaleoutMapping[0]); - - private Fragment[] _fragments; - private readonly uint _fragmentSize; - - private long _minMessageId; - private long _nextFreeMessageId; - - private ulong _minMappingId; - private ScaleoutMapping _maxMapping; - - // Creates a message store with the specified capacity. The actual capacity will be *at least* the - // specified value. That is, GetMessages may return more data than 'capacity'. - public ScaleoutStore(uint capacity) - { - // set a minimum capacity - if (capacity < 32) - { - capacity = 32; - } - - // Dynamically choose an appropriate number of fragments and the size of each fragment. - // This is chosen to avoid allocations on the large object heap and to minimize contention - // in the store. We allocate a small amount of additional space to act as an overflow - // buffer; this increases throughput of the data structure. - checked - { - uint fragmentCount = Math.Max(_minFragmentCount, capacity / _maxFragmentSize); - _fragmentSize = Math.Min((capacity + fragmentCount - 1) / fragmentCount, _maxFragmentSize); - _fragments = new Fragment[fragmentCount + 1]; // +1 for the overflow buffer - } - } - - internal ulong MinMappingId - { - get - { - return _minMappingId; - } - } - - public ScaleoutMapping MaxMapping - { - get - { - return _maxMapping; - } - } - - public uint FragmentSize - { - get - { - return _fragmentSize; - } - } - - public int FragmentCount - { - get - { - return _fragments.Length; - } - } - - // Adds a message to the store. Returns the ID of the newly added message. - public ulong Add(ScaleoutMapping mapping) - { - // keep looping in TryAddImpl until it succeeds - ulong newMessageId; - while (!TryAddImpl(mapping, out newMessageId)) ; - - // When TryAddImpl succeeds, record the fact that a message was just added to the - // store. We increment the next free id rather than set it explicitly since - // multiple threads might be trying to write simultaneously. There is a nifty - // side effect to this: _nextFreeMessageId will *always* return the total number - // of messages that *all* threads agree have ever been added to the store. (The - // actual number may be higher, but this field will eventually catch up as threads - // flush data.) - Interlocked.Increment(ref _nextFreeMessageId); - return newMessageId; - } - - private void GetFragmentOffsets(ulong messageId, out ulong fragmentNum, out int idxIntoFragmentsArray, out int idxIntoFragment) - { - fragmentNum = messageId / _fragmentSize; - - // from the bucket number, we can figure out where in _fragments this data sits - idxIntoFragmentsArray = (int)(fragmentNum % (uint)_fragments.Length); - idxIntoFragment = (int)(messageId % _fragmentSize); - } - - private int GetFragmentOffset(ulong messageId) - { - ulong fragmentNum = messageId / _fragmentSize; - - return (int)(fragmentNum % (uint)_fragments.Length); - } - - private ulong GetMessageId(ulong fragmentNum, uint offset) - { - return fragmentNum * _fragmentSize + offset; - } - - private bool TryAddImpl(ScaleoutMapping mapping, out ulong newMessageId) - { - ulong nextFreeMessageId = (ulong)Volatile.Read(ref _nextFreeMessageId); - - // locate the fragment containing the next free id, which is where we should write - ulong fragmentNum; - int idxIntoFragmentsArray, idxIntoFragment; - GetFragmentOffsets(nextFreeMessageId, out fragmentNum, out idxIntoFragmentsArray, out idxIntoFragment); - Fragment fragment = _fragments[idxIntoFragmentsArray]; - - if (fragment == null || fragment.FragmentNum < fragmentNum) - { - // the fragment is outdated (or non-existent) and must be replaced - bool overwrite = fragment != null && fragment.FragmentNum < fragmentNum; - - if (idxIntoFragment == 0) - { - // this thread is responsible for creating the fragment - Fragment newFragment = new Fragment(fragmentNum, _fragmentSize); - newFragment.Data[0] = mapping; - Fragment existingFragment = Interlocked.CompareExchange(ref _fragments[idxIntoFragmentsArray], newFragment, fragment); - if (existingFragment == fragment) - { - newMessageId = GetMessageId(fragmentNum, offset: 0); - newFragment.MinId = newMessageId; - newFragment.Length = 1; - newFragment.MaxId = GetMessageId(fragmentNum, offset: _fragmentSize - 1); - _maxMapping = mapping; - - // Move the minimum id when we overwrite - if (overwrite) - { - _minMessageId = (long)(existingFragment.MaxId + 1); - _minMappingId = existingFragment.MaxId; - } - else if (idxIntoFragmentsArray == 0) - { - _minMappingId = mapping.Id; - } - - return true; - } - } - - // another thread is responsible for updating the fragment, so fall to bottom of method - } - else if (fragment.FragmentNum == fragmentNum) - { - // the fragment is valid, and we can just try writing into it until we reach the end of the fragment - ScaleoutMapping[] fragmentData = fragment.Data; - for (int i = idxIntoFragment; i < fragmentData.Length; i++) - { - ScaleoutMapping originalMapping = Interlocked.CompareExchange(ref fragmentData[i], mapping, null); - if (originalMapping == null) - { - newMessageId = GetMessageId(fragmentNum, offset: (uint)i); - fragment.Length++; - _maxMapping = fragmentData[i]; - return true; - } - } - - // another thread used the last open space in this fragment, so fall to bottom of method - } - - // failure; caller will retry operation - newMessageId = 0; - return false; - } - - public MessageStoreResult GetMessages(ulong firstMessageIdRequestedByClient) - { - ulong nextFreeMessageId = (ulong)Volatile.Read(ref _nextFreeMessageId); - - // Case 1: - // The client is already up-to-date with the message store, so we return no data. - if (nextFreeMessageId <= firstMessageIdRequestedByClient) - { - return new MessageStoreResult(firstMessageIdRequestedByClient, _emptyArraySegment, hasMoreData: false); - } - - // look for the fragment containing the start of the data requested by the client - ulong fragmentNum; - int idxIntoFragmentsArray, idxIntoFragment; - GetFragmentOffsets(firstMessageIdRequestedByClient, out fragmentNum, out idxIntoFragmentsArray, out idxIntoFragment); - Fragment thisFragment = _fragments[idxIntoFragmentsArray]; - ulong firstMessageIdInThisFragment = GetMessageId(thisFragment.FragmentNum, offset: 0); - ulong firstMessageIdInNextFragment = firstMessageIdInThisFragment + _fragmentSize; - - // Case 2: - // This fragment contains the first part of the data the client requested. - if (firstMessageIdInThisFragment <= firstMessageIdRequestedByClient && firstMessageIdRequestedByClient < firstMessageIdInNextFragment) - { - int count = (int)(Math.Min(nextFreeMessageId, firstMessageIdInNextFragment) - firstMessageIdRequestedByClient); - - var retMessages = new ArraySegment(thisFragment.Data, idxIntoFragment, count); - - return new MessageStoreResult(firstMessageIdRequestedByClient, retMessages, hasMoreData: (nextFreeMessageId > firstMessageIdInNextFragment)); - } - - // Case 3: - // The client has missed messages, so we need to send him the earliest fragment we have. - while (true) - { - GetFragmentOffsets(nextFreeMessageId, out fragmentNum, out idxIntoFragmentsArray, out idxIntoFragment); - Fragment tailFragment = _fragments[(idxIntoFragmentsArray + 1) % _fragments.Length]; - if (tailFragment.FragmentNum < fragmentNum) - { - firstMessageIdInThisFragment = GetMessageId(tailFragment.FragmentNum, offset: 0); - - return new MessageStoreResult(firstMessageIdInThisFragment, new ArraySegment(tailFragment.Data, 0, tailFragment.Length), hasMoreData: true); - } - nextFreeMessageId = (ulong)Volatile.Read(ref _nextFreeMessageId); - } - } - - public MessageStoreResult GetMessagesByMappingId(ulong mappingId) - { - var minMessageId = (ulong)Volatile.Read(ref _minMessageId); - - int idxIntoFragment; - // look for the fragment containing the start of the data requested by the client - Fragment thisFragment; - if (TryGetFragmentFromMappingId(mappingId, out thisFragment)) - { - int lastSearchIndex; - ulong lastSearchId; - if (thisFragment.TrySearch(mappingId, - out idxIntoFragment, - out lastSearchIndex, - out lastSearchId)) - { - // Skip the first message - idxIntoFragment++; - ulong firstMessageIdRequestedByClient = GetMessageId(thisFragment.FragmentNum, (uint)idxIntoFragment); - - return GetMessages(firstMessageIdRequestedByClient); - } - else - { - if (mappingId > lastSearchId) - { - lastSearchIndex++; - } - - var segment = new ArraySegment(thisFragment.Data, - lastSearchIndex, - thisFragment.Length - lastSearchIndex); - - var firstMessageIdInThisFragment = GetMessageId(thisFragment.FragmentNum, offset: (uint)lastSearchIndex); - - return new MessageStoreResult(firstMessageIdInThisFragment, - segment, - hasMoreData: true); - } - } - - // If we're expired or we're at the first mapping or we're lower than the - // min then get everything - if (mappingId < _minMappingId || mappingId == UInt64.MaxValue) - { - return GetAllMessages(minMessageId); - } - - // We're up to date so do nothing - return new MessageStoreResult(0, _emptyArraySegment, hasMoreData: false); - } - - private MessageStoreResult GetAllMessages(ulong minMessageId) - { - ulong fragmentNum; - int idxIntoFragmentsArray, idxIntoFragment; - GetFragmentOffsets(minMessageId, out fragmentNum, out idxIntoFragmentsArray, out idxIntoFragment); - - Fragment fragment = _fragments[idxIntoFragmentsArray]; - - if (fragment == null) - { - return new MessageStoreResult(minMessageId, _emptyArraySegment, hasMoreData: false); - } - - var firstMessageIdInThisFragment = GetMessageId(fragment.FragmentNum, offset: 0); - - var messages = new ArraySegment(fragment.Data, 0, fragment.Length); - - return new MessageStoreResult(firstMessageIdInThisFragment, messages, hasMoreData: true); - } - - internal bool TryGetFragmentFromMappingId(ulong mappingId, out Fragment fragment) - { - long low = _minMessageId; - long high = _nextFreeMessageId; - - while (low <= high) - { - var mid = (ulong)((low + high) / 2); - - int midOffset = GetFragmentOffset(mid); - - fragment = _fragments[midOffset]; - - if (fragment == null) - { - return false; - } - - if (mappingId < fragment.MinValue) - { - high = (long)(fragment.MinId - 1); - } - else if (mappingId > fragment.MaxValue) - { - low = (long)(fragment.MaxId + 1); - } - else if (fragment.HasValue(mappingId)) - { - return true; - } - } - - fragment = null; - return false; - } - - internal sealed class Fragment - { - public readonly ulong FragmentNum; - public readonly ScaleoutMapping[] Data; - public int Length; - public ulong MinId; - public ulong MaxId; - - public Fragment(ulong fragmentNum, uint fragmentSize) - { - FragmentNum = fragmentNum; - Data = new ScaleoutMapping[fragmentSize]; - } - - public ulong? MinValue - { - get - { - var mapping = Data[0]; - if (mapping != null) - { - return mapping.Id; - } - - return null; - } - } - - public ulong? MaxValue - { - get - { - ScaleoutMapping mapping = null; - - if (Length == 0) - { - mapping = Data[Length]; - } - else - { - mapping = Data[Length - 1]; - } - - if (mapping != null) - { - return mapping.Id; - } - - return null; - } - } - - public bool HasValue(ulong id) - { - return id >= MinValue && id <= MaxValue; - } - - public bool TrySearch(ulong id, out int index, out int lastSearchIndex, out ulong lastSearchId) - { - lastSearchIndex = 0; - lastSearchId = id; - - var low = 0; - var high = Length; - - - while (low <= high) - { - int mid = (low + high) / 2; - - ScaleoutMapping mapping = Data[mid]; - - lastSearchIndex = mid; - lastSearchId = mapping.Id; - - if (id < mapping.Id) - { - high = mid - 1; - } - else if (id > mapping.Id) - { - low = mid + 1; - } - else if (id == mapping.Id) - { - index = mid; - return true; - } - } - - index = -1; - return false; - } - } - } - -} diff --git a/src/Microsoft.AspNet.SignalR.Core/Messaging/ScaleoutStream.cs b/src/Microsoft.AspNet.SignalR.Core/Messaging/ScaleoutStream.cs deleted file mode 100644 index 29c359a77..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/Messaging/ScaleoutStream.cs +++ /dev/null @@ -1,316 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -using System; -using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; -using System.Threading.Tasks; -using Microsoft.AspNet.SignalR.Infrastructure; - -namespace Microsoft.AspNet.SignalR.Messaging -{ - internal class ScaleoutStream - { - private TaskCompletionSource _taskCompletionSource; - private TaskQueue _queue; - private StreamState _state; - private Exception _error; - - private readonly int _size; - private readonly TraceSource _trace; - private readonly string _tracePrefix; - private readonly IPerformanceCounterManager _perfCounters; - - private readonly object _lockObj = new object(); - - public ScaleoutStream(TraceSource trace, string tracePrefix, int size, IPerformanceCounterManager performanceCounters) - { - if (trace == null) - { - throw new ArgumentNullException("trace"); - } - - _trace = trace; - _tracePrefix = tracePrefix; - _size = size; - _perfCounters = performanceCounters; - - InitializeCore(); - } - - private bool UsingTaskQueue - { - get - { - return _size > 0; - } - } - - public void Open() - { - lock (_lockObj) - { - if (ChangeState(StreamState.Open)) - { - _perfCounters.ScaleoutStreamCountOpen.Increment(); - _perfCounters.ScaleoutStreamCountBuffering.Decrement(); - - _error = null; - - if (UsingTaskQueue) - { - _taskCompletionSource.TrySetResult(null); - } - } - } - } - - public Task Send(Func send, object state) - { - lock (_lockObj) - { - if (_error != null) - { - throw _error; - } - - // If the queue is closed then stop sending - if (_state == StreamState.Closed) - { - throw new InvalidOperationException(Resources.Error_StreamClosed); - } - - if (_state == StreamState.Initial) - { - throw new InvalidOperationException(Resources.Error_StreamNotOpen); - } - - var context = new SendContext(this, send, state); - - if (UsingTaskQueue) - { - Task task = _queue.Enqueue(Send, context); - - if (task == null) - { - // The task is null if the queue is full - throw new InvalidOperationException(Resources.Error_TaskQueueFull); - } - - // Always observe the task in case the user doesn't handle it - return task.Catch(); - } - - _perfCounters.ScaleoutSendQueueLength.Increment(); - return Send(context).Finally(counter => - { - ((IPerformanceCounter)counter).Decrement(); - }, - _perfCounters.ScaleoutSendQueueLength); - } - } - - public void SetError(Exception error) - { - Trace("Error has happened with the following exception: {0}.", error); - - lock (_lockObj) - { - _perfCounters.ScaleoutErrorsTotal.Increment(); - _perfCounters.ScaleoutErrorsPerSec.Increment(); - - Buffer(); - - _error = error; - } - } - - public void Close() - { - Task task = TaskAsyncHelper.Empty; - - lock (_lockObj) - { - if (ChangeState(StreamState.Closed)) - { - _perfCounters.ScaleoutStreamCountOpen.RawValue = 0; - _perfCounters.ScaleoutStreamCountBuffering.RawValue = 0; - - if (UsingTaskQueue) - { - // Ensure the queue is started - EnsureQueueStarted(); - - // Drain the queue to stop all sends - task = Drain(_queue); - } - } - } - - if (UsingTaskQueue) - { - // Block until the queue is drained so no new work can be done - task.Wait(); - } - } - - private static Task Send(object state) - { - var context = (SendContext)state; - - context.InvokeSend().Then(tcs => - { - // Complete the task if the send is successful - tcs.TrySetResult(null); - }, - context.TaskCompletionSource) - .Catch((ex, obj) => - { - var ctx = (SendContext)obj; - - ctx.Stream.Trace("Send failed: {0}", ex); - - lock (ctx.Stream._lockObj) - { - // Set the queue into buffering state - ctx.Stream.SetError(ex.InnerException); - - // Otherwise just set this task as failed - ctx.TaskCompletionSource.TrySetUnwrappedException(ex); - } - }, - context); - - return context.TaskCompletionSource.Task; - } - - private void Buffer() - { - lock (_lockObj) - { - if (ChangeState(StreamState.Buffering)) - { - _perfCounters.ScaleoutStreamCountOpen.Decrement(); - _perfCounters.ScaleoutStreamCountBuffering.Increment(); - - InitializeCore(); - } - } - } - - private void InitializeCore() - { - if (UsingTaskQueue) - { - Task task = DrainQueue(); - _queue = new TaskQueue(task, _size); - _queue.QueueSizeCounter = _perfCounters.ScaleoutSendQueueLength; - } - } - - private Task DrainQueue() - { - // If the tcs is null or complete then create a new one - if (_taskCompletionSource == null || - _taskCompletionSource.Task.IsCompleted) - { - _taskCompletionSource = new TaskCompletionSource(); - } - - if (_queue != null) - { - // Drain the queue when the new queue is open - return _taskCompletionSource.Task.Then(q => Drain(q), _queue); - } - - // Nothing to drain - return _taskCompletionSource.Task; - } - - private void EnsureQueueStarted() - { - if (_taskCompletionSource != null) - { - _taskCompletionSource.TrySetResult(null); - } - } - - private bool ChangeState(StreamState newState) - { - // Do nothing if the state is closed - if (_state == StreamState.Closed) - { - return false; - } - - if (_state != newState) - { - Trace("Changed state from {0} to {1}", _state, newState); - - _state = newState; - return true; - } - - return false; - } - - private static Task Drain(TaskQueue queue) - { - if (queue == null) - { - return TaskAsyncHelper.Empty; - } - - var tcs = new TaskCompletionSource(); - - queue.Drain().Catch().ContinueWith(task => - { - tcs.SetResult(null); - }); - - return tcs.Task; - } - - private void Trace(string value, params object[] args) - { - _trace.TraceInformation(_tracePrefix + " - " + value, args); - } - - private class SendContext - { - private readonly Func _send; - private readonly object _state; - - public readonly ScaleoutStream Stream; - public readonly TaskCompletionSource TaskCompletionSource; - - public SendContext(ScaleoutStream stream, Func send, object state) - { - Stream = stream; - TaskCompletionSource = new TaskCompletionSource(); - _send = send; - _state = state; - } - - [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "The exception flows to the caller")] - public Task InvokeSend() - { - try - { - return _send(_state); - } - catch (Exception ex) - { - return TaskAsyncHelper.FromError(ex); - } - } - } - - private enum StreamState - { - Initial, - Open, - Buffering, - Closed - } - } -} diff --git a/src/Microsoft.AspNet.SignalR.Core/Messaging/ScaleoutStreamManager.cs b/src/Microsoft.AspNet.SignalR.Core/Messaging/ScaleoutStreamManager.cs deleted file mode 100644 index 003822377..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/Messaging/ScaleoutStreamManager.cs +++ /dev/null @@ -1,97 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Diagnostics; -using System.Threading.Tasks; -using Microsoft.AspNet.SignalR.Infrastructure; - -namespace Microsoft.AspNet.SignalR.Messaging -{ - internal class ScaleoutStreamManager - { - private readonly Func, Task> _send; - private readonly Action _receive; - private readonly ScaleoutStream[] _streams; - - public ScaleoutStreamManager(Func, Task> send, - Action receive, - int streamCount, - TraceSource trace, - IPerformanceCounterManager performanceCounters, - ScaleoutConfiguration configuration) - { - _streams = new ScaleoutStream[streamCount]; - _send = send; - _receive = receive; - - var receiveMapping = new ScaleoutMappingStore[streamCount]; - - performanceCounters.ScaleoutStreamCountTotal.RawValue = streamCount; - performanceCounters.ScaleoutStreamCountBuffering.RawValue = streamCount; - performanceCounters.ScaleoutStreamCountOpen.RawValue = 0; - - for (int i = 0; i < streamCount; i++) - { - _streams[i] = new ScaleoutStream(trace, "Stream(" + i + ")", configuration.MaxQueueLength, performanceCounters); - receiveMapping[i] = new ScaleoutMappingStore(); - } - - Streams = new ReadOnlyCollection(receiveMapping); - } - - public IList Streams { get; private set; } - - public void Open(int streamIndex) - { - _streams[streamIndex].Open(); - } - - public void Close(int streamIndex) - { - _streams[streamIndex].Close(); - } - - public void OnError(int streamIndex, Exception exception) - { - _streams[streamIndex].SetError(exception); - } - - public Task Send(int streamIndex, IList messages) - { - var context = new SendContext(this, streamIndex, messages); - - return _streams[streamIndex].Send(state => Send(state), context); - } - - public void OnReceived(int streamIndex, ulong id, ScaleoutMessage message) - { - _receive(streamIndex, id, message); - - // We assume if a message has come in then the stream is open - Open(streamIndex); - } - - private static Task Send(object state) - { - var context = (SendContext)state; - - return context.StreamManager._send(context.Index, context.Messages); - } - - private class SendContext - { - public ScaleoutStreamManager StreamManager; - public int Index; - public IList Messages; - - public SendContext(ScaleoutStreamManager scaleoutStream, int index, IList messages) - { - StreamManager = scaleoutStream; - Index = index; - Messages = messages; - } - } - } -} diff --git a/src/Microsoft.AspNet.SignalR.Core/Messaging/ScaleoutSubscription.cs b/src/Microsoft.AspNet.SignalR.Core/Messaging/ScaleoutSubscription.cs deleted file mode 100644 index d426f7a37..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/Messaging/ScaleoutSubscription.cs +++ /dev/null @@ -1,282 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Globalization; -using System.IO; -using System.Threading.Tasks; -using Microsoft.AspNet.SignalR.Infrastructure; - -namespace Microsoft.AspNet.SignalR.Messaging -{ - public class ScaleoutSubscription : Subscription - { - private const string _scaleoutCursorPrefix = "s-"; - - private readonly IList _streams; - private readonly List _cursors; - - public ScaleoutSubscription(string identity, - IList eventKeys, - string cursor, - IList streams, - Func> callback, - int maxMessages, - IPerformanceCounterManager counters, - object state) - : base(identity, eventKeys, callback, maxMessages, counters, state) - { - if (streams == null) - { - throw new ArgumentNullException("streams"); - } - - _streams = streams; - - List cursors = null; - - if (String.IsNullOrEmpty(cursor)) - { - cursors = new List(); - } - else - { - cursors = Cursor.GetCursors(cursor, _scaleoutCursorPrefix); - - // If the cursor had a default prefix, "d-", cursors might be null - if (cursors == null) - { - cursors = new List(); - } - // If the streams don't match the cursors then throw it out - else if (cursors.Count != _streams.Count) - { - cursors.Clear(); - } - } - - // No cursors so we need to populate them from the list of streams - if (cursors.Count == 0) - { - for (int streamIndex = 0; streamIndex < _streams.Count; streamIndex++) - { - AddCursorForStream(streamIndex, cursors); - } - } - - _cursors = cursors; - } - - public override void WriteCursor(TextWriter textWriter) - { - Cursor.WriteCursors(textWriter, _cursors, _scaleoutCursorPrefix); - } - - [SuppressMessage("Microsoft.Design", "CA1002:DoNotExposeGenericLists", Justification = "The list needs to be populated")] - [SuppressMessage("Microsoft.Design", "CA1062:Validate arguments of public methods", MessageId = "0", Justification = "It is called from the base class")] - protected override void PerformWork(IList> items, out int totalCount, out object state) - { - // The list of cursors represent (streamid, payloadid) - var nextCursors = new ulong?[_cursors.Count]; - totalCount = 0; - - // Get the enumerator so that we can extract messages for this subscription - IEnumerator> enumerator = GetMappings().GetEnumerator(); - - while (totalCount < MaxMessages && enumerator.MoveNext()) - { - ScaleoutMapping mapping = enumerator.Current.Item1; - int streamIndex = enumerator.Current.Item2; - - ulong? nextCursor = nextCursors[streamIndex]; - - // Only keep going with this stream if the cursor we're looking at is bigger than - // anything we already processed - if (nextCursor == null || mapping.Id > nextCursor) - { - ulong mappingId = ExtractMessages(streamIndex, mapping, items, ref totalCount); - - // Update the cursor id - nextCursors[streamIndex] = mappingId; - } - } - - state = nextCursors; - } - - protected override void BeforeInvoke(object state) - { - // Update the list of cursors before invoking anything - var nextCursors = (ulong?[])state; - for (int i = 0; i < _cursors.Count; i++) - { - // Only update non-null entries - ulong? nextCursor = nextCursors[i]; - - if (nextCursor.HasValue) - { - Cursor cursor = _cursors[i]; - - cursor.Id = nextCursor.Value; - } - } - } - - private IEnumerable> GetMappings() - { - var enumerators = new List(); - - for (var streamIndex = 0; streamIndex < _streams.Count; ++streamIndex) - { - // Get the mapping for this stream - ScaleoutMappingStore store = _streams[streamIndex]; - - Cursor cursor = _cursors[streamIndex]; - - // Try to find a local mapping for this payload - var enumerator = new CachedStreamEnumerator(store.GetEnumerator(cursor.Id), - streamIndex); - - enumerators.Add(enumerator); - } - - while (enumerators.Count > 0) - { - ScaleoutMapping minMapping = null; - CachedStreamEnumerator minEnumerator = null; - - for (int i = enumerators.Count - 1; i >= 0; i--) - { - CachedStreamEnumerator enumerator = enumerators[i]; - - ScaleoutMapping mapping; - if (enumerator.TryMoveNext(out mapping)) - { - if (minMapping == null || mapping.ServerCreationTime < minMapping.ServerCreationTime) - { - minMapping = mapping; - minEnumerator = enumerator; - } - } - else - { - enumerators.RemoveAt(i); - } - } - - if (minMapping != null) - { - minEnumerator.ClearCachedValue(); - yield return Tuple.Create(minMapping, minEnumerator.StreamIndex); - } - } - } - - private ulong ExtractMessages(int streamIndex, ScaleoutMapping mapping, IList> items, ref int totalCount) - { - // For each of the event keys we care about, extract all of the messages - // from the payload - lock (EventKeys) - { - for (var i = 0; i < EventKeys.Count; ++i) - { - string eventKey = EventKeys[i]; - - for (int j = 0; j < mapping.LocalKeyInfo.Count; j++) - { - LocalEventKeyInfo info = mapping.LocalKeyInfo[j]; - - if (info.MessageStore != null && info.Key.Equals(eventKey, StringComparison.OrdinalIgnoreCase)) - { - MessageStoreResult storeResult = info.MessageStore.GetMessages(info.Id, 1); - - if (storeResult.Messages.Count > 0) - { - // TODO: Figure out what to do when we have multiple event keys per mapping - Message message = storeResult.Messages.Array[storeResult.Messages.Offset]; - - // Only add the message to the list if the stream index matches - if (message.StreamIndex == streamIndex) - { - items.Add(storeResult.Messages); - totalCount += storeResult.Messages.Count; - - // We got a mapping id bigger than what we expected which - // means we missed messages. Use the new mappingId. - if (message.MappingId > mapping.Id) - { - return message.MappingId; - } - } - else - { - // REVIEW: When the stream indexes don't match should we leave the mapping id as is? - // If we do nothing then we'll end up querying old cursor ids until - // we eventually find a message id that matches this stream index. - } - } - } - } - } - } - - return mapping.Id; - } - - private void AddCursorForStream(int streamIndex, List cursors) - { - ScaleoutMapping maxMapping = _streams[streamIndex].MaxMapping; - - ulong id = UInt64.MaxValue; - string key = streamIndex.ToString(CultureInfo.InvariantCulture); - - if (maxMapping != null) - { - id = maxMapping.Id; - } - - var newCursor = new Cursor(key, id); - cursors.Add(newCursor); - } - - private class CachedStreamEnumerator - { - private readonly IEnumerator _enumerator; - private ScaleoutMapping _cachedValue; - - public CachedStreamEnumerator(IEnumerator enumerator, int streamIndex) - { - _enumerator = enumerator; - StreamIndex = streamIndex; - } - - public int StreamIndex { get; private set; } - - public bool TryMoveNext(out ScaleoutMapping mapping) - { - mapping = null; - - if (_cachedValue != null) - { - mapping = _cachedValue; - return true; - } - - if (_enumerator.MoveNext()) - { - mapping = _enumerator.Current; - _cachedValue = mapping; - return true; - } - - return false; - } - - public void ClearCachedValue() - { - _cachedValue = null; - } - } - } -} diff --git a/src/Microsoft.AspNet.SignalR.Core/Messaging/Subscription.cs b/src/Microsoft.AspNet.SignalR.Core/Messaging/Subscription.cs deleted file mode 100644 index 774ccbddc..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/Messaging/Subscription.cs +++ /dev/null @@ -1,338 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.IO; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.AspNet.SignalR.Infrastructure; - -namespace Microsoft.AspNet.SignalR.Messaging -{ - public abstract class Subscription : ISubscription, IDisposable - { - private readonly Func> _callback; - private readonly object _callbackState; - private readonly IPerformanceCounterManager _counters; - - private int _state; - private int _subscriptionState; - - private bool Alive - { - get - { - return _subscriptionState != SubscriptionState.Disposed; - } - } - - public string Identity { get; private set; } - - public IList EventKeys { get; private set; } - - public int MaxMessages { get; private set; } - - public IDisposable Disposable { get; set; } - - protected Subscription(string identity, IList eventKeys, Func> callback, int maxMessages, IPerformanceCounterManager counters, object state) - { - if (String.IsNullOrEmpty(identity)) - { - throw new ArgumentNullException("identity"); - } - - if (eventKeys == null) - { - throw new ArgumentNullException("eventKeys"); - } - - if (callback == null) - { - throw new ArgumentNullException("callback"); - } - - if (maxMessages < 0) - { - throw new ArgumentOutOfRangeException("maxMessages"); - } - - if (counters == null) - { - throw new ArgumentNullException("counters"); - } - - Identity = identity; - _callback = callback; - EventKeys = eventKeys; - MaxMessages = maxMessages; - _counters = counters; - _callbackState = state; - - _counters.MessageBusSubscribersTotal.Increment(); - _counters.MessageBusSubscribersCurrent.Increment(); - _counters.MessageBusSubscribersPerSec.Increment(); - } - - public virtual Task Invoke(MessageResult result) - { - return Invoke(result, state => { }, state: null); - } - - private Task Invoke(MessageResult result, Action beforeInvoke, object state) - { - // Change the state from idle to invoking callback - var prevState = Interlocked.CompareExchange(ref _subscriptionState, - SubscriptionState.InvokingCallback, - SubscriptionState.Idle); - - if (prevState == SubscriptionState.Disposed) - { - // Only allow terminal messages after dispose - if (!result.Terminal) - { - return TaskAsyncHelper.False; - } - } - - beforeInvoke(state); - - _counters.MessageBusMessagesReceivedTotal.IncrementBy(result.TotalCount); - _counters.MessageBusMessagesReceivedPerSec.IncrementBy(result.TotalCount); - - return _callback.Invoke(result, _callbackState).ContinueWith(task => - { - // Go from invoking callback to idle - Interlocked.CompareExchange(ref _subscriptionState, - SubscriptionState.Idle, - SubscriptionState.InvokingCallback); - return task; - }, - TaskContinuationOptions.ExecuteSynchronously).FastUnwrap(); - } - - public Task Work() - { - // Set the state to working - Interlocked.Exchange(ref _state, State.Working); - - var tcs = new TaskCompletionSource(); - - WorkImpl(tcs); - - return tcs.Task; - } - - public bool SetQueued() - { - return Interlocked.Increment(ref _state) == State.Working; - } - - public bool UnsetQueued() - { - // If we try to set the state to idle and we were not already in the working state then keep going - return Interlocked.CompareExchange(ref _state, State.Idle, State.Working) != State.Working; - } - - [SuppressMessage("Microsoft.Usage", "CA2202:Do not dispose objects multiple times", Justification = "We have a sync and async code path.")] - [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "We want to avoid user code taking the process down.")] - private void WorkImpl(TaskCompletionSource taskCompletionSource) - { - Process: - if (!Alive) - { - // If this subscription is dead then return immediately - taskCompletionSource.TrySetResult(null); - return; - } - - var items = new List>(); - int totalCount; - object state; - - PerformWork(items, out totalCount, out state); - - if (items.Count > 0) - { - var messageResult = new MessageResult(items, totalCount); - Task callbackTask = Invoke(messageResult, s => BeforeInvoke(s), state); - - if (callbackTask.IsCompleted) - { - try - { - // Make sure exceptions propagate - callbackTask.Wait(); - - if (callbackTask.Result) - { - // Sync path - goto Process; - } - else - { - // If we're done pumping messages through to this subscription - // then dispose - Dispose(); - - // If the callback said it's done then stop - taskCompletionSource.TrySetResult(null); - } - } - catch (Exception ex) - { - if (ex.InnerException is TaskCanceledException) - { - taskCompletionSource.TrySetCanceled(); - } - else - { - taskCompletionSource.TrySetUnwrappedException(ex); - } - } - } - else - { - WorkImplAsync(callbackTask, taskCompletionSource); - } - } - else - { - taskCompletionSource.TrySetResult(null); - } - } - - protected virtual void BeforeInvoke(object state) - { - } - - [SuppressMessage("Microsoft.Design", "CA1002:DoNotExposeGenericLists", Justification = "The list needs to be populated")] - [SuppressMessage("Microsoft.Design", "CA1007:UseGenericsWhereAppropriate", Justification = "The caller wouldn't be able to specify what the generic type argument is")] - [SuppressMessage("Microsoft.Design", "CA1021:AvoidOutParameters", MessageId = "1#", Justification = "The count needs to be returned")] - [SuppressMessage("Microsoft.Design", "CA1021:AvoidOutParameters", MessageId = "2#", Justification = "The state needs to be set by the callee")] - protected abstract void PerformWork(IList> items, out int totalCount, out object state); - - private void WorkImplAsync(Task callbackTask, TaskCompletionSource taskCompletionSource) - { - // Async path - callbackTask.ContinueWith(task => - { - if (task.IsFaulted) - { - taskCompletionSource.TrySetUnwrappedException(task.Exception); - } - else if (task.IsCanceled) - { - taskCompletionSource.TrySetCanceled(); - } - else if (task.Result) - { - WorkImpl(taskCompletionSource); - } - else - { - // If we're done pumping messages through to this subscription - // then dispose - Dispose(); - - // If the callback said it's done then stop - taskCompletionSource.TrySetResult(null); - } - }); - } - - public virtual bool AddEvent(string key, Topic topic) - { - return AddEventCore(key); - } - - public virtual void RemoveEvent(string key) - { - lock (EventKeys) - { - EventKeys.Remove(key); - } - } - - public virtual void SetEventTopic(string key, Topic topic) - { - // Don't call AddEvent since that's virtual - AddEventCore(key); - } - - protected virtual void Dispose(bool disposing) - { - if (disposing) - { - // REIVIEW: Consider sleeping instead of using a tight loop, or maybe timing out after some interval - // if the client is very slow then this invoke call might not end quickly and this will make the CPU - // hot waiting for the task to return. - - int disposeRetryCount = 0; - - while (true) - { - // Wait until the subscription isn't working anymore - var state = Interlocked.CompareExchange(ref _subscriptionState, - SubscriptionState.Disposed, - SubscriptionState.Idle); - - // If we're not working then stop - if (state != SubscriptionState.InvokingCallback || disposeRetryCount ++ > 10) - { - if (state != SubscriptionState.Disposed) - { - // Only decrement if we're not disposed already - _counters.MessageBusSubscribersCurrent.Decrement(); - _counters.MessageBusSubscribersPerSec.Decrement(); - } - - // Raise the disposed callback - if (Disposable != null) - { - Disposable.Dispose(); - } - - break; - } - - Thread.Sleep(500); - } - } - } - - public void Dispose() - { - Dispose(true); - } - - public abstract void WriteCursor(TextWriter textWriter); - - private bool AddEventCore(string key) - { - lock (EventKeys) - { - if (EventKeys.Contains(key)) - { - return false; - } - - EventKeys.Add(key); - return true; - } - } - - private static class State - { - public const int Idle = 0; - public const int Working = 1; - } - - private static class SubscriptionState - { - public const int Idle = 0; - public const int InvokingCallback = 1; - public const int Disposed = 2; - } - } -} diff --git a/src/Microsoft.AspNet.SignalR.Core/Messaging/Topic.cs b/src/Microsoft.AspNet.SignalR.Core/Messaging/Topic.cs deleted file mode 100644 index eab7934c1..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/Messaging/Topic.cs +++ /dev/null @@ -1,119 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -using System; -using System.Collections.Generic; -using System.Threading; - -namespace Microsoft.AspNet.SignalR.Messaging -{ - public class Topic - { - private readonly TimeSpan _lifespan; - - // Keeps track of the last time this subscription was used - private DateTime _lastUsed = DateTime.UtcNow; - - public IList Subscriptions { get; private set; } - public MessageStore Store { get; private set; } - public ReaderWriterLockSlim SubscriptionLock { get; private set; } - - // State of the topic - internal int State; - - public virtual bool IsExpired - { - get - { - try - { - SubscriptionLock.EnterReadLock(); - - TimeSpan timeSpan = DateTime.UtcNow - _lastUsed; - - return Subscriptions.Count == 0 && timeSpan > _lifespan; - } - finally - { - SubscriptionLock.ExitReadLock(); - } - } - } - - public DateTime LastUsed - { - get - { - return _lastUsed; - } - } - - public Topic(uint storeSize, TimeSpan lifespan) - { - _lifespan = lifespan; - Subscriptions = new List(); - Store = new MessageStore(storeSize); - SubscriptionLock = new ReaderWriterLockSlim(); - } - - public void MarkUsed() - { - this._lastUsed = DateTime.UtcNow; - } - - public void AddSubscription(ISubscription subscription) - { - if (subscription == null) - { - throw new ArgumentNullException("subscription"); - } - - try - { - SubscriptionLock.EnterWriteLock(); - - MarkUsed(); - - Subscriptions.Add(subscription); - - // Created -> HasSubscriptions - Interlocked.CompareExchange(ref State, - TopicState.HasSubscriptions, - TopicState.NoSubscriptions); - } - finally - { - SubscriptionLock.ExitWriteLock(); - } - } - - public void RemoveSubscription(ISubscription subscription) - { - if (subscription == null) - { - throw new ArgumentNullException("subscription"); - } - - try - { - SubscriptionLock.EnterWriteLock(); - - MarkUsed(); - - Subscriptions.Remove(subscription); - - - if (Subscriptions.Count == 0) - { - // HasSubscriptions -> NoSubscriptions - Interlocked.CompareExchange(ref State, - TopicState.NoSubscriptions, - TopicState.HasSubscriptions); - } - } - finally - { - SubscriptionLock.ExitWriteLock(); - } - } - } -} diff --git a/src/Microsoft.AspNet.SignalR.Core/Messaging/TopicLookup.cs b/src/Microsoft.AspNet.SignalR.Core/Messaging/TopicLookup.cs deleted file mode 100644 index d62d9084f..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/Messaging/TopicLookup.cs +++ /dev/null @@ -1,98 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -using System; -using System.Collections; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Linq; -using Microsoft.AspNet.SignalR.Infrastructure; - -namespace Microsoft.AspNet.SignalR.Messaging -{ - public sealed class TopicLookup : IEnumerable> - { - // General topics - private readonly ConcurrentDictionary _topics = new ConcurrentDictionary(); - - // All group topics - private readonly ConcurrentDictionary _groupTopics = new ConcurrentDictionary(new SipHashBasedStringEqualityComparer()); - - public int Count - { - get - { - return _topics.Count + _groupTopics.Count; - } - } - - public Topic this[string key] - { - get - { - Topic topic; - if (TryGetValue(key, out topic)) - { - return topic; - } - return null; - } - } - - public bool ContainsKey(string key) - { - if (PrefixHelper.HasGroupPrefix(key)) - { - return _groupTopics.ContainsKey(key); - } - - return _topics.ContainsKey(key); - } - - public bool TryGetValue(string key, out Topic topic) - { - if (PrefixHelper.HasGroupPrefix(key)) - { - return _groupTopics.TryGetValue(key, out topic); - } - - return _topics.TryGetValue(key, out topic); - } - - public IEnumerator> GetEnumerator() - { - return _topics.Concat(_groupTopics).GetEnumerator(); - } - - IEnumerator IEnumerable.GetEnumerator() - { - return GetEnumerator(); - } - - public bool TryRemove(string key) - { - Topic topic; - if (PrefixHelper.HasGroupPrefix(key)) - { - return _groupTopics.TryRemove(key, out topic); - } - - return _topics.TryRemove(key, out topic); - } - - public Topic GetOrAdd(string key, Func factory) - { - if (PrefixHelper.HasGroupPrefix(key)) - { - return _groupTopics.GetOrAdd(key, factory); - } - - return _topics.GetOrAdd(key, factory); - } - - public void Clear() - { - _topics.Clear(); - _groupTopics.Clear(); - } - } -} diff --git a/src/Microsoft.AspNet.SignalR.Core/Messaging/TopicState.cs b/src/Microsoft.AspNet.SignalR.Core/Messaging/TopicState.cs deleted file mode 100644 index e69193c89..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/Messaging/TopicState.cs +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -namespace Microsoft.AspNet.SignalR.Messaging -{ - internal class TopicState - { - public const int NoSubscriptions = 0; - public const int HasSubscriptions = 1; - public const int Dying = 2; - public const int Dead = 3; - } -} diff --git a/src/Microsoft.AspNet.SignalR.Core/Messaging/Volatile.cs b/src/Microsoft.AspNet.SignalR.Core/Messaging/Volatile.cs deleted file mode 100644 index d4ce8a10e..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/Messaging/Volatile.cs +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -using System.Threading; - -namespace Microsoft.AspNet.SignalR.Messaging -{ - // All methods here are guaranteed both volatile + atomic. - // TODO: Make this use the .NET 4.5 'Volatile' type. - internal static class Volatile - { - public static long Read(ref long location) - { - return Interlocked.Read(ref location); - } - } -} diff --git a/src/Microsoft.AspNet.SignalR.Core/Microsoft.AspNet.SignalR.Core.csproj b/src/Microsoft.AspNet.SignalR.Core/Microsoft.AspNet.SignalR.Core.csproj deleted file mode 100644 index 4fc44007d..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/Microsoft.AspNet.SignalR.Core.csproj +++ /dev/null @@ -1,284 +0,0 @@ - - - - Debug - x86 - 8.0.30703 - 2.0 - {1B9A82C4-BCA1-4834-A33E-226F17BE070B} - Library - Properties - Microsoft.AspNet.SignalR - Microsoft.AspNet.SignalR.Core - 512 - true - ..\..\ - true - - - true - bin\x86\Debug\ - TRACE;DEBUG;PERFCOUNTERS - bin\Debug\Microsoft.AspNet.SignalR.Core.XML - true - 1591 - full - x86 - prompt - C:\Dropbox\Git\NzbDrone\src\Common\Microsoft.AspNet.SignalR.ruleset - 4 - false - - - bin\x86\Release\ - TRACE;PERFCOUNTERS - bin\Release\Microsoft.AspNet.SignalR.Core.XML - true - true - 1591 - pdbonly - x86 - prompt - C:\Dropbox\Git\NzbDrone\src\Common\Microsoft.AspNet.SignalR.ruleset - 4 - - - - ..\packages\Newtonsoft.Json.9.0.1\lib\net40\Newtonsoft.Json.dll - True - - - - - - - - - Properties\CommonAssemblyInfo.cs - - - Properties\CommonVersionInfo.cs - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - True - True - Resources.resx - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ResXFileCodeGenerator - Resources.Designer.cs - Designer - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/Microsoft.AspNet.SignalR.Core/Microsoft.AspNet.SignalR.Core.csproj.DotSettings b/src/Microsoft.AspNet.SignalR.Core/Microsoft.AspNet.SignalR.Core.csproj.DotSettings deleted file mode 100644 index 5b8822215..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/Microsoft.AspNet.SignalR.Core.csproj.DotSettings +++ /dev/null @@ -1,2 +0,0 @@ - - DO_NOT_SHOW \ No newline at end of file diff --git a/src/Microsoft.AspNet.SignalR.Core/PersistentConnection.cs b/src/Microsoft.AspNet.SignalR.Core/PersistentConnection.cs deleted file mode 100644 index 49f2afdd6..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/PersistentConnection.cs +++ /dev/null @@ -1,522 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; -using System.Globalization; -using System.Linq; -using System.Threading.Tasks; -using Microsoft.AspNet.SignalR.Configuration; -using Microsoft.AspNet.SignalR.Hosting; -using Microsoft.AspNet.SignalR.Infrastructure; -using Microsoft.AspNet.SignalR.Json; -using Microsoft.AspNet.SignalR.Messaging; -using Microsoft.AspNet.SignalR.Tracing; -using Microsoft.AspNet.SignalR.Transports; - -namespace Microsoft.AspNet.SignalR -{ - /// - /// Represents a connection between client and server. - /// - public abstract class PersistentConnection - { - private const string WebSocketsTransportName = "webSockets"; - private static readonly char[] SplitChars = new[] { ':' }; - - private IConfigurationManager _configurationManager; - private ITransportManager _transportManager; - private bool _initialized; - private IServerCommandHandler _serverMessageHandler; - - public virtual void Initialize(IDependencyResolver resolver, HostContext context) - { - if (resolver == null) - { - throw new ArgumentNullException("resolver"); - } - - if (context == null) - { - throw new ArgumentNullException("context"); - } - - if (_initialized) - { - return; - } - - MessageBus = resolver.Resolve(); - JsonSerializer = resolver.Resolve(); - TraceManager = resolver.Resolve(); - Counters = resolver.Resolve(); - AckHandler = resolver.Resolve(); - ProtectedData = resolver.Resolve(); - - _configurationManager = resolver.Resolve(); - _transportManager = resolver.Resolve(); - _serverMessageHandler = resolver.Resolve(); - - _initialized = true; - } - - public bool Authorize(IRequest request) - { - return AuthorizeRequest(request); - } - - protected virtual TraceSource Trace - { - get - { - return TraceManager["SignalR.PersistentConnection"]; - } - } - - protected IProtectedData ProtectedData { get; private set; } - - protected IMessageBus MessageBus { get; private set; } - - protected IJsonSerializer JsonSerializer { get; private set; } - - protected IAckHandler AckHandler { get; private set; } - - protected ITraceManager TraceManager { get; private set; } - - protected IPerformanceCounterManager Counters { get; private set; } - - protected ITransport Transport { get; private set; } - - /// - /// Gets the for the . - /// - public IConnection Connection - { - get; - private set; - } - - /// - /// Gets the for the . - /// - public IConnectionGroupManager Groups - { - get; - private set; - } - - private string DefaultSignal - { - get - { - return PrefixHelper.GetPersistentConnectionName(DefaultSignalRaw); - } - } - - private string DefaultSignalRaw - { - get - { - return GetType().FullName; - } - } - - internal virtual string GroupPrefix - { - get - { - return PrefixHelper.PersistentConnectionGroupPrefix; - } - } - - /// - /// Handles all requests for s. - /// - /// The for the current request. - /// A that completes when the pipeline is complete. - /// - /// Thrown if connection wasn't initialized. - /// Thrown if the transport wasn't specified. - /// Thrown if the connection id wasn't specified. - /// - public virtual Task ProcessRequest(HostContext context) - { - if (context == null) - { - throw new ArgumentNullException("context"); - } - - if (!_initialized) - { - throw new InvalidOperationException(String.Format(CultureInfo.CurrentCulture, Resources.Error_ConnectionNotInitialized)); - } - - if (IsNegotiationRequest(context.Request)) - { - return ProcessNegotiationRequest(context); - } - else if (IsPingRequest(context.Request)) - { - return ProcessPingRequest(context); - } - - Transport = GetTransport(context); - - if (Transport == null) - { - return FailResponse(context.Response, String.Format(CultureInfo.CurrentCulture, Resources.Error_ProtocolErrorUnknownTransport)); - } - - string connectionToken = context.Request.QueryString["connectionToken"]; - - // If there's no connection id then this is a bad request - if (String.IsNullOrEmpty(connectionToken)) - { - return FailResponse(context.Response, String.Format(CultureInfo.CurrentCulture, Resources.Error_ProtocolErrorMissingConnectionToken)); - } - - string connectionId; - string message; - int statusCode; - - if (!TryGetConnectionId(context, connectionToken, out connectionId, out message, out statusCode)) - { - return FailResponse(context.Response, message, statusCode); - } - - // Set the transport's connection id to the unprotected one - Transport.ConnectionId = connectionId; - - IList signals = GetSignals(connectionId); - IList groups = AppendGroupPrefixes(context, connectionId); - - Connection connection = CreateConnection(connectionId, signals, groups); - - Connection = connection; - string groupName = PrefixHelper.GetPersistentConnectionGroupName(DefaultSignalRaw); - Groups = new GroupManager(connection, groupName); - - Transport.TransportConnected = () => - { - var command = new ServerCommand - { - ServerCommandType = ServerCommandType.RemoveConnection, - Value = connectionId - }; - - return _serverMessageHandler.SendCommand(command); - }; - - Transport.Connected = () => - { - return TaskAsyncHelper.FromMethod(() => OnConnected(context.Request, connectionId).OrEmpty()); - }; - - Transport.Reconnected = () => - { - return TaskAsyncHelper.FromMethod(() => OnReconnected(context.Request, connectionId).OrEmpty()); - }; - - Transport.Received = data => - { - Counters.ConnectionMessagesSentTotal.Increment(); - Counters.ConnectionMessagesSentPerSec.Increment(); - return TaskAsyncHelper.FromMethod(() => OnReceived(context.Request, connectionId, data).OrEmpty()); - }; - - Transport.Disconnected = () => - { - return TaskAsyncHelper.FromMethod(() => OnDisconnected(context.Request, connectionId).OrEmpty()); - }; - - return Transport.ProcessRequest(connection).OrEmpty().Catch(Counters.ErrorsAllTotal, Counters.ErrorsAllPerSec); - } - - [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "We want to catch any exception when unprotecting data.")] - internal bool TryGetConnectionId(HostContext context, - string connectionToken, - out string connectionId, - out string message, - out int statusCode) - { - string unprotectedConnectionToken = null; - - // connectionId is only valid when this method returns true - connectionId = null; - - // message and statusCode are only valid when this method returns false - message = null; - statusCode = 400; - - try - { - unprotectedConnectionToken = ProtectedData.Unprotect(connectionToken, Purposes.ConnectionToken); - } - catch (Exception ex) - { - Trace.TraceInformation("Failed to process connectionToken {0}: {1}", connectionToken, ex); - } - - if (String.IsNullOrEmpty(unprotectedConnectionToken)) - { - message = String.Format(CultureInfo.CurrentCulture, Resources.Error_ConnectionIdIncorrectFormat); - return false; - } - - var tokens = unprotectedConnectionToken.Split(SplitChars, 2); - - connectionId = tokens[0]; - string tokenUserName = tokens.Length > 1 ? tokens[1] : String.Empty; - string userName = GetUserIdentity(context); - - if (!String.Equals(tokenUserName, userName, StringComparison.OrdinalIgnoreCase)) - { - message = String.Format(CultureInfo.CurrentCulture, Resources.Error_UnrecognizedUserIdentity); - statusCode = 403; - return false; - } - - return true; - } - - [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "We want to prevent any failures in unprotecting")] - internal IList VerifyGroups(HostContext context, string connectionId) - { - string groupsToken = context.Request.QueryString["groupsToken"]; - - if (String.IsNullOrEmpty(groupsToken)) - { - Trace.TraceInformation("The groups token is missing"); - - return ListHelper.Empty; - } - - string unprotectedGroupsToken = null; - - try - { - unprotectedGroupsToken = ProtectedData.Unprotect(groupsToken, Purposes.Groups); - } - catch (Exception ex) - { - Trace.TraceInformation("Failed to process groupsToken {0}: {1}", groupsToken, ex); - } - - if (String.IsNullOrEmpty(unprotectedGroupsToken)) - { - return ListHelper.Empty; - } - - var tokens = unprotectedGroupsToken.Split(SplitChars, 2); - - string groupConnectionId = tokens[0]; - string groupsValue = tokens.Length > 1 ? tokens[1] : String.Empty; - - if (!String.Equals(groupConnectionId, connectionId, StringComparison.OrdinalIgnoreCase)) - { - return ListHelper.Empty; - } - - return JsonSerializer.Parse(groupsValue); - } - - private IList AppendGroupPrefixes(HostContext context, string connectionId) - { - return (from g in OnRejoiningGroups(context.Request, VerifyGroups(context, connectionId), connectionId) - select GroupPrefix + g).ToList(); - } - - private Connection CreateConnection(string connectionId, IList signals, IList groups) - { - return new Connection(MessageBus, - JsonSerializer, - DefaultSignal, - connectionId, - signals, - groups, - TraceManager, - AckHandler, - Counters, - ProtectedData); - } - - /// - /// Returns the default signals for the . - /// - /// The id of the incoming connection. - /// The default signals for this . - private IList GetDefaultSignals(string connectionId) - { - // The list of default signals this connection cares about: - // 1. The default signal (the type name) - // 2. The connection id (so we can message this particular connection) - // 3. Ack signal - - return new string[] { - DefaultSignal, - PrefixHelper.GetConnectionId(connectionId), - PrefixHelper.GetAck(connectionId) - }; - } - - /// - /// Returns the signals used in the . - /// - /// The id of the incoming connection. - /// The signals used for this . - protected virtual IList GetSignals(string connectionId) - { - return GetDefaultSignals(connectionId); - } - - /// - /// Called before every request and gives the user a authorize the user. - /// - /// The for the current connection. - /// A boolean value that represents if the request is authorized. - protected virtual bool AuthorizeRequest(IRequest request) - { - return true; - } - - /// - /// Called when a connection reconnects after a timeout to determine which groups should be rejoined. - /// - /// The for the current connection. - /// The groups the calling connection claims to be part of. - /// The id of the reconnecting client. - /// A collection of group names that should be joined on reconnect - protected virtual IList OnRejoiningGroups(IRequest request, IList groups, string connectionId) - { - return groups; - } - - /// - /// Called when a new connection is made. - /// - /// The for the current connection. - /// The id of the connecting client. - /// A that completes when the connect operation is complete. - protected virtual Task OnConnected(IRequest request, string connectionId) - { - return TaskAsyncHelper.Empty; - } - - /// - /// Called when a connection reconnects after a timeout. - /// - /// The for the current connection. - /// The id of the re-connecting client. - /// A that completes when the re-connect operation is complete. - protected virtual Task OnReconnected(IRequest request, string connectionId) - { - return TaskAsyncHelper.Empty; - } - - /// - /// Called when data is received from a connection. - /// - /// The for the current connection. - /// The id of the connection sending the data. - /// The payload sent to the connection. - /// A that completes when the receive operation is complete. - protected virtual Task OnReceived(IRequest request, string connectionId, string data) - { - return TaskAsyncHelper.Empty; - } - - /// - /// Called when a connection disconnects. - /// - /// The for the current connection. - /// The id of the disconnected connection. - /// A that completes when the disconnect operation is complete. - protected virtual Task OnDisconnected(IRequest request, string connectionId) - { - return TaskAsyncHelper.Empty; - } - - private Task ProcessPingRequest(HostContext context) - { - var payload = new - { - Response = "pong" - }; - - if (!String.IsNullOrEmpty(context.Request.QueryString["callback"])) - { - return ProcessJsonpRequest(context, payload); - } - - context.Response.ContentType = JsonUtility.JsonMimeType; - return context.Response.End(JsonSerializer.Stringify(payload)); - } - - private Task ProcessNegotiationRequest(HostContext context) - { - // Total amount of time without a keep alive before the client should attempt to reconnect in seconds. - var keepAliveTimeout = _configurationManager.KeepAliveTimeout(); - string connectionId = Guid.NewGuid().ToString("d"); - string connectionToken = connectionId + ':' + GetUserIdentity(context); - - var payload = new - { - Url = context.Request.Url.LocalPath.Replace("/negotiate", ""), - ConnectionToken = ProtectedData.Protect(connectionToken, Purposes.ConnectionToken), - ConnectionId = connectionId, - KeepAliveTimeout = keepAliveTimeout != null ? keepAliveTimeout.Value.TotalSeconds : (double?)null, - DisconnectTimeout = _configurationManager.DisconnectTimeout.TotalSeconds, - TryWebSockets = _transportManager.SupportsTransport(WebSocketsTransportName) && context.SupportsWebSockets(), - WebSocketServerUrl = context.WebSocketServerUrl(), - ProtocolVersion = "1.2" - }; - - if (!String.IsNullOrEmpty(context.Request.QueryString["callback"])) - { - return ProcessJsonpRequest(context, payload); - } - - context.Response.ContentType = JsonUtility.JsonMimeType; - return context.Response.End(JsonSerializer.Stringify(payload)); - } - - private static string GetUserIdentity(HostContext context) - { - if (context.Request.User != null && context.Request.User.Identity.IsAuthenticated) - { - return context.Request.User.Identity.Name ?? String.Empty; - } - return String.Empty; - } - - private Task ProcessJsonpRequest(HostContext context, object payload) - { - context.Response.ContentType = JsonUtility.JavaScriptMimeType; - var data = JsonUtility.CreateJsonpCallback(context.Request.QueryString["callback"], JsonSerializer.Stringify(payload)); - - return context.Response.End(data); - } - - private static Task FailResponse(IResponse response, string message, int statusCode = 400) - { - response.StatusCode = statusCode; - return response.End(message); - } - - private static bool IsNegotiationRequest(IRequest request) - { - return request.Url.LocalPath.EndsWith("/negotiate", StringComparison.OrdinalIgnoreCase); - } - - private static bool IsPingRequest(IRequest request) - { - return request.Url.LocalPath.EndsWith("/ping", StringComparison.OrdinalIgnoreCase); - } - - private ITransport GetTransport(HostContext context) - { - return _transportManager.GetTransport(context); - } - } -} diff --git a/src/Microsoft.AspNet.SignalR.Core/Properties/AssemblyInfo.cs b/src/Microsoft.AspNet.SignalR.Core/Properties/AssemblyInfo.cs deleted file mode 100644 index b73b3e766..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -using System.Reflection; -using System.Runtime.CompilerServices; - -[assembly: AssemblyTitle("Microsoft.AspNet.SignalR.Core")] -[assembly: AssemblyDescription("Async signaling library for .NET to help build real-time, multi-user interactive web applications.")] -#if SIGNED -[assembly: InternalsVisibleTo("Microsoft.AspNet.SignalR.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b5fc90e7027f67871e773a8fde8938c81dd402ba65b9201d60593e96c492651e889cc13f1415ebb53fac1131ae0bd333c5ee6021672d9718ea31a8aebd0da0072f25d87dba6fc90ffd598ed4da35e44c398c454307e8e33b8426143daec9f596836f97c8f74750e5975c64e2189f45def46b2a2b1247adc3652bf5c308055da9")] -[assembly: InternalsVisibleTo("Microsoft.AspNet.SignalR.FunctionalTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b5fc90e7027f67871e773a8fde8938c81dd402ba65b9201d60593e96c492651e889cc13f1415ebb53fac1131ae0bd333c5ee6021672d9718ea31a8aebd0da0072f25d87dba6fc90ffd598ed4da35e44c398c454307e8e33b8426143daec9f596836f97c8f74750e5975c64e2189f45def46b2a2b1247adc3652bf5c308055da9")] -[assembly: InternalsVisibleTo("Microsoft.AspNet.SignalR.Tests.Common, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b5fc90e7027f67871e773a8fde8938c81dd402ba65b9201d60593e96c492651e889cc13f1415ebb53fac1131ae0bd333c5ee6021672d9718ea31a8aebd0da0072f25d87dba6fc90ffd598ed4da35e44c398c454307e8e33b8426143daec9f596836f97c8f74750e5975c64e2189f45def46b2a2b1247adc3652bf5c308055da9")] -#else -[assembly: InternalsVisibleTo("Microsoft.AspNet.SignalR.Tests")] -[assembly: InternalsVisibleTo("Microsoft.AspNet.SignalR.FunctionalTests")] -[assembly: InternalsVisibleTo("Microsoft.AspNet.SignalR.Tests.Common")] -#endif diff --git a/src/Microsoft.AspNet.SignalR.Core/Resources.Designer.cs b/src/Microsoft.AspNet.SignalR.Core/Resources.Designer.cs deleted file mode 100644 index 92f0119ea..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/Resources.Designer.cs +++ /dev/null @@ -1,375 +0,0 @@ -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// Runtime Version:4.0.30319.18010 -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ - -namespace Microsoft.AspNet.SignalR { - /// - /// A strongly-typed resource class, for looking up localized strings, etc. - /// - // This class was auto-generated by the StronglyTypedResourceBuilder - // class via a tool like ResGen or Visual Studio. - // To add or remove a member, edit your .ResX file then rerun ResGen - // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - internal class Resources { - - private static global::System.Resources.ResourceManager resourceMan; - - private static global::System.Globalization.CultureInfo resourceCulture; - - [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] - internal Resources() { - } - - /// - /// Returns the cached ResourceManager instance used by this class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Resources.ResourceManager ResourceManager { - get { - if (object.ReferenceEquals(resourceMan, null)) { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Microsoft.AspNet.SignalR.Resources", typeof(Resources).Assembly); - resourceMan = temp; - } - return resourceMan; - } - } - - /// - /// Overrides the current thread's CurrentUICulture property for all - /// resource lookups using this strongly typed resource class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Globalization.CultureInfo Culture { - get { - return resourceCulture; - } - set { - resourceCulture = value; - } - } - - /// - /// Looks up a localized string similar to /// <summary>Calls the {0} method on the server-side {1} hub.&#10;Returns a jQuery.Deferred() promise.</summary>. - /// - internal static string DynamicComment_CallsMethodOnServerSideDeferredPromise { - get { - return ResourceManager.GetString("DynamicComment_CallsMethodOnServerSideDeferredPromise", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to /// <param name=\"{0}\" type=\"{1}\">Server side type is {2}</param>. - /// - internal static string DynamicComment_ServerSideTypeIs { - get { - return ResourceManager.GetString("DynamicComment_ServerSideTypeIs", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Argument cannot be null or empty. - /// - internal static string Error_ArgumentNullOrEmpty { - get { - return ResourceManager.GetString("Error_ArgumentNullOrEmpty", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The buffer size '{0}' is out of range.. - /// - internal static string Error_BufferSizeOutOfRange { - get { - return ResourceManager.GetString("Error_BufferSizeOutOfRange", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Caller is not authorized to invoke the {0} method on {1}.. - /// - internal static string Error_CallerNotAuthorizedToInvokeMethodOn { - get { - return ResourceManager.GetString("Error_CallerNotAuthorizedToInvokeMethodOn", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The connection id is in the incorrect format.. - /// - internal static string Error_ConnectionIdIncorrectFormat { - get { - return ResourceManager.GetString("Error_ConnectionIdIncorrectFormat", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The PersistentConnection is not initialized.. - /// - internal static string Error_ConnectionNotInitialized { - get { - return ResourceManager.GetString("Error_ConnectionNotInitialized", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to DisconnectTimeout cannot be configured after the KeepAlive.. - /// - internal static string Error_DisconnectTimeoutCannotBeConfiguredAfterKeepAlive { - get { - return ResourceManager.GetString("Error_DisconnectTimeoutCannotBeConfiguredAfterKeepAlive", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to DisconnectTimeout must be at least six seconds.. - /// - internal static string Error_DisconnectTimeoutMustBeAtLeastSixSeconds { - get { - return ResourceManager.GetString("Error_DisconnectTimeoutMustBeAtLeastSixSeconds", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Do not read RequireOutgoing. Use protected _requireOutgoing instead.. - /// - internal static string Error_DoNotReadRequireOutgoing { - get { - return ResourceManager.GetString("Error_DoNotReadRequireOutgoing", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Duplicate hub names found.. - /// - internal static string Error_DuplicateHubs { - get { - return ResourceManager.GetString("Error_DuplicateHubs", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Duplicate payload id detected for stream '{0}'.. - /// - internal static string Error_DuplicatePayloadsForStream { - get { - return ResourceManager.GetString("Error_DuplicatePayloadsForStream", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Error creating Hub {0}. . - /// - internal static string Error_ErrorCreatingHub { - get { - return ResourceManager.GetString("Error_ErrorCreatingHub", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to '{0}' Hub could not be resolved.. - /// - internal static string Error_HubCouldNotBeResolved { - get { - return ResourceManager.GetString("Error_HubCouldNotBeResolved", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to There was an error invoking Hub method '{0}.{1}'.. - /// - internal static string Error_HubInvocationFailed { - get { - return ResourceManager.GetString("Error_HubInvocationFailed", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Invalid cursor.. - /// - internal static string Error_InvalidCursorFormat { - get { - return ResourceManager.GetString("Error_InvalidCursorFormat", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The supplied frameId is in the incorrect format.. - /// - internal static string Error_InvalidForeverFrameId { - get { - return ResourceManager.GetString("Error_InvalidForeverFrameId", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to '{0}' is not a {1}.. - /// - internal static string Error_IsNotA { - get { - return ResourceManager.GetString("Error_IsNotA", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to SignalR: JavaScript Hub proxy generation has been disabled.. - /// - internal static string Error_JavaScriptProxyDisabled { - get { - return ResourceManager.GetString("Error_JavaScriptProxyDisabled", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Keep Alive value must be greater than two seconds.. - /// - internal static string Error_KeepAliveMustBeGreaterThanTwoSeconds { - get { - return ResourceManager.GetString("Error_KeepAliveMustBeGreaterThanTwoSeconds", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Keep Alive value must be no more than a third of the DisconnectTimeout.. - /// - internal static string Error_KeepAliveMustBeNoMoreThanAThirdOfTheDisconnectTimeout { - get { - return ResourceManager.GetString("Error_KeepAliveMustBeNoMoreThanAThirdOfTheDisconnectTimeout", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to '{0}' method could not be resolved.. - /// - internal static string Error_MethodCouldNotBeResolved { - get { - return ResourceManager.GetString("Error_MethodCouldNotBeResolved", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Outgoing authorization can only be required for an entire Hub, not a specific method.. - /// - internal static string Error_MethodLevelOutgoingAuthorization { - get { - return ResourceManager.GetString("Error_MethodLevelOutgoingAuthorization", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Multiple activators for type {0} are registered. Please call GetServices instead.. - /// - internal static string Error_MultipleActivatorsAreaRegisteredCallGetServices { - get { - return ResourceManager.GetString("Error_MultipleActivatorsAreaRegisteredCallGetServices", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Unexpected end when reading object.. - /// - internal static string Error_ParseObjectFailed { - get { - return ResourceManager.GetString("Error_ParseObjectFailed", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Protocol error: Missing connection token.. - /// - internal static string Error_ProtocolErrorMissingConnectionToken { - get { - return ResourceManager.GetString("Error_ProtocolErrorMissingConnectionToken", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Protocol error: Unknown transport.. - /// - internal static string Error_ProtocolErrorUnknownTransport { - get { - return ResourceManager.GetString("Error_ProtocolErrorUnknownTransport", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to State has exceeded the maximum length of 4096 bytes.. - /// - internal static string Error_StateExceededMaximumLength { - get { - return ResourceManager.GetString("Error_StateExceededMaximumLength", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The stream has been closed.. - /// - internal static string Error_StreamClosed { - get { - return ResourceManager.GetString("Error_StreamClosed", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The stream is not open.. - /// - internal static string Error_StreamNotOpen { - get { - return ResourceManager.GetString("Error_StreamNotOpen", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The queue is full.. - /// - internal static string Error_TaskQueueFull { - get { - return ResourceManager.GetString("Error_TaskQueueFull", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Unable to add module. The HubPipeline has already been invoked.. - /// - internal static string Error_UnableToAddModulePiplineAlreadyInvoked { - get { - return ResourceManager.GetString("Error_UnableToAddModulePiplineAlreadyInvoked", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Unrecognized user identity. The user identity cannot change during an active SignalR connection.. - /// - internal static string Error_UnrecognizedUserIdentity { - get { - return ResourceManager.GetString("Error_UnrecognizedUserIdentity", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Using a Hub instance not created by the HubPipeline is unsupported.. - /// - internal static string Error_UsingHubInstanceNotCreatedUnsupported { - get { - return ResourceManager.GetString("Error_UsingHubInstanceNotCreatedUnsupported", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to WebSockets is not supported.. - /// - internal static string Error_WebSocketsNotSupported { - get { - return ResourceManager.GetString("Error_WebSocketsNotSupported", resourceCulture); - } - } - } -} diff --git a/src/Microsoft.AspNet.SignalR.Core/Resources.resx b/src/Microsoft.AspNet.SignalR.Core/Resources.resx deleted file mode 100644 index a858f4b40..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/Resources.resx +++ /dev/null @@ -1,225 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - /// <summary>Calls the {0} method on the server-side {1} hub.&#10;Returns a jQuery.Deferred() promise.</summary> - - - /// <param name=\"{0}\" type=\"{1}\">Server side type is {2}</param> - - - Argument cannot be null or empty - - - The buffer size '{0}' is out of range. - - - Caller is not authorized to invoke the {0} method on {1}. - - - The connection id is in the incorrect format. - - - The PersistentConnection is not initialized. - - - DisconnectTimeout cannot be configured after the KeepAlive. - - - DisconnectTimeout must be at least six seconds. - - - Do not read RequireOutgoing. Use protected _requireOutgoing instead. - - - Duplicate hub names found. - - - Duplicate payload id detected for stream '{0}'. - - - Error creating Hub {0}. - - - '{0}' Hub could not be resolved. - - - There was an error invoking Hub method '{0}.{1}'. - - - Invalid cursor. - - - The supplied frameId is in the incorrect format. - - - '{0}' is not a {1}. - - - SignalR: JavaScript Hub proxy generation has been disabled. - - - Keep Alive value must be greater than two seconds. - - - Keep Alive value must be no more than a third of the DisconnectTimeout. - - - '{0}' method could not be resolved. - - - Outgoing authorization can only be required for an entire Hub, not a specific method. - - - Multiple activators for type {0} are registered. Please call GetServices instead. - - - Unexpected end when reading object. - - - Protocol error: Missing connection token. - - - Protocol error: Unknown transport. - - - State has exceeded the maximum length of 4096 bytes. - - - The stream has been closed. - - - The stream is not open. - - - The queue is full. - - - Unable to add module. The HubPipeline has already been invoked. - - - Unrecognized user identity. The user identity cannot change during an active SignalR connection. - - - Using a Hub instance not created by the HubPipeline is unsupported. - - - WebSockets is not supported. - - \ No newline at end of file diff --git a/src/Microsoft.AspNet.SignalR.Core/Scripts/hubs.js b/src/Microsoft.AspNet.SignalR.Core/Scripts/hubs.js deleted file mode 100644 index ca0f7977d..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/Scripts/hubs.js +++ /dev/null @@ -1,90 +0,0 @@ -/*! - * ASP.NET SignalR JavaScript Library v1.2.2 - * http://signalr.net/ - * - * Copyright Microsoft Open Technologies, Inc. All rights reserved. - * Licensed under the Apache 2.0 - * https://github.com/SignalR/SignalR/blob/master/LICENSE.md - * - */ - -/// -/// -(function ($, window, undefined) { - /// - "use strict"; - - if (typeof ($.signalR) !== "function") { - throw new Error("SignalR: SignalR is not loaded. Please ensure jquery.signalR-x.js is referenced before ~/signalr/hubs."); - } - - var signalR = $.signalR; - - function makeProxyCallback(hub, callback) { - return function () { - // Call the client hub method - callback.apply(hub, $.makeArray(arguments)); - }; - } - - function registerHubProxies(instance, shouldSubscribe) { - var key, hub, memberKey, memberValue, subscriptionMethod; - - for (key in instance) { - if (instance.hasOwnProperty(key)) { - hub = instance[key]; - - if (!(hub.hubName)) { - // Not a client hub - continue; - } - - if (shouldSubscribe) { - // We want to subscribe to the hub events - subscriptionMethod = hub.on; - } - else { - // We want to unsubscribe from the hub events - subscriptionMethod = hub.off; - } - - // Loop through all members on the hub and find client hub functions to subscribe/unsubscribe - for (memberKey in hub.client) { - if (hub.client.hasOwnProperty(memberKey)) { - memberValue = hub.client[memberKey]; - - if (!$.isFunction(memberValue)) { - // Not a client hub function - continue; - } - - subscriptionMethod.call(hub, memberKey, makeProxyCallback(hub, memberValue)); - } - } - } - } - } - - $.hubConnection.prototype.createHubProxies = function () { - var proxies = {}; - this.starting(function () { - // Register the hub proxies as subscribed - // (instance, shouldSubscribe) - registerHubProxies(proxies, true); - - this._registerSubscribedHubs(); - }).disconnected(function () { - // Unsubscribe all hub proxies when we "disconnect". This is to ensure that we do not re-add functional call backs. - // (instance, shouldSubscribe) - registerHubProxies(proxies, false); - }); - - /*hubs*/ - - return proxies; - }; - - signalR.hub = $.hubConnection("{serviceUrl}", { useDefaultPath: false }); - $.extend(signalR, signalR.hub.createHubProxies()); - -}(window.jQuery, window)); \ No newline at end of file diff --git a/src/Microsoft.AspNet.SignalR.Core/TaskAsyncHelper.cs b/src/Microsoft.AspNet.SignalR.Core/TaskAsyncHelper.cs deleted file mode 100644 index 34a604bb2..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/TaskAsyncHelper.cs +++ /dev/null @@ -1,1115 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -using System; -using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.AspNet.SignalR.Infrastructure; - -namespace Microsoft.AspNet.SignalR -{ - internal static class TaskAsyncHelper - { - private static readonly Task _emptyTask = MakeTask(null); - private static readonly Task _trueTask = MakeTask(true); - private static readonly Task _falseTask = MakeTask(false); - - private static Task MakeTask(T value) - { - return FromResult(value); - } - - [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "This is a shared file")] - public static Task Empty - { - get - { - return _emptyTask; - } - } - - [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "This is a shared file")] - public static Task True - { - get - { - return _trueTask; - } - } - - [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "This is a shared file")] - public static Task False - { - get - { - return _falseTask; - } - } - - [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "This is a shared file")] - public static Task OrEmpty(this Task task) - { - return task ?? Empty; - } - - [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "This is a shared file")] - public static Task OrEmpty(this Task task) - { - return task ?? TaskCache.Empty; - } - - [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "This is a shared file")] - [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Exceptions are set in a tcs")] - public static Task FromAsync(Func beginMethod, Action endMethod, object state) - { - try - { - return Task.Factory.FromAsync(beginMethod, endMethod, state); - } - catch (Exception ex) - { - return TaskAsyncHelper.FromError(ex); - } - } - - [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "This is a shared file")] - [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Exceptions are set in a tcs")] - public static Task FromAsync(Func beginMethod, Func endMethod, object state) - { - try - { - return Task.Factory.FromAsync(beginMethod, endMethod, state); - } - catch (Exception ex) - { - return TaskAsyncHelper.FromError(ex); - } - } - - [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "This is a shared file")] - public static Task Series(Func[] tasks, object[] state) - { - Task prev = TaskAsyncHelper.Empty; - Task finalTask = TaskAsyncHelper.Empty; - - for (int i = 0; i < tasks.Length; i++) - { - prev = finalTask; - finalTask = prev.Then(tasks[i], state[i]); - } - - return finalTask; - } - - [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "This is a shared file")] - public static TTask Catch(this TTask task) where TTask : Task - { - return Catch(task, ex => { }); - } - -#if PERFCOUNTERS - public static TTask Catch(this TTask task, params IPerformanceCounter[] counters) where TTask : Task - { - return Catch(task, _ => - { - if (counters == null) - { - return; - } - for (var i = 0; i < counters.Length; i++) - { - counters[i].Increment(); - } - }); - } -#endif - - [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "This is a shared file")] - public static TTask Catch(this TTask task, Action handler, object state) where TTask : Task - { - if (task != null && task.Status != TaskStatus.RanToCompletion) - { - if (task.Status == TaskStatus.Faulted) - { - ExecuteOnFaulted(handler, state, task.Exception); - } - else - { - AttachFaultedContinuation(task, handler, state); - } - } - - return task; - } - - [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "This is a shared file")] - private static void AttachFaultedContinuation(TTask task, Action handler, object state) where TTask : Task - { - task.ContinueWith(innerTask => - { - ExecuteOnFaulted(handler, state, innerTask.Exception); - }, - TaskContinuationOptions.OnlyOnFaulted | TaskContinuationOptions.ExecuteSynchronously); - } - - [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "This is a shared file")] - private static void ExecuteOnFaulted(Action handler, object state, AggregateException exception) - { - // observe Exception -#if !WINDOWS_PHONE && !SILVERLIGHT && !NETFX_CORE - Trace.TraceWarning("SignalR exception thrown by Task: {0}", exception); -#endif - handler(exception, state); - } - - [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "This is a shared file")] - public static TTask Catch(this TTask task, Action handler) where TTask : Task - { - return task.Catch((ex, state) => ((Action)state).Invoke(ex), - handler); - } - - [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "This is a shared file")] - [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Exceptions are set in a tcs")] - public static Task ContinueWithNotComplete(this Task task, Action action) - { - switch (task.Status) - { - case TaskStatus.Faulted: - case TaskStatus.Canceled: - try - { - action(); - return task; - } - catch (Exception e) - { - return FromError(e); - } - case TaskStatus.RanToCompletion: - return task; - default: - var tcs = new TaskCompletionSource(); - - task.ContinueWith(t => - { - if (t.IsFaulted || t.IsCanceled) - { - try - { - action(); - - if (t.IsFaulted) - { - tcs.TrySetUnwrappedException(t.Exception); - } - else - { - tcs.TrySetCanceled(); - } - } - catch (Exception e) - { - tcs.TrySetException(e); - } - } - else - { - tcs.TrySetResult(null); - } - }, - TaskContinuationOptions.ExecuteSynchronously); - - return tcs.Task; - } - } - - [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "This is a shared file")] - public static void ContinueWithNotComplete(this Task task, TaskCompletionSource tcs) - { - task.ContinueWith(t => - { - if (t.IsFaulted) - { - tcs.SetUnwrappedException(t.Exception); - } - else if (t.IsCanceled) - { - tcs.SetCanceled(); - } - }, - TaskContinuationOptions.NotOnRanToCompletion); - } - - [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "This is a shared file")] - public static void ContinueWith(this Task task, TaskCompletionSource tcs) - { - task.ContinueWith(t => - { - if (t.IsFaulted) - { - tcs.TrySetUnwrappedException(t.Exception); - } - else if (t.IsCanceled) - { - tcs.TrySetCanceled(); - } - else - { - tcs.TrySetResult(null); - } - }, - TaskContinuationOptions.ExecuteSynchronously); - } - - [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "This is a shared file")] - public static void ContinueWith(this Task task, TaskCompletionSource tcs) - { - task.ContinueWith(t => - { - if (t.IsFaulted) - { - tcs.TrySetUnwrappedException(t.Exception); - } - else if (t.IsCanceled) - { - tcs.TrySetCanceled(); - } - else - { - tcs.TrySetResult(t.Result); - } - }); - } - - [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "This is a shared file")] - public static Task Return(this Task[] tasks) - { - return Then(tasks, () => { }); - } - - // Then extesions - [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "This is a shared file")] - public static Task Then(this Task task, Action successor) - { - switch (task.Status) - { - case TaskStatus.Faulted: - case TaskStatus.Canceled: - return task; - - case TaskStatus.RanToCompletion: - return FromMethod(successor); - - default: - return RunTask(task, successor); - } - } - - [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "This is a shared file")] - public static Task Then(this Task task, Func successor) - { - switch (task.Status) - { - case TaskStatus.Faulted: - return FromError(task.Exception); - - case TaskStatus.Canceled: - return Canceled(); - - case TaskStatus.RanToCompletion: - return FromMethod(successor); - - default: - return TaskRunners.RunTask(task, successor); - } - } - - [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "This is a shared file")] - public static Task Then(this Task[] tasks, Action successor) - { - if (tasks.Length == 0) - { - return FromMethod(successor); - } - - var tcs = new TaskCompletionSource(); - Task.Factory.ContinueWhenAll(tasks, completedTasks => - { - var faulted = completedTasks.FirstOrDefault(t => t.IsFaulted); - if (faulted != null) - { - tcs.SetUnwrappedException(faulted.Exception); - return; - } - var cancelled = completedTasks.FirstOrDefault(t => t.IsCanceled); - if (cancelled != null) - { - tcs.SetCanceled(); - return; - } - - successor(); - tcs.SetResult(null); - }); - - return tcs.Task; - } - - [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "This is a shared file")] - public static Task Then(this Task task, Action successor, T1 arg1) - { - switch (task.Status) - { - case TaskStatus.Faulted: - case TaskStatus.Canceled: - return task; - - case TaskStatus.RanToCompletion: - return FromMethod(successor, arg1); - - default: - return GenericDelegates.ThenWithArgs(task, successor, arg1); - } - } - - [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "This is a shared file")] - public static Task Then(this Task task, Action successor, T1 arg1, T2 arg2) - { - switch (task.Status) - { - case TaskStatus.Faulted: - case TaskStatus.Canceled: - return task; - - case TaskStatus.RanToCompletion: - return FromMethod(successor, arg1, arg2); - - default: - return GenericDelegates.ThenWithArgs(task, successor, arg1, arg2); - } - } - - [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "This is a shared file")] - public static Task Then(this Task task, Func successor, T1 arg1) - { - switch (task.Status) - { - case TaskStatus.Faulted: - case TaskStatus.Canceled: - return task; - - case TaskStatus.RanToCompletion: - return FromMethod(successor, arg1); - - default: - return GenericDelegates.ThenWithArgs(task, successor, arg1) - .FastUnwrap(); - } - } - - [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "This is a shared file")] - public static Task Then(this Task task, Func successor, T1 arg1, T2 arg2) - { - switch (task.Status) - { - case TaskStatus.Faulted: - case TaskStatus.Canceled: - return task; - - case TaskStatus.RanToCompletion: - return FromMethod(successor, arg1, arg2); - - default: - return GenericDelegates.ThenWithArgs(task, successor, arg1, arg2) - .FastUnwrap(); - } - } - - [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "This is a shared file")] - public static Task Then(this Task task, Func> successor) - { - switch (task.Status) - { - case TaskStatus.Faulted: - return FromError(task.Exception); - - case TaskStatus.Canceled: - return Canceled(); - - case TaskStatus.RanToCompletion: - return FromMethod(successor, task.Result); - - default: - return TaskRunners>.RunTask(task, t => successor(t.Result)) - .FastUnwrap(); - } - } - - [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "This is a shared file")] - public static Task Then(this Task task, Func successor) - { - switch (task.Status) - { - case TaskStatus.Faulted: - return FromError(task.Exception); - - case TaskStatus.Canceled: - return Canceled(); - - case TaskStatus.RanToCompletion: - return FromMethod(successor, task.Result); - - default: - return TaskRunners.RunTask(task, t => successor(t.Result)); - } - } - - [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "This is a shared file")] - public static Task Then(this Task task, Func successor, T1 arg1) - { - switch (task.Status) - { - case TaskStatus.Faulted: - return FromError(task.Exception); - - case TaskStatus.Canceled: - return Canceled(); - - case TaskStatus.RanToCompletion: - return FromMethod(successor, task.Result, arg1); - - default: - return GenericDelegates.ThenWithArgs(task, successor, arg1); - } - } - - [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "This is a shared file")] - public static Task Then(this Task task, Func successor) - { - switch (task.Status) - { - case TaskStatus.Faulted: - case TaskStatus.Canceled: - return task; - - case TaskStatus.RanToCompletion: - return FromMethod(successor); - - default: - return TaskRunners.RunTask(task, successor) - .FastUnwrap(); - } - } - - [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "This is a shared file")] - public static Task Then(this Task task, Func> successor) - { - switch (task.Status) - { - case TaskStatus.Faulted: - return FromError(task.Exception); - - case TaskStatus.Canceled: - return Canceled(); - - case TaskStatus.RanToCompletion: - return FromMethod(successor); - - default: - return TaskRunners>.RunTask(task, successor) - .FastUnwrap(); - } - } - - [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "This is a shared file")] - public static Task Then(this Task task, Action successor) - { - switch (task.Status) - { - case TaskStatus.Faulted: - case TaskStatus.Canceled: - return task; - - case TaskStatus.RanToCompletion: - return FromMethod(successor, task.Result); - - default: - return TaskRunners.RunTask(task, successor); - } - } - - [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "This is a shared file")] - public static Task Then(this Task task, Func successor) - { - switch (task.Status) - { - case TaskStatus.Faulted: - case TaskStatus.Canceled: - return task; - - case TaskStatus.RanToCompletion: - return FromMethod(successor, task.Result); - - default: - return TaskRunners.RunTask(task, t => successor(t.Result)) - .FastUnwrap(); - } - } - - [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "This is a shared file")] - public static Task Then(this Task task, Func, T1, Task> successor, T1 arg1) - { - switch (task.Status) - { - case TaskStatus.Faulted: - case TaskStatus.Canceled: - return task; - - case TaskStatus.RanToCompletion: - return FromMethod(successor, task, arg1); - - default: - return GenericDelegates, T1, object>.ThenWithArgs(task, successor, arg1) - .FastUnwrap(); - } - } - - [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Exceptions are flowed to the caller")] - [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "This is a shared file")] - public static Task Finally(this Task task, Action next, object state) - { - try - { - switch (task.Status) - { - case TaskStatus.Faulted: - case TaskStatus.Canceled: - next(state); - return task; - case TaskStatus.RanToCompletion: - return FromMethod(next, state); - - default: - return RunTaskSynchronously(task, next, state, onlyOnSuccess: false); - } - } - catch (Exception ex) - { - return FromError(ex); - } - } - - [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "This is a shared file")] - public static Task RunSynchronously(this Task task, Action successor) - { - switch (task.Status) - { - case TaskStatus.Faulted: - case TaskStatus.Canceled: - return task; - - case TaskStatus.RanToCompletion: - return FromMethod(successor); - - default: - return RunTaskSynchronously(task, state => ((Action)state).Invoke(), successor); - } - } - - [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "This is a shared file")] - public static Task FastUnwrap(this Task task) - { - var innerTask = (task.Status == TaskStatus.RanToCompletion) ? task.Result : null; - return innerTask ?? task.Unwrap(); - } - - [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "This is a shared file")] - public static Task FastUnwrap(this Task> task) - { - var innerTask = (task.Status == TaskStatus.RanToCompletion) ? task.Result : null; - return innerTask ?? task.Unwrap(); - } - - [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "This is a shared file")] - public static Task Delay(TimeSpan timeOut) - { -#if NETFX_CORE - return Task.Delay(timeOut); -#else - var tcs = new TaskCompletionSource(); - - var timer = new Timer(tcs.SetResult, - null, - timeOut, - TimeSpan.FromMilliseconds(-1)); - - return tcs.Task.ContinueWith(_ => - { - timer.Dispose(); - }, - TaskContinuationOptions.ExecuteSynchronously); -#endif - } - - [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "This is a shared file")] - [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Exceptions are set in a tcs")] - public static Task FromMethod(Action func) - { - try - { - func(); - return Empty; - } - catch (Exception ex) - { - return FromError(ex); - } - } - - [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "This is a shared file")] - [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Exceptions are set in a tcs")] - public static Task FromMethod(Action func, T1 arg) - { - try - { - func(arg); - return Empty; - } - catch (Exception ex) - { - return FromError(ex); - } - } - - [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "This is a shared file")] - [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Exceptions are set in a tcs")] - public static Task FromMethod(Action func, T1 arg1, T2 arg2) - { - try - { - func(arg1, arg2); - return Empty; - } - catch (Exception ex) - { - return FromError(ex); - } - } - - [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "This is a shared file")] - [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Exceptions are set in a tcs")] - public static Task FromMethod(Func func) - { - try - { - return func(); - } - catch (Exception ex) - { - return FromError(ex); - } - } - - [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "This is a shared file")] - [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Exceptions are set in a tcs")] - public static Task FromMethod(Func> func) - { - try - { - return func(); - } - catch (Exception ex) - { - return FromError(ex); - } - } - - [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "This is a shared file")] - [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Exceptions are set in a tcs")] - public static Task FromMethod(Func func) - { - try - { - return FromResult(func()); - } - catch (Exception ex) - { - return FromError(ex); - } - } - - [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "This is a shared file")] - [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Exceptions are set in a tcs")] - public static Task FromMethod(Func func, T1 arg) - { - try - { - return func(arg); - } - catch (Exception ex) - { - return FromError(ex); - } - } - - [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "This is a shared file")] - [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Exceptions are set in a tcs")] - public static Task FromMethod(Func func, T1 arg1, T2 arg2) - { - try - { - return func(arg1, arg2); - } - catch (Exception ex) - { - return FromError(ex); - } - } - - [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "This is a shared file")] - [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Exceptions are set in a tcs")] - public static Task FromMethod(Func> func, T1 arg) - { - try - { - return func(arg); - } - catch (Exception ex) - { - return FromError(ex); - } - } - - [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "This is a shared file")] - [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Exceptions are set in a tcs")] - public static Task FromMethod(Func func, T1 arg) - { - try - { - return FromResult(func(arg)); - } - catch (Exception ex) - { - return FromError(ex); - } - } - - [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "This is a shared file")] - [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Exceptions are set in a tcs")] - public static Task FromMethod(Func> func, T1 arg1, T2 arg2) - { - try - { - return func(arg1, arg2); - } - catch (Exception ex) - { - return FromError(ex); - } - } - - [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "This is a shared file")] - [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Exceptions are set in a tcs")] - public static Task FromMethod(Func func, T1 arg1, T2 arg2) - { - try - { - return FromResult(func(arg1, arg2)); - } - catch (Exception ex) - { - return FromError(ex); - } - } - - [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "This is a shared file")] - public static Task FromResult(T value) - { - var tcs = new TaskCompletionSource(); - tcs.SetResult(value); - return tcs.Task; - } - - [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "This is a shared file")] - internal static Task FromError(Exception e) - { - return FromError(e); - } - - [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "This is a shared file")] - internal static Task FromError(Exception e) - { - var tcs = new TaskCompletionSource(); - tcs.SetUnwrappedException(e); - return tcs.Task; - } - - [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "This is a shared file")] - internal static void SetUnwrappedException(this TaskCompletionSource tcs, Exception e) - { - var aggregateException = e as AggregateException; - if (aggregateException != null) - { - tcs.SetException(aggregateException.InnerExceptions); - } - else - { - tcs.SetException(e); - } - } - - [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "This is a shared file")] - internal static bool TrySetUnwrappedException(this TaskCompletionSource tcs, Exception e) - { - var aggregateException = e as AggregateException; - if (aggregateException != null) - { - return tcs.TrySetException(aggregateException.InnerExceptions); - } - else - { - return tcs.TrySetException(e); - } - } - - [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "This is a shared file")] - private static Task Canceled() - { - var tcs = new TaskCompletionSource(); - tcs.SetCanceled(); - return tcs.Task; - } - - [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "This is a shared file")] - private static Task Canceled() - { - var tcs = new TaskCompletionSource(); - tcs.SetCanceled(); - return tcs.Task; - } - - [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Exceptions are set in a tcs")] - private static Task RunTask(Task task, Action successor) - { - var tcs = new TaskCompletionSource(); - task.ContinueWith(t => - { - if (t.IsFaulted) - { - tcs.SetUnwrappedException(t.Exception); - } - else if (t.IsCanceled) - { - tcs.SetCanceled(); - } - else - { - try - { - successor(); - tcs.SetResult(null); - } - catch (Exception ex) - { - tcs.SetUnwrappedException(ex); - } - } - }); - - return tcs.Task; - } - - [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Exceptions are set in a tcs")] - [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "This is a shared file")] - private static Task RunTaskSynchronously(Task task, Action next, object state, bool onlyOnSuccess = true) - { - var tcs = new TaskCompletionSource(); - task.ContinueWith(t => - { - try - { - if (t.IsFaulted) - { - if (!onlyOnSuccess) - { - next(state); - } - - tcs.SetUnwrappedException(t.Exception); - } - else if (t.IsCanceled) - { - if (!onlyOnSuccess) - { - next(state); - } - - tcs.SetCanceled(); - } - else - { - next(state); - tcs.SetResult(null); - } - } - catch (Exception ex) - { - tcs.SetUnwrappedException(ex); - } - }, - TaskContinuationOptions.ExecuteSynchronously); - - return tcs.Task; - } - - private static class TaskRunners - { - [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Exceptions are set in a tcs")] - internal static Task RunTask(Task task, Action successor) - { - var tcs = new TaskCompletionSource(); - task.ContinueWith(t => - { - if (t.IsFaulted) - { - tcs.SetUnwrappedException(t.Exception); - } - else if (t.IsCanceled) - { - tcs.SetCanceled(); - } - else - { - try - { - successor(t.Result); - tcs.SetResult(null); - } - catch (Exception ex) - { - tcs.SetUnwrappedException(ex); - } - } - }); - - return tcs.Task; - } - - [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Exceptions are set in a tcs")] - internal static Task RunTask(Task task, Func successor) - { - var tcs = new TaskCompletionSource(); - task.ContinueWith(t => - { - if (t.IsFaulted) - { - tcs.SetUnwrappedException(t.Exception); - } - else if (t.IsCanceled) - { - tcs.SetCanceled(); - } - else - { - try - { - tcs.SetResult(successor()); - } - catch (Exception ex) - { - tcs.SetUnwrappedException(ex); - } - } - }); - - return tcs.Task; - } - - [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Exceptions are set in a tcs")] - internal static Task RunTask(Task task, Func, TResult> successor) - { - var tcs = new TaskCompletionSource(); - task.ContinueWith(t => - { - if (task.IsFaulted) - { - tcs.SetUnwrappedException(t.Exception); - } - else if (task.IsCanceled) - { - tcs.SetCanceled(); - } - else - { - try - { - tcs.SetResult(successor(t)); - } - catch (Exception ex) - { - tcs.SetUnwrappedException(ex); - } - } - }); - - return tcs.Task; - } - } - - private static class GenericDelegates - { - internal static Task ThenWithArgs(Task task, Action successor, T1 arg1) - { - return RunTask(task, () => successor(arg1)); - } - - internal static Task ThenWithArgs(Task task, Action successor, T1 arg1, T2 arg2) - { - return RunTask(task, () => successor(arg1, arg2)); - } - - internal static Task ThenWithArgs(Task task, Func successor, T1 arg1) - { - return TaskRunners.RunTask(task, () => successor(arg1)); - } - - internal static Task ThenWithArgs(Task task, Func successor, T1 arg1, T2 arg2) - { - return TaskRunners.RunTask(task, () => successor(arg1, arg2)); - } - - internal static Task ThenWithArgs(Task task, Func successor, T1 arg1) - { - return TaskRunners.RunTask(task, t => successor(t.Result, arg1)); - } - - internal static Task ThenWithArgs(Task task, Func successor, T1 arg1) - { - return TaskRunners.RunTask(task, () => successor(arg1)); - } - - internal static Task ThenWithArgs(Task task, Func successor, T1 arg1, T2 arg2) - { - return TaskRunners.RunTask(task, () => successor(arg1, arg2)); - } - - internal static Task> ThenWithArgs(Task task, Func> successor, T1 arg1) - { - return TaskRunners>.RunTask(task, t => successor(t.Result, arg1)); - } - - internal static Task> ThenWithArgs(Task task, Func, T1, Task> successor, T1 arg1) - { - return TaskRunners>.RunTask(task, t => successor(t, arg1)); - } - } - - private static class TaskCache - { - public static Task Empty = MakeTask(default(T)); - } - } -} diff --git a/src/Microsoft.AspNet.SignalR.Core/Tracing/ITraceManager.cs b/src/Microsoft.AspNet.SignalR.Core/Tracing/ITraceManager.cs deleted file mode 100644 index 21b8f03dc..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/Tracing/ITraceManager.cs +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -using System.Diagnostics; - -namespace Microsoft.AspNet.SignalR.Tracing -{ - public interface ITraceManager - { - SourceSwitch Switch { get; } - TraceSource this[string name] { get; } - } -} diff --git a/src/Microsoft.AspNet.SignalR.Core/Tracing/TraceManager.cs b/src/Microsoft.AspNet.SignalR.Core/Tracing/TraceManager.cs deleted file mode 100644 index 8ca4264a6..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/Tracing/TraceManager.cs +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -using System; -using System.Collections.Concurrent; -using System.Diagnostics; - -namespace Microsoft.AspNet.SignalR.Tracing -{ - public class TraceManager : ITraceManager - { - private readonly ConcurrentDictionary _sources = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); - - public TraceManager() - { - Switch = new SourceSwitch("SignalRSwitch"); - } - - public SourceSwitch Switch { get; private set; } - - public TraceSource this[string name] - { - get - { - return _sources.GetOrAdd(name, key => new TraceSource(key, SourceLevels.Off) - { - Switch = Switch - }); - } - } - } -} diff --git a/src/Microsoft.AspNet.SignalR.Core/Tracing/TraceSourceExtensions.cs b/src/Microsoft.AspNet.SignalR.Core/Tracing/TraceSourceExtensions.cs deleted file mode 100644 index 745304bd5..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/Tracing/TraceSourceExtensions.cs +++ /dev/null @@ -1,50 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -namespace System.Diagnostics -{ - public static class TraceSourceExtensions - { - [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "msg")] - public static void TraceVerbose(this TraceSource traceSource, string msg) - { - Trace(traceSource, TraceEventType.Verbose, msg); - } - - public static void TraceVerbose(this TraceSource traceSource, string format, params object[] args) - { - Trace(traceSource, TraceEventType.Verbose, format, args); - } - - [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "msg")] - public static void TraceWarning(this TraceSource traceSource, string msg) - { - Trace(traceSource, TraceEventType.Warning, msg); - } - - public static void TraceWarning(this TraceSource traceSource, string format, params object[] args) - { - Trace(traceSource, TraceEventType.Warning, format, args); - } - - [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "msg")] - public static void TraceError(this TraceSource traceSource, string msg) - { - Trace(traceSource, TraceEventType.Error, msg); - } - - public static void TraceError(this TraceSource traceSource, string format, params object[] args) - { - Trace(traceSource, TraceEventType.Error, format, args); - } - - private static void Trace(TraceSource traceSource, TraceEventType eventType, string msg) - { - traceSource.TraceEvent(eventType, 0, msg); - } - - private static void Trace(TraceSource traceSource, TraceEventType eventType, string format, params object[] args) - { - traceSource.TraceEvent(eventType, 0, format, args); - } - } -} diff --git a/src/Microsoft.AspNet.SignalR.Core/Transports/ForeverFrameTransport.cs b/src/Microsoft.AspNet.SignalR.Core/Transports/ForeverFrameTransport.cs deleted file mode 100644 index 275e0a0bd..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/Transports/ForeverFrameTransport.cs +++ /dev/null @@ -1,188 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -using System; -using System.Diagnostics.CodeAnalysis; -using System.Globalization; -using System.IO; -using System.Threading.Tasks; -using Microsoft.AspNet.SignalR.Hosting; -using Microsoft.AspNet.SignalR.Infrastructure; - -namespace Microsoft.AspNet.SignalR.Transports -{ - [SuppressMessage("Microsoft.Design", "CA1001:TypesThatOwnDisposableFieldsShouldBeDisposable", Justification = "Disposable fields are disposed from a different method")] - public class ForeverFrameTransport : ForeverTransport - { - private const string _initPrefix = "" + - "" + - "" + - "SignalR Forever Frame Transport Stream\r\n" + - "" + - "\r\n"; - - private HTMLTextWriter _htmlOutputWriter; - - public ForeverFrameTransport(HostContext context, IDependencyResolver resolver) - : base(context, resolver) - { - } - - /// - /// Pointed to the HTMLOutputWriter to wrap output stream with an HTML friendly one - /// - public override TextWriter OutputWriter - { - get - { - return HTMLOutputWriter; - } - } - - private HTMLTextWriter HTMLOutputWriter - { - get - { - if (_htmlOutputWriter == null) - { - _htmlOutputWriter = new HTMLTextWriter(Context.Response); - _htmlOutputWriter.NewLine = "\n"; - } - - return _htmlOutputWriter; - } - } - - public override Task KeepAlive() - { - if (InitializeTcs == null || !InitializeTcs.Task.IsCompleted) - { - return TaskAsyncHelper.Empty; - } - - // Ensure delegate continues to use the C# Compiler static delegate caching optimization. - return EnqueueOperation(state => PerformKeepAlive(state), this); - } - - public override Task Send(PersistentResponse response) - { - OnSendingResponse(response); - - var context = new ForeverFrameTransportContext(this, response); - - // Ensure delegate continues to use the C# Compiler static delegate caching optimization. - return EnqueueOperation(s => PerformSend(s), context); - } - - protected internal override Task InitializeResponse(ITransportConnection connection) - { - uint frameId; - string rawFrameId = Context.Request.QueryString["frameId"]; - if (String.IsNullOrWhiteSpace(rawFrameId) || !UInt32.TryParse(rawFrameId, NumberStyles.None, CultureInfo.InvariantCulture, out frameId)) - { - // Invalid frameId passed in - throw new InvalidOperationException(Resources.Error_InvalidForeverFrameId); - } - - string initScript = _initPrefix + - frameId.ToString(CultureInfo.InvariantCulture) + - _initSuffix; - - var context = new ForeverFrameTransportContext(this, initScript); - - // Ensure delegate continues to use the C# Compiler static delegate caching optimization. - return base.InitializeResponse(connection).Then(s => Initialize(s), context); - } - - private static Task Initialize(object state) - { - var context = (ForeverFrameTransportContext)state; - - var initContext = new ForeverFrameTransportContext(context.Transport, context.State); - - // Ensure delegate continues to use the C# Compiler static delegate caching optimization. - return WriteInit(initContext); - } - - private static Task WriteInit(ForeverFrameTransportContext context) - { - context.Transport.Context.Response.ContentType = "text/html; charset=UTF-8"; - - context.Transport.HTMLOutputWriter.WriteRaw((string)context.State); - context.Transport.HTMLOutputWriter.Flush(); - - return context.Transport.Context.Response.Flush(); - } - - private static Task PerformSend(object state) - { - var context = (ForeverFrameTransportContext)state; - - context.Transport.HTMLOutputWriter.WriteRaw("\r\n"); - context.Transport.HTMLOutputWriter.Flush(); - - return context.Transport.Context.Response.Flush(); - } - - private static Task PerformKeepAlive(object state) - { - var transport = (ForeverFrameTransport)state; - - transport.HTMLOutputWriter.WriteRaw(""); - transport.HTMLOutputWriter.WriteLine(); - transport.HTMLOutputWriter.WriteLine(); - transport.HTMLOutputWriter.Flush(); - - return transport.Context.Response.Flush(); - } - - private class ForeverFrameTransportContext - { - public ForeverFrameTransport Transport; - public object State; - - public ForeverFrameTransportContext(ForeverFrameTransport transport, object state) - { - Transport = transport; - State = state; - } - } - - private class HTMLTextWriter : BufferTextWriter - { - public HTMLTextWriter(IResponse response) - : base(response) - { - } - - public void WriteRaw(string value) - { - base.Write(value); - } - - public override void Write(string value) - { - base.Write(JavascriptEncode(value)); - } - - public override void WriteLine(string value) - { - base.WriteLine(JavascriptEncode(value)); - } - - private static string JavascriptEncode(string input) - { - return input.Replace("<", "\\u003c").Replace(">", "\\u003e"); - } - } - } -} diff --git a/src/Microsoft.AspNet.SignalR.Core/Transports/ForeverTransport.cs b/src/Microsoft.AspNet.SignalR.Core/Transports/ForeverTransport.cs deleted file mode 100644 index 94b464cf4..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/Transports/ForeverTransport.cs +++ /dev/null @@ -1,405 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -using System; -using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; -using System.Threading.Tasks; -using Microsoft.AspNet.SignalR.Hosting; -using Microsoft.AspNet.SignalR.Infrastructure; -using Microsoft.AspNet.SignalR.Json; -using Microsoft.AspNet.SignalR.Tracing; - -namespace Microsoft.AspNet.SignalR.Transports -{ - [SuppressMessage("Microsoft.Design", "CA1001:TypesThatOwnDisposableFieldsShouldBeDisposable", Justification = "The disposer is an optimization")] - public abstract class ForeverTransport : TransportDisconnectBase, ITransport - { - private readonly IPerformanceCounterManager _counters; - private IJsonSerializer _jsonSerializer; - private string _lastMessageId; - - private const int MaxMessages = 10; - - protected ForeverTransport(HostContext context, IDependencyResolver resolver) - : this(context, - resolver.Resolve(), - resolver.Resolve(), - resolver.Resolve(), - resolver.Resolve()) - { - } - - protected ForeverTransport(HostContext context, - IJsonSerializer jsonSerializer, - ITransportHeartbeat heartbeat, - IPerformanceCounterManager performanceCounterWriter, - ITraceManager traceManager) - : base(context, heartbeat, performanceCounterWriter, traceManager) - { - _jsonSerializer = jsonSerializer; - _counters = performanceCounterWriter; - } - - protected string LastMessageId - { - get - { - if (_lastMessageId == null) - { - _lastMessageId = Context.Request.QueryString["messageId"]; - } - - return _lastMessageId; - } - } - - protected IJsonSerializer JsonSerializer - { - get { return _jsonSerializer; } - } - - internal TaskCompletionSource InitializeTcs { get; set; } - - protected virtual void OnSending(string payload) - { - Heartbeat.MarkConnection(this); - } - - protected virtual void OnSendingResponse(PersistentResponse response) - { - Heartbeat.MarkConnection(this); - } - - public Func Received { get; set; } - - public Func TransportConnected { get; set; } - - public Func Connected { get; set; } - - public Func Reconnected { get; set; } - - // Unit testing hooks - internal Action AfterReceive; - internal Action BeforeCancellationTokenCallbackRegistered; - internal Action BeforeReceive; - internal Action AfterRequestEnd; - - protected override void InitializePersistentState() - { - // PersistentConnection.OnConnected must complete before we can write to the output stream, - // so clients don't indicate the connection has started too early. - InitializeTcs = new TaskCompletionSource(); - WriteQueue = new TaskQueue(InitializeTcs.Task); - - base.InitializePersistentState(); - } - - protected Task ProcessRequestCore(ITransportConnection connection) - { - Connection = connection; - - if (Context.Request.Url.LocalPath.EndsWith("/send", StringComparison.OrdinalIgnoreCase)) - { - return ProcessSendRequest(); - } - else if (IsAbortRequest) - { - return Connection.Abort(ConnectionId); - } - else - { - InitializePersistentState(); - - return ProcessReceiveRequest(connection); - } - } - - public virtual Task ProcessRequest(ITransportConnection connection) - { - return ProcessRequestCore(connection); - } - - public abstract Task Send(PersistentResponse response); - - public virtual Task Send(object value) - { - var context = new ForeverTransportContext(this, value); - - return EnqueueOperation(state => PerformSend(state), context); - } - - protected internal virtual Task InitializeResponse(ITransportConnection connection) - { - return TaskAsyncHelper.Empty; - } - - protected internal override Task EnqueueOperation(Func writeAsync, object state) - { - Task task = base.EnqueueOperation(writeAsync, state); - - // If PersistentConnection.OnConnected has not completed (as indicated by InitializeTcs), - // the queue will be blocked to prevent clients from prematurely indicating the connection has - // started, but we must keep receive loop running to continue processing commands and to - // prevent deadlocks caused by waiting on ACKs. - if (InitializeTcs == null || InitializeTcs.Task.IsCompleted) - { - return task; - } - - return TaskAsyncHelper.Empty; - } - - private Task ProcessSendRequest() - { - string data = Context.Request.Form["data"]; - - if (Received != null) - { - return Received(data); - } - - return TaskAsyncHelper.Empty; - } - - [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Exceptions are flowed to the caller.")] - protected Task ProcessReceiveRequest(ITransportConnection connection) - { - Func initialize = null; - - bool newConnection = Heartbeat.AddConnection(this); - - if (IsConnectRequest) - { - if (newConnection) - { - initialize = Connected; - - _counters.ConnectionsConnected.Increment(); - } - } - else - { - initialize = Reconnected; - } - - var series = new Func[] - { - state => ((Func)state).Invoke(), - state => ((Func)state).Invoke() - }; - - var states = new object[] { TransportConnected ?? _emptyTaskFunc, - initialize ?? _emptyTaskFunc }; - - Func fullInit = () => TaskAsyncHelper.Series(series, states); - - return ProcessMessages(connection, fullInit); - } - - [SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "The object is disposed otherwise")] - [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Exceptions are flowed to the caller.")] - private Task ProcessMessages(ITransportConnection connection, Func initialize) - { - var disposer = new Disposer(); - - if (BeforeCancellationTokenCallbackRegistered != null) - { - BeforeCancellationTokenCallbackRegistered(); - } - - var cancelContext = new ForeverTransportContext(this, disposer); - - // Ensure delegate continues to use the C# Compiler static delegate caching optimization. - IDisposable registration = ConnectionEndToken.SafeRegister(state => Cancel(state), cancelContext); - - var lifetime = new RequestLifetime(this, _requestLifeTime); - var messageContext = new MessageContext(this, lifetime, registration); - - if (BeforeReceive != null) - { - BeforeReceive(); - } - - try - { - // Ensure we enqueue the response initialization before any messages are received - EnqueueOperation(state => InitializeResponse((ITransportConnection)state), connection) - .Catch((ex, state) => OnError(ex, state), messageContext); - - // Ensure delegate continues to use the C# Compiler static delegate caching optimization. - IDisposable subscription = connection.Receive(LastMessageId, - (response, state) => OnMessageReceived(response, state), - MaxMessages, - messageContext); - - - disposer.Set(subscription); - - if (AfterReceive != null) - { - AfterReceive(); - } - - // Ensure delegate continues to use the C# Compiler static delegate caching optimization. - initialize().Then(tcs => tcs.TrySetResult(null), InitializeTcs) - .Catch((ex, state) => OnError(ex, state), messageContext); - } - catch (OperationCanceledException ex) - { - InitializeTcs.TrySetCanceled(); - - lifetime.Complete(ex); - } - catch (Exception ex) - { - InitializeTcs.TrySetCanceled(); - - lifetime.Complete(ex); - } - - return _requestLifeTime.Task; - } - - private static void Cancel(object state) - { - var context = (ForeverTransportContext)state; - - context.Transport.Trace.TraceEvent(TraceEventType.Verbose, 0, "Cancel(" + context.Transport.ConnectionId + ")"); - - ((IDisposable)context.State).Dispose(); - } - - private static Task OnMessageReceived(PersistentResponse response, object state) - { - var context = (MessageContext)state; - - response.Reconnect = context.Transport.HostShutdownToken.IsCancellationRequested; - - // If we're telling the client to disconnect then clean up the instantiated connection. - if (response.Disconnect) - { - // Send the response before removing any connection data - return context.Transport.Send(response).Then(c => OnDisconnectMessage(c), context) - .Then(() => TaskAsyncHelper.False); - } - else if (context.Transport.IsTimedOut || response.Aborted) - { - context.Registration.Dispose(); - - if (response.Aborted) - { - // If this was a clean disconnect raise the event. - return context.Transport.Abort() - .Then(() => TaskAsyncHelper.False); - } - } - - if (response.Terminal) - { - // End the request on the terminal response - context.Lifetime.Complete(); - - return TaskAsyncHelper.False; - } - - // Ensure delegate continues to use the C# Compiler static delegate caching optimization. - return context.Transport.Send(response) - .Then(() => TaskAsyncHelper.True); - } - - private static void OnDisconnectMessage(MessageContext context) - { - context.Transport.ApplyState(TransportConnectionStates.DisconnectMessageReceived); - - context.Registration.Dispose(); - - // Remove connection without triggering disconnect - context.Transport.Heartbeat.RemoveConnection(context.Transport); - } - - private static Task PerformSend(object state) - { - var context = (ForeverTransportContext)state; - - if (!context.Transport.IsAlive) - { - return TaskAsyncHelper.Empty; - } - - context.Transport.Context.Response.ContentType = JsonUtility.JsonMimeType; - - context.Transport.JsonSerializer.Serialize(context.State, context.Transport.OutputWriter); - context.Transport.OutputWriter.Flush(); - - return context.Transport.Context.Response.End(); - } - - private static void OnError(AggregateException ex, object state) - { - var context = (MessageContext)state; - - context.Transport.IncrementErrors(); - - // Cancel any pending writes in the queue - context.Transport.InitializeTcs.TrySetCanceled(); - - // Complete the http request - context.Lifetime.Complete(ex); - } - - private class ForeverTransportContext - { - public object State; - public ForeverTransport Transport; - - public ForeverTransportContext(ForeverTransport foreverTransport, object state) - { - State = state; - Transport = foreverTransport; - } - } - - private class MessageContext - { - public ForeverTransport Transport; - public RequestLifetime Lifetime; - public IDisposable Registration; - - public MessageContext(ForeverTransport transport, RequestLifetime lifetime, IDisposable registration) - { - Registration = registration; - Lifetime = lifetime; - Transport = transport; - } - } - - private class RequestLifetime - { - private readonly HttpRequestLifeTime _lifetime; - private readonly ForeverTransport _transport; - - public RequestLifetime(ForeverTransport transport, HttpRequestLifeTime lifetime) - { - _lifetime = lifetime; - _transport = transport; - } - - public void Complete() - { - Complete(error: null); - } - - public void Complete(Exception error) - { - _lifetime.Complete(error); - - _transport.Dispose(); - - if (_transport.AfterRequestEnd != null) - { - _transport.AfterRequestEnd(error); - } - } - } - } -} diff --git a/src/Microsoft.AspNet.SignalR.Core/Transports/HttpRequestLifeTime.cs b/src/Microsoft.AspNet.SignalR.Core/Transports/HttpRequestLifeTime.cs deleted file mode 100644 index 1354dcb2c..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/Transports/HttpRequestLifeTime.cs +++ /dev/null @@ -1,94 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -using System; -using System.Diagnostics; -using System.Threading.Tasks; -using Microsoft.AspNet.SignalR.Infrastructure; - -namespace Microsoft.AspNet.SignalR.Transports -{ - internal class HttpRequestLifeTime - { - private readonly TaskCompletionSource _lifetimeTcs = new TaskCompletionSource(); - private readonly TransportDisconnectBase _transport; - private readonly TaskQueue _writeQueue; - private readonly TraceSource _trace; - private readonly string _connectionId; - - public HttpRequestLifeTime(TransportDisconnectBase transport, TaskQueue writeQueue, TraceSource trace, string connectionId) - { - _transport = transport; - _trace = trace; - _connectionId = connectionId; - _writeQueue = writeQueue; - } - - public Task Task - { - get - { - return _lifetimeTcs.Task; - } - } - - public void Complete() - { - Complete(error: null); - } - - public void Complete(Exception error) - { - _trace.TraceEvent(TraceEventType.Verbose, 0, "DrainWrites(" + _connectionId + ")"); - - var context = new LifetimeContext(_transport, _lifetimeTcs, error); - - _transport.ApplyState(TransportConnectionStates.QueueDrained); - - // Drain the task queue for pending write operations so we don't end the request and then try to write - // to a corrupted request object. - _writeQueue.Drain().Catch().Finally(state => - { - // Ensure delegate continues to use the C# Compiler static delegate caching optimization. - ((LifetimeContext)state).Complete(); - }, - context); - - if (error != null) - { - _trace.TraceEvent(TraceEventType.Error, 0, "CompleteRequest (" + _connectionId + ") failed: " + error.GetBaseException()); - } - else - { - _trace.TraceInformation("CompleteRequest (" + _connectionId + ")"); - } - } - - private class LifetimeContext - { - private readonly TaskCompletionSource _lifetimeTcs; - private readonly Exception _error; - private readonly TransportDisconnectBase _transport; - - public LifetimeContext(TransportDisconnectBase transport, TaskCompletionSource lifeTimetcs, Exception error) - { - _transport = transport; - _lifetimeTcs = lifeTimetcs; - _error = error; - } - - public void Complete() - { - _transport.ApplyState(TransportConnectionStates.HttpRequestEnded); - - if (_error != null) - { - _lifetimeTcs.TrySetUnwrappedException(_error); - } - else - { - _lifetimeTcs.TrySetResult(null); - } - } - } - } -} diff --git a/src/Microsoft.AspNet.SignalR.Core/Transports/ITrackingConnection.cs b/src/Microsoft.AspNet.SignalR.Core/Transports/ITrackingConnection.cs deleted file mode 100644 index 452e88def..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/Transports/ITrackingConnection.cs +++ /dev/null @@ -1,76 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -using System; -using System.Diagnostics.CodeAnalysis; -using System.Threading; -using System.Threading.Tasks; - -namespace Microsoft.AspNet.SignalR.Transports -{ - /// - /// Represents a connection that can be tracked by an . - /// - public interface ITrackingConnection : IDisposable - { - /// - /// Gets the id of the connection. - /// - string ConnectionId { get; } - - /// - /// Gets a cancellation token that represents the connection's lifetime. - /// - CancellationToken CancellationToken { get; } - - /// - /// Gets a value that represents if the connection is alive. - /// - bool IsAlive { get; } - - /// - /// Gets a value that represents if the connection is timed out. - /// - bool IsTimedOut { get; } - - /// - /// Gets a value that represents if the connection supprots keep alive. - /// - bool SupportsKeepAlive { get; } - - /// - /// Gets a value indicating the amount of time to wait after the connection dies before firing the disconnecting the connection. - /// - TimeSpan DisconnectThreshold { get; } - - /// - /// Gets the uri of the connection. - /// - Uri Url { get; } - - /// - /// Applies a new state to the connection. - /// - void ApplyState(TransportConnectionStates states); - - /// - /// Causes the connection to disconnect. - /// - Task Disconnect(); - - /// - /// Causes the connection to timeout. - /// - void Timeout(); - - /// - /// Sends a keep alive ping over the connection. - /// - Task KeepAlive(); - - /// - /// Kills the connection. - /// - [SuppressMessage("Microsoft.Naming", "CA1716:IdentifiersShouldNotMatchKeywords", MessageId = "End", Justification = "Ends the connction thus the name is appropriate.")] - void End(); - } -} diff --git a/src/Microsoft.AspNet.SignalR.Core/Transports/ITransport.cs b/src/Microsoft.AspNet.SignalR.Core/Transports/ITransport.cs deleted file mode 100644 index f9544306c..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/Transports/ITransport.cs +++ /dev/null @@ -1,57 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -using System; -using System.Threading.Tasks; - -namespace Microsoft.AspNet.SignalR.Transports -{ - /// - /// Represents a transport that communicates - /// - public interface ITransport - { - /// - /// Gets or sets a callback that is invoked when the transport receives data. - /// - Func Received { get; set; } - - /// - /// Gets or sets a callback that is invoked when the initial connection connects to the transport. - /// - Func Connected { get; set; } - - /// - /// Gets or sets a callback that is invoked when the transport connects. - /// - Func TransportConnected { get; set; } - - /// - /// Gets or sets a callback that is invoked when the transport reconnects. - /// - Func Reconnected { get; set; } - - /// - /// Gets or sets a callback that is invoked when the transport disconnects. - /// - Func Disconnected { get; set; } - - /// - /// Gets or sets the connection id for the transport. - /// - string ConnectionId { get; set; } - - /// - /// Processes the specified for this transport. - /// - /// The to process. - /// A that completes when the transport has finished processing the connection. - Task ProcessRequest(ITransportConnection connection); - - /// - /// Sends data over the transport. - /// - /// The value to be sent. - /// A that completes when the send is complete. - Task Send(object value); - } -} diff --git a/src/Microsoft.AspNet.SignalR.Core/Transports/ITransportConnection.cs b/src/Microsoft.AspNet.SignalR.Core/Transports/ITransportConnection.cs deleted file mode 100644 index 50465c2f8..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/Transports/ITransportConnection.cs +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -using System; -using System.Threading.Tasks; - -namespace Microsoft.AspNet.SignalR.Transports -{ - public interface ITransportConnection - { - IDisposable Receive(string messageId, Func> callback, int maxMessages, object state); - - Task Send(ConnectionMessage message); - } -} diff --git a/src/Microsoft.AspNet.SignalR.Core/Transports/ITransportHeartBeat.cs b/src/Microsoft.AspNet.SignalR.Core/Transports/ITransportHeartBeat.cs deleted file mode 100644 index 17a24e825..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/Transports/ITransportHeartBeat.cs +++ /dev/null @@ -1,38 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; - -namespace Microsoft.AspNet.SignalR.Transports -{ - /// - /// Manages tracking the state of connections. - /// - public interface ITransportHeartbeat - { - /// - /// Adds a new connection to the list of tracked connections. - /// - /// The connection to be added. - bool AddConnection(ITrackingConnection connection); - - /// - /// Marks an existing connection as active. - /// - /// The connection to mark. - void MarkConnection(ITrackingConnection connection); - - /// - /// Removes a connection from the list of tracked connections. - /// - /// The connection to remove. - void RemoveConnection(ITrackingConnection connection); - - /// - /// Gets a list of connections being tracked. - /// - /// A list of connections. - [SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate", Justification = "This might be expensive.")] - IList GetConnections(); - } -} diff --git a/src/Microsoft.AspNet.SignalR.Core/Transports/ITransportManager.cs b/src/Microsoft.AspNet.SignalR.Core/Transports/ITransportManager.cs deleted file mode 100644 index 630cb4639..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/Transports/ITransportManager.cs +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -using Microsoft.AspNet.SignalR.Hosting; -namespace Microsoft.AspNet.SignalR.Transports -{ - /// - /// Manages the transports for connections. - /// - public interface ITransportManager - { - /// - /// Gets the specified transport for the specified . - /// - /// The for the current request. - /// The for the specified . - ITransport GetTransport(HostContext hostContext); - - /// - /// Determines whether the specified transport is supported. - /// - /// The name of the transport to test. - /// True if the transport is supported, otherwise False. - bool SupportsTransport(string transportName); - } -} diff --git a/src/Microsoft.AspNet.SignalR.Core/Transports/LongPollingTransport.cs b/src/Microsoft.AspNet.SignalR.Core/Transports/LongPollingTransport.cs deleted file mode 100644 index a8fd15952..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/Transports/LongPollingTransport.cs +++ /dev/null @@ -1,400 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -using System; -using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; -using System.Threading.Tasks; -using Microsoft.AspNet.SignalR.Hosting; -using Microsoft.AspNet.SignalR.Infrastructure; -using Microsoft.AspNet.SignalR.Json; -using Microsoft.AspNet.SignalR.Tracing; - -namespace Microsoft.AspNet.SignalR.Transports -{ - public class LongPollingTransport : TransportDisconnectBase, ITransport - { - private readonly IJsonSerializer _jsonSerializer; - private readonly IPerformanceCounterManager _counters; - - // This should be ok to do since long polling request never hang around too long - // so we won't bloat memory - private const int MaxMessages = 5000; - - public LongPollingTransport(HostContext context, IDependencyResolver resolver) - : this(context, - resolver.Resolve(), - resolver.Resolve(), - resolver.Resolve(), - resolver.Resolve()) - { - - } - - public LongPollingTransport(HostContext context, - IJsonSerializer jsonSerializer, - ITransportHeartbeat heartbeat, - IPerformanceCounterManager performanceCounterManager, - ITraceManager traceManager) - : base(context, heartbeat, performanceCounterManager, traceManager) - { - _jsonSerializer = jsonSerializer; - _counters = performanceCounterManager; - } - - /// - /// The number of milliseconds to tell the browser to wait before restablishing a - /// long poll connection after data is sent from the server. Defaults to 0. - /// - [SuppressMessage("Microsoft.Naming", "CA1720:IdentifiersShouldNotContainTypeNames", MessageId = "long", Justification = "Longpolling is a well known term")] - public static long LongPollDelay - { - get; - set; - } - - public override TimeSpan DisconnectThreshold - { - get { return TimeSpan.FromMilliseconds(LongPollDelay); } - } - - public override bool IsConnectRequest - { - get - { - return Context.Request.Url.LocalPath.EndsWith("/connect", StringComparison.OrdinalIgnoreCase); - } - } - - private bool IsReconnectRequest - { - get - { - return Context.Request.Url.LocalPath.EndsWith("/reconnect", StringComparison.OrdinalIgnoreCase); - } - } - - private bool IsJsonp - { - get - { - return !String.IsNullOrEmpty(JsonpCallback); - } - } - - private bool IsSendRequest - { - get - { - return Context.Request.Url.LocalPath.EndsWith("/send", StringComparison.OrdinalIgnoreCase); - } - } - - private string MessageId - { - get - { - return Context.Request.QueryString["messageId"]; - } - } - - private string JsonpCallback - { - get - { - return Context.Request.QueryString["callback"]; - } - } - - public override bool SupportsKeepAlive - { - get - { - return false; - } - } - - public Func Received { get; set; } - - public Func TransportConnected { get; set; } - - public Func Connected { get; set; } - - public Func Reconnected { get; set; } - - public Task ProcessRequest(ITransportConnection connection) - { - Connection = connection; - - if (IsSendRequest) - { - return ProcessSendRequest(); - } - else if (IsAbortRequest) - { - return Connection.Abort(ConnectionId); - } - else - { - InitializePersistentState(); - - return ProcessReceiveRequest(connection); - } - } - - public Task Send(PersistentResponse response) - { - Heartbeat.MarkConnection(this); - - AddTransportData(response); - - return Send((object)response); - } - - public Task Send(object value) - { - var context = new LongPollingTransportContext(this, value); - - return EnqueueOperation(state => PerformSend(state), context); - } - - private Task ProcessSendRequest() - { - string data = Context.Request.Form["data"] ?? Context.Request.QueryString["data"]; - - if (Received != null) - { - return Received(data); - } - - return TaskAsyncHelper.Empty; - } - - private Task ProcessReceiveRequest(ITransportConnection connection) - { - Func initialize = null; - - bool newConnection = Heartbeat.AddConnection(this); - - if (IsConnectRequest) - { - if (newConnection) - { - initialize = Connected; - - _counters.ConnectionsConnected.Increment(); - } - } - else if (IsReconnectRequest) - { - initialize = Reconnected; - } - - var series = new Func[] - { - state => ((Func)state).Invoke(), - state => ((Func)state).Invoke() - }; - - var states = new object[] { TransportConnected ?? _emptyTaskFunc, - initialize ?? _emptyTaskFunc }; - - Func fullInit = () => TaskAsyncHelper.Series(series, states); - - return ProcessMessages(connection, fullInit); - } - - [SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "The subscription is disposed in the callback")] - [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "The exception is captured in a task")] - private Task ProcessMessages(ITransportConnection connection, Func initialize) - { - var disposer = new Disposer(); - - var cancelContext = new LongPollingTransportContext(this, disposer); - - // Ensure delegate continues to use the C# Compiler static delegate caching optimization. - IDisposable registration = ConnectionEndToken.SafeRegister(state => Cancel(state), cancelContext); - - var lifeTime = new RequestLifetime(this, _requestLifeTime, registration); - var messageContext = new MessageContext(this, lifeTime); - - try - { - // Ensure delegate continues to use the C# Compiler static delegate caching optimization. - IDisposable subscription = connection.Receive(MessageId, - (response, state) => OnMessageReceived(response, state), - MaxMessages, - messageContext); - - // Set the disposable - disposer.Set(subscription); - - // Ensure delegate continues to use the C# Compiler static delegate caching optimization. - initialize().Catch((ex, state) => OnError(ex, state), messageContext); - } - catch (Exception ex) - { - lifeTime.Complete(ex); - } - - return _requestLifeTime.Task; - } - - private static void Cancel(object state) - { - var context = (LongPollingTransportContext)state; - - context.Transport.Trace.TraceEvent(TraceEventType.Verbose, 0, "Cancel(" + context.Transport.ConnectionId + ")"); - - ((IDisposable)context.State).Dispose(); - } - - private static Task OnMessageReceived(PersistentResponse response, object state) - { - var context = (MessageContext)state; - - response.Reconnect = context.Transport.HostShutdownToken.IsCancellationRequested; - - Task task = TaskAsyncHelper.Empty; - - if (response.Aborted) - { - // If this was a clean disconnect then raise the event - task = context.Transport.Abort(); - } - - if (response.Terminal) - { - // If the response wasn't sent, send it before ending the request - if (!context.ResponseSent) - { - // Ensure delegate continues to use the C# Compiler static delegate caching optimization. - return task.Then((ctx, resp) => ctx.Transport.Send(resp), context, response) - .Then(() => - { - context.Lifetime.Complete(); - - return TaskAsyncHelper.False; - }); - } - - // Ensure delegate continues to use the C# Compiler static delegate caching optimization. - return task.Then(() => - { - context.Lifetime.Complete(); - - return TaskAsyncHelper.False; - }); - } - - // Mark the response as sent - context.ResponseSent = true; - - // Send the response and return false - // Ensure delegate continues to use the C# Compiler static delegate caching optimization. - return task.Then((ctx, resp) => ctx.Transport.Send(resp), context, response) - .Then(() => TaskAsyncHelper.False); - } - - private static Task PerformSend(object state) - { - var context = (LongPollingTransportContext)state; - - if (!context.Transport.IsAlive) - { - return TaskAsyncHelper.Empty; - } - - context.Transport.Context.Response.ContentType = context.Transport.IsJsonp ? JsonUtility.JavaScriptMimeType : JsonUtility.JsonMimeType; - - if (context.Transport.IsJsonp) - { - context.Transport.OutputWriter.Write(context.Transport.JsonpCallback); - context.Transport.OutputWriter.Write("("); - } - - context.Transport._jsonSerializer.Serialize(context.State, context.Transport.OutputWriter); - - if (context.Transport.IsJsonp) - { - context.Transport.OutputWriter.Write(");"); - } - - context.Transport.OutputWriter.Flush(); - - return context.Transport.Context.Response.End(); - } - - private static void OnError(AggregateException ex, object state) - { - var context = (MessageContext)state; - - context.Transport.IncrementErrors(); - - context.Lifetime.Complete(ex); - } - - private static void AddTransportData(PersistentResponse response) - { - if (LongPollDelay > 0) - { - response.LongPollDelay = LongPollDelay; - } - } - - private class LongPollingTransportContext - { - public object State; - public LongPollingTransport Transport; - - public LongPollingTransportContext(LongPollingTransport transport, object state) - { - State = state; - Transport = transport; - } - } - - private class MessageContext - { - public LongPollingTransport Transport; - public RequestLifetime Lifetime; - public bool ResponseSent; - - public MessageContext(LongPollingTransport longPollingTransport, RequestLifetime requestLifetime) - { - Transport = longPollingTransport; - Lifetime = requestLifetime; - } - } - - private class RequestLifetime - { - private readonly HttpRequestLifeTime _requestLifeTime; - private readonly LongPollingTransport _transport; - private readonly IDisposable _registration; - - public RequestLifetime(LongPollingTransport transport, HttpRequestLifeTime requestLifeTime, IDisposable registration) - { - _transport = transport; - _registration = registration; - _requestLifeTime = requestLifeTime; - } - - public void Complete() - { - Complete(exception: null); - } - - public void Complete(Exception exception) - { - // End the request - _requestLifeTime.Complete(exception); - - // Dispose of the cancellation token subscription - _registration.Dispose(); - - // Dispose any state on the transport - _transport.Dispose(); - } - } - } -} diff --git a/src/Microsoft.AspNet.SignalR.Core/Transports/PersistentResponse.cs b/src/Microsoft.AspNet.SignalR.Core/Transports/PersistentResponse.cs deleted file mode 100644 index ced8d90a3..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/Transports/PersistentResponse.cs +++ /dev/null @@ -1,181 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.IO; -using Microsoft.AspNet.SignalR.Infrastructure; -using Microsoft.AspNet.SignalR.Json; -using Microsoft.AspNet.SignalR.Messaging; -using Newtonsoft.Json; - -namespace Microsoft.AspNet.SignalR.Transports -{ - /// - /// Represents a response to a connection. - /// - public sealed class PersistentResponse : IJsonWritable - { - private readonly Func _exclude; - private readonly Action _writeCursor; - - public PersistentResponse() - : this(message => false, writer => { }) - { - - } - - /// - /// Creates a new instance of . - /// - /// A filter that determines whether messages should be written to the client. - /// The cursor writer. - public PersistentResponse(Func exclude, Action writeCursor) - { - _exclude = exclude; - _writeCursor = writeCursor; - } - - /// - /// The list of messages to be sent to the receiving connection. - /// - [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an optimization and this type is only used for serialization.")] - [SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Justification = "This type is only used for serialization")] - public IList> Messages { get; set; } - - public bool Terminal { get; set; } - - /// - /// The total count of the messages sent the receiving connection. - /// - public int TotalCount { get; set; } - - /// - /// True if the connection receives a disconnect command. - /// - public bool Disconnect { get; set; } - - /// - /// True if the connection was forcibly closed. - /// - public bool Aborted { get; set; } - - /// - /// True if the client should try reconnecting. - /// - // This is set when the host is shutting down. - public bool Reconnect { get; set; } - - /// - /// Signed token representing the list of groups. Updates on change. - /// - public string GroupsToken { get; set; } - - /// - /// The time the long polling client should wait before reestablishing a connection if no data is received. - /// - public long? LongPollDelay { get; set; } - - /// - /// Serializes only the necessary components of the to JSON - /// using Json.NET's JsonTextWriter to improve performance. - /// - /// The that receives the JSON serialization. - void IJsonWritable.WriteJson(TextWriter writer) - { - if (writer == null) - { - throw new ArgumentNullException("writer"); - } - - var jsonWriter = new JsonTextWriter(writer); - jsonWriter.WriteStartObject(); - - // REVIEW: Is this 100% correct? - writer.Write('"'); - writer.Write("C"); - writer.Write('"'); - writer.Write(':'); - writer.Write('"'); - _writeCursor(writer); - writer.Write('"'); - writer.Write(','); - - if (Disconnect) - { - jsonWriter.WritePropertyName("D"); - jsonWriter.WriteValue(1); - } - - if (Reconnect) - { - jsonWriter.WritePropertyName("T"); - jsonWriter.WriteValue(1); - } - - if (GroupsToken != null) - { - jsonWriter.WritePropertyName("G"); - jsonWriter.WriteValue(GroupsToken); - } - - if (LongPollDelay.HasValue) - { - jsonWriter.WritePropertyName("L"); - jsonWriter.WriteValue(LongPollDelay.Value); - } - - jsonWriter.WritePropertyName("M"); - jsonWriter.WriteStartArray(); - - WriteMessages(writer, jsonWriter); - - jsonWriter.WriteEndArray(); - jsonWriter.WriteEndObject(); - } - - private void WriteMessages(TextWriter writer, JsonTextWriter jsonWriter) - { - if (Messages == null) - { - return; - } - - // If the writer is a binary writer then write to the underlying writer directly - var binaryWriter = writer as IBinaryWriter; - - bool first = true; - - for (int i = 0; i < Messages.Count; i++) - { - ArraySegment segment = Messages[i]; - for (int j = segment.Offset; j < segment.Offset + segment.Count; j++) - { - Message message = segment.Array[j]; - - if (!message.IsCommand && !_exclude(message)) - { - if (binaryWriter != null) - { - if (!first) - { - // We need to write the array separator manually - writer.Write(','); - } - - // If we can write binary then just write it - binaryWriter.Write(message.Value); - - first = false; - } - else - { - // Write the raw JSON value - jsonWriter.WriteRawValue(message.GetString()); - } - } - } - } - } - } -} diff --git a/src/Microsoft.AspNet.SignalR.Core/Transports/ServerSentEventsTransport.cs b/src/Microsoft.AspNet.SignalR.Core/Transports/ServerSentEventsTransport.cs deleted file mode 100644 index ef22be398..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/Transports/ServerSentEventsTransport.cs +++ /dev/null @@ -1,93 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -using System.Threading.Tasks; -using Microsoft.AspNet.SignalR.Hosting; - -namespace Microsoft.AspNet.SignalR.Transports -{ - public class ServerSentEventsTransport : ForeverTransport - { - public ServerSentEventsTransport(HostContext context, IDependencyResolver resolver) - : base(context, resolver) - { - } - - public override Task KeepAlive() - { - if (InitializeTcs == null || !InitializeTcs.Task.IsCompleted) - { - return TaskAsyncHelper.Empty; - } - - // Ensure delegate continues to use the C# Compiler static delegate caching optimization. - return EnqueueOperation(state => PerformKeepAlive(state), this); - } - - public override Task Send(PersistentResponse response) - { - OnSendingResponse(response); - - var context = new SendContext(this, response); - - // Ensure delegate continues to use the C# Compiler static delegate caching optimization. - return EnqueueOperation(state => PerformSend(state), context); - } - - protected internal override Task InitializeResponse(ITransportConnection connection) - { - // Ensure delegate continues to use the C# Compiler static delegate caching optimization. - return base.InitializeResponse(connection) - .Then(s => WriteInit(s), this); - } - - private static Task PerformKeepAlive(object state) - { - var transport = (ServerSentEventsTransport)state; - - transport.OutputWriter.Write("data: {}"); - transport.OutputWriter.WriteLine(); - transport.OutputWriter.WriteLine(); - transport.OutputWriter.Flush(); - - return transport.Context.Response.Flush(); - } - - private static Task PerformSend(object state) - { - var context = (SendContext)state; - - context.Transport.OutputWriter.Write("data: "); - context.Transport.JsonSerializer.Serialize(context.State, context.Transport.OutputWriter); - context.Transport.OutputWriter.WriteLine(); - context.Transport.OutputWriter.WriteLine(); - context.Transport.OutputWriter.Flush(); - - return context.Transport.Context.Response.Flush(); - } - - private static Task WriteInit(ServerSentEventsTransport transport) - { - transport.Context.Response.ContentType = "text/event-stream"; - - // "data: initialized\n\n" - transport.OutputWriter.Write("data: initialized"); - transport.OutputWriter.WriteLine(); - transport.OutputWriter.WriteLine(); - transport.OutputWriter.Flush(); - - return transport.Context.Response.Flush(); - } - - private class SendContext - { - public ServerSentEventsTransport Transport; - public object State; - - public SendContext(ServerSentEventsTransport transport, object state) - { - Transport = transport; - State = state; - } - } - } -} diff --git a/src/Microsoft.AspNet.SignalR.Core/Transports/TransportConnectionExtensions.cs b/src/Microsoft.AspNet.SignalR.Core/Transports/TransportConnectionExtensions.cs deleted file mode 100644 index 1f12d533f..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/Transports/TransportConnectionExtensions.cs +++ /dev/null @@ -1,34 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -using System.Threading.Tasks; -using Microsoft.AspNet.SignalR.Infrastructure; -using Microsoft.AspNet.SignalR.Messaging; - -namespace Microsoft.AspNet.SignalR.Transports -{ - internal static class TransportConnectionExtensions - { - internal static Task Close(this ITransportConnection connection, string connectionId) - { - return SendCommand(connection, connectionId, CommandType.Disconnect); - } - - internal static Task Abort(this ITransportConnection connection, string connectionId) - { - return SendCommand(connection, connectionId, CommandType.Abort); - } - - private static Task SendCommand(ITransportConnection connection, string connectionId, CommandType commandType) - { - var command = new Command - { - CommandType = commandType - }; - - var message = new ConnectionMessage(PrefixHelper.GetConnectionId(connectionId), - command); - - return connection.Send(message); - } - } -} diff --git a/src/Microsoft.AspNet.SignalR.Core/Transports/TransportConnectionStates.cs b/src/Microsoft.AspNet.SignalR.Core/Transports/TransportConnectionStates.cs deleted file mode 100644 index 6ffddb4e5..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/Transports/TransportConnectionStates.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System; - -namespace Microsoft.AspNet.SignalR.Transports -{ - [Flags] - public enum TransportConnectionStates - { - None = 0, - Added = 1, - Removed = 2, - Replaced = 4, - QueueDrained = 8, - HttpRequestEnded = 16, - Disconnected = 32, - Aborted = 64, - DisconnectMessageReceived = 128, - Disposed = 65536, - } -} diff --git a/src/Microsoft.AspNet.SignalR.Core/Transports/TransportDisconnectBase.cs b/src/Microsoft.AspNet.SignalR.Core/Transports/TransportDisconnectBase.cs deleted file mode 100644 index 45f96e0bd..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/Transports/TransportDisconnectBase.cs +++ /dev/null @@ -1,344 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -using System; -using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; -using System.IO; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.AspNet.SignalR.Hosting; -using Microsoft.AspNet.SignalR.Infrastructure; -using Microsoft.AspNet.SignalR.Tracing; - -namespace Microsoft.AspNet.SignalR.Transports -{ - [SuppressMessage("Microsoft.Design", "CA1001:TypesThatOwnDisposableFieldsShouldBeDisposable", Justification = "Disposable fields are disposed from a different method")] - public abstract class TransportDisconnectBase : ITrackingConnection - { - private readonly HostContext _context; - private readonly ITransportHeartbeat _heartbeat; - private TextWriter _outputWriter; - - private TraceSource _trace; - - private int _timedOut; - private readonly IPerformanceCounterManager _counters; - private int _ended; - private TransportConnectionStates _state; - - internal static readonly Func _emptyTaskFunc = () => TaskAsyncHelper.Empty; - - // Token that represents the end of the connection based on a combination of - // conditions (timeout, disconnect, connection forcibly ended, host shutdown) - private CancellationToken _connectionEndToken; - private SafeCancellationTokenSource _connectionEndTokenSource; - - // Token that represents the host shutting down - private CancellationToken _hostShutdownToken; - private IDisposable _hostRegistration; - private IDisposable _connectionEndRegistration; - - internal HttpRequestLifeTime _requestLifeTime; - - protected TransportDisconnectBase(HostContext context, ITransportHeartbeat heartbeat, IPerformanceCounterManager performanceCounterManager, ITraceManager traceManager) - { - if (context == null) - { - throw new ArgumentNullException("context"); - } - - if (heartbeat == null) - { - throw new ArgumentNullException("heartbeat"); - } - - if (performanceCounterManager == null) - { - throw new ArgumentNullException("performanceCounterManager"); - } - - if (traceManager == null) - { - throw new ArgumentNullException("traceManager"); - } - - _context = context; - _heartbeat = heartbeat; - _counters = performanceCounterManager; - - // Queue to protect against overlapping writes to the underlying response stream - WriteQueue = new TaskQueue(); - - _trace = traceManager["SignalR.Transports." + GetType().Name]; - } - - protected TraceSource Trace - { - get - { - return _trace; - } - } - - public string ConnectionId - { - get; - set; - } - - public virtual TextWriter OutputWriter - { - get - { - if (_outputWriter == null) - { - _outputWriter = CreateResponseWriter(); - _outputWriter.NewLine = "\n"; - } - - return _outputWriter; - } - } - - internal TaskQueue WriteQueue - { - get; - set; - } - - public Func Disconnected { get; set; } - - public virtual CancellationToken CancellationToken - { - get { return _context.Response.CancellationToken; } - } - - public virtual bool IsAlive - { - get - { - // If the CTS is tripped or the request has ended then the connection isn't alive - return !(CancellationToken.IsCancellationRequested || (_requestLifeTime != null && _requestLifeTime.Task.IsCompleted)); - } - } - - protected CancellationToken ConnectionEndToken - { - get - { - return _connectionEndToken; - } - } - - protected CancellationToken HostShutdownToken - { - get - { - return _hostShutdownToken; - } - } - - public bool IsTimedOut - { - get - { - return _timedOut == 1; - } - } - - public virtual bool SupportsKeepAlive - { - get - { - return true; - } - } - - public virtual TimeSpan DisconnectThreshold - { - get { return TimeSpan.FromSeconds(5); } - } - - public virtual bool IsConnectRequest - { - get - { - return Context.Request.Url.LocalPath.EndsWith("/connect", StringComparison.OrdinalIgnoreCase); - } - } - - protected bool IsAbortRequest - { - get - { - return Context.Request.Url.LocalPath.EndsWith("/abort", StringComparison.OrdinalIgnoreCase); - } - } - - protected ITransportConnection Connection { get; set; } - - protected HostContext Context - { - get { return _context; } - } - - protected ITransportHeartbeat Heartbeat - { - get { return _heartbeat; } - } - - public Uri Url - { - get { return _context.Request.Url; } - } - - protected virtual TextWriter CreateResponseWriter() - { - return new BinaryTextWriter(Context.Response); - } - - protected void IncrementErrors() - { - _counters.ErrorsTransportTotal.Increment(); - _counters.ErrorsTransportPerSec.Increment(); - _counters.ErrorsAllTotal.Increment(); - _counters.ErrorsAllPerSec.Increment(); - } - - public Task Disconnect() - { - // Ensure delegate continues to use the C# Compiler static delegate caching optimization. - return Abort(clean: false).Then(transport => transport.Connection.Close(transport.ConnectionId), this); - } - - public Task Abort() - { - return Abort(clean: true); - } - - public Task Abort(bool clean) - { - if (clean) - { - ApplyState(TransportConnectionStates.Aborted); - } - else - { - ApplyState(TransportConnectionStates.Disconnected); - } - - Trace.TraceInformation("Abort(" + ConnectionId + ")"); - - // When a connection is aborted (graceful disconnect) we send a command to it - // telling to to disconnect. At that moment, we raise the disconnect event and - // remove this connection from the heartbeat so we don't end up raising it for the same connection. - Heartbeat.RemoveConnection(this); - - // End the connection - End(); - - var disconnected = Disconnected ?? _emptyTaskFunc; - - // Ensure delegate continues to use the C# Compiler static delegate caching optimization. - return disconnected().Catch((ex, state) => OnDisconnectError(ex, state), Trace) - .Then(counters => counters.ConnectionsDisconnected.Increment(), _counters); - } - - public void ApplyState(TransportConnectionStates states) - { - _state |= states; - } - - public void Timeout() - { - if (Interlocked.Exchange(ref _timedOut, 1) == 0) - { - Trace.TraceInformation("Timeout(" + ConnectionId + ")"); - - End(); - } - } - - public virtual Task KeepAlive() - { - return TaskAsyncHelper.Empty; - } - - public void End() - { - if (Interlocked.Exchange(ref _ended, 1) == 0) - { - Trace.TraceInformation("End(" + ConnectionId + ")"); - - if (_connectionEndTokenSource != null) - { - _connectionEndTokenSource.Cancel(); - } - } - } - - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - protected virtual void Dispose(bool disposing) - { - if (disposing) - { - _connectionEndTokenSource.Dispose(); - _connectionEndRegistration.Dispose(); - _hostRegistration.Dispose(); - - ApplyState(TransportConnectionStates.Disposed); - } - } - - protected virtual internal Task EnqueueOperation(Func writeAsync) - { - return EnqueueOperation(state => ((Func)state).Invoke(), writeAsync); - } - - protected virtual internal Task EnqueueOperation(Func writeAsync, object state) - { - if (!IsAlive) - { - return TaskAsyncHelper.Empty; - } - - // Only enqueue new writes if the connection is alive - return WriteQueue.Enqueue(writeAsync, state); - } - - protected virtual void InitializePersistentState() - { - _hostShutdownToken = _context.HostShutdownToken(); - - _requestLifeTime = new HttpRequestLifeTime(this, WriteQueue, Trace, ConnectionId); - - // Create a token that represents the end of this connection's life - _connectionEndTokenSource = new SafeCancellationTokenSource(); - _connectionEndToken = _connectionEndTokenSource.Token; - - // Handle the shutdown token's callback so we can end our token if it trips - _hostRegistration = _hostShutdownToken.SafeRegister(state => - { - ((SafeCancellationTokenSource)state).Cancel(); - }, - _connectionEndTokenSource); - - // When the connection ends release the request - _connectionEndRegistration = CancellationToken.SafeRegister(state => - { - ((HttpRequestLifeTime)state).Complete(); - }, - _requestLifeTime); - } - - private static void OnDisconnectError(AggregateException ex, object state) - { - ((TraceSource)state).TraceEvent(TraceEventType.Error, 0, "Failed to raise disconnect: " + ex.GetBaseException()); - } - } -} diff --git a/src/Microsoft.AspNet.SignalR.Core/Transports/TransportHeartBeat.cs b/src/Microsoft.AspNet.SignalR.Core/Transports/TransportHeartBeat.cs deleted file mode 100644 index f44bb8216..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/Transports/TransportHeartBeat.cs +++ /dev/null @@ -1,384 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; -using System.Linq; -using System.Threading; -using Microsoft.AspNet.SignalR.Configuration; -using Microsoft.AspNet.SignalR.Infrastructure; -using Microsoft.AspNet.SignalR.Tracing; - -namespace Microsoft.AspNet.SignalR.Transports -{ - /// - /// Default implementation of . - /// - public class TransportHeartbeat : ITransportHeartbeat, IDisposable - { - private readonly ConcurrentDictionary _connections = new ConcurrentDictionary(); - private readonly Timer _timer; - private readonly IConfigurationManager _configurationManager; - private readonly IServerCommandHandler _serverCommandHandler; - private readonly TraceSource _trace; - private readonly string _serverId; - private readonly IPerformanceCounterManager _counters; - private readonly object _counterLock = new object(); - - private int _running; - private ulong _heartbeatCount; - - /// - /// Initializes and instance of the class. - /// - /// The . - public TransportHeartbeat(IDependencyResolver resolver) - { - _configurationManager = resolver.Resolve(); - _serverCommandHandler = resolver.Resolve(); - _serverId = resolver.Resolve().ServerId; - _counters = resolver.Resolve(); - - var traceManager = resolver.Resolve(); - _trace = traceManager["SignalR.Transports.TransportHeartBeat"]; - - _serverCommandHandler.Command = ProcessServerCommand; - - // REVIEW: When to dispose the timer? - _timer = new Timer(Beat, - null, - _configurationManager.HeartbeatInterval(), - _configurationManager.HeartbeatInterval()); - } - - private TraceSource Trace - { - get - { - return _trace; - } - } - - private void ProcessServerCommand(ServerCommand command) - { - switch (command.ServerCommandType) - { - case ServerCommandType.RemoveConnection: - // Only remove connections if this command didn't originate from the owner - if (!command.IsFromSelf(_serverId)) - { - var connectionId = (string)command.Value; - - // Remove the connection - ConnectionMetadata metadata; - if (_connections.TryGetValue(connectionId, out metadata)) - { - metadata.Connection.End(); - - RemoveConnection(metadata.Connection); - } - } - break; - default: - break; - } - } - - /// - /// Adds a new connection to the list of tracked connections. - /// - /// The connection to be added. - public bool AddConnection(ITrackingConnection connection) - { - if (connection == null) - { - throw new ArgumentNullException("connection"); - } - - var newMetadata = new ConnectionMetadata(connection); - bool isNewConnection = true; - - _connections.AddOrUpdate(connection.ConnectionId, newMetadata, (key, old) => - { - Trace.TraceEvent(TraceEventType.Verbose, 0, "Connection {0} exists. Closing previous connection.", old.Connection.ConnectionId); - // Kick out the older connection. This should only happen when - // a previous connection attempt fails on the client side (e.g. transport fallback). - - old.Connection.ApplyState(TransportConnectionStates.Replaced); - - // Don't bother disposing the registration here since the token source - // gets disposed after the request has ended - old.Connection.End(); - - // If we have old metadata this isn't a new connection - isNewConnection = false; - - return newMetadata; - }); - - if (isNewConnection) - { - Trace.TraceInformation("Connection {0} is New.", connection.ConnectionId); - } - - lock (_counterLock) - { - _counters.ConnectionsCurrent.RawValue = _connections.Count; - } - - // Set the initial connection time - newMetadata.Initial = DateTime.UtcNow; - - newMetadata.Connection.ApplyState(TransportConnectionStates.Added); - - return isNewConnection; - } - - /// - /// Removes a connection from the list of tracked connections. - /// - /// The connection to remove. - public void RemoveConnection(ITrackingConnection connection) - { - if (connection == null) - { - throw new ArgumentNullException("connection"); - } - - // Remove the connection and associated metadata - ConnectionMetadata metadata; - if (_connections.TryRemove(connection.ConnectionId, out metadata)) - { - lock (_counterLock) - { - _counters.ConnectionsCurrent.RawValue = _connections.Count; - } - - connection.ApplyState(TransportConnectionStates.Removed); - - Trace.TraceInformation("Removing connection {0}", connection.ConnectionId); - } - } - - /// - /// Marks an existing connection as active. - /// - /// The connection to mark. - public void MarkConnection(ITrackingConnection connection) - { - if (connection == null) - { - throw new ArgumentNullException("connection"); - } - - // Do nothing if the connection isn't alive - if (!connection.IsAlive) - { - return; - } - - ConnectionMetadata metadata; - if (_connections.TryGetValue(connection.ConnectionId, out metadata)) - { - metadata.LastMarked = DateTime.UtcNow; - } - } - - public IList GetConnections() - { - return _connections.Values.Select(metadata => metadata.Connection).ToList(); - } - - [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "We're tracing exceptions and don't want to crash the process.")] - private void Beat(object state) - { - if (Interlocked.Exchange(ref _running, 1) == 1) - { - Trace.TraceEvent(TraceEventType.Verbose, 0, "Timer handler took longer than current interval"); - return; - } - - lock (_counterLock) - { - _counters.ConnectionsCurrent.RawValue = _connections.Count; - } - - try - { - _heartbeatCount++; - - foreach (var metadata in _connections.Values) - { - if (metadata.Connection.IsAlive) - { - CheckTimeoutAndKeepAlive(metadata); - } - else - { - Trace.TraceEvent(TraceEventType.Verbose, 0, metadata.Connection.ConnectionId + " is dead"); - - // Check if we need to disconnect this connection - CheckDisconnect(metadata); - } - } - } - catch (Exception ex) - { - Trace.TraceEvent(TraceEventType.Error, 0, "SignalR error during transport heart beat on background thread: {0}", ex); - } - finally - { - Interlocked.Exchange(ref _running, 0); - } - } - - private void CheckTimeoutAndKeepAlive(ConnectionMetadata metadata) - { - if (RaiseTimeout(metadata)) - { - // If we're past the expiration time then just timeout the connection - metadata.Connection.Timeout(); - } - else - { - // The connection is still alive so we need to keep it alive with a server side "ping". - // This is for scenarios where networking hardware (proxies, loadbalancers) get in the way - // of us handling timeout's or disconnects gracefully - if (RaiseKeepAlive(metadata)) - { - Trace.TraceEvent(TraceEventType.Verbose, 0, "KeepAlive(" + metadata.Connection.ConnectionId + ")"); - - // Ensure delegate continues to use the C# Compiler static delegate caching optimization. - metadata.Connection.KeepAlive().Catch((ex, state) => OnKeepAliveError(ex, state), Trace); - } - - MarkConnection(metadata.Connection); - } - } - - [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "We're tracing exceptions and don't want to crash the process.")] - private void CheckDisconnect(ConnectionMetadata metadata) - { - try - { - if (RaiseDisconnect(metadata)) - { - // Remove the connection from the list - RemoveConnection(metadata.Connection); - - // Fire disconnect on the connection - metadata.Connection.Disconnect(); - } - } - catch (Exception ex) - { - // Swallow exceptions that might happen during disconnect - Trace.TraceEvent(TraceEventType.Error, 0, "Raising Disconnect failed: {0}", ex); - } - } - - private bool RaiseDisconnect(ConnectionMetadata metadata) - { - // The transport is currently dead but it could just be reconnecting - // so we to check it's last active time to see if it's over the disconnect - // threshold - TimeSpan elapsed = DateTime.UtcNow - metadata.LastMarked; - - // The threshold for disconnect is the transport threshold + (potential network issues) - var threshold = metadata.Connection.DisconnectThreshold + _configurationManager.DisconnectTimeout; - - return elapsed >= threshold; - } - - private bool RaiseKeepAlive(ConnectionMetadata metadata) - { - var keepAlive = _configurationManager.KeepAlive; - - // Don't raise keep alive if it's set to 0 or the transport doesn't support - // keep alive - if (keepAlive == null || !metadata.Connection.SupportsKeepAlive) - { - return false; - } - - // Raise keep alive if the keep alive value has passed - return _heartbeatCount % (ulong)ConfigurationExtensions.HeartBeatsPerKeepAlive == 0; - } - - private bool RaiseTimeout(ConnectionMetadata metadata) - { - // The connection already timed out so do nothing - if (metadata.Connection.IsTimedOut) - { - return false; - } - - var keepAlive = _configurationManager.KeepAlive; - // If keep alive is configured and the connection supports keep alive - // don't ever time out - if (keepAlive != null && metadata.Connection.SupportsKeepAlive) - { - return false; - } - - TimeSpan elapsed = DateTime.UtcNow - metadata.Initial; - - // Only raise timeout if we're past the configured connection timeout. - return elapsed >= _configurationManager.ConnectionTimeout; - } - - protected virtual void Dispose(bool disposing) - { - if (disposing) - { - if (_timer != null) - { - _timer.Dispose(); - } - - Trace.TraceInformation("Dispose(). Closing all connections"); - - // Kill all connections - foreach (var pair in _connections) - { - ConnectionMetadata metadata; - if (_connections.TryGetValue(pair.Key, out metadata)) - { - metadata.Connection.End(); - } - } - } - } - - public void Dispose() - { - Dispose(true); - } - - private static void OnKeepAliveError(AggregateException ex, object state) - { - ((TraceSource)state).TraceEvent(TraceEventType.Error, 0, "Failed to send keep alive: " + ex.GetBaseException()); - } - - private class ConnectionMetadata - { - public ConnectionMetadata(ITrackingConnection connection) - { - Connection = connection; - Initial = DateTime.UtcNow; - LastMarked = DateTime.UtcNow; - } - - // The connection instance - public ITrackingConnection Connection { get; set; } - - // The last time the connection had any activity - public DateTime LastMarked { get; set; } - - // The initial connection time of the connection - public DateTime Initial { get; set; } - } - } -} diff --git a/src/Microsoft.AspNet.SignalR.Core/Transports/TransportManager.cs b/src/Microsoft.AspNet.SignalR.Core/Transports/TransportManager.cs deleted file mode 100644 index a3619dffc..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/Transports/TransportManager.cs +++ /dev/null @@ -1,113 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -using System; -using System.Collections.Concurrent; -using System.Diagnostics.CodeAnalysis; -using Microsoft.AspNet.SignalR.Hosting; - -namespace Microsoft.AspNet.SignalR.Transports -{ - /// - /// The default implementation. - /// - public class TransportManager : ITransportManager - { - private readonly ConcurrentDictionary> _transports = new ConcurrentDictionary>(StringComparer.OrdinalIgnoreCase); - - /// - /// Initializes a new instance of class. - /// - /// The default . - [SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "Those are factory methods")] - public TransportManager(IDependencyResolver resolver) - { - if (resolver == null) - { - throw new ArgumentNullException("resolver"); - } - - Register("foreverFrame", context => new ForeverFrameTransport(context, resolver)); - Register("serverSentEvents", context => new ServerSentEventsTransport(context, resolver)); - Register("longPolling", context => new LongPollingTransport(context, resolver)); - Register("webSockets", context => new WebSocketTransport(context, resolver)); - } - - /// - /// Adds a new transport to the list of supported transports. - /// - /// The specified transport. - /// The factory method for the specified transport. - public void Register(string transportName, Func transportFactory) - { - if (String.IsNullOrEmpty(transportName)) - { - throw new ArgumentNullException("transportName"); - } - - if (transportFactory == null) - { - throw new ArgumentNullException("transportFactory"); - } - - _transports.TryAdd(transportName, transportFactory); - } - - /// - /// Removes a transport from the list of supported transports. - /// - /// The specified transport. - public void Remove(string transportName) - { - if (String.IsNullOrEmpty(transportName)) - { - throw new ArgumentNullException("transportName"); - } - - Func removed; - _transports.TryRemove(transportName, out removed); - } - - /// - /// Gets the specified transport for the specified . - /// - /// The for the current request. - /// The for the specified . - public ITransport GetTransport(HostContext hostContext) - { - if (hostContext == null) - { - throw new ArgumentNullException("hostContext"); - } - - string transportName = hostContext.Request.QueryString["transport"]; - - if (String.IsNullOrEmpty(transportName)) - { - return null; - } - - Func factory; - if (_transports.TryGetValue(transportName, out factory)) - { - return factory(hostContext); - } - - return null; - } - - /// - /// Determines whether the specified transport is supported. - /// - /// The name of the transport to test. - /// True if the transport is supported, otherwise False. - public bool SupportsTransport(string transportName) - { - if (String.IsNullOrEmpty(transportName)) - { - return false; - } - - return _transports.ContainsKey(transportName); - } - } -} diff --git a/src/Microsoft.AspNet.SignalR.Core/Transports/WebSocketTransport.cs b/src/Microsoft.AspNet.SignalR.Core/Transports/WebSocketTransport.cs deleted file mode 100644 index 8f5db2cec..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/Transports/WebSocketTransport.cs +++ /dev/null @@ -1,175 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -using System; -using System.Diagnostics; -using System.IO; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.AspNet.SignalR.Hosting; -using Microsoft.AspNet.SignalR.Infrastructure; -using Microsoft.AspNet.SignalR.Json; -using Microsoft.AspNet.SignalR.Tracing; - -namespace Microsoft.AspNet.SignalR.Transports -{ - public class WebSocketTransport : ForeverTransport - { - private readonly HostContext _context; - private IWebSocket _socket; - private bool _isAlive = true; - - private readonly Action _message; - private readonly Action _closed; - private readonly Action _error; - - public WebSocketTransport(HostContext context, - IDependencyResolver resolver) - : this(context, - resolver.Resolve(), - resolver.Resolve(), - resolver.Resolve(), - resolver.Resolve()) - { - } - - public WebSocketTransport(HostContext context, - IJsonSerializer serializer, - ITransportHeartbeat heartbeat, - IPerformanceCounterManager performanceCounterWriter, - ITraceManager traceManager) - : base(context, serializer, heartbeat, performanceCounterWriter, traceManager) - { - _context = context; - _message = OnMessage; - _closed = OnClosed; - _error = OnError; - } - - public override bool IsAlive - { - get - { - return _isAlive; - } - } - - public override CancellationToken CancellationToken - { - get - { - return CancellationToken.None; - } - } - - public override Task KeepAlive() - { - // Ensure delegate continues to use the C# Compiler static delegate caching optimization. - return EnqueueOperation(state => - { - var webSocket = (IWebSocket)state; - return webSocket.Send("{}"); - }, - _socket); - } - - public override Task ProcessRequest(ITransportConnection connection) - { - if (IsAbortRequest) - { - return connection.Abort(ConnectionId); - } - else - { - var webSocketRequest = _context.Request as IWebSocketRequest; - - // Throw if the server implementation doesn't support websockets - if (webSocketRequest == null) - { - throw new InvalidOperationException(Resources.Error_WebSocketsNotSupported); - } - - Connection = connection; - InitializePersistentState(); - - return webSocketRequest.AcceptWebSocketRequest(socket => - { - _socket = socket; - socket.OnClose = _closed; - socket.OnMessage = _message; - socket.OnError = _error; - - return ProcessReceiveRequest(connection); - }, - InitializeTcs.Task); - } - } - - protected override TextWriter CreateResponseWriter() - { - return new BinaryTextWriter(_socket); - } - - public override Task Send(object value) - { - var context = new WebSocketTransportContext(this, value); - - // Ensure delegate continues to use the C# Compiler static delegate caching optimization. - return EnqueueOperation(state => PerformSend(state), context); - } - - public override Task Send(PersistentResponse response) - { - OnSendingResponse(response); - - return Send((object)response); - } - - protected internal override Task InitializeResponse(ITransportConnection connection) - { - return _socket.Send("{}"); - } - - private static Task PerformSend(object state) - { - var context = (WebSocketTransportContext)state; - - context.Transport.JsonSerializer.Serialize(context.State, context.Transport.OutputWriter); - context.Transport.OutputWriter.Flush(); - - return context.Transport._socket.Flush(); - } - - private void OnMessage(string message) - { - if (Received != null) - { - Received(message).Catch(); - } - } - - private void OnClosed() - { - Trace.TraceInformation("CloseSocket({0})", ConnectionId); - - // Require a request to /abort to stop tracking the connection. #2195 - _isAlive = false; - } - - private void OnError(Exception error) - { - Trace.TraceError("OnError({0}, {1})", ConnectionId, error); - } - - private class WebSocketTransportContext - { - public WebSocketTransport Transport; - public object State; - - public WebSocketTransportContext(WebSocketTransport transport, object state) - { - Transport = transport; - State = state; - } - } - } -} \ No newline at end of file diff --git a/src/Microsoft.AspNet.SignalR.Core/app.config b/src/Microsoft.AspNet.SignalR.Core/app.config deleted file mode 100644 index 44298137a..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/app.config +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - \ No newline at end of file diff --git a/src/Microsoft.AspNet.SignalR.Core/packages.config b/src/Microsoft.AspNet.SignalR.Core/packages.config deleted file mode 100644 index 7c276ed86..000000000 --- a/src/Microsoft.AspNet.SignalR.Core/packages.config +++ /dev/null @@ -1,4 +0,0 @@ - - - - \ No newline at end of file diff --git a/src/Microsoft.AspNet.SignalR.Owin/Handlers/CallHandler.cs b/src/Microsoft.AspNet.SignalR.Owin/Handlers/CallHandler.cs deleted file mode 100644 index 3dd5c6975..000000000 --- a/src/Microsoft.AspNet.SignalR.Owin/Handlers/CallHandler.cs +++ /dev/null @@ -1,110 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -using System; -using System.Collections.Generic; -using System.Security.Principal; -using System.Threading.Tasks; -using Microsoft.AspNet.SignalR.Hosting; -using Microsoft.AspNet.SignalR.Owin.Infrastructure; - -namespace Microsoft.AspNet.SignalR.Owin -{ - public class CallHandler - { - private readonly ConnectionConfiguration _configuration; - private readonly PersistentConnection _connection; - - public CallHandler(ConnectionConfiguration configuration, PersistentConnection connection) - { - _configuration = configuration; - _connection = connection; - } - - public Task Invoke(IDictionary environment) - { - var serverRequest = new ServerRequest(environment); - var serverResponse = new ServerResponse(environment); - var hostContext = new HostContext(serverRequest, serverResponse); - - string origin = serverRequest.RequestHeaders.GetHeader("Origin"); - - if (_configuration.EnableCrossDomain) - { - // Add CORS response headers support - if (!String.IsNullOrEmpty(origin)) - { - serverResponse.ResponseHeaders.SetHeader("Access-Control-Allow-Origin", origin); - serverResponse.ResponseHeaders.SetHeader("Access-Control-Allow-Credentials", "true"); - } - } - else - { - string callback = serverRequest.QueryString["callback"]; - - // If it's a JSONP request and we're not allowing cross domain requests then block it - // If there's an origin header and it's not a same origin request then block it. - - if (!String.IsNullOrEmpty(callback) || - (!String.IsNullOrEmpty(origin) && !IsSameOrigin(serverRequest.Url, origin))) - { - return EndResponse(environment, 403, Resources.Forbidden_CrossDomainIsDisabled); - } - } - - // Add the nosniff header for all responses to prevent IE from trying to sniff mime type from contents - serverResponse.ResponseHeaders.SetHeader("X-Content-Type-Options", "nosniff"); - - // REVIEW: Performance - hostContext.Items[HostConstants.SupportsWebSockets] = environment.SupportsWebSockets(); - hostContext.Items[HostConstants.ShutdownToken] = environment.GetShutdownToken(); - hostContext.Items[HostConstants.DebugMode] = environment.GetIsDebugEnabled(); - - serverRequest.DisableRequestCompression(); - serverResponse.DisableResponseBuffering(); - - _connection.Initialize(_configuration.Resolver, hostContext); - - if (!_connection.Authorize(serverRequest)) - { - IPrincipal user = hostContext.Request.User; - if (user != null && user.Identity.IsAuthenticated) - { - // If we failed to authorize the request then return a 403 since the request - // can't do anything - return EndResponse(environment, 403, "Forbidden"); - } - else - { - // If we failed to authorize the request and the user is not authenticated - // then return a 401 - return EndResponse(environment, 401, "Unauthorized"); - } - } - else - { - return _connection.ProcessRequest(hostContext); - } - } - - private static Task EndResponse(IDictionary environment, int statusCode, string reason) - { - environment[OwinConstants.ResponseStatusCode] = statusCode; - environment[OwinConstants.ResponseReasonPhrase] = reason; - - return TaskAsyncHelper.Empty; - } - - private static bool IsSameOrigin(Uri requestUri, string origin) - { - Uri originUri; - if (!Uri.TryCreate(origin.Trim(), UriKind.Absolute, out originUri)) - { - return false; - } - - return (requestUri.Scheme == originUri.Scheme) && - (requestUri.Host == originUri.Host) && - (requestUri.Port == originUri.Port); - } - } -} diff --git a/src/Microsoft.AspNet.SignalR.Owin/Handlers/HubDispatcherHandler.cs b/src/Microsoft.AspNet.SignalR.Owin/Handlers/HubDispatcherHandler.cs deleted file mode 100644 index fcf83e5e3..000000000 --- a/src/Microsoft.AspNet.SignalR.Owin/Handlers/HubDispatcherHandler.cs +++ /dev/null @@ -1,40 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using Microsoft.AspNet.SignalR.Hubs; -using Microsoft.AspNet.SignalR.Owin.Infrastructure; - -namespace Microsoft.AspNet.SignalR.Owin.Handlers -{ - using AppFunc = Func, Task>; - - public class HubDispatcherHandler - { - private readonly AppFunc _next; - private readonly string _path; - private readonly HubConfiguration _configuration; - - public HubDispatcherHandler(AppFunc next, string path, HubConfiguration configuration) - { - _next = next; - _path = path; - _configuration = configuration; - } - - public Task Invoke(IDictionary environment) - { - var path = environment.Get(OwinConstants.RequestPath); - if (path == null || !PrefixMatcher.IsMatch(_path, path)) - { - return _next(environment); - } - - var dispatcher = new HubDispatcher(_configuration); - - var handler = new CallHandler(_configuration, dispatcher); - return handler.Invoke(environment); - } - } -} diff --git a/src/Microsoft.AspNet.SignalR.Owin/Handlers/PersistentConnectionHandler.cs b/src/Microsoft.AspNet.SignalR.Owin/Handlers/PersistentConnectionHandler.cs deleted file mode 100644 index 71fb299eb..000000000 --- a/src/Microsoft.AspNet.SignalR.Owin/Handlers/PersistentConnectionHandler.cs +++ /dev/null @@ -1,43 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using Microsoft.AspNet.SignalR.Hosting; -using Microsoft.AspNet.SignalR.Owin.Infrastructure; - -namespace Microsoft.AspNet.SignalR.Owin.Handlers -{ - using AppFunc = Func, Task>; - - public class PersistentConnectionHandler - { - private readonly AppFunc _next; - private readonly string _path; - private readonly Type _connectionType; - private readonly ConnectionConfiguration _configuration; - - public PersistentConnectionHandler(AppFunc next, string path, Type connectionType, ConnectionConfiguration configuration) - { - _next = next; - _path = path; - _connectionType = connectionType; - _configuration = configuration; - } - - public Task Invoke(IDictionary environment) - { - var path = environment.Get(OwinConstants.RequestPath); - if (path == null || !PrefixMatcher.IsMatch(_path, path)) - { - return _next(environment); - } - - var connectionFactory = new PersistentConnectionFactory(_configuration.Resolver); - var connection = connectionFactory.CreateInstance(_connectionType); - - var handler = new CallHandler(_configuration, connection); - return handler.Invoke(environment); - } - } -} diff --git a/src/Microsoft.AspNet.SignalR.Owin/Infrastructure/Headers.cs b/src/Microsoft.AspNet.SignalR.Owin/Infrastructure/Headers.cs deleted file mode 100644 index c320568c9..000000000 --- a/src/Microsoft.AspNet.SignalR.Owin/Infrastructure/Headers.cs +++ /dev/null @@ -1,47 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -using System; -using System.Collections.Generic; - -namespace Microsoft.AspNet.SignalR.Owin.Infrastructure -{ - /// - /// Helper methods for creating and consuming CallParameters.Headers and ResultParameters.Headers. - /// - internal static class Headers - { - public static IDictionary SetHeader(this IDictionary headers, - string name, string value) - { - headers[name] = new[] { value }; - return headers; - } - - public static string[] GetHeaders(this IDictionary headers, - string name) - { - string[] value; - return headers != null && headers.TryGetValue(name, out value) ? value : null; - } - - public static string GetHeader(this IDictionary headers, - string name) - { - var values = GetHeaders(headers, name); - if (values == null) - { - return null; - } - - switch (values.Length) - { - case 0: - return String.Empty; - case 1: - return values[0]; - default: - return String.Join(",", values); - } - } - } -} diff --git a/src/Microsoft.AspNet.SignalR.Owin/Infrastructure/OwinConstants.cs b/src/Microsoft.AspNet.SignalR.Owin/Infrastructure/OwinConstants.cs deleted file mode 100644 index e2c246ba1..000000000 --- a/src/Microsoft.AspNet.SignalR.Owin/Infrastructure/OwinConstants.cs +++ /dev/null @@ -1,46 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -namespace Microsoft.AspNet.SignalR.Owin -{ - internal static class OwinConstants - { - public const string Version = "owin.Version"; - - public const string RequestBody = "owin.RequestBody"; - public const string RequestHeaders = "owin.RequestHeaders"; - public const string RequestScheme = "owin.RequestScheme"; - public const string RequestMethod = "owin.RequestMethod"; - public const string RequestPathBase = "owin.RequestPathBase"; - public const string RequestPath = "owin.RequestPath"; - public const string RequestQueryString = "owin.RequestQueryString"; - public const string RequestProtocol = "owin.RequestProtocol"; - - public const string CallCancelled = "owin.CallCancelled"; - - public const string ResponseStatusCode = "owin.ResponseStatusCode"; - public const string ResponseReasonPhrase = "owin.ResponseReasonPhrase"; - public const string ResponseHeaders = "owin.ResponseHeaders"; - public const string ResponseBody = "owin.ResponseBody"; - - public const string TraceOutput = "host.TraceOutput"; - - public const string User = "server.User"; - public const string RemoteIpAddress = "server.RemoteIpAddress"; - public const string RemotePort = "server.RemotePort"; - public const string LocalIpAddress = "server.LocalIpAddress"; - public const string LocalPort = "server.LocalPort"; - - public const string DisableRequestCompression = "systemweb.DisableResponseCompression"; - public const string DisableRequestBuffering = "server.DisableRequestBuffering"; - public const string DisableResponseBuffering = "server.DisableResponseBuffering"; - - public const string ServerCapabilities = "server.Capabilities"; - public const string WebSocketVersion = "websocket.Version"; - public const string WebSocketAccept = "websocket.Accept"; - - public const string HostOnAppDisposing = "host.OnAppDisposing"; - public const string HostAppNameKey = "host.AppName"; - public const string HostAppModeKey = "host.AppMode"; - public const string AppModeDevelopment = "development"; - } -} diff --git a/src/Microsoft.AspNet.SignalR.Owin/Infrastructure/OwinEnvironmentExtensions.cs b/src/Microsoft.AspNet.SignalR.Owin/Infrastructure/OwinEnvironmentExtensions.cs deleted file mode 100644 index 26459c76c..000000000 --- a/src/Microsoft.AspNet.SignalR.Owin/Infrastructure/OwinEnvironmentExtensions.cs +++ /dev/null @@ -1,69 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -using System; -using System.Collections.Generic; -using System.Threading; - -namespace Microsoft.AspNet.SignalR.Owin -{ - internal static class OwinEnvironmentExtensions - { - internal static T Get(this IDictionary environment, string key) - { - object value; - return environment.TryGetValue(key, out value) ? (T)value : default(T); - } - - internal static CancellationToken GetShutdownToken(this IDictionary env) - { - object value; - return env.TryGetValue(OwinConstants.HostOnAppDisposing, out value) - && value is CancellationToken - ? (CancellationToken)value - : default(CancellationToken); - } - - internal static string GetAppInstanceName(this IDictionary environment) - { - object value; - if (environment.TryGetValue(OwinConstants.HostAppNameKey, out value)) - { - var stringVal = value as string; - - if (!String.IsNullOrEmpty(stringVal)) - { - return stringVal; - } - } - - return null; - } - - internal static bool SupportsWebSockets(this IDictionary environment) - { - object value; - if (environment.TryGetValue(OwinConstants.ServerCapabilities, out value)) - { - var capabilities = value as IDictionary; - if (capabilities != null) - { - return capabilities.ContainsKey(OwinConstants.WebSocketVersion); - } - } - return false; - } - - internal static bool GetIsDebugEnabled(this IDictionary environment) - { - object value; - if (environment.TryGetValue(OwinConstants.HostAppModeKey, out value)) - { - var stringVal = value as string; - return !String.IsNullOrWhiteSpace(stringVal) && - OwinConstants.AppModeDevelopment.Equals(stringVal, StringComparison.OrdinalIgnoreCase); - } - - return false; - } - } -} diff --git a/src/Microsoft.AspNet.SignalR.Owin/Infrastructure/ParamDictionary.cs b/src/Microsoft.AspNet.SignalR.Owin/Infrastructure/ParamDictionary.cs deleted file mode 100644 index 18b159d4d..000000000 --- a/src/Microsoft.AspNet.SignalR.Owin/Infrastructure/ParamDictionary.cs +++ /dev/null @@ -1,35 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using Microsoft.AspNet.SignalR.Infrastructure; - -namespace Microsoft.AspNet.SignalR.Owin.Infrastructure -{ - [SuppressMessage("Microsoft.Performance", "CA1812:AvoidUninstantiatedInternalClasses", Justification = "It is instantiated in the static Parse method")] - internal sealed class ParamDictionary - { - private static readonly char[] DefaultParamSeparators = new[] { '&', ';' }; - private static readonly char[] ParamKeyValueSeparator = new[] { '=' }; - private static readonly char[] LeadingWhitespaceChars = new[] { ' ' }; - - internal static IEnumerable> ParseToEnumerable(string value, char[] delimiters = null) - { - value = value ?? String.Empty; - delimiters = delimiters ?? DefaultParamSeparators; - - var items = value.Split(delimiters, StringSplitOptions.RemoveEmptyEntries); - - foreach (var item in items) - { - string[] pair = item.Split(ParamKeyValueSeparator, 2, StringSplitOptions.None); - - string pairKey = UrlDecoder.UrlDecode(pair[0]).TrimStart(LeadingWhitespaceChars); - string pairValue = pair.Length < 2 ? String.Empty : UrlDecoder.UrlDecode(pair[1]); - - yield return new KeyValuePair(pairKey, pairValue); - } - } - } -} diff --git a/src/Microsoft.AspNet.SignalR.Owin/Infrastructure/PrefixMatcher.cs b/src/Microsoft.AspNet.SignalR.Owin/Infrastructure/PrefixMatcher.cs deleted file mode 100644 index 73186522a..000000000 --- a/src/Microsoft.AspNet.SignalR.Owin/Infrastructure/PrefixMatcher.cs +++ /dev/null @@ -1,50 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -using System; - -namespace Microsoft.AspNet.SignalR.Owin.Infrastructure -{ - internal static class PrefixMatcher - { - public static bool IsMatch(string pathBase, string path) - { - pathBase = EnsureStartsWithSlash(pathBase); - path = EnsureStartsWithSlash(path); - - var pathLength = path.Length; - var pathBaseLength = pathBase.Length; - - if (pathLength < pathBaseLength) - { - return false; - } - - if (pathLength > pathBaseLength && path[pathBaseLength] != '/') - { - return false; - } - - if (!path.StartsWith(pathBase, StringComparison.OrdinalIgnoreCase)) - { - return false; - } - - return true; - } - - private static string EnsureStartsWithSlash(string path) - { - if (path.Length == 0) - { - return path; - } - - if (path[0] == '/') - { - return path; - } - - return '/' + path; - } - } -} diff --git a/src/Microsoft.AspNet.SignalR.Owin/Infrastructure/UrlDecoder.cs b/src/Microsoft.AspNet.SignalR.Owin/Infrastructure/UrlDecoder.cs deleted file mode 100644 index 0d813a875..000000000 --- a/src/Microsoft.AspNet.SignalR.Owin/Infrastructure/UrlDecoder.cs +++ /dev/null @@ -1,150 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -using System; -using System.Text; - -namespace Microsoft.AspNet.SignalR.Infrastructure -{ - // Taken from System.Net.Http.Formatting.Internal.UrlDecoder.cs (http://aspnetwebstack.codeplex.com/) - - /// - /// Helpers for decoding URI query components. - /// - internal static class UrlDecoder - { - // The implementation below is ported from WebUtility for use in .Net 4 - - public static string UrlDecode(string str) - { - if (str == null) - return null; - - return UrlDecodeInternal(str, Encoding.UTF8); - } - - #region UrlDecode implementation - - private static string UrlDecodeInternal(string value, Encoding encoding) - { - if (value == null) - { - return null; - } - - int count = value.Length; - var helper = new DecoderHelper(count, encoding); - - // go through the string's chars collapsing %XX and %uXXXX and - // appending each char as char, with exception of %XX constructs - // that are appended as bytes - - for (int pos = 0; pos < count; pos++) - { - char ch = value[pos]; - - if (ch == '+') - { - ch = ' '; - } - else if (ch == '%' && pos < count - 2) - { - int h1 = HexToInt(value[pos + 1]); - int h2 = HexToInt(value[pos + 2]); - - if (h1 >= 0 && h2 >= 0) - { // valid 2 hex chars - byte b = (byte)((h1 << 4) | h2); - pos += 2; - - // don't add as char - helper.AddByte(b); - continue; - } - } - - if ((ch & 0xFF80) == 0) - helper.AddByte((byte)ch); // 7 bit have to go as bytes because of Unicode - else - helper.AddChar(ch); - } - - return helper.GetString(); - } - - private static int HexToInt(char h) - { - return (h >= '0' && h <= '9') ? h - '0' : - (h >= 'a' && h <= 'f') ? h - 'a' + 10 : - (h >= 'A' && h <= 'F') ? h - 'A' + 10 : - -1; - } - - #endregion - - #region DecoderHelper nested class - - // Internal class to facilitate URL decoding -- keeps char buffer and byte buffer, allows appending of either chars or bytes - private class DecoderHelper - { - private int _bufferSize; - - // Accumulate characters in a special array - private int _numChars; - private char[] _charBuffer; - - // Accumulate bytes for decoding into characters in a special array - private int _numBytes; - private byte[] _byteBuffer; - - // Encoding to convert chars to bytes - private Encoding _encoding; - - private void FlushBytes() - { - if (_numBytes > 0) - { - _numChars += _encoding.GetChars(_byteBuffer, 0, _numBytes, _charBuffer, _numChars); - _numBytes = 0; - } - } - - internal DecoderHelper(int bufferSize, Encoding encoding) - { - _bufferSize = bufferSize; - _encoding = encoding; - - _charBuffer = new char[bufferSize]; - // byte buffer created on demand - } - - internal void AddChar(char ch) - { - if (_numBytes > 0) - FlushBytes(); - - _charBuffer[_numChars++] = ch; - } - - internal void AddByte(byte b) - { - if (_byteBuffer == null) - _byteBuffer = new byte[_bufferSize]; - - _byteBuffer[_numBytes++] = b; - } - - internal String GetString() - { - if (_numBytes > 0) - FlushBytes(); - - if (_numChars > 0) - return new String(_charBuffer, 0, _numChars); - else - return String.Empty; - } - } - - #endregion - } -} diff --git a/src/Microsoft.AspNet.SignalR.Owin/Microsoft.AspNet.SignalR.Owin.csproj b/src/Microsoft.AspNet.SignalR.Owin/Microsoft.AspNet.SignalR.Owin.csproj deleted file mode 100644 index dbf0bb82a..000000000 --- a/src/Microsoft.AspNet.SignalR.Owin/Microsoft.AspNet.SignalR.Owin.csproj +++ /dev/null @@ -1,112 +0,0 @@ - - - - - Debug - x86 - {2B8C6DAD-4D85-41B1-83FD-248D9F347522} - Library - Properties - Microsoft.AspNet.SignalR.Owin - Microsoft.AspNet.SignalR.Owin - v4.0 - 512 - ..\..\ - true - - 12.0.0 - 2.0 - - - true - bin\x86\Debug\ - TRACE;DEBUG - bin\Debug\Microsoft.AspNet.SignalR.Owin.XML - true - 1591 - full - x86 - prompt - C:\Dropbox\Git\NzbDrone\src\Common\Microsoft.AspNet.SignalR.ruleset - 4 - false - - - bin\x86\Release\ - TRACE - bin\Release\Microsoft.AspNet.SignalR.Owin.XML - true - true - 1591 - pdbonly - x86 - prompt - C:\Dropbox\Git\NzbDrone\src\Common\Microsoft.AspNet.SignalR.ruleset - 4 - - - - - - - ..\packages\Owin.1.0\lib\net40\Owin.dll - - - - - Properties\CommonAssemblyInfo.cs - - - Properties\CommonVersionInfo.cs - - - Infrastructure\TaskAsyncHelper.cs - - - - - - - - - - - - - True - True - Resources.resx - - - - - - - - - - {1B9A82C4-BCA1-4834-A33E-226F17BE070B} - Microsoft.AspNet.SignalR.Core - - - - - - - - - ResXFileCodeGenerator - Resources.Designer.cs - Designer - - - - - - diff --git a/src/Microsoft.AspNet.SignalR.Owin/Microsoft.AspNet.SignalR.Owin.csproj.DotSettings b/src/Microsoft.AspNet.SignalR.Owin/Microsoft.AspNet.SignalR.Owin.csproj.DotSettings deleted file mode 100644 index 5b8822215..000000000 --- a/src/Microsoft.AspNet.SignalR.Owin/Microsoft.AspNet.SignalR.Owin.csproj.DotSettings +++ /dev/null @@ -1,2 +0,0 @@ - - DO_NOT_SHOW \ No newline at end of file diff --git a/src/Microsoft.AspNet.SignalR.Owin/OwinExtensions.cs b/src/Microsoft.AspNet.SignalR.Owin/OwinExtensions.cs deleted file mode 100644 index 7ff9c5d8c..000000000 --- a/src/Microsoft.AspNet.SignalR.Owin/OwinExtensions.cs +++ /dev/null @@ -1,86 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -using System; -using System.Diagnostics.CodeAnalysis; -using System.Threading; -using Microsoft.AspNet.SignalR; -using Microsoft.AspNet.SignalR.Hosting; -using Microsoft.AspNet.SignalR.Owin; -using Microsoft.AspNet.SignalR.Owin.Handlers; - -namespace Owin -{ - [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Owin", Justification = "The owin namespace is for consistentcy.")] - public static class OwinExtensions - { - public static IAppBuilder MapHubs(this IAppBuilder builder) - { - return builder.MapHubs(new HubConfiguration()); - } - - public static IAppBuilder MapHubs(this IAppBuilder builder, HubConfiguration configuration) - { - return builder.MapHubs("/signalr", configuration); - } - - public static IAppBuilder MapHubs(this IAppBuilder builder, string path, HubConfiguration configuration) - { - if (configuration == null) - { - throw new ArgumentNullException("configuration"); - } - - return builder.UseType(path, configuration); - } - - [SuppressMessage("Microsoft.Design", "CA1004:GenericMethodsShouldProvideTypeParameter", Justification = "The type parameter is syntactic sugar")] - public static IAppBuilder MapConnection(this IAppBuilder builder, string url) where T : PersistentConnection - { - return builder.MapConnection(url, typeof(T), new ConnectionConfiguration()); - } - - [SuppressMessage("Microsoft.Design", "CA1004:GenericMethodsShouldProvideTypeParameter", Justification = "The type parameter is syntactic sugar")] - public static IAppBuilder MapConnection(this IAppBuilder builder, string url, ConnectionConfiguration configuration) where T : PersistentConnection - { - return builder.MapConnection(url, typeof(T), configuration); - } - - public static IAppBuilder MapConnection(this IAppBuilder builder, string url, Type connectionType, ConnectionConfiguration configuration) - { - if (configuration == null) - { - throw new ArgumentNullException("configuration"); - } - - return builder.UseType(url, connectionType, configuration); - } - - private static IAppBuilder UseType(this IAppBuilder builder, params object[] args) - { - if (args.Length > 0) - { - var configuration = args[args.Length - 1] as ConnectionConfiguration; - - if (configuration == null) - { - throw new ArgumentException(Resources.Error_NoConfiguration); - } - - var resolver = configuration.Resolver; - - if (resolver == null) - { - throw new ArgumentException(Resources.Error_NoDepenendeyResolver); - } - - var env = builder.Properties; - CancellationToken token = env.GetShutdownToken(); - string instanceName = env.GetAppInstanceName(); - - resolver.InitializeHost(instanceName, token); - } - - return builder.Use(typeof(T), args); - } - } -} diff --git a/src/Microsoft.AspNet.SignalR.Owin/Properties/AssemblyInfo.cs b/src/Microsoft.AspNet.SignalR.Owin/Properties/AssemblyInfo.cs deleted file mode 100644 index c0e6f23c4..000000000 --- a/src/Microsoft.AspNet.SignalR.Owin/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,6 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -using System.Reflection; - -[assembly: AssemblyTitle("Microsoft.AspNet.SignalR.Owin")] -[assembly: AssemblyDescription("Assembly containing default SignalR host.")] diff --git a/src/Microsoft.AspNet.SignalR.Owin/RequestExtensions.cs b/src/Microsoft.AspNet.SignalR.Owin/RequestExtensions.cs deleted file mode 100644 index 03676f853..000000000 --- a/src/Microsoft.AspNet.SignalR.Owin/RequestExtensions.cs +++ /dev/null @@ -1,29 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -using System; -using System.Collections.Generic; -using Microsoft.AspNet.SignalR.Owin; - -namespace Microsoft.AspNet.SignalR -{ - public static class RequestExtensions - { - public static T GetOwinVariable(this IRequest request, string key) - { - if (request == null) - { - throw new ArgumentNullException("request"); - } - - var env = request.Items.Get>(ServerRequest.OwinEnvironmentKey); - - return env == null ? default(T) : env.Get(key); - } - - private static T Get(this IDictionary values, string key) - { - object value; - return values.TryGetValue(key, out value) ? (T)value : default(T); - } - } -} diff --git a/src/Microsoft.AspNet.SignalR.Owin/Resources.Designer.cs b/src/Microsoft.AspNet.SignalR.Owin/Resources.Designer.cs deleted file mode 100644 index 2925d4911..000000000 --- a/src/Microsoft.AspNet.SignalR.Owin/Resources.Designer.cs +++ /dev/null @@ -1,96 +0,0 @@ -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// Runtime Version:4.0.30319.18010 -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ - -namespace Microsoft.AspNet.SignalR.Owin { - /// - /// A strongly-typed resource class, for looking up localized strings, etc. - /// - // This class was auto-generated by the StronglyTypedResourceBuilder - // class via a tool like ResGen or Visual Studio. - // To add or remove a member, edit your .ResX file then rerun ResGen - // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - internal class Resources { - - private static global::System.Resources.ResourceManager resourceMan; - - private static global::System.Globalization.CultureInfo resourceCulture; - - [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] - internal Resources() { - } - - /// - /// Returns the cached ResourceManager instance used by this class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Resources.ResourceManager ResourceManager { - get { - if (object.ReferenceEquals(resourceMan, null)) { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Microsoft.AspNet.SignalR.Owin.Resources", typeof(Resources).Assembly); - resourceMan = temp; - } - return resourceMan; - } - } - - /// - /// Overrides the current thread's CurrentUICulture property for all - /// resource lookups using this strongly typed resource class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Globalization.CultureInfo Culture { - get { - return resourceCulture; - } - set { - resourceCulture = value; - } - } - - /// - /// Looks up a localized string similar to A configuration object must be specified.. - /// - internal static string Error_NoConfiguration { - get { - return ResourceManager.GetString("Error_NoConfiguration", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to A dependency resolver must be specified.. - /// - internal static string Error_NoDepenendeyResolver { - get { - return ResourceManager.GetString("Error_NoDepenendeyResolver", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Not a valid web socket request.. - /// - internal static string Error_NotWebSocketRequest { - get { - return ResourceManager.GetString("Error_NotWebSocketRequest", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Forbidden: SignalR cross domain is disabled.. - /// - internal static string Forbidden_CrossDomainIsDisabled { - get { - return ResourceManager.GetString("Forbidden_CrossDomainIsDisabled", resourceCulture); - } - } - } -} diff --git a/src/Microsoft.AspNet.SignalR.Owin/Resources.resx b/src/Microsoft.AspNet.SignalR.Owin/Resources.resx deleted file mode 100644 index c77be7797..000000000 --- a/src/Microsoft.AspNet.SignalR.Owin/Resources.resx +++ /dev/null @@ -1,132 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - A configuration object must be specified. - - - A dependency resolver must be specified. - - - Not a valid web socket request. - - - Forbidden: SignalR cross domain is disabled. - - \ No newline at end of file diff --git a/src/Microsoft.AspNet.SignalR.Owin/ServerRequest.Owin.cs b/src/Microsoft.AspNet.SignalR.Owin/ServerRequest.Owin.cs deleted file mode 100644 index 407b7eb98..000000000 --- a/src/Microsoft.AspNet.SignalR.Owin/ServerRequest.Owin.cs +++ /dev/null @@ -1,237 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Net; -using Microsoft.AspNet.SignalR.Owin.Infrastructure; - -namespace Microsoft.AspNet.SignalR.Owin -{ - public partial class ServerRequest - { - private readonly IDictionary _environment; - - public static readonly string OwinEnvironmentKey = "owin.environment"; - - public ServerRequest(IDictionary environment) - { - _environment = environment; - - Items = new Dictionary(StringComparer.OrdinalIgnoreCase) - { - { OwinEnvironmentKey , _environment } - }; - } - - private string RequestMethod - { - get { return _environment.Get(OwinConstants.RequestMethod); } - } - - public IDictionary RequestHeaders - { - get { return _environment.Get>(OwinConstants.RequestHeaders); } - } - - private Stream RequestBody - { - get { return _environment.Get(OwinConstants.RequestBody); } - } - - private string RequestScheme - { - get { return _environment.Get(OwinConstants.RequestScheme); } - } - - private string RequestPathBase - { - get { return _environment.Get(OwinConstants.RequestPathBase); } - } - - private string RequestPath - { - get { return _environment.Get(OwinConstants.RequestPath); } - } - - private string RequestQueryString - { - get { return _environment.Get(OwinConstants.RequestQueryString); } - } - - public Action DisableRequestCompression - { - get { return _environment.Get(OwinConstants.DisableRequestCompression) ?? (() => { }); } - } - - private bool TryParseHostHeader(out IPAddress address, out string host, out int port) - { - address = null; - host = null; - port = -1; - - var hostHeader = RequestHeaders.GetHeader("Host"); - if (String.IsNullOrWhiteSpace(hostHeader)) - { - return false; - } - - // IPv6 (http://www.ietf.org/rfc/rfc2732.txt) - if (hostHeader.StartsWith("[", StringComparison.Ordinal)) - { - var portIndex = hostHeader.LastIndexOf("]:", StringComparison.Ordinal); - if (portIndex != -1 && Int32.TryParse(hostHeader.Substring(portIndex + 2), out port)) - { - if (IPAddress.TryParse(hostHeader.Substring(1, portIndex - 1), out address)) - { - host = null; - return true; - } - host = hostHeader.Substring(0, portIndex + 1); - return true; - } - if (hostHeader.EndsWith("]", StringComparison.Ordinal)) - { - if (IPAddress.TryParse(hostHeader.Substring(1, hostHeader.Length - 2), out address)) - { - host = null; - port = -1; - return true; - } - } - } - else - { - // IPAddresses - if (IPAddress.TryParse(hostHeader, out address)) - { - host = null; - port = -1; - return true; - } - - var portIndex = hostHeader.LastIndexOf(':'); - if (portIndex != -1 && Int32.TryParse(hostHeader.Substring(portIndex + 1), out port)) - { - host = hostHeader.Substring(0, portIndex); - return true; - } - } - - // Plain - host = hostHeader; - return true; - } - - private string RequestHost - { - get - { - IPAddress address; - string host; - int port; - if (TryParseHostHeader(out address, out host, out port)) - { - return host ?? address.ToString(); - } - return _environment.Get(OwinConstants.LocalIpAddress) ?? IPAddress.Loopback.ToString(); - } - } - - private int RequestPort - { - get - { - IPAddress address; - string host; - int port; - if (TryParseHostHeader(out address, out host, out port)) - { - if (port == -1) - { - return DefaultPort; - } - return port; - } - - var portString = _environment.Get(OwinConstants.LocalPort); - if (Int32.TryParse(portString, out port) && port != 0) - { - return port; - } - - return DefaultPort; - } - } - - private int DefaultPort - { - get - { - return String.Equals(RequestScheme, "https", StringComparison.OrdinalIgnoreCase) ? 443 : 80; - } - } - - private string ContentType - { - get - { - return RequestHeaders.GetHeader("Content-Type"); - } - } - - private string MediaType - { - get - { - var contentType = ContentType; - if (contentType == null) - { - return null; - } - - var delimiterPos = contentType.IndexOfAny(CommaSemicolon); - return delimiterPos < 0 ? contentType : contentType.Substring(0, delimiterPos); - } - } - - private bool HasFormData - { - get - { - var mediaType = MediaType; - return (RequestMethod == "POST" && String.IsNullOrEmpty(mediaType)) - || mediaType == "application/x-www-form-urlencoded" - || mediaType == "multipart/form-data"; - } - } - - private bool HasParseableData - { - get - { - var mediaType = MediaType; - return mediaType == "application/x-www-form-urlencoded" - || mediaType == "multipart/form-data"; - } - } - - private IEnumerable> ReadForm() - { - if (!HasFormData && !HasParseableData) - { - return Enumerable.Empty>(); - } - - var body = RequestBody; - if (body.CanSeek) - { - body.Seek(0, SeekOrigin.Begin); - } - - var text = new StreamReader(body).ReadToEnd(); - return ParamDictionary.ParseToEnumerable(text); - } - } -} diff --git a/src/Microsoft.AspNet.SignalR.Owin/ServerRequest.cs b/src/Microsoft.AspNet.SignalR.Owin/ServerRequest.cs deleted file mode 100644 index 152580f8d..000000000 --- a/src/Microsoft.AspNet.SignalR.Owin/ServerRequest.cs +++ /dev/null @@ -1,155 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -using System; -using System.Collections.Generic; -using System.Collections.Specialized; -using System.Security.Principal; -using System.Threading; -using Microsoft.AspNet.SignalR.Owin.Infrastructure; - -namespace Microsoft.AspNet.SignalR.Owin -{ - public partial class ServerRequest : -#if NET45 - IWebSocketRequest -#else - IRequest -#endif - { - private static readonly char[] CommaSemicolon = new[] { ',', ';' }; - - private Uri _url; - private NameValueCollection _queryString; - private NameValueCollection _headers; - private NameValueCollection _form; - private bool _formInitialized; - private object _formLock = new object(); - private IDictionary _cookies; - - public Uri Url - { - get - { - return LazyInitializer.EnsureInitialized( - ref _url, () => - { - var uriBuilder = new UriBuilder(RequestScheme, RequestHost, RequestPort, RequestPathBase + RequestPath); - if (!String.IsNullOrEmpty(RequestQueryString)) - { - uriBuilder.Query = RequestQueryString; - } - return uriBuilder.Uri; - }); - } - } - - - public NameValueCollection QueryString - { - get - { - return LazyInitializer.EnsureInitialized( - ref _queryString, () => - { - var collection = new NameValueCollection(); - foreach (var kv in ParamDictionary.ParseToEnumerable(RequestQueryString)) - { - collection.Add(kv.Key, kv.Value); - } - return collection; - }); - } - } - - public NameValueCollection Headers - { - get - { - return LazyInitializer.EnsureInitialized( - ref _headers, () => - { - var collection = new NameValueCollection(); - foreach (var kv in RequestHeaders) - { - if (kv.Value != null) - { - for (var index = 0; index != kv.Value.Length; ++index) - { - collection.Add(kv.Key, kv.Value[index]); - } - } - } - return collection; - }); - } - } - - public NameValueCollection Form - { - get - { - return LazyInitializer.EnsureInitialized( - ref _form, ref _formInitialized, ref _formLock, () => - { - var collection = new NameValueCollection(); - foreach (var kv in ReadForm()) - { - collection.Add(kv.Key, kv.Value); - } - return collection; - }); - } - } - - - public IDictionary Cookies - { - get - { - return LazyInitializer.EnsureInitialized( - ref _cookies, () => - { - var cookies = new Dictionary(StringComparer.OrdinalIgnoreCase); - var text = RequestHeaders.GetHeader("Cookie"); - foreach (var kv in ParamDictionary.ParseToEnumerable(text, CommaSemicolon)) - { - if (!cookies.ContainsKey(kv.Key)) - { - cookies.Add(kv.Key, new Cookie(kv.Key, kv.Value)); - } - } - return cookies; - }); - } - } - - public IPrincipal User - { - get { return _environment.Get(OwinConstants.User); } - } - - - public IDictionary Items - { - get; - private set; - } - -#if NET45 - public Task AcceptWebSocketRequest(Func callback, Task initTask) - { - var accept = _environment.Get, WebSocketFunc>>(OwinConstants.WebSocketAccept); - if (accept == null) - { - var response = new ServerResponse(_environment); - response.StatusCode = 400; - return response.End(Resources.Error_NotWebSocketRequest); - } - - var handler = new OwinWebSocketHandler(callback, initTask); - accept(null, handler.ProcessRequestAsync); - return TaskAsyncHelper.Empty; - } -#endif - } -} diff --git a/src/Microsoft.AspNet.SignalR.Owin/ServerResponse.cs b/src/Microsoft.AspNet.SignalR.Owin/ServerResponse.cs deleted file mode 100644 index c268202b6..000000000 --- a/src/Microsoft.AspNet.SignalR.Owin/ServerResponse.cs +++ /dev/null @@ -1,90 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -using System; -using System.Collections.Generic; -using System.IO; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.AspNet.SignalR.Hosting; -using Microsoft.AspNet.SignalR.Owin.Infrastructure; - -namespace Microsoft.AspNet.SignalR.Owin -{ - public partial class ServerResponse : IResponse - { - private readonly CancellationToken _callCancelled; - private readonly IDictionary _environment; - private Stream _responseBody; - - public ServerResponse(IDictionary environment) - { - _environment = environment; - _callCancelled = _environment.Get(OwinConstants.CallCancelled); - } - - public CancellationToken CancellationToken - { - get { return _callCancelled; } - } - - public int StatusCode - { - get - { - return _environment.Get(OwinConstants.ResponseStatusCode); - } - set - { - _environment[OwinConstants.ResponseStatusCode] = value; - } - } - - public string ContentType - { - get { return ResponseHeaders.GetHeader("Content-Type"); } - set { ResponseHeaders.SetHeader("Content-Type", value); } - } - - public void Write(ArraySegment data) - { - ResponseBody.Write(data.Array, data.Offset, data.Count); - } - - public Task Flush() - { -#if NET45 - return ResponseBody.FlushAsync(); -#else - return TaskAsyncHelper.FromMethod(() => ResponseBody.Flush()); -#endif - } - - public Task End() - { - return TaskAsyncHelper.Empty; - } - - public IDictionary ResponseHeaders - { - get { return _environment.Get>(OwinConstants.ResponseHeaders); } - } - - public Stream ResponseBody - { - get - { - if (_responseBody == null) - { - _responseBody = _environment.Get(OwinConstants.ResponseBody); - } - - return _responseBody; - } - } - - public Action DisableResponseBuffering - { - get { return _environment.Get(OwinConstants.DisableResponseBuffering) ?? (() => { }); } - } - } -} diff --git a/src/Microsoft.AspNet.SignalR.Owin/app.config b/src/Microsoft.AspNet.SignalR.Owin/app.config deleted file mode 100644 index 44298137a..000000000 --- a/src/Microsoft.AspNet.SignalR.Owin/app.config +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - \ No newline at end of file diff --git a/src/Microsoft.AspNet.SignalR.Owin/packages.config b/src/Microsoft.AspNet.SignalR.Owin/packages.config deleted file mode 100644 index ac23ae5cb..000000000 --- a/src/Microsoft.AspNet.SignalR.Owin/packages.config +++ /dev/null @@ -1,4 +0,0 @@ - - - - \ No newline at end of file diff --git a/src/NzbDrone.Api.Test/ClientSchemaTests/SchemaBuilderFixture.cs b/src/NzbDrone.Api.Test/ClientSchemaTests/SchemaBuilderFixture.cs index 385a9b989..e78709c2f 100644 --- a/src/NzbDrone.Api.Test/ClientSchemaTests/SchemaBuilderFixture.cs +++ b/src/NzbDrone.Api.Test/ClientSchemaTests/SchemaBuilderFixture.cs @@ -1,6 +1,6 @@ -using FluentAssertions; +using FluentAssertions; using NUnit.Framework; -using NzbDrone.Api.ClientSchema; +using Lidarr.Http.ClientSchema; using NzbDrone.Core.Annotations; using NzbDrone.Test.Common; @@ -45,4 +45,4 @@ namespace NzbDrone.Api.Test.ClientSchemaTests public string Other { get; set; } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Api.Test/NzbDrone.Api.Test.csproj b/src/NzbDrone.Api.Test/NzbDrone.Api.Test.csproj index 69402ccc5..ffc34d23a 100644 --- a/src/NzbDrone.Api.Test/NzbDrone.Api.Test.csproj +++ b/src/NzbDrone.Api.Test/NzbDrone.Api.Test.csproj @@ -1,5 +1,5 @@  - + Debug @@ -8,13 +8,14 @@ Library Properties NzbDrone.Api.Test - NzbDrone.Api.Test - v4.0 + Lidarr.Api.Test + v4.6.1 512 ..\ true 12.0.0 2.0 + true @@ -26,6 +27,7 @@ MinimumRecommendedRules.ruleset 4 false + false bin\x86\Release\ @@ -36,23 +38,21 @@ prompt MinimumRecommendedRules.ruleset 4 + false ..\packages\NBuilder.4.0.0\lib\net40\FizzWare.NBuilder.dll - True - ..\packages\FluentAssertions.4.19.0\lib\net40\FluentAssertions.dll + ..\packages\FluentAssertions.4.19.0\lib\net45\FluentAssertions.dll - ..\packages\FluentAssertions.4.19.0\lib\net40\FluentAssertions.Core.dll - - - ..\packages\Moq.4.2.1510.2205\lib\net40\Moq.dll + ..\packages\FluentAssertions.4.19.0\lib\net45\FluentAssertions.Core.dll + - ..\packages\NUnit.3.6.0\lib\net40\nunit.framework.dll + ..\packages\NUnit.3.6.0\lib\net45\nunit.framework.dll @@ -70,14 +70,14 @@ + + {7140ff1f-79be-492f-9188-b21a050bf708} + Lidarr.Api.V3 + {F6FC6BE7-0847-4817-A1ED-223DC647C3D7} Marr.Data - - {FD286DF8-2D3A-4394-8AD5-443FADE55FB2} - NzbDrone.Api - {F2BE0FDF-6E47-4827-A420-DD4EF82407F8} NzbDrone.Common @@ -90,6 +90,10 @@ {CADDFCE0-7509-4430-8364-2074E1EEFCA2} NzbDrone.Test.Common + + {5370bff7-1bd7-46bc-af06-7d9ea5cda1d6} + Lidarr.Http + diff --git a/src/NzbDrone.Api.Test/Properties/AssemblyInfo.cs b/src/NzbDrone.Api.Test/Properties/AssemblyInfo.cs index 4d2901c1a..2bcd18392 100644 --- a/src/NzbDrone.Api.Test/Properties/AssemblyInfo.cs +++ b/src/NzbDrone.Api.Test/Properties/AssemblyInfo.cs @@ -1,14 +1,14 @@ -using System.Reflection; +using System.Reflection; using System.Runtime.InteropServices; // General Information about an assembly is controlled through the following // set of attributes. Change these attribute values to modify the information // associated with an assembly. -[assembly: AssemblyTitle("NzbDrone.Api.Test")] +[assembly: AssemblyTitle("Lidarr.Api.Test")] [assembly: AssemblyDescription("")] [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("NzbDrone.Api.Test")] +[assembly: AssemblyProduct("Lidarr.Api.Test")] [assembly: AssemblyCopyright("Copyright © 2013")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] @@ -21,4 +21,3 @@ using System.Runtime.InteropServices; // The following GUID is for the ID of the typelib if this project is exposed to COM [assembly: Guid("260b2ff9-d3b7-4d8a-b720-a12c93d045e5")] -[assembly: AssemblyVersion("10.0.0.*")] diff --git a/src/NzbDrone.Api.Test/packages.config b/src/NzbDrone.Api.Test/packages.config index b329faeb1..0018c93df 100644 --- a/src/NzbDrone.Api.Test/packages.config +++ b/src/NzbDrone.Api.Test/packages.config @@ -1,7 +1,7 @@  - - - - + + + + \ No newline at end of file diff --git a/src/NzbDrone.Api/AlbumStudio/AlbumStudioModule.cs b/src/NzbDrone.Api/AlbumStudio/AlbumStudioModule.cs deleted file mode 100644 index 4fa964a2f..000000000 --- a/src/NzbDrone.Api/AlbumStudio/AlbumStudioModule.cs +++ /dev/null @@ -1,31 +0,0 @@ -using Nancy; -using NzbDrone.Api.Extensions; -using NzbDrone.Core.Music; - -namespace NzbDrone.Api.AlbumPass -{ - public class AlbumStudioModule : NzbDroneApiModule - { - private readonly IAlbumMonitoredService _albumMonitoredService; - - public AlbumStudioModule(IAlbumMonitoredService albumMonitoredService) - : base("/albumstudio") - { - _albumMonitoredService = albumMonitoredService; - Post["/"] = artist => UpdateAll(); - } - - private Response UpdateAll() - { - //Read from request - var request = Request.Body.FromJson(); - - foreach (var s in request.Artist) - { - _albumMonitoredService.SetAlbumMonitoredStatus(s, request.MonitoringOptions); - } - - return "ok".AsResponse(HttpStatusCode.Accepted); - } - } -} diff --git a/src/NzbDrone.Api/AlbumStudio/AlbumStudioResource.cs b/src/NzbDrone.Api/AlbumStudio/AlbumStudioResource.cs deleted file mode 100644 index 1b0779af9..000000000 --- a/src/NzbDrone.Api/AlbumStudio/AlbumStudioResource.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System.Collections.Generic; -using NzbDrone.Core.Music; - -namespace NzbDrone.Api.AlbumPass -{ - public class AlbumStudioResource - { - public List Artist { get; set; } - public MonitoringOptions MonitoringOptions { get; set; } - } -} diff --git a/src/NzbDrone.Api/Albums/AlbumModule.cs b/src/NzbDrone.Api/Albums/AlbumModule.cs deleted file mode 100644 index e2d635fd0..000000000 --- a/src/NzbDrone.Api/Albums/AlbumModule.cs +++ /dev/null @@ -1,42 +0,0 @@ -using System.Collections.Generic; -using NzbDrone.Api.REST; -using NzbDrone.Core.Music; -using NzbDrone.Core.ArtistStats; -using NzbDrone.Core.DecisionEngine; -using NzbDrone.SignalR; - -namespace NzbDrone.Api.Albums -{ - public class AlbumModule : AlbumModuleWithSignalR - { - public AlbumModule(IArtistService artistService, - IArtistStatisticsService artistStatisticsService, - IAlbumService albumService, - IQualityUpgradableSpecification qualityUpgradableSpecification, - IBroadcastSignalRMessage signalRBroadcaster) - : base(albumService, artistStatisticsService, artistService, qualityUpgradableSpecification, signalRBroadcaster) - { - GetResourceAll = GetAlbums; - UpdateResource = SetMonitored; - } - - private List GetAlbums() - { - if (!Request.Query.ArtistId.HasValue) - { - throw new BadRequestException("artistId is missing"); - } - - var artistId = (int)Request.Query.ArtistId; - - var resources = MapToResource(_albumService.GetAlbumsByArtist(artistId), false); - - return resources; - } - - private void SetMonitored(AlbumResource albumResource) - { - _albumService.SetAlbumMonitored(albumResource.Id, albumResource.Monitored); - } - } -} diff --git a/src/NzbDrone.Api/Albums/AlbumResource.cs b/src/NzbDrone.Api/Albums/AlbumResource.cs deleted file mode 100644 index a283deebf..000000000 --- a/src/NzbDrone.Api/Albums/AlbumResource.cs +++ /dev/null @@ -1,87 +0,0 @@ -using NzbDrone.Core.Music; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using NzbDrone.Api.REST; -using NzbDrone.Api.Music; -using NzbDrone.Core.MediaCover; - -namespace NzbDrone.Api.Albums -{ - public class AlbumResource : RestResource - { - - public string Title { get; set; } - public int ArtistId { get; set; } - public string Label { get; set; } - public bool Monitored { get; set; } - public string Path { get; set; } - public int ProfileId { get; set; } - public int Duration { get; set; } - public string AlbumType { get; set; } - public Ratings Ratings { get; set; } - public DateTime? ReleaseDate { get; set; } - public List Genres { get; set; } - public ArtistResource Artist { get; set; } - public List Images { get; set; } - public AlbumStatisticsResource Statistics { get; set; } - - } - - public static class AlbumResourceMapper - { - public static AlbumResource ToResource(this Core.Music.Album model) - { - if (model == null) return null; - - return new AlbumResource - { - Id = model.Id, - ArtistId = model.ArtistId, - Label = model.Label, - Path = model.Path, - ProfileId = model.ProfileId, - Monitored = model.Monitored, - ReleaseDate = model.ReleaseDate, - Genres = model.Genres, - Title = model.Title, - Images = model.Images, - Ratings = model.Ratings, - Duration = model.Duration, - AlbumType = model.AlbumType - }; - } - - public static Album ToModel(this AlbumResource resource) - { - if (resource == null) return null; - - return new Core.Music.Album - { - Id = resource.Id, - ArtistId = resource.ArtistId, - Label = resource.Label, - Path = resource.Path, - Monitored = resource.Monitored, - ProfileId = resource.ProfileId, - ReleaseDate = resource.ReleaseDate, - Genres = resource.Genres, - Title = resource.Title, - Images = resource.Images, - Ratings = resource.Ratings, - AlbumType = resource.AlbumType - }; - } - - public static List ToResource(this IEnumerable models) - { - return models.Select(ToResource).ToList(); - } - - public static List ToModel(this IEnumerable resources) - { - return resources?.Select(ToModel).ToList() ?? new List(); - } - } -} diff --git a/src/NzbDrone.Api/Authentication/EnableAuthInNancy.cs b/src/NzbDrone.Api/Authentication/EnableAuthInNancy.cs deleted file mode 100644 index 580196363..000000000 --- a/src/NzbDrone.Api/Authentication/EnableAuthInNancy.cs +++ /dev/null @@ -1,88 +0,0 @@ -using System; -using System.Text; -using Nancy; -using Nancy.Authentication.Basic; -using Nancy.Authentication.Forms; -using Nancy.Bootstrapper; -using Nancy.Cryptography; -using NzbDrone.Api.Extensions; -using NzbDrone.Api.Extensions.Pipelines; -using NzbDrone.Core.Authentication; -using NzbDrone.Core.Configuration; - -namespace NzbDrone.Api.Authentication -{ - public class EnableAuthInNancy : IRegisterNancyPipeline - { - private readonly IAuthenticationService _authenticationService; - private readonly IConfigService _configService; - private readonly IConfigFileProvider _configFileProvider; - - public EnableAuthInNancy(IAuthenticationService authenticationService, - IConfigService configService, - IConfigFileProvider configFileProvider) - { - _authenticationService = authenticationService; - _configService = configService; - _configFileProvider = configFileProvider; - } - - public int Order => 10; - - public void Register(IPipelines pipelines) - { - if (_configFileProvider.AuthenticationMethod == AuthenticationType.Forms) - { - RegisterFormsAuth(pipelines); - } - - else if (_configFileProvider.AuthenticationMethod == AuthenticationType.Basic) - { - pipelines.EnableBasicAuthentication(new BasicAuthenticationConfiguration(_authenticationService, "Lidarr")); - } - - pipelines.BeforeRequest.AddItemToEndOfPipeline((Func) RequiresAuthentication); - pipelines.AfterRequest.AddItemToEndOfPipeline((Action) RemoveLoginHooksForApiCalls); - } - - private Response RequiresAuthentication(NancyContext context) - { - Response response = null; - - if (!_authenticationService.IsAuthenticated(context)) - { - response = new Response { StatusCode = HttpStatusCode.Unauthorized }; - } - - return response; - } - - private void RegisterFormsAuth(IPipelines pipelines) - { - var cryptographyConfiguration = new CryptographyConfiguration( - new RijndaelEncryptionProvider(new PassphraseKeyGenerator(_configService.RijndaelPassphrase, Encoding.ASCII.GetBytes(_configService.RijndaelSalt))), - new DefaultHmacProvider(new PassphraseKeyGenerator(_configService.HmacPassphrase, Encoding.ASCII.GetBytes(_configService.HmacSalt))) - ); - - FormsAuthentication.Enable(pipelines, new FormsAuthenticationConfiguration - { - RedirectUrl = _configFileProvider.UrlBase + "/login", - UserMapper = _authenticationService, - CryptographyConfiguration = cryptographyConfiguration - }); - } - - private void RemoveLoginHooksForApiCalls(NancyContext context) - { - if (context.Request.IsApiRequest()) - { - if ((context.Response.StatusCode == HttpStatusCode.SeeOther && - context.Response.Headers["Location"].StartsWith($"{_configFileProvider.UrlBase}/login", StringComparison.InvariantCultureIgnoreCase)) || - context.Response.StatusCode == HttpStatusCode.Unauthorized) - { - context.Response = new { Error = "Unauthorized" }.AsResponse(HttpStatusCode.Unauthorized); - } - } - } - } -} diff --git a/src/NzbDrone.Api/Blacklist/BlacklistModule.cs b/src/NzbDrone.Api/Blacklist/BlacklistModule.cs deleted file mode 100644 index 1687b31e3..000000000 --- a/src/NzbDrone.Api/Blacklist/BlacklistModule.cs +++ /dev/null @@ -1,29 +0,0 @@ -using NzbDrone.Core.Blacklisting; -using NzbDrone.Core.Datastore; - -namespace NzbDrone.Api.Blacklist -{ - public class BlacklistModule : NzbDroneRestModule - { - private readonly IBlacklistService _blacklistService; - - public BlacklistModule(IBlacklistService blacklistService) - { - _blacklistService = blacklistService; - GetResourcePaged = GetBlacklist; - DeleteResource = DeleteBlacklist; - } - - private PagingResource GetBlacklist(PagingResource pagingResource) - { - var pagingSpec = pagingResource.MapToPagingSpec("id", SortDirection.Ascending); - - return ApplyToPage(_blacklistService.Paged, pagingSpec, BlacklistResourceMapper.MapToResource); - } - - private void DeleteBlacklist(int id) - { - _blacklistService.Delete(id); - } - } -} diff --git a/src/NzbDrone.Api/Calendar/CalendarFeedModule.cs b/src/NzbDrone.Api/Calendar/CalendarFeedModule.cs deleted file mode 100644 index e65a42833..000000000 --- a/src/NzbDrone.Api/Calendar/CalendarFeedModule.cs +++ /dev/null @@ -1,130 +0,0 @@ -using Nancy; -using System; -using System.Collections.Generic; -using System.Linq; -using Ical.Net; -using Ical.Net.DataTypes; -using Ical.Net.Interfaces.Serialization; -using Ical.Net.Serialization; -using Ical.Net.Serialization.iCalendar.Factory; -using NzbDrone.Core.Tv; -using NzbDrone.Core.Music; -using Nancy.Responses; -using NzbDrone.Core.Tags; -using NzbDrone.Common.Extensions; - -namespace NzbDrone.Api.Calendar -{ - public class CalendarFeedModule : NzbDroneFeedModule - { - private readonly IAlbumService _albumService; - private readonly ITagService _tagService; - - public CalendarFeedModule(IAlbumService albumService, ITagService tagService) - : base("calendar") - { - _albumService = albumService; - _tagService = tagService; - - Get["/NzbDrone.ics"] = options => GetCalendarFeed(); - Get["/Lidarr.ics"] = options => GetCalendarFeed(); - } - - private Response GetCalendarFeed() - { - var pastDays = 7; - var futureDays = 28; - var start = DateTime.Today.AddDays(-pastDays); - var end = DateTime.Today.AddDays(futureDays); - var unmonitored = false; - var premiersOnly = false; - var asAllDay = false; - var tags = new List(); - - // TODO: Remove start/end parameters in v3, they don't work well for iCal - var queryStart = Request.Query.Start; - var queryEnd = Request.Query.End; - var queryPastDays = Request.Query.PastDays; - var queryFutureDays = Request.Query.FutureDays; - var queryUnmonitored = Request.Query.Unmonitored; - var queryPremiersOnly = Request.Query.PremiersOnly; - var queryAsAllDay = Request.Query.AsAllDay; - var queryTags = Request.Query.Tags; - - if (queryStart.HasValue) start = DateTime.Parse(queryStart.Value); - if (queryEnd.HasValue) end = DateTime.Parse(queryEnd.Value); - - if (queryPastDays.HasValue) - { - pastDays = int.Parse(queryPastDays.Value); - start = DateTime.Today.AddDays(-pastDays); - } - - if (queryFutureDays.HasValue) - { - futureDays = int.Parse(queryFutureDays.Value); - end = DateTime.Today.AddDays(futureDays); - } - - if (queryUnmonitored.HasValue) - { - unmonitored = bool.Parse(queryUnmonitored.Value); - } - - if (queryPremiersOnly.HasValue) - { - premiersOnly = bool.Parse(queryPremiersOnly.Value); - } - - if (queryAsAllDay.HasValue) - { - asAllDay = bool.Parse(queryAsAllDay.Value); - } - - if (queryTags.HasValue) - { - var tagInput = (string)queryTags.Value.ToString(); - tags.AddRange(tagInput.Split(',').Select(_tagService.GetTag).Select(t => t.Id)); - } - - var albums = _albumService.AlbumsBetweenDates(start, end, unmonitored); - var calendar = new Ical.Net.Calendar - { - // This will need to point to the hosted web site - // TODO - ProductId = "-//lidarr.audio//Lidarr//EN" - }; - - - - foreach (var album in albums.OrderBy(v => v.ReleaseDate)) - { - //if (premiersOnly && (album.SeasonNumber == 0 || album.EpisodeNumber != 1)) - //{ - // continue; - //} - - if (tags.Any() && tags.None(album.Artist.Tags.Contains)) - { - continue; - } - - var occurrence = calendar.Create(); - occurrence.Uid = "NzbDrone_album_" + album.Id; - //occurrence.Status = album.HasFile ? EventStatus.Confirmed : EventStatus.Tentative; - //occurrence.Description = album.Overview; - //occurrence.Categories = new List() { album.Artist. }; - - occurrence.Start = new CalDateTime(album.ReleaseDate.Value) { HasTime = false }; - - occurrence.Summary =$"{album.Artist.Name} - {album.Title}"; - - } - - var serializer = (IStringSerializer) new SerializerFactory().Build(calendar.GetType(), new SerializationContext()); - var icalendar = serializer.SerializeToString(calendar); - - return new TextResponse(icalendar, "text/calendar"); - } - } -} diff --git a/src/NzbDrone.Api/Calendar/CalendarModule.cs b/src/NzbDrone.Api/Calendar/CalendarModule.cs deleted file mode 100644 index 352fad3a6..000000000 --- a/src/NzbDrone.Api/Calendar/CalendarModule.cs +++ /dev/null @@ -1,45 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using NzbDrone.Api.Episodes; -using NzbDrone.Api.Albums; -using NzbDrone.Core.DecisionEngine; -using NzbDrone.Core.Tv; -using NzbDrone.Core.Music; -using NzbDrone.Core.ArtistStats; -using NzbDrone.SignalR; - -namespace NzbDrone.Api.Calendar -{ - public class CalendarModule : AlbumModuleWithSignalR - { - public CalendarModule(IAlbumService albumService, - IArtistStatisticsService artistStatisticsService, - IArtistService artistService, - IQualityUpgradableSpecification qualityUpgradableSpecification, - IBroadcastSignalRMessage signalRBroadcaster) - : base(albumService, artistStatisticsService, artistService, qualityUpgradableSpecification, signalRBroadcaster, "calendar") - { - GetResourceAll = GetCalendar; - } - - private List GetCalendar() - { - var start = DateTime.Today; - var end = DateTime.Today.AddDays(2); - var includeUnmonitored = false; - - var queryStart = Request.Query.Start; - var queryEnd = Request.Query.End; - var queryIncludeUnmonitored = Request.Query.Unmonitored; - - if (queryStart.HasValue) start = DateTime.Parse(queryStart.Value); - if (queryEnd.HasValue) end = DateTime.Parse(queryEnd.Value); - if (queryIncludeUnmonitored.HasValue) includeUnmonitored = Convert.ToBoolean(queryIncludeUnmonitored.Value); - - var resources = MapToResource(_albumService.AlbumsBetweenDates(start, end, includeUnmonitored), true); - - return resources.OrderBy(e => e.ReleaseDate).ToList(); - } - } -} diff --git a/src/NzbDrone.Api/ClientSchema/FieldDefinitionAttribute.cs b/src/NzbDrone.Api/ClientSchema/FieldDefinitionAttribute.cs deleted file mode 100644 index 4e796bd8c..000000000 --- a/src/NzbDrone.Api/ClientSchema/FieldDefinitionAttribute.cs +++ /dev/null @@ -1,4 +0,0 @@ -namespace NzbDrone.Api.ClientSchema -{ - -} \ No newline at end of file diff --git a/src/NzbDrone.Api/ClientSchema/SchemaDeserializer.cs b/src/NzbDrone.Api/ClientSchema/SchemaDeserializer.cs deleted file mode 100644 index 6af07257f..000000000 --- a/src/NzbDrone.Api/ClientSchema/SchemaDeserializer.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace NzbDrone.Api.ClientSchema -{ - public static class SchemaDeserializer - { - - } -} \ No newline at end of file diff --git a/src/NzbDrone.Api/Commands/CommandModule.cs b/src/NzbDrone.Api/Commands/CommandModule.cs deleted file mode 100644 index fcaeef9c4..000000000 --- a/src/NzbDrone.Api/Commands/CommandModule.cs +++ /dev/null @@ -1,66 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using NzbDrone.Api.Extensions; -using NzbDrone.Api.Validation; -using NzbDrone.Common; -using NzbDrone.Core.Datastore.Events; -using NzbDrone.Core.Messaging.Commands; -using NzbDrone.Core.Messaging.Events; -using NzbDrone.Core.ProgressMessaging; -using NzbDrone.SignalR; - - -namespace NzbDrone.Api.Commands -{ - public class CommandModule : NzbDroneRestModuleWithSignalR, IHandle - { - private readonly IManageCommandQueue _commandQueueManager; - private readonly IServiceFactory _serviceFactory; - - public CommandModule(IManageCommandQueue commandQueueManager, - IBroadcastSignalRMessage signalRBroadcaster, - IServiceFactory serviceFactory) - : base(signalRBroadcaster) - { - _commandQueueManager = commandQueueManager; - _serviceFactory = serviceFactory; - - GetResourceById = GetCommand; - CreateResource = StartCommand; - GetResourceAll = GetStartedCommands; - - PostValidator.RuleFor(c => c.Name).NotBlank(); - } - - private CommandResource GetCommand(int id) - { - return _commandQueueManager.Get(id).ToResource(); - } - - private int StartCommand(CommandResource commandResource) - { - var commandType = _serviceFactory.GetImplementations(typeof(Command)) - .Single(c => c.Name.Replace("Command", "").Equals(commandResource.Name, StringComparison.InvariantCultureIgnoreCase)); - - dynamic command = Request.Body.FromJson(commandType); - command.Trigger = CommandTrigger.Manual; - - var trackedCommand = _commandQueueManager.Push(command, CommandPriority.Normal, CommandTrigger.Manual); - return trackedCommand.Id; - } - - private List GetStartedCommands() - { - return _commandQueueManager.GetStarted().ToResource(); - } - - public void Handle(CommandUpdatedEvent message) - { - if (message.Command.Body.SendUpdatesToClient) - { - BroadcastResourceChange(ModelAction.Updated, message.Command.ToResource()); - } - } - } -} \ No newline at end of file diff --git a/src/NzbDrone.Api/Config/DownloadClientConfigModule.cs b/src/NzbDrone.Api/Config/DownloadClientConfigModule.cs deleted file mode 100644 index c504ffe08..000000000 --- a/src/NzbDrone.Api/Config/DownloadClientConfigModule.cs +++ /dev/null @@ -1,29 +0,0 @@ -using FluentValidation; -using NzbDrone.Core.Configuration; -using NzbDrone.Core.Validation.Paths; - -namespace NzbDrone.Api.Config -{ - public class DownloadClientConfigModule : NzbDroneConfigModule - { - public DownloadClientConfigModule(IConfigService configService, - RootFolderValidator rootFolderValidator, - PathExistsValidator pathExistsValidator, - MappedNetworkDriveValidator mappedNetworkDriveValidator) - : base(configService) - { - SharedValidator.RuleFor(c => c.DownloadedAlbumsFolder) - .Cascade(CascadeMode.StopOnFirstFailure) - .IsValidPath() - .SetValidator(rootFolderValidator) - .SetValidator(mappedNetworkDriveValidator) - .SetValidator(pathExistsValidator) - .When(c => !string.IsNullOrWhiteSpace(c.DownloadedAlbumsFolder)); - } - - protected override DownloadClientConfigResource ToResource(IConfigService model) - { - return DownloadClientConfigResourceMapper.ToResource(model); - } - } -} \ No newline at end of file diff --git a/src/NzbDrone.Api/Config/DownloadClientConfigResource.cs b/src/NzbDrone.Api/Config/DownloadClientConfigResource.cs deleted file mode 100644 index b9930ad0d..000000000 --- a/src/NzbDrone.Api/Config/DownloadClientConfigResource.cs +++ /dev/null @@ -1,37 +0,0 @@ -using NzbDrone.Api.REST; -using NzbDrone.Core.Configuration; - -namespace NzbDrone.Api.Config -{ - public class DownloadClientConfigResource : RestResource - { - public string DownloadedAlbumsFolder { get; set; } - public string DownloadClientWorkingFolders { get; set; } - public int DownloadedAlbumsScanInterval { get; set; } - - public bool EnableCompletedDownloadHandling { get; set; } - public bool RemoveCompletedDownloads { get; set; } - - public bool AutoRedownloadFailed { get; set; } - public bool RemoveFailedDownloads { get; set; } - } - - public static class DownloadClientConfigResourceMapper - { - public static DownloadClientConfigResource ToResource(IConfigService model) - { - return new DownloadClientConfigResource - { - DownloadedAlbumsFolder = model.DownloadedAlbumsFolder, - DownloadClientWorkingFolders = model.DownloadClientWorkingFolders, - DownloadedAlbumsScanInterval = model.DownloadedAlbumsScanInterval, - - EnableCompletedDownloadHandling = model.EnableCompletedDownloadHandling, - RemoveCompletedDownloads = model.RemoveCompletedDownloads, - - AutoRedownloadFailed = model.AutoRedownloadFailed, - RemoveFailedDownloads = model.RemoveFailedDownloads - }; - } - } -} diff --git a/src/NzbDrone.Api/Config/NamingConfigResource.cs b/src/NzbDrone.Api/Config/NamingConfigResource.cs deleted file mode 100644 index 47acd6a24..000000000 --- a/src/NzbDrone.Api/Config/NamingConfigResource.cs +++ /dev/null @@ -1,68 +0,0 @@ -using NzbDrone.Api.REST; -using NzbDrone.Core.Organizer; - -namespace NzbDrone.Api.Config -{ - public class NamingConfigResource : RestResource - { - public bool RenameTracks { get; set; } - public bool ReplaceIllegalCharacters { get; set; } - public int MultiEpisodeStyle { get; set; } - public string StandardTrackFormat { get; set; } - public string ArtistFolderFormat { get; set; } - public string AlbumFolderFormat { get; set; } - public bool IncludeArtistName { get; set; } - public bool IncludeAlbumTitle { get; set; } - public bool IncludeQuality { get; set; } - public bool ReplaceSpaces { get; set; } - public string Separator { get; set; } - public string NumberStyle { get; set; } - } - - public static class NamingConfigResourceMapper - { - public static NamingConfigResource ToResource(this NamingConfig model) - { - return new NamingConfigResource - { - Id = model.Id, - - RenameTracks = model.RenameTracks, - ReplaceIllegalCharacters = model.ReplaceIllegalCharacters, - StandardTrackFormat = model.StandardTrackFormat, - ArtistFolderFormat = model.ArtistFolderFormat, - AlbumFolderFormat = model.AlbumFolderFormat - //IncludeSeriesTitle - //IncludeEpisodeTitle - //IncludeQuality - //ReplaceSpaces - //Separator - //NumberStyle - }; - } - - public static void AddToResource(this BasicNamingConfig basicNamingConfig, NamingConfigResource resource) - { - resource.IncludeArtistName = basicNamingConfig.IncludeArtistName; - resource.IncludeAlbumTitle = basicNamingConfig.IncludeAlbumTitle; - resource.IncludeQuality = basicNamingConfig.IncludeQuality; - resource.ReplaceSpaces = basicNamingConfig.ReplaceSpaces; - resource.Separator = basicNamingConfig.Separator; - resource.NumberStyle = basicNamingConfig.NumberStyle; - } - - public static NamingConfig ToModel(this NamingConfigResource resource) - { - return new NamingConfig - { - Id = resource.Id, - - RenameTracks = resource.RenameTracks, - ReplaceIllegalCharacters = resource.ReplaceIllegalCharacters, - StandardTrackFormat = resource.StandardTrackFormat, - ArtistFolderFormat = resource.ArtistFolderFormat, - AlbumFolderFormat = resource.AlbumFolderFormat - }; - } - } -} diff --git a/src/NzbDrone.Api/Config/NamingSampleResource.cs b/src/NzbDrone.Api/Config/NamingSampleResource.cs deleted file mode 100644 index f6d6d15b3..000000000 --- a/src/NzbDrone.Api/Config/NamingSampleResource.cs +++ /dev/null @@ -1,16 +0,0 @@ -namespace NzbDrone.Api.Config -{ - public class NamingSampleResource - { - public string SingleEpisodeExample { get; set; } - public string SingleTrackExample { get; set; } - public string MultiEpisodeExample { get; set; } - public string DailyEpisodeExample { get; set; } - public string AnimeEpisodeExample { get; set; } - public string AnimeMultiEpisodeExample { get; set; } - public string SeriesFolderExample { get; set; } - public string SeasonFolderExample { get; set; } - public string ArtistFolderExample { get; set; } - public string AlbumFolderExample { get; set; } - } -} \ No newline at end of file diff --git a/src/NzbDrone.Api/Config/NzbDroneConfigModule.cs b/src/NzbDrone.Api/Config/NzbDroneConfigModule.cs deleted file mode 100644 index e5d324950..000000000 --- a/src/NzbDrone.Api/Config/NzbDroneConfigModule.cs +++ /dev/null @@ -1,51 +0,0 @@ -using System.Linq; -using System.Reflection; -using NzbDrone.Api.REST; -using NzbDrone.Core.Configuration; - -namespace NzbDrone.Api.Config -{ - public abstract class NzbDroneConfigModule : NzbDroneRestModule where TResource : RestResource, new() - { - private readonly IConfigService _configService; - - protected NzbDroneConfigModule(IConfigService configService) - : this(new TResource().ResourceName.Replace("config", ""), configService) - { - } - - protected NzbDroneConfigModule(string resource, IConfigService configService) : - base("config/" + resource.Trim('/')) - { - _configService = configService; - - GetResourceSingle = GetConfig; - GetResourceById = GetConfig; - UpdateResource = SaveConfig; - } - - private TResource GetConfig() - { - var resource = ToResource(_configService); - resource.Id = 1; - - return resource; - } - - protected abstract TResource ToResource(IConfigService model); - - private TResource GetConfig(int id) - { - return GetConfig(); - } - - private void SaveConfig(TResource resource) - { - var dictionary = resource.GetType() - .GetProperties(BindingFlags.Instance | BindingFlags.Public) - .ToDictionary(prop => prop.Name, prop => prop.GetValue(resource, null)); - - _configService.SaveConfigDictionary(dictionary); - } - } -} diff --git a/src/NzbDrone.Api/Config/UiConfigModule.cs b/src/NzbDrone.Api/Config/UiConfigModule.cs deleted file mode 100644 index 1762acaca..000000000 --- a/src/NzbDrone.Api/Config/UiConfigModule.cs +++ /dev/null @@ -1,18 +0,0 @@ -using NzbDrone.Core.Configuration; - -namespace NzbDrone.Api.Config -{ - public class UiConfigModule : NzbDroneConfigModule - { - public UiConfigModule(IConfigService configService) - : base(configService) - { - - } - - protected override UiConfigResource ToResource(IConfigService model) - { - return UiConfigResourceMapper.ToResource(model); - } - } -} \ No newline at end of file diff --git a/src/NzbDrone.Api/DownloadClient/DownloadClientModule.cs b/src/NzbDrone.Api/DownloadClient/DownloadClientModule.cs deleted file mode 100644 index d7568189f..000000000 --- a/src/NzbDrone.Api/DownloadClient/DownloadClientModule.cs +++ /dev/null @@ -1,34 +0,0 @@ -using NzbDrone.Core.Download; - -namespace NzbDrone.Api.DownloadClient -{ - public class DownloadClientModule : ProviderModuleBase - { - public DownloadClientModule(IDownloadClientFactory downloadClientFactory) - : base(downloadClientFactory, "downloadclient") - { - } - - protected override void MapToResource(DownloadClientResource resource, DownloadClientDefinition definition) - { - base.MapToResource(resource, definition); - - resource.Enable = definition.Enable; - resource.Protocol = definition.Protocol; - } - - protected override void MapToModel(DownloadClientDefinition definition, DownloadClientResource resource) - { - base.MapToModel(definition, resource); - - definition.Enable = resource.Enable; - definition.Protocol = resource.Protocol; - } - - protected override void Validate(DownloadClientDefinition definition, bool includeWarnings) - { - if (!definition.Enable) return; - base.Validate(definition, includeWarnings); - } - } -} \ No newline at end of file diff --git a/src/NzbDrone.Api/DownloadClient/DownloadClientResource.cs b/src/NzbDrone.Api/DownloadClient/DownloadClientResource.cs deleted file mode 100644 index a7156e08d..000000000 --- a/src/NzbDrone.Api/DownloadClient/DownloadClientResource.cs +++ /dev/null @@ -1,10 +0,0 @@ -using NzbDrone.Core.Indexers; - -namespace NzbDrone.Api.DownloadClient -{ - public class DownloadClientResource : ProviderResource - { - public bool Enable { get; set; } - public DownloadProtocol Protocol { get; set; } - } -} \ No newline at end of file diff --git a/src/NzbDrone.Api/EpisodeFiles/EpisodeFileModule.cs b/src/NzbDrone.Api/EpisodeFiles/EpisodeFileModule.cs deleted file mode 100644 index d89a0068c..000000000 --- a/src/NzbDrone.Api/EpisodeFiles/EpisodeFileModule.cs +++ /dev/null @@ -1,98 +0,0 @@ -using System.Collections.Generic; -using System.IO; -using NLog; -using NzbDrone.Api.REST; -using NzbDrone.Common.Disk; -using NzbDrone.Common.Extensions; -using NzbDrone.Core.Datastore.Events; -using NzbDrone.Core.MediaFiles; -using NzbDrone.Core.MediaFiles.Events; -using NzbDrone.Core.Messaging.Events; -using NzbDrone.Core.Tv; -using NzbDrone.Core.DecisionEngine; -using NzbDrone.SignalR; -using System; - -namespace NzbDrone.Api.EpisodeFiles -{ - public class EpisodeFileModule : NzbDroneRestModuleWithSignalR, - IHandle - { - private readonly IMediaFileService _mediaFileService; - private readonly IDiskProvider _diskProvider; - private readonly IRecycleBinProvider _recycleBinProvider; - private readonly ISeriesService _seriesService; - private readonly IQualityUpgradableSpecification _qualityUpgradableSpecification; - private readonly Logger _logger; - - public EpisodeFileModule(IBroadcastSignalRMessage signalRBroadcaster, - IMediaFileService mediaFileService, - IDiskProvider diskProvider, - IRecycleBinProvider recycleBinProvider, - ISeriesService seriesService, - IQualityUpgradableSpecification qualityUpgradableSpecification, - Logger logger) - : base(signalRBroadcaster) - { - _mediaFileService = mediaFileService; - _diskProvider = diskProvider; - _recycleBinProvider = recycleBinProvider; - _seriesService = seriesService; - _qualityUpgradableSpecification = qualityUpgradableSpecification; - _logger = logger; - GetResourceById = GetEpisodeFile; - GetResourceAll = GetEpisodeFiles; - UpdateResource = SetQuality; - DeleteResource = DeleteEpisodeFile; - } - - private EpisodeFileResource GetEpisodeFile(int id) - { - throw new NotImplementedException(); - //var episodeFile = _mediaFileService.Get(id); - //var series = _seriesService.GetSeries(episodeFile.SeriesId); - - //return episodeFile.ToResource(series, _qualityUpgradableSpecification); - } - - private List GetEpisodeFiles() - { - throw new NotImplementedException(); - //if (!Request.Query.SeriesId.HasValue) - //{ - // throw new BadRequestException("seriesId is missing"); - //} - - //var seriesId = (int)Request.Query.SeriesId; - - //var series = _seriesService.GetSeries(seriesId); - - //return _mediaFileService.GetFilesBySeries(seriesId).ConvertAll(f => f.ToResource(series, _qualityUpgradableSpecification)); - } - - private void SetQuality(EpisodeFileResource episodeFileResource) - { - var episodeFile = _mediaFileService.Get(episodeFileResource.Id); - episodeFile.Quality = episodeFileResource.Quality; - _mediaFileService.Update(episodeFile); - } - - private void DeleteEpisodeFile(int id) - { - throw new NotImplementedException(); - //var episodeFile = _mediaFileService.Get(id); - //var series = _seriesService.GetSeries(episodeFile.SeriesId); - //var fullPath = Path.Combine(series.Path, episodeFile.RelativePath); - //var subfolder = _diskProvider.GetParentFolder(series.Path).GetRelativePath(_diskProvider.GetParentFolder(fullPath)); - - //_logger.Info("Deleting episode file: {0}", fullPath); - //_recycleBinProvider.DeleteFile(fullPath, subfolder); - //_mediaFileService.Delete(episodeFile, DeleteMediaFileReason.Manual); - } - - public void Handle(EpisodeFileAddedEvent message) - { - BroadcastResourceChange(ModelAction.Updated, message.EpisodeFile.Id); - } - } -} \ No newline at end of file diff --git a/src/NzbDrone.Api/EpisodeFiles/EpisodeFileResource.cs b/src/NzbDrone.Api/EpisodeFiles/EpisodeFileResource.cs deleted file mode 100644 index bd856776d..000000000 --- a/src/NzbDrone.Api/EpisodeFiles/EpisodeFileResource.cs +++ /dev/null @@ -1,64 +0,0 @@ -using System; -using System.IO; -using NzbDrone.Api.REST; -using NzbDrone.Core.Qualities; - -namespace NzbDrone.Api.EpisodeFiles -{ - public class EpisodeFileResource : RestResource - { - public int SeriesId { get; set; } - public int SeasonNumber { get; set; } - public string RelativePath { get; set; } - public string Path { get; set; } - public long Size { get; set; } - public DateTime DateAdded { get; set; } - public string SceneName { get; set; } - public QualityModel Quality { get; set; } - - public bool QualityCutoffNotMet { get; set; } - } - - public static class EpisodeFileResourceMapper - { - private static EpisodeFileResource ToResource(this Core.MediaFiles.EpisodeFile model) - { - if (model == null) return null; - - return new EpisodeFileResource - { - Id = model.Id, - - SeriesId = model.SeriesId, - SeasonNumber = model.SeasonNumber, - RelativePath = model.RelativePath, - //Path - Size = model.Size, - DateAdded = model.DateAdded, - SceneName = model.SceneName, - Quality = model.Quality, - //QualityCutoffNotMet - }; - } - - public static EpisodeFileResource ToResource(this Core.MediaFiles.EpisodeFile model, Core.Tv.Series series, Core.DecisionEngine.IQualityUpgradableSpecification qualityUpgradableSpecification) - { - if (model == null) return null; - - return new EpisodeFileResource - { - Id = model.Id, - - SeriesId = model.SeriesId, - SeasonNumber = model.SeasonNumber, - RelativePath = model.RelativePath, - Path = Path.Combine(series.Path, model.RelativePath), - Size = model.Size, - DateAdded = model.DateAdded, - SceneName = model.SceneName, - Quality = model.Quality, - QualityCutoffNotMet = qualityUpgradableSpecification.CutoffNotMet(series.Profile.Value, model.Quality) - }; - } - } -} diff --git a/src/NzbDrone.Api/Episodes/EpisodeModule.cs b/src/NzbDrone.Api/Episodes/EpisodeModule.cs deleted file mode 100644 index 7f6f5692c..000000000 --- a/src/NzbDrone.Api/Episodes/EpisodeModule.cs +++ /dev/null @@ -1,40 +0,0 @@ -using System.Collections.Generic; -using NzbDrone.Api.REST; -using NzbDrone.Core.Tv; -using NzbDrone.Core.DecisionEngine; -using NzbDrone.SignalR; - -namespace NzbDrone.Api.Episodes -{ - public class EpisodeModule : EpisodeModuleWithSignalR - { - public EpisodeModule(ISeriesService seriesService, - IEpisodeService episodeService, - IQualityUpgradableSpecification qualityUpgradableSpecification, - IBroadcastSignalRMessage signalRBroadcaster) - : base(episodeService, seriesService, qualityUpgradableSpecification, signalRBroadcaster) - { - GetResourceAll = GetEpisodes; - UpdateResource = SetMonitored; - } - - private List GetEpisodes() - { - if (!Request.Query.SeriesId.HasValue) - { - throw new BadRequestException("seriesId is missing"); - } - - var seriesId = (int)Request.Query.SeriesId; - - var resources = MapToResource(_episodeService.GetEpisodeBySeries(seriesId), false, true); - - return resources; - } - - private void SetMonitored(EpisodeResource episodeResource) - { - _episodeService.SetEpisodeMonitored(episodeResource.Id, episodeResource.Monitored); - } - } -} diff --git a/src/NzbDrone.Api/Episodes/EpisodeModuleWithSignalR.cs b/src/NzbDrone.Api/Episodes/EpisodeModuleWithSignalR.cs deleted file mode 100644 index d4c1deb27..000000000 --- a/src/NzbDrone.Api/Episodes/EpisodeModuleWithSignalR.cs +++ /dev/null @@ -1,126 +0,0 @@ -using System.Collections.Generic; -using NzbDrone.Common.Extensions; -using NzbDrone.Api.EpisodeFiles; -using NzbDrone.Api.Series; -using NzbDrone.Core.Datastore.Events; -using NzbDrone.Core.DecisionEngine; -using NzbDrone.Core.Download; -using NzbDrone.Core.MediaFiles.Events; -using NzbDrone.Core.Messaging.Events; -using NzbDrone.Core.Tv; -using NzbDrone.SignalR; - -namespace NzbDrone.Api.Episodes -{ - public abstract class EpisodeModuleWithSignalR : NzbDroneRestModuleWithSignalR, - IHandle, - IHandle - { - protected readonly IEpisodeService _episodeService; - protected readonly ISeriesService _seriesService; - protected readonly IQualityUpgradableSpecification _qualityUpgradableSpecification; - - protected EpisodeModuleWithSignalR(IEpisodeService episodeService, - ISeriesService seriesService, - IQualityUpgradableSpecification qualityUpgradableSpecification, - IBroadcastSignalRMessage signalRBroadcaster) - : base(signalRBroadcaster) - { - _episodeService = episodeService; - _seriesService = seriesService; - _qualityUpgradableSpecification = qualityUpgradableSpecification; - - GetResourceById = GetEpisode; - } - - protected EpisodeModuleWithSignalR(IEpisodeService episodeService, - ISeriesService seriesService, - IQualityUpgradableSpecification qualityUpgradableSpecification, - IBroadcastSignalRMessage signalRBroadcaster, - string resource) - : base(signalRBroadcaster, resource) - { - _episodeService = episodeService; - _seriesService = seriesService; - _qualityUpgradableSpecification = qualityUpgradableSpecification; - - GetResourceById = GetEpisode; - } - - protected EpisodeResource GetEpisode(int id) - { - var episode = _episodeService.GetEpisode(id); - var resource = MapToResource(episode, true, true); - return resource; - } - - protected EpisodeResource MapToResource(Episode episode, bool includeSeries, bool includeEpisodeFile) - { - var resource = episode.ToResource(); - - if (includeSeries || includeEpisodeFile) - { - var series = episode.Series ?? _seriesService.GetSeries(episode.SeriesId); - - if (includeSeries) - { - resource.Series = series.ToResource(); - } - if (includeEpisodeFile && episode.EpisodeFileId != 0) - { - resource.EpisodeFile = episode.EpisodeFile.Value.ToResource(series, _qualityUpgradableSpecification); - } - } - - return resource; - } - - protected List MapToResource(List episodes, bool includeSeries, bool includeEpisodeFile) - { - var result = episodes.ToResource(); - - if (includeSeries || includeEpisodeFile) - { - var seriesDict = new Dictionary(); - for (var i = 0; i < episodes.Count; i++) - { - var episode = episodes[i]; - var resource = result[i]; - - var series = episode.Series ?? seriesDict.GetValueOrDefault(episodes[i].SeriesId) ?? _seriesService.GetSeries(episodes[i].SeriesId); - seriesDict[series.Id] = series; - - if (includeSeries) - { - resource.Series = series.ToResource(); - } - if (includeEpisodeFile && episodes[i].EpisodeFileId != 0) - { - resource.EpisodeFile = episodes[i].EpisodeFile.Value.ToResource(series, _qualityUpgradableSpecification); - } - } - } - - return result; - } - - public void Handle(EpisodeGrabbedEvent message) - { - foreach (var episode in message.Episode.Episodes) - { - var resource = episode.ToResource(); - resource.Grabbed = true; - - BroadcastResourceChange(ModelAction.Updated, resource); - } - } - - public void Handle(EpisodeDownloadedEvent message) - { - foreach (var episode in message.Episode.Episodes) - { - BroadcastResourceChange(ModelAction.Updated, episode.Id); - } - } - } -} diff --git a/src/NzbDrone.Api/Episodes/EpisodeResource.cs b/src/NzbDrone.Api/Episodes/EpisodeResource.cs deleted file mode 100644 index 3ff489f38..000000000 --- a/src/NzbDrone.Api/Episodes/EpisodeResource.cs +++ /dev/null @@ -1,78 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Newtonsoft.Json; -using NzbDrone.Api.EpisodeFiles; -using NzbDrone.Api.REST; -using NzbDrone.Api.Series; -using NzbDrone.Core.Tv; - -namespace NzbDrone.Api.Episodes -{ - public class EpisodeResource : RestResource - { - public int SeriesId { get; set; } - public int EpisodeFileId { get; set; } - public int SeasonNumber { get; set; } - public int EpisodeNumber { get; set; } - public string Title { get; set; } - public string AirDate { get; set; } - public DateTime? AirDateUtc { get; set; } - public string Overview { get; set; } - public EpisodeFileResource EpisodeFile { get; set; } - - public bool HasFile { get; set; } - public bool Monitored { get; set; } - public int? AbsoluteEpisodeNumber { get; set; } - public int? SceneAbsoluteEpisodeNumber { get; set; } - public int? SceneEpisodeNumber { get; set; } - public int? SceneSeasonNumber { get; set; } - public bool UnverifiedSceneNumbering { get; set; } - public string SeriesTitle { get; set; } - public SeriesResource Series { get; set; } - - //Hiding this so people don't think its usable (only used to set the initial state) - [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] - public bool Grabbed { get; set; } - } - - public static class EpisodeResourceMapper - { - public static EpisodeResource ToResource(this Episode model) - { - if (model == null) return null; - - return new EpisodeResource - { - Id = model.Id, - - SeriesId = model.SeriesId, - EpisodeFileId = model.EpisodeFileId, - SeasonNumber = model.SeasonNumber, - EpisodeNumber = model.EpisodeNumber, - Title = model.Title, - AirDate = model.AirDate, - AirDateUtc = model.AirDateUtc, - Overview = model.Overview, - //EpisodeFile - - HasFile = model.HasFile, - Monitored = model.Monitored, - AbsoluteEpisodeNumber = model.AbsoluteEpisodeNumber, - SceneAbsoluteEpisodeNumber = model.SceneAbsoluteEpisodeNumber, - SceneEpisodeNumber = model.SceneEpisodeNumber, - SceneSeasonNumber = model.SceneSeasonNumber, - UnverifiedSceneNumbering = model.UnverifiedSceneNumbering, - SeriesTitle = model.SeriesTitle, - //Series = model.Series.MapToResource(), - }; - } - - public static List ToResource(this IEnumerable models) - { - if (models == null) return null; - - return models.Select(ToResource).ToList(); - } - } -} diff --git a/src/NzbDrone.Api/ErrorManagement/NzbDroneErrorPipeline.cs b/src/NzbDrone.Api/ErrorManagement/NzbDroneErrorPipeline.cs deleted file mode 100644 index d98925f8e..000000000 --- a/src/NzbDrone.Api/ErrorManagement/NzbDroneErrorPipeline.cs +++ /dev/null @@ -1,78 +0,0 @@ -using System; -using System.Data.SQLite; -using FluentValidation; -using NLog; -using Nancy; -using NzbDrone.Api.Extensions; -using NzbDrone.Core.Exceptions; -using HttpStatusCode = Nancy.HttpStatusCode; - -namespace NzbDrone.Api.ErrorManagement -{ - public class NzbDroneErrorPipeline - { - private readonly Logger _logger; - - public NzbDroneErrorPipeline(Logger logger) - { - _logger = logger; - } - - public Response HandleException(NancyContext context, Exception exception) - { - _logger.Trace("Handling Exception"); - - var apiException = exception as ApiException; - - if (apiException != null) - { - _logger.Warn(apiException, "API Error"); - return apiException.ToErrorResponse(); - } - - var validationException = exception as ValidationException; - - if (validationException != null) - { - _logger.Warn("Invalid request {0}", validationException.Message); - - return validationException.Errors.AsResponse(HttpStatusCode.BadRequest); - } - - var clientException = exception as NzbDroneClientException; - - if (clientException != null) - { - return new ErrorModel - { - Message = exception.Message, - Description = exception.ToString() - }.AsResponse((HttpStatusCode)clientException.StatusCode); - } - - var sqLiteException = exception as SQLiteException; - - if (sqLiteException != null) - { - if (context.Request.Method == "PUT" || context.Request.Method == "POST") - { - if (sqLiteException.Message.Contains("constraint failed")) - return new ErrorModel - { - Message = exception.Message, - }.AsResponse(HttpStatusCode.Conflict); - } - - _logger.Error(sqLiteException, "[{0} {1}]", context.Request.Method, context.Request.Path); - } - - _logger.Fatal(exception, "Request Failed. {0} {1}", context.Request.Method, context.Request.Path); - - return new ErrorModel - { - Message = exception.Message, - Description = exception.ToString() - }.AsResponse(HttpStatusCode.InternalServerError); - } - } -} \ No newline at end of file diff --git a/src/NzbDrone.Api/Extensions/Pipelines/NzbDroneVersionPipeline.cs b/src/NzbDrone.Api/Extensions/Pipelines/NzbDroneVersionPipeline.cs deleted file mode 100644 index 00488657b..000000000 --- a/src/NzbDrone.Api/Extensions/Pipelines/NzbDroneVersionPipeline.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System; -using Nancy; -using Nancy.Bootstrapper; -using NzbDrone.Common.EnvironmentInfo; - -namespace NzbDrone.Api.Extensions.Pipelines -{ - public class NzbDroneVersionPipeline : IRegisterNancyPipeline - { - public int Order => 0; - - public void Register(IPipelines pipelines) - { - pipelines.AfterRequest.AddItemToStartOfPipeline((Action) Handle); - } - - private void Handle(NancyContext context) - { - if (!context.Response.Headers.ContainsKey("X-ApplicationVersion")) - { - context.Response.Headers.Add("X-ApplicationVersion", BuildInfo.Version.ToString()); - } - } - } -} \ No newline at end of file diff --git a/src/NzbDrone.Api/Extensions/RequestExtensions.cs b/src/NzbDrone.Api/Extensions/RequestExtensions.cs deleted file mode 100644 index 6c112c900..000000000 --- a/src/NzbDrone.Api/Extensions/RequestExtensions.cs +++ /dev/null @@ -1,40 +0,0 @@ -using System; -using Nancy; - -namespace NzbDrone.Api.Extensions -{ - public static class RequestExtensions - { - public static bool IsApiRequest(this Request request) - { - return request.Path.StartsWith("/api/", StringComparison.InvariantCultureIgnoreCase); - } - - public static bool IsFeedRequest(this Request request) - { - return request.Path.StartsWith("/feed/", StringComparison.InvariantCultureIgnoreCase); - } - - public static bool IsSignalRRequest(this Request request) - { - return request.Path.StartsWith("/signalr/", StringComparison.InvariantCultureIgnoreCase); - } - - public static bool IsLocalRequest(this Request request) - { - return (request.UserHostAddress.Equals("localhost") || - request.UserHostAddress.Equals("127.0.0.1") || - request.UserHostAddress.Equals("::1")); - } - - public static bool IsLoginRequest(this Request request) - { - return request.Path.Equals("/login", StringComparison.InvariantCultureIgnoreCase); - } - - public static bool IsContentRequest(this Request request) - { - return request.Path.StartsWith("/Content/", StringComparison.InvariantCultureIgnoreCase); - } - } -} diff --git a/src/NzbDrone.Api/Frontend/Mappers/LoginHtmlMapper.cs b/src/NzbDrone.Api/Frontend/Mappers/LoginHtmlMapper.cs deleted file mode 100644 index 974e117f9..000000000 --- a/src/NzbDrone.Api/Frontend/Mappers/LoginHtmlMapper.cs +++ /dev/null @@ -1,90 +0,0 @@ -using System; -using System.IO; -using System.Text.RegularExpressions; -using Nancy; -using NLog; -using NzbDrone.Common.Disk; -using NzbDrone.Common.EnvironmentInfo; -using NzbDrone.Core.Configuration; - -namespace NzbDrone.Api.Frontend.Mappers -{ - public class LoginHtmlMapper : StaticResourceMapperBase - { - private readonly IDiskProvider _diskProvider; - private readonly IConfigFileProvider _configFileProvider; - private readonly Func _cacheBreakProviderFactory; - private readonly string _indexPath; - private static readonly Regex ReplaceRegex = new Regex("(?<=(?:href|src|data-main)=\").*?(?=\")", RegexOptions.Compiled | RegexOptions.IgnoreCase); - - private static string URL_BASE; - private string _generatedContent; - - public LoginHtmlMapper(IAppFolderInfo appFolderInfo, - IDiskProvider diskProvider, - IConfigFileProvider configFileProvider, - Func cacheBreakProviderFactory, - Logger logger) - : base(diskProvider, logger) - { - _diskProvider = diskProvider; - _configFileProvider = configFileProvider; - _cacheBreakProviderFactory = cacheBreakProviderFactory; - _indexPath = Path.Combine(appFolderInfo.StartUpFolder, _configFileProvider.UiFolder, "login.html"); - - URL_BASE = configFileProvider.UrlBase; - } - - public override string Map(string resourceUrl) - { - return _indexPath; - } - - public override bool CanHandle(string resourceUrl) - { - return resourceUrl.StartsWith("/login"); - } - - public override Response GetResponse(string resourceUrl) - { - var response = base.GetResponse(resourceUrl); - response.Headers["X-UA-Compatible"] = "IE=edge"; - - return response; - } - - protected override Stream GetContentStream(string filePath) - { - var text = GetLoginText(); - - var stream = new MemoryStream(); - var writer = new StreamWriter(stream); - writer.Write(text); - writer.Flush(); - stream.Position = 0; - return stream; - } - - private string GetLoginText() - { - if (RuntimeInfo.IsProduction && _generatedContent != null) - { - return _generatedContent; - } - - var text = _diskProvider.ReadAllText(_indexPath); - - var cacheBreakProvider = _cacheBreakProviderFactory(); - - text = ReplaceRegex.Replace(text, match => - { - var url = cacheBreakProvider.AddCacheBreakerToPath(match.Value); - return URL_BASE + url; - }); - - _generatedContent = text; - - return _generatedContent; - } - } -} diff --git a/src/NzbDrone.Api/History/HistoryModule.cs b/src/NzbDrone.Api/History/HistoryModule.cs deleted file mode 100644 index 8d2c8bb21..000000000 --- a/src/NzbDrone.Api/History/HistoryModule.cs +++ /dev/null @@ -1,76 +0,0 @@ -using System; -using Nancy; -using NzbDrone.Api.Episodes; -using NzbDrone.Api.Albums; -using NzbDrone.Api.Extensions; -using NzbDrone.Api.Series; -using NzbDrone.Api.Music; -using NzbDrone.Core.Datastore; -using NzbDrone.Core.DecisionEngine; -using NzbDrone.Core.Download; -using NzbDrone.Core.History; - -namespace NzbDrone.Api.History -{ - public class HistoryModule : NzbDroneRestModule - { - private readonly IHistoryService _historyService; - private readonly IQualityUpgradableSpecification _qualityUpgradableSpecification; - private readonly IFailedDownloadService _failedDownloadService; - - public HistoryModule(IHistoryService historyService, - IQualityUpgradableSpecification qualityUpgradableSpecification, - IFailedDownloadService failedDownloadService) - { - _historyService = historyService; - _qualityUpgradableSpecification = qualityUpgradableSpecification; - _failedDownloadService = failedDownloadService; - GetResourcePaged = GetHistory; - - Post["/failed"] = x => MarkAsFailed(); - } - - protected HistoryResource MapToResource(Core.History.History model) - { - var resource = model.ToResource(); - - resource.Artist = model.Artist.ToResource(); - resource.Album = model.Album.ToResource(); - - if (model.Artist != null) - { - resource.QualityCutoffNotMet = _qualityUpgradableSpecification.CutoffNotMet(model.Artist.Profile.Value, model.Quality); - } - - return resource; - } - - private PagingResource GetHistory(PagingResource pagingResource) - { - var albumId = Request.Query.AlbumId; - - var pagingSpec = pagingResource.MapToPagingSpec("date", SortDirection.Descending); - - if (pagingResource.FilterKey == "eventType") - { - var filterValue = (HistoryEventType)Convert.ToInt32(pagingResource.FilterValue); - pagingSpec.FilterExpression = v => v.EventType == filterValue; - } - - if (albumId.HasValue) - { - int i = (int)albumId; - pagingSpec.FilterExpression = h => h.AlbumId == i; - } - - return ApplyToPage(_historyService.Paged, pagingSpec, MapToResource); - } - - private Response MarkAsFailed() - { - var id = (int)Request.Form.Id; - _failedDownloadService.MarkAsFailed(id); - return new object().AsResponse(); - } - } -} diff --git a/src/NzbDrone.Api/History/HistoryResource.cs b/src/NzbDrone.Api/History/HistoryResource.cs deleted file mode 100644 index 1279641be..000000000 --- a/src/NzbDrone.Api/History/HistoryResource.cs +++ /dev/null @@ -1,58 +0,0 @@ -using System; -using System.Collections.Generic; -using NzbDrone.Api.Episodes; -using NzbDrone.Api.Albums; -using NzbDrone.Api.REST; -using NzbDrone.Api.Series; -using NzbDrone.Api.Music; -using NzbDrone.Core.History; -using NzbDrone.Core.Qualities; - - -namespace NzbDrone.Api.History -{ - public class HistoryResource : RestResource - { - public int ArtistId { get; set; } - public int AlbumId { get; set; } - public string SourceTitle { get; set; } - public QualityModel Quality { get; set; } - public bool QualityCutoffNotMet { get; set; } - public DateTime Date { get; set; } - public string DownloadId { get; set; } - - public HistoryEventType EventType { get; set; } - - public Dictionary Data { get; set; } - - public AlbumResource Album { get; set; } - public ArtistResource Artist { get; set; } - } - - public static class HistoryResourceMapper - { - public static HistoryResource ToResource(this Core.History.History model) - { - if (model == null) return null; - - return new HistoryResource - { - Id = model.Id, - - AlbumId = model.AlbumId, - ArtistId = model.ArtistId, - SourceTitle = model.SourceTitle, - Quality = model.Quality, - //QualityCutoffNotMet - Date = model.Date, - DownloadId = model.DownloadId, - - EventType = model.EventType, - - Data = model.Data - //Episode - //Series - }; - } - } -} diff --git a/src/NzbDrone.Api/Indexers/IndexerModule.cs b/src/NzbDrone.Api/Indexers/IndexerModule.cs deleted file mode 100644 index c66fa7db6..000000000 --- a/src/NzbDrone.Api/Indexers/IndexerModule.cs +++ /dev/null @@ -1,37 +0,0 @@ -using NzbDrone.Core.Indexers; - -namespace NzbDrone.Api.Indexers -{ - public class IndexerModule : ProviderModuleBase - { - public IndexerModule(IndexerFactory indexerFactory) - : base(indexerFactory, "indexer") - { - } - - protected override void MapToResource(IndexerResource resource, IndexerDefinition definition) - { - base.MapToResource(resource, definition); - - resource.EnableRss = definition.EnableRss; - resource.EnableSearch = definition.EnableSearch; - resource.SupportsRss = definition.SupportsRss; - resource.SupportsSearch = definition.SupportsSearch; - resource.Protocol = definition.Protocol; - } - - protected override void MapToModel(IndexerDefinition definition, IndexerResource resource) - { - base.MapToModel(definition, resource); - - definition.EnableRss = resource.EnableRss; - definition.EnableSearch = resource.EnableSearch; - } - - protected override void Validate(IndexerDefinition definition, bool includeWarnings) - { - if (!definition.Enable) return; - base.Validate(definition, includeWarnings); - } - } -} \ No newline at end of file diff --git a/src/NzbDrone.Api/Indexers/IndexerResource.cs b/src/NzbDrone.Api/Indexers/IndexerResource.cs deleted file mode 100644 index 26bb27cb9..000000000 --- a/src/NzbDrone.Api/Indexers/IndexerResource.cs +++ /dev/null @@ -1,13 +0,0 @@ -using NzbDrone.Core.Indexers; - -namespace NzbDrone.Api.Indexers -{ - public class IndexerResource : ProviderResource - { - public bool EnableRss { get; set; } - public bool EnableSearch { get; set; } - public bool SupportsRss { get; set; } - public bool SupportsSearch { get; set; } - public DownloadProtocol Protocol { get; set; } - } -} \ No newline at end of file diff --git a/src/NzbDrone.Api/ManualImport/ManualImportResource.cs b/src/NzbDrone.Api/ManualImport/ManualImportResource.cs deleted file mode 100644 index 99aeb0897..000000000 --- a/src/NzbDrone.Api/ManualImport/ManualImportResource.cs +++ /dev/null @@ -1,56 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using NzbDrone.Api.Episodes; -using NzbDrone.Api.REST; -using NzbDrone.Api.Series; -using NzbDrone.Common.Crypto; -using NzbDrone.Core.DecisionEngine; -using NzbDrone.Core.Qualities; - -namespace NzbDrone.Api.ManualImport -{ - public class ManualImportResource : RestResource - { - public string Path { get; set; } - public string RelativePath { get; set; } - public string Name { get; set; } - public long Size { get; set; } - public SeriesResource Series { get; set; } - public int? SeasonNumber { get; set; } - public List Episodes { get; set; } - public QualityModel Quality { get; set; } - public int QualityWeight { get; set; } - public string DownloadId { get; set; } - public IEnumerable Rejections { get; set; } - } - - public static class ManualImportResourceMapper - { - public static ManualImportResource ToResource(this Core.MediaFiles.TrackImport.Manual.ManualImportItem model) - { - if (model == null) return null; - - return new ManualImportResource - { - Id = HashConverter.GetHashInt31(model.Path), - - Path = model.Path, - RelativePath = model.RelativePath, - Name = model.Name, - Size = model.Size, - Series = model.Series.ToResource(), - SeasonNumber = model.SeasonNumber, - Episodes = model.Episodes.ToResource(), - Quality = model.Quality, - //QualityWeight - DownloadId = model.DownloadId, - Rejections = model.Rejections - }; - } - - public static List ToResource(this IEnumerable models) - { - return models.Select(ToResource).ToList(); - } - } -} diff --git a/src/NzbDrone.Api/Metadata/MetadataModule.cs b/src/NzbDrone.Api/Metadata/MetadataModule.cs deleted file mode 100644 index ab88ab044..000000000 --- a/src/NzbDrone.Api/Metadata/MetadataModule.cs +++ /dev/null @@ -1,32 +0,0 @@ -using NzbDrone.Core.Extras.Metadata; - -namespace NzbDrone.Api.Metadata -{ - public class MetadataModule : ProviderModuleBase - { - public MetadataModule(IMetadataFactory metadataFactory) - : base(metadataFactory, "metadata") - { - } - - protected override void MapToResource(MetadataResource resource, MetadataDefinition definition) - { - base.MapToResource(resource, definition); - - resource.Enable = definition.Enable; - } - - protected override void MapToModel(MetadataDefinition definition, MetadataResource resource) - { - base.MapToModel(definition, resource); - - definition.Enable = resource.Enable; - } - - protected override void Validate(MetadataDefinition definition, bool includeWarnings) - { - if (!definition.Enable) return; - base.Validate(definition, includeWarnings); - } - } -} \ No newline at end of file diff --git a/src/NzbDrone.Api/Metadata/MetadataResource.cs b/src/NzbDrone.Api/Metadata/MetadataResource.cs deleted file mode 100644 index fa9f58b64..000000000 --- a/src/NzbDrone.Api/Metadata/MetadataResource.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace NzbDrone.Api.Metadata -{ - public class MetadataResource : ProviderResource - { - public bool Enable { get; set; } - } -} \ No newline at end of file diff --git a/src/NzbDrone.Api/Music/ArtistBulkImportModule.cs b/src/NzbDrone.Api/Music/ArtistBulkImportModule.cs deleted file mode 100644 index c2d92aae7..000000000 --- a/src/NzbDrone.Api/Music/ArtistBulkImportModule.cs +++ /dev/null @@ -1,152 +0,0 @@ -using System.Collections; -using System.Collections.Generic; -using Nancy; -using NzbDrone.Api.REST; -using NzbDrone.Api.Extensions; -using NzbDrone.Core.MediaCover; -using NzbDrone.Core.MetadataSource; -using NzbDrone.Core.Parser; -using System.Linq; -using System; -using Marr.Data; -using NzbDrone.Common.Extensions; -using NzbDrone.Core.Datastore; -using NzbDrone.Core.MediaFiles; -using NzbDrone.Core.MediaFiles.TrackImport; -using NzbDrone.Core.RootFolders; -using NzbDrone.Common.Cache; -using NzbDrone.Core.Music; - -namespace NzbDrone.Api.Music -{ - - public class UnmappedComparer : IComparer - { - public int Compare(UnmappedFolder a, UnmappedFolder b) - { - return a.Name.CompareTo(b.Name); - } - } - - public class MusicBulkImportModule : NzbDroneRestModule - { - private readonly ISearchForNewArtist _searchProxy; - private readonly IRootFolderService _rootFolderService; - private readonly IMakeImportDecision _importDecisionMaker; - private readonly IDiskScanService _diskScanService; - private readonly ICached _mappedArtists; - private readonly IArtistService _artistService; - - public MusicBulkImportModule(ISearchForNewArtist searchProxy, - IRootFolderService rootFolderService, - IMakeImportDecision importDecisionMaker, - IDiskScanService diskScanService, - ICacheManager cacheManager, - IArtistService artistService - ) - : base("/artist/bulkimport") - { - _searchProxy = searchProxy; - _rootFolderService = rootFolderService; - _importDecisionMaker = importDecisionMaker; - _diskScanService = diskScanService; - _mappedArtists = cacheManager.GetCache(GetType(), "mappedArtistsCache"); - _artistService = artistService; - Get["/"] = x => Search(); - } - - - private Response Search() - { - if (Request.Query.Id == 0) - { - throw new BadRequestException("Invalid Query"); - } - - RootFolder rootFolder = _rootFolderService.Get(Request.Query.Id); - - var unmapped = rootFolder.UnmappedFolders.OrderBy(f => f.Name).ToList(); - - var paged = unmapped; - - var mapped = paged.Select(page => - { - Artist m = null; - - var mappedArtist = _mappedArtists.Find(page.Name); - - if (mappedArtist != null) - { - return mappedArtist; - } - - var files = _diskScanService.GetAudioFiles(page.Path); - - // Check for music files in directory - if (files.Count() == 0) - { - return null; - } - - var parsedTitle = Parser.ParseMusicPath(files.FirstOrDefault()); - if (parsedTitle == null || parsedTitle.ArtistTitle == null) - { - m = new Artist - { - Name = page.Name.Replace(".", " ").Replace("-", " "), - Path = page.Path, - }; - } - else - { - m = new Artist - { - Name = parsedTitle.ArtistTitle, - Path = page.Path - }; - } - - var searchResults = _searchProxy.SearchForNewArtist(m.Name); - - if (searchResults == null || searchResults.Count == 0) - { - return null; - }; - - mappedArtist = searchResults.First(); - - if (mappedArtist != null) - { - mappedArtist.Monitored = true; - mappedArtist.Path = page.Path; - - _mappedArtists.Set(page.Name, mappedArtist, TimeSpan.FromDays(2)); - - return mappedArtist; - } - - return null; - }); - - var mapping = MapToResource(mapped.Where(m => m != null)).ToList().AsResponse(); - - return mapping; - } - - - private static IEnumerable MapToResource(IEnumerable artists) - { - foreach (var currentArtist in artists) - { - var resource = currentArtist.ToResource(); - var poster = currentArtist.Images.FirstOrDefault(c => c.CoverType == MediaCoverTypes.Poster); - if (poster != null) - { - resource.RemotePoster = poster.Url; - } - - yield return resource; - } - } - } -} diff --git a/src/NzbDrone.Api/Music/ArtistEditorModule.cs b/src/NzbDrone.Api/Music/ArtistEditorModule.cs deleted file mode 100644 index ca8ec3598..000000000 --- a/src/NzbDrone.Api/Music/ArtistEditorModule.cs +++ /dev/null @@ -1,31 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using Nancy; -using NzbDrone.Api.Extensions; -using NzbDrone.Core.Music; - -namespace NzbDrone.Api.Music -{ - public class ArtistEditorModule : NzbDroneApiModule - { - private readonly IArtistService _artistService; - - public ArtistEditorModule(IArtistService seriesService) - : base("/artist/editor") - { - _artistService = seriesService; - Put["/"] = artist => SaveAll(); - } - - private Response SaveAll() - { - var resources = Request.Body.FromJson>(); - - var artist = resources.Select(artistResource => artistResource.ToModel(_artistService.GetArtist(artistResource.Id))).ToList(); - - return _artistService.UpdateArtists(artist) - .ToResource() - .AsResponse(HttpStatusCode.Accepted); - } - } -} diff --git a/src/NzbDrone.Api/Music/ArtistLookupModule.cs b/src/NzbDrone.Api/Music/ArtistLookupModule.cs deleted file mode 100644 index faa0eca09..000000000 --- a/src/NzbDrone.Api/Music/ArtistLookupModule.cs +++ /dev/null @@ -1,46 +0,0 @@ -using Nancy; -using NzbDrone.Api.Extensions; -using NzbDrone.Core.MediaCover; -using NzbDrone.Core.MetadataSource; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; - -namespace NzbDrone.Api.Music -{ - public class ArtistLookupModule : NzbDroneRestModule - { - private readonly ISearchForNewArtist _searchProxy; - - public ArtistLookupModule(ISearchForNewArtist searchProxy) - : base("/artist/lookup") - { - _searchProxy = searchProxy; - Get["/"] = x => Search(); - } - - - private Response Search() - { - var iTunesResults = _searchProxy.SearchForNewArtist((string)Request.Query.term); - return MapToResource(iTunesResults).AsResponse(); - } - - - private static IEnumerable MapToResource(IEnumerable artists) - { - foreach (var currentArtist in artists) - { - var resource = currentArtist.ToResource(); - var poster = currentArtist.Images.FirstOrDefault(c => c.CoverType == MediaCoverTypes.Poster); - if (poster != null) - { - resource.RemotePoster = poster.Url; - } - - yield return resource; - } - } - } -} diff --git a/src/NzbDrone.Api/Music/ArtistModule.cs b/src/NzbDrone.Api/Music/ArtistModule.cs deleted file mode 100644 index 91ffc0f76..000000000 --- a/src/NzbDrone.Api/Music/ArtistModule.cs +++ /dev/null @@ -1,223 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using FluentValidation; -using NzbDrone.Common.Extensions; -using NzbDrone.Core.Datastore.Events; -using NzbDrone.Core.MediaCover; -using NzbDrone.Core.MediaFiles; -using NzbDrone.Core.MediaFiles.Events; -using NzbDrone.Core.Messaging.Events; -using NzbDrone.Core.Music; -using NzbDrone.Core.Music.Events; -using NzbDrone.Core.ArtistStats; -using NzbDrone.Core.Validation; -using NzbDrone.Core.Validation.Paths; -using NzbDrone.SignalR; - -namespace NzbDrone.Api.Music -{ - public class ArtistModule : NzbDroneRestModuleWithSignalR, - IHandle, - IHandle, - IHandle, - IHandle, - IHandle, - IHandle - //IHandle - { - private readonly IArtistService _artistService; - private readonly IAddArtistService _addArtistService; - private readonly IArtistStatisticsService _artistStatisticsService; - private readonly IMapCoversToLocal _coverMapper; - - public ArtistModule(IBroadcastSignalRMessage signalRBroadcaster, - IArtistService artistService, - IAddArtistService addArtistService, - IArtistStatisticsService artistStatisticsService, - IMapCoversToLocal coverMapper, - RootFolderValidator rootFolderValidator, - ArtistPathValidator seriesPathValidator, - ArtistExistsValidator artistExistsValidator, - DroneFactoryValidator droneFactoryValidator, - SeriesAncestorValidator seriesAncestorValidator, - ProfileExistsValidator profileExistsValidator - ) - : base(signalRBroadcaster) - { - _artistService = artistService; - _addArtistService = addArtistService; - _artistStatisticsService = artistStatisticsService; - - _coverMapper = coverMapper; - - GetResourceAll = AllArtist; - GetResourceById = GetArtist; - CreateResource = AddArtist; - UpdateResource = UpdatArtist; - DeleteResource = DeleteArtist; - - Validation.RuleBuilderExtensions.ValidId(SharedValidator.RuleFor(s => s.ProfileId)); - - SharedValidator.RuleFor(s => s.Path) - .Cascade(CascadeMode.StopOnFirstFailure) - .IsValidPath() - .SetValidator(rootFolderValidator) - .SetValidator(seriesPathValidator) - .SetValidator(droneFactoryValidator) - .SetValidator(seriesAncestorValidator) - .When(s => !s.Path.IsNullOrWhiteSpace()); - - SharedValidator.RuleFor(s => s.ProfileId).SetValidator(profileExistsValidator); - - PostValidator.RuleFor(s => s.Path).IsValidPath().When(s => s.RootFolderPath.IsNullOrWhiteSpace()); - PostValidator.RuleFor(s => s.RootFolderPath).IsValidPath().When(s => s.Path.IsNullOrWhiteSpace()); - PostValidator.RuleFor(s => s.ForeignArtistId).NotEqual("").SetValidator(artistExistsValidator); - - PutValidator.RuleFor(s => s.Path).IsValidPath(); - } - - private ArtistResource GetArtist(int id) - { - var artist = _artistService.GetArtist(id); - return MapToResource(artist); - } - - private ArtistResource MapToResource(Artist artist) - { - if (artist == null) return null; - - var resource = artist.ToResource(); - MapCoversToLocal(resource); - FetchAndLinkArtistStatistics(resource); - //PopulateAlternateTitles(resource); - - return resource; - } - - private List AllArtist() - { - var artistStats = _artistStatisticsService.ArtistStatistics(); - var artistResources = _artistService.GetAllArtists().ToResource(); - - MapCoversToLocal(artistResources.ToArray()); - LinkArtistStatistics(artistResources, artistStats); - //PopulateAlternateTitles(seriesResources); - - return artistResources; - } - - private int AddArtist(ArtistResource artistResource) - { - var model = artistResource.ToModel(); - - return _addArtistService.AddArtist(model).Id; - } - - private void UpdatArtist(ArtistResource artistResource) - { - var model = artistResource.ToModel(_artistService.GetArtist(artistResource.Id)); - - _artistService.UpdateArtist(model); - - BroadcastResourceChange(ModelAction.Updated, artistResource.Id); - } - - private void DeleteArtist(int id) - { - var deleteFiles = false; - var deleteFilesQuery = Request.Query.deleteFiles; - - if (deleteFilesQuery.HasValue) - { - deleteFiles = Convert.ToBoolean(deleteFilesQuery.Value); - } - - _artistService.DeleteArtist(id, deleteFiles); - } - - private void MapCoversToLocal(params ArtistResource[] artists) - { - foreach (var artistResource in artists) - { - _coverMapper.ConvertToLocalUrls(artistResource.Id, artistResource.Images); - } - } - - private void FetchAndLinkArtistStatistics(ArtistResource resource) - { - LinkArtistStatistics(resource, _artistStatisticsService.ArtistStatistics(resource.Id)); - } - - - private void LinkArtistStatistics(List resources, List artistStatistics) - { - var dictArtistStats = artistStatistics.ToDictionary(v => v.ArtistId); - - foreach (var artist in resources) - { - var stats = dictArtistStats.GetValueOrDefault(artist.Id); - if (stats == null) continue; - - LinkArtistStatistics(artist, stats); - } - } - - private void LinkArtistStatistics(ArtistResource resource, ArtistStatistics artistStatistics) - { - resource.TotalTrackCount = artistStatistics.TotalTrackCount; - resource.TrackCount = artistStatistics.TrackCount; - resource.TrackFileCount = artistStatistics.TrackFileCount; - resource.SizeOnDisk = artistStatistics.SizeOnDisk; - resource.AlbumCount = artistStatistics.AlbumCount; - - //if (artistStatistics.AlbumStatistics != null) - //{ - // var dictSeasonStats = artistStatistics.SeasonStatistics.ToDictionary(v => v.SeasonNumber); - - // foreach (var album in resource.Albums) - // { - // album.Statistics = dictSeasonStats.GetValueOrDefault(album.Id).ToResource(); - // } - //} - } - - public void Handle(TrackImportedEvent message) - { - BroadcastResourceChange(ModelAction.Updated, message.ImportedTrack.ArtistId); - } - - public void Handle(TrackFileDeletedEvent message) - { - if (message.Reason == DeleteMediaFileReason.Upgrade) return; - - BroadcastResourceChange(ModelAction.Updated, message.TrackFile.ArtistId); - } - - public void Handle(ArtistUpdatedEvent message) - { - BroadcastResourceChange(ModelAction.Updated, message.Artist.Id); - } - - public void Handle(ArtistEditedEvent message) - { - BroadcastResourceChange(ModelAction.Updated, message.Artist.Id); - } - - public void Handle(ArtistDeletedEvent message) - { - BroadcastResourceChange(ModelAction.Deleted, message.Artist.ToResource()); - } - - public void Handle(ArtistRenamedEvent message) - { - BroadcastResourceChange(ModelAction.Updated, message.Artist.Id); - } - - //public void Handle(MediaCoversUpdatedEvent message) - //{ - // BroadcastResourceChange(ModelAction.Updated, message.Artist.Id); - //} - - } -} diff --git a/src/NzbDrone.Api/Music/ArtistResource.cs b/src/NzbDrone.Api/Music/ArtistResource.cs deleted file mode 100644 index 6d9c447f5..000000000 --- a/src/NzbDrone.Api/Music/ArtistResource.cs +++ /dev/null @@ -1,187 +0,0 @@ -using NzbDrone.Api.REST; -using NzbDrone.Api.Series; -using NzbDrone.Api.Albums; -using NzbDrone.Core.MediaCover; -using NzbDrone.Core.Music; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; - -namespace NzbDrone.Api.Music -{ - public class ArtistResource : RestResource - { - public ArtistResource() - { - Monitored = true; - } - - - //View Only - public string Name { get; set; } - public string ForeignArtistId { get; set; } - public string MBId { get; set; } - public int TADBId { get; set; } - public int DiscogsId { get; set; } - public string AllMusicId { get; set; } - public string Overview { get; set; } - - public int? AlbumCount{ get; set; } - public int? TotalTrackCount { get; set; } - public int? TrackCount { get; set; } - public int? TrackFileCount { get; set; } - public long? SizeOnDisk { get; set; } - //public SeriesStatusType Status { get; set; } - - public List Images { get; set; } - public List Members { get; set; } - - public string RemotePoster { get; set; } - public List Albums { get; set; } - - - //View & Edit - public string Path { get; set; } - public int ProfileId { get; set; } - - //Editing Only - public bool AlbumFolder { get; set; } - public bool Monitored { get; set; } - - public string RootFolderPath { get; set; } - //public string Certification { get; set; } - public List Genres { get; set; } - public string CleanName { get; set; } - public string SortName { get; set; } - public HashSet Tags { get; set; } - public DateTime Added { get; set; } - public AddArtistOptions AddOptions { get; set; } - public Ratings Ratings { get; set; } - public string NameSlug { get; set; } - } - - public static class ArtistResourceMapper - { - public static ArtistResource ToResource(this Core.Music.Artist model) - { - if (model == null) return null; - - return new ArtistResource - { - Id = model.Id, - MBId = model.MBId, - TADBId = model.TADBId, - DiscogsId = model.DiscogsId, - AllMusicId = model.AMId, - Name = model.Name, - CleanName = model.CleanName, - - //AlternateTitles - SortName = model.SortName, - - //TotalTrackCount - //TrackCount - //TrackFileCount - //SizeOnDisk - //Status = resource.Status, - Overview = model.Overview, - //NextAiring - //PreviousAiring - //Network = resource.Network, - //AirTime = resource.AirTime, - Images = model.Images, - Members = model.Members, - //Albums = model.Albums.ToResource(), - //Year = resource.Year, - - Path = model.Path, - ProfileId = model.ProfileId, - - Monitored = model.Monitored, - AlbumFolder = model.AlbumFolder, - - //UseSceneNumbering = resource.UseSceneNumbering, - //Runtime = resource.Runtime, - //TvdbId = resource.TvdbId, - //TvRageId = resource.TvRageId, - //TvMazeId = resource.TvMazeId, - //FirstAired = resource.FirstAired, - //LastInfoSync = resource.LastInfoSync, - //SeriesType = resource.SeriesType, - ForeignArtistId = model.ForeignArtistId, - NameSlug = model.NameSlug, - - RootFolderPath = model.RootFolderPath, - Genres = model.Genres, - Tags = model.Tags, - Added = model.Added, - AddOptions = model.AddOptions, - Ratings = model.Ratings, - }; - } - - public static Core.Music.Artist ToModel(this ArtistResource resource) - { - if (resource == null) return null; - - return new Core.Music.Artist - { - Id = resource.Id, - - Name = resource.Name, - CleanName = resource.CleanName, - //AlternateTitles - SortName = resource.SortName, - MBId = resource.MBId, - TADBId = resource.TADBId, - DiscogsId = resource.DiscogsId, - AMId = resource.AllMusicId, //TODO change model and DB to AllMusic instead of AM - //TotalEpisodeCount - //TrackCount - //TrackFileCount - //SizeOnDisk - //Status = resource.Status, - Overview = resource.Overview, - //NextAiring - //PreviousAiring - //Network = resource.Network, - //AirTime = resource.AirTime, - Images = resource.Images, - Members = resource.Members, - //Albums = resource.Albums.ToModel(), - //Year = resource.Year, - - Path = resource.Path, - ProfileId = resource.ProfileId, - AlbumFolder = resource.AlbumFolder, - - Monitored = resource.Monitored, - //LastInfoSync = resource.LastInfoSync, - ForeignArtistId = resource.ForeignArtistId, - NameSlug = resource.NameSlug, - - RootFolderPath = resource.RootFolderPath, - Genres = resource.Genres, - Tags = resource.Tags, - Added = resource.Added, - AddOptions = resource.AddOptions, - Ratings = resource.Ratings - }; - } - - public static Core.Music.Artist ToModel(this ArtistResource resource, Core.Music.Artist artist) - { - var updatedArtist = resource.ToModel(); - - artist.ApplyChanges(updatedArtist); - - return artist; - } - - public static List ToResource(this IEnumerable artist) - { - return artist.Select(ToResource).ToList(); - } - } -} diff --git a/src/NzbDrone.Api/Music/ListImport.cs b/src/NzbDrone.Api/Music/ListImport.cs deleted file mode 100644 index 456f95243..000000000 --- a/src/NzbDrone.Api/Music/ListImport.cs +++ /dev/null @@ -1,30 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using Nancy; -using Nancy.Extensions; -using NzbDrone.Api.Extensions; -using NzbDrone.Core.Music; - -namespace NzbDrone.Api.Music -{ - public class ListImportModule : NzbDroneApiModule - { - private readonly IAddArtistService _artistService; - - public ListImportModule(IAddArtistService artistService) - : base("/artist/import") - { - _artistService = artistService; - Put["/"] = Artist => SaveAll(); - } - - private Response SaveAll() - { - var resources = Request.Body.FromJson>(); - - var Artists = resources.Select(ArtistResource => (ArtistResource.ToModel())).Where(m => m != null).DistinctBy(m => m.ForeignArtistId).ToList(); - - return _artistService.AddArtists(Artists).ToResource().AsResponse(HttpStatusCode.Accepted); - } - } -} \ No newline at end of file diff --git a/src/NzbDrone.Api/NancyBootstrapper.cs b/src/NzbDrone.Api/NancyBootstrapper.cs deleted file mode 100644 index 1415dd4c2..000000000 --- a/src/NzbDrone.Api/NancyBootstrapper.cs +++ /dev/null @@ -1,59 +0,0 @@ -using System.Linq; -using Nancy.Bootstrapper; -using Nancy.Diagnostics; -using NLog; -using NzbDrone.Api.Extensions.Pipelines; -using NzbDrone.Common.EnvironmentInfo; -using NzbDrone.Common.Instrumentation; -using NzbDrone.Core.Instrumentation; -using NzbDrone.Core.Lifecycle; -using NzbDrone.Core.Messaging.Events; -using TinyIoC; - -namespace NzbDrone.Api -{ - public class NancyBootstrapper : TinyIoCNancyBootstrapper - { - private readonly TinyIoCContainer _tinyIoCContainer; - private static readonly Logger Logger = NzbDroneLogger.GetLogger(typeof(NancyBootstrapper)); - - public NancyBootstrapper(TinyIoCContainer tinyIoCContainer) - { - _tinyIoCContainer = tinyIoCContainer; - } - - protected override void ApplicationStartup(TinyIoCContainer container, IPipelines pipelines) - { - Logger.Info("Starting Web Server"); - - if (RuntimeInfo.IsProduction) - { - DiagnosticsHook.Disable(pipelines); - } - - RegisterPipelines(pipelines); - - container.Resolve().Register(); - container.Resolve().PublishEvent(new ApplicationStartedEvent()); - } - - private void RegisterPipelines(IPipelines pipelines) - { - var pipelineRegistrars = _tinyIoCContainer.ResolveAll().OrderBy(v => v.Order).ToList(); - - foreach (var registerNancyPipeline in pipelineRegistrars) - { - registerNancyPipeline.Register(pipelines); - } - } - - protected override TinyIoCContainer GetApplicationContainer() - { - return _tinyIoCContainer; - } - - protected override DiagnosticsConfiguration DiagnosticsConfiguration => new DiagnosticsConfiguration { Password = @"password" }; - - protected override byte[] FavIcon => null; - } -} \ No newline at end of file diff --git a/src/NzbDrone.Api/Notifications/NotificationModule.cs b/src/NzbDrone.Api/Notifications/NotificationModule.cs deleted file mode 100644 index 88f42a043..000000000 --- a/src/NzbDrone.Api/Notifications/NotificationModule.cs +++ /dev/null @@ -1,48 +0,0 @@ -using NzbDrone.Core.Notifications; - -namespace NzbDrone.Api.Notifications -{ - public class NotificationModule : ProviderModuleBase - { - public NotificationModule(NotificationFactory notificationFactory) - : base(notificationFactory, "notification") - { - } - - protected override void MapToResource(NotificationResource resource, NotificationDefinition definition) - { - base.MapToResource(resource, definition); - - resource.OnGrab = definition.OnGrab; - resource.OnDownload = definition.OnDownload; - resource.OnUpgrade = definition.OnUpgrade; - resource.OnRename = definition.OnRename; - resource.SupportsOnGrab = definition.SupportsOnGrab; - resource.SupportsOnDownload = definition.SupportsOnDownload; - resource.SupportsOnUpgrade = definition.SupportsOnUpgrade; - resource.SupportsOnRename = definition.SupportsOnRename; - resource.Tags = definition.Tags; - } - - protected override void MapToModel(NotificationDefinition definition, NotificationResource resource) - { - base.MapToModel(definition, resource); - - definition.OnGrab = resource.OnGrab; - definition.OnDownload = resource.OnDownload; - definition.OnUpgrade = resource.OnUpgrade; - definition.OnRename = resource.OnRename; - definition.SupportsOnGrab = resource.SupportsOnGrab; - definition.SupportsOnDownload = resource.SupportsOnDownload; - definition.SupportsOnUpgrade = resource.SupportsOnUpgrade; - definition.SupportsOnRename = resource.SupportsOnRename; - definition.Tags = resource.Tags; - } - - protected override void Validate(NotificationDefinition definition, bool includeWarnings) - { - if (!definition.OnGrab && !definition.OnDownload) return; - base.Validate(definition, includeWarnings); - } - } -} \ No newline at end of file diff --git a/src/NzbDrone.Api/Notifications/NotificationResource.cs b/src/NzbDrone.Api/Notifications/NotificationResource.cs deleted file mode 100644 index f3fa11327..000000000 --- a/src/NzbDrone.Api/Notifications/NotificationResource.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System.Collections.Generic; - -namespace NzbDrone.Api.Notifications -{ - public class NotificationResource : ProviderResource - { - public bool OnGrab { get; set; } - public bool OnDownload { get; set; } - public bool OnUpgrade { get; set; } - public bool OnRename { get; set; } - public bool SupportsOnGrab { get; set; } - public bool SupportsOnDownload { get; set; } - public bool SupportsOnUpgrade { get; set; } - public bool SupportsOnRename { get; set; } - public HashSet Tags { get; set; } - } -} \ No newline at end of file diff --git a/src/NzbDrone.Api/NzbDrone.Api.csproj b/src/NzbDrone.Api/NzbDrone.Api.csproj deleted file mode 100644 index 47bed9e5e..000000000 --- a/src/NzbDrone.Api/NzbDrone.Api.csproj +++ /dev/null @@ -1,300 +0,0 @@ - - - - - Debug - x86 - {FD286DF8-2D3A-4394-8AD5-443FADE55FB2} - Library - Properties - NzbDrone.Api - NzbDrone.Api - v4.0 - 512 - ..\ - true - - - 12.0.0 - 2.0 - - - true - ..\..\_output\ - DEBUG;TRACE - full - x86 - prompt - MinimumRecommendedRules.ruleset - 4 - false - - - ..\..\_output\ - TRACE - true - pdbonly - x86 - prompt - MinimumRecommendedRules.ruleset - 4 - - - - ..\packages\Ical.Net.2.2.32\lib\net40\antlr.runtime.dll - - - ..\packages\FluentValidation.6.2.1.0\lib\portable-net40+sl50+wp80+win8+wpa81\FluentValidation.dll - True - - - ..\packages\Ical.Net.2.2.32\lib\net40\Ical.Net.dll - - - ..\packages\Ical.Net.2.2.32\lib\net40\Ical.Net.Collections.dll - - - ..\packages\Nancy.1.4.3\lib\net40\Nancy.dll - True - - - ..\packages\Nancy.Authentication.Basic.1.4.1\lib\net40\Nancy.Authentication.Basic.dll - True - - - ..\packages\Nancy.Authentication.Forms.1.4.1\lib\net40\Nancy.Authentication.Forms.dll - True - - - ..\packages\Newtonsoft.Json.9.0.1\lib\net40\Newtonsoft.Json.dll - True - - - ..\packages\NLog.4.4.3\lib\net40\NLog.dll - - - ..\packages\Ical.Net.2.2.32\lib\net40\NodaTime.dll - - - - - - - False - ..\Libraries\Sqlite\System.Data.SQLite.dll - - - - - Properties\SharedAssemblyInfo.cs - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Designer - - - - - {F6FC6BE7-0847-4817-A1ED-223DC647C3D7} - Marr.Data - - - {F2BE0FDF-6E47-4827-A420-DD4EF82407F8} - NzbDrone.Common - - - {FF5EE3B6-913B-47CE-9CEB-11C51B4E1205} - NzbDrone.Core - - - {7C2CC69F-5CA0-4E5C-85CB-983F9F6C3B36} - NzbDrone.SignalR - - - - - - \ No newline at end of file diff --git a/src/NzbDrone.Api/NzbDroneApiModule.cs b/src/NzbDrone.Api/NzbDroneApiModule.cs deleted file mode 100644 index ad8131487..000000000 --- a/src/NzbDrone.Api/NzbDroneApiModule.cs +++ /dev/null @@ -1,12 +0,0 @@ -using Nancy; - -namespace NzbDrone.Api -{ - public abstract class NzbDroneApiModule : NancyModule - { - protected NzbDroneApiModule(string resource) - : base("/api/" + resource.Trim('/')) - { - } - } -} \ No newline at end of file diff --git a/src/NzbDrone.Api/NzbDroneFeedModule.cs b/src/NzbDrone.Api/NzbDroneFeedModule.cs deleted file mode 100644 index d79307bef..000000000 --- a/src/NzbDrone.Api/NzbDroneFeedModule.cs +++ /dev/null @@ -1,12 +0,0 @@ -using Nancy; - -namespace NzbDrone.Api -{ - public abstract class NzbDroneFeedModule : NancyModule - { - protected NzbDroneFeedModule(string resource) - : base("/feed/" + resource.Trim('/')) - { - } - } -} \ No newline at end of file diff --git a/src/NzbDrone.Api/NzbDroneRestModule.cs b/src/NzbDrone.Api/NzbDroneRestModule.cs deleted file mode 100644 index 4cc103d95..000000000 --- a/src/NzbDrone.Api/NzbDroneRestModule.cs +++ /dev/null @@ -1,40 +0,0 @@ -using System; -using NzbDrone.Api.REST; -using NzbDrone.Api.Validation; -using NzbDrone.Core.Datastore; - -namespace NzbDrone.Api -{ - public abstract class NzbDroneRestModule : RestModule where TResource : RestResource, new() - { - protected string Resource { get; private set; } - - protected NzbDroneRestModule() - : this(new TResource().ResourceName) - { - } - - protected NzbDroneRestModule(string resource) - : base("/api/" + resource.Trim('/')) - { - Resource = resource; - PostValidator.RuleFor(r => r.Id).IsZero(); - PutValidator.RuleFor(r => r.Id).ValidId(); - } - - protected PagingResource ApplyToPage(Func, PagingSpec> function, PagingSpec pagingSpec, Converter mapper) - { - pagingSpec = function(pagingSpec); - - return new PagingResource - { - Page = pagingSpec.Page, - PageSize = pagingSpec.PageSize, - SortDirection = pagingSpec.SortDirection, - SortKey = pagingSpec.SortKey, - TotalRecords = pagingSpec.TotalRecords, - Records = pagingSpec.Records.ConvertAll(mapper) - }; - } - } -} \ No newline at end of file diff --git a/src/NzbDrone.Api/NzbDroneRestModuleWithSignalR.cs b/src/NzbDrone.Api/NzbDroneRestModuleWithSignalR.cs deleted file mode 100644 index a2061a770..000000000 --- a/src/NzbDrone.Api/NzbDroneRestModuleWithSignalR.cs +++ /dev/null @@ -1,66 +0,0 @@ -using NzbDrone.Api.REST; -using NzbDrone.Core.Datastore; -using NzbDrone.Core.Datastore.Events; -using NzbDrone.Core.Messaging.Events; -using NzbDrone.SignalR; - -namespace NzbDrone.Api -{ - public abstract class NzbDroneRestModuleWithSignalR : NzbDroneRestModule, IHandle> - where TResource : RestResource, new() - where TModel : ModelBase, new() - { - private readonly IBroadcastSignalRMessage _signalRBroadcaster; - - protected NzbDroneRestModuleWithSignalR(IBroadcastSignalRMessage signalRBroadcaster) - { - _signalRBroadcaster = signalRBroadcaster; - } - - protected NzbDroneRestModuleWithSignalR(IBroadcastSignalRMessage signalRBroadcaster, string resource) - : base(resource) - { - _signalRBroadcaster = signalRBroadcaster; - } - - public void Handle(ModelEvent message) - { - if (message.Action == ModelAction.Deleted || message.Action == ModelAction.Sync) - { - BroadcastResourceChange(message.Action); - } - - BroadcastResourceChange(message.Action, message.Model.Id); - } - - protected void BroadcastResourceChange(ModelAction action, int id) - { - var resource = GetResourceById(id); - BroadcastResourceChange(action, resource); - } - - - protected void BroadcastResourceChange(ModelAction action, TResource resource) - { - var signalRMessage = new SignalRMessage - { - Name = Resource, - Body = new ResourceChangeMessage(resource, action) - }; - - _signalRBroadcaster.BroadcastMessage(signalRMessage); - } - - - protected void BroadcastResourceChange(ModelAction action) - { - var signalRMessage = new SignalRMessage - { - Name = Resource, - Body = new ResourceChangeMessage(action) - }; - - _signalRBroadcaster.BroadcastMessage(signalRMessage); - } - } -} \ No newline at end of file diff --git a/src/NzbDrone.Api/Parse/ParseModule.cs b/src/NzbDrone.Api/Parse/ParseModule.cs deleted file mode 100644 index 486ed8ace..000000000 --- a/src/NzbDrone.Api/Parse/ParseModule.cs +++ /dev/null @@ -1,50 +0,0 @@ -using NzbDrone.Api.Albums; -using NzbDrone.Api.Music; -using NzbDrone.Core.Parser; - -namespace NzbDrone.Api.Parse -{ - public class ParseModule : NzbDroneRestModule - { - private readonly IParsingService _parsingService; - - public ParseModule(IParsingService parsingService) - { - _parsingService = parsingService; - - GetResourceSingle = Parse; - } - - private ParseResource Parse() - { - var title = Request.Query.Title.Value as string; - var parsedAlbumInfo = Parser.ParseAlbumTitle(title); - - if (parsedAlbumInfo == null) - { - return null; - } - - var remoteAlbum = _parsingService.Map(parsedAlbumInfo); - - if (remoteAlbum != null) - { - return new ParseResource - { - Title = title, - ParsedAlbumInfo = remoteAlbum.ParsedAlbumInfo, - Artist = remoteAlbum.Artist.ToResource(), - Albums = remoteAlbum.Albums.ToResource() - }; - } - else - { - return new ParseResource - { - Title = title, - ParsedAlbumInfo = parsedAlbumInfo - }; - } - } - } -} \ No newline at end of file diff --git a/src/NzbDrone.Api/Parse/ParseResource.cs b/src/NzbDrone.Api/Parse/ParseResource.cs deleted file mode 100644 index df19e42de..000000000 --- a/src/NzbDrone.Api/Parse/ParseResource.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System.Collections.Generic; -using NzbDrone.Api.REST; -using NzbDrone.Api.Music; -using NzbDrone.Api.Albums; -using NzbDrone.Core.Parser.Model; - -namespace NzbDrone.Api.Parse -{ - public class ParseResource : RestResource - { - public string Title { get; set; } - public ParsedAlbumInfo ParsedAlbumInfo { get; set; } - public ArtistResource Artist { get; set; } - public List Albums { get; set; } - } -} \ No newline at end of file diff --git a/src/NzbDrone.Api/Profiles/Languages/LanguageModule.cs b/src/NzbDrone.Api/Profiles/Languages/LanguageModule.cs deleted file mode 100644 index 147bc69aa..000000000 --- a/src/NzbDrone.Api/Profiles/Languages/LanguageModule.cs +++ /dev/null @@ -1,39 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using NzbDrone.Core.Parser; - -namespace NzbDrone.Api.Profiles.Languages -{ - public class LanguageModule : NzbDroneRestModule - { - public LanguageModule() - { - GetResourceAll = GetAll; - GetResourceById = GetById; - } - - private LanguageResource GetById(int id) - { - var language = (Language)id; - - return new LanguageResource - { - Id = (int)language, - Name = language.ToString() - }; - } - - private List GetAll() - { - return ((Language[])Enum.GetValues(typeof (Language))) - .Select(l => new LanguageResource - { - Id = (int) l, - Name = l.ToString() - }) - .OrderBy(l => l.Name) - .ToList(); - } - } -} \ No newline at end of file diff --git a/src/NzbDrone.Api/Profiles/Languages/LanguageResource.cs b/src/NzbDrone.Api/Profiles/Languages/LanguageResource.cs deleted file mode 100644 index 09e5ba28c..000000000 --- a/src/NzbDrone.Api/Profiles/Languages/LanguageResource.cs +++ /dev/null @@ -1,13 +0,0 @@ -using Newtonsoft.Json; -using NzbDrone.Api.REST; - -namespace NzbDrone.Api.Profiles.Languages -{ - public class LanguageResource : RestResource - { - [JsonProperty(DefaultValueHandling = DefaultValueHandling.Include)] - public new int Id { get; set; } - public string Name { get; set; } - public string NameLower => Name.ToLowerInvariant(); - } -} \ No newline at end of file diff --git a/src/NzbDrone.Api/Profiles/LegacyProfileModule.cs b/src/NzbDrone.Api/Profiles/LegacyProfileModule.cs deleted file mode 100644 index d0e6b744e..000000000 --- a/src/NzbDrone.Api/Profiles/LegacyProfileModule.cs +++ /dev/null @@ -1,34 +0,0 @@ -using System.Text; -using Nancy; - -namespace NzbDrone.Api.Profiles -{ - class LegacyProfileModule : NzbDroneApiModule - { - public LegacyProfileModule() - : base("qualityprofile") - { - Get["/"] = x => - { - string queryString = ConvertQueryParams(Request.Query); - var url = string.Format("/api/profile?{0}", queryString); - - return Response.AsRedirect(url); - }; - } - - private string ConvertQueryParams(DynamicDictionary query) - { - var sb = new StringBuilder(); - - foreach (var key in query) - { - var value = query[key]; - - sb.AppendFormat("&{0}={1}", key, value); - } - - return sb.ToString().Trim('&'); - } - } -} diff --git a/src/NzbDrone.Api/Profiles/ProfileModule.cs b/src/NzbDrone.Api/Profiles/ProfileModule.cs deleted file mode 100644 index e5803db20..000000000 --- a/src/NzbDrone.Api/Profiles/ProfileModule.cs +++ /dev/null @@ -1,56 +0,0 @@ -using System.Collections.Generic; -using FluentValidation; -using NzbDrone.Core.Profiles; -using NzbDrone.Core.Validation; - -namespace NzbDrone.Api.Profiles -{ - public class ProfileModule : NzbDroneRestModule - { - private readonly IProfileService _profileService; - - public ProfileModule(IProfileService profileService) - { - _profileService = profileService; - SharedValidator.RuleFor(c => c.Name).NotEmpty(); - SharedValidator.RuleFor(c => c.Cutoff).NotNull(); - SharedValidator.RuleFor(c => c.Items).MustHaveAllowedQuality(); - SharedValidator.RuleFor(c => c.Language).ValidLanguage(); - - GetResourceAll = GetAll; - GetResourceById = GetById; - UpdateResource = Update; - CreateResource = Create; - DeleteResource = DeleteProfile; - } - - private int Create(ProfileResource resource) - { - var model = resource.ToModel(); - - return _profileService.Add(model).Id; - } - - private void DeleteProfile(int id) - { - _profileService.Delete(id); - } - - private void Update(ProfileResource resource) - { - var model = resource.ToModel(); - - _profileService.Update(model); - } - - private ProfileResource GetById(int id) - { - return _profileService.Get(id).ToResource(); - } - - private List GetAll() - { - return _profileService.All().ToResource(); - } - } -} \ No newline at end of file diff --git a/src/NzbDrone.Api/Profiles/ProfileResource.cs b/src/NzbDrone.Api/Profiles/ProfileResource.cs deleted file mode 100644 index ee02bcb32..000000000 --- a/src/NzbDrone.Api/Profiles/ProfileResource.cs +++ /dev/null @@ -1,83 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using NzbDrone.Api.REST; -using NzbDrone.Core.Parser; -using NzbDrone.Core.Profiles; -using NzbDrone.Core.Qualities; - -namespace NzbDrone.Api.Profiles -{ - public class ProfileResource : RestResource - { - public string Name { get; set; } - public Quality Cutoff { get; set; } - public List Items { get; set; } - public Language Language { get; set; } - } - - public class ProfileQualityItemResource : RestResource - { - public Quality Quality { get; set; } - public bool Allowed { get; set; } - } - - public static class ProfileResourceMapper - { - public static ProfileResource ToResource(this Profile model) - { - if (model == null) return null; - - return new ProfileResource - { - Id = model.Id, - - Name = model.Name, - Cutoff = model.Cutoff, - Items = model.Items.ConvertAll(ToResource), - Language = model.Language - }; - } - - public static ProfileQualityItemResource ToResource(this ProfileQualityItem model) - { - if (model == null) return null; - - return new ProfileQualityItemResource - { - Quality = model.Quality, - Allowed = model.Allowed - }; - } - - public static Profile ToModel(this ProfileResource resource) - { - if (resource == null) return null; - - return new Profile - { - Id = resource.Id, - - Name = resource.Name, - Cutoff = (Quality)resource.Cutoff.Id, - Items = resource.Items.ConvertAll(ToModel), - Language = resource.Language - }; - } - - public static ProfileQualityItem ToModel(this ProfileQualityItemResource resource) - { - if (resource == null) return null; - - return new ProfileQualityItem - { - Quality = (Quality)resource.Quality.Id, - Allowed = resource.Allowed - }; - } - - public static List ToResource(this IEnumerable models) - { - return models.Select(ToResource).ToList(); - } - } -} \ No newline at end of file diff --git a/src/NzbDrone.Api/Profiles/ProfileSchemaModule.cs b/src/NzbDrone.Api/Profiles/ProfileSchemaModule.cs deleted file mode 100644 index ec5f3ae01..000000000 --- a/src/NzbDrone.Api/Profiles/ProfileSchemaModule.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using NzbDrone.Core.Parser; -using NzbDrone.Core.Profiles; -using NzbDrone.Core.Qualities; - -namespace NzbDrone.Api.Profiles -{ - public class ProfileSchemaModule : NzbDroneRestModule - { - private readonly IQualityDefinitionService _qualityDefinitionService; - - public ProfileSchemaModule(IQualityDefinitionService qualityDefinitionService) - : base("/profile/schema") - { - _qualityDefinitionService = qualityDefinitionService; - - GetResourceAll = GetAll; - } - - private List GetAll() - { - var items = _qualityDefinitionService.All() - .OrderBy(v => v.Weight) - .Select(v => new ProfileQualityItem { Quality = v.Quality, Allowed = false }) - .ToList(); - - var profile = new Profile(); - profile.Cutoff = Quality.Unknown; - profile.Items = items; - profile.Language = Language.English; - - return new List { profile.ToResource() }; - } - } -} \ No newline at end of file diff --git a/src/NzbDrone.Api/Profiles/ProfileValidation.cs b/src/NzbDrone.Api/Profiles/ProfileValidation.cs deleted file mode 100644 index 003c96f39..000000000 --- a/src/NzbDrone.Api/Profiles/ProfileValidation.cs +++ /dev/null @@ -1,43 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using FluentValidation; -using FluentValidation.Validators; - -namespace NzbDrone.Api.Profiles -{ - public static class ProfileValidation - { - public static IRuleBuilderOptions> MustHaveAllowedQuality(this IRuleBuilder> ruleBuilder) - { - ruleBuilder.SetValidator(new NotEmptyValidator(null)); - - return ruleBuilder.SetValidator(new AllowedValidator()); - } - } - - public class AllowedValidator : PropertyValidator - { - public AllowedValidator() - : base("Must contain at least one allowed quality") - { - - } - - protected override bool IsValid(PropertyValidatorContext context) - { - var list = context.PropertyValue as IList; - - if (list == null) - { - return false; - } - - if (!list.Any(c => c.Allowed)) - { - return false; - } - - return true; - } - } -} diff --git a/src/NzbDrone.Api/Properties/AssemblyInfo.cs b/src/NzbDrone.Api/Properties/AssemblyInfo.cs deleted file mode 100644 index 6149a06c4..000000000 --- a/src/NzbDrone.Api/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; - -[assembly: AssemblyTitle("NzbDrone.Api")] - -[assembly: Guid("4c0922d7-979e-4ff7-b44b-b8ac2100eeb5")] - -[assembly: AssemblyVersion("10.0.0.*")] - -[assembly: InternalsVisibleTo("NzbDrone.Core")] diff --git a/src/NzbDrone.Api/ProviderModuleBase.cs b/src/NzbDrone.Api/ProviderModuleBase.cs deleted file mode 100644 index fa5313b0a..000000000 --- a/src/NzbDrone.Api/ProviderModuleBase.cs +++ /dev/null @@ -1,226 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using FluentValidation; -using FluentValidation.Results; -using Nancy; -using NzbDrone.Api.ClientSchema; -using NzbDrone.Api.Extensions; -using NzbDrone.Common.Reflection; -using NzbDrone.Core.ThingiProvider; -using NzbDrone.Core.Validation; -using Newtonsoft.Json; - -namespace NzbDrone.Api -{ - public abstract class ProviderModuleBase : NzbDroneRestModule - where TProviderDefinition : ProviderDefinition, new() - where TProvider : IProvider - where TProviderResource : ProviderResource, new() - { - private readonly IProviderFactory _providerFactory; - - protected ProviderModuleBase(IProviderFactory providerFactory, string resource) - : base(resource) - { - _providerFactory = providerFactory; - - Get["schema"] = x => GetTemplates(); - Post["test"] = x => Test(ReadResourceFromRequest(true)); - Post["action/{action}"] = x => RequestAction(x.action, ReadResourceFromRequest(true)); - - GetResourceAll = GetAll; - GetResourceById = GetProviderById; - CreateResource = CreateProvider; - UpdateResource = UpdateProvider; - DeleteResource = DeleteProvider; - - SharedValidator.RuleFor(c => c.Name).NotEmpty(); - SharedValidator.RuleFor(c => c.Name).Must((v,c) => !_providerFactory.All().Any(p => p.Name == c && p.Id != v.Id)).WithMessage("Should be unique"); - SharedValidator.RuleFor(c => c.Implementation).NotEmpty(); - SharedValidator.RuleFor(c => c.ConfigContract).NotEmpty(); - - PostValidator.RuleFor(c => c.Fields).NotNull(); - } - - private TProviderResource GetProviderById(int id) - { - var definition = _providerFactory.Get(id); - _providerFactory.SetProviderCharacteristics(definition); - - var resource = new TProviderResource(); - MapToResource(resource, definition); - - return resource; - } - - private List GetAll() - { - var providerDefinitions = _providerFactory.All().OrderBy(p => p.ImplementationName); - - var result = new List(providerDefinitions.Count()); - - foreach (var definition in providerDefinitions) - { - _providerFactory.SetProviderCharacteristics(definition); - - var providerResource = new TProviderResource(); - MapToResource(providerResource, definition); - - result.Add(providerResource); - } - - return result.OrderBy(p => p.Name).ToList(); - } - - private int CreateProvider(TProviderResource providerResource) - { - var providerDefinition = GetDefinition(providerResource, false); - - if (providerDefinition.Enable) - { - Test(providerDefinition, false); - } - - providerDefinition = _providerFactory.Create(providerDefinition); - - return providerDefinition.Id; - } - - private void UpdateProvider(TProviderResource providerResource) - { - var providerDefinition = GetDefinition(providerResource, false); - - _providerFactory.Update(providerDefinition); - } - - private TProviderDefinition GetDefinition(TProviderResource providerResource, bool includeWarnings = false, bool validate = true) - { - var definition = new TProviderDefinition(); - - MapToModel(definition, providerResource); - - if (validate) - { - Validate(definition, includeWarnings); - } - - return definition; - } - - protected virtual void MapToResource(TProviderResource resource, TProviderDefinition definition) - { - resource.Id = definition.Id; - - resource.Name = definition.Name; - resource.ImplementationName = definition.ImplementationName; - resource.Implementation = definition.Implementation; - resource.ConfigContract = definition.ConfigContract; - resource.Message = definition.Message; - - resource.Fields = SchemaBuilder.ToSchema(definition.Settings); - - resource.InfoLink = string.Format("https://github.com/Lidarr/Lidarr/wiki/Supported-{0}#{1}", - typeof(TProviderResource).Name.Replace("Resource", "s"), - definition.Implementation.ToLower()); - } - - protected virtual void MapToModel(TProviderDefinition definition, TProviderResource resource) - { - definition.Id = resource.Id; - - definition.Name = resource.Name; - definition.ImplementationName = resource.ImplementationName; - definition.Implementation = resource.Implementation; - definition.ConfigContract = resource.ConfigContract; - definition.Message = resource.Message; - - var configContract = ReflectionExtensions.CoreAssembly.FindTypeByName(definition.ConfigContract); - definition.Settings = (IProviderConfig)SchemaBuilder.ReadFromSchema(resource.Fields, configContract); - } - - private void DeleteProvider(int id) - { - _providerFactory.Delete(id); - } - - private Response GetTemplates() - { - var defaultDefinitions = _providerFactory.GetDefaultDefinitions().OrderBy(p => p.ImplementationName).ToList(); - - var result = new List(defaultDefinitions.Count()); - - foreach (var providerDefinition in defaultDefinitions) - { - var providerResource = new TProviderResource(); - MapToResource(providerResource, providerDefinition); - - var presetDefinitions = _providerFactory.GetPresetDefinitions(providerDefinition); - - providerResource.Presets = presetDefinitions.Select(v => - { - var presetResource = new TProviderResource(); - MapToResource(presetResource, v); - - return presetResource as ProviderResource; - }).ToList(); - - result.Add(providerResource); - } - - return result.AsResponse(); - } - - private Response Test(TProviderResource providerResource) - { - // Don't validate when getting the definition so we can validate afterwards (avoids validation being skipped because the provider is disabled) - var providerDefinition = GetDefinition(providerResource, true, false); - - Validate(providerDefinition, true); - Test(providerDefinition, true); - - return "{}"; - } - - - private Response RequestAction(string action, TProviderResource providerResource) - { - var providerDefinition = GetDefinition(providerResource, true, false); - - var query = ((IDictionary)Request.Query.ToDictionary()).ToDictionary(k => k.Key, k => k.Value.ToString()); - - var data = _providerFactory.RequestAction(providerDefinition, action, query); - Response resp = JsonConvert.SerializeObject(data); - resp.ContentType = "application/json"; - return resp; - } - - protected virtual void Validate(TProviderDefinition definition, bool includeWarnings) - { - var validationResult = definition.Settings.Validate(); - - VerifyValidationResult(validationResult, includeWarnings); - } - - protected virtual void Test(TProviderDefinition definition, bool includeWarnings) - { - var validationResult = _providerFactory.Test(definition); - - VerifyValidationResult(validationResult, includeWarnings); - } - - protected void VerifyValidationResult(ValidationResult validationResult, bool includeWarnings) - { - var result = new NzbDroneValidationResult(validationResult.Errors); - - if (includeWarnings && (!result.IsValid || result.HasWarnings)) - { - throw new ValidationException(result.Failures); - } - - if (!result.IsValid) - { - throw new ValidationException(result.Errors); - } - } - } -} diff --git a/src/NzbDrone.Api/ProviderResource.cs b/src/NzbDrone.Api/ProviderResource.cs deleted file mode 100644 index 9927a09cc..000000000 --- a/src/NzbDrone.Api/ProviderResource.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System.Collections.Generic; -using NzbDrone.Api.ClientSchema; -using NzbDrone.Api.REST; -using NzbDrone.Core.ThingiProvider; - -namespace NzbDrone.Api -{ - public class ProviderResource : RestResource - { - public string Name { get; set; } - public List Fields { get; set; } - public string ImplementationName { get; set; } - public string Implementation { get; set; } - public string ConfigContract { get; set; } - public string InfoLink { get; set; } - public ProviderMessage Message { get; set; } - - public List Presets { get; set; } - } -} \ No newline at end of file diff --git a/src/NzbDrone.Api/Qualities/QualityDefinitionModule.cs b/src/NzbDrone.Api/Qualities/QualityDefinitionModule.cs deleted file mode 100644 index 1b5351300..000000000 --- a/src/NzbDrone.Api/Qualities/QualityDefinitionModule.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System.Collections.Generic; -using NzbDrone.Core.Qualities; - -namespace NzbDrone.Api.Qualities -{ - public class QualityDefinitionModule : NzbDroneRestModule - { - private readonly IQualityDefinitionService _qualityDefinitionService; - - public QualityDefinitionModule(IQualityDefinitionService qualityDefinitionService) - { - _qualityDefinitionService = qualityDefinitionService; - - GetResourceAll = GetAll; - - GetResourceById = GetById; - - UpdateResource = Update; - } - - private void Update(QualityDefinitionResource resource) - { - var model = resource.ToModel(); - _qualityDefinitionService.Update(model); - } - - private QualityDefinitionResource GetById(int id) - { - return _qualityDefinitionService.GetById(id).ToResource(); - } - - private List GetAll() - { - return _qualityDefinitionService.All().ToResource(); - } - } -} \ No newline at end of file diff --git a/src/NzbDrone.Api/Queue/QueueActionModule.cs b/src/NzbDrone.Api/Queue/QueueActionModule.cs deleted file mode 100644 index 89ca897b7..000000000 --- a/src/NzbDrone.Api/Queue/QueueActionModule.cs +++ /dev/null @@ -1,132 +0,0 @@ -using System; -using Nancy; -using Nancy.Responses; -using NzbDrone.Api.Extensions; -using NzbDrone.Api.REST; -using NzbDrone.Core.Download; -using NzbDrone.Core.Download.Pending; -using NzbDrone.Core.Download.TrackedDownloads; -using NzbDrone.Core.Queue; - -namespace NzbDrone.Api.Queue -{ - public class QueueActionModule : NzbDroneRestModule - { - private readonly IQueueService _queueService; - private readonly ITrackedDownloadService _trackedDownloadService; - private readonly ICompletedDownloadService _completedDownloadService; - private readonly IFailedDownloadService _failedDownloadService; - private readonly IProvideDownloadClient _downloadClientProvider; - private readonly IPendingReleaseService _pendingReleaseService; - private readonly IDownloadService _downloadService; - - public QueueActionModule(IQueueService queueService, - ITrackedDownloadService trackedDownloadService, - ICompletedDownloadService completedDownloadService, - IFailedDownloadService failedDownloadService, - IProvideDownloadClient downloadClientProvider, - IPendingReleaseService pendingReleaseService, - IDownloadService downloadService) - { - _queueService = queueService; - _trackedDownloadService = trackedDownloadService; - _completedDownloadService = completedDownloadService; - _failedDownloadService = failedDownloadService; - _downloadClientProvider = downloadClientProvider; - _pendingReleaseService = pendingReleaseService; - _downloadService = downloadService; - - Delete[@"/(?[\d]{1,10})"] = x => Remove((int)x.Id); - Post["/import"] = x => Import(); - Post["/grab"] = x => Grab(); - } - - private Response Remove(int id) - { - var blacklist = false; - var blacklistQuery = Request.Query.blacklist; - - if (blacklistQuery.HasValue) - { - blacklist = Convert.ToBoolean(blacklistQuery.Value); - } - - var pendingRelease = _pendingReleaseService.FindPendingQueueItem(id); - - if (pendingRelease != null) - { - _pendingReleaseService.RemovePendingQueueItems(pendingRelease.Id); - - return new object().AsResponse(); - } - - var trackedDownload = GetTrackedDownload(id); - - if (trackedDownload == null) - { - throw new NotFoundException(); - } - - var downloadClient = _downloadClientProvider.Get(trackedDownload.DownloadClient); - - if (downloadClient == null) - { - throw new BadRequestException(); - } - - downloadClient.RemoveItem(trackedDownload.DownloadItem.DownloadId, true); - - if (blacklist) - { - _failedDownloadService.MarkAsFailed(trackedDownload.DownloadItem.DownloadId); - } - - return new object().AsResponse(); - } - - private JsonResponse Import() - { - var resource = Request.Body.FromJson(); - var trackedDownload = GetTrackedDownload(resource.Id); - - _completedDownloadService.Process(trackedDownload, true); - - return resource.AsResponse(); - } - - private JsonResponse Grab() - { - var resource = Request.Body.FromJson(); - - var pendingRelease = _pendingReleaseService.FindPendingQueueItem(resource.Id); - - if (pendingRelease == null) - { - throw new NotFoundException(); - } - - _downloadService.DownloadReport(pendingRelease.RemoteAlbum); - - return resource.AsResponse(); - } - - private TrackedDownload GetTrackedDownload(int queueId) - { - var queueItem = _queueService.Find(queueId); - - if (queueItem == null) - { - throw new NotFoundException(); - } - - var trackedDownload = _trackedDownloadService.Find(queueItem.DownloadId); - - if (trackedDownload == null) - { - throw new NotFoundException(); - } - - return trackedDownload; - } - } -} diff --git a/src/NzbDrone.Api/Queue/QueueModule.cs b/src/NzbDrone.Api/Queue/QueueModule.cs deleted file mode 100644 index 00e614132..000000000 --- a/src/NzbDrone.Api/Queue/QueueModule.cs +++ /dev/null @@ -1,48 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using NzbDrone.Core.Datastore.Events; -using NzbDrone.Core.Download.Pending; -using NzbDrone.Core.Messaging.Events; -using NzbDrone.Core.Queue; -using NzbDrone.SignalR; - -namespace NzbDrone.Api.Queue -{ - public class QueueModule : NzbDroneRestModuleWithSignalR, - IHandle, IHandle - { - private readonly IQueueService _queueService; - private readonly IPendingReleaseService _pendingReleaseService; - - public QueueModule(IBroadcastSignalRMessage broadcastSignalRMessage, IQueueService queueService, IPendingReleaseService pendingReleaseService) - : base(broadcastSignalRMessage) - { - _queueService = queueService; - _pendingReleaseService = pendingReleaseService; - GetResourceAll = GetQueue; - } - - private List GetQueue() - { - return GetQueueItems().ToResource(); - } - - private IEnumerable GetQueueItems() - { - var queue = _queueService.GetQueue(); - var pending = _pendingReleaseService.GetPendingQueue(); - - return queue.Concat(pending); - } - - public void Handle(QueueUpdatedEvent message) - { - BroadcastResourceChange(ModelAction.Sync); - } - - public void Handle(PendingReleasesUpdatedEvent message) - { - BroadcastResourceChange(ModelAction.Sync); - } - } -} \ No newline at end of file diff --git a/src/NzbDrone.Api/Queue/QueueResource.cs b/src/NzbDrone.Api/Queue/QueueResource.cs deleted file mode 100644 index 858c10dcf..000000000 --- a/src/NzbDrone.Api/Queue/QueueResource.cs +++ /dev/null @@ -1,63 +0,0 @@ -using System; -using System.Collections.Generic; -using NzbDrone.Api.REST; -using NzbDrone.Core.Qualities; -using NzbDrone.Api.Series; -using NzbDrone.Api.Episodes; -using NzbDrone.Api.Music; -using NzbDrone.Api.Albums; -using NzbDrone.Core.Download.TrackedDownloads; -using NzbDrone.Core.Indexers; -using System.Linq; - -namespace NzbDrone.Api.Queue -{ - public class QueueResource : RestResource - { - public ArtistResource Artist { get; set; } - public AlbumResource Album { get; set; } - public QualityModel Quality { get; set; } - public decimal Size { get; set; } - public string Title { get; set; } - public decimal Sizeleft { get; set; } - public TimeSpan? Timeleft { get; set; } - public DateTime? EstimatedCompletionTime { get; set; } - public string Status { get; set; } - public string TrackedDownloadStatus { get; set; } - public List StatusMessages { get; set; } - public string DownloadId { get; set; } - public DownloadProtocol Protocol { get; set; } - } - - public static class QueueResourceMapper - { - public static QueueResource ToResource(this Core.Queue.Queue model) - { - if (model == null) return null; - - return new QueueResource - { - Id = model.Id, - - Artist = model.Artist.ToResource(), - Album = model.Album.ToResource(), - Quality = model.Quality, - Size = model.Size, - Title = model.Title, - Sizeleft = model.Sizeleft, - Timeleft = model.Timeleft, - EstimatedCompletionTime = model.EstimatedCompletionTime, - Status = model.Status, - TrackedDownloadStatus = model.TrackedDownloadStatus, - StatusMessages = model.StatusMessages, - DownloadId = model.DownloadId, - Protocol = model.Protocol - }; - } - - public static List ToResource(this IEnumerable models) - { - return models.Select(ToResource).ToList(); - } - } -} diff --git a/src/NzbDrone.Api/Restrictions/RestrictionModule.cs b/src/NzbDrone.Api/Restrictions/RestrictionModule.cs deleted file mode 100644 index 918b3a50b..000000000 --- a/src/NzbDrone.Api/Restrictions/RestrictionModule.cs +++ /dev/null @@ -1,59 +0,0 @@ -using System.Collections.Generic; -using FluentValidation.Results; -using NzbDrone.Common.Extensions; -using NzbDrone.Core.Restrictions; - -namespace NzbDrone.Api.Restrictions -{ - public class RestrictionModule : NzbDroneRestModule - { - private readonly IRestrictionService _restrictionService; - - - public RestrictionModule(IRestrictionService restrictionService) - { - _restrictionService = restrictionService; - - GetResourceById = GetRestriction; - GetResourceAll = GetAllRestrictions; - CreateResource = CreateRestriction; - UpdateResource = UpdateRestriction; - DeleteResource = DeleteRestriction; - - SharedValidator.Custom(restriction => - { - if (restriction.Ignored.IsNullOrWhiteSpace() && restriction.Required.IsNullOrWhiteSpace()) - { - return new ValidationFailure("", "Either 'Must contain' or 'Must not contain' is required"); - } - - return null; - }); - } - - private RestrictionResource GetRestriction(int id) - { - return _restrictionService.Get(id).ToResource(); - } - - private List GetAllRestrictions() - { - return _restrictionService.All().ToResource(); - } - - private int CreateRestriction(RestrictionResource resource) - { - return _restrictionService.Add(resource.ToModel()).Id; - } - - private void UpdateRestriction(RestrictionResource resource) - { - _restrictionService.Update(resource.ToModel()); - } - - private void DeleteRestriction(int id) - { - _restrictionService.Delete(id); - } - } -} diff --git a/src/NzbDrone.Api/RootFolders/RootFolderModule.cs b/src/NzbDrone.Api/RootFolders/RootFolderModule.cs deleted file mode 100644 index e87e581de..000000000 --- a/src/NzbDrone.Api/RootFolders/RootFolderModule.cs +++ /dev/null @@ -1,63 +0,0 @@ -using System.Collections.Generic; -using FluentValidation; -using NzbDrone.Core.RootFolders; -using NzbDrone.Core.Validation.Paths; -using NzbDrone.SignalR; - -namespace NzbDrone.Api.RootFolders -{ - public class RootFolderModule : NzbDroneRestModuleWithSignalR - { - private readonly IRootFolderService _rootFolderService; - - public RootFolderModule(IRootFolderService rootFolderService, - IBroadcastSignalRMessage signalRBroadcaster, - RootFolderValidator rootFolderValidator, - PathExistsValidator pathExistsValidator, - DroneFactoryValidator droneFactoryValidator, - MappedNetworkDriveValidator mappedNetworkDriveValidator, - StartupFolderValidator startupFolderValidator, - FolderWritableValidator folderWritableValidator) - : base(signalRBroadcaster) - { - _rootFolderService = rootFolderService; - - GetResourceAll = GetRootFolders; - GetResourceById = GetRootFolder; - CreateResource = CreateRootFolder; - DeleteResource = DeleteFolder; - - SharedValidator.RuleFor(c => c.Path) - .Cascade(CascadeMode.StopOnFirstFailure) - .IsValidPath() - .SetValidator(rootFolderValidator) - .SetValidator(droneFactoryValidator) - .SetValidator(mappedNetworkDriveValidator) - .SetValidator(startupFolderValidator) - .SetValidator(pathExistsValidator) - .SetValidator(folderWritableValidator); - } - - private RootFolderResource GetRootFolder(int id) - { - return _rootFolderService.Get(id).ToResource(); - } - - private int CreateRootFolder(RootFolderResource rootFolderResource) - { - var model = rootFolderResource.ToModel(); - - return _rootFolderService.Add(model).Id; - } - - private List GetRootFolders() - { - return _rootFolderService.AllWithUnmappedFolders().ToResource(); - } - - private void DeleteFolder(int id) - { - _rootFolderService.Remove(id); - } - } -} \ No newline at end of file diff --git a/src/NzbDrone.Api/SeasonPass/SeasonPassModule.cs b/src/NzbDrone.Api/SeasonPass/SeasonPassModule.cs deleted file mode 100644 index 93cd25ce5..000000000 --- a/src/NzbDrone.Api/SeasonPass/SeasonPassModule.cs +++ /dev/null @@ -1,31 +0,0 @@ -using Nancy; -using NzbDrone.Api.Extensions; -using NzbDrone.Core.Tv; - -namespace NzbDrone.Api.SeasonPass -{ - public class SeasonPassModule : NzbDroneApiModule - { - private readonly IEpisodeMonitoredService _episodeMonitoredService; - - public SeasonPassModule(IEpisodeMonitoredService episodeMonitoredService) - : base("/seasonpass") - { - _episodeMonitoredService = episodeMonitoredService; - Post["/"] = series => UpdateAll(); - } - - private Response UpdateAll() - { - //Read from request - var request = Request.Body.FromJson(); - - foreach (var s in request.Series) - { - _episodeMonitoredService.SetEpisodeMonitoredStatus(s, request.MonitoringOptions); - } - - return "ok".AsResponse(HttpStatusCode.Accepted); - } - } -} diff --git a/src/NzbDrone.Api/SeasonPass/SeasonPassResource.cs b/src/NzbDrone.Api/SeasonPass/SeasonPassResource.cs deleted file mode 100644 index af537e7f9..000000000 --- a/src/NzbDrone.Api/SeasonPass/SeasonPassResource.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System.Collections.Generic; -using NzbDrone.Core.Tv; - -namespace NzbDrone.Api.SeasonPass -{ - public class SeasonPassResource - { - public List Series { get; set; } - public MonitoringOptions MonitoringOptions { get; set; } - } -} diff --git a/src/NzbDrone.Api/Series/SeasonResource.cs b/src/NzbDrone.Api/Series/SeasonResource.cs deleted file mode 100644 index 4c20d8865..000000000 --- a/src/NzbDrone.Api/Series/SeasonResource.cs +++ /dev/null @@ -1,47 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using NzbDrone.Core.Tv; -namespace NzbDrone.Api.Series -{ - public class SeasonResource - { - public int SeasonNumber { get; set; } - public bool Monitored { get; set; } - public SeasonStatisticsResource Statistics { get; set; } - } - - public static class SeasonResourceMapper - { - public static SeasonResource ToResource(this Season model) - { - if (model == null) return null; - - return new SeasonResource - { - SeasonNumber = model.SeasonNumber, - Monitored = model.Monitored - }; - } - - public static Season ToModel(this SeasonResource resource) - { - if (resource == null) return null; - - return new Season - { - SeasonNumber = resource.SeasonNumber, - Monitored = resource.Monitored - }; - } - - public static List ToResource(this IEnumerable models) - { - return models.Select(ToResource).ToList(); - } - - public static List ToModel(this IEnumerable resources) - { - return resources?.Select(ToModel).ToList() ?? new List(); - } - } -} diff --git a/src/NzbDrone.Api/Series/SeasonStatisticsResource.cs b/src/NzbDrone.Api/Series/SeasonStatisticsResource.cs deleted file mode 100644 index 34acc721e..000000000 --- a/src/NzbDrone.Api/Series/SeasonStatisticsResource.cs +++ /dev/null @@ -1,43 +0,0 @@ -using System; -using NzbDrone.Core.SeriesStats; - -namespace NzbDrone.Api.Series -{ - public class SeasonStatisticsResource - { - public DateTime? NextAiring { get; set; } - public DateTime? PreviousAiring { get; set; } - public int EpisodeFileCount { get; set; } - public int EpisodeCount { get; set; } - public int TotalEpisodeCount { get; set; } - public long SizeOnDisk { get; set; } - - public decimal PercentOfEpisodes - { - get - { - if (EpisodeCount == 0) return 0; - - return (decimal)EpisodeFileCount / (decimal)EpisodeCount * 100; - } - } - } - - public static class SeasonStatisticsResourceMapper - { - public static SeasonStatisticsResource ToResource(this SeasonStatistics model) - { - if (model == null) return null; - - return new SeasonStatisticsResource - { - NextAiring = model.NextAiring, - PreviousAiring = model.PreviousAiring, - EpisodeFileCount = model.EpisodeFileCount, - EpisodeCount = model.EpisodeFileCount, - TotalEpisodeCount = model.TotalEpisodeCount, - SizeOnDisk = model.SizeOnDisk - }; - } - } -} diff --git a/src/NzbDrone.Api/Series/SeriesEditorModule.cs b/src/NzbDrone.Api/Series/SeriesEditorModule.cs deleted file mode 100644 index 87cd53113..000000000 --- a/src/NzbDrone.Api/Series/SeriesEditorModule.cs +++ /dev/null @@ -1,31 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using Nancy; -using NzbDrone.Api.Extensions; -using NzbDrone.Core.Tv; - -namespace NzbDrone.Api.Series -{ - public class SeriesEditorModule : NzbDroneApiModule - { - private readonly ISeriesService _seriesService; - - public SeriesEditorModule(ISeriesService seriesService) - : base("/series/editor") - { - _seriesService = seriesService; - Put["/"] = series => SaveAll(); - } - - private Response SaveAll() - { - var resources = Request.Body.FromJson>(); - - var series = resources.Select(seriesResource => seriesResource.ToModel(_seriesService.GetSeries(seriesResource.Id))).ToList(); - - return _seriesService.UpdateSeries(series) - .ToResource() - .AsResponse(HttpStatusCode.Accepted); - } - } -} diff --git a/src/NzbDrone.Api/Series/SeriesModule.cs b/src/NzbDrone.Api/Series/SeriesModule.cs deleted file mode 100644 index 693f9a360..000000000 --- a/src/NzbDrone.Api/Series/SeriesModule.cs +++ /dev/null @@ -1,244 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using FluentValidation; -using NzbDrone.Common.Extensions; -using NzbDrone.Core.Datastore.Events; -using NzbDrone.Core.MediaCover; -using NzbDrone.Core.MediaFiles; -using NzbDrone.Core.MediaFiles.Events; -using NzbDrone.Core.Messaging.Events; -using NzbDrone.Core.SeriesStats; -using NzbDrone.Core.Tv; -using NzbDrone.Core.Tv.Events; -using NzbDrone.Core.Validation.Paths; -using NzbDrone.Core.DataAugmentation.Scene; -using NzbDrone.Core.Validation; -using NzbDrone.SignalR; - -namespace NzbDrone.Api.Series -{ - public class SeriesModule : NzbDroneRestModuleWithSignalR, - IHandle, - IHandle, - IHandle, - IHandle, - IHandle, - IHandle, - IHandle - - { - private readonly ISeriesService _seriesService; - private readonly IAddSeriesService _addSeriesService; - private readonly ISeriesStatisticsService _seriesStatisticsService; - // private readonly ISceneMappingService _sceneMappingService; - private readonly IMapCoversToLocal _coverMapper; - - public SeriesModule(IBroadcastSignalRMessage signalRBroadcaster, - ISeriesService seriesService, - IAddSeriesService addSeriesService, - ISeriesStatisticsService seriesStatisticsService, - // ISceneMappingService sceneMappingService, - IMapCoversToLocal coverMapper, - RootFolderValidator rootFolderValidator, - SeriesPathValidator seriesPathValidator, - SeriesExistsValidator seriesExistsValidator, - DroneFactoryValidator droneFactoryValidator, - SeriesAncestorValidator seriesAncestorValidator, - ProfileExistsValidator profileExistsValidator - ) - : base(signalRBroadcaster) - { - _seriesService = seriesService; - _addSeriesService = addSeriesService; - _seriesStatisticsService = seriesStatisticsService; - // _sceneMappingService = sceneMappingService; - - _coverMapper = coverMapper; - - GetResourceAll = AllSeries; - GetResourceById = GetSeries; - CreateResource = AddSeries; - UpdateResource = UpdateSeries; - DeleteResource = DeleteSeries; - - Validation.RuleBuilderExtensions.ValidId(SharedValidator.RuleFor(s => s.ProfileId)); - - SharedValidator.RuleFor(s => s.Path) - .Cascade(CascadeMode.StopOnFirstFailure) - .IsValidPath() - .SetValidator(rootFolderValidator) - .SetValidator(seriesPathValidator) - .SetValidator(droneFactoryValidator) - .SetValidator(seriesAncestorValidator) - .When(s => !s.Path.IsNullOrWhiteSpace()); - - SharedValidator.RuleFor(s => s.ProfileId).SetValidator(profileExistsValidator); - - PostValidator.RuleFor(s => s.Path).IsValidPath().When(s => s.RootFolderPath.IsNullOrWhiteSpace()); - PostValidator.RuleFor(s => s.RootFolderPath).IsValidPath().When(s => s.Path.IsNullOrWhiteSpace()); - PostValidator.RuleFor(s => s.TvdbId).GreaterThan(0).SetValidator(seriesExistsValidator); - - PutValidator.RuleFor(s => s.Path).IsValidPath(); - } - - private SeriesResource GetSeries(int id) - { - var series = _seriesService.GetSeries(id); - return MapToResource(series); - } - - private SeriesResource MapToResource(Core.Tv.Series series) - { - if (series == null) return null; - - var resource = series.ToResource(); - MapCoversToLocal(resource); - FetchAndLinkSeriesStatistics(resource); - PopulateAlternateTitles(resource); - - return resource; - } - - private List AllSeries() - { - var seriesStats = _seriesStatisticsService.SeriesStatistics(); - var seriesResources = _seriesService.GetAllSeries().ToResource(); - - MapCoversToLocal(seriesResources.ToArray()); - LinkSeriesStatistics(seriesResources, seriesStats); - PopulateAlternateTitles(seriesResources); - - return seriesResources; - } - - private int AddSeries(SeriesResource seriesResource) - { - var model = seriesResource.ToModel(); - - return _addSeriesService.AddSeries(model).Id; - } - - private void UpdateSeries(SeriesResource seriesResource) - { - var model = seriesResource.ToModel(_seriesService.GetSeries(seriesResource.Id)); - - _seriesService.UpdateSeries(model); - - BroadcastResourceChange(ModelAction.Updated, seriesResource); - } - - private void DeleteSeries(int id) - { - var deleteFiles = false; - var deleteFilesQuery = Request.Query.deleteFiles; - - if (deleteFilesQuery.HasValue) - { - deleteFiles = Convert.ToBoolean(deleteFilesQuery.Value); - } - - _seriesService.DeleteSeries(id, deleteFiles); - } - - private void MapCoversToLocal(params SeriesResource[] series) - { - foreach (var seriesResource in series) - { - _coverMapper.ConvertToLocalUrls(seriesResource.Id, seriesResource.Images); - } - } - - private void FetchAndLinkSeriesStatistics(SeriesResource resource) - { - LinkSeriesStatistics(resource, _seriesStatisticsService.SeriesStatistics(resource.Id)); - } - - private void LinkSeriesStatistics(List resources, List seriesStatistics) - { - var dictSeriesStats = seriesStatistics.ToDictionary(v => v.SeriesId); - - foreach (var series in resources) - { - var stats = dictSeriesStats.GetValueOrDefault(series.Id); - if (stats == null) continue; - - LinkSeriesStatistics(series, stats); - } - } - - private void LinkSeriesStatistics(SeriesResource resource, SeriesStatistics seriesStatistics) - { - resource.TotalEpisodeCount = seriesStatistics.TotalEpisodeCount; - resource.EpisodeCount = seriesStatistics.EpisodeCount; - resource.EpisodeFileCount = seriesStatistics.EpisodeFileCount; - resource.NextAiring = seriesStatistics.NextAiring; - resource.PreviousAiring = seriesStatistics.PreviousAiring; - resource.SizeOnDisk = seriesStatistics.SizeOnDisk; - - if (seriesStatistics.SeasonStatistics != null) - { - var dictSeasonStats = seriesStatistics.SeasonStatistics.ToDictionary(v => v.SeasonNumber); - - foreach (var season in resource.Seasons) - { - season.Statistics = dictSeasonStats.GetValueOrDefault(season.SeasonNumber).ToResource(); - } - } - } - - private void PopulateAlternateTitles(List resources) - { - foreach (var resource in resources) - { - PopulateAlternateTitles(resource); - } - } - - private void PopulateAlternateTitles(SeriesResource resource) - { - //var mappings = null //_sceneMappingService.FindByTvdbId(resource.TvdbId); - - //if (mappings == null) return; - return; - // resource.AlternateTitles = mappings.Select(v => new AlternateTitleResource { Title = v.Title, SeasonNumber = v.SeasonNumber, SceneSeasonNumber = v.SceneSeasonNumber }).ToList(); - } - - public void Handle(EpisodeImportedEvent message) - { - BroadcastResourceChange(ModelAction.Updated, message.ImportedEpisode.SeriesId); - } - - public void Handle(EpisodeFileDeletedEvent message) - { - if (message.Reason == DeleteMediaFileReason.Upgrade) return; - - BroadcastResourceChange(ModelAction.Updated, message.EpisodeFile.SeriesId); - } - - public void Handle(SeriesUpdatedEvent message) - { - BroadcastResourceChange(ModelAction.Updated, message.Series.Id); - } - - public void Handle(SeriesEditedEvent message) - { - BroadcastResourceChange(ModelAction.Updated, message.Series.Id); - } - - public void Handle(SeriesDeletedEvent message) - { - BroadcastResourceChange(ModelAction.Deleted, message.Series.ToResource()); - } - - public void Handle(SeriesRenamedEvent message) - { - BroadcastResourceChange(ModelAction.Updated, message.Series.Id); - } - - public void Handle(MediaCoversUpdatedEvent message) - { - BroadcastResourceChange(ModelAction.Updated, message.Series.Id); - } - } -} diff --git a/src/NzbDrone.Api/Series/SeriesResource.cs b/src/NzbDrone.Api/Series/SeriesResource.cs deleted file mode 100644 index 86de03eb7..000000000 --- a/src/NzbDrone.Api/Series/SeriesResource.cs +++ /dev/null @@ -1,222 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using NzbDrone.Api.REST; -using NzbDrone.Core.MediaCover; -using NzbDrone.Core.Tv; - -namespace NzbDrone.Api.Series -{ - public class SeriesResource : RestResource - { - public SeriesResource() - { - Monitored = true; - } - - //Todo: Sorters should be done completely on the client - //Todo: Is there an easy way to keep IgnoreArticlesWhenSorting in sync between, Series, History, Missing? - //Todo: We should get the entire Profile instead of ID and Name separately - - //View Only - public string Title { get; set; } - public List AlternateTitles { get; set; } - public string SortTitle { get; set; } - - public int SeasonCount - { - get - { - if (Seasons == null) return 0; - - return Seasons.Where(s => s.SeasonNumber > 0).Count(); - } - } - - public int? TotalEpisodeCount { get; set; } - public int? EpisodeCount { get; set; } - public int? EpisodeFileCount { get; set; } - public long? SizeOnDisk { get; set; } - public SeriesStatusType Status { get; set; } - public string Overview { get; set; } - public DateTime? NextAiring { get; set; } - public DateTime? PreviousAiring { get; set; } - public string Network { get; set; } - public string AirTime { get; set; } - public List Images { get; set; } - - public string RemotePoster { get; set; } - public List Seasons { get; set; } - public int Year { get; set; } - - //View & Edit - public string Path { get; set; } - public int ProfileId { get; set; } - - //Editing Only - public bool SeasonFolder { get; set; } - public bool Monitored { get; set; } - - public bool UseSceneNumbering { get; set; } - public int Runtime { get; set; } - public int TvdbId { get; set; } - public int TvRageId { get; set; } - public int TvMazeId { get; set; } - public DateTime? FirstAired { get; set; } - public DateTime? LastInfoSync { get; set; } - public SeriesTypes SeriesType { get; set; } - public string CleanTitle { get; set; } - public string ImdbId { get; set; } - public string TitleSlug { get; set; } - public string RootFolderPath { get; set; } - public string Certification { get; set; } - public List Genres { get; set; } - public HashSet Tags { get; set; } - public DateTime Added { get; set; } - public AddSeriesOptions AddOptions { get; set; } - public Ratings Ratings { get; set; } - - //TODO: Add series statistics as a property of the series (instead of individual properties) - - //Used to support legacy consumers - public int QualityProfileId - { - get - { - return ProfileId; - } - set - { - if (value > 0 && ProfileId == 0) - { - ProfileId = value; - } - } - } - } - - public static class SeriesResourceMapper - { - public static SeriesResource ToResource(this Core.Tv.Series model) - { - if (model == null) return null; - - return new SeriesResource - { - Id = model.Id, - - Title = model.Title, - //AlternateTitles - SortTitle = model.SortTitle, - - //TotalEpisodeCount - //EpisodeCount - //EpisodeFileCount - //SizeOnDisk - Status = model.Status, - Overview = model.Overview, - //NextAiring - //PreviousAiring - Network = model.Network, - AirTime = model.AirTime, - Images = model.Images, - - Seasons = model.Seasons.ToResource(), - Year = model.Year, - - Path = model.Path, - ProfileId = model.ProfileId, - - SeasonFolder = model.SeasonFolder, - Monitored = model.Monitored, - - UseSceneNumbering = model.UseSceneNumbering, - Runtime = model.Runtime, - TvdbId = model.TvdbId, - TvRageId = model.TvRageId, - TvMazeId = model.TvMazeId, - FirstAired = model.FirstAired, - LastInfoSync = model.LastInfoSync, - SeriesType = model.SeriesType, - CleanTitle = model.CleanTitle, - ImdbId = model.ImdbId, - TitleSlug = model.TitleSlug, - RootFolderPath = model.RootFolderPath, - Certification = model.Certification, - Genres = model.Genres, - Tags = model.Tags, - Added = model.Added, - AddOptions = model.AddOptions, - Ratings = model.Ratings - }; - } - - public static Core.Tv.Series ToModel(this SeriesResource resource) - { - if (resource == null) return null; - - return new Core.Tv.Series - { - Id = resource.Id, - - Title = resource.Title, - //AlternateTitles - SortTitle = resource.SortTitle, - - //TotalEpisodeCount - //EpisodeCount - //EpisodeFileCount - //SizeOnDisk - Status = resource.Status, - Overview = resource.Overview, - //NextAiring - //PreviousAiring - Network = resource.Network, - AirTime = resource.AirTime, - Images = resource.Images, - - Seasons = resource.Seasons.ToModel(), - Year = resource.Year, - - Path = resource.Path, - ProfileId = resource.ProfileId, - - SeasonFolder = resource.SeasonFolder, - Monitored = resource.Monitored, - - UseSceneNumbering = resource.UseSceneNumbering, - Runtime = resource.Runtime, - TvdbId = resource.TvdbId, - TvRageId = resource.TvRageId, - TvMazeId = resource.TvMazeId, - FirstAired = resource.FirstAired, - LastInfoSync = resource.LastInfoSync, - SeriesType = resource.SeriesType, - CleanTitle = resource.CleanTitle, - ImdbId = resource.ImdbId, - TitleSlug = resource.TitleSlug, - RootFolderPath = resource.RootFolderPath, - Certification = resource.Certification, - Genres = resource.Genres, - Tags = resource.Tags, - Added = resource.Added, - AddOptions = resource.AddOptions, - Ratings = resource.Ratings - }; - } - - public static Core.Tv.Series ToModel(this SeriesResource resource, Core.Tv.Series series) - { - var updatedSeries = resource.ToModel(); - - series.ApplyChanges(updatedSeries); - - return series; - } - - public static List ToResource(this IEnumerable series) - { - return series.Select(ToResource).ToList(); - } - } -} diff --git a/src/NzbDrone.Api/System/Backup/BackupModule.cs b/src/NzbDrone.Api/System/Backup/BackupModule.cs deleted file mode 100644 index b5074793e..000000000 --- a/src/NzbDrone.Api/System/Backup/BackupModule.cs +++ /dev/null @@ -1,32 +0,0 @@ -using System.Collections.Generic; -using System.IO; -using System.Linq; -using NzbDrone.Core.Backup; - -namespace NzbDrone.Api.System.Backup -{ - public class BackupModule : NzbDroneRestModule - { - private readonly IBackupService _backupService; - - public BackupModule(IBackupService backupService) : base("system/backup") - { - _backupService = backupService; - GetResourceAll = GetBackupFiles; - } - - public List GetBackupFiles() - { - var backups = _backupService.GetBackups(); - - return backups.Select(b => new BackupResource - { - Id = b.Path.GetHashCode(), - Name = Path.GetFileName(b.Path), - Path = b.Path, - Type = b.Type, - Time = b.Time - }).ToList(); - } - } -} diff --git a/src/NzbDrone.Api/System/Tasks/TaskModule.cs b/src/NzbDrone.Api/System/Tasks/TaskModule.cs deleted file mode 100644 index db8c4f376..000000000 --- a/src/NzbDrone.Api/System/Tasks/TaskModule.cs +++ /dev/null @@ -1,49 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using System.Text.RegularExpressions; -using NzbDrone.Core.Datastore.Events; -using NzbDrone.Core.Jobs; -using NzbDrone.Core.Messaging.Events; -using NzbDrone.SignalR; - -namespace NzbDrone.Api.System.Tasks -{ - public class TaskModule : NzbDroneRestModuleWithSignalR, IHandle - { - private readonly ITaskManager _taskManager; - - private static readonly Regex NameRegex = new Regex("(? GetAll() - { - return _taskManager.GetAll().Select(ConvertToResource).ToList(); - } - - private static TaskResource ConvertToResource(ScheduledTask scheduledTask) - { - var taskName = scheduledTask.TypeName.Split('.').Last().Replace("Command", ""); - - return new TaskResource - { - Id = scheduledTask.Id, - Name = NameRegex.Replace(taskName, match => " " + match.Value), - TaskName = taskName, - Interval = scheduledTask.Interval, - LastExecution = scheduledTask.LastExecution, - NextExecution = scheduledTask.LastExecution.AddMinutes(scheduledTask.Interval) - }; - } - - public void Handle(CommandExecutedEvent message) - { - BroadcastResourceChange(ModelAction.Sync); - } - } -} diff --git a/src/NzbDrone.Api/Tags/TagModule.cs b/src/NzbDrone.Api/Tags/TagModule.cs deleted file mode 100644 index d2a01667c..000000000 --- a/src/NzbDrone.Api/Tags/TagModule.cs +++ /dev/null @@ -1,60 +0,0 @@ -using System.Collections.Generic; -using NzbDrone.Core.Datastore.Events; -using NzbDrone.Core.Messaging.Events; -using NzbDrone.Core.Tags; -using NzbDrone.SignalR; - -namespace NzbDrone.Api.Tags -{ - public class TagModule : NzbDroneRestModuleWithSignalR, IHandle - { - private readonly ITagService _tagService; - - public TagModule(IBroadcastSignalRMessage signalRBroadcaster, - ITagService tagService) - : base(signalRBroadcaster) - { - _tagService = tagService; - - GetResourceById = GetTag; - GetResourceAll = GetAllTags; - CreateResource = CreateTag; - UpdateResource = UpdateTag; - DeleteResource = DeleteTag; - } - - private TagResource GetTag(int id) - { - return _tagService.GetTag(id).ToResource(); - } - - private List GetAllTags() - { - return _tagService.All().ToResource(); - } - - private int CreateTag(TagResource resource) - { - var model = resource.ToModel(); - - return _tagService.Add(model).Id; - } - - private void UpdateTag(TagResource resource) - { - var model = resource.ToModel(); - - _tagService.Update(model); - } - - private void DeleteTag(int id) - { - _tagService.Delete(id); - } - - public void Handle(TagsUpdatedEvent message) - { - BroadcastResourceChange(ModelAction.Sync); - } - } -} diff --git a/src/NzbDrone.Api/TrackFiles/TrackFileModule.cs b/src/NzbDrone.Api/TrackFiles/TrackFileModule.cs deleted file mode 100644 index 6cbf8e09a..000000000 --- a/src/NzbDrone.Api/TrackFiles/TrackFileModule.cs +++ /dev/null @@ -1,100 +0,0 @@ -using System.Collections.Generic; -using System.IO; -using NLog; -using NzbDrone.Api.REST; -using NzbDrone.Common.Disk; -using NzbDrone.Common.Extensions; -using NzbDrone.Core.Datastore.Events; -using NzbDrone.Core.MediaFiles; -using NzbDrone.Core.MediaFiles.Events; -using NzbDrone.Core.Messaging.Events; -using NzbDrone.Core.Tv; -using NzbDrone.Core.Music; -using NzbDrone.Core.DecisionEngine; -using NzbDrone.SignalR; -using System; - -namespace NzbDrone.Api.TrackFiles -{ - public class TrackFileModule : NzbDroneRestModuleWithSignalR, - IHandle - { - private readonly IMediaFileService _mediaFileService; - private readonly IDiskProvider _diskProvider; - private readonly IRecycleBinProvider _recycleBinProvider; - private readonly ISeriesService _seriesService; - private readonly IArtistService _artistService; - private readonly IQualityUpgradableSpecification _qualityUpgradableSpecification; - private readonly Logger _logger; - - public TrackFileModule(IBroadcastSignalRMessage signalRBroadcaster, - IMediaFileService mediaFileService, - IDiskProvider diskProvider, - IRecycleBinProvider recycleBinProvider, - ISeriesService seriesService, - IArtistService artistService, - IQualityUpgradableSpecification qualityUpgradableSpecification, - Logger logger) - : base(signalRBroadcaster) - { - _mediaFileService = mediaFileService; - _diskProvider = diskProvider; - _recycleBinProvider = recycleBinProvider; - _seriesService = seriesService; - _artistService = artistService; - _qualityUpgradableSpecification = qualityUpgradableSpecification; - _logger = logger; - GetResourceById = GetTrackFile; - GetResourceAll = GetTrackFiles; - UpdateResource = SetQuality; - DeleteResource = DeleteTrackFile; - } - - private TrackFileResource GetTrackFile(int id) - { - var trackFile = _mediaFileService.Get(id); - var artist = _artistService.GetArtist(trackFile.ArtistId); - - return trackFile.ToResource(artist, _qualityUpgradableSpecification); - } - - private List GetTrackFiles() - { - if (!Request.Query.ArtistId.HasValue) - { - throw new BadRequestException("artistId is missing"); - } - - var artistId = (int)Request.Query.ArtistId; - - var artist = _artistService.GetArtist(artistId); - - return _mediaFileService.GetFilesByArtist(artistId).ConvertAll(f => f.ToResource(artist, _qualityUpgradableSpecification)); - } - - private void SetQuality(TrackFileResource trackFileResource) - { - var trackFile = _mediaFileService.Get(trackFileResource.Id); - trackFile.Quality = trackFileResource.Quality; - _mediaFileService.Update(trackFile); - } - - private void DeleteTrackFile(int id) - { - throw new NotImplementedException(); - //var episodeFile = _mediaFileService.Get(id); - //var series = _seriesService.GetSeries(episodeFile.SeriesId); - //var fullPath = Path.Combine(series.Path, episodeFile.RelativePath); - //var subfolder = _diskProvider.GetParentFolder(series.Path).GetRelativePath(_diskProvider.GetParentFolder(fullPath)); - - //_logger.Info("Deleting episode file: {0}", fullPath); - //_recycleBinProvider.DeleteFile(fullPath, subfolder); - //_mediaFileService.Delete(episodeFile, DeleteMediaFileReason.Manual); - } - - public void Handle(TrackFileAddedEvent message) - { - BroadcastResourceChange(ModelAction.Updated, message.TrackFile.Id); - } - } -} \ No newline at end of file diff --git a/src/NzbDrone.Api/TrackFiles/TrackFileResource.cs b/src/NzbDrone.Api/TrackFiles/TrackFileResource.cs deleted file mode 100644 index 4c75dc429..000000000 --- a/src/NzbDrone.Api/TrackFiles/TrackFileResource.cs +++ /dev/null @@ -1,64 +0,0 @@ -using System; -using System.IO; -using NzbDrone.Api.REST; -using NzbDrone.Core.Qualities; - -namespace NzbDrone.Api.TrackFiles -{ - public class TrackFileResource : RestResource - { - public int ArtistId { get; set; } - public int AlbumId { get; set; } - public string RelativePath { get; set; } - public string Path { get; set; } - public long Size { get; set; } - public DateTime DateAdded { get; set; } - //public string SceneName { get; set; } - public QualityModel Quality { get; set; } - - public bool QualityCutoffNotMet { get; set; } - } - - public static class TrackFileResourceMapper - { - private static TrackFileResource ToResource(this Core.MediaFiles.TrackFile model) - { - if (model == null) return null; - - return new TrackFileResource - { - Id = model.Id, - - ArtistId = model.ArtistId, - AlbumId = model.AlbumId, - RelativePath = model.RelativePath, - //Path - Size = model.Size, - DateAdded = model.DateAdded, - //SceneName = model.SceneName, - Quality = model.Quality, - //QualityCutoffNotMet - }; - } - - public static TrackFileResource ToResource(this Core.MediaFiles.TrackFile model, Core.Music.Artist artist, Core.DecisionEngine.IQualityUpgradableSpecification qualityUpgradableSpecification) - { - if (model == null) return null; - - return new TrackFileResource - { - Id = model.Id, - - ArtistId = model.ArtistId, - AlbumId = model.AlbumId, - RelativePath = model.RelativePath, - Path = Path.Combine(artist.Path, model.RelativePath), - Size = model.Size, - DateAdded = model.DateAdded, - //SceneName = model.SceneName, - Quality = model.Quality, - QualityCutoffNotMet = qualityUpgradableSpecification.CutoffNotMet(artist.Profile.Value, model.Quality) - }; - } - } -} diff --git a/src/NzbDrone.Api/Tracks/RenameTrackModule.cs b/src/NzbDrone.Api/Tracks/RenameTrackModule.cs deleted file mode 100644 index 9467ec43e..000000000 --- a/src/NzbDrone.Api/Tracks/RenameTrackModule.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System.Collections.Generic; -using NzbDrone.Api.REST; -using NzbDrone.Core.MediaFiles; - -namespace NzbDrone.Api.Tracks -{ - public class RenameTrackModule : NzbDroneRestModule - { - private readonly IRenameTrackFileService _renameTrackFileService; - - public RenameTrackModule(IRenameTrackFileService renameTrackFileService) - : base("rename") - { - _renameTrackFileService = renameTrackFileService; - - GetResourceAll = GetTracks; - } - - private List GetTracks() - { - if (!Request.Query.ArtistId.HasValue) - { - throw new BadRequestException("artistId is missing"); - } - - var artistId = (int)Request.Query.ArtistId; - - if (Request.Query.AlbumId.HasValue) - { - var albumId = (int)Request.Query.AlbumId; - return _renameTrackFileService.GetRenamePreviews(artistId, albumId).ToResource(); - } - - return _renameTrackFileService.GetRenamePreviews(artistId).ToResource(); - } - } -} diff --git a/src/NzbDrone.Api/Tracks/TrackModule.cs b/src/NzbDrone.Api/Tracks/TrackModule.cs deleted file mode 100644 index fa3222c51..000000000 --- a/src/NzbDrone.Api/Tracks/TrackModule.cs +++ /dev/null @@ -1,40 +0,0 @@ -using System.Collections.Generic; -using NzbDrone.Api.REST; -using NzbDrone.Core.Music; -using NzbDrone.Core.DecisionEngine; -using NzbDrone.SignalR; - -namespace NzbDrone.Api.Tracks -{ - public class TrackModule : TrackModuleWithSignalR - { - public TrackModule(IArtistService artistService, - ITrackService trackService, - IQualityUpgradableSpecification qualityUpgradableSpecification, - IBroadcastSignalRMessage signalRBroadcaster) - : base(trackService, artistService, qualityUpgradableSpecification, signalRBroadcaster) - { - GetResourceAll = GetTracks; - UpdateResource = SetMonitored; - } - - private List GetTracks() - { - if (!Request.Query.ArtistId.HasValue) - { - throw new BadRequestException("artistId is missing"); - } - - var artistId = (int)Request.Query.ArtistId; - - var resources = MapToResource(_trackService.GetTracksByArtist(artistId), false, true); - - return resources; - } - - private void SetMonitored(TrackResource trackResource) - { - _trackService.SetTrackMonitored(trackResource.Id, trackResource.Monitored); - } - } -} diff --git a/src/NzbDrone.Api/Tracks/TrackModuleWithSignalR.cs b/src/NzbDrone.Api/Tracks/TrackModuleWithSignalR.cs deleted file mode 100644 index f5926768e..000000000 --- a/src/NzbDrone.Api/Tracks/TrackModuleWithSignalR.cs +++ /dev/null @@ -1,126 +0,0 @@ -using System.Collections.Generic; -using NzbDrone.Common.Extensions; -using NzbDrone.Api.TrackFiles; -using NzbDrone.Api.Music; -using NzbDrone.Core.Datastore.Events; -using NzbDrone.Core.DecisionEngine; -using NzbDrone.Core.Download; -using NzbDrone.Core.MediaFiles.Events; -using NzbDrone.Core.Messaging.Events; -using NzbDrone.Core.Tv; -using NzbDrone.Core.Music; -using NzbDrone.SignalR; - -namespace NzbDrone.Api.Tracks -{ - public abstract class TrackModuleWithSignalR : NzbDroneRestModuleWithSignalR, - IHandle - { - protected readonly ITrackService _trackService; - protected readonly IArtistService _artistService; - protected readonly IQualityUpgradableSpecification _qualityUpgradableSpecification; - - protected TrackModuleWithSignalR(ITrackService trackService, - IArtistService artistService, - IQualityUpgradableSpecification qualityUpgradableSpecification, - IBroadcastSignalRMessage signalRBroadcaster) - : base(signalRBroadcaster) - { - _trackService = trackService; - _artistService = artistService; - _qualityUpgradableSpecification = qualityUpgradableSpecification; - - GetResourceById = GetTrack; - } - - protected TrackModuleWithSignalR(ITrackService trackService, - IArtistService artistService, - IQualityUpgradableSpecification qualityUpgradableSpecification, - IBroadcastSignalRMessage signalRBroadcaster, - string resource) - : base(signalRBroadcaster, resource) - { - _trackService = trackService; - _artistService = artistService; - _qualityUpgradableSpecification = qualityUpgradableSpecification; - - GetResourceById = GetTrack; - } - - protected TrackResource GetTrack(int id) - { - var track = _trackService.GetTrack(id); - var resource = MapToResource(track, true, true); - return resource; - } - - protected TrackResource MapToResource(Track track, bool includeArtist, bool includeTrackFile) - { - var resource = track.ToResource(); - - if (includeArtist || includeTrackFile) - { - var artist = track.Artist ?? _artistService.GetArtist(track.ArtistId); - - if (includeArtist) - { - resource.Artist = artist.ToResource(); - } - if (includeTrackFile && track.TrackFileId != 0) - { - resource.TrackFile = track.TrackFile.Value.ToResource(artist, _qualityUpgradableSpecification); - } - } - - return resource; - } - - protected List MapToResource(List tracks, bool includeArtist, bool includeTrackFile) - { - var result = tracks.ToResource(); - - if (includeArtist || includeTrackFile) - { - var artistDict = new Dictionary(); - for (var i = 0; i < tracks.Count; i++) - { - var track = tracks[i]; - var resource = result[i]; - - var artist = track.Artist ?? artistDict.GetValueOrDefault(tracks[i].ArtistId) ?? _artistService.GetArtist(tracks[i].ArtistId); - artistDict[artist.Id] = artist; - - if (includeArtist) - { - resource.Artist = artist.ToResource(); - } - if (includeTrackFile && tracks[i].TrackFileId != 0) - { - resource.TrackFile = tracks[i].TrackFile.Value.ToResource(artist, _qualityUpgradableSpecification); - } - } - } - - return result; - } - - //public void Handle(TrackGrabbedEvent message) - //{ - // foreach (var track in message.Track.Tracks) - // { - // var resource = track.ToResource(); - // resource.Grabbed = true; - - // BroadcastResourceChange(ModelAction.Updated, resource); - // } - //} - - public void Handle(TrackDownloadedEvent message) - { - foreach (var track in message.Track.Tracks) - { - BroadcastResourceChange(ModelAction.Updated, track.Id); - } - } - } -} diff --git a/src/NzbDrone.Api/Wanted/CutoffModule.cs b/src/NzbDrone.Api/Wanted/CutoffModule.cs deleted file mode 100644 index d2d08edab..000000000 --- a/src/NzbDrone.Api/Wanted/CutoffModule.cs +++ /dev/null @@ -1,42 +0,0 @@ -using NzbDrone.Api.Episodes; -using NzbDrone.Core.Datastore; -using NzbDrone.Core.DecisionEngine; -using NzbDrone.Core.Tv; -using NzbDrone.SignalR; - -namespace NzbDrone.Api.Wanted -{ - public class CutoffModule : EpisodeModuleWithSignalR - { - private readonly IEpisodeCutoffService _episodeCutoffService; - - public CutoffModule(IEpisodeCutoffService episodeCutoffService, - IEpisodeService episodeService, - ISeriesService seriesService, - IQualityUpgradableSpecification qualityUpgradableSpecification, - IBroadcastSignalRMessage signalRBroadcaster) - : base(episodeService, seriesService, qualityUpgradableSpecification, signalRBroadcaster, "wanted/cutoff") - { - _episodeCutoffService = episodeCutoffService; - GetResourcePaged = GetCutoffUnmetEpisodes; - } - - private PagingResource GetCutoffUnmetEpisodes(PagingResource pagingResource) - { - var pagingSpec = pagingResource.MapToPagingSpec("airDateUtc", SortDirection.Descending); - - if (pagingResource.FilterKey == "monitored" && pagingResource.FilterValue == "false") - { - pagingSpec.FilterExpression = v => v.Monitored == false || v.Series.Monitored == false; - } - else - { - pagingSpec.FilterExpression = v => v.Monitored == true && v.Series.Monitored == true; - } - - var resource = ApplyToPage(_episodeCutoffService.EpisodesWhereCutoffUnmet, pagingSpec, v => MapToResource(v, true, true)); - - return resource; - } - } -} diff --git a/src/NzbDrone.Api/Wanted/LegacyMissingModule.cs b/src/NzbDrone.Api/Wanted/LegacyMissingModule.cs deleted file mode 100644 index a5a503a5d..000000000 --- a/src/NzbDrone.Api/Wanted/LegacyMissingModule.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System.Text; -using Nancy; - -namespace NzbDrone.Api.Wanted -{ - class LegacyMissingModule : NzbDroneApiModule - { - public LegacyMissingModule() : base("missing") - { - Get["/"] = x => - { - string queryString = ConvertQueryParams(Request.Query); - var url = string.Format("/api/wanted/missing?{0}", queryString); - - return Response.AsRedirect(url); - }; - } - - private string ConvertQueryParams(DynamicDictionary query) - { - var sb = new StringBuilder(); - - foreach (var key in query) - { - var value = query[key]; - - sb.AppendFormat("&{0}={1}", key, value); - } - - return sb.ToString().Trim('&'); - } - } -} diff --git a/src/NzbDrone.Api/Wanted/MissingModule.cs b/src/NzbDrone.Api/Wanted/MissingModule.cs deleted file mode 100644 index 018ecec62..000000000 --- a/src/NzbDrone.Api/Wanted/MissingModule.cs +++ /dev/null @@ -1,41 +0,0 @@ -using NzbDrone.Api.Episodes; -using NzbDrone.Api.Albums; -using NzbDrone.Core.Datastore; -using NzbDrone.Core.DecisionEngine; -using NzbDrone.Core.ArtistStats; -using NzbDrone.Core.Music; -using NzbDrone.SignalR; - -namespace NzbDrone.Api.Wanted -{ - public class MissingModule : AlbumModuleWithSignalR - { - public MissingModule(IAlbumService albumService, - IArtistStatisticsService artistStatisticsService, - IArtistService artistService, - IQualityUpgradableSpecification qualityUpgradableSpecification, - IBroadcastSignalRMessage signalRBroadcaster) - : base(albumService, artistStatisticsService, artistService, qualityUpgradableSpecification, signalRBroadcaster, "wanted/missing") - { - GetResourcePaged = GetMissingAlbums; - } - - private PagingResource GetMissingAlbums(PagingResource pagingResource) - { - var pagingSpec = pagingResource.MapToPagingSpec("releaseDate", SortDirection.Descending); - - if (pagingResource.FilterKey == "monitored" && pagingResource.FilterValue == "false") - { - pagingSpec.FilterExpression = v => v.Monitored == false || v.Artist.Monitored == false; - } - else - { - pagingSpec.FilterExpression = v => v.Monitored == true && v.Artist.Monitored == true; - } - - var resource = ApplyToPage(_albumService.AlbumsWithoutFiles, pagingSpec, v => MapToResource(v, true)); - - return resource; - } - } -} diff --git a/src/NzbDrone.Api/app.config b/src/NzbDrone.Api/app.config deleted file mode 100644 index c1684a7be..000000000 --- a/src/NzbDrone.Api/app.config +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/NzbDrone.Api/packages.config b/src/NzbDrone.Api/packages.config deleted file mode 100644 index f68f69f6b..000000000 --- a/src/NzbDrone.Api/packages.config +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/src/NzbDrone.App.Test/ContainerFixture.cs b/src/NzbDrone.App.Test/ContainerFixture.cs index 1064d1c5b..0cd54bd0d 100644 --- a/src/NzbDrone.App.Test/ContainerFixture.cs +++ b/src/NzbDrone.App.Test/ContainerFixture.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using NUnit.Framework; using NzbDrone.Common; using NzbDrone.Common.EnvironmentInfo; @@ -76,8 +76,8 @@ namespace NzbDrone.App.Test [Test] public void should_return_same_instance_of_singletons_by_different_same_interface() { - var first = _container.ResolveAll>().OfType().Single(); - var second = _container.ResolveAll>().OfType().Single(); + var first = _container.ResolveAll>().OfType().Single(); + var second = _container.ResolveAll>().OfType().Single(); first.Should().BeSameAs(second); } @@ -85,10 +85,10 @@ namespace NzbDrone.App.Test [Test] public void should_return_same_instance_of_singletons_by_different_interfaces() { - var first = _container.ResolveAll>().OfType().Single(); + var first = _container.ResolveAll>().OfType().Single(); var second = (DownloadMonitoringService)_container.Resolve>(); first.Should().BeSameAs(second); } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.App.Test/NzbDrone.Host.Test.csproj b/src/NzbDrone.App.Test/NzbDrone.Host.Test.csproj index 1d6e658f9..30cc080cb 100644 --- a/src/NzbDrone.App.Test/NzbDrone.Host.Test.csproj +++ b/src/NzbDrone.App.Test/NzbDrone.Host.Test.csproj @@ -1,5 +1,5 @@  - + Debug x86 @@ -9,11 +9,12 @@ Library Properties NzbDrone.App.Test - NzbDrone.App.Test - v4.0 + Lidarr.App.Test + v4.6.1 512 ..\ true + true @@ -25,6 +26,7 @@ MinimumRecommendedRules.ruleset 4 false + false bin\x86\Release\ @@ -35,32 +37,32 @@ prompt MinimumRecommendedRules.ruleset 4 + false ..\packages\NBuilder.4.0.0\lib\net40\FizzWare.NBuilder.dll - True - ..\packages\FluentAssertions.4.19.0\lib\net40\FluentAssertions.dll + ..\packages\FluentAssertions.4.19.0\lib\net45\FluentAssertions.dll - ..\packages\FluentAssertions.4.19.0\lib\net40\FluentAssertions.Core.dll + ..\packages\FluentAssertions.4.19.0\lib\net45\FluentAssertions.Core.dll + + + ..\packages\Moq.4.0.10827\lib\NET40\Moq.dll - ..\packages\NLog.4.4.3\lib\net40\NLog.dll + ..\packages\NLog.4.4.12\lib\net45\NLog.dll - ..\packages\NUnit.3.6.0\lib\net40\nunit.framework.dll + ..\packages\NUnit.3.6.0\lib\net45\nunit.framework.dll - - ..\packages\Moq.4.0.10827\lib\NET40\Moq.dll - diff --git a/src/NzbDrone.App.Test/NzbDroneProcessServiceFixture.cs b/src/NzbDrone.App.Test/NzbDroneProcessServiceFixture.cs index 1ee1ee522..fed0c03e6 100644 --- a/src/NzbDrone.App.Test/NzbDroneProcessServiceFixture.cs +++ b/src/NzbDrone.App.Test/NzbDroneProcessServiceFixture.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using Moq; using NUnit.Framework; using NzbDrone.Common.Model; @@ -20,11 +20,11 @@ namespace NzbDrone.App.Test .Returns(new ProcessInfo() { Id = CURRENT_PROCESS_ID }); Mocker.GetMock() - .Setup(s => s.FindProcessByName(ProcessProvider.NZB_DRONE_CONSOLE_PROCESS_NAME)) + .Setup(s => s.FindProcessByName(ProcessProvider.LIDARR_CONSOLE_PROCESS_NAME)) .Returns(new List()); Mocker.GetMock() - .Setup(s => s.FindProcessByName(ProcessProvider.NZB_DRONE_PROCESS_NAME)) + .Setup(s => s.FindProcessByName(ProcessProvider.LIDARR_PROCESS_NAME)) .Returns(new List()); } @@ -47,7 +47,7 @@ namespace NzbDrone.App.Test public void should_enforce_if_another_console_is_running() { Mocker.GetMock() - .Setup(c => c.FindProcessByName(ProcessProvider.NZB_DRONE_CONSOLE_PROCESS_NAME)) + .Setup(c => c.FindProcessByName(ProcessProvider.LIDARR_CONSOLE_PROCESS_NAME)) .Returns(new List { new ProcessInfo {Id = 10}, @@ -63,7 +63,7 @@ namespace NzbDrone.App.Test public void should_return_false_if_another_gui_is_running() { Mocker.GetMock() - .Setup(c => c.FindProcessByName(ProcessProvider.NZB_DRONE_PROCESS_NAME)) + .Setup(c => c.FindProcessByName(ProcessProvider.LIDARR_PROCESS_NAME)) .Returns(new List { new ProcessInfo {Id = CURRENT_PROCESS_ID}, diff --git a/src/NzbDrone.App.Test/Properties/AssemblyInfo.cs b/src/NzbDrone.App.Test/Properties/AssemblyInfo.cs index 86a324eef..3c316c92f 100644 --- a/src/NzbDrone.App.Test/Properties/AssemblyInfo.cs +++ b/src/NzbDrone.App.Test/Properties/AssemblyInfo.cs @@ -1,14 +1,14 @@ -using System.Reflection; +using System.Reflection; using System.Runtime.InteropServices; // General Information about an assembly is controlled through the following // set of attributes. Change these attribute values to modify the information // associated with an assembly. -[assembly: AssemblyTitle("NzbDrone.App.Test")] +[assembly: AssemblyTitle("Lidarr.App.Test")] [assembly: AssemblyDescription("")] [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("Microsoft")] -[assembly: AssemblyProduct("NzbDrone.App.Test")] +[assembly: AssemblyProduct("Lidarr.App.Test")] [assembly: AssemblyCopyright("Copyright © Microsoft 2011")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] @@ -21,4 +21,3 @@ using System.Runtime.InteropServices; // The following GUID is for the ID of the typelib if this project is exposed to COM [assembly: Guid("b47d34ef-05e8-4826-8a57-9dd05106c964")] -[assembly: AssemblyVersion("10.0.0.*")] diff --git a/src/NzbDrone.App.Test/RouterTest.cs b/src/NzbDrone.App.Test/RouterTest.cs index 0cf7b6c3d..de1f7db63 100644 --- a/src/NzbDrone.App.Test/RouterTest.cs +++ b/src/NzbDrone.App.Test/RouterTest.cs @@ -1,8 +1,9 @@ -using System.ServiceProcess; +using System.ServiceProcess; using Moq; using NUnit.Framework; using NzbDrone.Common; using NzbDrone.Common.EnvironmentInfo; +using NzbDrone.Common.Processes; using NzbDrone.Host; using NzbDrone.Test.Common; @@ -22,14 +23,17 @@ namespace NzbDrone.App.Test public void Route_should_call_install_service_when_application_mode_is_install() { var serviceProviderMock = Mocker.GetMock(MockBehavior.Strict); - serviceProviderMock.Setup(c => c.Install(ServiceProvider.NZBDRONE_SERVICE_NAME)); - serviceProviderMock.Setup(c => c.ServiceExist(ServiceProvider.NZBDRONE_SERVICE_NAME)).Returns(false); - serviceProviderMock.Setup(c => c.Start(ServiceProvider.NZBDRONE_SERVICE_NAME)); + serviceProviderMock.Setup(c => c.ServiceExist(ServiceProvider.SERVICE_NAME)).Returns(false); + serviceProviderMock.Setup(c => c.Install(ServiceProvider.SERVICE_NAME)); + serviceProviderMock.Setup(c => c.SetPermissions(ServiceProvider.SERVICE_NAME)); + + Mocker.GetMock() + .Setup(c => c.SpawnNewProcess("sc.exe", It.IsAny(), null, true)); Mocker.GetMock().SetupGet(c => c.IsUserInteractive).Returns(true); Subject.Route(ApplicationModes.InstallService); - serviceProviderMock.Verify(c => c.Install(ServiceProvider.NZBDRONE_SERVICE_NAME), Times.Once()); + serviceProviderMock.Verify(c => c.Install(ServiceProvider.SERVICE_NAME), Times.Once()); } @@ -37,13 +41,13 @@ namespace NzbDrone.App.Test public void Route_should_call_uninstall_service_when_application_mode_is_uninstall() { var serviceProviderMock = Mocker.GetMock(); - serviceProviderMock.Setup(c => c.UnInstall(ServiceProvider.NZBDRONE_SERVICE_NAME)); + serviceProviderMock.Setup(c => c.Uninstall(ServiceProvider.SERVICE_NAME)); Mocker.GetMock().SetupGet(c => c.IsUserInteractive).Returns(true); - serviceProviderMock.Setup(c => c.ServiceExist(ServiceProvider.NZBDRONE_SERVICE_NAME)).Returns(true); + serviceProviderMock.Setup(c => c.ServiceExist(ServiceProvider.SERVICE_NAME)).Returns(true); Subject.Route(ApplicationModes.UninstallService); - serviceProviderMock.Verify(c => c.UnInstall(ServiceProvider.NZBDRONE_SERVICE_NAME), Times.Once()); + serviceProviderMock.Verify(c => c.Uninstall(ServiceProvider.SERVICE_NAME), Times.Once()); } [Test] @@ -82,7 +86,7 @@ namespace NzbDrone.App.Test Mocker.GetMock().SetupGet(c => c.IsUserInteractive).Returns(true); consoleMock.Setup(c => c.PrintServiceAlreadyExist()); - serviceMock.Setup(c => c.ServiceExist(ServiceProvider.NZBDRONE_SERVICE_NAME)).Returns(true); + serviceMock.Setup(c => c.ServiceExist(ServiceProvider.SERVICE_NAME)).Returns(true); Subject.Route(ApplicationModes.InstallService); @@ -96,7 +100,7 @@ namespace NzbDrone.App.Test Mocker.GetMock().SetupGet(c => c.IsUserInteractive).Returns(true); consoleMock.Setup(c => c.PrintServiceDoesNotExist()); - serviceMock.Setup(c => c.ServiceExist(ServiceProvider.NZBDRONE_SERVICE_NAME)).Returns(false); + serviceMock.Setup(c => c.ServiceExist(ServiceProvider.SERVICE_NAME)).Returns(false); Subject.Route(ApplicationModes.UninstallService); diff --git a/src/NzbDrone.App.Test/packages.config b/src/NzbDrone.App.Test/packages.config index dc7aef2ad..045dc1d94 100644 --- a/src/NzbDrone.App.Test/packages.config +++ b/src/NzbDrone.App.Test/packages.config @@ -1,8 +1,8 @@  - - - - - + + + + + \ No newline at end of file diff --git a/src/NzbDrone.Automation.Test/NzbDrone.Automation.Test.csproj b/src/NzbDrone.Automation.Test/NzbDrone.Automation.Test.csproj index d3861c667..34e239a38 100644 --- a/src/NzbDrone.Automation.Test/NzbDrone.Automation.Test.csproj +++ b/src/NzbDrone.Automation.Test/NzbDrone.Automation.Test.csproj @@ -1,5 +1,5 @@  - + Debug @@ -8,13 +8,14 @@ Library Properties NzbDrone.Automation.Test - NzbDrone.Automation.Test - v4.0 + Lidarr.Automation.Test + v4.6.1 512 ..\ true 12.0.0 2.0 + true @@ -26,6 +27,7 @@ MinimumRecommendedRules.ruleset 4 false + false bin\x86\Release\ @@ -36,19 +38,20 @@ prompt MinimumRecommendedRules.ruleset 4 + false - ..\packages\FluentAssertions.4.19.0\lib\net40\FluentAssertions.dll + ..\packages\FluentAssertions.4.19.0\lib\net45\FluentAssertions.dll - ..\packages\FluentAssertions.4.19.0\lib\net40\FluentAssertions.Core.dll + ..\packages\FluentAssertions.4.19.0\lib\net45\FluentAssertions.Core.dll - ..\packages\NLog.4.4.3\lib\net40\NLog.dll + ..\packages\NLog.4.4.12\lib\net45\NLog.dll - ..\packages\NUnit.3.6.0\lib\net40\nunit.framework.dll + ..\packages\NUnit.3.6.0\lib\net45\nunit.framework.dll diff --git a/src/NzbDrone.Automation.Test/Properties/AssemblyInfo.cs b/src/NzbDrone.Automation.Test/Properties/AssemblyInfo.cs index a5d255084..cea10d8a1 100644 --- a/src/NzbDrone.Automation.Test/Properties/AssemblyInfo.cs +++ b/src/NzbDrone.Automation.Test/Properties/AssemblyInfo.cs @@ -1,14 +1,14 @@ -using System.Reflection; +using System.Reflection; using System.Runtime.InteropServices; // General Information about an assembly is controlled through the following // set of attributes. Change these attribute values to modify the information // associated with an assembly. -[assembly: AssemblyTitle("NzbDrone.Automation.Test")] +[assembly: AssemblyTitle("Lidarr.Automation.Test")] [assembly: AssemblyDescription("")] [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("NzbDrone.Automation.Test")] +[assembly: AssemblyProduct("Lidarr.Automation.Test")] [assembly: AssemblyCopyright("Copyright © 2013")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] diff --git a/src/NzbDrone.Automation.Test/app.config b/src/NzbDrone.Automation.Test/app.config index c1684a7be..077a6a061 100644 --- a/src/NzbDrone.Automation.Test/app.config +++ b/src/NzbDrone.Automation.Test/app.config @@ -10,6 +10,10 @@ + + + + - \ No newline at end of file + diff --git a/src/NzbDrone.Automation.Test/packages.config b/src/NzbDrone.Automation.Test/packages.config index c5405c724..fb60eeeaf 100644 --- a/src/NzbDrone.Automation.Test/packages.config +++ b/src/NzbDrone.Automation.Test/packages.config @@ -1,8 +1,8 @@  - - - - - + + + + + \ No newline at end of file diff --git a/src/NzbDrone.Common.Test/ExtensionTests/UrlExtensionsFixture.cs b/src/NzbDrone.Common.Test/ExtensionTests/UrlExtensionsFixture.cs new file mode 100644 index 000000000..eae0736dc --- /dev/null +++ b/src/NzbDrone.Common.Test/ExtensionTests/UrlExtensionsFixture.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using FluentAssertions; +using NUnit.Framework; +using NzbDrone.Common.Extensions; + +namespace NzbDrone.Common.Test.ExtensionTests +{ + [TestFixture] + public class UrlExtensionsFixture + { + [TestCase("http://my.local/url")] + [TestCase("https://my.local/url")] + public void should_report_as_valid_url(string url) + { + url.IsValidUrl().Should().BeTrue(); + } + + [TestCase("")] + [TestCase(" http://my.local/url")] + [TestCase("http://my.local/url ")] + public void should_report_as_invalid_url(string url) + { + url.IsValidUrl().Should().BeFalse(); + } + } +} diff --git a/src/NzbDrone.Common.Test/Http/HttpClientFixture.cs b/src/NzbDrone.Common.Test/Http/HttpClientFixture.cs index 823e5cdd9..7d79dd656 100644 --- a/src/NzbDrone.Common.Test/Http/HttpClientFixture.cs +++ b/src/NzbDrone.Common.Test/Http/HttpClientFixture.cs @@ -61,7 +61,7 @@ namespace NzbDrone.Common.Test.Http response.Content.Should().NotBeNullOrWhiteSpace(); } - + [Test] public void should_execute_https_get() { @@ -139,7 +139,54 @@ namespace NzbDrone.Common.Test.Http var request = new HttpRequest($"http://{_httpBinHost}/redirect/1"); request.AllowAutoRedirect = true; - Subject.Get(request); + var response = Subject.Get(request); + + response.StatusCode.Should().Be(HttpStatusCode.OK); + + ExceptionVerification.ExpectedErrors(0); + } + + [Test] + public void should_not_follow_redirects() + { + var request = new HttpRequest($"http://{_httpBinHost}/redirect/1"); + request.AllowAutoRedirect = false; + + var response = Subject.Get(request); + + response.StatusCode.Should().Be(HttpStatusCode.Found); + + ExceptionVerification.ExpectedErrors(1); + } + + [Test] + public void should_follow_redirects_to_https() + { + if (typeof(TDispatcher) == typeof(ManagedHttpDispatcher) && PlatformInfo.IsMono) + { + Assert.Ignore("Will fail on tls1.2 via managed dispatcher, ignore."); + } + + var request = new HttpRequestBuilder($"http://{_httpBinHost}/redirect-to") + .AddQueryParam("url", $"https://sonarr.tv/") + .Build(); + request.AllowAutoRedirect = true; + + var response = Subject.Get(request); + + response.StatusCode.Should().Be(HttpStatusCode.OK); + response.Content.Should().Contain("Sonarr"); + + ExceptionVerification.ExpectedErrors(0); + } + + [Test] + public void should_throw_on_too_many_redirects() + { + var request = new HttpRequest($"http://{_httpBinHost}/redirect/4"); + request.AllowAutoRedirect = true; + + Assert.Throws(() => Subject.Get(request)); ExceptionVerification.ExpectedErrors(0); } @@ -407,4 +454,4 @@ namespace NzbDrone.Common.Test.Http public string Url { get; set; } public string Data { get; set; } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Common.Test/Http/HttpUriFixture.cs b/src/NzbDrone.Common.Test/Http/HttpUriFixture.cs index 099ab990f..b76f6ca1f 100644 --- a/src/NzbDrone.Common.Test/Http/HttpUriFixture.cs +++ b/src/NzbDrone.Common.Test/Http/HttpUriFixture.cs @@ -1,4 +1,4 @@ -using FluentAssertions; +using FluentAssertions; using NUnit.Framework; using NzbDrone.Common.Http; using NzbDrone.Test.Common; @@ -7,6 +7,13 @@ namespace NzbDrone.Common.Test.Http { public class HttpUriFixture : TestBase { + [TestCase("abc://my_host.com:8080/root/api/")] + public void should_parse(string uri) + { + var newUri = new HttpUri(uri); + newUri.FullUri.Should().Be(uri); + } + [TestCase("", "", "")] [TestCase("/", "", "/")] [TestCase("base", "", "base")] diff --git a/src/NzbDrone.Common.Test/InstrumentationTests/CleanseLogMessageFixture.cs b/src/NzbDrone.Common.Test/InstrumentationTests/CleanseLogMessageFixture.cs index 6031f5ba1..fbf8fdee0 100644 --- a/src/NzbDrone.Common.Test/InstrumentationTests/CleanseLogMessageFixture.cs +++ b/src/NzbDrone.Common.Test/InstrumentationTests/CleanseLogMessageFixture.cs @@ -15,6 +15,7 @@ namespace NzbDrone.Common.Test.InstrumentationTests [TestCase(@"https://rss.omgwtfnzbs.org/rss-search.php?catid=19,20&user=Lidarr&api=mySecret&eng=1")] [TestCase(@"https://dognzb.cr/fetch/2b51db35e1912ffc138825a12b9933d2/2b51db35e1910123321025a12b9933d2")] [TestCase(@"https://baconbits.org/feeds.php?feed=torrents_tv&user=12345&auth=2b51db35e1910123321025a12b9933d2&passkey=mySecret&authkey=2b51db35e1910123321025a12b9933d2")] + [TestCase(@"http://127.0.0.1:9117/dl/indexername?jackett_apikey=flwjiefewklfjacketmySecretsdfldskjfsdlk&path=we0re9f0sdfbase64sfdkfjsdlfjk&file=The+Torrent+File+Name.torrent")] // NzbGet [TestCase(@"{ ""Name"" : ""ControlUsername"", ""Value"" : ""mySecret"" }, { ""Name"" : ""ControlPassword"", ""Value"" : ""mySecret"" }, ")] [TestCase(@"{ ""Name"" : ""Server1.Username"", ""Value"" : ""mySecret"" }, { ""Name"" : ""Server1.Password"", ""Value"" : ""mySecret"" }, ")] diff --git a/src/NzbDrone.Common.Test/NzbDrone.Common.Test.csproj b/src/NzbDrone.Common.Test/NzbDrone.Common.Test.csproj index 8f80dbe36..952513f98 100644 --- a/src/NzbDrone.Common.Test/NzbDrone.Common.Test.csproj +++ b/src/NzbDrone.Common.Test/NzbDrone.Common.Test.csproj @@ -1,5 +1,5 @@  - + Debug x86 @@ -9,11 +9,12 @@ Library Properties NzbDrone.Common.Test - NzbDrone.Common.Test - v4.0 + Lidarr.Common.Test + v4.6.1 512 ..\ true + true @@ -25,6 +26,7 @@ MinimumRecommendedRules.ruleset 4 false + false bin\x86\Release\ @@ -35,19 +37,23 @@ prompt MinimumRecommendedRules.ruleset 4 + false - ..\packages\FluentAssertions.4.19.0\lib\net40\FluentAssertions.dll + ..\packages\FluentAssertions.4.19.0\lib\net45\FluentAssertions.dll - ..\packages\FluentAssertions.4.19.0\lib\net40\FluentAssertions.Core.dll + ..\packages\FluentAssertions.4.19.0\lib\net45\FluentAssertions.Core.dll + + + ..\packages\Moq.4.0.10827\lib\NET40\Moq.dll - ..\packages\NLog.4.4.3\lib\net40\NLog.dll + ..\packages\NLog.4.4.12\lib\net45\NLog.dll - ..\packages\NUnit.3.6.0\lib\net40\nunit.framework.dll + ..\packages\NUnit.3.6.0\lib\net45\nunit.framework.dll @@ -57,9 +63,6 @@ - - ..\packages\Moq.4.0.10827\lib\NET40\Moq.dll - @@ -80,6 +83,7 @@ + diff --git a/src/NzbDrone.Common.Test/PathExtensionFixture.cs b/src/NzbDrone.Common.Test/PathExtensionFixture.cs index 5922574fb..70c75aec4 100644 --- a/src/NzbDrone.Common.Test/PathExtensionFixture.cs +++ b/src/NzbDrone.Common.Test/PathExtensionFixture.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.IO; using FluentAssertions; using Moq; @@ -113,6 +113,7 @@ namespace NzbDrone.Common.Test [TestCase(@"C:\Test\", @"C:\Test\mydir")] [TestCase(@"C:\Test\", @"C:\Test\mydir\")] [TestCase(@"C:\Test", @"C:\Test\30.Rock.S01E01.Pilot.avi")] + [TestCase(@"C:\", @"C:\Test\30.Rock.S01E01.Pilot.avi")] public void path_should_be_parent(string parentPath, string childPath) { parentPath.AsOsAgnostic().IsParentPath(childPath.AsOsAgnostic()).Should().BeTrue(); @@ -257,7 +258,7 @@ namespace NzbDrone.Common.Test [Test] public void GetUpdateClientFolder() { - GetIAppDirectoryInfo().GetUpdateClientFolder().Should().BeEquivalentTo(@"C:\Temp\lidarr_update\Lidarr\NzbDrone.Update\".AsOsAgnostic()); + GetIAppDirectoryInfo().GetUpdateClientFolder().Should().BeEquivalentTo(@"C:\Temp\lidarr_update\Lidarr\Lidarr.Update\".AsOsAgnostic()); } [Test] diff --git a/src/NzbDrone.Common.Test/ReflectionTests/ReflectionExtensionFixture.cs b/src/NzbDrone.Common.Test/ReflectionTests/ReflectionExtensionFixture.cs index 3e6c3fab9..4480b6449 100644 --- a/src/NzbDrone.Common.Test/ReflectionTests/ReflectionExtensionFixture.cs +++ b/src/NzbDrone.Common.Test/ReflectionTests/ReflectionExtensionFixture.cs @@ -1,4 +1,4 @@ -using System.Reflection; +using System.Reflection; using FluentAssertions; using NUnit.Framework; using NzbDrone.Common.Reflection; @@ -12,7 +12,7 @@ namespace NzbDrone.Common.Test.ReflectionTests [Test] public void should_get_properties_from_models() { - var models = Assembly.Load("NzbDrone.Core").ImplementationsOf(); + var models = Assembly.Load("Lidarr.Core").ImplementationsOf(); foreach (var model in models) { @@ -23,9 +23,9 @@ namespace NzbDrone.Common.Test.ReflectionTests [Test] public void should_be_able_to_get_implementations() { - var models = Assembly.Load("NzbDrone.Core").ImplementationsOf(); + var models = Assembly.Load("Lidarr.Core").ImplementationsOf(); models.Should().NotBeEmpty(); } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Common.Test/ServiceProviderTests.cs b/src/NzbDrone.Common.Test/ServiceProviderTests.cs index 68d7b1789..4ebf15c93 100644 --- a/src/NzbDrone.Common.Test/ServiceProviderTests.cs +++ b/src/NzbDrone.Common.Test/ServiceProviderTests.cs @@ -36,7 +36,7 @@ namespace NzbDrone.Common.Test { if (Subject.ServiceExist(TEMP_SERVICE_NAME)) { - Subject.UnInstall(TEMP_SERVICE_NAME); + Subject.Uninstall(TEMP_SERVICE_NAME); } if (Subject.IsServiceRunning(ALWAYS_INSTALLED_SERVICE)) @@ -65,7 +65,7 @@ namespace NzbDrone.Common.Test Subject.ServiceExist(TEMP_SERVICE_NAME).Should().BeFalse("Service already installed"); Subject.Install(TEMP_SERVICE_NAME); Subject.ServiceExist(TEMP_SERVICE_NAME).Should().BeTrue(); - Subject.UnInstall(TEMP_SERVICE_NAME); + Subject.Uninstall(TEMP_SERVICE_NAME); Subject.ServiceExist(TEMP_SERVICE_NAME).Should().BeFalse(); ExceptionVerification.ExpectedWarns(1); @@ -76,8 +76,8 @@ namespace NzbDrone.Common.Test [ManualTest] public void UnInstallService() { - Subject.UnInstall(ServiceProvider.NZBDRONE_SERVICE_NAME); - Subject.ServiceExist(ServiceProvider.NZBDRONE_SERVICE_NAME).Should().BeFalse(); + Subject.Uninstall(ServiceProvider.SERVICE_NAME); + Subject.ServiceExist(ServiceProvider.SERVICE_NAME).Should().BeFalse(); } [Test] diff --git a/src/NzbDrone.Common.Test/packages.config b/src/NzbDrone.Common.Test/packages.config index a974434ae..54ebba83a 100644 --- a/src/NzbDrone.Common.Test/packages.config +++ b/src/NzbDrone.Common.Test/packages.config @@ -1,7 +1,7 @@  - - - - + + + + \ No newline at end of file diff --git a/src/NzbDrone.Common/Cloud/LidarrCloudRequestBuilder.cs b/src/NzbDrone.Common/Cloud/LidarrCloudRequestBuilder.cs new file mode 100644 index 000000000..96f771ed4 --- /dev/null +++ b/src/NzbDrone.Common/Cloud/LidarrCloudRequestBuilder.cs @@ -0,0 +1,29 @@ +using NzbDrone.Common.Http; + +namespace NzbDrone.Common.Cloud +{ + public interface ILidarrCloudRequestBuilder + { + IHttpRequestBuilderFactory Services { get; } + IHttpRequestBuilderFactory Search { get; } + IHttpRequestBuilderFactory InternalSearch { get; } + } + + public class LidarrCloudRequestBuilder : ILidarrCloudRequestBuilder + { + public LidarrCloudRequestBuilder() + { + Services = new HttpRequestBuilder("http://services.lidarr.audio/v1/") + .CreateFactory(); + + Search = new HttpRequestBuilder("https://api.lidarr.audio/api/v0/{route}/") // TODO: Add {version} once LidarrAPI.Metadata is released. + .CreateFactory(); + } + + public IHttpRequestBuilderFactory Services { get; } + + public IHttpRequestBuilderFactory Search { get; } + + public IHttpRequestBuilderFactory InternalSearch { get; } + } +} diff --git a/src/NzbDrone.Common/Cloud/SonarrCloudRequestBuilder.cs b/src/NzbDrone.Common/Cloud/SonarrCloudRequestBuilder.cs deleted file mode 100644 index 007ce3945..000000000 --- a/src/NzbDrone.Common/Cloud/SonarrCloudRequestBuilder.cs +++ /dev/null @@ -1,37 +0,0 @@ -using NzbDrone.Common.Http; - -namespace NzbDrone.Common.Cloud -{ - public interface ILidarrCloudRequestBuilder - { - IHttpRequestBuilderFactory Services { get; } - IHttpRequestBuilderFactory Search { get; } - IHttpRequestBuilderFactory InternalSearch { get; } - IHttpRequestBuilderFactory SkyHookTvdb { get; } - } - - public class LidarrCloudRequestBuilder : ILidarrCloudRequestBuilder - { - public LidarrCloudRequestBuilder() - { - Services = new HttpRequestBuilder("http://services.lidarr.tv/v1/") - .CreateFactory(); - - Search = new HttpRequestBuilder("http://localhost:5000/{route}/") // TODO: Add {version} once LidarrAPI.Metadata is released. - .CreateFactory(); - - - SkyHookTvdb = new HttpRequestBuilder("http://skyhook.lidarr.tv/v1/tvdb/{route}/{language}/") - .SetSegment("language", "en") - .CreateFactory(); - } - - public IHttpRequestBuilderFactory Services { get; } - - public IHttpRequestBuilderFactory Search { get; } - - public IHttpRequestBuilderFactory InternalSearch { get; } - - public IHttpRequestBuilderFactory SkyHookTvdb { get; } - } -} diff --git a/src/NzbDrone.Common/Composition/ContainerBuilderBase.cs b/src/NzbDrone.Common/Composition/ContainerBuilderBase.cs index a4174a26f..7323c06f9 100644 --- a/src/NzbDrone.Common/Composition/ContainerBuilderBase.cs +++ b/src/NzbDrone.Common/Composition/ContainerBuilderBase.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Reflection; @@ -18,8 +18,8 @@ namespace NzbDrone.Common.Composition { _loadedTypes = new List(); - assemblies.Add(OsInfo.IsWindows ? "NzbDrone.Windows" : "NzbDrone.Mono"); - assemblies.Add("NzbDrone.Common"); + assemblies.Add(OsInfo.IsWindows ? "Lidarr.Windows" : "Lidarr.Mono"); + assemblies.Add("Lidarr.Common"); foreach (var assembly in assemblies) { diff --git a/src/NzbDrone.Common/ConsoleService.cs b/src/NzbDrone.Common/ConsoleService.cs index ed54db216..7801b21bc 100644 --- a/src/NzbDrone.Common/ConsoleService.cs +++ b/src/NzbDrone.Common/ConsoleService.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Diagnostics; using System.IO; using NzbDrone.Common.EnvironmentInfo; @@ -21,20 +21,22 @@ namespace NzbDrone.Common Console.WriteLine(); Console.WriteLine(" Usage: {0} ", Process.GetCurrentProcess().MainModule.ModuleName); Console.WriteLine(" Commands:"); - Console.WriteLine(" /{0} Install the application as a Windows Service ({1}).", StartupContext.INSTALL_SERVICE, ServiceProvider.NZBDRONE_SERVICE_NAME); - Console.WriteLine(" /{0} Uninstall already installed Windows Service ({1}).", StartupContext.UNINSTALL_SERVICE, ServiceProvider.NZBDRONE_SERVICE_NAME); + Console.WriteLine(" /{0} Install the application as a Windows Service ({1}).", StartupContext.INSTALL_SERVICE, ServiceProvider.SERVICE_NAME); + Console.WriteLine(" /{0} Uninstall already installed Windows Service ({1}).", StartupContext.UNINSTALL_SERVICE, ServiceProvider.SERVICE_NAME); Console.WriteLine(" /{0} Don't open Lidarr in a browser", StartupContext.NO_BROWSER); + Console.WriteLine(" /{0} Start Lidarr terminating any other instances", StartupContext.TERMINATE); + Console.WriteLine(" /{0}=path Path to use as the AppData location (stores database, config, logs, etc)", StartupContext.APPDATA); Console.WriteLine(" Run application in console mode."); } public void PrintServiceAlreadyExist() { - Console.WriteLine("A service with the same name ({0}) already exists. Aborting installation", ServiceProvider.NZBDRONE_SERVICE_NAME); + Console.WriteLine("A service with the same name ({0}) already exists. Aborting installation", ServiceProvider.SERVICE_NAME); } public void PrintServiceDoesNotExist() { - Console.WriteLine("Can't find service ({0})", ServiceProvider.NZBDRONE_SERVICE_NAME); + Console.WriteLine("Can't find service ({0})", ServiceProvider.SERVICE_NAME); } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Common/Disk/DestinationAlreadyExistsException.cs b/src/NzbDrone.Common/Disk/DestinationAlreadyExistsException.cs new file mode 100644 index 000000000..986413742 --- /dev/null +++ b/src/NzbDrone.Common/Disk/DestinationAlreadyExistsException.cs @@ -0,0 +1,29 @@ +using System; +using System.IO; +using System.Runtime.Serialization; + +namespace NzbDrone.Common.Disk +{ + public class DestinationAlreadyExistsException : IOException + { + public DestinationAlreadyExistsException() + { + } + + public DestinationAlreadyExistsException(string message) : base(message) + { + } + + public DestinationAlreadyExistsException(string message, int hresult) : base(message, hresult) + { + } + + public DestinationAlreadyExistsException(string message, Exception innerException) : base(message, innerException) + { + } + + protected DestinationAlreadyExistsException(SerializationInfo info, StreamingContext context) : base(info, context) + { + } + } +} diff --git a/src/NzbDrone.Common/Disk/DiskTransferService.cs b/src/NzbDrone.Common/Disk/DiskTransferService.cs index 7bfe67ed1..baf7134a2 100644 --- a/src/NzbDrone.Common/Disk/DiskTransferService.cs +++ b/src/NzbDrone.Common/Disk/DiskTransferService.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.IO; using System.Linq; using System.Threading; @@ -340,7 +340,7 @@ namespace NzbDrone.Common.Disk } else { - throw new IOException(string.Format("Destination already exists. [{0}] to [{1}]", sourcePath, targetPath)); + throw new DestinationAlreadyExistsException($"Destination {targetPath} already exists."); } } } diff --git a/src/NzbDrone.Common/Disk/DriveInfoMount.cs b/src/NzbDrone.Common/Disk/DriveInfoMount.cs index ac039d719..1ddd9f43f 100644 --- a/src/NzbDrone.Common/Disk/DriveInfoMount.cs +++ b/src/NzbDrone.Common/Disk/DriveInfoMount.cs @@ -1,4 +1,5 @@ -using System.IO; +using System.Collections.Generic; +using System.IO; using NzbDrone.Common.Extensions; namespace NzbDrone.Common.Disk @@ -8,10 +9,11 @@ namespace NzbDrone.Common.Disk private readonly DriveInfo _driveInfo; private readonly DriveType _driveType; - public DriveInfoMount(DriveInfo driveInfo, DriveType driveType = DriveType.Unknown) + public DriveInfoMount(DriveInfo driveInfo, DriveType driveType = DriveType.Unknown, MountOptions mountOptions = null) { _driveInfo = driveInfo; _driveType = driveType; + MountOptions = mountOptions; } public long AvailableFreeSpace => _driveInfo.AvailableFreeSpace; @@ -33,6 +35,8 @@ namespace NzbDrone.Common.Disk public bool IsReady => _driveInfo.IsReady; + public MountOptions MountOptions { get; private set; } + public string Name => _driveInfo.Name; public string RootDirectory => _driveInfo.RootDirectory.FullName; @@ -47,7 +51,7 @@ namespace NzbDrone.Common.Disk { get { - if (VolumeLabel.IsNullOrWhiteSpace()) + if (VolumeLabel.IsNullOrWhiteSpace() || VolumeLabel.StartsWith("UUID=") || Name == VolumeLabel) { return Name; } diff --git a/src/NzbDrone.Common/Disk/FileSystemLookupService.cs b/src/NzbDrone.Common/Disk/FileSystemLookupService.cs index b262c9918..f58e7942c 100644 --- a/src/NzbDrone.Common/Disk/FileSystemLookupService.cs +++ b/src/NzbDrone.Common/Disk/FileSystemLookupService.cs @@ -1,8 +1,7 @@ -using System; +using System; using System.Collections.Generic; using System.IO; using System.Linq; -using NLog; using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.Extensions; @@ -16,7 +15,6 @@ namespace NzbDrone.Common.Disk public class FileSystemLookupService : IFileSystemLookupService { private readonly IDiskProvider _diskProvider; - private readonly Logger _logger; private readonly HashSet _setToRemove = new HashSet { @@ -48,10 +46,9 @@ namespace NzbDrone.Common.Disk "@eadir" }; - public FileSystemLookupService(IDiskProvider diskProvider, Logger logger) + public FileSystemLookupService(IDiskProvider diskProvider) { _diskProvider = diskProvider; - _logger = logger; } public FileSystemResult LookupContents(string query, bool includeFiles) @@ -154,6 +151,16 @@ namespace NzbDrone.Common.Disk .ToList(); } + private static string GetVolumeName(IMount mountInfo) + { + if (mountInfo.VolumeLabel.IsNullOrWhiteSpace()) + { + return mountInfo.Name; + } + + return $"{mountInfo.Name} ({mountInfo.VolumeLabel})"; + } + private string GetDirectoryPath(string path) { if (path.Last() != Path.DirectorySeparatorChar) @@ -164,7 +171,7 @@ namespace NzbDrone.Common.Disk return path; } - private string GetParent(string path) + private static string GetParent(string path) { var di = new DirectoryInfo(path); diff --git a/src/NzbDrone.Common/Disk/IMount.cs b/src/NzbDrone.Common/Disk/IMount.cs index 285673d69..3b15a4cb2 100644 --- a/src/NzbDrone.Common/Disk/IMount.cs +++ b/src/NzbDrone.Common/Disk/IMount.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using System.IO; namespace NzbDrone.Common.Disk @@ -8,6 +9,7 @@ namespace NzbDrone.Common.Disk string DriveFormat { get; } DriveType DriveType { get; } bool IsReady { get; } + MountOptions MountOptions { get; } string Name { get; } string RootDirectory { get; } long TotalFreeSpace { get; } diff --git a/src/NzbDrone.Common/Disk/MountOptions.cs b/src/NzbDrone.Common/Disk/MountOptions.cs new file mode 100644 index 000000000..749c0a739 --- /dev/null +++ b/src/NzbDrone.Common/Disk/MountOptions.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; + +namespace NzbDrone.Common.Disk +{ + public class MountOptions + { + private readonly Dictionary _options; + + public MountOptions(Dictionary options) + { + _options = options; + } + + public bool IsReadOnly => _options.ContainsKey("ro"); + } +} diff --git a/src/NzbDrone.Common/Exceptions/NotParentException.cs b/src/NzbDrone.Common/Disk/NotParentException.cs similarity index 80% rename from src/NzbDrone.Common/Exceptions/NotParentException.cs rename to src/NzbDrone.Common/Disk/NotParentException.cs index d9b78247e..66dae7789 100644 --- a/src/NzbDrone.Common/Exceptions/NotParentException.cs +++ b/src/NzbDrone.Common/Disk/NotParentException.cs @@ -1,4 +1,6 @@ -namespace NzbDrone.Common.Exceptions +using NzbDrone.Common.Exceptions; + +namespace NzbDrone.Common.Disk { public class NotParentException : NzbDroneException { diff --git a/src/NzbDrone.Common/EnvironmentInfo/AppFolderFactory.cs b/src/NzbDrone.Common/EnvironmentInfo/AppFolderFactory.cs index 7132d539f..0b13dadc4 100644 --- a/src/NzbDrone.Common/EnvironmentInfo/AppFolderFactory.cs +++ b/src/NzbDrone.Common/EnvironmentInfo/AppFolderFactory.cs @@ -1,8 +1,9 @@ -using System; +using System; using System.Security.AccessControl; using System.Security.Principal; using NLog; using NzbDrone.Common.Disk; +using NzbDrone.Common.Exceptions; using NzbDrone.Common.Instrumentation; namespace NzbDrone.Common.EnvironmentInfo @@ -18,7 +19,10 @@ namespace NzbDrone.Common.EnvironmentInfo private readonly IDiskProvider _diskProvider; private readonly Logger _logger; - public AppFolderFactory(IAppFolderInfo appFolderInfo, IDiskProvider diskProvider) + public AppFolderFactory(IAppFolderInfo appFolderInfo, + IStartupContext startupContext, + IDiskProvider diskProvider, + IDiskTransferService diskTransferService) { _appFolderInfo = appFolderInfo; _diskProvider = diskProvider; @@ -33,6 +37,11 @@ namespace NzbDrone.Common.EnvironmentInfo { SetPermissions(); } + + if (!_diskProvider.FolderWritable(_appFolderInfo.AppDataFolder)) + { + throw new LidarrStartupException("AppFolder {0} is not writable", _appFolderInfo.AppDataFolder); + } } private void SetPermissions() diff --git a/src/NzbDrone.Common/EnvironmentInfo/IRuntimeInfo.cs b/src/NzbDrone.Common/EnvironmentInfo/IRuntimeInfo.cs index cb432addc..a8e4bd9ad 100644 --- a/src/NzbDrone.Common/EnvironmentInfo/IRuntimeInfo.cs +++ b/src/NzbDrone.Common/EnvironmentInfo/IRuntimeInfo.cs @@ -1,14 +1,18 @@ -using System; +using System; namespace NzbDrone.Common.EnvironmentInfo { public interface IRuntimeInfo { + DateTime StartTime { get; } bool IsUserInteractive { get; } bool IsAdmin { get; } bool IsWindowsService { get; } + bool IsWindowsTray { get; } bool IsExiting { get; set; } + bool IsTray { get; } + RuntimeMode Mode { get; } bool RestartPending { get; set; } string ExecutingApplication { get; } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Common/EnvironmentInfo/RuntimeInfo.cs b/src/NzbDrone.Common/EnvironmentInfo/RuntimeInfo.cs index a53862311..0c6369a07 100644 --- a/src/NzbDrone.Common/EnvironmentInfo/RuntimeInfo.cs +++ b/src/NzbDrone.Common/EnvironmentInfo/RuntimeInfo.cs @@ -1,25 +1,28 @@ -using System; +using System; using System.Diagnostics; using System.IO; using System.Reflection; using System.Security.Principal; using System.ServiceProcess; using NLog; +using NzbDrone.Common.Processes; namespace NzbDrone.Common.EnvironmentInfo { public class RuntimeInfo : IRuntimeInfo { private readonly Logger _logger; + private readonly DateTime _startTime = DateTime.UtcNow; public RuntimeInfo(IServiceProvider serviceProvider, Logger logger) { _logger = logger; + IsWindowsService = !IsUserInteractive && OsInfo.IsWindows && - serviceProvider.ServiceExist(ServiceProvider.NZBDRONE_SERVICE_NAME) && - serviceProvider.GetStatus(ServiceProvider.NZBDRONE_SERVICE_NAME) == ServiceControllerStatus.StartPending; + serviceProvider.ServiceExist(ServiceProvider.SERVICE_NAME) && + serviceProvider.GetStatus(ServiceProvider.SERVICE_NAME) == ServiceControllerStatus.StartPending; //Guarded to avoid issues when running in a non-managed process var entry = Assembly.GetEntryAssembly(); @@ -27,6 +30,8 @@ namespace NzbDrone.Common.EnvironmentInfo if (entry != null) { ExecutingApplication = entry.Location; + IsWindowsTray = OsInfo.IsWindows && entry.ManifestModule.Name == $"{ProcessProvider.LIDARR_PROCESS_NAME}.exe"; + } } @@ -35,6 +40,14 @@ namespace NzbDrone.Common.EnvironmentInfo IsProduction = InternalIsProduction(); } + public DateTime StartTime + { + get + { + return _startTime; + } + } + public static bool IsUserInteractive => Environment.UserInteractive; bool IRuntimeInfo.IsUserInteractive => IsUserInteractive; @@ -59,6 +72,39 @@ namespace NzbDrone.Common.EnvironmentInfo public bool IsWindowsService { get; private set; } public bool IsExiting { get; set; } + + public bool IsTray + { + get + { + if (OsInfo.IsWindows) + { + return IsUserInteractive && Process.GetCurrentProcess().ProcessName.Equals(ProcessProvider.LIDARR_PROCESS_NAME, StringComparison.InvariantCultureIgnoreCase); + } + + return false; + } + } + + public RuntimeMode Mode + { + get + { + if (IsWindowsService) + { + return RuntimeMode.Service; + } + + if (IsTray) + { + return RuntimeMode.Tray; + } + + return RuntimeMode.Console; + } + } + + public bool RestartPending { get; set; } public string ExecutingApplication { get; } @@ -102,5 +148,7 @@ namespace NzbDrone.Common.EnvironmentInfo return true; } + + public bool IsWindowsTray { get; private set; } } } diff --git a/src/NzbDrone.Common/EnvironmentInfo/RuntimeMode.cs b/src/NzbDrone.Common/EnvironmentInfo/RuntimeMode.cs new file mode 100644 index 000000000..fc5a1867d --- /dev/null +++ b/src/NzbDrone.Common/EnvironmentInfo/RuntimeMode.cs @@ -0,0 +1,9 @@ +namespace NzbDrone.Common.EnvironmentInfo +{ + public enum RuntimeMode + { + Console, + Service, + Tray + } +} diff --git a/src/NzbDrone.Common/Exceptions/LidarrStartupException.cs b/src/NzbDrone.Common/Exceptions/LidarrStartupException.cs new file mode 100644 index 000000000..64e04de71 --- /dev/null +++ b/src/NzbDrone.Common/Exceptions/LidarrStartupException.cs @@ -0,0 +1,44 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace NzbDrone.Common.Exceptions +{ + public class LidarrStartupException : NzbDroneException + { + public LidarrStartupException(string message, params object[] args) + : base("Lidarr failed to start: " + string.Format(message, args)) + { + + } + + public LidarrStartupException(string message) + : base("Lidarr failed to start: " + message) + { + + } + + public LidarrStartupException() + : base("Lidarr failed to start") + { + + } + + public LidarrStartupException(Exception innerException, string message, params object[] args) + : base("Lidarr failed to start: " + string.Format(message, args), innerException) + { + } + + public LidarrStartupException(Exception innerException, string message) + : base("Lidarr failed to start: " + message, innerException) + { + } + + public LidarrStartupException(Exception innerException) + : base("Lidarr failed to start: " + innerException.Message) + { + + } + } +} diff --git a/src/NzbDrone.Common/Extensions/PathExtensions.cs b/src/NzbDrone.Common/Extensions/PathExtensions.cs index 11a1972dd..598bf2494 100644 --- a/src/NzbDrone.Common/Extensions/PathExtensions.cs +++ b/src/NzbDrone.Common/Extensions/PathExtensions.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.IO; using System.Text.RegularExpressions; @@ -11,8 +11,8 @@ namespace NzbDrone.Common.Extensions public static class PathExtensions { private const string APP_CONFIG_FILE = "config.xml"; - private const string NZBDRONE_DB = "lidarr.db"; - private const string NZBDRONE_LOG_DB = "logs.db"; + private const string DB = "lidarr.db"; + private const string LOG_DB = "logs.db"; private const string NLOG_CONFIG_FILE = "nlog.config"; private const string UPDATE_CLIENT_EXE = "Lidarr.Update.exe"; private const string BACKUP_FOLDER = "Backups"; @@ -21,7 +21,7 @@ namespace NzbDrone.Common.Extensions private static readonly string UPDATE_PACKAGE_FOLDER_NAME = "Lidarr" + Path.DirectorySeparatorChar; private static readonly string UPDATE_BACKUP_FOLDER_NAME = "lidarr_backup" + Path.DirectorySeparatorChar; private static readonly string UPDATE_BACKUP_APPDATA_FOLDER_NAME = "lidarr_appdata_backup" + Path.DirectorySeparatorChar; - private static readonly string UPDATE_CLIENT_FOLDER_NAME = "NzbDrone.Update" + Path.DirectorySeparatorChar; + private static readonly string UPDATE_CLIENT_FOLDER_NAME = "Lidarr.Update" + Path.DirectorySeparatorChar; private static readonly string UPDATE_LOG_FOLDER_NAME = "UpdateLogs" + Path.DirectorySeparatorChar; public static string CleanFilePath(this string path) @@ -59,7 +59,7 @@ namespace NzbDrone.Common.Extensions { if (!parentPath.IsParentPath(childPath)) { - throw new Exceptions.NotParentException("{0} is not a child of {1}", childPath, parentPath); + throw new NotParentException("{0} is not a child of {1}", childPath, parentPath); } return childPath.Substring(parentPath.Length).Trim(Path.DirectorySeparatorChar); @@ -80,11 +80,11 @@ namespace NzbDrone.Common.Extensions public static bool IsParentPath(this string parentPath, string childPath) { - if (parentPath != "/") + if (parentPath != "/" && !parentPath.EndsWith(":\\")) { parentPath = parentPath.TrimEnd(Path.DirectorySeparatorChar); } - if (childPath != "/") + if (childPath != "/" && !parentPath.EndsWith(":\\")) { childPath = childPath.TrimEnd(Path.DirectorySeparatorChar); } @@ -238,7 +238,7 @@ namespace NzbDrone.Common.Extensions public static string GetUpdateBackupDatabase(this IAppFolderInfo appFolderInfo) { - return Path.Combine(GetUpdateBackUpAppDataFolder(appFolderInfo), NZBDRONE_DB); + return Path.Combine(GetUpdateBackUpAppDataFolder(appFolderInfo), DB); } public static string GetUpdatePackageFolder(this IAppFolderInfo appFolderInfo) @@ -261,14 +261,14 @@ namespace NzbDrone.Common.Extensions return Path.Combine(GetAppDataPath(appFolderInfo), BACKUP_FOLDER); } - public static string GetNzbDroneDatabase(this IAppFolderInfo appFolderInfo) + public static string GetDatabase(this IAppFolderInfo appFolderInfo) { - return Path.Combine(GetAppDataPath(appFolderInfo), NZBDRONE_DB); + return Path.Combine(GetAppDataPath(appFolderInfo), DB); } public static string GetLogDatabase(this IAppFolderInfo appFolderInfo) { - return Path.Combine(GetAppDataPath(appFolderInfo), NZBDRONE_LOG_DB); + return Path.Combine(GetAppDataPath(appFolderInfo), LOG_DB); } public static string GetNlogConfigPath(this IAppFolderInfo appFolderInfo) @@ -276,4 +276,4 @@ namespace NzbDrone.Common.Extensions return Path.Combine(appFolderInfo.StartUpFolder, NLOG_CONFIG_FILE); } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Common/Extensions/StringExtensions.cs b/src/NzbDrone.Common/Extensions/StringExtensions.cs index 9784794b8..c75c8ab2b 100644 --- a/src/NzbDrone.Common/Extensions/StringExtensions.cs +++ b/src/NzbDrone.Common/Extensions/StringExtensions.cs @@ -1,4 +1,5 @@ -using System; +using System; +using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Text; @@ -63,7 +64,12 @@ namespace NzbDrone.Common.Extensions return text; } - public static string CleanSpaces(this string text) + public static string Join(this IEnumerable values, string separator) + { + return string.Join(separator, values); + } + + public static string CleanSpaces(this string text) { return CollapseSpace.Replace(text, " ").Trim(); } @@ -128,4 +134,4 @@ namespace NzbDrone.Common.Extensions return Encoding.ASCII.GetString(new [] { byteResult }); } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Common/Extensions/UrlExtensions.cs b/src/NzbDrone.Common/Extensions/UrlExtensions.cs index b2dac6c19..50e0b9856 100644 --- a/src/NzbDrone.Common/Extensions/UrlExtensions.cs +++ b/src/NzbDrone.Common/Extensions/UrlExtensions.cs @@ -11,6 +11,11 @@ namespace NzbDrone.Common.Extensions return false; } + if (path.StartsWith(" ") || path.EndsWith(" ")) + { + return false; + } + Uri uri; if (!Uri.TryCreate(path, UriKind.Absolute, out uri)) { diff --git a/src/NzbDrone.Common/Http/Dispatchers/CurlHttpDispatcher.cs b/src/NzbDrone.Common/Http/Dispatchers/CurlHttpDispatcher.cs index 83d6fb1d1..17574982d 100644 --- a/src/NzbDrone.Common/Http/Dispatchers/CurlHttpDispatcher.cs +++ b/src/NzbDrone.Common/Http/Dispatchers/CurlHttpDispatcher.cs @@ -38,7 +38,7 @@ namespace NzbDrone.Common.Http.Dispatchers _caBundleFilePath = _caBundleFileName; } } - + public CurlHttpDispatcher(IHttpProxySettingsProvider proxySettingsProvider, IUserAgentBuilder userAgentBuilder, Logger logger) { _proxySettingsProvider = proxySettingsProvider; @@ -107,7 +107,7 @@ namespace NzbDrone.Common.Http.Dispatchers throw new NotSupportedException($"HttpCurl method {request.Method} not supported"); } curlEasy.UserAgent = _userAgentBuilder.GetUserAgent(request.UseSimplifiedUserAgent); - curlEasy.FollowLocation = request.AllowAutoRedirect; + curlEasy.FollowLocation = false; if (request.RequestTimeout != TimeSpan.Zero) { diff --git a/src/NzbDrone.Common/Http/Dispatchers/FallbackHttpDispatcher.cs b/src/NzbDrone.Common/Http/Dispatchers/FallbackHttpDispatcher.cs index 707004c9d..01b60e012 100644 --- a/src/NzbDrone.Common/Http/Dispatchers/FallbackHttpDispatcher.cs +++ b/src/NzbDrone.Common/Http/Dispatchers/FallbackHttpDispatcher.cs @@ -34,18 +34,11 @@ namespace NzbDrone.Common.Http.Dispatchers { return _managedDispatcher.GetResponse(request, cookies); } - catch (Exception ex) + catch (TlsFailureException) { - if (ex.ToString().Contains("The authentication or decryption has failed.")) - { - _logger.Debug("https request failed in tls error for {0}, trying curl fallback.", request.Url.Host); - - _curlTLSFallbackCache.Set(request.Url.Host, true); - } - else - { - throw; - } + _logger.Debug("https request failed in tls error for {0}, trying curl fallback.", request.Url.Host); + + _curlTLSFallbackCache.Set(request.Url.Host, true); } } diff --git a/src/NzbDrone.Common/Http/Dispatchers/ManagedHttpDispatcher.cs b/src/NzbDrone.Common/Http/Dispatchers/ManagedHttpDispatcher.cs index 6024d4395..0ed952fdc 100644 --- a/src/NzbDrone.Common/Http/Dispatchers/ManagedHttpDispatcher.cs +++ b/src/NzbDrone.Common/Http/Dispatchers/ManagedHttpDispatcher.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Net; using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.Extensions; @@ -32,7 +32,7 @@ namespace NzbDrone.Common.Http.Dispatchers webRequest.Method = request.Method.ToString(); webRequest.UserAgent = _userAgentBuilder.GetUserAgent(request.UseSimplifiedUserAgent); webRequest.KeepAlive = request.ConnectionKeepAlive; - webRequest.AllowAutoRedirect = request.AllowAutoRedirect; + webRequest.AllowAutoRedirect = false; webRequest.CookieContainer = cookies; if (request.RequestTimeout != TimeSpan.Zero) @@ -47,19 +47,19 @@ namespace NzbDrone.Common.Http.Dispatchers AddRequestHeaders(webRequest, request.Headers); } - if (request.ContentData != null) - { - webRequest.ContentLength = request.ContentData.Length; - using (var writeStream = webRequest.GetRequestStream()) - { - writeStream.Write(request.ContentData, 0, request.ContentData.Length); - } - } - HttpWebResponse httpWebResponse; try { + if (request.ContentData != null) + { + webRequest.ContentLength = request.ContentData.Length; + using (var writeStream = webRequest.GetRequestStream()) + { + writeStream.Write(request.ContentData, 0, request.ContentData.Length); + } + } + httpWebResponse = (HttpWebResponse)webRequest.GetResponse(); } catch (WebException e) @@ -73,7 +73,27 @@ namespace NzbDrone.Common.Http.Dispatchers if (httpWebResponse == null) { - throw; + // The default messages for WebException on mono are pretty horrible. + if (e.Status == WebExceptionStatus.NameResolutionFailure) + { + throw new WebException($"DNS Name Resolution Failure: '{webRequest.RequestUri.Host}'", e.Status); + } + else if (e.ToString().Contains("TLS Support not")) + { + throw new TlsFailureException(webRequest, e); + } + else if (e.ToString().Contains("The authentication or decryption has failed.")) + { + throw new TlsFailureException(webRequest, e); + } + else if (OsInfo.IsNotWindows) + { + throw new WebException($"{e.Message}: '{webRequest.RequestUri}'", e, e.Status, e.Response); + } + else + { + throw; + }; } } @@ -83,7 +103,14 @@ namespace NzbDrone.Common.Http.Dispatchers { if (responseStream != null) { - data = responseStream.ToBytes(); + try + { + data = responseStream.ToBytes(); + } + catch (Exception ex) + { + throw new WebException("Failed to read complete http response", ex, WebExceptionStatus.ReceiveFailure, httpWebResponse); + } } } diff --git a/src/NzbDrone.Common/Http/HttpClient.cs b/src/NzbDrone.Common/Http/HttpClient.cs index 849647f64..3ebb2907a 100644 --- a/src/NzbDrone.Common/Http/HttpClient.cs +++ b/src/NzbDrone.Common/Http/HttpClient.cs @@ -7,6 +7,7 @@ using System.Net; using NLog; using NzbDrone.Common.Cache; using NzbDrone.Common.EnvironmentInfo; +using NzbDrone.Common.Extensions; using NzbDrone.Common.Http.Dispatchers; using NzbDrone.Common.TPL; @@ -50,6 +51,54 @@ namespace NzbDrone.Common.Http } public HttpResponse Execute(HttpRequest request) + { + var response = ExecuteRequest(request); + + if (request.AllowAutoRedirect && response.HasHttpRedirect) + { + var autoRedirectChain = new List(); + autoRedirectChain.Add(request.Url.ToString()); + + do + { + request.Url += new HttpUri(response.Headers.GetSingleValue("Location")); + autoRedirectChain.Add(request.Url.ToString()); + + _logger.Trace("Redirected to {0}", request.Url); + + if (autoRedirectChain.Count > 3) + { + throw new WebException($"Too many automatic redirections were attempted for {autoRedirectChain.Join(" -> ")}", WebExceptionStatus.ProtocolError); + } + + response = ExecuteRequest(request); + } + while (response.HasHttpRedirect); + } + + if (response.HasHttpRedirect && !RuntimeInfo.IsProduction) + { + _logger.Error("Server requested a redirect to [{0}] while in developer mode. Update the request URL to avoid this redirect.", response.Headers["Location"]); + } + + if (!request.SuppressHttpError && response.HasHttpError) + { + _logger.Warn("HTTP Error - {0}", response); + + if ((int)response.StatusCode == 429) + { + throw new TooManyRequestsException(request, response); + } + else + { + throw new HttpException(request, response); + } + } + + return response; + } + + private HttpResponse ExecuteRequest(HttpRequest request) { foreach (var interceptor in _requestInterceptors) { @@ -85,28 +134,6 @@ namespace NzbDrone.Common.Http _logger.Trace("Response content ({0} bytes): {1}", response.ResponseData.Length, response.Content); } - if (!RuntimeInfo.IsProduction && - (response.StatusCode == HttpStatusCode.Moved || - response.StatusCode == HttpStatusCode.MovedPermanently || - response.StatusCode == HttpStatusCode.Found)) - { - _logger.Error("Server requested a redirect to [{0}]. Update the request URL to avoid this redirect.", response.Headers["Location"]); - } - - if (!request.SuppressHttpError && response.HasHttpError) - { - _logger.Warn("HTTP Error - {0}", response); - - if ((int)response.StatusCode == 429) - { - throw new TooManyRequestsException(request, response); - } - else - { - throw new HttpException(request, response); - } - } - return response; } @@ -217,4 +244,4 @@ namespace NzbDrone.Common.Http return new HttpResponse(response); } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Common/Http/HttpHeader.cs b/src/NzbDrone.Common/Http/HttpHeader.cs index 88e0ab81e..747015b93 100644 --- a/src/NzbDrone.Common/Http/HttpHeader.cs +++ b/src/NzbDrone.Common/Http/HttpHeader.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Linq; using System.Collections.Generic; using System.Collections.Specialized; @@ -37,7 +37,7 @@ namespace NzbDrone.Common.Http } if (values.Length > 1) { - throw new ApplicationException(string.Format("Expected {0} to occur only once.", key)); + throw new ApplicationException($"Expected {key} to occur only once, but was {values.Join("|")}."); } return values[0]; @@ -175,4 +175,4 @@ namespace NzbDrone.Common.Http .ToList(); } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Common/Http/HttpMethod.cs b/src/NzbDrone.Common/Http/HttpMethod.cs index 1fa33a823..8964bbef6 100644 --- a/src/NzbDrone.Common/Http/HttpMethod.cs +++ b/src/NzbDrone.Common/Http/HttpMethod.cs @@ -3,11 +3,12 @@ namespace NzbDrone.Common.Http public enum HttpMethod { GET, - PUT, POST, - HEAD, + PUT, DELETE, + HEAD, + OPTIONS, PATCH, - OPTIONS + MERGE } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Common/Http/HttpResponse.cs b/src/NzbDrone.Common/Http/HttpResponse.cs index dd9df22c7..734238cfc 100644 --- a/src/NzbDrone.Common/Http/HttpResponse.cs +++ b/src/NzbDrone.Common/Http/HttpResponse.cs @@ -35,7 +35,7 @@ namespace NzbDrone.Common.Http private string _content; - public string Content + public string Content { get { @@ -51,6 +51,10 @@ namespace NzbDrone.Common.Http public bool HasHttpError => (int)StatusCode >= 400; + public bool HasHttpRedirect => StatusCode == HttpStatusCode.Moved || + StatusCode == HttpStatusCode.MovedPermanently || + StatusCode == HttpStatusCode.Found; + public Dictionary GetCookies() { var result = new Dictionary(); @@ -95,4 +99,4 @@ namespace NzbDrone.Common.Http public T Resource { get; private set; } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Common/Http/HttpUri.cs b/src/NzbDrone.Common/Http/HttpUri.cs index 23e47be94..27794e307 100644 --- a/src/NzbDrone.Common/Http/HttpUri.cs +++ b/src/NzbDrone.Common/Http/HttpUri.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Text; using System.Text.RegularExpressions; @@ -8,7 +8,7 @@ namespace NzbDrone.Common.Http { public class HttpUri : IEquatable { - private static readonly Regex RegexUri = new Regex(@"^(?:(?[a-z]+):)?(?://(?[-A-Z0-9.]+)(?::(?[0-9]{1,5}))?)?(?(?:(?:(?<=^)|/)[^/?#\r\n]+)+/?|/)?(?:\?(?[^#\r\n]*))?(?:\#(?.*))?$", RegexOptions.Compiled | RegexOptions.IgnoreCase); + private static readonly Regex RegexUri = new Regex(@"^(?:(?[a-z]+):)?(?://(?[-_A-Z0-9.]+)(?::(?[0-9]{1,5}))?)?(?(?:(?:(?<=^)|/)[^/?#\r\n]+)+/?|/)?(?:\?(?[^#\r\n]*))?(?:\#(?.*))?$", RegexOptions.Compiled | RegexOptions.IgnoreCase); private readonly string _uri; public string FullUri => _uri; diff --git a/src/NzbDrone.Common/Http/TlsFailureException.cs b/src/NzbDrone.Common/Http/TlsFailureException.cs new file mode 100644 index 000000000..c1dcdd991 --- /dev/null +++ b/src/NzbDrone.Common/Http/TlsFailureException.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Text; + +namespace NzbDrone.Common.Http +{ + public class TlsFailureException : WebException + { + public TlsFailureException(WebRequest request, WebException innerException) + : base("Failed to establish secure https connection to '" + request.RequestUri + "', libcurl fallback might be unavailable.", innerException, WebExceptionStatus.SecureChannelFailure, innerException.Response) + { + + } + + } +} diff --git a/src/NzbDrone.Common/Instrumentation/CleanseLogMessage.cs b/src/NzbDrone.Common/Instrumentation/CleanseLogMessage.cs index ef33968e5..a53e8e282 100644 --- a/src/NzbDrone.Common/Instrumentation/CleanseLogMessage.cs +++ b/src/NzbDrone.Common/Instrumentation/CleanseLogMessage.cs @@ -6,10 +6,10 @@ namespace NzbDrone.Common.Instrumentation { public class CleanseLogMessage { - private static readonly Regex[] CleansingRules = new[] + private static readonly Regex[] CleansingRules = new[] { // Url - new Regex(@"(?<=\?|&)(apikey|token|passkey|auth|authkey|user|uid|api)=(?[^&=]+?)(?= |&|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase), + new Regex(@"(?<=\?|&)(apikey|token|passkey|auth|authkey|user|uid|api|[a-z_]*apikey)=(?[^&=]+?)(?= |&|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase), new Regex(@"(?<=\?|&)[^=]*?(username|password)=(?[^&=]+?)(?= |&|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase), new Regex(@"torrentleech\.org/(?!rss)(?[0-9a-z]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase), new Regex(@"torrentleech\.org/rss/download/[0-9]+/(?[0-9a-z]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase), diff --git a/src/NzbDrone.Common/Instrumentation/NzbDroneLogger.cs b/src/NzbDrone.Common/Instrumentation/NzbDroneLogger.cs index 0845caeaa..5fd0e2d11 100644 --- a/src/NzbDrone.Common/Instrumentation/NzbDroneLogger.cs +++ b/src/NzbDrone.Common/Instrumentation/NzbDroneLogger.cs @@ -94,14 +94,14 @@ namespace NzbDrone.Common.Instrumentation { dsn = RuntimeInfo.IsProduction ? "https://bbb13f4547294da1bcd52069420aaa5d:950541e562cf43c594fe2dcfaf4c3271@sentry.io/209545" - : "https://bbb13f4547294da1bcd52069420aaa5d:950541e562cf43c594fe2dcfaf4c3271@sentry.io/209545"; + : "https://edab7530cf9544dba1f86ac28aa0110b:b84a1425fc304f0188ef968576fe9690@sentry.io/227247"; } else { dsn = RuntimeInfo.IsProduction ? "https://bbb13f4547294da1bcd52069420aaa5d:950541e562cf43c594fe2dcfaf4c3271@sentry.io/209545" - : "https://bbb13f4547294da1bcd52069420aaa5d:950541e562cf43c594fe2dcfaf4c3271@sentry.io/209545"; + : "https://edab7530cf9544dba1f86ac28aa0110b:b84a1425fc304f0188ef968576fe9690@sentry.io/227247"; } var target = new SentryTarget(dsn) diff --git a/src/NzbDrone.Common/NzbDrone.Common.csproj b/src/NzbDrone.Common/NzbDrone.Common.csproj index 611cdc4cc..9f424c291 100644 --- a/src/NzbDrone.Common/NzbDrone.Common.csproj +++ b/src/NzbDrone.Common/NzbDrone.Common.csproj @@ -1,5 +1,5 @@  - + Debug x86 @@ -9,8 +9,8 @@ Library Properties NzbDrone.Common - NzbDrone.Common - v4.0 + Lidarr.Common + v4.6.1 512 ..\ true @@ -27,6 +27,7 @@ MinimumRecommendedRules.ruleset 4 false + false ..\..\_output\ @@ -37,24 +38,27 @@ prompt MinimumRecommendedRules.ruleset 4 + false + + ..\packages\ICSharpCode.SharpZipLib.Patched.0.86.5\lib\net20\ICSharpCode.SharpZipLib.dll + - ..\packages\Newtonsoft.Json.9.0.1\lib\net40\Newtonsoft.Json.dll - True + ..\packages\Newtonsoft.Json.9.0.1\lib\net45\Newtonsoft.Json.dll - ..\packages\NLog.4.4.3\lib\net40\NLog.dll + ..\packages\NLog.4.4.12\lib\net45\NLog.dll - - ..\packages\DotNet4.SocksProxy.1.3.2.0\lib\net40\Org.Mentalis.dll + + ..\packages\DotNet4.SocksProxy.1.3.4.0\lib\net40\Org.Mentalis.dll True - ..\packages\SharpRaven.2.2.0\lib\net40\SharpRaven.dll + ..\packages\SharpRaven.2.2.0\lib\net45\SharpRaven.dll - - ..\packages\DotNet4.SocksProxy.1.3.2.0\lib\net40\SocksWebProxy.dll + + ..\packages\DotNet4.SocksProxy.1.3.4.0\lib\net40\SocksWebProxy.dll True @@ -64,9 +68,6 @@ - - ..\packages\ICSharpCode.SharpZipLib.Patched.0.86.5\lib\net20\ICSharpCode.SharpZipLib.dll - @@ -77,22 +78,26 @@ - + + + + + @@ -125,7 +130,7 @@ - + @@ -172,6 +177,7 @@ + @@ -225,9 +231,7 @@ Always - - Designer - + diff --git a/src/NzbDrone.Common/Processes/PidFileProvider.cs b/src/NzbDrone.Common/Processes/PidFileProvider.cs index c04ff445f..a272b1c7b 100644 --- a/src/NzbDrone.Common/Processes/PidFileProvider.cs +++ b/src/NzbDrone.Common/Processes/PidFileProvider.cs @@ -1,7 +1,8 @@ -using System; +using System; using System.IO; using NLog; using NzbDrone.Common.EnvironmentInfo; +using NzbDrone.Common.Exceptions; namespace NzbDrone.Common.Processes { @@ -30,7 +31,7 @@ namespace NzbDrone.Common.Processes return; } - var filename = Path.Combine(_appFolderInfo.AppDataFolder, "nzbdrone.pid"); + var filename = Path.Combine(_appFolderInfo.AppDataFolder, "lidarr.pid"); try { File.WriteAllText(filename, _processProvider.GetCurrentProcessId().ToString()); @@ -38,7 +39,7 @@ namespace NzbDrone.Common.Processes catch (Exception ex) { _logger.Error(ex, "Unable to write PID file {0}", filename); - throw; + throw new LidarrStartupException(ex, "Unable to write PID file {0}", filename); } } } diff --git a/src/NzbDrone.Common/Processes/ProcessProvider.cs b/src/NzbDrone.Common/Processes/ProcessProvider.cs index b0ba09f08..dcc86bebe 100644 --- a/src/NzbDrone.Common/Processes/ProcessProvider.cs +++ b/src/NzbDrone.Common/Processes/ProcessProvider.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections; using System.Collections.Generic; using System.Collections.Specialized; @@ -27,7 +27,7 @@ namespace NzbDrone.Common.Processes bool Exists(string processName); ProcessPriorityClass GetCurrentProcessPriority(); Process Start(string path, string args = null, StringDictionary environmentVariables = null, Action onOutputDataReceived = null, Action onErrorDataReceived = null); - Process SpawnNewProcess(string path, string args = null, StringDictionary environmentVariables = null); + Process SpawnNewProcess(string path, string args = null, StringDictionary environmentVariables = null, bool noWindow = false); ProcessOutput StartAndCapture(string path, string args = null, StringDictionary environmentVariables = null); } @@ -35,8 +35,8 @@ namespace NzbDrone.Common.Processes { private readonly Logger _logger; - public const string NZB_DRONE_PROCESS_NAME = "Lidarr"; - public const string NZB_DRONE_CONSOLE_PROCESS_NAME = "Lidarr.Console"; + public const string LIDARR_PROCESS_NAME = "Lidarr"; + public const string LIDARR_CONSOLE_PROCESS_NAME = "Lidarr.Console"; public ProcessProvider(Logger logger) { @@ -172,7 +172,7 @@ namespace NzbDrone.Common.Processes return process; } - public Process SpawnNewProcess(string path, string args = null, StringDictionary environmentVariables = null) + public Process SpawnNewProcess(string path, string args = null, StringDictionary environmentVariables = null, bool noWindow = false) { if (PlatformInfo.IsMono && path.EndsWith(".exe", StringComparison.InvariantCultureIgnoreCase)) { @@ -183,6 +183,9 @@ namespace NzbDrone.Common.Processes _logger.Debug("Starting {0} {1}", path, args); var startInfo = new ProcessStartInfo(path, args); + startInfo.CreateNoWindow = noWindow; + startInfo.UseShellExecute = !noWindow; + var process = new Process { StartInfo = startInfo diff --git a/src/NzbDrone.Common/Properties/AssemblyInfo.cs b/src/NzbDrone.Common/Properties/AssemblyInfo.cs index e8cdf90c1..77d21a2e6 100644 --- a/src/NzbDrone.Common/Properties/AssemblyInfo.cs +++ b/src/NzbDrone.Common/Properties/AssemblyInfo.cs @@ -1,12 +1,11 @@ -using System.Reflection; +using System.Reflection; using System.Runtime.InteropServices; // General Information about an assembly is controlled through the following // set of attributes. Change these attribute values to modify the information // associated with an assembly. -[assembly: AssemblyTitle("NzbDrone.Common")] +[assembly: AssemblyTitle("Lidarr.Common")] // The following GUID is for the ID of the typelib if this project is exposed to COM [assembly: Guid("b6eaa144-e13b-42e5-a738-c60d89c0f728")] -[assembly: AssemblyVersion("10.0.0.*")] diff --git a/src/NzbDrone.Common/Properties/SharedAssemblyInfo.cs b/src/NzbDrone.Common/Properties/SharedAssemblyInfo.cs index 872d4d276..2234c9d14 100644 --- a/src/NzbDrone.Common/Properties/SharedAssemblyInfo.cs +++ b/src/NzbDrone.Common/Properties/SharedAssemblyInfo.cs @@ -1,11 +1,12 @@ -using System.Reflection; +using System.Reflection; using System.Runtime.InteropServices; // Gets updated at build time by TeamCity to branch name [assembly: AssemblyConfiguration("debug")] [assembly: AssemblyCompany("lidarr.audio")] -[assembly: AssemblyProduct("NzbDrone")] +[assembly: AssemblyProduct("Lidarr")] +[assembly: AssemblyVersion("10.0.0.*")] [assembly: AssemblyCopyright("GNU General Public v3")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] diff --git a/src/NzbDrone.Common/Reflection/ReflectionExtensions.cs b/src/NzbDrone.Common/Reflection/ReflectionExtensions.cs index db7edc31b..1e778b843 100644 --- a/src/NzbDrone.Common/Reflection/ReflectionExtensions.cs +++ b/src/NzbDrone.Common/Reflection/ReflectionExtensions.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Reflection; @@ -7,7 +7,7 @@ namespace NzbDrone.Common.Reflection { public static class ReflectionExtensions { - public static readonly Assembly CoreAssembly = Assembly.Load("NzbDrone.Core"); + public static readonly Assembly CoreAssembly = Assembly.Load("Lidarr.Core"); public static List GetSimpleProperties(this Type type) { @@ -60,6 +60,11 @@ namespace NzbDrone.Common.Reflection return (T)attribute; } + public static T[] GetAttributes(this MemberInfo member) where T : Attribute + { + return member.GetCustomAttributes(typeof(T), false).OfType().ToArray(); + } + public static Type FindTypeByName(this Assembly assembly, string name) { return assembly.GetTypes().SingleOrDefault(c => c.Name.Equals(name, StringComparison.InvariantCultureIgnoreCase)); @@ -70,4 +75,4 @@ namespace NzbDrone.Common.Reflection return type.GetCustomAttributes(typeof(TAttribute), true).Any(); } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Common/Security/SecurityProtocolPolicy.cs b/src/NzbDrone.Common/Security/SecurityProtocolPolicy.cs index 03fcb97d2..c0ebc56eb 100644 --- a/src/NzbDrone.Common/Security/SecurityProtocolPolicy.cs +++ b/src/NzbDrone.Common/Security/SecurityProtocolPolicy.cs @@ -1,6 +1,7 @@ -using System; +using System; using System.Net; using NLog; +using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.Instrumentation; namespace NzbDrone.Common.Security @@ -14,6 +15,12 @@ namespace NzbDrone.Common.Security public static void Register() { + if (OsInfo.IsNotWindows) + { + // This was never meant to be used on mono, and will cause issues with mono 5 and higher if btls is enabled. + return; + } + try { // TODO: In v3 we should drop support for SSL3 because its very insecure. Only leaving it enabled because some people might rely on it. diff --git a/src/NzbDrone.Common/Serializer/Json.cs b/src/NzbDrone.Common/Serializer/Json.cs index 31e0d3f0b..be68ff08f 100644 --- a/src/NzbDrone.Common/Serializer/Json.cs +++ b/src/NzbDrone.Common/Serializer/Json.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.IO; using Newtonsoft.Json; using Newtonsoft.Json.Converters; @@ -9,37 +9,40 @@ namespace NzbDrone.Common.Serializer public static class Json { private static readonly JsonSerializer Serializer; - private static readonly JsonSerializerSettings SerializerSetting; + private static readonly JsonSerializerSettings SerializerSettings; static Json() { - SerializerSetting = new JsonSerializerSettings - { - DateTimeZoneHandling = DateTimeZoneHandling.Utc, - NullValueHandling = NullValueHandling.Ignore, - Formatting = Formatting.Indented, - DefaultValueHandling = DefaultValueHandling.Include, - ContractResolver = new CamelCasePropertyNamesContractResolver() - }; - + SerializerSettings = GetSerializerSettings(); + Serializer = JsonSerializer.Create(SerializerSettings); + } - SerializerSetting.Converters.Add(new StringEnumConverter { CamelCaseText = true }); - //SerializerSetting.Converters.Add(new IntConverter()); - SerializerSetting.Converters.Add(new VersionConverter()); - SerializerSetting.Converters.Add(new HttpUriConverter()); + public static JsonSerializerSettings GetSerializerSettings() + { + var serializerSettings = new JsonSerializerSettings + { + DateTimeZoneHandling = DateTimeZoneHandling.Utc, + NullValueHandling = NullValueHandling.Ignore, + Formatting = Formatting.Indented, + DefaultValueHandling = DefaultValueHandling.Include, + ContractResolver = new CamelCasePropertyNamesContractResolver() + }; - Serializer = JsonSerializer.Create(SerializerSetting); + serializerSettings.Converters.Add(new StringEnumConverter { CamelCaseText = true }); + serializerSettings.Converters.Add(new VersionConverter()); + serializerSettings.Converters.Add(new HttpUriConverter()); + return serializerSettings; } public static T Deserialize(string json) where T : new() { - return JsonConvert.DeserializeObject(json, SerializerSetting); + return JsonConvert.DeserializeObject(json, SerializerSettings); } public static object Deserialize(string json, Type type) { - return JsonConvert.DeserializeObject(json, type, SerializerSetting); + return JsonConvert.DeserializeObject(json, type, SerializerSettings); } public static bool TryDeserialize(string json, out T result) where T : new() @@ -63,7 +66,7 @@ namespace NzbDrone.Common.Serializer public static string ToJson(this object obj) { - return JsonConvert.SerializeObject(obj, SerializerSetting); + return JsonConvert.SerializeObject(obj, SerializerSettings); } public static void Serialize(TModel model, TextWriter outputStream) diff --git a/src/NzbDrone.Common/ServiceProvider.cs b/src/NzbDrone.Common/ServiceProvider.cs index d26b27736..2819c2736 100644 --- a/src/NzbDrone.Common/ServiceProvider.cs +++ b/src/NzbDrone.Common/ServiceProvider.cs @@ -1,10 +1,11 @@ -using System; +using System; using System.Collections.Specialized; using System.Configuration.Install; using System.Diagnostics; using System.Linq; using System.ServiceProcess; using NLog; +using NzbDrone.Common.Extensions; using NzbDrone.Common.Processes; namespace NzbDrone.Common @@ -14,23 +15,23 @@ namespace NzbDrone.Common bool ServiceExist(string name); bool IsServiceRunning(string name); void Install(string serviceName); - void UnInstall(string serviceName); + void Uninstall(string serviceName); void Run(ServiceBase service); ServiceController GetService(string serviceName); void Stop(string serviceName); void Start(string serviceName); ServiceControllerStatus GetStatus(string serviceName); void Restart(string serviceName); + void SetPermissions(string serviceName); } public class ServiceProvider : IServiceProvider { - public const string NZBDRONE_SERVICE_NAME = "Lidarr"; + public const string SERVICE_NAME = "Lidarr"; private readonly IProcessProvider _processProvider; private readonly Logger _logger; - public ServiceProvider(IProcessProvider processProvider, Logger logger) { _processProvider = processProvider; @@ -66,7 +67,7 @@ namespace NzbDrone.Common var installer = new ServiceProcessInstaller { - Account = ServiceAccount.LocalSystem + Account = ServiceAccount.LocalService }; var serviceInstaller = new ServiceInstaller(); @@ -89,7 +90,7 @@ namespace NzbDrone.Common _logger.Info("Service Has installed successfully."); } - public virtual void UnInstall(string serviceName) + public virtual void Uninstall(string serviceName) { _logger.Info("Uninstalling {0} service", serviceName); @@ -189,5 +190,42 @@ namespace NzbDrone.Common _processProvider.Start("cmd.exe", args); } + + public void SetPermissions(string serviceName) + { + var dacls = GetServiceDacls(serviceName); + SetServiceDacls(serviceName, dacls); + } + + private string GetServiceDacls(string serviceName) + { + var output = _processProvider.StartAndCapture("sc.exe", $"sdshow {serviceName}"); + + var dacls = output.Standard.Select(s => s.Content).Where(s => s.IsNotNullOrWhiteSpace()).ToList(); + + if (dacls.Count == 1) + { + return dacls[0]; + } + + throw new ArgumentException("Invalid DACL output"); + } + + private void SetServiceDacls(string serviceName, string dacls) + { + const string authenticatedUsersDacl = "(A;;CCLCSWRPWPLOCRRC;;;AU)"; + + if (dacls.Contains(authenticatedUsersDacl)) + { + // Permssions already set + return; + } + + var indexOfS = dacls.IndexOf("S:", StringComparison.InvariantCultureIgnoreCase); + + dacls = indexOfS == -1 ? $"{dacls}{authenticatedUsersDacl}" : dacls.Insert(indexOfS, authenticatedUsersDacl); + + _processProvider.Start("sc.exe", $"sdset {serviceName} {dacls}").WaitForExit(); + } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Common/app.config b/src/NzbDrone.Common/app.config index 8460dd432..9566343db 100644 --- a/src/NzbDrone.Common/app.config +++ b/src/NzbDrone.Common/app.config @@ -8,4 +8,4 @@ - \ No newline at end of file + diff --git a/src/NzbDrone.Common/packages.config b/src/NzbDrone.Common/packages.config index c3f5e4d62..c376e8f5b 100644 --- a/src/NzbDrone.Common/packages.config +++ b/src/NzbDrone.Common/packages.config @@ -1,8 +1,8 @@  - - - - - + + + + + \ No newline at end of file diff --git a/src/NzbDrone.Console/ConsoleApp.cs b/src/NzbDrone.Console/ConsoleApp.cs index f15eb1b56..385991765 100644 --- a/src/NzbDrone.Console/ConsoleApp.cs +++ b/src/NzbDrone.Console/ConsoleApp.cs @@ -1,7 +1,8 @@ -using System; +using System; using System.Net.Sockets; using NLog; using NzbDrone.Common.EnvironmentInfo; +using NzbDrone.Common.Exceptions; using NzbDrone.Common.Instrumentation; using NzbDrone.Host; @@ -11,6 +12,14 @@ namespace NzbDrone.Console { private static readonly Logger Logger = NzbDroneLogger.GetLogger(typeof(ConsoleApp)); + private enum ExitCodes : int + { + Normal = 0, + UnknownFailure = 1, + RecoverableFailure = 2, + NonRecoverableFailure = 3 + } + public static void Main(string[] args) { try @@ -19,30 +28,61 @@ namespace NzbDrone.Console NzbDroneLogger.Register(startupArgs, false, true); Bootstrap.Start(startupArgs, new ConsoleAlerts()); } - catch (SocketException exception) + catch (LidarrStartupException ex) { System.Console.WriteLine(""); System.Console.WriteLine(""); - Logger.Fatal(exception.Message + ". This can happen if another instance of Lidarr is already running another application is using the same port (default: 8686) or the user has insufficient permissions"); - System.Console.WriteLine("Press enter to exit..."); - System.Console.ReadLine(); - Environment.Exit(1); + Logger.Fatal(ex, "EPIC FAIL!"); + Exit(ExitCodes.NonRecoverableFailure); } - catch (Exception e) + catch (SocketException ex) { System.Console.WriteLine(""); System.Console.WriteLine(""); - Logger.Fatal(e, "EPIC FAIL!"); - System.Console.WriteLine("Press enter to exit..."); - System.Console.ReadLine(); - Environment.Exit(1); + Logger.Fatal(ex.Message + ". This can happen if another instance of Sonarr is already running another application is using the same port (default: 8989) or the user has insufficient permissions"); + Exit(ExitCodes.RecoverableFailure); + } + catch (Exception ex) + { + System.Console.WriteLine(""); + System.Console.WriteLine(""); + Logger.Fatal(ex, "EPIC FAIL!"); + Exit(ExitCodes.UnknownFailure); } Logger.Info("Exiting main."); - //Need this to terminate on mono (thanks nlog) - LogManager.Configuration = null; - Environment.Exit(0); + Exit(ExitCodes.Normal); + } + + private static void Exit(ExitCodes exitCode) + { + LogManager.Flush(); + + if (exitCode != ExitCodes.Normal) + { + System.Console.WriteLine("Press enter to exit..."); + + System.Threading.Thread.Sleep(1000); + + if (exitCode == ExitCodes.NonRecoverableFailure) + { + System.Console.WriteLine("Non-recoverable failure, waiting for user intervention..."); + for (int i = 0; i< 3600; i++) + { + System.Threading.Thread.Sleep(1000); + + if (System.Console.KeyAvailable) break; + } + } + + // Please note that ReadLine silently succeeds if there is no console, KeyAvailable does not. + System.Console.ReadLine(); + } + + //Need this to terminate on mono (thanks nlog) + LogManager.Configuration = null; + Environment.Exit((int)exitCode); } } } diff --git a/src/NzbDrone.Console/NzbDrone.Console.csproj b/src/NzbDrone.Console/NzbDrone.Console.csproj index cca21186e..26f46e32d 100644 --- a/src/NzbDrone.Console/NzbDrone.Console.csproj +++ b/src/NzbDrone.Console/NzbDrone.Console.csproj @@ -1,5 +1,5 @@  - + Debug x86 @@ -10,7 +10,7 @@ Properties NzbDrone.Console Lidarr.Console - v4.0 + v4.6.1 512 @@ -43,6 +43,7 @@ 4 true BasicCorrectnessRules.ruleset + false x86 @@ -52,6 +53,7 @@ TRACE prompt 4 + false ..\NzbDrone.Host\NzbDrone.ico @@ -66,26 +68,23 @@ app.manifest - - False - ..\packages\Microsoft.Owin.2.1.0\lib\net40\Microsoft.Owin.dll + + ..\packages\Microsoft.Owin.3.1.0\lib\net45\Microsoft.Owin.dll - - False - ..\packages\Microsoft.Owin.Hosting.2.1.0\lib\net40\Microsoft.Owin.Hosting.dll + + ..\packages\Microsoft.Owin.Hosting.3.1.0\lib\net45\Microsoft.Owin.Hosting.dll - ..\packages\Newtonsoft.Json.9.0.1\lib\net40\Newtonsoft.Json.dll - True + ..\packages\Newtonsoft.Json.9.0.1\lib\net45\Newtonsoft.Json.dll - ..\packages\NLog.4.4.3\lib\net40\NLog.dll + ..\packages\NLog.4.4.12\lib\net45\NLog.dll - - - + ..\packages\Owin.1.0\lib\net40\Owin.dll + + @@ -118,14 +117,6 @@ - - {1B9A82C4-BCA1-4834-A33E-226F17BE070B} - Microsoft.AspNet.SignalR.Core - - - {2B8C6DAD-4D85-41B1-83FD-248D9F347522} - Microsoft.AspNet.SignalR.Owin - {F2BE0FDF-6E47-4827-A420-DD4EF82407F8} NzbDrone.Common diff --git a/src/NzbDrone.Console/Properties/AssemblyInfo.cs b/src/NzbDrone.Console/Properties/AssemblyInfo.cs index 3573b0932..78d2227d3 100644 --- a/src/NzbDrone.Console/Properties/AssemblyInfo.cs +++ b/src/NzbDrone.Console/Properties/AssemblyInfo.cs @@ -1,4 +1,4 @@ -using System.Reflection; +using System.Reflection; using System.Runtime.InteropServices; // General Information about an assembly is controlled through the following @@ -7,5 +7,3 @@ using System.Runtime.InteropServices; [assembly: AssemblyTitle("Lidarr.Host")] [assembly: Guid("67AADCD9-89AA-4D95-8281-3193740E70E5")] - -[assembly: AssemblyVersion("10.0.0.*")] \ No newline at end of file diff --git a/src/NzbDrone.Console/packages.config b/src/NzbDrone.Console/packages.config index 11b77285e..8c5ab74a0 100644 --- a/src/NzbDrone.Console/packages.config +++ b/src/NzbDrone.Console/packages.config @@ -1,8 +1,8 @@  - - - - - + + + + + \ No newline at end of file diff --git a/src/NzbDrone.Core.Test/DataAugmentation/Scene/SceneMappingProxyFixture.cs b/src/NzbDrone.Core.Test/DataAugmentation/Scene/SceneMappingProxyFixture.cs deleted file mode 100644 index ce59cf37c..000000000 --- a/src/NzbDrone.Core.Test/DataAugmentation/Scene/SceneMappingProxyFixture.cs +++ /dev/null @@ -1,33 +0,0 @@ -using FluentAssertions; -using NUnit.Framework; -using NzbDrone.Common.Extensions; -using NzbDrone.Core.DataAugmentation.Scene; -using NzbDrone.Core.Test.Framework; -using NzbDrone.Test.Common.Categories; - -namespace NzbDrone.Core.Test.DataAugmentation.Scene -{ - [TestFixture] - [IntegrationTest] - public class SceneMappingProxyFixture : CoreTest - { - [SetUp] - public void Setup() - { - UseRealHttp(); - } - - [Test] - public void fetch_should_return_list_of_mappings() - { - var mappings = Subject.Fetch(); - - mappings.Should().NotBeEmpty(); - - mappings.Should().NotContain(c => c.SearchTerm.IsNullOrWhiteSpace()); - mappings.Should().NotContain(c => c.Title.IsNullOrWhiteSpace()); - mappings.Should().Contain(c => c.SeasonNumber > 0); - } - - } -} diff --git a/src/NzbDrone.Core.Test/DataAugmentation/Scene/SceneMappingServiceFixture.cs b/src/NzbDrone.Core.Test/DataAugmentation/Scene/SceneMappingServiceFixture.cs deleted file mode 100644 index 7953de12d..000000000 --- a/src/NzbDrone.Core.Test/DataAugmentation/Scene/SceneMappingServiceFixture.cs +++ /dev/null @@ -1,338 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using System.Net; -using FizzWare.NBuilder; -using Moq; -using NUnit.Framework; -using NzbDrone.Core.DataAugmentation.Scene; -using NzbDrone.Core.Test.Framework; -using NzbDrone.Test.Common; -using FluentAssertions; -using NzbDrone.Common.Extensions; -using NzbDrone.Core.DataAugmentation; - -namespace NzbDrone.Core.Test.DataAugmentation.Scene -{ - [TestFixture] - - public class SceneMappingServiceFixture : CoreTest - { - private List _fakeMappings; - - private Mock _provider1; - private Mock _provider2; - - [SetUp] - public void Setup() - { - _fakeMappings = Builder.CreateListOfSize(5).BuildListOfNew(); - - _fakeMappings[0].SearchTerm = "Words"; - _fakeMappings[1].SearchTerm = "That"; - _fakeMappings[2].SearchTerm = "Can"; - _fakeMappings[3].SearchTerm = "Be"; - _fakeMappings[4].SearchTerm = "Cleaned"; - - _fakeMappings[0].ParseTerm = "Words"; - _fakeMappings[1].ParseTerm = "That"; - _fakeMappings[2].ParseTerm = "Can"; - _fakeMappings[3].ParseTerm = "Be"; - _fakeMappings[4].ParseTerm = "Cleaned"; - - _provider1 = new Mock(); - _provider1.Setup(s => s.GetSceneMappings()).Returns(_fakeMappings); - - _provider2 = new Mock(); - _provider2.Setup(s => s.GetSceneMappings()).Returns(_fakeMappings); - } - - private void GivenProviders(IEnumerable> providers) - { - Mocker.SetConstant>(providers.Select(s => s.Object)); - } - - [Test] - public void should_purge_existing_mapping_and_add_new_ones() - { - GivenProviders(new [] { _provider1 }); - - Mocker.GetMock().Setup(c => c.All()).Returns(_fakeMappings); - - Subject.Execute(new UpdateSceneMappingCommand()); - - AssertMappingUpdated(); - } - - [Test] - public void should_not_delete_if_fetch_fails() - { - GivenProviders(new[] { _provider1 }); - - _provider1.Setup(c => c.GetSceneMappings()).Throws(new WebException()); - - Subject.Execute(new UpdateSceneMappingCommand()); - - AssertNoUpdate(); - - ExceptionVerification.ExpectedErrors(1); - } - - [Test] - public void should_not_delete_if_fetch_returns_empty_list() - { - GivenProviders(new[] { _provider1 }); - - _provider1.Setup(c => c.GetSceneMappings()).Returns(new List()); - - Subject.Execute(new UpdateSceneMappingCommand()); - - AssertNoUpdate(); - - ExceptionVerification.ExpectedWarns(1); - } - - [Test] - public void should_get_mappings_for_all_providers() - { - GivenProviders(new[] { _provider1, _provider2 }); - - Mocker.GetMock().Setup(c => c.All()).Returns(_fakeMappings); - - Subject.Execute(new UpdateSceneMappingCommand()); - - _provider1.Verify(c => c.GetSceneMappings(), Times.Once()); - _provider2.Verify(c => c.GetSceneMappings(), Times.Once()); - } - - [Test] - public void should_refresh_cache_if_cache_is_empty_when_looking_for_tvdb_id() - { - Subject.FindTvdbId("title"); - - Mocker.GetMock() - .Verify(v => v.All(), Times.Once()); - } - - [Test] - public void should_not_refresh_cache_if_cache_is_not_empty_when_looking_for_tvdb_id() - { - GivenProviders(new[] { _provider1 }); - - Mocker.GetMock() - .Setup(s => s.All()) - .Returns(Builder.CreateListOfSize(1).Build()); - - - Subject.Execute(new UpdateSceneMappingCommand()); - - Mocker.GetMock() - .Verify(v => v.All(), Times.Once()); - - Subject.FindTvdbId("title"); - - Mocker.GetMock() - .Verify(v => v.All(), Times.Once()); - } - - [Test] - public void should_not_add_mapping_with_blank_title() - { - GivenProviders(new[] { _provider1 }); - - var fakeMappings = Builder.CreateListOfSize(2) - .TheLast(1) - .With(m => m.Title = null) - .Build() - .ToList(); - - _provider1.Setup(s => s.GetSceneMappings()).Returns(fakeMappings); - - Mocker.GetMock().Setup(c => c.All()).Returns(_fakeMappings); - - Subject.Execute(new UpdateSceneMappingCommand()); - - Mocker.GetMock().Verify(c => c.InsertMany(It.Is>(m => !m.Any(s => s.Title.IsNullOrWhiteSpace()))), Times.Once()); - ExceptionVerification.ExpectedWarns(1); - } - - [Test] - public void should_not_add_mapping_with_blank_search_title() - { - GivenProviders(new[] { _provider1 }); - - var fakeMappings = Builder.CreateListOfSize(2) - .TheLast(1) - .With(m => m.SearchTerm = null) - .Build() - .ToList(); - - _provider1.Setup(s => s.GetSceneMappings()).Returns(fakeMappings); - - Mocker.GetMock().Setup(c => c.All()).Returns(_fakeMappings); - - Subject.Execute(new UpdateSceneMappingCommand()); - - Mocker.GetMock().Verify(c => c.InsertMany(It.Is>(m => !m.Any(s => s. SearchTerm.IsNullOrWhiteSpace()))), Times.Once()); - ExceptionVerification.ExpectedWarns(1); - } - - - [TestCase("Working!!", "Working!!", 1)] - [TestCase("Working`!!", "Working`!!", 2)] - [TestCase("Working!!!", "Working!!!", 3)] - [TestCase("Working!!!!", "Working!!!", 3)] - [TestCase("Working !!", "Working!!", 1)] - public void should_return_single_match(string parseTitle, string title, int expectedSeasonNumber) - { - var mappings = new List - { - new SceneMapping { Title = "Working!!", ParseTerm = "working", SearchTerm = "Working!!", TvdbId = 100, SceneSeasonNumber = 1 }, - new SceneMapping { Title = "Working`!!", ParseTerm = "working", SearchTerm = "Working`!!", TvdbId = 100, SceneSeasonNumber = 2 }, - new SceneMapping { Title = "Working!!!", ParseTerm = "working", SearchTerm = "Working!!!", TvdbId = 100, SceneSeasonNumber = 3 }, - }; - - Mocker.GetMock().Setup(c => c.All()).Returns(mappings); - - var tvdbId = Subject.FindTvdbId(parseTitle); - var seasonNumber = Subject.GetSceneSeasonNumber(parseTitle); - - tvdbId.Should().Be(100); - seasonNumber.Should().Be(expectedSeasonNumber); - } - - [Test] - public void should_return_alternate_title_for_global_season() - { - var mappings = new List - { - new SceneMapping { Title = "Fudanshi Koukou Seikatsu 1", ParseTerm = "fudanshikoukouseikatsu1", SearchTerm = "Fudanshi Koukou Seikatsu 1", TvdbId = 100, SeasonNumber = null, SceneSeasonNumber = null }, - new SceneMapping { Title = "Fudanshi Koukou Seikatsu 2", ParseTerm = "fudanshikoukouseikatsu2", SearchTerm = "Fudanshi Koukou Seikatsu 2", TvdbId = 100, SeasonNumber = -1, SceneSeasonNumber = null }, - new SceneMapping { Title = "Fudanshi Koukou Seikatsu 3", ParseTerm = "fudanshikoukouseikatsu3", SearchTerm = "Fudanshi Koukou Seikatsu 3", TvdbId = 100, SeasonNumber = null, SceneSeasonNumber = -1 }, - new SceneMapping { Title = "Fudanshi Koukou Seikatsu 4", ParseTerm = "fudanshikoukouseikatsu4", SearchTerm = "Fudanshi Koukou Seikatsu 4", TvdbId = 100, SeasonNumber = -1, SceneSeasonNumber = -1 }, - }; - - Mocker.GetMock().Setup(c => c.All()).Returns(mappings); - - var names = Subject.GetSceneNames(100, new List { 10 }, new List { 10 }); - names.Should().HaveCount(4); - } - - [Test] - public void should_return_alternate_title_for_season() - { - var mappings = new List - { - new SceneMapping { Title = "Fudanshi Koukou Seikatsu", ParseTerm = "fudanshikoukouseikatsu", SearchTerm = "Fudanshi Koukou Seikatsu", TvdbId = 100, SeasonNumber = 1, SceneSeasonNumber = null } - }; - - Mocker.GetMock().Setup(c => c.All()).Returns(mappings); - - var names = Subject.GetSceneNames(100, new List { 1 }, new List { 10 }); - names.Should().HaveCount(1); - } - - [Test] - public void should_not_return_alternate_title_for_season() - { - var mappings = new List - { - new SceneMapping { Title = "Fudanshi Koukou Seikatsu", ParseTerm = "fudanshikoukouseikatsu", SearchTerm = "Fudanshi Koukou Seikatsu", TvdbId = 100, SeasonNumber = 1, SceneSeasonNumber = null } - }; - - Mocker.GetMock().Setup(c => c.All()).Returns(mappings); - - var names = Subject.GetSceneNames(100, new List { 2 }, new List { 10 }); - names.Should().BeEmpty(); - } - - [Test] - public void should_return_alternate_title_for_sceneseason() - { - var mappings = new List - { - new SceneMapping { Title = "Fudanshi Koukou Seikatsu", ParseTerm = "fudanshikoukouseikatsu", SearchTerm = "Fudanshi Koukou Seikatsu", TvdbId = 100, SeasonNumber = null, SceneSeasonNumber = 1 } - }; - - Mocker.GetMock().Setup(c => c.All()).Returns(mappings); - - var names = Subject.GetSceneNames(100, new List { 10 }, new List { 1 }); - names.Should().HaveCount(1); - } - - [Test] - public void should_not_return_alternate_title_for_sceneseason() - { - var mappings = new List - { - new SceneMapping { Title = "Fudanshi Koukou Seikatsu", ParseTerm = "fudanshikoukouseikatsu", SearchTerm = "Fudanshi Koukou Seikatsu", TvdbId = 100, SeasonNumber = null, SceneSeasonNumber = 1 } - }; - - Mocker.GetMock().Setup(c => c.All()).Returns(mappings); - - var names = Subject.GetSceneNames(100, new List { 10 }, new List { 2 }); - names.Should().BeEmpty(); - } - - [Test] - public void should_return_alternate_title_for_fairy_tail() - { - var mappings = new List - { - new SceneMapping { Title = "Fairy Tail S2", ParseTerm = "fairytails2", SearchTerm = "Fairy Tail S2", TvdbId = 100, SeasonNumber = null, SceneSeasonNumber = 2 } - }; - - Mocker.GetMock().Setup(c => c.All()).Returns(mappings); - - Subject.GetSceneNames(100, new List { 4 }, new List { 20 }).Should().BeEmpty(); - Subject.GetSceneNames(100, new List { 5 }, new List { 20 }).Should().BeEmpty(); - Subject.GetSceneNames(100, new List { 6 }, new List { 20 }).Should().BeEmpty(); - Subject.GetSceneNames(100, new List { 7 }, new List { 20 }).Should().BeEmpty(); - - Subject.GetSceneNames(100, new List { 20 }, new List { 1 }).Should().BeEmpty(); - Subject.GetSceneNames(100, new List { 20 }, new List { 2 }).Should().NotBeEmpty(); - Subject.GetSceneNames(100, new List { 20 }, new List { 3 }).Should().BeEmpty(); - Subject.GetSceneNames(100, new List { 20 }, new List { 4 }).Should().BeEmpty(); - } - - [Test] - public void should_return_alternate_title_for_fudanshi() - { - var mappings = new List - { - new SceneMapping { Title = "Fudanshi Koukou Seikatsu", ParseTerm = "fudanshikoukouseikatsu", SearchTerm = "Fudanshi Koukou Seikatsu", TvdbId = 100, SeasonNumber = null, SceneSeasonNumber = 1 } - }; - - Mocker.GetMock().Setup(c => c.All()).Returns(mappings); - - Subject.GetSceneNames(100, new List { 1 }, new List { 20 }).Should().BeEmpty(); - Subject.GetSceneNames(100, new List { 2 }, new List { 20 }).Should().BeEmpty(); - Subject.GetSceneNames(100, new List { 3 }, new List { 20 }).Should().BeEmpty(); - Subject.GetSceneNames(100, new List { 4 }, new List { 20 }).Should().BeEmpty(); - - Subject.GetSceneNames(100, new List { 1 }, new List { 1 }).Should().NotBeEmpty(); - Subject.GetSceneNames(100, new List { 2 }, new List { 2 }).Should().BeEmpty(); - Subject.GetSceneNames(100, new List { 3 }, new List { 3 }).Should().BeEmpty(); - Subject.GetSceneNames(100, new List { 4 }, new List { 4 }).Should().BeEmpty(); - } - - private void AssertNoUpdate() - { - _provider1.Verify(c => c.GetSceneMappings(), Times.Once()); - Mocker.GetMock().Verify(c => c.Clear(It.IsAny()), Times.Never()); - Mocker.GetMock().Verify(c => c.InsertMany(_fakeMappings), Times.Never()); - } - - private void AssertMappingUpdated() - { - _provider1.Verify(c => c.GetSceneMappings(), Times.Once()); - Mocker.GetMock().Verify(c => c.Clear(It.IsAny()), Times.Once()); - Mocker.GetMock().Verify(c => c.InsertMany(_fakeMappings), Times.Once()); - - foreach (var sceneMapping in _fakeMappings) - { - Subject.GetSceneNames(sceneMapping.TvdbId, _fakeMappings.Select(m => m.SeasonNumber.Value).Distinct().ToList(), new List()).Should().Contain(sceneMapping.SearchTerm); - Subject.FindTvdbId(sceneMapping.ParseTerm).Should().Be(sceneMapping.TvdbId); - } - } - } -} diff --git a/src/NzbDrone.Core.Test/DataAugmentation/SceneNumbering/XemServiceFixture.cs b/src/NzbDrone.Core.Test/DataAugmentation/SceneNumbering/XemServiceFixture.cs deleted file mode 100644 index 3f263c6dd..000000000 --- a/src/NzbDrone.Core.Test/DataAugmentation/SceneNumbering/XemServiceFixture.cs +++ /dev/null @@ -1,312 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using FizzWare.NBuilder; -using FluentAssertions; -using Moq; -using NUnit.Framework; -using NzbDrone.Core.DataAugmentation.Xem; -using NzbDrone.Core.DataAugmentation.Xem.Model; -using NzbDrone.Core.Test.Framework; -using NzbDrone.Core.Tv; -using NzbDrone.Core.Tv.Events; -using NzbDrone.Test.Common; - -namespace NzbDrone.Core.Test.DataAugmentation.SceneNumbering -{ - [TestFixture] - public class XemServiceFixture : CoreTest - { - private Series _series; - private List _theXemSeriesIds; - private List _theXemTvdbMappings; - private List _episodes; - - [SetUp] - public void SetUp() - { - _series = Builder.CreateNew() - .With(v => v.TvdbId = 10) - .With(v => v.UseSceneNumbering = false) - .BuildNew(); - - _theXemSeriesIds = new List { 120 }; - Mocker.GetMock() - .Setup(v => v.GetXemSeriesIds()) - .Returns(_theXemSeriesIds); - - _theXemTvdbMappings = new List(); - Mocker.GetMock() - .Setup(v => v.GetSceneTvdbMappings(10)) - .Returns(_theXemTvdbMappings); - - _episodes = new List(); - _episodes.Add(new Episode { SeasonNumber = 1, EpisodeNumber = 1 }); - _episodes.Add(new Episode { SeasonNumber = 1, EpisodeNumber = 2 }); - _episodes.Add(new Episode { SeasonNumber = 2, EpisodeNumber = 1 }); - _episodes.Add(new Episode { SeasonNumber = 2, EpisodeNumber = 2 }); - _episodes.Add(new Episode { SeasonNumber = 2, EpisodeNumber = 3 }); - _episodes.Add(new Episode { SeasonNumber = 2, EpisodeNumber = 4 }); - _episodes.Add(new Episode { SeasonNumber = 2, EpisodeNumber = 5 }); - _episodes.Add(new Episode { SeasonNumber = 3, EpisodeNumber = 1 }); - _episodes.Add(new Episode { SeasonNumber = 3, EpisodeNumber = 2 }); - - Mocker.GetMock() - .Setup(v => v.GetEpisodeBySeries(It.IsAny())) - .Returns(_episodes); - } - - private void GivenTvdbMappings() - { - _theXemSeriesIds.Add(10); - - AddTvdbMapping(1, 1, 1, 1, 1, 1); // 1x01 -> 1x01 - AddTvdbMapping(2, 1, 2, 2, 1, 2); // 1x02 -> 1x02 - AddTvdbMapping(3, 2, 1, 3, 2, 1); // 2x01 -> 2x01 - AddTvdbMapping(4, 2, 2, 4, 2, 2); // 2x02 -> 2x02 - AddTvdbMapping(5, 2, 3, 5, 2, 3); // 2x03 -> 2x03 - AddTvdbMapping(6, 3, 1, 6, 2, 4); // 3x01 -> 2x04 - AddTvdbMapping(7, 3, 2, 7, 2, 5); // 3x02 -> 2x05 - } - - private void GivenExistingMapping() - { - _series.UseSceneNumbering = true; - - _episodes[0].SceneSeasonNumber = 1; - _episodes[0].SceneEpisodeNumber = 1; - _episodes[1].SceneSeasonNumber = 1; - _episodes[1].SceneEpisodeNumber = 2; - _episodes[2].SceneSeasonNumber = 2; - _episodes[2].SceneEpisodeNumber = 1; - _episodes[3].SceneSeasonNumber = 2; - _episodes[3].SceneEpisodeNumber = 2; - _episodes[4].SceneSeasonNumber = 2; - _episodes[4].SceneEpisodeNumber = 3; - _episodes[5].SceneSeasonNumber = 3; - _episodes[5].SceneEpisodeNumber = 1; - _episodes[6].SceneSeasonNumber = 3; - _episodes[6].SceneEpisodeNumber = 1; - } - - private void AddTvdbMapping(int sceneAbsolute, int sceneSeason, int sceneEpisode, int tvdbAbsolute, int tvdbSeason, int tvdbEpisode) - { - _theXemTvdbMappings.Add(new XemSceneTvdbMapping - { - Scene = new XemValues { Absolute = sceneAbsolute, Season = sceneSeason, Episode = sceneEpisode }, - Tvdb = new XemValues { Absolute = tvdbAbsolute, Season = tvdbSeason, Episode = tvdbEpisode }, - }); - } - - - [Test] - public void should_not_fetch_scenenumbering_if_not_listed() - { - Subject.Handle(new SeriesUpdatedEvent(_series)); - - Mocker.GetMock() - .Verify(v => v.GetSceneTvdbMappings(10), Times.Never()); - - Mocker.GetMock() - .Verify(v => v.UpdateSeries(It.IsAny()), Times.Never()); - } - - [Test] - public void should_fetch_scenenumbering() - { - GivenTvdbMappings(); - - Subject.Handle(new SeriesUpdatedEvent(_series)); - - Mocker.GetMock() - .Verify(v => v.UpdateSeries(It.Is(s => s.UseSceneNumbering == true)), Times.Once()); - } - - [Test] - public void should_clear_scenenumbering_if_removed_from_thexem() - { - GivenExistingMapping(); - - Subject.Handle(new SeriesUpdatedEvent(_series)); - - Mocker.GetMock() - .Verify(v => v.UpdateSeries(It.IsAny()), Times.Once()); - } - - [Test] - public void should_not_clear_scenenumbering_if_no_results_at_all_from_thexem() - { - GivenExistingMapping(); - - _theXemSeriesIds.Clear(); - - Subject.Handle(new SeriesUpdatedEvent(_series)); - - Mocker.GetMock() - .Verify(v => v.UpdateSeries(It.IsAny()), Times.Never()); - - ExceptionVerification.ExpectedWarns(1); - } - - [Test] - public void should_not_clear_scenenumbering_if_thexem_throws() - { - GivenExistingMapping(); - - Mocker.GetMock() - .Setup(v => v.GetXemSeriesIds()) - .Throws(new InvalidOperationException()); - - Subject.Handle(new SeriesUpdatedEvent(_series)); - - Mocker.GetMock() - .Verify(v => v.UpdateSeries(It.IsAny()), Times.Never()); - - ExceptionVerification.ExpectedWarns(1); - } - - [Test] - public void should_flag_unknown_future_episodes_if_existing_season_is_mapped() - { - GivenTvdbMappings(); - _theXemTvdbMappings.RemoveAll(v => v.Tvdb.Season == 2 && v.Tvdb.Episode == 5); - - Subject.Handle(new SeriesUpdatedEvent(_series)); - - var episode = _episodes.First(v => v.SeasonNumber == 2 && v.EpisodeNumber == 5); - - episode.UnverifiedSceneNumbering.Should().BeTrue(); - } - - [Test] - public void should_flag_unknown_future_season_if_future_season_is_shifted() - { - GivenTvdbMappings(); - - Subject.Handle(new SeriesUpdatedEvent(_series)); - - var episode = _episodes.First(v => v.SeasonNumber == 3 && v.EpisodeNumber == 1); - - episode.UnverifiedSceneNumbering.Should().BeTrue(); - } - - [Test] - public void should_not_flag_unknown_future_season_if_future_season_is_not_shifted() - { - GivenTvdbMappings(); - _theXemTvdbMappings.RemoveAll(v => v.Scene.Season == 3); - - Subject.Handle(new SeriesUpdatedEvent(_series)); - - var episode = _episodes.First(v => v.SeasonNumber == 3 && v.EpisodeNumber == 1); - - episode.UnverifiedSceneNumbering.Should().BeFalse(); - } - - [Test] - public void should_not_flag_past_episodes_if_not_causing_overlaps() - { - GivenTvdbMappings(); - _theXemTvdbMappings.RemoveAll(v => v.Scene.Season == 2); - - Subject.Handle(new SeriesUpdatedEvent(_series)); - - var episode = _episodes.First(v => v.SeasonNumber == 2 && v.EpisodeNumber == 1); - - episode.UnverifiedSceneNumbering.Should().BeFalse(); - } - - [Test] - public void should_flag_past_episodes_if_causing_overlap() - { - GivenTvdbMappings(); - _theXemTvdbMappings.RemoveAll(v => v.Scene.Season == 2 && v.Tvdb.Episode <= 1); - _theXemTvdbMappings.First(v => v.Scene.Season == 2 && v.Scene.Episode == 2).Scene.Episode = 1; - - Subject.Handle(new SeriesUpdatedEvent(_series)); - - var episode = _episodes.First(v => v.SeasonNumber == 2 && v.EpisodeNumber == 1); - - episode.UnverifiedSceneNumbering.Should().BeTrue(); - } - - [Test] - public void should_not_extrapolate_season_with_specials() - { - GivenTvdbMappings(); - var specialMapping = _theXemTvdbMappings.First(v => v.Tvdb.Season == 2 && v.Tvdb.Episode == 5); - specialMapping.Tvdb.Season = 0; - specialMapping.Tvdb.Episode = 1; - - Subject.Handle(new SeriesUpdatedEvent(_series)); - - var episode = _episodes.First(v => v.SeasonNumber == 2 && v.EpisodeNumber == 5); - - episode.UnverifiedSceneNumbering.Should().BeTrue(); - episode.SceneSeasonNumber.Should().NotHaveValue(); - episode.SceneEpisodeNumber.Should().NotHaveValue(); - } - - [Test] - public void should_extrapolate_season_with_future_episodes() - { - GivenTvdbMappings(); - _theXemTvdbMappings.RemoveAll(v => v.Tvdb.Season == 2 && v.Tvdb.Episode == 5); - - Subject.Handle(new SeriesUpdatedEvent(_series)); - - var episode = _episodes.First(v => v.SeasonNumber == 2 && v.EpisodeNumber == 5); - - episode.UnverifiedSceneNumbering.Should().BeTrue(); - episode.SceneSeasonNumber.Should().Be(3); - episode.SceneEpisodeNumber.Should().Be(2); - } - - [Test] - public void should_extrapolate_season_with_shifted_episodes() - { - GivenTvdbMappings(); - _theXemTvdbMappings.RemoveAll(v => v.Tvdb.Season == 2 && v.Tvdb.Episode == 5); - var dualMapping = _theXemTvdbMappings.First(v => v.Tvdb.Season == 2 && v.Tvdb.Episode == 4); - dualMapping.Scene.Season = 2; - dualMapping.Scene.Episode = 3; - - Subject.Handle(new SeriesUpdatedEvent(_series)); - - var episode = _episodes.First(v => v.SeasonNumber == 2 && v.EpisodeNumber == 5); - - episode.UnverifiedSceneNumbering.Should().BeTrue(); - episode.SceneSeasonNumber.Should().Be(2); - episode.SceneEpisodeNumber.Should().Be(4); - } - - [Test] - public void should_extrapolate_shifted_future_seasons() - { - GivenTvdbMappings(); - - Subject.Handle(new SeriesUpdatedEvent(_series)); - - var episode = _episodes.First(v => v.SeasonNumber == 3 && v.EpisodeNumber == 2); - - episode.UnverifiedSceneNumbering.Should().BeTrue(); - episode.SceneSeasonNumber.Should().Be(4); - episode.SceneEpisodeNumber.Should().Be(2); - } - - [Test] - public void should_not_extrapolate_matching_future_seasons() - { - GivenTvdbMappings(); - _theXemTvdbMappings.RemoveAll(v => v.Scene.Season != 1); - - Subject.Handle(new SeriesUpdatedEvent(_series)); - - var episode = _episodes.First(v => v.SeasonNumber == 3 && v.EpisodeNumber == 2); - - episode.UnverifiedSceneNumbering.Should().BeFalse(); - episode.SceneSeasonNumber.Should().NotHaveValue(); - episode.SceneEpisodeNumber.Should().NotHaveValue(); - } - } -} diff --git a/src/NzbDrone.Core.Test/Datastore/DatabaseFixture.cs b/src/NzbDrone.Core.Test/Datastore/DatabaseFixture.cs index e1942d6c3..be8b6c7ec 100644 --- a/src/NzbDrone.Core.Test/Datastore/DatabaseFixture.cs +++ b/src/NzbDrone.Core.Test/Datastore/DatabaseFixture.cs @@ -4,7 +4,7 @@ using FluentAssertions; using NUnit.Framework; using NzbDrone.Core.Datastore; using NzbDrone.Core.Test.Framework; -using NzbDrone.Core.Tv; +using NzbDrone.Core.Music; namespace NzbDrone.Core.Test.Datastore { @@ -14,8 +14,8 @@ namespace NzbDrone.Core.Test.Datastore public void SingleOrDefault_should_return_null_on_empty_db() { Mocker.Resolve() - .GetDataMapper().Query() - .SingleOrDefault(c => c.CleanTitle == "SomeTitle") + .GetDataMapper().Query() + .SingleOrDefault(c => c.CleanName == "SomeTitle") .Should() .BeNull(); } @@ -33,4 +33,4 @@ namespace NzbDrone.Core.Test.Datastore Mocker.Resolve().Version.Should().BeGreaterThan(new Version("3.0.0")); } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Core.Test/Datastore/DatabaseRelationshipFixture.cs b/src/NzbDrone.Core.Test/Datastore/DatabaseRelationshipFixture.cs index 3878c41c9..0eb08c688 100644 --- a/src/NzbDrone.Core.Test/Datastore/DatabaseRelationshipFixture.cs +++ b/src/NzbDrone.Core.Test/Datastore/DatabaseRelationshipFixture.cs @@ -1,11 +1,12 @@ -using System.Linq; +using System.Linq; using FizzWare.NBuilder; using FluentAssertions; using NUnit.Framework; using NzbDrone.Core.MediaFiles; using NzbDrone.Core.Qualities; using NzbDrone.Core.Test.Framework; -using NzbDrone.Core.Tv; +using NzbDrone.Core.Music; +using NzbDrone.Core.Languages; namespace NzbDrone.Core.Test.Datastore { @@ -15,40 +16,41 @@ namespace NzbDrone.Core.Test.Datastore [Test] public void one_to_one() { - var episodeFile = Builder.CreateNew() + var trackFile = Builder.CreateNew() .With(c => c.Quality = new QualityModel()) + .With(c => c.Language = Language.English) .BuildNew(); - Db.Insert(episodeFile); + Db.Insert(trackFile); - var episode = Builder.CreateNew() - .With(c => c.EpisodeFileId = episodeFile.Id) + var track = Builder.CreateNew() + .With(c => c.TrackFileId = trackFile.Id) .BuildNew(); - Db.Insert(episode); + Db.Insert(track); - var loadedEpisodeFile = Db.Single().EpisodeFile.Value; + var loadedTrackFile = Db.Single().TrackFile.Value; - loadedEpisodeFile.Should().NotBeNull(); - loadedEpisodeFile.ShouldBeEquivalentTo(episodeFile, + loadedTrackFile.Should().NotBeNull(); + loadedTrackFile.ShouldBeEquivalentTo(trackFile, options => options .IncludingAllRuntimeProperties() .Excluding(c => c.DateAdded) .Excluding(c => c.Path) - .Excluding(c => c.Series) - .Excluding(c => c.Episodes)); + .Excluding(c => c.Artist) + .Excluding(c => c.Tracks)); } [Test] public void one_to_one_should_not_query_db_if_foreign_key_is_zero() { - var episode = Builder.CreateNew() - .With(c => c.EpisodeFileId = 0) + var track = Builder.CreateNew() + .With(c => c.TrackFileId = 0) .BuildNew(); - Db.Insert(episode); + Db.Insert(track); - Db.Single().EpisodeFile.Value.Should().BeNull(); + Db.Single().TrackFile.Value.Should().BeNull(); } diff --git a/src/NzbDrone.Core.Test/Datastore/MappingExtentionFixture.cs b/src/NzbDrone.Core.Test/Datastore/MappingExtentionFixture.cs index 76558e6f1..7db539fc3 100644 --- a/src/NzbDrone.Core.Test/Datastore/MappingExtentionFixture.cs +++ b/src/NzbDrone.Core.Test/Datastore/MappingExtentionFixture.cs @@ -1,11 +1,11 @@ -using System.Collections.Generic; +using System.Collections.Generic; using FluentAssertions; using Marr.Data; using NUnit.Framework; using NzbDrone.Core.Datastore; using NzbDrone.Core.Datastore.Converters; using NzbDrone.Core.Datastore.Extensions; -using NzbDrone.Core.Tv; +using NzbDrone.Core.Music; namespace NzbDrone.Core.Test.Datastore { @@ -30,7 +30,7 @@ namespace NzbDrone.Core.Test.Datastore public class TypeWithNoMappableProperties { - public Series Series { get; set; } + public Artist Artist { get; set; } public int ReadOnly { get; private set; } public int WriteOnly { private get; set; } @@ -62,4 +62,4 @@ namespace NzbDrone.Core.Test.Datastore properties.Should().NotContain(c => MappingExtensions.IsMappableProperty(c)); } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Core.Test/Datastore/MarrDataLazyLoadingFixture.cs b/src/NzbDrone.Core.Test/Datastore/MarrDataLazyLoadingFixture.cs index 1633baae4..f4c9d7cba 100644 --- a/src/NzbDrone.Core.Test/Datastore/MarrDataLazyLoadingFixture.cs +++ b/src/NzbDrone.Core.Test/Datastore/MarrDataLazyLoadingFixture.cs @@ -1,11 +1,14 @@ -using FizzWare.NBuilder; +using FizzWare.NBuilder; using NUnit.Framework; using NzbDrone.Core.Datastore; -using NzbDrone.Core.Profiles; +using NzbDrone.Core.Profiles.Qualities; using NzbDrone.Core.Test.Framework; -using NzbDrone.Core.Tv; +using NzbDrone.Core.Music; using NzbDrone.Core.Qualities; using NzbDrone.Core.MediaFiles; +using NzbDrone.Core.Languages; +using NzbDrone.Core.Profiles.Languages; +using NzbDrone.Core.Test.Languages; namespace NzbDrone.Core.Test.Datastore { @@ -17,38 +20,48 @@ namespace NzbDrone.Core.Test.Datastore public void Setup() { var profile = new Profile - { - Name = "Test", - Cutoff = Quality.MP3_320, - Items = Qualities.QualityFixture.GetDefaultQualities() - }; + { + Name = "Test", + Cutoff = Quality.MP3_320, + Items = Qualities.QualityFixture.GetDefaultQualities() + }; + + var languageProfile = new LanguageProfile + { + Name = "Test", + Languages = LanguageFixture.GetDefaultLanguages(Language.English), + Cutoff = Language.English + }; + + - profile = Db.Insert(profile); + languageProfile = Db.Insert(languageProfile); - var series = Builder.CreateListOfSize(1) + var artist = Builder.CreateListOfSize(1) .All() .With(v => v.ProfileId = profile.Id) + .With(v => v.LanguageProfileId = languageProfile.Id) .BuildListOfNew(); - Db.InsertMany(series); + Db.InsertMany(artist); - var episodeFiles = Builder.CreateListOfSize(1) + var trackFiles = Builder.CreateListOfSize(1) .All() - .With(v => v.SeriesId = series[0].Id) + .With(v => v.ArtistId = artist[0].Id) .With(v => v.Quality = new QualityModel()) .BuildListOfNew(); - Db.InsertMany(episodeFiles); + Db.InsertMany(trackFiles); - var episodes = Builder.CreateListOfSize(10) + var tracks = Builder.CreateListOfSize(10) .All() .With(v => v.Monitored = true) - .With(v => v.EpisodeFileId = episodeFiles[0].Id) - .With(v => v.SeriesId = series[0].Id) + .With(v => v.TrackFileId = trackFiles[0].Id) + .With(v => v.ArtistId = artist[0].Id) .BuildListOfNew(); - Db.InsertMany(episodes); + Db.InsertMany(tracks); } [Test] @@ -57,31 +70,32 @@ namespace NzbDrone.Core.Test.Datastore var db = Mocker.Resolve(); var DataMapper = db.GetDataMapper(); - var episodes = DataMapper.Query() - .Join(Marr.Data.QGen.JoinType.Inner, v => v.Series, (l, r) => l.SeriesId == r.Id) + var tracks = DataMapper.Query() + .Join(Marr.Data.QGen.JoinType.Inner, v => v.Artist, (l, r) => l.ArtistId == r.Id) .ToList(); - foreach (var episode in episodes) + foreach (var track in tracks) { - Assert.IsNotNull(episode.Series); - Assert.IsFalse(episode.Series.Profile.IsLoaded); + Assert.IsNotNull(track.Artist); + Assert.IsFalse(track.Artist.Profile.IsLoaded); + Assert.IsFalse(track.Artist.LanguageProfile.IsLoaded); } } [Test] - public void should_explicit_load_episodefile_if_joined() + public void should_explicit_load_trackfile_if_joined() { var db = Mocker.Resolve(); var DataMapper = db.GetDataMapper(); - var episodes = DataMapper.Query() - .Join(Marr.Data.QGen.JoinType.Inner, v => v.EpisodeFile, (l, r) => l.EpisodeFileId == r.Id) + var tracks = DataMapper.Query() + .Join(Marr.Data.QGen.JoinType.Inner, v => v.TrackFile, (l, r) => l.TrackFileId == r.Id) .ToList(); - foreach (var episode in episodes) + foreach (var track in tracks) { - Assert.IsNull(episode.Series); - Assert.IsTrue(episode.EpisodeFile.IsLoaded); + Assert.IsNull(track.Artist); + Assert.IsTrue(track.TrackFile.IsLoaded); } } @@ -91,17 +105,37 @@ namespace NzbDrone.Core.Test.Datastore var db = Mocker.Resolve(); var DataMapper = db.GetDataMapper(); - var episodes = DataMapper.Query() - .Join(Marr.Data.QGen.JoinType.Inner, v => v.Series, (l, r) => l.SeriesId == r.Id) - .Join(Marr.Data.QGen.JoinType.Inner, v => v.Profile, (l, r) => l.ProfileId == r.Id) + var tracks = DataMapper.Query() + .Join(Marr.Data.QGen.JoinType.Inner, v => v.Artist, (l, r) => l.ArtistId == r.Id) + .Join(Marr.Data.QGen.JoinType.Inner, v => v.Profile, (l, r) => l.ProfileId == r.Id) .ToList(); - foreach (var episode in episodes) + foreach (var track in tracks) + { + Assert.IsNotNull(track.Artist); + Assert.IsTrue(track.Artist.Profile.IsLoaded); + Assert.IsFalse(track.Artist.LanguageProfile.IsLoaded); + } + } + + [Test] + public void should_explicit_load_languageprofile_if_joined() + { + var db = Mocker.Resolve(); + var DataMapper = db.GetDataMapper(); + + var tracks = DataMapper.Query() + .Join(Marr.Data.QGen.JoinType.Inner, v => v.Artist, (l, r) => l.ArtistId == r.Id) + .Join(Marr.Data.QGen.JoinType.Inner, v => v.LanguageProfile, (l, r) => l.ProfileId == r.Id) + .ToList(); + + foreach (var track in tracks) { - Assert.IsNotNull(episode.Series); - Assert.IsTrue(episode.Series.Profile.IsLoaded); + Assert.IsNotNull(track.Artist); + Assert.IsFalse(track.Artist.Profile.IsLoaded); + Assert.IsTrue(track.Artist.LanguageProfile.IsLoaded); } } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Core.Test/Datastore/Migration/070_delay_profileFixture.cs b/src/NzbDrone.Core.Test/Datastore/Migration/070_delay_profileFixture.cs deleted file mode 100644 index be2b07b66..000000000 --- a/src/NzbDrone.Core.Test/Datastore/Migration/070_delay_profileFixture.cs +++ /dev/null @@ -1,102 +0,0 @@ -using System.Linq; -using FluentAssertions; -using NUnit.Framework; -using NzbDrone.Core.Datastore.Migration; -using NzbDrone.Core.Test.Framework; - -namespace NzbDrone.Core.Test.Datastore.Migration -{ - [TestFixture] - public class delay_profileFixture : MigrationTest - { - [Test] - public void should_migrate_old_delays() - { - var db = WithMigrationTestDb(c => - { - c.Insert.IntoTable("Profiles").Row(new - { - GrabDelay = 1, - Name = "OneHour", - Cutoff = 0, - Items = "[]" - }); - - c.Insert.IntoTable("Profiles").Row(new - { - GrabDelay = 2, - Name = "TwoHours", - Cutoff = "{}", - Items = "[]" - }); - }); - - var allProfiles = db.Query("SELECT * FROM DelayProfiles"); - - allProfiles.Should().HaveCount(3); - allProfiles.Should().OnlyContain(c => c.PreferredProtocol == 1); - allProfiles.Should().OnlyContain(c => c.TorrentDelay == 0); - allProfiles.Should().Contain(c => c.UsenetDelay == 60); - allProfiles.Should().Contain(c => c.UsenetDelay == 120); - } - - [Test] - public void should_create_tag_for_delay_profile() - { - var db = WithMigrationTestDb(c => - { - c.Insert.IntoTable("Profiles").Row(new - { - GrabDelay = 1, - Name = "OneHour", - Cutoff = 0, - Items = "[]" - }); - }); - - var tags = db.Query("SELECT * FROM Tags"); - - tags.Should().HaveCount(1); - tags.First().Label.Should().Be("delay-60"); - } - - [Test] - public void should_add_tag_to_series_that_had_a_profile_with_delay_attached() - { - var db = WithMigrationTestDb(c => - { - c.Insert.IntoTable("Profiles").Row(new - { - GrabDelay = 1, - Name = "OneHour", - Cutoff = 0, - Items = "[]" - }); - - c.Insert.IntoTable("Series").Row(new - { - TvdbId = 0, - TvRageId = 0, - Title = "Series", - TitleSlug = "series", - CleanTitle = "series", - Status = 0, - Images = "[]", - Path = @"C:\Test\Series", - Monitored = 1, - SeasonFolder = 1, - RunTime = 0, - SeriesType = 0, - UseSceneNumbering = 0, - Tags = "[1]" - }); - }); - - var tag = db.Query("SELECT Id, Label FROM Tags").Single(); - var series = db.Query("SELECT Tags FROM Series"); - - series.Should().HaveCount(1); - series.First().Tags.Should().BeEquivalentTo(tag.Id); - } - } -} diff --git a/src/NzbDrone.Core.Test/Datastore/Migration/071_unknown_quality_in_profileFixture.cs b/src/NzbDrone.Core.Test/Datastore/Migration/071_unknown_quality_in_profileFixture.cs deleted file mode 100644 index ad31df44c..000000000 --- a/src/NzbDrone.Core.Test/Datastore/Migration/071_unknown_quality_in_profileFixture.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System.Linq; -using FluentAssertions; -using NUnit.Framework; -using NzbDrone.Core.Datastore.Migration; -using NzbDrone.Core.Test.Framework; - -namespace NzbDrone.Core.Test.Datastore.Migration -{ - [TestFixture] - public class unknown_quality_in_profileFixture : MigrationTest - { - [Test] - public void should_add_unknown_to_old_profile() - { - var db = WithMigrationTestDb(c => - { - c.Insert.IntoTable("Profiles").Row(new - { - Id = 0, - Name = "SDTV", - Cutoff = 1, - Items = "[ { \"quality\": 1, \"allowed\": true } ]", - Language = 1 - }); - }); - - var profiles = db.Query("SELECT Items FROM Profiles LIMIT 1"); - - var items = profiles.First().Items; - items.Should().HaveCount(2); - items.First().Quality.Should().Be(0); - items.First().Allowed.Should().Be(false); - } - } -} diff --git a/src/NzbDrone.Core.Test/Datastore/Migration/072_history_downloadIdFixture.cs b/src/NzbDrone.Core.Test/Datastore/Migration/072_history_downloadIdFixture.cs deleted file mode 100644 index c976f9b10..000000000 --- a/src/NzbDrone.Core.Test/Datastore/Migration/072_history_downloadIdFixture.cs +++ /dev/null @@ -1,106 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using FluentAssertions; -using FluentMigrator; -using NUnit.Framework; -using NzbDrone.Common.Serializer; -using NzbDrone.Core.Datastore.Migration; -using NzbDrone.Core.Test.Framework; - -namespace NzbDrone.Core.Test.Datastore.Migration -{ - [TestFixture] - public class history_downloadIdFixture : MigrationTest - { - [Test] - public void should_move_grab_id_from_date_to_columns() - { - var db = WithMigrationTestDb(c => - { - InsertHistory(c, new Dictionary - { - {"indexer","test"}, - {"downloadClientId","123"} - }); - - InsertHistory(c, new Dictionary - { - {"indexer","test"}, - {"downloadClientId","abc"} - }); - - }); - - var history = db.Query("SELECT DownloadId, Data FROM History"); - - history.Should().HaveCount(2); - history.Should().NotContain(c => c.Data.ContainsKey("downloadClientId")); - history.Should().Contain(c => c.DownloadId == "123"); - history.Should().Contain(c => c.DownloadId == "abc"); - } - - - [Test] - public void should_leave_items_with_no_grabid() - { - var db = WithMigrationTestDb(c => - { - InsertHistory(c, new Dictionary - { - {"indexer","test"}, - {"downloadClientId","123"} - }); - - InsertHistory(c, new Dictionary - { - {"indexer","test"} - }); - - }); - - var history = db.Query("SELECT DownloadId, Data FROM History"); - - history.Should().HaveCount(2); - history.Should().NotContain(c => c.Data.ContainsKey("downloadClientId")); - history.Should().Contain(c => c.DownloadId == "123"); - history.Should().Contain(c => c.DownloadId == null); - } - - [Test] - public void should_leave_other_data() - { - var db = WithMigrationTestDb(c => - { - InsertHistory(c, new Dictionary - { - {"indexer","test"}, - {"group","test2"}, - {"downloadClientId","123"} - }); - }); - - var history = db.Query("SELECT DownloadId, Data FROM History").Single(); - - history.Data.Should().NotContainKey("downloadClientId"); - history.Data.Should().Contain(new KeyValuePair("indexer", "test")); - history.Data.Should().Contain(new KeyValuePair("group", "test2")); - - history.DownloadId.Should().Be("123"); - } - - private void InsertHistory(MigrationBase migrationBase, Dictionary data) - { - migrationBase.Insert.IntoTable("History").Row(new - { - EpisodeId = 1, - SeriesId = 1, - SourceTitle = "Test", - Date = DateTime.Now, - Quality = "{}", - Data = data.ToJson(), - EventType = 1 - }); - } - } -} diff --git a/src/NzbDrone.Core.Test/Datastore/Migration/075_force_lib_updateFixture.cs b/src/NzbDrone.Core.Test/Datastore/Migration/075_force_lib_updateFixture.cs deleted file mode 100644 index 1d6c113b8..000000000 --- a/src/NzbDrone.Core.Test/Datastore/Migration/075_force_lib_updateFixture.cs +++ /dev/null @@ -1,98 +0,0 @@ -using System.Linq; -using FluentAssertions; -using NUnit.Framework; -using NzbDrone.Core.Test.Framework; -using NzbDrone.Core.Datastore.Migration; - -namespace NzbDrone.Core.Test.Datastore.Migration -{ - [TestFixture] - public class force_lib_updateFixture : MigrationTest - { - [Test] - public void should_not_fail_on_empty_db() - { - var db = WithMigrationTestDb(); - - db.Query("SELECT * FROM ScheduledTasks").Should().BeEmpty(); - db.Query("SELECT * FROM Series").Should().BeEmpty(); - } - - - [Test] - public void should_reset_job_last_execution_time() - { - var db = WithMigrationTestDb(c => - { - c.Insert.IntoTable("ScheduledTasks").Row(new - { - TypeName = "NzbDrone.Core.Tv.Commands.RefreshSeriesCommand", - Interval = 10, - LastExecution = "2000-01-01 00:00:00" - }); - - c.Insert.IntoTable("ScheduledTasks").Row(new - { - TypeName = "NzbDrone.Core.Backup.BackupCommand", - Interval = 10, - LastExecution = "2000-01-01 00:00:00" - }); - }); - - var jobs = db.Query("SELECT TypeName, LastExecution FROM ScheduledTasks"); - - jobs.Single(c => c.TypeName == "NzbDrone.Core.Tv.Commands.RefreshSeriesCommand") - .LastExecution.Year.Should() - .Be(2014); - - jobs.Single(c => c.TypeName == "NzbDrone.Core.Backup.BackupCommand") - .LastExecution.Year.Should() - .Be(2000); - } - - [Test] - public void should_reset_series_last_sync_time() - { - var db = WithMigrationTestDb(c => - { - c.Insert.IntoTable("Series").Row(new - { - Tvdbid = 1, - TvRageId =1, - Title ="Title1", - CleanTitle ="CleanTitle1", - Status =1, - Images ="", - Path ="c:\\test", - Monitored =1, - SeasonFolder =1, - Runtime= 0, - SeriesType=0, - UseSceneNumbering =0, - LastInfoSync = "2000-01-01 00:00:00" - }); - - c.Insert.IntoTable("Series").Row(new - { - Tvdbid = 2, - TvRageId = 2, - Title = "Title2", - CleanTitle = "CleanTitle2", - Status = 1, - Images = "", - Path = "c:\\test2", - Monitored = 1, - SeasonFolder = 1, - Runtime = 0, - SeriesType = 0, - UseSceneNumbering = 0, - LastInfoSync = "2000-01-01 00:00:00" - }); - }); - - var series = db.Query("SELECT LastInfoSync FROM Series"); - - series.Should().OnlyContain(c => c.LastInfoSync.Value.Year == 2014); - } - } -} \ No newline at end of file diff --git a/src/NzbDrone.Core.Test/Datastore/Migration/079_dedupe_tagsFixture.cs b/src/NzbDrone.Core.Test/Datastore/Migration/079_dedupe_tagsFixture.cs deleted file mode 100644 index e333fb9a1..000000000 --- a/src/NzbDrone.Core.Test/Datastore/Migration/079_dedupe_tagsFixture.cs +++ /dev/null @@ -1,205 +0,0 @@ -using System.Linq; -using FluentAssertions; -using NUnit.Framework; -using NzbDrone.Core.Datastore.Migration; -using NzbDrone.Core.Test.Framework; - -namespace NzbDrone.Core.Test.Datastore.Migration -{ - [TestFixture] - public class dedupe_tagsFixture : MigrationTest - { - [Test] - public void should_not_fail_if_series_tags_are_null() - { - var db = WithMigrationTestDb(c => - { - c.Insert.IntoTable("Series").Row(new - { - Tvdbid = 1, - TvRageId = 1, - Title = "Title1", - CleanTitle = "CleanTitle1", - Status = 1, - Images = "", - Path = "c:\\test", - Monitored = 1, - SeasonFolder = 1, - Runtime = 0, - SeriesType = 0, - UseSceneNumbering = 0, - LastInfoSync = "2000-01-01 00:00:00" - }); - - c.Insert.IntoTable("Tags").Row(new - { - Label = "test" - }); - }); - - var tags = db.Query("SELECT * FROM Tags"); - tags.Should().HaveCount(1); - } - - [Test] - public void should_not_fail_if_series_tags_are_empty() - { - var db = WithMigrationTestDb(c => - { - c.Insert.IntoTable("Series").Row(new - { - Tvdbid = 1, - TvRageId = 1, - Title = "Title1", - CleanTitle = "CleanTitle1", - Status = 1, - Images = "", - Path = "c:\\test", - Monitored = 1, - SeasonFolder = 1, - Runtime = 0, - SeriesType = 0, - UseSceneNumbering = 0, - LastInfoSync = "2000-01-01 00:00:00", - Tags = "[]" - }); - - c.Insert.IntoTable("Tags").Row(new - { - Label = "test" - }); - }); - - var tags = db.Query("SELECT * FROM Tags"); - tags.Should().HaveCount(1); - } - - [Test] - public void should_remove_duplicate_labels_from_tags() - { - var db = WithMigrationTestDb(c => - { - c.Insert.IntoTable("Tags").Row(new - { - Label = "test" - }); - - c.Insert.IntoTable("Tags").Row(new - { - Label = "test" - }); - }); - - var tags = db.Query("SELECT * FROM Tags"); - tags.Should().HaveCount(1); - } - - [Test] - public void should_not_allow_duplicate_tag_to_be_inserted() - { - var db = WithMigrationTestDb(c => - { - c.Insert.IntoTable("Tags").Row(new - { - Label = "test" - }); - }); - - Assert.That(() => db.Query("INSERT INTO Tags (Label) VALUES ('test')"), Throws.Exception); - } - - [Test] - public void should_replace_duplicated_tag_with_proper_tag() - { - var db = WithMigrationTestDb(c => - { - c.Insert.IntoTable("Series").Row(new - { - Tvdbid = 1, - TvRageId = 1, - Title = "Title1", - CleanTitle = "CleanTitle1", - Status = 1, - Images = "", - Path = "c:\\test", - Monitored = 1, - SeasonFolder = 1, - Runtime = 0, - SeriesType = 0, - UseSceneNumbering = 0, - LastInfoSync = "2000-01-01 00:00:00", - Tags = "[2]" - }); - - c.Insert.IntoTable("Tags").Row(new - { - Label = "test" - }); - - c.Insert.IntoTable("Tags").Row(new - { - Label = "test" - }); - }); - - var series = db.Query("SELECT Tags FROM Series WHERE Id = 1").Single(); - series.Tags.First().Should().Be(1); - } - - [Test] - public void should_only_update_affected_series() - { - var db = WithMigrationTestDb(c => - { - c.Insert.IntoTable("Series").Row(new - { - Tvdbid = 1, - TvRageId = 1, - Title = "Title1", - CleanTitle = "CleanTitle1", - Status = 1, - Images = "", - Path = "c:\\test", - Monitored = 1, - SeasonFolder = 1, - Runtime = 0, - SeriesType = 0, - UseSceneNumbering = 0, - LastInfoSync = "2000-01-01 00:00:00", - Tags = "[2]" - }); - - c.Insert.IntoTable("Series").Row(new - { - Tvdbid = 2, - TvRageId = 2, - Title = "Title2", - CleanTitle = "CleanTitle2", - Status = 1, - Images = "", - Path = "c:\\test", - Monitored = 1, - SeasonFolder = 1, - Runtime = 0, - SeriesType = 0, - UseSceneNumbering = 0, - LastInfoSync = "2000-01-01 00:00:00", - Tags = "[]" - }); - - c.Insert.IntoTable("Tags").Row(new - { - Label = "test" - }); - - c.Insert.IntoTable("Tags").Row(new - { - Label = "test" - }); - }); - - var series = db.Query("SELECT Tags FROM Series WHERE Id = 2").Single(); - series.Tags.Should().BeEmpty(); - } - } -} diff --git a/src/NzbDrone.Core.Test/Datastore/Migration/081_move_dot_prefix_to_transmission_categoryFixture.cs b/src/NzbDrone.Core.Test/Datastore/Migration/081_move_dot_prefix_to_transmission_categoryFixture.cs deleted file mode 100644 index 7aae7010e..000000000 --- a/src/NzbDrone.Core.Test/Datastore/Migration/081_move_dot_prefix_to_transmission_categoryFixture.cs +++ /dev/null @@ -1,88 +0,0 @@ -using System.Linq; -using FluentAssertions; -using NUnit.Framework; -using NzbDrone.Common.Serializer; -using NzbDrone.Core.Test.Framework; -using NzbDrone.Core.Datastore.Migration; - -namespace NzbDrone.Core.Test.Datastore.Migration -{ - [TestFixture] - public class move_dot_prefix_to_transmission_categoryFixture : MigrationTest - { - [Test] - public void should_not_fail_if_no_transmission() - { - var db = WithMigrationTestDb(c => - { - c.Insert.IntoTable("DownloadClients").Row(new - { - Enable = 1, - Name = "Sab", - Implementation = "Sabnzbd", - Settings = new - { - Host = "127.0.0.1", - TvCategory = "abc" - }.ToJson(), - ConfigContract = "SabnzbdSettings" - }); - }); - - var downloadClients = db.Query("SELECT Settings FROM DownloadClients"); - - downloadClients.Should().HaveCount(1); - downloadClients.First().Settings.ToObject().TvCategory.Should().Be("abc"); - } - - [Test] - public void should_be_updated_for_transmission() - { - var db = WithMigrationTestDb(c => - { - c.Insert.IntoTable("DownloadClients").Row(new - { - Enable = 1, - Name = "Trans", - Implementation = "Transmission", - Settings = new - { - Host = "127.0.0.1", - TvCategory = "abc" - }.ToJson(), - ConfigContract = "TransmissionSettings" - }); - }); - - var downloadClients = db.Query("SELECT Settings FROM DownloadClients"); - - downloadClients.Should().HaveCount(1); - downloadClients.First().Settings.ToObject().TvCategory.Should().Be(".abc"); - } - - [Test] - public void should_leave_empty_category_untouched() - { - var db = WithMigrationTestDb(c => - { - c.Insert.IntoTable("DownloadClients").Row(new - { - Enable = 1, - Name = "Trans", - Implementation = "Transmission", - Settings = new - { - Host = "127.0.0.1", - TvCategory = "" - }.ToJson(), - ConfigContract = "TransmissionSettings" - }); - }); - - var downloadClients = db.Query("SELECT Settings FROM DownloadClients"); - - downloadClients.Should().HaveCount(1); - downloadClients.First().Settings.ToObject().TvCategory.Should().Be(""); - } - } -} diff --git a/src/NzbDrone.Core.Test/Datastore/Migration/084_update_quality_minmax_sizeFixture.cs b/src/NzbDrone.Core.Test/Datastore/Migration/084_update_quality_minmax_sizeFixture.cs deleted file mode 100644 index 8b4b237e6..000000000 --- a/src/NzbDrone.Core.Test/Datastore/Migration/084_update_quality_minmax_sizeFixture.cs +++ /dev/null @@ -1,96 +0,0 @@ -using System.Linq; -using FluentAssertions; -using NUnit.Framework; -using NzbDrone.Core.Datastore.Migration; -using NzbDrone.Core.Test.Framework; - -namespace NzbDrone.Core.Test.Datastore.Migration -{ - [TestFixture] - public class update_quality_minmax_sizeFixture : MigrationTest - { - [Test] - public void should_not_fail_if_empty() - { - var db = WithMigrationTestDb(); - - var qualityDefinitions = db.Query("SELECT * FROM QualityDefinitions"); - - qualityDefinitions.Should().BeEmpty(); - } - - [Test] - public void should_set_rawhd_to_null() - { - var db = WithMigrationTestDb(c => - { - c.Insert.IntoTable("QualityDefinitions").Row(new - { - Quality = 1, - Title = "SDTV", - MinSize = 0, - MaxSize = 100 - }) - .Row(new - { - Quality = 10, - Title = "RawHD", - MinSize = 0, - MaxSize = 100 - }); - }); - - var qualityDefinitions = db.Query("SELECT * FROM QualityDefinitions"); - - qualityDefinitions.Should().HaveCount(2); - qualityDefinitions.First(v => v.Quality == 10).MaxSize.Should().NotHaveValue(); - } - - [Test] - public void should_set_zero_maxsize_to_null() - { - var db = WithMigrationTestDb(c => - { - c.Insert.IntoTable("QualityDefinitions").Row(new - { - Quality = 1, - Title = "SDTV", - MinSize = 0, - MaxSize = 0 - }); - }); - - var qualityDefinitions = db.Query("SELECT * FROM QualityDefinitions"); - - qualityDefinitions.Should().HaveCount(1); - qualityDefinitions.First(v => v.Quality == 1).MaxSize.Should().NotHaveValue(); - } - - [Test] - public void should_preserve_values() - { - var db = WithMigrationTestDb(c => - { - c.Insert.IntoTable("QualityDefinitions").Row(new - { - Quality = 1, - Title = "SDTV", - MinSize = 0, - MaxSize = 100 - }) - .Row(new - { - Quality = 10, - Title = "RawHD", - MinSize = 0, - MaxSize = 100 - }); - }); - - var qualityDefinitions = db.Query("SELECT * FROM QualityDefinitions"); - - qualityDefinitions.Should().HaveCount(2); - qualityDefinitions.First(v => v.Quality == 1).MaxSize.Should().Be(100); - } - } -} diff --git a/src/NzbDrone.Core.Test/Datastore/Migration/085_expand_transmission_urlbaseFixture.cs b/src/NzbDrone.Core.Test/Datastore/Migration/085_expand_transmission_urlbaseFixture.cs deleted file mode 100644 index 0b1f7460d..000000000 --- a/src/NzbDrone.Core.Test/Datastore/Migration/085_expand_transmission_urlbaseFixture.cs +++ /dev/null @@ -1,90 +0,0 @@ -using System.Linq; -using FluentAssertions; -using NUnit.Framework; -using NzbDrone.Common.Serializer; -using NzbDrone.Core.Datastore.Migration; -using NzbDrone.Core.Test.Framework; - -namespace NzbDrone.Core.Test.Datastore.Migration -{ - [TestFixture] - public class expand_transmission_urlbaseFixture : MigrationTest - { - [Test] - public void should_not_fail_if_no_transmission() - { - var db = WithMigrationTestDb(c => - { - c.Insert.IntoTable("DownloadClients").Row(new - { - Enable = 1, - Name = "Deluge", - Implementation = "Deluge", - Settings = new DelugeSettings85 - { - Host = "127.0.0.1", - TvCategory = "abc", - UrlBase = "/my/" - }.ToJson(), - ConfigContract = "DelugeSettings" - }); - }); - - var items = db.Query("SELECT * FROM DownloadClients"); - - items.Should().HaveCount(1); - items.First().Settings.ToObject().UrlBase.Should().Be("/my/"); - } - - [Test] - public void should_be_updated_for_transmission() - { - var db = WithMigrationTestDb(c => - { - c.Insert.IntoTable("DownloadClients").Row(new - { - Enable = 1, - Name = "Trans", - Implementation = "Transmission", - Settings = new TransmissionSettings81 - { - Host = "127.0.0.1", - TvCategory = "abc" - }.ToJson(), - ConfigContract = "TransmissionSettings" - }); - }); - - var items = db.Query("SELECT * FROM DownloadClients"); - - items.Should().HaveCount(1); - items.First().Settings.ToObject().UrlBase.Should().Be("/transmission/"); - } - - [Test] - public void should_be_append_to_existing_urlbase() - { - var db = WithMigrationTestDb(c => - { - c.Insert.IntoTable("DownloadClients").Row(new - { - Enable = 1, - Name = "Trans", - Implementation = "Transmission", - Settings = new TransmissionSettings81 - { - Host = "127.0.0.1", - TvCategory = "abc", - UrlBase = "/my/url/" - }.ToJson(), - ConfigContract = "TransmissionSettings" - }); - }); - - var items = db.Query("SELECT * FROM DownloadClients"); - - items.Should().HaveCount(1); - items.First().Settings.ToObject().UrlBase.Should().Be("/my/url/transmission/"); - } - } -} diff --git a/src/NzbDrone.Core.Test/Datastore/Migration/086_pushbullet_device_idsFixture.cs b/src/NzbDrone.Core.Test/Datastore/Migration/086_pushbullet_device_idsFixture.cs deleted file mode 100644 index 20a8e063a..000000000 --- a/src/NzbDrone.Core.Test/Datastore/Migration/086_pushbullet_device_idsFixture.cs +++ /dev/null @@ -1,89 +0,0 @@ -using System.Linq; -using FluentAssertions; -using NUnit.Framework; -using NzbDrone.Common.Serializer; -using NzbDrone.Core.Datastore.Migration; -using NzbDrone.Core.Test.Framework; - -namespace NzbDrone.Core.Test.Datastore.Migration -{ - [TestFixture] - public class pushbullet_device_idsFixture : MigrationTest - { - [Test] - public void should_not_fail_if_no_pushbullet() - { - var db = WithMigrationTestDb(c => - { - c.Insert.IntoTable("Notifications").Row(new - { - OnGrab = false, - OnDownload = false, - OnUpgrade = false, - Name = "Pushover", - Implementation = "Pushover", - Settings = "{}", - ConfigContract = "PushoverSettings" - }); - }); - - var items = db.Query("SELECT * FROM Notifications"); - - items.Should().HaveCount(1); - } - - [Test] - public void should_not_fail_if_deviceId_is_not_set() - { - var db = WithMigrationTestDb(c => - { - c.Insert.IntoTable("Notifications").Row(new - { - OnGrab = false, - OnDownload = false, - OnUpgrade = false, - Name = "PushBullet", - Implementation = "PushBullet", - Settings = new - { - ApiKey = "my_api_key" - }.ToJson(), - ConfigContract = "PushBulletSettings" - }); - }); - - var items = db.Query("SELECT * FROM Notifications"); - - items.Should().HaveCount(1); - } - - [Test] - public void should_add_deviceIds_setting_matching_deviceId() - { - var deviceId = "device_id"; - - var db = WithMigrationTestDb(c => - { - c.Insert.IntoTable("Notifications").Row(new - { - OnGrab = false, - OnDownload = false, - OnUpgrade = false, - Name = "PushBullet", - Implementation = "PushBullet", - Settings = new - { - ApiKey = "my_api_key", - DeviceId = deviceId - }.ToJson(), - ConfigContract = "PushBulletSettings" - }); - }); - - var items = db.Query("SELECT * FROM Notifications"); - - items.Should().HaveCount(1); - items.First().Settings.ToObject().DeviceIds.First().Should().Be(deviceId); - } - } -} diff --git a/src/NzbDrone.Core.Test/Datastore/Migration/088_pushbullet_devices_channels_listFixture.cs b/src/NzbDrone.Core.Test/Datastore/Migration/088_pushbullet_devices_channels_listFixture.cs deleted file mode 100644 index 37679998c..000000000 --- a/src/NzbDrone.Core.Test/Datastore/Migration/088_pushbullet_devices_channels_listFixture.cs +++ /dev/null @@ -1,40 +0,0 @@ -using System.Linq; -using FluentAssertions; -using NUnit.Framework; -using NzbDrone.Common.Serializer; -using NzbDrone.Core.Datastore.Migration; -using NzbDrone.Core.Test.Framework; - -namespace NzbDrone.Core.Test.Datastore.Migration -{ - [TestFixture] - public class pushbullet_devices_channels_listFixture : MigrationTest - { - [Test] - public void should_convert_comma_separted_string_to_list() - { - var db = WithMigrationTestDb(c => - { - c.Insert.IntoTable("Notifications").Row(new - { - OnGrab = false, - OnDownload = false, - OnUpgrade = false, - Name = "PushBullet", - Implementation = "PushBullet", - Settings = new - { - ApiKey = "my_api_key", - ChannelTags = "channel1,channel2" - }.ToJson(), - ConfigContract = "PushBulletSettings" - }); - }); - - var items = db.Query("SELECT * FROM Notifications"); - - items.Should().HaveCount(1); - items.First().Settings.ToObject().ChannelTags.Should().HaveCount(2); - } - } -} diff --git a/src/NzbDrone.Core.Test/Datastore/Migration/090_update_kickass_urlFixture.cs b/src/NzbDrone.Core.Test/Datastore/Migration/090_update_kickass_urlFixture.cs deleted file mode 100644 index 292344127..000000000 --- a/src/NzbDrone.Core.Test/Datastore/Migration/090_update_kickass_urlFixture.cs +++ /dev/null @@ -1,64 +0,0 @@ -using System.Linq; -using FluentAssertions; -using NUnit.Framework; -using NzbDrone.Common.Serializer; -using NzbDrone.Core.Datastore.Migration; -using NzbDrone.Core.Test.Framework; - -namespace NzbDrone.Core.Test.Datastore.Migration -{ - [TestFixture] - public class update_kickass_url_migration_fixture : MigrationTest - { - [TestCase("http://kickass.so")] - [TestCase("https://kickass.so")] - [TestCase("http://kickass.to")] - [TestCase("https://kickass.to")] - [TestCase("http://kat.cr")] - // [TestCase("HTTP://KICKASS.SO")] Not sure if there is an easy way to do this, not sure if worth it. - public void should_replace_old_url(string oldUrl) - { - var db = WithMigrationTestDb(c => - { - c.Insert.IntoTable("Indexers").Row(new - { - Name = "Kickass_wrong_url", - Implementation = "KickassTorrents", - Settings = new KickassTorrentsSettings90 - { - BaseUrl = oldUrl - }.ToJson(), - ConfigContract = "KickassTorrentsSettings" - }); - }); - - var items = db.Query("SELECT * FROM Indexers"); - - items.Should().HaveCount(1); - items.First().Settings.ToObject().BaseUrl.Should().Be("https://kat.cr"); - } - - [Test] - public void should_not_replace_other_indexers() - { - var db = WithMigrationTestDb(c => - { - c.Insert.IntoTable("Indexers").Row(new - { - Name = "not_kickass", - Implementation = "NotKickassTorrents", - Settings = new KickassTorrentsSettings90 - { - BaseUrl = "kickass.so", - }.ToJson(), - ConfigContract = "KickassTorrentsSettings" - }); - }); - - var items = db.Query("SELECT * FROM Indexers"); - - items.Should().HaveCount(1); - items.First().Settings.ToObject().BaseUrl.Should().Be("kickass.so"); - } - } -} diff --git a/src/NzbDrone.Core.Test/Datastore/Migration/099_extra_and_subtitle_filesFixture.cs b/src/NzbDrone.Core.Test/Datastore/Migration/099_extra_and_subtitle_filesFixture.cs deleted file mode 100644 index f72d950f0..000000000 --- a/src/NzbDrone.Core.Test/Datastore/Migration/099_extra_and_subtitle_filesFixture.cs +++ /dev/null @@ -1,54 +0,0 @@ -using System.Linq; -using FluentAssertions; -using NUnit.Framework; -using NzbDrone.Core.Datastore.Migration; -using NzbDrone.Core.Test.Framework; - -namespace NzbDrone.Core.Test.Datastore.Migration -{ - [TestFixture] - public class metadata_files_extensionFixture : MigrationTest - { - [Test] - public void should_set_extension_using_relative_path() - { - var db = WithMigrationTestDb(c => - { - c.Insert.IntoTable("MetadataFiles").Row(new - { - SeriesId = 1, - RelativePath = "banner.jpg", - LastUpdated = "2016-05-30 20:23:02.3725923", - Type = 3, - Consumer = "XbmcMetadata" - }); - - c.Insert.IntoTable("MetadataFiles").Row(new - { - SeriesId = 1, - SeasonNumber = 1, - EpisodeFileId = 1, - RelativePath = "Series.Title.S01E01.jpg", - LastUpdated = "2016-05-30 20:23:02.3725923", - Type = 5, - Consumer = "XbmcMetadata" - }); - - c.Insert.IntoTable("MetadataFiles").Row(new - { - SeriesId = 1, - RelativePath = "Series Title", - LastUpdated = "2016-05-30 20:23:02.3725923", - Type = 3, - Consumer = "RoksboxMetadata" - }); - }); - - var items = db.Query("SELECT * FROM MetadataFiles"); - - items.Should().HaveCount(2); - items.First().Extension.Should().Be(".jpg"); - items.Last().Extension.Should().Be(".jpg"); - } - } -} diff --git a/src/NzbDrone.Core.Test/Datastore/Migration/101_add_ultrahd_quality_in_profilesFixture.cs b/src/NzbDrone.Core.Test/Datastore/Migration/101_add_ultrahd_quality_in_profilesFixture.cs deleted file mode 100644 index 8e5562824..000000000 --- a/src/NzbDrone.Core.Test/Datastore/Migration/101_add_ultrahd_quality_in_profilesFixture.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System.Linq; -using FluentAssertions; -using NUnit.Framework; -using NzbDrone.Core.Datastore.Migration; -using NzbDrone.Core.Test.Framework; - -namespace NzbDrone.Core.Test.Datastore.Migration -{ - [TestFixture] - public class add_ultrahd_quality_in_profilesFixture : MigrationTest - { - [Test] - public void should_add_ultrahd_to_old_profile() - { - var db = WithMigrationTestDb(c => - { - c.Insert.IntoTable("Profiles").Row(new - { - Id = 0, - Name = "SDTV", - Cutoff = 1, - Items = "[ { \"quality\": 1, \"allowed\": true } ]", - Language = 1 - }); - }); - - var profiles = db.Query("SELECT Items FROM Profiles LIMIT 1"); - - var items = profiles.First().Items; - items.Should().HaveCount(4); - items.Select(v => v.Quality).Should().BeEquivalentTo(1, 16, 18, 19); - items.Select(v => v.Allowed).Should().BeEquivalentTo(true, false, false, false); - } - } -} diff --git a/src/NzbDrone.Core.Test/Datastore/Migration/103_fix_metadata_file_extensionsFixture.cs b/src/NzbDrone.Core.Test/Datastore/Migration/103_fix_metadata_file_extensionsFixture.cs deleted file mode 100644 index 86905df18..000000000 --- a/src/NzbDrone.Core.Test/Datastore/Migration/103_fix_metadata_file_extensionsFixture.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System.Linq; -using FluentAssertions; -using NUnit.Framework; -using NzbDrone.Core.Datastore.Migration; -using NzbDrone.Core.Test.Framework; - -namespace NzbDrone.Core.Test.Datastore.Migration -{ - [TestFixture] - public class fix_metadata_file_extensionsFixture : MigrationTest - { - [Test] - public void should_fix_extension_when_relative_path_contained_multiple_periods() - { - var db = WithMigrationTestDb(c => - { - c.Insert.IntoTable("MetadataFiles").Row(new - { - SeriesId = 1, - SeasonNumber = 1, - EpisodeFileId = 1, - RelativePath = "Series.Title.S01E01.jpg", - LastUpdated = "2016-05-30 20:23:02.3725923", - Type = 5, - Consumer = "XbmcMetadata", - Extension = ".S01E01.jpg" - }); - }); - - var items = db.Query("SELECT * FROM MetadataFiles"); - - items.Should().HaveCount(1); - items.First().Extension.Should().Be(".jpg"); - } - } -} diff --git a/src/NzbDrone.Core.Test/Datastore/Migration/106_update_btn_urlFixture.cs b/src/NzbDrone.Core.Test/Datastore/Migration/106_update_btn_urlFixture.cs deleted file mode 100644 index 3b719d42e..000000000 --- a/src/NzbDrone.Core.Test/Datastore/Migration/106_update_btn_urlFixture.cs +++ /dev/null @@ -1,62 +0,0 @@ -using System.Linq; -using FluentAssertions; -using NUnit.Framework; -using NzbDrone.Common.Serializer; -using NzbDrone.Core.Datastore.Migration; -using NzbDrone.Core.Test.Framework; - -namespace NzbDrone.Core.Test.Datastore.Migration -{ - [TestFixture] - public class update_btn_url_migration_fixture : MigrationTest - { - [TestCase("http://api.btnapps.net")] - [TestCase("https://api.btnapps.net")] - [TestCase("http://api.btnapps.net/")] - [TestCase("https://api.btnapps.net/")] - public void should_replace_old_url(string oldUrl) - { - var db = WithMigrationTestDb(c => - { - c.Insert.IntoTable("Indexers").Row(new - { - Name = "btn_old_url", - Implementation = "BroadcastheNet", - Settings = new BroadcastheNetSettings106 - { - BaseUrl = oldUrl - }.ToJson(), - ConfigContract = "BroadcastheNetSettings" - }); - }); - - var items = db.Query("SELECT * FROM Indexers"); - - items.Should().HaveCount(1); - items.First().Settings.ToObject().BaseUrl.Should().Contain("api.broadcasthe.net"); - } - - [Test] - public void should_not_replace_other_indexers() - { - var db = WithMigrationTestDb(c => - { - c.Insert.IntoTable("Indexers").Row(new - { - Name = "not_btn", - Implementation = "NotBroadcastheNet", - Settings = new BroadcastheNetSettings106 - { - BaseUrl = "http://api.btnapps.net", - }.ToJson(), - ConfigContract = "BroadcastheNetSettings" - }); - }); - - var items = db.Query("SELECT * FROM Indexers"); - - items.Should().HaveCount(1); - items.First().Settings.ToObject().BaseUrl.Should().Be("http://api.btnapps.net"); - } - } -} diff --git a/src/NzbDrone.Core.Test/Datastore/Migration/108_fix_metadata_file_extensionsFixture.cs b/src/NzbDrone.Core.Test/Datastore/Migration/108_fix_metadata_file_extensionsFixture.cs deleted file mode 100644 index 5a8a1fe02..000000000 --- a/src/NzbDrone.Core.Test/Datastore/Migration/108_fix_metadata_file_extensionsFixture.cs +++ /dev/null @@ -1,83 +0,0 @@ -using System.Linq; -using FluentAssertions; -using NUnit.Framework; -using NzbDrone.Core.Datastore.Migration; -using NzbDrone.Core.Parser; -using NzbDrone.Core.Test.Framework; - -namespace NzbDrone.Core.Test.Datastore.Migration -{ - [TestFixture] - public class fix_extra_file_extensionsFixture : MigrationTest - { - [Test] - public void should_extra_files_that_do_not_have_an_extension() - { - var db = WithMigrationTestDb(c => - { - c.Insert.IntoTable("ExtraFiles").Row(new - { - SeriesId = 1, - SeasonNumber = 1, - EpisodeFileId = 1, - RelativePath = "Series.Title.S01E01", - Added = "2016-05-30 20:23:02.3725923", - LastUpdated = "2016-05-30 20:23:02.3725923", - Extension = "" - }); - }); - - var items = db.Query("Select * from ExtraFiles"); - - items.Should().BeEmpty(); - } - - [Test] - public void should_fix_double_extension() - { - var db = WithMigrationTestDb(c => - { - c.Insert.IntoTable("SubtitleFiles").Row(new - { - SeriesId = 1, - SeasonNumber = 1, - EpisodeFileId = 1, - RelativePath = "Series.Title.S01E01.en.srt", - Added = "2016-05-30 20:23:02.3725923", - LastUpdated = "2016-05-30 20:23:02.3725923", - Language = Language.English, - Extension = "en.srt" - }); - }); - - var items = db.Query("Select * from SubtitleFiles"); - - items.Should().HaveCount(1); - items.First()["Extension"].Should().Be(".srt"); - } - - [Test] - public void should_fix_extension_missing_a_leading_period() - { - var db = WithMigrationTestDb(c => - { - c.Insert.IntoTable("ExtraFiles").Row(new - { - SeriesId = 1, - SeasonNumber = 1, - EpisodeFileId = 1, - RelativePath = "Series.Title.S01E01.nfo-orig", - Added = "2016-05-30 20:23:02.3725923", - LastUpdated = "2016-05-30 20:23:02.3725923", - Extension = "nfo-orig" - }); - }); - - var items = db.Query("Select * from ExtraFiles"); - - items.Should().HaveCount(1); - items.First()["Extension"].Should().Be(".nfo-orig"); - } - } - -} diff --git a/src/NzbDrone.Core.Test/Datastore/Migration/109_import_extra_files_configFixture.cs b/src/NzbDrone.Core.Test/Datastore/Migration/109_import_extra_files_configFixture.cs deleted file mode 100644 index 4ef9e4979..000000000 --- a/src/NzbDrone.Core.Test/Datastore/Migration/109_import_extra_files_configFixture.cs +++ /dev/null @@ -1,54 +0,0 @@ -using System.Linq; -using FluentAssertions; -using NUnit.Framework; -using NzbDrone.Core.Datastore.Migration; -using NzbDrone.Core.Parser; -using NzbDrone.Core.Test.Framework; - -namespace NzbDrone.Core.Test.Datastore.Migration -{ - [TestFixture] - public class import_extra_files_configFixture : MigrationTest - { - [Test] - public void should_not_insert_if_missing() - { - var db = WithMigrationTestDb(); - - var items = db.QueryScalar("SELECT Value FROM Config WHERE Key = 'importextrafiles'"); - items.Should().BeNull(); - } - - [Test] - public void should_not_insert_if_empty() - { - var db = WithMigrationTestDb(c => - { - c.Insert.IntoTable("Config").Row(new - { - Key = "extrafileextensions", - Value = "" - }); - }); - - var items = db.QueryScalar("SELECT Value FROM Config WHERE Key = 'importextrafiles'"); - items.Should().BeNull(); - } - - [Test] - public void should_insert_True_if_configured() - { - var db = WithMigrationTestDb(c => - { - c.Insert.IntoTable("Config").Row(new - { - Key = "extrafileextensions", - Value = "srt" - }); - }); - - var items = db.QueryScalar("SELECT Value FROM Config WHERE Key = 'importextrafiles'"); - items.Should().Be("True"); - } - } -} diff --git a/src/NzbDrone.Core.Test/Datastore/Migration/110_fix_extra_files_configFixture.cs b/src/NzbDrone.Core.Test/Datastore/Migration/110_fix_extra_files_configFixture.cs deleted file mode 100644 index 1a38efc1c..000000000 --- a/src/NzbDrone.Core.Test/Datastore/Migration/110_fix_extra_files_configFixture.cs +++ /dev/null @@ -1,133 +0,0 @@ -using System.Linq; -using FluentAssertions; -using NUnit.Framework; -using NzbDrone.Core.Datastore.Migration; -using NzbDrone.Core.Parser; -using NzbDrone.Core.Test.Framework; - -namespace NzbDrone.Core.Test.Datastore.Migration -{ - [TestFixture] - public class fix_extra_files_configFixture : MigrationTest - { - [Test] - public void should_not_update_importextrafiles_disabled() - { - var db = WithMigrationTestDb(); - - var itemEnabled = db.QueryScalar("SELECT Value FROM Config WHERE Key = 'importextrafiles'"); - itemEnabled.Should().BeNull(); - } - - [Test] - public void should_fix_importextrafiles_if_wrong() - { - var db = WithMigrationTestDb(c => - { - c.Insert.IntoTable("Config").Row(new - { - Key = "importextrafiles", - Value = 1 - }); - }); - - - var itemEnabled = db.QueryScalar("SELECT Value FROM Config WHERE Key = 'importextrafiles'"); - itemEnabled.Should().Be("True"); - } - - [Test] - public void should_fill_in_default_extensions() - { - var db = WithMigrationTestDb(c => - { - c.Insert.IntoTable("Config").Row(new - { - Key = "importextrafiles", - Value = "False" - }); - - c.Insert.IntoTable("Config").Row(new - { - Key = "extrafileextensions", - Value = "" - }); - }); - - var itemEnabled = db.QueryScalar("SELECT Value FROM Config WHERE Key = 'importextrafiles'"); - itemEnabled.Should().Be("False"); - - var itemExtensions = db.QueryScalar("SELECT Value FROM Config WHERE Key = 'extrafileextensions'"); - itemExtensions.Should().Be("srt"); - } - - [Test] - public void should_not_fill_in_default_extensions() - { - var db = WithMigrationTestDb(c => - { - c.Insert.IntoTable("Config").Row(new - { - Key = "importextrafiles", - Value = "True" - }); - - c.Insert.IntoTable("Config").Row(new - { - Key = "extrafileextensions", - Value = "" - }); - }); - - var itemEnabled = db.QueryScalar("SELECT Value FROM Config WHERE Key = 'importextrafiles'"); - itemEnabled.Should().Be("True"); - - var itemExtensions = db.QueryScalar("SELECT Value FROM Config WHERE Key = 'extrafileextensions'"); - itemExtensions.Should().Be(""); - } - - [Test] - public void should_not_fill_in_default_extensions_if_not_defined() - { - var db = WithMigrationTestDb(c => - { - c.Insert.IntoTable("Config").Row(new - { - Key = "importextrafiles", - Value = "False" - }); - }); - - var itemEnabled = db.QueryScalar("SELECT Value FROM Config WHERE Key = 'importextrafiles'"); - itemEnabled.Should().Be("False"); - - var itemExtensions = db.QueryScalar("SELECT Value FROM Config WHERE Key = 'extrafileextensions'"); - itemExtensions.Should().BeNull(); - } - - [Test] - public void should_not_fill_in_default_extensions_if_already_defined() - { - var db = WithMigrationTestDb(c => - { - c.Insert.IntoTable("Config").Row(new - { - Key = "importextrafiles", - Value = "False" - }); - - c.Insert.IntoTable("Config").Row(new - { - Key = "extrafileextensions", - Value = "sub" - }); - }); - - var itemEnabled = db.QueryScalar("SELECT Value FROM Config WHERE Key = 'importextrafiles'"); - itemEnabled.Should().Be("False"); - - var itemExtensions = db.QueryScalar("SELECT Value FROM Config WHERE Key = 'extrafileextensions'"); - itemExtensions.Should().Be("sub"); - } - } -} diff --git a/src/NzbDrone.Core.Test/Datastore/PagingSpecExtensionsTests/PagingOffsetFixture.cs b/src/NzbDrone.Core.Test/Datastore/PagingSpecExtensionsTests/PagingOffsetFixture.cs index 5ece0f8a4..7a39cc708 100644 --- a/src/NzbDrone.Core.Test/Datastore/PagingSpecExtensionsTests/PagingOffsetFixture.cs +++ b/src/NzbDrone.Core.Test/Datastore/PagingSpecExtensionsTests/PagingOffsetFixture.cs @@ -1,8 +1,8 @@ -using FluentAssertions; +using FluentAssertions; using NUnit.Framework; using NzbDrone.Core.Datastore; using NzbDrone.Core.Datastore.Extensions; -using NzbDrone.Core.Tv; +using NzbDrone.Core.Music; namespace NzbDrone.Core.Test.Datastore.PagingSpecExtensionsTests { @@ -14,12 +14,12 @@ namespace NzbDrone.Core.Test.Datastore.PagingSpecExtensionsTests [TestCase(1, 100, 0)] public void should_calcuate_expected_offset(int page, int pageSize, int expected) { - var pagingSpec = new PagingSpec + var pagingSpec = new PagingSpec { Page = page, PageSize = pageSize, SortDirection = SortDirection.Ascending, - SortKey = "AirDate" + SortKey = "ReleaseDate" }; pagingSpec.PagingOffset().Should().Be(expected); diff --git a/src/NzbDrone.Core.Test/Datastore/PagingSpecExtensionsTests/ToSortDirectionFixture.cs b/src/NzbDrone.Core.Test/Datastore/PagingSpecExtensionsTests/ToSortDirectionFixture.cs index d2cecde84..cd70cb4e3 100644 --- a/src/NzbDrone.Core.Test/Datastore/PagingSpecExtensionsTests/ToSortDirectionFixture.cs +++ b/src/NzbDrone.Core.Test/Datastore/PagingSpecExtensionsTests/ToSortDirectionFixture.cs @@ -1,8 +1,8 @@ -using FluentAssertions; +using FluentAssertions; using NUnit.Framework; using NzbDrone.Core.Datastore; using NzbDrone.Core.Datastore.Extensions; -using NzbDrone.Core.Tv; +using NzbDrone.Core.Music; namespace NzbDrone.Core.Test.Datastore.PagingSpecExtensionsTests { @@ -11,12 +11,12 @@ namespace NzbDrone.Core.Test.Datastore.PagingSpecExtensionsTests [Test] public void should_convert_default_to_asc() { - var pagingSpec = new PagingSpec + var pagingSpec = new PagingSpec { Page = 1, PageSize = 10, SortDirection = SortDirection.Default, - SortKey = "AirDate" + SortKey = "ReleaseDate" }; pagingSpec.ToSortDirection().Should().Be(Marr.Data.QGen.SortDirection.Asc); @@ -25,13 +25,13 @@ namespace NzbDrone.Core.Test.Datastore.PagingSpecExtensionsTests [Test] public void should_convert_ascending_to_asc() { - var pagingSpec = new PagingSpec + var pagingSpec = new PagingSpec { Page = 1, PageSize = 10, SortDirection = SortDirection.Ascending, - SortKey = "AirDate" - }; + SortKey = "ReleaseDate" + }; pagingSpec.ToSortDirection().Should().Be(Marr.Data.QGen.SortDirection.Asc); } @@ -39,12 +39,12 @@ namespace NzbDrone.Core.Test.Datastore.PagingSpecExtensionsTests [Test] public void should_convert_descending_to_desc() { - var pagingSpec = new PagingSpec + var pagingSpec = new PagingSpec { Page = 1, PageSize = 10, SortDirection = SortDirection.Descending, - SortKey = "AirDate" + SortKey = "ReleaseDate" }; pagingSpec.ToSortDirection().Should().Be(Marr.Data.QGen.SortDirection.Desc); diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/BlockedIndexerSpecificationFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/BlockedIndexerSpecificationFixture.cs new file mode 100644 index 000000000..e5f872c0d --- /dev/null +++ b/src/NzbDrone.Core.Test/DecisionEngineTests/BlockedIndexerSpecificationFixture.cs @@ -0,0 +1,54 @@ +using System; +using System.Collections.Generic; +using FluentAssertions; +using NUnit.Framework; +using NzbDrone.Core.DecisionEngine; +using NzbDrone.Core.DecisionEngine.Specifications; +using NzbDrone.Core.Indexers; +using NzbDrone.Core.Parser.Model; +using NzbDrone.Core.Test.Framework; + +namespace NzbDrone.Core.Test.DecisionEngineTests +{ + [TestFixture] + + public class BlockedIndexerSpecificationFixture : CoreTest + { + private RemoteAlbum _remoteAlbum; + + [SetUp] + public void Setup() + { + _remoteAlbum = new RemoteAlbum + { + Release = new ReleaseInfo { IndexerId = 1 } + }; + + Mocker.GetMock() + .Setup(v => v.GetBlockedProviders()) + .Returns(new List()); + } + + private void WithBlockedIndexer() + { + Mocker.GetMock() + .Setup(v => v.GetBlockedProviders()) + .Returns(new List { new IndexerStatus { ProviderId = 1, DisabledTill = DateTime.UtcNow } }); + } + + [Test] + public void should_return_true_if_no_blocked_indexer() + { + Subject.IsSatisfiedBy(_remoteAlbum, null).Accepted.Should().BeTrue(); + } + + [Test] + public void should_return_false_if_blocked_indexer() + { + WithBlockedIndexer(); + + Subject.IsSatisfiedBy(_remoteAlbum, null).Accepted.Should().BeFalse(); + Subject.Type.Should().Be(RejectionType.Temporary); + } + } +} diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/CutoffSpecificationFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/CutoffSpecificationFixture.cs index ab91dba4a..81adab1aa 100644 --- a/src/NzbDrone.Core.Test/DecisionEngineTests/CutoffSpecificationFixture.cs +++ b/src/NzbDrone.Core.Test/DecisionEngineTests/CutoffSpecificationFixture.cs @@ -1,50 +1,226 @@ -using FluentAssertions; +using FluentAssertions; using NUnit.Framework; -using NzbDrone.Core.Profiles; +using NzbDrone.Core.Profiles.Qualities; using NzbDrone.Core.Qualities; using NzbDrone.Core.DecisionEngine; using NzbDrone.Core.Test.Framework; +using NzbDrone.Core.Languages; +using NzbDrone.Core.Profiles.Languages; +using NzbDrone.Core.Test.Languages; namespace NzbDrone.Core.Test.DecisionEngineTests { [TestFixture] - public class CutoffSpecificationFixture : CoreTest + public class CutoffSpecificationFixture : CoreTest { [Test] public void should_return_true_if_current_album_is_less_than_cutoff() { - Subject.CutoffNotMet(new Profile { Cutoff = Quality.MP3_512, Items = Qualities.QualityFixture.GetDefaultQualities() }, - new QualityModel(Quality.MP3_192, new Revision(version: 2))).Should().BeTrue(); + Subject.CutoffNotMet( + new Profile + + { + Cutoff = Quality.MP3_256, + Items = Qualities.QualityFixture.GetDefaultQualities() + }, + new LanguageProfile + { + Languages = LanguageFixture.GetDefaultLanguages(Language.English), + Cutoff = Language.English + }, + new QualityModel(Quality.MP3_192, new Revision(version: 2)), Language.English).Should().BeTrue(); } [Test] public void should_return_false_if_current_album_is_equal_to_cutoff() { - Subject.CutoffNotMet(new Profile { Cutoff = Quality.MP3_256, Items = Qualities.QualityFixture.GetDefaultQualities() }, - new QualityModel(Quality.MP3_256, new Revision(version: 2))).Should().BeFalse(); + Subject.CutoffNotMet( + new Profile + { + Cutoff = Quality.MP3_256, + Items = Qualities.QualityFixture.GetDefaultQualities() + }, + new LanguageProfile + { + Languages = LanguageFixture.GetDefaultLanguages(Language.English), + Cutoff = Language.English + }, + new QualityModel(Quality.MP3_256, new Revision(version: 2)), Language.English).Should().BeFalse(); } [Test] public void should_return_false_if_current_album_is_greater_than_cutoff() { - Subject.CutoffNotMet(new Profile { Cutoff = Quality.MP3_256, Items = Qualities.QualityFixture.GetDefaultQualities() }, - new QualityModel(Quality.MP3_512, new Revision(version: 2))).Should().BeFalse(); + Subject.CutoffNotMet( + new Profile + + { + Cutoff = Quality.MP3_256, + Items = Qualities.QualityFixture.GetDefaultQualities() + }, + new LanguageProfile + { + Languages = LanguageFixture.GetDefaultLanguages(Language.English), + Cutoff = Language.English + }, + new QualityModel(Quality.MP3_320, new Revision(version: 2)), Language.English).Should().BeFalse(); } [Test] public void should_return_true_when_new_album_is_proper_but_existing_is_not() { - Subject.CutoffNotMet(new Profile { Cutoff = Quality.MP3_256, Items = Qualities.QualityFixture.GetDefaultQualities() }, - new QualityModel(Quality.MP3_256, new Revision(version: 1)), - new QualityModel(Quality.MP3_256, new Revision(version: 2))).Should().BeTrue(); + Subject.CutoffNotMet( + new Profile + + { + Cutoff = Quality.MP3_320, + Items = Qualities.QualityFixture.GetDefaultQualities() + }, + new LanguageProfile + { + Languages = LanguageFixture.GetDefaultLanguages(Language.English), + Cutoff = Language.English + }, + new QualityModel(Quality.MP3_320, new Revision(version: 1)),Language.English, + new QualityModel(Quality.MP3_320, new Revision(version: 2))).Should().BeTrue(); + } [Test] public void should_return_false_if_cutoff_is_met_and_quality_is_higher() { - Subject.CutoffNotMet(new Profile { Cutoff = Quality.MP3_256, Items = Qualities.QualityFixture.GetDefaultQualities() }, - new QualityModel(Quality.MP3_256, new Revision(version: 2)), - new QualityModel(Quality.MP3_512, new Revision(version: 2))).Should().BeFalse(); + Subject.CutoffNotMet( + new Profile + + { + Cutoff = Quality.MP3_320, + Items = Qualities.QualityFixture.GetDefaultQualities() + }, + new LanguageProfile + { + Languages = LanguageFixture.GetDefaultLanguages(Language.English), + Cutoff = Language.English + }, + new QualityModel(Quality.MP3_320, new Revision(version: 2)),Language.English, + new QualityModel(Quality.FLAC, new Revision(version: 2))).Should().BeFalse(); + } + + [Test] + public void should_return_true_if_quality_cutoff_is_met_and_quality_is_higher_but_language_is_not_met() + { + + Profile _profile = new Profile + { + Cutoff = Quality.MP3_320, + Items = Qualities.QualityFixture.GetDefaultQualities(), + }; + + LanguageProfile _langProfile = new LanguageProfile + { + Cutoff = Language.Spanish, + Languages = LanguageFixture.GetDefaultLanguages() + }; + + Subject.CutoffNotMet(_profile, + _langProfile, + new QualityModel(Quality.MP3_320, new Revision(version: 2)), + Language.English, + new QualityModel(Quality.FLAC, new Revision(version: 2))).Should().BeTrue(); + } + + [Test] + public void should_return_false_if_cutoff_is_met_and_quality_is_higher_and_language_is_met() + { + + Profile _profile = new Profile + { + Cutoff = Quality.MP3_320, + Items = Qualities.QualityFixture.GetDefaultQualities(), + }; + + LanguageProfile _langProfile = new LanguageProfile + { + Cutoff = Language.Spanish, + Languages = LanguageFixture.GetDefaultLanguages() + }; + + Subject.CutoffNotMet( + _profile, + _langProfile, + new QualityModel(Quality.MP3_320, new Revision(version: 2)), + Language.Spanish, + new QualityModel(Quality.FLAC, new Revision(version: 2))).Should().BeFalse(); + } + + [Test] + public void should_return_false_if_cutoff_is_met_and_quality_is_higher_and_language_is_higher() + { + + Profile _profile = new Profile + { + Cutoff = Quality.MP3_320, + Items = Qualities.QualityFixture.GetDefaultQualities(), + }; + + LanguageProfile _langProfile = new LanguageProfile + { + Cutoff = Language.Spanish, + Languages = LanguageFixture.GetDefaultLanguages() + }; + + Subject.CutoffNotMet( + _profile, + _langProfile, + new QualityModel(Quality.MP3_320, new Revision(version: 2)), + Language.French, + new QualityModel(Quality.FLAC, new Revision(version: 2))).Should().BeFalse(); + } + + [Test] + public void should_return_true_if_cutoff_is_not_met_and_new_quality_is_higher_and_language_is_higher() + { + + Profile _profile = new Profile + { + Cutoff = Quality.MP3_320, + Items = Qualities.QualityFixture.GetDefaultQualities(), + }; + + LanguageProfile _langProfile = new LanguageProfile + { + Cutoff = Language.Spanish, + Languages = LanguageFixture.GetDefaultLanguages() + }; + + Subject.CutoffNotMet( + _profile, + _langProfile, + new QualityModel(Quality.MP3_256, new Revision(version: 2)), + Language.French, + new QualityModel(Quality.FLAC, new Revision(version: 2))).Should().BeTrue(); + } + + [Test] + public void should_return_true_if_cutoff_is_not_met_and_language_is_higher() + { + + Profile _profile = new Profile + { + Cutoff = Quality.MP3_320, + Items = Qualities.QualityFixture.GetDefaultQualities(), + }; + + LanguageProfile _langProfile = new LanguageProfile + { + Cutoff = Language.Spanish, + Languages = LanguageFixture.GetDefaultLanguages() + }; + + Subject.CutoffNotMet( + _profile, + _langProfile, + new QualityModel(Quality.MP3_256, new Revision(version: 2)), + Language.French).Should().BeTrue(); } } } diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/DownloadDecisionMakerFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/DownloadDecisionMakerFixture.cs index b99d13b26..4d09ed606 100644 --- a/src/NzbDrone.Core.Test/DecisionEngineTests/DownloadDecisionMakerFixture.cs +++ b/src/NzbDrone.Core.Test/DecisionEngineTests/DownloadDecisionMakerFixture.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using FluentAssertions; using Moq; @@ -28,6 +28,8 @@ namespace NzbDrone.Core.Test.DecisionEngineTests private Mock _fail2; private Mock _fail3; + private Mock _failDelayed1; + [SetUp] public void Setup() { @@ -39,6 +41,8 @@ namespace NzbDrone.Core.Test.DecisionEngineTests _fail2 = new Mock(); _fail3 = new Mock(); + _failDelayed1 = new Mock(); + _pass1.Setup(c => c.IsSatisfiedBy(It.IsAny(), null)).Returns(Decision.Accept); _pass2.Setup(c => c.IsSatisfiedBy(It.IsAny(), null)).Returns(Decision.Accept); _pass3.Setup(c => c.IsSatisfiedBy(It.IsAny(), null)).Returns(Decision.Accept); @@ -47,6 +51,9 @@ namespace NzbDrone.Core.Test.DecisionEngineTests _fail2.Setup(c => c.IsSatisfiedBy(It.IsAny(), null)).Returns(Decision.Reject("fail2")); _fail3.Setup(c => c.IsSatisfiedBy(It.IsAny(), null)).Returns(Decision.Reject("fail3")); + _failDelayed1.Setup(c => c.IsSatisfiedBy(It.IsAny(), null)).Returns(Decision.Reject("failDelayed1")); + _failDelayed1.SetupGet(c => c.Priority).Returns(SpecificationPriority.Disk); + _reports = new List { new ReleaseInfo { Title = "Coldplay-A Head Full Of Dreams-CD-FLAC-2015-PERFECT" } }; _remoteAlbum = new RemoteAlbum { Artist = new Artist(), @@ -78,6 +85,25 @@ namespace NzbDrone.Core.Test.DecisionEngineTests _pass3.Verify(c => c.IsSatisfiedBy(_remoteAlbum, null), Times.Once()); } + [Test] + public void should_call_delayed_specifications_if_non_delayed_passed() + { + GivenSpecifications(_pass1, _failDelayed1); + + Subject.GetRssDecision(_reports).ToList(); + _failDelayed1.Verify(c => c.IsSatisfiedBy(_remoteAlbum, null), Times.Once()); + } + + [Test] + public void should_not_call_delayed_specifications_if_non_delayed_failed() + { + GivenSpecifications(_fail1, _failDelayed1); + + Subject.GetRssDecision(_reports).ToList(); + + _failDelayed1.Verify(c => c.IsSatisfiedBy(_remoteAlbum, null), Times.Never()); + } + [Test] public void should_return_rejected_if_single_specs_fail() { @@ -287,4 +313,4 @@ namespace NzbDrone.Core.Test.DecisionEngineTests ExceptionVerification.ExpectedErrors(1); } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/HistorySpecificationFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/HistorySpecificationFixture.cs index df79537ae..18f92f69d 100644 --- a/src/NzbDrone.Core.Test/DecisionEngineTests/HistorySpecificationFixture.cs +++ b/src/NzbDrone.Core.Test/DecisionEngineTests/HistorySpecificationFixture.cs @@ -9,11 +9,14 @@ using NzbDrone.Core.DecisionEngine.Specifications.RssSync; using NzbDrone.Core.History; using NzbDrone.Core.IndexerSearch.Definitions; using NzbDrone.Core.Parser.Model; -using NzbDrone.Core.Profiles; using NzbDrone.Core.Qualities; using NzbDrone.Core.Music; using NzbDrone.Core.DecisionEngine; using NzbDrone.Core.Test.Framework; +using NzbDrone.Core.Profiles.Qualities; +using NzbDrone.Core.Profiles.Languages; +using NzbDrone.Core.Languages; +using NzbDrone.Core.Test.Languages; namespace NzbDrone.Core.Test.DecisionEngineTests { @@ -24,8 +27,8 @@ namespace NzbDrone.Core.Test.DecisionEngineTests private RemoteAlbum _parseResultMulti; private RemoteAlbum _parseResultSingle; - private QualityModel _upgradableQuality; - private QualityModel _notupgradableQuality; + private Tuple _upgradableQuality; + private Tuple _notupgradableQuality; private Artist _fakeArtist; private const int FIRST_ALBUM_ID = 1; private const int SECOND_ALBUM_ID = 2; @@ -33,7 +36,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests [SetUp] public void Setup() { - Mocker.Resolve(); + Mocker.Resolve(); _upgradeHistory = Mocker.Resolve(); var singleAlbumList = new List { new Album { Id = FIRST_ALBUM_ID} }; @@ -45,34 +48,35 @@ namespace NzbDrone.Core.Test.DecisionEngineTests _fakeArtist = Builder.CreateNew() .With(c => c.Profile = new Profile { Cutoff = Quality.MP3_512, Items = Qualities.QualityFixture.GetDefaultQualities() }) + .With(l => l.LanguageProfile = new LanguageProfile { Cutoff = Language.Spanish, Languages = LanguageFixture.GetDefaultLanguages() }) .Build(); _parseResultMulti = new RemoteAlbum { Artist = _fakeArtist, - ParsedAlbumInfo = new ParsedAlbumInfo { Quality = new QualityModel(Quality.MP3_192, new Revision(version: 2)) }, + ParsedAlbumInfo = new ParsedAlbumInfo { Quality = new QualityModel(Quality.MP3_192, new Revision(version: 2)), Language = Language.English }, Albums = doubleAlbumList }; _parseResultSingle = new RemoteAlbum { Artist = _fakeArtist, - ParsedAlbumInfo = new ParsedAlbumInfo { Quality = new QualityModel(Quality.MP3_192, new Revision(version: 2)) }, + ParsedAlbumInfo = new ParsedAlbumInfo { Quality = new QualityModel(Quality.MP3_192, new Revision(version: 2)), Language = Language.English }, Albums = singleAlbumList }; - _upgradableQuality = new QualityModel(Quality.MP3_192, new Revision(version: 1)); - _notupgradableQuality = new QualityModel(Quality.MP3_512, new Revision(version: 2)); + _upgradableQuality = new Tuple(new QualityModel(Quality.MP3_192, new Revision(version: 1)), Language.English); + _notupgradableQuality = new Tuple(new QualityModel(Quality.MP3_512, new Revision(version: 2)), Language.English); Mocker.GetMock() .SetupGet(s => s.EnableCompletedDownloadHandling) .Returns(true); } - private void GivenMostRecentForAlbum(int albumId, string downloadId, QualityModel quality, DateTime date, HistoryEventType eventType) + private void GivenMostRecentForAlbum(int albumId, string downloadId, Tuple quality, DateTime date, HistoryEventType eventType) { Mocker.GetMock().Setup(s => s.MostRecentForAlbum(albumId)) - .Returns(new History.History { DownloadId = downloadId, Quality = quality, Date = date, EventType = eventType }); + .Returns(new History.History { DownloadId = downloadId, Quality = quality.Item1, Date = date, EventType = eventType, Language = quality.Item2 }); } private void GivenCdhDisabled() @@ -160,7 +164,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests { _fakeArtist.Profile = new Profile { Cutoff = Quality.MP3_512, Items = Qualities.QualityFixture.GetDefaultQualities() }; _parseResultSingle.ParsedAlbumInfo.Quality = new QualityModel(Quality.MP3_512, new Revision(version: 1)); - _upgradableQuality = new QualityModel(Quality.MP3_512, new Revision(version: 1)); + _upgradableQuality = new Tuple(new QualityModel(Quality.MP3_512, new Revision(version: 1)), Language.English); GivenMostRecentForAlbum(FIRST_ALBUM_ID, string.Empty, _upgradableQuality, DateTime.UtcNow, HistoryEventType.Grabbed); @@ -172,7 +176,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests { _fakeArtist.Profile = new Profile { Cutoff = Quality.MP3_512, Items = Qualities.QualityFixture.GetDefaultQualities() }; _parseResultSingle.ParsedAlbumInfo.Quality = new QualityModel(Quality.MP3_512, new Revision(version: 1)); - _upgradableQuality = new QualityModel(Quality.MP3_512, new Revision(version: 1)); + _upgradableQuality = new Tuple(new QualityModel(Quality.MP3_512, new Revision(version: 1)), Language.Spanish); GivenMostRecentForAlbum(FIRST_ALBUM_ID, string.Empty, _upgradableQuality, DateTime.UtcNow, HistoryEventType.Grabbed); @@ -200,7 +204,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests GivenCdhDisabled(); _fakeArtist.Profile = new Profile { Cutoff = Quality.MP3_512, Items = Qualities.QualityFixture.GetDefaultQualities() }; _parseResultSingle.ParsedAlbumInfo.Quality = new QualityModel(Quality.MP3_512, new Revision(version: 1)); - _upgradableQuality = new QualityModel(Quality.MP3_512, new Revision(version: 1)); + _upgradableQuality = new Tuple(new QualityModel(Quality.MP3_512, new Revision(version: 1)), Language.Spanish); GivenMostRecentForAlbum(FIRST_ALBUM_ID, "test", _upgradableQuality, DateTime.UtcNow.AddDays(-100), HistoryEventType.Grabbed); diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/LanguageSpecificationFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/LanguageSpecificationFixture.cs index 5bb65c62e..d9b21e3f6 100644 --- a/src/NzbDrone.Core.Test/DecisionEngineTests/LanguageSpecificationFixture.cs +++ b/src/NzbDrone.Core.Test/DecisionEngineTests/LanguageSpecificationFixture.cs @@ -1,10 +1,11 @@ -using FluentAssertions; +using FluentAssertions; using Marr.Data; using NUnit.Framework; using NzbDrone.Core.DecisionEngine.Specifications; -using NzbDrone.Core.Parser; +using NzbDrone.Core.Languages; using NzbDrone.Core.Parser.Model; -using NzbDrone.Core.Profiles; +using NzbDrone.Core.Profiles.Languages; +using NzbDrone.Core.Test.Languages; using NzbDrone.Core.Test.Framework; using NzbDrone.Core.Music; @@ -19,19 +20,25 @@ namespace NzbDrone.Core.Test.DecisionEngineTests [SetUp] public void Setup() { + + LanguageProfile _profile = new LazyLoaded(new LanguageProfile + { + Languages = LanguageFixture.GetDefaultLanguages(Language.English, Language.Spanish), + Cutoff = Language.Spanish + }); + + _remoteAlbum = new RemoteAlbum { ParsedAlbumInfo = new ParsedAlbumInfo { Language = Language.English }, + Artist = new Artist - { - Profile = new LazyLoaded(new Profile - { - Language = Language.English - }) - } + { + LanguageProfile = _profile + } }; } @@ -42,7 +49,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests private void WithGermanRelease() { - _remoteAlbum.ParsedAlbumInfo.Language = Language.German; + _remoteAlbum.ParsedAlbumInfo.Language = Language.German; } [Test] @@ -61,4 +68,4 @@ namespace NzbDrone.Core.Test.DecisionEngineTests Mocker.Resolve().IsSatisfiedBy(_remoteAlbum, null).Accepted.Should().BeFalse(); } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/PrioritizeDownloadDecisionFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/PrioritizeDownloadDecisionFixture.cs index 1f3d4d03d..722cc71e8 100644 --- a/src/NzbDrone.Core.Test/DecisionEngineTests/PrioritizeDownloadDecisionFixture.cs +++ b/src/NzbDrone.Core.Test/DecisionEngineTests/PrioritizeDownloadDecisionFixture.cs @@ -1,11 +1,11 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using Moq; using NzbDrone.Core.Indexers; using NzbDrone.Core.Profiles.Delay; using NzbDrone.Core.Music; -using NzbDrone.Core.Profiles; +using NzbDrone.Core.Profiles.Qualities; using NzbDrone.Core.Qualities; using NzbDrone.Core.Parser.Model; using NzbDrone.Core.DecisionEngine; @@ -14,6 +14,9 @@ using FluentAssertions; using FizzWare.NBuilder; using NzbDrone.Common.Extensions; using NzbDrone.Core.Test.Framework; +using NzbDrone.Core.Languages; +using NzbDrone.Core.Profiles.Languages; +using NzbDrone.Core.Test.Languages; namespace NzbDrone.Core.Test.DecisionEngineTests { @@ -33,11 +36,12 @@ namespace NzbDrone.Core.Test.DecisionEngineTests .Build(); } - private RemoteAlbum GivenRemoteAlbum(List albums, QualityModel quality, int age = 0, long size = 0, DownloadProtocol downloadProtocol = DownloadProtocol.Usenet) + private RemoteAlbum GivenRemoteAlbum(List albums, QualityModel quality, Language language, int age = 0, long size = 0, DownloadProtocol downloadProtocol = DownloadProtocol.Usenet) { var remoteAlbum = new RemoteAlbum(); remoteAlbum.ParsedAlbumInfo = new ParsedAlbumInfo(); remoteAlbum.ParsedAlbumInfo.Quality = quality; + remoteAlbum.ParsedAlbumInfo.Language = language; remoteAlbum.Albums = new List(); remoteAlbum.Albums.AddRange(albums); @@ -48,8 +52,15 @@ namespace NzbDrone.Core.Test.DecisionEngineTests remoteAlbum.Release.DownloadProtocol = downloadProtocol; remoteAlbum.Artist = Builder.CreateNew() - .With(e => e.Profile = new Profile { Items = Qualities.QualityFixture.GetDefaultQualities() }) - .Build(); + .With(e => e.Profile = new Profile + { + Items = Qualities.QualityFixture.GetDefaultQualities() + }) + .With(l => l.LanguageProfile = new LanguageProfile + { + Languages = LanguageFixture.GetDefaultLanguages(), + Cutoff = Language.Spanish + }).Build(); return remoteAlbum; } @@ -67,8 +78,8 @@ namespace NzbDrone.Core.Test.DecisionEngineTests [Test] public void should_put_propers_before_non_propers() { - var remoteAlbum1 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.MP3_256, new Revision(version: 1))); - var remoteAlbum2 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.MP3_256, new Revision(version: 2))); + var remoteAlbum1 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.MP3_256, new Revision(version: 1)), Language.English); + var remoteAlbum2 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.MP3_256, new Revision(version: 2)), Language.English); var decisions = new List(); decisions.Add(new DownloadDecision(remoteAlbum1)); @@ -81,8 +92,8 @@ namespace NzbDrone.Core.Test.DecisionEngineTests [Test] public void should_put_higher_quality_before_lower() { - var remoteAlbum1 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.MP3_192)); - var remoteAlbum2 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.MP3_256)); + var remoteAlbum1 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.MP3_192), Language.English); + var remoteAlbum2 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.MP3_256), Language.English); var decisions = new List(); decisions.Add(new DownloadDecision(remoteAlbum1)); @@ -95,10 +106,10 @@ namespace NzbDrone.Core.Test.DecisionEngineTests [Test] public void should_order_by_age_then_largest_rounded_to_200mb() { - var remoteAlbumSd = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.MP3_192), size: 100.Megabytes(), age: 1); - var remoteAlbumHdSmallOld = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.MP3_256), size: 1200.Megabytes(), age: 1000); - var remoteAlbumSmallYoung = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.MP3_256), size: 1250.Megabytes(), age: 10); - var remoteAlbumHdLargeYoung = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.MP3_256), size: 3000.Megabytes(), age: 1); + var remoteAlbumSd = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.MP3_192), Language.English, size: 100.Megabytes(), age: 1); + var remoteAlbumHdSmallOld = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.MP3_256), Language.English, size: 1200.Megabytes(), age: 1000); + var remoteAlbumSmallYoung = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.MP3_256), Language.English, size: 1250.Megabytes(), age: 10); + var remoteAlbumHdLargeYoung = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.MP3_256), Language.English, size: 3000.Megabytes(), age: 1); var decisions = new List(); decisions.Add(new DownloadDecision(remoteAlbumSd)); @@ -113,8 +124,8 @@ namespace NzbDrone.Core.Test.DecisionEngineTests [Test] public void should_order_by_youngest() { - var remoteAlbum1 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.MP3_256), age: 10); - var remoteAlbum2 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.MP3_256), age: 5); + var remoteAlbum1 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.MP3_256), Language.English, age: 10); + var remoteAlbum2 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.MP3_256), Language.English, age: 5); var decisions = new List(); @@ -128,8 +139,8 @@ namespace NzbDrone.Core.Test.DecisionEngineTests [Test] public void should_not_throw_if_no_episodes_are_found() { - var remoteAlbum1 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.MP3_256), size: 500.Megabytes()); - var remoteAlbum2 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.MP3_256), size: 500.Megabytes()); + var remoteAlbum1 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.MP3_256), Language.English, size: 500.Megabytes()); + var remoteAlbum2 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.MP3_256), Language.English, size: 500.Megabytes()); remoteAlbum1.Albums = new List(); @@ -145,8 +156,8 @@ namespace NzbDrone.Core.Test.DecisionEngineTests { GivenPreferredDownloadProtocol(DownloadProtocol.Usenet); - var remoteAlbum1 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.MP3_256), downloadProtocol: DownloadProtocol.Torrent); - var remoteAlbum2 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.MP3_256), downloadProtocol: DownloadProtocol.Usenet); + var remoteAlbum1 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.MP3_256), Language.English, downloadProtocol: DownloadProtocol.Torrent); + var remoteAlbum2 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.MP3_256), Language.English, downloadProtocol: DownloadProtocol.Usenet); var decisions = new List(); decisions.Add(new DownloadDecision(remoteAlbum1)); @@ -161,8 +172,8 @@ namespace NzbDrone.Core.Test.DecisionEngineTests { GivenPreferredDownloadProtocol(DownloadProtocol.Torrent); - var remoteAlbum1 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.MP3_256), downloadProtocol: DownloadProtocol.Torrent); - var remoteAlbum2 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.MP3_256), downloadProtocol: DownloadProtocol.Usenet); + var remoteAlbum1 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.MP3_256), Language.English, downloadProtocol: DownloadProtocol.Torrent); + var remoteAlbum2 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.MP3_256), Language.English, downloadProtocol: DownloadProtocol.Usenet); var decisions = new List(); decisions.Add(new DownloadDecision(remoteAlbum1)); @@ -175,8 +186,8 @@ namespace NzbDrone.Core.Test.DecisionEngineTests [Test] public void should_prefer_single_album_over_multi_album() { - var remoteAlbum1 = GivenRemoteAlbum(new List { GivenAlbum(1), GivenAlbum(2) }, new QualityModel(Quality.MP3_256)); - var remoteAlbum2 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.MP3_256)); + var remoteAlbum1 = GivenRemoteAlbum(new List { GivenAlbum(1), GivenAlbum(2) }, new QualityModel(Quality.MP3_256), Language.English); + var remoteAlbum2 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.MP3_256), Language.English); var decisions = new List(); decisions.Add(new DownloadDecision(remoteAlbum1)); @@ -189,8 +200,8 @@ namespace NzbDrone.Core.Test.DecisionEngineTests [Test] public void should_prefer_releases_with_more_seeders() { - var remoteAlbum1 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.MP3_256)); - var remoteAlbum2 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.MP3_256)); + var remoteAlbum1 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.MP3_256), Language.English); + var remoteAlbum2 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.MP3_256), Language.English); var torrentInfo1 = new TorrentInfo(); torrentInfo1.PublishDate = DateTime.Now; @@ -209,14 +220,14 @@ namespace NzbDrone.Core.Test.DecisionEngineTests decisions.Add(new DownloadDecision(remoteAlbum2)); var qualifiedReports = Subject.PrioritizeDecisions(decisions); - ((TorrentInfo) qualifiedReports.First().RemoteAlbum.Release).Seeders.Should().Be(torrentInfo2.Seeders); + ((TorrentInfo)qualifiedReports.First().RemoteAlbum.Release).Seeders.Should().Be(torrentInfo2.Seeders); } [Test] public void should_prefer_releases_with_more_peers_given_equal_number_of_seeds() { - var remoteAlbum1 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.MP3_256)); - var remoteAlbum2 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.MP3_256)); + var remoteAlbum1 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.MP3_256), Language.English); + var remoteAlbum2 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.MP3_256), Language.English); var torrentInfo1 = new TorrentInfo(); torrentInfo1.PublishDate = DateTime.Now; @@ -243,8 +254,8 @@ namespace NzbDrone.Core.Test.DecisionEngineTests [Test] public void should_prefer_releases_with_more_peers_no_seeds() { - var remoteAlbum1 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.MP3_256)); - var remoteAlbum2 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.MP3_256)); + var remoteAlbum1 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.MP3_256), Language.English); + var remoteAlbum2 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.MP3_256), Language.English); var torrentInfo1 = new TorrentInfo(); torrentInfo1.PublishDate = DateTime.Now; @@ -272,8 +283,8 @@ namespace NzbDrone.Core.Test.DecisionEngineTests [Test] public void should_prefer_first_release_if_peers_and_size_are_too_similar() { - var remoteAlbum1 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.MP3_256)); - var remoteAlbum2 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.MP3_256)); + var remoteAlbum1 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.MP3_256), Language.English); + var remoteAlbum2 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.MP3_256), Language.English); var torrentInfo1 = new TorrentInfo(); torrentInfo1.PublishDate = DateTime.Now; @@ -295,14 +306,14 @@ namespace NzbDrone.Core.Test.DecisionEngineTests decisions.Add(new DownloadDecision(remoteAlbum2)); var qualifiedReports = Subject.PrioritizeDecisions(decisions); - ((TorrentInfo) qualifiedReports.First().RemoteAlbum.Release).Should().Be(torrentInfo1); + ((TorrentInfo)qualifiedReports.First().RemoteAlbum.Release).Should().Be(torrentInfo1); } [Test] public void should_prefer_first_release_if_age_and_size_are_too_similar() { - var remoteAlbum1 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.MP3_256)); - var remoteAlbum2 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.MP3_256)); + var remoteAlbum1 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.MP3_256), Language.English); + var remoteAlbum2 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.MP3_256), Language.English); remoteAlbum1.Release.PublishDate = DateTime.UtcNow.AddDays(-100); remoteAlbum1.Release.Size = 200.Megabytes(); @@ -321,8 +332,8 @@ namespace NzbDrone.Core.Test.DecisionEngineTests [Test] public void should_prefer_quality_over_the_number_of_peers() { - var remoteAlbum1 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.MP3_512)); - var remoteAlbum2 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.MP3_192)); + var remoteAlbum1 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.MP3_512), Language.English); + var remoteAlbum2 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.MP3_192), Language.English); var torrentInfo1 = new TorrentInfo(); torrentInfo1.PublishDate = DateTime.Now; @@ -346,5 +357,37 @@ namespace NzbDrone.Core.Test.DecisionEngineTests var qualifiedReports = Subject.PrioritizeDecisions(decisions); ((TorrentInfo)qualifiedReports.First().RemoteAlbum.Release).Should().Be(torrentInfo1); } + + [Test] + public void should_order_by_language() + { + var remoteAlbum1 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.MP3_320), Language.English); + var remoteAlbum2 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.MP3_320), Language.French); + var remoteAlbum3 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.MP3_320), Language.German); + + + var decisions = new List(); + decisions.Add(new DownloadDecision(remoteAlbum1)); + decisions.Add(new DownloadDecision(remoteAlbum2)); + decisions.Add(new DownloadDecision(remoteAlbum3)); + + var qualifiedReports = Subject.PrioritizeDecisions(decisions); + qualifiedReports.First().RemoteAlbum.ParsedAlbumInfo.Language.Should().Be(Language.French); + qualifiedReports.Last().RemoteAlbum.ParsedAlbumInfo.Language.Should().Be(Language.German); + } + + [Test] + public void should_put_higher_quality_before_lower_allways() + { + var remoteAlbum1 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.MP3_256), Language.French); + var remoteAlbum2 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.MP3_320), Language.German); + + var decisions = new List(); + decisions.Add(new DownloadDecision(remoteAlbum1)); + decisions.Add(new DownloadDecision(remoteAlbum2)); + + var qualifiedReports = Subject.PrioritizeDecisions(decisions); + qualifiedReports.First().RemoteAlbum.ParsedAlbumInfo.Quality.Quality.Should().Be(Quality.MP3_320); + } } } diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/QualityAllowedByProfileSpecificationFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/QualityAllowedByProfileSpecificationFixture.cs index d362c6d0e..2fc4cf993 100644 --- a/src/NzbDrone.Core.Test/DecisionEngineTests/QualityAllowedByProfileSpecificationFixture.cs +++ b/src/NzbDrone.Core.Test/DecisionEngineTests/QualityAllowedByProfileSpecificationFixture.cs @@ -1,10 +1,10 @@ -using FizzWare.NBuilder; +using FizzWare.NBuilder; using FluentAssertions; using Marr.Data; using NUnit.Framework; using NzbDrone.Core.DecisionEngine.Specifications; using NzbDrone.Core.Parser.Model; -using NzbDrone.Core.Profiles; +using NzbDrone.Core.Profiles.Qualities; using NzbDrone.Core.Qualities; using NzbDrone.Core.Music; using NzbDrone.Core.Test.Framework; @@ -63,4 +63,4 @@ namespace NzbDrone.Core.Test.DecisionEngineTests Subject.IsSatisfiedBy(remoteAlbum, null).Accepted.Should().BeFalse(); } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/QualityUpgradeSpecificationFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/QualityUpgradeSpecificationFixture.cs index a3791ba77..7461f4ca2 100644 --- a/src/NzbDrone.Core.Test/DecisionEngineTests/QualityUpgradeSpecificationFixture.cs +++ b/src/NzbDrone.Core.Test/DecisionEngineTests/QualityUpgradeSpecificationFixture.cs @@ -1,16 +1,19 @@ -using FluentAssertions; +using FluentAssertions; using NUnit.Framework; using NzbDrone.Core.Configuration; -using NzbDrone.Core.Profiles; +using NzbDrone.Core.Profiles.Qualities; using NzbDrone.Core.Qualities; using NzbDrone.Core.DecisionEngine; using NzbDrone.Core.Test.Framework; +using NzbDrone.Core.Languages; +using NzbDrone.Core.Profiles.Languages; +using NzbDrone.Core.Test.Languages; namespace NzbDrone.Core.Test.DecisionEngineTests { [TestFixture] - - public class QualityUpgradeSpecificationFixture : CoreTest + + public class QualityUpgradeSpecificationFixture : CoreTest { public static object[] IsUpgradeTestCases = { @@ -22,7 +25,17 @@ namespace NzbDrone.Core.Test.DecisionEngineTests new object[] { Quality.MP3_320, 1, Quality.MP3_320, 1, Quality.MP3_320, false }, new object[] { Quality.MP3_512, 1, Quality.MP3_512, 1, Quality.MP3_512, false } }; - + + public static object[] IsUpgradeTestCasesLanguages = + { + new object[] { Quality.MP3_192, 1, Language.English, Quality.MP3_192, 2, Language.English, Quality.MP3_192, Language.Spanish, true }, + new object[] { Quality.MP3_192, 1, Language.English, Quality.MP3_192, 1, Language.Spanish, Quality.MP3_192, Language.Spanish, true }, + new object[] { Quality.MP3_320, 1, Language.French, Quality.MP3_320, 2, Language.English, Quality.MP3_320, Language.Spanish, true }, + new object[] { Quality.MP3_192, 1, Language.English, Quality.MP3_192, 1, Language.English, Quality.MP3_192, Language.English, false }, + new object[] { Quality.MP3_320, 1, Language.English, Quality.MP3_320, 2, Language.Spanish, Quality.FLAC, Language.Spanish, false }, + new object[] { Quality.MP3_320, 1, Language.Spanish, Quality.MP3_320, 2, Language.French, Quality.MP3_320, Language.Spanish, false } + }; + [SetUp] public void Setup() { @@ -41,10 +54,41 @@ namespace NzbDrone.Core.Test.DecisionEngineTests { GivenAutoDownloadPropers(true); - var profile = new Profile { Items = Qualities.QualityFixture.GetDefaultQualities() }; + var profile = new Profile - Subject.IsUpgradable(profile, new QualityModel(current, new Revision(version: currentVersion)), new QualityModel(newQuality, new Revision(version: newVersion))) - .Should().Be(expected); + { + Items = Qualities.QualityFixture.GetDefaultQualities() + }; + + var langProfile = new LanguageProfile + + { + Languages = LanguageFixture.GetDefaultLanguages(), + Cutoff = Language.English + }; + + Subject.IsUpgradable(profile, langProfile, new QualityModel(current, new Revision(version: currentVersion)), Language.English, new QualityModel(newQuality, new Revision(version: newVersion)), Language.English) + .Should().Be(expected); + } + + [Test, TestCaseSource("IsUpgradeTestCasesLanguages")] + public void IsUpgradeTestLanguage(Quality current, int currentVersion, Language currentLanguage, Quality newQuality, int newVersion, Language newLanguage, Quality cutoff, Language languageCutoff, bool expected) + { + GivenAutoDownloadPropers(true); + + var profile = new Profile + { + Items = Qualities.QualityFixture.GetDefaultQualities(), + Cutoff = cutoff, + }; + + var langProfile = new LanguageProfile + { + Languages = LanguageFixture.GetDefaultLanguages(), + Cutoff = languageCutoff + }; + + Subject.IsUpgradable(profile, langProfile, new QualityModel(current, new Revision(version: currentVersion)), currentLanguage, new QualityModel(newQuality, new Revision(version: newVersion)), newLanguage).Should().Be(expected); } [Test] @@ -52,10 +96,20 @@ namespace NzbDrone.Core.Test.DecisionEngineTests { GivenAutoDownloadPropers(false); - var profile = new Profile { Items = Qualities.QualityFixture.GetDefaultQualities() }; + var profile = new Profile + { + Items = Qualities.QualityFixture.GetDefaultQualities(), + }; + + var langProfile = new LanguageProfile + + { + Languages = LanguageFixture.GetDefaultLanguages(), + Cutoff = Language.English + }; - Subject.IsUpgradable(profile, new QualityModel(Quality.MP3_192, new Revision(version: 2)), new QualityModel(Quality.MP3_192, new Revision(version: 1))) - .Should().BeFalse(); + Subject.IsUpgradable(profile, langProfile, new QualityModel(Quality.MP3_256, new Revision(version: 2)), Language.English, new QualityModel(Quality.MP3_256, new Revision(version: 1)), Language.English) + .Should().BeFalse(); } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/QueueSpecificationFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/QueueSpecificationFixture.cs index 38dc8d420..2e51b0989 100644 --- a/src/NzbDrone.Core.Test/DecisionEngineTests/QueueSpecificationFixture.cs +++ b/src/NzbDrone.Core.Test/DecisionEngineTests/QueueSpecificationFixture.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using FizzWare.NBuilder; using FluentAssertions; @@ -6,11 +6,13 @@ using NUnit.Framework; using NzbDrone.Core.DecisionEngine; using NzbDrone.Core.DecisionEngine.Specifications; using NzbDrone.Core.Parser.Model; -using NzbDrone.Core.Profiles; +using NzbDrone.Core.Profiles.Qualities; using NzbDrone.Core.Qualities; using NzbDrone.Core.Queue; using NzbDrone.Core.Music; using NzbDrone.Core.Test.Framework; +using NzbDrone.Core.Profiles.Languages; +using NzbDrone.Core.Languages; namespace NzbDrone.Core.Test.DecisionEngineTests { @@ -27,11 +29,18 @@ namespace NzbDrone.Core.Test.DecisionEngineTests [SetUp] public void Setup() { - Mocker.Resolve(); + Mocker.Resolve(); _artist = Builder.CreateNew() - .With(e => e.Profile = new Profile { Items = Qualities.QualityFixture.GetDefaultQualities() }) - .Build(); + .With(e => e.Profile = new Profile + { + Items = Qualities.QualityFixture.GetDefaultQualities(), + }) + .With(l => l.LanguageProfile = new LanguageProfile + { + Languages = Languages.LanguageFixture.GetDefaultLanguages(), + Cutoff = Language.Spanish + }).Build(); _album = Builder.CreateNew() .With(e => e.ArtistId = _artist.Id) @@ -49,7 +58,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests _remoteAlbum = Builder.CreateNew() .With(r => r.Artist = _artist) .With(r => r.Albums = new List { _album }) - .With(r => r.ParsedAlbumInfo = new ParsedAlbumInfo { Quality = new QualityModel(Quality.MP3_192) }) + .With(r => r.ParsedAlbumInfo = new ParsedAlbumInfo { Quality = new QualityModel(Quality.MP3_256), Language = Language.Spanish }) .Build(); } @@ -95,14 +104,36 @@ namespace NzbDrone.Core.Test.DecisionEngineTests public void should_return_true_when_quality_in_queue_is_lower() { _artist.Profile.Value.Cutoff = Quality.MP3_512; + _artist.LanguageProfile.Value.Cutoff = Language.Spanish; var remoteAlbum = Builder.CreateNew() .With(r => r.Artist = _artist) .With(r => r.Albums = new List { _album }) .With(r => r.ParsedAlbumInfo = new ParsedAlbumInfo - { - Quality = new QualityModel(Quality.MP3_192) - }) + { + Quality = new QualityModel(Quality.MP3_192), + Language = Language.Spanish + }) + .Build(); + + GivenQueue(new List { remoteAlbum }); + Subject.IsSatisfiedBy(_remoteAlbum, null).Accepted.Should().BeTrue(); + } + + [Test] + public void should_return_true_when_quality_in_queue_is_lower_but_language_is_higher() + { + _artist.Profile.Value.Cutoff = Quality.FLAC; + _artist.LanguageProfile.Value.Cutoff = Language.Spanish; + + var remoteAlbum = Builder.CreateNew() + .With(r => r.Artist = _artist) + .With(r => r.Albums = new List { _album }) + .With(r => r.ParsedAlbumInfo = new ParsedAlbumInfo + { + Quality = new QualityModel(Quality.MP3_192), + Language = Language.English + }) .Build(); GivenQueue(new List { remoteAlbum }); @@ -116,9 +147,9 @@ namespace NzbDrone.Core.Test.DecisionEngineTests .With(r => r.Artist = _artist) .With(r => r.Albums = new List { _otherAlbum }) .With(r => r.ParsedAlbumInfo = new ParsedAlbumInfo - { - Quality = new QualityModel(Quality.MP3_192) - }) + { + Quality = new QualityModel(Quality.MP3_192) + }) .Build(); GivenQueue(new List { remoteAlbum }); @@ -126,21 +157,39 @@ namespace NzbDrone.Core.Test.DecisionEngineTests } [Test] - public void should_return_false_when_qualities_are_the_same() + public void should_return_false_when_qualities_are_the_same_and_languages_are_the_same() { var remoteAlbum = Builder.CreateNew() .With(r => r.Artist = _artist) .With(r => r.Albums = new List { _album }) .With(r => r.ParsedAlbumInfo = new ParsedAlbumInfo - { - Quality = new QualityModel(Quality.MP3_192) - }) + { + Quality = new QualityModel(Quality.MP3_192), + Language = Language.Spanish + }) .Build(); GivenQueue(new List { remoteAlbum }); Subject.IsSatisfiedBy(_remoteAlbum, null).Accepted.Should().BeFalse(); } + [Test] + public void should_return_true_when_qualities_are_the_same_but_language_is_better() + { + var remoteAlbum = Builder.CreateNew() + .With(r => r.Artist = _artist) + .With(r => r.Albums = new List { _album }) + .With(r => r.ParsedAlbumInfo = new ParsedAlbumInfo + { + Quality = new QualityModel(Quality.MP3_192), + Language = Language.English, + }) + .Build(); + + GivenQueue(new List { remoteAlbum }); + Subject.IsSatisfiedBy(_remoteAlbum, null).Accepted.Should().BeTrue(); + } + [Test] public void should_return_false_when_quality_in_queue_is_better() { @@ -150,9 +199,10 @@ namespace NzbDrone.Core.Test.DecisionEngineTests .With(r => r.Artist = _artist) .With(r => r.Albums = new List { _album }) .With(r => r.ParsedAlbumInfo = new ParsedAlbumInfo - { - Quality = new QualityModel(Quality.MP3_256) - }) + { + Quality = new QualityModel(Quality.MP3_256), + Language = Language.English + }) .Build(); GivenQueue(new List { remoteAlbum }); @@ -167,7 +217,8 @@ namespace NzbDrone.Core.Test.DecisionEngineTests .With(r => r.Albums = new List { _album, _otherAlbum }) .With(r => r.ParsedAlbumInfo = new ParsedAlbumInfo { - Quality = new QualityModel(Quality.MP3_256) + Quality = new QualityModel(Quality.MP3_256), + Language = Language.English }) .Build(); @@ -183,7 +234,8 @@ namespace NzbDrone.Core.Test.DecisionEngineTests .With(r => r.Albums = new List { _album }) .With(r => r.ParsedAlbumInfo = new ParsedAlbumInfo { - Quality = new QualityModel(Quality.MP3_256) + Quality = new QualityModel(Quality.MP3_256), + Language = Language.English }) .Build(); @@ -201,7 +253,8 @@ namespace NzbDrone.Core.Test.DecisionEngineTests .With(r => r.Albums = new List { _album, _otherAlbum }) .With(r => r.ParsedAlbumInfo = new ParsedAlbumInfo { - Quality = new QualityModel(Quality.MP3_256) + Quality = new QualityModel(Quality.MP3_256), + Language = Language.English }) .Build(); @@ -218,11 +271,10 @@ namespace NzbDrone.Core.Test.DecisionEngineTests .All() .With(r => r.Artist = _artist) .With(r => r.ParsedAlbumInfo = new ParsedAlbumInfo - { - Quality = - new QualityModel( - Quality.MP3_256) - }) + { + Quality = new QualityModel(Quality.MP3_256), + Language = Language.English + }) .TheFirst(1) .With(r => r.Albums = new List { _album }) .TheNext(1) @@ -235,7 +287,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests } [Test] - public void should_return_false_if_quality_in_queue_meets_cutoff() + public void should_return_false_if_quality_and_language_in_queue_meets_cutoff() { _artist.Profile.Value.Cutoff = _remoteAlbum.ParsedAlbumInfo.Quality.Quality; @@ -244,7 +296,8 @@ namespace NzbDrone.Core.Test.DecisionEngineTests .With(r => r.Albums = new List { _album }) .With(r => r.ParsedAlbumInfo = new ParsedAlbumInfo { - Quality = new QualityModel(Quality.MP3_256) + Quality = new QualityModel(Quality.MP3_256), + Language = Language.Spanish }) .Build(); @@ -253,4 +306,4 @@ namespace NzbDrone.Core.Test.DecisionEngineTests Subject.IsSatisfiedBy(_remoteAlbum, null).Accepted.Should().BeFalse(); } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/RssSync/DelaySpecificationFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/RssSync/DelaySpecificationFixture.cs index 9416dc0c7..35c174ccf 100644 --- a/src/NzbDrone.Core.Test/DecisionEngineTests/RssSync/DelaySpecificationFixture.cs +++ b/src/NzbDrone.Core.Test/DecisionEngineTests/RssSync/DelaySpecificationFixture.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using FizzWare.NBuilder; @@ -13,7 +13,9 @@ using NzbDrone.Core.Indexers; using NzbDrone.Core.IndexerSearch.Definitions; using NzbDrone.Core.MediaFiles; using NzbDrone.Core.Parser.Model; -using NzbDrone.Core.Profiles; +using NzbDrone.Core.Profiles.Qualities; +using NzbDrone.Core.Profiles.Languages; +using NzbDrone.Core.Languages; using NzbDrone.Core.Profiles.Delay; using NzbDrone.Core.Qualities; using NzbDrone.Core.Test.Framework; @@ -25,6 +27,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests.RssSync public class DelaySpecificationFixture : CoreTest { private Profile _profile; + private LanguageProfile _langProfile; private DelayProfile _delayProfile; private RemoteAlbum _remoteAlbum; @@ -34,12 +37,17 @@ namespace NzbDrone.Core.Test.DecisionEngineTests.RssSync _profile = Builder.CreateNew() .Build(); + _langProfile = Builder.CreateNew() + .Build(); + + _delayProfile = Builder.CreateNew() - .With(d => d.PreferredProtocol = DownloadProtocol.Usenet) - .Build(); + .With(d => d.PreferredProtocol = DownloadProtocol.Usenet) + .Build(); var artist = Builder.CreateNew() .With(s => s.Profile = _profile) + .With(s => s.LanguageProfile = _langProfile) .Build(); _remoteAlbum = Builder.CreateNew() @@ -53,6 +61,9 @@ namespace NzbDrone.Core.Test.DecisionEngineTests.RssSync _profile.Cutoff = Quality.MP3_320; + _langProfile.Cutoff = Language.Spanish; + _langProfile.Languages = Languages.LanguageFixture.GetDefaultLanguages(); + _remoteAlbum.ParsedAlbumInfo = new ParsedAlbumInfo(); _remoteAlbum.Release = new ReleaseInfo(); _remoteAlbum.Release.DownloadProtocol = DownloadProtocol.Usenet; @@ -61,7 +72,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests.RssSync Mocker.GetMock() .Setup(s => s.GetFilesByAlbum(It.IsAny(), It.IsAny())) - .Returns(new List {}); + .Returns(new List { }); Mocker.GetMock() .Setup(s => s.BestForTags(It.IsAny>())) @@ -72,17 +83,20 @@ namespace NzbDrone.Core.Test.DecisionEngineTests.RssSync .Returns(new List()); } - private void GivenExistingFile(QualityModel quality) + private void GivenExistingFile(QualityModel quality, Language language) { Mocker.GetMock() .Setup(s => s.GetFilesByAlbum(It.IsAny(), It.IsAny())) - .Returns(new List { new TrackFile { Quality = quality } }); + .Returns(new List { new TrackFile { + Quality = quality, + Language = language + } }); } private void GivenUpgradeForExistingFile() { - Mocker.GetMock() - .Setup(s => s.IsUpgradable(It.IsAny(), It.IsAny(), It.IsAny())) + Mocker.GetMock() + .Setup(s => s.IsUpgradable(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .Returns(true); } @@ -112,9 +126,10 @@ namespace NzbDrone.Core.Test.DecisionEngineTests.RssSync } [Test] - public void should_be_true_when_quality_is_last_allowed_in_profile() + public void should_be_true_when_quality_and_language_is_last_allowed_in_profile() { _remoteAlbum.ParsedAlbumInfo.Quality = new QualityModel(Quality.MP3_320); + _remoteAlbum.ParsedAlbumInfo.Language = Language.French; Subject.IsSatisfiedBy(_remoteAlbum, null).Accepted.Should().BeTrue(); } @@ -147,10 +162,10 @@ namespace NzbDrone.Core.Test.DecisionEngineTests.RssSync _remoteAlbum.ParsedAlbumInfo.Quality = new QualityModel(Quality.MP3_256, new Revision(version: 2)); _remoteAlbum.Release.PublishDate = DateTime.UtcNow; - GivenExistingFile(new QualityModel(Quality.MP3_256)); + GivenExistingFile(new QualityModel(Quality.MP3_256), Language.English); GivenUpgradeForExistingFile(); - Mocker.GetMock() + Mocker.GetMock() .Setup(s => s.IsRevisionUpgrade(It.IsAny(), It.IsAny())) .Returns(true); @@ -165,10 +180,10 @@ namespace NzbDrone.Core.Test.DecisionEngineTests.RssSync _remoteAlbum.ParsedAlbumInfo.Quality = new QualityModel(Quality.MP3_256, new Revision(real: 1)); _remoteAlbum.Release.PublishDate = DateTime.UtcNow; - GivenExistingFile(new QualityModel(Quality.MP3_256)); + GivenExistingFile(new QualityModel(Quality.MP3_256), Language.English); GivenUpgradeForExistingFile(); - Mocker.GetMock() + Mocker.GetMock() .Setup(s => s.IsRevisionUpgrade(It.IsAny(), It.IsAny())) .Returns(true); @@ -183,7 +198,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests.RssSync _remoteAlbum.ParsedAlbumInfo.Quality = new QualityModel(Quality.MP3_256, new Revision(version: 2)); _remoteAlbum.Release.PublishDate = DateTime.UtcNow; - GivenExistingFile(new QualityModel(Quality.MP3_192)); + GivenExistingFile(new QualityModel(Quality.MP3_192), Language.English); _delayProfile.UsenetDelay = 720; diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/RssSync/DeletedTrackFileSpecificationFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/RssSync/DeletedTrackFileSpecificationFixture.cs new file mode 100644 index 000000000..173b061e3 --- /dev/null +++ b/src/NzbDrone.Core.Test/DecisionEngineTests/RssSync/DeletedTrackFileSpecificationFixture.cs @@ -0,0 +1,156 @@ +using System; +using System.Collections.Generic; +using FizzWare.NBuilder; +using FluentAssertions; +using NUnit.Framework; +using NzbDrone.Core.Configuration; +using NzbDrone.Core.DecisionEngine.Specifications.RssSync; +using NzbDrone.Core.IndexerSearch.Definitions; +using NzbDrone.Core.MediaFiles; +using NzbDrone.Core.Parser.Model; +using NzbDrone.Core.Profiles.Qualities; +using NzbDrone.Core.Qualities; +using NzbDrone.Core.Music; +using NzbDrone.Core.DecisionEngine; +using NzbDrone.Core.Test.Framework; +using NzbDrone.Common.Disk; +using Moq; +using NzbDrone.Test.Common; +using System.IO; + +namespace NzbDrone.Core.Test.DecisionEngineTests.RssSync +{ + [TestFixture] + public class DeletedTrackFileSpecificationFixture : CoreTest + { + private RemoteAlbum _parseResultMulti; + private RemoteAlbum _parseResultSingle; + private TrackFile _firstFile; + private TrackFile _secondFile; + + [SetUp] + public void Setup() + { + _firstFile = + new TrackFile{ + Id = 1, + RelativePath = "My.Artist.S01E01.mp3", + Quality = new QualityModel(Quality.FLAC, new Revision(version: 1)), + DateAdded = DateTime.Now, + AlbumId = 1 + + }; + _secondFile = + new TrackFile{ + Id = 2, + RelativePath = "My.Artist.S01E02.mp3", + Quality = new QualityModel(Quality.FLAC, new Revision(version: 1)), + DateAdded = DateTime.Now, + AlbumId = 2 + + }; + + var singleAlbumList = new List { new Album { Id = 1 } }; + var doubleAlbumList = new List { + new Album { Id = 1 }, + new Album { Id = 2 } + }; + + var firstTrack = new Track { TrackFile = _firstFile, TrackFileId = 1, AlbumId = 1 }; + var secondTrack = new Track { TrackFile = _secondFile, TrackFileId = 2, AlbumId = 2 }; + + var fakeArtist = Builder.CreateNew() + .With(c => c.Profile = new Profile { Cutoff = Quality.FLAC }) + .With(c => c.Path = @"C:\Music\My.Artist".AsOsAgnostic()) + .Build(); + + _parseResultMulti = new RemoteAlbum + { + Artist = fakeArtist, + ParsedAlbumInfo = new ParsedAlbumInfo { Quality = new QualityModel(Quality.MP3_256, new Revision(version: 2)) }, + Albums = doubleAlbumList + }; + + _parseResultSingle = new RemoteAlbum + { + Artist = fakeArtist, + ParsedAlbumInfo = new ParsedAlbumInfo { Quality = new QualityModel(Quality.MP3_256, new Revision(version: 2)) }, + Albums = singleAlbumList + }; + + GivenUnmonitorDeletedTracks(true); + } + + private void GivenUnmonitorDeletedTracks(bool enabled) + { + Mocker.GetMock() + .SetupGet(v => v.AutoUnmonitorPreviouslyDownloadedTracks) + .Returns(enabled); + } + + private void SetupMediaFile(List files) + { + Mocker.GetMock() + .Setup(v => v.GetFilesByAlbum(It.IsAny(), It.IsAny())) + .Returns(files); + } + + private void WithExistingFile(TrackFile trackFile) + { + var path = Path.Combine(@"C:\Music\My.Artist".AsOsAgnostic(), trackFile.RelativePath); + + Mocker.GetMock() + .Setup(v => v.FileExists(path)) + .Returns(true); + } + + [Test] + public void should_return_true_when_unmonitor_deleted_tracks_is_off() + { + GivenUnmonitorDeletedTracks(false); + + Subject.IsSatisfiedBy(_parseResultSingle, null).Accepted.Should().BeTrue(); + } + + [Test] + public void should_return_true_when_searching() + { + Subject.IsSatisfiedBy(_parseResultSingle, new ArtistSearchCriteria()).Accepted.Should().BeTrue(); + } + + [Test] + public void should_return_true_if_file_exists() + { + WithExistingFile(_firstFile); + SetupMediaFile(new List { _firstFile }); + + Subject.IsSatisfiedBy(_parseResultSingle, null).Accepted.Should().BeTrue(); + } + + [Test] + public void should_return_false_if_file_is_missing() + { + SetupMediaFile(new List { _firstFile }); + Subject.IsSatisfiedBy(_parseResultSingle, null).Accepted.Should().BeFalse(); + } + + [Test] + public void should_return_true_if_both_of_multiple_episode_exist() + { + WithExistingFile(_firstFile); + WithExistingFile(_secondFile); + SetupMediaFile(new List { _firstFile, _secondFile }); + + Subject.IsSatisfiedBy(_parseResultMulti, null).Accepted.Should().BeTrue(); + } + + [Test] + public void should_return_false_if_one_of_multiple_episode_is_missing() + { + WithExistingFile(_firstFile); + SetupMediaFile(new List { _firstFile, _secondFile }); + + Subject.IsSatisfiedBy(_parseResultMulti, null).Accepted.Should().BeFalse(); + } + } +} diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/RssSync/ProperSpecificationFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/RssSync/ProperSpecificationFixture.cs index 3a02acf7f..ab557ef4b 100644 --- a/src/NzbDrone.Core.Test/DecisionEngineTests/RssSync/ProperSpecificationFixture.cs +++ b/src/NzbDrone.Core.Test/DecisionEngineTests/RssSync/ProperSpecificationFixture.cs @@ -9,7 +9,7 @@ using NzbDrone.Core.DecisionEngine.Specifications.RssSync; using NzbDrone.Core.IndexerSearch.Definitions; using NzbDrone.Core.MediaFiles; using NzbDrone.Core.Parser.Model; -using NzbDrone.Core.Profiles; +using NzbDrone.Core.Profiles.Qualities; using NzbDrone.Core.Qualities; using NzbDrone.Core.Music; using NzbDrone.Core.DecisionEngine; @@ -30,7 +30,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests.RssSync [SetUp] public void Setup() { - Mocker.Resolve(); + Mocker.Resolve(); _firstFile = new TrackFile { Quality = new QualityModel(Quality.FLAC, new Revision(version: 1)), DateAdded = DateTime.Now }; _secondFile = new TrackFile { Quality = new QualityModel(Quality.FLAC, new Revision(version: 1)), DateAdded = DateTime.Now }; diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/SameEpisodesSpecificationFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/SameEpisodesSpecificationFixture.cs deleted file mode 100644 index 183b6cc77..000000000 --- a/src/NzbDrone.Core.Test/DecisionEngineTests/SameEpisodesSpecificationFixture.cs +++ /dev/null @@ -1,76 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using FizzWare.NBuilder; -using FluentAssertions; -using Moq; -using NUnit.Framework; -using NzbDrone.Core.DecisionEngine; -using NzbDrone.Core.Tv; - -using NzbDrone.Core.Test.Framework; - -namespace NzbDrone.Core.Test.DecisionEngineTests -{ - [TestFixture] - public class SameEpisodesSpecificationFixture : CoreTest - { - private List _episodes; - - [SetUp] - public void Setup() - { - _episodes = Builder.CreateListOfSize(2) - .All() - .With(e => e.EpisodeFileId = 1) - .BuildList(); - } - - private void GivenEpisodesInFile(List episodes) - { - Mocker.GetMock() - .Setup(s => s.GetEpisodesByFileId(It.IsAny())) - .Returns(episodes); - } - - [Test] - public void should_not_upgrade_when_new_release_contains_less_episodes() - { - GivenEpisodesInFile(_episodes); - - Subject.IsSatisfiedBy(new List { _episodes.First() }).Should().BeFalse(); - } - - [Test] - public void should_upgrade_when_new_release_contains_more_episodes() - { - GivenEpisodesInFile(new List { _episodes.First() }); - - Subject.IsSatisfiedBy(_episodes).Should().BeTrue(); - } - - [Test] - public void should_upgrade_when_new_release_contains_the_same_episodes() - { - GivenEpisodesInFile(_episodes); - - Subject.IsSatisfiedBy(_episodes).Should().BeTrue(); - } - - [Test] - public void should_upgrade_when_release_contains_the_same_episodes_as_multiple_files() - { - var episodes = Builder.CreateListOfSize(2) - .BuildList(); - - Mocker.GetMock() - .Setup(s => s.GetEpisodesByFileId(episodes.First().EpisodeFileId)) - .Returns(new List { episodes.First() }); - - Mocker.GetMock() - .Setup(s => s.GetEpisodesByFileId(episodes.Last().EpisodeFileId)) - .Returns(new List { episodes.Last() }); - - Subject.IsSatisfiedBy(episodes).Should().BeTrue(); - } - } -} \ No newline at end of file diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/Search/ArtistSpecificationFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/Search/ArtistSpecificationFixture.cs index 443bb0c75..423d2614f 100644 --- a/src/NzbDrone.Core.Test/DecisionEngineTests/Search/ArtistSpecificationFixture.cs +++ b/src/NzbDrone.Core.Test/DecisionEngineTests/Search/ArtistSpecificationFixture.cs @@ -1,10 +1,9 @@ -using FizzWare.NBuilder; +using FizzWare.NBuilder; using FluentAssertions; using NUnit.Framework; using NzbDrone.Core.DecisionEngine.Specifications.Search; using NzbDrone.Core.IndexerSearch.Definitions; using NzbDrone.Core.Parser.Model; -using NzbDrone.Core.Tv; using NzbDrone.Core.Music; using NzbDrone.Test.Common; diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/Search/TorrentSeedingSpecificationFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/Search/TorrentSeedingSpecificationFixture.cs new file mode 100644 index 000000000..7a4b86f02 --- /dev/null +++ b/src/NzbDrone.Core.Test/DecisionEngineTests/Search/TorrentSeedingSpecificationFixture.cs @@ -0,0 +1,111 @@ +using FizzWare.NBuilder; +using FluentAssertions; +using Moq; +using NUnit.Framework; +using NzbDrone.Core.Datastore; +using NzbDrone.Core.DecisionEngine.Specifications.Search; +using NzbDrone.Core.Indexers; +using NzbDrone.Core.Indexers.TorrentRss; +using NzbDrone.Core.IndexerSearch.Definitions; +using NzbDrone.Core.Parser.Model; +using NzbDrone.Core.Music; +using NzbDrone.Test.Common; + +namespace NzbDrone.Core.Test.DecisionEngineTests.Search +{ + [TestFixture] + public class TorrentSeedingSpecificationFixture : TestBase + { + private Artist _artist; + private RemoteAlbum _remoteAlbum; + private IndexerDefinition _indexerDefinition; + + [SetUp] + public void Setup() + { + _artist = Builder.CreateNew().With(s => s.Id = 1).Build(); + + _remoteAlbum = new RemoteAlbum + { + Artist = _artist, + Release = new TorrentInfo + { + IndexerId = 1, + Title = "Artist - Album [FLAC-RlsGrp]", + Seeders = 0 + } + }; + + _indexerDefinition = new IndexerDefinition + { + Settings = new TorrentRssIndexerSettings { MinimumSeeders = 5 } + }; + + Mocker.GetMock() + .Setup(v => v.Get(1)) + .Returns(_indexerDefinition); + + } + + private void GivenReleaseSeeders(int? seeders) + { + (_remoteAlbum.Release as TorrentInfo).Seeders = seeders; + } + + [Test] + public void should_return_true_if_not_torrent() + { + _remoteAlbum.Release = new ReleaseInfo + { + IndexerId = 1, + Title = "Artist - Album [FLAC-RlsGrp]" + }; + + Subject.IsSatisfiedBy(_remoteAlbum, null).Accepted.Should().BeTrue(); + } + + [Test] + public void should_return_true_if_indexer_not_specified() + { + _remoteAlbum.Release.IndexerId = 0; + + Subject.IsSatisfiedBy(_remoteAlbum, null).Accepted.Should().BeTrue(); + } + + [Test] + public void should_return_true_if_indexer_no_longer_exists() + { + Mocker.GetMock() + .Setup(v => v.Get(It.IsAny())) + .Callback(i => { throw new ModelNotFoundException(typeof(IndexerDefinition), i); }); + + Subject.IsSatisfiedBy(_remoteAlbum, null).Accepted.Should().BeTrue(); + } + + [Test] + public void should_return_true_if_seeds_unknown() + { + GivenReleaseSeeders(null); + + Subject.IsSatisfiedBy(_remoteAlbum, null).Accepted.Should().BeTrue(); + } + + [TestCase(5)] + [TestCase(6)] + public void should_return_true_if_seeds_above_or_equal_to_limit(int seeders) + { + GivenReleaseSeeders(seeders); + + Subject.IsSatisfiedBy(_remoteAlbum, null).Accepted.Should().BeTrue(); + } + + [TestCase(0)] + [TestCase(4)] + public void should_return_false_if_seeds_belove_limit(int seeders) + { + GivenReleaseSeeders(seeders); + + Subject.IsSatisfiedBy(_remoteAlbum, null).Accepted.Should().BeFalse(); + } + } +} diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/UpgradeDiskSpecificationFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/UpgradeDiskSpecificationFixture.cs index 214cb2196..d66b23b0c 100644 --- a/src/NzbDrone.Core.Test/DecisionEngineTests/UpgradeDiskSpecificationFixture.cs +++ b/src/NzbDrone.Core.Test/DecisionEngineTests/UpgradeDiskSpecificationFixture.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using FizzWare.NBuilder; using FluentAssertions; @@ -7,11 +7,13 @@ using Moq; using NzbDrone.Core.DecisionEngine.Specifications; using NzbDrone.Core.MediaFiles; using NzbDrone.Core.Parser.Model; -using NzbDrone.Core.Profiles; +using NzbDrone.Core.Profiles.Qualities; using NzbDrone.Core.Qualities; using NzbDrone.Core.Music; using NzbDrone.Core.DecisionEngine; using NzbDrone.Core.Test.Framework; +using NzbDrone.Core.Languages; +using NzbDrone.Core.Profiles.Languages; namespace NzbDrone.Core.Test.DecisionEngineTests { @@ -27,16 +29,19 @@ namespace NzbDrone.Core.Test.DecisionEngineTests [SetUp] public void Setup() { - Mocker.Resolve(); + Mocker.Resolve(); - _firstFile = new TrackFile { Quality = new QualityModel(Quality.FLAC, new Revision(version: 2)), DateAdded = DateTime.Now }; - _secondFile = new TrackFile { Quality = new QualityModel(Quality.FLAC, new Revision(version: 2)), DateAdded = DateTime.Now }; + _firstFile = new TrackFile { Quality = new QualityModel(Quality.FLAC, new Revision(version: 2)), DateAdded = DateTime.Now, Language = Language.English }; + _secondFile = new TrackFile { Quality = new QualityModel(Quality.FLAC, new Revision(version: 2)), DateAdded = DateTime.Now, Language = Language.English }; var singleEpisodeList = new List { new Album {}}; var doubleEpisodeList = new List { new Album {}, new Album {}, new Album {} }; + var languages = Languages.LanguageFixture.GetDefaultLanguages(Language.English, Language.Spanish); + var fakeArtist = Builder.CreateNew() - .With(c => c.Profile = new Profile { Cutoff = Quality.MP3_512, Items = Qualities.QualityFixture.GetDefaultQualities() }) + .With(c => c.Profile = new Profile { Cutoff = Quality.MP3_512, Items = Qualities.QualityFixture.GetDefaultQualities()}) + .With(l => l.LanguageProfile = new LanguageProfile { Cutoff = Language.Spanish, Languages = languages }) .Build(); Mocker.GetMock() @@ -46,14 +51,14 @@ namespace NzbDrone.Core.Test.DecisionEngineTests _parseResultMulti = new RemoteAlbum { Artist = fakeArtist, - ParsedAlbumInfo = new ParsedAlbumInfo { Quality = new QualityModel(Quality.MP3_256, new Revision(version: 2)) }, + ParsedAlbumInfo = new ParsedAlbumInfo { Quality = new QualityModel(Quality.MP3_256, new Revision(version: 2)), Language = Language.English }, Albums = doubleEpisodeList }; _parseResultSingle = new RemoteAlbum { Artist = fakeArtist, - ParsedAlbumInfo = new ParsedAlbumInfo { Quality = new QualityModel(Quality.MP3_256, new Revision(version: 2)) }, + ParsedAlbumInfo = new ParsedAlbumInfo { Quality = new QualityModel(Quality.MP3_256, new Revision(version: 2)), Language = Language.English }, Albums = singleEpisodeList }; @@ -108,4 +113,4 @@ namespace NzbDrone.Core.Test.DecisionEngineTests Subject.IsSatisfiedBy(_parseResultSingle, null).Accepted.Should().BeFalse(); } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Core.Test/DiskSpace/DiskSpaceServiceFixture.cs b/src/NzbDrone.Core.Test/DiskSpace/DiskSpaceServiceFixture.cs index e92dba2ad..31d5f5fca 100644 --- a/src/NzbDrone.Core.Test/DiskSpace/DiskSpaceServiceFixture.cs +++ b/src/NzbDrone.Core.Test/DiskSpace/DiskSpaceServiceFixture.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Text; @@ -9,7 +9,7 @@ using NzbDrone.Common.Disk; using NzbDrone.Core.Configuration; using NzbDrone.Core.DiskSpace; using NzbDrone.Core.Test.Framework; -using NzbDrone.Core.Tv; +using NzbDrone.Core.Music; using NzbDrone.Test.Common; namespace NzbDrone.Core.Test.DiskSpace @@ -17,16 +17,14 @@ namespace NzbDrone.Core.Test.DiskSpace [TestFixture] public class DiskSpaceServiceFixture : CoreTest { - private string _seriesFolder; - private string _seriesFolder2; - private string _droneFactoryFolder; + private string _artistFolder; + private string _artostFolder2; [SetUp] public void SetUp() { - _seriesFolder = @"G:\fasdlfsdf\series".AsOsAgnostic(); - _seriesFolder2 = @"G:\fasdlfsdf\series2".AsOsAgnostic(); - _droneFactoryFolder = @"G:\dronefactory".AsOsAgnostic(); + _artistFolder = @"G:\fasdlfsdf\artist".AsOsAgnostic(); + _artostFolder2 = @"G:\fasdlfsdf\artist2".AsOsAgnostic(); Mocker.GetMock() .Setup(v => v.GetMounts()) @@ -44,14 +42,14 @@ namespace NzbDrone.Core.Test.DiskSpace .Setup(v => v.GetTotalSize(It.IsAny())) .Returns(0); - GivenSeries(); + GivenArtist(); } - private void GivenSeries(params Series[] series) + private void GivenArtist(params Artist[] artist) { - Mocker.GetMock() - .Setup(v => v.GetAllSeries()) - .Returns(series.ToList()); + Mocker.GetMock() + .Setup(v => v.GetAllArtists()) + .Returns(artist.ToList()); } private void GivenExistingFolder(string folder) @@ -62,11 +60,11 @@ namespace NzbDrone.Core.Test.DiskSpace } [Test] - public void should_check_diskspace_for_series_folders() + public void should_check_diskspace_for_artist_folders() { - GivenSeries(new Series { Path = _seriesFolder }); + GivenArtist(new Artist { Path = _artistFolder }); - GivenExistingFolder(_seriesFolder); + GivenExistingFolder(_artistFolder); var freeSpace = Subject.GetFreeSpace(); @@ -76,10 +74,10 @@ namespace NzbDrone.Core.Test.DiskSpace [Test] public void should_check_diskspace_for_same_root_folder_only_once() { - GivenSeries(new Series { Path = _seriesFolder }, new Series { Path = _seriesFolder2 }); + GivenArtist(new Artist { Path = _artistFolder }, new Artist { Path = _artostFolder2 }); - GivenExistingFolder(_seriesFolder); - GivenExistingFolder(_seriesFolder2); + GivenExistingFolder(_artistFolder); + GivenExistingFolder(_artostFolder2); var freeSpace = Subject.GetFreeSpace(); @@ -90,9 +88,9 @@ namespace NzbDrone.Core.Test.DiskSpace } [Test] - public void should_not_check_diskspace_for_missing_series_folders() + public void should_not_check_diskspace_for_missing_artist_folders() { - GivenSeries(new Series { Path = _seriesFolder }); + GivenArtist(new Artist { Path = _artistFolder }); var freeSpace = Subject.GetFreeSpace(); @@ -102,33 +100,25 @@ namespace NzbDrone.Core.Test.DiskSpace .Verify(v => v.GetAvailableSpace(It.IsAny()), Times.Never()); } - [Test] - public void should_check_diskspace_for_dronefactory_folder() + [TestCase("/boot")] + [TestCase("/var/lib/rancher")] + [TestCase("/var/lib/rancher/volumes")] + [TestCase("/var/lib/kubelet")] + [TestCase("/var/lib/docker")] + [TestCase("/some/place/docker/aufs")] + public void should_not_check_diskspace_for_irrelevant_mounts(string path) { - Mocker.GetMock() - .SetupGet(v => v.DownloadedAlbumsFolder) - .Returns(_droneFactoryFolder); - - GivenExistingFolder(_droneFactoryFolder); - - var freeSpace = Subject.GetFreeSpace(); - - freeSpace.Should().NotBeEmpty(); - } + var mount = new Mock(); + mount.SetupGet(v => v.RootDirectory).Returns(path); + mount.SetupGet(v => v.DriveType).Returns(System.IO.DriveType.Fixed); - [Test] - public void should_not_check_diskspace_for_missing_dronefactory_folder() - { - Mocker.GetMock() - .SetupGet(v => v.DownloadedAlbumsFolder) - .Returns(_droneFactoryFolder); + Mocker.GetMock() + .Setup(v => v.GetMounts()) + .Returns(new List { mount.Object }); var freeSpace = Subject.GetFreeSpace(); freeSpace.Should().BeEmpty(); - - Mocker.GetMock() - .Verify(v => v.GetAvailableSpace(It.IsAny()), Times.Never()); } } } diff --git a/src/NzbDrone.Core.Test/Download/CompletedDownloadServiceFixture.cs b/src/NzbDrone.Core.Test/Download/CompletedDownloadServiceFixture.cs index 164120012..b2fbc558f 100644 --- a/src/NzbDrone.Core.Test/Download/CompletedDownloadServiceFixture.cs +++ b/src/NzbDrone.Core.Test/Download/CompletedDownloadServiceFixture.cs @@ -15,7 +15,6 @@ using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Parser; using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Test.Framework; -using NzbDrone.Core.Tv; using NzbDrone.Core.Music; using NzbDrone.Test.Common; @@ -35,12 +34,12 @@ namespace NzbDrone.Core.Test.Download .With(h => h.Title = "Drone.S01E01.HDTV") .Build(); - var remoteEpisode = BuildRemoteEpisode(); + var remoteAlbum = BuildRemoteAlbum(); _trackedDownload = Builder.CreateNew() .With(c => c.State = TrackedDownloadStage.Downloading) .With(c => c.DownloadItem = completed) - .With(c => c.RemoteEpisode = remoteEpisode) + .With(c => c.RemoteAlbum = remoteAlbum) .Build(); @@ -57,17 +56,17 @@ namespace NzbDrone.Core.Test.Download .Returns(new History.History()); Mocker.GetMock() - .Setup(s => s.GetSeries("Drone.S01E01.HDTV")) - .Returns(remoteEpisode.Series); + .Setup(s => s.GetArtist("Drone.S01E01.HDTV")) + .Returns(remoteAlbum.Artist); } - private RemoteEpisode BuildRemoteEpisode() + private RemoteAlbum BuildRemoteAlbum() { - return new RemoteEpisode + return new RemoteAlbum { - Series = new Series(), - Episodes = new List { new Episode { Id = 1 } } + Artist = new Artist(), + Albums = new List { new Album { Id = 1 } } }; } @@ -81,8 +80,8 @@ namespace NzbDrone.Core.Test.Download private void GivenSuccessfulImport() { - Mocker.GetMock() - .Setup(v => v.ProcessPath(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + Mocker.GetMock() + .Setup(v => v.ProcessPath(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .Returns(new List { new ImportResult(new ImportDecision(new LocalTrack() { Path = @"C:\TestPath\Droned.S01E01.mkv" })) @@ -99,19 +98,19 @@ namespace NzbDrone.Core.Test.Download .Returns(new History.History() { SourceTitle = "Droned S01E01" }); Mocker.GetMock() - .Setup(s => s.GetSeries(It.IsAny())) - .Returns((Series)null); + .Setup(s => s.GetArtist(It.IsAny())) + .Returns((Artist)null); Mocker.GetMock() - .Setup(s => s.GetSeries("Droned S01E01")) - .Returns(BuildRemoteEpisode().Series); + .Setup(s => s.GetArtist("Droned S01E01")) + .Returns(BuildRemoteAlbum().Artist); } - private void GivenSeriesMatch() + private void GivenArtistMatch() { Mocker.GetMock() - .Setup(s => s.GetSeries(It.IsAny())) - .Returns(_trackedDownload.RemoteEpisode.Series); + .Setup(s => s.GetArtist(It.IsAny())) + .Returns(_trackedDownload.RemoteAlbum.Artist); } [TestCase(DownloadItemStatus.Downloading)] @@ -144,7 +143,7 @@ namespace NzbDrone.Core.Test.Download { _trackedDownload.DownloadItem.Category = "tv"; GivenNoGrabbedHistory(); - GivenSeriesMatch(); + GivenArtistMatch(); GivenSuccessfulImport(); Subject.Process(_trackedDownload); @@ -152,20 +151,6 @@ namespace NzbDrone.Core.Test.Download AssertCompletedDownload(); } - [Test] - public void should_not_process_if_storage_directory_in_drone_factory() - { - Mocker.GetMock() - .SetupGet(v => v.DownloadedAlbumsFolder) - .Returns(@"C:\DropFolder".AsOsAgnostic()); - - _trackedDownload.DownloadItem.OutputPath = new OsPath(@"C:\DropFolder\SomeOtherFolder".AsOsAgnostic()); - - Subject.Process(_trackedDownload); - - AssertNoAttemptedImport(); - } - [Test] public void should_not_process_if_output_path_is_empty() { @@ -179,8 +164,8 @@ namespace NzbDrone.Core.Test.Download [Test] public void should_mark_as_imported_if_all_episodes_were_imported() { - Mocker.GetMock() - .Setup(v => v.ProcessPath(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + Mocker.GetMock() + .Setup(v => v.ProcessPath(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .Returns(new List { new ImportResult( @@ -200,8 +185,8 @@ namespace NzbDrone.Core.Test.Download [Test] public void should_not_mark_as_imported_if_all_files_were_rejected() { - Mocker.GetMock() - .Setup(v => v.ProcessPath(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + Mocker.GetMock() + .Setup(v => v.ProcessPath(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .Returns(new List { new ImportResult( @@ -224,8 +209,8 @@ namespace NzbDrone.Core.Test.Download [Test] public void should_not_mark_as_imported_if_no_episodes_were_parsed() { - Mocker.GetMock() - .Setup(v => v.ProcessPath(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + Mocker.GetMock() + .Setup(v => v.ProcessPath(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .Returns(new List { new ImportResult( @@ -237,7 +222,7 @@ namespace NzbDrone.Core.Test.Download new LocalTrack {Path = @"C:\TestPath\Droned.S01E02.mkv"},new Rejection("Rejected!")), "Test Failure") }); - _trackedDownload.RemoteEpisode.Episodes.Clear(); + _trackedDownload.RemoteAlbum.Albums.Clear(); Subject.Process(_trackedDownload); @@ -247,8 +232,8 @@ namespace NzbDrone.Core.Test.Download [Test] public void should_not_mark_as_imported_if_all_files_were_skipped() { - Mocker.GetMock() - .Setup(v => v.ProcessPath(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + Mocker.GetMock() + .Setup(v => v.ProcessPath(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .Returns(new List { new ImportResult(new ImportDecision(new LocalTrack {Path = @"C:\TestPath\Droned.S01E01.mkv"}),"Test Failure"), @@ -264,15 +249,15 @@ namespace NzbDrone.Core.Test.Download [Test] public void should_mark_as_imported_if_all_episodes_were_imported_but_extra_files_were_not() { - GivenSeriesMatch(); + GivenArtistMatch(); - _trackedDownload.RemoteEpisode.Episodes = new List + _trackedDownload.RemoteAlbum.Albums = new List { - new Episode() + new Album() }; - Mocker.GetMock() - .Setup(v => v.ProcessPath(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + Mocker.GetMock() + .Setup(v => v.ProcessPath(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .Returns(new List { new ImportResult(new ImportDecision(new LocalTrack {Path = @"C:\TestPath\Droned.S01E01.mkv"})), @@ -287,15 +272,15 @@ namespace NzbDrone.Core.Test.Download [Test] public void should_mark_as_failed_if_some_of_episodes_were_not_imported() { - _trackedDownload.RemoteEpisode.Episodes = new List + _trackedDownload.RemoteAlbum.Albums = new List { - new Episode(), - new Episode(), - new Episode() + new Album(), + new Album(), + new Album() }; - Mocker.GetMock() - .Setup(v => v.ProcessPath(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + Mocker.GetMock() + .Setup(v => v.ProcessPath(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .Returns(new List { new ImportResult(new ImportDecision(new LocalTrack {Path = @"C:\TestPath\Droned.S01E01.mkv"})), @@ -314,16 +299,16 @@ namespace NzbDrone.Core.Test.Download { GivenABadlyNamedDownload(); - Mocker.GetMock() - .Setup(v => v.ProcessPath(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + Mocker.GetMock() + .Setup(v => v.ProcessPath(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .Returns(new List { new ImportResult(new ImportDecision(new LocalTrack {Path = @"C:\TestPath\Droned.S01E01.mkv"})) }); - Mocker.GetMock() - .Setup(v => v.GetSeries(It.IsAny())) - .Returns(BuildRemoteEpisode().Series); + Mocker.GetMock() + .Setup(v => v.GetArtist(It.IsAny())) + .Returns(BuildRemoteAlbum().Artist); Subject.Process(_trackedDownload); @@ -335,8 +320,8 @@ namespace NzbDrone.Core.Test.Download { GivenABadlyNamedDownload(); - Mocker.GetMock() - .Setup(v => v.ProcessPath(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + Mocker.GetMock() + .Setup(v => v.ProcessPath(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .Returns(new List { new ImportResult(new ImportDecision(new LocalTrack {Path = @"C:\TestPath\Droned.S01E01.mkv"})) @@ -354,8 +339,8 @@ namespace NzbDrone.Core.Test.Download public void should_not_import_when_there_is_a_title_mismatch() { Mocker.GetMock() - .Setup(s => s.GetSeries("Drone.S01E01.HDTV")) - .Returns((Series)null); + .Setup(s => s.GetArtist("Drone.S01E01.HDTV")) + .Returns((Artist)null); Subject.Process(_trackedDownload); @@ -365,13 +350,13 @@ namespace NzbDrone.Core.Test.Download [Test] public void should_mark_as_import_title_mismatch_if_ignore_warnings_is_true() { - _trackedDownload.RemoteEpisode.Episodes = new List + _trackedDownload.RemoteAlbum.Albums = new List { - new Episode() + new Album() }; - Mocker.GetMock() - .Setup(v => v.ProcessPath(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + Mocker.GetMock() + .Setup(v => v.ProcessPath(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .Returns(new List { new ImportResult(new ImportDecision(new LocalTrack {Path = @"C:\TestPath\Droned.S01E01.mkv"})) @@ -408,8 +393,8 @@ namespace NzbDrone.Core.Test.Download private void AssertNoAttemptedImport() { - Mocker.GetMock() - .Verify(v => v.ProcessPath(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Never()); + Mocker.GetMock() + .Verify(v => v.ProcessPath(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Never()); AssertNoCompletedDownload(); } @@ -424,8 +409,8 @@ namespace NzbDrone.Core.Test.Download private void AssertCompletedDownload() { - Mocker.GetMock() - .Verify(v => v.ProcessPath(_trackedDownload.DownloadItem.OutputPath.FullPath, ImportMode.Auto, _trackedDownload.RemoteEpisode.Series, _trackedDownload.DownloadItem), Times.Once()); + Mocker.GetMock() + .Verify(v => v.ProcessPath(_trackedDownload.DownloadItem.OutputPath.FullPath, ImportMode.Auto, _trackedDownload.RemoteAlbum.Artist, _trackedDownload.DownloadItem), Times.Once()); Mocker.GetMock() .Verify(v => v.PublishEvent(It.IsAny()), Times.Once()); diff --git a/src/NzbDrone.Core.Test/Download/DownloadApprovedReportsTests/DownloadApprovedFixture.cs b/src/NzbDrone.Core.Test/Download/DownloadApprovedReportsTests/DownloadApprovedFixture.cs index 07ffe6f95..cac1446ad 100644 --- a/src/NzbDrone.Core.Test/Download/DownloadApprovedReportsTests/DownloadApprovedFixture.cs +++ b/src/NzbDrone.Core.Test/Download/DownloadApprovedReportsTests/DownloadApprovedFixture.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using FizzWare.NBuilder; using FluentAssertions; @@ -6,9 +6,11 @@ using Moq; using NUnit.Framework; using NzbDrone.Core.DecisionEngine; using NzbDrone.Core.Download; +using NzbDrone.Core.Download.Clients; using NzbDrone.Core.Download.Pending; +using NzbDrone.Core.Indexers; using NzbDrone.Core.Parser.Model; -using NzbDrone.Core.Profiles; +using NzbDrone.Core.Profiles.Qualities; using NzbDrone.Core.Qualities; using NzbDrone.Core.Test.Framework; using NzbDrone.Core.Music; @@ -34,7 +36,7 @@ namespace NzbDrone.Core.Test.Download.DownloadApprovedReportsTests .Build(); } - private RemoteAlbum GetRemoteAlbum(List albums, QualityModel quality) + private RemoteAlbum GetRemoteAlbum(List albums, QualityModel quality, DownloadProtocol downloadProtocol = DownloadProtocol.Usenet) { var remoteAlbum = new RemoteAlbum(); remoteAlbum.ParsedAlbumInfo = new ParsedAlbumInfo(); @@ -44,6 +46,7 @@ namespace NzbDrone.Core.Test.Download.DownloadApprovedReportsTests remoteAlbum.Albums.AddRange(albums); remoteAlbum.Release = new ReleaseInfo(); + remoteAlbum.Release.DownloadProtocol = downloadProtocol; remoteAlbum.Release.PublishDate = DateTime.UtcNow; remoteAlbum.Artist = Builder.CreateNew() @@ -191,7 +194,6 @@ namespace NzbDrone.Core.Test.Download.DownloadApprovedReportsTests var decisions = new List(); decisions.Add(new DownloadDecision(remoteAlbum, new Rejection("Failure!", RejectionType.Temporary))); - decisions.Add(new DownloadDecision(remoteAlbum)); Subject.ProcessDecisions(decisions); Mocker.GetMock().Verify(v => v.DownloadReport(It.IsAny()), Times.Never()); @@ -208,7 +210,7 @@ namespace NzbDrone.Core.Test.Download.DownloadApprovedReportsTests decisions.Add(new DownloadDecision(remoteAlbum, new Rejection("Failure!", RejectionType.Temporary))); Subject.ProcessDecisions(decisions); - Mocker.GetMock().Verify(v => v.Add(It.IsAny()), Times.Never()); + Mocker.GetMock().Verify(v => v.Add(It.IsAny(), It.IsAny()), Times.Never()); } [Test] @@ -222,7 +224,43 @@ namespace NzbDrone.Core.Test.Download.DownloadApprovedReportsTests decisions.Add(new DownloadDecision(remoteAlbum, new Rejection("Failure!", RejectionType.Temporary))); Subject.ProcessDecisions(decisions); - Mocker.GetMock().Verify(v => v.Add(It.IsAny()), Times.Exactly(2)); + Mocker.GetMock().Verify(v => v.Add(It.IsAny(), It.IsAny()), Times.Exactly(2)); + } + + [Test] + public void should_add_to_failed_if_already_failed_for_that_protocol() + { + var albums = new List { GetAlbum(1) }; + var remoteAlbum = GetRemoteAlbum(albums, new QualityModel(Quality.MP3_320)); + + var decisions = new List(); + decisions.Add(new DownloadDecision(remoteAlbum)); + decisions.Add(new DownloadDecision(remoteAlbum)); + + Mocker.GetMock().Setup(s => s.DownloadReport(It.IsAny())) + .Throws(new DownloadClientUnavailableException("Download client failed")); + + Subject.ProcessDecisions(decisions); + Mocker.GetMock().Verify(v => v.DownloadReport(It.IsAny()), Times.Once()); + } + + [Test] + public void should_not_add_to_failed_if_failed_for_a_different_protocol() + { + var episodes = new List { GetAlbum(1) }; + var remoteEpisode = GetRemoteAlbum(episodes, new QualityModel(Quality.MP3_320), DownloadProtocol.Usenet); + var remoteEpisode2 = GetRemoteAlbum(episodes, new QualityModel(Quality.MP3_320), DownloadProtocol.Torrent); + + var decisions = new List(); + decisions.Add(new DownloadDecision(remoteEpisode)); + decisions.Add(new DownloadDecision(remoteEpisode2)); + + Mocker.GetMock().Setup(s => s.DownloadReport(It.Is(r => r.Release.DownloadProtocol == DownloadProtocol.Usenet))) + .Throws(new DownloadClientUnavailableException("Download client failed")); + + Subject.ProcessDecisions(decisions); + Mocker.GetMock().Verify(v => v.DownloadReport(It.Is(r => r.Release.DownloadProtocol == DownloadProtocol.Usenet)), Times.Once()); + Mocker.GetMock().Verify(v => v.DownloadReport(It.Is(r => r.Release.DownloadProtocol == DownloadProtocol.Torrent)), Times.Once()); } } } diff --git a/src/NzbDrone.Core.Test/Download/DownloadClientStatusServiceFixture.cs b/src/NzbDrone.Core.Test/Download/DownloadClientStatusServiceFixture.cs new file mode 100644 index 000000000..c54698f2e --- /dev/null +++ b/src/NzbDrone.Core.Test/Download/DownloadClientStatusServiceFixture.cs @@ -0,0 +1,156 @@ +using System; +using System.Linq; +using FluentAssertions; +using Moq; +using NUnit.Framework; +using NzbDrone.Core.Download; +using NzbDrone.Core.Test.Framework; + +namespace NzbDrone.Core.Test.Download +{ + public class DownloadClientStatusServiceFixture : CoreTest + { + private DateTime _epoch; + + [SetUp] + public void SetUp() + { + _epoch = DateTime.UtcNow; + } + + private DownloadClientStatus WithStatus(DownloadClientStatus status) + { + Mocker.GetMock() + .Setup(v => v.FindByProviderId(1)) + .Returns(status); + + Mocker.GetMock() + .Setup(v => v.All()) + .Returns(new[] { status }); + + return status; + } + + private void VerifyUpdate() + { + Mocker.GetMock() + .Verify(v => v.Upsert(It.IsAny()), Times.Once()); + } + + private void VerifyNoUpdate() + { + Mocker.GetMock() + .Verify(v => v.Upsert(It.IsAny()), Times.Never()); + } + + [Test] + public void should_not_consider_blocked_within_5_minutes_since_initial_failure() + { + WithStatus(new DownloadClientStatus + { + InitialFailure = _epoch - TimeSpan.FromMinutes(4), + MostRecentFailure = _epoch - TimeSpan.FromSeconds(4), + EscalationLevel = 3 + }); + + Subject.RecordFailure(1); + + VerifyUpdate(); + + var status = Subject.GetBlockedProviders().FirstOrDefault(); + status.Should().BeNull(); + } + + [Test] + public void should_consider_blocked_after_5_minutes_since_initial_failure() + { + WithStatus(new DownloadClientStatus + { + InitialFailure = _epoch - TimeSpan.FromMinutes(6), + MostRecentFailure = _epoch - TimeSpan.FromSeconds(120), + EscalationLevel = 3 + }); + + Subject.RecordFailure(1); + + VerifyUpdate(); + + var status = Subject.GetBlockedProviders().FirstOrDefault(); + status.Should().NotBeNull(); + } + + [Test] + public void should_not_escalate_further_till_after_5_minutes_since_initial_failure() + { + var origStatus = WithStatus(new DownloadClientStatus + { + InitialFailure = _epoch - TimeSpan.FromMinutes(4), + MostRecentFailure = _epoch - TimeSpan.FromSeconds(4), + EscalationLevel = 3 + }); + + Subject.RecordFailure(1); + Subject.RecordFailure(1); + Subject.RecordFailure(1); + Subject.RecordFailure(1); + Subject.RecordFailure(1); + Subject.RecordFailure(1); + Subject.RecordFailure(1); + + var status = Subject.GetBlockedProviders().FirstOrDefault(); + status.Should().BeNull(); + + origStatus.EscalationLevel.Should().Be(3); + } + + [Test] + public void should_escalate_further_after_5_minutes_since_initial_failure() + { + WithStatus(new DownloadClientStatus + { + InitialFailure = _epoch - TimeSpan.FromMinutes(6), + MostRecentFailure = _epoch - TimeSpan.FromSeconds(120), + EscalationLevel = 3 + }); + + Subject.RecordFailure(1); + Subject.RecordFailure(1); + Subject.RecordFailure(1); + Subject.RecordFailure(1); + Subject.RecordFailure(1); + Subject.RecordFailure(1); + Subject.RecordFailure(1); + + var status = Subject.GetBlockedProviders().FirstOrDefault(); + status.Should().NotBeNull(); + + status.EscalationLevel.Should().BeGreaterThan(3); + } + + [Test] + public void should_not_escalate_beyond_3_hours() + { + WithStatus(new DownloadClientStatus + { + InitialFailure = _epoch - TimeSpan.FromMinutes(6), + MostRecentFailure = _epoch - TimeSpan.FromSeconds(120), + EscalationLevel = 3 + }); + + Subject.RecordFailure(1); + Subject.RecordFailure(1); + Subject.RecordFailure(1); + Subject.RecordFailure(1); + Subject.RecordFailure(1); + Subject.RecordFailure(1); + Subject.RecordFailure(1); + Subject.RecordFailure(1); + Subject.RecordFailure(1); + + var status = Subject.GetBlockedProviders().FirstOrDefault(); + status.Should().NotBeNull(); + status.DisabledTill.Should().HaveValue(); + status.DisabledTill.Should().NotBeAfter(_epoch + TimeSpan.FromHours(3.1)); + } + } +} diff --git a/src/NzbDrone.Core.Test/Download/DownloadClientTests/Blackhole/ScanWatchFolderFixture.cs b/src/NzbDrone.Core.Test/Download/DownloadClientTests/Blackhole/ScanWatchFolderFixture.cs index 40aada01a..a63bfba6a 100644 --- a/src/NzbDrone.Core.Test/Download/DownloadClientTests/Blackhole/ScanWatchFolderFixture.cs +++ b/src/NzbDrone.Core.Test/Download/DownloadClientTests/Blackhole/ScanWatchFolderFixture.cs @@ -1,15 +1,17 @@ -using System; +using System; +using System.Collections.Generic; using System.IO; using System.Linq; +using System.Threading; using FluentAssertions; using Moq; using NUnit.Framework; using NzbDrone.Common.Disk; using NzbDrone.Core.Download; -using NzbDrone.Test.Common; -using System.Threading; -using NzbDrone.Core.Test.Framework; using NzbDrone.Core.Download.Clients.Blackhole; +using NzbDrone.Core.MediaFiles; +using NzbDrone.Core.Test.Framework; +using NzbDrone.Test.Common; namespace NzbDrone.Core.Test.Download.DownloadClientTests.Blackhole { @@ -33,6 +35,9 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.Blackhole Mocker.GetMock() .Setup(c => c.GetFileSize(It.IsAny())) .Returns(1000000); + + Mocker.GetMock().Setup(c => c.FilterFiles(It.IsAny(), It.IsAny>())) + .Returns>((b, s) => s.ToList()); } protected void GivenChangedItem() diff --git a/src/NzbDrone.Core.Test/Download/DownloadClientTests/Blackhole/TorrentBlackholeFixture.cs b/src/NzbDrone.Core.Test/Download/DownloadClientTests/Blackhole/TorrentBlackholeFixture.cs index fafb5f30d..d43459362 100644 --- a/src/NzbDrone.Core.Test/Download/DownloadClientTests/Blackhole/TorrentBlackholeFixture.cs +++ b/src/NzbDrone.Core.Test/Download/DownloadClientTests/Blackhole/TorrentBlackholeFixture.cs @@ -1,4 +1,5 @@ -using System; +using System; +using System.Collections.Generic; using System.IO; using System.Linq; using System.Net; @@ -10,6 +11,7 @@ using NzbDrone.Common.Http; using NzbDrone.Core.Download; using NzbDrone.Core.Download.Clients.Blackhole; using NzbDrone.Core.Exceptions; +using NzbDrone.Core.MediaFiles; using NzbDrone.Core.MediaFiles.TorrentInfo; using NzbDrone.Core.Parser.Model; using NzbDrone.Test.Common; @@ -48,6 +50,9 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.Blackhole Mocker.GetMock() .Setup(c => c.GetHashFromTorrentFile(It.IsAny())) .Returns("myhash"); + + Mocker.GetMock().Setup(c => c.FilterFiles(It.IsAny(), It.IsAny>())) + .Returns>((b, s) => s.ToList()); } protected void GivenFailedDownload() @@ -99,6 +104,9 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.Blackhole var result = Subject.GetItems().Single(); VerifyCompleted(result); + + result.CanBeRemoved.Should().BeFalse(); + result.CanMoveFiles.Should().BeFalse(); } [Test] diff --git a/src/NzbDrone.Core.Test/Download/DownloadClientTests/Blackhole/UsenetBlackholeFixture.cs b/src/NzbDrone.Core.Test/Download/DownloadClientTests/Blackhole/UsenetBlackholeFixture.cs index 3927a60a1..938643ad7 100644 --- a/src/NzbDrone.Core.Test/Download/DownloadClientTests/Blackhole/UsenetBlackholeFixture.cs +++ b/src/NzbDrone.Core.Test/Download/DownloadClientTests/Blackhole/UsenetBlackholeFixture.cs @@ -1,5 +1,6 @@ - + using System; +using System.Collections.Generic; using System.IO; using System.Linq; using System.Net; @@ -10,6 +11,7 @@ using NzbDrone.Common.Disk; using NzbDrone.Common.Http; using NzbDrone.Core.Download; using NzbDrone.Core.Download.Clients.Blackhole; +using NzbDrone.Core.MediaFiles; using NzbDrone.Test.Common; namespace NzbDrone.Core.Test.Download.DownloadClientTests.Blackhole @@ -41,6 +43,9 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.Blackhole Mocker.GetMock() .Setup(c => c.OpenWriteStream(It.IsAny())) .Returns(() => new FileStream(GetTempFilePath(), FileMode.Create)); + + Mocker.GetMock().Setup(c => c.FilterFiles(It.IsAny(), It.IsAny>())) + .Returns>((b, s) => s.ToList()); } protected void GivenFailedDownload() @@ -77,6 +82,9 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.Blackhole var result = Subject.GetItems().Single(); VerifyCompleted(result); + + result.CanBeRemoved.Should().BeTrue(); + result.CanMoveFiles.Should().BeTrue(); } [Test] diff --git a/src/NzbDrone.Core.Test/Download/DownloadClientTests/DelugeTests/DelugeFixture.cs b/src/NzbDrone.Core.Test/Download/DownloadClientTests/DelugeTests/DelugeFixture.cs index 6d7cfb0c0..49d157a45 100644 --- a/src/NzbDrone.Core.Test/Download/DownloadClientTests/DelugeTests/DelugeFixture.cs +++ b/src/NzbDrone.Core.Test/Download/DownloadClientTests/DelugeTests/DelugeFixture.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Linq; using System.Collections.Generic; using FluentAssertions; @@ -19,6 +19,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.DelugeTests protected DelugeTorrent _downloading; protected DelugeTorrent _failed; protected DelugeTorrent _completed; + protected DelugeTorrent _seeding; [SetUp] public void Setup() @@ -75,8 +76,12 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.DelugeTests Size = 1000, BytesDownloaded = 1000, Progress = 100.0, - DownloadPath = "somepath" - }; + DownloadPath = "somepath", + IsAutoManaged = true, + StopAtRatio = true, + StopRatio = 1.0, + Ratio = 1.5 + }; Mocker.GetMock() .Setup(s => s.GetHashFromTorrentFile(It.IsAny())) @@ -189,6 +194,9 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.DelugeTests PrepareClientToReturnCompletedItem(); var item = Subject.GetItems().Single(); VerifyCompleted(item); + + item.CanBeRemoved.Should().BeTrue(); + item.CanMoveFiles.Should().BeTrue(); } [Test] @@ -248,11 +256,11 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.DelugeTests item.Status.Should().Be(expectedItemStatus); } - [TestCase(DelugeTorrentStatus.Paused, DownloadItemStatus.Completed, true)] - [TestCase(DelugeTorrentStatus.Checking, DownloadItemStatus.Downloading, true)] - [TestCase(DelugeTorrentStatus.Queued, DownloadItemStatus.Completed, true)] - [TestCase(DelugeTorrentStatus.Seeding, DownloadItemStatus.Completed, true)] - public void GetItems_should_return_completed_item_as_downloadItemStatus(string apiStatus, DownloadItemStatus expectedItemStatus, bool expectedReadOnly) + [TestCase(DelugeTorrentStatus.Paused, DownloadItemStatus.Completed)] + [TestCase(DelugeTorrentStatus.Checking, DownloadItemStatus.Downloading)] + [TestCase(DelugeTorrentStatus.Queued, DownloadItemStatus.Completed)] + [TestCase(DelugeTorrentStatus.Seeding, DownloadItemStatus.Completed)] + public void GetItems_should_return_completed_item_as_downloadItemStatus(string apiStatus, DownloadItemStatus expectedItemStatus) { _completed.State = apiStatus; @@ -261,24 +269,25 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.DelugeTests var item = Subject.GetItems().Single(); item.Status.Should().Be(expectedItemStatus); - item.IsReadOnly.Should().Be(expectedReadOnly); } - [Test] - public void GetItems_should_check_share_ratio_for_readonly() + [TestCase(0.5, false)] + [TestCase(1.01, true)] + public void GetItems_should_check_share_ratio_for_moveFiles_and_remove(double ratio, bool canBeRemoved) { _completed.State = DelugeTorrentStatus.Paused; _completed.IsAutoManaged = true; _completed.StopAtRatio = true; _completed.StopRatio = 1.0; - _completed.Ratio = 1.01; + _completed.Ratio = ratio; PrepareClientToReturnCompletedItem(); var item = Subject.GetItems().Single(); item.Status.Should().Be(DownloadItemStatus.Completed); - item.IsReadOnly.Should().BeFalse(); + item.CanMoveFiles.Should().Be(canBeRemoved); + item.CanBeRemoved.Should().Be(canBeRemoved); } [Test] diff --git a/src/NzbDrone.Core.Test/Download/DownloadClientTests/DownloadClientFixtureBase.cs b/src/NzbDrone.Core.Test/Download/DownloadClientTests/DownloadClientFixtureBase.cs index f22b7f77e..1f0dfd4c0 100644 --- a/src/NzbDrone.Core.Test/Download/DownloadClientTests/DownloadClientFixtureBase.cs +++ b/src/NzbDrone.Core.Test/Download/DownloadClientTests/DownloadClientFixtureBase.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using Moq; using NUnit.Framework; @@ -8,7 +8,6 @@ using NzbDrone.Core.IndexerSearch.Definitions; using NzbDrone.Core.Test.Framework; using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Parser; -using NzbDrone.Core.Tv; using NzbDrone.Core.Music; using NzbDrone.Core.Download; using NzbDrone.Core.Configuration; diff --git a/src/NzbDrone.Core.Test/Download/DownloadClientTests/DownloadStationTests/TorrentDownloadStationFixture.cs b/src/NzbDrone.Core.Test/Download/DownloadClientTests/DownloadStationTests/TorrentDownloadStationFixture.cs index b3a6f121b..5ec0739a1 100644 --- a/src/NzbDrone.Core.Test/Download/DownloadClientTests/DownloadStationTests/TorrentDownloadStationFixture.cs +++ b/src/NzbDrone.Core.Test/Download/DownloadClientTests/DownloadStationTests/TorrentDownloadStationFixture.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using FluentAssertions; @@ -576,11 +576,11 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.DownloadStationTests items.Should().OnlyContain(v => !v.OutputPath.IsEmpty); } - [TestCase(DownloadStationTaskStatus.Downloading, DownloadItemStatus.Downloading, true)] - [TestCase(DownloadStationTaskStatus.Finished, DownloadItemStatus.Completed, false)] - [TestCase(DownloadStationTaskStatus.Seeding, DownloadItemStatus.Completed, true)] - [TestCase(DownloadStationTaskStatus.Waiting, DownloadItemStatus.Queued, true)] - public void GetItems_should_return_readonly_expected(DownloadStationTaskStatus apiStatus, DownloadItemStatus expectedItemStatus, bool readOnlyExpected) + [TestCase(DownloadStationTaskStatus.Downloading, false, false)] + [TestCase(DownloadStationTaskStatus.Finished, true, true)] + [TestCase(DownloadStationTaskStatus.Seeding, true, false)] + [TestCase(DownloadStationTaskStatus.Waiting, false, false)] + public void GetItems_should_return_canBeMoved_and_canBeDeleted_as_expected(DownloadStationTaskStatus apiStatus, bool canMoveFilesExpected, bool canBeRemovedExpected) { GivenSerialNumber(); GivenSharedFolder(); @@ -592,7 +592,10 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.DownloadStationTests var items = Subject.GetItems(); items.Should().HaveCount(1); - items.First().IsReadOnly.Should().Be(readOnlyExpected); + var item = items.First(); + + item.CanBeRemoved.Should().Be(canBeRemovedExpected); + item.CanMoveFiles.Should().Be(canMoveFilesExpected); } [TestCase(DownloadStationTaskStatus.Downloading, DownloadItemStatus.Downloading)] diff --git a/src/NzbDrone.Core.Test/Download/DownloadClientTests/DownloadStationTests/UsenetDownloadStationFixture.cs b/src/NzbDrone.Core.Test/Download/DownloadClientTests/DownloadStationTests/UsenetDownloadStationFixture.cs index d0f73378e..abf497b49 100644 --- a/src/NzbDrone.Core.Test/Download/DownloadClientTests/DownloadStationTests/UsenetDownloadStationFixture.cs +++ b/src/NzbDrone.Core.Test/Download/DownloadClientTests/DownloadStationTests/UsenetDownloadStationFixture.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using FluentAssertions; @@ -408,24 +408,6 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.DownloadStationTests items.Should().OnlyContain(v => !v.OutputPath.IsEmpty); } - [TestCase(DownloadStationTaskStatus.Downloading, DownloadItemStatus.Downloading, true)] - [TestCase(DownloadStationTaskStatus.Finished, DownloadItemStatus.Completed, false)] - [TestCase(DownloadStationTaskStatus.Waiting, DownloadItemStatus.Queued, true)] - public void GetItems_should_return_readonly_expected(DownloadStationTaskStatus apiStatus, DownloadItemStatus expectedItemStatus, bool readOnlyExpected) - { - GivenSerialNumber(); - GivenSharedFolder(); - - _queued.Status = apiStatus; - - GivenTasks(new List() { _queued }); - - var items = Subject.GetItems(); - - items.Should().HaveCount(1); - items.First().IsReadOnly.Should().Be(readOnlyExpected); - } - [TestCase(DownloadStationTaskStatus.Downloading, DownloadItemStatus.Downloading)] [TestCase(DownloadStationTaskStatus.Error, DownloadItemStatus.Failed)] [TestCase(DownloadStationTaskStatus.Extracting, DownloadItemStatus.Downloading)] diff --git a/src/NzbDrone.Core.Test/Download/DownloadClientTests/HadoukenTests/HadoukenFixture.cs b/src/NzbDrone.Core.Test/Download/DownloadClientTests/HadoukenTests/HadoukenFixture.cs index 42a6e25fd..648244472 100644 --- a/src/NzbDrone.Core.Test/Download/DownloadClientTests/HadoukenTests/HadoukenFixture.cs +++ b/src/NzbDrone.Core.Test/Download/DownloadClientTests/HadoukenTests/HadoukenFixture.cs @@ -1,4 +1,4 @@ -using Moq; +using Moq; using NUnit.Framework; using NzbDrone.Common.Http; using NzbDrone.Core.Download; @@ -190,6 +190,9 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.HadoukenTests PrepareClientToReturnCompletedItem(); var item = Subject.GetItems().Single(); VerifyCompleted(item); + + item.CanBeRemoved.Should().BeTrue(); + item.CanMoveFiles.Should().BeTrue(); } [Test] diff --git a/src/NzbDrone.Core.Test/Download/DownloadClientTests/NzbVortexTests/NzbVortexFixture.cs b/src/NzbDrone.Core.Test/Download/DownloadClientTests/NzbVortexTests/NzbVortexFixture.cs index 501dfb2e6..c33bd564a 100644 --- a/src/NzbDrone.Core.Test/Download/DownloadClientTests/NzbVortexTests/NzbVortexFixture.cs +++ b/src/NzbDrone.Core.Test/Download/DownloadClientTests/NzbVortexTests/NzbVortexFixture.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Linq; using System.Collections.Generic; using FluentAssertions; @@ -107,6 +107,9 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.NzbVortexTests var result = Subject.GetItems().Single(); VerifyQueued(result); + + result.CanBeRemoved.Should().BeTrue(); + result.CanMoveFiles.Should().BeTrue(); } [Test] @@ -118,6 +121,9 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.NzbVortexTests var result = Subject.GetItems().Single(); VerifyPaused(result); + + result.CanBeRemoved.Should().BeTrue(); + result.CanMoveFiles.Should().BeTrue(); } [Test] @@ -129,6 +135,9 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.NzbVortexTests var result = Subject.GetItems().Single(); VerifyDownloading(result); + + result.CanBeRemoved.Should().BeTrue(); + result.CanMoveFiles.Should().BeTrue(); } [Test] @@ -139,6 +148,9 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.NzbVortexTests var result = Subject.GetItems().Single(); VerifyCompleted(result); + + result.CanBeRemoved.Should().BeTrue(); + result.CanMoveFiles.Should().BeTrue(); } [Test] @@ -149,6 +161,9 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.NzbVortexTests var result = Subject.GetItems().Single(); VerifyFailed(result); + + result.CanBeRemoved.Should().BeTrue(); + result.CanMoveFiles.Should().BeTrue(); } [Test] diff --git a/src/NzbDrone.Core.Test/Download/DownloadClientTests/NzbgetTests/NzbgetFixture.cs b/src/NzbDrone.Core.Test/Download/DownloadClientTests/NzbgetTests/NzbgetFixture.cs index 5ad07cc34..2da8f4d50 100644 --- a/src/NzbDrone.Core.Test/Download/DownloadClientTests/NzbgetTests/NzbgetFixture.cs +++ b/src/NzbDrone.Core.Test/Download/DownloadClientTests/NzbgetTests/NzbgetFixture.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Linq; using System.Collections.Generic; using FluentAssertions; @@ -19,6 +19,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.NzbgetTests private NzbgetQueueItem _queued; private NzbgetHistoryItem _failed; private NzbgetHistoryItem _completed; + private Dictionary _configItems; [SetUp] public void Setup() @@ -80,13 +81,17 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.NzbgetTests DownloadRate = 7000000 }); - var configItems = new Dictionary(); - configItems.Add("Category1.Name", "music"); - configItems.Add("Category1.DestDir", @"/remote/mount/music"); + Mocker.GetMock() + .Setup(v => v.GetVersion(It.IsAny())) + .Returns("14.0"); + + _configItems = new Dictionary(); + _configItems.Add("Category1.Name", "music"); + _configItems.Add("Category1.DestDir", @"/remote/mount/music"); Mocker.GetMock() .Setup(v => v.GetConfig(It.IsAny())) - .Returns(configItems); + .Returns(_configItems); } protected void GivenFailedDownload() @@ -167,6 +172,9 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.NzbgetTests var result = Subject.GetItems().Single(); VerifyQueued(result); + + result.CanBeRemoved.Should().BeTrue(); + result.CanMoveFiles.Should().BeTrue(); } [Test] @@ -180,6 +188,9 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.NzbgetTests var result = Subject.GetItems().Single(); VerifyPaused(result); + + result.CanBeRemoved.Should().BeTrue(); + result.CanMoveFiles.Should().BeTrue(); } [Test] @@ -193,6 +204,25 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.NzbgetTests var result = Subject.GetItems().Single(); VerifyDownloading(result); + + result.CanBeRemoved.Should().BeTrue(); + result.CanMoveFiles.Should().BeTrue(); + } + + [Test] + public void post_processing_item_should_have_required_properties() + { + _queued.ActiveDownloads = 1; + + GivenQueue(_queued); + GivenHistory(null); + + _queued.RemainingSizeLo = 0; + + var result = Subject.GetItems().Single(); + + result.CanBeRemoved.Should().BeTrue(); + result.CanMoveFiles.Should().BeTrue(); } [Test] @@ -204,6 +234,9 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.NzbgetTests var result = Subject.GetItems().Single(); VerifyCompleted(result); + + result.CanBeRemoved.Should().BeTrue(); + result.CanMoveFiles.Should().BeTrue(); } [Test] @@ -386,5 +419,18 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.NzbgetTests error.IsValid.Should().Be(expected); } + + [TestCase("0", false)] + [TestCase("1", true)] + [TestCase(" 7", false)] + [TestCase("5000000", false)] + public void should_test_keephistory(string keephistory, bool expected) + { + _configItems["KeepHistory"] = keephistory; + + var error = Subject.Test(); + + error.IsValid.Should().Be(expected); + } } } diff --git a/src/NzbDrone.Core.Test/Download/DownloadClientTests/PneumaticProviderFixture.cs b/src/NzbDrone.Core.Test/Download/DownloadClientTests/PneumaticProviderFixture.cs index a27cfc6b4..97aacaf56 100644 --- a/src/NzbDrone.Core.Test/Download/DownloadClientTests/PneumaticProviderFixture.cs +++ b/src/NzbDrone.Core.Test/Download/DownloadClientTests/PneumaticProviderFixture.cs @@ -1,10 +1,9 @@ -using System; +using System; using System.IO; using System.Net; using Moq; using NUnit.Framework; using NzbDrone.Common.Http; -using NzbDrone.Core.Configuration; using NzbDrone.Core.Download; using NzbDrone.Core.Download.Clients.Pneumatic; using NzbDrone.Core.Parser.Model; @@ -19,9 +18,9 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests private const string _nzbUrl = "http://www.nzbs.com/url"; private const string _title = "30.Rock.S01E05.hdtv.xvid-LoL"; private string _pneumaticFolder; - private string _sabDrop; + private string _strmFolder; private string _nzbPath; - private RemoteAlbum _remoteEpisode; + private RemoteAlbum _remoteAlbum; [SetUp] public void Setup() @@ -29,21 +28,20 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests _pneumaticFolder = @"d:\nzb\pneumatic\".AsOsAgnostic(); _nzbPath = Path.Combine(_pneumaticFolder, _title + ".nzb").AsOsAgnostic(); - _sabDrop = @"d:\unsorted tv\".AsOsAgnostic(); + _strmFolder = @"d:\unsorted tv\".AsOsAgnostic(); - Mocker.GetMock().SetupGet(c => c.DownloadedAlbumsFolder).Returns(_sabDrop); + _remoteAlbum = new RemoteAlbum(); + _remoteAlbum.Release = new ReleaseInfo(); + _remoteAlbum.Release.Title = _title; + _remoteAlbum.Release.DownloadUrl = _nzbUrl; - _remoteEpisode = new RemoteAlbum(); - _remoteEpisode.Release = new ReleaseInfo(); - _remoteEpisode.Release.Title = _title; - _remoteEpisode.Release.DownloadUrl = _nzbUrl; - - _remoteEpisode.ParsedAlbumInfo = new ParsedAlbumInfo(); + _remoteAlbum.ParsedAlbumInfo = new ParsedAlbumInfo(); Subject.Definition = new DownloadClientDefinition(); Subject.Definition.Settings = new PneumaticSettings { - NzbFolder = _pneumaticFolder + NzbFolder = _pneumaticFolder, + StrmFolder = _strmFolder }; } @@ -55,26 +53,25 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests [Test] public void should_download_file_if_it_doesnt_exist() { - Subject.Download(_remoteEpisode); + Subject.Download(_remoteAlbum); Mocker.GetMock().Verify(c => c.DownloadFile(_nzbUrl, _nzbPath), Times.Once()); } - [Test] public void should_throw_on_failed_download() { WithFailedDownload(); - Assert.Throws(() => Subject.Download(_remoteEpisode)); + Assert.Throws(() => Subject.Download(_remoteAlbum)); } [Test] public void should_throw_if_full_season_download() { - _remoteEpisode.Release.Title = "30 Rock - Season 1"; + _remoteAlbum.Release.Title = "30 Rock - Season 1"; - Assert.Throws(() => Subject.Download(_remoteEpisode)); + Assert.Throws(() => Subject.Download(_remoteAlbum)); } [Test] @@ -88,9 +85,9 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests { var illegalTitle = "Saturday Night Live - S38E08 - Jeremy Renner/Maroon 5 [SDTV]"; var expectedFilename = Path.Combine(_pneumaticFolder, "Saturday Night Live - S38E08 - Jeremy Renner+Maroon 5 [SDTV].nzb"); - _remoteEpisode.Release.Title = illegalTitle; + _remoteAlbum.Release.Title = illegalTitle; - Subject.Download(_remoteEpisode); + Subject.Download(_remoteAlbum); Mocker.GetMock().Verify(c => c.DownloadFile(It.IsAny(), expectedFilename), Times.Once()); } diff --git a/src/NzbDrone.Core.Test/Download/DownloadClientTests/QBittorrentTests/QBittorrentFixture.cs b/src/NzbDrone.Core.Test/Download/DownloadClientTests/QBittorrentTests/QBittorrentFixture.cs index cf1105c3d..0bd729670 100644 --- a/src/NzbDrone.Core.Test/Download/DownloadClientTests/QBittorrentTests/QBittorrentFixture.cs +++ b/src/NzbDrone.Core.Test/Download/DownloadClientTests/QBittorrentTests/QBittorrentFixture.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Linq; using System.Collections.Generic; using FluentAssertions; @@ -89,7 +89,13 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests }); } - protected void GivenMaxRatio(float maxRatio, bool removeOnMaxRatio = true) + protected void GivenHighPriority() + { + Subject.Definition.Settings.As().OlderTvPriority = (int) QBittorrentPriority.First; + Subject.Definition.Settings.As().RecentTvPriority = (int) QBittorrentPriority.First; + } + + protected void GivenMaxRatio(float maxRatio, bool removeOnMaxRatio = true) { Mocker.GetMock() .Setup(s => s.GetConfig(It.IsAny())) @@ -266,6 +272,39 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests } [Test] + public void Download_should_set_top_priority() + { + GivenHighPriority(); + GivenSuccessfulDownload(); + + var remoteAlbum = CreateRemoteAlbum(); + + var id = Subject.Download(remoteAlbum); + + Mocker.GetMock() + .Verify(v => v.MoveTorrentToTopInQueue(It.IsAny(), It.IsAny()), Times.Once()); + } + + [Test] + public void Download_should_not_fail_if_top_priority_not_available() + { + GivenHighPriority(); + GivenSuccessfulDownload(); + + Mocker.GetMock() + .Setup(v => v.MoveTorrentToTopInQueue(It.IsAny(), It.IsAny())) + .Throws(new HttpException(new HttpResponse(new HttpRequest("http://me.local/"), new HttpHeader(), new byte[0], System.Net.HttpStatusCode.Forbidden))); + + var remoteAlbum = CreateRemoteAlbum(); + + var id = Subject.Download(remoteAlbum); + + id.Should().NotBeNullOrEmpty(); + + ExceptionVerification.ExpectedWarns(1); + } + +[Test] public void should_return_status_with_outputdirs() { var config = new QBittorrentPreferences @@ -311,7 +350,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests } [Test] - public void should_be_read_only_if_max_ratio_not_reached() + public void should_not_be_removable_and_should_not_allow_move_files_if_max_ratio_not_reached() { GivenMaxRatio(1.0f); @@ -330,11 +369,12 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests GivenTorrents(new List { torrent }); var item = Subject.GetItems().Single(); - item.IsReadOnly.Should().BeTrue(); + item.CanBeRemoved.Should().BeFalse(); + item.CanMoveFiles.Should().BeFalse(); } [Test] - public void should_be_read_only_if_max_ratio_reached_and_not_paused() + public void should_not_be_removable_and_should_not_allow_move_files_if_max_ratio_reached_and_not_paused() { GivenMaxRatio(1.0f); @@ -353,11 +393,12 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests GivenTorrents(new List { torrent }); var item = Subject.GetItems().Single(); - item.IsReadOnly.Should().BeTrue(); + item.CanBeRemoved.Should().BeFalse(); + item.CanMoveFiles.Should().BeFalse(); } [Test] - public void should_be_read_only_if_max_ratio_is_not_set() + public void should_not_be_removable_and_should_not_allow_move_files_if_max_ratio_is_not_set() { GivenMaxRatio(1.0f, false); @@ -376,11 +417,12 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests GivenTorrents(new List { torrent }); var item = Subject.GetItems().Single(); - item.IsReadOnly.Should().BeTrue(); + item.CanBeRemoved.Should().BeFalse(); + item.CanMoveFiles.Should().BeFalse(); } [Test] - public void should_not_be_read_only_if_max_ratio_reached_and_paused() + public void should_be_removable_and_should_allow_move_files_if_max_ratio_reached_and_paused() { GivenMaxRatio(1.0f); @@ -399,7 +441,8 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests GivenTorrents(new List { torrent }); var item = Subject.GetItems().Single(); - item.IsReadOnly.Should().BeFalse(); + item.CanBeRemoved.Should().BeTrue(); + item.CanMoveFiles.Should().BeTrue(); } [Test] diff --git a/src/NzbDrone.Core.Test/Download/DownloadClientTests/SabnzbdTests/SabnzbdFixture.cs b/src/NzbDrone.Core.Test/Download/DownloadClientTests/SabnzbdTests/SabnzbdFixture.cs index 1ce5215aa..c0940032b 100644 --- a/src/NzbDrone.Core.Test/Download/DownloadClientTests/SabnzbdTests/SabnzbdFixture.cs +++ b/src/NzbDrone.Core.Test/Download/DownloadClientTests/SabnzbdTests/SabnzbdFixture.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Linq; using System.Collections.Generic; using FizzWare.NBuilder; @@ -191,7 +191,10 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.SabnzbdTests var result = Subject.GetItems().Single(); VerifyQueued(result); + result.RemainingTime.Should().NotBe(TimeSpan.Zero); + result.CanBeRemoved.Should().BeTrue(); + result.CanMoveFiles.Should().BeTrue(); } [TestCase(SabnzbdDownloadStatus.Paused)] @@ -205,6 +208,9 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.SabnzbdTests var result = Subject.GetItems().Single(); VerifyPaused(result); + + result.CanBeRemoved.Should().BeTrue(); + result.CanMoveFiles.Should().BeTrue(); } [TestCase(SabnzbdDownloadStatus.Checking)] @@ -227,7 +233,10 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.SabnzbdTests var result = Subject.GetItems().Single(); VerifyDownloading(result); + result.RemainingTime.Should().NotBe(TimeSpan.Zero); + result.CanBeRemoved.Should().BeTrue(); + result.CanMoveFiles.Should().BeTrue(); } [Test] @@ -239,6 +248,9 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.SabnzbdTests var result = Subject.GetItems().Single(); VerifyCompleted(result); + + result.CanBeRemoved.Should().BeTrue(); + result.CanMoveFiles.Should().BeTrue(); } [Test] @@ -252,6 +264,9 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.SabnzbdTests var result = Subject.GetItems().Single(); VerifyFailed(result); + + result.CanBeRemoved.Should().BeTrue(); + result.CanMoveFiles.Should().BeTrue(); } [Test] diff --git a/src/NzbDrone.Core.Test/Download/DownloadClientTests/TransmissionTests/TransmissionFixture.cs b/src/NzbDrone.Core.Test/Download/DownloadClientTests/TransmissionTests/TransmissionFixture.cs index 0ab9f227d..166679f39 100644 --- a/src/NzbDrone.Core.Test/Download/DownloadClientTests/TransmissionTests/TransmissionFixture.cs +++ b/src/NzbDrone.Core.Test/Download/DownloadClientTests/TransmissionTests/TransmissionFixture.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using FluentAssertions; using Moq; @@ -41,6 +41,9 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.TransmissionTests PrepareClientToReturnCompletedItem(); var item = Subject.GetItems().Single(); VerifyCompleted(item); + + item.CanBeRemoved.Should().BeTrue(); + item.CanMoveFiles.Should().BeTrue(); } [Test] @@ -145,8 +148,8 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.TransmissionTests [TestCase(TransmissionTorrentStatus.Check, DownloadItemStatus.Downloading)] [TestCase(TransmissionTorrentStatus.Queued, DownloadItemStatus.Queued)] [TestCase(TransmissionTorrentStatus.Downloading, DownloadItemStatus.Downloading)] - [TestCase(TransmissionTorrentStatus.SeedingWait, DownloadItemStatus.Completed)] - [TestCase(TransmissionTorrentStatus.Seeding, DownloadItemStatus.Completed)] + [TestCase(TransmissionTorrentStatus.SeedingWait, DownloadItemStatus.Downloading)] + [TestCase(TransmissionTorrentStatus.Seeding, DownloadItemStatus.Downloading)] public void GetItems_should_return_queued_item_as_downloadItemStatus(TransmissionTorrentStatus apiStatus, DownloadItemStatus expectedItemStatus) { _queued.Status = apiStatus; @@ -160,7 +163,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.TransmissionTests [TestCase(TransmissionTorrentStatus.Queued, DownloadItemStatus.Queued)] [TestCase(TransmissionTorrentStatus.Downloading, DownloadItemStatus.Downloading)] - [TestCase(TransmissionTorrentStatus.Seeding, DownloadItemStatus.Completed)] + [TestCase(TransmissionTorrentStatus.Seeding, DownloadItemStatus.Downloading)] public void GetItems_should_return_downloading_item_as_downloadItemStatus(TransmissionTorrentStatus apiStatus, DownloadItemStatus expectedItemStatus) { _downloading.Status = apiStatus; @@ -172,13 +175,13 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.TransmissionTests item.Status.Should().Be(expectedItemStatus); } - [TestCase(TransmissionTorrentStatus.Stopped, DownloadItemStatus.Completed, false)] - [TestCase(TransmissionTorrentStatus.CheckWait, DownloadItemStatus.Downloading, true)] - [TestCase(TransmissionTorrentStatus.Check, DownloadItemStatus.Downloading, true)] - [TestCase(TransmissionTorrentStatus.Queued, DownloadItemStatus.Completed, true)] - [TestCase(TransmissionTorrentStatus.SeedingWait, DownloadItemStatus.Completed, true)] - [TestCase(TransmissionTorrentStatus.Seeding, DownloadItemStatus.Completed, true)] - public void GetItems_should_return_completed_item_as_downloadItemStatus(TransmissionTorrentStatus apiStatus, DownloadItemStatus expectedItemStatus, bool expectedReadOnly) + [TestCase(TransmissionTorrentStatus.Stopped, DownloadItemStatus.Completed, true)] + [TestCase(TransmissionTorrentStatus.CheckWait, DownloadItemStatus.Downloading, false)] + [TestCase(TransmissionTorrentStatus.Check, DownloadItemStatus.Downloading, false)] + [TestCase(TransmissionTorrentStatus.Queued, DownloadItemStatus.Completed, false)] + [TestCase(TransmissionTorrentStatus.SeedingWait, DownloadItemStatus.Completed, false)] + [TestCase(TransmissionTorrentStatus.Seeding, DownloadItemStatus.Completed, false)] + public void GetItems_should_return_completed_item_as_downloadItemStatus(TransmissionTorrentStatus apiStatus, DownloadItemStatus expectedItemStatus, bool expectedValue) { _completed.Status = apiStatus; @@ -187,7 +190,8 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.TransmissionTests var item = Subject.GetItems().Single(); item.Status.Should().Be(expectedItemStatus); - item.IsReadOnly.Should().Be(expectedReadOnly); + item.CanBeRemoved.Should().Be(expectedValue); + item.CanMoveFiles.Should().Be(expectedValue); } [Test] diff --git a/src/NzbDrone.Core.Test/Download/DownloadClientTests/UTorrentTests/UTorrentFixture.cs b/src/NzbDrone.Core.Test/Download/DownloadClientTests/UTorrentTests/UTorrentFixture.cs index c77741cce..9071d8050 100644 --- a/src/NzbDrone.Core.Test/Download/DownloadClientTests/UTorrentTests/UTorrentFixture.cs +++ b/src/NzbDrone.Core.Test/Download/DownloadClientTests/UTorrentTests/UTorrentFixture.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Linq; using System.Collections.Generic; using FluentAssertions; @@ -222,6 +222,9 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.UTorrentTests PrepareClientToReturnCompletedItem(); var item = Subject.GetItems().Single(); VerifyCompleted(item); + + item.CanBeRemoved.Should().BeTrue(); + item.CanMoveFiles.Should().BeTrue(); } [Test] @@ -292,12 +295,12 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.UTorrentTests item.Status.Should().Be(expectedItemStatus); } - [TestCase(UTorrentTorrentStatus.Loaded | UTorrentTorrentStatus.Checking, DownloadItemStatus.Queued, false)] - [TestCase(UTorrentTorrentStatus.Loaded | UTorrentTorrentStatus.Checked, DownloadItemStatus.Completed, false)] - [TestCase(UTorrentTorrentStatus.Loaded | UTorrentTorrentStatus.Checked | UTorrentTorrentStatus.Queued, DownloadItemStatus.Completed, true)] - [TestCase(UTorrentTorrentStatus.Loaded | UTorrentTorrentStatus.Checked | UTorrentTorrentStatus.Started, DownloadItemStatus.Completed, true)] - [TestCase(UTorrentTorrentStatus.Loaded | UTorrentTorrentStatus.Checked | UTorrentTorrentStatus.Queued | UTorrentTorrentStatus.Paused, DownloadItemStatus.Completed, true)] - public void GetItems_should_return_completed_item_as_downloadItemStatus(UTorrentTorrentStatus apiStatus, DownloadItemStatus expectedItemStatus, bool expectedReadOnly) + [TestCase(UTorrentTorrentStatus.Loaded | UTorrentTorrentStatus.Checking, DownloadItemStatus.Queued, true)] + [TestCase(UTorrentTorrentStatus.Loaded | UTorrentTorrentStatus.Checked, DownloadItemStatus.Completed, true)] + [TestCase(UTorrentTorrentStatus.Loaded | UTorrentTorrentStatus.Checked | UTorrentTorrentStatus.Queued, DownloadItemStatus.Completed, false)] + [TestCase(UTorrentTorrentStatus.Loaded | UTorrentTorrentStatus.Checked | UTorrentTorrentStatus.Started, DownloadItemStatus.Completed, false)] + [TestCase(UTorrentTorrentStatus.Loaded | UTorrentTorrentStatus.Checked | UTorrentTorrentStatus.Queued | UTorrentTorrentStatus.Paused, DownloadItemStatus.Completed, false)] + public void GetItems_should_return_completed_item_as_downloadItemStatus(UTorrentTorrentStatus apiStatus, DownloadItemStatus expectedItemStatus, bool expectedValue) { _completed.Status = apiStatus; @@ -306,7 +309,8 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.UTorrentTests var item = Subject.GetItems().Single(); item.Status.Should().Be(expectedItemStatus); - item.IsReadOnly.Should().Be(expectedReadOnly); + item.CanBeRemoved.Should().Be(expectedValue); + item.CanMoveFiles.Should().Be(expectedValue); } [Test] diff --git a/src/NzbDrone.Core.Test/Download/DownloadClientTests/VuzeTests/VuzeFixture.cs b/src/NzbDrone.Core.Test/Download/DownloadClientTests/VuzeTests/VuzeFixture.cs index 160ad1384..b772f61ef 100644 --- a/src/NzbDrone.Core.Test/Download/DownloadClientTests/VuzeTests/VuzeFixture.cs +++ b/src/NzbDrone.Core.Test/Download/DownloadClientTests/VuzeTests/VuzeFixture.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using FluentAssertions; using Moq; @@ -13,6 +13,13 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.VuzeTests [TestFixture] public class VuzeFixture : TransmissionFixtureBase { + [SetUp] + public void Setup_Vuze() + { + // Vuze never sets isFinished. + _completed.IsFinished = false; + } + [Test] public void queued_item_should_have_required_properties() { @@ -43,6 +50,9 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.VuzeTests PrepareClientToReturnCompletedItem(); var item = Subject.GetItems().Single(); VerifyCompleted(item); + + item.CanBeRemoved.Should().BeTrue(); + item.CanMoveFiles.Should().BeTrue(); } [Test] @@ -147,8 +157,8 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.VuzeTests [TestCase(TransmissionTorrentStatus.Check, DownloadItemStatus.Downloading)] [TestCase(TransmissionTorrentStatus.Queued, DownloadItemStatus.Queued)] [TestCase(TransmissionTorrentStatus.Downloading, DownloadItemStatus.Downloading)] - [TestCase(TransmissionTorrentStatus.SeedingWait, DownloadItemStatus.Completed)] - [TestCase(TransmissionTorrentStatus.Seeding, DownloadItemStatus.Completed)] + [TestCase(TransmissionTorrentStatus.SeedingWait, DownloadItemStatus.Downloading)] + [TestCase(TransmissionTorrentStatus.Seeding, DownloadItemStatus.Downloading)] public void GetItems_should_return_queued_item_as_downloadItemStatus(TransmissionTorrentStatus apiStatus, DownloadItemStatus expectedItemStatus) { _queued.Status = apiStatus; @@ -162,7 +172,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.VuzeTests [TestCase(TransmissionTorrentStatus.Queued, DownloadItemStatus.Queued)] [TestCase(TransmissionTorrentStatus.Downloading, DownloadItemStatus.Downloading)] - [TestCase(TransmissionTorrentStatus.Seeding, DownloadItemStatus.Completed)] + [TestCase(TransmissionTorrentStatus.Seeding, DownloadItemStatus.Downloading)] public void GetItems_should_return_downloading_item_as_downloadItemStatus(TransmissionTorrentStatus apiStatus, DownloadItemStatus expectedItemStatus) { _downloading.Status = apiStatus; @@ -174,13 +184,13 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.VuzeTests item.Status.Should().Be(expectedItemStatus); } - [TestCase(TransmissionTorrentStatus.Stopped, DownloadItemStatus.Completed, false)] - [TestCase(TransmissionTorrentStatus.CheckWait, DownloadItemStatus.Downloading, true)] - [TestCase(TransmissionTorrentStatus.Check, DownloadItemStatus.Downloading, true)] - [TestCase(TransmissionTorrentStatus.Queued, DownloadItemStatus.Completed, true)] - [TestCase(TransmissionTorrentStatus.SeedingWait, DownloadItemStatus.Completed, true)] - [TestCase(TransmissionTorrentStatus.Seeding, DownloadItemStatus.Completed, true)] - public void GetItems_should_return_completed_item_as_downloadItemStatus(TransmissionTorrentStatus apiStatus, DownloadItemStatus expectedItemStatus, bool expectedReadOnly) + [TestCase(TransmissionTorrentStatus.Stopped, DownloadItemStatus.Completed, true)] + [TestCase(TransmissionTorrentStatus.CheckWait, DownloadItemStatus.Downloading, false)] + [TestCase(TransmissionTorrentStatus.Check, DownloadItemStatus.Downloading, false)] + [TestCase(TransmissionTorrentStatus.Queued, DownloadItemStatus.Queued, false)] + [TestCase(TransmissionTorrentStatus.SeedingWait, DownloadItemStatus.Completed, false)] + [TestCase(TransmissionTorrentStatus.Seeding, DownloadItemStatus.Completed, false)] + public void GetItems_should_return_completed_item_as_downloadItemStatus(TransmissionTorrentStatus apiStatus, DownloadItemStatus expectedItemStatus, bool expectedValue) { _completed.Status = apiStatus; @@ -189,7 +199,8 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.VuzeTests var item = Subject.GetItems().Single(); item.Status.Should().Be(expectedItemStatus); - item.IsReadOnly.Should().Be(expectedReadOnly); + item.CanBeRemoved.Should().Be(expectedValue); + item.CanMoveFiles.Should().Be(expectedValue); } [Test] @@ -294,7 +305,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.VuzeTests } [Test] - public void should_have_correct_output_directory() + public void should_have_correct_output_directory_for_multifile_torrents() { WindowsOnly(); @@ -311,5 +322,25 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.VuzeTests items.First().OutputPath.Should().Be(@"C:\Downloads\" + _title); } + [Test] + public void should_have_correct_output_directory_for_singlefile_torrents() + { + WindowsOnly(); + + var fileName = _title + ".mkv"; + _downloading.Name = fileName; + _downloading.DownloadDir = @"C:/Downloads"; + + GivenTorrents(new List + { + _downloading + }); + + var items = Subject.GetItems().ToList(); + + items.Should().HaveCount(1); + items.First().OutputPath.Should().Be(@"C:\Downloads\" + fileName); + } + } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Core.Test/Download/DownloadServiceFixture.cs b/src/NzbDrone.Core.Test/Download/DownloadServiceFixture.cs index dca02000f..1cc7d81f7 100644 --- a/src/NzbDrone.Core.Test/Download/DownloadServiceFixture.cs +++ b/src/NzbDrone.Core.Test/Download/DownloadServiceFixture.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Net; @@ -180,14 +180,27 @@ namespace NzbDrone.Core.Test.Download } [Test] - public void should_not_attempt_download_if_client_isnt_configure() + public void should_not_attempt_download_if_client_isnt_configured() { - Subject.DownloadReport(_parseResult); + Assert.Throws(() => Subject.DownloadReport(_parseResult)); Mocker.GetMock().Verify(c => c.Download(It.IsAny()), Times.Never()); - VerifyEventNotPublished(); + VerifyEventNotPublished(); + } + + [Test] + public void should_not_attempt_download_if_client_is_disabled() + { + WithUsenetClient(); + + Mocker.GetMock() + .Setup(v => v.IsDisabled(It.IsAny())) + .Returns(true); + + Assert.Throws(() => Subject.DownloadReport(_parseResult)); - ExceptionVerification.ExpectedWarns(1); + Mocker.GetMock().Verify(c => c.Download(It.IsAny()), Times.Never()); + VerifyEventNotPublished(); } [Test] diff --git a/src/NzbDrone.Core.Test/Download/FailedDownloadServiceFixture.cs b/src/NzbDrone.Core.Test/Download/FailedDownloadServiceFixture.cs index 42b589e6b..420647de4 100644 --- a/src/NzbDrone.Core.Test/Download/FailedDownloadServiceFixture.cs +++ b/src/NzbDrone.Core.Test/Download/FailedDownloadServiceFixture.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using FizzWare.NBuilder; using FluentAssertions; using Moq; @@ -10,7 +10,7 @@ using NzbDrone.Core.History; using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Test.Framework; -using NzbDrone.Core.Tv; +using NzbDrone.Core.Music; using NzbDrone.Test.Common; namespace NzbDrone.Core.Test.Download @@ -27,21 +27,21 @@ namespace NzbDrone.Core.Test.Download var completed = Builder.CreateNew() .With(h => h.Status = DownloadItemStatus.Completed) .With(h => h.OutputPath = new OsPath(@"C:\DropFolder\MyDownload".AsOsAgnostic())) - .With(h => h.Title = "Drone.S01E01.HDTV") + .With(h => h.Title = "Drone.DroneTheAlbum.FLAC") .Build(); _grabHistory = Builder.CreateListOfSize(2).BuildList(); - var remoteEpisode = new RemoteEpisode + var remoteAlbum = new RemoteAlbum { - Series = new Series(), - Episodes = new List { new Episode { Id = 1 } } + Artist = new Artist(), + Albums = new List { new Album { Id = 1 } } }; _trackedDownload = Builder.CreateNew() .With(c => c.State = TrackedDownloadStage.Downloading) .With(c => c.DownloadItem = completed) - .With(c => c.RemoteEpisode = remoteEpisode) + .With(c => c.RemoteAlbum = remoteAlbum) .Build(); diff --git a/src/NzbDrone.Core.Test/Download/Pending/PendingReleaseServiceTests/AddFixture.cs b/src/NzbDrone.Core.Test/Download/Pending/PendingReleaseServiceTests/AddFixture.cs index 9e9652ab7..c245ba895 100644 --- a/src/NzbDrone.Core.Test/Download/Pending/PendingReleaseServiceTests/AddFixture.cs +++ b/src/NzbDrone.Core.Test/Download/Pending/PendingReleaseServiceTests/AddFixture.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using FizzWare.NBuilder; using Marr.Data; @@ -9,7 +9,7 @@ using NzbDrone.Core.DecisionEngine; using NzbDrone.Core.Download.Pending; using NzbDrone.Core.Parser; using NzbDrone.Core.Parser.Model; -using NzbDrone.Core.Profiles; +using NzbDrone.Core.Profiles.Qualities; using NzbDrone.Core.Qualities; using NzbDrone.Core.Test.Framework; using NzbDrone.Core.Music; @@ -102,7 +102,7 @@ namespace NzbDrone.Core.Test.Download.Pending.PendingReleaseServiceTests [Test] public void should_add() { - Subject.Add(_temporarilyRejected); + Subject.Add(_temporarilyRejected, PendingReleaseReason.Delay); VerifyInsert(); } @@ -112,7 +112,7 @@ namespace NzbDrone.Core.Test.Download.Pending.PendingReleaseServiceTests { GivenHeldRelease(_release.Title, _release.Indexer, _release.PublishDate); - Subject.Add(_temporarilyRejected); + Subject.Add(_temporarilyRejected, PendingReleaseReason.Delay); VerifyNoInsert(); } @@ -122,7 +122,7 @@ namespace NzbDrone.Core.Test.Download.Pending.PendingReleaseServiceTests { GivenHeldRelease(_release.Title + "-RP", _release.Indexer, _release.PublishDate); - Subject.Add(_temporarilyRejected); + Subject.Add(_temporarilyRejected, PendingReleaseReason.Delay); VerifyInsert(); } @@ -132,7 +132,7 @@ namespace NzbDrone.Core.Test.Download.Pending.PendingReleaseServiceTests { GivenHeldRelease(_release.Title, "AnotherIndexer", _release.PublishDate); - Subject.Add(_temporarilyRejected); + Subject.Add(_temporarilyRejected, PendingReleaseReason.Delay); VerifyInsert(); } @@ -142,7 +142,7 @@ namespace NzbDrone.Core.Test.Download.Pending.PendingReleaseServiceTests { GivenHeldRelease(_release.Title, _release.Indexer, _release.PublishDate.AddHours(1)); - Subject.Add(_temporarilyRejected); + Subject.Add(_temporarilyRejected, PendingReleaseReason.Delay); VerifyInsert(); } diff --git a/src/NzbDrone.Core.Test/Download/Pending/PendingReleaseServiceTests/PendingReleaseServiceFixture.cs b/src/NzbDrone.Core.Test/Download/Pending/PendingReleaseServiceTests/PendingReleaseServiceFixture.cs index 37f979ba9..8ff0f0ea0 100644 --- a/src/NzbDrone.Core.Test/Download/Pending/PendingReleaseServiceTests/PendingReleaseServiceFixture.cs +++ b/src/NzbDrone.Core.Test/Download/Pending/PendingReleaseServiceTests/PendingReleaseServiceFixture.cs @@ -27,7 +27,7 @@ namespace NzbDrone.Core.Test.Download.Pending.PendingReleaseServiceTests public void should_not_ignore_pending_items_from_available_indexer() { Mocker.GetMock() - .Setup(v => v.GetBlockedIndexers()) + .Setup(v => v.GetBlockedProviders()) .Returns(new List()); GivenPendingRelease(); @@ -43,8 +43,8 @@ namespace NzbDrone.Core.Test.Download.Pending.PendingReleaseServiceTests public void should_ignore_pending_items_from_unavailable_indexer() { Mocker.GetMock() - .Setup(v => v.GetBlockedIndexers()) - .Returns(new List { new IndexerStatus { IndexerId = 1, DisabledTill = DateTime.UtcNow.AddHours(2) } }); + .Setup(v => v.GetBlockedProviders()) + .Returns(new List { new IndexerStatus { ProviderId = 1, DisabledTill = DateTime.UtcNow.AddHours(2) } }); GivenPendingRelease(); diff --git a/src/NzbDrone.Core.Test/Download/Pending/PendingReleaseServiceTests/RemoveGrabbedFixture.cs b/src/NzbDrone.Core.Test/Download/Pending/PendingReleaseServiceTests/RemoveGrabbedFixture.cs index 4fb99c8f8..ec98ba123 100644 --- a/src/NzbDrone.Core.Test/Download/Pending/PendingReleaseServiceTests/RemoveGrabbedFixture.cs +++ b/src/NzbDrone.Core.Test/Download/Pending/PendingReleaseServiceTests/RemoveGrabbedFixture.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using FizzWare.NBuilder; using Marr.Data; using Moq; @@ -9,7 +9,7 @@ using NzbDrone.Core.Download; using NzbDrone.Core.Download.Pending; using NzbDrone.Core.Parser; using NzbDrone.Core.Parser.Model; -using NzbDrone.Core.Profiles; +using NzbDrone.Core.Profiles.Qualities; using NzbDrone.Core.Qualities; using NzbDrone.Core.Test.Framework; using NzbDrone.Core.Music; diff --git a/src/NzbDrone.Core.Test/Download/Pending/PendingReleaseServiceTests/RemoveRejectedFixture.cs b/src/NzbDrone.Core.Test/Download/Pending/PendingReleaseServiceTests/RemoveRejectedFixture.cs index 46a16830a..da3d791fa 100644 --- a/src/NzbDrone.Core.Test/Download/Pending/PendingReleaseServiceTests/RemoveRejectedFixture.cs +++ b/src/NzbDrone.Core.Test/Download/Pending/PendingReleaseServiceTests/RemoveRejectedFixture.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using FizzWare.NBuilder; using Marr.Data; @@ -11,7 +11,7 @@ using NzbDrone.Core.Download.Pending; using NzbDrone.Core.Indexers; using NzbDrone.Core.Parser; using NzbDrone.Core.Parser.Model; -using NzbDrone.Core.Profiles; +using NzbDrone.Core.Profiles.Qualities; using NzbDrone.Core.Qualities; using NzbDrone.Core.Test.Framework; using NzbDrone.Core.Music; diff --git a/src/NzbDrone.Core.Test/Files/Indexers/TorrentRss/EvolutionWorld.xml b/src/NzbDrone.Core.Test/Files/Indexers/TorrentRss/EvolutionWorld.xml new file mode 100644 index 000000000..48c5b651b --- /dev/null +++ b/src/NzbDrone.Core.Test/Files/Indexers/TorrentRss/EvolutionWorld.xml @@ -0,0 +1,30 @@ + + + + Evolution World + Advanced RSS Feed for xbtitFM by Petr1fied + http://ew.pw + Tue, 15 Aug 2017 00:00:00 +0000 + (c) 2017 Evolution World + + + + <![CDATA[[TVShow --> TVShow Bluray 720p] Fargo S01 Complete Season 1 720p BRRip DD5.1 x264-PSYPHER [SEEDERS (3)/LEECHERS (0)]]]> + Fargo S01 Complete Season 1 720p BRRip DD5.1 x264-PSYPHER



Plot:

Various chronicles of deception, intrigue and murder in and around frozen Minnesota.

Note::-
Encode is tested and its perfect, no sync issue there in any episode.
All episodes comes with AC3 Audio for better audio and video plaback and English Subs are muxed in video .

TECHNiCAL Information:


RUNTIME..................: 1 hr x 10
Total SIZE...............: 9.75 GiB
Total Episodes...........: 10
VIDEO CODEC..............: x264 2nd Pass (High,L4.1)
RESOLUTION...............: 1280x720
BITRATE (Video)..........: 2100 Kbps - 2400 kbps
Aspect Ratio.............: 16:9
Video Container..........: MKV
FRAMERATE................: 23.967 fps
AUDIO....................: English AC3 6 Channel 384 kbps
SUBTITLES................: English, English (SDH)
CHAPTERS.................: Yes
SOURCE...................: DON (Thanks !)


GENRE...................: Crime | Drama | Thriller
RATING..................: 9.1/10 from 140,765 users
IMDB link...............: http://www.imdb.com/title/tt2802850/]]>
+ http://ew.pw/index.php?page=torrent-details&id=dea071a7a62a0d662538d46402fb112f30b8c9fa + http://ew.pw/index.php?page=torrent-details&id=dea071a7a62a0d662538d46402fb112f30b8c9fa + + Sun, 13 Aug 2017 22:21:43 +0000 +
+ + + <![CDATA[[TVShow --> TVShow Bluray 720p] American Horror Story S04 Complete Season 4 720p BRRip DD5.1 x264 - PSYPHER [SEEDERS (2)/LEECHERS (0)]]]> + + http://ew.pw/index.php?page=torrent-details&id=2725fe19ea2addf5aafbd523d134191b8abbb2ee + http://ew.pw/index.php?page=torrent-details&id=2725fe19ea2addf5aafbd523d134191b8abbb2ee + + Fri, 28 Jul 2017 16:29:51 +0000 + +
+
+ \ No newline at end of file diff --git a/src/NzbDrone.Core.Test/HealthCheck/Checks/DeleteBadMediaCovers.cs b/src/NzbDrone.Core.Test/HealthCheck/Checks/DeleteBadMediaCovers.cs index 5b454ae3c..16639271d 100644 --- a/src/NzbDrone.Core.Test/HealthCheck/Checks/DeleteBadMediaCovers.cs +++ b/src/NzbDrone.Core.Test/HealthCheck/Checks/DeleteBadMediaCovers.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.IO; using System.Linq; @@ -11,7 +11,7 @@ using NzbDrone.Core.Extras.Metadata; using NzbDrone.Core.Extras.Metadata.Files; using NzbDrone.Core.Housekeeping.Housekeepers; using NzbDrone.Core.Test.Framework; -using NzbDrone.Core.Tv; +using NzbDrone.Core.Music; using NzbDrone.Test.Common; namespace NzbDrone.Core.Test.HealthCheck.Checks @@ -20,27 +20,27 @@ namespace NzbDrone.Core.Test.HealthCheck.Checks public class DeleteBadMediaCoversFixture : CoreTest { private List _metadata; - private List _series; + private List _artist; [SetUp] public void Setup() { - _series = Builder.CreateListOfSize(1) + _artist = Builder.CreateListOfSize(1) .All() - .With(c => c.Path = "C:\\TV\\".AsOsAgnostic()) + .With(c => c.Path = "C:\\Music\\".AsOsAgnostic()) .Build().ToList(); _metadata = Builder.CreateListOfSize(1) .Build().ToList(); - Mocker.GetMock() - .Setup(c => c.GetAllSeries()) - .Returns(_series); + Mocker.GetMock() + .Setup(c => c.GetAllArtists()) + .Returns(_artist); Mocker.GetMock() - .Setup(c => c.GetFilesBySeries(_series.First().Id)) + .Setup(c => c.GetFilesByArtist(_artist.First().Id)) .Returns(_metadata); @@ -51,8 +51,8 @@ namespace NzbDrone.Core.Test.HealthCheck.Checks [Test] public void should_not_process_non_image_files() { - _metadata.First().RelativePath = "season\\file.xml".AsOsAgnostic(); - _metadata.First().Type = MetadataType.EpisodeMetadata; + _metadata.First().RelativePath = "album\\file.xml".AsOsAgnostic(); + _metadata.First().Type = MetadataType.TrackMetadata; Subject.Clean(); @@ -80,7 +80,7 @@ namespace NzbDrone.Core.Test.HealthCheck.Checks Subject.Clean(); Mocker.GetMock().VerifySet(c => c.CleanupMetadataImages = true, Times.Never()); - Mocker.GetMock().Verify(c => c.GetAllSeries(), Times.Never()); + Mocker.GetMock().Verify(c => c.GetAllArtists(), Times.Never()); AssertImageWasNotRemoved(); } @@ -101,10 +101,10 @@ namespace NzbDrone.Core.Test.HealthCheck.Checks public void should_delete_html_images() { - var imagePath = "C:\\TV\\Season\\image.jpg".AsOsAgnostic(); + var imagePath = "C:\\Music\\Album\\image.jpg".AsOsAgnostic(); _metadata.First().LastUpdated = new DateTime(2014, 12, 29); - _metadata.First().RelativePath = "Season\\image.jpg".AsOsAgnostic(); - _metadata.First().Type = MetadataType.SeriesImage; + _metadata.First().RelativePath = "Album\\image.jpg".AsOsAgnostic(); + _metadata.First().Type = MetadataType.ArtistImage; Mocker.GetMock() .Setup(c => c.OpenReadStream(imagePath)) @@ -123,10 +123,10 @@ namespace NzbDrone.Core.Test.HealthCheck.Checks public void should_delete_empty_images() { - var imagePath = "C:\\TV\\Season\\image.jpg".AsOsAgnostic(); + var imagePath = "C:\\Music\\Album\\image.jpg".AsOsAgnostic(); _metadata.First().LastUpdated = new DateTime(2014, 12, 29); - _metadata.First().Type = MetadataType.SeasonImage; - _metadata.First().RelativePath = "Season\\image.jpg".AsOsAgnostic(); + _metadata.First().Type = MetadataType.AlbumImage; + _metadata.First().RelativePath = "Album\\image.jpg".AsOsAgnostic(); Mocker.GetMock() .Setup(c => c.OpenReadStream(imagePath)) @@ -144,9 +144,9 @@ namespace NzbDrone.Core.Test.HealthCheck.Checks public void should_not_delete_non_html_files() { - var imagePath = "C:\\TV\\Season\\image.jpg".AsOsAgnostic(); + var imagePath = "C:\\Music\\Album\\image.jpg".AsOsAgnostic(); _metadata.First().LastUpdated = new DateTime(2014, 12, 29); - _metadata.First().RelativePath = "Season\\image.jpg".AsOsAgnostic(); + _metadata.First().RelativePath = "Album\\image.jpg".AsOsAgnostic(); Mocker.GetMock() .Setup(c => c.OpenReadStream(imagePath)) diff --git a/src/NzbDrone.Core.Test/HealthCheck/Checks/DownloadClientCheckFixture.cs b/src/NzbDrone.Core.Test/HealthCheck/Checks/DownloadClientCheckFixture.cs index dc6986d79..56f83e0a9 100644 --- a/src/NzbDrone.Core.Test/HealthCheck/Checks/DownloadClientCheckFixture.cs +++ b/src/NzbDrone.Core.Test/HealthCheck/Checks/DownloadClientCheckFixture.cs @@ -1,9 +1,8 @@ -using System; +using System; using System.Collections.Generic; using NUnit.Framework; using NzbDrone.Core.Download; using NzbDrone.Core.HealthCheck.Checks; -using NzbDrone.Core.Indexers; using NzbDrone.Core.Test.Framework; using NzbDrone.Test.Common; @@ -26,7 +25,7 @@ namespace NzbDrone.Core.Test.HealthCheck.Checks public void should_return_error_when_download_client_throws() { var downloadClient = Mocker.GetMock(); - downloadClient.Setup(s => s.Definition).Returns(new IndexerDefinition{Name = "Test"}); + downloadClient.Setup(s => s.Definition).Returns(new DownloadClientDefinition{Name = "Test"}); downloadClient.Setup(s => s.GetItems()) .Throws(); @@ -36,8 +35,6 @@ namespace NzbDrone.Core.Test.HealthCheck.Checks .Returns(new IDownloadClient[] { downloadClient.Object }); Subject.Check().ShouldBeError(); - - ExceptionVerification.ExpectedErrors(1); } [Test] diff --git a/src/NzbDrone.Core.Test/HealthCheck/Checks/DroneFactoryCheckFixture.cs b/src/NzbDrone.Core.Test/HealthCheck/Checks/DroneFactoryCheckFixture.cs deleted file mode 100644 index 8f1224ad5..000000000 --- a/src/NzbDrone.Core.Test/HealthCheck/Checks/DroneFactoryCheckFixture.cs +++ /dev/null @@ -1,54 +0,0 @@ -using Moq; -using NUnit.Framework; -using NzbDrone.Common.Disk; -using NzbDrone.Core.Configuration; -using NzbDrone.Core.HealthCheck.Checks; -using NzbDrone.Core.Test.Framework; - -namespace NzbDrone.Core.Test.HealthCheck.Checks -{ - [TestFixture] - public class DroneFactoryCheckFixture : CoreTest - { - private const string DRONE_FACTORY_FOLDER = @"C:\Test\Unsorted"; - - private void GivenDroneFactoryFolder(bool exists = false, bool writable = true) - { - Mocker.GetMock() - .SetupGet(s => s.DownloadedAlbumsFolder) - .Returns(DRONE_FACTORY_FOLDER); - - Mocker.GetMock() - .Setup(s => s.FolderExists(DRONE_FACTORY_FOLDER)) - .Returns(exists); - - Mocker.GetMock() - .Setup(s => s.FolderWritable(It.IsAny())) - .Returns(exists && writable); - } - - [Test] - public void should_return_error_when_drone_factory_folder_does_not_exist() - { - GivenDroneFactoryFolder(); - - Subject.Check().ShouldBeError(); - } - - [Test] - public void should_return_error_when_unable_to_write_to_drone_factory_folder() - { - GivenDroneFactoryFolder(true, false); - - Subject.Check().ShouldBeError(); - } - - [Test] - public void should_return_ok_when_no_issues_found() - { - GivenDroneFactoryFolder(true); - - Subject.Check().ShouldBeOk(); - } - } -} diff --git a/src/NzbDrone.Core.Test/HealthCheck/Checks/ImportMechanismCheckFixture.cs b/src/NzbDrone.Core.Test/HealthCheck/Checks/ImportMechanismCheckFixture.cs index 49be9e435..060076142 100644 --- a/src/NzbDrone.Core.Test/HealthCheck/Checks/ImportMechanismCheckFixture.cs +++ b/src/NzbDrone.Core.Test/HealthCheck/Checks/ImportMechanismCheckFixture.cs @@ -1,4 +1,4 @@ -using NUnit.Framework; +using NUnit.Framework; using NzbDrone.Common.Disk; using NzbDrone.Core.Configuration; using NzbDrone.Core.HealthCheck.Checks; @@ -10,8 +10,6 @@ namespace NzbDrone.Core.Test.HealthCheck.Checks [TestFixture] public class ImportMechanismCheckFixture : CoreTest { - private const string DRONE_FACTORY_FOLDER = @"C:\Test\Unsorted"; - private void GivenCompletedDownloadHandling(bool? enabled = null) { @@ -27,17 +25,6 @@ namespace NzbDrone.Core.Test.HealthCheck.Checks } } - private void GivenDroneFactoryFolder(bool exists = false) - { - Mocker.GetMock() - .SetupGet(s => s.DownloadedAlbumsFolder) - .Returns(DRONE_FACTORY_FOLDER.AsOsAgnostic()); - - Mocker.GetMock() - .Setup(s => s.FolderExists(DRONE_FACTORY_FOLDER.AsOsAgnostic())) - .Returns(exists); - } - [Test] public void should_return_warning_when_completed_download_handling_not_configured() { @@ -56,7 +43,6 @@ namespace NzbDrone.Core.Test.HealthCheck.Checks public void should_return_ok_when_no_issues_found() { GivenCompletedDownloadHandling(true); - GivenDroneFactoryFolder(true); Subject.Check().ShouldBeOk(); } diff --git a/src/NzbDrone.Core.Test/HealthCheck/Checks/IndexerStatusCheckFixture.cs b/src/NzbDrone.Core.Test/HealthCheck/Checks/IndexerStatusCheckFixture.cs index 6592e2a76..1d71d3a80 100644 --- a/src/NzbDrone.Core.Test/HealthCheck/Checks/IndexerStatusCheckFixture.cs +++ b/src/NzbDrone.Core.Test/HealthCheck/Checks/IndexerStatusCheckFixture.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using Moq; using NUnit.Framework; @@ -22,7 +22,7 @@ namespace NzbDrone.Core.Test.HealthCheck.Checks .Returns(_indexers); Mocker.GetMock() - .Setup(v => v.GetBlockedIndexers()) + .Setup(v => v.GetBlockedProviders()) .Returns(_blockedIndexers); } @@ -40,7 +40,7 @@ namespace NzbDrone.Core.Test.HealthCheck.Checks { _blockedIndexers.Add(new IndexerStatus { - IndexerId = id, + ProviderId = id, InitialFailure = DateTime.UtcNow.AddHours(-failureHours), MostRecentFailure = DateTime.UtcNow.AddHours(-0.1), EscalationLevel = 5, @@ -57,13 +57,6 @@ namespace NzbDrone.Core.Test.HealthCheck.Checks { Subject.Check().ShouldBeOk(); } - [Test] - public void should_not_return_error_when_indexer_failed_less_than_an_hour() - { - GivenIndexer(1, 0.1, 0.5); - - Subject.Check().ShouldBeOk(); - } [Test] public void should_return_warning_if_indexer_unavailable() diff --git a/src/NzbDrone.Core.Test/HealthCheck/Checks/MonoVersionCheckFixture.cs b/src/NzbDrone.Core.Test/HealthCheck/Checks/MonoVersionCheckFixture.cs index baca51b08..399a996dc 100644 --- a/src/NzbDrone.Core.Test/HealthCheck/Checks/MonoVersionCheckFixture.cs +++ b/src/NzbDrone.Core.Test/HealthCheck/Checks/MonoVersionCheckFixture.cs @@ -1,4 +1,4 @@ -using System; +using System; using NUnit.Framework; using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Core.HealthCheck.Checks; @@ -18,11 +18,13 @@ namespace NzbDrone.Core.Test.HealthCheck.Checks .Returns(new Version(version)); } - [TestCase("3.10")] - [TestCase("4.0.0.0")] - [TestCase("4.2")] [TestCase("4.6")] [TestCase("4.4.2")] + [TestCase("4.6")] + [TestCase("4.8")] + [TestCase("5.0")] + [TestCase("5.2")] + [TestCase("5.4")] public void should_return_ok(string version) { GivenOutput(version); @@ -38,6 +40,9 @@ namespace NzbDrone.Core.Test.HealthCheck.Checks [TestCase("3.2.7")] [TestCase("3.6.1")] [TestCase("3.8")] + [TestCase("3.10")] + [TestCase("4.0.0.0")] + [TestCase("4.2")] public void should_return_warning(string version) { GivenOutput(version); diff --git a/src/NzbDrone.Core.Test/HealthCheck/Checks/RootFolderCheckFixture.cs b/src/NzbDrone.Core.Test/HealthCheck/Checks/RootFolderCheckFixture.cs index 45ad31207..6755a83f9 100644 --- a/src/NzbDrone.Core.Test/HealthCheck/Checks/RootFolderCheckFixture.cs +++ b/src/NzbDrone.Core.Test/HealthCheck/Checks/RootFolderCheckFixture.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using FizzWare.NBuilder; using Moq; @@ -6,7 +6,7 @@ using NUnit.Framework; using NzbDrone.Common.Disk; using NzbDrone.Core.HealthCheck.Checks; using NzbDrone.Core.Test.Framework; -using NzbDrone.Core.Tv; +using NzbDrone.Core.Music; namespace NzbDrone.Core.Test.HealthCheck.Checks { @@ -15,17 +15,17 @@ namespace NzbDrone.Core.Test.HealthCheck.Checks { private void GivenMissingRootFolder() { - var series = Builder.CreateListOfSize(1) + var artist = Builder.CreateListOfSize(1) .Build() .ToList(); - Mocker.GetMock() - .Setup(s => s.GetAllSeries()) - .Returns(series); + Mocker.GetMock() + .Setup(s => s.GetAllArtists()) + .Returns(artist); Mocker.GetMock() - .Setup(s => s.GetParentFolder(series.First().Path)) - .Returns(@"C:\TV"); + .Setup(s => s.GetParentFolder(artist.First().Path)) + .Returns(@"C:\Music"); Mocker.GetMock() .Setup(s => s.FolderExists(It.IsAny())) @@ -33,17 +33,17 @@ namespace NzbDrone.Core.Test.HealthCheck.Checks } [Test] - public void should_not_return_error_when_no_series() + public void should_not_return_error_when_no_artist() { - Mocker.GetMock() - .Setup(s => s.GetAllSeries()) - .Returns(new List()); + Mocker.GetMock() + .Setup(s => s.GetAllArtists()) + .Returns(new List()); Subject.Check().ShouldBeOk(); } [Test] - public void should_return_error_if_series_parent_is_missing() + public void should_return_error_if_artist_parent_is_missing() { GivenMissingRootFolder(); diff --git a/src/NzbDrone.Core.Test/HistoryTests/HistoryServiceFixture.cs b/src/NzbDrone.Core.Test/HistoryTests/HistoryServiceFixture.cs index 34e5f2962..898d43033 100644 --- a/src/NzbDrone.Core.Test/HistoryTests/HistoryServiceFixture.cs +++ b/src/NzbDrone.Core.Test/HistoryTests/HistoryServiceFixture.cs @@ -1,4 +1,4 @@ -using System.IO; +using System.IO; using System.Linq; using FizzWare.NBuilder; using Moq; @@ -6,14 +6,16 @@ using NUnit.Framework; using NzbDrone.Core.MediaFiles; using NzbDrone.Core.MediaFiles.Events; using NzbDrone.Core.Parser.Model; -using NzbDrone.Core.Profiles; +using NzbDrone.Core.Profiles.Qualities; using NzbDrone.Core.Test.Framework; using NzbDrone.Core.History; using NzbDrone.Core.Qualities; using System.Collections.Generic; using NzbDrone.Core.Test.Qualities; -using FluentAssertions; -using NzbDrone.Core.Tv; +using NzbDrone.Core.Download; +using NzbDrone.Core.Music; +using NzbDrone.Core.Languages; +using NzbDrone.Core.Profiles.Languages; namespace NzbDrone.Core.Test.HistoryTests { @@ -21,70 +23,62 @@ namespace NzbDrone.Core.Test.HistoryTests { private Profile _profile; private Profile _profileCustom; + private LanguageProfile _languageProfile; [SetUp] public void Setup() { - _profile = new Profile { Cutoff = Quality.MP3_256, Items = QualityFixture.GetDefaultQualities() }; - _profileCustom = new Profile { Cutoff = Quality.MP3_256, Items = QualityFixture.GetDefaultQualities(Quality.MP3_192) }; - } + _profile = new Profile + { + Cutoff = Quality.MP3_320, + Items = QualityFixture.GetDefaultQualities(), + }; - [Test] - public void should_return_null_if_no_history() - { - Mocker.GetMock() - .Setup(v => v.GetBestQualityInHistory(2)) - .Returns(new List()); + _profileCustom = new Profile - var quality = Subject.GetBestQualityInHistory(_profile, 2); + { + Cutoff = Quality.MP3_320, + Items = QualityFixture.GetDefaultQualities(Quality.MP3_256), - quality.Should().BeNull(); - } + }; - [Test] - public void should_return_best_quality() - { - Mocker.GetMock() - .Setup(v => v.GetBestQualityInHistory(2)) - .Returns(new List { new QualityModel(Quality.MP3_192), new QualityModel(Quality.MP3_256) }); - - var quality = Subject.GetBestQualityInHistory(_profile, 2); - quality.Should().Be(new QualityModel(Quality.MP3_256)); - } + _languageProfile = new LanguageProfile - [Test] - public void should_return_best_quality_with_custom_order() - { - Mocker.GetMock() - .Setup(v => v.GetBestQualityInHistory(2)) - .Returns(new List { new QualityModel(Quality.MP3_192), new QualityModel(Quality.MP3_256) }); + { + Cutoff = Language.Spanish, + Languages = Languages.LanguageFixture.GetDefaultLanguages() + }; - var quality = Subject.GetBestQualityInHistory(_profileCustom, 2); - quality.Should().Be(new QualityModel(Quality.MP3_192)); } [Test] public void should_use_file_name_for_source_title_if_scene_name_is_null() { - var series = Builder.CreateNew().Build(); - var episodes = Builder.CreateListOfSize(1).Build().ToList(); - var episodeFile = Builder.CreateNew() + var artist = Builder.CreateNew().Build(); + var tracks = Builder.CreateListOfSize(1).Build().ToList(); + var trackFile = Builder.CreateNew() .With(f => f.SceneName = null) .Build(); - var localEpisode = new LocalEpisode - { - Series = series, - Episodes = episodes, - Path = @"C:\Test\Unsorted\Series.s01e01.mkv" - }; + var localTrack = new LocalTrack + { + Artist = artist, + Tracks = tracks, + Path = @"C:\Test\Unsorted\Artist.01.Hymn.mp3" + }; - Subject.Handle(new EpisodeImportedEvent(localEpisode, episodeFile, true, "sab", "abcd", true)); + var downloadClientItem = new DownloadClientItem + { + DownloadClient = "sab", + DownloadId = "abcd" + }; + + Subject.Handle(new TrackImportedEvent(localTrack, trackFile, new List(), true, downloadClientItem)); Mocker.GetMock() - .Verify(v => v.Insert(It.Is(h => h.SourceTitle == Path.GetFileNameWithoutExtension(localEpisode.Path)))); + .Verify(v => v.Insert(It.Is(h => h.SourceTitle == Path.GetFileNameWithoutExtension(localTrack.Path)))); } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Core.Test/Housekeeping/Housekeepers/CleanupDownloadClientUnavailablePendingReleasesFixture.cs b/src/NzbDrone.Core.Test/Housekeeping/Housekeepers/CleanupDownloadClientUnavailablePendingReleasesFixture.cs new file mode 100644 index 000000000..cda91729d --- /dev/null +++ b/src/NzbDrone.Core.Test/Housekeeping/Housekeepers/CleanupDownloadClientUnavailablePendingReleasesFixture.cs @@ -0,0 +1,60 @@ +using System; +using FizzWare.NBuilder; +using FluentAssertions; +using NUnit.Framework; +using NzbDrone.Core.Download.Pending; +using NzbDrone.Core.Housekeeping.Housekeepers; +using NzbDrone.Core.Parser.Model; +using NzbDrone.Core.Test.Framework; + +namespace NzbDrone.Core.Test.Housekeeping.Housekeepers +{ + [TestFixture] + public class CleanupDownloadClientUnavailablePendingReleasesFixture : DbTest + { + [Test] + public void should_delete_old_DownloadClientUnavailable_pending_items() + { + var pendingRelease = Builder.CreateNew() + .With(h => h.Reason = PendingReleaseReason.DownloadClientUnavailable) + .With(h => h.Added = DateTime.UtcNow.AddDays(-21)) + .With(h => h.ParsedAlbumInfo = new ParsedAlbumInfo()) + .With(h => h.Release = new ReleaseInfo()) + .BuildNew(); + + Db.Insert(pendingRelease); + Subject.Clean(); + AllStoredModels.Should().BeEmpty(); + } + + [Test] + public void should_delete_old_Fallback_pending_items() + { + var pendingRelease = Builder.CreateNew() + .With(h => h.Reason = PendingReleaseReason.Fallback) + .With(h => h.Added = DateTime.UtcNow.AddDays(-21)) + .With(h => h.ParsedAlbumInfo = new ParsedAlbumInfo()) + .With(h => h.Release = new ReleaseInfo()) + .BuildNew(); + + Db.Insert(pendingRelease); + Subject.Clean(); + AllStoredModels.Should().BeEmpty(); + } + + [Test] + public void should_not_delete_old_Delay_pending_items() + { + var pendingRelease = Builder.CreateNew() + .With(h => h.Reason = PendingReleaseReason.Delay) + .With(h => h.Added = DateTime.UtcNow.AddDays(-21)) + .With(h => h.ParsedAlbumInfo = new ParsedAlbumInfo()) + .With(h => h.Release = new ReleaseInfo()) + .BuildNew(); + + Db.Insert(pendingRelease); + Subject.Clean(); + AllStoredModels.Should().HaveCount(1); + } + } +} diff --git a/src/NzbDrone.Core.Test/Housekeeping/Housekeepers/CleanupDuplicateMetadataFilesFixture.cs b/src/NzbDrone.Core.Test/Housekeeping/Housekeepers/CleanupDuplicateMetadataFilesFixture.cs index 5bfeaefc0..2f4180a89 100644 --- a/src/NzbDrone.Core.Test/Housekeeping/Housekeepers/CleanupDuplicateMetadataFilesFixture.cs +++ b/src/NzbDrone.Core.Test/Housekeeping/Housekeepers/CleanupDuplicateMetadataFilesFixture.cs @@ -1,4 +1,4 @@ -using FizzWare.NBuilder; +using FizzWare.NBuilder; using FluentAssertions; using NUnit.Framework; using NzbDrone.Core.Extras.Metadata; @@ -12,12 +12,12 @@ namespace NzbDrone.Core.Test.Housekeeping.Housekeepers public class CleanupDuplicateMetadataFilesFixture : DbTest { [Test] - public void should_not_delete_metadata_files_when_they_are_for_the_same_series_but_different_consumers() + public void should_not_delete_metadata_files_when_they_are_for_the_same_artist_but_different_consumers() { var files = Builder.CreateListOfSize(2) .All() - .With(m => m.Type = MetadataType.SeriesMetadata) - .With(m => m.SeriesId = 1) + .With(m => m.Type = MetadataType.ArtistMetadata) + .With(m => m.ArtistId = 1) .BuildListOfNew(); Db.InsertMany(files); @@ -26,11 +26,11 @@ namespace NzbDrone.Core.Test.Housekeeping.Housekeepers } [Test] - public void should_not_delete_metadata_files_for_different_series() + public void should_not_delete_metadata_files_for_different_artist() { var files = Builder.CreateListOfSize(2) .All() - .With(m => m.Type = MetadataType.SeriesMetadata) + .With(m => m.Type = MetadataType.ArtistMetadata) .With(m => m.Consumer = "XbmcMetadata") .BuildListOfNew(); @@ -40,12 +40,12 @@ namespace NzbDrone.Core.Test.Housekeeping.Housekeepers } [Test] - public void should_delete_metadata_files_when_they_are_for_the_same_series_and_consumer() + public void should_delete_metadata_files_when_they_are_for_the_same_artist_and_consumer() { var files = Builder.CreateListOfSize(2) .All() - .With(m => m.Type = MetadataType.SeriesMetadata) - .With(m => m.SeriesId = 1) + .With(m => m.Type = MetadataType.ArtistMetadata) + .With(m => m.ArtistId = 1) .With(m => m.Consumer = "XbmcMetadata") .BuildListOfNew(); @@ -55,7 +55,7 @@ namespace NzbDrone.Core.Test.Housekeeping.Housekeepers } [Test] - public void should_not_delete_metadata_files_when_there_is_only_one_for_that_series_and_consumer() + public void should_not_delete_metadata_files_when_there_is_only_one_for_that_artist_and_consumer() { var file = Builder.CreateNew() .BuildNew(); @@ -66,12 +66,12 @@ namespace NzbDrone.Core.Test.Housekeeping.Housekeepers } [Test] - public void should_not_delete_metadata_files_when_they_are_for_the_same_episode_but_different_consumers() + public void should_not_delete_metadata_files_when_they_are_for_the_same_track_but_different_consumers() { var files = Builder.CreateListOfSize(2) .All() - .With(m => m.Type = MetadataType.EpisodeMetadata) - .With(m => m.EpisodeFileId = 1) + .With(m => m.Type = MetadataType.TrackMetadata) + .With(m => m.TrackFileId = 1) .BuildListOfNew(); Db.InsertMany(files); @@ -80,11 +80,11 @@ namespace NzbDrone.Core.Test.Housekeeping.Housekeepers } [Test] - public void should_not_delete_metadata_files_for_different_episode() + public void should_not_delete_metadata_files_for_different_track() { var files = Builder.CreateListOfSize(2) .All() - .With(m => m.Type = MetadataType.EpisodeMetadata) + .With(m => m.Type = MetadataType.TrackMetadata) .With(m => m.Consumer = "XbmcMetadata") .BuildListOfNew(); @@ -94,12 +94,12 @@ namespace NzbDrone.Core.Test.Housekeeping.Housekeepers } [Test] - public void should_delete_metadata_files_when_they_are_for_the_same_episode_and_consumer() + public void should_delete_metadata_files_when_they_are_for_the_same_track_and_consumer() { var files = Builder.CreateListOfSize(2) .All() - .With(m => m.Type = MetadataType.EpisodeMetadata) - .With(m => m.EpisodeFileId = 1) + .With(m => m.Type = MetadataType.TrackMetadata) + .With(m => m.TrackFileId = 1) .With(m => m.Consumer = "XbmcMetadata") .BuildListOfNew(); @@ -109,7 +109,7 @@ namespace NzbDrone.Core.Test.Housekeeping.Housekeepers } [Test] - public void should_not_delete_metadata_files_when_there_is_only_one_for_that_episode_and_consumer() + public void should_not_delete_metadata_files_when_there_is_only_one_for_that_track_and_consumer() { var file = Builder.CreateNew() .BuildNew(); @@ -120,12 +120,12 @@ namespace NzbDrone.Core.Test.Housekeeping.Housekeepers } [Test] - public void should_not_delete_image_when_they_are_for_the_same_episode_but_different_consumers() + public void should_not_delete_image_when_they_are_for_the_same_track_but_different_consumers() { var files = Builder.CreateListOfSize(2) .All() - .With(m => m.Type = MetadataType.EpisodeImage) - .With(m => m.EpisodeFileId = 1) + .With(m => m.Type = MetadataType.TrackImage) + .With(m => m.TrackFileId = 1) .BuildListOfNew(); Db.InsertMany(files); @@ -134,11 +134,11 @@ namespace NzbDrone.Core.Test.Housekeeping.Housekeepers } [Test] - public void should_not_delete_image_for_different_episode() + public void should_not_delete_image_for_different_track() { var files = Builder.CreateListOfSize(2) .All() - .With(m => m.Type = MetadataType.EpisodeImage) + .With(m => m.Type = MetadataType.TrackImage) .With(m => m.Consumer = "XbmcMetadata") .BuildListOfNew(); @@ -148,12 +148,12 @@ namespace NzbDrone.Core.Test.Housekeeping.Housekeepers } [Test] - public void should_delete_image_when_they_are_for_the_same_episode_and_consumer() + public void should_delete_image_when_they_are_for_the_same_track_and_consumer() { var files = Builder.CreateListOfSize(2) .All() - .With(m => m.Type = MetadataType.EpisodeImage) - .With(m => m.EpisodeFileId = 1) + .With(m => m.Type = MetadataType.TrackImage) + .With(m => m.TrackFileId = 1) .With(m => m.Consumer = "XbmcMetadata") .BuildListOfNew(); @@ -163,7 +163,7 @@ namespace NzbDrone.Core.Test.Housekeeping.Housekeepers } [Test] - public void should_not_delete_image_when_there_is_only_one_for_that_episode_and_consumer() + public void should_not_delete_image_when_there_is_only_one_for_that_track_and_consumer() { var file = Builder.CreateNew() .BuildNew(); diff --git a/src/NzbDrone.Core.Test/Housekeeping/Housekeepers/CleanupOrphanedIndexerStatusFixture.cs b/src/NzbDrone.Core.Test/Housekeeping/Housekeepers/CleanupOrphanedIndexerStatusFixture.cs index 189c1672d..c5e757188 100644 --- a/src/NzbDrone.Core.Test/Housekeeping/Housekeepers/CleanupOrphanedIndexerStatusFixture.cs +++ b/src/NzbDrone.Core.Test/Housekeeping/Housekeepers/CleanupOrphanedIndexerStatusFixture.cs @@ -28,7 +28,7 @@ namespace NzbDrone.Core.Test.Housekeeping.Housekeepers public void should_delete_orphaned_indexerstatus() { var status = Builder.CreateNew() - .With(h => h.IndexerId = _indexer.Id) + .With(h => h.ProviderId = _indexer.Id) .BuildNew(); Db.Insert(status); @@ -42,13 +42,13 @@ namespace NzbDrone.Core.Test.Housekeeping.Housekeepers GivenIndexer(); var status = Builder.CreateNew() - .With(h => h.IndexerId = _indexer.Id) + .With(h => h.ProviderId = _indexer.Id) .BuildNew(); Db.Insert(status); Subject.Clean(); AllStoredModels.Should().HaveCount(1); - AllStoredModels.Should().Contain(h => h.IndexerId == _indexer.Id); + AllStoredModels.Should().Contain(h => h.ProviderId == _indexer.Id); } } } \ No newline at end of file diff --git a/src/NzbDrone.Core.Test/Housekeeping/Housekeepers/CleanupOrphanedMetadataFilesFixture.cs b/src/NzbDrone.Core.Test/Housekeeping/Housekeepers/CleanupOrphanedMetadataFilesFixture.cs index 27679d8d3..843622445 100644 --- a/src/NzbDrone.Core.Test/Housekeeping/Housekeepers/CleanupOrphanedMetadataFilesFixture.cs +++ b/src/NzbDrone.Core.Test/Housekeeping/Housekeepers/CleanupOrphanedMetadataFilesFixture.cs @@ -1,4 +1,4 @@ -using FizzWare.NBuilder; +using FizzWare.NBuilder; using FluentAssertions; using NUnit.Framework; using NzbDrone.Core.Extras.Metadata; @@ -7,7 +7,7 @@ using NzbDrone.Core.Housekeeping.Housekeepers; using NzbDrone.Core.MediaFiles; using NzbDrone.Core.Qualities; using NzbDrone.Core.Test.Framework; -using NzbDrone.Core.Tv; +using NzbDrone.Core.Music; namespace NzbDrone.Core.Test.Housekeeping.Housekeepers { @@ -15,10 +15,10 @@ namespace NzbDrone.Core.Test.Housekeeping.Housekeepers public class CleanupOrphanedMetadataFilesFixture : DbTest { [Test] - public void should_delete_metadata_files_that_dont_have_a_coresponding_series() + public void should_delete_metadata_files_that_dont_have_a_coresponding_artist() { var metadataFile = Builder.CreateNew() - .With(m => m.EpisodeFileId = null) + .With(m => m.TrackFileId = null) .BuildNew(); Db.Insert(metadataFile); @@ -27,16 +27,59 @@ namespace NzbDrone.Core.Test.Housekeeping.Housekeepers } [Test] - public void should_not_delete_metadata_files_that_have_a_coresponding_series() + public void should_delete_metadata_files_that_dont_have_a_coresponding_album() { - var series = Builder.CreateNew() + var artist = Builder.CreateNew() .BuildNew(); - Db.Insert(series); + Db.Insert(artist); var metadataFile = Builder.CreateNew() - .With(m => m.SeriesId = series.Id) - .With(m => m.EpisodeFileId = null) + .With(m => m.ArtistId = artist.Id) + .With(m => m.TrackFileId = null) + .BuildNew(); + + Db.Insert(metadataFile); + Subject.Clean(); + AllStoredModels.Should().BeEmpty(); + } + + [Test] + public void should_not_delete_metadata_files_that_have_a_coresponding_artist() + { + var artist = Builder.CreateNew() + .BuildNew(); + + Db.Insert(artist); + + var metadataFile = Builder.CreateNew() + .With(m => m.ArtistId = artist.Id) + .With(m => m.AlbumId = null) + .With(m => m.TrackFileId = null) + .BuildNew(); + + Db.Insert(metadataFile); + var countMods = AllStoredModels.Count; + Subject.Clean(); + AllStoredModels.Should().HaveCount(1); + } + + [Test] + public void should_not_delete_metadata_files_that_have_a_coresponding_album() + { + var artist = Builder.CreateNew() + .BuildNew(); + + var album = Builder.CreateNew() + .BuildNew(); + + Db.Insert(artist); + Db.Insert(album); + + var metadataFile = Builder.CreateNew() + .With(m => m.ArtistId = artist.Id) + .With(m => m.AlbumId = album.Id) + .With(m => m.TrackFileId = null) .BuildNew(); Db.Insert(metadataFile); @@ -45,16 +88,16 @@ namespace NzbDrone.Core.Test.Housekeeping.Housekeepers } [Test] - public void should_delete_metadata_files_that_dont_have_a_coresponding_episode_file() + public void should_delete_metadata_files_that_dont_have_a_coresponding_track_file() { - var series = Builder.CreateNew() + var artist = Builder.CreateNew() .BuildNew(); - Db.Insert(series); + Db.Insert(artist); var metadataFile = Builder.CreateNew() - .With(m => m.SeriesId = series.Id) - .With(m => m.EpisodeFileId = 10) + .With(m => m.ArtistId = artist.Id) + .With(m => m.TrackFileId = 10) .BuildNew(); Db.Insert(metadataFile); @@ -63,21 +106,26 @@ namespace NzbDrone.Core.Test.Housekeeping.Housekeepers } [Test] - public void should_not_delete_metadata_files_that_have_a_coresponding_episode_file() + public void should_not_delete_metadata_files_that_have_a_coresponding_track_file() { - var series = Builder.CreateNew() + var artist = Builder.CreateNew() + .BuildNew(); + + var album = Builder.CreateNew() .BuildNew(); - var episodeFile = Builder.CreateNew() + var trackFile = Builder.CreateNew() .With(h => h.Quality = new QualityModel()) .BuildNew(); - Db.Insert(series); - Db.Insert(episodeFile); + Db.Insert(artist); + Db.Insert(album); + Db.Insert(trackFile); var metadataFile = Builder.CreateNew() - .With(m => m.SeriesId = series.Id) - .With(m => m.EpisodeFileId = episodeFile.Id) + .With(m => m.ArtistId = artist.Id) + .With(m => m.AlbumId = album.Id) + .With(m => m.TrackFileId = trackFile.Id) .BuildNew(); Db.Insert(metadataFile); @@ -86,17 +134,17 @@ namespace NzbDrone.Core.Test.Housekeeping.Housekeepers } [Test] - public void should_delete_episode_metadata_files_that_have_episodefileid_of_zero() + public void should_delete_track_metadata_files_that_have_trackfileid_of_zero() { - var series = Builder.CreateNew() + var artist = Builder.CreateNew() .BuildNew(); - Db.Insert(series); + Db.Insert(artist); var metadataFile = Builder.CreateNew() - .With(m => m.SeriesId = series.Id) - .With(m => m.Type = MetadataType.EpisodeMetadata) - .With(m => m.EpisodeFileId = 0) + .With(m => m.ArtistId = artist.Id) + .With(m => m.Type = MetadataType.TrackMetadata) + .With(m => m.TrackFileId = 0) .BuildNew(); Db.Insert(metadataFile); @@ -105,17 +153,17 @@ namespace NzbDrone.Core.Test.Housekeeping.Housekeepers } [Test] - public void should_delete_episode_image_files_that_have_episodefileid_of_zero() + public void should_delete_track_image_files_that_have_trackfileid_of_zero() { - var series = Builder.CreateNew() + var artist = Builder.CreateNew() .BuildNew(); - Db.Insert(series); + Db.Insert(artist); var metadataFile = Builder.CreateNew() - .With(m => m.SeriesId = series.Id) - .With(m => m.Type = MetadataType.EpisodeImage) - .With(m => m.EpisodeFileId = 0) + .With(m => m.ArtistId = artist.Id) + .With(m => m.Type = MetadataType.TrackImage) + .With(m => m.TrackFileId = 0) .BuildNew(); Db.Insert(metadataFile); diff --git a/src/NzbDrone.Core.Test/Housekeeping/Housekeepers/FixFutureDownloadClientStatusTimesFixture.cs b/src/NzbDrone.Core.Test/Housekeeping/Housekeepers/FixFutureDownloadClientStatusTimesFixture.cs new file mode 100644 index 000000000..96c3f0676 --- /dev/null +++ b/src/NzbDrone.Core.Test/Housekeeping/Housekeepers/FixFutureDownloadClientStatusTimesFixture.cs @@ -0,0 +1,119 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using FizzWare.NBuilder; +using Moq; +using NUnit.Framework; +using NzbDrone.Core.Download; +using NzbDrone.Core.Housekeeping.Housekeepers; +using NzbDrone.Core.Test.Framework; +using NzbDrone.Core.ThingiProvider.Status; + +namespace NzbDrone.Core.Test.Housekeeping.Housekeepers +{ + [TestFixture] + public class FixFutureDownloadClientStatusTimesFixture : CoreTest + { + [Test] + public void should_set_disabled_till_when_its_too_far_in_the_future() + { + var disabledTillTime = EscalationBackOff.Periods[1]; + var downloadClientStatuses = Builder.CreateListOfSize(5) + .All() + .With(t => t.DisabledTill = DateTime.UtcNow.AddDays(5)) + .With(t => t.InitialFailure = DateTime.UtcNow.AddDays(-5)) + .With(t => t.MostRecentFailure = DateTime.UtcNow.AddDays(-5)) + .With(t => t.EscalationLevel = 1) + .BuildListOfNew(); + + Mocker.GetMock() + .Setup(s => s.All()) + .Returns(downloadClientStatuses); + + Subject.Clean(); + + Mocker.GetMock() + .Verify(v => v.UpdateMany( + It.Is>(i => i.All( + s => s.DisabledTill.Value < DateTime.UtcNow.AddMinutes(disabledTillTime))) + ) + ); + } + + [Test] + public void should_set_initial_failure_when_its_in_the_future() + { + var downloadClientStatuses = Builder.CreateListOfSize(5) + .All() + .With(t => t.DisabledTill = DateTime.UtcNow.AddDays(-5)) + .With(t => t.InitialFailure = DateTime.UtcNow.AddDays(5)) + .With(t => t.MostRecentFailure = DateTime.UtcNow.AddDays(-5)) + .With(t => t.EscalationLevel = 1) + .BuildListOfNew(); + + Mocker.GetMock() + .Setup(s => s.All()) + .Returns(downloadClientStatuses); + + Subject.Clean(); + + Mocker.GetMock() + .Verify(v => v.UpdateMany( + It.Is>(i => i.All( + s => s.InitialFailure.Value < DateTime.UtcNow)) + ) + ); + } + + [Test] + public void should_set_most_recent_failure_when_its_in_the_future() + { + var downloadClientStatuses = Builder.CreateListOfSize(5) + .All() + .With(t => t.DisabledTill = DateTime.UtcNow.AddDays(-5)) + .With(t => t.InitialFailure = DateTime.UtcNow.AddDays(-5)) + .With(t => t.MostRecentFailure = DateTime.UtcNow.AddDays(5)) + .With(t => t.EscalationLevel = 1) + .BuildListOfNew(); + + Mocker.GetMock() + .Setup(s => s.All()) + .Returns(downloadClientStatuses); + + Subject.Clean(); + + Mocker.GetMock() + .Verify(v => v.UpdateMany( + It.Is>(i => i.All( + s => s.MostRecentFailure.Value < DateTime.UtcNow)) + ) + ); + } + + [Test] + public void should_not_change_statuses_when_times_are_in_the_past() + { + var downloadClientStatuses = Builder.CreateListOfSize(5) + .All() + .With(t => t.DisabledTill = DateTime.UtcNow.AddDays(-5)) + .With(t => t.InitialFailure = DateTime.UtcNow.AddDays(-5)) + .With(t => t.MostRecentFailure = DateTime.UtcNow.AddDays(-5)) + .With(t => t.EscalationLevel = 0) + .BuildListOfNew(); + + Mocker.GetMock() + .Setup(s => s.All()) + .Returns(downloadClientStatuses); + + Subject.Clean(); + + Mocker.GetMock() + .Verify(v => v.UpdateMany( + It.Is>(i => i.Count == 0) + ) + ); + } + + + } +} \ No newline at end of file diff --git a/src/NzbDrone.Core.Test/Housekeeping/Housekeepers/FixFutureIndexerStatusTimesFixture.cs b/src/NzbDrone.Core.Test/Housekeeping/Housekeepers/FixFutureIndexerStatusTimesFixture.cs new file mode 100644 index 000000000..c25494cc3 --- /dev/null +++ b/src/NzbDrone.Core.Test/Housekeeping/Housekeepers/FixFutureIndexerStatusTimesFixture.cs @@ -0,0 +1,119 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using FizzWare.NBuilder; +using Moq; +using NUnit.Framework; +using NzbDrone.Core.Housekeeping.Housekeepers; +using NzbDrone.Core.Indexers; +using NzbDrone.Core.Test.Framework; +using NzbDrone.Core.ThingiProvider.Status; + +namespace NzbDrone.Core.Test.Housekeeping.Housekeepers +{ + [TestFixture] + public class FixFutureIndexerStatusTimesFixture : CoreTest + { + [Test] + public void should_set_disabled_till_when_its_too_far_in_the_future() + { + var disabledTillTime = EscalationBackOff.Periods[1]; + var indexerStatuses = Builder.CreateListOfSize(5) + .All() + .With(t => t.DisabledTill = DateTime.UtcNow.AddDays(5)) + .With(t => t.InitialFailure = DateTime.UtcNow.AddDays(-5)) + .With(t => t.MostRecentFailure = DateTime.UtcNow.AddDays(-5)) + .With(t => t.EscalationLevel = 1) + .BuildListOfNew(); + + Mocker.GetMock() + .Setup(s => s.All()) + .Returns(indexerStatuses); + + Subject.Clean(); + + Mocker.GetMock() + .Verify(v => v.UpdateMany( + It.Is>(i => i.All( + s => s.DisabledTill.Value < DateTime.UtcNow.AddMinutes(disabledTillTime))) + ) + ); + } + + [Test] + public void should_set_initial_failure_when_its_in_the_future() + { + var indexerStatuses = Builder.CreateListOfSize(5) + .All() + .With(t => t.DisabledTill = DateTime.UtcNow.AddDays(-5)) + .With(t => t.InitialFailure = DateTime.UtcNow.AddDays(5)) + .With(t => t.MostRecentFailure = DateTime.UtcNow.AddDays(-5)) + .With(t => t.EscalationLevel = 1) + .BuildListOfNew(); + + Mocker.GetMock() + .Setup(s => s.All()) + .Returns(indexerStatuses); + + Subject.Clean(); + + Mocker.GetMock() + .Verify(v => v.UpdateMany( + It.Is>(i => i.All( + s => s.InitialFailure.Value < DateTime.UtcNow)) + ) + ); + } + + [Test] + public void should_set_most_recent_failure_when_its_in_the_future() + { + var indexerStatuses = Builder.CreateListOfSize(5) + .All() + .With(t => t.DisabledTill = DateTime.UtcNow.AddDays(-5)) + .With(t => t.InitialFailure = DateTime.UtcNow.AddDays(-5)) + .With(t => t.MostRecentFailure = DateTime.UtcNow.AddDays(5)) + .With(t => t.EscalationLevel = 1) + .BuildListOfNew(); + + Mocker.GetMock() + .Setup(s => s.All()) + .Returns(indexerStatuses); + + Subject.Clean(); + + Mocker.GetMock() + .Verify(v => v.UpdateMany( + It.Is>(i => i.All( + s => s.MostRecentFailure.Value < DateTime.UtcNow)) + ) + ); + } + + [Test] + public void should_not_change_statuses_when_times_are_in_the_past() + { + var indexerStatuses = Builder.CreateListOfSize(5) + .All() + .With(t => t.DisabledTill = DateTime.UtcNow.AddDays(-5)) + .With(t => t.InitialFailure = DateTime.UtcNow.AddDays(-5)) + .With(t => t.MostRecentFailure = DateTime.UtcNow.AddDays(-5)) + .With(t => t.EscalationLevel = 0) + .BuildListOfNew(); + + Mocker.GetMock() + .Setup(s => s.All()) + .Returns(indexerStatuses); + + Subject.Clean(); + + Mocker.GetMock() + .Verify(v => v.UpdateMany( + It.Is>(i => i.Count == 0) + ) + ); + } + + + } +} \ No newline at end of file diff --git a/src/NzbDrone.Core.Test/Housekeeping/Housekeepers/FixFutureRunScheduledTasksFixture.cs b/src/NzbDrone.Core.Test/Housekeeping/Housekeepers/FixFutureRunScheduledTasksFixture.cs deleted file mode 100644 index 4235b217e..000000000 --- a/src/NzbDrone.Core.Test/Housekeeping/Housekeepers/FixFutureRunScheduledTasksFixture.cs +++ /dev/null @@ -1,47 +0,0 @@ -using System; -using FizzWare.NBuilder; -using FluentAssertions; -using Microsoft.Practices.ObjectBuilder2; -using NUnit.Framework; -using NzbDrone.Core.Housekeeping.Housekeepers; -using NzbDrone.Core.Jobs; -using NzbDrone.Core.Test.Framework; - -namespace NzbDrone.Core.Test.Housekeeping.Housekeepers -{ - [TestFixture] - public class FixFutureRunScheduledTasksFixture : DbTest - { - [Test] - public void should_set_last_execution_time_to_now_when_its_in_the_future() - { - var tasks = Builder.CreateListOfSize(5) - .All() - .With(t => t.LastExecution = DateTime.UtcNow.AddDays(5)) - .BuildListOfNew(); - - Db.InsertMany(tasks); - - Subject.Clean(); - - AllStoredModels.ForEach(t => t.LastExecution.Should().BeBefore(DateTime.UtcNow)); - } - - [Test] - public void should_not_change_last_execution_time_when_its_in_the_past() - { - var expectedTime = DateTime.UtcNow.AddHours(-1); - - var tasks = Builder.CreateListOfSize(5) - .All() - .With(t => t.LastExecution = expectedTime) - .BuildListOfNew(); - - Db.InsertMany(tasks); - - Subject.Clean(); - - AllStoredModels.ForEach(t => t.LastExecution.Should().Be(expectedTime)); - } - } -} \ No newline at end of file diff --git a/src/NzbDrone.Core.Test/IndexerSearchTests/ArtistSearchServiceFixture.cs b/src/NzbDrone.Core.Test/IndexerSearchTests/ArtistSearchServiceFixture.cs index 102728827..1bfc87813 100644 --- a/src/NzbDrone.Core.Test/IndexerSearchTests/ArtistSearchServiceFixture.cs +++ b/src/NzbDrone.Core.Test/IndexerSearchTests/ArtistSearchServiceFixture.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using FluentAssertions; using Moq; @@ -7,7 +7,6 @@ using NzbDrone.Core.DecisionEngine; using NzbDrone.Core.Download; using NzbDrone.Core.IndexerSearch; using NzbDrone.Core.Test.Framework; -using NzbDrone.Core.Tv; using NzbDrone.Core.Messaging.Commands; using NzbDrone.Core.Music; diff --git a/src/NzbDrone.Core.Test/IndexerTests/BasicRssParserFixture.cs b/src/NzbDrone.Core.Test/IndexerTests/BasicRssParserFixture.cs index b2819434d..d75079fbd 100644 --- a/src/NzbDrone.Core.Test/IndexerTests/BasicRssParserFixture.cs +++ b/src/NzbDrone.Core.Test/IndexerTests/BasicRssParserFixture.cs @@ -1,4 +1,4 @@ -using System.Linq; +using System.Linq; using System.Text; using FluentAssertions; using NUnit.Framework; @@ -30,6 +30,7 @@ namespace NzbDrone.Core.Test.IndexerTests [TestCase("100 Kb/s")] [TestCase(" 12341234")] [TestCase("12341234 other")] + [TestCase("")] public void should_not_parse_size(string sizeString) { var result = RssParser.ParseSize(sizeString, true); @@ -58,4 +59,4 @@ namespace NzbDrone.Core.Test.IndexerTests result.First().DownloadUrl.Should().Be("http://my.indexer.com/getnzb/123.nzb&i=782&r=123"); } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Core.Test/IndexerTests/IPTorrentsTests/IPTorrentsFixture.cs b/src/NzbDrone.Core.Test/IndexerTests/IPTorrentsTests/IPTorrentsFixture.cs index d48c06f6c..7b5b4fc1a 100644 --- a/src/NzbDrone.Core.Test/IndexerTests/IPTorrentsTests/IPTorrentsFixture.cs +++ b/src/NzbDrone.Core.Test/IndexerTests/IPTorrentsTests/IPTorrentsFixture.cs @@ -20,7 +20,7 @@ namespace NzbDrone.Core.Test.IndexerTests.IPTorrentsTests Subject.Definition = new IndexerDefinition() { Name = "IPTorrents", - Settings = new IPTorrentsSettings() { Url = "http://fake.com/" } + Settings = new IPTorrentsSettings() { BaseUrl = "http://fake.com/" } }; } diff --git a/src/NzbDrone.Core.Test/IndexerTests/IndexerStatusServiceFixture.cs b/src/NzbDrone.Core.Test/IndexerTests/IndexerStatusServiceFixture.cs index d7bee11f2..3f1699c9f 100644 --- a/src/NzbDrone.Core.Test/IndexerTests/IndexerStatusServiceFixture.cs +++ b/src/NzbDrone.Core.Test/IndexerTests/IndexerStatusServiceFixture.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Linq; using FluentAssertions; using Moq; @@ -11,7 +11,7 @@ namespace NzbDrone.Core.Test.IndexerTests public class IndexerStatusServiceFixture : CoreTest { private DateTime _epoch; - + [SetUp] public void SetUp() { @@ -21,7 +21,7 @@ namespace NzbDrone.Core.Test.IndexerTests private void WithStatus(IndexerStatus status) { Mocker.GetMock() - .Setup(v => v.FindByIndexerId(1)) + .Setup(v => v.FindByProviderId(1)) .Returns(status); Mocker.GetMock() @@ -29,25 +29,16 @@ namespace NzbDrone.Core.Test.IndexerTests .Returns(new[] { status }); } - private void VerifyUpdate(bool updated = true) + private void VerifyUpdate() { Mocker.GetMock() - .Verify(v => v.Upsert(It.IsAny()), Times.Exactly(updated ? 1 : 0)); + .Verify(v => v.Upsert(It.IsAny()), Times.Once()); } - [Test] - public void should_start_backoff_on_first_failure() + private void VerifyNoUpdate() { - WithStatus(new IndexerStatus()); - - Subject.RecordFailure(1); - - VerifyUpdate(); - - var status = Subject.GetBlockedIndexers().FirstOrDefault(); - status.Should().NotBeNull(); - status.DisabledTill.Should().HaveValue(); - status.DisabledTill.Value.Should().BeCloseTo(_epoch + TimeSpan.FromMinutes(5), 500); + Mocker.GetMock() + .Verify(v => v.Upsert(It.IsAny()), Times.Never()); } [Test] @@ -59,7 +50,7 @@ namespace NzbDrone.Core.Test.IndexerTests VerifyUpdate(); - var status = Subject.GetBlockedIndexers().FirstOrDefault(); + var status = Subject.GetBlockedProviders().FirstOrDefault(); status.Should().BeNull(); } @@ -70,22 +61,7 @@ namespace NzbDrone.Core.Test.IndexerTests Subject.RecordSuccess(1); - VerifyUpdate(false); - } - - [Test] - public void should_preserve_escalation_on_intermittent_success() - { - WithStatus(new IndexerStatus { MostRecentFailure = _epoch - TimeSpan.FromSeconds(4), EscalationLevel = 3 }); - - Subject.RecordSuccess(1); - Subject.RecordSuccess(1); - Subject.RecordFailure(1); - - var status = Subject.GetBlockedIndexers().FirstOrDefault(); - status.Should().NotBeNull(); - status.DisabledTill.Should().HaveValue(); - status.DisabledTill.Value.Should().BeCloseTo(_epoch + TimeSpan.FromMinutes(15), 500); + VerifyNoUpdate(); } } } diff --git a/src/NzbDrone.Core.Test/IndexerTests/NewznabTests/NewznabCapabilitiesProviderFixture.cs b/src/NzbDrone.Core.Test/IndexerTests/NewznabTests/NewznabCapabilitiesProviderFixture.cs index 15f93b0c0..f7afd220d 100644 --- a/src/NzbDrone.Core.Test/IndexerTests/NewznabTests/NewznabCapabilitiesProviderFixture.cs +++ b/src/NzbDrone.Core.Test/IndexerTests/NewznabTests/NewznabCapabilitiesProviderFixture.cs @@ -21,7 +21,7 @@ namespace NzbDrone.Core.Test.IndexerTests.NewznabTests { _settings = new NewznabSettings() { - Url = "http://indxer.local" + BaseUrl = "http://indxer.local" }; _caps = ReadAllText("Files/Indexers/Newznab/newznab_caps.xml"); diff --git a/src/NzbDrone.Core.Test/IndexerTests/NewznabTests/NewznabFixture.cs b/src/NzbDrone.Core.Test/IndexerTests/NewznabTests/NewznabFixture.cs index 5fcfb4947..4f63dc2ce 100644 --- a/src/NzbDrone.Core.Test/IndexerTests/NewznabTests/NewznabFixture.cs +++ b/src/NzbDrone.Core.Test/IndexerTests/NewznabTests/NewznabFixture.cs @@ -1,5 +1,6 @@ -using System; +using System; using System.Linq; +using System.Net; using FluentAssertions; using Moq; using NUnit.Framework; @@ -7,6 +8,7 @@ using NzbDrone.Common.Http; using NzbDrone.Core.Indexers; using NzbDrone.Core.Indexers.Newznab; using NzbDrone.Core.Test.Framework; +using NzbDrone.Test.Common; namespace NzbDrone.Core.Test.IndexerTests.NewznabTests { @@ -24,7 +26,7 @@ namespace NzbDrone.Core.Test.IndexerTests.NewznabTests Name = "Newznab", Settings = new NewznabSettings() { - Url = "http://indexer.local/", + BaseUrl = "http://indexer.local/", Categories = new int[] { 1 } } }; @@ -69,5 +71,27 @@ namespace NzbDrone.Core.Test.IndexerTests.NewznabTests Subject.PageSize.Should().Be(25); } - } + + [Test] + public void should_record_indexer_failure_if_caps_throw() + { + var request = new HttpRequest("http://my.indexer.com"); + var response = new HttpResponse(request, new HttpHeader(), new byte[0], (HttpStatusCode)429); + response.Headers["Retry-After"] = "300"; + + Mocker.GetMock() + .Setup(v => v.GetCapabilities(It.IsAny())) + .Throws(new TooManyRequestsException(request, response)); + + _caps.MaxPageSize = 30; + _caps.DefaultPageSize = 25; + + Subject.FetchRecent().Should().BeEmpty(); + + Mocker.GetMock() + .Verify(v => v.RecordFailure(It.IsAny(), TimeSpan.FromMinutes(5.0)), Times.Once()); + + ExceptionVerification.ExpectedWarns(1); + } +} } diff --git a/src/NzbDrone.Core.Test/IndexerTests/NewznabTests/NewznabRequestGeneratorFixture.cs b/src/NzbDrone.Core.Test/IndexerTests/NewznabTests/NewznabRequestGeneratorFixture.cs index 208675279..f36ad951b 100644 --- a/src/NzbDrone.Core.Test/IndexerTests/NewznabTests/NewznabRequestGeneratorFixture.cs +++ b/src/NzbDrone.Core.Test/IndexerTests/NewznabTests/NewznabRequestGeneratorFixture.cs @@ -19,7 +19,7 @@ namespace NzbDrone.Core.Test.IndexerTests.NewznabTests { Subject.Settings = new NewznabSettings() { - Url = "http://127.0.0.1:1234/", + BaseUrl = "http://127.0.0.1:1234/", Categories = new [] { 1, 2 }, ApiKey = "abcd", }; diff --git a/src/NzbDrone.Core.Test/IndexerTests/NewznabTests/NewznabSettingFixture.cs b/src/NzbDrone.Core.Test/IndexerTests/NewznabTests/NewznabSettingFixture.cs index 4bd26817d..d1d5bb8fe 100644 --- a/src/NzbDrone.Core.Test/IndexerTests/NewznabTests/NewznabSettingFixture.cs +++ b/src/NzbDrone.Core.Test/IndexerTests/NewznabTests/NewznabSettingFixture.cs @@ -15,7 +15,7 @@ namespace NzbDrone.Core.Test.IndexerTests.NewznabTests var setting = new NewznabSettings() { ApiKey = "", - Url = url + BaseUrl = url }; @@ -32,7 +32,7 @@ namespace NzbDrone.Core.Test.IndexerTests.NewznabTests var setting = new NewznabSettings { ApiKey = "", - Url = url + BaseUrl = url }; @@ -49,7 +49,7 @@ namespace NzbDrone.Core.Test.IndexerTests.NewznabTests var setting = new NewznabSettings() { ApiKey = "", - Url = url + BaseUrl = url }; diff --git a/src/NzbDrone.Core.Test/IndexerTests/TestIndexerSettings.cs b/src/NzbDrone.Core.Test/IndexerTests/TestIndexerSettings.cs index 3006c6b36..2e9e1fa82 100644 --- a/src/NzbDrone.Core.Test/IndexerTests/TestIndexerSettings.cs +++ b/src/NzbDrone.Core.Test/IndexerTests/TestIndexerSettings.cs @@ -1,14 +1,17 @@ -using System; +using System; +using NzbDrone.Core.Indexers; using NzbDrone.Core.ThingiProvider; using NzbDrone.Core.Validation; namespace NzbDrone.Core.Test.IndexerTests { - public class TestIndexerSettings : IProviderConfig + public class TestIndexerSettings : IIndexerSettings { public NzbDroneValidationResult Validate() { throw new NotImplementedException(); } + + public string BaseUrl { get; set; } } } diff --git a/src/NzbDrone.Core.Test/IndexerTests/TorrentRssIndexerTests/TorrentRssIndexerFixture.cs b/src/NzbDrone.Core.Test/IndexerTests/TorrentRssIndexerTests/TorrentRssIndexerFixture.cs index 23d653e5a..169d40888 100644 --- a/src/NzbDrone.Core.Test/IndexerTests/TorrentRssIndexerTests/TorrentRssIndexerFixture.cs +++ b/src/NzbDrone.Core.Test/IndexerTests/TorrentRssIndexerTests/TorrentRssIndexerFixture.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Linq; using FluentAssertions; using Moq; @@ -262,6 +262,33 @@ namespace NzbDrone.Core.Test.IndexerTests.TorrentRssIndexerTests } [Test] + public void should_parse_recent_feed_from_EveolutionWorld_without_size() + { + Subject.Definition.Settings.As().AllowZeroSize = true; + GivenRecentFeedResponse("TorrentRss/EvolutionWorld.xml"); + + var releases = Subject.FetchRecent(); + + releases.Should().HaveCount(2); + releases.First().Should().BeOfType(); + + var torrentInfo = releases.First() as TorrentInfo; + + torrentInfo.Title.Should().Be("[TVShow --> TVShow Bluray 720p] Fargo S01 Complete Season 1 720p BRRip DD5.1 x264-PSYPHER [SEEDERS (3)/LEECHERS (0)]"); + torrentInfo.DownloadProtocol.Should().Be(DownloadProtocol.Torrent); + torrentInfo.DownloadUrl.Should().Be("http://ew.pw/download.php?id=dea071a7a62a0d662538d46402fb112f30b8c9fa&f=Fargo%20S01%20Complete%20Season%201%20720p%20BRRip%20DD5.1%20x264-PSYPHER.torrent&auth=secret"); + torrentInfo.InfoUrl.Should().BeNullOrEmpty(); + torrentInfo.CommentUrl.Should().BeNullOrEmpty(); + torrentInfo.Indexer.Should().Be(Subject.Definition.Name); + torrentInfo.PublishDate.Should().Be(DateTime.Parse("2017-08-13T22:21:43Z").ToUniversalTime()); + torrentInfo.Size.Should().Be(0); + torrentInfo.InfoHash.Should().BeNull(); + torrentInfo.MagnetUrl.Should().BeNull(); + torrentInfo.Peers.Should().NotHaveValue(); + torrentInfo.Seeders.Should().NotHaveValue(); + } + + [Test] public void should_record_indexer_failure_if_unsupported_feed() { GivenRecentFeedResponse("TorrentRss/invalid/TorrentDay_NoPubDate.xml"); diff --git a/src/NzbDrone.Core.Test/IndexerTests/TorrentRssIndexerTests/TorrentRssSettingsDetectorFixture.cs b/src/NzbDrone.Core.Test/IndexerTests/TorrentRssIndexerTests/TorrentRssSettingsDetectorFixture.cs index 74934b160..e8222f7d5 100644 --- a/src/NzbDrone.Core.Test/IndexerTests/TorrentRssIndexerTests/TorrentRssSettingsDetectorFixture.cs +++ b/src/NzbDrone.Core.Test/IndexerTests/TorrentRssIndexerTests/TorrentRssSettingsDetectorFixture.cs @@ -1,4 +1,4 @@ -using FluentAssertions; +using FluentAssertions; using Moq; using NUnit.Framework; using NzbDrone.Common.Http; @@ -253,12 +253,9 @@ namespace NzbDrone.Core.Test.IndexerTests.TorrentRssIndexerTests }); } - [TestCase("BitMeTv/BitMeTv.xml")] - [TestCase("Fanzub/fanzub.xml")] [TestCase("IPTorrents/IPTorrents.xml")] [TestCase("Newznab/newznab_nzb_su.xml")] [TestCase("Nyaa/Nyaa.xml")] - [TestCase("Omgwtfnzbs/Omgwtfnzbs.xml")] [TestCase("Torznab/torznab_hdaccess_net.xml")] [TestCase("Torznab/torznab_tpb.xml")] public void should_detect_recent_feed(string rssXmlFile) diff --git a/src/NzbDrone.Core.Test/IndexerTests/TorznabTests/TorznabFixture.cs b/src/NzbDrone.Core.Test/IndexerTests/TorznabTests/TorznabFixture.cs index 95963a75f..4c22d22bb 100644 --- a/src/NzbDrone.Core.Test/IndexerTests/TorznabTests/TorznabFixture.cs +++ b/src/NzbDrone.Core.Test/IndexerTests/TorznabTests/TorznabFixture.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Linq; using FluentAssertions; using Moq; @@ -25,7 +25,7 @@ namespace NzbDrone.Core.Test.IndexerTests.TorznabTests Name = "Torznab", Settings = new TorznabSettings() { - Url = "http://indexer.local/", + BaseUrl = "http://indexer.local/", Categories = new int[] { 1 } } }; @@ -44,7 +44,7 @@ namespace NzbDrone.Core.Test.IndexerTests.TorznabTests Mocker.GetMock() .Setup(o => o.Execute(It.Is(v => v.Method == HttpMethod.GET))) .Returns(r => new HttpResponse(r, new HttpHeader(), recentFeed)); - + var releases = Subject.FetchRecent(); releases.Should().HaveCount(5); diff --git a/src/NzbDrone.Core.Test/Instrumentation/DatabaseTargetFixture.cs b/src/NzbDrone.Core.Test/Instrumentation/DatabaseTargetFixture.cs index 716b5c042..d67035ffb 100644 --- a/src/NzbDrone.Core.Test/Instrumentation/DatabaseTargetFixture.cs +++ b/src/NzbDrone.Core.Test/Instrumentation/DatabaseTargetFixture.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Threading; using FluentAssertions; using Marr.Data; @@ -118,7 +118,7 @@ namespace NzbDrone.Core.Test.Instrumentation [Test] public void null_string_as_arg_should_not_fail() { - var epFile = new EpisodeFile(); + var epFile = new TrackFile(); _logger.Debug("File {0} no longer exists on disk. removing from database.", epFile.RelativePath); Thread.Sleep(600); diff --git a/src/NzbDrone.Core.Test/Languages/LanguageFixture.cs b/src/NzbDrone.Core.Test/Languages/LanguageFixture.cs new file mode 100644 index 000000000..4db1719df --- /dev/null +++ b/src/NzbDrone.Core.Test/Languages/LanguageFixture.cs @@ -0,0 +1,100 @@ +using System.Linq; +using System.Collections.Generic; +using FluentAssertions; +using NUnit.Framework; +using NzbDrone.Core.Profiles.Languages; +using NzbDrone.Core.Test.Framework; +using NzbDrone.Core.Languages; + +namespace NzbDrone.Core.Test.Languages +{ + [TestFixture] + public class LanguageFixture : CoreTest + { + public static object[] FromIntCases = + { + new object[] {1, Language.English}, + new object[] {2, Language.French}, + new object[] {3, Language.Spanish}, + new object[] {4, Language.German}, + new object[] {5, Language.Italian}, + new object[] {6, Language.Danish}, + new object[] {7, Language.Dutch}, + new object[] {8, Language.Japanese}, + new object[] {9, Language.Cantonese}, + new object[] {10, Language.Mandarin}, + new object[] {11, Language.Russian}, + new object[] {12, Language.Polish}, + new object[] {13, Language.Vietnamese}, + new object[] {14, Language.Swedish}, + new object[] {15, Language.Norwegian}, + new object[] {16, Language.Finnish}, + new object[] {17, Language.Turkish}, + new object[] {18, Language.Portuguese}, + new object[] {19, Language.Flemish}, + new object[] {20, Language.Greek}, + new object[] {21, Language.Korean}, + new object[] {22, Language.Hungarian} + }; + + public static object[] ToIntCases = + { + new object[] {Language.English, 1}, + new object[] {Language.French, 2}, + new object[] {Language.Spanish, 3}, + new object[] {Language.German, 4}, + new object[] {Language.Italian, 5}, + new object[] {Language.Danish, 6}, + new object[] {Language.Dutch, 7}, + new object[] {Language.Japanese, 8}, + new object[] {Language.Cantonese, 9}, + new object[] {Language.Mandarin, 10}, + new object[] {Language.Russian, 11}, + new object[] {Language.Polish, 12}, + new object[] {Language.Vietnamese, 13}, + new object[] {Language.Swedish, 14}, + new object[] {Language.Norwegian, 15}, + new object[] {Language.Finnish, 16}, + new object[] {Language.Turkish, 17}, + new object[] {Language.Portuguese, 18}, + new object[] {Language.Flemish, 19}, + new object[] {Language.Greek, 20}, + new object[] {Language.Korean, 21}, + new object[] {Language.Hungarian, 22} + }; + + [Test, TestCaseSource("FromIntCases")] + public void should_be_able_to_convert_int_to_languageTypes(int source, Language expected) + { + var language = (Language)source; + language.Should().Be(expected); + } + + [Test, TestCaseSource("ToIntCases")] + public void should_be_able_to_convert_languageTypes_to_int(Language source, int expected) + { + var i = (int)source; + i.Should().Be(expected); + } + + public static List GetDefaultLanguages(params Language[] allowed) + { + var languages = new List + { + Language.English, + Language.Spanish, + Language.French + }; + + if (allowed.Length == 0) + allowed = languages.ToArray(); + + var items = languages + .Except(allowed) + .Concat(allowed) + .Select(v => new ProfileLanguageItem { Language = v, Allowed = allowed.Contains(v) }).ToList(); + + return items; + } + } +} diff --git a/src/NzbDrone.Core.Test/Languages/LanguageProfileRepositoryFixture.cs b/src/NzbDrone.Core.Test/Languages/LanguageProfileRepositoryFixture.cs new file mode 100644 index 000000000..c262d1eeb --- /dev/null +++ b/src/NzbDrone.Core.Test/Languages/LanguageProfileRepositoryFixture.cs @@ -0,0 +1,32 @@ +using FluentAssertions; +using System.Linq; +using NUnit.Framework; +using NzbDrone.Core.Languages; +using NzbDrone.Core.Test.Framework; +using NzbDrone.Core.Profiles.Languages; + +namespace NzbDrone.Core.Test.Languages +{ + [TestFixture] + public class LanguageProfileRepositoryFixture : DbTest + { + [Test] + public void should_be_able_to_read_and_write() + { + var profile = new LanguageProfile + { + Languages = Language.All.OrderByDescending(l => l.Name).Select(l => new ProfileLanguageItem {Language = l, Allowed = l == Language.English}).ToList(), + Name = "TestProfile", + Cutoff = Language.English + }; + + Subject.Insert(profile); + + + StoredModel.Name.Should().Be(profile.Name); + StoredModel.Cutoff.Should().Be(profile.Cutoff); + + StoredModel.Languages.Should().Equal(profile.Languages, (a, b) => a.Language == b.Language && a.Allowed == b.Allowed); + } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Core.Test/Languages/LanguageProfileServiceFixture.cs b/src/NzbDrone.Core.Test/Languages/LanguageProfileServiceFixture.cs new file mode 100644 index 000000000..94ff97dcb --- /dev/null +++ b/src/NzbDrone.Core.Test/Languages/LanguageProfileServiceFixture.cs @@ -0,0 +1,75 @@ +using System.Linq; +using FizzWare.NBuilder; +using Moq; +using NUnit.Framework; +using NzbDrone.Core.Lifecycle; +using NzbDrone.Core.Test.Framework; +using NzbDrone.Core.Music; +using NzbDrone.Core.Profiles.Languages; + +namespace NzbDrone.Core.Test.Languages +{ + [TestFixture] + + public class LanguageProfileServiceFixture : CoreTest + { + [Test] + public void init_should_add_default_profiles() + { + Subject.Handle(new ApplicationStartedEvent()); + + Mocker.GetMock() + .Verify(v => v.Insert(It.IsAny()), Times.Once()); + } + + [Test] + //This confirms that new profiles are added only if no other profiles exists. + //We don't want to keep adding them back if a user deleted them on purpose. + public void Init_should_skip_if_any_profiles_already_exist() + { + Mocker.GetMock() + .Setup(s => s.All()) + .Returns(Builder.CreateListOfSize(2).Build().ToList()); + + Subject.Handle(new ApplicationStartedEvent()); + + Mocker.GetMock() + .Verify(v => v.Insert(It.IsAny()), Times.Never()); + } + + + [Test] + public void should_not_be_able_to_delete_profile_if_assigned_to_artist() + { + var artistList = Builder.CreateListOfSize(3) + .Random(1) + .With(c => c.LanguageProfileId = 2) + .Build().ToList(); + + + Mocker.GetMock().Setup(c => c.GetAllArtists()).Returns(artistList); + + Assert.Throws(() => Subject.Delete(2)); + + Mocker.GetMock().Verify(c => c.Delete(It.IsAny()), Times.Never()); + + } + + + [Test] + public void should_delete_profile_if_not_assigned_to_series() + { + var artistList = Builder.CreateListOfSize(3) + .All() + .With(c => c.LanguageProfileId = 2) + .Build().ToList(); + + + Mocker.GetMock().Setup(c => c.GetAllArtists()).Returns(artistList); + + Subject.Delete(1); + + Mocker.GetMock().Verify(c => c.Delete(1), Times.Once()); + } + } +} diff --git a/src/NzbDrone.Core.Test/MediaCoverTests/MediaCoverServiceFixture.cs b/src/NzbDrone.Core.Test/MediaCoverTests/MediaCoverServiceFixture.cs index fdf2efb07..6de8ece98 100644 --- a/src/NzbDrone.Core.Test/MediaCoverTests/MediaCoverServiceFixture.cs +++ b/src/NzbDrone.Core.Test/MediaCoverTests/MediaCoverServiceFixture.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using FizzWare.NBuilder; @@ -9,25 +9,31 @@ using NzbDrone.Common.Disk; using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Core.MediaCover; using NzbDrone.Core.Test.Framework; -using NzbDrone.Core.Tv; -using NzbDrone.Core.Tv.Events; +using NzbDrone.Core.Music; +using NzbDrone.Core.Music.Events; namespace NzbDrone.Core.Test.MediaCoverTests { [TestFixture] public class MediaCoverServiceFixture : CoreTest { - Series _series; + Artist _artist; + Album _album; [SetUp] public void Setup() { Mocker.SetConstant(new AppFolderInfo(Mocker.Resolve())); - _series = Builder.CreateNew() + _artist = Builder.CreateNew() .With(v => v.Id = 2) .With(v => v.Images = new List { new MediaCover.MediaCover(MediaCoverTypes.Poster, "") }) .Build(); + + _album = Builder.CreateNew() + .With(v => v.Id = 4) + .With(v => v.Images = new List { new MediaCover.MediaCover(MediaCoverTypes.Cover, "") }) + .Build(); } [Test] @@ -50,6 +56,26 @@ namespace NzbDrone.Core.Test.MediaCoverTests covers.Single().Url.Should().Be("/MediaCover/12/banner.jpg?lastWrite=1234"); } + [Test] + public void should_convert_album_cover_urls_to_local() + { + var covers = new List + { + new MediaCover.MediaCover {CoverType = MediaCoverTypes.Disc} + }; + + Mocker.GetMock().Setup(c => c.FileGetLastWrite(It.IsAny())) + .Returns(new DateTime(1234)); + + Mocker.GetMock().Setup(c => c.FileExists(It.IsAny())) + .Returns(true); + + Subject.ConvertToLocalUrls(12, covers, 6); + + + covers.Single().Url.Should().Be("/MediaCover/12/6/disc.jpg?lastWrite=1234"); + } + [Test] public void should_convert_media_urls_to_local_without_time_if_file_doesnt_exist() { @@ -76,7 +102,7 @@ namespace NzbDrone.Core.Test.MediaCoverTests .Setup(v => v.FileExists(It.IsAny())) .Returns(true); - Subject.HandleAsync(new SeriesUpdatedEvent(_series)); + Subject.HandleAsync(new ArtistUpdatedEvent(_artist)); Mocker.GetMock() .Verify(v => v.Resize(It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(2)); @@ -93,7 +119,7 @@ namespace NzbDrone.Core.Test.MediaCoverTests .Setup(v => v.FileExists(It.IsAny())) .Returns(false); - Subject.HandleAsync(new SeriesUpdatedEvent(_series)); + Subject.HandleAsync(new ArtistUpdatedEvent(_artist)); Mocker.GetMock() .Verify(v => v.Resize(It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(2)); @@ -114,7 +140,7 @@ namespace NzbDrone.Core.Test.MediaCoverTests .Setup(v => v.GetFileSize(It.IsAny())) .Returns(1000); - Subject.HandleAsync(new SeriesUpdatedEvent(_series)); + Subject.HandleAsync(new ArtistUpdatedEvent(_artist)); Mocker.GetMock() .Verify(v => v.Resize(It.IsAny(), It.IsAny(), It.IsAny()), Times.Never()); @@ -135,7 +161,7 @@ namespace NzbDrone.Core.Test.MediaCoverTests .Setup(v => v.GetFileSize(It.IsAny())) .Returns(0); - Subject.HandleAsync(new SeriesUpdatedEvent(_series)); + Subject.HandleAsync(new ArtistUpdatedEvent(_artist)); Mocker.GetMock() .Verify(v => v.Resize(It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(2)); @@ -156,10 +182,10 @@ namespace NzbDrone.Core.Test.MediaCoverTests .Setup(v => v.Resize(It.IsAny(), It.IsAny(), It.IsAny())) .Throws(); - Subject.HandleAsync(new SeriesUpdatedEvent(_series)); + Subject.HandleAsync(new ArtistUpdatedEvent(_artist)); Mocker.GetMock() .Verify(v => v.Resize(It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(2)); } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Core.Test/MediaFiles/DiskScanServiceTests/ScanFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/DiskScanServiceTests/ScanFixture.cs index 399cff69b..e85236dd7 100644 --- a/src/NzbDrone.Core.Test/MediaFiles/DiskScanServiceTests/ScanFixture.cs +++ b/src/NzbDrone.Core.Test/MediaFiles/DiskScanServiceTests/ScanFixture.cs @@ -250,6 +250,22 @@ namespace NzbDrone.Core.Test.MediaFiles.DiskScanServiceTests .Verify(v => v.GetImportDecisions(It.Is>(l => l.Count == 4), _artist), Times.Once()); } + [Test] + public void should_scan_files_that_start_with_period() + { + GivenArtistFolder(); + + GivenFiles(new List + { + Path.Combine(_artist.Path, "Album 1", ".t01.mp3").AsOsAgnostic() + }); + + Subject.Scan(_artist); + + Mocker.GetMock() + .Verify(v => v.GetImportDecisions(It.Is>(l => l.Count == 1), _artist), Times.Once()); + } + [Test] public void should_not_scan_subfolders_that_start_with_period() { diff --git a/src/NzbDrone.Core.Test/MediaFiles/DownloadedAlbumsCommandServiceFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/DownloadedAlbumsCommandServiceFixture.cs index 0bb230743..a5360f84c 100644 --- a/src/NzbDrone.Core.Test/MediaFiles/DownloadedAlbumsCommandServiceFixture.cs +++ b/src/NzbDrone.Core.Test/MediaFiles/DownloadedAlbumsCommandServiceFixture.cs @@ -1,10 +1,10 @@ +using System; using System.Collections.Generic; using System.IO; using FizzWare.NBuilder; using Moq; using NUnit.Framework; using NzbDrone.Common.Disk; -using NzbDrone.Core.Configuration; using NzbDrone.Core.Download; using NzbDrone.Core.Download.TrackedDownloads; using NzbDrone.Core.MediaFiles; @@ -12,7 +12,7 @@ using NzbDrone.Core.MediaFiles.Commands; using NzbDrone.Core.MediaFiles.TrackImport; using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Test.Framework; -using NzbDrone.Core.Tv; +using NzbDrone.Core.Music; using NzbDrone.Test.Common; namespace NzbDrone.Core.Test.MediaFiles @@ -20,7 +20,6 @@ namespace NzbDrone.Core.Test.MediaFiles [TestFixture] public class DownloadedAlbumsCommandServiceFixture : CoreTest { - private string _droneFactory = "c:\\drop\\".AsOsAgnostic(); private string _downloadFolder = "c:\\drop_other\\Show.S01E01\\".AsOsAgnostic(); private string _downloadFile = "c:\\drop_other\\Show.S01E01.mkv".AsOsAgnostic(); @@ -29,15 +28,13 @@ namespace NzbDrone.Core.Test.MediaFiles [SetUp] public void Setup() { - Mocker.GetMock().SetupGet(c => c.DownloadedAlbumsFolder) - .Returns(_droneFactory); - Mocker.GetMock() + Mocker.GetMock() .Setup(v => v.ProcessRootFolder(It.IsAny())) .Returns(new List()); - Mocker.GetMock() - .Setup(v => v.ProcessPath(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + Mocker.GetMock() + .Setup(v => v.ProcessPath(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .Returns(new List()); var downloadItem = Builder.CreateNew() @@ -45,14 +42,14 @@ namespace NzbDrone.Core.Test.MediaFiles .With(v => v.Status = DownloadItemStatus.Downloading) .Build(); - var remoteEpisode = Builder.CreateNew() - .With(v => v.Series = new Series()) + var remoteAlbum = Builder.CreateNew() + .With(v => v.Artist = new Artist()) .Build(); _trackedDownload = new TrackedDownload { DownloadItem = downloadItem, - RemoteEpisode = remoteEpisode, + RemoteAlbum = remoteAlbum, State = TrackedDownloadStage.Downloading }; } @@ -76,35 +73,15 @@ namespace NzbDrone.Core.Test.MediaFiles .Returns(_trackedDownload); } - [Test] - public void should_process_dronefactory_if_path_is_not_specified() - { - GivenExistingFolder(_droneFactory); - - Subject.Execute(new DownloadedAlbumsScanCommand()); - - Mocker.GetMock().Verify(c => c.ProcessRootFolder(It.IsAny()), Times.Once()); - } - [Test] public void should_skip_import_if_dronefactory_doesnt_exist() { - Subject.Execute(new DownloadedAlbumsScanCommand()); + Assert.Throws(() => Subject.Execute(new DownloadedAlbumsScanCommand())); - Mocker.GetMock().Verify(c => c.ProcessRootFolder(It.IsAny()), Times.Never()); + Mocker.GetMock().Verify(c => c.ProcessRootFolder(It.IsAny()), Times.Never()); - ExceptionVerification.ExpectedWarns(1); } - [Test] - public void should_ignore_downloadclientid_if_path_is_not_specified() - { - GivenExistingFolder(_droneFactory); - - Subject.Execute(new DownloadedAlbumsScanCommand() { DownloadClientId = "sab1" }); - - Mocker.GetMock().Verify(c => c.ProcessRootFolder(It.IsAny()), Times.Once()); - } [Test] public void should_process_folder_if_downloadclientid_is_not_specified() @@ -113,7 +90,7 @@ namespace NzbDrone.Core.Test.MediaFiles Subject.Execute(new DownloadedAlbumsScanCommand() { Path = _downloadFolder }); - Mocker.GetMock().Verify(c => c.ProcessPath(It.IsAny(), ImportMode.Auto, null, null), Times.Once()); + Mocker.GetMock().Verify(c => c.ProcessPath(It.IsAny(), ImportMode.Auto, null, null), Times.Once()); } [Test] @@ -123,7 +100,7 @@ namespace NzbDrone.Core.Test.MediaFiles Subject.Execute(new DownloadedAlbumsScanCommand() { Path = _downloadFile }); - Mocker.GetMock().Verify(c => c.ProcessPath(It.IsAny(), ImportMode.Auto, null, null), Times.Once()); + Mocker.GetMock().Verify(c => c.ProcessPath(It.IsAny(), ImportMode.Auto, null, null), Times.Once()); } [Test] @@ -134,7 +111,7 @@ namespace NzbDrone.Core.Test.MediaFiles Subject.Execute(new DownloadedAlbumsScanCommand() { Path = _downloadFolder, DownloadClientId = "sab1" }); - Mocker.GetMock().Verify(c => c.ProcessPath(_downloadFolder, ImportMode.Auto, _trackedDownload.RemoteEpisode.Series, _trackedDownload.DownloadItem), Times.Once()); + Mocker.GetMock().Verify(c => c.ProcessPath(_downloadFolder, ImportMode.Auto, _trackedDownload.RemoteAlbum.Artist, _trackedDownload.DownloadItem), Times.Once()); } [Test] @@ -144,7 +121,7 @@ namespace NzbDrone.Core.Test.MediaFiles Subject.Execute(new DownloadedAlbumsScanCommand() { Path = _downloadFolder, DownloadClientId = "sab1" }); - Mocker.GetMock().Verify(c => c.ProcessPath(_downloadFolder, ImportMode.Auto, null, null), Times.Once()); + Mocker.GetMock().Verify(c => c.ProcessPath(_downloadFolder, ImportMode.Auto, null, null), Times.Once()); ExceptionVerification.ExpectedWarns(1); } @@ -154,7 +131,7 @@ namespace NzbDrone.Core.Test.MediaFiles { Subject.Execute(new DownloadedAlbumsScanCommand() { Path = _downloadFolder }); - Mocker.GetMock().Verify(c => c.ProcessPath(It.IsAny(), ImportMode.Auto, null, null), Times.Never()); + Mocker.GetMock().Verify(c => c.ProcessPath(It.IsAny(), ImportMode.Auto, null, null), Times.Never()); ExceptionVerification.ExpectedWarns(1); } @@ -166,7 +143,7 @@ namespace NzbDrone.Core.Test.MediaFiles Subject.Execute(new DownloadedAlbumsScanCommand() { Path = _downloadFile, ImportMode = ImportMode.Copy }); - Mocker.GetMock().Verify(c => c.ProcessPath(It.IsAny(), ImportMode.Copy, null, null), Times.Once()); + Mocker.GetMock().Verify(c => c.ProcessPath(It.IsAny(), ImportMode.Copy, null, null), Times.Once()); } } } diff --git a/src/NzbDrone.Core.Test/MediaFiles/DownloadedEpisodesImportServiceFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/DownloadedEpisodesImportServiceFixture.cs deleted file mode 100644 index d2d517caa..000000000 --- a/src/NzbDrone.Core.Test/MediaFiles/DownloadedEpisodesImportServiceFixture.cs +++ /dev/null @@ -1,378 +0,0 @@ -using System.Collections.Generic; -using System.IO; -using System.Linq; -using FizzWare.NBuilder; -using Moq; -using NUnit.Framework; -using NzbDrone.Common.Disk; -using NzbDrone.Core.MediaFiles; -using NzbDrone.Core.MediaFiles.TrackImport; -using NzbDrone.Core.Parser; -using NzbDrone.Core.Parser.Model; -using NzbDrone.Core.Qualities; -using NzbDrone.Core.Test.Framework; -using NzbDrone.Core.Tv; -using NzbDrone.Core.Music; -using NzbDrone.Test.Common; -using FluentAssertions; - -namespace NzbDrone.Core.Test.MediaFiles -{ - [TestFixture] - public class DownloadedTracksImportServiceFixture : CoreTest - { - private string _droneFactory = "c:\\drop\\".AsOsAgnostic(); - private string[] _subFolders = new[] { "c:\\root\\foldername".AsOsAgnostic() }; - private string[] _videoFiles = new[] { "c:\\root\\foldername\\30.rock.s01e01.ext".AsOsAgnostic() }; - - [SetUp] - public void Setup() - { - Mocker.GetMock().Setup(c => c.GetAudioFiles(It.IsAny(), It.IsAny())) - .Returns(_videoFiles); - - Mocker.GetMock().Setup(c => c.GetDirectories(It.IsAny())) - .Returns(_subFolders); - - Mocker.GetMock().Setup(c => c.FolderExists(It.IsAny())) - .Returns(true); - - Mocker.GetMock() - .Setup(s => s.Import(It.IsAny>(), true, null, ImportMode.Auto)) - .Returns(new List()); - } - - private void GivenValidSeries() - { - Mocker.GetMock() - .Setup(s => s.GetSeries(It.IsAny())) - .Returns(Builder.CreateNew().Build()); - } - - [Test] - public void should_search_for_series_using_folder_name() - { - Subject.ProcessRootFolder(new DirectoryInfo(_droneFactory)); - - Mocker.GetMock().Verify(c => c.GetSeries("foldername"), Times.Once()); - } - - [Test] - public void should_skip_if_file_is_in_use_by_another_process() - { - GivenValidSeries(); - - Mocker.GetMock().Setup(c => c.IsFileLocked(It.IsAny())) - .Returns(true); - - Subject.ProcessRootFolder(new DirectoryInfo(_droneFactory)); - - VerifyNoImport(); - } - - [Test] - public void should_skip_if_no_series_found() - { - Mocker.GetMock().Setup(c => c.GetSeries("foldername")).Returns((Series)null); - - Subject.ProcessRootFolder(new DirectoryInfo(_droneFactory)); - - Mocker.GetMock() - .Verify(c => c.GetImportDecisions(It.IsAny>(), It.IsAny(), It.IsAny()), - Times.Never()); - - VerifyNoImport(); - } - - [Test] - public void should_not_import_if_folder_is_a_series_path() - { - GivenValidSeries(); - - Mocker.GetMock() - .Setup(s => s.SeriesPathExists(It.IsAny())) - .Returns(true); - - Mocker.GetMock() - .Setup(c => c.GetAudioFiles(It.IsAny(), It.IsAny())) - .Returns(new string[0]); - - Subject.ProcessRootFolder(new DirectoryInfo(_droneFactory)); - - Mocker.GetMock() - .Verify(v => v.GetAudioFiles(It.IsAny(), true), Times.Never()); - - ExceptionVerification.ExpectedWarns(1); - } - - [Test] - public void should_not_delete_folder_if_no_files_were_imported() - { - Mocker.GetMock() - .Setup(s => s.Import(It.IsAny>(), false, null, ImportMode.Auto)) - .Returns(new List()); - - Subject.ProcessRootFolder(new DirectoryInfo(_droneFactory)); - - Mocker.GetMock() - .Verify(v => v.GetFolderSize(It.IsAny()), Times.Never()); - } - - [Test] - public void should_not_delete_folder_if_files_were_imported_and_video_files_remain() - { - GivenValidSeries(); - - var localEpisode = new LocalTrack(); - - var imported = new List(); - imported.Add(new ImportDecision(localEpisode)); - - Mocker.GetMock() - .Setup(s => s.GetImportDecisions(It.IsAny>(), It.IsAny(), null)) - .Returns(imported); - - Mocker.GetMock() - .Setup(s => s.Import(It.IsAny>(), true, null, ImportMode.Auto)) - .Returns(imported.Select(i => new ImportResult(i)).ToList()); - - Subject.ProcessRootFolder(new DirectoryInfo(_droneFactory)); - - Mocker.GetMock() - .Verify(v => v.DeleteFolder(It.IsAny(), true), Times.Never()); - - ExceptionVerification.ExpectedWarns(1); - } - - [Test] - public void should_delete_folder_if_files_were_imported_and_only_sample_files_remain() - { - GivenValidSeries(); - - var localEpisode = new LocalTrack(); - - var imported = new List(); - imported.Add(new ImportDecision(localEpisode)); - - Mocker.GetMock() - .Setup(s => s.GetImportDecisions(It.IsAny>(), It.IsAny(), null)) - .Returns(imported); - - Mocker.GetMock() - .Setup(s => s.Import(It.IsAny>(), true, null, ImportMode.Auto)) - .Returns(imported.Select(i => new ImportResult(i)).ToList()); - - Mocker.GetMock() - .Setup(s => s.IsSample(It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny())) - .Returns(true); - - Subject.ProcessRootFolder(new DirectoryInfo(_droneFactory)); - - Mocker.GetMock() - .Verify(v => v.DeleteFolder(It.IsAny(), true), Times.Once()); - } - - [TestCase("_UNPACK_")] - [TestCase("_FAILED_")] - public void should_remove_unpack_from_folder_name(string prefix) - { - var folderName = "30.rock.s01e01.pilot.hdtv-lol"; - var folders = new[] { string.Format(@"C:\Test\Unsorted\{0}{1}", prefix, folderName).AsOsAgnostic() }; - - Mocker.GetMock() - .Setup(c => c.GetDirectories(It.IsAny())) - .Returns(folders); - - Subject.ProcessRootFolder(new DirectoryInfo(_droneFactory)); - - Mocker.GetMock() - .Verify(v => v.GetSeries(folderName), Times.Once()); - - Mocker.GetMock() - .Verify(v => v.GetSeries(It.Is(s => s.StartsWith(prefix))), Times.Never()); - } - - [Test] - public void should_return_importresult_on_unknown_series() - { - Mocker.GetMock().Setup(c => c.FolderExists(It.IsAny())) - .Returns(false); - - Mocker.GetMock().Setup(c => c.FileExists(It.IsAny())) - .Returns(true); - - var fileName = @"C:\folder\file.mkv".AsOsAgnostic(); - - var result = Subject.ProcessPath(fileName); - - result.Should().HaveCount(1); - result.First().ImportDecision.Should().NotBeNull(); - result.First().ImportDecision.LocalTrack.Should().NotBeNull(); - result.First().ImportDecision.LocalTrack.Path.Should().Be(fileName); - result.First().Result.Should().Be(ImportResultType.Rejected); - } - - [Test] - public void should_not_delete_if_there_is_large_rar_file() - { - GivenValidSeries(); - - var localEpisode = new LocalTrack(); - - var imported = new List(); - imported.Add(new ImportDecision(localEpisode)); - - Mocker.GetMock() - .Setup(s => s.GetImportDecisions(It.IsAny>(), It.IsAny(), null)) - .Returns(imported); - - Mocker.GetMock() - .Setup(s => s.Import(It.IsAny>(), true, null, ImportMode.Auto)) - .Returns(imported.Select(i => new ImportResult(i)).ToList()); - - Mocker.GetMock() - .Setup(s => s.IsSample(It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny())) - .Returns(true); - - Mocker.GetMock() - .Setup(s => s.GetFiles(It.IsAny(), SearchOption.AllDirectories)) - .Returns(new []{ _videoFiles.First().Replace(".ext", ".rar") }); - - Mocker.GetMock() - .Setup(s => s.GetFileSize(It.IsAny())) - .Returns(15.Megabytes()); - - Subject.ProcessRootFolder(new DirectoryInfo(_droneFactory)); - - Mocker.GetMock() - .Verify(v => v.DeleteFolder(It.IsAny(), true), Times.Never()); - - ExceptionVerification.ExpectedWarns(1); - } - - [Test] - public void should_use_folder_if_folder_import() - { - GivenValidSeries(); - - var folderName = @"C:\media\ba09030e-1234-1234-1234-123456789abc\[HorribleSubs] Maria the Virgin Witch - 09 [720p]".AsOsAgnostic(); - var fileName = @"C:\media\ba09030e-1234-1234-1234-123456789abc\[HorribleSubs] Maria the Virgin Witch - 09 [720p]\[HorribleSubs] Maria the Virgin Witch - 09 [720p].mkv".AsOsAgnostic(); - - Mocker.GetMock().Setup(c => c.FolderExists(folderName)) - .Returns(true); - - Mocker.GetMock().Setup(c => c.GetFiles(folderName, SearchOption.TopDirectoryOnly)) - .Returns(new[] { fileName }); - - var localEpisode = new LocalTrack(); - - var imported = new List(); - imported.Add(new ImportDecision(localEpisode)); - - - Subject.ProcessPath(fileName); - - Mocker.GetMock() - .Verify(s => s.GetImportDecisions(It.IsAny>(), It.IsAny(), It.Is(v => v.TrackNumbers.First() == 9)), Times.Once()); - } - - [Test] - public void should_not_use_folder_if_file_import() - { - GivenValidSeries(); - - var fileName = @"C:\media\ba09030e-1234-1234-1234-123456789abc\Torrents\[HorribleSubs] Maria the Virgin Witch - 09 [720p].mkv".AsOsAgnostic(); - - Mocker.GetMock().Setup(c => c.FolderExists(fileName)) - .Returns(false); - - Mocker.GetMock().Setup(c => c.FileExists(fileName)) - .Returns(true); - - var localEpisode = new LocalTrack(); - - var imported = new List(); - imported.Add(new ImportDecision(localEpisode)); - - var result = Subject.ProcessPath(fileName); - - Mocker.GetMock() - .Verify(s => s.GetImportDecisions(It.IsAny>(), It.IsAny(), null), Times.Once()); - } - - [Test] - public void should_not_process_if_file_and_folder_do_not_exist() - { - var folderName = @"C:\media\ba09030e-1234-1234-1234-123456789abc\[HorribleSubs] Maria the Virgin Witch - 09 [720p]".AsOsAgnostic(); - - Mocker.GetMock().Setup(c => c.FolderExists(folderName)) - .Returns(false); - - Mocker.GetMock().Setup(c => c.FileExists(folderName)) - .Returns(false); - - Subject.ProcessPath(folderName).Should().BeEmpty(); - - Mocker.GetMock() - .Verify(v => v.GetSeries(It.IsAny()), Times.Never()); - - ExceptionVerification.ExpectedErrors(1); - } - - [Test] - public void should_not_delete_if_no_files_were_imported() - { - GivenValidSeries(); - - var localEpisode = new LocalTrack(); - - var imported = new List(); - imported.Add(new ImportDecision(localEpisode)); - - Mocker.GetMock() - .Setup(s => s.GetImportDecisions(It.IsAny>(), It.IsAny(), null)) - .Returns(imported); - - Mocker.GetMock() - .Setup(s => s.Import(It.IsAny>(), true, null, ImportMode.Auto)) - .Returns(new List()); - - Mocker.GetMock() - .Setup(s => s.IsSample(It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny())) - .Returns(true); - - Mocker.GetMock() - .Setup(s => s.GetFileSize(It.IsAny())) - .Returns(15.Megabytes()); - - Subject.ProcessRootFolder(new DirectoryInfo(_droneFactory)); - - Mocker.GetMock() - .Verify(v => v.DeleteFolder(It.IsAny(), true), Times.Never()); - } - - private void VerifyNoImport() - { - Mocker.GetMock().Verify(c => c.Import(It.IsAny>(), true, null, ImportMode.Auto), - Times.Never()); - } - - private void VerifyImport() - { - Mocker.GetMock().Verify(c => c.Import(It.IsAny>(), true, null, ImportMode.Auto), - Times.Once()); - } - } -} diff --git a/src/NzbDrone.Core.Test/MediaFiles/DownloadedTracksImportServiceFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/DownloadedTracksImportServiceFixture.cs new file mode 100644 index 000000000..138ba56f5 --- /dev/null +++ b/src/NzbDrone.Core.Test/MediaFiles/DownloadedTracksImportServiceFixture.cs @@ -0,0 +1,469 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; +using FizzWare.NBuilder; +using FluentAssertions; +using Moq; +using NUnit.Framework; +using NzbDrone.Common.Disk; +using NzbDrone.Core.Download; +using NzbDrone.Core.Download.TrackedDownloads; +using NzbDrone.Core.MediaFiles; +using NzbDrone.Core.MediaFiles.TrackImport; +using NzbDrone.Core.Parser; +using NzbDrone.Core.Parser.Model; +using NzbDrone.Core.Qualities; +using NzbDrone.Core.Test.Framework; +using NzbDrone.Core.Music; +using NzbDrone.Test.Common; + +namespace NzbDrone.Core.Test.MediaFiles +{ + [TestFixture] + public class DownloadedTracksImportServiceFixture : CoreTest + { + private string _droneFactory = "c:\\drop\\".AsOsAgnostic(); + private string[] _subFolders = new[] { "c:\\root\\foldername".AsOsAgnostic() }; + private string[] _videoFiles = new[] { "c:\\root\\foldername\\30.rock.s01e01.ext".AsOsAgnostic() }; + + private TrackedDownload _trackedDownload; + + [SetUp] + public void Setup() + { + Mocker.GetMock().Setup(c => c.GetAudioFiles(It.IsAny(), It.IsAny())) + .Returns(_videoFiles); + + Mocker.GetMock().Setup(c => c.FilterFiles(It.IsAny(), It.IsAny>())) + .Returns>((b, s) => s.ToList()); + + Mocker.GetMock().Setup(c => c.GetDirectories(It.IsAny())) + .Returns(_subFolders); + + Mocker.GetMock().Setup(c => c.FolderExists(It.IsAny())) + .Returns(true); + + Mocker.GetMock() + .Setup(s => s.Import(It.IsAny>(), true, null, ImportMode.Auto)) + .Returns(new List()); + + var downloadItem = Builder.CreateNew() + .With(v => v.DownloadId = "sab1") + .With(v => v.Status = DownloadItemStatus.Downloading) + .Build(); + + var remoteAlbum = Builder.CreateNew() + .With(v => v.Artist = new Artist()) + .Build(); + + _trackedDownload = new TrackedDownload + + { + DownloadItem = downloadItem, + RemoteAlbum = remoteAlbum, + State = TrackedDownloadStage.Downloading + }; + } + + private void GivenValidArtist() + { + Mocker.GetMock() + .Setup(s => s.GetArtist(It.IsAny())) + .Returns(Builder.CreateNew().Build()); + } + + private void GivenSuccessfulImport() + { + var localTrack = new LocalTrack(); + + var imported = new List(); + imported.Add(new ImportDecision(localTrack)); + + Mocker.GetMock() + .Setup(s => s.GetImportDecisions(It.IsAny>(), It.IsAny(), null)) + .Returns(imported); + + Mocker.GetMock() + .Setup(s => s.Import(It.IsAny>(), It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(imported.Select(i => new ImportResult(i)).ToList()) + .Callback(() => WasImportedResponse()); + } + + private void WasImportedResponse() + { + Mocker.GetMock().Setup(c => c.GetAudioFiles(It.IsAny(), It.IsAny())) + .Returns(new string[0]); + } + + [Test] + public void should_search_for_artist_using_folder_name() + { + Subject.ProcessRootFolder(new DirectoryInfo(_droneFactory)); + + Mocker.GetMock().Verify(c => c.GetArtist("foldername"), Times.Once()); + } + + [Test] + public void should_skip_if_file_is_in_use_by_another_process() + { + GivenValidArtist(); + + Mocker.GetMock().Setup(c => c.IsFileLocked(It.IsAny())) + .Returns(true); + + Subject.ProcessRootFolder(new DirectoryInfo(_droneFactory)); + + VerifyNoImport(); + } + + [Test] + public void should_skip_if_no_artist_found() + { + Mocker.GetMock().Setup(c => c.GetArtist("foldername")).Returns((Artist)null); + + Subject.ProcessRootFolder(new DirectoryInfo(_droneFactory)); + + Mocker.GetMock() + .Verify(c => c.GetImportDecisions(It.IsAny>(), It.IsAny(), It.IsAny()), + Times.Never()); + + VerifyNoImport(); + } + + [Test] + public void should_not_import_if_folder_is_a_artist_path() + { + GivenValidArtist(); + + Mocker.GetMock() + .Setup(s => s.ArtistPathExists(It.IsAny())) + .Returns(true); + + Mocker.GetMock() + .Setup(c => c.GetAudioFiles(It.IsAny(), It.IsAny())) + .Returns(new string[0]); + + Subject.ProcessRootFolder(new DirectoryInfo(_droneFactory)); + + Mocker.GetMock() + .Verify(v => v.GetAudioFiles(It.IsAny(), true), Times.Never()); + + ExceptionVerification.ExpectedWarns(1); + } + + [Test] + public void should_not_delete_folder_if_no_files_were_imported() + { + Mocker.GetMock() + .Setup(s => s.Import(It.IsAny>(), false, null, ImportMode.Auto)) + .Returns(new List()); + + Subject.ProcessRootFolder(new DirectoryInfo(_droneFactory)); + + Mocker.GetMock() + .Verify(v => v.GetFolderSize(It.IsAny()), Times.Never()); + } + + [Test] + public void should_not_delete_folder_if_files_were_imported_and_video_files_remain() + { + GivenValidArtist(); + + var localTrack = new LocalTrack(); + + var imported = new List(); + imported.Add(new ImportDecision(localTrack)); + + Mocker.GetMock() + .Setup(s => s.GetImportDecisions(It.IsAny>(), It.IsAny(), null)) + .Returns(imported); + + Mocker.GetMock() + .Setup(s => s.Import(It.IsAny>(), true, null, ImportMode.Auto)) + .Returns(imported.Select(i => new ImportResult(i)).ToList()); + + Subject.ProcessRootFolder(new DirectoryInfo(_droneFactory)); + + Mocker.GetMock() + .Verify(v => v.DeleteFolder(It.IsAny(), true), Times.Never()); + + ExceptionVerification.ExpectedWarns(1); + } + + [Test] + public void should_delete_folder_if_files_were_imported_and_only_sample_files_remain() + { + GivenValidArtist(); + + var localEpisode = new LocalTrack(); + + var imported = new List(); + imported.Add(new ImportDecision(localEpisode)); + + Mocker.GetMock() + .Setup(s => s.GetImportDecisions(It.IsAny>(), It.IsAny(), null)) + .Returns(imported); + + Mocker.GetMock() + .Setup(s => s.Import(It.IsAny>(), true, null, ImportMode.Auto)) + .Returns(imported.Select(i => new ImportResult(i)).ToList()); + + //Mocker.GetMock() + // .Setup(s => s.IsSample(It.IsAny(), + // It.IsAny(), + // It.IsAny(), + // It.IsAny(), + // It.IsAny())) + // .Returns(true); + + Subject.ProcessRootFolder(new DirectoryInfo(_droneFactory)); + + Mocker.GetMock() + .Verify(v => v.DeleteFolder(It.IsAny(), true), Times.Once()); + } + + [TestCase("_UNPACK_")] + [TestCase("_FAILED_")] + public void should_remove_unpack_from_folder_name(string prefix) + { + var folderName = "30.rock.s01e01.pilot.hdtv-lol"; + var folders = new[] { string.Format(@"C:\Test\Unsorted\{0}{1}", prefix, folderName).AsOsAgnostic() }; + + Mocker.GetMock() + .Setup(c => c.GetDirectories(It.IsAny())) + .Returns(folders); + + Subject.ProcessRootFolder(new DirectoryInfo(_droneFactory)); + + Mocker.GetMock() + .Verify(v => v.GetArtist(folderName), Times.Once()); + + Mocker.GetMock() + .Verify(v => v.GetArtist(It.Is(s => s.StartsWith(prefix))), Times.Never()); + } + + [Test] + public void should_return_importresult_on_unknown_artist() + { + Mocker.GetMock().Setup(c => c.FolderExists(It.IsAny())) + .Returns(false); + + Mocker.GetMock().Setup(c => c.FileExists(It.IsAny())) + .Returns(true); + + var fileName = @"C:\folder\file.mkv".AsOsAgnostic(); + + var result = Subject.ProcessPath(fileName); + + result.Should().HaveCount(1); + result.First().ImportDecision.Should().NotBeNull(); + result.First().ImportDecision.LocalTrack.Should().NotBeNull(); + result.First().ImportDecision.LocalTrack.Path.Should().Be(fileName); + result.First().Result.Should().Be(ImportResultType.Rejected); + } + + [Test] + public void should_not_delete_if_there_is_large_rar_file() + { + GivenValidArtist(); + + var localEpisode = new LocalTrack(); + + var imported = new List(); + imported.Add(new ImportDecision(localEpisode)); + + Mocker.GetMock() + .Setup(s => s.GetImportDecisions(It.IsAny>(), It.IsAny(), null)) + .Returns(imported); + + Mocker.GetMock() + .Setup(s => s.Import(It.IsAny>(), true, null, ImportMode.Auto)) + .Returns(imported.Select(i => new ImportResult(i)).ToList()); + + //Mocker.GetMock() + // .Setup(s => s.IsSample(It.IsAny(), + // It.IsAny(), + // It.IsAny(), + // It.IsAny(), + // It.IsAny())) + // .Returns(true); + + Mocker.GetMock() + .Setup(s => s.GetFiles(It.IsAny(), SearchOption.AllDirectories)) + .Returns(new []{ _videoFiles.First().Replace(".ext", ".rar") }); + + Mocker.GetMock() + .Setup(s => s.GetFileSize(It.IsAny())) + .Returns(15.Megabytes()); + + Subject.ProcessRootFolder(new DirectoryInfo(_droneFactory)); + + Mocker.GetMock() + .Verify(v => v.DeleteFolder(It.IsAny(), true), Times.Never()); + + ExceptionVerification.ExpectedWarns(1); + } + + [Test] + public void should_use_folder_if_folder_import() + { + GivenValidArtist(); + + var folderName = @"C:\media\ba09030e-1234-1234-1234-123456789abc\[HorribleSubs] Maria the Virgin Witch - 09 [720p]".AsOsAgnostic(); + var fileName = @"C:\media\ba09030e-1234-1234-1234-123456789abc\[HorribleSubs] Maria the Virgin Witch - 09 [720p]\[HorribleSubs] Maria the Virgin Witch - 09 [720p].mkv".AsOsAgnostic(); + + Mocker.GetMock().Setup(c => c.FolderExists(folderName)) + .Returns(true); + + Mocker.GetMock().Setup(c => c.GetFiles(folderName, SearchOption.TopDirectoryOnly)) + .Returns(new[] { fileName }); + + var localEpisode = new LocalTrack(); + + var imported = new List(); + imported.Add(new ImportDecision(localEpisode)); + + + Subject.ProcessPath(fileName); + + Mocker.GetMock() + .Verify(s => s.GetImportDecisions(It.IsAny>(), It.IsAny(), It.Is(v => v.TrackNumbers.First() == 9)), Times.Once()); + } + + [Test] + public void should_not_use_folder_if_file_import() + { + GivenValidArtist(); + + var fileName = @"C:\media\ba09030e-1234-1234-1234-123456789abc\Torrents\[HorribleSubs] Maria the Virgin Witch - 09 [720p].mkv".AsOsAgnostic(); + + Mocker.GetMock().Setup(c => c.FolderExists(fileName)) + .Returns(false); + + Mocker.GetMock().Setup(c => c.FileExists(fileName)) + .Returns(true); + + var localEpisode = new LocalTrack(); + + var imported = new List(); + imported.Add(new ImportDecision(localEpisode)); + + var result = Subject.ProcessPath(fileName); + + Mocker.GetMock() + .Verify(s => s.GetImportDecisions(It.IsAny>(), It.IsAny(), null), Times.Once()); + } + + [Test] + public void should_not_process_if_file_and_folder_do_not_exist() + { + var folderName = @"C:\media\ba09030e-1234-1234-1234-123456789abc\[HorribleSubs] Maria the Virgin Witch - 09 [720p]".AsOsAgnostic(); + + Mocker.GetMock().Setup(c => c.FolderExists(folderName)) + .Returns(false); + + Mocker.GetMock().Setup(c => c.FileExists(folderName)) + .Returns(false); + + Subject.ProcessPath(folderName).Should().BeEmpty(); + + Mocker.GetMock() + .Verify(v => v.GetArtist(It.IsAny()), Times.Never()); + + ExceptionVerification.ExpectedErrors(1); + } + + [Test] + public void should_not_delete_if_no_files_were_imported() + { + GivenValidArtist(); + + var localEpisode = new LocalTrack(); + + var imported = new List(); + imported.Add(new ImportDecision(localEpisode)); + + Mocker.GetMock() + .Setup(s => s.GetImportDecisions(It.IsAny>(), It.IsAny(), null)) + .Returns(imported); + + Mocker.GetMock() + .Setup(s => s.Import(It.IsAny>(), true, null, ImportMode.Auto)) + .Returns(new List()); + + //Mocker.GetMock() + // .Setup(s => s.IsSample(It.IsAny(), + // It.IsAny(), + // It.IsAny(), + // It.IsAny(), + // It.IsAny())) + // .Returns(true); + + Mocker.GetMock() + .Setup(s => s.GetFileSize(It.IsAny())) + .Returns(15.Megabytes()); + + Subject.ProcessRootFolder(new DirectoryInfo(_droneFactory)); + + Mocker.GetMock() + .Verify(v => v.DeleteFolder(It.IsAny(), true), Times.Never()); + } + + [Test] + public void should_not_delete_folder_after_import() + { + GivenValidArtist(); + + GivenSuccessfulImport(); + + _trackedDownload.DownloadItem.CanMoveFiles = false; + + Subject.ProcessPath(_droneFactory, ImportMode.Auto, _trackedDownload.RemoteAlbum.Artist, _trackedDownload.DownloadItem); + + Mocker.GetMock() + .Verify(v => v.DeleteFolder(It.IsAny(), true), Times.Never()); + } + + [Test] + public void should_delete_folder_if_importmode_move() + { + GivenValidArtist(); + + GivenSuccessfulImport(); + + _trackedDownload.DownloadItem.CanMoveFiles = false; + + Subject.ProcessPath(_droneFactory, ImportMode.Move, _trackedDownload.RemoteAlbum.Artist, _trackedDownload.DownloadItem); + + Mocker.GetMock() + .Verify(v => v.DeleteFolder(It.IsAny(), true), Times.Once()); + } + + [Test] + public void should_not_delete_folder_if_importmode_copy() + { + GivenValidArtist(); + + GivenSuccessfulImport(); + + _trackedDownload.DownloadItem.CanMoveFiles = true; + + Subject.ProcessPath(_droneFactory, ImportMode.Copy, _trackedDownload.RemoteAlbum.Artist, _trackedDownload.DownloadItem); + + Mocker.GetMock() + .Verify(v => v.DeleteFolder(It.IsAny(), true), Times.Never()); + } + + private void VerifyNoImport() + { + Mocker.GetMock().Verify(c => c.Import(It.IsAny>(), true, null, ImportMode.Auto), + Times.Never()); + } + + private void VerifyImport() + { + Mocker.GetMock().Verify(c => c.Import(It.IsAny>(), true, null, ImportMode.Auto), + Times.Once()); + } + } +} diff --git a/src/NzbDrone.Core.Test/MediaFiles/ImportApprovedEpisodesFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/ImportApprovedEpisodesFixture.cs index 1245e0d12..200b7a9ec 100644 --- a/src/NzbDrone.Core.Test/MediaFiles/ImportApprovedEpisodesFixture.cs +++ b/src/NzbDrone.Core.Test/MediaFiles/ImportApprovedEpisodesFixture.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.IO; using System.Linq; using FizzWare.NBuilder; @@ -12,11 +12,13 @@ using NzbDrone.Core.MediaFiles.TrackImport; using NzbDrone.Core.MediaFiles.Events; using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Parser.Model; -using NzbDrone.Core.Profiles; +using NzbDrone.Core.Profiles.Qualities; using NzbDrone.Core.Qualities; using NzbDrone.Core.Test.Framework; using NzbDrone.Core.Music; using NzbDrone.Test.Common; +using NzbDrone.Core.Languages; +using NzbDrone.Core.Profiles.Languages; namespace NzbDrone.Core.Test.MediaFiles { @@ -36,6 +38,11 @@ namespace NzbDrone.Core.Test.MediaFiles var artist = Builder.CreateNew() .With(e => e.Profile = new Profile { Items = Qualities.QualityFixture.GetDefaultQualities() }) + .With(l => l.LanguageProfile = new LanguageProfile + { + Cutoff = Language.Spanish, + Languages = Languages.LanguageFixture.GetDefaultLanguages() + }) .With(s => s.Path = @"C:\Test\Music\Alien Ant Farm".AsOsAgnostic()) .Build(); @@ -53,16 +60,16 @@ namespace NzbDrone.Core.Test.MediaFiles _approvedDecisions.Add(new ImportDecision ( new LocalTrack + { + Artist = artist, + Tracks = new List { track }, + Path = Path.Combine(artist.Path, "30 Rock - S01E01 - Pilot.avi"), + Quality = new QualityModel(Quality.MP3_256), + ParsedTrackInfo = new ParsedTrackInfo { - Artist = artist, - Tracks = new List { track }, - Path = Path.Combine(artist.Path, "30 Rock - S01E01 - Pilot.avi"), - Quality = new QualityModel(Quality.MP3_256), - ParsedTrackInfo = new ParsedTrackInfo - { - ReleaseGroup = "DRONE" - } - })); + ReleaseGroup = "DRONE" + } + })); } Mocker.GetMock() @@ -121,14 +128,14 @@ namespace NzbDrone.Core.Test.MediaFiles Times.Once()); } - [Test] - public void should_publish_EpisodeImportedEvent_for_new_downloads() - { - Subject.Import(new List { _approvedDecisions.First() }, true); + //[Test] + //public void should_publish_EpisodeImportedEvent_for_new_downloads() + //{ + // Subject.Import(new List { _approvedDecisions.First() }, true); - Mocker.GetMock() - .Verify(v => v.PublishEvent(It.IsAny()), Times.Once()); - } + // Mocker.GetMock() + // .Verify(v => v.PublishEvent(It.IsAny()), Times.Once()); + //} [Test] public void should_not_move_existing_files() @@ -203,13 +210,13 @@ namespace NzbDrone.Core.Test.MediaFiles var sampleDecision = new ImportDecision (new LocalTrack - { - Artist = fileDecision.LocalTrack.Artist, - Tracks = new List { fileDecision.LocalTrack.Tracks.First() }, - Path = @"C:\Test\TV\30 Rock\30 Rock - S01E01 - Pilot.avi".AsOsAgnostic(), - Quality = new QualityModel(Quality.MP3_256), - Size = 80.Megabytes() - }); + { + Artist = fileDecision.LocalTrack.Artist, + Tracks = new List { fileDecision.LocalTrack.Tracks.First() }, + Path = @"C:\Test\TV\30 Rock\30 Rock - S01E01 - Pilot.avi".AsOsAgnostic(), + Quality = new QualityModel(Quality.MP3_256), + Size = 80.Megabytes() + }); var all = new List(); @@ -224,9 +231,9 @@ namespace NzbDrone.Core.Test.MediaFiles } [Test] - public void should_copy_readonly_downloads() + public void should_copy_when_cannot_move_files_downloads() { - Subject.Import(new List { _approvedDecisions.First() }, true, new DownloadClientItem { Title = "30.Rock.S01E01", IsReadOnly = true }); + Subject.Import(new List { _approvedDecisions.First() }, true, new DownloadClientItem { Title = "30.Rock.S01E01", CanMoveFiles = false }); Mocker.GetMock() .Verify(v => v.UpgradeTrackFile(It.IsAny(), _approvedDecisions.First().LocalTrack, true), Times.Once()); @@ -235,7 +242,7 @@ namespace NzbDrone.Core.Test.MediaFiles [Test] public void should_use_override_importmode() { - Subject.Import(new List { _approvedDecisions.First() }, true, new DownloadClientItem { Title = "30.Rock.S01E01", IsReadOnly = true }, ImportMode.Move); + Subject.Import(new List { _approvedDecisions.First() }, true, new DownloadClientItem { Title = "30.Rock.S01E01", CanMoveFiles = false }, ImportMode.Move); Mocker.GetMock() .Verify(v => v.UpgradeTrackFile(It.IsAny(), _approvedDecisions.First().LocalTrack, false), Times.Once()); diff --git a/src/NzbDrone.Core.Test/MediaFiles/MediaFileDeletionService/DeleteTrackFileFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/MediaFileDeletionService/DeleteTrackFileFixture.cs new file mode 100644 index 000000000..2e03893ee --- /dev/null +++ b/src/NzbDrone.Core.Test/MediaFiles/MediaFileDeletionService/DeleteTrackFileFixture.cs @@ -0,0 +1,140 @@ +using System.IO; +using FizzWare.NBuilder; +using Moq; +using NUnit.Framework; +using NzbDrone.Common.Disk; +using NzbDrone.Core.Exceptions; +using NzbDrone.Core.MediaFiles; +using NzbDrone.Core.Test.Framework; +using NzbDrone.Core.Music; +using NzbDrone.Test.Common; + +namespace NzbDrone.Core.Test.MediaFiles.MediaFileDeletionService +{ + [TestFixture] + public class DeleteTrackFileFixture : CoreTest + { + private static readonly string RootFolder = @"C:\Test\Music"; + private Artist _artist; + private TrackFile _trackFile; + + [SetUp] + public void Setup() + { + _artist = Builder.CreateNew() + .With(s => s.Path = Path.Combine(RootFolder, "Artist Name")) + .Build(); + + _trackFile = Builder.CreateNew() + .With(f => f.RelativePath = "Artist Name - Track01") + .With(f => f.Path = Path.Combine(_artist.Path, "Artist Name - Track01")) + .Build(); + + Mocker.GetMock() + .Setup(s => s.GetParentFolder(_artist.Path)) + .Returns(RootFolder); + + Mocker.GetMock() + .Setup(s => s.GetParentFolder(_trackFile.Path)) + .Returns(_artist.Path); + } + + private void GivenRootFolderExists() + { + Mocker.GetMock() + .Setup(s => s.FolderExists(RootFolder)) + .Returns(true); + } + + private void GivenRootFolderHasFolders() + { + Mocker.GetMock() + .Setup(s => s.GetDirectories(RootFolder)) + .Returns(new[] { _artist.Path }); + } + + private void GivenSeriesFolderExists() + { + Mocker.GetMock() + .Setup(s => s.FolderExists(_artist.Path)) + .Returns(true); + } + + [Test] + public void should_throw_if_root_folder_does_not_exist() + { + Assert.Throws(() => Subject.DeleteTrackFile(_artist, _trackFile)); + } + + [Test] + public void should_should_throw_if_root_folder_is_empty() + { + GivenRootFolderExists(); + Assert.Throws(() => Subject.DeleteTrackFile(_artist, _trackFile)); + } + + [Test] + public void should_delete_from_db_if_artist_folder_does_not_exist() + { + GivenRootFolderExists(); + GivenRootFolderHasFolders(); + + Subject.DeleteTrackFile(_artist, _trackFile); + + Mocker.GetMock().Verify(v => v.Delete(_trackFile, DeleteMediaFileReason.Manual), Times.Once()); + Mocker.GetMock().Verify(v => v.DeleteFile(_trackFile.Path, It.IsAny()), Times.Never()); + } + + [Test] + public void should_delete_from_db_if_track_file_does_not_exist() + { + GivenRootFolderExists(); + GivenRootFolderHasFolders(); + GivenSeriesFolderExists(); + + Subject.DeleteTrackFile(_artist, _trackFile); + + Mocker.GetMock().Verify(v => v.Delete(_trackFile, DeleteMediaFileReason.Manual), Times.Once()); + Mocker.GetMock().Verify(v => v.DeleteFile(_trackFile.Path, It.IsAny()), Times.Never()); + } + + [Test] + public void should_delete_from_disk_and_db_if_track_file_exists() + { + GivenRootFolderExists(); + GivenRootFolderHasFolders(); + GivenSeriesFolderExists(); + + Mocker.GetMock() + .Setup(s => s.FileExists(_trackFile.Path)) + .Returns(true); + + Subject.DeleteTrackFile(_artist, _trackFile); + + Mocker.GetMock().Verify(v => v.DeleteFile(_trackFile.Path, "Series Title"), Times.Once()); + Mocker.GetMock().Verify(v => v.Delete(_trackFile, DeleteMediaFileReason.Manual), Times.Once()); + } + + [Test] + public void should_handle_error_deleting_track_file() + { + GivenRootFolderExists(); + GivenRootFolderHasFolders(); + GivenSeriesFolderExists(); + + Mocker.GetMock() + .Setup(s => s.FileExists(_trackFile.Path)) + .Returns(true); + + Mocker.GetMock() + .Setup(s => s.DeleteFile(_trackFile.Path, "Artist Name")) + .Throws(new IOException()); + + Assert.Throws(() => Subject.DeleteTrackFile(_artist, _trackFile)); + + ExceptionVerification.ExpectedErrors(1); + Mocker.GetMock().Verify(v => v.DeleteFile(_trackFile.Path, "Artist Name"), Times.Once()); + Mocker.GetMock().Verify(v => v.Delete(_trackFile, DeleteMediaFileReason.Manual), Times.Never()); + } + } +} diff --git a/src/NzbDrone.Core.Test/MediaFiles/MediaFileRepositoryFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/MediaFileRepositoryFixture.cs index 1f2dd20a9..03805aaa8 100644 --- a/src/NzbDrone.Core.Test/MediaFiles/MediaFileRepositoryFixture.cs +++ b/src/NzbDrone.Core.Test/MediaFiles/MediaFileRepositoryFixture.cs @@ -1,4 +1,4 @@ -using FizzWare.NBuilder; +using FizzWare.NBuilder; using FluentAssertions; using NUnit.Framework; using NzbDrone.Core.MediaFiles; @@ -11,7 +11,7 @@ namespace NzbDrone.Core.Test.MediaFiles public class MediaFileRepositoryFixture : DbTest { [Test] - public void get_files_by_series() + public void get_files_by_artist() { var files = Builder.CreateListOfSize(10) .All() @@ -24,11 +24,11 @@ namespace NzbDrone.Core.Test.MediaFiles Db.InsertMany(files); - var seriesFiles = Subject.GetFilesByArtist(12); + var artistFiles = Subject.GetFilesByArtist(12); - seriesFiles.Should().HaveCount(4); - seriesFiles.Should().OnlyContain(c => c.ArtistId == 12); + artistFiles.Should().HaveCount(4); + artistFiles.Should().OnlyContain(c => c.ArtistId == 12); } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Core.Test/MediaFiles/MediaFileServiceTests/FilterFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/MediaFileServiceTests/FilterFixture.cs index bc5f5c0b0..96aee6685 100644 --- a/src/NzbDrone.Core.Test/MediaFiles/MediaFileServiceTests/FilterFixture.cs +++ b/src/NzbDrone.Core.Test/MediaFiles/MediaFileServiceTests/FilterFixture.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.IO; using System.Linq; using FluentAssertions; @@ -6,7 +6,6 @@ using Moq; using NUnit.Framework; using NzbDrone.Core.MediaFiles; using NzbDrone.Core.Test.Framework; -using NzbDrone.Core.Tv; using NzbDrone.Core.Music; using NzbDrone.Test.Common; @@ -148,4 +147,4 @@ namespace NzbDrone.Core.Test.MediaFiles.MediaFileServiceTests Subject.FilterExistingFiles(files, _artist).Should().Contain(files.First()); } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Core.Test/MediaFiles/MediaFileTableCleanupServiceFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/MediaFileTableCleanupServiceFixture.cs index 84c9ded48..db296276a 100644 --- a/src/NzbDrone.Core.Test/MediaFiles/MediaFileTableCleanupServiceFixture.cs +++ b/src/NzbDrone.Core.Test/MediaFiles/MediaFileTableCleanupServiceFixture.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using System.IO; using FizzWare.NBuilder; @@ -88,7 +88,7 @@ namespace NzbDrone.Core.Test.MediaFiles } [Test] - public void should_delete_files_that_dont_belong_to_any_episodes() + public void should_delete_files_that_dont_belong_to_any_tracks() { var trackFiles = Builder.CreateListOfSize(10) .Random(10) @@ -104,7 +104,7 @@ namespace NzbDrone.Core.Test.MediaFiles } [Test] - public void should_unlink_episode_when_episodeFile_does_not_exist() + public void should_unlink_track_when_trackFile_does_not_exist() { GivenTrackFiles(new List()); diff --git a/src/NzbDrone.Core.Test/MediaFiles/MediaInfo/MediaInfoFormatterTests/FormatAudioChannelsFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/MediaInfo/MediaInfoFormatterTests/FormatAudioChannelsFixture.cs new file mode 100644 index 000000000..5de655cd5 --- /dev/null +++ b/src/NzbDrone.Core.Test/MediaFiles/MediaInfo/MediaInfoFormatterTests/FormatAudioChannelsFixture.cs @@ -0,0 +1,149 @@ +using FluentAssertions; +using NUnit.Framework; +using NzbDrone.Core.MediaFiles.MediaInfo; +using NzbDrone.Test.Common; + +namespace NzbDrone.Core.Test.MediaFiles.MediaInfo.MediaInfoFormatterTests +{ + [TestFixture] + public class FormatAudioChannelsFixture : TestBase + { + [Test] + public void should_subtract_one_from_AudioChannels_as_total_channels_if_LFE_in_AudioChannelPositionsText() + { + var mediaInfoModel = new MediaInfoModel + { + AudioChannels = 6, + AudioChannelPositions = null, + AudioChannelPositionsText = "Front: L C R, Side: L R, LFE" + }; + + MediaInfoFormatter.FormatAudioChannels(mediaInfoModel).Should().Be(5.1m); + } + + [Test] + public void should_use_AudioChannels_as_total_channels_if_LFE_not_in_AudioChannelPositionsText() + { + var mediaInfoModel = new MediaInfoModel + { + AudioChannels = 2, + AudioChannelPositions = null, + AudioChannelPositionsText = "Front: L R" + }; + + MediaInfoFormatter.FormatAudioChannels(mediaInfoModel).Should().Be(2); + } + + [Test] + public void should_return_0_if_schema_revision_is_less_than_3_and_other_properties_are_null() + { + var mediaInfoModel = new MediaInfoModel + { + AudioChannels = 2, + AudioChannelPositions = null, + AudioChannelPositionsText = null, + SchemaRevision = 2 + }; + + MediaInfoFormatter.FormatAudioChannels(mediaInfoModel).Should().Be(0); + } + + [Test] + public void should_use_AudioChannels_if_schema_revision_is_3_and_other_properties_are_null() + { + var mediaInfoModel = new MediaInfoModel + { + AudioChannels = 2, + AudioChannelPositions = null, + AudioChannelPositionsText = null, + SchemaRevision = 3 + }; + + MediaInfoFormatter.FormatAudioChannels(mediaInfoModel).Should().Be(2); + } + + [Test] + public void should_sum_AudioChannelPositions() + { + var mediaInfoModel = new MediaInfoModel + { + AudioChannels = 2, + AudioChannelPositions = "2/0/0", + AudioChannelPositionsText = null, + SchemaRevision = 3 + }; + + MediaInfoFormatter.FormatAudioChannels(mediaInfoModel).Should().Be(2); + } + + [Test] + public void should_sum_AudioChannelPositions_including_decimal() + { + var mediaInfoModel = new MediaInfoModel + { + AudioChannels = 2, + AudioChannelPositions = "3/2/0.1", + AudioChannelPositionsText = null, + SchemaRevision = 3 + }; + + MediaInfoFormatter.FormatAudioChannels(mediaInfoModel).Should().Be(5.1m); + } + + [Test] + public void should_cleanup_extraneous_text_from_AudioChannelPositions() + { + var mediaInfoModel = new MediaInfoModel + { + AudioChannels = 2, + AudioChannelPositions = "Object Based / 3/2/2.1", + AudioChannelPositionsText = null, + SchemaRevision = 3 + }; + + MediaInfoFormatter.FormatAudioChannels(mediaInfoModel).Should().Be(7.1m); + } + + [Test] + public void should_skip_empty_groups_in_AudioChannelPositions() + { + var mediaInfoModel = new MediaInfoModel + { + AudioChannels = 2, + AudioChannelPositions = " / 2/0/0.0", + AudioChannelPositionsText = null, + SchemaRevision = 3 + }; + + MediaInfoFormatter.FormatAudioChannels(mediaInfoModel).Should().Be(2); + } + + [Test] + public void should_sum_first_series_of_numbers_from_AudioChannelPositions() + { + var mediaInfoModel = new MediaInfoModel + { + AudioChannels = 2, + AudioChannelPositions = "3/2/2.1 / 3/2/2.1", + AudioChannelPositionsText = null, + SchemaRevision = 3 + }; + + MediaInfoFormatter.FormatAudioChannels(mediaInfoModel).Should().Be(7.1m); + } + + [Test] + public void should_sum_dual_mono_representation_AudioChannelPositions() + { + var mediaInfoModel = new MediaInfoModel + { + AudioChannels = 2, + AudioChannelPositions = "1+1", + AudioChannelPositionsText = null, + SchemaRevision = 3 + }; + + MediaInfoFormatter.FormatAudioChannels(mediaInfoModel).Should().Be(2.0m); + } + } +} diff --git a/src/NzbDrone.Core.Test/MediaFiles/MediaInfo/MediaInfoFormatterTests/FormatAudioCodecFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/MediaInfo/MediaInfoFormatterTests/FormatAudioCodecFixture.cs index 2e7e1d227..bcee3170a 100644 --- a/src/NzbDrone.Core.Test/MediaFiles/MediaInfo/MediaInfoFormatterTests/FormatAudioCodecFixture.cs +++ b/src/NzbDrone.Core.Test/MediaFiles/MediaInfo/MediaInfoFormatterTests/FormatAudioCodecFixture.cs @@ -1,4 +1,4 @@ -using FluentAssertions; +using FluentAssertions; using NUnit.Framework; using NzbDrone.Core.MediaFiles.MediaInfo; using NzbDrone.Test.Common; @@ -22,6 +22,26 @@ namespace NzbDrone.Core.Test.MediaFiles.MediaInfo.MediaInfoFormatterTests MediaInfoFormatter.FormatAudioCodec(mediaInfoModel).Should().Be(expectedFormat); } + [TestCase("MPEG Audio, A_MPEG/L2, , ", "droned.s01e03.swedish.720p.hdtv.x264-prince", "MP2")] + [TestCase("Vorbis, A_VORBIS, , Xiph.Org libVorbis I 20101101 (Schaufenugget)", "DB Super HDTV", "Vorbis")] + [TestCase("PCM, 1, , ", "DW DVDRip XviD-idTV", "PCM")] // Dubbed most likely + [TestCase("TrueHD, A_TRUEHD, , ", "", "TrueHD")] + [TestCase("WMA, 161, , ", "Droned.wmv", "WMA")] + [TestCase("WMA, 162, Pro, ", "B.N.S04E18.720p.WEB-DL", "WMA")] + public void should_format_audio_format(string audioFormatPack, string sceneName, string expectedFormat) + { + var split = audioFormatPack.Split(new string[] { ", " }, System.StringSplitOptions.None); + var mediaInfoModel = new MediaInfoModel + { + AudioFormat = split[0], + AudioCodecID = split[1], + AudioProfile = split[2], + AudioCodecLibrary = split[3] + }; + + MediaInfoFormatter.FormatAudioCodec(mediaInfoModel).Should().Be(expectedFormat); + } + [Test] public void should_return_MP3_for_MPEG_Audio_with_Layer_3_for_the_profile() { @@ -47,4 +67,4 @@ namespace NzbDrone.Core.Test.MediaFiles.MediaInfo.MediaInfoFormatterTests ExceptionVerification.ExpectedWarns(1); } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Core.Test/MediaFiles/MediaInfo/VideoFileInfoReaderFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/MediaInfo/VideoFileInfoReaderFixture.cs index 5ccd1e4eb..9c984f070 100644 --- a/src/NzbDrone.Core.Test/MediaFiles/MediaInfo/VideoFileInfoReaderFixture.cs +++ b/src/NzbDrone.Core.Test/MediaFiles/MediaInfo/VideoFileInfoReaderFixture.cs @@ -42,18 +42,23 @@ namespace NzbDrone.Core.Test.MediaFiles.MediaInfo var info = Subject.GetMediaInfo(path); - + info.VideoCodec.Should().BeNull(); + info.VideoFormat.Should().Be("AVC"); + info.VideoCodecID.Should().Be("avc1"); + info.VideoProfile.Should().Be("Baseline@L2.1"); + info.VideoCodecLibrary.Should().Be(""); + info.AudioFormat.Should().Be("AAC"); + info.AudioCodecID.Should().Be("40"); + info.AudioProfile.Should().Be("LC"); + info.AudioCodecLibrary.Should().Be(""); info.AudioBitrate.Should().Be(128000); info.AudioChannels.Should().Be(2); - info.AudioFormat.Should().Be("AAC"); info.AudioLanguages.Should().Be("English"); - info.AudioProfile.Should().Be("LC"); info.Height.Should().Be(320); info.RunTime.Seconds.Should().Be(10); info.ScanType.Should().Be("Progressive"); info.Subtitles.Should().Be(""); info.VideoBitrate.Should().Be(193329); - info.VideoCodec.Should().Be("AVC"); info.VideoFps.Should().Be(24); info.Width.Should().Be(480); @@ -73,17 +78,23 @@ namespace NzbDrone.Core.Test.MediaFiles.MediaInfo var info = Subject.GetMediaInfo(path); + info.VideoCodec.Should().BeNull(); + info.VideoFormat.Should().Be("AVC"); + info.VideoCodecID.Should().Be("avc1"); + info.VideoProfile.Should().Be("Baseline@L2.1"); + info.VideoCodecLibrary.Should().Be(""); + info.AudioFormat.Should().Be("AAC"); + info.AudioCodecID.Should().Be("40"); + info.AudioProfile.Should().Be("LC"); + info.AudioCodecLibrary.Should().Be(""); info.AudioBitrate.Should().Be(128000); info.AudioChannels.Should().Be(2); - info.AudioFormat.Should().Be("AAC"); info.AudioLanguages.Should().Be("English"); - info.AudioProfile.Should().Be("LC"); info.Height.Should().Be(320); info.RunTime.Seconds.Should().Be(10); info.ScanType.Should().Be("Progressive"); info.Subtitles.Should().Be(""); info.VideoBitrate.Should().Be(193329); - info.VideoCodec.Should().Be("AVC"); info.VideoFps.Should().Be(24); info.Width.Should().Be(480); @@ -101,4 +112,4 @@ namespace NzbDrone.Core.Test.MediaFiles.MediaInfo stream.Close(); } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Core.Test/MediaFiles/TrackImport/ImportDecisionMakerFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/TrackImport/ImportDecisionMakerFixture.cs index 451c2cf45..c0fca112a 100644 --- a/src/NzbDrone.Core.Test/MediaFiles/TrackImport/ImportDecisionMakerFixture.cs +++ b/src/NzbDrone.Core.Test/MediaFiles/TrackImport/ImportDecisionMakerFixture.cs @@ -8,12 +8,14 @@ using NzbDrone.Core.MediaFiles; using NzbDrone.Core.MediaFiles.TrackImport; using NzbDrone.Core.Parser; using NzbDrone.Core.Parser.Model; -using NzbDrone.Core.Profiles; +using NzbDrone.Core.Profiles.Qualities; using NzbDrone.Core.Qualities; using NzbDrone.Core.Test.Framework; using NzbDrone.Core.Music; using NzbDrone.Test.Common; using FizzWare.NBuilder; +using NzbDrone.Core.Languages; +using NzbDrone.Core.Profiles.Languages; namespace NzbDrone.Core.Test.MediaFiles.TrackImport { @@ -54,6 +56,7 @@ namespace NzbDrone.Core.Test.MediaFiles.TrackImport _artist = Builder.CreateNew() .With(e => e.Profile = new Profile { Items = Qualities.QualityFixture.GetDefaultQualities() }) + .With(e => e.LanguageProfile = new LanguageProfile { Languages = Languages.LanguageFixture.GetDefaultLanguages() }) .Build(); _quality = new QualityModel(Quality.MP3_256); @@ -62,15 +65,16 @@ namespace NzbDrone.Core.Test.MediaFiles.TrackImport { Artist = _artist, Quality = _quality, + Language = Language.Spanish, Tracks = new List { new Track() }, - Path = @"C:\Test\Unsorted\The.Office.S03E115.DVDRip.XviD-OSiTV.avi" + Path = @"C:\Test\Unsorted\The.Office.S03E115.DVDRip.Spanish.XviD-OSiTV.avi" }; Mocker.GetMock() .Setup(c => c.GetLocalTrack(It.IsAny(), It.IsAny(), It.IsAny())) .Returns(_localTrack); - GivenVideoFiles(new List { @"C:\Test\Unsorted\The.Office.S03E115.DVDRip.XviD-OSiTV.avi".AsOsAgnostic() }); + GivenVideoFiles(new List { @"C:\Test\Unsorted\The.Office.S03E115.DVDRip.Spanish.XviD-OSiTV.avi".AsOsAgnostic() }); } private void GivenSpecifications(params Mock[] mocks) @@ -179,6 +183,17 @@ namespace NzbDrone.Core.Test.MediaFiles.TrackImport } [Test] + public void should_use_file_language_if_folder_language_is_null() + { + GivenSpecifications(_pass1, _pass2, _pass3); + var expectedLanguage = Parser.Parser.ParseLanguage(_audioFiles.Single()); + + var result = Subject.GetImportDecisions(_audioFiles, _artist); + + result.Single().LocalTrack.Language.Should().Be(expectedLanguage); + } + + [Test] public void should_use_file_quality_if_file_quality_was_determined_by_name() { GivenSpecifications(_pass1, _pass2, _pass3); @@ -223,6 +238,23 @@ namespace NzbDrone.Core.Test.MediaFiles.TrackImport } [Test] + public void should_use_folder_language_when_greater_than_file_language() + { + GivenSpecifications(_pass1, _pass2, _pass3); + GivenVideoFiles(new string[] { @"C:\Test\Unsorted\The.Office.S03E115.Spanish.mkv".AsOsAgnostic() }); + + _localTrack.Path = _audioFiles.Single(); + _localTrack.Quality.Quality = Quality.MP3_320; + _localTrack.Language = Language.Spanish; + + var expectedLanguage = Language.French; + + var result = Subject.GetImportDecisions(_audioFiles, _artist, new ParsedTrackInfo { Language = expectedLanguage, Quality = new QualityModel(Quality.MP3_192) }); + + result.Single().LocalTrack.Language.Should().Be(expectedLanguage); + } + +[Test] public void should_not_throw_if_episodes_are_not_found() { GivenSpecifications(_pass1); diff --git a/src/NzbDrone.Core.Test/MediaFiles/TrackImport/SampleServiceFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/TrackImport/SampleServiceFixture.cs deleted file mode 100644 index 0b9070b17..000000000 --- a/src/NzbDrone.Core.Test/MediaFiles/TrackImport/SampleServiceFixture.cs +++ /dev/null @@ -1,182 +0,0 @@ -using System; -using System.Linq; -using FizzWare.NBuilder; -using FluentAssertions; -using Moq; -using NUnit.Framework; -using NzbDrone.Core.MediaFiles.TrackImport; -using NzbDrone.Core.MediaFiles.MediaInfo; -using NzbDrone.Core.Parser.Model; -using NzbDrone.Core.Qualities; -using NzbDrone.Core.Test.Framework; -using NzbDrone.Core.Tv; - -namespace NzbDrone.Core.Test.MediaFiles.TrackImport -{ - [TestFixture] - public class SampleServiceFixture : CoreTest - { - private Series _series; - private LocalEpisode _localEpisode; - - [SetUp] - public void Setup() - { - _series = Builder.CreateNew() - .With(s => s.SeriesType = SeriesTypes.Standard) - .With(s => s.Runtime = 30) - .Build(); - - var episodes = Builder.CreateListOfSize(1) - .All() - .With(e => e.SeasonNumber = 1) - .Build() - .ToList(); - - _localEpisode = new LocalEpisode - { - Path = @"C:\Test\30 Rock\30.rock.s01e01.avi", - Episodes = episodes, - Series = _series, - Quality = new QualityModel(Quality.MP3_256) - }; - } - - private void GivenFileSize(long size) - { - _localEpisode.Size = size; - } - - private void GivenRuntime(int seconds) - { - Mocker.GetMock() - .Setup(s => s.GetRunTime(It.IsAny())) - .Returns(new TimeSpan(0, 0, seconds)); - } - - [Test] - public void should_return_false_if_season_zero() - { - _localEpisode.Episodes[0].SeasonNumber = 0; - ShouldBeFalse(); - } - - [Test] - public void should_return_false_for_flv() - { - _localEpisode.Path = @"C:\Test\some.show.s01e01.flv"; - - ShouldBeFalse(); - - Mocker.GetMock().Verify(c => c.GetRunTime(It.IsAny()), Times.Never()); - } - - [Test] - public void should_return_false_for_strm() - { - _localEpisode.Path = @"C:\Test\some.show.s01e01.strm"; - - ShouldBeFalse(); - - Mocker.GetMock().Verify(c => c.GetRunTime(It.IsAny()), Times.Never()); - } - - [Test] - public void should_use_runtime() - { - GivenRuntime(120); - GivenFileSize(1000.Megabytes()); - - Subject.IsSample(_localEpisode.Series, - _localEpisode.Quality, - _localEpisode.Path, - _localEpisode.Size, - _localEpisode.IsSpecial); - - Mocker.GetMock().Verify(v => v.GetRunTime(It.IsAny()), Times.Once()); - } - - [Test] - public void should_return_true_if_runtime_is_less_than_minimum() - { - GivenRuntime(60); - - ShouldBeTrue(); - } - - [Test] - public void should_return_false_if_runtime_greater_than_minimum() - { - GivenRuntime(600); - - ShouldBeFalse(); - } - - [Test] - public void should_return_false_if_runtime_greater_than_webisode_minimum() - { - _series.Runtime = 6; - GivenRuntime(299); - - ShouldBeFalse(); - } - - [Test] - public void should_fall_back_to_file_size_if_mediainfo_dll_not_found_acceptable_size() - { - Mocker.GetMock() - .Setup(s => s.GetRunTime(It.IsAny())) - .Throws(); - - GivenFileSize(1000.Megabytes()); - ShouldBeFalse(); - } - - [Test] - public void should_fall_back_to_file_size_if_mediainfo_dll_not_found_undersize() - { - Mocker.GetMock() - .Setup(s => s.GetRunTime(It.IsAny())) - .Throws(); - - GivenFileSize(1.Megabytes()); - ShouldBeTrue(); - } - - [Test] - public void should_not_treat_daily_episode_a_special() - { - GivenRuntime(600); - _series.SeriesType = SeriesTypes.Daily; - _localEpisode.Episodes[0].SeasonNumber = 0; - ShouldBeFalse(); - } - - [Test] - public void should_return_false_for_anime_special() - { - _series.SeriesType = SeriesTypes.Anime; - _localEpisode.Episodes[0].SeasonNumber = 0; - - ShouldBeFalse(); - } - - private void ShouldBeTrue() - { - Subject.IsSample(_localEpisode.Series, - _localEpisode.Quality, - _localEpisode.Path, - _localEpisode.Size, - _localEpisode.IsSpecial).Should().BeTrue(); - } - - private void ShouldBeFalse() - { - Subject.IsSample(_localEpisode.Series, - _localEpisode.Quality, - _localEpisode.Path, - _localEpisode.Size, - _localEpisode.IsSpecial).Should().BeFalse(); - } - } -} diff --git a/src/NzbDrone.Core.Test/MediaFiles/TrackImport/Specifications/SameFileSpecificationFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/TrackImport/Specifications/SameFileSpecificationFixture.cs new file mode 100644 index 000000000..073ce9cd5 --- /dev/null +++ b/src/NzbDrone.Core.Test/MediaFiles/TrackImport/Specifications/SameFileSpecificationFixture.cs @@ -0,0 +1,96 @@ +using System.Linq; +using FizzWare.NBuilder; +using FluentAssertions; +using Marr.Data; +using NUnit.Framework; +using NzbDrone.Core.MediaFiles; +using NzbDrone.Core.MediaFiles.TrackImport.Specifications; +using NzbDrone.Core.Parser.Model; +using NzbDrone.Core.Test.Framework; +using NzbDrone.Core.Music; + +namespace NzbDrone.Core.Test.MediaFiles.TrackImport.Specifications +{ + [TestFixture] + public class SameFileSpecificationFixture : CoreTest + { + private LocalTrack _localTrack; + + [SetUp] + public void Setup() + { + _localTrack = Builder.CreateNew() + .With(l => l.Size = 150.Megabytes()) + .Build(); + } + + [Test] + public void should_be_accepted_if_no_existing_file() + { + _localTrack.Tracks = Builder.CreateListOfSize(1) + .TheFirst(1) + .With(e => e.TrackFileId = 0) + .BuildList(); + + Subject.IsSatisfiedBy(_localTrack).Accepted.Should().BeTrue(); + } + + [Test] + public void should_be_accepted_if_multiple_existing_files() + { + _localTrack.Tracks = Builder.CreateListOfSize(2) + .TheFirst(1) + .With(e => e.TrackFileId = 1) + .With(e => e.TrackFile = new LazyLoaded( + new TrackFile + { + Size = _localTrack.Size + })) + .TheNext(1) + .With(e => e.TrackFileId = 2) + .With(e => e.TrackFile = new LazyLoaded( + new TrackFile + { + Size = _localTrack.Size + })) + .Build() + .ToList(); + + Subject.IsSatisfiedBy(_localTrack).Accepted.Should().BeTrue(); + } + + [Test] + public void should_be_accepted_if_file_size_is_different() + { + _localTrack.Tracks = Builder.CreateListOfSize(1) + .TheFirst(1) + .With(e => e.TrackFileId = 1) + .With(e => e.TrackFile = new LazyLoaded( + new TrackFile + { + Size = _localTrack.Size + 100.Megabytes() + })) + .Build() + .ToList(); + + Subject.IsSatisfiedBy(_localTrack).Accepted.Should().BeTrue(); + } + + [Test] + public void should_be_reject_if_file_size_is_the_same() + { + _localTrack.Tracks = Builder.CreateListOfSize(1) + .TheFirst(1) + .With(e => e.TrackFileId = 1) + .With(e => e.TrackFile = new LazyLoaded( + new TrackFile + { + Size = _localTrack.Size + })) + .Build() + .ToList(); + + Subject.IsSatisfiedBy(_localTrack).Accepted.Should().BeFalse(); + } + } +} diff --git a/src/NzbDrone.Core.Test/MediaFiles/TrackImport/Specifications/UpgradeSpecificationFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/TrackImport/Specifications/UpgradeSpecificationFixture.cs index fc21f0b62..22158edf6 100644 --- a/src/NzbDrone.Core.Test/MediaFiles/TrackImport/Specifications/UpgradeSpecificationFixture.cs +++ b/src/NzbDrone.Core.Test/MediaFiles/TrackImport/Specifications/UpgradeSpecificationFixture.cs @@ -6,10 +6,12 @@ using NUnit.Framework; using NzbDrone.Core.MediaFiles; using NzbDrone.Core.MediaFiles.TrackImport.Specifications; using NzbDrone.Core.Parser.Model; -using NzbDrone.Core.Profiles; +using NzbDrone.Core.Profiles.Qualities; using NzbDrone.Core.Qualities; using NzbDrone.Core.Test.Framework; using NzbDrone.Core.Music; +using NzbDrone.Core.Languages; +using NzbDrone.Core.Profiles.Languages; namespace NzbDrone.Core.Test.MediaFiles.TrackImport.Specifications { @@ -23,15 +25,23 @@ namespace NzbDrone.Core.Test.MediaFiles.TrackImport.Specifications public void Setup() { _artist = Builder.CreateNew() - .With(e => e.Profile = new Profile { Items = Qualities.QualityFixture.GetDefaultQualities() }) - .Build(); + .With(e => e.Profile = new Profile + { + Items = Qualities.QualityFixture.GetDefaultQualities(), + }) + .With(l => l.LanguageProfile = new LanguageProfile + { + Languages = Languages.LanguageFixture.GetDefaultLanguages(), + Cutoff = Language.Spanish, + }).Build(); _localTrack = new LocalTrack - { - Path = @"C:\Test\Imagine Dragons\Imagine.Dragons.Song.1.mp3", - Quality = new QualityModel(Quality.MP3_256, new Revision(version: 1)), - Artist = _artist - }; + { + Path = @"C:\Test\Imagine Dragons\Imagine.Dragons.Song.1.mp3", + Quality = new QualityModel(Quality.MP3_256, new Revision(version: 1)), + Language = Language.Spanish, + Artist = _artist + }; } [Test] @@ -77,6 +87,24 @@ namespace NzbDrone.Core.Test.MediaFiles.TrackImport.Specifications Subject.IsSatisfiedBy(_localTrack).Accepted.Should().BeTrue(); } + [Test] + public void should_return_false_if_language_upgrade_for_existing_trackFile_and_quality_is_worse() + { + _localTrack.Tracks = Builder.CreateListOfSize(1) + .All() + .With(e => e.TrackFileId = 1) + .With(e => e.TrackFile = new LazyLoaded( + new TrackFile + { + Quality = new QualityModel(Quality.FLAC, new Revision(version: 1)), + Language = Language.English + })) + .Build() + .ToList(); + + Subject.IsSatisfiedBy(_localTrack).Accepted.Should().BeFalse(); + } + [Test] public void should_return_true_if_upgrade_for_existing_trackFile_for_multi_tracks() { @@ -94,6 +122,42 @@ namespace NzbDrone.Core.Test.MediaFiles.TrackImport.Specifications Subject.IsSatisfiedBy(_localTrack).Accepted.Should().BeTrue(); } + [Test] + public void should_return_true_if_language_upgrade_for_existing_trackFile_for_multi_tracks_and_quality_is_same() + { + _localTrack.Tracks = Builder.CreateListOfSize(2) + .All() + .With(e => e.TrackFileId = 1) + .With(e => e.TrackFile = new LazyLoaded( + new TrackFile + { + Quality = new QualityModel(Quality.MP3_320, new Revision(version: 1)), + Language = Language.English + })) + .Build() + .ToList(); + + Subject.IsSatisfiedBy(_localTrack).Accepted.Should().BeTrue(); + } + + [Test] + public void should_return_false_if_language_upgrade_for_existing_trackFile_for_multi_tracks_and_quality_is_worse() + { + _localTrack.Tracks = Builder.CreateListOfSize(2) + .All() + .With(e => e.TrackFileId = 1) + .With(e => e.TrackFile = new LazyLoaded( + new TrackFile + { + Quality = new QualityModel(Quality.FLAC, new Revision(version: 1)), + Language = Language.English + })) + .Build() + .ToList(); + + Subject.IsSatisfiedBy(_localTrack).Accepted.Should().BeFalse(); + } + [Test] public void should_return_false_if_not_an_upgrade_for_existing_trackFile() { diff --git a/src/NzbDrone.Core.Test/MediaFiles/UpgradeMediaFileServiceFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/UpgradeMediaFileServiceFixture.cs index b61649d42..350c1f257 100644 --- a/src/NzbDrone.Core.Test/MediaFiles/UpgradeMediaFileServiceFixture.cs +++ b/src/NzbDrone.Core.Test/MediaFiles/UpgradeMediaFileServiceFixture.cs @@ -1,4 +1,5 @@ -using System.Linq; +using System.IO; +using System.Linq; using FizzWare.NBuilder; using FluentAssertions; using Marr.Data; @@ -8,7 +9,6 @@ using NzbDrone.Common.Disk; using NzbDrone.Core.MediaFiles; using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Test.Framework; -using NzbDrone.Core.Tv; using NzbDrone.Core.Music; using NzbDrone.Test.Common; @@ -36,6 +36,10 @@ namespace NzbDrone.Core.Test.MediaFiles Mocker.GetMock() .Setup(c => c.FileExists(It.IsAny())) .Returns(true); + + Mocker.GetMock() + .Setup(c => c.GetParentFolder(It.IsAny())) + .Returns(c => Path.GetDirectoryName(c)); } private void GivenSingleTrackWithSingleTrackFile() diff --git a/src/NzbDrone.Core.Test/Messaging/Commands/CommandExecutorFixture.cs b/src/NzbDrone.Core.Test/Messaging/Commands/CommandExecutorFixture.cs index 4a039e699..e0b3d37c5 100644 --- a/src/NzbDrone.Core.Test/Messaging/Commands/CommandExecutorFixture.cs +++ b/src/NzbDrone.Core.Test/Messaging/Commands/CommandExecutorFixture.cs @@ -1,121 +1,211 @@ -//using System; -//using System.Collections.Generic; -//using Moq; -//using NUnit.Framework; -//using NzbDrone.Common; -//using NzbDrone.Core.Messaging.Commands; -//using NzbDrone.Core.Messaging.Commands.Tracking; -//using NzbDrone.Core.Messaging.Events; -//using NzbDrone.Test.Common; -// -//namespace NzbDrone.Core.Test.Messaging.Commands -//{ -// [TestFixture] -// public class CommandExecutorFixture : TestBase -// { -// private Mock> _executorA; -// private Mock> _executorB; -// -// [SetUp] -// public void Setup() -// { -// _executorA = new Mock>(); -// _executorB = new Mock>(); -// -// Mocker.GetMock() -// .Setup(c => c.Build(typeof(IExecute))) -// .Returns(_executorA.Object); -// -// Mocker.GetMock() -// .Setup(c => c.Build(typeof(IExecute))) -// .Returns(_executorB.Object); -// -// -// Mocker.GetMock() -// .Setup(c => c.FindExisting(It.IsAny())) -// .Returns(null); -// } -// -// [Test] -// public void should_publish_command_to_executor() -// { -// var commandA = new CommandA(); -// -// Subject.Push(commandA); -// -// _executorA.Verify(c => c.Execute(commandA), Times.Once()); -// } -// -// [Test] -// public void should_publish_command_by_with_optional_arg_using_name() -// { -// Mocker.GetMock().Setup(c => c.GetImplementations(typeof(Command))) -// .Returns(new List { typeof(CommandA), typeof(CommandB) }); -// -// Subject.Push(typeof(CommandA).FullName); -// _executorA.Verify(c => c.Execute(It.IsAny()), Times.Once()); -// } -// -// -// [Test] -// public void should_not_publish_to_incompatible_executor() -// { -// var commandA = new CommandA(); -// -// Subject.Push(commandA); -// -// _executorA.Verify(c => c.Execute(commandA), Times.Once()); -// _executorB.Verify(c => c.Execute(It.IsAny()), Times.Never()); -// } -// -// [Test] -// public void broken_executor_should_throw_the_exception() -// { -// var commandA = new CommandA(); -// -// _executorA.Setup(c => c.Execute(It.IsAny())) -// .Throws(new NotImplementedException()); -// -// Assert.Throws(() => Subject.Push(commandA)); -// } -// -// -// [Test] -// public void broken_executor_should_publish_executed_event() -// { -// var commandA = new CommandA(); -// -// _executorA.Setup(c => c.Execute(It.IsAny())) -// .Throws(new NotImplementedException()); -// -// Assert.Throws(() => Subject.Push(commandA)); -// -// VerifyEventPublished(); -// } -// -// [Test] -// public void should_publish_executed_event_on_success() -// { -// var commandA = new CommandA(); -// Subject.Push(commandA); -// -// VerifyEventPublished(); -// } -// } -// -// public class CommandA : Command -// { -// public CommandA(int id = 0) -// { -// } -// } -// -// public class CommandB : Command -// { -// -// public CommandB() -// { -// } -// } -// -//} \ No newline at end of file +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Threading; +using Moq; +using NUnit.Framework; +using NzbDrone.Common; +using NzbDrone.Core.Lifecycle; +using NzbDrone.Core.Messaging.Commands; +using NzbDrone.Core.Messaging.Events; +using NzbDrone.Test.Common; + +namespace NzbDrone.Core.Test.Messaging.Commands +{ + [TestFixture] + public class CommandExecutorFixture : TestBase + { + private BlockingCollection _commandQueue; + private Mock> _executorA; + private Mock> _executorB; + private bool _commandExecuted = false; + + [SetUp] + public void Setup() + { + _executorA = new Mock>(); + _executorB = new Mock>(); + + Mocker.GetMock() + .Setup(c => c.Build(typeof(IExecute))) + .Returns(_executorA.Object); + + Mocker.GetMock() + .Setup(c => c.Build(typeof(IExecute))) + .Returns(_executorB.Object); + } + + private void GivenCommandQueue() + { + _commandQueue = new BlockingCollection(new CommandQueue()); + + Mocker.GetMock() + .Setup(s => s.Queue(It.IsAny())) + .Returns(_commandQueue.GetConsumingEnumerable); + } + + private void WaitForExecution(CommandModel commandModel) + { + Mocker.GetMock() + .Setup(s => s.Complete(It.Is(c => c == commandModel), It.IsAny())) + .Callback(() => _commandExecuted = true); + + Mocker.GetMock() + .Setup(s => s.Fail(It.Is(c => c == commandModel), It.IsAny(), It.IsAny())) + .Callback(() => _commandExecuted = true); + + while (!_commandExecuted) + { + Thread.Sleep(100); + } + + var t1 = 1; + } + + [Test] + public void should_start_executor_threads() + { + Subject.Handle(new ApplicationStartedEvent()); + + Mocker.GetMock() + .Verify(v => v.Queue(It.IsAny()), Times.AtLeastOnce()); + } + + [Test] + public void should_execute_on_executor() + { + GivenCommandQueue(); + var commandA = new CommandA(); + var commandModel = new CommandModel + { + Body = commandA + }; + + Subject.Handle(new ApplicationStartedEvent()); + _commandQueue.Add(commandModel); + + WaitForExecution(commandModel); + + _executorA.Verify(c => c.Execute(commandA), Times.Once()); + } + + [Test] + public void should_not_execute_on_incompatible_executor() + { + GivenCommandQueue(); + var commandA = new CommandA(); + var commandModel = new CommandModel + { + Body = commandA + }; + + Subject.Handle(new ApplicationStartedEvent()); + _commandQueue.Add(commandModel); + + WaitForExecution(commandModel); + + _executorA.Verify(c => c.Execute(commandA), Times.Once()); + _executorB.Verify(c => c.Execute(It.IsAny()), Times.Never()); + } + + [Test] + public void broken_executor_should_publish_executed_event() + { + GivenCommandQueue(); + var commandA = new CommandA(); + var commandModel = new CommandModel + { + Body = commandA + }; + + _executorA.Setup(s => s.Execute(It.IsAny())) + .Throws(new NotImplementedException()); + + Subject.Handle(new ApplicationStartedEvent()); + _commandQueue.Add(commandModel); + + WaitForExecution(commandModel); + + VerifyEventPublished(); + ExceptionVerification.ExpectedErrors(1); + } + + [Test] + public void should_publish_executed_event_on_success() + { + GivenCommandQueue(); + var commandA = new CommandA(); + var commandModel = new CommandModel + { + Body = commandA + }; + + Subject.Handle(new ApplicationStartedEvent()); + _commandQueue.Add(commandModel); + + WaitForExecution(commandModel); + + VerifyEventPublished(); + } + + [Test] + public void should_use_completion_message() + { + GivenCommandQueue(); + var commandA = new CommandA(); + var commandModel = new CommandModel + { + Body = commandA + }; + + Subject.Handle(new ApplicationStartedEvent()); + _commandQueue.Add(commandModel); + + WaitForExecution(commandModel); + + Mocker.GetMock() + .Setup(s => s.Complete(It.Is(c => c == commandModel), commandA.CompletionMessage)) + .Callback(() => _commandExecuted = true); + } + + [Test] + public void should_use_last_progress_message_if_completion_message_is_null() + { + GivenCommandQueue(); + var commandA = new CommandA(); + var commandModel = new CommandModel + { + Body = commandA, + Message = "Do work" + }; + + Subject.Handle(new ApplicationStartedEvent()); + _commandQueue.Add(commandModel); + + WaitForExecution(commandModel); + + Mocker.GetMock() + .Setup(s => s.Complete(It.Is(c => c == commandModel), commandModel.Message)) + .Callback(() => _commandExecuted = true); + } + } + + public class CommandA : Command + { + public CommandA(int id = 0) + { + } + } + + public class CommandB : Command + { + + public CommandB() + { + + } + + public override string CompletionMessage => null; + } + +} diff --git a/src/NzbDrone.Core.Test/Metadata/Consumers/Roksbox/FindMetadataFileFixture.cs b/src/NzbDrone.Core.Test/Metadata/Consumers/Roksbox/FindMetadataFileFixture.cs index 6d4328b32..9e7326d1f 100644 --- a/src/NzbDrone.Core.Test/Metadata/Consumers/Roksbox/FindMetadataFileFixture.cs +++ b/src/NzbDrone.Core.Test/Metadata/Consumers/Roksbox/FindMetadataFileFixture.cs @@ -1,11 +1,11 @@ -using System.IO; +using System.IO; using FizzWare.NBuilder; using FluentAssertions; using NUnit.Framework; using NzbDrone.Core.Extras.Metadata; using NzbDrone.Core.Extras.Metadata.Consumers.Roksbox; using NzbDrone.Core.Test.Framework; -using NzbDrone.Core.Tv; +using NzbDrone.Core.Music; using NzbDrone.Test.Common; namespace NzbDrone.Core.Test.Metadata.Consumers.Roksbox @@ -13,12 +13,12 @@ namespace NzbDrone.Core.Test.Metadata.Consumers.Roksbox [TestFixture] public class FindMetadataFileFixture : CoreTest { - private Series _series; + private Artist _series; [SetUp] public void Setup() { - _series = Builder.CreateNew() + _series = Builder.CreateNew() .With(s => s.Path = @"C:\Test\TV\The.Series".AsOsAgnostic()) .Build(); } @@ -38,11 +38,11 @@ namespace NzbDrone.Core.Test.Metadata.Consumers.Roksbox { var path = Path.Combine(_series.Path, folder, folder + ".jpg"); - Subject.FindMetadataFile(_series, path).Type.Should().Be(MetadataType.SeasonImage); + Subject.FindMetadataFile(_series, path).Type.Should().Be(MetadataType.AlbumImage); } - [TestCase(".xml", MetadataType.EpisodeMetadata)] - [TestCase(".jpg", MetadataType.EpisodeImage)] + [TestCase(".xml", MetadataType.TrackMetadata)] + [TestCase(".jpg", MetadataType.TrackImage)] public void should_return_metadata_for_episode_if_valid_file_for_episode(string extension, MetadataType type) { var path = Path.Combine(_series.Path, "the.series.s01e01.episode" + extension); @@ -72,7 +72,7 @@ namespace NzbDrone.Core.Test.Metadata.Consumers.Roksbox { var path = Path.Combine(_series.Path, new DirectoryInfo(_series.Path).Name + ".jpg"); - Subject.FindMetadataFile(_series, path).Type.Should().Be(MetadataType.SeriesImage); + Subject.FindMetadataFile(_series, path).Type.Should().Be(MetadataType.ArtistImage); } } } diff --git a/src/NzbDrone.Core.Test/Metadata/Consumers/Wdtv/FindMetadataFileFixture.cs b/src/NzbDrone.Core.Test/Metadata/Consumers/Wdtv/FindMetadataFileFixture.cs index 078744ec8..e3d956b71 100644 --- a/src/NzbDrone.Core.Test/Metadata/Consumers/Wdtv/FindMetadataFileFixture.cs +++ b/src/NzbDrone.Core.Test/Metadata/Consumers/Wdtv/FindMetadataFileFixture.cs @@ -1,11 +1,11 @@ -using System.IO; +using System.IO; using FizzWare.NBuilder; using FluentAssertions; using NUnit.Framework; using NzbDrone.Core.Extras.Metadata; using NzbDrone.Core.Extras.Metadata.Consumers.Wdtv; using NzbDrone.Core.Test.Framework; -using NzbDrone.Core.Tv; +using NzbDrone.Core.Music; using NzbDrone.Test.Common; namespace NzbDrone.Core.Test.Metadata.Consumers.Wdtv @@ -13,12 +13,12 @@ namespace NzbDrone.Core.Test.Metadata.Consumers.Wdtv [TestFixture] public class FindMetadataFileFixture : CoreTest { - private Series _series; + private Artist _series; [SetUp] public void Setup() { - _series = Builder.CreateNew() + _series = Builder.CreateNew() .With(s => s.Path = @"C:\Test\TV\The.Series".AsOsAgnostic()) .Build(); } @@ -38,11 +38,11 @@ namespace NzbDrone.Core.Test.Metadata.Consumers.Wdtv { var path = Path.Combine(_series.Path, folder, "folder.jpg"); - Subject.FindMetadataFile(_series, path).Type.Should().Be(MetadataType.SeasonImage); + Subject.FindMetadataFile(_series, path).Type.Should().Be(MetadataType.AlbumImage); } - [TestCase(".xml", MetadataType.EpisodeMetadata)] - [TestCase(".metathumb", MetadataType.EpisodeImage)] + [TestCase(".xml", MetadataType.TrackMetadata)] + [TestCase(".metathumb", MetadataType.TrackImage)] public void should_return_metadata_for_episode_if_valid_file_for_episode(string extension, MetadataType type) { var path = Path.Combine(_series.Path, "the.series.s01e01.episode" + extension); @@ -64,7 +64,7 @@ namespace NzbDrone.Core.Test.Metadata.Consumers.Wdtv { var path = Path.Combine(_series.Path, "folder.jpg"); - Subject.FindMetadataFile(_series, path).Type.Should().Be(MetadataType.SeriesImage); + Subject.FindMetadataFile(_series, path).Type.Should().Be(MetadataType.ArtistImage); } } } diff --git a/src/NzbDrone.Core.Test/MetadataSource/SearchArtistComparerFixture.cs b/src/NzbDrone.Core.Test/MetadataSource/SearchArtistComparerFixture.cs new file mode 100644 index 000000000..9a70a3e38 --- /dev/null +++ b/src/NzbDrone.Core.Test/MetadataSource/SearchArtistComparerFixture.cs @@ -0,0 +1,71 @@ +using System.Collections.Generic; +using System.Linq; +using FluentAssertions; +using NUnit.Framework; +using NzbDrone.Core.MetadataSource; +using NzbDrone.Core.Test.Framework; +using NzbDrone.Core.Music; + +namespace NzbDrone.Core.Test.MetadataSource +{ + [TestFixture] + public class SearchArtistComparerFixture : CoreTest + { + private List _artist; + + [SetUp] + public void Setup() + { + _artist = new List(); + } + + private void WithSeries(string name) + { + _artist.Add(new Artist { Name = name }); + } + + [Test] + public void should_prefer_the_walking_dead_over_talking_dead_when_searching_for_the_walking_dead() + { + WithSeries("Talking Dead"); + WithSeries("The Walking Dead"); + + _artist.Sort(new SearchArtistComparer("the walking dead")); + + _artist.First().Name.Should().Be("The Walking Dead"); + } + + [Test] + public void should_prefer_the_walking_dead_over_talking_dead_when_searching_for_walking_dead() + { + WithSeries("Talking Dead"); + WithSeries("The Walking Dead"); + + _artist.Sort(new SearchArtistComparer("walking dead")); + + _artist.First().Name.Should().Be("The Walking Dead"); + } + + [Test] + public void should_prefer_blacklist_over_the_blacklist_when_searching_for_blacklist() + { + WithSeries("The Blacklist"); + WithSeries("Blacklist"); + + _artist.Sort(new SearchArtistComparer("blacklist")); + + _artist.First().Name.Should().Be("Blacklist"); + } + + [Test] + public void should_prefer_the_blacklist_over_blacklist_when_searching_for_the_blacklist() + { + WithSeries("Blacklist"); + WithSeries("The Blacklist"); + + _artist.Sort(new SearchArtistComparer("the blacklist")); + + _artist.First().Name.Should().Be("The Blacklist"); + } + } +} diff --git a/src/NzbDrone.Core.Test/MetadataSource/SearchSeriesComparerFixture.cs b/src/NzbDrone.Core.Test/MetadataSource/SearchSeriesComparerFixture.cs deleted file mode 100644 index f7f9053dd..000000000 --- a/src/NzbDrone.Core.Test/MetadataSource/SearchSeriesComparerFixture.cs +++ /dev/null @@ -1,71 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using FluentAssertions; -using NUnit.Framework; -using NzbDrone.Core.MetadataSource; -using NzbDrone.Core.Test.Framework; -using NzbDrone.Core.Tv; - -namespace NzbDrone.Core.Test.MetadataSource -{ - [TestFixture] - public class SearchSeriesComparerFixture : CoreTest - { - private List _series; - - [SetUp] - public void Setup() - { - _series = new List(); - } - - private void WithSeries(string title) - { - _series.Add(new Series { Title = title }); - } - - [Test] - public void should_prefer_the_walking_dead_over_talking_dead_when_searching_for_the_walking_dead() - { - WithSeries("Talking Dead"); - WithSeries("The Walking Dead"); - - _series.Sort(new SearchSeriesComparer("the walking dead")); - - _series.First().Title.Should().Be("The Walking Dead"); - } - - [Test] - public void should_prefer_the_walking_dead_over_talking_dead_when_searching_for_walking_dead() - { - WithSeries("Talking Dead"); - WithSeries("The Walking Dead"); - - _series.Sort(new SearchSeriesComparer("walking dead")); - - _series.First().Title.Should().Be("The Walking Dead"); - } - - [Test] - public void should_prefer_blacklist_over_the_blacklist_when_searching_for_blacklist() - { - WithSeries("The Blacklist"); - WithSeries("Blacklist"); - - _series.Sort(new SearchSeriesComparer("blacklist")); - - _series.First().Title.Should().Be("Blacklist"); - } - - [Test] - public void should_prefer_the_blacklist_over_blacklist_when_searching_for_the_blacklist() - { - WithSeries("Blacklist"); - WithSeries("The Blacklist"); - - _series.Sort(new SearchSeriesComparer("the blacklist")); - - _series.First().Title.Should().Be("The Blacklist"); - } - } -} diff --git a/src/NzbDrone.Core.Test/MetadataSource/SkyHook/SkyHookProxyFixture.cs b/src/NzbDrone.Core.Test/MetadataSource/SkyHook/SkyHookProxyFixture.cs index e6178c0d2..3a61b9ca8 100644 --- a/src/NzbDrone.Core.Test/MetadataSource/SkyHook/SkyHookProxyFixture.cs +++ b/src/NzbDrone.Core.Test/MetadataSource/SkyHook/SkyHookProxyFixture.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using FluentAssertions; @@ -7,7 +7,7 @@ using NzbDrone.Core.Exceptions; using NzbDrone.Core.MediaCover; using NzbDrone.Core.MetadataSource.SkyHook; using NzbDrone.Core.Test.Framework; -using NzbDrone.Core.Tv; +using NzbDrone.Core.Music; using NzbDrone.Test.Common.Categories; namespace NzbDrone.Core.Test.MetadataSource.SkyHook @@ -22,88 +22,71 @@ namespace NzbDrone.Core.Test.MetadataSource.SkyHook UseRealHttp(); } - [TestCase(75978, "Family Guy")] - [TestCase(83462, "Castle (2009)")] - [TestCase(266189, "The Blacklist")] - public void should_be_able_to_get_series_detail(int tvdbId, string title) + [TestCase("f59c5520-5f46-4d2c-b2c4-822eabf53419", "Linkin Park")] + [TestCase("66c662b6-6e2f-4930-8610-912e24c63ed1", "AC/DC")] + public void should_be_able_to_get_artist_detail(string mbId, string name) { - var details = Subject.GetSeriesInfo(tvdbId); + var details = Subject.GetArtistInfo(mbId, new List { "Album" }, new List { "Studio" }); - ValidateSeries(details.Item1); - ValidateEpisodes(details.Item2); + ValidateArtist(details.Item1); + ValidateAlbums(details.Item2); - details.Item1.Title.Should().Be(title); + details.Item1.Name.Should().Be(name); } [Test] - public void getting_details_of_invalid_series() + public void getting_details_of_invalid_artist() { - Assert.Throws(() => Subject.GetSeriesInfo(int.MaxValue)); + Assert.Throws(() => Subject.GetArtistInfo("aaaaaa-aaa-aaaa-aaaa", new List { "Album" }, new List { "Studio" })); } [Test] - public void should_not_have_period_at_start_of_title_slug() + public void should_not_have_period_at_start_of_name_slug() { - var details = Subject.GetSeriesInfo(79099); + var details = Subject.GetArtistInfo("b6db95cd-88d9-492f-bbf6-a34e0e89b2e5", new List { "Album" }, new List { "Studio" }); - details.Item1.TitleSlug.Should().Be("dothack"); + details.Item1.NameSlug.Should().Be("dothack"); } - private void ValidateSeries(Series series) + private void ValidateArtist(Artist artist) { - series.Should().NotBeNull(); - series.Title.Should().NotBeNullOrWhiteSpace(); - series.CleanTitle.Should().Be(Parser.Parser.CleanSeriesTitle(series.Title)); - series.SortTitle.Should().Be(SeriesTitleNormalizer.Normalize(series.Title, series.TvdbId)); - series.Overview.Should().NotBeNullOrWhiteSpace(); - series.AirTime.Should().NotBeNullOrWhiteSpace(); - series.FirstAired.Should().HaveValue(); - series.FirstAired.Value.Kind.Should().Be(DateTimeKind.Utc); - series.Images.Should().NotBeEmpty(); - series.ImdbId.Should().NotBeNullOrWhiteSpace(); - series.Network.Should().NotBeNullOrWhiteSpace(); - series.Runtime.Should().BeGreaterThan(0); - series.TitleSlug.Should().NotBeNullOrWhiteSpace(); + artist.Should().NotBeNull(); + artist.Name.Should().NotBeNullOrWhiteSpace(); + artist.CleanName.Should().Be(Parser.Parser.CleanArtistName(artist.Name)); + artist.SortName.Should().Be(Parser.Parser.NormalizeTitle(artist.Name)); + artist.Overview.Should().NotBeNullOrWhiteSpace(); + artist.Images.Should().NotBeEmpty(); + artist.NameSlug.Should().NotBeNullOrWhiteSpace(); //series.TvRageId.Should().BeGreaterThan(0); - series.TvdbId.Should().BeGreaterThan(0); + artist.ForeignArtistId.Should().NotBeNullOrWhiteSpace(); } - private void ValidateEpisodes(List episodes) + private void ValidateAlbums(List albums) { - episodes.Should().NotBeEmpty(); + albums.Should().NotBeEmpty(); - var episodeGroup = episodes.GroupBy(e => e.SeasonNumber.ToString("000") + e.EpisodeNumber.ToString("000")); - episodeGroup.Should().OnlyContain(c => c.Count() == 1); - - episodes.Should().Contain(c => c.SeasonNumber > 0); - episodes.Should().Contain(c => !string.IsNullOrWhiteSpace(c.Overview)); - - foreach (var episode in episodes) + foreach (var episode in albums) { - ValidateEpisode(episode); + ValidateAlbum(episode); - //if atleast one episdoe has title it means parse it working. - episodes.Should().Contain(c => !string.IsNullOrWhiteSpace(c.Title)); + //if atleast one album has title it means parse it working. + albums.Should().Contain(c => !string.IsNullOrWhiteSpace(c.Title)); } } - private void ValidateEpisode(Episode episode) + private void ValidateAlbum(Album album) { - episode.Should().NotBeNull(); + album.Should().NotBeNull(); + + album.Title.Should().NotBeNullOrWhiteSpace(); + album.AlbumType.Should().NotBeNullOrWhiteSpace(); - //TODO: Is there a better way to validate that episode number or season number is greater than zero? - (episode.EpisodeNumber + episode.SeasonNumber).Should().NotBe(0); + album.Should().NotBeNull(); - episode.Should().NotBeNull(); - - if (episode.AirDateUtc.HasValue) + if (album.ReleaseDate.HasValue) { - episode.AirDateUtc.Value.Kind.Should().Be(DateTimeKind.Utc); + album.ReleaseDate.Value.Kind.Should().Be(DateTimeKind.Utc); } - - episode.Images.Any(i => i.CoverType == MediaCoverTypes.Screenshot && i.Url.Contains("-940.")) - .Should() - .BeFalse(); } } } diff --git a/src/NzbDrone.Core.Test/MusicTests/AddArtistFixture.cs b/src/NzbDrone.Core.Test/MusicTests/AddArtistFixture.cs index ac90ed5d0..98edf35ca 100644 --- a/src/NzbDrone.Core.Test/MusicTests/AddArtistFixture.cs +++ b/src/NzbDrone.Core.Test/MusicTests/AddArtistFixture.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.IO; using FizzWare.NBuilder; @@ -33,7 +33,7 @@ namespace NzbDrone.Core.Test.MusicTests private void GivenValidArtist(string lidarrId) { Mocker.GetMock() - .Setup(s => s.GetArtistInfo(lidarrId)) + .Setup(s => s.GetArtistInfo(lidarrId, It.IsAny>(), It.IsAny>())) .Returns(new Tuple>(_fakeArtist, new List())); } @@ -53,7 +53,7 @@ namespace NzbDrone.Core.Test.MusicTests { var newArtist = new Artist { - ForeignArtistId = "123456", + ForeignArtistId = "ce09ea31-3d4a-4487-a797-e315175457a0", RootFolderPath = @"C:\Test\Music" }; @@ -70,7 +70,7 @@ namespace NzbDrone.Core.Test.MusicTests { var newArtist = new Artist { - ForeignArtistId = "123456", + ForeignArtistId = "ce09ea31-3d4a-4487-a797-e315175457a0", RootFolderPath = @"C:\Test\Music" }; @@ -87,7 +87,7 @@ namespace NzbDrone.Core.Test.MusicTests { var newArtist = new Artist { - ForeignArtistId = "123456", + ForeignArtistId = "ce09ea31-3d4a-4487-a797-e315175457a0", Path = @"C:\Test\Music\Name1" }; @@ -108,12 +108,12 @@ namespace NzbDrone.Core.Test.MusicTests { var newArtist = new Artist { - ForeignArtistId = "123456", + ForeignArtistId = "ce09ea31-3d4a-4487-a797-e315175457a0", Path = @"C:\Test\Music\Name1" }; Mocker.GetMock() - .Setup(s => s.GetArtistInfo(newArtist.ForeignArtistId)) + .Setup(s => s.GetArtistInfo(newArtist.ForeignArtistId, newArtist.PrimaryAlbumTypes, newArtist.SecondaryAlbumTypes)) .Throws(new ArtistNotFoundException(newArtist.ForeignArtistId)); Mocker.GetMock() @@ -128,4 +128,4 @@ namespace NzbDrone.Core.Test.MusicTests ExceptionVerification.ExpectedErrors(1); } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Core.Test/MusicTests/ArtistNameSlugValidatorFixture.cs b/src/NzbDrone.Core.Test/MusicTests/ArtistNameSlugValidatorFixture.cs new file mode 100644 index 000000000..1a96907d3 --- /dev/null +++ b/src/NzbDrone.Core.Test/MusicTests/ArtistNameSlugValidatorFixture.cs @@ -0,0 +1,76 @@ +using System.Collections.Generic; +using System.Linq; +using FizzWare.NBuilder; +using FluentAssertions; +using FluentValidation.Validators; +using NUnit.Framework; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.Test.Framework; +using NzbDrone.Core.Music; +using NzbDrone.Test.Common; + +namespace NzbDrone.Core.Test.TvTests +{ + [TestFixture] + public class ArtistNameSlugValidatorFixture : CoreTest + { + private List _artist; + private TestValidator _validator; + + [SetUp] + public void Setup() + { + _artist = Builder.CreateListOfSize(1) + .Build() + .ToList(); + + _validator = new TestValidator + { + v => v.RuleFor(s => s.NameSlug).SetValidator(Subject) + }; + + Mocker.GetMock() + .Setup(s => s.GetAllArtists()) + .Returns(_artist); + } + + [Test] + public void should_not_be_valid_if_there_is_an_existing_artist_with_the_same_title_slug() + { + var series = Builder.CreateNew() + .With(s => s.Id = 100) + .With(s => s.NameSlug = _artist.First().NameSlug) + .Build(); + + _validator.Validate(series).IsValid.Should().BeFalse(); + } + + [Test] + public void should_be_valid_if_there_is_not_an_existing_artist_with_the_same_title_slug() + { + var series = Builder.CreateNew() + .With(s => s.NameSlug = "MyNameSlug") + .Build(); + + _validator.Validate(series).IsValid.Should().BeTrue(); + } + + [Test] + public void should_be_valid_if_there_is_an_existing_artist_with_a_null_title_slug() + { + _artist.First().NameSlug = null; + + var series = Builder.CreateNew() + .With(s => s.NameSlug = "MyNameSlug") + .Build(); + + _validator.Validate(series).IsValid.Should().BeTrue(); + } + + [Test] + public void should_be_valid_when_updating_an_existing_artist() + { + _validator.Validate(_artist.First().JsonClone()).IsValid.Should().BeTrue(); + } + } +} diff --git a/src/NzbDrone.Core.Test/MusicTests/RefreshArtistServiceFixture.cs b/src/NzbDrone.Core.Test/MusicTests/RefreshArtistServiceFixture.cs new file mode 100644 index 000000000..77b1febe5 --- /dev/null +++ b/src/NzbDrone.Core.Test/MusicTests/RefreshArtistServiceFixture.cs @@ -0,0 +1,140 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using FizzWare.NBuilder; +using Moq; +using NUnit.Framework; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.Exceptions; +using NzbDrone.Core.MetadataSource; +using NzbDrone.Core.Test.Framework; +using NzbDrone.Core.Music; +using NzbDrone.Core.Music.Commands; +using NzbDrone.Test.Common; + +namespace NzbDrone.Core.Test.MusicTests +{ + [TestFixture] + public class RefreshArtistServiceFixture : CoreTest + { + private Artist _artist; + + [SetUp] + public void Setup() + { + var season1 = Builder.CreateNew() + .With(s => s.ForeignAlbumId = "1") + .Build(); + + _artist = Builder.CreateNew() + .With(s => s.Albums = new List + { + season1 + }) + .Build(); + + Mocker.GetMock() + .Setup(s => s.GetArtist(_artist.Id)) + .Returns(_artist); + + Mocker.GetMock() + .Setup(s => s.GetArtistInfo(It.IsAny(), It.IsAny>(), It.IsAny>())) + .Callback(() => { throw new ArtistNotFoundException(_artist.ForeignArtistId); }); + } + + private void GivenNewArtistInfo(Artist artist) + { + Mocker.GetMock() + .Setup(s => s.GetArtistInfo(_artist.ForeignArtistId, _artist.PrimaryAlbumTypes, _artist.SecondaryAlbumTypes)) + .Returns(new Tuple>(artist, new List())); + } + + [Test] + public void should_monitor_new_albums_automatically() + { + var newArtistInfo = _artist.JsonClone(); + newArtistInfo.Albums.Add(Builder.CreateNew() + .With(s => s.ForeignAlbumId = "2") + .Build()); + + GivenNewArtistInfo(newArtistInfo); + + Subject.Execute(new RefreshArtistCommand(_artist.Id)); + + Mocker.GetMock() + .Verify(v => v.UpdateArtist(It.Is(s => s.Albums.Count == 2 && s.Albums.Single(season => season.ForeignAlbumId == "2").Monitored == true))); + } + + [Test] + public void should_log_error_if_musicbrainz_id_not_found() + { + Subject.Execute(new RefreshArtistCommand(_artist.Id)); + + Mocker.GetMock() + .Verify(v => v.UpdateArtist(It.IsAny()), Times.Never()); + + ExceptionVerification.ExpectedErrors(1); + } + + [Test] + public void should_update_if_musicbrainz_id_changed() + { + var newArtistInfo = _artist.JsonClone(); + newArtistInfo.ForeignArtistId = _artist.ForeignArtistId + 1; + + GivenNewArtistInfo(newArtistInfo); + + Subject.Execute(new RefreshArtistCommand(_artist.Id)); + + Mocker.GetMock() + .Verify(v => v.UpdateArtist(It.Is(s => s.ForeignArtistId == newArtistInfo.ForeignArtistId))); + + ExceptionVerification.ExpectedWarns(1); + } + + [Test] + public void should_not_throw_if_duplicate_album_is_in_existing_info() + { + var newArtistInfo = _artist.JsonClone(); + newArtistInfo.Albums.Add(Builder.CreateNew() + .With(s => s.ForeignAlbumId = "2") + .Build()); + + _artist.Albums.Add(Builder.CreateNew() + .With(s => s.ForeignAlbumId = "2") + .Build()); + + _artist.Albums.Add(Builder.CreateNew() + .With(s => s.ForeignAlbumId = "2") + .Build()); + + GivenNewArtistInfo(newArtistInfo); + + Subject.Execute(new RefreshArtistCommand(_artist.Id)); + + Mocker.GetMock() + .Verify(v => v.UpdateArtist(It.Is(s => s.Albums.Count == 2))); + } + + [Test] + public void should_filter_duplicate_albums() + { + var newArtistInfo = _artist.JsonClone(); + newArtistInfo.Albums.Add(Builder.CreateNew() + .With(s => s.ForeignAlbumId = "2") + .Build()); + + newArtistInfo.Albums.Add(Builder.CreateNew() + .With(s => s.ForeignAlbumId = "2") + .Build()); + + GivenNewArtistInfo(newArtistInfo); + + Subject.Execute(new RefreshArtistCommand(_artist.Id)); + + Mocker.GetMock() + .Verify(v => v.UpdateArtist(It.Is(s => s.Albums.Count == 2))); + + } + } +} diff --git a/src/NzbDrone.Core.Test/MusicTests/ShouldRefreshArtistFixture.cs b/src/NzbDrone.Core.Test/MusicTests/ShouldRefreshArtistFixture.cs new file mode 100644 index 000000000..61d528402 --- /dev/null +++ b/src/NzbDrone.Core.Test/MusicTests/ShouldRefreshArtistFixture.cs @@ -0,0 +1,135 @@ +using System; +using System.Linq; +using FizzWare.NBuilder; +using FluentAssertions; +using NUnit.Framework; +using NzbDrone.Core.Music; +using NzbDrone.Test.Common; + +namespace NzbDrone.Core.Test.MusicTests +{ + [TestFixture] + public class ShouldRefreshArtistFixture : TestBase + { + private Artist _artist; + + [SetUp] + public void Setup() + { + _artist = Builder.CreateNew() + .With(v => v.Status == ArtistStatusType.Continuing) + .Build(); + + Mocker.GetMock() + .Setup(s => s.GetAlbumsByArtist(_artist.Id)) + .Returns(Builder.CreateListOfSize(2) + .All() + .With(e => e.ReleaseDate = DateTime.Today.AddDays(-100)) + .Build() + .ToList()); + } + + private void GivenArtistIsEnded() + { + _artist.Status = ArtistStatusType.Ended; + } + + private void GivenArtistLastRefreshedMonthsAgo() + { + _artist.LastInfoSync = DateTime.UtcNow.AddDays(-90); + } + + private void GivenArtistLastRefreshedYesterday() + { + _artist.LastInfoSync = DateTime.UtcNow.AddDays(-1); + } + + private void GivenArtistLastRefreshedHalfADayAgo() + { + _artist.LastInfoSync = DateTime.UtcNow.AddHours(-12); + } + + private void GivenArtistLastRefreshedRecently() + { + _artist.LastInfoSync = DateTime.UtcNow.AddHours(-1); + } + + private void GivenRecentlyAired() + { + Mocker.GetMock() + .Setup(s => s.GetAlbumsByArtist(_artist.Id)) + .Returns(Builder.CreateListOfSize(2) + .TheFirst(1) + .With(e => e.ReleaseDate = DateTime.Today.AddDays(-7)) + .TheLast(1) + .With(e => e.ReleaseDate = DateTime.Today.AddDays(-100)) + .Build() + .ToList()); + } + + [Test] + public void should_return_true_if_running_artist_last_refreshed_more_than_6_hours_ago() + { + GivenArtistLastRefreshedHalfADayAgo(); + + Subject.ShouldRefresh(_artist).Should().BeTrue(); + } + + [Test] + public void should_return_false_if_running_artist_last_refreshed_less_than_6_hours_ago() + { + GivenArtistLastRefreshedRecently(); + + Subject.ShouldRefresh(_artist).Should().BeFalse(); + } + + [Test] + public void should_return_false_if_ended_artist_last_refreshed_yesterday() + { + GivenArtistIsEnded(); + GivenArtistLastRefreshedYesterday(); + + Subject.ShouldRefresh(_artist).Should().BeFalse(); + } + + [Test] + public void should_return_true_if_artist_last_refreshed_more_than_30_days_ago() + { + GivenArtistIsEnded(); + GivenArtistLastRefreshedMonthsAgo(); + + Subject.ShouldRefresh(_artist).Should().BeTrue(); + } + + [Test] + public void should_return_true_if_album_released_in_last_30_days() + { + GivenArtistIsEnded(); + GivenArtistLastRefreshedYesterday(); + + GivenRecentlyAired(); + + Subject.ShouldRefresh(_artist).Should().BeTrue(); + } + + [Test] + public void should_return_false_when_recently_refreshed_ended_show_has_not_aired_for_30_days() + { + GivenArtistIsEnded(); + GivenArtistLastRefreshedYesterday(); + + Subject.ShouldRefresh(_artist).Should().BeFalse(); + } + + [Test] + public void should_return_false_when_recently_refreshed_ended_show_aired_in_last_30_days() + { + GivenArtistIsEnded(); + GivenArtistLastRefreshedRecently(); + + GivenRecentlyAired(); + + Subject.ShouldRefresh(_artist).Should().BeFalse(); + } + } +} diff --git a/src/NzbDrone.Core.Test/NotificationTests/NotificationBaseFixture.cs b/src/NzbDrone.Core.Test/NotificationTests/NotificationBaseFixture.cs index 0230fa5e8..c017a56f2 100644 --- a/src/NzbDrone.Core.Test/NotificationTests/NotificationBaseFixture.cs +++ b/src/NzbDrone.Core.Test/NotificationTests/NotificationBaseFixture.cs @@ -1,10 +1,10 @@ -using System; +using System; using FluentAssertions; using FluentValidation.Results; using NUnit.Framework; using NzbDrone.Core.Notifications; using NzbDrone.Core.ThingiProvider; -using NzbDrone.Core.Tv; +using NzbDrone.Core.Music; using NzbDrone.Core.Validation; using NzbDrone.Test.Common; @@ -60,7 +60,7 @@ namespace NzbDrone.Core.Test.NotificationTests TestLogger.Info("OnDownload was called"); } - public override void OnRename(Series series) + public override void OnRename(Artist artist) { TestLogger.Info("OnRename was called"); } diff --git a/src/NzbDrone.Core.Test/NotificationTests/SynologyIndexerFixture.cs b/src/NzbDrone.Core.Test/NotificationTests/SynologyIndexerFixture.cs index 724bfb0d7..53b2f081f 100644 --- a/src/NzbDrone.Core.Test/NotificationTests/SynologyIndexerFixture.cs +++ b/src/NzbDrone.Core.Test/NotificationTests/SynologyIndexerFixture.cs @@ -1,11 +1,11 @@ -using System.Collections.Generic; +using System.Collections.Generic; using Moq; using NUnit.Framework; using NzbDrone.Core.MediaFiles; using NzbDrone.Core.Notifications; using NzbDrone.Core.Notifications.Synology; using NzbDrone.Core.Test.Framework; -using NzbDrone.Core.Tv; +using NzbDrone.Core.Music; using NzbDrone.Test.Common; namespace NzbDrone.Core.Test.NotificationTests @@ -13,33 +13,33 @@ namespace NzbDrone.Core.Test.NotificationTests [TestFixture] public class SynologyIndexerFixture : CoreTest { - private Series _series; + private Artist _artist; private DownloadMessage _upgrade; [SetUp] public void SetUp() { - _series = new Series() + _artist = new Artist() { Path = @"C:\Test\".AsOsAgnostic() }; _upgrade = new DownloadMessage() { - Series = _series, + Artist = _artist, - EpisodeFile = new EpisodeFile + TrackFile = new TrackFile { RelativePath = "file1.S01E01E02.mkv" }, - OldFiles = new List + OldFiles = new List { - new EpisodeFile + new TrackFile { RelativePath = "file1.S01E01.mkv" }, - new EpisodeFile + new TrackFile { RelativePath = "file1.S01E02.mkv" } @@ -60,10 +60,10 @@ namespace NzbDrone.Core.Test.NotificationTests { (Subject.Definition.Settings as SynologyIndexerSettings).UpdateLibrary = false; - Subject.OnRename(_series); + Subject.OnRename(_artist); Mocker.GetMock() - .Verify(v => v.UpdateFolder(_series.Path), Times.Never()); + .Verify(v => v.UpdateFolder(_artist.Path), Times.Never()); } [Test] @@ -90,7 +90,7 @@ namespace NzbDrone.Core.Test.NotificationTests [Test] public void should_update_entire_series_folder_on_rename() { - Subject.OnRename(_series); + Subject.OnRename(_artist); Mocker.GetMock() .Verify(v => v.UpdateFolder(@"C:\Test\".AsOsAgnostic()), Times.Once()); diff --git a/src/NzbDrone.Core.Test/NotificationTests/Xbmc/Http/GetSeriesPathFixture.cs b/src/NzbDrone.Core.Test/NotificationTests/Xbmc/Http/GetSeriesPathFixture.cs index 15ec93960..7b48db3de 100644 --- a/src/NzbDrone.Core.Test/NotificationTests/Xbmc/Http/GetSeriesPathFixture.cs +++ b/src/NzbDrone.Core.Test/NotificationTests/Xbmc/Http/GetSeriesPathFixture.cs @@ -1,9 +1,9 @@ -using FluentAssertions; +using FluentAssertions; using NUnit.Framework; using NzbDrone.Common.Http; using NzbDrone.Core.Notifications.Xbmc; using NzbDrone.Core.Test.Framework; -using NzbDrone.Core.Tv; +using NzbDrone.Core.Music; namespace NzbDrone.Core.Test.NotificationTests.Xbmc.Http { @@ -11,7 +11,7 @@ namespace NzbDrone.Core.Test.NotificationTests.Xbmc.Http public class GetSeriesPathFixture : CoreTest { private XbmcSettings _settings; - private Series _series; + private Artist _artist; [SetUp] public void Setup() @@ -27,10 +27,10 @@ namespace NzbDrone.Core.Test.NotificationTests.Xbmc.Http UpdateLibrary = true }; - _series = new Series + _artist = new Artist { - TvdbId = 79488, - Title = "30 Rock" + ForeignArtistId = "123d45d-d154f5d-1f5d1-5df18d5", + Name = "30 Rock" }; const string setResponseUrl = "http://localhost:8080/xbmcCmds/xbmcHttp?command=SetResponseFormat(webheader;false;webfooter;false;header;;footer;;opentag;;closetag;;closefinaltag;false)"; @@ -57,7 +57,7 @@ namespace NzbDrone.Core.Test.NotificationTests.Xbmc.Http .Setup(s => s.DownloadString(query, _settings.Username, _settings.Password)) .Returns(queryResult); - Subject.GetSeriesPath(_settings, _series) + Subject.GetSeriesPath(_settings, _artist) .Should().Be("smb://xbmc:xbmc@HOMESERVER/TV/30 Rock/"); } @@ -72,7 +72,7 @@ namespace NzbDrone.Core.Test.NotificationTests.Xbmc.Http .Returns(queryResult); - Subject.GetSeriesPath(_settings, _series) + Subject.GetSeriesPath(_settings, _artist) .Should().BeNull(); } @@ -87,7 +87,7 @@ namespace NzbDrone.Core.Test.NotificationTests.Xbmc.Http .Returns(queryResult); - Subject.GetSeriesPath(_settings, _series) + Subject.GetSeriesPath(_settings, _artist) .Should().Be("smb://xbmc:xbmc@HOMESERVER/TV/Law & Order- Special Victims Unit/"); } } diff --git a/src/NzbDrone.Core.Test/NotificationTests/Xbmc/Http/UpdateFixture.cs b/src/NzbDrone.Core.Test/NotificationTests/Xbmc/Http/UpdateFixture.cs index aad928f95..d11ab4ad4 100644 --- a/src/NzbDrone.Core.Test/NotificationTests/Xbmc/Http/UpdateFixture.cs +++ b/src/NzbDrone.Core.Test/NotificationTests/Xbmc/Http/UpdateFixture.cs @@ -1,9 +1,9 @@ -using FizzWare.NBuilder; +using FizzWare.NBuilder; using NUnit.Framework; using NzbDrone.Common.Http; using NzbDrone.Core.Notifications.Xbmc; using NzbDrone.Core.Test.Framework; -using NzbDrone.Core.Tv; +using NzbDrone.Core.Music; namespace NzbDrone.Core.Test.NotificationTests.Xbmc.Http { @@ -12,7 +12,7 @@ namespace NzbDrone.Core.Test.NotificationTests.Xbmc.Http { private XbmcSettings _settings; private string _seriesQueryUrl = "http://localhost:8080/xbmcCmds/xbmcHttp?command=QueryVideoDatabase(select path.strPath from path, tvshow, tvshowlinkpath where tvshow.c12 = 79488 and tvshowlinkpath.idShow = tvshow.idShow and tvshowlinkpath.idPath = path.idPath)"; - private Series _fakeSeries; + private Artist _fakeSeries; [SetUp] public void Setup() @@ -28,9 +28,9 @@ namespace NzbDrone.Core.Test.NotificationTests.Xbmc.Http UpdateLibrary = true }; - _fakeSeries = Builder.CreateNew() - .With(s => s.TvdbId = 79488) - .With(s => s.Title = "30 Rock") + _fakeSeries = Builder.CreateNew() + .With(s => s.ForeignArtistId = "79488") + .With(s => s.Name = "30 Rock") .Build(); } diff --git a/src/NzbDrone.Core.Test/NotificationTests/Xbmc/Json/GetSeriesPathFixture.cs b/src/NzbDrone.Core.Test/NotificationTests/Xbmc/Json/GetSeriesPathFixture.cs index b4b29dff2..650939619 100644 --- a/src/NzbDrone.Core.Test/NotificationTests/Xbmc/Json/GetSeriesPathFixture.cs +++ b/src/NzbDrone.Core.Test/NotificationTests/Xbmc/Json/GetSeriesPathFixture.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using FizzWare.NBuilder; using FluentAssertions; @@ -6,16 +6,16 @@ using NUnit.Framework; using NzbDrone.Core.Notifications.Xbmc; using NzbDrone.Core.Notifications.Xbmc.Model; using NzbDrone.Core.Test.Framework; -using NzbDrone.Core.Tv; +using NzbDrone.Core.Music; namespace NzbDrone.Core.Test.NotificationTests.Xbmc.Json { [TestFixture] public class GetSeriesPathFixture : CoreTest { - private const int TVDB_ID = 5; + private const string MB_ID = "5"; private XbmcSettings _settings; - private Series _series; + private Artist _artist; private List _xbmcSeries; [SetUp] @@ -28,39 +28,39 @@ namespace NzbDrone.Core.Test.NotificationTests.Xbmc.Json .All() .With(s => s.ImdbNumber = "0") .TheFirst(1) - .With(s => s.ImdbNumber = TVDB_ID.ToString()) + .With(s => s.ImdbNumber = MB_ID.ToString()) .Build() .ToList(); Mocker.GetMock() - .Setup(s => s.GetSeries(_settings)) + .Setup(s => s.GetArtist(_settings)) .Returns(_xbmcSeries); } private void GivenMatchingTvdbId() { - _series = new Series - { - TvdbId = TVDB_ID, - Title = "TV Show" + _artist = new Artist + { + ForeignArtistId = MB_ID, + Name = "TV Show" }; } private void GivenMatchingTitle() { - _series = new Series + _artist = new Artist { - TvdbId = 1000, - Title = _xbmcSeries.First().Label + ForeignArtistId = "1000", + Name = _xbmcSeries.First().Label }; } private void GivenMatchingSeries() { - _series = new Series + _artist = new Artist { - TvdbId = 1000, - Title = "Does not exist" + ForeignArtistId = "1000", + Name = "Does not exist" }; } @@ -69,7 +69,7 @@ namespace NzbDrone.Core.Test.NotificationTests.Xbmc.Json { GivenMatchingSeries(); - Subject.GetSeriesPath(_settings, _series).Should().BeNull(); + Subject.GetSeriesPath(_settings, _artist).Should().BeNull(); } [Test] @@ -77,7 +77,7 @@ namespace NzbDrone.Core.Test.NotificationTests.Xbmc.Json { GivenMatchingTvdbId(); - Subject.GetSeriesPath(_settings, _series).Should().Be(_xbmcSeries.First().File); + Subject.GetSeriesPath(_settings, _artist).Should().Be(_xbmcSeries.First().File); } [Test] @@ -85,7 +85,7 @@ namespace NzbDrone.Core.Test.NotificationTests.Xbmc.Json { GivenMatchingTitle(); - Subject.GetSeriesPath(_settings, _series).Should().Be(_xbmcSeries.First().File); + Subject.GetSeriesPath(_settings, _artist).Should().Be(_xbmcSeries.First().File); } [Test] @@ -94,13 +94,13 @@ namespace NzbDrone.Core.Test.NotificationTests.Xbmc.Json GivenMatchingTvdbId(); _xbmcSeries.ForEach(s => s.ImdbNumber = "tt12345"); - _xbmcSeries.Last().ImdbNumber = TVDB_ID.ToString(); + _xbmcSeries.Last().ImdbNumber = MB_ID.ToString(); Mocker.GetMock() - .Setup(s => s.GetSeries(_settings)) + .Setup(s => s.GetArtist(_settings)) .Returns(_xbmcSeries); - Subject.GetSeriesPath(_settings, _series).Should().NotBeNull(); + Subject.GetSeriesPath(_settings, _artist).Should().NotBeNull(); } } } diff --git a/src/NzbDrone.Core.Test/NotificationTests/Xbmc/Json/UpdateFixture.cs b/src/NzbDrone.Core.Test/NotificationTests/Xbmc/Json/UpdateFixture.cs index 408f2eeba..4e84fbd0c 100644 --- a/src/NzbDrone.Core.Test/NotificationTests/Xbmc/Json/UpdateFixture.cs +++ b/src/NzbDrone.Core.Test/NotificationTests/Xbmc/Json/UpdateFixture.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using FizzWare.NBuilder; using Moq; @@ -6,16 +6,16 @@ using NUnit.Framework; using NzbDrone.Core.Notifications.Xbmc; using NzbDrone.Core.Notifications.Xbmc.Model; using NzbDrone.Core.Test.Framework; -using NzbDrone.Core.Tv; +using NzbDrone.Core.Music; namespace NzbDrone.Core.Test.NotificationTests.Xbmc.Json { [TestFixture] public class UpdateFixture : CoreTest { - private const int TVDB_ID = 5; + private const string MB_ID = "5"; private XbmcSettings _settings; - private List _xbmcSeries; + private List _xbmcArtist; [SetUp] public void Setup() @@ -23,15 +23,15 @@ namespace NzbDrone.Core.Test.NotificationTests.Xbmc.Json _settings = Builder.CreateNew() .Build(); - _xbmcSeries = Builder.CreateListOfSize(3) + _xbmcArtist = Builder.CreateListOfSize(3) .TheFirst(1) - .With(s => s.ImdbNumber = TVDB_ID.ToString()) + .With(s => s.ImdbNumber = MB_ID.ToString()) .Build() .ToList(); Mocker.GetMock() - .Setup(s => s.GetSeries(_settings)) - .Returns(_xbmcSeries); + .Setup(s => s.GetArtist(_settings)) + .Returns(_xbmcArtist); Mocker.GetMock() .Setup(s => s.GetActivePlayers(_settings)) @@ -41,8 +41,8 @@ namespace NzbDrone.Core.Test.NotificationTests.Xbmc.Json [Test] public void should_update_using_series_path() { - var series = Builder.CreateNew() - .With(s => s.TvdbId = TVDB_ID) + var series = Builder.CreateNew() + .With(s => s.ForeignArtistId = MB_ID) .Build(); Subject.Update(_settings, series); @@ -54,9 +54,9 @@ namespace NzbDrone.Core.Test.NotificationTests.Xbmc.Json [Test] public void should_update_all_paths_when_series_path_not_found() { - var fakeSeries = Builder.CreateNew() - .With(s => s.TvdbId = 1000) - .With(s => s.Title = "Not 30 Rock") + var fakeSeries = Builder.CreateNew() + .With(s => s.ForeignArtistId = "1000") + .With(s => s.Name = "Not 30 Rock") .Build(); Subject.Update(_settings, fakeSeries); diff --git a/src/NzbDrone.Core.Test/NotificationTests/Xbmc/OnDownloadFixture.cs b/src/NzbDrone.Core.Test/NotificationTests/Xbmc/OnDownloadFixture.cs index c43786614..2dba7a3af 100644 --- a/src/NzbDrone.Core.Test/NotificationTests/Xbmc/OnDownloadFixture.cs +++ b/src/NzbDrone.Core.Test/NotificationTests/Xbmc/OnDownloadFixture.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using FizzWare.NBuilder; using Moq; @@ -7,7 +7,7 @@ using NzbDrone.Core.MediaFiles; using NzbDrone.Core.Notifications; using NzbDrone.Core.Notifications.Xbmc; using NzbDrone.Core.Test.Framework; -using NzbDrone.Core.Tv; +using NzbDrone.Core.Music; namespace NzbDrone.Core.Test.NotificationTests.Xbmc { @@ -19,16 +19,16 @@ namespace NzbDrone.Core.Test.NotificationTests.Xbmc [SetUp] public void Setup() { - var series = Builder.CreateNew() + var artist = Builder.CreateNew() .Build(); - var episodeFile = Builder.CreateNew() + var trackFile = Builder.CreateNew() .Build(); _downloadMessage = Builder.CreateNew() - .With(d => d.Series = series) - .With(d => d.EpisodeFile = episodeFile) - .With(d => d.OldFiles = new List()) + .With(d => d.Artist = artist) + .With(d => d.TrackFile = trackFile) + .With(d => d.OldFiles = new List()) .Build(); Subject.Definition = new NotificationDefinition(); @@ -40,7 +40,7 @@ namespace NzbDrone.Core.Test.NotificationTests.Xbmc private void GivenOldFiles() { - _downloadMessage.OldFiles = Builder.CreateListOfSize(1) + _downloadMessage.OldFiles = Builder.CreateListOfSize(1) .Build() .ToList(); diff --git a/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj b/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj index d296761bb..2326c120a 100644 --- a/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj +++ b/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj @@ -9,8 +9,8 @@ Library Properties NzbDrone.Core.Test - NzbDrone.Core.Test - v4.0 + Lidarr.Core.Test + v4.6.1 512 ..\ true @@ -47,50 +47,45 @@ ..\packages\AutoMoq.1.8.1.0\lib\net40\AutoMoq.dll - True ..\packages\NBuilder.4.0.0\lib\net40\FizzWare.NBuilder.dll - True - ..\packages\FluentAssertions.4.19.0\lib\net40\FluentAssertions.dll + ..\packages\FluentAssertions.4.19.0\lib\net45\FluentAssertions.dll - ..\packages\FluentAssertions.4.19.0\lib\net40\FluentAssertions.Core.dll + ..\packages\FluentAssertions.4.19.0\lib\net45\FluentAssertions.Core.dll ..\packages\FluentMigrator.1.6.2\lib\40\FluentMigrator.dll - True ..\packages\FluentMigrator.Runner.1.6.2\lib\40\FluentMigrator.Runner.dll - True - ..\packages\FluentValidation.6.2.1.0\lib\portable-net40+sl50+wp80+win8+wpa81\FluentValidation.dll - True + ..\packages\FluentValidation.6.2.1.0\lib\Net45\FluentValidation.dll ..\packages\CommonServiceLocator.1.3\lib\portable-net4+sl5+netcore45+wpa81+wp8\Microsoft.Practices.ServiceLocation.dll ..\packages\Unity.2.1.505.2\lib\NET35\Microsoft.Practices.Unity.dll - True ..\packages\Unity.2.1.505.2\lib\NET35\Microsoft.Practices.Unity.Configuration.dll - True - ..\packages\Newtonsoft.Json.9.0.1\lib\net40\Newtonsoft.Json.dll - True + ..\packages\Newtonsoft.Json.9.0.1\lib\net45\Newtonsoft.Json.dll - ..\packages\NLog.4.4.3\lib\net40\NLog.dll + ..\packages\NLog.4.4.12\lib\net45\NLog.dll - ..\packages\NUnit.3.6.0\lib\net40\nunit.framework.dll + ..\packages\NUnit.3.6.0\lib\net45\nunit.framework.dll + + + ..\packages\Prowlin.0.9.4456.26422\lib\net40\Prowlin.dll @@ -101,15 +96,6 @@ ..\packages\Moq.4.0.10827\lib\NET40\Moq.dll - - ..\packages\Prowlin.0.9.4456.26422\lib\net40\Prowlin.dll - - - ..\packages\xunit.abstractions.2.0.1\lib\net35\xunit.abstractions.dll - - - ..\packages\xunit.runner.utility.2.2.0\lib\net35\xunit.runner.utility.net35.dll - @@ -118,9 +104,6 @@ - - - @@ -128,30 +111,13 @@ - - - - - - - - - - - - - - - - - - + @@ -166,14 +132,16 @@ + - + + @@ -219,7 +187,6 @@ - @@ -234,6 +201,7 @@ + @@ -244,7 +212,8 @@ - + + @@ -275,30 +244,36 @@ + + + - + + + - + - + + - + @@ -328,29 +303,23 @@ - - - - - - + - + - @@ -361,29 +330,16 @@ - + - - - - - - - - - - - - - - - - - + + + + @@ -416,6 +372,10 @@ sqlite3.dll Always + + Designer + Always + Always Designer @@ -567,11 +527,13 @@ Always - + + + diff --git a/src/NzbDrone.Core.Test/OrganizerTests/FileNameBuilderTests/TitleTheFixture.cs b/src/NzbDrone.Core.Test/OrganizerTests/FileNameBuilderTests/TitleTheFixture.cs new file mode 100644 index 000000000..ee407d3e0 --- /dev/null +++ b/src/NzbDrone.Core.Test/OrganizerTests/FileNameBuilderTests/TitleTheFixture.cs @@ -0,0 +1,87 @@ +using System.Collections.Generic; +using System.Linq; +using FizzWare.NBuilder; +using FluentAssertions; +using NUnit.Framework; +using NzbDrone.Core.MediaFiles; +using NzbDrone.Core.Organizer; +using NzbDrone.Core.Qualities; +using NzbDrone.Core.Test.Framework; +using NzbDrone.Core.Music; + +namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests +{ + [TestFixture] + public class TitleTheFixture : CoreTest + { + private Artist _artist; + private Album _album; + private Track _track; + private TrackFile _trackFile; + private NamingConfig _namingConfig; + + [SetUp] + public void Setup() + { + _artist = Builder + .CreateNew() + .With(s => s.Name = "Alien Ant Farm") + .Build(); + + _album = Builder + .CreateNew() + .With(s => s.Title = "Anthology") + .Build(); + + _track = Builder.CreateNew() + .With(e => e.Title = "City Sushi") + .With(e => e.TrackNumber = 6) + .Build(); + + _trackFile = new TrackFile { Quality = new QualityModel(Quality.MP3_320), ReleaseGroup = "LidarrTest" }; + + _namingConfig = NamingConfig.Default; + _namingConfig.RenameTracks = true; + + Mocker.GetMock() + .Setup(c => c.GetConfig()).Returns(_namingConfig); + + Mocker.GetMock() + .Setup(v => v.Get(Moq.It.IsAny())) + .Returns(v => Quality.DefaultQualityDefinitions.First(c => c.Quality == v)); + } + + [TestCase("The Mist", "Mist, The")] + [TestCase("A Place to Call Home", "Place to Call Home, A")] + [TestCase("An Adventure in Space and Time", "Adventure in Space and Time, An")] + [TestCase("The Flash (2010)", "Flash, The (2010)")] + [TestCase("A League Of Their Own (AU)", "League Of Their Own, A (AU)")] + [TestCase("The Fixer (ZH) (2015)", "Fixer, The (ZH) (2015)")] + [TestCase("The Sixth Sense 2 (Thai)", "Sixth Sense 2, The (Thai)")] + [TestCase("The Amazing Race (Latin America)", "Amazing Race, The (Latin America)")] + [TestCase("The Rat Pack (A&E)", "Rat Pack, The (A&E)")] + [TestCase("The Climax: I (Almost) Got Away With It (2016)", "Climax- I (Almost) Got Away With It, The (2016)")] + //[TestCase("", "")] + public void should_get_expected_title_back(string name, string expected) + { + _artist.Name = name; + _namingConfig.StandardTrackFormat = "{Artist NameThe}"; + + Subject.BuildTrackFileName(new List { _track }, _artist, _album, _trackFile) + .Should().Be(expected); + } + + [TestCase("A")] + [TestCase("Anne")] + [TestCase("Theodore")] + [TestCase("3%")] + public void should_not_change_title(string name) + { + _artist.Name = name; + _namingConfig.StandardTrackFormat = "{Artist NameThe}"; + + Subject.BuildTrackFileName(new List { _track }, _artist, _album, _trackFile) + .Should().Be(name); + } + } +} diff --git a/src/NzbDrone.Core.Test/ParserTests/AbsoluteEpisodeNumberParserFixture.cs b/src/NzbDrone.Core.Test/ParserTests/AbsoluteEpisodeNumberParserFixture.cs deleted file mode 100644 index 9cdbf08e4..000000000 --- a/src/NzbDrone.Core.Test/ParserTests/AbsoluteEpisodeNumberParserFixture.cs +++ /dev/null @@ -1,131 +0,0 @@ -using System.Linq; -using FluentAssertions; -using NUnit.Framework; -using NzbDrone.Core.Test.Framework; - -namespace NzbDrone.Core.Test.ParserTests -{ - - [TestFixture] - public class AbsoluteEpisodeNumberParserFixture : CoreTest - { - [TestCase("[SubDESU]_High_School_DxD_07_(1280x720_x264-AAC)_[6B7FD717]", "High School DxD", 7, 0, 0)] - [TestCase("[Chihiro]_Working!!_-_06_[848x480_H.264_AAC][859EEAFA]", "Working!!", 6, 0, 0)] - [TestCase("[Commie]_Senki_Zesshou_Symphogear_-_11_[65F220B4]", "Senki Zesshou Symphogear", 11, 0, 0)] - [TestCase("[Underwater]_Rinne_no_Lagrange_-_12_(720p)_[5C7BC4F9]", "Rinne no Lagrange", 12, 0, 0)] - [TestCase("[Commie]_Rinne_no_Lagrange_-_15_[E76552EA]", "Rinne no Lagrange", 15, 0, 0)] - [TestCase("[HorribleSubs]_Hunter_X_Hunter_-_33_[720p]", "Hunter X Hunter", 33, 0, 0)] - [TestCase("[HorribleSubs]_Fairy_Tail_-_145_[720p]", "Fairy Tail", 145, 0, 0)] - [TestCase("[HorribleSubs] Tonari no Kaibutsu-kun - 13 [1080p].mkv", "Tonari no Kaibutsu-kun", 13, 0, 0)] - [TestCase("[Doremi].Yes.Pretty.Cure.5.Go.Go!.31.[1280x720].[C65D4B1F].mkv", "Yes Pretty Cure 5 Go Go!", 31, 0, 0)] - [TestCase("[K-F] One Piece 214", "One Piece", 214, 0, 0)] - [TestCase("[K-F] One Piece S10E14 214", "One Piece", 214, 10, 14)] - [TestCase("[K-F] One Piece 10x14 214", "One Piece", 214, 10, 14)] - [TestCase("[K-F] One Piece 214 10x14", "One Piece", 214, 10, 14)] -// [TestCase("One Piece S10E14 214", "One Piece", 214, 10, 14)] -// [TestCase("One Piece 10x14 214", "One Piece", 214, 10, 14)] -// [TestCase("One Piece 214 10x14", "One Piece", 214, 10, 14)] -// [TestCase("214 One Piece 10x14", "One Piece", 214, 10, 14)] - [TestCase("Bleach - 031 - The Resolution to Kill [Lunar].avi", "Bleach", 31, 0, 0)] - [TestCase("Bleach - 031 - The Resolution to Kill [Lunar]", "Bleach", 31, 0, 0)] - [TestCase("[ACX]Hack Sign 01 Role Play [Kosaka] [9C57891E].mkv", "Hack Sign", 1, 0, 0)] - [TestCase("[SFW-sage] Bakuman S3 - 12 [720p][D07C91FC]", "Bakuman S3", 12, 0, 0)] - [TestCase("ducktales_e66_time_is_money_part_one_marking_time", "ducktales", 66, 0, 0)] - [TestCase("[Underwater-FFF] No Game No Life - 01 (720p) [27AAA0A0].mkv", "No Game No Life", 1, 0, 0)] - [TestCase("[FroZen] Miyuki - 23 [DVD][7F6170E6]", "Miyuki", 23, 0, 0)] - [TestCase("[Commie] Yowamushi Pedal - 32 [0BA19D5B]", "Yowamushi Pedal", 32, 0, 0)] - [TestCase("[Doki] Mahouka Koukou no Rettousei - 07 (1280x720 Hi10P AAC) [80AF7DDE]", "Mahouka Koukou no Rettousei", 7, 0, 0)] - [TestCase("[HorribleSubs] Yowamushi Pedal - 32 [480p]", "Yowamushi Pedal", 32, 0, 0)] - [TestCase("[CR] Sailor Moon - 004 [480p][48CE2D0F]", "Sailor Moon", 4, 0, 0)] - [TestCase("[Chibiki] Puchimas!! - 42 [360p][7A4FC77B]", "Puchimas!!", 42, 0, 0)] - [TestCase("[HorribleSubs] Yowamushi Pedal - 32 [1080p]", "Yowamushi Pedal", 32, 0, 0)] - [TestCase("[HorribleSubs] Love Live! S2 - 07 [720p]", "Love Live! S2", 7, 0, 0)] - [TestCase("[DeadFish] Onee-chan ga Kita - 09v2 [720p][AAC]", "Onee-chan ga Kita", 9, 0, 0)] - [TestCase("[Underwater-FFF] No Game No Life - 01 (720p) [27AAA0A0]", "No Game No Life", 1, 0, 0)] - [TestCase("[S-T-D] Soul Eater Not! - 06 (1280x720 10bit AAC) [59B3F2EA].mkv", "Soul Eater Not!", 6, 0, 0)] - [TestCase("No Game No Life - 010 (720p) [27AAA0A0].mkv", "No Game No Life", 10, 0, 0)] - [TestCase("Initial D Fifth Stage - 01 DVD - Central Anime", "Initial D Fifth Stage", 1, 0, 0)] - [TestCase("Initial_D_Fifth_Stage_-_01(DVD)_-_(Central_Anime)[5AF6F1E4].mkv", "Initial D Fifth Stage", 1, 0, 0)] - [TestCase("Initial_D_Fifth_Stage_-_02(DVD)_-_(Central_Anime)[0CA65F00].mkv", "Initial D Fifth Stage", 2, 0, 0)] - [TestCase("Initial D Fifth Stage - 03 DVD - Central Anime", "Initial D Fifth Stage", 3, 0, 0)] - [TestCase("Initial_D_Fifth_Stage_-_03(DVD)_-_(Central_Anime)[629BD592].mkv", "Initial D Fifth Stage", 3, 0, 0)] - [TestCase("Initial D Fifth Stage - 14 DVD - Central Anime", "Initial D Fifth Stage", 14, 0, 0)] - [TestCase("Initial_D_Fifth_Stage_-_14(DVD)_-_(Central_Anime)[0183D922].mkv", "Initial D Fifth Stage", 14, 0, 0)] -// [TestCase("Initial D - 4th Stage Ep 01.mkv", "Initial D - 4th Stage", 1, 0, 0)] - [TestCase("[ChihiroDesuYo].No.Game.No.Life.-.09.1280x720.10bit.AAC.[24CCE81D]", "No Game No Life", 9, 0, 0)] - [TestCase("Fairy Tail - 001 - Fairy Tail", "Fairy Tail", 001, 0, 0)] - [TestCase("Fairy Tail - 049 - The Day of Fated Meeting", "Fairy Tail", 049, 0, 0)] - [TestCase("Fairy Tail - 050 - Special Request Watch Out for the Guy You Like!", "Fairy Tail", 050, 0, 0)] - [TestCase("Fairy Tail - 099 - Natsu vs. Gildarts", "Fairy Tail", 099, 0, 0)] - [TestCase("Fairy Tail - 100 - Mest", "Fairy Tail", 100, 0, 0)] -// [TestCase("Fairy Tail - 101 - Mest", "Fairy Tail", 101, 0, 0)] //This gets caught up in the 'see' numbering - [TestCase("[Exiled-Destiny] Angel Beats Ep01 (D2201EC5).mkv", "Angel Beats", 1, 0, 0)] - [TestCase("[Commie] Nobunaga the Fool - 23 [5396CA24].mkv", "Nobunaga the Fool", 23, 0, 0)] - [TestCase("[FFF] Seikoku no Dragonar - 01 [1FB538B5].mkv", "Seikoku no Dragonar", 1, 0, 0)] - [TestCase("[Hatsuyuki]Fate_Zero-01[1280x720][122E6EF8]", "Fate Zero", 1, 0, 0)] - [TestCase("[CBM]_Monster_-_11_-_511_Kinderheim_[6C70C4E4].mkv", "Monster", 11, 0, 0)] - [TestCase("[HorribleSubs] Log Horizon 2 - 05 [720p].mkv", "Log Horizon 2", 5, 0, 0)] - [TestCase("[Commie] Log Horizon 2 - 05 [FCE4D070].mkv", "Log Horizon 2", 5, 0, 0)] - [TestCase("[DRONE]Series.Title.100", "Series Title", 100, 0, 0)] - [TestCase("[RlsGrp]Series.Title.2010.S01E01.001.HDTV-720p.x264-DTS", "Series Title 2010", 1, 1, 1)] - [TestCase("Dragon Ball Kai - 130 - Found You, Gohan! Harsh Training in the Kaioshin Realm! [Baaro][720p][5A1AD35B].mkv", "Dragon Ball Kai", 130, 0, 0)] - [TestCase("Dragon Ball Kai - 131 - A Merged Super-Warrior Is Born, His Name Is Gotenks!! [Baaro][720p][32E03F96].mkv", "Dragon Ball Kai", 131, 0, 0)] - [TestCase("[HorribleSubs] Magic Kaito 1412 - 01 [1080p]", "Magic Kaito 1412", 1, 0, 0)] - [TestCase("[Jumonji-Giri]_[F-B]_Kagihime_Monogatari_Eikyuu_Alice_Rondo_Ep04_(0b0e2c10).mkv", "Kagihime Monogatari Eikyuu Alice Rondo", 4, 0, 0)] - [TestCase("[Jumonji-Giri]_[F-B]_Kagihime_Monogatari_Eikyuu_Alice_Rondo_Ep08_(8246e542).mkv", "Kagihime Monogatari Eikyuu Alice Rondo", 8, 0, 0)] - [TestCase("Knights of Sidonia - 01 [1080p 10b DTSHD-MA eng sub].mkv", "Knights of Sidonia", 1, 0, 0)] - [TestCase("Series Title (2010) {01} Episode Title (1).hdtv-720p", "Series Title (2010)", 1, 0, 0)] - [TestCase("[HorribleSubs] Shirobako - 20 [720p].mkv", "Shirobako", 20, 0, 0)] - [TestCase("[Hatsuyuki] Dragon Ball Kai (2014) - 017 (115) [1280x720][B2CFBC0F]", "Dragon Ball Kai (2014)", 17, 0, 0)] - [TestCase("[Hatsuyuki] Dragon Ball Kai (2014) - 018 (116) [1280x720][C4A3B16E]", "Dragon Ball Kai (2014)", 18, 0, 0)] - [TestCase("Dragon Ball Kai (2014) - 39 (137) [v2][720p.HDTV][Unison Fansub]", "Dragon Ball Kai (2014)", 39, 0, 0)] - [TestCase("[HorribleSubs] Eyeshield 21 - 101 [480p].mkv", "Eyeshield 21", 101, 0, 0)] - [TestCase("[Cthuyuu].Taimadou.Gakuen.35.Shiken.Shoutai.-.03.[720p.H264.AAC][8AD82C3A]", "Taimadou Gakuen 35 Shiken Shoutai", 3, 0, 0)] - //[TestCase("Taimadou.Gakuen.35.Shiken.Shoutai.-.03.(1280x720.HEVC.AAC)", "Taimadou Gakuen 35 Shiken Shoutai", 3, 0, 0)] - [TestCase("[Cthuyuu] Taimadou Gakuen 35 Shiken Shoutai - 03 [720p H264 AAC][8AD82C3A]", "Taimadou Gakuen 35 Shiken Shoutai", 3, 0, 0)] - [TestCase("Dragon Ball Super Episode 56 [VOSTFR V2][720p][AAC]-Mystic Z-Team", "Dragon Ball Super", 56, 0, 0)] - [TestCase("[Mystic Z-Team] Dragon Ball Super Episode 69 [VOSTFR_Finale][1080p][AAC].mp4", "Dragon Ball Super", 69, 0, 0)] - //[TestCase("", "", 0, 0, 0)] - public void should_parse_absolute_numbers(string postTitle, string title, int absoluteEpisodeNumber, int seasonNumber, int episodeNumber) - { - var result = Parser.Parser.ParseTitle(postTitle); - result.Should().NotBeNull(); - result.AbsoluteEpisodeNumbers.Single().Should().Be(absoluteEpisodeNumber); - result.SeasonNumber.Should().Be(seasonNumber); - result.EpisodeNumbers.SingleOrDefault().Should().Be(episodeNumber); - result.SeriesTitle.Should().Be(title); - result.FullSeason.Should().BeFalse(); - } - - [TestCase("[DeadFish] Kenzen Robo Daimidaler - 01 - Special [BD][720p][AAC]", "Kenzen Robo Daimidaler", 1)] - [TestCase("[DeadFish] Kenzen Robo Daimidaler - 01 - OVA [BD][720p][AAC]", "Kenzen Robo Daimidaler", 1)] - [TestCase("[DeadFish] Kenzen Robo Daimidaler - 01 - OVD [BD][720p][AAC]", "Kenzen Robo Daimidaler", 1)] - public void should_parse_absolute_specials(string postTitle, string title, int absoluteEpisodeNumber) - { - var result = Parser.Parser.ParseTitle(postTitle); - result.Should().NotBeNull(); - result.AbsoluteEpisodeNumbers.Single().Should().Be(absoluteEpisodeNumber); - result.SeasonNumber.Should().Be(0); - result.EpisodeNumbers.SingleOrDefault().Should().Be(0); - result.SeriesTitle.Should().Be(title); - result.FullSeason.Should().BeFalse(); - result.Special.Should().BeTrue(); - } - - [TestCase("[ANBU-AonE]_Naruto_26-27_[F224EF26].avi", "Naruto", new[] { 26, 27 })] - [TestCase("[Doutei] Recently, My Sister is Unusual - 01-12 [BD][720p-AAC]", "Recently, My Sister is Unusual", new [] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12 })] - [TestCase("Series Title (2010) - 01-02-03 - Episode Title (1) HDTV-720p", "Series Title (2010)", new [] { 1, 2, 3 })] - [TestCase("[RlsGrp] Series Title (2010) - S01E01-02-03 - 001-002-003 - Episode Title HDTV-720p v2", "Series Title (2010)", new[] { 1, 2, 3 })] - [TestCase("[RlsGrp] Series Title (2010) - S01E01-02 - 001-002 - Episode Title HDTV-720p v2", "Series Title (2010)", new[] { 1, 2 })] - [TestCase("Series Title (2010) - S01E01-02 (001-002) - Episode Title (1) HDTV-720p v2 [RlsGrp]", "Series Title (2010)", new[] { 1, 2 })] - [TestCase("[HorribleSubs] Haikyuu!! (01-25) [1080p] (Batch)", "Haikyuu!!", new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25 })] - public void should_parse_multi_episode_absolute_numbers(string postTitle, string title, int[] absoluteEpisodeNumbers) - { - var result = Parser.Parser.ParseTitle(postTitle); - result.Should().NotBeNull(); - result.AbsoluteEpisodeNumbers.Should().BeEquivalentTo(absoluteEpisodeNumbers); - result.SeriesTitle.Should().Be(title); - result.FullSeason.Should().BeFalse(); - } - } -} diff --git a/src/NzbDrone.Core.Test/ParserTests/AnimeMetadataParserFixture.cs b/src/NzbDrone.Core.Test/ParserTests/AnimeMetadataParserFixture.cs deleted file mode 100644 index 599da12aa..000000000 --- a/src/NzbDrone.Core.Test/ParserTests/AnimeMetadataParserFixture.cs +++ /dev/null @@ -1,34 +0,0 @@ -using FluentAssertions; -using NUnit.Framework; -using NzbDrone.Core.Test.Framework; - -namespace NzbDrone.Core.Test.ParserTests -{ - - [TestFixture] - public class AnimeMetadataParserFixture : CoreTest - { - [TestCase("[SubDESU]_High_School_DxD_07_(1280x720_x264-AAC)_[6B7FD717]", "SubDESU", "6B7FD717")] - [TestCase("[Chihiro]_Working!!_-_06_[848x480_H.264_AAC][859EEAFA]", "Chihiro", "859EEAFA")] - [TestCase("[Underwater]_Rinne_no_Lagrange_-_12_(720p)_[5C7BC4F9]", "Underwater", "5C7BC4F9")] - [TestCase("[HorribleSubs]_Hunter_X_Hunter_-_33_[720p]", "HorribleSubs", "")] - [TestCase("[HorribleSubs] Tonari no Kaibutsu-kun - 13 [1080p].mkv", "HorribleSubs", "")] - [TestCase("[Doremi].Yes.Pretty.Cure.5.Go.Go!.31.[1280x720].[C65D4B1F].mkv", "Doremi", "C65D4B1F")] - [TestCase("[Doremi].Yes.Pretty.Cure.5.Go.Go!.31.[1280x720].[C65D4B1F]", "Doremi", "C65D4B1F")] - [TestCase("[Doremi].Yes.Pretty.Cure.5.Go.Go!.31.[1280x720].mkv", "Doremi", "")] - [TestCase("[K-F] One Piece 214", "K-F", "")] - [TestCase("[K-F] One Piece S10E14 214", "K-F", "")] - [TestCase("[K-F] One Piece 10x14 214", "K-F", "")] - [TestCase("[K-F] One Piece 214 10x14", "K-F", "")] - [TestCase("Bleach - 031 - The Resolution to Kill [Lunar].avi", "Lunar", "")] - [TestCase("[ACX]Hack Sign 01 Role Play [Kosaka] [9C57891E].mkv", "ACX", "9C57891E")] - [TestCase("[S-T-D] Soul Eater Not! - 06 (1280x720 10bit AAC) [59B3F2EA].mkv", "S-T-D", "59B3F2EA")] - public void should_parse_absolute_numbers(string postTitle, string subGroup, string hash) - { - var result = Parser.Parser.ParseTitle(postTitle); - result.Should().NotBeNull(); - result.ReleaseGroup.Should().Be(subGroup); - result.ReleaseHash.Should().Be(hash); - } - } -} diff --git a/src/NzbDrone.Core.Test/ParserTests/ArtistTitleInfoFixture.cs b/src/NzbDrone.Core.Test/ParserTests/ArtistTitleInfoFixture.cs new file mode 100644 index 000000000..3d38c3eed --- /dev/null +++ b/src/NzbDrone.Core.Test/ParserTests/ArtistTitleInfoFixture.cs @@ -0,0 +1,60 @@ +using FluentAssertions; +using NUnit.Framework; +using NzbDrone.Core.Test.Framework; + +namespace NzbDrone.Core.Test.ParserTests +{ + [TestFixture] + public class ArtistTitleInfoFixture : CoreTest + { + [Test] + public void should_have_year_zero_when_title_doesnt_have_a_year() + { + const string title = "House.S01E01.pilot.720p.hdtv"; + + var result = Parser.Parser.ParseAlbumTitle(title).ArtistTitleInfo; + + result.Year.Should().Be(0); + } + + [Test] + public void should_have_same_title_for_title_and_title_without_year_when_title_doesnt_have_a_year() + { + const string title = "House.S01E01.pilot.720p.hdtv"; + + var result = Parser.Parser.ParseAlbumTitle(title).ArtistTitleInfo; + + result.Title.Should().Be(result.TitleWithoutYear); + } + + [Test] + public void should_have_year_when_title_has_a_year() + { + const string title = "House.2004.S01E01.pilot.720p.hdtv"; + + var result = Parser.Parser.ParseAlbumTitle(title).ArtistTitleInfo; + + result.Year.Should().Be(2004); + } + + [Test] + public void should_have_year_in_title_when_title_has_a_year() + { + const string title = "House.2004.S01E01.pilot.720p.hdtv"; + + var result = Parser.Parser.ParseAlbumTitle(title).ArtistTitleInfo; + + result.Title.Should().Be("House 2004"); + } + + [Test] + public void should_title_without_year_should_not_contain_year() + { + const string title = "House.2004.S01E01.pilot.720p.hdtv"; + + var result = Parser.Parser.ParseAlbumTitle(title).ArtistTitleInfo; + + result.TitleWithoutYear.Should().Be("House"); + } + } +} diff --git a/src/NzbDrone.Core.Test/ParserTests/CrapParserFixture.cs b/src/NzbDrone.Core.Test/ParserTests/CrapParserFixture.cs index e678bf6a1..1a0493273 100644 --- a/src/NzbDrone.Core.Test/ParserTests/CrapParserFixture.cs +++ b/src/NzbDrone.Core.Test/ParserTests/CrapParserFixture.cs @@ -33,7 +33,7 @@ namespace NzbDrone.Core.Test.ParserTests [TestCase("QZC4HDl7ncmzyUj9amucWe1ddKU1oFMZDd8r0dEDUsTd")] public void should_not_parse_crap(string title) { - Parser.Parser.ParseTitle(title).Should().BeNull(); + Parser.Parser.ParseAlbumTitle(title).Should().BeNull(); ExceptionVerification.IgnoreWarns(); } @@ -52,7 +52,7 @@ namespace NzbDrone.Core.Test.ParserTests hash = BitConverter.ToString(hashData).Replace("-", ""); - if (Parser.Parser.ParseTitle(hash) == null) + if (Parser.Parser.ParseAlbumTitle(hash) == null) success++; } @@ -78,7 +78,7 @@ namespace NzbDrone.Core.Test.ParserTests hash.Append(charset[hashAlgo.Next() % charset.Length]); } - if (Parser.Parser.ParseTitle(hash.ToString()) == null) + if (Parser.Parser.ParseAlbumTitle(hash.ToString()) == null) success++; } @@ -88,7 +88,7 @@ namespace NzbDrone.Core.Test.ParserTests [TestCase("thebiggestloser1618finale")] public void should_not_parse_file_name_without_proper_spacing(string fileName) { - Parser.Parser.ParseTitle(fileName).Should().BeNull(); + Parser.Parser.ParseAlbumTitle(fileName).Should().BeNull(); } } } diff --git a/src/NzbDrone.Core.Test/ParserTests/DailyEpisodeParserFixture.cs b/src/NzbDrone.Core.Test/ParserTests/DailyEpisodeParserFixture.cs deleted file mode 100644 index 7b5cbaaf6..000000000 --- a/src/NzbDrone.Core.Test/ParserTests/DailyEpisodeParserFixture.cs +++ /dev/null @@ -1,80 +0,0 @@ -using System; -using FluentAssertions; -using NUnit.Framework; -using NzbDrone.Common.Expansive; -using NzbDrone.Core.Test.Framework; -using NzbDrone.Core.Tv; - -namespace NzbDrone.Core.Test.ParserTests -{ - - [TestFixture] - public class DailyEpisodeParserFixture : CoreTest - { - [TestCase("Conan 2011 04 18 Emma Roberts HDTV XviD BFF", "Conan", 2011, 04, 18)] - [TestCase("The Tonight Show With Jay Leno 2011 04 15 1080i HDTV DD5 1 MPEG2 TrollHD", "The Tonight Show With Jay Leno", 2011, 04, 15)] - [TestCase("The.Daily.Show.2010.10.11.Johnny.Knoxville.iTouch-MW", "The Daily Show", 2010, 10, 11)] - [TestCase("The Daily Show - 2011-04-12 - Gov. Deval Patrick", "The Daily Show", 2011, 04, 12)] - [TestCase("2011.01.10 - Denis Leary - HD TV.mkv", "", 2011, 1, 10)] - [TestCase("2011.03.13 - Denis Leary - HD TV.mkv", "", 2011, 3, 13)] - [TestCase("The Tonight Show with Jay Leno - 2011-06-16 - Larry David, \"Bachelorette\" Ashley Hebert, Pitbull with Ne-Yo", "The Tonight Show with Jay Leno", 2011, 6, 16)] - [TestCase("2020.NZ.2012.16.02.PDTV.XviD-C4TV", "2020 NZ", 2012, 2, 16)] - [TestCase("2020.NZ.2012.13.02.PDTV.XviD-C4TV", "2020 NZ", 2012, 2, 13)] - [TestCase("2020.NZ.2011.12.02.PDTV.XviD-C4TV", "2020 NZ", 2011, 12, 2)] - [TestCase("Series Title - 2013-10-30 - Episode Title (1) [HDTV-720p]", "Series Title", 2013, 10, 30)] - [TestCase("The_Voice_US_04.28.2014_hdtv.x264.Poke.mp4", "The Voice US", 2014, 4, 28)] - [TestCase("At.Midnight.140722.720p.HDTV.x264-YesTV", "At Midnight", 2014, 07, 22)] - [TestCase("At_Midnight_140722_720p_HDTV_x264-YesTV", "At Midnight", 2014, 07, 22)] - //[TestCase("Corrie.07.01.15", "Corrie", 2015, 1, 7)] - [TestCase("The Nightly Show with Larry Wilmore 2015 02 09 WEBRIP s01e13", "The Nightly Show with Larry Wilmore", 2015, 2, 9)] - //[TestCase("", "", 0, 0, 0)] - public void should_parse_daily_episode(string postTitle, string title, int year, int month, int day) - { - var result = Parser.Parser.ParseTitle(postTitle); - var airDate = new DateTime(year, month, day); - result.Should().NotBeNull(); - result.SeriesTitle.Should().Be(title); - result.AirDate.Should().Be(airDate.ToString(Episode.AIR_DATE_FORMAT)); - result.EpisodeNumbers.Should().BeEmpty(); - result.AbsoluteEpisodeNumbers.Should().BeEmpty(); - result.FullSeason.Should().BeFalse(); - } - - [TestCase("Conan {year} {month} {day} Emma Roberts HDTV XviD BFF")] - [TestCase("The Tonight Show With Jay Leno {year} {month} {day} 1080i HDTV DD5 1 MPEG2 TrollHD")] - [TestCase("The.Daily.Show.{year}.{month}.{day}.Johnny.Knoxville.iTouch-MW")] - [TestCase("The Daily Show - {year}-{month}-{day} - Gov. Deval Patrick")] - [TestCase("{year}.{month}.{day} - Denis Leary - HD TV.mkv")] - [TestCase("The Tonight Show with Jay Leno - {year}-{month}-{day} - Larry David, \"Bachelorette\" Ashley Hebert, Pitbull with Ne-Yo")] - [TestCase("2020.NZ.{year}.{month}.{day}.PDTV.XviD-C4TV")] - public void should_not_accept_ancient_daily_series(string title) - { - var yearTooLow = title.Expand(new { year = 1950, month = 10, day = 14 }); - Parser.Parser.ParseTitle(yearTooLow).Should().BeNull(); - } - - [TestCase("Conan {year} {month} {day} Emma Roberts HDTV XviD BFF")] - [TestCase("The Tonight Show With Jay Leno {year} {month} {day} 1080i HDTV DD5 1 MPEG2 TrollHD")] - [TestCase("The.Daily.Show.{year}.{month}.{day}.Johnny.Knoxville.iTouch-MW")] - [TestCase("The Daily Show - {year}-{month}-{day} - Gov. Deval Patrick")] - [TestCase("{year}.{month}.{day} - Denis Leary - HD TV.mkv")] - [TestCase("The Tonight Show with Jay Leno - {year}-{month}-{day} - Larry David, \"Bachelorette\" Ashley Hebert, Pitbull with Ne-Yo")] - [TestCase("2020.NZ.{year}.{month}.{day}.PDTV.XviD-C4TV")] - public void should_not_accept_future_dates(string title) - { - var twoDaysFromNow = DateTime.Now.AddDays(2); - - var validDate = title.Expand(new { year = twoDaysFromNow.Year, month = twoDaysFromNow.Month.ToString("00"), day = twoDaysFromNow.Day.ToString("00") }); - - Parser.Parser.ParseTitle(validDate).Should().BeNull(); - } - - [Test] - public void should_fail_if_episode_is_far_in_future() - { - var title = string.Format("{0:yyyy.MM.dd} - Denis Leary - HD TV.mkv", DateTime.Now.AddDays(2)); - - Parser.Parser.ParseTitle(title).Should().BeNull(); - } - } -} diff --git a/src/NzbDrone.Core.Test/ParserTests/HashedReleaseFixture.cs b/src/NzbDrone.Core.Test/ParserTests/HashedReleaseFixture.cs index 2e644ba89..c04270181 100644 --- a/src/NzbDrone.Core.Test/ParserTests/HashedReleaseFixture.cs +++ b/src/NzbDrone.Core.Test/ParserTests/HashedReleaseFixture.cs @@ -1,4 +1,4 @@ -using FluentAssertions; +using FluentAssertions; using NUnit.Framework; using NzbDrone.Core.Qualities; using NzbDrone.Core.Test.Framework; @@ -86,7 +86,7 @@ namespace NzbDrone.Core.Test.ParserTests [Test, TestCaseSource(nameof(HashedReleaseParserCases))] public void should_properly_parse_hashed_releases(string path, string title, Quality quality, string releaseGroup) { - var result = Parser.Parser.ParsePath(path); + var result = Parser.Parser.ParseMusicPath(path); //result.SeriesTitle.Should().Be(title); result.Quality.Quality.Should().Be(quality); } diff --git a/src/NzbDrone.Core.Test/ParserTests/IsPossibleSpecialEpisodeFixture.cs b/src/NzbDrone.Core.Test/ParserTests/IsPossibleSpecialEpisodeFixture.cs deleted file mode 100644 index 11f68da85..000000000 --- a/src/NzbDrone.Core.Test/ParserTests/IsPossibleSpecialEpisodeFixture.cs +++ /dev/null @@ -1,43 +0,0 @@ -using FluentAssertions; -using NUnit.Framework; -using NzbDrone.Core.Parser.Model; - -namespace NzbDrone.Core.Test.ParserTests -{ - [TestFixture] - public class IsPossibleSpecialEpisodeFixture - { - [Test] - public void should_not_treat_files_without_a_series_title_as_a_special() - { - var parsedEpisodeInfo = new ParsedEpisodeInfo - { - EpisodeNumbers = new[]{ 7 }, - SeasonNumber = 1, - SeriesTitle = "" - }; - - parsedEpisodeInfo.IsPossibleSpecialEpisode.Should().BeFalse(); - } - - [Test] - public void should_return_true_when_episode_numbers_is_empty() - { - var parsedEpisodeInfo = new ParsedEpisodeInfo - { - SeasonNumber = 1, - SeriesTitle = "" - }; - - parsedEpisodeInfo.IsPossibleSpecialEpisode.Should().BeTrue(); - } - - [TestCase("Under.the.Dome.S02.Special-Inside.Chesters.Mill.HDTV.x264-BAJSKORV")] - [TestCase("Under.the.Dome.S02.Special-Inside.Chesters.Mill.720p.HDTV.x264-BAJSKORV")] - [TestCase("Rookie.Blue.Behind.the.Badge.S05.Special.HDTV.x264-2HD")] - public void IsPossibleSpecialEpisode_should_be_true(string title) - { - Parser.Parser.ParseTitle(title).IsPossibleSpecialEpisode.Should().BeTrue(); - } - } -} diff --git a/src/NzbDrone.Core.Test/ParserTests/LanguageParserFixture.cs b/src/NzbDrone.Core.Test/ParserTests/LanguageParserFixture.cs index c86948b17..cb394d951 100644 --- a/src/NzbDrone.Core.Test/ParserTests/LanguageParserFixture.cs +++ b/src/NzbDrone.Core.Test/ParserTests/LanguageParserFixture.cs @@ -1,5 +1,6 @@ using FluentAssertions; using NUnit.Framework; +using NzbDrone.Core.Languages; using NzbDrone.Core.Parser; using NzbDrone.Core.Test.Framework; @@ -9,58 +10,207 @@ namespace NzbDrone.Core.Test.ParserTests [TestFixture] public class LanguageParserFixture : CoreTest { - [TestCase("Castle.2009.S01E14.English.HDTV.XviD-LOL", Language.English)] - [TestCase("Castle.2009.S01E14.French.HDTV.XviD-LOL", Language.French)] - [TestCase("Castle.2009.S01E14.Spanish.HDTV.XviD-LOL", Language.Spanish)] - [TestCase("Castle.2009.S01E14.German.HDTV.XviD-LOL", Language.German)] - [TestCase("Castle.2009.S01E14.Germany.HDTV.XviD-LOL", Language.English)] - [TestCase("Castle.2009.S01E14.Italian.HDTV.XviD-LOL", Language.Italian)] - [TestCase("Castle.2009.S01E14.Danish.HDTV.XviD-LOL", Language.Danish)] - [TestCase("Castle.2009.S01E14.Dutch.HDTV.XviD-LOL", Language.Dutch)] - [TestCase("Castle.2009.S01E14.Japanese.HDTV.XviD-LOL", Language.Japanese)] - [TestCase("Castle.2009.S01E14.Cantonese.HDTV.XviD-LOL", Language.Cantonese)] - [TestCase("Castle.2009.S01E14.Mandarin.HDTV.XviD-LOL", Language.Mandarin)] - [TestCase("Castle.2009.S01E14.Korean.HDTV.XviD-LOL", Language.Korean)] - [TestCase("Castle.2009.S01E14.Russian.HDTV.XviD-LOL", Language.Russian)] - [TestCase("Castle.2009.S01E14.Polish.HDTV.XviD-LOL", Language.Polish)] - [TestCase("Castle.2009.S01E14.Vietnamese.HDTV.XviD-LOL", Language.Vietnamese)] - [TestCase("Castle.2009.S01E14.Swedish.HDTV.XviD-LOL", Language.Swedish)] - [TestCase("Castle.2009.S01E14.Norwegian.HDTV.XviD-LOL", Language.Norwegian)] - [TestCase("Castle.2009.S01E14.Finnish.HDTV.XviD-LOL", Language.Finnish)] - [TestCase("Castle.2009.S01E14.Turkish.HDTV.XviD-LOL", Language.Turkish)] - [TestCase("Castle.2009.S01E14.Portuguese.HDTV.XviD-LOL", Language.Portuguese)] - [TestCase("Castle.2009.S01E14.HDTV.XviD-LOL", Language.English)] - [TestCase("person.of.interest.1x19.ita.720p.bdmux.x264-novarip", Language.Italian)] - [TestCase("Salamander.S01E01.FLEMISH.HDTV.x264-BRiGAND", Language.Flemish)] - [TestCase("H.Polukatoikia.S03E13.Greek.PDTV.XviD-Ouzo", Language.Greek)] - [TestCase("Burn.Notice.S04E15.Brotherly.Love.GERMAN.DUBBED.WS.WEBRiP.XviD.REPACK-TVP", Language.German)] - [TestCase("Ray Donovan - S01E01.720p.HDtv.x264-Evolve (NLsub)", Language.Dutch)] - [TestCase("Shield,.The.1x13.Tueurs.De.Flics.FR.DVDRip.XviD", Language.French)] - [TestCase("True.Detective.S01E01.1080p.WEB-DL.Rus.Eng.TVKlondike", Language.Russian)] - [TestCase("The.Trip.To.Italy.S02E01.720p.HDTV.x264-TLA", Language.English)] - [TestCase("Revolution S01E03 No Quarter 2012 WEB-DL 720p Nordic-philipo mkv", Language.Norwegian)] - [TestCase("Extant.S01E01.VOSTFR.HDTV.x264-RiDERS", Language.French)] - [TestCase("Constantine.2014.S01E01.WEBRiP.H264.AAC.5.1-NL.SUBS", Language.Dutch)] - [TestCase("Elementary - S02E16 - Kampfhaehne - mkv - by Videomann", Language.German)] - [TestCase("Two.Greedy.Italians.S01E01.The.Family.720p.HDTV.x264-FTP", Language.English)] - [TestCase("Castle.2009.S01E14.HDTV.XviD.HUNDUB-LOL", Language.Hungarian)] - [TestCase("Castle.2009.S01E14.HDTV.XviD.ENG.HUN-LOL", Language.Hungarian)] - [TestCase("Castle.2009.S01E14.HDTV.XviD.HUN-LOL", Language.Hungarian)] - public void should_parse_language(string postTitle, Language language) + [TestCase("Castle.2009.S01E14.English.HDTV.XviD-LOL")] + [TestCase("Castle.2009.S01E14.Germany.HDTV.XviD-LOL")] + [TestCase("Castle.2009.S01E14.HDTV.XviD-LOL")] + [TestCase("Two.Greedy.Italians.S01E01.The.Family.720p.HDTV.x264-FTP")] + [TestCase("The.Trip.To.Italy.S02E01.720p.HDTV.x264-TLA")] + [TestCase("2 Broke Girls - S01E01 - Pilot.en.sub")] + [TestCase("2 Broke Girls - S01E01 - Pilot.eng.sub")] + [TestCase("2 Broke Girls - S01E01 - Pilot.English.sub")] + [TestCase("2 Broke Girls - S01E01 - Pilot.english.sub")] + public void should_parse_language_english(string postTitle) { var result = LanguageParser.ParseLanguage(postTitle); - result.Should().Be(language); + result.Should().Be(Language.English); } - [TestCase("2 Broke Girls - S01E01 - Pilot.en.sub", Language.English)] - [TestCase("2 Broke Girls - S01E01 - Pilot.eng.sub", Language.English)] - [TestCase("2 Broke Girls - S01E01 - Pilot.English.sub", Language.English)] - [TestCase("2 Broke Girls - S01E01 - Pilot.english.sub", Language.English)] - [TestCase("2 Broke Girls - S01E01 - Pilot.sub", Language.Unknown)] - public void should_parse_subtitle_language(string fileName, Language language) + [TestCase("2 Broke Girls - S01E01 - Pilot.sub")] + public void should_parse_subtitle_language_unknown(string fileName) { var result = LanguageParser.ParseSubtitleLanguage(fileName); - result.Should().Be(language); + result.Should().Be(Language.Unknown); + } + + [TestCase("Castle.2009.S01E14.French.HDTV.XviD-LOL")] + [TestCase("Extant.S01E01.VOSTFR.HDTV.x264-RiDERS")] + [TestCase("Shield,.The.1x13.Tueurs.De.Flics.FR.DVDRip.XviD")] + public void should_parse_language_french(string postTitle) + { + var result = Parser.Parser.ParseAlbumTitle(postTitle); + result.Language.Id.Should().Be(Language.French.Id); + } + + [TestCase("Castle.2009.S01E14.Spanish.HDTV.XviD-LOL")] + public void should_parse_language_spanish(string postTitle) + { + var result = Parser.Parser.ParseAlbumTitle(postTitle); + result.Language.Id.Should().Be(Language.Spanish.Id); + } + + [TestCase("Castle.2009.S01E14.German.HDTV.XviD-LOL")] + [TestCase("Burn.Notice.S04E15.Brotherly.Love.GERMAN.DUBBED.WS.WEBRiP.XviD.REPACK-TVP")] + [TestCase("Elementary - S02E16 - Kampfhaehne - mkv - by Videomann")] + public void should_parse_language_german(string postTitle) + { + var result = Parser.Parser.ParseAlbumTitle(postTitle); + result.Language.Id.Should().Be(Language.German.Id); + } + + [TestCase("Castle.2009.S01E14.Italian.HDTV.XviD-LOL")] + [TestCase("person.of.interest.1x19.ita.720p.bdmux.x264-novarip")] + public void should_parse_language_italian(string postTitle) + { + var result = Parser.Parser.ParseAlbumTitle(postTitle); + result.Language.Id.Should().Be(Language.Italian.Id); + } + + [TestCase("Castle.2009.S01E14.Danish.HDTV.XviD-LOL")] + public void should_parse_language_danish(string postTitle) + { + var result = Parser.Parser.ParseAlbumTitle(postTitle); + result.Language.Id.Should().Be(Language.Danish.Id); + } + + [TestCase("Castle.2009.S01E14.Dutch.HDTV.XviD-LOL")] + [TestCase("Constantine.2014.S01E01.WEBRiP.H264.AAC.5.1-NL.SUBS")] + [TestCase("Ray Donovan - S01E01.720p.HDtv.x264-Evolve (NLsub)")] + public void should_parse_language_dutch(string postTitle) + { + var result = Parser.Parser.ParseAlbumTitle(postTitle); + result.Language.Id.Should().Be(Language.Dutch.Id); + } + + [TestCase("Castle.2009.S01E14.Japanese.HDTV.XviD-LOL")] + public void should_parse_language_japanese(string postTitle) + { + var result = Parser.Parser.ParseAlbumTitle(postTitle); + result.Language.Id.Should().Be(Language.Japanese.Id); + } + + [TestCase("Castle.2009.S01E14.Cantonese.HDTV.XviD-LOL")] + public void should_parse_language_cantonese(string postTitle) + { + var result = Parser.Parser.ParseAlbumTitle(postTitle); + result.Language.Id.Should().Be(Language.Cantonese.Id); + } + + [TestCase("Castle.2009.S01E14.Mandarin.HDTV.XviD-LOL")] + public void should_parse_language_mandarin(string postTitle) + { + var result = Parser.Parser.ParseAlbumTitle(postTitle); + result.Language.Id.Should().Be(Language.Mandarin.Id); + } + + [TestCase("Castle.2009.S01E14.Korean.HDTV.XviD-LOL")] + public void should_parse_language_korean(string postTitle) + { + var result = Parser.Parser.ParseAlbumTitle(postTitle); + result.Language.Id.Should().Be(Language.Korean.Id); + } + + [TestCase("Castle.2009.S01E14.Russian.HDTV.XviD-LOL")] + [TestCase("True.Detective.S01E01.1080p.WEB-DL.Rus.Eng.TVKlondike")] + public void should_parse_language_russian(string postTitle) + { + var result = Parser.Parser.ParseAlbumTitle(postTitle); + result.Language.Id.Should().Be(Language.Russian.Id); + } + + [TestCase("Castle.2009.S01E14.Polish.HDTV.XviD-LOL")] + public void should_parse_language_polish(string postTitle) + { + var result = Parser.Parser.ParseAlbumTitle(postTitle); + result.Language.Id.Should().Be(Language.Polish.Id); + } + + [TestCase("Castle.2009.S01E14.Vietnamese.HDTV.XviD-LOL")] + public void should_parse_language_vietnamese(string postTitle) + { + var result = Parser.Parser.ParseAlbumTitle(postTitle); + result.Language.Id.Should().Be(Language.Vietnamese.Id); + } + + [TestCase("Castle.2009.S01E14.Swedish.HDTV.XviD-LOL")] + public void should_parse_language_swedish(string postTitle) + { + var result = Parser.Parser.ParseAlbumTitle(postTitle); + result.Language.Id.Should().Be(Language.Swedish.Id); + } + + [TestCase("Castle.2009.S01E14.Norwegian.HDTV.XviD-LOL")] + [TestCase("Revolution S01E03 No Quarter 2012 WEB-DL 720p Nordic-philipo mkv")] + public void should_parse_language_norwegian(string postTitle) + { + var result = Parser.Parser.ParseAlbumTitle(postTitle); + result.Language.Id.Should().Be(Language.Norwegian.Id); + } + + [TestCase("Castle.2009.S01E14.Finnish.HDTV.XviD-LOL")] + public void should_parse_language_finnish(string postTitle) + { + var result = Parser.Parser.ParseAlbumTitle(postTitle); + result.Language.Id.Should().Be(Language.Finnish.Id); + } + + [TestCase("Castle.2009.S01E14.Turkish.HDTV.XviD-LOL")] + public void should_parse_language_turkish(string postTitle) + { + var result = Parser.Parser.ParseAlbumTitle(postTitle); + result.Language.Id.Should().Be(Language.Turkish.Id); + } + + [TestCase("Castle.2009.S01E14.Portuguese.HDTV.XviD-LOL")] + public void should_parse_language_portuguese(string postTitle) + { + var result = Parser.Parser.ParseAlbumTitle(postTitle); + result.Language.Id.Should().Be(Language.Portuguese.Id); + } + + [TestCase("Salamander.S01E01.FLEMISH.HDTV.x264-BRiGAND")] + public void should_parse_language_flemish(string postTitle) + { + var result = Parser.Parser.ParseAlbumTitle(postTitle); + result.Language.Id.Should().Be(Language.Flemish.Id); + } + + [TestCase("H.Polukatoikia.S03E13.Greek.PDTV.XviD-Ouzo")] + public void should_parse_language_greek(string postTitle) + { + var result = Parser.Parser.ParseAlbumTitle(postTitle); + result.Language.Id.Should().Be(Language.Greek.Id); + } + + [TestCase("Castle.2009.S01E14.HDTV.XviD.HUNDUB-LOL")] + [TestCase("Castle.2009.S01E14.HDTV.XviD.ENG.HUN-LOL")] + [TestCase("Castle.2009.S01E14.HDTV.XviD.HUN-LOL")] + public void should_parse_language_hungarian(string postTitle) + { + var result = Parser.Parser.ParseAlbumTitle(postTitle); + result.Language.Id.Should().Be(Language.Hungarian.Id); + } + + [TestCase("Avatar.The.Last.Airbender.S01-03.DVDRip.HebDub")] + public void should_parse_language_hebrew(string postTitle) + { + var result = Parser.Parser.ParseAlbumTitle(postTitle); + result.Language.Id.Should().Be(Language.Hebrew.Id); + } + + + [TestCase("Prison.Break.S05E01.WEBRip.x264.AC3.LT.EN-CNN")] + public void should_parse_language_lithuanian(string postTitle) + { + var result = Parser.Parser.ParseAlbumTitle(postTitle); + result.Language.Id.Should().Be(Language.Lithuanian.Id); + } + + + [TestCase("The.​Walking.​Dead.​S07E11.​WEB Rip.​XviD.​Louige-​CZ.​EN.​5.​1")] + public void should_parse_language_czech(string postTitle) + { + var result = Parser.Parser.ParseAlbumTitle(postTitle); + result.Language.Id.Should().Be(Language.Czech.Id); } } } diff --git a/src/NzbDrone.Core.Test/ParserTests/MiniSeriesEpisodeParserFixture.cs b/src/NzbDrone.Core.Test/ParserTests/MiniSeriesEpisodeParserFixture.cs deleted file mode 100644 index 667a3a74c..000000000 --- a/src/NzbDrone.Core.Test/ParserTests/MiniSeriesEpisodeParserFixture.cs +++ /dev/null @@ -1,30 +0,0 @@ -using System.Linq; -using FluentAssertions; -using NUnit.Framework; -using NzbDrone.Core.Test.Framework; - -namespace NzbDrone.Core.Test.ParserTests -{ - - [TestFixture] - public class MiniSeriesEpisodeParserFixture : CoreTest - { - [TestCase("The.Kennedys.Part.2.DSR.XviD-SYS", "The Kennedys", 2)] - [TestCase("the-pacific-e07-720p", "the-pacific", 7)] - [TestCase("Hatfields and McCoys 2012 Part 1 REPACK 720p HDTV x264 2HD", "Hatfields and McCoys 2012", 1)] - //[TestCase("Band.Of.Brothers.EP02.Day.Of.Days.DVDRiP.XviD-DEiTY", "Band.Of.Brothers", 2)] - //[TestCase("", "", 0, 0)] - [TestCase("Mars.2016.E04.Power.720p.WEB-DL.DD5.1.H.264-MARS", "Mars 2016", 4)] - public void should_parse_mini_series_episode(string postTitle, string title, int episodeNumber) - { - var result = Parser.Parser.ParseTitle(postTitle); - result.Should().NotBeNull(); - result.EpisodeNumbers.Should().HaveCount(1); - result.SeasonNumber.Should().Be(1); - result.EpisodeNumbers.First().Should().Be(episodeNumber); - result.SeriesTitle.Should().Be(title); - result.AbsoluteEpisodeNumbers.Should().BeEmpty(); - result.FullSeason.Should().BeFalse(); - } - } -} diff --git a/src/NzbDrone.Core.Test/ParserTests/MultiEpisodeParserFixture.cs b/src/NzbDrone.Core.Test/ParserTests/MultiEpisodeParserFixture.cs deleted file mode 100644 index 9d694c665..000000000 --- a/src/NzbDrone.Core.Test/ParserTests/MultiEpisodeParserFixture.cs +++ /dev/null @@ -1,71 +0,0 @@ -using FluentAssertions; -using NUnit.Framework; -using NzbDrone.Core.Test.Framework; - -namespace NzbDrone.Core.Test.ParserTests -{ - - [TestFixture] - public class MultiEpisodeParserFixture : CoreTest - { - [TestCase("WEEDS.S03E01-06.DUAL.BDRip.XviD.AC3.-HELLYWOOD", "WEEDS", 3, new[] { 1, 2, 3, 4, 5, 6 })] - [TestCase("Two.and.a.Half.Men.103.104.720p.HDTV.X264-DIMENSION", "Two and a Half Men", 1, new[] { 3, 4 })] - [TestCase("Weeds.S03E01.S03E02.720p.HDTV.X264-DIMENSION", "Weeds", 3, new[] { 1, 2 })] - [TestCase("The Borgias S01e01 e02 ShoHD On Demand 1080i DD5 1 ALANiS", "The Borgias", 1, new[] { 1, 2 })] - [TestCase("White.Collar.2x04.2x05.720p.BluRay-FUTV", "White Collar", 2, new[] { 4, 5 })] - [TestCase("Desperate.Housewives.S07E22E23.720p.HDTV.X264-DIMENSION", "Desperate Housewives", 7, new[] { 22, 23 })] - [TestCase("Desparate Housewives - S07E22 - S07E23 - And Lots of Security.. [HDTV-720p].mkv", "Desparate Housewives", 7, new[] { 22, 23 })] - [TestCase("S03E01.S03E02.720p.HDTV.X264-DIMENSION", "", 3, new[] { 1, 2 })] - [TestCase("Desparate Housewives - S07E22 - 7x23 - And Lots of Security.. [HDTV-720p].mkv", "Desparate Housewives", 7, new[] { 22, 23 })] - [TestCase("S07E22 - 7x23 - And Lots of Security.. [HDTV-720p].mkv", "", 7, new[] { 22, 23 })] - [TestCase("2x04x05.720p.BluRay-FUTV", "", 2, new[] { 4, 5 })] - [TestCase("S02E04E05.720p.BluRay-FUTV", "", 2, new[] { 4, 5 })] - [TestCase("S02E03-04-05.720p.BluRay-FUTV", "", 2, new[] { 3, 4, 5 })] - [TestCase("Breakout.Kings.S02E09-E10.HDTV.x264-ASAP", "Breakout Kings", 2, new[] { 9, 10 })] - [TestCase("Breakout Kings - 2x9-2x10 - Served Cold [SDTV] ", "Breakout Kings", 2, new[] { 9, 10 })] - [TestCase("Breakout Kings - 2x09-2x10 - Served Cold [SDTV] ", "Breakout Kings", 2, new[] { 9, 10 })] - [TestCase("Hell on Wheels S02E09 E10 HDTV x264 EVOLVE", "Hell on Wheels", 2, new[] { 9, 10 })] - [TestCase("Hell.on.Wheels.S02E09-E10.720p.HDTV.x264-EVOLVE", "Hell on Wheels", 2, new[] { 9, 10 })] - [TestCase("Grey's Anatomy - 8x01_02 - Free Falling", "Grey's Anatomy", 8, new [] { 1,2 })] - [TestCase("8x01_02 - Free Falling", "", 8, new[] { 1, 2 })] - [TestCase("Kaamelott.S01E91-E100", "Kaamelott", 1, new[] { 91, 92, 93, 94, 95, 96, 97, 98, 99, 100 })] - [TestCase("Neighbours.S29E161-E165.PDTV.x264-FQM", "Neighbours", 29, new[] { 161, 162, 163, 164, 165 })] - [TestCase("Shortland.Street.S22E5363-E5366.HDTV.x264-FiHTV", "Shortland Street", 22, new[] { 5363, 5364, 5365, 5366 })] - [TestCase("the.office.101.102.hdtv-lol", "the office", 1, new[] { 1, 2 })] - [TestCase("extant.10708.hdtv-lol.mp4", "extant", 1, new[] { 7, 8 })] - [TestCase("extant.10910.hdtv-lol.mp4", "extant", 1, new[] { 9, 10 })] - [TestCase("E.010910.HDTVx264REPACKLOL.mp4", "E", 1, new[] { 9, 10 })] - [TestCase("World Series of Poker - 2013x15 - 2013x16 - HD TV.mkv", "World Series of Poker", 2013, new[] { 15, 16 })] - [TestCase("The Librarians US S01E01-E02 720p HDTV x264", "The Librarians US", 1, new [] { 1, 2 })] - [TestCase("Series Title Season 01 Episode 05-06 720p", "Series Title", 1,new [] { 5, 6 })] - //[TestCase("My Name Is Earl - S03E01-E02 - My Name Is Inmate 28301-016 [SDTV]", "My Name Is Earl", 3, new[] { 1, 2 })] - //[TestCase("Adventure Time - 5x01 - x02 - Finn the Human (2) & Jake the Dog (3)", "Adventure Time", 5, new [] { 1, 2 })] - [TestCase("The Young And The Restless - S42 Ep10718 - Ep10722", "The Young And The Restless", 42, new[] { 10718, 10719, 10720, 10721, 10722 })] - [TestCase("The Young And The Restless - S42 Ep10688 - Ep10692", "The Young And The Restless", 42, new[] { 10688, 10689, 10690, 10691, 10692 })] - [TestCase("RWBY.S01E02E03.1080p.BluRay.x264-DeBTViD", "RWBY", 1, new [] { 2, 3 })] - [TestCase("grp-zoos01e11e12-1080p", "grp-zoo", 1, new [] { 11, 12 })] - [TestCase("grp-zoo-s01e11e12-1080p", "grp-zoo", 1, new [] { 11, 12 })] - [TestCase("Series Title.S6.E1.E2.Episode Name.1080p.WEB-DL", "Series Title", 6, new [] { 1, 2 })] - [TestCase("Series Title.S6E1-E2.Episode Name.1080p.WEB-DL", "Series Title", 6, new [] { 1, 2 })] - [TestCase("Series Title.S6E1-S6E2.Episode Name.1080p.WEB-DL", "Series Title", 6, new [] { 1, 2 })] - [TestCase("Series Title.S6E1E2.Episode Name.1080p.WEB-DL", "Series Title", 6, new [] { 1, 2 })] - [TestCase("Series Title.S6E1-E2-E3.Episode Name.1080p.WEB-DL", "Series Title", 6, new [] { 1, 2, 3})] - [TestCase("Series Title.S6.E1E3.Episode Name.1080p.WEB-DL", "Series Title", 6, new [] { 1, 2, 3 })] - [TestCase("Series Title.S6.E1-E2.Episode Name.1080p.WEB-DL", "Series Title", 6, new[] { 1, 2 })] - [TestCase("Series Title.S6.E1-S6E2.Episode Name.1080p.WEB-DL", "Series Title", 6, new[] { 1, 2 })] - [TestCase("Series Title.S6.E1E2.Episode Name.1080p.WEB-DL", "Series Title", 6, new[] { 1, 2 })] - [TestCase("Series Title.S6.E1-E2-E3.Episode Name.1080p.WEB-DL", "Series Title", 6, new[] { 1, 2, 3 })] - [TestCase("Mad.Men.S05E01-E02.720p.5.1Ch.BluRay", "Mad Men", 5, new[] { 1, 2 })] - [TestCase("Mad.Men.S05E01-02.720p.5.1Ch.BluRay", "Mad Men", 5, new[] { 1, 2 })] - //[TestCase("", "", , new [] { })] - public void should_parse_multiple_episodes(string postTitle, string title, int season, int[] episodes) - { - var result = Parser.Parser.ParseTitle(postTitle); - result.SeasonNumber.Should().Be(season); - result.EpisodeNumbers.Should().BeEquivalentTo(episodes); - result.SeriesTitle.Should().Be(title); - result.AbsoluteEpisodeNumbers.Should().BeEmpty(); - result.FullSeason.Should().BeFalse(); - } - } -} diff --git a/src/NzbDrone.Core.Test/ParserTests/ParserFixture.cs b/src/NzbDrone.Core.Test/ParserTests/ParserFixture.cs index d794172aa..f51dff304 100644 --- a/src/NzbDrone.Core.Test/ParserTests/ParserFixture.cs +++ b/src/NzbDrone.Core.Test/ParserTests/ParserFixture.cs @@ -1,6 +1,7 @@ -using FluentAssertions; +using FluentAssertions; using NUnit.Framework; using NzbDrone.Core.Parser; +using NzbDrone.Core.Qualities; using NzbDrone.Core.Test.Framework; namespace NzbDrone.Core.Test.ParserTests @@ -39,7 +40,7 @@ namespace NzbDrone.Core.Test.ParserTests [TestCase("Reno.911.S01.DVDRip.DD2.0.x264-DEEP", "Reno 911")] public void should_parse_series_name(string postTitle, string title) { - var result = Parser.Parser.ParseSeriesName(postTitle).CleanSeriesTitle(); + var result = Parser.Parser.ParseArtistName(postTitle).CleanSeriesTitle(); result.Should().Be(title.CleanSeriesTitle()); } @@ -54,13 +55,21 @@ namespace NzbDrone.Core.Test.ParserTests [TestCase("Discovery TV - Gold Rush : 02 Road From Hell [S04].mp4")] public void should_clean_up_invalid_path_characters(string postTitle) { - Parser.Parser.ParseTitle(postTitle); + Parser.Parser.ParseAlbumTitle(postTitle); } [TestCase("[scnzbefnet][509103] 2.Broke.Girls.S03E18.720p.HDTV.X264-DIMENSION", "2 Broke Girls")] public void should_remove_request_info_from_title(string postTitle, string title) { - Parser.Parser.ParseTitle(postTitle).SeriesTitle.Should().Be(title); + Parser.Parser.ParseAlbumTitle(postTitle).ArtistName.Should().Be(title); + } + + [TestCase("Revolution.S01E02.Chained.Heat.mkv")] + [TestCase("Dexter - S01E01 - Title.avi")] + public void should_parse_quality_from_extension(string title) + { + Parser.Parser.ParseAlbumTitle(title).Quality.Quality.Should().NotBe(Quality.Unknown); + Parser.Parser.ParseAlbumTitle(title).Quality.QualitySource.Should().Be(QualitySource.Extension); } [TestCase("VA - The Best 101 Love Ballads (2017) MP3 [192 kbps]", "The Best 101 Love Ballads")] diff --git a/src/NzbDrone.Core.Test/ParserTests/ParsingServiceTests/GetArtistFixture.cs b/src/NzbDrone.Core.Test/ParserTests/ParsingServiceTests/GetArtistFixture.cs new file mode 100644 index 000000000..a24043507 --- /dev/null +++ b/src/NzbDrone.Core.Test/ParserTests/ParsingServiceTests/GetArtistFixture.cs @@ -0,0 +1,34 @@ +using Moq; +using NUnit.Framework; +using NzbDrone.Core.Parser; +using NzbDrone.Core.Test.Framework; +using NzbDrone.Core.Music; + +namespace NzbDrone.Core.Test.ParserTests.ParsingServiceTests +{ + [TestFixture] + public class GetArtistFixture : CoreTest + { + [Test] + public void should_use_passed_in_title_when_it_cannot_be_parsed() + { + const string title = "30 Rock"; + + Subject.GetArtist(title); + + Mocker.GetMock() + .Verify(s => s.FindByName(title), Times.Once()); + } + + [Test] + public void should_use_parsed_artist_title() + { + const string title = "30 Rock - Get Some [FLAC]"; + + Subject.GetArtist(title); + + Mocker.GetMock() + .Verify(s => s.FindByName(Parser.Parser.ParseAlbumTitle(title).ArtistName), Times.Once()); + } + } +} diff --git a/src/NzbDrone.Core.Test/ParserTests/ParsingServiceTests/GetSeriesFixture.cs b/src/NzbDrone.Core.Test/ParserTests/ParsingServiceTests/GetSeriesFixture.cs deleted file mode 100644 index bf4b399b5..000000000 --- a/src/NzbDrone.Core.Test/ParserTests/ParsingServiceTests/GetSeriesFixture.cs +++ /dev/null @@ -1,47 +0,0 @@ -using Moq; -using NUnit.Framework; -using NzbDrone.Core.Parser; -using NzbDrone.Core.Test.Framework; -using NzbDrone.Core.Tv; - -namespace NzbDrone.Core.Test.ParserTests.ParsingServiceTests -{ - [TestFixture] - public class GetSeriesFixture : CoreTest - { - [Test] - public void should_use_passed_in_title_when_it_cannot_be_parsed() - { - const string title = "30 Rock"; - - Subject.GetSeries(title); - - Mocker.GetMock() - .Verify(s => s.FindByTitle(title), Times.Once()); - } - - [Test] - public void should_use_parsed_series_title() - { - const string title = "30.Rock.S01E01.720p.hdtv"; - - Subject.GetSeries(title); - - Mocker.GetMock() - .Verify(s => s.FindByTitle(Parser.Parser.ParseTitle(title).SeriesTitle), Times.Once()); - } - - [Test] - public void should_fallback_to_title_without_year_and_year_when_title_lookup_fails() - { - const string title = "House.2004.S01E01.720p.hdtv"; - var parsedEpisodeInfo = Parser.Parser.ParseTitle(title); - - Subject.GetSeries(title); - - Mocker.GetMock() - .Verify(s => s.FindByTitle(parsedEpisodeInfo.SeriesTitleInfo.TitleWithoutYear, - parsedEpisodeInfo.SeriesTitleInfo.Year), Times.Once()); - } - } -} diff --git a/src/NzbDrone.Core.Test/ParserTests/PathParserFixture.cs b/src/NzbDrone.Core.Test/ParserTests/PathParserFixture.cs index 9dfdeb851..75aa18d49 100644 --- a/src/NzbDrone.Core.Test/ParserTests/PathParserFixture.cs +++ b/src/NzbDrone.Core.Test/ParserTests/PathParserFixture.cs @@ -33,12 +33,12 @@ namespace NzbDrone.Core.Test.ParserTests // [TestCase(@"C:\CSI.NY.S02E04.720p.WEB-DL.DD5.1.H.264\73696S02-04.mkv", 2, 4)] //Gets treated as S01E04 (because it gets parsed as anime) public void should_parse_from_path(string path, int season, int episode) { - var result = Parser.Parser.ParsePath(path.AsOsAgnostic()); - result.EpisodeNumbers.Should().HaveCount(1); - result.SeasonNumber.Should().Be(season); - result.EpisodeNumbers[0].Should().Be(episode); - result.AbsoluteEpisodeNumbers.Should().BeEmpty(); - result.FullSeason.Should().BeFalse(); + var result = Parser.Parser.ParseMusicPath(path.AsOsAgnostic()); + //result.EpisodeNumbers.Should().HaveCount(1); + //result.SeasonNumber.Should().Be(season); + //result.EpisodeNumbers[0].Should().Be(episode); + //result.AbsoluteEpisodeNumbers.Should().BeEmpty(); + //result.FullSeason.Should().BeFalse(); ExceptionVerification.IgnoreWarns(); } diff --git a/src/NzbDrone.Core.Test/ParserTests/ReleaseGroupParserFixture.cs b/src/NzbDrone.Core.Test/ParserTests/ReleaseGroupParserFixture.cs index 18fd75856..34a18b469 100644 --- a/src/NzbDrone.Core.Test/ParserTests/ReleaseGroupParserFixture.cs +++ b/src/NzbDrone.Core.Test/ParserTests/ReleaseGroupParserFixture.cs @@ -37,7 +37,7 @@ namespace NzbDrone.Core.Test.ParserTests { const string path = @"C:\Test\Doctor.Who.2005.s01e01.internal.bdrip.x264-archivist.mkv"; - Parser.Parser.ParsePath(path).ReleaseGroup.Should().Be("archivist"); + Parser.Parser.ParseMusicPath(path).ReleaseGroup.Should().Be("archivist"); } [TestCase("Marvels.Daredevil.S02E04.720p.WEBRip.x264-SKGTV English", "SKGTV")] @@ -57,6 +57,7 @@ namespace NzbDrone.Core.Test.ParserTests [TestCase("Lost.S04E04.720p.BluRay.x264-xHD-1", "xHD")] [TestCase("Blue.Bloods.S05E11.720p.HDTV.X264-DIMENSION-1", "DIMENSION")] [TestCase("saturday.night.live.s40e11.kevin.hart_sia.720p.hdtv.x264-w4f-sample.mkv", "w4f")] + [TestCase("The.Sequel.2017.S05E02.1080p.WEB-DL.DD5.1.H264-EVL-Scrambled", "EVL")] public void should_not_include_repost_in_release_group(string title, string expected) { Parser.Parser.ParseReleaseGroup(title).Should().Be(expected); diff --git a/src/NzbDrone.Core.Test/ParserTests/SeasonParserFixture.cs b/src/NzbDrone.Core.Test/ParserTests/SeasonParserFixture.cs index 7a4ed0b9f..ff42c8e4a 100644 --- a/src/NzbDrone.Core.Test/ParserTests/SeasonParserFixture.cs +++ b/src/NzbDrone.Core.Test/ParserTests/SeasonParserFixture.cs @@ -26,12 +26,12 @@ namespace NzbDrone.Core.Test.ParserTests [TestCase("My.Series.S2014.720p.HDTV.x264-ME", "My Series", 2014)] public void should_parse_full_season_release(string postTitle, string title, int season) { - var result = Parser.Parser.ParseTitle(postTitle); - result.SeasonNumber.Should().Be(season); - result.SeriesTitle.Should().Be(title); - result.EpisodeNumbers.Should().BeEmpty(); - result.AbsoluteEpisodeNumbers.Should().BeEmpty(); - result.FullSeason.Should().BeTrue(); + var result = Parser.Parser.ParseAlbumTitle(postTitle); + //result.SeasonNumber.Should().Be(season); + //result.SeriesTitle.Should().Be(title); + //result.EpisodeNumbers.Should().BeEmpty(); + //result.AbsoluteEpisodeNumbers.Should().BeEmpty(); + //result.FullSeason.Should().BeTrue(); } [TestCase("Acropolis Now S05 EXTRAS DVDRip XviD RUNNER")] @@ -39,7 +39,7 @@ namespace NzbDrone.Core.Test.ParserTests [TestCase("Instant Star S03 EXTRAS DVDRip XviD OSiTV")] public void should_parse_season_extras(string postTitle) { - var result = Parser.Parser.ParseTitle(postTitle); + var result = Parser.Parser.ParseAlbumTitle(postTitle); result.Should().BeNull(); } @@ -49,7 +49,7 @@ namespace NzbDrone.Core.Test.ParserTests [TestCase("CSI.S11.SUBPACK.DVDRip.XviD-REWARD")] public void should_parse_season_subpack(string postTitle) { - var result = Parser.Parser.ParseTitle(postTitle); + var result = Parser.Parser.ParseAlbumTitle(postTitle); result.Should().BeNull(); } diff --git a/src/NzbDrone.Core.Test/ParserTests/SeriesTitleInfoFixture.cs b/src/NzbDrone.Core.Test/ParserTests/SeriesTitleInfoFixture.cs deleted file mode 100644 index 0809aae05..000000000 --- a/src/NzbDrone.Core.Test/ParserTests/SeriesTitleInfoFixture.cs +++ /dev/null @@ -1,60 +0,0 @@ -using FluentAssertions; -using NUnit.Framework; -using NzbDrone.Core.Test.Framework; - -namespace NzbDrone.Core.Test.ParserTests -{ - [TestFixture] - public class SeriesTitleInfoFixture : CoreTest - { - [Test] - public void should_have_year_zero_when_title_doesnt_have_a_year() - { - const string title = "House.S01E01.pilot.720p.hdtv"; - - var result = Parser.Parser.ParseTitle(title).SeriesTitleInfo; - - result.Year.Should().Be(0); - } - - [Test] - public void should_have_same_title_for_title_and_title_without_year_when_title_doesnt_have_a_year() - { - const string title = "House.S01E01.pilot.720p.hdtv"; - - var result = Parser.Parser.ParseTitle(title).SeriesTitleInfo; - - result.Title.Should().Be(result.TitleWithoutYear); - } - - [Test] - public void should_have_year_when_title_has_a_year() - { - const string title = "House.2004.S01E01.pilot.720p.hdtv"; - - var result = Parser.Parser.ParseTitle(title).SeriesTitleInfo; - - result.Year.Should().Be(2004); - } - - [Test] - public void should_have_year_in_title_when_title_has_a_year() - { - const string title = "House.2004.S01E01.pilot.720p.hdtv"; - - var result = Parser.Parser.ParseTitle(title).SeriesTitleInfo; - - result.Title.Should().Be("House 2004"); - } - - [Test] - public void should_title_without_year_should_not_contain_year() - { - const string title = "House.2004.S01E01.pilot.720p.hdtv"; - - var result = Parser.Parser.ParseTitle(title).SeriesTitleInfo; - - result.TitleWithoutYear.Should().Be("House"); - } - } -} diff --git a/src/NzbDrone.Core.Test/ParserTests/SingleEpisodeParserFixture.cs b/src/NzbDrone.Core.Test/ParserTests/SingleEpisodeParserFixture.cs index 05cdaa6eb..ce3312b17 100644 --- a/src/NzbDrone.Core.Test/ParserTests/SingleEpisodeParserFixture.cs +++ b/src/NzbDrone.Core.Test/ParserTests/SingleEpisodeParserFixture.cs @@ -127,17 +127,18 @@ namespace NzbDrone.Core.Test.ParserTests [TestCase("this.is.not.happening.2015.0308-yestv", "this is not happening 2015", 3, 8)] [TestCase("Jeopardy - S2016E231", "Jeopardy", 2016, 231)] [TestCase("Jeopardy - 2016x231", "Jeopardy", 2016, 231)] + [TestCase("Shortland.Street.S26E022.HDTV.x264-FiHTV", "Shortland Street", 26, 22)] //[TestCase("", "", 0, 0)] public void should_parse_single_episode(string postTitle, string title, int seasonNumber, int episodeNumber) { - var result = Parser.Parser.ParseTitle(postTitle); + var result = Parser.Parser.ParseAlbumTitle(postTitle); result.Should().NotBeNull(); - result.EpisodeNumbers.Should().HaveCount(1); - result.SeasonNumber.Should().Be(seasonNumber); - result.EpisodeNumbers.First().Should().Be(episodeNumber); - result.SeriesTitle.Should().Be(title); - result.AbsoluteEpisodeNumbers.Should().BeEmpty(); - result.FullSeason.Should().BeFalse(); + //result.EpisodeNumbers.Should().HaveCount(1); + //result.SeasonNumber.Should().Be(seasonNumber); + //result.EpisodeNumbers.First().Should().Be(episodeNumber); + //result.SeriesTitle.Should().Be(title); + //result.AbsoluteEpisodeNumbers.Should().BeEmpty(); + //result.FullSeason.Should().BeFalse(); } } } diff --git a/src/NzbDrone.Core.Test/Profiles/ProfileRepositoryFixture.cs b/src/NzbDrone.Core.Test/Profiles/ProfileRepositoryFixture.cs index 97f5c7259..1c754c0c7 100644 --- a/src/NzbDrone.Core.Test/Profiles/ProfileRepositoryFixture.cs +++ b/src/NzbDrone.Core.Test/Profiles/ProfileRepositoryFixture.cs @@ -1,6 +1,6 @@ -using FluentAssertions; +using FluentAssertions; using NUnit.Framework; -using NzbDrone.Core.Profiles; +using NzbDrone.Core.Profiles.Qualities; using NzbDrone.Core.Qualities; using NzbDrone.Core.Test.Framework; @@ -29,4 +29,4 @@ namespace NzbDrone.Core.Test.Profiles } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Core.Test/Profiles/ProfileServiceFixture.cs b/src/NzbDrone.Core.Test/Profiles/ProfileServiceFixture.cs index 4f799fa7d..65e103ec0 100644 --- a/src/NzbDrone.Core.Test/Profiles/ProfileServiceFixture.cs +++ b/src/NzbDrone.Core.Test/Profiles/ProfileServiceFixture.cs @@ -3,9 +3,9 @@ using FizzWare.NBuilder; using Moq; using NUnit.Framework; using NzbDrone.Core.Lifecycle; -using NzbDrone.Core.Profiles; +using NzbDrone.Core.Profiles.Qualities; using NzbDrone.Core.Test.Framework; -using NzbDrone.Core.Tv; +using NzbDrone.Core.Music; namespace NzbDrone.Core.Test.Profiles { @@ -19,7 +19,7 @@ namespace NzbDrone.Core.Test.Profiles Subject.Handle(new ApplicationStartedEvent()); Mocker.GetMock() - .Verify(v => v.Insert(It.IsAny()), Times.Exactly(6)); + .Verify(v => v.Insert(It.IsAny()), Times.Exactly(3)); } [Test] @@ -39,15 +39,15 @@ namespace NzbDrone.Core.Test.Profiles [Test] - public void should_not_be_able_to_delete_profile_if_assigned_to_series() + public void should_not_be_able_to_delete_profile_if_assigned_to_artist() { - var seriesList = Builder.CreateListOfSize(3) + var artistList = Builder.CreateListOfSize(3) .Random(1) .With(c => c.ProfileId = 2) .Build().ToList(); - Mocker.GetMock().Setup(c => c.GetAllSeries()).Returns(seriesList); + Mocker.GetMock().Setup(c => c.GetAllArtists()).Returns(artistList); Assert.Throws(() => Subject.Delete(2)); @@ -57,19 +57,19 @@ namespace NzbDrone.Core.Test.Profiles [Test] - public void should_delete_profile_if_not_assigned_to_series() + public void should_delete_profile_if_not_assigned_to_artist() { - var seriesList = Builder.CreateListOfSize(3) + var artistList = Builder.CreateListOfSize(3) .All() .With(c => c.ProfileId = 2) .Build().ToList(); - Mocker.GetMock().Setup(c => c.GetAllSeries()).Returns(seriesList); + Mocker.GetMock().Setup(c => c.GetAllArtists()).Returns(artistList); Subject.Delete(1); Mocker.GetMock().Verify(c => c.Delete(1), Times.Once()); } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Core.Test/Properties/AssemblyInfo.cs b/src/NzbDrone.Core.Test/Properties/AssemblyInfo.cs index fca9cdaa2..606b5b963 100644 --- a/src/NzbDrone.Core.Test/Properties/AssemblyInfo.cs +++ b/src/NzbDrone.Core.Test/Properties/AssemblyInfo.cs @@ -1,4 +1,4 @@ -using System.Reflection; +using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; @@ -6,11 +6,11 @@ using System.Runtime.InteropServices; // set of attributes. Change these attribute values to modify the information // associated with an assembly. -[assembly: AssemblyTitle("NzbDrone.Core.Test")] +[assembly: AssemblyTitle("Lidarr.Core.Test")] [assembly: AssemblyDescription("")] [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("Microsoft")] -[assembly: AssemblyProduct("NzbDrone.Core.Test")] +[assembly: AssemblyProduct("Lidarr.Core.Test")] [assembly: AssemblyCopyright("Copyright © Microsoft 2010")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] @@ -25,6 +25,4 @@ using System.Runtime.InteropServices; [assembly: Guid("699aed1b-015e-4f0d-9c81-d5557b05d260")] -[assembly: AssemblyVersion("10.0.0.*")] - -[assembly: InternalsVisibleTo("NzbDrone.Core")] \ No newline at end of file +[assembly: InternalsVisibleTo("Lidarr.Core")] diff --git a/src/NzbDrone.Core.Test/ProviderTests/DiskScanProviderTests/GetAudioFilesFixture.cs b/src/NzbDrone.Core.Test/ProviderTests/DiskScanProviderTests/GetAudioFilesFixture.cs index 6a1aa73c1..4247b5b4d 100644 --- a/src/NzbDrone.Core.Test/ProviderTests/DiskScanProviderTests/GetAudioFilesFixture.cs +++ b/src/NzbDrone.Core.Test/ProviderTests/DiskScanProviderTests/GetAudioFilesFixture.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.IO; using System.Linq; using FizzWare.NBuilder; @@ -93,6 +93,7 @@ namespace NzbDrone.Core.Test.ProviderTests.DiskScanProviderTests [TestCase("Plex Versions")] [TestCase(".secret")] [TestCase(".hidden")] + [TestCase(".unwanted")] public void should_filter_certain_sub_folders(string subFolder) { var path = @"C:\Test\"; @@ -100,13 +101,9 @@ namespace NzbDrone.Core.Test.ProviderTests.DiskScanProviderTests var specialFiles = GetFiles(path, subFolder).ToList(); var allFiles = files.Concat(specialFiles); - var artist = Builder.CreateNew() - .With(s => s.Path = path) - .Build(); - - var filteredFiles = Subject.FilterFiles(artist, allFiles); + var filteredFiles = Subject.FilterFiles(path, allFiles); filteredFiles.Should().NotContain(specialFiles); filteredFiles.Count.Should().BeGreaterThan(0); } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Core.Test/Providers/XemProxyFixture.cs b/src/NzbDrone.Core.Test/Providers/XemProxyFixture.cs deleted file mode 100644 index a46ab935c..000000000 --- a/src/NzbDrone.Core.Test/Providers/XemProxyFixture.cs +++ /dev/null @@ -1,54 +0,0 @@ -using FluentAssertions; -using NUnit.Framework; -using NzbDrone.Core.DataAugmentation.Xem; -using NzbDrone.Core.Test.Framework; -using NzbDrone.Test.Common.Categories; - -namespace NzbDrone.Core.Test.Providers -{ - [TestFixture] - [IntegrationTest] - public class XemProxyFixture : CoreTest - { - [SetUp] - public void Setup() - { - UseRealHttp(); - } - - [Test] - public void get_series_ids() - { - var ids = Subject.GetXemSeriesIds(); - - ids.Should().NotBeEmpty(); - ids.Should().Contain(i => i == 73141); - } - - [TestCase(12345, Description = "invalid id")] - [TestCase(279042, Description = "no single connection")] - public void should_return_empty_when_known_error(int id) - { - Subject.GetSceneTvdbMappings(id).Should().BeEmpty(); - } - - [TestCase(82807)] - [TestCase(73141, Description = "American Dad!")] - public void should_get_mapping(int seriesId) - { - var result = Subject.GetSceneTvdbMappings(seriesId); - - result.Should().NotBeEmpty(); - result.Should().OnlyContain(c => c.Scene != null); - result.Should().OnlyContain(c => c.Tvdb != null); - } - - [TestCase(78916)] - public void should_filter_out_episodes_without_scene_mapping(int seriesId) - { - var result = Subject.GetSceneTvdbMappings(seriesId); - - result.Should().NotContain(c => c.Tvdb == null); - } - } -} \ No newline at end of file diff --git a/src/NzbDrone.Core.Test/Qualities/QualityFixture.cs b/src/NzbDrone.Core.Test/Qualities/QualityFixture.cs index 9b0e3e1c0..ce04b4c20 100644 --- a/src/NzbDrone.Core.Test/Qualities/QualityFixture.cs +++ b/src/NzbDrone.Core.Test/Qualities/QualityFixture.cs @@ -1,8 +1,8 @@ -using System.Linq; +using System.Linq; using System.Collections.Generic; using FluentAssertions; using NUnit.Framework; -using NzbDrone.Core.Profiles; +using NzbDrone.Core.Profiles.Qualities; using NzbDrone.Core.Qualities; using NzbDrone.Core.Test.Framework; diff --git a/src/NzbDrone.Core.Test/Qualities/QualityModelComparerFixture.cs b/src/NzbDrone.Core.Test/Qualities/QualityModelComparerFixture.cs index c42d93b7d..49a728375 100644 --- a/src/NzbDrone.Core.Test/Qualities/QualityModelComparerFixture.cs +++ b/src/NzbDrone.Core.Test/Qualities/QualityModelComparerFixture.cs @@ -1,6 +1,6 @@ -using FluentAssertions; +using FluentAssertions; using NUnit.Framework; -using NzbDrone.Core.Profiles; +using NzbDrone.Core.Profiles.Qualities; using NzbDrone.Core.Qualities; using NzbDrone.Core.Test.Framework; diff --git a/src/NzbDrone.Core.Test/QueueTests/QueueServiceFixture.cs b/src/NzbDrone.Core.Test/QueueTests/QueueServiceFixture.cs index 81ca1e28d..b2d8121ee 100644 --- a/src/NzbDrone.Core.Test/QueueTests/QueueServiceFixture.cs +++ b/src/NzbDrone.Core.Test/QueueTests/QueueServiceFixture.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using NUnit.Framework; @@ -7,7 +7,7 @@ using NzbDrone.Core.Queue; using NzbDrone.Core.Test.Framework; using FizzWare.NBuilder; using FluentAssertions; -using NzbDrone.Core.Tv; +using NzbDrone.Core.Music; using NzbDrone.Core.Parser.Model; namespace NzbDrone.Core.Test.QueueTests @@ -24,24 +24,24 @@ namespace NzbDrone.Core.Test.QueueTests .With(v => v.RemainingTime = TimeSpan.FromSeconds(10)) .Build(); - var series = Builder.CreateNew() + var series = Builder.CreateNew() .Build(); - var episodes = Builder.CreateListOfSize(3) + var episodes = Builder.CreateListOfSize(3) .All() - .With(e => e.SeriesId = series.Id) + .With(e => e.ArtistId = series.Id) .Build(); - var remoteEpisode = Builder.CreateNew() - .With(r => r.Series = series) - .With(r => r.Episodes = new List(episodes)) - .With(r => r.ParsedEpisodeInfo = new ParsedEpisodeInfo()) + var remoteEpisode = Builder.CreateNew() + .With(r => r.Artist = series) + .With(r => r.Albums = new List(episodes)) + .With(r => r.ParsedAlbumInfo = new ParsedAlbumInfo()) .Build(); _trackedDownloads = Builder.CreateListOfSize(1) .All() .With(v => v.DownloadItem = downloadItem) - .With(v => v.RemoteEpisode = remoteEpisode) + .With(v => v.RemoteAlbum = remoteEpisode) .Build() .ToList(); } diff --git a/src/NzbDrone.Core.Test/RootFolderTests/RootFolderServiceFixture.cs b/src/NzbDrone.Core.Test/RootFolderTests/RootFolderServiceFixture.cs index 9f932bca5..2cc2ff9ce 100644 --- a/src/NzbDrone.Core.Test/RootFolderTests/RootFolderServiceFixture.cs +++ b/src/NzbDrone.Core.Test/RootFolderTests/RootFolderServiceFixture.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.IO; using System.Linq; @@ -10,7 +10,7 @@ using NzbDrone.Common.Disk; using NzbDrone.Core.Configuration; using NzbDrone.Core.RootFolders; using NzbDrone.Core.Test.Framework; -using NzbDrone.Core.Tv; +using NzbDrone.Core.Music; using NzbDrone.Test.Common; namespace NzbDrone.Core.Test.RootFolderTests @@ -42,7 +42,7 @@ namespace NzbDrone.Core.Test.RootFolderTests .Returns(false); } - [TestCase("D:\\TV Shows\\")] + [TestCase("D:\\Music\\")] [TestCase("//server//folder")] public void should_be_able_to_add_root_dir(string path) { @@ -81,9 +81,9 @@ namespace NzbDrone.Core.Test.RootFolderTests [Test] public void adding_duplicated_root_folder_should_throw() { - Mocker.GetMock().Setup(c => c.All()).Returns(new List { new RootFolder { Path = "C:\\TV".AsOsAgnostic() } }); + Mocker.GetMock().Setup(c => c.All()).Returns(new List { new RootFolder { Path = "C:\\Music".AsOsAgnostic() } }); - Assert.Throws(() => Subject.Add(new RootFolder { Path = @"C:\TV".AsOsAgnostic() })); + Assert.Throws(() => Subject.Add(new RootFolder { Path = @"C:\Music".AsOsAgnostic() })); } [Test] @@ -93,21 +93,9 @@ namespace NzbDrone.Core.Test.RootFolderTests .Setup(m => m.FolderWritable(It.IsAny())) .Returns(false); - Assert.Throws(() => Subject.Add(new RootFolder { Path = @"C:\TV".AsOsAgnostic() })); + Assert.Throws(() => Subject.Add(new RootFolder { Path = @"C:\Music".AsOsAgnostic() })); } - [Test] - public void should_throw_when_same_path_as_drone_factory() - { - var path = @"C:\TV".AsOsAgnostic(); - - Mocker.GetMock() - .SetupGet(s => s.DownloadedAlbumsFolder) - .Returns(path); - - Assert.Throws(() => Subject.Add(new RootFolder { Path = path })); -} - [TestCase("$recycle.bin")] [TestCase("system volume information")] [TestCase("recycler")] @@ -119,16 +107,16 @@ namespace NzbDrone.Core.Test.RootFolderTests [TestCase(".grab")] public void should_get_root_folder_with_subfolders_excluding_special_sub_folders(string subFolder) { - var rootFolderPath = @"C:\Test\TV".AsOsAgnostic(); + var rootFolderPath = @"C:\Test\Music".AsOsAgnostic(); var rootFolder = Builder.CreateNew() .With(r => r.Path = rootFolderPath) .Build(); var subFolders = new[] { - "Series1", - "Series2", - "Series3", + "Artist1", + "Artist2", + "Artist3", subFolder }; @@ -138,9 +126,9 @@ namespace NzbDrone.Core.Test.RootFolderTests .Setup(s => s.Get(It.IsAny())) .Returns(rootFolder); - Mocker.GetMock() - .Setup(s => s.GetAllSeries()) - .Returns(new List()); + Mocker.GetMock() + .Setup(s => s.GetAllArtists()) + .Returns(new List()); Mocker.GetMock() .Setup(s => s.GetDirectories(rootFolder.Path)) @@ -152,4 +140,4 @@ namespace NzbDrone.Core.Test.RootFolderTests unmappedFolders.Should().NotContain(u => u.Name == subFolder); } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Core.Test/ThingiProvider/ProviderBaseFixture.cs b/src/NzbDrone.Core.Test/ThingiProviderTests/ProviderBaseFixture.cs similarity index 92% rename from src/NzbDrone.Core.Test/ThingiProvider/ProviderBaseFixture.cs rename to src/NzbDrone.Core.Test/ThingiProviderTests/ProviderBaseFixture.cs index db1e21c61..02bbdd8b2 100644 --- a/src/NzbDrone.Core.Test/ThingiProvider/ProviderBaseFixture.cs +++ b/src/NzbDrone.Core.Test/ThingiProviderTests/ProviderBaseFixture.cs @@ -1,13 +1,12 @@ -using FizzWare.NBuilder; +using FizzWare.NBuilder; using FluentAssertions; using NUnit.Framework; using NzbDrone.Core.Indexers; using NzbDrone.Core.Indexers.Newznab; using NzbDrone.Core.Test.Framework; -namespace NzbDrone.Core.Test.ThingiProvider +namespace NzbDrone.Core.Test.ThingiProviderTests { - public class ProviderRepositoryFixture : DbTest { [Test] @@ -27,4 +26,4 @@ namespace NzbDrone.Core.Test.ThingiProvider storedSetting.ShouldBeEquivalentTo(newznabSettings, o=>o.IncludingAllRuntimeProperties()); } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Core.Test/ThingiProviderTests/ProviderStatusServiceFixture.cs b/src/NzbDrone.Core.Test/ThingiProviderTests/ProviderStatusServiceFixture.cs new file mode 100644 index 000000000..ba63a2b6b --- /dev/null +++ b/src/NzbDrone.Core.Test/ThingiProviderTests/ProviderStatusServiceFixture.cs @@ -0,0 +1,126 @@ +using System; +using System.Linq; +using FluentAssertions; +using Moq; +using NLog; +using NUnit.Framework; +using NzbDrone.Core.Messaging.Events; +using NzbDrone.Core.Test.Framework; +using NzbDrone.Core.ThingiProvider; +using NzbDrone.Core.ThingiProvider.Status; + +namespace NzbDrone.Core.Test.ThingiProviderTests +{ + public class MockProviderStatus : ProviderStatusBase + { + } + + public interface IMockProvider : IProvider + { + } + + public interface IMockProviderStatusRepository : IProviderStatusRepository + { + } + + public class MockProviderStatusService : ProviderStatusServiceBase + { + public MockProviderStatusService(IMockProviderStatusRepository providerStatusRepository, IEventAggregator eventAggregator, Logger logger) + : base(providerStatusRepository, eventAggregator, logger) + { + + } + } + + public class ProviderStatusServiceFixture : CoreTest + { + private DateTime _epoch; + + [SetUp] + public void SetUp() + { + _epoch = DateTime.UtcNow; + } + + private void WithStatus(MockProviderStatus status) + { + Mocker.GetMock() + .Setup(v => v.FindByProviderId(1)) + .Returns(status); + + Mocker.GetMock() + .Setup(v => v.All()) + .Returns(new[] { status }); + } + + private void VerifyUpdate() + { + Mocker.GetMock() + .Verify(v => v.Upsert(It.IsAny()), Times.Once()); + } + + private void VerifyNoUpdate() + { + Mocker.GetMock() + .Verify(v => v.Upsert(It.IsAny()), Times.Never()); + } + + [Test] + public void should_start_backoff_on_first_failure() + { + WithStatus(new MockProviderStatus()); + + Subject.RecordFailure(1); + + VerifyUpdate(); + + var status = Subject.GetBlockedProviders().FirstOrDefault(); + status.Should().NotBeNull(); + status.DisabledTill.Should().HaveValue(); + status.DisabledTill.Value.Should().BeCloseTo(_epoch + TimeSpan.FromMinutes(5), 500); + } + + [Test] + public void should_cancel_backoff_on_success() + { + WithStatus(new MockProviderStatus { EscalationLevel = 2 }); + + Subject.RecordSuccess(1); + + VerifyUpdate(); + + var status = Subject.GetBlockedProviders().FirstOrDefault(); + status.Should().BeNull(); + } + + [Test] + public void should_not_store_update_if_already_okay() + { + WithStatus(new MockProviderStatus { EscalationLevel = 0 }); + + Subject.RecordSuccess(1); + + VerifyNoUpdate(); + } + + [Test] + public void should_preserve_escalation_on_intermittent_success() + { + WithStatus(new MockProviderStatus + { + InitialFailure = _epoch - TimeSpan.FromSeconds(20), + MostRecentFailure = _epoch - TimeSpan.FromSeconds(4), + EscalationLevel = 3 + }); + + Subject.RecordSuccess(1); + Subject.RecordSuccess(1); + Subject.RecordFailure(1); + + var status = Subject.GetBlockedProviders().FirstOrDefault(); + status.Should().NotBeNull(); + status.DisabledTill.Should().HaveValue(); + status.DisabledTill.Value.Should().BeCloseTo(_epoch + TimeSpan.FromMinutes(15), 500); + } + } +} diff --git a/src/NzbDrone.Core.Test/TvTests/EpisodeMonitoredServiceTests/SetEpisodeMontitoredFixture.cs b/src/NzbDrone.Core.Test/TvTests/EpisodeMonitoredServiceTests/SetEpisodeMontitoredFixture.cs deleted file mode 100644 index 058a09b86..000000000 --- a/src/NzbDrone.Core.Test/TvTests/EpisodeMonitoredServiceTests/SetEpisodeMontitoredFixture.cs +++ /dev/null @@ -1,221 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using FizzWare.NBuilder; -using Moq; -using NUnit.Framework; -using NzbDrone.Common.Extensions; -using NzbDrone.Core.Test.Framework; -using NzbDrone.Core.Tv; - -namespace NzbDrone.Core.Test.TvTests.EpisodeMonitoredServiceTests -{ - [TestFixture] - public class SetEpisodeMontitoredFixture : CoreTest - { - private Series _series; - private List _episodes; - - [SetUp] - public void Setup() - { - var seasons = 4; - - _series = Builder.CreateNew() - .With(s => s.Seasons = Builder.CreateListOfSize(seasons) - .All() - .With(n => n.Monitored = true) - .Build() - .ToList()) - .Build(); - - _episodes = Builder.CreateListOfSize(seasons) - .All() - .With(e => e.Monitored = true) - .With(e => e.AirDateUtc = DateTime.UtcNow.AddDays(-7)) - //Missing - .TheFirst(1) - .With(e => e.EpisodeFileId = 0) - //Has File - .TheNext(1) - .With(e => e.EpisodeFileId = 1) - //Future - .TheNext(1) - .With(e => e.EpisodeFileId = 0) - .With(e => e.AirDateUtc = DateTime.UtcNow.AddDays(7)) - //Future/TBA - .TheNext(1) - .With(e => e.EpisodeFileId = 0) - .With(e => e.AirDateUtc = null) - .Build() - .ToList(); - - Mocker.GetMock() - .Setup(s => s.GetEpisodeBySeries(It.IsAny())) - .Returns(_episodes); - } - - private void GivenSpecials() - { - foreach (var episode in _episodes) - { - episode.SeasonNumber = 0; - } - - _series.Seasons = new List{new Season { Monitored = false, SeasonNumber = 0 }}; - } - - [Test] - public void should_be_able_to_monitor_series_without_changing_episodes() - { - Subject.SetEpisodeMonitoredStatus(_series, null); - - Mocker.GetMock() - .Verify(v => v.UpdateSeries(It.IsAny()), Times.Once()); - - Mocker.GetMock() - .Verify(v => v.UpdateEpisodes(It.IsAny>()), Times.Never()); - } - - [Test] - public void should_be_able_to_monitor_all_episodes() - { - Subject.SetEpisodeMonitoredStatus(_series, new MonitoringOptions()); - - Mocker.GetMock() - .Verify(v => v.UpdateEpisodes(It.Is>(l => l.All(e => e.Monitored)))); - } - - [Test] - public void should_be_able_to_monitor_missing_episodes_only() - { - var monitoringOptions = new MonitoringOptions - { - IgnoreEpisodesWithFiles = true, - IgnoreEpisodesWithoutFiles = false - }; - - Subject.SetEpisodeMonitoredStatus(_series, monitoringOptions); - - VerifyMonitored(e => !e.HasFile); - VerifyNotMonitored(e => e.HasFile); - } - - [Test] - public void should_be_able_to_monitor_new_episodes_only() - { - var monitoringOptions = new MonitoringOptions - { - IgnoreEpisodesWithFiles = true, - IgnoreEpisodesWithoutFiles = true - }; - - Subject.SetEpisodeMonitoredStatus(_series, monitoringOptions); - - VerifyMonitored(e => e.AirDateUtc.HasValue && e.AirDateUtc.Value.After(DateTime.UtcNow)); - VerifyMonitored(e => !e.AirDateUtc.HasValue); - VerifyNotMonitored(e => e.AirDateUtc.HasValue && e.AirDateUtc.Value.Before(DateTime.UtcNow)); - } - - [Test] - public void should_not_monitor_missing_specials() - { - GivenSpecials(); - - var monitoringOptions = new MonitoringOptions - { - IgnoreEpisodesWithFiles = true, - IgnoreEpisodesWithoutFiles = false - }; - - Subject.SetEpisodeMonitoredStatus(_series, monitoringOptions); - - VerifyNotMonitored(e => e.SeasonNumber == 0); - } - - [Test] - public void should_not_monitor_new_specials() - { - GivenSpecials(); - - var monitoringOptions = new MonitoringOptions - { - IgnoreEpisodesWithFiles = true, - IgnoreEpisodesWithoutFiles = true - }; - - Subject.SetEpisodeMonitoredStatus(_series, monitoringOptions); - - VerifyNotMonitored(e => e.SeasonNumber == 0); - } - - [Test] - public void should_not_monitor_season_when_all_episodes_are_monitored_except_latest_season() - { - _series.Seasons = Builder.CreateListOfSize(2) - .All() - .With(n => n.Monitored = true) - .Build() - .ToList(); - - _episodes = Builder.CreateListOfSize(5) - .All() - .With(e => e.SeasonNumber = 1) - .With(e => e.EpisodeFileId = 0) - .With(e => e.AirDateUtc = DateTime.UtcNow.AddDays(-5)) - .TheLast(1) - .With(e => e.SeasonNumber = 2) - .Build() - .ToList(); - - Mocker.GetMock() - .Setup(s => s.GetEpisodeBySeries(It.IsAny())) - .Returns(_episodes); - - var monitoringOptions = new MonitoringOptions - { - IgnoreEpisodesWithoutFiles = true - }; - - Subject.SetEpisodeMonitoredStatus(_series, monitoringOptions); - - VerifySeasonMonitored(n => n.SeasonNumber == 2); - VerifySeasonNotMonitored(n => n.SeasonNumber == 1); - } - - [Test] - public void should_ignore_episodes_when_season_is_not_monitored() - { - _series.Seasons.ForEach(s => s.Monitored = false); - - Subject.SetEpisodeMonitoredStatus(_series, new MonitoringOptions()); - - Mocker.GetMock() - .Verify(v => v.UpdateEpisodes(It.Is>(l => l.All(e => !e.Monitored)))); - } - - private void VerifyMonitored(Func predicate) - { - Mocker.GetMock() - .Verify(v => v.UpdateEpisodes(It.Is>(l => l.Where(predicate).All(e => e.Monitored)))); - } - - private void VerifyNotMonitored(Func predicate) - { - Mocker.GetMock() - .Verify(v => v.UpdateEpisodes(It.Is>(l => l.Where(predicate).All(e => !e.Monitored)))); - } - - private void VerifySeasonMonitored(Func predicate) - { - Mocker.GetMock() - .Verify(v => v.UpdateSeries(It.Is(s => s.Seasons.Where(predicate).All(n => n.Monitored)))); - } - - private void VerifySeasonNotMonitored(Func predicate) - { - Mocker.GetMock() - .Verify(v => v.UpdateSeries(It.Is(s => s.Seasons.Where(predicate).All(n => !n.Monitored)))); - } - } -} diff --git a/src/NzbDrone.Core.Test/TvTests/EpisodeRepositoryTests/ByAirDateFixture.cs b/src/NzbDrone.Core.Test/TvTests/EpisodeRepositoryTests/ByAirDateFixture.cs deleted file mode 100644 index 2f6c0cef5..000000000 --- a/src/NzbDrone.Core.Test/TvTests/EpisodeRepositoryTests/ByAirDateFixture.cs +++ /dev/null @@ -1,71 +0,0 @@ -using System; -using FizzWare.NBuilder; -using FluentAssertions; -using NUnit.Framework; -using NzbDrone.Core.Test.Framework; -using NzbDrone.Core.Tv; - -namespace NzbDrone.Core.Test.TvTests.EpisodeRepositoryTests -{ - [TestFixture] - public class ByAirDateFixture : DbTest - { - private const int SERIES_ID = 1; - private const string AIR_DATE = "2014-04-02"; - - private void GivenEpisode(int seasonNumber) - { - var episode = Builder.CreateNew() - .With(e => e.SeriesId = 1) - .With(e => e.SeasonNumber = seasonNumber) - .With(e => e.AirDate = AIR_DATE) - .BuildNew(); - - Db.Insert(episode); - } - - [Test] - public void should_throw_when_multiple_regular_episodes_are_found() - { - GivenEpisode(1); - GivenEpisode(2); - - Assert.Throws(() => Subject.Get(SERIES_ID, AIR_DATE)); - Assert.Throws(() => Subject.Find(SERIES_ID, AIR_DATE)); - } - - [Test] - public void should_throw_when_get_finds_no_episode() - { - Assert.Throws(() => Subject.Get(SERIES_ID, AIR_DATE)); - } - - [Test] - public void should_get_episode_when_single_episode_exists_for_air_date() - { - GivenEpisode(1); - - Subject.Get(SERIES_ID, AIR_DATE).Should().NotBeNull(); - Subject.Find(SERIES_ID, AIR_DATE).Should().NotBeNull(); - } - - [Test] - public void should_get_episode_when_regular_episode_and_special_share_the_same_air_date() - { - GivenEpisode(1); - GivenEpisode(0); - - Subject.Get(SERIES_ID, AIR_DATE).Should().NotBeNull(); - Subject.Find(SERIES_ID, AIR_DATE).Should().NotBeNull(); - } - - [Test] - public void should_get_special_when_its_the_only_episode_for_the_date_provided() - { - GivenEpisode(0); - - Subject.Get(SERIES_ID, AIR_DATE).Should().NotBeNull(); - Subject.Find(SERIES_ID, AIR_DATE).Should().NotBeNull(); - } - } -} diff --git a/src/NzbDrone.Core.Test/TvTests/EpisodeRepositoryTests/EpisodesBetweenDatesFixture.cs b/src/NzbDrone.Core.Test/TvTests/EpisodeRepositoryTests/EpisodesBetweenDatesFixture.cs deleted file mode 100644 index 10cb1393f..000000000 --- a/src/NzbDrone.Core.Test/TvTests/EpisodeRepositoryTests/EpisodesBetweenDatesFixture.cs +++ /dev/null @@ -1,40 +0,0 @@ -using System; -using FizzWare.NBuilder; -using FluentAssertions; -using NUnit.Framework; -using NzbDrone.Core.Test.Framework; -using NzbDrone.Core.Tv; - -namespace NzbDrone.Core.Test.TvTests.EpisodeRepositoryTests -{ - [TestFixture] - public class EpisodesBetweenDatesFixture : DbTest - { - [SetUp] - public void Setup() - { - var series = Builder.CreateNew() - .With(s => s.Id = 0) - .With(s => s.Runtime = 30) - .With(s => s.Monitored = true) - .Build(); - - series.Id = Db.Insert(series).Id; - - var episode = Builder.CreateNew() - .With(e => e.Id = 0) - .With(e => e.SeriesId = series.Id) - .With(e => e.Monitored = true) - .Build(); - - Db.Insert(episode); - } - - [Test] - public void should_get_episodes() - { - var episodes = Subject.EpisodesBetweenDates(DateTime.Today.AddDays(-1), DateTime.Today.AddDays(3), false); - episodes.Should().HaveCount(1); - } - } -} diff --git a/src/NzbDrone.Core.Test/TvTests/EpisodeRepositoryTests/EpisodesRepositoryReadFixture.cs b/src/NzbDrone.Core.Test/TvTests/EpisodeRepositoryTests/EpisodesRepositoryReadFixture.cs deleted file mode 100644 index 07a43b9ca..000000000 --- a/src/NzbDrone.Core.Test/TvTests/EpisodeRepositoryTests/EpisodesRepositoryReadFixture.cs +++ /dev/null @@ -1,47 +0,0 @@ -using FizzWare.NBuilder; -using FluentAssertions; -using NUnit.Framework; -using NzbDrone.Core.MediaFiles; -using NzbDrone.Core.Qualities; -using NzbDrone.Core.Test.Framework; -using NzbDrone.Core.Tv; - -namespace NzbDrone.Core.Test.TvTests.EpisodeRepositoryTests -{ - [TestFixture] - public class EpisodesRepositoryReadFixture : DbTest - { - private Series series; - - [SetUp] - public void Setup() - { - series = Builder.CreateNew() - .With(s => s.Runtime = 30) - .BuildNew(); - - Db.Insert(series); - } - - [Test] - public void should_get_episodes_by_file() - { - var episodeFile = Builder.CreateNew() - .With(h => h.Quality = new QualityModel()) - .BuildNew(); - - Db.Insert(episodeFile); - - var episode = Builder.CreateListOfSize(2) - .All() - .With(e => e.SeriesId = series.Id) - .With(e => e.EpisodeFileId = episodeFile.Id) - .BuildListOfNew(); - - Db.InsertMany(episode); - - var episodes = Subject.GetEpisodeByFileId(episodeFile.Id); - episodes.Should().HaveCount(2); - } - } -} diff --git a/src/NzbDrone.Core.Test/TvTests/EpisodeRepositoryTests/EpisodesWhereCutoffUnmetFixture.cs b/src/NzbDrone.Core.Test/TvTests/EpisodeRepositoryTests/EpisodesWhereCutoffUnmetFixture.cs deleted file mode 100644 index 1759b5322..000000000 --- a/src/NzbDrone.Core.Test/TvTests/EpisodeRepositoryTests/EpisodesWhereCutoffUnmetFixture.cs +++ /dev/null @@ -1,183 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using FizzWare.NBuilder; -using FluentAssertions; -using NUnit.Framework; -using NzbDrone.Core.Datastore; -using NzbDrone.Core.Profiles; -using NzbDrone.Core.Test.Framework; -using NzbDrone.Core.Tv; -using NzbDrone.Core.Qualities; -using NzbDrone.Core.MediaFiles; - -namespace NzbDrone.Core.Test.TvTests.EpisodeRepositoryTests -{ - [TestFixture] - public class EpisodesWhereCutoffUnmetFixture : DbTest - { - private Series _monitoredSeries; - private Series _unmonitoredSeries; - private PagingSpec _pagingSpec; - private List _qualitiesBelowCutoff; - private List _unairedEpisodes; - - [SetUp] - public void Setup() - { - var profile = new Profile - { - Id = 1, - Cutoff = Quality.MP3_256, - Items = new List - { - new ProfileQualityItem { Allowed = true, Quality = Quality.MP3_192 }, - new ProfileQualityItem { Allowed = true, Quality = Quality.MP3_256 }, - new ProfileQualityItem { Allowed = true, Quality = Quality.FLAC } - } - }; - - _monitoredSeries = Builder.CreateNew() - .With(s => s.TvRageId = RandomNumber) - .With(s => s.Runtime = 30) - .With(s => s.Monitored = true) - .With(s => s.TitleSlug = "Title3") - .With(s => s.Id = profile.Id) - .BuildNew(); - - _unmonitoredSeries = Builder.CreateNew() - .With(s => s.TvdbId = RandomNumber) - .With(s => s.Runtime = 30) - .With(s => s.Monitored = false) - .With(s => s.TitleSlug = "Title2") - .With(s => s.Id = profile.Id) - .BuildNew(); - - _monitoredSeries.Id = Db.Insert(_monitoredSeries).Id; - _unmonitoredSeries.Id = Db.Insert(_unmonitoredSeries).Id; - - _pagingSpec = new PagingSpec - { - Page = 1, - PageSize = 10, - SortKey = "AirDate", - SortDirection = SortDirection.Ascending - }; - - _qualitiesBelowCutoff = new List - { - new QualitiesBelowCutoff(profile.Id, new[] {Quality.MP3_192.Id}) - }; - - var qualityMet = new TrackFile { RelativePath = "a", Quality = new QualityModel { Quality = Quality.MP3_256 } }; - var qualityUnmet = new TrackFile { RelativePath = "b", Quality = new QualityModel { Quality = Quality.MP3_192 } }; - var qualityRawHD = new TrackFile { RelativePath = "c", Quality = new QualityModel { Quality = Quality.FLAC } }; - - MediaFileRepository fileRepository = Mocker.Resolve(); - - qualityMet = fileRepository.Insert(qualityMet); - qualityUnmet = fileRepository.Insert(qualityUnmet); - qualityRawHD = fileRepository.Insert(qualityRawHD); - - var monitoredSeriesEpisodes = Builder.CreateListOfSize(4) - .All() - .With(e => e.Id = 0) - .With(e => e.SeriesId = _monitoredSeries.Id) - .With(e => e.AirDateUtc = DateTime.Now.AddDays(-5)) - .With(e => e.Monitored = true) - .With(e => e.EpisodeFileId = qualityUnmet.Id) - .TheFirst(1) - .With(e => e.Monitored = false) - .With(e => e.EpisodeFileId = qualityMet.Id) - .TheNext(1) - .With(e => e.EpisodeFileId = qualityRawHD.Id) - .TheLast(1) - .With(e => e.SeasonNumber = 0) - .Build(); - - var unmonitoredSeriesEpisodes = Builder.CreateListOfSize(3) - .All() - .With(e => e.Id = 0) - .With(e => e.SeriesId = _unmonitoredSeries.Id) - .With(e => e.AirDateUtc = DateTime.Now.AddDays(-5)) - .With(e => e.Monitored = true) - .With(e => e.EpisodeFileId = qualityUnmet.Id) - .TheFirst(1) - .With(e => e.Monitored = false) - .With(e => e.EpisodeFileId = qualityMet.Id) - .TheLast(1) - .With(e => e.SeasonNumber = 0) - .Build(); - - - _unairedEpisodes = Builder.CreateListOfSize(1) - .All() - .With(e => e.Id = 0) - .With(e => e.SeriesId = _monitoredSeries.Id) - .With(e => e.AirDateUtc = DateTime.Now.AddDays(5)) - .With(e => e.Monitored = true) - .With(e => e.EpisodeFileId = qualityUnmet.Id) - .Build() - .ToList(); - - Db.InsertMany(monitoredSeriesEpisodes); - Db.InsertMany(unmonitoredSeriesEpisodes); - } - - private void GivenMonitoredFilterExpression() - { - _pagingSpec.FilterExpression = e => e.Monitored == true && e.Series.Monitored == true; - } - - private void GivenUnmonitoredFilterExpression() - { - _pagingSpec.FilterExpression = e => e.Monitored == false || e.Series.Monitored == false; - } - - [Test] - public void should_include_episodes_where_cutoff_has_not_be_met() - { - GivenMonitoredFilterExpression(); - - var spec = Subject.EpisodesWhereCutoffUnmet(_pagingSpec, _qualitiesBelowCutoff, false); - - spec.Records.Should().HaveCount(1); - spec.Records.Should().OnlyContain(e => e.EpisodeFile.Value.Quality.Quality == Quality.MP3_192); - } - - [Test] - public void should_only_contain_monitored_episodes() - { - GivenMonitoredFilterExpression(); - - var spec = Subject.EpisodesWhereCutoffUnmet(_pagingSpec, _qualitiesBelowCutoff, false); - - spec.Records.Should().HaveCount(1); - spec.Records.Should().OnlyContain(e => e.Monitored); - } - - [Test] - public void should_only_contain_episode_with_monitored_series() - { - GivenMonitoredFilterExpression(); - - var spec = Subject.EpisodesWhereCutoffUnmet(_pagingSpec, _qualitiesBelowCutoff, false); - - spec.Records.Should().HaveCount(1); - spec.Records.Should().OnlyContain(e => e.Series.Monitored); - } - - [Test] - public void should_contain_unaired_episodes_if_file_does_not_meet_cutoff() - { - Db.InsertMany(_unairedEpisodes); - - GivenMonitoredFilterExpression(); - - var spec = Subject.EpisodesWhereCutoffUnmet(_pagingSpec, _qualitiesBelowCutoff, false); - - spec.Records.Should().HaveCount(2); - spec.Records.Should().OnlyContain(e => e.Series.Monitored); - } - } -} diff --git a/src/NzbDrone.Core.Test/TvTests/EpisodeRepositoryTests/EpisodesWithFilesFixture.cs b/src/NzbDrone.Core.Test/TvTests/EpisodeRepositoryTests/EpisodesWithFilesFixture.cs deleted file mode 100644 index e12a8b1c0..000000000 --- a/src/NzbDrone.Core.Test/TvTests/EpisodeRepositoryTests/EpisodesWithFilesFixture.cs +++ /dev/null @@ -1,81 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using FizzWare.NBuilder; -using FluentAssertions; -using NUnit.Framework; -using NzbDrone.Core.MediaFiles; -using NzbDrone.Core.Test.Framework; -using NzbDrone.Core.Tv; -using NzbDrone.Core.Qualities; - -namespace NzbDrone.Core.Test.TvTests.EpisodeRepositoryTests -{ - [TestFixture] - public class EpisodesWithFilesFixture : DbTest - { - private const int SERIES_ID = 1; - private List _episodes; - private List _episodeFiles; - - [SetUp] - public void Setup() - { - _episodeFiles = Builder.CreateListOfSize(5) - .All() - .With(c => c.Quality = new QualityModel()) - .BuildListOfNew(); - - Db.InsertMany(_episodeFiles); - - _episodes = Builder.CreateListOfSize(10) - .All() - .With(e => e.EpisodeFileId = 0) - .With(e => e.SeriesId = SERIES_ID) - .BuildListOfNew() - .ToList(); - - for (int i = 0; i < _episodeFiles.Count; i++) - { - _episodes[i].EpisodeFileId = _episodeFiles[i].Id; - } - - Db.InsertMany(_episodes); - } - - - [Test] - public void should_only_get_files_that_have_episode_files() - { - var result = Subject.EpisodesWithFiles(SERIES_ID); - - result.Should().OnlyContain(e => e.EpisodeFileId > 0); - result.Should().HaveCount(_episodeFiles.Count); - } - - [Test] - public void should_only_contain_episodes_for_the_given_series() - { - var episodeFile = Builder.CreateNew() - .With(f => f.RelativePath = "another path") - .With(c => c.Quality = new QualityModel()) - .BuildNew(); - - Db.Insert(episodeFile); - - var episode = Builder.CreateNew() - .With(e => e.SeriesId = SERIES_ID + 10) - .With(e => e.EpisodeFileId = episodeFile.Id) - .BuildNew(); - - Db.Insert(episode); - - Subject.EpisodesWithFiles(episode.SeriesId).Should().OnlyContain(e => e.SeriesId == episode.SeriesId); - } - - [Test] - public void should_have_episode_file_loaded() - { - Subject.EpisodesWithFiles(SERIES_ID).Should().OnlyContain(e => e.EpisodeFile.IsLoaded); - } - } -} diff --git a/src/NzbDrone.Core.Test/TvTests/EpisodeRepositoryTests/EpisodesWithoutFilesFixture.cs b/src/NzbDrone.Core.Test/TvTests/EpisodeRepositoryTests/EpisodesWithoutFilesFixture.cs deleted file mode 100644 index 4f8f9eb23..000000000 --- a/src/NzbDrone.Core.Test/TvTests/EpisodeRepositoryTests/EpisodesWithoutFilesFixture.cs +++ /dev/null @@ -1,167 +0,0 @@ -using System; -using System.Linq; -using FizzWare.NBuilder; -using FluentAssertions; -using NUnit.Framework; -using NzbDrone.Core.Datastore; -using NzbDrone.Core.Test.Framework; -using NzbDrone.Core.Tv; - -namespace NzbDrone.Core.Test.TvTests.EpisodeRepositoryTests -{ - [TestFixture] - public class EpisodesWithoutFilesFixture : DbTest - { - private Series _monitoredSeries; - private Series _unmonitoredSeries; - private PagingSpec _pagingSpec; - - [SetUp] - public void Setup() - { - _monitoredSeries = Builder.CreateNew() - .With(s => s.Id = 0) - .With(s => s.TvRageId = RandomNumber) - .With(s => s.Runtime = 30) - .With(s => s.Monitored = true) - .With(s => s.TitleSlug = "Title3") - .Build(); - - _unmonitoredSeries = Builder.CreateNew() - .With(s => s.Id = 0) - .With(s => s.TvdbId = RandomNumber) - .With(s => s.Runtime = 30) - .With(s => s.Monitored = false) - .With(s => s.TitleSlug = "Title2") - .Build(); - - _monitoredSeries.Id = Db.Insert(_monitoredSeries).Id; - _unmonitoredSeries.Id = Db.Insert(_unmonitoredSeries).Id; - - _pagingSpec = new PagingSpec - { - Page = 1, - PageSize = 10, - SortKey = "AirDate", - SortDirection = SortDirection.Ascending - }; - - var monitoredSeriesEpisodes = Builder.CreateListOfSize(3) - .All() - .With(e => e.Id = 0) - .With(e => e.SeriesId = _monitoredSeries.Id) - .With(e => e.EpisodeFileId = 0) - .With(e => e.AirDateUtc = DateTime.Now.AddDays(-5)) - .With(e => e.Monitored = true) - .TheFirst(1) - .With(e => e.Monitored = false) - .TheLast(1) - .With(e => e.SeasonNumber = 0) - .Build(); - - var unmonitoredSeriesEpisodes = Builder.CreateListOfSize(3) - .All() - .With(e => e.Id = 0) - .With(e => e.SeriesId = _unmonitoredSeries.Id) - .With(e => e.EpisodeFileId = 0) - .With(e => e.AirDateUtc = DateTime.Now.AddDays(-5)) - .With(e => e.Monitored = true) - .TheFirst(1) - .With(e => e.Monitored = false) - .TheLast(1) - .With(e => e.SeasonNumber = 0) - .Build(); - - - var unairedEpisodes = Builder.CreateListOfSize(1) - .All() - .With(e => e.Id = 0) - .With(e => e.SeriesId = _monitoredSeries.Id) - .With(e => e.EpisodeFileId = 0) - .With(e => e.AirDateUtc = DateTime.Now.AddDays(5)) - .With(e => e.Monitored = true) - .Build(); - - - Db.InsertMany(monitoredSeriesEpisodes); - Db.InsertMany(unmonitoredSeriesEpisodes); - Db.InsertMany(unairedEpisodes); - } - - private void GivenMonitoredFilterExpression() - { - _pagingSpec.FilterExpression = e => e.Monitored == true && e.Series.Monitored == true; - } - - private void GivenUnmonitoredFilterExpression() - { - _pagingSpec.FilterExpression = e => e.Monitored == false || e.Series.Monitored == false; - } - - [Test] - public void should_get_monitored_episodes() - { - GivenMonitoredFilterExpression(); - - var episodes = Subject.EpisodesWithoutFiles(_pagingSpec, false); - - episodes.Records.Should().HaveCount(1); - } - - [Test] - [Ignore("Specials not implemented")] - public void should_get_episode_including_specials() - { - var episodes = Subject.EpisodesWithoutFiles(_pagingSpec, true); - - episodes.Records.Should().HaveCount(2); - } - - [Test] - public void should_not_include_unmonitored_episodes() - { - GivenMonitoredFilterExpression(); - - var episodes = Subject.EpisodesWithoutFiles(_pagingSpec, false); - - episodes.Records.Should().NotContain(e => e.Monitored == false); - } - - [Test] - public void should_not_contain_unmonitored_series() - { - GivenMonitoredFilterExpression(); - - var episodes = Subject.EpisodesWithoutFiles(_pagingSpec, false); - - episodes.Records.Should().NotContain(e => e.SeriesId == _unmonitoredSeries.Id); - } - - [Test] - public void should_not_return_unaired() - { - var episodes = Subject.EpisodesWithoutFiles(_pagingSpec, false); - - episodes.TotalRecords.Should().Be(4); - } - - [Test] - public void should_not_return_episodes_on_air() - { - var onAirEpisode = Builder.CreateNew() - .With(e => e.Id = 0) - .With(e => e.SeriesId = _monitoredSeries.Id) - .With(e => e.EpisodeFileId = 0) - .With(e => e.AirDateUtc = DateTime.Now.AddMinutes(-15)) - .With(e => e.Monitored = true) - .Build(); - - Db.Insert(onAirEpisode); - - var episodes = Subject.EpisodesWithoutFiles(_pagingSpec, false); - - episodes.TotalRecords.Should().Be(4); - episodes.Records.Where(e => e.Id == onAirEpisode.Id).Should().BeEmpty(); - } - } -} diff --git a/src/NzbDrone.Core.Test/TvTests/EpisodeRepositoryTests/FindEpisodeFixture.cs b/src/NzbDrone.Core.Test/TvTests/EpisodeRepositoryTests/FindEpisodeFixture.cs deleted file mode 100644 index 29730bb60..000000000 --- a/src/NzbDrone.Core.Test/TvTests/EpisodeRepositoryTests/FindEpisodeFixture.cs +++ /dev/null @@ -1,85 +0,0 @@ -using System.Linq; -using FizzWare.NBuilder; -using FluentAssertions; -using NUnit.Framework; -using NzbDrone.Core.Test.Framework; -using NzbDrone.Core.Tv; - -namespace NzbDrone.Core.Test.TvTests.EpisodeRepositoryTests -{ - [TestFixture] - public class FindEpisodeFixture : DbTest - { - private Episode _episode1; - private Episode _episode2; - - [SetUp] - public void Setup() - { - _episode1 = Builder.CreateNew() - .With(e => e.SeriesId = 1) - .With(e => e.SeasonNumber = 1) - .With(e => e.SceneSeasonNumber = 2) - .With(e => e.EpisodeNumber = 3) - .With(e => e.AbsoluteEpisodeNumber = 3) - .With(e => e.SceneEpisodeNumber = 4) - .BuildNew(); - - _episode2 = Builder.CreateNew() - .With(e => e.SeriesId = 1) - .With(e => e.SeasonNumber = 1) - .With(e => e.SceneSeasonNumber = 2) - .With(e => e.EpisodeNumber = 4) - .With(e => e.SceneEpisodeNumber = 4) - .BuildNew(); - - _episode1 = Db.Insert(_episode1); - } - - [Test] - public void should_find_episode_by_scene_numbering() - { - Subject.FindEpisodesBySceneNumbering(_episode1.SeriesId, _episode1.SceneSeasonNumber.Value, _episode1.SceneEpisodeNumber.Value) - .First() - .Id - .Should() - .Be(_episode1.Id); - } - - [Test] - public void should_find_episode_by_standard_numbering() - { - Subject.Find(_episode1.SeriesId, _episode1.SeasonNumber, _episode1.EpisodeNumber) - .Id - .Should() - .Be(_episode1.Id); - } - - [Test] - public void should_not_find_episode_that_does_not_exist() - { - Subject.Find(_episode1.SeriesId, _episode1.SeasonNumber + 1, _episode1.EpisodeNumber) - .Should() - .BeNull(); - } - - [Test] - public void should_find_episode_by_absolute_numbering() - { - Subject.Find(_episode1.SeriesId, _episode1.AbsoluteEpisodeNumber.Value) - .Id - .Should() - .Be(_episode1.Id); - } - - [Test] - public void should_return_multiple_episode_if_multiple_match_by_scene_numbering() - { - _episode2 = Db.Insert(_episode2); - - Subject.FindEpisodesBySceneNumbering(_episode1.SeriesId, _episode1.SceneSeasonNumber.Value, _episode1.SceneEpisodeNumber.Value) - .Should() - .HaveCount(2); - } - } -} diff --git a/src/NzbDrone.Core.Test/TvTests/EpisodeServiceTests/FindEpisodeByTitleFixture.cs b/src/NzbDrone.Core.Test/TvTests/EpisodeServiceTests/FindEpisodeByTitleFixture.cs deleted file mode 100644 index 46fafec3c..000000000 --- a/src/NzbDrone.Core.Test/TvTests/EpisodeServiceTests/FindEpisodeByTitleFixture.cs +++ /dev/null @@ -1,71 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using FizzWare.NBuilder; -using FluentAssertions; -using Moq; -using NUnit.Framework; -using NzbDrone.Core.Test.Framework; -using NzbDrone.Core.Tv; - -namespace NzbDrone.Core.Test.TvTests.EpisodeServiceTests -{ - [TestFixture] - public class FindEpisodeByTitleFixture : CoreTest - { - private List _episodes; - - [SetUp] - public void Setup() - { - _episodes = Builder.CreateListOfSize(5) - .Build() - .ToList(); - } - - private void GivenEpisodesWithTitles(params string[] titles) - { - for (int i = 0; i < titles.Count(); i++) - { - _episodes[i].Title = titles[i]; - } - - Mocker.GetMock() - .Setup(s => s.GetEpisodes(It.IsAny(), It.IsAny())) - .Returns(_episodes); - } - - [Test] - public void should_find_episode_by_title() - { - const string expectedTitle = "A Journey to the Highlands"; - GivenEpisodesWithTitles(expectedTitle); - - Subject.FindEpisodeByTitle(1, 1, "Downton.Abbey.A.Journey.To.The.Highlands.720p.BluRay.x264-aAF") - .Title - .Should() - .Be(expectedTitle); - } - - [Test] - public void should_prefer_longer_match() - { - const string expectedTitle = "Inside The Walking Dead: Walker University"; - GivenEpisodesWithTitles("Inside The Walking Dead", expectedTitle); - - Subject.FindEpisodeByTitle(1, 1, "The.Walking.Dead.S04.Special.Inside.The.Walking.Dead.Walker.University.720p.HDTV.x264-W4F") - .Title - .Should() - .Be(expectedTitle); - } - - [Test] - public void should_return_null_when_no_match_is_found() - { - GivenEpisodesWithTitles(); - - Subject.FindEpisodeByTitle(1, 1, "The.Walking.Dead.S04.Special.Inside.The.Walking.Dead.Walker.University.720p.HDTV.x264-W4F") - .Should() - .BeNull(); - } - } -} diff --git a/src/NzbDrone.Core.Test/TvTests/EpisodeServiceTests/HandleEpisodeFileDeletedFixture.cs b/src/NzbDrone.Core.Test/TvTests/EpisodeServiceTests/HandleEpisodeFileDeletedFixture.cs deleted file mode 100644 index 4cb575007..000000000 --- a/src/NzbDrone.Core.Test/TvTests/EpisodeServiceTests/HandleEpisodeFileDeletedFixture.cs +++ /dev/null @@ -1,123 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using FizzWare.NBuilder; -using Moq; -using NUnit.Framework; -using NzbDrone.Core.Configuration; -using NzbDrone.Core.MediaFiles; -using NzbDrone.Core.MediaFiles.Events; -using NzbDrone.Core.Test.Framework; -using NzbDrone.Core.Tv; - -namespace NzbDrone.Core.Test.TvTests.EpisodeServiceTests -{ - [TestFixture] - public class HandleEpisodeFileDeletedFixture : CoreTest - { - private EpisodeFile _episodeFile; - private List _episodes; - - [SetUp] - public void Setup() - { - _episodeFile = Builder - .CreateNew() - .Build(); - } - - private void GivenSingleEpisodeFile() - { - _episodes = Builder - .CreateListOfSize(1) - .All() - .With(e => e.Monitored = true) - .Build() - .ToList(); - - Mocker.GetMock() - .Setup(s => s.GetEpisodeByFileId(_episodeFile.Id)) - .Returns(_episodes); - } - - private void GivenMultiEpisodeFile() - { - _episodes = Builder - .CreateListOfSize(2) - .All() - .With(e => e.Monitored = true) - .Build() - .ToList(); - - Mocker.GetMock() - .Setup(s => s.GetEpisodeByFileId(_episodeFile.Id)) - .Returns(_episodes); - } - - [Test] - public void should_set_EpisodeFileId_to_zero() - { - GivenSingleEpisodeFile(); - - Subject.Handle(new EpisodeFileDeletedEvent(_episodeFile, DeleteMediaFileReason.MissingFromDisk)); - - Mocker.GetMock() - .Verify(v => v.Update(It.Is(e => e.EpisodeFileId == 0)), Times.Once()); - } - - [Test] - public void should_update_each_episode_for_file() - { - GivenMultiEpisodeFile(); - - Subject.Handle(new EpisodeFileDeletedEvent(_episodeFile, DeleteMediaFileReason.MissingFromDisk)); - - Mocker.GetMock() - .Verify(v => v.Update(It.Is(e => e.EpisodeFileId == 0)), Times.Exactly(2)); - } - - [Test] - public void should_set_monitored_to_false_if_autoUnmonitor_is_true_and_is_not_for_an_upgrade() - { - GivenSingleEpisodeFile(); - - Mocker.GetMock() - .SetupGet(s => s.AutoUnmonitorPreviouslyDownloadedTracks) - .Returns(true); - - Subject.Handle(new EpisodeFileDeletedEvent(_episodeFile, DeleteMediaFileReason.MissingFromDisk)); - - Mocker.GetMock() - .Verify(v => v.Update(It.Is(e => e.Monitored == false)), Times.Once()); - } - - [Test] - public void should_leave_monitored_to_true_if_autoUnmonitor_is_false() - { - GivenSingleEpisodeFile(); - - Mocker.GetMock() - .SetupGet(s => s.AutoUnmonitorPreviouslyDownloadedTracks) - .Returns(false); - - Subject.Handle(new EpisodeFileDeletedEvent(_episodeFile, DeleteMediaFileReason.Upgrade)); - - Mocker.GetMock() - .Verify(v => v.Update(It.Is(e => e.Monitored == true)), Times.Once()); - } - - [Test] - public void should_leave_monitored_to_true_if_autoUnmonitor_is_true_and_is_for_an_upgrade() - { - GivenSingleEpisodeFile(); - - Mocker.GetMock() - .SetupGet(s => s.AutoUnmonitorPreviouslyDownloadedTracks) - .Returns(true); - - Subject.Handle(new EpisodeFileDeletedEvent(_episodeFile, DeleteMediaFileReason.Upgrade)); - - Mocker.GetMock() - .Verify(v => v.Update(It.Is(e => e.Monitored == true)), Times.Once()); - } - } -} diff --git a/src/NzbDrone.Core.Test/TvTests/RefreshEpisodeServiceFixture.cs b/src/NzbDrone.Core.Test/TvTests/RefreshEpisodeServiceFixture.cs deleted file mode 100644 index 592b56dc3..000000000 --- a/src/NzbDrone.Core.Test/TvTests/RefreshEpisodeServiceFixture.cs +++ /dev/null @@ -1,397 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using FizzWare.NBuilder; -using FluentAssertions; -using Moq; -using NUnit.Framework; -using NzbDrone.Common.Extensions; -using NzbDrone.Core.MetadataSource.SkyHook; -using NzbDrone.Core.Tv; -using NzbDrone.Core.Test.Framework; - -namespace NzbDrone.Core.Test.TvTests -{ - [TestFixture] - public class RefreshEpisodeServiceFixture : CoreTest - { - private List _insertedEpisodes; - private List _updatedEpisodes; - private List _deletedEpisodes; - private Tuple> _gameOfThrones; - - [TestFixtureSetUp] - public void TestFixture() - { - UseRealHttp(); - - _gameOfThrones = Mocker.Resolve().GetSeriesInfo(121361);//Game of thrones - - // Remove specials. - _gameOfThrones.Item2.RemoveAll(v => v.SeasonNumber == 0); - } - - private List GetEpisodes() - { - return _gameOfThrones.Item2.JsonClone(); - } - - private Series GetSeries() - { - var series = _gameOfThrones.Item1.JsonClone(); - series.Seasons = new List(); - - return series; - } - - private Series GetAnimeSeries() - { - var series = Builder.CreateNew().Build(); - series.SeriesType = SeriesTypes.Anime; - series.Seasons = new List(); - - return series; - } - - [SetUp] - public void Setup() - { - _insertedEpisodes = new List(); - _updatedEpisodes = new List(); - _deletedEpisodes = new List(); - - Mocker.GetMock().Setup(c => c.InsertMany(It.IsAny>())) - .Callback>(e => _insertedEpisodes = e); - - - Mocker.GetMock().Setup(c => c.UpdateMany(It.IsAny>())) - .Callback>(e => _updatedEpisodes = e); - - - Mocker.GetMock().Setup(c => c.DeleteMany(It.IsAny>())) - .Callback>(e => _deletedEpisodes = e); - } - - [Test] - public void should_create_all_when_no_existing_episodes() - { - Mocker.GetMock().Setup(c => c.GetEpisodeBySeries(It.IsAny())) - .Returns(new List()); - - Subject.RefreshEpisodeInfo(GetSeries(), GetEpisodes()); - - _insertedEpisodes.Should().HaveSameCount(GetEpisodes()); - _updatedEpisodes.Should().BeEmpty(); - _deletedEpisodes.Should().BeEmpty(); - } - - [Test] - public void should_update_all_when_all_existing_episodes() - { - Mocker.GetMock().Setup(c => c.GetEpisodeBySeries(It.IsAny())) - .Returns(GetEpisodes()); - - Subject.RefreshEpisodeInfo(GetSeries(), GetEpisodes()); - - _insertedEpisodes.Should().BeEmpty(); - _updatedEpisodes.Should().HaveSameCount(GetEpisodes()); - _deletedEpisodes.Should().BeEmpty(); - } - - [Test] - public void should_delete_all_when_all_existing_episodes_are_gone_from_datasource() - { - Mocker.GetMock().Setup(c => c.GetEpisodeBySeries(It.IsAny())) - .Returns(GetEpisodes()); - - Subject.RefreshEpisodeInfo(GetSeries(), new List()); - - _insertedEpisodes.Should().BeEmpty(); - _updatedEpisodes.Should().BeEmpty(); - _deletedEpisodes.Should().HaveSameCount(GetEpisodes()); - } - - [Test] - public void should_delete_duplicated_episodes_based_on_season_episode_number() - { - var duplicateEpisodes = GetEpisodes().Skip(5).Take(2).ToList(); - - Mocker.GetMock().Setup(c => c.GetEpisodeBySeries(It.IsAny())) - .Returns(GetEpisodes().Union(duplicateEpisodes).ToList()); - - Subject.RefreshEpisodeInfo(GetSeries(), GetEpisodes()); - - _insertedEpisodes.Should().BeEmpty(); - _updatedEpisodes.Should().HaveSameCount(GetEpisodes()); - _deletedEpisodes.Should().HaveSameCount(duplicateEpisodes); - } - - [Test] - public void should_not_change_monitored_status_for_existing_episodes() - { - var series = GetSeries(); - series.Seasons = new List(); - series.Seasons.Add(new Season { SeasonNumber = 1, Monitored = false }); - - var episodes = GetEpisodes(); - - episodes.ForEach(e => e.Monitored = true); - - Mocker.GetMock().Setup(c => c.GetEpisodeBySeries(It.IsAny())) - .Returns(episodes); - - Subject.RefreshEpisodeInfo(series, GetEpisodes()); - - _updatedEpisodes.Should().HaveSameCount(GetEpisodes()); - _updatedEpisodes.Should().OnlyContain(e => e.Monitored == true); - } - - [Test] - public void should_remove_duplicate_remote_episodes_before_processing() - { - Mocker.GetMock().Setup(c => c.GetEpisodeBySeries(It.IsAny())) - .Returns(new List()); - - var episodes = Builder.CreateListOfSize(5) - .TheFirst(2) - .With(e => e.SeasonNumber = 1) - .With(e => e.EpisodeNumber = 1) - .Build() - .ToList(); - - Subject.RefreshEpisodeInfo(GetSeries(), episodes); - - _insertedEpisodes.Should().HaveCount(episodes.Count - 1); - _updatedEpisodes.Should().BeEmpty(); - _deletedEpisodes.Should().BeEmpty(); - } - - [Test] - public void should_set_absolute_episode_number_for_anime() - { - var episodes = Builder.CreateListOfSize(3).Build().ToList(); - - Mocker.GetMock().Setup(c => c.GetEpisodeBySeries(It.IsAny())) - .Returns(new List()); - - Subject.RefreshEpisodeInfo(GetAnimeSeries(), episodes); - - _insertedEpisodes.All(e => e.AbsoluteEpisodeNumber.HasValue).Should().BeTrue(); - _updatedEpisodes.Should().BeEmpty(); - _deletedEpisodes.Should().BeEmpty(); - } - - [Test] - public void should_set_absolute_episode_number_even_if_not_previously_set_for_anime() - { - var episodes = Builder.CreateListOfSize(3).Build().ToList(); - - var existingEpisodes = episodes.JsonClone(); - existingEpisodes.ForEach(e => e.AbsoluteEpisodeNumber = null); - - Mocker.GetMock().Setup(c => c.GetEpisodeBySeries(It.IsAny())) - .Returns(existingEpisodes); - - Subject.RefreshEpisodeInfo(GetAnimeSeries(), episodes); - - _insertedEpisodes.Should().BeEmpty(); - _updatedEpisodes.All(e => e.AbsoluteEpisodeNumber.HasValue).Should().BeTrue(); - _deletedEpisodes.Should().BeEmpty(); - } - - [Test] - public void should_get_new_season_and_episode_numbers_when_absolute_episode_number_match_found() - { - const int expectedSeasonNumber = 10; - const int expectedEpisodeNumber = 5; - const int expectedAbsoluteNumber = 3; - - var episode = Builder.CreateNew() - .With(e => e.SeasonNumber = expectedSeasonNumber) - .With(e => e.EpisodeNumber = expectedEpisodeNumber) - .With(e => e.AbsoluteEpisodeNumber = expectedAbsoluteNumber) - .Build(); - - var existingEpisode = episode.JsonClone(); - existingEpisode.SeasonNumber = 1; - existingEpisode.EpisodeNumber = 1; - existingEpisode.AbsoluteEpisodeNumber = expectedAbsoluteNumber; - - Mocker.GetMock().Setup(c => c.GetEpisodeBySeries(It.IsAny())) - .Returns(new List{ existingEpisode }); - - Subject.RefreshEpisodeInfo(GetAnimeSeries(), new List { episode }); - - _insertedEpisodes.Should().BeEmpty(); - _deletedEpisodes.Should().BeEmpty(); - - _updatedEpisodes.First().SeasonNumber.Should().Be(expectedSeasonNumber); - _updatedEpisodes.First().EpisodeNumber.Should().Be(expectedEpisodeNumber); - _updatedEpisodes.First().AbsoluteEpisodeNumber.Should().Be(expectedAbsoluteNumber); - } - - [Test] - public void should_prefer_absolute_match_over_season_and_epsiode_match() - { - var episodes = Builder.CreateListOfSize(2) - .Build() - .ToList(); - - episodes[0].AbsoluteEpisodeNumber = null; - episodes[0].SeasonNumber.Should().NotBe(episodes[1].SeasonNumber); - episodes[0].EpisodeNumber.Should().NotBe(episodes[1].EpisodeNumber); - - var existingEpisode = new Episode - { - SeasonNumber = episodes[0].SeasonNumber, - EpisodeNumber = episodes[0].EpisodeNumber, - AbsoluteEpisodeNumber = episodes[1].AbsoluteEpisodeNumber - }; - - Mocker.GetMock().Setup(c => c.GetEpisodeBySeries(It.IsAny())) - .Returns(new List { existingEpisode }); - - Subject.RefreshEpisodeInfo(GetAnimeSeries(), episodes); - - _updatedEpisodes.First().SeasonNumber.Should().Be(episodes[1].SeasonNumber); - _updatedEpisodes.First().EpisodeNumber.Should().Be(episodes[1].EpisodeNumber); - _updatedEpisodes.First().AbsoluteEpisodeNumber.Should().Be(episodes[1].AbsoluteEpisodeNumber); - } - - [Test] - public void should_ignore_episodes_with_no_absolute_episode_in_distinct_by_absolute() - { - var episodes = Builder.CreateListOfSize(10) - .Build() - .ToList(); - - episodes[0].AbsoluteEpisodeNumber = null; - episodes[1].AbsoluteEpisodeNumber = null; - episodes[2].AbsoluteEpisodeNumber = null; - episodes[3].AbsoluteEpisodeNumber = null; - episodes[4].AbsoluteEpisodeNumber = null; - - Mocker.GetMock().Setup(c => c.GetEpisodeBySeries(It.IsAny())) - .Returns(new List()); - - Subject.RefreshEpisodeInfo(GetAnimeSeries(), episodes); - - _insertedEpisodes.Should().HaveCount(episodes.Count); - - } - - [Test] - public void should_override_empty_airdate_for_direct_to_dvd() - { - var series = GetSeries(); - series.Status = SeriesStatusType.Ended; - - var episodes = Builder.CreateListOfSize(10) - .All() - .With(v => v.AirDateUtc = null) - .BuildListOfNew(); - - Mocker.GetMock().Setup(c => c.GetEpisodeBySeries(It.IsAny())) - .Returns(new List()); - - List updateEpisodes = null; - Mocker.GetMock().Setup(c => c.InsertMany(It.IsAny>())) - .Callback>(c => updateEpisodes = c); - - Subject.RefreshEpisodeInfo(series, episodes); - - updateEpisodes.Should().NotBeNull(); - updateEpisodes.Should().NotBeEmpty(); - updateEpisodes.All(v => v.AirDateUtc.HasValue).Should().BeTrue(); - } - - [Test] - public void should_use_tba_for_episode_title_when_null() - { - Mocker.GetMock().Setup(c => c.GetEpisodeBySeries(It.IsAny())) - .Returns(new List()); - - var episodes = Builder.CreateListOfSize(1) - .All() - .With(e => e.Title = null) - .Build() - .ToList(); - - Subject.RefreshEpisodeInfo(GetSeries(), episodes); - - _insertedEpisodes.First().Title.Should().Be("TBA"); - } - - [Test] - public void should_update_air_date_when_multiple_episodes_air_on_the_same_day() - { - Mocker.GetMock().Setup(c => c.GetEpisodeBySeries(It.IsAny())) - .Returns(new List()); - - var series = GetSeries(); - - var episodes = Builder.CreateListOfSize(2) - .All() - .With(e => e.SeasonNumber = 1) - .With(e => e.AirDate = DateTime.Now.ToShortDateString()) - .With(e => e.AirDateUtc = DateTime.UtcNow) - .Build() - .ToList(); - - Subject.RefreshEpisodeInfo(series, episodes); - - _insertedEpisodes.First().AirDateUtc.Value.ToString("s").Should().Be(episodes.First().AirDateUtc.Value.ToString("s")); - _insertedEpisodes.Last().AirDateUtc.Value.ToString("s").Should().Be(episodes.First().AirDateUtc.Value.AddMinutes(series.Runtime).ToString("s")); - } - - [Test] - public void should_not_update_air_date_when_multiple_episodes_air_on_the_same_day_for_netflix() - { - Mocker.GetMock().Setup(c => c.GetEpisodeBySeries(It.IsAny())) - .Returns(new List()); - - var series = GetSeries(); - series.Network = "Netflix"; - - var episodes = Builder.CreateListOfSize(2) - .All() - .With(e => e.SeasonNumber = 1) - .With(e => e.AirDate = DateTime.Now.ToShortDateString()) - .With(e => e.AirDateUtc = DateTime.UtcNow) - .Build() - .ToList(); - - Subject.RefreshEpisodeInfo(series, episodes); - - _insertedEpisodes.Should().OnlyContain(e => e.AirDateUtc.Value.ToString("s") == episodes.First().AirDateUtc.Value.ToString("s")); - } - - [Test] - public void should_prefer_regular_season_when_absolute_numbers_conflict() - { - var episodes = Builder.CreateListOfSize(2) - .Build() - .ToList(); - - episodes[0].AbsoluteEpisodeNumber = episodes[1].AbsoluteEpisodeNumber; - episodes[0].SeasonNumber = 0; - episodes[0].EpisodeNumber.Should().NotBe(episodes[1].EpisodeNumber); - - var existingEpisode = new Episode - { - SeasonNumber = episodes[0].SeasonNumber, - EpisodeNumber = episodes[0].EpisodeNumber, - AbsoluteEpisodeNumber = episodes[1].AbsoluteEpisodeNumber - }; - - Mocker.GetMock().Setup(c => c.GetEpisodeBySeries(It.IsAny())) - .Returns(new List { existingEpisode }); - - Subject.RefreshEpisodeInfo(GetAnimeSeries(), episodes); - - _updatedEpisodes.First().SeasonNumber.Should().Be(episodes[1].SeasonNumber); - _updatedEpisodes.First().EpisodeNumber.Should().Be(episodes[1].EpisodeNumber); - _updatedEpisodes.First().AbsoluteEpisodeNumber.Should().Be(episodes[1].AbsoluteEpisodeNumber); - } - } -} \ No newline at end of file diff --git a/src/NzbDrone.Core.Test/TvTests/RefreshSeriesServiceFixture.cs b/src/NzbDrone.Core.Test/TvTests/RefreshSeriesServiceFixture.cs deleted file mode 100644 index f441496cd..000000000 --- a/src/NzbDrone.Core.Test/TvTests/RefreshSeriesServiceFixture.cs +++ /dev/null @@ -1,184 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using FizzWare.NBuilder; -using Moq; -using NUnit.Framework; -using NzbDrone.Common.Extensions; -using NzbDrone.Core.Exceptions; -using NzbDrone.Core.MetadataSource; -using NzbDrone.Core.Test.Framework; -using NzbDrone.Core.Tv; -using NzbDrone.Core.Tv.Commands; -using NzbDrone.Test.Common; - -namespace NzbDrone.Core.Test.TvTests -{ - [TestFixture] - public class RefreshSeriesServiceFixture : CoreTest - { - private Series _series; - - [SetUp] - public void Setup() - { - var season1 = Builder.CreateNew() - .With(s => s.SeasonNumber = 1) - .Build(); - - _series = Builder.CreateNew() - .With(s => s.Seasons = new List - { - season1 - }) - .Build(); - - Mocker.GetMock() - .Setup(s => s.GetSeries(_series.Id)) - .Returns(_series); - - Mocker.GetMock() - .Setup(s => s.GetSeriesInfo(It.IsAny())) - .Callback(p => { throw new SeriesNotFoundException(p); }); - } - - private void GivenNewSeriesInfo(Series series) - { - Mocker.GetMock() - .Setup(s => s.GetSeriesInfo(_series.TvdbId)) - .Returns(new Tuple>(series, new List())); - } - - [Test] - public void should_monitor_new_seasons_automatically() - { - var newSeriesInfo = _series.JsonClone(); - newSeriesInfo.Seasons.Add(Builder.CreateNew() - .With(s => s.SeasonNumber = 2) - .Build()); - - GivenNewSeriesInfo(newSeriesInfo); - - Subject.Execute(new RefreshSeriesCommand(_series.Id)); - - Mocker.GetMock() - .Verify(v => v.UpdateSeries(It.Is(s => s.Seasons.Count == 2 && s.Seasons.Single(season => season.SeasonNumber == 2).Monitored == true))); - } - - [Test] - public void should_not_monitor_new_special_season_automatically() - { - var series = _series.JsonClone(); - series.Seasons.Add(Builder.CreateNew() - .With(s => s.SeasonNumber = 0) - .Build()); - - GivenNewSeriesInfo(series); - - Subject.Execute(new RefreshSeriesCommand(_series.Id)); - - Mocker.GetMock() - .Verify(v => v.UpdateSeries(It.Is(s => s.Seasons.Count == 2 && s.Seasons.Single(season => season.SeasonNumber == 0).Monitored == false))); - } - - [Test] - public void should_update_tvrage_id_if_changed() - { - var newSeriesInfo = _series.JsonClone(); - newSeriesInfo.TvRageId = _series.TvRageId + 1; - - GivenNewSeriesInfo(newSeriesInfo); - - Subject.Execute(new RefreshSeriesCommand(_series.Id)); - - Mocker.GetMock() - .Verify(v => v.UpdateSeries(It.Is(s => s.TvRageId == newSeriesInfo.TvRageId))); - } - - [Test] - public void should_update_tvmaze_id_if_changed() - { - var newSeriesInfo = _series.JsonClone(); - newSeriesInfo.TvMazeId = _series.TvMazeId + 1; - - GivenNewSeriesInfo(newSeriesInfo); - - Subject.Execute(new RefreshSeriesCommand(_series.Id)); - - Mocker.GetMock() - .Verify(v => v.UpdateSeries(It.Is(s => s.TvMazeId == newSeriesInfo.TvMazeId))); - } - - [Test] - public void should_log_error_if_tvdb_id_not_found() - { - Subject.Execute(new RefreshSeriesCommand(_series.Id)); - - Mocker.GetMock() - .Verify(v => v.UpdateSeries(It.IsAny()), Times.Never()); - - ExceptionVerification.ExpectedErrors(1); - } - - [Test] - public void should_update_if_tvdb_id_changed() - { - var newSeriesInfo = _series.JsonClone(); - newSeriesInfo.TvdbId = _series.TvdbId + 1; - - GivenNewSeriesInfo(newSeriesInfo); - - Subject.Execute(new RefreshSeriesCommand(_series.Id)); - - Mocker.GetMock() - .Verify(v => v.UpdateSeries(It.Is(s => s.TvdbId == newSeriesInfo.TvdbId))); - - ExceptionVerification.ExpectedWarns(1); - } - - [Test] - public void should_not_throw_if_duplicate_season_is_in_existing_info() - { - var newSeriesInfo = _series.JsonClone(); - newSeriesInfo.Seasons.Add(Builder.CreateNew() - .With(s => s.SeasonNumber = 2) - .Build()); - - _series.Seasons.Add(Builder.CreateNew() - .With(s => s.SeasonNumber = 2) - .Build()); - - _series.Seasons.Add(Builder.CreateNew() - .With(s => s.SeasonNumber = 2) - .Build()); - - GivenNewSeriesInfo(newSeriesInfo); - - Subject.Execute(new RefreshSeriesCommand(_series.Id)); - - Mocker.GetMock() - .Verify(v => v.UpdateSeries(It.Is(s => s.Seasons.Count == 2))); - } - - [Test] - public void should_filter_duplicate_seasons() - { - var newSeriesInfo = _series.JsonClone(); - newSeriesInfo.Seasons.Add(Builder.CreateNew() - .With(s => s.SeasonNumber = 2) - .Build()); - - newSeriesInfo.Seasons.Add(Builder.CreateNew() - .With(s => s.SeasonNumber = 2) - .Build()); - - GivenNewSeriesInfo(newSeriesInfo); - - Subject.Execute(new RefreshSeriesCommand(_series.Id)); - - Mocker.GetMock() - .Verify(v => v.UpdateSeries(It.Is(s => s.Seasons.Count == 2))); - - } - } -} diff --git a/src/NzbDrone.Core.Test/TvTests/SeriesRepositoryTests/SeriesRepositoryFixture.cs b/src/NzbDrone.Core.Test/TvTests/SeriesRepositoryTests/SeriesRepositoryFixture.cs deleted file mode 100644 index e0febe4bc..000000000 --- a/src/NzbDrone.Core.Test/TvTests/SeriesRepositoryTests/SeriesRepositoryFixture.cs +++ /dev/null @@ -1,40 +0,0 @@ -using FizzWare.NBuilder; -using FluentAssertions; -using NUnit.Framework; -using NzbDrone.Core.Profiles; -using NzbDrone.Core.Qualities; -using NzbDrone.Core.Test.Framework; -using NzbDrone.Core.Tv; - -namespace NzbDrone.Core.Test.TvTests.SeriesRepositoryTests -{ - [TestFixture] - - public class SeriesRepositoryFixture : DbTest - { - [Test] - public void should_lazyload_quality_profile() - { - var profile = new Profile - { - Items = Qualities.QualityFixture.GetDefaultQualities(Quality.MP3_320, Quality.MP3_256, Quality.MP3_192), - - Cutoff = Quality.MP3_320, - Name = "TestProfile" - }; - - - Mocker.Resolve().Insert(profile); - - var series = Builder.CreateNew().BuildNew(); - series.ProfileId = profile.Id; - - Subject.Insert(series); - - - StoredModel.Profile.Should().NotBeNull(); - - - } - } -} \ No newline at end of file diff --git a/src/NzbDrone.Core.Test/TvTests/SeriesServiceTests/AddSeriesFixture.cs b/src/NzbDrone.Core.Test/TvTests/SeriesServiceTests/AddSeriesFixture.cs deleted file mode 100644 index cdc1041e7..000000000 --- a/src/NzbDrone.Core.Test/TvTests/SeriesServiceTests/AddSeriesFixture.cs +++ /dev/null @@ -1,40 +0,0 @@ -using FizzWare.NBuilder; -using FluentAssertions; -using NUnit.Framework; -using NzbDrone.Core.Organizer; -using NzbDrone.Core.Tv; -using NzbDrone.Core.Test.Framework; -using NzbDrone.Core.Tv.Events; - -namespace NzbDrone.Core.Test.TvTests.SeriesServiceTests -{ - [TestFixture] - public class AddSeriesFixture : CoreTest - { - private Series fakeSeries; - - [SetUp] - public void Setup() - { - fakeSeries = Builder.CreateNew().Build(); - } - - [Test] - public void series_added_event_should_have_proper_path() - { - fakeSeries.Path = null; - fakeSeries.RootFolderPath = @"C:\Test\TV"; - - Mocker.GetMock() - .Setup(s => s.GetSeriesFolder(fakeSeries, null)) - .Returns(fakeSeries.Title); - - var series = Subject.AddSeries(fakeSeries); - - series.Path.Should().NotBeNull(); - - VerifyEventPublished(); - } - - } -} \ No newline at end of file diff --git a/src/NzbDrone.Core.Test/TvTests/SeriesServiceTests/UpdateMultipleSeriesFixture.cs b/src/NzbDrone.Core.Test/TvTests/SeriesServiceTests/UpdateMultipleSeriesFixture.cs deleted file mode 100644 index 0fa33a68f..000000000 --- a/src/NzbDrone.Core.Test/TvTests/SeriesServiceTests/UpdateMultipleSeriesFixture.cs +++ /dev/null @@ -1,73 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using FizzWare.NBuilder; -using FluentAssertions; -using Moq; -using NUnit.Framework; -using NzbDrone.Core.Tv; -using NzbDrone.Core.Test.Framework; -using NzbDrone.Test.Common; - -namespace NzbDrone.Core.Test.TvTests.SeriesServiceTests -{ - [TestFixture] - public class UpdateMultipleSeriesFixture : CoreTest - { - private List _series; - - [SetUp] - public void Setup() - { - _series = Builder.CreateListOfSize(5) - .All() - .With(s => s.ProfileId = 1) - .With(s => s.Monitored) - .With(s => s.SeasonFolder) - .With(s => s.Path = @"C:\Test\name".AsOsAgnostic()) - .With(s => s.RootFolderPath = "") - .Build().ToList(); - } - - [Test] - public void should_call_repo_updateMany() - { - Subject.UpdateSeries(_series); - - Mocker.GetMock().Verify(v => v.UpdateMany(_series), Times.Once()); - } - - [Test] - public void should_update_path_when_rootFolderPath_is_supplied() - { - var newRoot = @"C:\Test\TV2".AsOsAgnostic(); - _series.ForEach(s => s.RootFolderPath = newRoot); - - Subject.UpdateSeries(_series).ForEach(s => s.Path.Should().StartWith(newRoot)); - } - - [Test] - public void should_not_update_path_when_rootFolderPath_is_empty() - { - Subject.UpdateSeries(_series).ForEach(s => - { - var expectedPath = _series.Single(ser => ser.Id == s.Id).Path; - s.Path.Should().Be(expectedPath); - }); - } - - [Test] - public void should_be_able_to_update_many_series() - { - var series = Builder.CreateListOfSize(50) - .All() - .With(s => s.Path = (@"C:\Test\TV\" + s.Path).AsOsAgnostic()) - .Build() - .ToList(); - - var newRoot = @"C:\Test\TV2".AsOsAgnostic(); - series.ForEach(s => s.RootFolderPath = newRoot); - - Subject.UpdateSeries(series); - } - } -} \ No newline at end of file diff --git a/src/NzbDrone.Core.Test/TvTests/SeriesServiceTests/UpdateSeriesFixture.cs b/src/NzbDrone.Core.Test/TvTests/SeriesServiceTests/UpdateSeriesFixture.cs deleted file mode 100644 index 23f77223c..000000000 --- a/src/NzbDrone.Core.Test/TvTests/SeriesServiceTests/UpdateSeriesFixture.cs +++ /dev/null @@ -1,72 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using FizzWare.NBuilder; -using Moq; -using NUnit.Framework; -using NzbDrone.Core.Tv; -using NzbDrone.Core.Test.Framework; - -namespace NzbDrone.Core.Test.TvTests.SeriesServiceTests -{ - [TestFixture] - public class UpdateSeriesFixture : CoreTest - { - private Series _fakeSeries; - private Series _existingSeries; - - [SetUp] - public void Setup() - { - _fakeSeries = Builder.CreateNew().Build(); - _existingSeries = Builder.CreateNew().Build(); - - _fakeSeries.Seasons = new List - { - new Season{ SeasonNumber = 1, Monitored = true }, - new Season{ SeasonNumber = 2, Monitored = true } - }; - - _existingSeries.Seasons = new List - { - new Season{ SeasonNumber = 1, Monitored = true }, - new Season{ SeasonNumber = 2, Monitored = true } - }; - } - - private void GivenExistingSeries() - { - Mocker.GetMock() - .Setup(s => s.Get(It.IsAny())) - .Returns(_existingSeries); - } - - [Test] - public void should_not_update_episodes_if_season_hasnt_changed() - { - GivenExistingSeries(); - - Subject.UpdateSeries(_fakeSeries); - - Mocker.GetMock() - .Verify(v => v.SetEpisodeMonitoredBySeason(_fakeSeries.Id, It.IsAny(), It.IsAny()), Times.Never()); - } - - [Test] - public void should_update_series_when_it_changes() - { - GivenExistingSeries(); - var seasonNumber = 1; - var monitored = false; - - _fakeSeries.Seasons.Single(s => s.SeasonNumber == seasonNumber).Monitored = monitored; - - Subject.UpdateSeries(_fakeSeries); - - Mocker.GetMock() - .Verify(v => v.SetEpisodeMonitoredBySeason(_fakeSeries.Id, seasonNumber, monitored), Times.Once()); - - Mocker.GetMock() - .Verify(v => v.SetEpisodeMonitoredBySeason(_fakeSeries.Id, It.IsAny(), It.IsAny()), Times.Once()); - } - } -} \ No newline at end of file diff --git a/src/NzbDrone.Core.Test/TvTests/SeriesTitleNormalizerFixture.cs b/src/NzbDrone.Core.Test/TvTests/SeriesTitleNormalizerFixture.cs deleted file mode 100644 index 4355f77e0..000000000 --- a/src/NzbDrone.Core.Test/TvTests/SeriesTitleNormalizerFixture.cs +++ /dev/null @@ -1,29 +0,0 @@ -using FluentAssertions; -using NUnit.Framework; -using NzbDrone.Core.Tv; - -namespace NzbDrone.Core.Test.TvTests -{ - [TestFixture] - public class SeriesTitleNormalizerFixture - { - [TestCase("A to Z", 281588, "a to z")] - [TestCase("A. D. - The Trials & Triumph of the Early Church", 266757, "ad trials triumph early church")] - public void should_use_precomputed_title(string title, int tvdbId, string expected) - { - SeriesTitleNormalizer.Normalize(title, tvdbId).Should().Be(expected); - } - - [TestCase("2 Broke Girls", "2 broke girls")] - [TestCase("Archer (2009)", "archer 2009")] - [TestCase("The Office (US)", "office us")] - [TestCase("The Mentalist", "mentalist")] - [TestCase("The Good Wife", "good wife")] - [TestCase("The Newsroom (2012)", "newsroom 2012")] - [TestCase("Special Agent Oso", "special agent oso")] - public void should_normalize_title(string title, string expected) - { - SeriesTitleNormalizer.Normalize(title, 0).Should().Be(expected); - } - } -} diff --git a/src/NzbDrone.Core.Test/TvTests/ShouldRefreshSeriesFixture.cs b/src/NzbDrone.Core.Test/TvTests/ShouldRefreshSeriesFixture.cs deleted file mode 100644 index 6fb44c09a..000000000 --- a/src/NzbDrone.Core.Test/TvTests/ShouldRefreshSeriesFixture.cs +++ /dev/null @@ -1,135 +0,0 @@ -using System; -using System.Linq; -using FizzWare.NBuilder; -using FluentAssertions; -using NUnit.Framework; -using NzbDrone.Core.Tv; -using NzbDrone.Test.Common; - -namespace NzbDrone.Core.Test.TvTests -{ - [TestFixture] - public class ShouldRefreshSeriesFixture : TestBase - { - private Series _series; - - [SetUp] - public void Setup() - { - _series = Builder.CreateNew() - .With(v => v.Status == SeriesStatusType.Continuing) - .Build(); - - Mocker.GetMock() - .Setup(s => s.GetEpisodeBySeries(_series.Id)) - .Returns(Builder.CreateListOfSize(2) - .All() - .With(e => e.AirDateUtc = DateTime.Today.AddDays(-100)) - .Build() - .ToList()); - } - - private void GivenSeriesIsEnded() - { - _series.Status = SeriesStatusType.Ended; - } - - private void GivenSeriesLastRefreshedMonthsAgo() - { - _series.LastInfoSync = DateTime.UtcNow.AddDays(-90); - } - - private void GivenSeriesLastRefreshedYesterday() - { - _series.LastInfoSync = DateTime.UtcNow.AddDays(-1); - } - - private void GivenSeriesLastRefreshedHalfADayAgo() - { - _series.LastInfoSync = DateTime.UtcNow.AddHours(-12); - } - - private void GivenSeriesLastRefreshedRecently() - { - _series.LastInfoSync = DateTime.UtcNow.AddHours(-1); - } - - private void GivenRecentlyAired() - { - Mocker.GetMock() - .Setup(s => s.GetEpisodeBySeries(_series.Id)) - .Returns(Builder.CreateListOfSize(2) - .TheFirst(1) - .With(e => e.AirDateUtc = DateTime.Today.AddDays(-7)) - .TheLast(1) - .With(e => e.AirDateUtc = DateTime.Today.AddDays(-100)) - .Build() - .ToList()); - } - - [Test] - public void should_return_true_if_running_series_last_refreshed_more_than_6_hours_ago() - { - GivenSeriesLastRefreshedHalfADayAgo(); - - Subject.ShouldRefresh(_series).Should().BeTrue(); - } - - [Test] - public void should_return_false_if_running_series_last_refreshed_less_than_6_hours_ago() - { - GivenSeriesLastRefreshedRecently(); - - Subject.ShouldRefresh(_series).Should().BeFalse(); - } - - [Test] - public void should_return_false_if_ended_series_last_refreshed_yesterday() - { - GivenSeriesIsEnded(); - GivenSeriesLastRefreshedYesterday(); - - Subject.ShouldRefresh(_series).Should().BeFalse(); - } - - [Test] - public void should_return_true_if_series_last_refreshed_more_than_30_days_ago() - { - GivenSeriesIsEnded(); - GivenSeriesLastRefreshedMonthsAgo(); - - Subject.ShouldRefresh(_series).Should().BeTrue(); - } - - [Test] - public void should_return_true_if_episode_aired_in_last_30_days() - { - GivenSeriesIsEnded(); - GivenSeriesLastRefreshedYesterday(); - - GivenRecentlyAired(); - - Subject.ShouldRefresh(_series).Should().BeTrue(); - } - - [Test] - public void should_return_false_when_recently_refreshed_ended_show_has_not_aired_for_30_days() - { - GivenSeriesIsEnded(); - GivenSeriesLastRefreshedYesterday(); - - Subject.ShouldRefresh(_series).Should().BeFalse(); - } - - [Test] - public void should_return_false_when_recently_refreshed_ended_show_aired_in_last_30_days() - { - GivenSeriesIsEnded(); - GivenSeriesLastRefreshedRecently(); - - GivenRecentlyAired(); - - Subject.ShouldRefresh(_series).Should().BeFalse(); - } - } -} diff --git a/src/NzbDrone.Core.Test/packages.config b/src/NzbDrone.Core.Test/packages.config index f7d03d187..a6546fd94 100644 --- a/src/NzbDrone.Core.Test/packages.config +++ b/src/NzbDrone.Core.Test/packages.config @@ -1,18 +1,18 @@  - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/NzbDrone.Core/Annotations/FieldDefinitionAttribute.cs b/src/NzbDrone.Core/Annotations/FieldDefinitionAttribute.cs index 85b9b044c..32e77e266 100644 --- a/src/NzbDrone.Core/Annotations/FieldDefinitionAttribute.cs +++ b/src/NzbDrone.Core/Annotations/FieldDefinitionAttribute.cs @@ -1,4 +1,4 @@ -using System; +using System; namespace NzbDrone.Core.Annotations { @@ -31,6 +31,7 @@ namespace NzbDrone.Core.Annotations Tag, Action, Url, - Captcha + Captcha, + OAuth } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Core/App.config b/src/NzbDrone.Core/App.config index 043c42fe9..3129d272c 100644 --- a/src/NzbDrone.Core/App.config +++ b/src/NzbDrone.Core/App.config @@ -1,6 +1,6 @@  - + diff --git a/src/NzbDrone.Core/ArtistStats/AlbumStatistics.cs b/src/NzbDrone.Core/ArtistStats/AlbumStatistics.cs index ec291a43f..dfcb9f4d9 100644 --- a/src/NzbDrone.Core/ArtistStats/AlbumStatistics.cs +++ b/src/NzbDrone.Core/ArtistStats/AlbumStatistics.cs @@ -1,4 +1,4 @@ -using System; +using System; using NzbDrone.Core.Datastore; namespace NzbDrone.Core.ArtistStats @@ -9,6 +9,7 @@ namespace NzbDrone.Core.ArtistStats public int AlbumId { get; set; } public int TrackFileCount { get; set; } public int TrackCount { get; set; } + public int AvailableTrackCount { get; set; } public int TotalTrackCount { get; set; } public long SizeOnDisk { get; set; } } diff --git a/src/NzbDrone.Core/ArtistStats/ArtistStatisticsRepository.cs b/src/NzbDrone.Core/ArtistStats/ArtistStatisticsRepository.cs index c67af6921..298ee3bdc 100644 --- a/src/NzbDrone.Core/ArtistStats/ArtistStatisticsRepository.cs +++ b/src/NzbDrone.Core/ArtistStats/ArtistStatisticsRepository.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Text; using NzbDrone.Core.Datastore; @@ -58,7 +58,8 @@ namespace NzbDrone.Core.ArtistStats (SELECT Tracks.ArtistId, Tracks.AlbumId, - SUM(CASE WHEN TrackFileId > 0 THEN 1 ELSE 0 END) AS TotalTrackCount, + COUNT(*) AS TotalTrackCount, + SUM(CASE WHEN TrackFileId > 0 THEN 1 ELSE 0 END) AS AvailableTrackCount, SUM(CASE WHEN Monitored = 1 OR TrackFileId > 0 THEN 1 ELSE 0 END) AS TrackCount, SUM(CASE WHEN TrackFileId > 0 THEN 1 ELSE 0 END) AS TrackFileCount FROM Tracks diff --git a/src/NzbDrone.Core/Backup/Backup.cs b/src/NzbDrone.Core/Backup/Backup.cs index a4505d991..4dafd4394 100644 --- a/src/NzbDrone.Core/Backup/Backup.cs +++ b/src/NzbDrone.Core/Backup/Backup.cs @@ -1,10 +1,10 @@ -using System; +using System; namespace NzbDrone.Core.Backup { public class Backup { - public string Path { get; set; } + public string Name { get; set; } public BackupType Type { get; set; } public DateTime Time { get; set; } } diff --git a/src/NzbDrone.Core/Backup/BackupCommand.cs b/src/NzbDrone.Core/Backup/BackupCommand.cs index 3a852cf7a..fd6086830 100644 --- a/src/NzbDrone.Core/Backup/BackupCommand.cs +++ b/src/NzbDrone.Core/Backup/BackupCommand.cs @@ -1,10 +1,21 @@ -using NzbDrone.Core.Messaging.Commands; +using NzbDrone.Core.Messaging.Commands; namespace NzbDrone.Core.Backup { public class BackupCommand : Command { - public BackupType Type { get; set; } + public BackupType Type + { + get + { + if (Trigger == CommandTrigger.Scheduled) + { + return BackupType.Scheduled; + } + + return BackupType.Manual; + } + } public override bool SendUpdatesToClient => true; diff --git a/src/NzbDrone.Core/Backup/BackupService.cs b/src/NzbDrone.Core/Backup/BackupService.cs index 8cc89d87b..8728e7dc1 100644 --- a/src/NzbDrone.Core/Backup/BackupService.cs +++ b/src/NzbDrone.Core/Backup/BackupService.cs @@ -1,6 +1,7 @@ -using System; +using System; using System.Collections.Generic; using System.Data; +using System.Data.SQLite; using System.IO; using System.Linq; using System.Text.RegularExpressions; @@ -25,6 +26,7 @@ namespace NzbDrone.Core.Backup public class BackupService : IBackupService, IExecute { private readonly IMainDatabase _maindDb; + private readonly IMakeDatabaseBackup _makeDatabaseBackup; private readonly IDiskTransferService _diskTransferService; private readonly IDiskProvider _diskProvider; private readonly IAppFolderInfo _appFolderInfo; @@ -36,6 +38,7 @@ namespace NzbDrone.Core.Backup private static readonly Regex BackupFileRegex = new Regex(@"nzbdrone_backup_[._0-9]+\.zip", RegexOptions.Compiled | RegexOptions.IgnoreCase); public BackupService(IMainDatabase maindDb, + IMakeDatabaseBackup makeDatabaseBackup, IDiskTransferService diskTransferService, IDiskProvider diskProvider, IAppFolderInfo appFolderInfo, @@ -43,6 +46,7 @@ namespace NzbDrone.Core.Backup Logger logger) { _maindDb = maindDb; + _makeDatabaseBackup = makeDatabaseBackup; _diskTransferService = diskTransferService; _diskProvider = diskProvider; _appFolderInfo = appFolderInfo; @@ -89,7 +93,7 @@ namespace NzbDrone.Core.Backup { backups.AddRange(GetBackupFiles(folder).Select(b => new Backup { - Path = Path.GetFileName(b), + Name = Path.GetFileName(b), Type = backupType, Time = _diskProvider.FileGetLastWrite(b) })); @@ -111,17 +115,7 @@ namespace NzbDrone.Core.Backup { _logger.ProgressDebug("Backing up database"); - using (var unitOfWork = new UnitOfWork(() => _maindDb.GetDataMapper())) - { - unitOfWork.BeginTransaction(IsolationLevel.Serializable); - - var databaseFile = _appFolderInfo.GetNzbDroneDatabase(); - var tempDatabaseFile = Path.Combine(_backupTempFolder, Path.GetFileName(databaseFile)); - - _diskTransferService.TransferFile(databaseFile, tempDatabaseFile, TransferMode.Copy); - - unitOfWork.Commit(); - } + _makeDatabaseBackup.BackupDatabase(_maindDb, _backupTempFolder); } private void BackupConfigFile() diff --git a/src/NzbDrone.Core/Backup/MakeDatabaseBackup.cs b/src/NzbDrone.Core/Backup/MakeDatabaseBackup.cs new file mode 100644 index 000000000..6460daab1 --- /dev/null +++ b/src/NzbDrone.Core/Backup/MakeDatabaseBackup.cs @@ -0,0 +1,59 @@ +using System; +using System.Collections.Generic; +using System.Data.SQLite; +using System.IO; +using System.Linq; +using System.Text; +using NLog; +using NzbDrone.Core.Datastore; +using System.Data; + +namespace NzbDrone.Core.Backup +{ + public interface IMakeDatabaseBackup + { + void BackupDatabase(IDatabase database, string targetDirectory); + } + + public class MakeDatabaseBackup : IMakeDatabaseBackup + { + private readonly Logger _logger; + + public MakeDatabaseBackup(Logger logger) + { + _logger = logger; + } + + public void BackupDatabase(IDatabase database, string targetDirectory) + { + var sourceConnectionString = database.GetDataMapper().ConnectionString; + var backupConnectionStringBuilder = new SQLiteConnectionStringBuilder(sourceConnectionString); + + backupConnectionStringBuilder.DataSource = Path.Combine(targetDirectory, Path.GetFileName(backupConnectionStringBuilder.DataSource)); + // We MUST use journal mode instead of WAL coz WAL has issues when page sizes change. This should also automatically deal with the -journal and -wal files during restore. + backupConnectionStringBuilder.JournalMode = SQLiteJournalModeEnum.Truncate; + + using (var sourceConnection = (SQLiteConnection)SQLiteFactory.Instance.CreateConnection()) + using (var backupConnection = (SQLiteConnection)SQLiteFactory.Instance.CreateConnection()) + { + sourceConnection.ConnectionString = sourceConnectionString; + backupConnection.ConnectionString = backupConnectionStringBuilder.ToString(); + + sourceConnection.Open(); + backupConnection.Open(); + + sourceConnection.BackupDatabase(backupConnection, "main", "main", -1, null, 500); + + // The backup changes the journal_mode, force it to truncate again. + using (var command = backupConnection.CreateCommand()) + { + command.CommandText = "pragma journal_mode=truncate"; + command.ExecuteNonQuery(); + } + + // Make sure there are no lingering connections. + SQLiteConnection.ClearAllPools(); + } + } + } +} diff --git a/src/NzbDrone.Core/Blacklisting/Blacklist.cs b/src/NzbDrone.Core/Blacklisting/Blacklist.cs index 44fa2e74c..b635a2f67 100644 --- a/src/NzbDrone.Core/Blacklisting/Blacklist.cs +++ b/src/NzbDrone.Core/Blacklisting/Blacklist.cs @@ -1,9 +1,10 @@ -using System; +using System; using System.Collections.Generic; using NzbDrone.Core.Datastore; using NzbDrone.Core.Indexers; using NzbDrone.Core.Qualities; using NzbDrone.Core.Music; +using NzbDrone.Core.Languages; namespace NzbDrone.Core.Blacklisting { @@ -21,5 +22,6 @@ namespace NzbDrone.Core.Blacklisting public string Indexer { get; set; } public string Message { get; set; } public string TorrentInfoHash { get; set; } + public Language Language { get; set; } } } diff --git a/src/NzbDrone.Core/Blacklisting/BlacklistService.cs b/src/NzbDrone.Core/Blacklisting/BlacklistService.cs index 74efb3b1f..30e86d510 100644 --- a/src/NzbDrone.Core/Blacklisting/BlacklistService.cs +++ b/src/NzbDrone.Core/Blacklisting/BlacklistService.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Linq; using NzbDrone.Common.Extensions; using NzbDrone.Core.Datastore; @@ -138,8 +138,9 @@ namespace NzbDrone.Core.Blacklisting Indexer = message.Data.GetValueOrDefault("indexer"), Protocol = (DownloadProtocol)Convert.ToInt32(message.Data.GetValueOrDefault("protocol")), Message = message.Message, - TorrentInfoHash = message.Data.GetValueOrDefault("torrentInfoHash") - }; + TorrentInfoHash = message.Data.GetValueOrDefault("torrentInfoHash"), + Language = message.Language + }; _blacklistRepository.Insert(blacklist); } diff --git a/src/NzbDrone.Core/Configuration/ConfigService.cs b/src/NzbDrone.Core/Configuration/ConfigService.cs index 85d4c4215..83817a5d2 100644 --- a/src/NzbDrone.Core/Configuration/ConfigService.cs +++ b/src/NzbDrone.Core/Configuration/ConfigService.cs @@ -74,13 +74,6 @@ namespace NzbDrone.Core.Configuration return _repository.Get(key.ToLower()) != null; } - public string DownloadedAlbumsFolder - { - get { return GetValue(ConfigKey.DownloadedAlbumsFolder.ToString()); } - - set { SetValue(ConfigKey.DownloadedAlbumsFolder.ToString(), value); } - } - public bool AutoUnmonitorPreviouslyDownloadedTracks { get { return GetValueBoolean("AutoUnmonitorPreviouslyDownloadedTracks"); } @@ -168,13 +161,6 @@ namespace NzbDrone.Core.Configuration set { SetValue("DownloadClientWorkingFolders", value); } } - public int DownloadedAlbumsScanInterval - { - get { return GetValueInt("DownloadedAlbumsScanInterval", 1); } - - set { SetValue("DownloadedAlbumsScanInterval", value); } - } - public int DownloadClientHistoryLimit { get { return GetValueInt("DownloadClientHistoryLimit", 30); } @@ -252,6 +238,13 @@ namespace NzbDrone.Core.Configuration set { SetValue("ChownGroup", value); } } + public string MetadataSource + { + get { return GetValue("MetadataSource", ""); } + + set { SetValue("MetadataSource", value); } + } + public int FirstDayOfWeek { get { return GetValueInt("FirstDayOfWeek", (int)CultureInfo.CurrentCulture.DateTimeFormat.FirstDayOfWeek); } diff --git a/src/NzbDrone.Core/Configuration/IConfigService.cs b/src/NzbDrone.Core/Configuration/IConfigService.cs index 6ceab7c72..ff39ae96f 100644 --- a/src/NzbDrone.Core/Configuration/IConfigService.cs +++ b/src/NzbDrone.Core/Configuration/IConfigService.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using NzbDrone.Core.MediaFiles; using NzbDrone.Common.Http.Proxy; @@ -11,9 +11,7 @@ namespace NzbDrone.Core.Configuration bool IsDefined(string key); //Download Client - string DownloadedAlbumsFolder { get; set; } string DownloadClientWorkingFolders { get; set; } - int DownloadedAlbumsScanInterval { get; set; } int DownloadClientHistoryLimit { get; set; } //Completed/Failed Download Handling (Download client) @@ -60,6 +58,9 @@ namespace NzbDrone.Core.Configuration //Internal bool CleanupMetadataImages { get; set; } + //MetadataSource + string MetadataSource { get; set; } + //Forms Auth string RijndaelPassphrase { get; } diff --git a/src/NzbDrone.Core/DataAugmentation/Scene/ISceneMappingProvider.cs b/src/NzbDrone.Core/DataAugmentation/Scene/ISceneMappingProvider.cs deleted file mode 100644 index 58b69f2b9..000000000 --- a/src/NzbDrone.Core/DataAugmentation/Scene/ISceneMappingProvider.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System.Collections.Generic; - -namespace NzbDrone.Core.DataAugmentation.Scene -{ - public interface ISceneMappingProvider - { - List GetSceneMappings(); - } -} diff --git a/src/NzbDrone.Core/DataAugmentation/Scene/SceneMapping.cs b/src/NzbDrone.Core/DataAugmentation/Scene/SceneMapping.cs deleted file mode 100644 index b992aa029..000000000 --- a/src/NzbDrone.Core/DataAugmentation/Scene/SceneMapping.cs +++ /dev/null @@ -1,22 +0,0 @@ -using Newtonsoft.Json; -using NzbDrone.Core.Datastore; - -namespace NzbDrone.Core.DataAugmentation.Scene -{ - public class SceneMapping : ModelBase - { - public string Title { get; set; } - public string ParseTerm { get; set; } - - [JsonProperty("searchTitle")] - public string SearchTerm { get; set; } - - public int TvdbId { get; set; } - - [JsonProperty("season")] - public int? SeasonNumber { get; set; } - - public int? SceneSeasonNumber { get; set; } - public string Type { get; set; } - } -} \ No newline at end of file diff --git a/src/NzbDrone.Core/DataAugmentation/Scene/SceneMappingProxy.cs b/src/NzbDrone.Core/DataAugmentation/Scene/SceneMappingProxy.cs deleted file mode 100644 index 4c4d15b76..000000000 --- a/src/NzbDrone.Core/DataAugmentation/Scene/SceneMappingProxy.cs +++ /dev/null @@ -1,32 +0,0 @@ -using System.Collections.Generic; -using NzbDrone.Common.Cloud; -using NzbDrone.Common.Http; - -namespace NzbDrone.Core.DataAugmentation.Scene -{ - public interface ISceneMappingProxy - { - List Fetch(); - } - - public class SceneMappingProxy : ISceneMappingProxy - { - private readonly IHttpClient _httpClient; - private readonly IHttpRequestBuilderFactory _requestBuilder; - - public SceneMappingProxy(IHttpClient httpClient, ILidarrCloudRequestBuilder requestBuilder) - { - _httpClient = httpClient; - _requestBuilder = requestBuilder.Services; - } - - public List Fetch() - { - var request = _requestBuilder.Create() - .Resource("/scenemapping") - .Build(); - - return _httpClient.Get>(request).Resource; - } - } -} \ No newline at end of file diff --git a/src/NzbDrone.Core/DataAugmentation/Scene/SceneMappingRepository.cs b/src/NzbDrone.Core/DataAugmentation/Scene/SceneMappingRepository.cs deleted file mode 100644 index ce86916ec..000000000 --- a/src/NzbDrone.Core/DataAugmentation/Scene/SceneMappingRepository.cs +++ /dev/null @@ -1,31 +0,0 @@ -using NzbDrone.Core.Datastore; -using NzbDrone.Core.Messaging.Events; -using System.Collections.Generic; - - -namespace NzbDrone.Core.DataAugmentation.Scene -{ - public interface ISceneMappingRepository : IBasicRepository - { - List FindByTvdbid(int tvdbId); - void Clear(string type); - } - - public class SceneMappingRepository : BasicRepository, ISceneMappingRepository - { - public SceneMappingRepository(IMainDatabase database, IEventAggregator eventAggregator) - : base(database, eventAggregator) - { - } - - public List FindByTvdbid(int tvdbId) - { - return Query.Where(x => x.TvdbId == tvdbId); - } - - public void Clear(string type) - { - Delete(s => s.Type == type); - } - } -} \ No newline at end of file diff --git a/src/NzbDrone.Core/DataAugmentation/Scene/SceneMappingService.cs b/src/NzbDrone.Core/DataAugmentation/Scene/SceneMappingService.cs deleted file mode 100644 index 1afaf456a..000000000 --- a/src/NzbDrone.Core/DataAugmentation/Scene/SceneMappingService.cs +++ /dev/null @@ -1,253 +0,0 @@ -using System; -using System.Linq; -using NLog; -using NzbDrone.Common.Cache; -using NzbDrone.Common.Extensions; -using NzbDrone.Core.Messaging.Commands; -using NzbDrone.Core.Messaging.Events; -using NzbDrone.Core.Parser; -using System.Collections.Generic; -using NzbDrone.Core.Tv.Events; - -namespace NzbDrone.Core.DataAugmentation.Scene -{ - public interface ISceneMappingService - { - List GetSceneNames(int tvdbId, List seasonNumbers, List sceneSeasonNumbers); - int? FindTvdbId(string title); - List FindByTvdbId(int tvdbId); - SceneMapping FindSceneMapping(string title); - int? GetSceneSeasonNumber(string title); - int? GetTvdbSeasonNumber(string title); - int? GetSceneSeasonNumber(int tvdbId, int seasonNumber); - } - - public class SceneMappingService : ISceneMappingService, - IHandle, - IExecute - { - private readonly ISceneMappingRepository _repository; - private readonly IEnumerable _sceneMappingProviders; - private readonly IEventAggregator _eventAggregator; - private readonly Logger _logger; - private readonly ICachedDictionary> _getTvdbIdCache; - private readonly ICachedDictionary> _findByTvdbIdCache; - - public SceneMappingService(ISceneMappingRepository repository, - ICacheManager cacheManager, - IEnumerable sceneMappingProviders, - IEventAggregator eventAggregator, - Logger logger) - { - _repository = repository; - _sceneMappingProviders = sceneMappingProviders; - _eventAggregator = eventAggregator; - _logger = logger; - - _getTvdbIdCache = cacheManager.GetCacheDictionary>(GetType(), "tvdb_id"); - _findByTvdbIdCache = cacheManager.GetCacheDictionary>(GetType(), "find_tvdb_id"); - } - - public List GetSceneNames(int tvdbId, List seasonNumbers, List sceneSeasonNumbers) - { - var mappings = FindByTvdbId(tvdbId); - - if (mappings == null) - { - return new List(); - } - - var names = mappings.Where(n => n.SeasonNumber.HasValue && seasonNumbers.Contains(n.SeasonNumber.Value) || - n.SceneSeasonNumber.HasValue && sceneSeasonNumbers.Contains(n.SceneSeasonNumber.Value) || - (n.SeasonNumber ?? -1) == -1 && (n.SceneSeasonNumber ?? -1) == -1) - .Select(n => n.SearchTerm).Distinct().ToList(); - - return FilterNonEnglish(names); - } - - public int? FindTvdbId(string title) - { - var mapping = FindMapping(title); - - if (mapping == null) - return null; - - return mapping.TvdbId; - } - - public List FindByTvdbId(int tvdbId) - { - if (_findByTvdbIdCache.Count == 0) - { - RefreshCache(); - } - - var mappings = _findByTvdbIdCache.Find(tvdbId.ToString()); - - if (mappings == null) - { - return new List(); - } - - return mappings; - } - - public SceneMapping FindSceneMapping(string title) - { - return FindMapping(title); - } - - public int? GetSceneSeasonNumber(string title) - { - var mapping = FindMapping(title); - - if (mapping == null) - { - return null; - } - - return mapping.SceneSeasonNumber; - } - - public int? GetTvdbSeasonNumber(string title) - { - var mapping = FindMapping(title); - - if (mapping == null) - { - return null; - } - - return mapping.SeasonNumber; - } - - public int? GetSceneSeasonNumber(int tvdbId, int seasonNumber) - { - var mappings = FindByTvdbId(tvdbId); - - if (mappings == null) - { - return null; - } - - var mapping = mappings.FirstOrDefault(e => e.SeasonNumber == seasonNumber && e.SceneSeasonNumber.HasValue); - - if (mapping == null) - { - return null; - } - - return mapping.SceneSeasonNumber; - } - - private void UpdateMappings() - { - _logger.Info("Updating Scene mappings"); - - foreach (var sceneMappingProvider in _sceneMappingProviders) - { - try - { - var mappings = sceneMappingProvider.GetSceneMappings(); - - if (mappings.Any()) - { - _repository.Clear(sceneMappingProvider.GetType().Name); - - mappings.RemoveAll(sceneMapping => - { - if (sceneMapping.Title.IsNullOrWhiteSpace() || - sceneMapping.SearchTerm.IsNullOrWhiteSpace()) - { - _logger.Warn("Invalid scene mapping found for: {0}, skipping", sceneMapping.TvdbId); - return true; - } - - return false; - }); - - foreach (var sceneMapping in mappings) - { - sceneMapping.ParseTerm = sceneMapping.Title.CleanSeriesTitle(); - sceneMapping.Type = sceneMappingProvider.GetType().Name; - } - - _repository.InsertMany(mappings.ToList()); - } - else - { - _logger.Warn("Received empty list of mapping. will not update."); - } - } - catch (Exception ex) - { - _logger.Error(ex, "Failed to Update Scene Mappings."); - } - } - - RefreshCache(); - - _eventAggregator.PublishEvent(new SceneMappingsUpdatedEvent()); - } - - private SceneMapping FindMapping(string title) - { - if (_getTvdbIdCache.Count == 0) - { - RefreshCache(); - } - - var candidates = _getTvdbIdCache.Find(title.CleanSeriesTitle()); - - if (candidates == null) - { - return null; - } - - if (candidates.Count == 1) - { - return candidates.First(); - } - - var exactMatch = candidates.OrderByDescending(v => v.SeasonNumber) - .FirstOrDefault(v => v.Title == title); - - if (exactMatch != null) - { - return exactMatch; - } - - var closestMatch = candidates.OrderBy(v => title.LevenshteinDistance(v.Title, 10, 1, 10)) - .ThenByDescending(v => v.SeasonNumber) - .First(); - - return closestMatch; - } - - private void RefreshCache() - { - var mappings = _repository.All().ToList(); - - _getTvdbIdCache.Update(mappings.GroupBy(v => v.ParseTerm).ToDictionary(v => v.Key, v => v.ToList())); - _findByTvdbIdCache.Update(mappings.GroupBy(v => v.TvdbId).ToDictionary(v => v.Key.ToString(), v => v.ToList())); - } - - private List FilterNonEnglish(List titles) - { - return titles.Where(title => title.All(c => c <= 255)).ToList(); - } - - public void Handle(SeriesRefreshStartingEvent message) - { - if (message.ManualTrigger && _findByTvdbIdCache.IsExpired(TimeSpan.FromMinutes(1))) - { - UpdateMappings(); - } - } - - public void Execute(UpdateSceneMappingCommand message) - { - UpdateMappings(); - } - } -} diff --git a/src/NzbDrone.Core/DataAugmentation/Scene/SceneMappingsUpdatedEvent.cs b/src/NzbDrone.Core/DataAugmentation/Scene/SceneMappingsUpdatedEvent.cs deleted file mode 100644 index 06f6d4a3f..000000000 --- a/src/NzbDrone.Core/DataAugmentation/Scene/SceneMappingsUpdatedEvent.cs +++ /dev/null @@ -1,8 +0,0 @@ -using NzbDrone.Common.Messaging; - -namespace NzbDrone.Core.DataAugmentation.Scene -{ - public class SceneMappingsUpdatedEvent : IEvent - { - } -} diff --git a/src/NzbDrone.Core/DataAugmentation/Scene/ServicesProvider.cs b/src/NzbDrone.Core/DataAugmentation/Scene/ServicesProvider.cs deleted file mode 100644 index 605488cf9..000000000 --- a/src/NzbDrone.Core/DataAugmentation/Scene/ServicesProvider.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System.Collections.Generic; - -namespace NzbDrone.Core.DataAugmentation.Scene -{ - public class ServicesProvider : ISceneMappingProvider - { - private readonly ISceneMappingProxy _sceneMappingProxy; - - public ServicesProvider(ISceneMappingProxy sceneMappingProxy) - { - _sceneMappingProxy = sceneMappingProxy; - } - - public List GetSceneMappings() - { - return _sceneMappingProxy.Fetch(); - } - } -} diff --git a/src/NzbDrone.Core/DataAugmentation/Scene/UpdateSceneMappingCommand.cs b/src/NzbDrone.Core/DataAugmentation/Scene/UpdateSceneMappingCommand.cs deleted file mode 100644 index 215f8e033..000000000 --- a/src/NzbDrone.Core/DataAugmentation/Scene/UpdateSceneMappingCommand.cs +++ /dev/null @@ -1,9 +0,0 @@ -using NzbDrone.Core.Messaging.Commands; - -namespace NzbDrone.Core.DataAugmentation.Scene -{ - public class UpdateSceneMappingCommand : Command - { - - } -} \ No newline at end of file diff --git a/src/NzbDrone.Core/DataAugmentation/Xem/Model/XemResult.cs b/src/NzbDrone.Core/DataAugmentation/Xem/Model/XemResult.cs deleted file mode 100644 index 2b041709d..000000000 --- a/src/NzbDrone.Core/DataAugmentation/Xem/Model/XemResult.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace NzbDrone.Core.DataAugmentation.Xem.Model -{ - public class XemResult - { - public string Result { get; set; } - public T Data { get; set; } - public string Message { get; set; } - } -} diff --git a/src/NzbDrone.Core/DataAugmentation/Xem/Model/XemSceneTvdbMapping.cs b/src/NzbDrone.Core/DataAugmentation/Xem/Model/XemSceneTvdbMapping.cs deleted file mode 100644 index 1cc65524a..000000000 --- a/src/NzbDrone.Core/DataAugmentation/Xem/Model/XemSceneTvdbMapping.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace NzbDrone.Core.DataAugmentation.Xem.Model -{ - public class XemSceneTvdbMapping - { - public XemValues Scene { get; set; } - public XemValues Tvdb { get; set; } - } -} diff --git a/src/NzbDrone.Core/DataAugmentation/Xem/Model/XemValues.cs b/src/NzbDrone.Core/DataAugmentation/Xem/Model/XemValues.cs deleted file mode 100644 index ab6764e18..000000000 --- a/src/NzbDrone.Core/DataAugmentation/Xem/Model/XemValues.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace NzbDrone.Core.DataAugmentation.Xem.Model -{ - public class XemValues - { - public int Season { get; set; } - public int Episode { get; set; } - public int Absolute { get; set; } - } -} diff --git a/src/NzbDrone.Core/DataAugmentation/Xem/XemProxy.cs b/src/NzbDrone.Core/DataAugmentation/Xem/XemProxy.cs deleted file mode 100644 index 384ff4d94..000000000 --- a/src/NzbDrone.Core/DataAugmentation/Xem/XemProxy.cs +++ /dev/null @@ -1,127 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Newtonsoft.Json.Linq; -using NLog; -using NzbDrone.Common.Http; -using NzbDrone.Core.DataAugmentation.Scene; -using NzbDrone.Core.DataAugmentation.Xem.Model; - -namespace NzbDrone.Core.DataAugmentation.Xem -{ - public interface IXemProxy - { - List GetXemSeriesIds(); - List GetSceneTvdbMappings(int id); - List GetSceneTvdbNames(); - } - - public class XemProxy : IXemProxy - { - private const string ROOT_URL = "http://thexem.de.broken/map/"; - - private readonly Logger _logger; - private readonly IHttpClient _httpClient; - private readonly IHttpRequestBuilderFactory _xemRequestBuilder; - - private static readonly string[] IgnoredErrors = { "no single connection", "no show with the tvdb_id" }; - - public XemProxy(IHttpClient httpClient, Logger logger) - { - _httpClient = httpClient; - _logger = logger; - - _xemRequestBuilder = new HttpRequestBuilder(ROOT_URL) - .AddSuffixQueryParam("origin", "tvdb") - .CreateFactory(); - } - - public List GetXemSeriesIds() - { - _logger.Debug("Fetching Series IDs from"); - - var request = _xemRequestBuilder.Create() - .Resource("/havemap") - .Build(); - - var response = _httpClient.Get>>(request).Resource; - CheckForFailureResult(response); - - return response.Data.Select(d => - { - int tvdbId = 0; - int.TryParse(d, out tvdbId); - - return tvdbId; - }).Where(t => t > 0).ToList(); - } - - public List GetSceneTvdbMappings(int id) - { - _logger.Debug("Fetching Mappings for: {0}", id); - - var request = _xemRequestBuilder.Create() - .Resource("/all") - .AddQueryParam("id", id) - .Build(); - - var response = _httpClient.Get>>(request).Resource; - - return response.Data.Where(c => c.Scene != null).ToList(); - } - - public List GetSceneTvdbNames() - { - _logger.Debug("Fetching alternate names"); - - var request = _xemRequestBuilder.Create() - .Resource("/allNames") - .AddQueryParam("seasonNumbers", true) - .Build(); - - var response = _httpClient.Get>>>(request).Resource; - - var result = new List(); - - foreach (var series in response.Data) - { - foreach (var name in series.Value) - { - foreach (var n in name) - { - int seasonNumber; - if (!int.TryParse(n.Value.ToString(), out seasonNumber)) - { - continue; - } - - //hack to deal with Fate/Zero - if (series.Key == 79151 && seasonNumber > 1) - { - continue; - } - - result.Add(new SceneMapping - { - Title = n.Key, - SearchTerm = n.Key, - SceneSeasonNumber = seasonNumber, - TvdbId = series.Key - }); - } - } - } - - return result; - } - - private static void CheckForFailureResult(XemResult response) - { - if (response.Result.Equals("failure", StringComparison.InvariantCultureIgnoreCase) && - !IgnoredErrors.Any(knowError => response.Message.Contains(knowError))) - { - throw new Exception("Error response received from Xem: " + response.Message); - } - } - } -} diff --git a/src/NzbDrone.Core/DataAugmentation/Xem/XemService.cs b/src/NzbDrone.Core/DataAugmentation/Xem/XemService.cs deleted file mode 100644 index 5e06431c4..000000000 --- a/src/NzbDrone.Core/DataAugmentation/Xem/XemService.cs +++ /dev/null @@ -1,243 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using NLog; -using NzbDrone.Common.Cache; -using NzbDrone.Core.DataAugmentation.Scene; -using NzbDrone.Core.Messaging.Events; -using NzbDrone.Core.Tv; -using NzbDrone.Core.Tv.Events; - -namespace NzbDrone.Core.DataAugmentation.Xem -{ - public class XemService : ISceneMappingProvider, IHandle, IHandle - { - private readonly IEpisodeService _episodeService; - private readonly IXemProxy _xemProxy; - private readonly ISeriesService _seriesService; - private readonly Logger _logger; - private readonly ICachedDictionary _cache; - - public XemService(IEpisodeService episodeService, - IXemProxy xemProxy, - ISeriesService seriesService, ICacheManager cacheManager, Logger logger) - { - _episodeService = episodeService; - _xemProxy = xemProxy; - _seriesService = seriesService; - _logger = logger; - _cache = cacheManager.GetCacheDictionary(GetType(), "mappedTvdbid"); - } - - private void PerformUpdate(Series series) - { - _logger.Debug("Updating scene numbering mapping for: {0}", series); - - //try - //{ - // var mappings = _xemProxy.GetSceneTvdbMappings(series.TvdbId); - - // if (!mappings.Any() && !series.UseSceneNumbering) - // { - // _logger.Debug("Mappings for: {0} are empty, skipping", series); - // return; - // } - - // var episodes = _episodeService.GetEpisodeBySeries(series.Id); - - // foreach (var episode in episodes) - // { - // episode.SceneAbsoluteEpisodeNumber = null; - // episode.SceneSeasonNumber = null; - // episode.SceneEpisodeNumber = null; - // episode.UnverifiedSceneNumbering = false; - // } - - // foreach (var mapping in mappings) - // { - // _logger.Debug("Setting scene numbering mappings for {0} S{1:00}E{2:00}", series, mapping.Tvdb.Season, mapping.Tvdb.Episode); - - // var episode = episodes.SingleOrDefault(e => e.SeasonNumber == mapping.Tvdb.Season && e.EpisodeNumber == mapping.Tvdb.Episode); - - // if (episode == null) - // { - // _logger.Debug("Information hasn't been added to TheTVDB yet, skipping."); - // continue; - // } - - // episode.SceneAbsoluteEpisodeNumber = mapping.Scene.Absolute; - // episode.SceneSeasonNumber = mapping.Scene.Season; - // episode.SceneEpisodeNumber = mapping.Scene.Episode; - // } - - // if (episodes.Any(v => v.SceneEpisodeNumber.HasValue && v.SceneSeasonNumber != 0)) - // { - // ExtrapolateMappings(series, episodes, mappings); - // } - - // _episodeService.UpdateEpisodes(episodes); - // series.UseSceneNumbering = mappings.Any(); - // _seriesService.UpdateSeries(series); - - // _logger.Debug("XEM mapping updated for {0}", series); - //} - //catch (Exception ex) - //{ - // _logger.Error(ex, "Error updating scene numbering mappings for {0}", series); - //} - } - - private void ExtrapolateMappings(Series series, List episodes, List mappings) - { - var mappedEpisodes = episodes.Where(v => v.SeasonNumber != 0 && v.SceneEpisodeNumber.HasValue).ToList(); - var mappedSeasons = new HashSet(mappedEpisodes.Select(v => v.SeasonNumber).Distinct()); - - var sceneEpisodeMappings = mappings.ToLookup(v => v.Scene.Season) - .ToDictionary(v => v.Key, e => new HashSet(e.Select(v => v.Scene.Episode))); - - var firstTvdbEpisodeBySeason = mappings.ToLookup(v => v.Tvdb.Season) - .ToDictionary(v => v.Key, e => e.Min(v => v.Tvdb.Episode)); - - var lastSceneSeason = mappings.Select(v => v.Scene.Season).Max(); - var lastTvdbSeason = mappings.Select(v => v.Tvdb.Season).Max(); - - // Mark all episodes not on the xem as unverified. - foreach (var episode in episodes) - { - if (episode.SeasonNumber == 0) continue; - if (episode.SceneEpisodeNumber.HasValue) continue; - - if (mappedSeasons.Contains(episode.SeasonNumber)) - { - // Mark if a mapping exists for an earlier episode in this season. - if (firstTvdbEpisodeBySeason[episode.SeasonNumber] <= episode.EpisodeNumber) - { - episode.UnverifiedSceneNumbering = true; - continue; - } - - // Mark if a mapping exists with a scene number to this episode. - if (sceneEpisodeMappings.ContainsKey(episode.SeasonNumber) && - sceneEpisodeMappings[episode.SeasonNumber].Contains(episode.EpisodeNumber)) - { - episode.UnverifiedSceneNumbering = true; - continue; - } - } - else if (lastSceneSeason != lastTvdbSeason && episode.SeasonNumber > lastTvdbSeason) - { - episode.UnverifiedSceneNumbering = true; - } - } - - foreach (var episode in episodes) - { - if (episode.SeasonNumber == 0) continue; - if (episode.SceneEpisodeNumber.HasValue) continue; - if (episode.SeasonNumber < lastTvdbSeason) continue; - if (!episode.UnverifiedSceneNumbering) continue; - - var seasonMappings = mappings.Where(v => v.Tvdb.Season == episode.SeasonNumber).ToList(); - if (seasonMappings.Any(v => v.Tvdb.Episode >= episode.EpisodeNumber)) - { - continue; - } - - if (seasonMappings.Any()) - { - var lastEpisodeMapping = seasonMappings.OrderBy(v => v.Tvdb.Episode).Last(); - var lastSceneSeasonMapping = mappings.Where(v => v.Scene.Season == lastEpisodeMapping.Scene.Season).OrderBy(v => v.Scene.Episode).Last(); - - if (lastSceneSeasonMapping.Tvdb.Season == 0) - { - continue; - } - - var offset = episode.EpisodeNumber - lastEpisodeMapping.Tvdb.Episode; - - episode.SceneSeasonNumber = lastEpisodeMapping.Scene.Season; - episode.SceneEpisodeNumber = lastEpisodeMapping.Scene.Episode + offset; - episode.SceneAbsoluteEpisodeNumber = lastEpisodeMapping.Scene.Absolute + offset; - } - else if (lastTvdbSeason != lastSceneSeason) - { - var offset = episode.SeasonNumber - lastTvdbSeason; - - episode.SceneSeasonNumber = lastSceneSeason + offset; - episode.SceneEpisodeNumber = episode.EpisodeNumber; - // TODO: SceneAbsoluteEpisodeNumber. - } - } - } - - private void UpdateXemSeriesIds() - { - try - { - var ids = _xemProxy.GetXemSeriesIds(); - - if (ids.Any()) - { - _cache.Update(ids.ToDictionary(v => v.ToString(), v => true)); - return; - } - - _cache.ExtendTTL(); - _logger.Warn("Failed to update Xem series list."); - } - catch (Exception ex) - { - _cache.ExtendTTL(); - _logger.Warn(ex, "Failed to update Xem series list."); - } - } - - public List GetSceneMappings() - { - var mappings = _xemProxy.GetSceneTvdbNames(); - - return mappings.Where(m => - { - int id; - - if (int.TryParse(m.Title, out id)) - { - _logger.Debug("Skipping all numeric name: {0} for {1}", m.Title, m.TvdbId); - return false; - } - - return true; - }).ToList(); - } - - public void Handle(SeriesUpdatedEvent message) - { - //if (_cache.IsExpired(TimeSpan.FromHours(3))) - //{ - // UpdateXemSeriesIds(); - //} - - //if (_cache.Count == 0) - //{ - // _logger.Debug("Scene numbering is not available"); - // return; - //} - - //if (!_cache.Find(message.Series.TvdbId.ToString()) && !message.Series.UseSceneNumbering) - //{ - // _logger.Debug("Scene numbering is not available for {0} [{1}]", message.Series.Title, message.Series.TvdbId); - // return; - //} - - //PerformUpdate(message.Series); - } - - public void Handle(SeriesRefreshStartingEvent message) - { - //if (message.ManualTrigger && _cache.IsExpired(TimeSpan.FromMinutes(1))) - //{ - // UpdateXemSeriesIds(); - //} - } - } -} diff --git a/src/NzbDrone.Core/Datastore/ConnectionStringFactory.cs b/src/NzbDrone.Core/Datastore/ConnectionStringFactory.cs index 522b72e94..bfb7f5c8e 100644 --- a/src/NzbDrone.Core/Datastore/ConnectionStringFactory.cs +++ b/src/NzbDrone.Core/Datastore/ConnectionStringFactory.cs @@ -16,7 +16,7 @@ namespace NzbDrone.Core.Datastore { public ConnectionStringFactory(IAppFolderInfo appFolderInfo) { - MainDbConnectionString = GetConnectionString(appFolderInfo.GetNzbDroneDatabase()); + MainDbConnectionString = GetConnectionString(appFolderInfo.GetDatabase()); LogDbConnectionString = GetConnectionString(appFolderInfo.GetLogDatabase()); } diff --git a/src/NzbDrone.Core/Datastore/Converters/GuidConverter.cs b/src/NzbDrone.Core/Datastore/Converters/GuidConverter.cs index b2bf33526..ecf29d351 100644 --- a/src/NzbDrone.Core/Datastore/Converters/GuidConverter.cs +++ b/src/NzbDrone.Core/Datastore/Converters/GuidConverter.cs @@ -1,4 +1,4 @@ -using System; +using System; using Marr.Data.Converters; using Marr.Data.Mapping; diff --git a/src/NzbDrone.Core/Datastore/Converters/LanguageIntConverter.cs b/src/NzbDrone.Core/Datastore/Converters/LanguageIntConverter.cs new file mode 100644 index 000000000..0d71b1b72 --- /dev/null +++ b/src/NzbDrone.Core/Datastore/Converters/LanguageIntConverter.cs @@ -0,0 +1,65 @@ +using System; +using Marr.Data.Converters; +using Marr.Data.Mapping; +using Newtonsoft.Json; +using NzbDrone.Core.Languages; + +namespace NzbDrone.Core.Datastore.Converters +{ + public class LanguageIntConverter : JsonConverter, IConverter + { + public object FromDB(ConverterContext context) + { + if (context.DbValue == DBNull.Value) + { + return Language.Unknown; + } + + var val = Convert.ToInt32(context.DbValue); + + return (Language)val; + } + + public object FromDB(ColumnMap map, object dbValue) + { + return FromDB(new ConverterContext { ColumnMap = map, DbValue = dbValue }); + } + + public object ToDB(object clrValue) + { + if (clrValue == DBNull.Value) return 0; + + if (clrValue as Language == null) + { + throw new InvalidOperationException("Attempted to save a language that isn't really a language"); + } + + var language = clrValue as Language; + return (int)language; + } + + public Type DbType + { + get + { + return typeof(int); + } + } + + public override bool CanConvert(Type objectType) + { + return objectType == typeof(Language); + } + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + var item = reader.Value; + return (Language)Convert.ToInt32(item); + } + + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + writer.WriteValue(ToDB(value)); + } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Core/Datastore/Database.cs b/src/NzbDrone.Core/Datastore/Database.cs index c4e59f983..8d60ec009 100644 --- a/src/NzbDrone.Core/Datastore/Database.cs +++ b/src/NzbDrone.Core/Datastore/Database.cs @@ -1,4 +1,4 @@ -using System; +using System; using Marr.Data; using NLog; using NzbDrone.Common.Instrumentation; @@ -25,7 +25,6 @@ namespace NzbDrone.Core.Datastore _datamapperFactory = datamapperFactory; } - public IDataMapper GetDataMapper() { return _datamapperFactory(); @@ -54,4 +53,4 @@ namespace NzbDrone.Core.Datastore } } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Core/Datastore/Migration/001_initial_setup.cs b/src/NzbDrone.Core/Datastore/Migration/001_initial_setup.cs index b2792fe56..d6cdaa68c 100644 --- a/src/NzbDrone.Core/Datastore/Migration/001_initial_setup.cs +++ b/src/NzbDrone.Core/Datastore/Migration/001_initial_setup.cs @@ -1,4 +1,4 @@ -using FluentMigrator; +using FluentMigrator; using NzbDrone.Core.Datastore.Migration.Framework; namespace NzbDrone.Core.Datastore.Migration @@ -15,127 +15,297 @@ namespace NzbDrone.Core.Datastore.Migration Create.TableForModel("RootFolders") .WithColumn("Path").AsString().Unique(); - Create.TableForModel("Series") - .WithColumn("TvdbId").AsInt32().Unique() - .WithColumn("TvRageId").AsInt32().Unique() - .WithColumn("ImdbId").AsString().Unique() - .WithColumn("Title").AsString() - .WithColumn("TitleSlug").AsString().Unique() - .WithColumn("CleanTitle").AsString() + Create.TableForModel("Artists") + .WithColumn("ForeignArtistId").AsString().Unique() + .WithColumn("MBId").AsString().Nullable() + .WithColumn("AMId").AsString().Nullable() + .WithColumn("TADBId").AsInt32().Nullable() + .WithColumn("DiscogsId").AsInt32().Nullable() + .WithColumn("Name").AsString() + .WithColumn("NameSlug").AsString().Nullable().Unique() + .WithColumn("CleanName").AsString().Indexed() .WithColumn("Status").AsInt32() .WithColumn("Overview").AsString().Nullable() - .WithColumn("AirTime").AsString().Nullable() .WithColumn("Images").AsString() - .WithColumn("Path").AsString() + .WithColumn("Path").AsString().Indexed() .WithColumn("Monitored").AsBoolean() - .WithColumn("QualityProfileId").AsInt32() - .WithColumn("SeasonFolder").AsBoolean() + .WithColumn("AlbumFolder").AsBoolean() .WithColumn("LastInfoSync").AsDateTime().Nullable() .WithColumn("LastDiskSync").AsDateTime().Nullable() - .WithColumn("Runtime").AsInt32() - .WithColumn("SeriesType").AsInt32() - .WithColumn("BacklogSetting").AsInt32() - .WithColumn("Network").AsString().Nullable() - .WithColumn("CustomStartDate").AsDateTime().Nullable() - .WithColumn("UseSceneNumbering").AsBoolean() - .WithColumn("FirstAired").AsDateTime().Nullable() - .WithColumn("NextAiring").AsDateTime().Nullable(); - - Create.TableForModel("Seasons") - .WithColumn("SeriesId").AsInt32() - .WithColumn("SeasonNumber").AsInt32() - .WithColumn("Ignored").AsBoolean(); - - Create.TableForModel("Episodes") - .WithColumn("TvDbEpisodeId").AsInt32().Unique() - .WithColumn("SeriesId").AsInt32() - .WithColumn("SeasonNumber").AsInt32() - .WithColumn("EpisodeNumber").AsInt32() - .WithColumn("Title").AsString().Nullable() + .WithColumn("DateFormed").AsDateTime().Nullable() + .WithColumn("Members").AsString().Nullable() + .WithColumn("Ratings").AsString().Nullable() + .WithColumn("Genres").AsString().Nullable() + .WithColumn("SortName").AsString().Nullable() + .WithColumn("ProfileId").AsInt32().Nullable() + .WithColumn("Tags").AsString().Nullable() + .WithColumn("Added").AsDateTime().Nullable() + .WithColumn("AddOptions").AsString().Nullable() + .WithColumn("LanguageProfileId").AsInt32().WithDefaultValue(1) + .WithColumn("Links").AsString().Nullable() + .WithColumn("ArtistType").AsString().Nullable() + .WithColumn("Disambiguation").AsString().Nullable() + .WithColumn("PrimaryAlbumTypes").AsString().Nullable() + .WithColumn("SecondaryAlbumTypes").AsString().Nullable(); + + Create.TableForModel("Albums") + .WithColumn("ForeignAlbumId").AsString().Unique() + .WithColumn("ArtistId").AsInt32() + .WithColumn("MBId").AsString().Nullable().Indexed() + .WithColumn("AMId").AsString().Nullable() + .WithColumn("TADBId").AsInt32().Nullable().Indexed() + .WithColumn("DiscogsId").AsInt32().Nullable() + .WithColumn("Title").AsString() + .WithColumn("TitleSlug").AsString().Nullable().Unique() + .WithColumn("CleanTitle").AsString().Indexed() .WithColumn("Overview").AsString().Nullable() - .WithColumn("Ignored").AsBoolean().Nullable() - .WithColumn("EpisodeFileId").AsInt32().Nullable() - .WithColumn("AirDate").AsDateTime().Nullable() - .WithColumn("AbsoluteEpisodeNumber").AsInt32().Nullable() - .WithColumn("SceneAbsoluteEpisodeNumber").AsInt32().Nullable() - .WithColumn("SceneSeasonNumber").AsInt32().Nullable() - .WithColumn("SceneEpisodeNumber").AsInt32().Nullable(); - - Create.TableForModel("EpisodeFiles") - .WithColumn("SeriesId").AsInt32() - .WithColumn("Path").AsString().Unique() - .WithColumn("Quality").AsString() - .WithColumn("Size").AsInt64() - .WithColumn("DateAdded").AsDateTime() - .WithColumn("SeasonNumber").AsInt32() - .WithColumn("SceneName").AsString().Nullable() - .WithColumn("ReleaseGroup").AsString().Nullable(); + .WithColumn("Images").AsString() + .WithColumn("Path").AsString().Indexed() + .WithColumn("Monitored").AsBoolean() + .WithColumn("LastInfoSync").AsDateTime().Nullable() + .WithColumn("LastDiskSync").AsDateTime().Nullable() + .WithColumn("ReleaseDate").AsDateTime().Nullable() + .WithColumn("Ratings").AsString().Nullable() + .WithColumn("Genres").AsString().Nullable() + .WithColumn("Label").AsString().Nullable() + .WithColumn("SortTitle").AsString().Nullable() + .WithColumn("ProfileId").AsInt32().Nullable() + .WithColumn("Tags").AsString().Nullable() + .WithColumn("Added").AsDateTime().Nullable() + .WithColumn("AlbumType").AsString() + .WithColumn("AddOptions").AsString().Nullable() + .WithColumn("Duration").AsInt32().WithDefaultValue(0); + + + Create.TableForModel("Tracks") + .WithColumn("ForeignTrackId").AsString().Unique() + .WithColumn("ArtistId").AsInt32().Indexed() + .WithColumn("AlbumId").AsInt32() + .WithColumn("TrackNumber").AsInt32() + .WithColumn("Title").AsString().Nullable() + .WithColumn("Explicit").AsBoolean() + .WithColumn("Compilation").AsBoolean() + .WithColumn("DiscNumber").AsInt32().Nullable() + .WithColumn("TrackFileId").AsInt32().Nullable().Indexed() + .WithColumn("Monitored").AsBoolean() + .WithColumn("Ratings").AsString().Nullable() + .WithColumn("Duration").AsInt32().WithDefaultValue(0); + + Create.Index().OnTable("Tracks").OnColumn("ArtistId").Ascending() + .OnColumn("AlbumId").Ascending() + .OnColumn("TrackNumber").Ascending(); + + Create.TableForModel("TrackFiles") + .WithColumn("ArtistId").AsInt32().Indexed() + .WithColumn("AlbumId").AsInt32().Indexed() + .WithColumn("Quality").AsString() + .WithColumn("Size").AsInt64() + .WithColumn("SceneName").AsString().Nullable() + .WithColumn("DateAdded").AsDateTime() + .WithColumn("ReleaseGroup").AsString().Nullable() + .WithColumn("MediaInfo").AsString().Nullable() + .WithColumn("RelativePath").AsString().Nullable() + .WithColumn("Language").AsInt32().WithDefaultValue(0); Create.TableForModel("History") - .WithColumn("EpisodeId").AsInt32() - .WithColumn("SeriesId").AsInt32() - .WithColumn("NzbTitle").AsString() - .WithColumn("Date").AsDateTime() - .WithColumn("Quality").AsString() - .WithColumn("Indexer").AsString() - .WithColumn("NzbInfoUrl").AsString().Nullable() - .WithColumn("ReleaseGroup").AsString().Nullable(); + .WithColumn("SourceTitle").AsString() + .WithColumn("Date").AsDateTime().Indexed() + .WithColumn("Quality").AsString() + .WithColumn("Data").AsString() + .WithColumn("EventType").AsInt32().Nullable().Indexed() + .WithColumn("DownloadId").AsString().Nullable().Indexed() + .WithColumn("Language").AsInt32().NotNullable().WithDefaultValue(0) + .WithColumn("ArtistId").AsInt32().WithDefaultValue(0) + .WithColumn("AlbumId").AsInt32().Indexed().WithDefaultValue(0) + .WithColumn("TrackId").AsInt32().WithDefaultValue(0); Create.TableForModel("Notifications") - .WithColumn("Name").AsString() - .WithColumn("OnGrab").AsBoolean() - .WithColumn("OnDownload").AsBoolean() - .WithColumn("Settings").AsString() - .WithColumn("Implementation").AsString(); + .WithColumn("Name").AsString() + .WithColumn("OnGrab").AsBoolean() + .WithColumn("OnDownload").AsBoolean() + .WithColumn("Settings").AsString() + .WithColumn("Implementation").AsString() + .WithColumn("ConfigContract").AsString().Nullable() + .WithColumn("OnUpgrade").AsBoolean().Nullable() + .WithColumn("Tags").AsString().Nullable() + .WithColumn("OnRename").AsBoolean().NotNullable(); Create.TableForModel("ScheduledTasks") - .WithColumn("TypeName").AsString().Unique() - .WithColumn("Interval").AsInt32() - .WithColumn("LastExecution").AsDateTime(); + .WithColumn("TypeName").AsString().Unique() + .WithColumn("Interval").AsInt32() + .WithColumn("LastExecution").AsDateTime(); Create.TableForModel("Indexers") - .WithColumn("Enable").AsBoolean() - .WithColumn("Name").AsString().Unique() - .WithColumn("Implementation").AsString() - .WithColumn("Settings").AsString().Nullable(); + .WithColumn("Name").AsString().Unique() + .WithColumn("Implementation").AsString() + .WithColumn("Settings").AsString().Nullable() + .WithColumn("ConfigContract").AsString().Nullable() + .WithColumn("EnableRss").AsBoolean().Nullable() + .WithColumn("EnableSearch").AsBoolean().Nullable(); + + Create.TableForModel("Profiles") + .WithColumn("Name").AsString().Unique() + .WithColumn("Cutoff").AsInt32() + .WithColumn("Items").AsString().NotNullable(); - Create.TableForModel("QualityProfiles") - .WithColumn("Name").AsString().Unique() - .WithColumn("Cutoff").AsInt32() - .WithColumn("Allowed").AsString(); + Create.TableForModel("QualityDefinitions") + .WithColumn("Quality").AsInt32().Unique() + .WithColumn("Title").AsString().Unique() + .WithColumn("MinSize").AsDouble().Nullable() + .WithColumn("MaxSize").AsDouble().Nullable(); - Create.TableForModel("QualitySizes") - .WithColumn("QualityId").AsInt32().Unique() - .WithColumn("Name").AsString().Unique() - .WithColumn("MinSize").AsInt32() - .WithColumn("MaxSize").AsInt32(); + Create.TableForModel("NamingConfig") + .WithColumn("ReplaceIllegalCharacters").AsBoolean().WithDefaultValue(true) + .WithColumn("ArtistFolderFormat").AsString().Nullable() + .WithColumn("RenameTracks").AsBoolean().Nullable() + .WithColumn("StandardTrackFormat").AsString().Nullable() + .WithColumn("AlbumFolderFormat").AsString().Nullable(); - Create.TableForModel("SceneMappings") - .WithColumn("CleanTitle").AsString() - .WithColumn("SceneName").AsString() - .WithColumn("TvdbId").AsInt32() - .WithColumn("SeasonNumber").AsInt32(); + Create.TableForModel("Blacklist") + .WithColumn("SourceTitle").AsString() + .WithColumn("Quality").AsString() + .WithColumn("Date").AsDateTime() + .WithColumn("PublishedDate").AsDateTime().Nullable() + .WithColumn("Size").AsInt64().Nullable() + .WithColumn("Protocol").AsInt32().Nullable() + .WithColumn("Indexer").AsString().Nullable() + .WithColumn("Message").AsString().Nullable() + .WithColumn("TorrentInfoHash").AsString().Nullable() + .WithColumn("Language").AsInt32().NotNullable().WithDefaultValue(0) + .WithColumn("ArtistId").AsInt32().WithDefaultValue(0) + .WithColumn("AlbumIds").AsString().WithDefaultValue(""); + Create.TableForModel("Metadata") + .WithColumn("Enable").AsBoolean().NotNullable() + .WithColumn("Name").AsString().NotNullable() + .WithColumn("Implementation").AsString().NotNullable() + .WithColumn("Settings").AsString().NotNullable() + .WithColumn("ConfigContract").AsString().NotNullable(); - Create.TableForModel("NamingConfig") - .WithColumn("UseSceneName").AsBoolean() - .WithColumn("Separator").AsString() - .WithColumn("NumberStyle").AsInt32() - .WithColumn("IncludeSeriesTitle").AsBoolean() - .WithColumn("MultiEpisodeStyle").AsInt32() - .WithColumn("IncludeEpisodeTitle").AsBoolean() - .WithColumn("IncludeQuality").AsBoolean() - .WithColumn("ReplaceSpaces").AsBoolean() - .WithColumn("SeasonFolderFormat").AsString(); + Create.TableForModel("MetadataFiles") + .WithColumn("ArtistId").AsInt32().NotNullable() + .WithColumn("Consumer").AsString().NotNullable() + .WithColumn("Type").AsInt32().NotNullable() + .WithColumn("RelativePath").AsString().NotNullable() + .WithColumn("LastUpdated").AsDateTime().NotNullable() + .WithColumn("AlbumId").AsInt32().Nullable() + .WithColumn("TrackFileId").AsInt32().Nullable() + .WithColumn("Hash").AsString().Nullable() + .WithColumn("Added").AsDateTime().Nullable() + .WithColumn("Extension").AsString().NotNullable(); + + Create.TableForModel("DownloadClients") + .WithColumn("Enable").AsBoolean().NotNullable() + .WithColumn("Name").AsString().NotNullable() + .WithColumn("Implementation").AsString().NotNullable() + .WithColumn("Settings").AsString().NotNullable() + .WithColumn("ConfigContract").AsString().NotNullable(); + + Create.TableForModel("PendingReleases") + .WithColumn("Title").AsString() + .WithColumn("Added").AsDateTime() + .WithColumn("Release").AsString() + .WithColumn("ArtistId").AsInt32().WithDefaultValue(0) + .WithColumn("ParsedAlbumInfo").AsString().WithDefaultValue(""); + + + Create.TableForModel("RemotePathMappings") + .WithColumn("Host").AsString() + .WithColumn("RemotePath").AsString() + .WithColumn("LocalPath").AsString(); + + Create.TableForModel("Tags") + .WithColumn("Label").AsString().Unique(); + + Create.TableForModel("Restrictions") + .WithColumn("Required").AsString().Nullable() + .WithColumn("Preferred").AsString().Nullable() + .WithColumn("Ignored").AsString().Nullable() + .WithColumn("Tags").AsString().NotNullable(); + + Create.TableForModel("DelayProfiles") + .WithColumn("EnableUsenet").AsBoolean().NotNullable() + .WithColumn("EnableTorrent").AsBoolean().NotNullable() + .WithColumn("PreferredProtocol").AsInt32().NotNullable() + .WithColumn("UsenetDelay").AsInt32().NotNullable() + .WithColumn("TorrentDelay").AsInt32().NotNullable() + .WithColumn("Order").AsInt32().NotNullable() + .WithColumn("Tags").AsString().NotNullable(); + + Create.TableForModel("Users") + .WithColumn("Identifier").AsString().NotNullable().Unique() + .WithColumn("Username").AsString().NotNullable().Unique() + .WithColumn("Password").AsString().NotNullable(); + + Create.TableForModel("Commands") + .WithColumn("Name").AsString().NotNullable() + .WithColumn("Body").AsString().NotNullable() + .WithColumn("Priority").AsInt32().NotNullable() + .WithColumn("Status").AsInt32().NotNullable() + .WithColumn("QueuedAt").AsDateTime().NotNullable() + .WithColumn("StartedAt").AsDateTime().Nullable() + .WithColumn("EndedAt").AsDateTime().Nullable() + .WithColumn("Duration").AsString().Nullable() + .WithColumn("Exception").AsString().Nullable() + .WithColumn("Trigger").AsInt32().NotNullable(); + + Create.TableForModel("IndexerStatus") + .WithColumn("ProviderId").AsInt32().NotNullable().Unique() + .WithColumn("InitialFailure").AsDateTime().Nullable() + .WithColumn("MostRecentFailure").AsDateTime().Nullable() + .WithColumn("EscalationLevel").AsInt32().NotNullable() + .WithColumn("DisabledTill").AsDateTime().Nullable() + .WithColumn("LastRssSyncReleaseInfo").AsString().Nullable(); + + Create.TableForModel("ExtraFiles") + .WithColumn("ArtistId").AsInt32().NotNullable() + .WithColumn("AlbumId").AsInt32().NotNullable() + .WithColumn("TrackFileId").AsInt32().NotNullable() + .WithColumn("RelativePath").AsString().NotNullable() + .WithColumn("Extension").AsString().NotNullable() + .WithColumn("Added").AsDateTime().NotNullable() + .WithColumn("LastUpdated").AsDateTime().NotNullable(); + + Create.TableForModel("LyricFiles") + .WithColumn("ArtistId").AsInt32().NotNullable() + .WithColumn("AlbumId").AsInt32().NotNullable() + .WithColumn("TrackFileId").AsInt32().NotNullable() + .WithColumn("RelativePath").AsString().NotNullable() + .WithColumn("Extension").AsString().NotNullable() + .WithColumn("Added").AsDateTime().NotNullable() + .WithColumn("LastUpdated").AsDateTime().NotNullable() + .WithColumn("Language").AsInt32().NotNullable(); + + Create.TableForModel("LanguageProfiles") + .WithColumn("Name").AsString().Unique() + .WithColumn("Languages").AsString() + .WithColumn("Cutoff").AsInt32(); + + Create.TableForModel("DownloadClientStatus") + .WithColumn("ProviderId").AsInt32().NotNullable().Unique() + .WithColumn("InitialFailure").AsDateTime().Nullable() + .WithColumn("MostRecentFailure").AsDateTime().Nullable() + .WithColumn("EscalationLevel").AsInt32().NotNullable() + .WithColumn("DisabledTill").AsDateTime().Nullable(); + + Insert.IntoTable("DelayProfiles").Row(new + { + EnableUsenet = 1, + EnableTorrent = 1, + PreferredProtocol = 1, + UsenetDelay = 0, + TorrentDelay = 0, + Order = int.MaxValue, + Tags = "[]" + }); } protected override void LogDbUpgrade() { Create.TableForModel("Logs") .WithColumn("Message").AsString() - .WithColumn("Time").AsDateTime() + .WithColumn("Time").AsDateTime().Indexed() .WithColumn("Logger").AsString() - .WithColumn("Method").AsString().Nullable() .WithColumn("Exception").AsString().Nullable() .WithColumn("ExceptionType").AsString().Nullable() .WithColumn("Level").AsString(); diff --git a/src/NzbDrone.Core/Datastore/Migration/002_add_release_to_pending_releases.cs b/src/NzbDrone.Core/Datastore/Migration/002_add_release_to_pending_releases.cs new file mode 100644 index 000000000..e4ef749e7 --- /dev/null +++ b/src/NzbDrone.Core/Datastore/Migration/002_add_release_to_pending_releases.cs @@ -0,0 +1,14 @@ +using FluentMigrator; +using NzbDrone.Core.Datastore.Migration.Framework; + +namespace NzbDrone.Core.Datastore.Migration +{ + [Migration(2)] + public class add_reason_to_pending_releases : NzbDroneMigrationBase + { + protected override void MainDbUpgrade() + { + Alter.Table("PendingReleases").AddColumn("Reason").AsInt32().WithDefaultValue(0); + } + } +} diff --git a/src/NzbDrone.Core/Datastore/Migration/002_remove_tvrage_imdb_unique_constraint.cs b/src/NzbDrone.Core/Datastore/Migration/002_remove_tvrage_imdb_unique_constraint.cs deleted file mode 100644 index 6fc6a6cd3..000000000 --- a/src/NzbDrone.Core/Datastore/Migration/002_remove_tvrage_imdb_unique_constraint.cs +++ /dev/null @@ -1,15 +0,0 @@ -using FluentMigrator; -using NzbDrone.Core.Datastore.Migration.Framework; - -namespace NzbDrone.Core.Datastore.Migration -{ - [Migration(2)] - public class remove_tvrage_imdb_unique_constraint : NzbDroneMigrationBase - { - protected override void MainDbUpgrade() - { - Delete.Index().OnTable("Series").OnColumn("TvRageId"); - Delete.Index().OnTable("Series").OnColumn("ImdbId"); - } - } -} diff --git a/src/NzbDrone.Core/Datastore/Migration/003_remove_clean_title_from_scene_mapping.cs b/src/NzbDrone.Core/Datastore/Migration/003_remove_clean_title_from_scene_mapping.cs deleted file mode 100644 index a19bae93c..000000000 --- a/src/NzbDrone.Core/Datastore/Migration/003_remove_clean_title_from_scene_mapping.cs +++ /dev/null @@ -1,20 +0,0 @@ -using FluentMigrator; -using NzbDrone.Core.Datastore.Migration.Framework; - -namespace NzbDrone.Core.Datastore.Migration -{ - [Migration(3)] - public class remove_renamed_scene_mapping_columns : NzbDroneMigrationBase - { - protected override void MainDbUpgrade() - { - Delete.Table("SceneMappings"); - - Create.TableForModel("SceneMappings") - .WithColumn("TvdbId").AsInt32() - .WithColumn("SeasonNumber").AsInt32() - .WithColumn("SearchTerm").AsString() - .WithColumn("ParseTerm").AsString(); - } - } -} diff --git a/src/NzbDrone.Core/Datastore/Migration/004_updated_history.cs b/src/NzbDrone.Core/Datastore/Migration/004_updated_history.cs deleted file mode 100644 index 5ebc51ac8..000000000 --- a/src/NzbDrone.Core/Datastore/Migration/004_updated_history.cs +++ /dev/null @@ -1,23 +0,0 @@ -using FluentMigrator; -using NzbDrone.Core.Datastore.Migration.Framework; - -namespace NzbDrone.Core.Datastore.Migration -{ - [Migration(4)] - public class updated_history : NzbDroneMigrationBase - { - protected override void MainDbUpgrade() - { - Delete.Table("History"); - - - Create.TableForModel("History") - .WithColumn("EpisodeId").AsInt32() - .WithColumn("SeriesId").AsInt32() - .WithColumn("SourceTitle").AsString() - .WithColumn("Date").AsDateTime() - .WithColumn("Quality").AsString() - .WithColumn("Data").AsString(); - } - } -} diff --git a/src/NzbDrone.Core/Datastore/Migration/005_added_eventtype_to_history.cs b/src/NzbDrone.Core/Datastore/Migration/005_added_eventtype_to_history.cs deleted file mode 100644 index bead4c96f..000000000 --- a/src/NzbDrone.Core/Datastore/Migration/005_added_eventtype_to_history.cs +++ /dev/null @@ -1,17 +0,0 @@ -using FluentMigrator; -using NzbDrone.Core.Datastore.Migration.Framework; - -namespace NzbDrone.Core.Datastore.Migration -{ - [Migration(5)] - public class added_eventtype_to_history : NzbDroneMigrationBase - { - protected override void MainDbUpgrade() - { - Alter.Table("History") - .AddColumn("EventType") - .AsInt32() - .Nullable(); - } - } -} diff --git a/src/NzbDrone.Core/Datastore/Migration/006_add_index_to_log_time.cs b/src/NzbDrone.Core/Datastore/Migration/006_add_index_to_log_time.cs deleted file mode 100644 index add668fdc..000000000 --- a/src/NzbDrone.Core/Datastore/Migration/006_add_index_to_log_time.cs +++ /dev/null @@ -1,23 +0,0 @@ -using FluentMigrator; -using NzbDrone.Core.Datastore.Migration.Framework; - -namespace NzbDrone.Core.Datastore.Migration -{ - [Migration(6)] - public class add_index_to_log_time : NzbDroneMigrationBase - { - protected override void LogDbUpgrade() - { - Delete.Table("Logs"); - - Create.TableForModel("Logs") - .WithColumn("Message").AsString() - .WithColumn("Time").AsDateTime().Indexed() - .WithColumn("Logger").AsString() - .WithColumn("Method").AsString().Nullable() - .WithColumn("Exception").AsString().Nullable() - .WithColumn("ExceptionType").AsString().Nullable() - .WithColumn("Level").AsString(); - } - } -} diff --git a/src/NzbDrone.Core/Datastore/Migration/007_add_renameEpisodes_to_naming.cs b/src/NzbDrone.Core/Datastore/Migration/007_add_renameEpisodes_to_naming.cs deleted file mode 100644 index 3fc4abef9..000000000 --- a/src/NzbDrone.Core/Datastore/Migration/007_add_renameEpisodes_to_naming.cs +++ /dev/null @@ -1,19 +0,0 @@ -using FluentMigrator; -using NzbDrone.Core.Datastore.Migration.Framework; - -namespace NzbDrone.Core.Datastore.Migration -{ - [Migration(7)] - public class add_renameEpisodes_to_naming : NzbDroneMigrationBase - { - protected override void MainDbUpgrade() - { - Alter.Table("NamingConfig") - .AddColumn("RenameEpisodes") - .AsBoolean() - .Nullable(); - - Execute.Sql("UPDATE NamingConfig SET RenameEpisodes =~ UseSceneName"); - } - } -} diff --git a/src/NzbDrone.Core/Datastore/Migration/008_remove_backlog.cs b/src/NzbDrone.Core/Datastore/Migration/008_remove_backlog.cs deleted file mode 100644 index 19e16242c..000000000 --- a/src/NzbDrone.Core/Datastore/Migration/008_remove_backlog.cs +++ /dev/null @@ -1,15 +0,0 @@ -using FluentMigrator; -using NzbDrone.Core.Datastore.Migration.Framework; - -namespace NzbDrone.Core.Datastore.Migration -{ - [Migration(8)] - public class remove_backlog : NzbDroneMigrationBase - { - protected override void MainDbUpgrade() - { - Delete.Column("BacklogSetting").FromTable("Series"); - Delete.Column("UseSceneName").FromTable("NamingConfig"); - } - } -} diff --git a/src/NzbDrone.Core/Datastore/Migration/009_fix_renameEpisodes.cs b/src/NzbDrone.Core/Datastore/Migration/009_fix_renameEpisodes.cs deleted file mode 100644 index bdc0c54e5..000000000 --- a/src/NzbDrone.Core/Datastore/Migration/009_fix_renameEpisodes.cs +++ /dev/null @@ -1,17 +0,0 @@ -using FluentMigrator; -using NzbDrone.Core.Datastore.Migration.Framework; - -namespace NzbDrone.Core.Datastore.Migration -{ - [Migration(9)] - public class fix_rename_episodes : NzbDroneMigrationBase - { - protected override void MainDbUpgrade() - { - Delete.Column("SeasonFolderFormat").FromTable("NamingConfig"); - - Execute.Sql("UPDATE NamingConfig SET RenameEpisodes = 1 WHERE RenameEpisodes = -1"); - Execute.Sql("UPDATE NamingConfig SET RenameEpisodes = 0 WHERE RenameEpisodes = -2"); - } - } -} diff --git a/src/NzbDrone.Core/Datastore/Migration/010_add_monitored.cs b/src/NzbDrone.Core/Datastore/Migration/010_add_monitored.cs deleted file mode 100644 index a64f44877..000000000 --- a/src/NzbDrone.Core/Datastore/Migration/010_add_monitored.cs +++ /dev/null @@ -1,21 +0,0 @@ -using FluentMigrator; -using NzbDrone.Core.Datastore.Migration.Framework; - -namespace NzbDrone.Core.Datastore.Migration -{ - [Migration(10)] - public class add_monitored : NzbDroneMigrationBase - { - protected override void MainDbUpgrade() - { - Alter.Table("Episodes").AddColumn("Monitored").AsBoolean().Nullable(); - Alter.Table("Seasons").AddColumn("Monitored").AsBoolean().Nullable(); - - Execute.Sql("UPDATE Episodes SET Monitored = 1 WHERE Ignored = 0"); - Execute.Sql("UPDATE Episodes SET Monitored = 0 WHERE Ignored = 1"); - - Execute.Sql("UPDATE Seasons SET Monitored = 1 WHERE Ignored = 0"); - Execute.Sql("UPDATE Seasons SET Monitored = 0 WHERE Ignored = 1"); - } - } -} diff --git a/src/NzbDrone.Core/Datastore/Migration/011_remove_ignored.cs b/src/NzbDrone.Core/Datastore/Migration/011_remove_ignored.cs deleted file mode 100644 index 193b25094..000000000 --- a/src/NzbDrone.Core/Datastore/Migration/011_remove_ignored.cs +++ /dev/null @@ -1,15 +0,0 @@ -using FluentMigrator; -using NzbDrone.Core.Datastore.Migration.Framework; - -namespace NzbDrone.Core.Datastore.Migration -{ - [Migration(11)] - public class remove_ignored : NzbDroneMigrationBase - { - protected override void MainDbUpgrade() - { - Delete.Column("Ignored").FromTable("Seasons"); - Delete.Column("Ignored").FromTable("Episodes"); - } - } -} diff --git a/src/NzbDrone.Core/Datastore/Migration/012_remove_custom_start_date.cs b/src/NzbDrone.Core/Datastore/Migration/012_remove_custom_start_date.cs deleted file mode 100644 index 8b19f1a3f..000000000 --- a/src/NzbDrone.Core/Datastore/Migration/012_remove_custom_start_date.cs +++ /dev/null @@ -1,14 +0,0 @@ -using FluentMigrator; -using NzbDrone.Core.Datastore.Migration.Framework; - -namespace NzbDrone.Core.Datastore.Migration -{ - [Migration(12)] - public class remove_custom_start_date : NzbDroneMigrationBase - { - protected override void MainDbUpgrade() - { - Delete.Column("CustomStartDate").FromTable("Series"); - } - } -} diff --git a/src/NzbDrone.Core/Datastore/Migration/013_add_air_date_utc.cs b/src/NzbDrone.Core/Datastore/Migration/013_add_air_date_utc.cs deleted file mode 100644 index ece91b397..000000000 --- a/src/NzbDrone.Core/Datastore/Migration/013_add_air_date_utc.cs +++ /dev/null @@ -1,16 +0,0 @@ -using FluentMigrator; -using NzbDrone.Core.Datastore.Migration.Framework; - -namespace NzbDrone.Core.Datastore.Migration -{ - [Migration(13)] - public class add_air_date_utc : NzbDroneMigrationBase - { - protected override void MainDbUpgrade() - { - Alter.Table("Episodes").AddColumn("AirDateUtc").AsDateTime().Nullable(); - - Execute.Sql("UPDATE Episodes SET AirDateUtc = AirDate"); - } - } -} diff --git a/src/NzbDrone.Core/Datastore/Migration/014_drop_air_date.cs b/src/NzbDrone.Core/Datastore/Migration/014_drop_air_date.cs deleted file mode 100644 index 00af970b9..000000000 --- a/src/NzbDrone.Core/Datastore/Migration/014_drop_air_date.cs +++ /dev/null @@ -1,14 +0,0 @@ -using FluentMigrator; -using NzbDrone.Core.Datastore.Migration.Framework; - -namespace NzbDrone.Core.Datastore.Migration -{ - [Migration(14)] - public class drop_air_date : NzbDroneMigrationBase - { - protected override void MainDbUpgrade() - { - Delete.Column("AirDate").FromTable("Episodes"); - } - } -} diff --git a/src/NzbDrone.Core/Datastore/Migration/015_add_air_date_as_string.cs b/src/NzbDrone.Core/Datastore/Migration/015_add_air_date_as_string.cs deleted file mode 100644 index 7638e6df5..000000000 --- a/src/NzbDrone.Core/Datastore/Migration/015_add_air_date_as_string.cs +++ /dev/null @@ -1,14 +0,0 @@ -using FluentMigrator; -using NzbDrone.Core.Datastore.Migration.Framework; - -namespace NzbDrone.Core.Datastore.Migration -{ - [Migration(15)] - public class add_air_date_as_string : NzbDroneMigrationBase - { - protected override void MainDbUpgrade() - { - Alter.Table("Episodes").AddColumn("AirDate").AsString().Nullable(); - } - } -} diff --git a/src/NzbDrone.Core/Datastore/Migration/016_updated_imported_history_item.cs b/src/NzbDrone.Core/Datastore/Migration/016_updated_imported_history_item.cs deleted file mode 100644 index 7a2c50e71..000000000 --- a/src/NzbDrone.Core/Datastore/Migration/016_updated_imported_history_item.cs +++ /dev/null @@ -1,14 +0,0 @@ -using FluentMigrator; -using NzbDrone.Core.Datastore.Migration.Framework; - -namespace NzbDrone.Core.Datastore.Migration -{ - [Migration(16)] - public class updated_imported_history_item : NzbDroneMigrationBase - { - protected override void MainDbUpgrade() - { - Execute.Sql(@"UPDATE HISTORY SET Data = replace( Data, '""Path""', '""ImportedPath""' ) WHERE EventType=3"); - } - } -} diff --git a/src/NzbDrone.Core/Datastore/Migration/017_reset_scene_names.cs b/src/NzbDrone.Core/Datastore/Migration/017_reset_scene_names.cs deleted file mode 100644 index e2e3a21d6..000000000 --- a/src/NzbDrone.Core/Datastore/Migration/017_reset_scene_names.cs +++ /dev/null @@ -1,15 +0,0 @@ -using FluentMigrator; -using NzbDrone.Core.Datastore.Migration.Framework; - -namespace NzbDrone.Core.Datastore.Migration -{ - [Migration(17)] - public class reset_scene_names : NzbDroneMigrationBase - { - protected override void MainDbUpgrade() - { - //we were storing new file name as scene name. - Execute.Sql(@"UPDATE EpisodeFiles SET SceneName = NULL where SceneName != NULL"); - } - } -} diff --git a/src/NzbDrone.Core/Datastore/Migration/018_remove_duplicates.cs b/src/NzbDrone.Core/Datastore/Migration/018_remove_duplicates.cs deleted file mode 100644 index d788dd7dc..000000000 --- a/src/NzbDrone.Core/Datastore/Migration/018_remove_duplicates.cs +++ /dev/null @@ -1,99 +0,0 @@ -using FluentMigrator; -using NzbDrone.Core.Datastore.Migration.Framework; -using System.Linq; -using System.Data; -using System.Collections.Generic; -using System; - -namespace NzbDrone.Core.Datastore.Migration -{ - [Migration(18)] - public class remove_duplicates : NzbDroneMigrationBase - { - protected override void MainDbUpgrade() - { - Execute.WithConnection(RemoveDuplicates); - } - - private void RemoveDuplicates(IDbConnection conn, IDbTransaction tran) - { - RemoveDuplicateSeries(conn, tran, "TvdbId"); - RemoveDuplicateSeries(conn, tran, "TitleSlug"); - - var duplicatedEpisodes = GetDuplicates(conn, tran, "Episodes", "TvDbEpisodeId"); - - foreach (var duplicate in duplicatedEpisodes) - { - foreach (var episodeId in duplicate.OrderBy(c => c.Key).Skip(1).Select(c => c.Key)) - { - RemoveEpisodeRows(conn, tran, episodeId); - } - } - } - - private IEnumerable>> GetDuplicates(IDbConnection conn, IDbTransaction tran, string tableName, string columnName) - { - var getDuplicates = conn.CreateCommand(); - getDuplicates.Transaction = tran; - getDuplicates.CommandText = string.Format("select id, {0} from {1}", columnName, tableName); - - var result = new List>(); - - using (var reader = getDuplicates.ExecuteReader()) - { - while (reader.Read()) - { - result.Add(new KeyValuePair(reader.GetInt32(0), (T)Convert.ChangeType(reader[1], typeof(T)))); - } - } - - return result.GroupBy(c => c.Value).Where(g => g.Count() > 1); - } - - private void RemoveDuplicateSeries(IDbConnection conn, IDbTransaction tran, string field) - { - var duplicatedSeries = GetDuplicates(conn, tran, "Series", field); - - foreach (var duplicate in duplicatedSeries) - { - foreach (var seriesId in duplicate.OrderBy(c => c.Key).Skip(1).Select(c => c.Key)) - { - RemoveSeriesRows(conn, tran, seriesId); - } - } - } - - private void RemoveSeriesRows(IDbConnection conn, IDbTransaction tran, int seriesId) - { - var deleteCmd = conn.CreateCommand(); - deleteCmd.Transaction = tran; - - deleteCmd.CommandText = string.Format("DELETE FROM Series WHERE Id = {0}", seriesId.ToString()); - deleteCmd.ExecuteNonQuery(); - - deleteCmd.CommandText = string.Format("DELETE FROM Episodes WHERE SeriesId = {0}", seriesId.ToString()); - deleteCmd.ExecuteNonQuery(); - - deleteCmd.CommandText = string.Format("DELETE FROM Seasons WHERE SeriesId = {0}", seriesId.ToString()); - deleteCmd.ExecuteNonQuery(); - - deleteCmd.CommandText = string.Format("DELETE FROM History WHERE SeriesId = {0}", seriesId.ToString()); - deleteCmd.ExecuteNonQuery(); - - deleteCmd.CommandText = string.Format("DELETE FROM EpisodeFiles WHERE SeriesId = {0}", seriesId.ToString()); - deleteCmd.ExecuteNonQuery(); - } - - private void RemoveEpisodeRows(IDbConnection conn, IDbTransaction tran, int episodeId) - { - var deleteCmd = conn.CreateCommand(); - deleteCmd.Transaction = tran; - - deleteCmd.CommandText = string.Format("DELETE FROM Episodes WHERE Id = {0}", episodeId.ToString()); - deleteCmd.ExecuteNonQuery(); - - deleteCmd.CommandText = string.Format("DELETE FROM History WHERE EpisodeId = {0}", episodeId.ToString()); - deleteCmd.ExecuteNonQuery(); - } - } -} diff --git a/src/NzbDrone.Core/Datastore/Migration/019_restore_unique_constraints.cs b/src/NzbDrone.Core/Datastore/Migration/019_restore_unique_constraints.cs deleted file mode 100644 index bf70a9532..000000000 --- a/src/NzbDrone.Core/Datastore/Migration/019_restore_unique_constraints.cs +++ /dev/null @@ -1,22 +0,0 @@ -using FluentMigrator; -using NzbDrone.Core.Datastore.Migration.Framework; - -namespace NzbDrone.Core.Datastore.Migration -{ - [Migration(19)] - public class restore_unique_constraints : NzbDroneMigrationBase - { - protected override void MainDbUpgrade() - { - // During an earlier version of drone, the indexes weren't recreated during alter table. - Execute.Sql("DROP INDEX IF EXISTS \"IX_Series_TvdbId\""); - Execute.Sql("DROP INDEX IF EXISTS \"IX_Series_TitleSlug\""); - Execute.Sql("DROP INDEX IF EXISTS \"IX_Episodes_TvDbEpisodeId\""); - - Create.Index().OnTable("Series").OnColumn("TvdbId").Unique(); - Create.Index().OnTable("Series").OnColumn("TitleSlug").Unique(); - Create.Index().OnTable("Episodes").OnColumn("TvDbEpisodeId").Unique(); - } - - } -} diff --git a/src/NzbDrone.Core/Datastore/Migration/020_add_year_and_seasons_to_series.cs b/src/NzbDrone.Core/Datastore/Migration/020_add_year_and_seasons_to_series.cs deleted file mode 100644 index 0e2136141..000000000 --- a/src/NzbDrone.Core/Datastore/Migration/020_add_year_and_seasons_to_series.cs +++ /dev/null @@ -1,68 +0,0 @@ -using System.Collections.Generic; -using System.Data; -using FluentMigrator; -using NzbDrone.Common.Serializer; -using NzbDrone.Core.Datastore.Migration.Framework; - -namespace NzbDrone.Core.Datastore.Migration -{ - [Migration(20)] - public class add_year_and_seasons_to_series : NzbDroneMigrationBase - { - protected override void MainDbUpgrade() - { - Alter.Table("Series").AddColumn("Year").AsInt32().Nullable(); - Alter.Table("Series").AddColumn("Seasons").AsString().Nullable(); - - Execute.WithConnection(ConvertSeasons); - } - - private void ConvertSeasons(IDbConnection conn, IDbTransaction tran) - { - using (IDbCommand allSeriesCmd = conn.CreateCommand()) - { - allSeriesCmd.Transaction = tran; - allSeriesCmd.CommandText = @"SELECT Id FROM Series"; - using (IDataReader allSeriesReader = allSeriesCmd.ExecuteReader()) - { - while (allSeriesReader.Read()) - { - int seriesId = allSeriesReader.GetInt32(0); - var seasons = new List(); - - using (IDbCommand seasonsCmd = conn.CreateCommand()) - { - seasonsCmd.Transaction = tran; - seasonsCmd.CommandText = string.Format(@"SELECT SeasonNumber, Monitored FROM Seasons WHERE SeriesId = {0}", seriesId); - - using (IDataReader seasonReader = seasonsCmd.ExecuteReader()) - { - while (seasonReader.Read()) - { - int seasonNumber = seasonReader.GetInt32(0); - bool monitored = seasonReader.GetBoolean(1); - - if (seasonNumber == 0) - { - monitored = false; - } - - seasons.Add(new { seasonNumber, monitored }); - } - } - } - - using (IDbCommand updateCmd = conn.CreateCommand()) - { - var text = string.Format("UPDATE Series SET Seasons = '{0}' WHERE Id = {1}", seasons.ToJson() , seriesId); - - updateCmd.Transaction = tran; - updateCmd.CommandText = text; - updateCmd.ExecuteNonQuery(); - } - } - } - } - } - } -} diff --git a/src/NzbDrone.Core/Datastore/Migration/021_drop_seasons_table.cs b/src/NzbDrone.Core/Datastore/Migration/021_drop_seasons_table.cs deleted file mode 100644 index d2527c755..000000000 --- a/src/NzbDrone.Core/Datastore/Migration/021_drop_seasons_table.cs +++ /dev/null @@ -1,14 +0,0 @@ -using FluentMigrator; -using NzbDrone.Core.Datastore.Migration.Framework; - -namespace NzbDrone.Core.Datastore.Migration -{ - [Migration(21)] - public class drop_seasons_table : NzbDroneMigrationBase - { - protected override void MainDbUpgrade() - { - Delete.Table("Seasons"); - } - } -} diff --git a/src/NzbDrone.Core/Datastore/Migration/022_move_indexer_to_generic_provider.cs b/src/NzbDrone.Core/Datastore/Migration/022_move_indexer_to_generic_provider.cs deleted file mode 100644 index ea1908901..000000000 --- a/src/NzbDrone.Core/Datastore/Migration/022_move_indexer_to_generic_provider.cs +++ /dev/null @@ -1,14 +0,0 @@ -using FluentMigrator; -using NzbDrone.Core.Datastore.Migration.Framework; - -namespace NzbDrone.Core.Datastore.Migration -{ - [Migration(22)] - public class move_indexer_to_generic_provider : NzbDroneMigrationBase - { - protected override void MainDbUpgrade() - { - Alter.Table("Indexers").AddColumn("ConfigContract").AsString().Nullable(); - } - } -} diff --git a/src/NzbDrone.Core/Datastore/Migration/023_add_config_contract_to_indexers.cs b/src/NzbDrone.Core/Datastore/Migration/023_add_config_contract_to_indexers.cs deleted file mode 100644 index 1a40a5a26..000000000 --- a/src/NzbDrone.Core/Datastore/Migration/023_add_config_contract_to_indexers.cs +++ /dev/null @@ -1,19 +0,0 @@ -using FluentMigrator; -using NzbDrone.Core.Datastore.Migration.Framework; - -namespace NzbDrone.Core.Datastore.Migration -{ - [Migration(23)] - public class add_config_contract_to_indexers : NzbDroneMigrationBase - { - protected override void MainDbUpgrade() - { - Update.Table("Indexers").Set(new { ConfigContract = "NewznabSettings" }).Where(new { Implementation = "Newznab" }); - Update.Table("Indexers").Set(new { ConfigContract = "OmgwtfnzbsSettings" }).Where(new { Implementation = "Omgwtfnzbs" }); - Update.Table("Indexers").Set(new { ConfigContract = "NullConfig" }).Where(new { Implementation = "Wombles" }); - Update.Table("Indexers").Set(new { ConfigContract = "NullConfig" }).Where(new { Implementation = "Eztv" }); - - Delete.FromTable("Indexers").IsNull("ConfigContract"); - } - } -} diff --git a/src/NzbDrone.Core/Datastore/Migration/024_drop_tvdb_episodeid.cs b/src/NzbDrone.Core/Datastore/Migration/024_drop_tvdb_episodeid.cs deleted file mode 100644 index c723f462c..000000000 --- a/src/NzbDrone.Core/Datastore/Migration/024_drop_tvdb_episodeid.cs +++ /dev/null @@ -1,14 +0,0 @@ -using FluentMigrator; -using NzbDrone.Core.Datastore.Migration.Framework; - -namespace NzbDrone.Core.Datastore.Migration -{ - [Migration(24)] - public class drop_tvdb_episodeid : NzbDroneMigrationBase - { - protected override void MainDbUpgrade() - { - Delete.Column("TvDbEpisodeId").FromTable("Episodes"); - } - } -} diff --git a/src/NzbDrone.Core/Datastore/Migration/025_move_notification_to_generic_provider.cs b/src/NzbDrone.Core/Datastore/Migration/025_move_notification_to_generic_provider.cs deleted file mode 100644 index 1937f76eb..000000000 --- a/src/NzbDrone.Core/Datastore/Migration/025_move_notification_to_generic_provider.cs +++ /dev/null @@ -1,14 +0,0 @@ -using FluentMigrator; -using NzbDrone.Core.Datastore.Migration.Framework; - -namespace NzbDrone.Core.Datastore.Migration -{ - [Migration(25)] - public class move_notification_to_generic_provider : NzbDroneMigrationBase - { - protected override void MainDbUpgrade() - { - Alter.Table("Notifications").AddColumn("ConfigContract").AsString().Nullable(); - } - } -} diff --git a/src/NzbDrone.Core/Datastore/Migration/026_add_config_contract_to_notifications.cs b/src/NzbDrone.Core/Datastore/Migration/026_add_config_contract_to_notifications.cs deleted file mode 100644 index 8eb24daae..000000000 --- a/src/NzbDrone.Core/Datastore/Migration/026_add_config_contract_to_notifications.cs +++ /dev/null @@ -1,24 +0,0 @@ -using FluentMigrator; -using NzbDrone.Core.Datastore.Migration.Framework; - -namespace NzbDrone.Core.Datastore.Migration -{ - [Migration(26)] - public class add_config_contract_to_notifications : NzbDroneMigrationBase - { - protected override void MainDbUpgrade() - { - Update.Table("Notifications").Set(new { ConfigContract = "EmailSettings" }).Where(new { Implementation = "Email" }); - Update.Table("Notifications").Set(new { ConfigContract = "GrowlSettings" }).Where(new { Implementation = "Growl" }); - Update.Table("Notifications").Set(new { ConfigContract = "NotifyMyAndroidSettings" }).Where(new { Implementation = "NotifyMyAndroid" }); - Update.Table("Notifications").Set(new { ConfigContract = "PlexClientSettings" }).Where(new { Implementation = "PlexClient" }); - Update.Table("Notifications").Set(new { ConfigContract = "PlexServerSettings" }).Where(new { Implementation = "PlexServer" }); - Update.Table("Notifications").Set(new { ConfigContract = "ProwlSettings" }).Where(new { Implementation = "Prowl" }); - Update.Table("Notifications").Set(new { ConfigContract = "PushBulletSettings" }).Where(new { Implementation = "PushBullet" }); - Update.Table("Notifications").Set(new { ConfigContract = "PushoverSettings" }).Where(new { Implementation = "Pushover" }); - Update.Table("Notifications").Set(new { ConfigContract = "XbmcSettings" }).Where(new { Implementation = "Xbmc" }); - - Delete.FromTable("Notifications").IsNull("ConfigContract"); - } - } -} diff --git a/src/NzbDrone.Core/Datastore/Migration/027_fix_omgwtfnzbs.cs b/src/NzbDrone.Core/Datastore/Migration/027_fix_omgwtfnzbs.cs deleted file mode 100644 index d7b8b31fc..000000000 --- a/src/NzbDrone.Core/Datastore/Migration/027_fix_omgwtfnzbs.cs +++ /dev/null @@ -1,24 +0,0 @@ -using FluentMigrator; -using NzbDrone.Core.Datastore.Migration.Framework; - -namespace NzbDrone.Core.Datastore.Migration -{ - [Migration(27)] - public class fix_omgwtfnzbs : NzbDroneMigrationBase - { - protected override void MainDbUpgrade() - { - Update.Table("Indexers") - .Set(new {ConfigContract = "OmgwtfnzbsSettings"}) - .Where(new {Implementation = "Omgwtfnzbs"}); - - Update.Table("Indexers") - .Set(new {Settings = "{}"}) - .Where(new {Implementation = "Omgwtfnzbs", Settings = (string) null}); - - Update.Table("Indexers") - .Set(new { Settings = "{}" }) - .Where(new { Implementation = "Omgwtfnzbs", Settings = "" }); - } - } -} diff --git a/src/NzbDrone.Core/Datastore/Migration/028_add_blacklist_table.cs b/src/NzbDrone.Core/Datastore/Migration/028_add_blacklist_table.cs deleted file mode 100644 index 0514c9689..000000000 --- a/src/NzbDrone.Core/Datastore/Migration/028_add_blacklist_table.cs +++ /dev/null @@ -1,19 +0,0 @@ -using FluentMigrator; -using NzbDrone.Core.Datastore.Migration.Framework; - -namespace NzbDrone.Core.Datastore.Migration -{ - [Migration(28)] - public class add_blacklist_table : NzbDroneMigrationBase - { - protected override void MainDbUpgrade() - { - Create.TableForModel("Blacklist") - .WithColumn("SeriesId").AsInt32() - .WithColumn("EpisodeIds").AsString() - .WithColumn("SourceTitle").AsString() - .WithColumn("Quality").AsString() - .WithColumn("Date").AsDateTime(); - } - } -} diff --git a/src/NzbDrone.Core/Datastore/Migration/029_add_formats_to_naming_config.cs b/src/NzbDrone.Core/Datastore/Migration/029_add_formats_to_naming_config.cs deleted file mode 100644 index 6d3dd897b..000000000 --- a/src/NzbDrone.Core/Datastore/Migration/029_add_formats_to_naming_config.cs +++ /dev/null @@ -1,154 +0,0 @@ -using System.Collections.Generic; -using System.Data; -using System.Linq; -using FluentMigrator; -using NzbDrone.Core.Datastore.Migration.Framework; - -namespace NzbDrone.Core.Datastore.Migration -{ - [Migration(29)] - public class add_formats_to_naming_config : NzbDroneMigrationBase - { - protected override void MainDbUpgrade() - { - Alter.Table("NamingConfig").AddColumn("StandardEpisodeFormat").AsString().Nullable(); - Alter.Table("NamingConfig").AddColumn("DailyEpisodeFormat").AsString().Nullable(); - - Execute.WithConnection(ConvertConfig); - } - - private void ConvertConfig(IDbConnection conn, IDbTransaction tran) - { - using (IDbCommand namingConfigCmd = conn.CreateCommand()) - { - namingConfigCmd.Transaction = tran; - namingConfigCmd.CommandText = @"SELECT * FROM NamingConfig LIMIT 1"; - using (IDataReader namingConfigReader = namingConfigCmd.ExecuteReader()) - { - var separatorIndex = namingConfigReader.GetOrdinal("Separator"); - var numberStyleIndex = namingConfigReader.GetOrdinal("NumberStyle"); - var includeSeriesTitleIndex = namingConfigReader.GetOrdinal("IncludeSeriesTitle"); - var includeEpisodeTitleIndex = namingConfigReader.GetOrdinal("IncludeEpisodeTitle"); - var includeQualityIndex = namingConfigReader.GetOrdinal("IncludeQuality"); - var replaceSpacesIndex = namingConfigReader.GetOrdinal("ReplaceSpaces"); - - while (namingConfigReader.Read()) - { - var separator = namingConfigReader.GetString(separatorIndex); - var numberStyle = namingConfigReader.GetInt32(numberStyleIndex); - var includeSeriesTitle = namingConfigReader.GetBoolean(includeSeriesTitleIndex); - var includeEpisodeTitle = namingConfigReader.GetBoolean(includeEpisodeTitleIndex); - var includeQuality = namingConfigReader.GetBoolean(includeQualityIndex); - var replaceSpaces = namingConfigReader.GetBoolean(replaceSpacesIndex); - - //Output settings - var seriesTitlePattern = ""; - var episodeTitlePattern = ""; - var dailyEpisodePattern = "{Air-Date}"; - var qualityFormat = " [{Quality Title}]"; - - if (includeSeriesTitle) - { - if (replaceSpaces) - { - seriesTitlePattern = "{Series.Title}"; - } - - else - { - seriesTitlePattern = "{Series Title}"; - } - - seriesTitlePattern += separator; - } - - if (includeEpisodeTitle) - { - episodeTitlePattern = separator; - - if (replaceSpaces) - { - episodeTitlePattern += "{Episode.Title}"; - } - - else - { - episodeTitlePattern += "{Episode Title}"; - } - } - - var standardEpisodeFormat = string.Format("{0}{1}{2}", seriesTitlePattern, - GetNumberStyle(numberStyle).Pattern, - episodeTitlePattern); - - var dailyEpisodeFormat = string.Format("{0}{1}{2}", seriesTitlePattern, - dailyEpisodePattern, - episodeTitlePattern); - - if (includeQuality) - { - if (replaceSpaces) - { - qualityFormat = ".[{Quality.Title}]"; - } - - standardEpisodeFormat += qualityFormat; - dailyEpisodeFormat += qualityFormat; - } - - using (IDbCommand updateCmd = conn.CreateCommand()) - { - var text = string.Format("UPDATE NamingConfig " + - "SET StandardEpisodeFormat = '{0}', " + - "DailyEpisodeFormat = '{1}'", - standardEpisodeFormat, - dailyEpisodeFormat); - - updateCmd.Transaction = tran; - updateCmd.CommandText = text; - updateCmd.ExecuteNonQuery(); - } - } - } - } - } - - private static readonly List NumberStyles = new List - { - new - { - Id = 0, - Name = "1x05", - Pattern = "{season}x{episode:00}", - EpisodeSeparator = "x" - - }, - new - { - Id = 1, - Name = "01x05", - Pattern = "{season:00}x{episode:00}", - EpisodeSeparator = "x" - }, - new - { - Id = 2, - Name = "S01E05", - Pattern = "S{season:00}E{episode:00}", - EpisodeSeparator = "E" - }, - new - { - Id = 3, - Name = "s01e05", - Pattern = "s{season:00}e{episode:00}", - EpisodeSeparator = "e" - } - }; - - private static dynamic GetNumberStyle(int id) - { - return NumberStyles.Single(s => s.Id == id); - } - } -} diff --git a/src/NzbDrone.Core/Datastore/Migration/030_add_season_folder_format_to_naming_config.cs b/src/NzbDrone.Core/Datastore/Migration/030_add_season_folder_format_to_naming_config.cs deleted file mode 100644 index 185a53a19..000000000 --- a/src/NzbDrone.Core/Datastore/Migration/030_add_season_folder_format_to_naming_config.cs +++ /dev/null @@ -1,55 +0,0 @@ -using System.Data; -using FluentMigrator; -using NzbDrone.Core.Datastore.Migration.Framework; - -namespace NzbDrone.Core.Datastore.Migration -{ - [Migration(30)] - public class add_season_folder_format_to_naming_config : NzbDroneMigrationBase - { - protected override void MainDbUpgrade() - { - Alter.Table("NamingConfig").AddColumn("SeasonFolderFormat").AsString().Nullable(); - Execute.WithConnection(ConvertConfig); - Execute.Sql("DELETE FROM Config WHERE [Key] = 'seasonfolderformat'"); - Execute.Sql("DELETE FROM Config WHERE [Key] = 'useseasonfolder'"); - } - - private void ConvertConfig(IDbConnection conn, IDbTransaction tran) - { - using (IDbCommand namingConfigCmd = conn.CreateCommand()) - { - namingConfigCmd.Transaction = tran; - namingConfigCmd.CommandText = @"SELECT [Value] FROM Config WHERE [Key] = 'seasonfolderformat'"; - var seasonFormat = "Season {season}"; - - using (IDataReader namingConfigReader = namingConfigCmd.ExecuteReader()) - { - while (namingConfigReader.Read()) - { - //only getting one column, so its index is 0 - seasonFormat = namingConfigReader.GetString(0); - - seasonFormat = seasonFormat.Replace("%sn", "{Series Title}") - .Replace("%s.n", "{Series.Title}") - .Replace("%s", "{season}") - .Replace("%0s", "{season:00}") - .Replace("%e", "{episode}") - .Replace("%0e", "{episode:00}"); - } - } - - using (IDbCommand updateCmd = conn.CreateCommand()) - { - var text = string.Format("UPDATE NamingConfig " + - "SET SeasonFolderFormat = '{0}'", - seasonFormat); - - updateCmd.Transaction = tran; - updateCmd.CommandText = text; - updateCmd.ExecuteNonQuery(); - } - } - } - } -} diff --git a/src/NzbDrone.Core/Datastore/Migration/031_delete_old_naming_config_columns.cs b/src/NzbDrone.Core/Datastore/Migration/031_delete_old_naming_config_columns.cs deleted file mode 100644 index 90c7571af..000000000 --- a/src/NzbDrone.Core/Datastore/Migration/031_delete_old_naming_config_columns.cs +++ /dev/null @@ -1,20 +0,0 @@ -using FluentMigrator; -using NzbDrone.Core.Datastore.Migration.Framework; - -namespace NzbDrone.Core.Datastore.Migration -{ - [Migration(31)] - public class delete_old_naming_config_columns : NzbDroneMigrationBase - { - protected override void MainDbUpgrade() - { - Delete.Column("Separator") - .Column("NumberStyle") - .Column("IncludeSeriesTitle") - .Column("IncludeEpisodeTitle") - .Column("IncludeQuality") - .Column("ReplaceSpaces") - .FromTable("NamingConfig"); - } - } -} diff --git a/src/NzbDrone.Core/Datastore/Migration/032_set_default_release_group.cs b/src/NzbDrone.Core/Datastore/Migration/032_set_default_release_group.cs deleted file mode 100644 index 5ecc4e2c0..000000000 --- a/src/NzbDrone.Core/Datastore/Migration/032_set_default_release_group.cs +++ /dev/null @@ -1,14 +0,0 @@ -using FluentMigrator; -using NzbDrone.Core.Datastore.Migration.Framework; - -namespace NzbDrone.Core.Datastore.Migration -{ - [Migration(32)] - public class set_default_release_group : NzbDroneMigrationBase - { - protected override void MainDbUpgrade() - { - Execute.Sql("UPDATE EpisodeFiles SET ReleaseGroup = 'DRONE' WHERE ReleaseGroup IS NULL"); - } - } -} diff --git a/src/NzbDrone.Core/Datastore/Migration/033_add_api_key_to_pushover.cs b/src/NzbDrone.Core/Datastore/Migration/033_add_api_key_to_pushover.cs deleted file mode 100644 index 670d1f8ab..000000000 --- a/src/NzbDrone.Core/Datastore/Migration/033_add_api_key_to_pushover.cs +++ /dev/null @@ -1,66 +0,0 @@ -using System.Data; -using FluentMigrator; -using NzbDrone.Common.Serializer; -using NzbDrone.Core.Datastore.Migration.Framework; - -namespace NzbDrone.Core.Datastore.Migration -{ - [Migration(33)] - public class add_api_key_to_pushover : NzbDroneMigrationBase - { - private const string API_KEY = "yz9b4U215iR4vrKFRfjNXP24NMNPKJ"; - - protected override void MainDbUpgrade() - { - Execute.WithConnection(UpdatePushoverSettings); - } - - private void UpdatePushoverSettings(IDbConnection conn, IDbTransaction tran) - { - using (IDbCommand selectCommand = conn.CreateCommand()) - { - selectCommand.Transaction = tran; - selectCommand.CommandText = @"SELECT * FROM Notifications WHERE ConfigContract = 'PushoverSettings'"; - - using (IDataReader reader = selectCommand.ExecuteReader()) - { - while (reader.Read()) - { - var idIndex = reader.GetOrdinal("Id"); - var settingsIndex = reader.GetOrdinal("Settings"); - - var id = reader.GetInt32(idIndex); - var settings = Json.Deserialize(reader.GetString(settingsIndex)); - settings.ApiKey = API_KEY; - - //Set priority to high if its currently emergency - if (settings.Priority == 2) - { - settings.Priority = 1; - } - - using (IDbCommand updateCmd = conn.CreateCommand()) - { - var text = string.Format("UPDATE Notifications " + - "SET Settings = '{0}'" + - "WHERE Id = {1}", - settings.ToJson(), id - ); - - updateCmd.Transaction = tran; - updateCmd.CommandText = text; - updateCmd.ExecuteNonQuery(); - } - } - } - } - } - - private class PushoverSettingsForV33 - { - public string ApiKey { get; set; } - public string UserKey { get; set; } - public int Priority { get; set; } - } - } -} diff --git a/src/NzbDrone.Core/Datastore/Migration/034_remove_series_contraints.cs b/src/NzbDrone.Core/Datastore/Migration/034_remove_series_contraints.cs deleted file mode 100644 index 8eb08798a..000000000 --- a/src/NzbDrone.Core/Datastore/Migration/034_remove_series_contraints.cs +++ /dev/null @@ -1,16 +0,0 @@ -using FluentMigrator; -using NzbDrone.Core.Datastore.Migration.Framework; - -namespace NzbDrone.Core.Datastore.Migration -{ - [Migration(34)] - public class remove_series_contraints : NzbDroneMigrationBase - { - protected override void MainDbUpgrade() - { - Alter.Table("Series") - .AlterColumn("ImdbId").AsString().Nullable() - .AlterColumn("TitleSlug").AsString().Nullable(); - } - } -} diff --git a/src/NzbDrone.Core/Datastore/Migration/035_add_series_folder_format_to_naming_config.cs b/src/NzbDrone.Core/Datastore/Migration/035_add_series_folder_format_to_naming_config.cs deleted file mode 100644 index 9423a54f0..000000000 --- a/src/NzbDrone.Core/Datastore/Migration/035_add_series_folder_format_to_naming_config.cs +++ /dev/null @@ -1,16 +0,0 @@ -using FluentMigrator; -using NzbDrone.Core.Datastore.Migration.Framework; - -namespace NzbDrone.Core.Datastore.Migration -{ - [Migration(35)] - public class add_series_folder_format_to_naming_config : NzbDroneMigrationBase - { - protected override void MainDbUpgrade() - { - Alter.Table("NamingConfig").AddColumn("SeriesFolderFormat").AsString().Nullable(); - - Execute.Sql("UPDATE NamingConfig SET SeriesFolderFormat = '{Series Title}'"); - } - } -} diff --git a/src/NzbDrone.Core/Datastore/Migration/036_update_with_quality_converters.cs b/src/NzbDrone.Core/Datastore/Migration/036_update_with_quality_converters.cs deleted file mode 100644 index 37d94e33d..000000000 --- a/src/NzbDrone.Core/Datastore/Migration/036_update_with_quality_converters.cs +++ /dev/null @@ -1,129 +0,0 @@ -using FluentMigrator; -using NzbDrone.Core.Datastore.Migration.Framework; -using System.Data; -using System.Linq; -using NzbDrone.Common.Serializer; -using NzbDrone.Core.Profiles; -using NzbDrone.Core.Qualities; -using System.Collections.Generic; -using NzbDrone.Core.Datastore.Converters; - -namespace NzbDrone.Core.Datastore.Migration -{ - [Migration(36)] - public class update_with_quality_converters : NzbDroneMigrationBase - { - protected override void MainDbUpgrade() - { - if (!Schema.Table("QualityProfiles").Column("Items").Exists()) - { - Alter.Table("QualityProfiles").AddColumn("Items").AsString().Nullable(); - } - - Execute.WithConnection(ConvertQualityProfiles); - Execute.WithConnection(ConvertQualityModels); - } - - private void ConvertQualityProfiles(IDbConnection conn, IDbTransaction tran) - { - var qualityProfileItemConverter = new EmbeddedDocumentConverter(new QualityIntConverter()); - - // Convert 'Allowed' column in QualityProfiles from Json List to Json List (int = Quality) - using (IDbCommand qualityProfileCmd = conn.CreateCommand()) - { - qualityProfileCmd.Transaction = tran; - qualityProfileCmd.CommandText = @"SELECT Id, Allowed FROM QualityProfiles"; - using (IDataReader qualityProfileReader = qualityProfileCmd.ExecuteReader()) - { - while (qualityProfileReader.Read()) - { - var id = qualityProfileReader.GetInt32(0); - var allowedJson = qualityProfileReader.GetString(1); - - var allowed = Json.Deserialize>(allowedJson); - - var items = Quality.DefaultQualityDefinitions.OrderBy(v => v.Weight).Select(v => new ProfileQualityItem { Quality = v.Quality, Allowed = allowed.Contains(v.Quality) }).ToList(); - - var allowedNewJson = qualityProfileItemConverter.ToDB(items); - - using (IDbCommand updateCmd = conn.CreateCommand()) - { - updateCmd.Transaction = tran; - updateCmd.CommandText = "UPDATE QualityProfiles SET Items = ? WHERE Id = ?"; - updateCmd.AddParameter(allowedNewJson); - updateCmd.AddParameter(id); - - updateCmd.ExecuteNonQuery(); - } - } - } - } - } - - private void ConvertQualityModels(IDbConnection conn, IDbTransaction tran) - { - // Converts the QualityModel JSON objects to their new format (only storing the QualityId instead of the entire object) - ConvertQualityModel(conn, tran, "Blacklist"); - ConvertQualityModel(conn, tran, "EpisodeFiles"); - ConvertQualityModel(conn, tran, "History"); - } - - private void ConvertQualityModel(IDbConnection conn, IDbTransaction tran, string tableName) - { - var qualityModelConverter = new EmbeddedDocumentConverter(new QualityIntConverter()); - - using (IDbCommand qualityModelCmd = conn.CreateCommand()) - { - qualityModelCmd.Transaction = tran; - qualityModelCmd.CommandText = @"SELECT Distinct Quality FROM " + tableName; - using (IDataReader qualityModelReader = qualityModelCmd.ExecuteReader()) - { - while (qualityModelReader.Read()) - { - var qualityJson = qualityModelReader.GetString(0); - - SourceQualityModel036 sourceQuality; - - if (!Json.TryDeserialize(qualityJson, out sourceQuality)) - { - continue; - } - - var qualityNewJson = qualityModelConverter.ToDB(new DestinationQualityModel036 - { - Quality = sourceQuality.Quality.Id, - Proper = sourceQuality.Proper - }); - - using (IDbCommand updateCmd = conn.CreateCommand()) - { - updateCmd.Transaction = tran; - updateCmd.CommandText = "UPDATE " + tableName + " SET Quality = ? WHERE Quality = ?"; - updateCmd.AddParameter(qualityNewJson); - updateCmd.AddParameter(qualityJson); - - updateCmd.ExecuteNonQuery(); - } - } - } - } - } - - private class DestinationQualityModel036 - { - public int Quality { get; set; } - public bool Proper { get; set; } - } - - private class SourceQualityModel036 - { - public SourceQuality036 Quality { get; set; } - public bool Proper { get; set; } - } - - private class SourceQuality036 - { - public int Id { get; set; } - } - } -} diff --git a/src/NzbDrone.Core/Datastore/Migration/037_add_configurable_qualities.cs b/src/NzbDrone.Core/Datastore/Migration/037_add_configurable_qualities.cs deleted file mode 100644 index 06ced4854..000000000 --- a/src/NzbDrone.Core/Datastore/Migration/037_add_configurable_qualities.cs +++ /dev/null @@ -1,64 +0,0 @@ -using FluentMigrator; -using NzbDrone.Core.Datastore.Migration.Framework; -using System.Data; -using System.Linq; -using NzbDrone.Core.Qualities; - -namespace NzbDrone.Core.Datastore.Migration -{ - [Migration(37)] - public class add_configurable_qualities : NzbDroneMigrationBase - { - protected override void MainDbUpgrade() - { - Delete.Column("Allowed").FromTable("QualityProfiles"); - - Alter.Column("Items").OnTable("QualityProfiles").AsString().NotNullable(); - - Create.TableForModel("QualityDefinitions") - .WithColumn("Quality").AsInt32().Unique() - .WithColumn("Title").AsString().Unique() - .WithColumn("Weight").AsInt32().Unique() - .WithColumn("MinSize").AsInt32() - .WithColumn("MaxSize").AsInt32(); - - Execute.WithConnection(ConvertQualities); - - Delete.Table("QualitySizes"); - } - - private void ConvertQualities(IDbConnection conn, IDbTransaction tran) - { - // Convert QualitySizes to a more generic QualityDefinitions table. - using (IDbCommand qualitySizeCmd = conn.CreateCommand()) - { - qualitySizeCmd.Transaction = tran; - qualitySizeCmd.CommandText = @"SELECT QualityId, MinSize, MaxSize FROM QualitySizes"; - using (IDataReader qualitySizeReader = qualitySizeCmd.ExecuteReader()) - { - while (qualitySizeReader.Read()) - { - var qualityId = qualitySizeReader.GetInt32(0); - var minSize = qualitySizeReader.GetInt32(1); - var maxSize = qualitySizeReader.GetInt32(2); - - var defaultConfig = Quality.DefaultQualityDefinitions.Single(p => (int)p.Quality == qualityId); - - using (IDbCommand updateCmd = conn.CreateCommand()) - { - updateCmd.Transaction = tran; - updateCmd.CommandText = "INSERT INTO QualityDefinitions (Quality, Title, Weight, MinSize, MaxSize) VALUES (?, ?, ?, ?, ?)"; - updateCmd.AddParameter(qualityId); - updateCmd.AddParameter(defaultConfig.Title); - updateCmd.AddParameter(defaultConfig.Weight); - updateCmd.AddParameter(minSize); - updateCmd.AddParameter(maxSize); - - updateCmd.ExecuteNonQuery(); - } - } - } - } - } - } -} diff --git a/src/NzbDrone.Core/Datastore/Migration/038_add_on_upgrade_to_notifications.cs b/src/NzbDrone.Core/Datastore/Migration/038_add_on_upgrade_to_notifications.cs deleted file mode 100644 index f5cae2ba0..000000000 --- a/src/NzbDrone.Core/Datastore/Migration/038_add_on_upgrade_to_notifications.cs +++ /dev/null @@ -1,16 +0,0 @@ -using FluentMigrator; -using NzbDrone.Core.Datastore.Migration.Framework; - -namespace NzbDrone.Core.Datastore.Migration -{ - [Migration(38)] - public class add_on_upgrade_to_notifications : NzbDroneMigrationBase - { - protected override void MainDbUpgrade() - { - Alter.Table("Notifications").AddColumn("OnUpgrade").AsBoolean().Nullable(); - - Execute.Sql("UPDATE Notifications SET OnUpgrade = OnDownload"); - } - } -} diff --git a/src/NzbDrone.Core/Datastore/Migration/039_add_metadata_tables.cs b/src/NzbDrone.Core/Datastore/Migration/039_add_metadata_tables.cs deleted file mode 100644 index fdc7f2545..000000000 --- a/src/NzbDrone.Core/Datastore/Migration/039_add_metadata_tables.cs +++ /dev/null @@ -1,28 +0,0 @@ -using FluentMigrator; -using NzbDrone.Core.Datastore.Migration.Framework; - -namespace NzbDrone.Core.Datastore.Migration -{ - [Migration(39)] - public class add_metadata_tables : NzbDroneMigrationBase - { - protected override void MainDbUpgrade() - { - Create.TableForModel("Metadata") - .WithColumn("Enable").AsBoolean().NotNullable() - .WithColumn("Name").AsString().NotNullable() - .WithColumn("Implementation").AsString().NotNullable() - .WithColumn("Settings").AsString().NotNullable() - .WithColumn("ConfigContract").AsString().NotNullable(); - - Create.TableForModel("MetadataFiles") - .WithColumn("SeriesId").AsInt32().NotNullable() - .WithColumn("Consumer").AsString().NotNullable() - .WithColumn("Type").AsInt32().NotNullable() - .WithColumn("RelativePath").AsString().NotNullable() - .WithColumn("LastUpdated").AsDateTime().NotNullable() - .WithColumn("SeasonNumber").AsInt32().Nullable() - .WithColumn("EpisodeFileId").AsInt32().Nullable(); - } - } -} diff --git a/src/NzbDrone.Core/Datastore/Migration/040_add_metadata_to_episodes_and_series.cs b/src/NzbDrone.Core/Datastore/Migration/040_add_metadata_to_episodes_and_series.cs deleted file mode 100644 index bf8119831..000000000 --- a/src/NzbDrone.Core/Datastore/Migration/040_add_metadata_to_episodes_and_series.cs +++ /dev/null @@ -1,22 +0,0 @@ -using FluentMigrator; -using NzbDrone.Core.Datastore.Migration.Framework; - -namespace NzbDrone.Core.Datastore.Migration -{ - [Migration(40)] - public class add_metadata_to_episodes_and_series : NzbDroneMigrationBase - { - protected override void MainDbUpgrade() - { - Alter.Table("Series") - .AddColumn("Actors").AsString().Nullable() - .AddColumn("Ratings").AsString().Nullable() - .AddColumn("Genres").AsString().Nullable() - .AddColumn("Certification").AsString().Nullable(); - - Alter.Table("Episodes") - .AddColumn("Ratings").AsString().Nullable() - .AddColumn("Images").AsString().Nullable(); - } - } -} diff --git a/src/NzbDrone.Core/Datastore/Migration/041_fix_xbmc_season_images_metadata.cs b/src/NzbDrone.Core/Datastore/Migration/041_fix_xbmc_season_images_metadata.cs deleted file mode 100644 index 25cbc8ed4..000000000 --- a/src/NzbDrone.Core/Datastore/Migration/041_fix_xbmc_season_images_metadata.cs +++ /dev/null @@ -1,14 +0,0 @@ -using FluentMigrator; -using NzbDrone.Core.Datastore.Migration.Framework; - -namespace NzbDrone.Core.Datastore.Migration -{ - [Migration(41)] - public class fix_xbmc_season_images_metadata : NzbDroneMigrationBase - { - protected override void MainDbUpgrade() - { - Execute.Sql("UPDATE MetadataFiles SET Type = 4 WHERE Consumer = 'XbmcMetadata' AND SeasonNumber IS NOT NULL"); - } - } -} diff --git a/src/NzbDrone.Core/Datastore/Migration/042_add_download_clients_table.cs b/src/NzbDrone.Core/Datastore/Migration/042_add_download_clients_table.cs deleted file mode 100644 index 08cf7622b..000000000 --- a/src/NzbDrone.Core/Datastore/Migration/042_add_download_clients_table.cs +++ /dev/null @@ -1,20 +0,0 @@ -using FluentMigrator; -using NzbDrone.Core.Datastore.Migration.Framework; - -namespace NzbDrone.Core.Datastore.Migration -{ - [Migration(42)] - public class add_download_clients_table : NzbDroneMigrationBase - { - protected override void MainDbUpgrade() - { - Create.TableForModel("DownloadClients") - .WithColumn("Enable").AsBoolean().NotNullable() - .WithColumn("Name").AsString().NotNullable() - .WithColumn("Implementation").AsString().NotNullable() - .WithColumn("Settings").AsString().NotNullable() - .WithColumn("ConfigContract").AsString().NotNullable() - .WithColumn("Protocol").AsInt32().NotNullable(); - } - } -} diff --git a/src/NzbDrone.Core/Datastore/Migration/043_convert_config_to_download_clients.cs b/src/NzbDrone.Core/Datastore/Migration/043_convert_config_to_download_clients.cs deleted file mode 100644 index 505962776..000000000 --- a/src/NzbDrone.Core/Datastore/Migration/043_convert_config_to_download_clients.cs +++ /dev/null @@ -1,197 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Data; -using FluentMigrator; -using NzbDrone.Common.Serializer; -using NzbDrone.Core.Datastore.Migration.Framework; - -namespace NzbDrone.Core.Datastore.Migration -{ - [Migration(43)] - public class convert_config_to_download_clients : NzbDroneMigrationBase - { - protected override void MainDbUpgrade() - { - Execute.WithConnection(ConvertToThingyProvder); - } - - private void ConvertToThingyProvder(IDbConnection conn, IDbTransaction tran) - { - var config = new Dictionary(); - - using (IDbCommand configCmd = conn.CreateCommand()) - { - configCmd.Transaction = tran; - configCmd.CommandText = @"SELECT * FROM Config"; - using (IDataReader configReader = configCmd.ExecuteReader()) - { - var keyIndex = configReader.GetOrdinal("Key"); - var valueIndex = configReader.GetOrdinal("Value"); - - while (configReader.Read()) - { - var key = configReader.GetString(keyIndex); - var value = configReader.GetString(valueIndex); - - config.Add(key.ToLowerInvariant(), value); - } - } - } - - var client = GetConfigValue(config, "DownloadClient", ""); - - if (string.IsNullOrWhiteSpace(client)) - { - return; - } - - if (client.Equals("sabnzbd", StringComparison.InvariantCultureIgnoreCase)) - { - var settings = new ClientSettingsForMigration - { - Host = GetConfigValue(config, "SabHost", "localhost"), - Port = GetConfigValue(config, "SabPort", 8080), - ApiKey = GetConfigValue(config, "SabApiKey", ""), - Username = GetConfigValue(config, "SabUsername", ""), - Password = GetConfigValue(config, "SabPassword", ""), - TvCategory = GetConfigValue(config, "SabTvCategory", "tv"), - RecentTvPriority = GetSabnzbdPriority(GetConfigValue(config, "NzbgetRecentTvPriority", "Default")), - OlderTvPriority = GetSabnzbdPriority(GetConfigValue(config, "NzbgetOlderTvPriority", "Default")), - UseSsl = GetConfigValue(config, "SabUseSsl", false) - }; - - AddDownloadClient(conn, tran, "Sabnzbd", "Sabnzbd", settings.ToJson(), "SabnzbdSettings", 1); - } - - else if (client.Equals("nzbget", StringComparison.InvariantCultureIgnoreCase)) - { - var settings = new ClientSettingsForMigration - { - Host = GetConfigValue(config, "NzbGetHost", "localhost"), - Port = GetConfigValue(config, "NzbgetPort", 6789), - Username = GetConfigValue(config, "NzbgetUsername", "nzbget"), - Password = GetConfigValue(config, "NzbgetPassword", ""), - TvCategory = GetConfigValue(config, "NzbgetTvCategory", "tv"), - RecentTvPriority = GetNzbgetPriority(GetConfigValue(config, "NzbgetRecentTvPriority", "Normal")), - OlderTvPriority = GetNzbgetPriority(GetConfigValue(config, "NzbgetOlderTvPriority", "Normal")), - }; - - AddDownloadClient(conn, tran, "Nzbget", "Nzbget", settings.ToJson(), "NzbgetSettings", 1); - } - - else if (client.Equals("pneumatic", StringComparison.InvariantCultureIgnoreCase)) - { - var settings = new FolderSettingsForMigration - { - Folder = GetConfigValue(config, "PneumaticFolder", "") - }; - - AddDownloadClient(conn, tran, "Pneumatic", "Pneumatic", settings.ToJson(), "FolderSettings", 1); - } - - else if (client.Equals("blackhole", StringComparison.InvariantCultureIgnoreCase)) - { - var settings = new FolderSettingsForMigration - { - Folder = GetConfigValue(config, "BlackholeFolder", "") - }; - - AddDownloadClient(conn, tran, "Blackhole", "Blackhole", settings.ToJson(), "FolderSettings", 1); - } - - DeleteOldConfigValues(conn, tran); - } - - private T GetConfigValue(Dictionary config, string key, T defaultValue) - { - key = key.ToLowerInvariant(); - - if (config.ContainsKey(key)) - { - return (T) Convert.ChangeType(config[key], typeof (T)); - } - - return defaultValue; - } - - private void AddDownloadClient(IDbConnection conn, IDbTransaction tran, string name, string implementation, string settings, - string configContract, int protocol) - { - using (IDbCommand updateCmd = conn.CreateCommand()) - { - var text = string.Format("INSERT INTO DownloadClients (Enable, Name, Implementation, Settings, ConfigContract, Protocol) VALUES (1, ?, ?, ?, ?, ?)"); - updateCmd.AddParameter(name); - updateCmd.AddParameter(implementation); - updateCmd.AddParameter(settings); - updateCmd.AddParameter(configContract); - updateCmd.AddParameter(protocol); - - updateCmd.Transaction = tran; - updateCmd.CommandText = text; - updateCmd.ExecuteNonQuery(); - } - } - - private void DeleteOldConfigValues(IDbConnection conn, IDbTransaction tran) - { - using (IDbCommand updateCmd = conn.CreateCommand()) - { - var text = "DELETE FROM Config WHERE [KEY] IN ('nzbgetusername', 'nzbgetpassword', 'nzbgethost', 'nzbgetport', " + - "'nzbgettvcategory', 'nzbgetrecenttvpriority', 'nzbgetoldertvpriority', 'sabhost', 'sabport', " + - "'sabapikey', 'sabusername', 'sabpassword', 'sabtvcategory', 'sabrecenttvpriority', " + - "'saboldertvpriority', 'sabusessl', 'downloadclient', 'blackholefolder', 'pneumaticfolder')"; - - updateCmd.Transaction = tran; - updateCmd.CommandText = text; - updateCmd.ExecuteNonQuery(); - } - } - - private int GetSabnzbdPriority(string priority) - { - return (int)Enum.Parse(typeof(SabnzbdPriorityForMigration), priority, true); - } - - private int GetNzbgetPriority(string priority) - { - return (int)Enum.Parse(typeof(NzbGetPriorityForMigration), priority, true); - } - - private class ClientSettingsForMigration - { - public string Host { get; set; } - public int Port { get; set; } - public string ApiKey { get; set; } - public string Username { get; set; } - public string Password { get; set; } - public string TvCategory { get; set; } - public int RecentTvPriority { get; set; } - public int OlderTvPriority { get; set; } - public bool UseSsl { get; set; } - } - - private class FolderSettingsForMigration - { - public string Folder { get; set; } - } - - private enum SabnzbdPriorityForMigration - { - Default = -100, - Paused = -2, - Low = -1, - Normal = 0, - High = 1, - Force = 2 - } - - private enum NzbGetPriorityForMigration - { - VeryLow = -100, - Low = -50, - Normal = 0, - High = 50, - VeryHigh = 100 - } - } -} diff --git a/src/NzbDrone.Core/Datastore/Migration/044_fix_xbmc_episode_metadata.cs b/src/NzbDrone.Core/Datastore/Migration/044_fix_xbmc_episode_metadata.cs deleted file mode 100644 index 0c645259b..000000000 --- a/src/NzbDrone.Core/Datastore/Migration/044_fix_xbmc_episode_metadata.cs +++ /dev/null @@ -1,27 +0,0 @@ -using FluentMigrator; -using NzbDrone.Core.Datastore.Migration.Framework; - -namespace NzbDrone.Core.Datastore.Migration -{ - [Migration(44)] - public class fix_xbmc_episode_metadata : NzbDroneMigrationBase - { - protected override void MainDbUpgrade() - { - //Convert Episode Metadata to proper type - Execute.Sql("UPDATE MetadataFiles " + - "SET Type = 2 " + - "WHERE Consumer = 'XbmcMetadata' " + - "AND EpisodeFileId IS NOT NULL " + - "AND Type = 4 " + - "AND RelativePath LIKE '%.nfo'"); - - //Convert Episode Images to proper type - Execute.Sql("UPDATE MetadataFiles " + - "SET Type = 5 " + - "WHERE Consumer = 'XbmcMetadata' " + - "AND EpisodeFileId IS NOT NULL " + - "AND Type = 4"); - } - } -} diff --git a/src/NzbDrone.Core/Datastore/Migration/045_add_indexes.cs b/src/NzbDrone.Core/Datastore/Migration/045_add_indexes.cs deleted file mode 100644 index 4ac4e0034..000000000 --- a/src/NzbDrone.Core/Datastore/Migration/045_add_indexes.cs +++ /dev/null @@ -1,26 +0,0 @@ -using FluentMigrator; -using NzbDrone.Core.Datastore.Migration.Framework; - -namespace NzbDrone.Core.Datastore.Migration -{ - [Migration(45)] - public class add_indexes : NzbDroneMigrationBase - { - protected override void MainDbUpgrade() - { - Create.Index().OnTable("Blacklist").OnColumn("SeriesId"); - - Create.Index().OnTable("EpisodeFiles").OnColumn("SeriesId"); - - Create.Index().OnTable("Episodes").OnColumn("EpisodeFileId"); - Create.Index().OnTable("Episodes").OnColumn("SeriesId"); - - Create.Index().OnTable("History").OnColumn("EpisodeId"); - Create.Index().OnTable("History").OnColumn("Date"); - - Create.Index().OnTable("Series").OnColumn("Path"); - Create.Index().OnTable("Series").OnColumn("CleanTitle"); - Create.Index().OnTable("Series").OnColumn("TvRageId"); - } - } -} diff --git a/src/NzbDrone.Core/Datastore/Migration/046_fix_nzb_su_url.cs b/src/NzbDrone.Core/Datastore/Migration/046_fix_nzb_su_url.cs deleted file mode 100644 index 6d5496c0a..000000000 --- a/src/NzbDrone.Core/Datastore/Migration/046_fix_nzb_su_url.cs +++ /dev/null @@ -1,16 +0,0 @@ -using FluentMigrator; -using NzbDrone.Core.Datastore.Migration.Framework; - -namespace NzbDrone.Core.Datastore.Migration -{ - [Migration(46)] - public class fix_nzb_su_url : NzbDroneMigrationBase - { - protected override void MainDbUpgrade() - { - Execute.Sql("UPDATE Indexers SET Settings = replace(Settings, '//nzb.su', '//api.nzb.su')" + - "WHERE Implementation = 'Newznab'" + - "AND Settings LIKE '%//nzb.su%'"); - } - } -} diff --git a/src/NzbDrone.Core/Datastore/Migration/047_add_published_date_blacklist_column.cs b/src/NzbDrone.Core/Datastore/Migration/047_add_published_date_blacklist_column.cs deleted file mode 100644 index a7bbc9b9b..000000000 --- a/src/NzbDrone.Core/Datastore/Migration/047_add_published_date_blacklist_column.cs +++ /dev/null @@ -1,14 +0,0 @@ -using FluentMigrator; -using NzbDrone.Core.Datastore.Migration.Framework; - -namespace NzbDrone.Core.Datastore.Migration -{ - [Migration(47)] - public class add_temporary_blacklist_columns : NzbDroneMigrationBase - { - protected override void MainDbUpgrade() - { - Alter.Table("Blacklist").AddColumn("PublishedDate").AsDateTime().Nullable(); - } - } -} diff --git a/src/NzbDrone.Core/Datastore/Migration/048_add_title_to_scenemappings.cs b/src/NzbDrone.Core/Datastore/Migration/048_add_title_to_scenemappings.cs deleted file mode 100644 index 4a2e94bbf..000000000 --- a/src/NzbDrone.Core/Datastore/Migration/048_add_title_to_scenemappings.cs +++ /dev/null @@ -1,14 +0,0 @@ -using NzbDrone.Core.Datastore.Migration.Framework; -using FluentMigrator; - -namespace NzbDrone.Core.Datastore.Migration -{ - [Migration(48)] - public class add_title_to_scenemappings : NzbDroneMigrationBase - { - protected override void MainDbUpgrade() - { - Alter.Table("SceneMappings").AddColumn("Title").AsString().Nullable(); - } - } -} diff --git a/src/NzbDrone.Core/Datastore/Migration/049_fix_dognzb_url.cs b/src/NzbDrone.Core/Datastore/Migration/049_fix_dognzb_url.cs deleted file mode 100644 index ebbe8d8c0..000000000 --- a/src/NzbDrone.Core/Datastore/Migration/049_fix_dognzb_url.cs +++ /dev/null @@ -1,16 +0,0 @@ -using FluentMigrator; -using NzbDrone.Core.Datastore.Migration.Framework; - -namespace NzbDrone.Core.Datastore.Migration -{ - [Migration(49)] - public class fix_dognzb_url : NzbDroneMigrationBase - { - protected override void MainDbUpgrade() - { - Execute.Sql("UPDATE Indexers SET Settings = replace(Settings, '//dognzb.cr', '//api.dognzb.cr')" + - "WHERE Implementation = 'Newznab'" + - "AND Settings LIKE '%//dognzb.cr%'"); - } - } -} diff --git a/src/NzbDrone.Core/Datastore/Migration/050_add_hash_to_metadata_files.cs b/src/NzbDrone.Core/Datastore/Migration/050_add_hash_to_metadata_files.cs deleted file mode 100644 index 8986a7fba..000000000 --- a/src/NzbDrone.Core/Datastore/Migration/050_add_hash_to_metadata_files.cs +++ /dev/null @@ -1,14 +0,0 @@ -using NzbDrone.Core.Datastore.Migration.Framework; -using FluentMigrator; - -namespace NzbDrone.Core.Datastore.Migration -{ - [Migration(50)] - public class add_hash_to_metadata_files : NzbDroneMigrationBase - { - protected override void MainDbUpgrade() - { - Alter.Table("MetadataFiles").AddColumn("Hash").AsString().Nullable(); - } - } -} diff --git a/src/NzbDrone.Core/Datastore/Migration/051_download_client_import.cs b/src/NzbDrone.Core/Datastore/Migration/051_download_client_import.cs deleted file mode 100644 index 549b92e59..000000000 --- a/src/NzbDrone.Core/Datastore/Migration/051_download_client_import.cs +++ /dev/null @@ -1,243 +0,0 @@ -using System; -using System.Data; -using System.Linq; -using System.Collections.Generic; -using FluentMigrator; -using Newtonsoft.Json; -using NzbDrone.Common.Extensions; -using NzbDrone.Common.Serializer; -using NzbDrone.Core.Datastore.Migration.Framework; -using System.IO; - -namespace NzbDrone.Core.Datastore.Migration -{ - [Migration(51)] - public class download_client_import : NzbDroneMigrationBase - { - protected override void MainDbUpgrade() - { - Execute.WithConnection(EnableCompletedDownloadHandlingForNewUsers); - - Execute.WithConnection(ConvertFolderSettings); - - Execute.WithConnection(AssociateImportedHistoryItems); - } - - private void EnableCompletedDownloadHandlingForNewUsers(IDbConnection conn, IDbTransaction tran) - { - using (IDbCommand cmd = conn.CreateCommand()) - { - cmd.Transaction = tran; - cmd.CommandText = @"SELECT Value FROM Config WHERE Key = 'downloadedepisodesfolder'"; - - var result = cmd.ExecuteScalar(); - - if (result == null) - { - cmd.CommandText = @"INSERT INTO Config (Key, Value) VALUES ('enablecompleteddownloadhandling', 'True')"; - cmd.ExecuteNonQuery(); - } - } - } - - private void ConvertFolderSettings(IDbConnection conn, IDbTransaction tran) - { - using (IDbCommand downloadClientsCmd = conn.CreateCommand()) - { - downloadClientsCmd.Transaction = tran; - downloadClientsCmd.CommandText = @"SELECT Value FROM Config WHERE Key = 'downloadedepisodesfolder'"; - var downloadedEpisodesFolder = downloadClientsCmd.ExecuteScalar() as string; - - downloadClientsCmd.Transaction = tran; - downloadClientsCmd.CommandText = @"SELECT Id, Implementation, Settings, ConfigContract FROM DownloadClients WHERE ConfigContract = 'FolderSettings'"; - using (IDataReader downloadClientReader = downloadClientsCmd.ExecuteReader()) - { - while (downloadClientReader.Read()) - { - var id = downloadClientReader.GetInt32(0); - var implementation = downloadClientReader.GetString(1); - var settings = downloadClientReader.GetString(2); - var configContract = downloadClientReader.GetString(3); - - var settingsJson = JsonConvert.DeserializeObject(settings) as Newtonsoft.Json.Linq.JObject; - - if (implementation == "Blackhole") - { - var newSettings = new - { - NzbFolder = settingsJson.Value("folder"), - WatchFolder = downloadedEpisodesFolder - }.ToJson(); - - using (IDbCommand updateCmd = conn.CreateCommand()) - { - updateCmd.Transaction = tran; - updateCmd.CommandText = "UPDATE DownloadClients SET Implementation = ?, Settings = ?, ConfigContract = ? WHERE Id = ?"; - updateCmd.AddParameter("UsenetBlackhole"); - updateCmd.AddParameter(newSettings); - updateCmd.AddParameter("UsenetBlackholeSettings"); - updateCmd.AddParameter(id); - - updateCmd.ExecuteNonQuery(); - } - } - else if (implementation == "Pneumatic") - { - var newSettings = new - { - NzbFolder = settingsJson.Value("folder") - }.ToJson(); - - using (IDbCommand updateCmd = conn.CreateCommand()) - { - updateCmd.Transaction = tran; - updateCmd.CommandText = "UPDATE DownloadClients SET Settings = ?, ConfigContract = ? WHERE Id = ?"; - updateCmd.AddParameter(newSettings); - updateCmd.AddParameter("PneumaticSettings"); - updateCmd.AddParameter(id); - - updateCmd.ExecuteNonQuery(); - } - } - else - { - using (IDbCommand updateCmd = conn.CreateCommand()) - { - updateCmd.Transaction = tran; - updateCmd.CommandText = "DELETE FROM DownloadClients WHERE Id = ?"; - updateCmd.AddParameter(id); - - updateCmd.ExecuteNonQuery(); - } - } - } - } - } - } - - private sealed class MigrationHistoryItem - { - public int Id { get; set; } - public int EpisodeId { get; set; } - public int SeriesId { get; set; } - public string SourceTitle { get; set; } - public DateTime Date { get; set; } - public Dictionary Data { get; set; } - public MigrationHistoryEventType EventType { get; set; } - } - - private enum MigrationHistoryEventType - { - Unknown = 0, - Grabbed = 1, - SeriesFolderImported = 2, - DownloadFolderImported = 3, - DownloadFailed = 4 - } - - private void AssociateImportedHistoryItems(IDbConnection conn, IDbTransaction tran) - { - var historyItems = new List(); - - using (IDbCommand historyCmd = conn.CreateCommand()) - { - historyCmd.Transaction = tran; - historyCmd.CommandText = @"SELECT Id, EpisodeId, SeriesId, SourceTitle, Date, Data, EventType FROM History WHERE EventType NOT NULL"; - using (IDataReader historyRead = historyCmd.ExecuteReader()) - { - while (historyRead.Read()) - { - historyItems.Add(new MigrationHistoryItem - { - Id = historyRead.GetInt32(0), - EpisodeId = historyRead.GetInt32(1), - SeriesId = historyRead.GetInt32(2), - SourceTitle = historyRead.GetString(3), - Date = historyRead.GetDateTime(4), - Data = Json.Deserialize>(historyRead.GetString(5)), - EventType = (MigrationHistoryEventType)historyRead.GetInt32(6) - }); - } - } - } - - var numHistoryItemsNotAssociated = historyItems.Count(v => v.EventType == MigrationHistoryEventType.DownloadFolderImported && - v.Data.GetValueOrDefault("downloadClientId") == null); - - if (numHistoryItemsNotAssociated == 0) - { - return; - } - - var historyItemsToAssociate = new Dictionary(); - - var historyItemsLookup = historyItems.ToLookup(v => v.EpisodeId); - - foreach (var historyItemGroup in historyItemsLookup) - { - var list = historyItemGroup.ToList(); - - for (int i = 0; i < list.Count - 1; i++) - { - var grabbedEvent = list[i]; - if (grabbedEvent.EventType != MigrationHistoryEventType.Grabbed) continue; - if (grabbedEvent.Data.GetValueOrDefault("downloadClient") == null || grabbedEvent.Data.GetValueOrDefault("downloadClientId") == null) continue; - - // Check if it is already associated with a failed/imported event. - int j; - for (j = i + 1; j < list.Count;j++) - { - if (list[j].EventType != MigrationHistoryEventType.DownloadFolderImported && - list[j].EventType != MigrationHistoryEventType.DownloadFailed) - { - continue; - } - - if (list[j].Data.ContainsKey("downloadClient") && list[j].Data["downloadClient"] == grabbedEvent.Data["downloadClient"] && - list[j].Data.ContainsKey("downloadClientId") && list[j].Data["downloadClientId"] == grabbedEvent.Data["downloadClientId"]) - { - break; - } - } - - if (j != list.Count) - { - list.RemoveAt(j); - list.RemoveAt(i--); - continue; - } - - var importedEvent = list[i + 1]; - if (importedEvent.EventType != MigrationHistoryEventType.DownloadFolderImported) continue; - - var droppedPath = importedEvent.Data.GetValueOrDefault("droppedPath"); - if (droppedPath != null && new FileInfo(droppedPath).Directory.Name == grabbedEvent.SourceTitle) - { - historyItemsToAssociate[importedEvent] = grabbedEvent; - - list.RemoveAt(i + 1); - list.RemoveAt(i--); - } - } - } - - foreach (var pair in historyItemsToAssociate) - { - using (IDbCommand updateHistoryCmd = conn.CreateCommand()) - { - pair.Key.Data["downloadClient"] = pair.Value.Data["downloadClient"]; - pair.Key.Data["downloadClientId"] = pair.Value.Data["downloadClientId"]; - - updateHistoryCmd.Transaction = tran; - updateHistoryCmd.CommandText = "UPDATE History SET Data = ? WHERE Id = ?"; - updateHistoryCmd.AddParameter(pair.Key.Data.ToJson()); - updateHistoryCmd.AddParameter(pair.Key.Id); - - updateHistoryCmd.ExecuteNonQuery(); - } - } - - _logger.Info("Updated old History items. {0}/{1} old ImportedEvents were associated with GrabbedEvents.", historyItemsToAssociate.Count, numHistoryItemsNotAssociated); - } - } -} diff --git a/src/NzbDrone.Core/Datastore/Migration/052_add_columns_for_anime.cs b/src/NzbDrone.Core/Datastore/Migration/052_add_columns_for_anime.cs deleted file mode 100644 index e781ca010..000000000 --- a/src/NzbDrone.Core/Datastore/Migration/052_add_columns_for_anime.cs +++ /dev/null @@ -1,20 +0,0 @@ -using NzbDrone.Core.Datastore.Migration.Framework; -using FluentMigrator; - -namespace NzbDrone.Core.Datastore.Migration -{ - [Migration(52)] - public class add_columns_for_anime : NzbDroneMigrationBase - { - protected override void MainDbUpgrade() - { - //Support XEM names - Alter.Table("SceneMappings").AddColumn("Type").AsString().Nullable(); - Execute.Sql("DELETE FROM SceneMappings"); - - //Add AnimeEpisodeFormat (set to Stardard Episode format for now) - Alter.Table("NamingConfig").AddColumn("AnimeEpisodeFormat").AsString().Nullable(); - Execute.Sql("UPDATE NamingConfig SET AnimeEpisodeFormat = StandardEpisodeFormat"); - } - } -} diff --git a/src/NzbDrone.Core/Datastore/Migration/053_add_series_sorttitle.cs b/src/NzbDrone.Core/Datastore/Migration/053_add_series_sorttitle.cs deleted file mode 100644 index 46e1b8ce3..000000000 --- a/src/NzbDrone.Core/Datastore/Migration/053_add_series_sorttitle.cs +++ /dev/null @@ -1,46 +0,0 @@ -using System.Data; -using FluentMigrator; -using NzbDrone.Core.Datastore.Migration.Framework; - -namespace NzbDrone.Core.Datastore.Migration -{ - [Migration(53)] - public class add_series_sorttitle : NzbDroneMigrationBase - { - protected override void MainDbUpgrade() - { - Create.Column("SortTitle").OnTable("Series").AsString().Nullable(); - - Execute.WithConnection(SetSortTitles); - } - - private void SetSortTitles(IDbConnection conn, IDbTransaction tran) - { - using (IDbCommand getSeriesCmd = conn.CreateCommand()) - { - getSeriesCmd.Transaction = tran; - getSeriesCmd.CommandText = @"SELECT Id, Title FROM Series"; - using (IDataReader seriesReader = getSeriesCmd.ExecuteReader()) - { - while (seriesReader.Read()) - { - var id = seriesReader.GetInt32(0); - var title = seriesReader.GetString(1); - - var sortTitle = Parser.Parser.NormalizeTitle(title).ToLower(); - - using (IDbCommand updateCmd = conn.CreateCommand()) - { - updateCmd.Transaction = tran; - updateCmd.CommandText = "UPDATE Series SET SortTitle = ? WHERE Id = ?"; - updateCmd.AddParameter(sortTitle); - updateCmd.AddParameter(id); - - updateCmd.ExecuteNonQuery(); - } - } - } - } - } - } -} diff --git a/src/NzbDrone.Core/Datastore/Migration/054_rename_profiles.cs b/src/NzbDrone.Core/Datastore/Migration/054_rename_profiles.cs deleted file mode 100644 index e665c14a4..000000000 --- a/src/NzbDrone.Core/Datastore/Migration/054_rename_profiles.cs +++ /dev/null @@ -1,31 +0,0 @@ -using FluentMigrator; -using NzbDrone.Core.Datastore.Migration.Framework; - -namespace NzbDrone.Core.Datastore.Migration -{ - [Migration(54)] - public class rename_profiles : NzbDroneMigrationBase - { - protected override void MainDbUpgrade() - { - Rename.Table("QualityProfiles").To("Profiles"); - - Alter.Table("Profiles").AddColumn("Language").AsInt32().Nullable(); - Alter.Table("Profiles").AddColumn("GrabDelay").AsInt32().Nullable(); - Alter.Table("Profiles").AddColumn("GrabDelayMode").AsInt32().Nullable(); - Execute.Sql("UPDATE Profiles SET Language = 1, GrabDelay = 0, GrabDelayMode = 0"); - - //Rename QualityProfileId in Series - Alter.Table("Series").AddColumn("ProfileId").AsInt32().Nullable(); - Execute.Sql("UPDATE Series SET ProfileId = QualityProfileId"); - - //Add HeldReleases - Create.TableForModel("PendingReleases") - .WithColumn("SeriesId").AsInt32() - .WithColumn("Title").AsString() - .WithColumn("Added").AsDateTime() - .WithColumn("ParsedEpisodeInfo").AsString() - .WithColumn("Release").AsString(); - } - } -} diff --git a/src/NzbDrone.Core/Datastore/Migration/055_drop_old_profile_columns.cs b/src/NzbDrone.Core/Datastore/Migration/055_drop_old_profile_columns.cs deleted file mode 100644 index 3f13f5e84..000000000 --- a/src/NzbDrone.Core/Datastore/Migration/055_drop_old_profile_columns.cs +++ /dev/null @@ -1,14 +0,0 @@ -using FluentMigrator; -using NzbDrone.Core.Datastore.Migration.Framework; - -namespace NzbDrone.Core.Datastore.Migration -{ - [Migration(55)] - public class drop_old_profile_columns : NzbDroneMigrationBase - { - protected override void MainDbUpgrade() - { - Delete.Column("QualityProfileId").FromTable("Series"); - } - } -} diff --git a/src/NzbDrone.Core/Datastore/Migration/056_add_mediainfo_to_episodefile.cs b/src/NzbDrone.Core/Datastore/Migration/056_add_mediainfo_to_episodefile.cs deleted file mode 100644 index 42ad68493..000000000 --- a/src/NzbDrone.Core/Datastore/Migration/056_add_mediainfo_to_episodefile.cs +++ /dev/null @@ -1,14 +0,0 @@ -using FluentMigrator; -using NzbDrone.Core.Datastore.Migration.Framework; - -namespace NzbDrone.Core.Datastore.Migration -{ - [Migration(56)] - public class add_mediainfo_to_episodefile : NzbDroneMigrationBase - { - protected override void MainDbUpgrade() - { - Alter.Table("EpisodeFiles").AddColumn("MediaInfo").AsString().Nullable(); - } - } -} diff --git a/src/NzbDrone.Core/Datastore/Migration/057_convert_episode_file_path_to_relative.cs b/src/NzbDrone.Core/Datastore/Migration/057_convert_episode_file_path_to_relative.cs deleted file mode 100644 index a1bf307fd..000000000 --- a/src/NzbDrone.Core/Datastore/Migration/057_convert_episode_file_path_to_relative.cs +++ /dev/null @@ -1,48 +0,0 @@ -using System.Data; -using System.IO; -using FluentMigrator; -using NzbDrone.Core.Datastore.Migration.Framework; - -namespace NzbDrone.Core.Datastore.Migration -{ - [Migration(57)] - public class convert_episode_file_path_to_relative : NzbDroneMigrationBase - { - protected override void MainDbUpgrade() - { - Create.Column("RelativePath").OnTable("EpisodeFiles").AsString().Nullable(); - - //TODO: Add unique contraint for series ID and Relative Path - //TODO: Warn if multiple series share the same path - - Execute.WithConnection(UpdateRelativePaths); - } - - private void UpdateRelativePaths(IDbConnection conn, IDbTransaction tran) - { - using (IDbCommand getSeriesCmd = conn.CreateCommand()) - { - getSeriesCmd.Transaction = tran; - getSeriesCmd.CommandText = @"SELECT Id, Path FROM Series"; - using (IDataReader seriesReader = getSeriesCmd.ExecuteReader()) - { - while (seriesReader.Read()) - { - var seriesId = seriesReader.GetInt32(0); - var seriesPath = seriesReader.GetString(1) + Path.DirectorySeparatorChar; - - using (IDbCommand updateCmd = conn.CreateCommand()) - { - updateCmd.Transaction = tran; - updateCmd.CommandText = "UPDATE EpisodeFiles SET RelativePath = REPLACE(Path, ?, '') WHERE SeriesId = ?"; - updateCmd.AddParameter(seriesPath); - updateCmd.AddParameter(seriesId); - - updateCmd.ExecuteNonQuery(); - } - } - } - } - } - } -} diff --git a/src/NzbDrone.Core/Datastore/Migration/058_drop_epsiode_file_path.cs b/src/NzbDrone.Core/Datastore/Migration/058_drop_epsiode_file_path.cs deleted file mode 100644 index d2bfbcfd9..000000000 --- a/src/NzbDrone.Core/Datastore/Migration/058_drop_epsiode_file_path.cs +++ /dev/null @@ -1,14 +0,0 @@ -using FluentMigrator; -using NzbDrone.Core.Datastore.Migration.Framework; - -namespace NzbDrone.Core.Datastore.Migration -{ - [Migration(58)] - public class drop_episode_file_path : NzbDroneMigrationBase - { - protected override void MainDbUpgrade() - { - Delete.Column("Path").FromTable("EpisodeFiles"); - } - } -} diff --git a/src/NzbDrone.Core/Datastore/Migration/059_add_enable_options_to_indexers.cs b/src/NzbDrone.Core/Datastore/Migration/059_add_enable_options_to_indexers.cs deleted file mode 100644 index 0905578c5..000000000 --- a/src/NzbDrone.Core/Datastore/Migration/059_add_enable_options_to_indexers.cs +++ /dev/null @@ -1,19 +0,0 @@ -using FluentMigrator; -using NzbDrone.Core.Datastore.Migration.Framework; - -namespace NzbDrone.Core.Datastore.Migration -{ - [Migration(59)] - public class add_enable_options_to_indexers : NzbDroneMigrationBase - { - protected override void MainDbUpgrade() - { - Alter.Table("Indexers") - .AddColumn("EnableRss").AsBoolean().Nullable() - .AddColumn("EnableSearch").AsBoolean().Nullable(); - - Execute.Sql("UPDATE Indexers SET EnableRss = Enable, EnableSearch = Enable"); - Execute.Sql("UPDATE Indexers SET EnableSearch = 0 WHERE Implementation = 'Wombles'"); - } - } -} diff --git a/src/NzbDrone.Core/Datastore/Migration/060_remove_enable_from_indexers.cs b/src/NzbDrone.Core/Datastore/Migration/060_remove_enable_from_indexers.cs deleted file mode 100644 index 05376c1d2..000000000 --- a/src/NzbDrone.Core/Datastore/Migration/060_remove_enable_from_indexers.cs +++ /dev/null @@ -1,15 +0,0 @@ -using FluentMigrator; -using NzbDrone.Core.Datastore.Migration.Framework; - -namespace NzbDrone.Core.Datastore.Migration -{ - [Migration(60)] - public class remove_enable_from_indexers : NzbDroneMigrationBase - { - protected override void MainDbUpgrade() - { - Delete.Column("Enable").FromTable("Indexers"); - Delete.Column("Protocol").FromTable("DownloadClients"); - } - } -} diff --git a/src/NzbDrone.Core/Datastore/Migration/061_clear_bad_scene_names.cs b/src/NzbDrone.Core/Datastore/Migration/061_clear_bad_scene_names.cs deleted file mode 100644 index 4bc2275dc..000000000 --- a/src/NzbDrone.Core/Datastore/Migration/061_clear_bad_scene_names.cs +++ /dev/null @@ -1,22 +0,0 @@ -using FluentMigrator; -using NzbDrone.Core.Datastore.Migration.Framework; - -namespace NzbDrone.Core.Datastore.Migration -{ - [Migration(61)] - public class clear_bad_scene_names : NzbDroneMigrationBase - { - protected override void MainDbUpgrade() - { - Execute.Sql("UPDATE [EpisodeFiles] " + - "SET ReleaseGroup = NULL , SceneName = NULL " + - "WHERE " + - " ReleaseGroup IS NULL " + - " OR SceneName IS NULL " + - " OR ReleaseGroup =='DRONE' " + - " OR LENGTH(SceneName) <10 " + - " OR LENGTH(ReleaseGroup) > 20 " + - " OR SceneName NOT LIKE '%.%'"); - } - } -} diff --git a/src/NzbDrone.Core/Datastore/Migration/062_convert_quality_models.cs b/src/NzbDrone.Core/Datastore/Migration/062_convert_quality_models.cs deleted file mode 100644 index cc9cea68a..000000000 --- a/src/NzbDrone.Core/Datastore/Migration/062_convert_quality_models.cs +++ /dev/null @@ -1,83 +0,0 @@ -using System.Collections.Generic; -using System.Data; -using FluentMigrator; -using NzbDrone.Common.Serializer; -using NzbDrone.Core.Datastore.Migration.Framework; -using NzbDrone.Core.Qualities; - -namespace NzbDrone.Core.Datastore.Migration -{ - [Migration(62)] - public class convert_quality_models : NzbDroneMigrationBase - { - protected override void MainDbUpgrade() - { - Execute.WithConnection(ConvertQualityModels); - } - - private void ConvertQualityModels(IDbConnection conn, IDbTransaction tran) - { - ConvertQualityModelsOnTable(conn, tran, "EpisodeFiles"); - ConvertQualityModelsOnTable(conn, tran, "Blacklist"); - ConvertQualityModelsOnTable(conn, tran, "History"); - } - - private void ConvertQualityModelsOnTable(IDbConnection conn, IDbTransaction tran, string tableName) - { - var qualitiesToUpdate = new Dictionary(); - - using (IDbCommand qualityModelCmd = conn.CreateCommand()) - { - qualityModelCmd.Transaction = tran; - qualityModelCmd.CommandText = @"SELECT Distinct Quality FROM " + tableName; - - using (IDataReader qualityModelReader = qualityModelCmd.ExecuteReader()) - { - while (qualityModelReader.Read()) - { - var qualityJson = qualityModelReader.GetString(0); - - LegacyQualityModel062 quality; - - if (!Json.TryDeserialize(qualityJson, out quality)) - { - continue; - } - - var newQualityModel = new QualityModel062 { Quality = quality.Quality, Revision = new Revision() }; - if (quality.Proper) - newQualityModel.Revision.Version = 2; - var newQualityJson = newQualityModel.ToJson(); - - qualitiesToUpdate.Add(qualityJson, newQualityJson); - } - } - } - - foreach (var quality in qualitiesToUpdate) - { - using (IDbCommand updateCmd = conn.CreateCommand()) - { - updateCmd.Transaction = tran; - updateCmd.CommandText = "UPDATE " + tableName + " SET Quality = ? WHERE Quality = ?"; - updateCmd.AddParameter(quality.Value); - updateCmd.AddParameter(quality.Key); - - updateCmd.ExecuteNonQuery(); - } - } - } - - private class LegacyQualityModel062 - { - public int Quality { get; set; } - public bool Proper { get; set; } - } - - private class QualityModel062 - { - public int Quality { get; set; } - public Revision Revision { get; set; } - } - } -} diff --git a/src/NzbDrone.Core/Datastore/Migration/063_add_remotepathmappings.cs b/src/NzbDrone.Core/Datastore/Migration/063_add_remotepathmappings.cs deleted file mode 100644 index 2f8c6b755..000000000 --- a/src/NzbDrone.Core/Datastore/Migration/063_add_remotepathmappings.cs +++ /dev/null @@ -1,17 +0,0 @@ -using FluentMigrator; -using NzbDrone.Core.Datastore.Migration.Framework; - -namespace NzbDrone.Core.Datastore.Migration -{ - [Migration(63)] - public class add_remotepathmappings : NzbDroneMigrationBase - { - protected override void MainDbUpgrade() - { - Create.TableForModel("RemotePathMappings") - .WithColumn("Host").AsString() - .WithColumn("RemotePath").AsString() - .WithColumn("LocalPath").AsString(); - } - } -} diff --git a/src/NzbDrone.Core/Datastore/Migration/064_add_remove_method_from_logs.cs b/src/NzbDrone.Core/Datastore/Migration/064_add_remove_method_from_logs.cs deleted file mode 100644 index 2fd04ea97..000000000 --- a/src/NzbDrone.Core/Datastore/Migration/064_add_remove_method_from_logs.cs +++ /dev/null @@ -1,14 +0,0 @@ -using FluentMigrator; -using NzbDrone.Core.Datastore.Migration.Framework; - -namespace NzbDrone.Core.Datastore.Migration -{ - [Migration(64)] - public class remove_method_from_logs : NzbDroneMigrationBase - { - protected override void LogDbUpgrade() - { - Delete.Column("Method").FromTable("Logs"); - } - } -} diff --git a/src/NzbDrone.Core/Datastore/Migration/065_make_scene_numbering_nullable.cs b/src/NzbDrone.Core/Datastore/Migration/065_make_scene_numbering_nullable.cs deleted file mode 100644 index 7936f04dd..000000000 --- a/src/NzbDrone.Core/Datastore/Migration/065_make_scene_numbering_nullable.cs +++ /dev/null @@ -1,16 +0,0 @@ -using FluentMigrator; -using NzbDrone.Core.Datastore.Migration.Framework; - -namespace NzbDrone.Core.Datastore.Migration -{ - [Migration(65)] - public class make_scene_numbering_nullable : NzbDroneMigrationBase - { - protected override void MainDbUpgrade() - { - Execute.Sql("UPDATE Episodes SET AbsoluteEpisodeNumber = NULL WHERE AbsoluteEpisodeNumber = 0"); - Execute.Sql("UPDATE Episodes SET SceneAbsoluteEpisodeNumber = NULL WHERE SceneAbsoluteEpisodeNumber = 0"); - Execute.Sql("UPDATE Episodes SET SceneSeasonNumber = NULL, SceneEpisodeNumber = NULL WHERE SceneSeasonNumber = 0 AND SceneEpisodeNumber = 0"); - } - } -} diff --git a/src/NzbDrone.Core/Datastore/Migration/066_add_tags.cs b/src/NzbDrone.Core/Datastore/Migration/066_add_tags.cs deleted file mode 100644 index 7a0c09838..000000000 --- a/src/NzbDrone.Core/Datastore/Migration/066_add_tags.cs +++ /dev/null @@ -1,24 +0,0 @@ -using FluentMigrator; -using NzbDrone.Core.Datastore.Migration.Framework; - -namespace NzbDrone.Core.Datastore.Migration -{ - [Migration(66)] - public class add_tags : NzbDroneMigrationBase - { - protected override void MainDbUpgrade() - { - Create.TableForModel("Tags") - .WithColumn("Label").AsString().NotNullable(); - - Alter.Table("Series") - .AddColumn("Tags").AsString().Nullable(); - - Alter.Table("Notifications") - .AddColumn("Tags").AsString().Nullable(); - - Execute.Sql("UPDATE Series SET Tags = '[]'"); - Execute.Sql("UPDATE Notifications SET Tags = '[]'"); - } - } -} diff --git a/src/NzbDrone.Core/Datastore/Migration/067_add_added_to_series.cs b/src/NzbDrone.Core/Datastore/Migration/067_add_added_to_series.cs deleted file mode 100644 index cb0923e18..000000000 --- a/src/NzbDrone.Core/Datastore/Migration/067_add_added_to_series.cs +++ /dev/null @@ -1,14 +0,0 @@ -using FluentMigrator; -using NzbDrone.Core.Datastore.Migration.Framework; - -namespace NzbDrone.Core.Datastore.Migration -{ - [Migration(67)] - public class add_added_to_series : NzbDroneMigrationBase - { - protected override void MainDbUpgrade() - { - Alter.Table("Series").AddColumn("Added").AsDateTime().Nullable(); - } - } -} diff --git a/src/NzbDrone.Core/Datastore/Migration/068_add_release_restrictions.cs b/src/NzbDrone.Core/Datastore/Migration/068_add_release_restrictions.cs deleted file mode 100644 index a7cc93e0c..000000000 --- a/src/NzbDrone.Core/Datastore/Migration/068_add_release_restrictions.cs +++ /dev/null @@ -1,49 +0,0 @@ -using System.Data; -using FluentMigrator; -using NzbDrone.Core.Datastore.Migration.Framework; - -namespace NzbDrone.Core.Datastore.Migration -{ - [Migration(68)] - public class add_release_restrictions : NzbDroneMigrationBase - { - protected override void MainDbUpgrade() - { - Create.TableForModel("Restrictions") - .WithColumn("Required").AsString().Nullable() - .WithColumn("Preferred").AsString().Nullable() - .WithColumn("Ignored").AsString().Nullable() - .WithColumn("Tags").AsString().NotNullable(); - - Execute.WithConnection(ConvertRestrictions); - Execute.Sql("DELETE FROM Config WHERE [Key] = 'releaserestrictions'"); - } - - private void ConvertRestrictions(IDbConnection conn, IDbTransaction tran) - { - using (IDbCommand getRestictionsCmd = conn.CreateCommand()) - { - getRestictionsCmd.Transaction = tran; - getRestictionsCmd.CommandText = @"SELECT [Value] FROM Config WHERE [Key] = 'releaserestrictions'"; - - using (IDataReader configReader = getRestictionsCmd.ExecuteReader()) - { - while (configReader.Read()) - { - var restrictions = configReader.GetString(0); - restrictions = restrictions.Replace("\n", ","); - - using (IDbCommand insertCmd = conn.CreateCommand()) - { - insertCmd.Transaction = tran; - insertCmd.CommandText = "INSERT INTO Restrictions (Ignored, Tags) VALUES (?, '[]')"; - insertCmd.AddParameter(restrictions); - - insertCmd.ExecuteNonQuery(); - } - } - } - } - } - } -} diff --git a/src/NzbDrone.Core/Datastore/Migration/069_quality_proper.cs b/src/NzbDrone.Core/Datastore/Migration/069_quality_proper.cs deleted file mode 100644 index 9db5f2955..000000000 --- a/src/NzbDrone.Core/Datastore/Migration/069_quality_proper.cs +++ /dev/null @@ -1,99 +0,0 @@ -using System.Data; -using System.Linq; -using System.Text.RegularExpressions; -using FluentMigrator; -using NzbDrone.Core.Datastore.Migration.Framework; - -namespace NzbDrone.Core.Datastore.Migration -{ - [Migration(69)] - public class quality_proper : NzbDroneMigrationBase - { - protected override void MainDbUpgrade() - { - Execute.WithConnection(ConvertQualityTitle); - } - - private static readonly Regex QualityTitleRegex = new Regex(@"\{(?[- ._\[(]*)(?(?:quality)(?:(?[- ._]+)(?:title))?)(?[- ._)\]]*)\}", - RegexOptions.Compiled | RegexOptions.IgnoreCase); - - private void ConvertQualityTitle(IDbConnection conn, IDbTransaction tran) - { - using (IDbCommand namingConfigCmd = conn.CreateCommand()) - { - namingConfigCmd.Transaction = tran; - namingConfigCmd.CommandText = @"SELECT StandardEpisodeFormat, DailyEpisodeFormat, AnimeEpisodeFormat FROM NamingConfig LIMIT 1"; - - using (IDataReader configReader = namingConfigCmd.ExecuteReader()) - { - while (configReader.Read()) - { - var currentStandard = configReader.GetString(0); - var currentDaily = configReader.GetString(1); - var currentAnime = configReader.GetString(2); - - var newStandard = GetNewFormat(currentStandard); - var newDaily = GetNewFormat(currentDaily); - var newAnime = GetNewFormat(currentAnime); - - using (IDbCommand updateCmd = conn.CreateCommand()) - { - updateCmd.Transaction = tran; - - updateCmd.CommandText = "UPDATE NamingConfig SET StandardEpisodeFormat = ?, DailyEpisodeFormat = ?, AnimeEpisodeFormat = ?"; - updateCmd.AddParameter(newStandard); - updateCmd.AddParameter(newDaily); - updateCmd.AddParameter(newAnime); - - updateCmd.ExecuteNonQuery(); - } - } - } - } - } - - private string GetNewFormat(string currentFormat) - { - var matches = QualityTitleRegex.Matches(currentFormat); - var result = currentFormat; - - foreach (Match match in matches) - { - var tokenMatch = GetTokenMatch(match); - var qualityFullToken = string.Format("Quality{0}Full", tokenMatch.Separator); ; - - if (tokenMatch.Token.All(t => !char.IsLetter(t) || char.IsLower(t))) - { - qualityFullToken = string.Format("quality{0}full", tokenMatch.Separator); - } - else if (tokenMatch.Token.All(t => !char.IsLetter(t) || char.IsUpper(t))) - { - qualityFullToken = string.Format("QUALITY{0}FULL", tokenMatch.Separator); - } - - result = result.Replace(match.Groups["token"].Value, qualityFullToken); - } - - return result; - } - - private TokenMatch69 GetTokenMatch(Match match) - { - return new TokenMatch69 - { - Prefix = match.Groups["prefix"].Value, - Token = match.Groups["token"].Value, - Separator = match.Groups["separator"].Value, - Suffix = match.Groups["suffix"].Value, - }; - } - - private class TokenMatch69 - { - public string Prefix { get; set; } - public string Token { get; set; } - public string Separator { get; set; } - public string Suffix { get; set; } - } - } -} diff --git a/src/NzbDrone.Core/Datastore/Migration/070_delay_profile.cs b/src/NzbDrone.Core/Datastore/Migration/070_delay_profile.cs deleted file mode 100644 index 1c9c7e58b..000000000 --- a/src/NzbDrone.Core/Datastore/Migration/070_delay_profile.cs +++ /dev/null @@ -1,182 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Data; -using System.Linq; -using FluentMigrator; -using NzbDrone.Common.Extensions; -using NzbDrone.Common.Serializer; -using NzbDrone.Core.Datastore.Migration.Framework; - -namespace NzbDrone.Core.Datastore.Migration -{ - [Migration(70)] - public class delay_profile : NzbDroneMigrationBase - { - protected override void MainDbUpgrade() - { - Create.TableForModel("DelayProfiles") - .WithColumn("EnableUsenet").AsBoolean().NotNullable() - .WithColumn("EnableTorrent").AsBoolean().NotNullable() - .WithColumn("PreferredProtocol").AsInt32().NotNullable() - .WithColumn("UsenetDelay").AsInt32().NotNullable() - .WithColumn("TorrentDelay").AsInt32().NotNullable() - .WithColumn("Order").AsInt32().NotNullable() - .WithColumn("Tags").AsString().NotNullable(); - - Insert.IntoTable("DelayProfiles").Row(new - { - EnableUsenet = 1, - EnableTorrent = 1, - PreferredProtocol = 1, - UsenetDelay = 0, - TorrentDelay = 0, - Order = int.MaxValue, - Tags = "[]" - }); - - Execute.WithConnection(ConvertProfile); - - Delete.Column("GrabDelay").FromTable("Profiles"); - Delete.Column("GrabDelayMode").FromTable("Profiles"); - } - - private void ConvertProfile(IDbConnection conn, IDbTransaction tran) - { - var profiles = GetProfiles(conn, tran); - var order = 1; - - foreach (var profileClosure in profiles.DistinctBy(p => p.GrabDelay)) - { - var profile = profileClosure; - if (profile.GrabDelay == 0) continue; - - var tag = string.Format("delay-{0}", profile.GrabDelay); - var tagId = InsertTag(conn, tran, tag); - var tags = string.Format("[{0}]", tagId); - - using (IDbCommand insertDelayProfileCmd = conn.CreateCommand()) - { - insertDelayProfileCmd.Transaction = tran; - insertDelayProfileCmd.CommandText = "INSERT INTO DelayProfiles (EnableUsenet, EnableTorrent, PreferredProtocol, TorrentDelay, UsenetDelay, [Order], Tags) VALUES (1, 1, 1, 0, ?, ?, ?)"; - insertDelayProfileCmd.AddParameter(profile.GrabDelay); - insertDelayProfileCmd.AddParameter(order); - insertDelayProfileCmd.AddParameter(tags); - - insertDelayProfileCmd.ExecuteNonQuery(); - } - - var matchingProfileIds = profiles.Where(p => p.GrabDelay == profile.GrabDelay) - .Select(p => p.Id); - - UpdateSeries(conn, tran, matchingProfileIds, tagId); - - order++; - } - } - - private List GetProfiles(IDbConnection conn, IDbTransaction tran) - { - var profiles = new List(); - - using (IDbCommand getProfilesCmd = conn.CreateCommand()) - { - getProfilesCmd.Transaction = tran; - getProfilesCmd.CommandText = @"SELECT Id, GrabDelay FROM Profiles"; - - using (IDataReader profileReader = getProfilesCmd.ExecuteReader()) - { - while (profileReader.Read()) - { - var id = profileReader.GetInt32(0); - var delay = profileReader.GetInt32(1); - - profiles.Add(new Profile69 - { - Id = id, - GrabDelay = delay * 60 - }); - } - } - } - - return profiles; - } - - private int InsertTag(IDbConnection conn, IDbTransaction tran, string tagLabel) - { - using (IDbCommand insertCmd = conn.CreateCommand()) - { - insertCmd.Transaction = tran; - insertCmd.CommandText = @"INSERT INTO Tags (Label) VALUES (?); SELECT last_insert_rowid()"; - insertCmd.AddParameter(tagLabel); - - var id = insertCmd.ExecuteScalar(); - - return Convert.ToInt32(id); - } - } - - private void UpdateSeries(IDbConnection conn, IDbTransaction tran, IEnumerable profileIds, int tagId) - { - using (IDbCommand getSeriesCmd = conn.CreateCommand()) - { - getSeriesCmd.Transaction = tran; - getSeriesCmd.CommandText = "SELECT Id, Tags FROM Series WHERE ProfileId IN (?)"; - getSeriesCmd.AddParameter(string.Join(",", profileIds)); - - using (IDataReader seriesReader = getSeriesCmd.ExecuteReader()) - { - while (seriesReader.Read()) - { - var id = seriesReader.GetInt32(0); - var tagString = seriesReader.GetString(1); - - var tags = Json.Deserialize>(tagString); - tags.Add(tagId); - - using (IDbCommand updateSeriesCmd = conn.CreateCommand()) - { - updateSeriesCmd.Transaction = tran; - updateSeriesCmd.CommandText = "UPDATE Series SET Tags = ? WHERE Id = ?"; - updateSeriesCmd.AddParameter(tags.ToJson()); - updateSeriesCmd.AddParameter(id); - - updateSeriesCmd.ExecuteNonQuery(); - } - } - } - } - } - } - - public class Profile69 - { - public int Id { get; set; } - public int GrabDelay { get; set; } - } - - public class Series69 - { - public int Id { get; set; } - public List Tags { get; set; } - public DateTime? LastInfoSync { get; set; } - } - - public class Tag69 - { - public int Id { get; set; } - public string Label { get; set; } - } - - public class DelayProfile70 - { - public int Id { get; set; } - public bool EnableUsenet { get; set; } - public bool EnableTorrent { get; set; } - public int PreferredProtocol { get; set; } - public int UsenetDelay { get; set; } - public int TorrentDelay { get; set; } - public int Order { get; set; } - public List Tags { get; set; } - } -} diff --git a/src/NzbDrone.Core/Datastore/Migration/071_unknown_quality_in_profile.cs b/src/NzbDrone.Core/Datastore/Migration/071_unknown_quality_in_profile.cs deleted file mode 100644 index a033e8410..000000000 --- a/src/NzbDrone.Core/Datastore/Migration/071_unknown_quality_in_profile.cs +++ /dev/null @@ -1,180 +0,0 @@ -using System.Collections.Generic; -using System.Data; -using System.Linq; -using FluentMigrator; -using NzbDrone.Common.Serializer; -using NzbDrone.Core.Datastore.Migration.Framework; - -namespace NzbDrone.Core.Datastore.Migration -{ - [Migration(71)] - public class unknown_quality_in_profile : NzbDroneMigrationBase - { - protected override void MainDbUpgrade() - { - Delete.Column("Weight").FromTable("QualityDefinitions"); - - Execute.WithConnection(ConvertProfile); - } - - private void ConvertProfile(IDbConnection conn, IDbTransaction tran) - { - var updater = new ProfileUpdater70(conn, tran); - updater.PrependQuality(0); - updater.Commit(); - } - } - public class Profile70 - { - public int Id { get; set; } - public string Name { get; set; } - public int Cutoff { get; set; } - public List Items { get; set; } - public int Language { get; set; } - } - - public class ProfileItem70 - { - public int Quality { get; set; } - public bool Allowed { get; set; } - } - - public class ProfileUpdater70 - { - private readonly IDbConnection _connection; - private readonly IDbTransaction _transaction; - - private List _profiles; - private HashSet _changedProfiles = new HashSet(); - - public ProfileUpdater70(IDbConnection conn, IDbTransaction tran) - { - _connection = conn; - _transaction = tran; - - _profiles = GetProfiles(); - } - - public void Commit() - { - foreach (var profile in _changedProfiles) - { - using (var updateProfileCmd = _connection.CreateCommand()) - { - updateProfileCmd.Transaction = _transaction; - updateProfileCmd.CommandText = "UPDATE Profiles SET Name = ?, Cutoff = ?, Items = ?, Language = ? WHERE Id = ?"; - updateProfileCmd.AddParameter(profile.Name); - updateProfileCmd.AddParameter(profile.Cutoff); - updateProfileCmd.AddParameter(profile.Items.ToJson()); - updateProfileCmd.AddParameter(profile.Language); - updateProfileCmd.AddParameter(profile.Id); - - updateProfileCmd.ExecuteNonQuery(); - } - } - - _changedProfiles.Clear(); - } - - public void PrependQuality(int quality) - { - foreach (var profile in _profiles) - { - if (profile.Items.Any(v => v.Quality == quality)) continue; - - profile.Items.Insert(0, new ProfileItem70 - { - Quality = quality, - Allowed = false - }); - - _changedProfiles.Add(profile); - } - } - - public void AppendQuality(int quality) - { - foreach (var profile in _profiles) - { - if (profile.Items.Any(v => v.Quality == quality)) continue; - - profile.Items.Add(new ProfileItem70 - { - Quality = quality, - Allowed = false - }); - - _changedProfiles.Add(profile); - } - } - - public void SplitQualityPrepend(int find, int quality) - { - foreach (var profile in _profiles) - { - if (profile.Items.Any(v => v.Quality == quality)) continue; - - var findIndex = profile.Items.FindIndex(v => v.Quality == find); - - profile.Items.Insert(findIndex, new ProfileItem70 - { - Quality = quality, - Allowed = profile.Items[findIndex].Allowed - }); - - if (profile.Cutoff == find) - { - profile.Cutoff = quality; - } - - _changedProfiles.Add(profile); - } - } - - public void SplitQualityAppend(int find, int quality) - { - foreach (var profile in _profiles) - { - if (profile.Items.Any(v => v.Quality == quality)) continue; - - var findIndex = profile.Items.FindIndex(v => v.Quality == find); - - profile.Items.Insert(findIndex + 1, new ProfileItem70 - { - Quality = quality, - Allowed = false - }); - - _changedProfiles.Add(profile); - } - } - - private List GetProfiles() - { - var profiles = new List(); - - using (var getProfilesCmd = _connection.CreateCommand()) - { - getProfilesCmd.Transaction = _transaction; - getProfilesCmd.CommandText = @"SELECT Id, Name, Cutoff, Items, Language FROM Profiles"; - - using (var profileReader = getProfilesCmd.ExecuteReader()) - { - while (profileReader.Read()) - { - profiles.Add(new Profile70 - { - Id = profileReader.GetInt32(0), - Name = profileReader.GetString(1), - Cutoff = profileReader.GetInt32(2), - Items = Json.Deserialize>(profileReader.GetString(3)), - Language = profileReader.GetInt32(4) - }); - } - } - } - - return profiles; - } - } -} diff --git a/src/NzbDrone.Core/Datastore/Migration/072_history_grabid.cs b/src/NzbDrone.Core/Datastore/Migration/072_history_grabid.cs deleted file mode 100644 index 23523808f..000000000 --- a/src/NzbDrone.Core/Datastore/Migration/072_history_grabid.cs +++ /dev/null @@ -1,77 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Data; -using FluentMigrator; -using NzbDrone.Common.Serializer; -using NzbDrone.Core.Datastore.Migration.Framework; - -namespace NzbDrone.Core.Datastore.Migration -{ - [Migration(72)] - public class history_downloadId : NzbDroneMigrationBase - { - protected override void MainDbUpgrade() - { - Alter.Table("History") - .AddColumn("DownloadId").AsString() - .Nullable() - .Indexed(); - - Execute.WithConnection(MoveToColumn); - } - - private void MoveToColumn(IDbConnection conn, IDbTransaction tran) - { - using (IDbCommand getHistory = conn.CreateCommand()) - { - getHistory.Transaction = tran; - getHistory.CommandText = @"SELECT Id, Data FROM History WHERE Data LIKE '%downloadClientId%'"; - - using (var historyReader = getHistory.ExecuteReader()) - { - while (historyReader.Read()) - { - var id = historyReader.GetInt32(0); - var data = historyReader.GetString(1); - - UpdateHistory(tran, conn, id, data); - } - } - } - } - - private void UpdateHistory(IDbTransaction tran, IDbConnection conn, int id, string data) - { - var dic = Json.Deserialize>(data); - - var downloadId = dic["downloadClientId"]; - dic.Remove("downloadClientId"); - - using (var updateHistoryCmd = conn.CreateCommand()) - { - updateHistoryCmd.Transaction = tran; - updateHistoryCmd.CommandText = @"UPDATE History SET DownloadId = ?, Data = ? WHERE Id = ?"; - - updateHistoryCmd.AddParameter(downloadId); - updateHistoryCmd.AddParameter(dic.ToJson()); - updateHistoryCmd.AddParameter(id); - - updateHistoryCmd.ExecuteNonQuery(); - - } - } - } - - public class History72 - { - public int EpisodeId { get; set; } - public int SeriesId { get; set; } - public string SourceTitle { get; set; } - public string Quality { get; set; } - public DateTime Date { get; set; } - public int EventType { get; set; } - public Dictionary Data { get; set; } - - public string DownloadId { get; set; } - } -} diff --git a/src/NzbDrone.Core/Datastore/Migration/073_clear_ratings.cs b/src/NzbDrone.Core/Datastore/Migration/073_clear_ratings.cs deleted file mode 100644 index ef9c4074f..000000000 --- a/src/NzbDrone.Core/Datastore/Migration/073_clear_ratings.cs +++ /dev/null @@ -1,20 +0,0 @@ -using FluentMigrator; -using NzbDrone.Core.Datastore.Migration.Framework; - -namespace NzbDrone.Core.Datastore.Migration -{ - [Migration(73)] - public class clear_ratings : NzbDroneMigrationBase - { - protected override void MainDbUpgrade() - { - Update.Table("Series") - .Set(new {Ratings = "{}"}) - .AllRows(); - - Update.Table("Episodes") - .Set(new { Ratings = "{}" }) - .AllRows(); - } - } -} diff --git a/src/NzbDrone.Core/Datastore/Migration/074_disable_eztv.cs b/src/NzbDrone.Core/Datastore/Migration/074_disable_eztv.cs deleted file mode 100644 index c090df19b..000000000 --- a/src/NzbDrone.Core/Datastore/Migration/074_disable_eztv.cs +++ /dev/null @@ -1,14 +0,0 @@ -using FluentMigrator; -using NzbDrone.Core.Datastore.Migration.Framework; - -namespace NzbDrone.Core.Datastore.Migration -{ - [Migration(74)] - public class disable_eztv : NzbDroneMigrationBase - { - protected override void MainDbUpgrade() - { - Execute.Sql("UPDATE Indexers SET EnableRss = 0, EnableSearch = 0 WHERE Implementation = 'Eztv' AND Settings LIKE '%ezrss.it%'"); - } - } -} diff --git a/src/NzbDrone.Core/Datastore/Migration/075_force_lib_update.cs b/src/NzbDrone.Core/Datastore/Migration/075_force_lib_update.cs deleted file mode 100644 index 5a9336f64..000000000 --- a/src/NzbDrone.Core/Datastore/Migration/075_force_lib_update.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System; -using FluentMigrator; -using NzbDrone.Core.Datastore.Migration.Framework; - -namespace NzbDrone.Core.Datastore.Migration -{ - [Migration(75)] - public class force_lib_update : NzbDroneMigrationBase - { - protected override void MainDbUpgrade() - { - Update.Table("ScheduledTasks") - .Set(new { LastExecution = "2014-01-01 00:00:00" }) - .Where(new { TypeName = "NzbDrone.Core.Tv.Commands.RefreshSeriesCommand" }); - - Update.Table("Series") - .Set(new { LastInfoSync = "2014-01-01 00:00:00" }) - .AllRows(); - } - } - - public class ScheduledTasks75 - { - public int Id { get; set; } - public string TypeName { get; set; } - public int Interval { get; set; } - public DateTime LastExecution { get; set; } - } -} diff --git a/src/NzbDrone.Core/Datastore/Migration/076_add_users_table.cs b/src/NzbDrone.Core/Datastore/Migration/076_add_users_table.cs deleted file mode 100644 index 7933d90d4..000000000 --- a/src/NzbDrone.Core/Datastore/Migration/076_add_users_table.cs +++ /dev/null @@ -1,17 +0,0 @@ -using FluentMigrator; -using NzbDrone.Core.Datastore.Migration.Framework; - -namespace NzbDrone.Core.Datastore.Migration -{ - [Migration(76)] - public class add_users_table : NzbDroneMigrationBase - { - protected override void MainDbUpgrade() - { - Create.TableForModel("Users") - .WithColumn("Identifier").AsString().NotNullable().Unique() - .WithColumn("Username").AsString().NotNullable().Unique() - .WithColumn("Password").AsString().NotNullable(); - } - } -} diff --git a/src/NzbDrone.Core/Datastore/Migration/077_add_add_options_to_series.cs b/src/NzbDrone.Core/Datastore/Migration/077_add_add_options_to_series.cs deleted file mode 100644 index 5c4891e5c..000000000 --- a/src/NzbDrone.Core/Datastore/Migration/077_add_add_options_to_series.cs +++ /dev/null @@ -1,14 +0,0 @@ -using FluentMigrator; -using NzbDrone.Core.Datastore.Migration.Framework; - -namespace NzbDrone.Core.Datastore.Migration -{ - [Migration(77)] - public class add_add_options_to_series : NzbDroneMigrationBase - { - protected override void MainDbUpgrade() - { - Alter.Table("Series").AddColumn("AddOptions").AsString().Nullable(); - } - } -} diff --git a/src/NzbDrone.Core/Datastore/Migration/078_add_commands_table.cs b/src/NzbDrone.Core/Datastore/Migration/078_add_commands_table.cs deleted file mode 100644 index 5a3d93716..000000000 --- a/src/NzbDrone.Core/Datastore/Migration/078_add_commands_table.cs +++ /dev/null @@ -1,24 +0,0 @@ -using FluentMigrator; -using NzbDrone.Core.Datastore.Migration.Framework; - -namespace NzbDrone.Core.Datastore.Migration -{ - [Migration(78)] - public class add_commands_table : NzbDroneMigrationBase - { - protected override void MainDbUpgrade() - { - Create.TableForModel("Commands") - .WithColumn("Name").AsString().NotNullable() - .WithColumn("Body").AsString().NotNullable() - .WithColumn("Priority").AsInt32().NotNullable() - .WithColumn("Status").AsInt32().NotNullable() - .WithColumn("QueuedAt").AsDateTime().NotNullable() - .WithColumn("StartedAt").AsDateTime().Nullable() - .WithColumn("EndedAt").AsDateTime().Nullable() - .WithColumn("Duration").AsString().Nullable() - .WithColumn("Exception").AsString().Nullable() - .WithColumn("Trigger").AsInt32().NotNullable(); - } - } -} diff --git a/src/NzbDrone.Core/Datastore/Migration/079_dedupe_tags.cs b/src/NzbDrone.Core/Datastore/Migration/079_dedupe_tags.cs deleted file mode 100644 index b786747a2..000000000 --- a/src/NzbDrone.Core/Datastore/Migration/079_dedupe_tags.cs +++ /dev/null @@ -1,157 +0,0 @@ -using System.Collections.Generic; -using System.Data; -using System.Linq; -using FluentMigrator; -using NzbDrone.Common.Extensions; -using NzbDrone.Common.Serializer; -using NzbDrone.Core.Datastore.Migration.Framework; - -namespace NzbDrone.Core.Datastore.Migration -{ - [Migration(79)] - public class dedupe_tags : NzbDroneMigrationBase - { - protected override void MainDbUpgrade() - { - Execute.WithConnection(CleanupTags); - - Alter.Table("Tags").AlterColumn("Label").AsString().Unique(); - } - - private void CleanupTags(IDbConnection conn, IDbTransaction tran) - { - var tags = GetTags(conn, tran); - var grouped = tags.GroupBy(t => t.Label.ToLowerInvariant()); - var replacements = new List(); - - foreach (var group in grouped.Where(g => g.Count() > 1)) - { - var first = group.First().Id; - - foreach (var other in group.Skip(1).Select(t => t.Id)) - { - replacements.Add(new TagReplacement079 { OldId = other, NewId = first }); - } - } - - UpdateTaggedModel(conn, tran, "Series", replacements); - UpdateTaggedModel(conn, tran, "Notifications", replacements); - UpdateTaggedModel(conn, tran, "DelayProfiles", replacements); - UpdateTaggedModel(conn, tran, "Restrictions", replacements); - - DeleteTags(conn, tran, replacements); - } - - private List GetTags(IDbConnection conn, IDbTransaction tran) - { - var tags = new List(); - - using (IDbCommand tagCmd = conn.CreateCommand()) - { - tagCmd.Transaction = tran; - tagCmd.CommandText = @"SELECT Id, Label FROM Tags"; - - using (IDataReader tagReader = tagCmd.ExecuteReader()) - { - while (tagReader.Read()) - { - var id = tagReader.GetInt32(0); - var label = tagReader.GetString(1); - - tags.Add(new Tag079 { Id = id, Label = label }); - } - } - } - - return tags; - } - - private void UpdateTaggedModel(IDbConnection conn, IDbTransaction tran, string table, List replacements) - { - var tagged = new List(); - - using (IDbCommand tagCmd = conn.CreateCommand()) - { - tagCmd.Transaction = tran; - tagCmd.CommandText = string.Format("SELECT Id, Tags FROM {0}", table); - - using (IDataReader tagReader = tagCmd.ExecuteReader()) - { - while (tagReader.Read()) - { - if (!tagReader.IsDBNull(1)) - { - var id = tagReader.GetInt32(0); - var tags = tagReader.GetString(1); - - tagged.Add(new TaggedModel079 - { - Id = id, - Tags = Json.Deserialize>(tags) - }); - } - } - } - } - - var toUpdate = new List(); - - foreach (var model in tagged) - { - foreach (var replacement in replacements) - { - if (model.Tags.Contains(replacement.OldId)) - { - model.Tags.Remove(replacement.OldId); - model.Tags.Add(replacement.NewId); - - toUpdate.Add(model); - } - } - } - - foreach (var model in toUpdate.DistinctBy(m => m.Id)) - { - using (IDbCommand updateCmd = conn.CreateCommand()) - { - updateCmd.Transaction = tran; - updateCmd.CommandText = string.Format(@"UPDATE {0} SET Tags = ? WHERE Id = ?", table); - updateCmd.AddParameter(model.Tags.ToJson()); - updateCmd.AddParameter(model.Id); - - updateCmd.ExecuteNonQuery(); - } - } - } - - private void DeleteTags(IDbConnection conn, IDbTransaction tran, List replacements) - { - var idsToRemove = replacements.Select(r => r.OldId).Distinct(); - - using (IDbCommand removeCmd = conn.CreateCommand()) - { - removeCmd.Transaction = tran; - removeCmd.CommandText = string.Format("DELETE FROM Tags WHERE Id IN ({0})", string.Join(",", idsToRemove)); - removeCmd.ExecuteNonQuery(); - } - } - - private class Tag079 - { - public int Id { get; set; } - public string Label { get; set; } - } - - private class TagReplacement079 - { - public int OldId { get; set; } - public int NewId { get; set; } - } - - private class TaggedModel079 - { - public int Id { get; set; } - public HashSet Tags { get; set; } - } - } -} diff --git a/src/NzbDrone.Core/Datastore/Migration/081_move_dot_prefix_to_transmission_category.cs b/src/NzbDrone.Core/Datastore/Migration/081_move_dot_prefix_to_transmission_category.cs deleted file mode 100644 index c48fd75a8..000000000 --- a/src/NzbDrone.Core/Datastore/Migration/081_move_dot_prefix_to_transmission_category.cs +++ /dev/null @@ -1,92 +0,0 @@ -using System.Collections.Generic; -using System.Data; -using FluentMigrator; -using Newtonsoft.Json.Linq; -using NzbDrone.Common.Extensions; -using NzbDrone.Common.Serializer; -using NzbDrone.Core.Datastore.Migration.Framework; - -namespace NzbDrone.Core.Datastore.Migration -{ - [Migration(81)] - public class move_dot_prefix_to_transmission_category : NzbDroneMigrationBase - { - protected override void MainDbUpgrade() - { - Execute.WithConnection(UpdateTransmissionSettings); - } - - private void UpdateTransmissionSettings(IDbConnection conn, IDbTransaction tran) - { - using (var cmd = conn.CreateCommand()) - { - cmd.Transaction = tran; - cmd.CommandText = "SELECT Id, Settings FROM DownloadClients WHERE Implementation = 'Transmission'"; - - using (var reader = cmd.ExecuteReader()) - { - while (reader.Read()) - { - var id = reader.GetInt32(0); - var settingsJson = reader.GetString(1); - - var settings = Json.Deserialize>(settingsJson); - - var tvCategory = settings.GetValueOrDefault("tvCategory") as string; - if (tvCategory.IsNotNullOrWhiteSpace()) - { - settings["tvCategory"] = "." + tvCategory; - - using (var updateCmd = conn.CreateCommand()) - { - updateCmd.Transaction = tran; - updateCmd.CommandText = "UPDATE DownloadClients SET Settings = ? WHERE Id = ?"; - updateCmd.AddParameter(settings.ToJson()); - updateCmd.AddParameter(id); - - updateCmd.ExecuteNonQuery(); - } - } - } - } - } - } - } - - public class DownloadClientDefinition81 - { - public int Id { get; set; } - public bool Enable { get; set; } - public string Name { get; set; } - public string Implementation { get; set; } - public JObject Settings { get; set; } - public string ConfigContract { get; set; } - } - - public class SabnzbdSettings81 - { - public string Host { get; set; } - public int Port { get; set; } - public string ApiKey { get; set; } - public string Username { get; set; } - public string Password { get; set; } - public string TvCategory { get; set; } - public int RecentTvPriority { get; set; } - public int OlderTvPriority { get; set; } - public bool UseSsl { get; set; } - } - - public class TransmissionSettings81 - { - public string Host { get; set; } - public int Port { get; set; } - public string UrlBase { get; set; } - public string Username { get; set; } - public string Password { get; set; } - public string TvCategory { get; set; } - public string TvDirectory { get; set; } - public int RecentTvPriority { get; set; } - public int OlderTvPriority { get; set; } - public bool UseSsl { get; set; } - } -} diff --git a/src/NzbDrone.Core/Datastore/Migration/082_add_fanzub_settings.cs b/src/NzbDrone.Core/Datastore/Migration/082_add_fanzub_settings.cs deleted file mode 100644 index 43d332224..000000000 --- a/src/NzbDrone.Core/Datastore/Migration/082_add_fanzub_settings.cs +++ /dev/null @@ -1,14 +0,0 @@ -using FluentMigrator; -using NzbDrone.Core.Datastore.Migration.Framework; - -namespace NzbDrone.Core.Datastore.Migration -{ - [Migration(82)] - public class add_fanzub_settings : NzbDroneMigrationBase - { - protected override void MainDbUpgrade() - { - Execute.Sql("UPDATE Indexers SET ConfigContract = 'FanzubSettings' WHERE Implementation = 'Fanzub' AND ConfigContract = 'NullConfig'"); - } - } -} diff --git a/src/NzbDrone.Core/Datastore/Migration/083_additonal_blacklist_columns.cs b/src/NzbDrone.Core/Datastore/Migration/083_additonal_blacklist_columns.cs deleted file mode 100644 index 93fa2fd51..000000000 --- a/src/NzbDrone.Core/Datastore/Migration/083_additonal_blacklist_columns.cs +++ /dev/null @@ -1,22 +0,0 @@ -using FluentMigrator; -using NzbDrone.Core.Datastore.Migration.Framework; - -namespace NzbDrone.Core.Datastore.Migration -{ - [Migration(83)] - public class additonal_blacklist_columns : NzbDroneMigrationBase - { - protected override void MainDbUpgrade() - { - Alter.Table("Blacklist").AddColumn("Size").AsInt64().Nullable(); - Alter.Table("Blacklist").AddColumn("Protocol").AsInt32().Nullable(); - Alter.Table("Blacklist").AddColumn("Indexer").AsString().Nullable(); - Alter.Table("Blacklist").AddColumn("Message").AsString().Nullable(); - Alter.Table("Blacklist").AddColumn("TorrentInfoHash").AsString().Nullable(); - - Update.Table("Blacklist") - .Set(new { Protocol = 1 }) - .AllRows(); - } - } -} diff --git a/src/NzbDrone.Core/Datastore/Migration/084_update_quality_minmax_size.cs b/src/NzbDrone.Core/Datastore/Migration/084_update_quality_minmax_size.cs deleted file mode 100644 index 03a0a8ea3..000000000 --- a/src/NzbDrone.Core/Datastore/Migration/084_update_quality_minmax_size.cs +++ /dev/null @@ -1,26 +0,0 @@ -using FluentMigrator; -using NzbDrone.Core.Datastore.Migration.Framework; - -namespace NzbDrone.Core.Datastore.Migration -{ - [Migration(84)] - public class update_quality_minmax_size : NzbDroneMigrationBase - { - protected override void MainDbUpgrade() - { - Alter.Table("QualityDefinitions").AlterColumn("MinSize").AsDouble().Nullable(); - Alter.Table("QualityDefinitions").AlterColumn("MaxSize").AsDouble().Nullable(); - - Execute.Sql("UPDATE QualityDefinitions SET MaxSize = NULL WHERE Quality = 10 OR MaxSize = 0"); - } - } - - public class QualityDefinition84 - { - public int Id { get; set; } - public int Quality { get; set; } - public string Title { get; set; } - public int? MinSize { get; set; } - public int? MaxSize { get; set; } - } -} diff --git a/src/NzbDrone.Core/Datastore/Migration/085_expand_transmission_urlbase.cs b/src/NzbDrone.Core/Datastore/Migration/085_expand_transmission_urlbase.cs deleted file mode 100644 index 956f87bcd..000000000 --- a/src/NzbDrone.Core/Datastore/Migration/085_expand_transmission_urlbase.cs +++ /dev/null @@ -1,71 +0,0 @@ -using System.Collections.Generic; -using System.Data; -using FluentMigrator; -using NzbDrone.Common.Extensions; -using NzbDrone.Common.Serializer; -using NzbDrone.Core.Datastore.Migration.Framework; - -namespace NzbDrone.Core.Datastore.Migration -{ - [Migration(85)] - public class expand_transmission_urlbase : NzbDroneMigrationBase - { - protected override void MainDbUpgrade() - { - Execute.WithConnection(UpdateTransmissionSettings); - } - - private void UpdateTransmissionSettings(IDbConnection conn, IDbTransaction tran) - { - using (var cmd = conn.CreateCommand()) - { - cmd.Transaction = tran; - cmd.CommandText = "SELECT Id, Settings FROM DownloadClients WHERE Implementation = 'Transmission'"; - - using (var reader = cmd.ExecuteReader()) - { - while (reader.Read()) - { - var id = reader.GetInt32(0); - var settingsJson = reader.GetString(1); - - var settings = Json.Deserialize>(settingsJson); - - var urlBase = settings.GetValueOrDefault("urlBase", "") as string; - - if (urlBase.IsNullOrWhiteSpace()) - { - settings["urlBase"] = "/transmission/"; - } - else - { - settings["urlBase"] = string.Format("/{0}/transmission/", urlBase.Trim('/')); - } - - using (var updateCmd = conn.CreateCommand()) - { - updateCmd.Transaction = tran; - updateCmd.CommandText = "UPDATE DownloadClients SET Settings = ? WHERE Id = ?"; - updateCmd.AddParameter(settings.ToJson()); - updateCmd.AddParameter(id); - - updateCmd.ExecuteNonQuery(); - } - } - } - } - } - } - - public class DelugeSettings85 - { - public string Host { get; set; } - public int Port { get; set; } - public string UrlBase { get; set; } - public string Password { get; set; } - public string TvCategory { get; set; } - public int RecentTvPriority { get; set; } - public int OlderTvPriority { get; set; } - public bool UseSsl { get; set; } - } -} diff --git a/src/NzbDrone.Core/Datastore/Migration/086_pushbullet_device_ids.cs b/src/NzbDrone.Core/Datastore/Migration/086_pushbullet_device_ids.cs deleted file mode 100644 index 432a13ff3..000000000 --- a/src/NzbDrone.Core/Datastore/Migration/086_pushbullet_device_ids.cs +++ /dev/null @@ -1,77 +0,0 @@ -using System.Collections.Generic; -using System.Data; -using FluentMigrator; -using Newtonsoft.Json.Linq; -using NzbDrone.Common.Extensions; -using NzbDrone.Common.Serializer; -using NzbDrone.Core.Datastore.Migration.Framework; - -namespace NzbDrone.Core.Datastore.Migration -{ - [Migration(86)] - public class pushbullet_device_ids : NzbDroneMigrationBase - { - protected override void MainDbUpgrade() - { - Execute.WithConnection(UpdateTransmissionSettings); - } - - private void UpdateTransmissionSettings(IDbConnection conn, IDbTransaction tran) - { - using (var cmd = conn.CreateCommand()) - { - cmd.Transaction = tran; - cmd.CommandText = "SELECT Id, Settings FROM Notifications WHERE Implementation = 'PushBullet'"; - - using (var reader = cmd.ExecuteReader()) - { - while (reader.Read()) - { - var id = reader.GetInt32(0); - var settingsJson = reader.GetString(1); - var settings = Json.Deserialize>(settingsJson); - - if (settings.ContainsKey("deviceId")) - { - var deviceId = settings.GetValueOrDefault("deviceId", "") as string; - - settings.Add("deviceIds", new[] { deviceId }); - settings.Remove("deviceId"); - - using (var updateCmd = conn.CreateCommand()) - { - updateCmd.Transaction = tran; - updateCmd.CommandText = "UPDATE Notifications SET Settings = ? WHERE Id = ?"; - updateCmd.AddParameter(settings.ToJson()); - updateCmd.AddParameter(id); - - updateCmd.ExecuteNonQuery(); - } - } - } - } - } - } - } - - public class Notification86 - { - public int Id { get; set; } - public string Name { get; set; } - public int OnGrab { get; set; } - public int OnDownload { get; set; } - public JObject Settings { get; set; } - public string Implementation { get; set; } - public string ConfigContract { get; set; } - public int OnUpgrade { get; set; } - public List Tags { get; set; } - } - - public class PushBulletSettings86 - { - public string ApiKey { get; set; } - public string[] DeviceIds { get; set; } - public string ChannelTags { get; set; } - public string SenderId { get; set; } - } -} diff --git a/src/NzbDrone.Core/Datastore/Migration/087_remove_eztv.cs b/src/NzbDrone.Core/Datastore/Migration/087_remove_eztv.cs deleted file mode 100644 index d6990053a..000000000 --- a/src/NzbDrone.Core/Datastore/Migration/087_remove_eztv.cs +++ /dev/null @@ -1,14 +0,0 @@ -using FluentMigrator; -using NzbDrone.Core.Datastore.Migration.Framework; - -namespace NzbDrone.Core.Datastore.Migration -{ - [Migration(87)] - public class remove_eztv : NzbDroneMigrationBase - { - protected override void MainDbUpgrade() - { - Execute.Sql("DELETE FROM Indexers WHERE Implementation = 'Eztv'"); - } - } -} diff --git a/src/NzbDrone.Core/Datastore/Migration/088_pushbullet_devices_channels_list.cs b/src/NzbDrone.Core/Datastore/Migration/088_pushbullet_devices_channels_list.cs deleted file mode 100644 index b219dfd59..000000000 --- a/src/NzbDrone.Core/Datastore/Migration/088_pushbullet_devices_channels_list.cs +++ /dev/null @@ -1,80 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Data; -using FluentMigrator; -using NzbDrone.Common.Extensions; -using NzbDrone.Common.Serializer; -using NzbDrone.Core.Datastore.Migration.Framework; - -namespace NzbDrone.Core.Datastore.Migration -{ - [Migration(88)] - public class pushbullet_devices_channels_list : NzbDroneMigrationBase - { - protected override void MainDbUpgrade() - { - Execute.WithConnection(UpdateTransmissionSettings); - } - - private void UpdateTransmissionSettings(IDbConnection conn, IDbTransaction tran) - { - using (var cmd = conn.CreateCommand()) - { - cmd.Transaction = tran; - cmd.CommandText = "SELECT Id, Settings FROM Notifications WHERE Implementation = 'PushBullet'"; - - using (var reader = cmd.ExecuteReader()) - { - while (reader.Read()) - { - var id = reader.GetInt32(0); - var settingsJson = reader.GetString(1); - var settings = Json.Deserialize>(settingsJson); - - if (settings.ContainsKey("deviceIds")) - { - var deviceIdsString = settings.GetValueOrDefault("deviceIds", "") as string; - - if (deviceIdsString.IsNotNullOrWhiteSpace()) - { - var deviceIds = deviceIdsString.Split(new[] { "," }, StringSplitOptions.RemoveEmptyEntries); - - settings["deviceIds"] = deviceIds; - } - } - - if (settings.ContainsKey("channelTags")) - { - var channelTagsString = settings.GetValueOrDefault("channelTags", "") as string; - - if (channelTagsString.IsNotNullOrWhiteSpace()) - { - var channelTags = channelTagsString.Split(new[] {","}, StringSplitOptions.RemoveEmptyEntries); - - settings["channelTags"] = channelTags; - } - } - - using (var updateCmd = conn.CreateCommand()) - { - updateCmd.Transaction = tran; - updateCmd.CommandText = "UPDATE Notifications SET Settings = ? WHERE Id = ?"; - updateCmd.AddParameter(settings.ToJson()); - updateCmd.AddParameter(id); - - updateCmd.ExecuteNonQuery(); - } - } - } - } - } - } - - public class PushBulletSettings88 - { - public string ApiKey { get; set; } - public string[] DeviceIds { get; set; } - public string[] ChannelTags { get; set; } - public string SenderId { get; set; } - } -} diff --git a/src/NzbDrone.Core/Datastore/Migration/089_add_on_rename_to_notifcations.cs b/src/NzbDrone.Core/Datastore/Migration/089_add_on_rename_to_notifcations.cs deleted file mode 100644 index e06db676b..000000000 --- a/src/NzbDrone.Core/Datastore/Migration/089_add_on_rename_to_notifcations.cs +++ /dev/null @@ -1,21 +0,0 @@ -using FluentMigrator; -using NzbDrone.Core.Datastore.Migration.Framework; - -namespace NzbDrone.Core.Datastore.Migration -{ - [Migration(89)] - public class add_on_rename_to_notifcations : NzbDroneMigrationBase - { - protected override void MainDbUpgrade() - { - Alter.Table("Notifications").AddColumn("OnRename").AsBoolean().Nullable(); - - Execute.Sql("UPDATE Notifications SET OnRename = OnDownload WHERE Implementation IN ('PlexServer', 'Xbmc', 'MediaBrowser')"); - Execute.Sql("UPDATE Notifications SET OnRename = 0 WHERE Implementation NOT IN ('PlexServer', 'Xbmc', 'MediaBrowser')"); - - Alter.Table("Notifications").AlterColumn("OnRename").AsBoolean().NotNullable(); - - Execute.Sql("UPDATE Notifications SET OnGrab = 0 WHERE Implementation = 'PlexServer'"); - } - } -} diff --git a/src/NzbDrone.Core/Datastore/Migration/090_update_kickass_url.cs b/src/NzbDrone.Core/Datastore/Migration/090_update_kickass_url.cs deleted file mode 100644 index ec96f48dc..000000000 --- a/src/NzbDrone.Core/Datastore/Migration/090_update_kickass_url.cs +++ /dev/null @@ -1,36 +0,0 @@ -using FluentMigrator; -using Newtonsoft.Json.Linq; -using NzbDrone.Core.Datastore.Migration.Framework; - -namespace NzbDrone.Core.Datastore.Migration -{ - [Migration(90)] - public class update_kickass_url : NzbDroneMigrationBase - { - protected override void MainDbUpgrade() - { - Execute.Sql( - "UPDATE Indexers SET Settings = Replace(Settings, 'kickass.so', 'kat.cr') WHERE Implementation = 'KickassTorrents';" + - "UPDATE Indexers SET Settings = Replace(Settings, 'kickass.to', 'kat.cr') WHERE Implementation = 'KickassTorrents';" + - "UPDATE Indexers SET Settings = Replace(Settings, 'http://', 'https://') WHERE Implementation = 'KickassTorrents';" - ); - } - } - - public class IndexerDefinition90 - { - public int Id { get; set; } - public string Name { get; set; } - public JObject Settings { get; set; } - public string Implementation { get; set; } - public string ConfigContract { get; set; } - public bool EnableRss { get; set; } - public bool EnableSearch { get; set; } - } - - public class KickassTorrentsSettings90 - { - public string BaseUrl { get; set; } - public bool VerifiedOnly { get; set; } - } -} diff --git a/src/NzbDrone.Core/Datastore/Migration/091_added_indexerstatus.cs b/src/NzbDrone.Core/Datastore/Migration/091_added_indexerstatus.cs deleted file mode 100644 index 9a384c298..000000000 --- a/src/NzbDrone.Core/Datastore/Migration/091_added_indexerstatus.cs +++ /dev/null @@ -1,20 +0,0 @@ -using FluentMigrator; -using NzbDrone.Core.Datastore.Migration.Framework; - -namespace NzbDrone.Core.Datastore.Migration -{ - [Migration(91)] - public class added_indexerstatus : NzbDroneMigrationBase - { - protected override void MainDbUpgrade() - { - Create.TableForModel("IndexerStatus") - .WithColumn("IndexerId").AsInt32().NotNullable().Unique() - .WithColumn("InitialFailure").AsDateTime().Nullable() - .WithColumn("MostRecentFailure").AsDateTime().Nullable() - .WithColumn("EscalationLevel").AsInt32().NotNullable() - .WithColumn("DisabledTill").AsDateTime().Nullable() - .WithColumn("LastRssSyncReleaseInfo").AsString().Nullable(); - } - } -} diff --git a/src/NzbDrone.Core/Datastore/Migration/092_add_unverifiedscenenumbering.cs b/src/NzbDrone.Core/Datastore/Migration/092_add_unverifiedscenenumbering.cs deleted file mode 100644 index 5366b0fad..000000000 --- a/src/NzbDrone.Core/Datastore/Migration/092_add_unverifiedscenenumbering.cs +++ /dev/null @@ -1,14 +0,0 @@ -using FluentMigrator; -using NzbDrone.Core.Datastore.Migration.Framework; - -namespace NzbDrone.Core.Datastore.Migration -{ - [Migration(92)] - public class add_unverifiedscenenumbering : NzbDroneMigrationBase - { - protected override void MainDbUpgrade() - { - Alter.Table("Episodes").AddColumn("UnverifiedSceneNumbering").AsBoolean().WithDefaultValue(false); - } - } -} diff --git a/src/NzbDrone.Core/Datastore/Migration/093_naming_config_replace_characters.cs b/src/NzbDrone.Core/Datastore/Migration/093_naming_config_replace_characters.cs deleted file mode 100644 index 4ba4be853..000000000 --- a/src/NzbDrone.Core/Datastore/Migration/093_naming_config_replace_characters.cs +++ /dev/null @@ -1,15 +0,0 @@ -using FluentMigrator; -using NzbDrone.Core.Datastore.Migration.Framework; - -namespace NzbDrone.Core.Datastore.Migration -{ - [Migration(93)] - public class naming_config_replace_illegal_characters : NzbDroneMigrationBase - { - protected override void MainDbUpgrade() - { - Alter.Table("NamingConfig").AddColumn("ReplaceIllegalCharacters").AsBoolean().WithDefaultValue(true); - Update.Table("NamingConfig").Set(new { ReplaceIllegalCharacters = true }).AllRows(); - } - } -} diff --git a/src/NzbDrone.Core/Datastore/Migration/094_add_tvmazeid.cs b/src/NzbDrone.Core/Datastore/Migration/094_add_tvmazeid.cs deleted file mode 100644 index 007716bfc..000000000 --- a/src/NzbDrone.Core/Datastore/Migration/094_add_tvmazeid.cs +++ /dev/null @@ -1,15 +0,0 @@ -using FluentMigrator; -using NzbDrone.Core.Datastore.Migration.Framework; - -namespace NzbDrone.Core.Datastore.Migration -{ - [Migration(94)] - public class add_tvmazeid : NzbDroneMigrationBase - { - protected override void MainDbUpgrade() - { - Alter.Table("Series").AddColumn("TvMazeId").AsInt32().WithDefaultValue(0); - Create.Index().OnTable("Series").OnColumn("TvMazeId"); - } - } -} diff --git a/src/NzbDrone.Core/Datastore/Migration/095_add_additional_episodes_index.cs b/src/NzbDrone.Core/Datastore/Migration/095_add_additional_episodes_index.cs deleted file mode 100644 index 5d7a08b62..000000000 --- a/src/NzbDrone.Core/Datastore/Migration/095_add_additional_episodes_index.cs +++ /dev/null @@ -1,16 +0,0 @@ -using FluentMigrator; -using NzbDrone.Core.Datastore.Migration.Framework; - -namespace NzbDrone.Core.Datastore.Migration -{ - [Migration(95)] - public class add_additional_episodes_index : NzbDroneMigrationBase - { - protected override void MainDbUpgrade() - { - Create.Index().OnTable("Episodes").OnColumn("SeriesId").Ascending() - .OnColumn("SeasonNumber").Ascending() - .OnColumn("EpisodeNumber").Ascending(); - } - } -} diff --git a/src/NzbDrone.Core/Datastore/Migration/096_disable_kickass.cs b/src/NzbDrone.Core/Datastore/Migration/096_disable_kickass.cs deleted file mode 100644 index 69894cbce..000000000 --- a/src/NzbDrone.Core/Datastore/Migration/096_disable_kickass.cs +++ /dev/null @@ -1,14 +0,0 @@ -using FluentMigrator; -using NzbDrone.Core.Datastore.Migration.Framework; - -namespace NzbDrone.Core.Datastore.Migration -{ - [Migration(96)] - public class disable_kickass : NzbDroneMigrationBase - { - protected override void MainDbUpgrade() - { - Execute.Sql("UPDATE Indexers SET EnableRss = 0, EnableSearch = 0, Settings = Replace(Settings, 'https://kat.cr', '') WHERE Implementation = 'KickassTorrents' AND Settings LIKE '%kat.cr%';"); - } - } -} diff --git a/src/NzbDrone.Core/Datastore/Migration/098_remove_titans_of_tv.cs b/src/NzbDrone.Core/Datastore/Migration/098_remove_titans_of_tv.cs deleted file mode 100644 index 86b2eba1e..000000000 --- a/src/NzbDrone.Core/Datastore/Migration/098_remove_titans_of_tv.cs +++ /dev/null @@ -1,14 +0,0 @@ -using FluentMigrator; -using NzbDrone.Core.Datastore.Migration.Framework; - -namespace NzbDrone.Core.Datastore.Migration -{ - [Migration(98)] - public class remove_titans_of_tv : NzbDroneMigrationBase - { - protected override void MainDbUpgrade() - { - Delete.FromTable("Indexers").Row(new { Implementation = "TitansOfTv" }); - } - } -} diff --git a/src/NzbDrone.Core/Datastore/Migration/099_extra_and_subtitle_files.cs b/src/NzbDrone.Core/Datastore/Migration/099_extra_and_subtitle_files.cs deleted file mode 100644 index f7a173157..000000000 --- a/src/NzbDrone.Core/Datastore/Migration/099_extra_and_subtitle_files.cs +++ /dev/null @@ -1,89 +0,0 @@ -using System; -using System.Data; -using FluentMigrator; -using NzbDrone.Core.Datastore.Migration.Framework; - -namespace NzbDrone.Core.Datastore.Migration -{ - [Migration(99)] - public class extra_and_subtitle_files : NzbDroneMigrationBase - { - protected override void MainDbUpgrade() - { - Create.TableForModel("ExtraFiles") - .WithColumn("SeriesId").AsInt32().NotNullable() - .WithColumn("SeasonNumber").AsInt32().NotNullable() - .WithColumn("EpisodeFileId").AsInt32().NotNullable() - .WithColumn("RelativePath").AsString().NotNullable() - .WithColumn("Extension").AsString().NotNullable() - .WithColumn("Added").AsDateTime().NotNullable() - .WithColumn("LastUpdated").AsDateTime().NotNullable(); - - Create.TableForModel("SubtitleFiles") - .WithColumn("SeriesId").AsInt32().NotNullable() - .WithColumn("SeasonNumber").AsInt32().NotNullable() - .WithColumn("EpisodeFileId").AsInt32().NotNullable() - .WithColumn("RelativePath").AsString().NotNullable() - .WithColumn("Extension").AsString().NotNullable() - .WithColumn("Added").AsDateTime().NotNullable() - .WithColumn("LastUpdated").AsDateTime().NotNullable() - .WithColumn("Language").AsInt32().NotNullable(); - - Alter.Table("MetadataFiles") - .AddColumn("Added").AsDateTime().Nullable() - .AddColumn("Extension").AsString().Nullable(); - - // Remove Metadata files that don't have an extension - Execute.Sql("DELETE FROM MetadataFiles WHERE RelativePath NOT LIKE '%.%'"); - - // Set Extension using the extension from RelativePath - Execute.WithConnection(SetMetadataFileExtension); - - Alter.Table("MetadataFiles").AlterColumn("Extension").AsString().NotNullable(); - } - - private void SetMetadataFileExtension(IDbConnection conn, IDbTransaction tran) - { - using (var cmd = conn.CreateCommand()) - { - cmd.Transaction = tran; - cmd.CommandText = "SELECT Id, RelativePath FROM MetadataFiles"; - - using (var reader = cmd.ExecuteReader()) - { - while (reader.Read()) - { - var id = reader.GetInt32(0); - var relativePath = reader.GetString(1); - var extension = relativePath.Substring(relativePath.LastIndexOf(".", StringComparison.InvariantCultureIgnoreCase)); - - using (var updateCmd = conn.CreateCommand()) - { - updateCmd.Transaction = tran; - updateCmd.CommandText = "UPDATE MetadataFiles SET Extension = ? WHERE Id = ?"; - updateCmd.AddParameter(extension); - updateCmd.AddParameter(id); - - updateCmd.ExecuteNonQuery(); - } - } - } - } - } - } - - public class MetadataFile99 - { - public int Id { get; set; } - public int SeriesId { get; set; } - public int? EpisodeFileId { get; set; } - public int? SeasonNumber { get; set; } - public string RelativePath { get; set; } - public DateTime Added { get; set; } - public DateTime LastUpdated { get; set; } - public string Extension { get; set; } - public string Hash { get; set; } - public string Consumer { get; set; } - public int Type { get; set; } - } -} diff --git a/src/NzbDrone.Core/Datastore/Migration/100_add_scene_season_number.cs b/src/NzbDrone.Core/Datastore/Migration/100_add_scene_season_number.cs deleted file mode 100644 index 3cd11f6f0..000000000 --- a/src/NzbDrone.Core/Datastore/Migration/100_add_scene_season_number.cs +++ /dev/null @@ -1,15 +0,0 @@ -using FluentMigrator; -using NzbDrone.Core.Datastore.Migration.Framework; - -namespace NzbDrone.Core.Datastore.Migration -{ - [Migration(100)] - public class add_scene_season_number : NzbDroneMigrationBase - { - protected override void MainDbUpgrade() - { - Alter.Table("SceneMappings").AlterColumn("SeasonNumber").AsInt32().Nullable(); - Alter.Table("SceneMappings").AddColumn("SceneSeasonNumber").AsInt32().Nullable(); - } - } -} diff --git a/src/NzbDrone.Core/Datastore/Migration/101_add_ultrahd_quality_in_profiles.cs b/src/NzbDrone.Core/Datastore/Migration/101_add_ultrahd_quality_in_profiles.cs deleted file mode 100644 index 171607fa6..000000000 --- a/src/NzbDrone.Core/Datastore/Migration/101_add_ultrahd_quality_in_profiles.cs +++ /dev/null @@ -1,32 +0,0 @@ -using System.Data; -using FluentMigrator; -using NzbDrone.Core.Datastore.Migration.Framework; - -namespace NzbDrone.Core.Datastore.Migration -{ - [Migration(101)] - public class add_ultrahd_quality_in_profiles : NzbDroneMigrationBase - { - protected override void MainDbUpgrade() - { - Execute.WithConnection(ConvertProfile); - } - - private void ConvertProfile(IDbConnection conn, IDbTransaction tran) - { - var updater = new ProfileUpdater70(conn, tran); - updater.AppendQuality(16); // HDTV2160p - updater.AppendQuality(18); // WEBDL2160p - updater.AppendQuality(19); // Bluray2160p - updater.Commit(); - - // WEBRip migrations. - //updater.SplitQualityAppend(1, 11); // HDTV480p after SDTV - //updater.SplitQualityPrepend(8, 12); // WEBRip480p before WEBDL480p - //updater.SplitQualityAppend(2, 13); // Bluray480p after DVD - //updater.SplitQualityPrepend(5, 14); // WEBRip720p before WEBDL720p - //updater.SplitQualityPrepend(3, 15); // WEBRip1080p before WEBDL1080p - //updater.SplitQualityPrepend(18, 17); // WEBRip2160p before WEBDL2160p - } - } -} diff --git a/src/NzbDrone.Core/Datastore/Migration/103_fix_metadata_file_extensions.cs b/src/NzbDrone.Core/Datastore/Migration/103_fix_metadata_file_extensions.cs deleted file mode 100644 index d4ed853a3..000000000 --- a/src/NzbDrone.Core/Datastore/Migration/103_fix_metadata_file_extensions.cs +++ /dev/null @@ -1,45 +0,0 @@ -using System; -using System.Data; -using FluentMigrator; -using NzbDrone.Core.Datastore.Migration.Framework; - -namespace NzbDrone.Core.Datastore.Migration -{ - [Migration(103)] - public class fix_metadata_file_extensions : NzbDroneMigrationBase - { - protected override void MainDbUpgrade() - { - Execute.WithConnection(SetMetadataFileExtension); - } - - private void SetMetadataFileExtension(IDbConnection conn, IDbTransaction tran) - { - using (var cmd = conn.CreateCommand()) - { - cmd.Transaction = tran; - cmd.CommandText = "SELECT Id, Extension FROM MetadataFiles"; - - using (var reader = cmd.ExecuteReader()) - { - while (reader.Read()) - { - var id = reader.GetInt32(0); - var extension = reader.GetString(1); - extension = extension.Substring(extension.LastIndexOf(".", StringComparison.InvariantCultureIgnoreCase)); - - using (var updateCmd = conn.CreateCommand()) - { - updateCmd.Transaction = tran; - updateCmd.CommandText = "UPDATE MetadataFiles SET Extension = ? WHERE Id = ?"; - updateCmd.AddParameter(extension); - updateCmd.AddParameter(id); - - updateCmd.ExecuteNonQuery(); - } - } - } - } - } - } -} diff --git a/src/NzbDrone.Core/Datastore/Migration/104_remove_kickass.cs b/src/NzbDrone.Core/Datastore/Migration/104_remove_kickass.cs deleted file mode 100644 index c7f21fe38..000000000 --- a/src/NzbDrone.Core/Datastore/Migration/104_remove_kickass.cs +++ /dev/null @@ -1,14 +0,0 @@ -using FluentMigrator; -using NzbDrone.Core.Datastore.Migration.Framework; - -namespace NzbDrone.Core.Datastore.Migration -{ - [Migration(104)] - public class remove_kickass : NzbDroneMigrationBase - { - protected override void MainDbUpgrade() - { - Execute.Sql("DELETE FROM Indexers WHERE Implementation = 'KickassTorrents';"); - } - } -} diff --git a/src/NzbDrone.Core/Datastore/Migration/105_rename_torrent_downloadstation.cs b/src/NzbDrone.Core/Datastore/Migration/105_rename_torrent_downloadstation.cs deleted file mode 100644 index c5a71c885..000000000 --- a/src/NzbDrone.Core/Datastore/Migration/105_rename_torrent_downloadstation.cs +++ /dev/null @@ -1,14 +0,0 @@ -using FluentMigrator; -using NzbDrone.Core.Datastore.Migration.Framework; - -namespace NzbDrone.Core.Datastore.Migration -{ - [Migration(105)] - public class rename_torrent_downloadstation : NzbDroneMigrationBase - { - protected override void MainDbUpgrade() - { - Execute.Sql("UPDATE DownloadClients SET Implementation = 'TorrentDownloadStation' WHERE Implementation = 'DownloadStation';"); - } - } -} diff --git a/src/NzbDrone.Core/Datastore/Migration/106_update_btn_url.cs b/src/NzbDrone.Core/Datastore/Migration/106_update_btn_url.cs deleted file mode 100644 index f2989a2c8..000000000 --- a/src/NzbDrone.Core/Datastore/Migration/106_update_btn_url.cs +++ /dev/null @@ -1,22 +0,0 @@ -using FluentMigrator; -using Newtonsoft.Json.Linq; -using NzbDrone.Core.Datastore.Migration.Framework; - -namespace NzbDrone.Core.Datastore.Migration -{ - [Migration(106)] - public class update_btn_url : NzbDroneMigrationBase - { - protected override void MainDbUpgrade() - { - Execute.Sql("UPDATE Indexers SET Settings = Replace(Settings, 'api.btnapps.net', 'api.broadcasthe.net') WHERE Implementation = 'BroadcastheNet';"); - } - } - - public class BroadcastheNetSettings106 - { - public string BaseUrl { get; set; } - - public string ApiKey { get; set; } - } -} diff --git a/src/NzbDrone.Core/Datastore/Migration/107_remove_wombles.cs b/src/NzbDrone.Core/Datastore/Migration/107_remove_wombles.cs deleted file mode 100644 index 7bbb5ceb7..000000000 --- a/src/NzbDrone.Core/Datastore/Migration/107_remove_wombles.cs +++ /dev/null @@ -1,14 +0,0 @@ -using FluentMigrator; -using NzbDrone.Core.Datastore.Migration.Framework; - -namespace NzbDrone.Core.Datastore.Migration -{ - [Migration(107)] - public class remove_wombles : NzbDroneMigrationBase - { - protected override void MainDbUpgrade() - { - Execute.Sql("DELETE FROM Indexers WHERE Implementation = 'Wombles';"); - } - } -} diff --git a/src/NzbDrone.Core/Datastore/Migration/108_fix_extra_file_extension.cs b/src/NzbDrone.Core/Datastore/Migration/108_fix_extra_file_extension.cs deleted file mode 100644 index 8ef387b26..000000000 --- a/src/NzbDrone.Core/Datastore/Migration/108_fix_extra_file_extension.cs +++ /dev/null @@ -1,56 +0,0 @@ -using System; -using System.Data; -using FluentMigrator; -using NzbDrone.Core.Datastore.Migration.Framework; - -namespace NzbDrone.Core.Datastore.Migration -{ - [Migration(108)] - public class fix_extra_file_extension : NzbDroneMigrationBase - { - protected override void MainDbUpgrade() - { - // Delete extraneous files without extensions that Lidarr found previously, - // these will be blocked from importing as well. - Execute.Sql("DELETE FROM ExtraFiles WHERE TRIM(Extension) = ''"); - - Execute.WithConnection(FixExtraFileExtension); - } - - private void FixExtraFileExtension(IDbConnection conn, IDbTransaction tran) - { - FixExtraFileExtensionForTable(conn, tran, "ExtraFiles"); - FixExtraFileExtensionForTable(conn, tran, "SubtitleFiles"); - } - - private void FixExtraFileExtensionForTable(IDbConnection conn, IDbTransaction tran, string table) - { - - using (var cmd = conn.CreateCommand()) - { - cmd.Transaction = tran; - cmd.CommandText = $"SELECT Id, RelativePath FROM {table}"; - - using (var reader = cmd.ExecuteReader()) - { - while (reader.Read()) - { - var id = reader.GetInt32(0); - var relativePath = reader.GetString(1); - var extension = relativePath.Substring(relativePath.LastIndexOf(".", StringComparison.InvariantCultureIgnoreCase)); - - using (var updateCmd = conn.CreateCommand()) - { - updateCmd.Transaction = tran; - updateCmd.CommandText = $"UPDATE {table} SET Extension = ? WHERE Id = ?"; - updateCmd.AddParameter(extension); - updateCmd.AddParameter(id); - - updateCmd.ExecuteNonQuery(); - } - } - } - } - } - } -} diff --git a/src/NzbDrone.Core/Datastore/Migration/109_import_extra_files.cs b/src/NzbDrone.Core/Datastore/Migration/109_import_extra_files.cs deleted file mode 100644 index e92e00a78..000000000 --- a/src/NzbDrone.Core/Datastore/Migration/109_import_extra_files.cs +++ /dev/null @@ -1,44 +0,0 @@ -using System.Data; -using FluentMigrator; -using NzbDrone.Common.Extensions; -using NzbDrone.Core.Datastore.Migration.Framework; - -namespace NzbDrone.Core.Datastore.Migration -{ - [Migration(109)] - public class import_extra_files : NzbDroneMigrationBase - { - protected override void MainDbUpgrade() - { - Execute.WithConnection(ImportExtraFiles); - } - - private void ImportExtraFiles(IDbConnection conn, IDbTransaction tran) - { - - using (var cmd = conn.CreateCommand()) - { - cmd.Transaction = tran; - cmd.CommandText = "SELECT Value from Config WHERE Key = 'extrafileextensions'"; - - using (var reader = cmd.ExecuteReader()) - { - while (reader.Read()) - { - var value = reader.GetString(0); - - if (value.IsNotNullOrWhiteSpace()) - { - using (var insertCmd = conn.CreateCommand()) - { - insertCmd.Transaction = tran; - insertCmd.CommandText = "INSERT INTO Config (Key, Value) VALUES('importextrafiles', 'True')"; - insertCmd.ExecuteNonQuery(); - } - } - } - } - } - } - } -} diff --git a/src/NzbDrone.Core/Datastore/Migration/110_fix_extra_files_config.cs b/src/NzbDrone.Core/Datastore/Migration/110_fix_extra_files_config.cs deleted file mode 100644 index 5f136deb1..000000000 --- a/src/NzbDrone.Core/Datastore/Migration/110_fix_extra_files_config.cs +++ /dev/null @@ -1,57 +0,0 @@ -using System.Data; -using FluentMigrator; -using NzbDrone.Common.Extensions; -using NzbDrone.Core.Datastore.Migration.Framework; - -namespace NzbDrone.Core.Datastore.Migration -{ - [Migration(110)] - public class fix_extra_files_config : NzbDroneMigrationBase - { - protected override void MainDbUpgrade() - { - Execute.WithConnection(FixExtraFilesConfig); - } - - private void FixExtraFilesConfig(IDbConnection conn, IDbTransaction tran) - { - string extraFileExtensions; - string importExtraFiles; - - using (var cmd = conn.CreateCommand()) - { - cmd.Transaction = tran; - cmd.CommandText = "SELECT Value FROM Config WHERE Key = 'extrafileextensions'"; - - extraFileExtensions = (string)cmd.ExecuteScalar(); - } - - using (var cmd = conn.CreateCommand()) - { - cmd.Transaction = tran; - cmd.CommandText = "SELECT Value FROM Config WHERE Key = 'importextrafiles'"; - - importExtraFiles = (string)cmd.ExecuteScalar(); - } - - if (importExtraFiles == "1" || importExtraFiles == "True") - { - using (var insertCmd = conn.CreateCommand()) - { - insertCmd.Transaction = tran; - insertCmd.CommandText = "UPDATE Config SET Value = 'True' WHERE Key = 'importextrafiles'"; - insertCmd.ExecuteNonQuery(); - } - } - else if (extraFileExtensions.IsNullOrWhiteSpace()) - { - using (var insertCmd = conn.CreateCommand()) - { - insertCmd.Transaction = tran; - insertCmd.CommandText = "UPDATE Config SET Value = 'srt' WHERE Key = 'extrafileextensions'"; - insertCmd.ExecuteNonQuery(); - } - } - } - } -} diff --git a/src/NzbDrone.Core/Datastore/Migration/111_setup_music.cs b/src/NzbDrone.Core/Datastore/Migration/111_setup_music.cs deleted file mode 100644 index 117412062..000000000 --- a/src/NzbDrone.Core/Datastore/Migration/111_setup_music.cs +++ /dev/null @@ -1,106 +0,0 @@ -using FluentMigrator; -using NzbDrone.Core.Datastore.Migration.Framework; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; - -namespace NzbDrone.Core.Datastore.Migration -{ - [Migration(111)] - public class setup_music : NzbDroneMigrationBase - { - protected override void MainDbUpgrade() - { - Create.TableForModel("Artists") - .WithColumn("ForeignArtistId").AsString().Unique() - .WithColumn("MBId").AsString().Nullable() - .WithColumn("AMId").AsString().Nullable() - .WithColumn("TADBId").AsInt32().Nullable() - .WithColumn("DiscogsId").AsInt32().Nullable() - .WithColumn("Name").AsString() - .WithColumn("NameSlug").AsString().Nullable().Unique() - .WithColumn("CleanName").AsString().Indexed() - .WithColumn("Status").AsInt32() - .WithColumn("Overview").AsString().Nullable() - .WithColumn("Images").AsString() - .WithColumn("Path").AsString().Indexed() - .WithColumn("Monitored").AsBoolean() - .WithColumn("AlbumFolder").AsBoolean() - .WithColumn("LastInfoSync").AsDateTime().Nullable() - .WithColumn("LastDiskSync").AsDateTime().Nullable() - .WithColumn("DateFormed").AsDateTime().Nullable() - .WithColumn("Members").AsString().Nullable() - .WithColumn("Ratings").AsString().Nullable() - .WithColumn("Genres").AsString().Nullable() - .WithColumn("SortName").AsString().Nullable() - .WithColumn("ProfileId").AsInt32().Nullable() - .WithColumn("Tags").AsString().Nullable() - .WithColumn("Added").AsDateTime().Nullable() - .WithColumn("AddOptions").AsString().Nullable(); - - Create.TableForModel("Albums") - .WithColumn("ForeignAlbumId").AsString().Unique() - .WithColumn("ArtistId").AsInt32() - .WithColumn("MBId").AsString().Nullable().Indexed() - .WithColumn("AMId").AsString().Nullable() - .WithColumn("TADBId").AsInt32().Nullable().Indexed() - .WithColumn("DiscogsId").AsInt32().Nullable() - .WithColumn("Title").AsString() - .WithColumn("TitleSlug").AsString().Nullable().Unique() - .WithColumn("CleanTitle").AsString().Indexed() - .WithColumn("Overview").AsString().Nullable() - .WithColumn("Images").AsString() - .WithColumn("Path").AsString().Indexed() - .WithColumn("Monitored").AsBoolean() - .WithColumn("LastInfoSync").AsDateTime().Nullable() - .WithColumn("LastDiskSync").AsDateTime().Nullable() - .WithColumn("ReleaseDate").AsDateTime().Nullable() - .WithColumn("Ratings").AsString().Nullable() - .WithColumn("Genres").AsString().Nullable() - .WithColumn("Label").AsString().Nullable() - .WithColumn("SortTitle").AsString().Nullable() - .WithColumn("ProfileId").AsInt32().Nullable() - .WithColumn("Tags").AsString().Nullable() - .WithColumn("Added").AsDateTime().Nullable() - .WithColumn("AlbumType").AsString() - .WithColumn("AddOptions").AsString().Nullable(); - - Create.TableForModel("Tracks") - .WithColumn("ForeignTrackId").AsString().Unique() - .WithColumn("ArtistId").AsInt32().Indexed() - .WithColumn("AlbumId").AsInt32() - .WithColumn("MBId").AsString().Nullable().Indexed() - .WithColumn("TrackNumber").AsInt32() - .WithColumn("Title").AsString().Nullable() - .WithColumn("Explicit").AsBoolean() - .WithColumn("Compilation").AsBoolean() - .WithColumn("DiscNumber").AsInt32().Nullable() - .WithColumn("TrackFileId").AsInt32().Nullable().Indexed() - .WithColumn("Monitored").AsBoolean() - .WithColumn("Ratings").AsString().Nullable(); - - Create.Index().OnTable("Tracks").OnColumn("ArtistId").Ascending() - .OnColumn("AlbumId").Ascending() - .OnColumn("TrackNumber").Ascending(); - - Create.TableForModel("TrackFiles") - .WithColumn("ArtistId").AsInt32().Indexed() - .WithColumn("AlbumId").AsInt32().Indexed() - .WithColumn("Quality").AsString() - .WithColumn("Size").AsInt64() - .WithColumn("DateAdded").AsDateTime() - .WithColumn("SceneName").AsString().Nullable() - .WithColumn("ReleaseGroup").AsString().Nullable() - .WithColumn("MediaInfo").AsString().Nullable() - .WithColumn("RelativePath").AsString().Nullable(); - - Alter.Table("NamingConfig") - .AddColumn("ArtistFolderFormat").AsString().Nullable() - .AddColumn("RenameTracks").AsBoolean().Nullable() - .AddColumn("StandardTrackFormat").AsString().Nullable() - .AddColumn("AlbumFolderFormat").AsString().Nullable(); - } - - } -} diff --git a/src/NzbDrone.Core/Datastore/Migration/112_music_history.cs b/src/NzbDrone.Core/Datastore/Migration/112_music_history.cs deleted file mode 100644 index 48d287843..000000000 --- a/src/NzbDrone.Core/Datastore/Migration/112_music_history.cs +++ /dev/null @@ -1,36 +0,0 @@ -using FluentMigrator; -using NzbDrone.Core.Datastore.Migration.Framework; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; - -namespace NzbDrone.Core.Datastore.Migration -{ - [Migration(112)] - public class music_history : NzbDroneMigrationBase - { - protected override void MainDbUpgrade() - { - Alter.Table("History") - .AddColumn("ArtistId").AsInt32().WithDefaultValue(0) - .AddColumn("AlbumId").AsInt32().WithDefaultValue(0); - - Alter.Table("PendingReleases") - .AddColumn("ArtistId").AsInt32().WithDefaultValue(0) - .AddColumn("ParsedAlbumInfo").AsString().WithDefaultValue(""); - - Alter.Table("Tracks") - .AddColumn("Duration").AsInt32().WithDefaultValue(0); - - Alter.Table("Albums") - .AddColumn("Duration").AsInt32().WithDefaultValue(0); - - Delete.Column("SeriesId").FromTable("History"); - Delete.Column("EpisodeId").FromTable("History"); - Delete.Column("SeriesId").FromTable("PendingReleases"); - Delete.Column("ParsedEpisodeInfo").FromTable("PendingReleases"); - } - - } -} diff --git a/src/NzbDrone.Core/Datastore/Migration/113_music_blacklist.cs b/src/NzbDrone.Core/Datastore/Migration/113_music_blacklist.cs deleted file mode 100644 index 49c151f51..000000000 --- a/src/NzbDrone.Core/Datastore/Migration/113_music_blacklist.cs +++ /dev/null @@ -1,24 +0,0 @@ -using FluentMigrator; -using NzbDrone.Core.Datastore.Migration.Framework; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; - -namespace NzbDrone.Core.Datastore.Migration -{ - [Migration(113)] - public class music_blacklist : NzbDroneMigrationBase - { - protected override void MainDbUpgrade() - { - Alter.Table("Blacklist") - .AddColumn("ArtistId").AsInt32().WithDefaultValue(0) - .AddColumn("AlbumIds").AsString().WithDefaultValue(""); - - Delete.Column("SeriesId").FromTable("Blacklist"); - Delete.Column("EpisodeIds").FromTable("Blacklist"); - } - - } -} diff --git a/src/NzbDrone.Core/Datastore/Migration/114_remove_tv_naming.cs b/src/NzbDrone.Core/Datastore/Migration/114_remove_tv_naming.cs deleted file mode 100644 index 7d24e8626..000000000 --- a/src/NzbDrone.Core/Datastore/Migration/114_remove_tv_naming.cs +++ /dev/null @@ -1,27 +0,0 @@ -using FluentMigrator; -using NzbDrone.Core.Datastore.Migration.Framework; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; - -namespace NzbDrone.Core.Datastore.Migration -{ - [Migration(114)] - public class remove_tv_naming : NzbDroneMigrationBase - { - protected override void MainDbUpgrade() - { - Delete.Column("RenameEpisodes").FromTable("NamingConfig"); - Delete.Column("StandardEpisodeFormat").FromTable("NamingConfig"); - Delete.Column("DailyEpisodeFormat").FromTable("NamingConfig"); - Delete.Column("AnimeEpisodeFormat").FromTable("NamingConfig"); - Delete.Column("SeasonFolderFormat").FromTable("NamingConfig"); - Delete.Column("SeriesFolderFormat").FromTable("NamingConfig"); - Delete.Column("MultiEpisodeStyle").FromTable("NamingConfig"); - - Execute.Sql("DELETE FROM Config WHERE [Key] = 'filedate'"); - } - - } -} diff --git a/src/NzbDrone.Core/Datastore/Migration/115_change_drone_factory_variable_name.cs b/src/NzbDrone.Core/Datastore/Migration/115_change_drone_factory_variable_name.cs deleted file mode 100644 index cb625ccdc..000000000 --- a/src/NzbDrone.Core/Datastore/Migration/115_change_drone_factory_variable_name.cs +++ /dev/null @@ -1,20 +0,0 @@ -using FluentMigrator; -using NzbDrone.Core.Datastore.Migration.Framework; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; - -namespace NzbDrone.Core.Datastore.Migration -{ - [Migration(115)] - public class change_drone_factory_variable_name : NzbDroneMigrationBase - { - protected override void MainDbUpgrade() - { - Execute.Sql("DELETE FROM Config WHERE [Key] = 'downloadedepisodesfolder'"); - Execute.Sql("DELETE FROM Config WHERE [Key] = 'downloadedepisodesscaninterval'"); - } - - } -} diff --git a/src/NzbDrone.Core/Datastore/Migration/Framework/NzbDroneMigrationBase.cs b/src/NzbDrone.Core/Datastore/Migration/Framework/NzbDroneMigrationBase.cs index 7ebac899c..a59bceb66 100644 --- a/src/NzbDrone.Core/Datastore/Migration/Framework/NzbDroneMigrationBase.cs +++ b/src/NzbDrone.Core/Datastore/Migration/Framework/NzbDroneMigrationBase.cs @@ -1,4 +1,5 @@ -using System; +using System; +using System.Windows.Forms; using FluentMigrator; using NLog; using NzbDrone.Common.Instrumentation; diff --git a/src/NzbDrone.Core/Datastore/Migration/Framework/NzbDroneSqliteProcessor.cs b/src/NzbDrone.Core/Datastore/Migration/Framework/NzbDroneSqliteProcessor.cs index 79a9eca45..929a89e9d 100644 --- a/src/NzbDrone.Core/Datastore/Migration/Framework/NzbDroneSqliteProcessor.cs +++ b/src/NzbDrone.Core/Datastore/Migration/Framework/NzbDroneSqliteProcessor.cs @@ -1,12 +1,15 @@ -using System; +using System; +using System.Collections.Generic; using System.Data; using System.Linq; using FluentMigrator; using FluentMigrator.Expressions; using FluentMigrator.Model; using FluentMigrator.Runner; +using FluentMigrator.Runner.Announcers; using FluentMigrator.Runner.Generators.SQLite; using FluentMigrator.Runner.Processors.SQLite; +using System.Text.RegularExpressions; namespace NzbDrone.Core.Datastore.Migration.Framework { @@ -62,15 +65,55 @@ namespace NzbDrone.Core.Datastore.Migration.Framework ProcessAlterTable(tableDefinition); } + public override void Process(RenameColumnExpression expression) + { + var tableDefinition = GetTableSchema(expression.TableName); + + var oldColumnDefinitions = tableDefinition.Columns.ToList(); + var columnDefinitions = tableDefinition.Columns.ToList(); + var columnIndex = columnDefinitions.FindIndex(c => c.Name == expression.OldName); + + if (columnIndex == -1) + { + throw new ApplicationException(string.Format("Column {0} does not exist on table {1}.", expression.OldName, expression.TableName)); + } + if (columnDefinitions.Any(c => c.Name == expression.NewName)) + { + throw new ApplicationException(string.Format("Column {0} already exists on table {1}.", expression.NewName, expression.TableName)); + } + + oldColumnDefinitions[columnIndex] = (ColumnDefinition)columnDefinitions[columnIndex].Clone(); + columnDefinitions[columnIndex].Name = expression.NewName; + + foreach (var index in tableDefinition.Indexes) + { + if (index.Name.StartsWith("IX_")) + { + index.Name = Regex.Replace(index.Name, "(?<=_)" + Regex.Escape(expression.OldName) + "(?=_|$)", Regex.Escape(expression.NewName)); + } + + foreach (var column in index.Columns) + { + if (column.Name == expression.OldName) + { + column.Name = expression.NewName; + } + } + } + + ProcessAlterTable(tableDefinition, oldColumnDefinitions); + } + + protected virtual TableDefinition GetTableSchema(string tableName) { - var schemaDumper = new SqliteSchemaDumper(this, Announcer); + var schemaDumper = new SqliteSchemaDumper(this, Announcer); var schema = schemaDumper.ReadDbSchema(); return schema.Single(v => v.Name == tableName); } - protected virtual void ProcessAlterTable(TableDefinition tableDefinition) + protected virtual void ProcessAlterTable(TableDefinition tableDefinition, List oldColumnDefinitions = null) { var tableName = tableDefinition.Name; var tempTableName = tableName + "_temp"; @@ -83,11 +126,13 @@ namespace NzbDrone.Core.Datastore.Migration.Framework // What is the cleanest way to do this? Add function to Generator? var quoter = new SQLiteQuoter(); - var columnsToTransfer = string.Join(", ", tableDefinition.Columns.Select(c => quoter.QuoteColumnName(c.Name))); + var columnsToInsert = string.Join(", ", tableDefinition.Columns.Select(c => quoter.QuoteColumnName(c.Name))); + var columnsToFetch = string.Join(", ", (oldColumnDefinitions ?? tableDefinition.Columns).Select(c => quoter.QuoteColumnName(c.Name))); + Process(new CreateTableExpression() { TableName = tempTableName, Columns = tableDefinition.Columns.ToList() }); - Process(string.Format("INSERT INTO {0} SELECT {1} FROM {2}", quoter.QuoteTableName(tempTableName), columnsToTransfer, quoter.QuoteTableName(tableName))); + Process(string.Format("INSERT INTO {0} ({1}) SELECT {2} FROM {3}", quoter.QuoteTableName(tempTableName), columnsToInsert, columnsToFetch, quoter.QuoteTableName(tableName))); Process(new DeleteTableExpression() { TableName = tableName }); diff --git a/src/NzbDrone.Core/Datastore/TableMapping.cs b/src/NzbDrone.Core/Datastore/TableMapping.cs index 37a3a7658..9d04ba326 100644 --- a/src/NzbDrone.Core/Datastore/TableMapping.cs +++ b/src/NzbDrone.Core/Datastore/TableMapping.cs @@ -1,11 +1,10 @@ -using System; +using System; using System.Collections.Generic; using Marr.Data; using Marr.Data.Mapping; using NzbDrone.Common.Reflection; using NzbDrone.Core.Blacklisting; using NzbDrone.Core.Configuration; -using NzbDrone.Core.DataAugmentation.Scene; using NzbDrone.Core.Datastore.Converters; using NzbDrone.Core.Datastore.Extensions; using NzbDrone.Core.Download; @@ -19,23 +18,23 @@ using NzbDrone.Core.RemotePathMappings; using NzbDrone.Core.Notifications; using NzbDrone.Core.Organizer; using NzbDrone.Core.Parser.Model; -using NzbDrone.Core.Profiles; +using NzbDrone.Core.Profiles.Qualities; using NzbDrone.Core.Qualities; using NzbDrone.Core.Restrictions; using NzbDrone.Core.RootFolders; using NzbDrone.Core.ArtistStats; -using NzbDrone.Core.SeriesStats; using NzbDrone.Core.Tags; using NzbDrone.Core.ThingiProvider; -using NzbDrone.Core.Tv; using NzbDrone.Common.Disk; using NzbDrone.Core.Authentication; using NzbDrone.Core.Extras.Metadata; using NzbDrone.Core.Extras.Metadata.Files; using NzbDrone.Core.Extras.Others; -using NzbDrone.Core.Extras.Subtitles; +using NzbDrone.Core.Extras.Lyrics; using NzbDrone.Core.Messaging.Commands; using NzbDrone.Core.Music; +using NzbDrone.Core.Languages; +using NzbDrone.Core.Profiles.Languages; namespace NzbDrone.Core.Datastore { @@ -55,7 +54,8 @@ namespace NzbDrone.Core.Datastore .Ignore(i => i.Enable) .Ignore(i => i.Protocol) .Ignore(i => i.SupportsRss) - .Ignore(i => i.SupportsSearch); + .Ignore(i => i.SupportsSearch) + .Ignore(d => d.Tags); Mapper.Entity().RegisterDefinition("Notifications") .Ignore(i => i.SupportsOnGrab) @@ -63,40 +63,21 @@ namespace NzbDrone.Core.Datastore .Ignore(i => i.SupportsOnUpgrade) .Ignore(i => i.SupportsOnRename); - Mapper.Entity().RegisterDefinition("Metadata"); + Mapper.Entity().RegisterDefinition("Metadata") + .Ignore(d => d.Tags); Mapper.Entity().RegisterDefinition("DownloadClients") - .Ignore(d => d.Protocol); - - Mapper.Entity().RegisterModel("SceneMappings"); + .Ignore(d => d.Protocol) + .Ignore(d => d.Tags); Mapper.Entity().RegisterModel("History") .AutoMapChildModels(); - Mapper.Entity().RegisterModel("Series") - .Ignore(s => s.RootFolderPath) - .Relationship() - .HasOne(s => s.Profile, s => s.ProfileId); - - Mapper.Entity().RegisterModel("EpisodeFiles") - .Ignore(f => f.Path) - .Relationships.AutoMapICollectionOrComplexProperties() - .For("Episodes") - .LazyLoad(condition: parent => parent.Id > 0, - query: (db, parent) => db.Query().Where(c => c.EpisodeFileId == parent.Id).ToList()) - .HasOne(file => file.Series, file => file.SeriesId); - - Mapper.Entity().RegisterModel("Episodes") - .Ignore(e => e.SeriesTitle) - .Ignore(e => e.Series) - .Ignore(e => e.HasFile) - .Relationship() - .HasOne(episode => episode.EpisodeFile, episode => episode.EpisodeFileId); - Mapper.Entity().RegisterModel("Artists") .Ignore(s => s.RootFolderPath) .Relationship() - .HasOne(a => a.Profile, a => a.ProfileId); + .HasOne(a => a.Profile, a => a.ProfileId) + .HasOne(s => s.LanguageProfile, s => s.LanguageProfileId); Mapper.Entity().RegisterModel("Albums"); @@ -105,7 +86,7 @@ namespace NzbDrone.Core.Datastore .Relationships.AutoMapICollectionOrComplexProperties() .For("Tracks") .LazyLoad(condition: parent => parent.Id > 0, - query: (db, parent) => db.Query().Where(c => c.ArtistId == parent.Id).ToList()) // TODO: Figure what the hell to do here + query: (db, parent) => db.Query().Where(c => c.TrackFileId == parent.Id).ToList()) // TODO: Figure what the hell to do here .HasOne(file => file.Artist, file => file.ArtistId); Mapper.Entity().RegisterModel("Tracks") @@ -120,16 +101,17 @@ namespace NzbDrone.Core.Datastore .Ignore(d => d.Weight); Mapper.Entity().RegisterModel("Profiles"); + Mapper.Entity().RegisterModel("LanguageProfiles"); Mapper.Entity().RegisterModel("Logs"); Mapper.Entity().RegisterModel("NamingConfig"); Mapper.Entity().MapResultSet(); Mapper.Entity().RegisterModel("Blacklist"); Mapper.Entity().RegisterModel("MetadataFiles"); - Mapper.Entity().RegisterModel("SubtitleFiles"); + Mapper.Entity().RegisterModel("LyricFiles"); Mapper.Entity().RegisterModel("ExtraFiles"); Mapper.Entity().RegisterModel("PendingReleases") - .Ignore(e => e.RemoteEpisode); + .Ignore(e => e.RemoteAlbum); Mapper.Entity().RegisterModel("RemotePathMappings"); Mapper.Entity().RegisterModel("Tags"); @@ -141,6 +123,7 @@ namespace NzbDrone.Core.Datastore .Ignore(c => c.Message); Mapper.Entity().RegisterModel("IndexerStatus"); + Mapper.Entity().RegisterModel("DownloadClientStatus"); } private static void RegisterMappers() @@ -158,8 +141,11 @@ namespace NzbDrone.Core.Datastore MapRepository.Instance.RegisterTypeConverter(typeof(QualityModel), new EmbeddedDocumentConverter(new QualityIntConverter())); MapRepository.Instance.RegisterTypeConverter(typeof(Dictionary), new EmbeddedDocumentConverter()); MapRepository.Instance.RegisterTypeConverter(typeof(List), new EmbeddedDocumentConverter()); + MapRepository.Instance.RegisterTypeConverter(typeof(Language), new LanguageIntConverter()); MapRepository.Instance.RegisterTypeConverter(typeof(List), new EmbeddedDocumentConverter()); - MapRepository.Instance.RegisterTypeConverter(typeof(ParsedEpisodeInfo), new EmbeddedDocumentConverter()); + MapRepository.Instance.RegisterTypeConverter(typeof(List), new EmbeddedDocumentConverter(new LanguageIntConverter())); + MapRepository.Instance.RegisterTypeConverter(typeof(ParsedAlbumInfo), new EmbeddedDocumentConverter()); + MapRepository.Instance.RegisterTypeConverter(typeof(ParsedTrackInfo), new EmbeddedDocumentConverter()); MapRepository.Instance.RegisterTypeConverter(typeof(ReleaseInfo), new EmbeddedDocumentConverter()); MapRepository.Instance.RegisterTypeConverter(typeof(HashSet), new EmbeddedDocumentConverter()); MapRepository.Instance.RegisterTypeConverter(typeof(OsPath), new OsPathConverter()); @@ -196,4 +182,4 @@ namespace NzbDrone.Core.Datastore } } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Core/DecisionEngine/DownloadDecisionComparer.cs b/src/NzbDrone.Core/DecisionEngine/DownloadDecisionComparer.cs index ab7020625..1410dfcc7 100644 --- a/src/NzbDrone.Core/DecisionEngine/DownloadDecisionComparer.cs +++ b/src/NzbDrone.Core/DecisionEngine/DownloadDecisionComparer.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using NzbDrone.Core.Indexers; @@ -23,6 +23,7 @@ namespace NzbDrone.Core.DecisionEngine var comparers = new List { CompareQuality, + CompareLanguage, CompareProtocol, ComparePeersIfTorrent, CompareAlbumCount, @@ -45,7 +46,7 @@ namespace NzbDrone.Core.DecisionEngine private int CompareByReverse(TSubject left, TSubject right, Func funcValue) where TValue : IComparable { - return CompareBy(left, right, funcValue)*-1; + return CompareBy(left, right, funcValue) * -1; } private int CompareAll(params int[] comparers) @@ -60,6 +61,11 @@ namespace NzbDrone.Core.DecisionEngine CompareBy(x.RemoteAlbum, y.RemoteAlbum, remoteAlbum => remoteAlbum.ParsedAlbumInfo.Quality.Revision.Version)); } + private int CompareLanguage(DownloadDecision x, DownloadDecision y) + { + return CompareBy(x.RemoteAlbum, y.RemoteAlbum, remoteAlbum => remoteAlbum.Artist.LanguageProfile.Value.Languages.FindIndex(l => l.Language == remoteAlbum.ParsedAlbumInfo.Language)); + } + private int CompareProtocol(DownloadDecision x, DownloadDecision y) { var result = CompareBy(x.RemoteAlbum, y.RemoteAlbum, remoteAlbum => diff --git a/src/NzbDrone.Core/DecisionEngine/DownloadDecisionMaker.cs b/src/NzbDrone.Core/DecisionEngine/DownloadDecisionMaker.cs index 7976f57b8..c7aea4b34 100644 --- a/src/NzbDrone.Core/DecisionEngine/DownloadDecisionMaker.cs +++ b/src/NzbDrone.Core/DecisionEngine/DownloadDecisionMaker.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using NLog; @@ -62,23 +62,36 @@ namespace NzbDrone.Core.DecisionEngine { var parsedAlbumInfo = Parser.Parser.ParseAlbumTitle(report.Title); - if (parsedAlbumInfo != null && !parsedAlbumInfo.ArtistName.IsNullOrWhiteSpace()) + if (parsedAlbumInfo != null) { - var remoteAlbum = _parsingService.Map(parsedAlbumInfo, searchCriteria); - remoteAlbum.Release = report; - - if (remoteAlbum.Artist == null) + if (!report.Artist.IsNullOrWhiteSpace()) { - decision = new DownloadDecision(remoteAlbum, new Rejection("Unknown Artist")); + parsedAlbumInfo.ArtistName = report.Artist; } - else if (remoteAlbum.Albums.Empty()) + + if (!report.Album.IsNullOrWhiteSpace()) { - decision = new DownloadDecision(remoteAlbum, new Rejection("Unable to parse albums from release name")); + parsedAlbumInfo.AlbumTitle = report.Album; } - else + + if (!parsedAlbumInfo.ArtistName.IsNullOrWhiteSpace()) { - remoteAlbum.DownloadAllowed = remoteAlbum.Albums.Any(); - decision = GetDecisionForReport(remoteAlbum, searchCriteria); + var remoteAlbum = _parsingService.Map(parsedAlbumInfo, searchCriteria); + remoteAlbum.Release = report; + + if (remoteAlbum.Artist == null) + { + decision = new DownloadDecision(remoteAlbum, new Rejection("Unknown Artist")); + } + else if (remoteAlbum.Albums.Empty()) + { + decision = new DownloadDecision(remoteAlbum, new Rejection("Unable to parse albums from release name")); + } + else + { + remoteAlbum.DownloadAllowed = remoteAlbum.Albums.Any(); + decision = GetDecisionForReport(remoteAlbum, searchCriteria); + } } } } @@ -111,9 +124,16 @@ namespace NzbDrone.Core.DecisionEngine private DownloadDecision GetDecisionForReport(RemoteAlbum remoteAlbum, SearchCriteriaBase searchCriteria = null) { - var reasons = _specifications.Select(c => EvaluateSpec(c, remoteAlbum, searchCriteria)) - .Where(c => c != null); + var reasons = new Rejection[0]; + foreach (var specifications in _specifications.GroupBy(v => v.Priority).OrderBy(v => v.Key)) + { + reasons = specifications.Select(c => EvaluateSpec(c, remoteAlbum, searchCriteria)) + .Where(c => c != null) + .ToArray(); + + if (reasons.Any()) break; + } return new DownloadDecision(remoteAlbum, reasons.ToArray()); } diff --git a/src/NzbDrone.Core/DecisionEngine/DownloadDecisionPriorizationService.cs b/src/NzbDrone.Core/DecisionEngine/DownloadDecisionPriorizationService.cs index 469141328..710cdf2db 100644 --- a/src/NzbDrone.Core/DecisionEngine/DownloadDecisionPriorizationService.cs +++ b/src/NzbDrone.Core/DecisionEngine/DownloadDecisionPriorizationService.cs @@ -1,6 +1,7 @@ -using System.Linq; +using System.Linq; using System.Collections.Generic; using NzbDrone.Core.Profiles.Delay; +using NzbDrone.Core.Languages; namespace NzbDrone.Core.DecisionEngine { diff --git a/src/NzbDrone.Core/DecisionEngine/IDecisionEngineSpecification.cs b/src/NzbDrone.Core/DecisionEngine/IDecisionEngineSpecification.cs index 937ce8d40..cafede0fb 100644 --- a/src/NzbDrone.Core/DecisionEngine/IDecisionEngineSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/IDecisionEngineSpecification.cs @@ -1,4 +1,4 @@ -using NzbDrone.Core.IndexerSearch.Definitions; +using NzbDrone.Core.IndexerSearch.Definitions; using NzbDrone.Core.Parser.Model; namespace NzbDrone.Core.DecisionEngine @@ -7,6 +7,8 @@ namespace NzbDrone.Core.DecisionEngine { RejectionType Type { get; } + SpecificationPriority Priority { get; } + Decision IsSatisfiedBy(RemoteAlbum subject, SearchCriteriaBase searchCriteria); } } diff --git a/src/NzbDrone.Core/DecisionEngine/LanguageUpgradableSpecification.cs b/src/NzbDrone.Core/DecisionEngine/LanguageUpgradableSpecification.cs new file mode 100644 index 000000000..4f150279b --- /dev/null +++ b/src/NzbDrone.Core/DecisionEngine/LanguageUpgradableSpecification.cs @@ -0,0 +1,75 @@ +using NLog; +using NzbDrone.Core.Languages; +using NzbDrone.Core.Profiles; +using NzbDrone.Core.Qualities; + +namespace NzbDrone.Core.DecisionEngine +{ + public interface ILanguageUpgradableSpecification + { + bool IsUpgradable(Profile profile, LanguageModel currentLanguage, LanguageModel newLanguage = null); + bool CutoffNotMet(Profile profile, LanguageModel currentLanguage, LanguageModel newLanguage = null); + bool IsRevisionUpgrade(LanguageModel currentLanguage, LanguageModel newLanguage); + } + + public class LanguageUpgradableSpecification : ILanguageUpgradableSpecification + { + private readonly Logger _logger; + + public LanguageUpgradableSpecification(Logger logger) + { + _logger = logger; + } + + public bool IsUpgradable(Profile profile, LanguageModel currentLanguage, LanguageModel newLanguage = null) + { + if (newLanguage != null) + { + int compare = new LanguageModelComparer(profile).Compare(newLanguage, currentLanguage); + if (compare <= 0) + { + _logger.Debug("existing item has better or equal language. skipping"); + return false; + } + + if (IsRevisionUpgrade(currentLanguage, newLanguage)) + { + return true; + } + } + + return true; + } + + public bool CutoffNotMet(Profile profile, LanguageModel currentLanguage, LanguageModel newLanguage = null) + { + int compare = new LanguageModelComparer(profile).Compare(currentLanguage.Language, profile.Languages.Find(v => v.Allowed == true).Language); + + if (compare >= 0) + { + if (newLanguage != null && IsRevisionUpgrade(currentLanguage, newLanguage)) + { + return true; + } + + _logger.Debug("Existing item meets cut-off. skipping."); + return false; + } + + return true; + } + + public bool IsRevisionUpgrade(LanguageModel currentLanguage, LanguageModel newLanguage) + { + int compare = newLanguage.Revision.CompareTo(currentLanguage.Revision); + + if (currentLanguage.Language == newLanguage.Language && compare > 0) + { + _logger.Debug("New language is a better revision for existing quality"); + return true; + } + + return false; + } + } +} diff --git a/src/NzbDrone.Core/DecisionEngine/QualityUpgradableSpecification.cs b/src/NzbDrone.Core/DecisionEngine/QualityUpgradableSpecification.cs deleted file mode 100644 index 22c4824af..000000000 --- a/src/NzbDrone.Core/DecisionEngine/QualityUpgradableSpecification.cs +++ /dev/null @@ -1,72 +0,0 @@ -using NLog; -using NzbDrone.Core.Profiles; -using NzbDrone.Core.Qualities; - -namespace NzbDrone.Core.DecisionEngine -{ - public interface IQualityUpgradableSpecification - { - bool IsUpgradable(Profile profile, QualityModel currentQuality, QualityModel newQuality = null); - bool CutoffNotMet(Profile profile, QualityModel currentQuality, QualityModel newQuality = null); - bool IsRevisionUpgrade(QualityModel currentQuality, QualityModel newQuality); - } - - public class QualityUpgradableSpecification : IQualityUpgradableSpecification - { - private readonly Logger _logger; - - public QualityUpgradableSpecification(Logger logger) - { - _logger = logger; - } - - public bool IsUpgradable(Profile profile, QualityModel currentQuality, QualityModel newQuality = null) - { - if (newQuality != null) - { - int compare = new QualityModelComparer(profile).Compare(newQuality, currentQuality); - if (compare <= 0) - { - return false; - } - - if (IsRevisionUpgrade(currentQuality, newQuality)) - { - return true; - } - } - - return true; - } - - public bool CutoffNotMet(Profile profile, QualityModel currentQuality, QualityModel newQuality = null) - { - var compare = new QualityModelComparer(profile).Compare(currentQuality.Quality, profile.Cutoff); - - if (compare < 0) - { - return true; - } - - if (newQuality != null && IsRevisionUpgrade(currentQuality, newQuality)) - { - return true; - } - - return false; - - } - - public bool IsRevisionUpgrade(QualityModel currentQuality, QualityModel newQuality) - { - var compare = newQuality.Revision.CompareTo(currentQuality.Revision); - - if (currentQuality.Quality == newQuality.Quality && compare > 0) - { - return true; - } - - return false; - } - } -} diff --git a/src/NzbDrone.Core/DecisionEngine/SameEpisodesSpecification.cs b/src/NzbDrone.Core/DecisionEngine/SameEpisodesSpecification.cs deleted file mode 100644 index 65bf4f1ec..000000000 --- a/src/NzbDrone.Core/DecisionEngine/SameEpisodesSpecification.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using NzbDrone.Common.Extensions; -using NzbDrone.Core.Tv; - -namespace NzbDrone.Core.DecisionEngine -{ - public class SameEpisodesSpecification - { - private readonly IEpisodeService _episodeService; - - public SameEpisodesSpecification(IEpisodeService episodeService) - { - _episodeService = episodeService; - } - - public bool IsSatisfiedBy(List episodes) - { - var episodeIds = episodes.SelectList(e => e.Id); - var episodeFileIds = episodes.Where(c => c.EpisodeFileId != 0).Select(c => c.EpisodeFileId).Distinct(); - - foreach (var episodeFileId in episodeFileIds) - { - var episodesInFile = _episodeService.GetEpisodesByFileId(episodeFileId); - - if (episodesInFile.Select(e => e.Id).Except(episodeIds).Any()) - { - return false; - } - } - - return true; - } - } -} diff --git a/src/NzbDrone.Core/DecisionEngine/SpecificationPriority.cs b/src/NzbDrone.Core/DecisionEngine/SpecificationPriority.cs new file mode 100644 index 000000000..2d720c4b9 --- /dev/null +++ b/src/NzbDrone.Core/DecisionEngine/SpecificationPriority.cs @@ -0,0 +1,10 @@ +namespace NzbDrone.Core.DecisionEngine +{ + public enum SpecificationPriority + { + Default = 0, + Parsing = 0, + Database = 0, + Disk = 1 + } +} diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/AcceptableSizeSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/AcceptableSizeSpecification.cs index 09045d5fe..62a7738f8 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/AcceptableSizeSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/AcceptableSizeSpecification.cs @@ -1,4 +1,4 @@ -using System.Linq; +using System.Linq; using NLog; using NzbDrone.Common.Extensions; using NzbDrone.Core.IndexerSearch.Definitions; @@ -19,6 +19,7 @@ namespace NzbDrone.Core.DecisionEngine.Specifications _logger = logger; } + public SpecificationPriority Priority => SpecificationPriority.Default; public RejectionType Type => RejectionType.Permanent; public Decision IsSatisfiedBy(RemoteAlbum subject, SearchCriteriaBase searchCriteria) diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/BlacklistSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/BlacklistSpecification.cs index 3e487abb0..0f471dad9 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/BlacklistSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/BlacklistSpecification.cs @@ -1,4 +1,4 @@ -using NLog; +using NLog; using NzbDrone.Core.Blacklisting; using NzbDrone.Core.IndexerSearch.Definitions; using NzbDrone.Core.Parser.Model; @@ -16,6 +16,7 @@ namespace NzbDrone.Core.DecisionEngine.Specifications _logger = logger; } + public SpecificationPriority Priority => SpecificationPriority.Database; public RejectionType Type => RejectionType.Permanent; public Decision IsSatisfiedBy(RemoteAlbum subject, SearchCriteriaBase searchCriteria) diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/BlockedIndexerSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/BlockedIndexerSpecification.cs new file mode 100644 index 000000000..3ec06fe7e --- /dev/null +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/BlockedIndexerSpecification.cs @@ -0,0 +1,46 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using NLog; +using NzbDrone.Common.Cache; +using NzbDrone.Core.Indexers; +using NzbDrone.Core.IndexerSearch.Definitions; +using NzbDrone.Core.Parser.Model; + +namespace NzbDrone.Core.DecisionEngine.Specifications +{ + public class BlockedIndexerSpecification : IDecisionEngineSpecification + { + private readonly IIndexerStatusService _indexerStatusService; + private readonly Logger _logger; + + private readonly ICachedDictionary _blockedIndexerCache; + + public BlockedIndexerSpecification(IIndexerStatusService indexerStatusService, ICacheManager cacheManager, Logger logger) + { + _indexerStatusService = indexerStatusService; + _logger = logger; + + _blockedIndexerCache = cacheManager.GetCacheDictionary(GetType(), "blocked", FetchBlockedIndexer, TimeSpan.FromSeconds(15)); + } + + public SpecificationPriority Priority => SpecificationPriority.Database; + public RejectionType Type => RejectionType.Temporary; + + public virtual Decision IsSatisfiedBy(RemoteAlbum subject, SearchCriteriaBase searchCriteria) + { + var status = _blockedIndexerCache.Find(subject.Release.IndexerId.ToString()); + if (status != null) + { + return Decision.Reject($"Indexer {subject.Release.Indexer} is blocked till {status.DisabledTill} due to failures, cannot grab release."); + } + + return Decision.Accept(); + } + + private IDictionary FetchBlockedIndexer() + { + return _indexerStatusService.GetBlockedProviders().ToDictionary(v => v.ProviderId.ToString()); + } + } +} diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/CutoffSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/CutoffSpecification.cs index ff02eacdf..1801235fc 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/CutoffSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/CutoffSpecification.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Linq; using NLog; using NzbDrone.Core.IndexerSearch.Definitions; @@ -9,17 +9,18 @@ namespace NzbDrone.Core.DecisionEngine.Specifications { public class CutoffSpecification : IDecisionEngineSpecification { - private readonly QualityUpgradableSpecification _qualityUpgradableSpecification; + private readonly UpgradableSpecification _upgradableSpecification; private readonly IMediaFileService _mediaFileService; private readonly Logger _logger; - public CutoffSpecification(QualityUpgradableSpecification qualityUpgradableSpecification, Logger logger, IMediaFileService mediaFileService) + public CutoffSpecification(UpgradableSpecification upgradableSpecification, Logger logger, IMediaFileService mediaFileService) { - _qualityUpgradableSpecification = qualityUpgradableSpecification; + _upgradableSpecification = upgradableSpecification; _logger = logger; _mediaFileService = mediaFileService; } + public SpecificationPriority Priority => SpecificationPriority.Default; public RejectionType Type => RejectionType.Permanent; public virtual Decision IsSatisfiedBy(RemoteAlbum subject, SearchCriteriaBase searchCriteria) @@ -33,9 +34,13 @@ namespace NzbDrone.Core.DecisionEngine.Specifications { var lowestQuality = trackFiles.Select(c => c.Quality).OrderBy(c => c.Quality.Id).First(); - _logger.Debug("Comparing file quality with report. Existing file is {0}", lowestQuality); + _logger.Debug("Comparing file quality and language with report. Existing file is {0}", lowestQuality.Quality); - if (!_qualityUpgradableSpecification.CutoffNotMet(subject.Artist.Profile, lowestQuality, subject.ParsedAlbumInfo.Quality)) + if (!_upgradableSpecification.CutoffNotMet(subject.Artist.Profile, + subject.Artist.LanguageProfile, + lowestQuality, + trackFiles[0].Language, + subject.ParsedAlbumInfo.Quality)) { _logger.Debug("Cutoff already met, rejecting."); return Decision.Reject("Existing file meets cutoff: {0}", subject.Artist.Profile.Value.Cutoff); diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/DiscographySpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/DiscographySpecification.cs index ff25de723..a1e79112d 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/DiscographySpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/DiscographySpecification.cs @@ -1,4 +1,4 @@ -using System; +using System; using NLog; using NzbDrone.Core.IndexerSearch.Definitions; using NzbDrone.Core.Parser.Model; @@ -16,6 +16,7 @@ namespace NzbDrone.Core.DecisionEngine.Specifications _logger = logger; } + public SpecificationPriority Priority => SpecificationPriority.Default; public RejectionType Type => RejectionType.Permanent; public virtual Decision IsSatisfiedBy(RemoteAlbum subject, SearchCriteriaBase searchCriteria) diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/LanguageSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/LanguageSpecification.cs index 3ee173b22..51b9be10d 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/LanguageSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/LanguageSpecification.cs @@ -1,4 +1,5 @@ -using NLog; +using NLog; +using System.Linq; using NzbDrone.Core.IndexerSearch.Definitions; using NzbDrone.Core.Parser.Model; @@ -13,15 +14,17 @@ namespace NzbDrone.Core.DecisionEngine.Specifications _logger = logger; } + public SpecificationPriority Priority => SpecificationPriority.Default; public RejectionType Type => RejectionType.Permanent; public virtual Decision IsSatisfiedBy(RemoteAlbum subject, SearchCriteriaBase searchCriteria) { - var wantedLanguage = subject.Artist.Profile.Value.Language; + var wantedLanguage = subject.Artist.LanguageProfile.Value.Languages; + var _language = subject.ParsedAlbumInfo.Language; _logger.Debug("Checking if report meets language requirements. {0}", subject.ParsedAlbumInfo.Language); - if (subject.ParsedAlbumInfo.Language != wantedLanguage) + if (!wantedLanguage.Exists(v => v.Allowed && v.Language == _language)) { _logger.Debug("Report Language: {0} rejected because it is not wanted, wanted {1}", subject.ParsedAlbumInfo.Language, wantedLanguage); return Decision.Reject("{0} is wanted, but found {1}", wantedLanguage, subject.ParsedAlbumInfo.Language); diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/MinimumAgeSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/MinimumAgeSpecification.cs index 47b09ab66..3ec9b900e 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/MinimumAgeSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/MinimumAgeSpecification.cs @@ -1,4 +1,5 @@ -using NLog; +using System; +using NLog; using NzbDrone.Core.Configuration; using NzbDrone.Core.IndexerSearch.Definitions; using NzbDrone.Core.Parser.Model; @@ -16,6 +17,7 @@ namespace NzbDrone.Core.DecisionEngine.Specifications _logger = logger; } + public SpecificationPriority Priority => SpecificationPriority.Default; public RejectionType Type => RejectionType.Temporary; public virtual Decision IsSatisfiedBy(RemoteAlbum subject, SearchCriteriaBase searchCriteria) @@ -28,6 +30,7 @@ namespace NzbDrone.Core.DecisionEngine.Specifications var age = subject.Release.AgeMinutes; var minimumAge = _configService.MinimumAge; + var ageRounded = Math.Round(age, 1); if (minimumAge == 0) { @@ -36,15 +39,15 @@ namespace NzbDrone.Core.DecisionEngine.Specifications } - _logger.Debug("Checking if report meets minimum age requirements. {0}", age); + _logger.Debug("Checking if report meets minimum age requirements. {0}", ageRounded); if (age < minimumAge) { - _logger.Debug("Only {0} minutes old, minimum age is {1} minutes", age, minimumAge); - return Decision.Reject("Only {0} minutes old, minimum age is {1} minutes", age, minimumAge); + _logger.Debug("Only {0} minutes old, minimum age is {1} minutes", ageRounded, minimumAge); + return Decision.Reject("Only {0} minutes old, minimum age is {1} minutes", ageRounded, minimumAge); } - _logger.Debug("Release is {0} minutes old, greater than minimum age of {1} minutes", age, minimumAge); + _logger.Debug("Release is {0} minutes old, greater than minimum age of {1} minutes", ageRounded, minimumAge); return Decision.Accept(); } diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/NotSampleSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/NotSampleSpecification.cs index ab4e2e3ff..825ca3c3d 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/NotSampleSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/NotSampleSpecification.cs @@ -1,4 +1,4 @@ -using System; +using System; using NLog; using NzbDrone.Core.IndexerSearch.Definitions; using NzbDrone.Core.Parser.Model; @@ -9,6 +9,7 @@ namespace NzbDrone.Core.DecisionEngine.Specifications { private readonly Logger _logger; + public SpecificationPriority Priority => SpecificationPriority.Default; public RejectionType Type => RejectionType.Permanent; public NotSampleSpecification(Logger logger) diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/ProtocolSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/ProtocolSpecification.cs index 7d37b8770..dae103196 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/ProtocolSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/ProtocolSpecification.cs @@ -1,4 +1,4 @@ -using NLog; +using NLog; using NzbDrone.Core.Indexers; using NzbDrone.Core.IndexerSearch.Definitions; using NzbDrone.Core.Parser.Model; @@ -18,6 +18,7 @@ namespace NzbDrone.Core.DecisionEngine.Specifications _logger = logger; } + public SpecificationPriority Priority => SpecificationPriority.Default; public RejectionType Type => RejectionType.Permanent; public virtual Decision IsSatisfiedBy(RemoteAlbum subject, SearchCriteriaBase searchCriteria) diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/QualityAllowedByProfileSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/QualityAllowedByProfileSpecification.cs index cc2f44331..a02ee49e2 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/QualityAllowedByProfileSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/QualityAllowedByProfileSpecification.cs @@ -1,4 +1,4 @@ -using NLog; +using NLog; using NzbDrone.Core.IndexerSearch.Definitions; using NzbDrone.Core.Parser.Model; @@ -13,6 +13,7 @@ namespace NzbDrone.Core.DecisionEngine.Specifications _logger = logger; } + public SpecificationPriority Priority => SpecificationPriority.Default; public RejectionType Type => RejectionType.Permanent; public virtual Decision IsSatisfiedBy(RemoteAlbum subject, SearchCriteriaBase searchCriteria) diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/QueueSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/QueueSpecification.cs index 38464e36c..28cbc52b9 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/QueueSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/QueueSpecification.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Linq; using NLog; using NzbDrone.Core.IndexerSearch.Definitions; @@ -10,18 +10,19 @@ namespace NzbDrone.Core.DecisionEngine.Specifications public class QueueSpecification : IDecisionEngineSpecification { private readonly IQueueService _queueService; - private readonly QualityUpgradableSpecification _qualityUpgradableSpecification; + private readonly UpgradableSpecification _upgradableSpecification; private readonly Logger _logger; public QueueSpecification(IQueueService queueService, - QualityUpgradableSpecification qualityUpgradableSpecification, + UpgradableSpecification qualityUpgradableSpecification, Logger logger) { _queueService = queueService; - _qualityUpgradableSpecification = qualityUpgradableSpecification; + _upgradableSpecification = qualityUpgradableSpecification; _logger = logger; } + public SpecificationPriority Priority => SpecificationPriority.Default; public RejectionType Type => RejectionType.Permanent; public Decision IsSatisfiedBy(RemoteAlbum subject, SearchCriteriaBase searchCriteria) @@ -36,14 +37,23 @@ namespace NzbDrone.Core.DecisionEngine.Specifications { _logger.Debug("Checking if existing release in queue meets cutoff. Queued quality is: {0}", remoteAlbum.ParsedAlbumInfo.Quality); - if (!_qualityUpgradableSpecification.CutoffNotMet(subject.Artist.Profile, remoteAlbum.ParsedAlbumInfo.Quality, subject.ParsedAlbumInfo.Quality)) + if (!_upgradableSpecification.CutoffNotMet(subject.Artist.Profile, + subject.Artist.LanguageProfile, + remoteAlbum.ParsedAlbumInfo.Quality, + remoteAlbum.ParsedAlbumInfo.Language, + subject.ParsedAlbumInfo.Quality)) { return Decision.Reject("Quality for release in queue already meets cutoff: {0}", remoteAlbum.ParsedAlbumInfo.Quality); } _logger.Debug("Checking if release is higher quality than queued release. Queued quality is: {0}", remoteAlbum.ParsedAlbumInfo.Quality); - if (!_qualityUpgradableSpecification.IsUpgradable(subject.Artist.Profile, remoteAlbum.ParsedAlbumInfo.Quality, subject.ParsedAlbumInfo.Quality)) + if (!_upgradableSpecification.IsUpgradable(subject.Artist.Profile, + subject.Artist.LanguageProfile, + remoteAlbum.ParsedAlbumInfo.Quality, + remoteAlbum.ParsedAlbumInfo.Language, + subject.ParsedAlbumInfo.Quality, + subject.ParsedAlbumInfo.Language)) { return Decision.Reject("Quality for release in queue is of equal or higher preference: {0}", remoteAlbum.ParsedAlbumInfo.Quality); } diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/RawDiskSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/RawDiskSpecification.cs index c079cefce..a6a6acd54 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/RawDiskSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/RawDiskSpecification.cs @@ -1,4 +1,4 @@ -using System.Linq; +using System.Linq; using NLog; using NzbDrone.Common.Extensions; using NzbDrone.Core.IndexerSearch.Definitions; @@ -17,6 +17,7 @@ namespace NzbDrone.Core.DecisionEngine.Specifications _logger = logger; } + public SpecificationPriority Priority => SpecificationPriority.Default; public RejectionType Type => RejectionType.Permanent; public virtual Decision IsSatisfiedBy(RemoteAlbum subject, SearchCriteriaBase searchCriteria) diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/ReleaseRestrictionsSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/ReleaseRestrictionsSpecification.cs index 37de16679..ffa752f94 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/ReleaseRestrictionsSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/ReleaseRestrictionsSpecification.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using NLog; @@ -20,6 +20,7 @@ namespace NzbDrone.Core.DecisionEngine.Specifications _logger = logger; } + public SpecificationPriority Priority => SpecificationPriority.Default; public RejectionType Type => RejectionType.Permanent; public virtual Decision IsSatisfiedBy(RemoteAlbum subject, SearchCriteriaBase searchCriteria) diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/RetentionSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/RetentionSpecification.cs index f2f3b1fa3..c3deb05fd 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/RetentionSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/RetentionSpecification.cs @@ -1,4 +1,4 @@ -using NLog; +using NLog; using NzbDrone.Core.Configuration; using NzbDrone.Core.IndexerSearch.Definitions; using NzbDrone.Core.Parser.Model; @@ -16,6 +16,7 @@ namespace NzbDrone.Core.DecisionEngine.Specifications _logger = logger; } + public SpecificationPriority Priority => SpecificationPriority.Default; public RejectionType Type => RejectionType.Permanent; public virtual Decision IsSatisfiedBy(RemoteAlbum subject, SearchCriteriaBase searchCriteria) diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/DelaySpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/DelaySpecification.cs index ae9c64ec8..0627459df 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/DelaySpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/DelaySpecification.cs @@ -1,4 +1,4 @@ -using System.Linq; +using System.Linq; using NLog; using NzbDrone.Core.Download.Pending; using NzbDrone.Core.IndexerSearch.Definitions; @@ -6,30 +6,32 @@ using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Profiles.Delay; using NzbDrone.Core.Qualities; using NzbDrone.Core.MediaFiles; +using NzbDrone.Core.Languages; namespace NzbDrone.Core.DecisionEngine.Specifications.RssSync { public class DelaySpecification : IDecisionEngineSpecification { private readonly IPendingReleaseService _pendingReleaseService; - private readonly IQualityUpgradableSpecification _qualityUpgradableSpecification; + private readonly IUpgradableSpecification _upgradableSpecification; private readonly IDelayProfileService _delayProfileService; private readonly IMediaFileService _mediaFileService; private readonly Logger _logger; public DelaySpecification(IPendingReleaseService pendingReleaseService, - IQualityUpgradableSpecification qualityUpgradableSpecification, + IUpgradableSpecification qualityUpgradableSpecification, IDelayProfileService delayProfileService, IMediaFileService mediaFileService, Logger logger) { _pendingReleaseService = pendingReleaseService; - _qualityUpgradableSpecification = qualityUpgradableSpecification; + _upgradableSpecification = qualityUpgradableSpecification; _delayProfileService = delayProfileService; _mediaFileService = mediaFileService; _logger = logger; } + public SpecificationPriority Priority => SpecificationPriority.Database; public RejectionType Type => RejectionType.Temporary; public virtual Decision IsSatisfiedBy(RemoteAlbum subject, SearchCriteriaBase searchCriteria) @@ -41,6 +43,7 @@ namespace NzbDrone.Core.DecisionEngine.Specifications.RssSync } var profile = subject.Artist.Profile.Value; + var languageProfile = subject.Artist.LanguageProfile.Value; var delayProfile = _delayProfileService.BestForTags(subject.Artist.Tags); var delay = delayProfile.GetProtocolDelay(subject.Release.DownloadProtocol); var isPreferredProtocol = subject.Release.DownloadProtocol == delayProfile.PreferredProtocol; @@ -52,6 +55,7 @@ namespace NzbDrone.Core.DecisionEngine.Specifications.RssSync } var comparer = new QualityModelComparer(profile); + var comparerLanguage = new LanguageComparer(languageProfile); if (isPreferredProtocol) { @@ -62,17 +66,16 @@ namespace NzbDrone.Core.DecisionEngine.Specifications.RssSync if (trackFiles.Any()) { var lowestQuality = trackFiles.Select(c => c.Quality).OrderBy(c => c.Quality.Id).First(); - var upgradable = _qualityUpgradableSpecification.IsUpgradable(profile, lowestQuality, subject.ParsedAlbumInfo.Quality); - + var upgradable = _upgradableSpecification.IsUpgradable(profile, + languageProfile, + lowestQuality, + trackFiles[0].Language, + subject.ParsedAlbumInfo.Quality, + subject.ParsedAlbumInfo.Language); if (upgradable) { - var revisionUpgrade = _qualityUpgradableSpecification.IsRevisionUpgrade(lowestQuality, subject.ParsedAlbumInfo.Quality); - - if (revisionUpgrade) - { - _logger.Debug("New quality is a better revision for existing quality, skipping delay"); - return Decision.Accept(); - } + _logger.Debug("New quality is a better revision for existing quality, skipping delay"); + return Decision.Accept(); } } } @@ -81,10 +84,11 @@ namespace NzbDrone.Core.DecisionEngine.Specifications.RssSync // If quality meets or exceeds the best allowed quality in the profile accept it immediately var bestQualityInProfile = new QualityModel(profile.LastAllowedQuality()); var isBestInProfile = comparer.Compare(subject.ParsedAlbumInfo.Quality, bestQualityInProfile) >= 0; + var isBestInProfileLanguage = comparerLanguage.Compare(subject.ParsedAlbumInfo.Language, languageProfile.LastAllowedLanguage()) >= 0; - if (isBestInProfile && isPreferredProtocol) + if (isBestInProfile && isBestInProfileLanguage && isPreferredProtocol) { - _logger.Debug("Quality is highest in profile for preferred protocol, will not delay"); + _logger.Debug("Quality and language is highest in profile for preferred protocol, will not delay"); return Decision.Accept(); } diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/DeletedTrackFileSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/DeletedTrackFileSpecification.cs new file mode 100644 index 000000000..232753992 --- /dev/null +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/DeletedTrackFileSpecification.cs @@ -0,0 +1,76 @@ +using System.IO; +using System.Linq; +using NLog; +using NzbDrone.Common.Disk; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.Configuration; +using NzbDrone.Core.IndexerSearch.Definitions; +using NzbDrone.Core.MediaFiles; +using NzbDrone.Core.Parser.Model; +using NzbDrone.Core.Music; + +namespace NzbDrone.Core.DecisionEngine.Specifications.RssSync +{ + public class DeletedTrackFileSpecification : IDecisionEngineSpecification + { + private readonly IDiskProvider _diskProvider; + private readonly IConfigService _configService; + private readonly IMediaFileService _albumService; + private readonly Logger _logger; + + public DeletedTrackFileSpecification(IDiskProvider diskProvider, + IConfigService configService, + IMediaFileService albumService, + Logger logger) + { + _diskProvider = diskProvider; + _configService = configService; + _albumService = albumService; + _logger = logger; + } + + public SpecificationPriority Priority => SpecificationPriority.Disk; + public RejectionType Type => RejectionType.Temporary; + + public virtual Decision IsSatisfiedBy(RemoteAlbum subject, SearchCriteriaBase searchCriteria) + { + if (!_configService.AutoUnmonitorPreviouslyDownloadedTracks) + { + return Decision.Accept(); + } + + if (searchCriteria != null) + { + _logger.Debug("Skipping deleted trackfile check during search"); + return Decision.Accept(); + } + + var missingTrackFiles = subject.Albums + .SelectMany(v => _albumService.GetFilesByAlbum(v.ArtistId, v.Id)) + .DistinctBy(v => v.Id) + .Where(v => IsTrackFileMissing(subject.Artist, v)) + .ToArray(); + + + if (missingTrackFiles.Any()) + { + foreach (var missingTrackFile in missingTrackFiles) + { + _logger.Trace("Track file {0} is missing from disk.", missingTrackFile.RelativePath); + } + + _logger.Debug("Files for this album exist in the database but not on disk, will be unmonitored on next diskscan. skipping."); + return Decision.Reject("Artist is not monitored"); + } + + return Decision.Accept(); + } + + private bool IsTrackFileMissing(Artist artist, TrackFile trackFile) + { + var fullPath = Path.Combine(artist.Path, trackFile.RelativePath); + + return !_diskProvider.FileExists(fullPath); + } + } +} diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/HistorySpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/HistorySpecification.cs index d654cfd84..ab9b4f30b 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/HistorySpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/HistorySpecification.cs @@ -1,4 +1,4 @@ -using System; +using System; using NLog; using NzbDrone.Common.Extensions; using NzbDrone.Core.Configuration; @@ -11,21 +11,22 @@ namespace NzbDrone.Core.DecisionEngine.Specifications.RssSync public class HistorySpecification : IDecisionEngineSpecification { private readonly IHistoryService _historyService; - private readonly QualityUpgradableSpecification _qualityUpgradableSpecification; + private readonly UpgradableSpecification _upgradableSpecification; private readonly IConfigService _configService; private readonly Logger _logger; public HistorySpecification(IHistoryService historyService, - QualityUpgradableSpecification qualityUpgradableSpecification, + UpgradableSpecification qualityUpgradableSpecification, IConfigService configService, Logger logger) { _historyService = historyService; - _qualityUpgradableSpecification = qualityUpgradableSpecification; + _upgradableSpecification = qualityUpgradableSpecification; _configService = configService; _logger = logger; } + public SpecificationPriority Priority => SpecificationPriority.Database; public RejectionType Type => RejectionType.Permanent; public virtual Decision IsSatisfiedBy(RemoteAlbum subject, SearchCriteriaBase searchCriteria) @@ -47,8 +48,8 @@ namespace NzbDrone.Core.DecisionEngine.Specifications.RssSync if (mostRecent != null && mostRecent.EventType == HistoryEventType.Grabbed) { var recent = mostRecent.Date.After(DateTime.UtcNow.AddHours(-12)); - var cutoffUnmet = _qualityUpgradableSpecification.CutoffNotMet(subject.Artist.Profile, mostRecent.Quality, subject.ParsedAlbumInfo.Quality); - var upgradeable = _qualityUpgradableSpecification.IsUpgradable(subject.Artist.Profile, mostRecent.Quality, subject.ParsedAlbumInfo.Quality); + var cutoffUnmet = _upgradableSpecification.CutoffNotMet(subject.Artist.Profile, subject.Artist.LanguageProfile, mostRecent.Quality, mostRecent.Language, subject.ParsedAlbumInfo.Quality); + var upgradeable = _upgradableSpecification.IsUpgradable(subject.Artist.Profile, subject.Artist.LanguageProfile, mostRecent.Quality, mostRecent.Language, subject.ParsedAlbumInfo.Quality, subject.ParsedAlbumInfo.Language); if (!recent && cdhEnabled) { diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/MonitoredAlbumSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/MonitoredAlbumSpecification.cs index 335d31497..404b39c3d 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/MonitoredAlbumSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/MonitoredAlbumSpecification.cs @@ -1,4 +1,4 @@ -using System.Linq; +using System.Linq; using NLog; using NzbDrone.Core.IndexerSearch.Definitions; using NzbDrone.Core.Parser.Model; @@ -14,6 +14,7 @@ namespace NzbDrone.Core.DecisionEngine.Specifications.RssSync _logger = logger; } + public SpecificationPriority Priority => SpecificationPriority.Default; public RejectionType Type => RejectionType.Permanent; public virtual Decision IsSatisfiedBy(RemoteAlbum subject, SearchCriteriaBase searchCriteria) diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/ProperSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/ProperSpecification.cs index 09d9f4828..64418cfb6 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/ProperSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/ProperSpecification.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Linq; using NLog; using NzbDrone.Core.Configuration; @@ -10,12 +10,12 @@ namespace NzbDrone.Core.DecisionEngine.Specifications.RssSync { public class ProperSpecification : IDecisionEngineSpecification { - private readonly QualityUpgradableSpecification _qualityUpgradableSpecification; + private readonly UpgradableSpecification _qualityUpgradableSpecification; private readonly IConfigService _configService; private readonly IMediaFileService _mediaFileService; private readonly Logger _logger; - public ProperSpecification(QualityUpgradableSpecification qualityUpgradableSpecification, IConfigService configService, IMediaFileService mediaFileService, Logger logger) + public ProperSpecification(UpgradableSpecification qualityUpgradableSpecification, IConfigService configService, IMediaFileService mediaFileService, Logger logger) { _qualityUpgradableSpecification = qualityUpgradableSpecification; _configService = configService; @@ -23,6 +23,7 @@ namespace NzbDrone.Core.DecisionEngine.Specifications.RssSync _logger = logger; } + public SpecificationPriority Priority => SpecificationPriority.Default; public RejectionType Type => RejectionType.Permanent; public virtual Decision IsSatisfiedBy(RemoteAlbum subject, SearchCriteriaBase searchCriteria) diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/SameTracksGrabSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/SameTracksGrabSpecification.cs index c7098f11d..8f03c4cee 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/SameTracksGrabSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/SameTracksGrabSpecification.cs @@ -1,4 +1,4 @@ -using System; +using System; using NLog; using NzbDrone.Core.IndexerSearch.Definitions; using NzbDrone.Core.Parser.Model; @@ -16,6 +16,7 @@ namespace NzbDrone.Core.DecisionEngine.Specifications _logger = logger; } + public SpecificationPriority Priority => SpecificationPriority.Default; public RejectionType Type => RejectionType.Permanent; public Decision IsSatisfiedBy(RemoteAlbum subject, SearchCriteriaBase searchCriteria) diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/Search/AlbumRequestedSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/Search/AlbumRequestedSpecification.cs index b513b575d..2be1a20b8 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/Search/AlbumRequestedSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/Search/AlbumRequestedSpecification.cs @@ -1,4 +1,4 @@ -using System.Linq; +using System.Linq; using NLog; using NzbDrone.Core.IndexerSearch.Definitions; using NzbDrone.Core.Parser.Model; @@ -16,6 +16,7 @@ namespace NzbDrone.Core.DecisionEngine.Specifications.Search _logger = logger; } + public SpecificationPriority Priority => SpecificationPriority.Default; public RejectionType Type => RejectionType.Permanent; public Decision IsSatisfiedBy(RemoteAlbum remoteAlbum, SearchCriteriaBase searchCriteria) diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/Search/ArtistSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/Search/ArtistSpecification.cs index cef67f820..7eb972b80 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/Search/ArtistSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/Search/ArtistSpecification.cs @@ -1,4 +1,4 @@ -using NLog; +using NLog; using NzbDrone.Core.IndexerSearch.Definitions; using NzbDrone.Core.Parser.Model; @@ -13,6 +13,7 @@ namespace NzbDrone.Core.DecisionEngine.Specifications.Search _logger = logger; } + public SpecificationPriority Priority => SpecificationPriority.Default; public RejectionType Type => RejectionType.Permanent; public Decision IsSatisfiedBy(RemoteAlbum remoteAlbum, SearchCriteriaBase searchCriteria) @@ -33,4 +34,4 @@ namespace NzbDrone.Core.DecisionEngine.Specifications.Search return Decision.Accept(); } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/Search/DailyAudioMatchSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/Search/DailyAudioMatchSpecification.cs index e023a92f4..76fc116b9 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/Search/DailyAudioMatchSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/Search/DailyAudioMatchSpecification.cs @@ -1,4 +1,4 @@ -using System; +using System; using NLog; using NzbDrone.Core.IndexerSearch.Definitions; using NzbDrone.Core.Parser.Model; @@ -14,6 +14,7 @@ namespace NzbDrone.Core.DecisionEngine.Specifications.Search _logger = logger; } + public SpecificationPriority Priority => SpecificationPriority.Database; public RejectionType Type => RejectionType.Permanent; public Decision IsSatisfiedBy(RemoteAlbum subject, SearchCriteriaBase searchCriteria) @@ -23,4 +24,4 @@ namespace NzbDrone.Core.DecisionEngine.Specifications.Search // TODO Rework for Daily Audio/Podcasts } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/Search/SeasonMatchSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/Search/SeasonMatchSpecification.cs index 0a78db5e6..f934f2e55 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/Search/SeasonMatchSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/Search/SeasonMatchSpecification.cs @@ -1,4 +1,4 @@ -using System; +using System; using NLog; using NzbDrone.Core.IndexerSearch.Definitions; using NzbDrone.Core.Parser.Model; @@ -14,6 +14,7 @@ namespace NzbDrone.Core.DecisionEngine.Specifications.Search _logger = logger; } + public SpecificationPriority Priority => SpecificationPriority.Default; public RejectionType Type => RejectionType.Permanent; public virtual Decision IsSatisfiedBy(RemoteAlbum subject, SearchCriteriaBase searchCriteria) @@ -21,4 +22,4 @@ namespace NzbDrone.Core.DecisionEngine.Specifications.Search throw new NotImplementedException(); } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/Search/SingleAlbumSearchMatchSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/Search/SingleAlbumSearchMatchSpecification.cs index 08919597d..c2ea90be6 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/Search/SingleAlbumSearchMatchSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/Search/SingleAlbumSearchMatchSpecification.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Linq; using NLog; using NzbDrone.Core.IndexerSearch.Definitions; @@ -15,6 +15,7 @@ namespace NzbDrone.Core.DecisionEngine.Specifications.Search _logger = logger; } + public SpecificationPriority Priority => SpecificationPriority.Default; public RejectionType Type => RejectionType.Permanent; public virtual Decision IsSatisfiedBy(RemoteAlbum remoteAlbum, SearchCriteriaBase searchCriteria) @@ -30,7 +31,7 @@ namespace NzbDrone.Core.DecisionEngine.Specifications.Search return Decision.Accept(); } - if (Parser.Parser.CleanArtistTitle(singleAlbumSpec.AlbumTitle) != Parser.Parser.CleanArtistTitle(remoteAlbum.ParsedAlbumInfo.AlbumTitle)) + if (Parser.Parser.CleanArtistName(singleAlbumSpec.AlbumTitle) != Parser.Parser.CleanArtistName(remoteAlbum.ParsedAlbumInfo.AlbumTitle)) { _logger.Debug("Album does not match searched album title, skipping."); return Decision.Reject("Wrong album"); @@ -45,4 +46,4 @@ namespace NzbDrone.Core.DecisionEngine.Specifications.Search return Decision.Accept(); } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/Search/TorrentSeedingSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/Search/TorrentSeedingSpecification.cs deleted file mode 100644 index 3ceb3ef1a..000000000 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/Search/TorrentSeedingSpecification.cs +++ /dev/null @@ -1,36 +0,0 @@ -using NLog; -using NzbDrone.Core.IndexerSearch.Definitions; -using NzbDrone.Core.Parser.Model; - -namespace NzbDrone.Core.DecisionEngine.Specifications.Search -{ - public class TorrentSeedingSpecification : IDecisionEngineSpecification - { - private readonly Logger _logger; - - public TorrentSeedingSpecification(Logger logger) - { - _logger = logger; - } - - public RejectionType Type => RejectionType.Permanent; - - public Decision IsSatisfiedBy(RemoteAlbum remoteAlbum, SearchCriteriaBase searchCriteria) - { - var torrentInfo = remoteAlbum.Release as TorrentInfo; - - if (torrentInfo == null) - { - return Decision.Accept(); - } - - if (torrentInfo.Seeders != null && torrentInfo.Seeders < 1) - { - _logger.Debug("Not enough seeders. ({0})", torrentInfo.Seeders); - return Decision.Reject("Not enough seeders. ({0})", torrentInfo.Seeders); - } - - return Decision.Accept(); - } - } -} \ No newline at end of file diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/TorrentSeedingSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/TorrentSeedingSpecification.cs new file mode 100644 index 000000000..1a150be94 --- /dev/null +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/TorrentSeedingSpecification.cs @@ -0,0 +1,60 @@ +using NLog; +using NzbDrone.Core.Datastore; +using NzbDrone.Core.Indexers; +using NzbDrone.Core.IndexerSearch.Definitions; +using NzbDrone.Core.Parser.Model; + +namespace NzbDrone.Core.DecisionEngine.Specifications.Search +{ + public class TorrentSeedingSpecification : IDecisionEngineSpecification + { + private readonly IIndexerFactory _indexerFactory; + private readonly Logger _logger; + + public TorrentSeedingSpecification(IIndexerFactory indexerFactory, Logger logger) + { + _indexerFactory = indexerFactory; + _logger = logger; + } + + public SpecificationPriority Priority => SpecificationPriority.Default; + public RejectionType Type => RejectionType.Permanent; + + + public Decision IsSatisfiedBy(RemoteAlbum remoteAlbum, SearchCriteriaBase searchCriteria) + { + var torrentInfo = remoteAlbum.Release as TorrentInfo; + + if (torrentInfo == null || torrentInfo.IndexerId == 0) + { + return Decision.Accept(); + } + + IndexerDefinition indexer; + try + { + indexer = _indexerFactory.Get(torrentInfo.IndexerId); + } + catch (ModelNotFoundException) + { + _logger.Debug("Indexer with id {0} does not exist, skipping seeders check", torrentInfo.IndexerId); + return Decision.Accept(); + } + + var torrentIndexerSettings = indexer.Settings as ITorrentIndexerSettings; + + if (torrentIndexerSettings != null) + { + var minimumSeeders = torrentIndexerSettings.MinimumSeeders; + + if (torrentInfo.Seeders.HasValue && torrentInfo.Seeders.Value < minimumSeeders) + { + _logger.Debug("Not enough seeders: {0}. Minimum seeders: {1}", torrentInfo.Seeders, minimumSeeders); + return Decision.Reject("Not enough seeders: {0}. Minimum seeders: {1}", torrentInfo.Seeders, minimumSeeders); + } + } + + return Decision.Accept(); + } + } +} diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/UpgradeDiskSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/UpgradeDiskSpecification.cs index f5e75c7db..853a04a12 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/UpgradeDiskSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/UpgradeDiskSpecification.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Linq; using NLog; using NzbDrone.Core.IndexerSearch.Definitions; @@ -10,16 +10,17 @@ namespace NzbDrone.Core.DecisionEngine.Specifications public class UpgradeDiskSpecification : IDecisionEngineSpecification { private readonly IMediaFileService _mediaFileService; - private readonly QualityUpgradableSpecification _qualityUpgradableSpecification; + private readonly UpgradableSpecification _upgradableSpecification; private readonly Logger _logger; - public UpgradeDiskSpecification(QualityUpgradableSpecification qualityUpgradableSpecification, IMediaFileService mediaFileService, Logger logger) + public UpgradeDiskSpecification(UpgradableSpecification qualityUpgradableSpecification, IMediaFileService mediaFileService, Logger logger) { - _qualityUpgradableSpecification = qualityUpgradableSpecification; + _upgradableSpecification = qualityUpgradableSpecification; _mediaFileService = mediaFileService; _logger = logger; } + public SpecificationPriority Priority => SpecificationPriority.Default; public RejectionType Type => RejectionType.Permanent; public virtual Decision IsSatisfiedBy(RemoteAlbum subject, SearchCriteriaBase searchCriteria) @@ -33,7 +34,12 @@ namespace NzbDrone.Core.DecisionEngine.Specifications { var lowestQuality = trackFiles.Select(c => c.Quality).OrderBy(c => c.Quality.Id).First(); - if (!_qualityUpgradableSpecification.IsUpgradable(subject.Artist.Profile, lowestQuality, subject.ParsedAlbumInfo.Quality)) + if (!_upgradableSpecification.IsUpgradable(subject.Artist.Profile, + subject.Artist.LanguageProfile, + lowestQuality, + trackFiles[0].Language, + subject.ParsedAlbumInfo.Quality, + subject.ParsedAlbumInfo.Language)) { return Decision.Reject("Quality for existing file on disk is of equal or higher preference: {0}", lowestQuality); } diff --git a/src/NzbDrone.Core/DecisionEngine/UpgradableSpecification.cs b/src/NzbDrone.Core/DecisionEngine/UpgradableSpecification.cs new file mode 100644 index 000000000..3c0cebf0d --- /dev/null +++ b/src/NzbDrone.Core/DecisionEngine/UpgradableSpecification.cs @@ -0,0 +1,109 @@ +using NLog; +using NzbDrone.Core.Languages; +using NzbDrone.Core.Profiles.Languages; +using NzbDrone.Core.Profiles.Qualities; +using NzbDrone.Core.Qualities; + +namespace NzbDrone.Core.DecisionEngine +{ + public interface IUpgradableSpecification + { + bool IsUpgradable(Profile profile, LanguageProfile languageProfile, QualityModel currentQuality, Language currentLanguage, QualityModel newQuality, Language newLanguage); + bool CutoffNotMet(Profile profile, LanguageProfile languageProfile, QualityModel currentQuality, Language currentLanguage, QualityModel newQuality = null); + bool IsRevisionUpgrade(QualityModel currentQuality, QualityModel newQuality); + } + + public class UpgradableSpecification : IUpgradableSpecification + { + private readonly Logger _logger; + + public UpgradableSpecification(Logger logger) + { + _logger = logger; + } + + private bool IsLanguageUpgradable(LanguageProfile profile, Language currentLanguage, Language newLanguage = null) + { + if (newLanguage != null) + { + var compare = new LanguageComparer(profile).Compare(newLanguage, currentLanguage); + if (compare <= 0) + { + return false; + } + } + return true; + } + + private bool IsQualityUpgradable(Profile profile, QualityModel currentQuality, QualityModel newQuality = null) + { + if (newQuality != null) + { + var compare = new QualityModelComparer(profile).Compare(newQuality, currentQuality); + if (compare <= 0) + { + _logger.Debug("existing item has better quality. skipping"); + return false; + } + } + return true; + } + + + public bool IsUpgradable(Profile profile, LanguageProfile languageProfile, QualityModel currentQuality, Language currentLanguage, QualityModel newQuality, Language newLanguage) + { + // If qualities are the same then check language + if (newQuality != null && currentQuality == newQuality) + { + return IsLanguageUpgradable(languageProfile, currentLanguage, newLanguage); + } + + // If quality is worse then always return false + if (!IsQualityUpgradable(profile, currentQuality, newQuality)) + { + _logger.Debug("existing item has better quality. skipping"); + return false; + } + + return true; + } + + public bool CutoffNotMet(Profile profile, LanguageProfile languageProfile, QualityModel currentQuality, Language currentLanguage, QualityModel newQuality = null) + { + var languageCompare = new LanguageComparer(languageProfile).Compare(currentLanguage, languageProfile.Cutoff); + var qualityCompare = new QualityModelComparer(profile).Compare(currentQuality.Quality, profile.Cutoff); + + // If we can upgrade the language (it is not the cutoff) then doesn't matter the quality we can always get same quality with prefered language + if (languageCompare < 0) + { + return true; + } + + if (qualityCompare >= 0) + { + if (newQuality != null && IsRevisionUpgrade(currentQuality, newQuality)) + { + return true; + } + + _logger.Debug("Existing item meets cut-off. skipping."); + return false; + } + + return true; + } + + public bool IsRevisionUpgrade(QualityModel currentQuality, QualityModel newQuality) + { + var compare = newQuality.Revision.CompareTo(currentQuality.Revision); + + if (currentQuality.Quality == newQuality.Quality && compare > 0) + { + _logger.Debug("New quality is a better revision for existing quality"); + return true; + } + + return false; + } + } +} diff --git a/src/NzbDrone.Core/DiskSpace/DiskSpaceService.cs b/src/NzbDrone.Core/DiskSpace/DiskSpaceService.cs index 408839f88..74e9b0945 100644 --- a/src/NzbDrone.Core/DiskSpace/DiskSpaceService.cs +++ b/src/NzbDrone.Core/DiskSpace/DiskSpaceService.cs @@ -1,12 +1,11 @@ -using System; -using System.IO; +using System; using System.Collections.Generic; +using System.IO; using System.Linq; +using System.Text.RegularExpressions; using NLog; using NzbDrone.Common.Disk; -using NzbDrone.Common.Extensions; -using NzbDrone.Core.Configuration; -using NzbDrone.Core.Tv; +using NzbDrone.Core.Music; namespace NzbDrone.Core.DiskSpace { @@ -17,52 +16,44 @@ namespace NzbDrone.Core.DiskSpace public class DiskSpaceService : IDiskSpaceService { - private readonly ISeriesService _seriesService; - private readonly IConfigService _configService; + private readonly IArtistService _artistService; private readonly IDiskProvider _diskProvider; private readonly Logger _logger; - public DiskSpaceService(ISeriesService seriesService, IConfigService configService, IDiskProvider diskProvider, Logger logger) + private static readonly Regex _regexSpecialDrive = new Regex("^/var/lib/(docker|rancher|kubelet)(/|$)|^/boot(/|$)|/docker(/var)?/aufs(/|$)", RegexOptions.Compiled); + + public DiskSpaceService(IArtistService artistService, IDiskProvider diskProvider, Logger logger) { - _seriesService = seriesService; - _configService = configService; + _artistService = artistService; _diskProvider = diskProvider; _logger = logger; } public List GetFreeSpace() { - var diskSpace = new List(); - diskSpace.AddRange(GetSeriesFreeSpace()); - diskSpace.AddRange(GetDroneFactoryFreeSpace()); - diskSpace.AddRange(GetFixedDisksFreeSpace()); + var importantRootFolders = GetArtistRootPaths().Distinct().ToList(); - return diskSpace.DistinctBy(d => d.Path).ToList(); + var optionalRootFolders = GetFixedDisksRootPaths().Except(importantRootFolders).Distinct().ToList(); + + var diskSpace = GetDiskSpace(importantRootFolders).Concat(GetDiskSpace(optionalRootFolders, true)).ToList(); + + return diskSpace; } - private IEnumerable GetSeriesFreeSpace() + private IEnumerable GetArtistRootPaths() { - var seriesRootPaths = _seriesService.GetAllSeries() + return _artistService.GetAllArtists() .Where(s => _diskProvider.FolderExists(s.Path)) .Select(s => _diskProvider.GetPathRoot(s.Path)) .Distinct(); - - return GetDiskSpace(seriesRootPaths); - } - - private IEnumerable GetDroneFactoryFreeSpace() - { - if (_configService.DownloadedAlbumsFolder.IsNotNullOrWhiteSpace() && _diskProvider.FolderExists(_configService.DownloadedAlbumsFolder)) - { - return GetDiskSpace(new[] { _diskProvider.GetPathRoot(_configService.DownloadedAlbumsFolder) }); - } - - return new List(); } - private IEnumerable GetFixedDisksFreeSpace() + private IEnumerable GetFixedDisksRootPaths() { - return GetDiskSpace(_diskProvider.GetMounts().Where(d => d.DriveType == DriveType.Fixed).Select(d => d.RootDirectory), true); + return _diskProvider.GetMounts() + .Where(d => d.DriveType == DriveType.Fixed) + .Where(d => !_regexSpecialDrive.IsMatch(d.RootDirectory)) + .Select(d => d.RootDirectory); } private IEnumerable GetDiskSpace(IEnumerable paths, bool suppressWarnings = false) diff --git a/src/NzbDrone.Core/Download/Clients/Blackhole/ScanWatchFolder.cs b/src/NzbDrone.Core/Download/Clients/Blackhole/ScanWatchFolder.cs index be868d51c..924436600 100644 --- a/src/NzbDrone.Core/Download/Clients/Blackhole/ScanWatchFolder.cs +++ b/src/NzbDrone.Core/Download/Clients/Blackhole/ScanWatchFolder.cs @@ -1,15 +1,15 @@ -using NLog; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using NLog; using NzbDrone.Common.Cache; using NzbDrone.Common.Crypto; using NzbDrone.Common.Disk; using NzbDrone.Common.Extensions; using NzbDrone.Core.MediaFiles; using NzbDrone.Core.Organizer; -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text; namespace NzbDrone.Core.Download.Clients.Blackhole { @@ -50,7 +50,7 @@ namespace NzbDrone.Core.Download.Clients.Blackhole private IEnumerable GetDownloadItems(string watchFolder, Dictionary lastWatchItems, TimeSpan waitPeriod) { - foreach (var folder in _diskProvider.GetDirectories(watchFolder)) + foreach (var folder in _diskScanService.FilterFiles(watchFolder, _diskProvider.GetDirectories(watchFolder))) { var title = FileNameBuilder.CleanFileName(Path.GetFileName(folder)); @@ -86,16 +86,16 @@ namespace NzbDrone.Core.Download.Clients.Blackhole yield return newWatchItem; } - foreach (var videoFile in _diskScanService.GetAudioFiles(watchFolder, false)) + foreach (var audioFile in _diskScanService.FilterFiles(watchFolder, _diskScanService.GetAudioFiles(watchFolder, false))) { - var title = FileNameBuilder.CleanFileName(Path.GetFileName(videoFile)); + var title = FileNameBuilder.CleanFileName(Path.GetFileName(audioFile)); var newWatchItem = new WatchFolderItem { - DownloadId = Path.GetFileName(videoFile) + "_" + _diskProvider.FileGetLastWrite(videoFile).Ticks, + DownloadId = Path.GetFileName(audioFile) + "_" + _diskProvider.FileGetLastWrite(audioFile).Ticks, Title = title, - OutputPath = new OsPath(videoFile), + OutputPath = new OsPath(audioFile), Status = DownloadItemStatus.Completed, RemainingTime = TimeSpan.Zero @@ -105,10 +105,10 @@ namespace NzbDrone.Core.Download.Clients.Blackhole if (PreCheckWatchItemExpiry(newWatchItem, oldWatchItem)) { - newWatchItem.TotalSize = _diskProvider.GetFileSize(videoFile); - newWatchItem.Hash = GetHash(videoFile); + newWatchItem.TotalSize = _diskProvider.GetFileSize(audioFile); + newWatchItem.Hash = GetHash(audioFile); - if (_diskProvider.IsFileLocked(videoFile)) + if (_diskProvider.IsFileLocked(audioFile)) { newWatchItem.Status = DownloadItemStatus.Downloading; } @@ -193,4 +193,4 @@ namespace NzbDrone.Core.Download.Clients.Blackhole return HashConverter.GetHash(data.ToString()).ToHexString(); } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Core/Download/Clients/Blackhole/TorrentBlackhole.cs b/src/NzbDrone.Core/Download/Clients/Blackhole/TorrentBlackhole.cs index 4eef97920..ea315fcc5 100644 --- a/src/NzbDrone.Core/Download/Clients/Blackhole/TorrentBlackhole.cs +++ b/src/NzbDrone.Core/Download/Clients/Blackhole/TorrentBlackhole.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.IO; using System.Text; @@ -103,7 +103,8 @@ namespace NzbDrone.Core.Download.Clients.Blackhole Status = item.Status, - IsReadOnly = Settings.ReadOnly + CanMoveFiles = !Settings.ReadOnly, + CanBeRemoved = !Settings.ReadOnly }; } } @@ -118,9 +119,9 @@ namespace NzbDrone.Core.Download.Clients.Blackhole DeleteItemData(downloadId); } - public override DownloadClientStatus GetStatus() + public override DownloadClientInfo GetStatus() { - return new DownloadClientStatus + return new DownloadClientInfo { IsLocalhost = true, OutputRootFolders = new List { new OsPath(Settings.WatchFolder) } diff --git a/src/NzbDrone.Core/Download/Clients/Blackhole/UsenetBlackhole.cs b/src/NzbDrone.Core/Download/Clients/Blackhole/UsenetBlackhole.cs index 5181fee64..0e999b67b 100644 --- a/src/NzbDrone.Core/Download/Clients/Blackhole/UsenetBlackhole.cs +++ b/src/NzbDrone.Core/Download/Clients/Blackhole/UsenetBlackhole.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.IO; using FluentValidation.Results; @@ -68,7 +68,10 @@ namespace NzbDrone.Core.Download.Clients.Blackhole OutputPath = item.OutputPath, - Status = item.Status + Status = item.Status, + + CanBeRemoved = true, + CanMoveFiles = true }; } } @@ -83,9 +86,9 @@ namespace NzbDrone.Core.Download.Clients.Blackhole DeleteItemData(downloadId); } - public override DownloadClientStatus GetStatus() + public override DownloadClientInfo GetStatus() { - return new DownloadClientStatus + return new DownloadClientInfo { IsLocalhost = true, OutputRootFolders = new List { new OsPath(Settings.WatchFolder) } diff --git a/src/NzbDrone.Core/Download/Clients/Deluge/Deluge.cs b/src/NzbDrone.Core/Download/Clients/Deluge/Deluge.cs index a8bf1e336..c0636cdd0 100644 --- a/src/NzbDrone.Core/Download/Clients/Deluge/Deluge.cs +++ b/src/NzbDrone.Core/Download/Clients/Deluge/Deluge.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Linq; using System.Collections.Generic; using NzbDrone.Common.Disk; @@ -57,6 +57,11 @@ namespace NzbDrone.Core.Download.Clients.Deluge { var actualHash = _proxy.AddTorrentFromFile(filename, fileContent, Settings); + if (actualHash.IsNullOrWhiteSpace()) + { + throw new DownloadClientException("Deluge failed to add torrent " + filename); + } + if (!Settings.TvCategory.IsNullOrWhiteSpace()) { _proxy.SetLabel(actualHash, Settings.TvCategory, Settings); @@ -81,21 +86,13 @@ namespace NzbDrone.Core.Download.Clients.Deluge { IEnumerable torrents; - try + if (!Settings.TvCategory.IsNullOrWhiteSpace()) { - if (!Settings.TvCategory.IsNullOrWhiteSpace()) - { - torrents = _proxy.GetTorrentsByLabel(Settings.TvCategory, Settings); - } - else - { - torrents = _proxy.GetTorrents(Settings); - } + torrents = _proxy.GetTorrentsByLabel(Settings.TvCategory, Settings); } - catch (DownloadClientException ex) + else { - _logger.Error(ex, "Couldn't get list of torrents"); - return Enumerable.Empty(); + torrents = _proxy.GetTorrents(Settings); } var items = new List(); @@ -112,7 +109,16 @@ namespace NzbDrone.Core.Download.Clients.Deluge var outputPath = _remotePathMappingService.RemapRemoteToLocal(Settings.Host, new OsPath(torrent.DownloadPath)); item.OutputPath = outputPath + torrent.Name; item.RemainingSize = torrent.Size - torrent.BytesDownloaded; - item.RemainingTime = TimeSpan.FromSeconds(torrent.Eta); + try + { + item.RemainingTime = TimeSpan.FromSeconds(torrent.Eta); + } + catch (OverflowException ex) + { + _logger.Debug(ex, "ETA for {0} is too long: {1}", torrent.Name, torrent.Eta); + item.RemainingTime = TimeSpan.MaxValue; + } + item.TotalSize = torrent.Size; if (torrent.State == DelugeTorrentStatus.Error) @@ -138,14 +144,7 @@ namespace NzbDrone.Core.Download.Clients.Deluge } // Here we detect if Deluge is managing the torrent and whether the seed criteria has been met. This allows drone to delete the torrent as appropriate. - if (torrent.IsAutoManaged && torrent.StopAtRatio && torrent.Ratio >= torrent.StopRatio && torrent.State == DelugeTorrentStatus.Paused) - { - item.IsReadOnly = false; - } - else - { - item.IsReadOnly = true; - } + item.CanMoveFiles = item.CanBeRemoved = (torrent.IsAutoManaged && torrent.StopAtRatio && torrent.Ratio >= torrent.StopRatio && torrent.State == DelugeTorrentStatus.Paused); items.Add(item); } @@ -158,7 +157,7 @@ namespace NzbDrone.Core.Download.Clients.Deluge _proxy.RemoveTorrent(downloadId.ToLower(), deleteData, Settings); } - public override DownloadClientStatus GetStatus() + public override DownloadClientInfo GetStatus() { var config = _proxy.GetConfig(Settings); @@ -169,7 +168,7 @@ namespace NzbDrone.Core.Download.Clients.Deluge destDir = new OsPath(config.GetValueOrDefault("move_completed_path") as string); } - var status = new DownloadClientStatus + var status = new DownloadClientInfo { IsLocalhost = Settings.Host == "127.0.0.1" || Settings.Host == "localhost" }; @@ -198,12 +197,12 @@ namespace NzbDrone.Core.Download.Clients.Deluge } catch (DownloadClientAuthenticationException ex) { - _logger.Error(ex); + _logger.Error(ex, ex.Message); return new NzbDroneValidationFailure("Password", "Authentication failed"); } catch (WebException ex) { - _logger.Error(ex); + _logger.Error(ex, "Unable to test connection"); switch (ex.Status) { case WebExceptionStatus.ConnectFailure: @@ -227,7 +226,7 @@ namespace NzbDrone.Core.Download.Clients.Deluge } catch (Exception ex) { - _logger.Error(ex); + _logger.Error(ex, "Failed to test connection"); return new NzbDroneValidationFailure(string.Empty, "Unknown exception: " + ex.Message); } @@ -278,7 +277,7 @@ namespace NzbDrone.Core.Download.Clients.Deluge } catch (Exception ex) { - _logger.Error(ex); + _logger.Error(ex, "Unable to get torrents"); return new NzbDroneValidationFailure(string.Empty, "Failed to get the list of torrents: " + ex.Message); } diff --git a/src/NzbDrone.Core/Download/Clients/Deluge/DelugeProxy.cs b/src/NzbDrone.Core/Download/Clients/Deluge/DelugeProxy.cs index 3406685db..62dd60529 100644 --- a/src/NzbDrone.Core/Download/Clients/Deluge/DelugeProxy.cs +++ b/src/NzbDrone.Core/Download/Clients/Deluge/DelugeProxy.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Net; @@ -231,7 +231,7 @@ namespace NzbDrone.Core.Download.Clients.Deluge } catch (WebException ex) { - throw new DownloadClientException("Unable to connect to Deluge, please check your settings", ex); + throw new DownloadClientUnavailableException("Unable to connect to Deluge, please check your settings", ex); } } diff --git a/src/NzbDrone.Core/Download/Clients/DownloadClientException.cs b/src/NzbDrone.Core/Download/Clients/DownloadClientException.cs index 9598e04ef..79a93ef1f 100644 --- a/src/NzbDrone.Core/Download/Clients/DownloadClientException.cs +++ b/src/NzbDrone.Core/Download/Clients/DownloadClientException.cs @@ -1,4 +1,4 @@ -using System; +using System; using NzbDrone.Common.Exceptions; namespace NzbDrone.Core.Download.Clients @@ -8,19 +8,16 @@ namespace NzbDrone.Core.Download.Clients public DownloadClientException(string message, params object[] args) : base(string.Format(message, args)) { - } public DownloadClientException(string message) : base(message) { - } public DownloadClientException(string message, Exception innerException, params object[] args) : base(string.Format(message, args), innerException) { - } public DownloadClientException(string message, Exception innerException) diff --git a/src/NzbDrone.Core/Download/Clients/DownloadClientUnavailableException.cs b/src/NzbDrone.Core/Download/Clients/DownloadClientUnavailableException.cs new file mode 100644 index 000000000..923698cef --- /dev/null +++ b/src/NzbDrone.Core/Download/Clients/DownloadClientUnavailableException.cs @@ -0,0 +1,27 @@ +using System; + +namespace NzbDrone.Core.Download.Clients +{ + public class DownloadClientUnavailableException : DownloadClientException + { + public DownloadClientUnavailableException(string message, params object[] args) + : base(string.Format(message, args)) + { + } + + public DownloadClientUnavailableException(string message) + : base(message) + { + } + + public DownloadClientUnavailableException(string message, Exception innerException, params object[] args) + : base(string.Format(message, args), innerException) + { + } + + public DownloadClientUnavailableException(string message, Exception innerException) + : base(message, innerException) + { + } + } +} diff --git a/src/NzbDrone.Core/Download/Clients/DownloadStation/Proxies/DiskStationProxyBase.cs b/src/NzbDrone.Core/Download/Clients/DownloadStation/Proxies/DiskStationProxyBase.cs index 2a8e4b144..2162a3d57 100644 --- a/src/NzbDrone.Core/Download/Clients/DownloadStation/Proxies/DiskStationProxyBase.cs +++ b/src/NzbDrone.Core/Download/Clients/DownloadStation/Proxies/DiskStationProxyBase.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Net; @@ -72,7 +72,20 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation.Proxies DownloadStationSettings settings) where T : new() { var request = requestBuilder.Build(); - var response = _httpClient.Execute(request); + HttpResponse response; + + try + { + response = _httpClient.Execute(request); + } + catch (HttpException ex) + { + throw new DownloadClientException("Unable to connect to Diskstation, please check your settings", ex); + } + catch (WebException ex) + { + throw new DownloadClientUnavailableException("Unable to connect to Diskstation, please check your settings", ex); + } _logger.Debug("Trying to {0}", operation); diff --git a/src/NzbDrone.Core/Download/Clients/DownloadStation/TorrentDownloadStation.cs b/src/NzbDrone.Core/Download/Clients/DownloadStation/TorrentDownloadStation.cs index 20615515f..0d2ca3d01 100644 --- a/src/NzbDrone.Core/Download/Clients/DownloadStation/TorrentDownloadStation.cs +++ b/src/NzbDrone.Core/Download/Clients/DownloadStation/TorrentDownloadStation.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.IO; using System.Linq; @@ -90,7 +90,8 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation RemainingTime = GetRemainingTime(torrent), Status = GetStatus(torrent), Message = GetMessage(torrent), - IsReadOnly = !IsFinished(torrent) + CanMoveFiles = IsCompleted(torrent), + CanBeRemoved = IsFinished(torrent) }; if (item.Status == DownloadItemStatus.Completed || item.Status == DownloadItemStatus.Failed) @@ -104,13 +105,13 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation return items; } - public override DownloadClientStatus GetStatus() + public override DownloadClientInfo GetStatus() { try { var path = GetDownloadDirectory(); - return new DownloadClientStatus + return new DownloadClientInfo { IsLocalhost = Settings.Host == "127.0.0.1" || Settings.Host == "localhost", OutputRootFolders = new List { _remotePathMappingService.RemapRemoteToLocal(Settings.Host, new OsPath(path)) } @@ -199,7 +200,12 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation return torrent.Status == DownloadStationTaskStatus.Finished; } - protected string GetMessage(DownloadStationTask torrent) + protected bool IsCompleted(DownloadStationTask torrent) + { + return torrent.Status == DownloadStationTaskStatus.Seeding || IsFinished(torrent) || (torrent.Status == DownloadStationTaskStatus.Waiting && torrent.Size != 0 && GetRemainingSize(torrent) <= 0); + } + + protected string GetMessage(DownloadStationTask torrent) { if (torrent.StatusExtra != null) { @@ -314,12 +320,12 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation } catch (DownloadClientAuthenticationException ex) // User could not have permission to access to downloadstation { - _logger.Error(ex); + _logger.Error(ex, ex.Message); return new NzbDroneValidationFailure(string.Empty, ex.Message); } catch (Exception ex) { - _logger.Error(ex); + _logger.Error(ex, "Error testing Torrent Download Station"); return new NzbDroneValidationFailure(string.Empty, $"Unknown exception: {ex.Message}"); } } @@ -340,7 +346,7 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation } catch (WebException ex) { - _logger.Error(ex); + _logger.Error(ex, "Unable to connect to Torrent Download Station"); if (ex.Status == WebExceptionStatus.ConnectFailure) { @@ -353,7 +359,7 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation } catch (Exception ex) { - _logger.Error(ex); + _logger.Error(ex, "Error testing Torrent Download Station"); return new NzbDroneValidationFailure(string.Empty, $"Unknown exception: {ex.Message}"); } } diff --git a/src/NzbDrone.Core/Download/Clients/DownloadStation/UsenetDownloadStation.cs b/src/NzbDrone.Core/Download/Clients/DownloadStation/UsenetDownloadStation.cs index 3a44405ae..a2fb7f783 100644 --- a/src/NzbDrone.Core/Download/Clients/DownloadStation/UsenetDownloadStation.cs +++ b/src/NzbDrone.Core/Download/Clients/DownloadStation/UsenetDownloadStation.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Net; @@ -99,7 +99,8 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation RemainingSize = taskRemainingSize, Status = GetStatus(nzb), Message = GetMessage(nzb), - IsReadOnly = !IsFinished(nzb) + CanBeRemoved = true, + CanMoveFiles = true }; if (item.Status != DownloadItemStatus.Paused) @@ -129,13 +130,13 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation return finalPath; } - public override DownloadClientStatus GetStatus() + public override DownloadClientInfo GetStatus() { try { var path = GetDownloadDirectory(); - return new DownloadClientStatus + return new DownloadClientInfo { IsLocalhost = Settings.Host == "127.0.0.1" || Settings.Host == "localhost", OutputRootFolders = new List { _remotePathMappingService.RemapRemoteToLocal(Settings.Host, new OsPath(path)) } @@ -233,12 +234,12 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation } catch (DownloadClientAuthenticationException ex) // User could not have permission to access to downloadstation { - _logger.Error(ex); + _logger.Error(ex, ex.Message); return new NzbDroneValidationFailure(string.Empty, ex.Message); } catch (Exception ex) { - _logger.Error(ex); + _logger.Error(ex, "Error testing Usenet Download Station"); return new NzbDroneValidationFailure(string.Empty, $"Unknown exception: {ex.Message}"); } } @@ -259,7 +260,7 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation } catch (WebException ex) { - _logger.Error(ex); + _logger.Error(ex, "Unable to connect to Usenet Download Station"); if (ex.Status == WebExceptionStatus.ConnectFailure) { @@ -272,7 +273,7 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation } catch (Exception ex) { - _logger.Error(ex); + _logger.Error(ex, "Error testing Torrent Download Station"); return new NzbDroneValidationFailure(string.Empty, "Unknown exception: " + ex.Message); } } @@ -291,11 +292,6 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation return null; } - protected bool IsFinished(DownloadStationTask task) - { - return task.Status == DownloadStationTaskStatus.Finished; - } - protected string GetMessage(DownloadStationTask task) { if (task.StatusExtra != null) diff --git a/src/NzbDrone.Core/Download/Clients/Hadouken/Hadouken.cs b/src/NzbDrone.Core/Download/Clients/Hadouken/Hadouken.cs index ac629b63b..4d0ee1049 100644 --- a/src/NzbDrone.Core/Download/Clients/Hadouken/Hadouken.cs +++ b/src/NzbDrone.Core/Download/Clients/Hadouken/Hadouken.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using FluentValidation.Results; @@ -35,17 +35,7 @@ namespace NzbDrone.Core.Download.Clients.Hadouken public override IEnumerable GetItems() { - HadoukenTorrent[] torrents; - - try - { - torrents = _proxy.GetTorrents(Settings); - } - catch (DownloadClientException ex) - { - _logger.ErrorException(ex.Message, ex); - return Enumerable.Empty(); - } + var torrents = _proxy.GetTorrents(Settings); var items = new List(); @@ -97,14 +87,7 @@ namespace NzbDrone.Core.Download.Clients.Hadouken item.Status = DownloadItemStatus.Downloading; } - if (torrent.IsFinished && torrent.State == HadoukenTorrentState.Paused) - { - item.IsReadOnly = false; - } - else - { - item.IsReadOnly = true; - } + item.CanMoveFiles = item.CanBeRemoved = (torrent.IsFinished && torrent.State == HadoukenTorrentState.Paused); items.Add(item); } @@ -124,12 +107,12 @@ namespace NzbDrone.Core.Download.Clients.Hadouken } } - public override DownloadClientStatus GetStatus() + public override DownloadClientInfo GetStatus() { var config = _proxy.GetConfig(Settings); var destDir = new OsPath(config.GetValueOrDefault("bittorrent.defaultSavePath") as string); - var status = new DownloadClientStatus + var status = new DownloadClientInfo { IsLocalhost = Settings.Host == "127.0.0.1" || Settings.Host == "localhost" }; @@ -170,7 +153,7 @@ namespace NzbDrone.Core.Download.Clients.Hadouken if (version < new Version("5.1")) { - return new ValidationFailure(string.Empty, "Old Hadouken client with unsupported API, need 5.1 or higher"); + return new ValidationFailure(string.Empty, "Old Hadouken client with unsupported API, need 5.1 or higher"); } } catch (DownloadClientAuthenticationException ex) diff --git a/src/NzbDrone.Core/Download/Clients/Hadouken/HadoukenProxy.cs b/src/NzbDrone.Core/Download/Clients/Hadouken/HadoukenProxy.cs index e044dd912..9eee399a3 100644 --- a/src/NzbDrone.Core/Download/Clients/Hadouken/HadoukenProxy.cs +++ b/src/NzbDrone.Core/Download/Clients/Hadouken/HadoukenProxy.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Net; using NLog; @@ -77,7 +77,21 @@ namespace NzbDrone.Core.Download.Clients.Hadouken requestBuilder.Headers.Add("Accept-Encoding", "gzip,deflate"); var httpRequest = requestBuilder.Build(); - var response = _httpClient.Execute(httpRequest); + HttpResponse response; + + try + { + response = _httpClient.Execute(httpRequest); + } + catch (HttpException ex) + { + throw new DownloadClientException("Unable to connect to Hadouken, please check your settings", ex); + } + catch (WebException ex) + { + throw new DownloadClientUnavailableException("Unable to connect to Hadouken, please check your settings", ex); + } + var result = Json.Deserialize>(response.Content); if (result.Error != null) @@ -160,4 +174,4 @@ namespace NzbDrone.Core.Download.Clients.Hadouken return HadoukenTorrentState.Unknown; } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Core/Download/Clients/NzbVortex/NzbVortex.cs b/src/NzbDrone.Core/Download/Clients/NzbVortex/NzbVortex.cs index b5a07c980..f4a8db185 100644 --- a/src/NzbDrone.Core/Download/Clients/NzbVortex/NzbVortex.cs +++ b/src/NzbDrone.Core/Download/Clients/NzbVortex/NzbVortex.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.IO; using System.Linq; @@ -47,17 +47,7 @@ namespace NzbDrone.Core.Download.Clients.NzbVortex public override IEnumerable GetItems() { - List vortexQueue; - - try - { - vortexQueue = _proxy.GetQueue(30, Settings); - } - catch (DownloadClientException ex) - { - _logger.Warn("Couldn't get download queue. {0}", ex.Message); - return Enumerable.Empty(); - } + var vortexQueue = _proxy.GetQueue(30, Settings); var queueItems = new List(); @@ -72,7 +62,10 @@ namespace NzbDrone.Core.Download.Clients.NzbVortex queueItem.TotalSize = vortexQueueItem.TotalDownloadSize; queueItem.RemainingSize = vortexQueueItem.TotalDownloadSize - vortexQueueItem.DownloadedSize; queueItem.RemainingTime = null; - + + queueItem.CanBeRemoved = true; + queueItem.CanMoveFiles = true; + if (vortexQueueItem.IsPaused) { queueItem.Status = DownloadItemStatus.Paused; @@ -132,7 +125,7 @@ namespace NzbDrone.Core.Download.Clients.NzbVortex { _proxy.Remove(queueItem.Id, deleteData, Settings); } - } + } } protected List GetGroups() @@ -140,9 +133,9 @@ namespace NzbDrone.Core.Download.Clients.NzbVortex return _proxy.GetGroups(Settings); } - public override DownloadClientStatus GetStatus() + public override DownloadClientInfo GetStatus() { - var status = new DownloadClientStatus + var status = new DownloadClientInfo { IsLocalhost = Settings.Host == "127.0.0.1" || Settings.Host == "localhost" }; @@ -166,7 +159,7 @@ namespace NzbDrone.Core.Download.Clients.NzbVortex } catch (Exception ex) { - _logger.Error(ex); + _logger.Error(ex, "Unable to connect to NZBVortex"); return new ValidationFailure("Host", "Unable to connect to NZBVortex"); } @@ -187,7 +180,7 @@ namespace NzbDrone.Core.Download.Clients.NzbVortex } catch (Exception ex) { - _logger.Error(ex); + _logger.Error(ex, "Unable to connect to NZBVortex"); return new ValidationFailure("Host", "Unable to connect to NZBVortex"); } @@ -256,4 +249,4 @@ namespace NzbDrone.Core.Download.Clients.NzbVortex return new OsPath(Path.Combine(outputPath.FullPath, filesResponse.First().FileName)); } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Core/Download/Clients/NzbVortex/NzbVortexProxy.cs b/src/NzbDrone.Core/Download/Clients/NzbVortex/NzbVortexProxy.cs index 15450c280..c93d18b57 100644 --- a/src/NzbDrone.Core/Download/Clients/NzbVortex/NzbVortexProxy.cs +++ b/src/NzbDrone.Core/Download/Clients/NzbVortex/NzbVortexProxy.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Net; using Newtonsoft.Json; @@ -164,7 +164,7 @@ namespace NzbDrone.Core.Download.Clients.NzbVortex } catch (WebException ex) { - throw new DownloadClientException("Unable to connect to NZBVortex, please check your settings", ex); + throw new DownloadClientUnavailableException("Unable to connect to NZBVortex, please check your settings", ex); } } diff --git a/src/NzbDrone.Core/Download/Clients/Nzbget/Nzbget.cs b/src/NzbDrone.Core/Download/Clients/Nzbget/Nzbget.cs index 746e4d259..d682b6a6c 100644 --- a/src/NzbDrone.Core/Download/Clients/Nzbget/Nzbget.cs +++ b/src/NzbDrone.Core/Download/Clients/Nzbget/Nzbget.cs @@ -1,7 +1,8 @@ -using System; +using System; +using System.Collections.Generic; +using System.Globalization; using System.IO; using System.Linq; -using System.Collections.Generic; using FluentValidation.Results; using NLog; using NzbDrone.Common.Disk; @@ -9,8 +10,8 @@ using NzbDrone.Common.Extensions; using NzbDrone.Common.Http; using NzbDrone.Core.Configuration; using NzbDrone.Core.Parser.Model; -using NzbDrone.Core.Validation; using NzbDrone.Core.RemotePathMappings; +using NzbDrone.Core.Validation; namespace NzbDrone.Core.Download.Clients.Nzbget { @@ -51,19 +52,8 @@ namespace NzbDrone.Core.Download.Clients.Nzbget private IEnumerable GetQueue() { - NzbgetGlobalStatus globalStatus; - List queue; - - try - { - globalStatus = _proxy.GetGlobalStatus(Settings); - queue = _proxy.GetQueue(Settings); - } - catch (DownloadClientException ex) - { - _logger.Error(ex); - return Enumerable.Empty(); - } + var globalStatus = _proxy.GetGlobalStatus(Settings); + var queue = _proxy.GetQueue(Settings); var queueItems = new List(); @@ -83,6 +73,8 @@ namespace NzbDrone.Core.Download.Clients.Nzbget queueItem.TotalSize = totalSize; queueItem.Category = item.Category; queueItem.DownloadClient = Definition.Name; + queueItem.CanMoveFiles = true; + queueItem.CanBeRemoved = true; if (globalStatus.DownloadPaused || remainingSize == pausedSize && remainingSize != 0) { @@ -117,17 +109,7 @@ namespace NzbDrone.Core.Download.Clients.Nzbget private IEnumerable GetHistory() { - List history; - - try - { - history = _proxy.GetHistory(Settings).Take(_configService.DownloadClientHistoryLimit).ToList(); - } - catch (DownloadClientException ex) - { - _logger.Error(ex); - return Enumerable.Empty(); - } + var history = _proxy.GetHistory(Settings).Take(_configService.DownloadClientHistoryLimit).ToList(); var historyItems = new List(); @@ -145,6 +127,8 @@ namespace NzbDrone.Core.Download.Clients.Nzbget historyItem.Message = $"PAR Status: {item.ParStatus} - Unpack Status: {item.UnpackStatus} - Move Status: {item.MoveStatus} - Script Status: {item.ScriptStatus} - Delete Status: {item.DeleteStatus} - Mark Status: {item.MarkStatus}"; historyItem.Status = DownloadItemStatus.Completed; historyItem.RemainingTime = TimeSpan.Zero; + historyItem.CanMoveFiles = true; + historyItem.CanBeRemoved = true; if (item.DeleteStatus == "MANUAL") { @@ -210,13 +194,13 @@ namespace NzbDrone.Core.Download.Clients.Nzbget _proxy.RemoveItem(downloadId, Settings); } - public override DownloadClientStatus GetStatus() + public override DownloadClientInfo GetStatus() { var config = _proxy.GetConfig(Settings); var category = GetCategories(config).FirstOrDefault(v => v.Name == Settings.TvCategory); - var status = new DownloadClientStatus + var status = new DownloadClientInfo { IsLocalhost = Settings.Host == "127.0.0.1" || Settings.Host == "localhost" }; @@ -285,7 +269,7 @@ namespace NzbDrone.Core.Download.Clients.Nzbget { return new ValidationFailure("Username", "Authentication failed"); } - _logger.Error(ex); + _logger.Error(ex, "Unable to connect to NZBGet"); return new ValidationFailure("Host", "Unable to connect to NZBGet"); } @@ -313,8 +297,9 @@ namespace NzbDrone.Core.Download.Clients.Nzbget { var config = _proxy.GetConfig(Settings); - var keepHistory = config.GetValueOrDefault("KeepHistory"); - if (keepHistory == "0") + var keepHistory = config.GetValueOrDefault("KeepHistory", "7"); + int value; + if (!int.TryParse(keepHistory, NumberStyles.None, CultureInfo.InvariantCulture, out value) || value == 0) { return new NzbDroneValidationFailure(string.Empty, "NzbGet setting KeepHistory should be greater than 0") { @@ -322,6 +307,14 @@ namespace NzbDrone.Core.Download.Clients.Nzbget DetailedDescription = "NzbGet setting KeepHistory is set to 0. Which prevents Lidarr from seeing completed downloads." }; } + else if (value > 25000) + { + return new NzbDroneValidationFailure(string.Empty, "NzbGet setting KeepHistory should be less than 25000") + { + InfoLink = string.Format("http://{0}:{1}/", Settings.Host, Settings.Port), + DetailedDescription = "NzbGet setting KeepHistory is set too high." + }; + } return null; } diff --git a/src/NzbDrone.Core/Download/Clients/Nzbget/NzbgetProxy.cs b/src/NzbDrone.Core/Download/Clients/Nzbget/NzbgetProxy.cs index 366bb9a2c..129d48450 100644 --- a/src/NzbDrone.Core/Download/Clients/Nzbget/NzbgetProxy.cs +++ b/src/NzbDrone.Core/Download/Clients/Nzbget/NzbgetProxy.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using NLog; @@ -235,14 +235,14 @@ namespace NzbDrone.Core.Download.Clients.Nzbget { if (ex.Response.StatusCode == HttpStatusCode.Unauthorized) { - throw new DownloadClientException("Authentication failed for NzbGet, please check your settings", ex); + throw new DownloadClientAuthenticationException("Authentication failed for NzbGet, please check your settings", ex); } throw new DownloadClientException("Unable to connect to NzbGet. " + ex.Message, ex); } catch (WebException ex) { - throw new DownloadClientException("Unable to connect to NzbGet. " + ex.Message, ex); + throw new DownloadClientUnavailableException("Unable to connect to NzbGet. " + ex.Message, ex); } var result = Json.Deserialize>(response.Content); diff --git a/src/NzbDrone.Core/Download/Clients/Pneumatic/Pneumatic.cs b/src/NzbDrone.Core/Download/Clients/Pneumatic/Pneumatic.cs index 32fad0d99..5a05652bf 100644 --- a/src/NzbDrone.Core/Download/Clients/Pneumatic/Pneumatic.cs +++ b/src/NzbDrone.Core/Download/Clients/Pneumatic/Pneumatic.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.IO; using FluentValidation.Results; @@ -72,6 +72,9 @@ namespace NzbDrone.Core.Download.Clients.Pneumatic DownloadId = GetDownloadClientId(file), Title = title, + CanBeRemoved = true, + CanMoveFiles = true, + TotalSize = _diskProvider.GetFileSize(file), OutputPath = new OsPath(file) @@ -95,9 +98,9 @@ namespace NzbDrone.Core.Download.Clients.Pneumatic throw new NotSupportedException(); } - public override DownloadClientStatus GetStatus() + public override DownloadClientInfo GetStatus() { - var status = new DownloadClientStatus + var status = new DownloadClientInfo { IsLocalhost = true }; @@ -113,25 +116,14 @@ namespace NzbDrone.Core.Download.Clients.Pneumatic private string WriteStrmFile(string title, string nzbFile) { - string folder; if (Settings.StrmFolder.IsNullOrWhiteSpace()) { - folder = _configService.DownloadedAlbumsFolder; - - if (folder.IsNullOrWhiteSpace()) - { - throw new DownloadClientException("Strm Folder needs to be set for Pneumatic Downloader"); - } - } - - else - { - folder = Settings.StrmFolder; + throw new DownloadClientException("Strm Folder needs to be set for Pneumatic Downloader"); } var contents = string.Format("plugin://plugin.program.pneumatic/?mode=strm&type=add_file&nzb={0}&nzbname={1}", nzbFile, title); - var filename = Path.Combine(folder, title + ".strm"); + var filename = Path.Combine(Settings.StrmFolder, title + ".strm"); _diskProvider.WriteAllText(filename, contents); diff --git a/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrent.cs b/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrent.cs index b589aa19d..1f0985162 100644 --- a/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrent.cs +++ b/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrent.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Linq; using System.Collections.Generic; using NzbDrone.Common.Disk; @@ -48,6 +48,8 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent _proxy.MoveTorrentToTopInQueue(hash.ToLower(), Settings); } + SetInitialState(hash.ToLower()); + return hash; } @@ -55,19 +57,35 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent { _proxy.AddTorrentFromFile(filename, fileContent, Settings); - if (Settings.TvCategory.IsNotNullOrWhiteSpace()) + try { - _proxy.SetTorrentLabel(hash.ToLower(), Settings.TvCategory, Settings); + if (Settings.TvCategory.IsNotNullOrWhiteSpace()) + { + _proxy.SetTorrentLabel(hash.ToLower(), Settings.TvCategory, Settings); + } + } + catch (Exception ex) + { + _logger.Warn(ex, "Failed to set the torrent label for {0}.", filename); } - var isRecentEpisode = remoteAlbum.IsRecentAlbum(); + try + { + var isRecentAlbum = remoteAlbum.IsRecentAlbum(); - if (isRecentEpisode && Settings.RecentTvPriority == (int)QBittorrentPriority.First || - !isRecentEpisode && Settings.OlderTvPriority == (int)QBittorrentPriority.First) + if (isRecentAlbum && Settings.RecentTvPriority == (int)QBittorrentPriority.First || + !isRecentAlbum && Settings.OlderTvPriority == (int)QBittorrentPriority.First) + { + _proxy.MoveTorrentToTopInQueue(hash.ToLower(), Settings); + } + } + catch (Exception ex) { - _proxy.MoveTorrentToTopInQueue(hash.ToLower(), Settings); + _logger.Warn(ex, "Failed to set the torrent priority for {0}.", filename); } + SetInitialState(hash); + return hash; } @@ -75,19 +93,8 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent public override IEnumerable GetItems() { - QBittorrentPreferences config; - List torrents; - - try - { - config = _proxy.GetConfig(Settings); - torrents = _proxy.GetTorrents(Settings); - } - catch (DownloadClientException ex) - { - _logger.Error(ex); - return Enumerable.Empty(); - } + var config = _proxy.GetConfig(Settings); + var torrents = _proxy.GetTorrents(Settings); var queueItems = new List(); @@ -106,7 +113,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent // Avoid removing torrents that haven't reached the global max ratio. // Removal also requires the torrent to be paused, in case a higher max ratio was set on the torrent itself (which is not exposed by the api). - item.IsReadOnly = (config.MaxRatioEnabled && config.MaxRatio > torrent.Ratio) || torrent.State != "pausedUP"; + item.CanMoveFiles = item.CanBeRemoved = (!config.MaxRatioEnabled || config.MaxRatio <= torrent.Ratio) && torrent.State == "pausedUP"; if (!item.OutputPath.IsEmpty && item.OutputPath.FileName != torrent.Name) { @@ -117,7 +124,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent { case "error": // some error occurred, applies to paused torrents item.Status = DownloadItemStatus.Failed; - item.Message = "QBittorrent is reporting an error"; + item.Message = "qBittorrent is reporting an error"; break; case "pausedDL": // torrent is paused and has NOT finished downloading @@ -160,13 +167,13 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent _proxy.RemoveTorrent(hash.ToLower(), deleteData, Settings); } - public override DownloadClientStatus GetStatus() + public override DownloadClientInfo GetStatus() { var config = _proxy.GetConfig(Settings); var destDir = new OsPath(config.SavePath); - return new DownloadClientStatus + return new DownloadClientInfo { IsLocalhost = Settings.Host == "127.0.0.1" || Settings.Host == "localhost", OutputRootFolders = new List { _remotePathMappingService.RemapRemoteToLocal(Settings.Host, destDir) } @@ -177,6 +184,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent { failures.AddIfNotNull(TestConnection()); if (failures.Any()) return; + failures.AddIfNotNull(TestPrioritySupport()); failures.AddIfNotNull(TestGetTorrents()); } @@ -218,7 +226,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent var config = _proxy.GetConfig(Settings); if (config.MaxRatioEnabled && config.RemoveOnMaxRatio) { - return new NzbDroneValidationFailure(String.Empty, "QBittorrent is configured to remove torrents when they reach their Share Ratio Limit") + return new NzbDroneValidationFailure(String.Empty, "qBittorrent is configured to remove torrents when they reach their Share Ratio Limit") { DetailedDescription = "Lidarr will be unable to perform Completed Download Handling as configured. You can fix this in qBittorrent ('Tools -> Options...' in the menu) by changing 'Options -> BitTorrent -> Share Ratio Limiting' from 'Remove them' to 'Pause them'." }; @@ -226,7 +234,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent } catch (DownloadClientAuthenticationException ex) { - _logger.Error(ex); + _logger.Error(ex, ex.Message); return new NzbDroneValidationFailure("Username", "Authentication failure") { DetailedDescription = "Please verify your username and password." @@ -234,7 +242,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent } catch (WebException ex) { - _logger.Error(ex); + _logger.Error(ex, "Unable to connect to qBittorrent"); if (ex.Status == WebExceptionStatus.ConnectFailure) { return new NzbDroneValidationFailure("Host", "Unable to connect") @@ -246,7 +254,42 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent } catch (Exception ex) { - _logger.Error(ex); + _logger.Error(ex, "Unable to test qBittorrent"); + return new NzbDroneValidationFailure(String.Empty, "Unknown exception: " + ex.Message); + } + + return null; + } + + private ValidationFailure TestPrioritySupport() + { + var recentPriorityDefault = Settings.RecentTvPriority == (int)QBittorrentPriority.Last; + var olderPriorityDefault = Settings.OlderTvPriority == (int)QBittorrentPriority.Last; + + if (olderPriorityDefault && recentPriorityDefault) + { + return null; + } + + try + { + var config = _proxy.GetConfig(Settings); + + if (!config.QueueingEnabled) + { + if (!recentPriorityDefault) + { + return new NzbDroneValidationFailure(nameof(Settings.RecentTvPriority), "Queueing not enabled") { DetailedDescription = "Torrent Queueing is not enabled in your qBittorrent settings. Enable it in qBittorrent or select 'Last' as priority." }; + } + else if (!olderPriorityDefault) + { + return new NzbDroneValidationFailure(nameof(Settings.OlderTvPriority), "Queueing not enabled") { DetailedDescription = "Torrent Queueing is not enabled in your qBittorrent settings. Enable it in qBittorrent or select 'Last' as priority." }; + } + } + } + catch (Exception ex) + { + _logger.Error(ex, "Failed to test qBittorrent"); return new NzbDroneValidationFailure(String.Empty, "Unknown exception: " + ex.Message); } @@ -261,11 +304,34 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent } catch (Exception ex) { - _logger.Error(ex); + _logger.Error(ex, "Failed to get torrents"); return new NzbDroneValidationFailure(String.Empty, "Failed to get the list of torrents: " + ex.Message); } return null; } + + private void SetInitialState(string hash) + { + try + { + switch ((QBittorrentState) Settings.InitialState) + { + case QBittorrentState.ForceStart: + _proxy.SetForceStart(hash, true, Settings); + break; + case QBittorrentState.Start: + _proxy.ResumeTorrent(hash, Settings); + break; + case QBittorrentState.Pause: + _proxy.PauseTorrent(hash, Settings); + break; + } + } + catch (Exception ex) + { + _logger.Warn(ex, "Failed to set inital state for {0}.", hash); + } + } } } diff --git a/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentPreferences.cs b/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentPreferences.cs index 9fddb1116..2f647f5c9 100644 --- a/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentPreferences.cs +++ b/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentPreferences.cs @@ -1,4 +1,4 @@ -using Newtonsoft.Json; +using Newtonsoft.Json; namespace NzbDrone.Core.Download.Clients.QBittorrent { @@ -16,5 +16,8 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent [JsonProperty(PropertyName = "max_ratio_act")] public bool RemoveOnMaxRatio { get; set; } // Action performed when a torrent reaches the maximum share ratio. [false = pause, true = remove] + + [JsonProperty(PropertyName = "queueing_enabled")] + public bool QueueingEnabled { get; set; } = true; } } diff --git a/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentProxy.cs b/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentProxy.cs index e00c57585..cdb79255d 100644 --- a/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentProxy.cs +++ b/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentProxy.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Net; using NLog; @@ -23,6 +23,9 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent void RemoveTorrent(string hash, Boolean removeData, QBittorrentSettings settings); void SetTorrentLabel(string hash, string label, QBittorrentSettings settings); void MoveTorrentToTopInQueue(string hash, QBittorrentSettings settings); + void PauseTorrent(string hash, QBittorrentSettings settings); + void ResumeTorrent(string hash, QBittorrentSettings settings); + void SetForceStart(string hash, bool enabled, QBittorrentSettings settings); } public class QBittorrentProxy : IQBittorrentProxy @@ -72,7 +75,13 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent .Post() .AddFormParameter("urls", torrentUrl); - ProcessRequest(request, settings); + var result = ProcessRequest(request, settings); + + // Note: Older qbit versions returned nothing, so we can't do != "Ok." here. + if (result == "Fails.") + { + throw new DownloadClientException("Download client failed to add torrent by url"); + } } public void AddTorrentFromFile(string fileName, Byte[] fileContent, QBittorrentSettings settings) @@ -81,7 +90,13 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent .Post() .AddFormUpload("torrents", fileName, fileContent); - ProcessRequest(request, settings); + var result = ProcessRequest(request, settings); + + // Note: Current qbit versions return nothing, so we can't do != "Ok." here. + if (result == "Fails.") + { + throw new DownloadClientException("Download client failed to add torrent"); + } } public void RemoveTorrent(string hash, Boolean removeData, QBittorrentSettings settings) @@ -90,7 +105,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent .Post() .AddFormParameter("hashes", hash); - ProcessRequest(request, settings); + ProcessRequest(request, settings); } public void SetTorrentLabel(string hash, string label, QBittorrentSettings settings) @@ -101,18 +116,19 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent .AddFormParameter("category", label); try { - ProcessRequest(setCategoryRequest, settings); + ProcessRequest(setCategoryRequest, settings); } catch(DownloadClientException ex) { - // if setCategory fails due to method not being found, then try older setLabel command for qbittorent < v.3.3.5 + // if setCategory fails due to method not being found, then try older setLabel command for qBittorrent < v.3.3.5 if (ex.InnerException is HttpException && (ex.InnerException as HttpException).Response.StatusCode == HttpStatusCode.NotFound) { var setLabelRequest = BuildRequest(settings).Resource("/command/setLabel") .Post() .AddFormParameter("hashes", hash) .AddFormParameter("label", label); - ProcessRequest(setLabelRequest, settings); + + ProcessRequest(setLabelRequest, settings); } } } @@ -125,7 +141,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent try { - var response = ProcessRequest(request, settings); + ProcessRequest(request, settings); } catch (DownloadClientException ex) { @@ -141,6 +157,34 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent } + public void PauseTorrent(string hash, QBittorrentSettings settings) + { + var request = BuildRequest(settings).Resource("/command/pause") + .Post() + .AddFormParameter("hash", hash); + + ProcessRequest(request, settings); + } + + public void ResumeTorrent(string hash, QBittorrentSettings settings) + { + var request = BuildRequest(settings).Resource("/command/resume") + .Post() + .AddFormParameter("hash", hash); + + ProcessRequest(request, settings); + } + + public void SetForceStart(string hash, bool enabled, QBittorrentSettings settings) + { + var request = BuildRequest(settings).Resource("/command/setForceStart") + .Post() + .AddFormParameter("hashes", hash) + .AddFormParameter("value", enabled ? "true" : "false"); + + ProcessRequest(request, settings); + } + private HttpRequestBuilder BuildRequest(QBittorrentSettings settings) { var requestBuilder = new HttpRequestBuilder(settings.UseSsl, settings.Host, settings.Port); @@ -152,10 +196,18 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent private TResult ProcessRequest(HttpRequestBuilder requestBuilder, QBittorrentSettings settings) where TResult : new() + { + var responseContent = ProcessRequest(requestBuilder, settings); + + return Json.Deserialize(responseContent); + } + + private string ProcessRequest(HttpRequestBuilder requestBuilder, QBittorrentSettings settings) { AuthenticateClient(requestBuilder, settings); var request = requestBuilder.Build(); + request.LogResponseContent = true; HttpResponse response; try @@ -176,15 +228,15 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent } else { - throw new DownloadClientException("Failed to connect to qBitTorrent, check your settings.", ex); + throw new DownloadClientException("Failed to connect to qBittorrent, check your settings.", ex); } } catch (WebException ex) { - throw new DownloadClientException("Failed to connect to qBitTorrent, please check your settings.", ex); + throw new DownloadClientException("Failed to connect to qBittorrent, please check your settings.", ex); } - return Json.Deserialize(response.Content); + return response.Content; } private void AuthenticateClient(HttpRequestBuilder requestBuilder, QBittorrentSettings settings, bool reauthenticate = false) @@ -218,23 +270,23 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent _logger.Debug("qbitTorrent authentication failed."); if (ex.Response.StatusCode == HttpStatusCode.Forbidden) { - throw new DownloadClientAuthenticationException("Failed to authenticate with qbitTorrent.", ex); + throw new DownloadClientAuthenticationException("Failed to authenticate with qBittorrent.", ex); } - throw new DownloadClientException("Failed to connect to qBitTorrent, please check your settings.", ex); + throw new DownloadClientException("Failed to connect to qBittorrent, please check your settings.", ex); } catch (WebException ex) { - throw new DownloadClientException("Failed to connect to qBitTorrent, please check your settings.", ex); + throw new DownloadClientUnavailableException("Failed to connect to qBittorrent, please check your settings.", ex); } if (response.Content != "Ok.") // returns "Fails." on bad login { _logger.Debug("qbitTorrent authentication failed."); - throw new DownloadClientAuthenticationException("Failed to authenticate with qbitTorrent."); + throw new DownloadClientAuthenticationException("Failed to authenticate with qBittorrent."); } - _logger.Debug("qbitTorrent authentication succeeded."); + _logger.Debug("qBittorrent authentication succeeded."); cookies = response.GetCookies(); diff --git a/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentSettings.cs b/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentSettings.cs index 6bdffa80c..27be15c12 100644 --- a/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentSettings.cs +++ b/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentSettings.cs @@ -1,4 +1,4 @@ -using FluentValidation; +using FluentValidation; using NzbDrone.Core.Annotations; using NzbDrone.Core.ThingiProvider; using NzbDrone.Core.Validation; @@ -21,7 +21,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent public QBittorrentSettings() { Host = "localhost"; - Port = 9091; + Port = 8080; TvCategory = "lidarr"; } @@ -46,7 +46,10 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent [FieldDefinition(6, Label = "Older Priority", Type = FieldType.Select, SelectOptions = typeof(QBittorrentPriority), HelpText = "Priority to use when grabbing albums released over 14 days ago")] public int OlderTvPriority { get; set; } - [FieldDefinition(7, Label = "Use SSL", Type = FieldType.Checkbox, HelpText = "Use a secure connection. See Options -> Web UI -> 'Use HTTPS instead of HTTP' in qBittorrent.")] + [FieldDefinition(7, Label = "Initial State", Type = FieldType.Select, SelectOptions = typeof(QBittorrentState), HelpText = "Initial state for torrents added to qBittorrent")] + public int InitialState { get; set; } + + [FieldDefinition(8, Label = "Use SSL", Type = FieldType.Checkbox, HelpText = "Use a secure connection. See Options -> Web UI -> 'Use HTTPS instead of HTTP' in qBittorrent.")] public bool UseSsl { get; set; } public NzbDroneValidationResult Validate() diff --git a/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentState.cs b/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentState.cs new file mode 100644 index 000000000..56c5ddf1a --- /dev/null +++ b/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentState.cs @@ -0,0 +1,9 @@ +namespace NzbDrone.Core.Download.Clients.QBittorrent +{ + public enum QBittorrentState + { + Start = 0, + ForceStart = 1, + Pause = 2 + } +} diff --git a/src/NzbDrone.Core/Download/Clients/Sabnzbd/Sabnzbd.cs b/src/NzbDrone.Core/Download/Clients/Sabnzbd/Sabnzbd.cs index 27ecc4cf9..93ba3df81 100644 --- a/src/NzbDrone.Core/Download/Clients/Sabnzbd/Sabnzbd.cs +++ b/src/NzbDrone.Core/Download/Clients/Sabnzbd/Sabnzbd.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; @@ -57,7 +57,7 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd } catch (DownloadClientException ex) { - _logger.Warn("Couldn't get download queue. {0}", ex.Message); + _logger.Warn(ex, "Couldn't get download queue. {0}", ex.Message); return Enumerable.Empty(); } @@ -78,8 +78,11 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd queueItem.TotalSize = (long)(sabQueueItem.Size * 1024 * 1024); queueItem.RemainingSize = (long)(sabQueueItem.Sizeleft * 1024 * 1024); queueItem.RemainingTime = sabQueueItem.Timeleft; + queueItem.CanBeRemoved = true; + queueItem.CanMoveFiles = true; - if (sabQueue.Paused || sabQueueItem.Status == SabnzbdDownloadStatus.Paused) + if ((sabQueue.Paused && sabQueueItem.Priority != SabnzbdPriority.Force) || + sabQueueItem.Status == SabnzbdDownloadStatus.Paused) { queueItem.Status = DownloadItemStatus.Paused; @@ -110,17 +113,7 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd private IEnumerable GetHistory() { - SabnzbdHistory sabHistory; - - try - { - sabHistory = _proxy.GetHistory(0, _configService.DownloadClientHistoryLimit, Settings.TvCategory, Settings); - } - catch (DownloadClientException ex) - { - _logger.Error(ex); - return Enumerable.Empty(); - } + var sabHistory = _proxy.GetHistory(0, _configService.DownloadClientHistoryLimit, Settings.TvCategory, Settings); var historyItems = new List(); @@ -142,7 +135,10 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd RemainingSize = 0, RemainingTime = TimeSpan.Zero, - Message = sabHistoryItem.FailMessage + Message = sabHistoryItem.FailMessage, + + CanBeRemoved = true, + CanMoveFiles = true }; if (sabHistoryItem.Status == SabnzbdDownloadStatus.Failed) @@ -244,7 +240,7 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd } } - public override DownloadClientStatus GetStatus() + public override DownloadClientInfo GetStatus() { var config = _proxy.GetConfig(Settings); var categories = GetCategories(config).ToArray(); @@ -256,7 +252,7 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd category = categories.FirstOrDefault(v => v.Name == "*"); } - var status = new DownloadClientStatus + var status = new DownloadClientInfo { IsLocalhost = Settings.Host == "127.0.0.1" || Settings.Host == "localhost" }; @@ -319,6 +315,11 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd private Version ParseVersion(string version) { + if (version.IsNullOrWhiteSpace()) + { + return null; + } + var parsed = VersionRegex.Match(version); int major; @@ -356,7 +357,7 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd if (version == null) { - return new ValidationFailure("Version", "Unknown Version: " + version); + return new ValidationFailure("Version", "Unknown Version: " + rawVersion); } if (rawVersion.Equals("develop", StringComparison.InvariantCultureIgnoreCase)) @@ -382,7 +383,7 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd } catch (Exception ex) { - _logger.Error(ex); + _logger.Error(ex, ex.Message); return new ValidationFailure("Host", "Unable to connect to SABnzbd"); } } diff --git a/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabnzbdProxy.cs b/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabnzbdProxy.cs index 397771ff2..26185cf44 100644 --- a/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabnzbdProxy.cs +++ b/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabnzbdProxy.cs @@ -1,4 +1,4 @@ -using System; +using System; using Newtonsoft.Json.Linq; using NLog; using NzbDrone.Common.Extensions; @@ -183,7 +183,7 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd } catch (WebException ex) { - throw new DownloadClientException("Unable to connect to SABnzbd, please check your settings", ex); + throw new DownloadClientUnavailableException("Unable to connect to SABnzbd, please check your settings", ex); } CheckForError(response); diff --git a/src/NzbDrone.Core/Download/Clients/Transmission/TransmissionBase.cs b/src/NzbDrone.Core/Download/Clients/Transmission/TransmissionBase.cs index 2185c03f0..72dfa4913 100644 --- a/src/NzbDrone.Core/Download/Clients/Transmission/TransmissionBase.cs +++ b/src/NzbDrone.Core/Download/Clients/Transmission/TransmissionBase.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Net; @@ -33,17 +33,7 @@ namespace NzbDrone.Core.Download.Clients.Transmission public override IEnumerable GetItems() { - List torrents; - - try - { - torrents = _proxy.GetTorrents(Settings); - } - catch (DownloadClientException ex) - { - _logger.Error(ex); - return Enumerable.Empty(); - } + var torrents = _proxy.GetTorrents(Settings); var items = new List(); @@ -86,8 +76,9 @@ namespace NzbDrone.Core.Download.Clients.Transmission item.Status = DownloadItemStatus.Warning; item.Message = torrent.ErrorString; } - else if (torrent.Status == TransmissionTorrentStatus.Seeding || - torrent.Status == TransmissionTorrentStatus.SeedingWait) + else if (torrent.LeftUntilDone == 0 && (torrent.Status == TransmissionTorrentStatus.Stopped || + torrent.Status == TransmissionTorrentStatus.Seeding || + torrent.Status == TransmissionTorrentStatus.SeedingWait)) { item.Status = DownloadItemStatus.Completed; } @@ -105,7 +96,7 @@ namespace NzbDrone.Core.Download.Clients.Transmission item.Status = DownloadItemStatus.Downloading; } - item.IsReadOnly = torrent.Status != TransmissionTorrentStatus.Stopped; + item.CanMoveFiles = item.CanBeRemoved = torrent.Status == TransmissionTorrentStatus.Stopped; items.Add(item); } @@ -118,7 +109,7 @@ namespace NzbDrone.Core.Download.Clients.Transmission _proxy.RemoveTorrent(downloadId.ToLower(), deleteData, Settings); } - public override DownloadClientStatus GetStatus() + public override DownloadClientInfo GetStatus() { var config = _proxy.GetConfig(Settings); var destDir = config.GetValueOrDefault("download-dir") as string; @@ -128,7 +119,7 @@ namespace NzbDrone.Core.Download.Clients.Transmission destDir = string.Format("{0}/.{1}", destDir, Settings.TvCategory); } - return new DownloadClientStatus + return new DownloadClientInfo { IsLocalhost = Settings.Host == "127.0.0.1" || Settings.Host == "localhost", OutputRootFolders = new List { _remotePathMappingService.RemapRemoteToLocal(Settings.Host, new OsPath(destDir)) } @@ -204,27 +195,23 @@ namespace NzbDrone.Core.Download.Clients.Transmission } catch (DownloadClientAuthenticationException ex) { - _logger.Error(ex); + _logger.Error(ex, ex.Message); return new NzbDroneValidationFailure("Username", "Authentication failure") { DetailedDescription = string.Format("Please verify your username and password. Also verify if the host running Lidarr isn't blocked from accessing {0} by WhiteList limitations in the {0} configuration.", Name) }; } - catch (WebException ex) + catch (DownloadClientUnavailableException ex) { - _logger.Error(ex); - if (ex.Status == WebExceptionStatus.ConnectFailure) + _logger.Error(ex, ex.Message); + return new NzbDroneValidationFailure("Host", "Unable to connect") { - return new NzbDroneValidationFailure("Host", "Unable to connect") - { - DetailedDescription = "Please verify the hostname and port." - }; - } - return new NzbDroneValidationFailure(string.Empty, "Unknown exception: " + ex.Message); + DetailedDescription = "Please verify the hostname and port." + }; } catch (Exception ex) { - _logger.Error(ex); + _logger.Error(ex, "Failed to test"); return new NzbDroneValidationFailure(string.Empty, "Unknown exception: " + ex.Message); } } @@ -239,11 +226,11 @@ namespace NzbDrone.Core.Download.Clients.Transmission } catch (Exception ex) { - _logger.Error(ex); + _logger.Error(ex, "Failed to get torrents"); return new NzbDroneValidationFailure(string.Empty, "Failed to get the list of torrents: " + ex.Message); } return null; } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Core/Download/Clients/Transmission/TransmissionProxy.cs b/src/NzbDrone.Core/Download/Clients/Transmission/TransmissionProxy.cs index cada83cae..7c06b9278 100644 --- a/src/NzbDrone.Core/Download/Clients/Transmission/TransmissionProxy.cs +++ b/src/NzbDrone.Core/Download/Clients/Transmission/TransmissionProxy.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Net; using System.Collections.Generic; using NzbDrone.Common.Extensions; @@ -238,54 +238,65 @@ namespace NzbDrone.Core.Download.Clients.Transmission public TransmissionResponse ProcessRequest(string action, object arguments, TransmissionSettings settings) { - var requestBuilder = BuildRequest(settings); - requestBuilder.Headers.ContentType = "application/json"; - requestBuilder.SuppressHttpError = true; + try + { + var requestBuilder = BuildRequest(settings); + requestBuilder.Headers.ContentType = "application/json"; + requestBuilder.SuppressHttpError = true; - AuthenticateClient(requestBuilder, settings); + AuthenticateClient(requestBuilder, settings); - var request = requestBuilder.Post().Build(); + var request = requestBuilder.Post().Build(); - var data = new Dictionary(); - data.Add("method", action); + var data = new Dictionary(); + data.Add("method", action); - if (arguments != null) - { - data.Add("arguments", arguments); - } + if (arguments != null) + { + data.Add("arguments", arguments); + } - request.SetContent(data.ToJson()); - request.ContentSummary = string.Format("{0}(...)", action); + request.SetContent(data.ToJson()); + request.ContentSummary = string.Format("{0}(...)", action); - var response = _httpClient.Execute(request); - if (response.StatusCode == HttpStatusCode.Conflict) - { - AuthenticateClient(requestBuilder, settings, true); + var response = _httpClient.Execute(request); - request = requestBuilder.Post().Build(); + if (response.StatusCode == HttpStatusCode.Conflict) + { + AuthenticateClient(requestBuilder, settings, true); - request.SetContent(data.ToJson()); - request.ContentSummary = string.Format("{0}(...)", action); + request = requestBuilder.Post().Build(); - response = _httpClient.Execute(request); - } - else if (response.StatusCode == HttpStatusCode.Unauthorized) - { - throw new DownloadClientAuthenticationException("User authentication failed."); - } + request.SetContent(data.ToJson()); + request.ContentSummary = string.Format("{0}(...)", action); + + response = _httpClient.Execute(request); + } + else if (response.StatusCode == HttpStatusCode.Unauthorized) + { + throw new DownloadClientAuthenticationException("User authentication failed."); + } - var transmissionResponse = Json.Deserialize(response.Content); + var transmissionResponse = Json.Deserialize(response.Content); - if (transmissionResponse == null) + if (transmissionResponse == null) + { + throw new TransmissionException("Unexpected response"); + } + else if (transmissionResponse.Result != "success") + { + throw new TransmissionException(transmissionResponse.Result); + } + return transmissionResponse; + } + catch (HttpException ex) { - throw new TransmissionException("Unexpected response"); + throw new DownloadClientException("Unable to connect to Transmission, please check your settings", ex); } - else if (transmissionResponse.Result != "success") + catch (WebException ex) { - throw new TransmissionException(transmissionResponse.Result); + throw new DownloadClientUnavailableException("Unable to connect to Transmission, please check your settings", ex); } - - return transmissionResponse; } } } diff --git a/src/NzbDrone.Core/Download/Clients/Vuze/Vuze.cs b/src/NzbDrone.Core/Download/Clients/Vuze/Vuze.cs index 1da02e835..dc3bb712e 100644 --- a/src/NzbDrone.Core/Download/Clients/Vuze/Vuze.cs +++ b/src/NzbDrone.Core/Download/Clients/Vuze/Vuze.cs @@ -26,7 +26,19 @@ namespace NzbDrone.Core.Download.Clients.Vuze protected override OsPath GetOutputPath(OsPath outputPath, TransmissionTorrent torrent) { - _logger.Debug("Vuze output directory: {0}", outputPath); + // Vuze has similar behavior as uTorrent: + // - A multi-file torrent is downloaded in a job folder and 'outputPath' points to that directory directly. + // - A single-file torrent is downloaded in the root folder and 'outputPath' poinst to that root folder. + // We have to make sure the return value points to the job folder OR file. + if (outputPath == null || outputPath.FileName == torrent.Name) + { + _logger.Trace("Vuze output directory: {0}", outputPath); + } + else + { + outputPath = outputPath + torrent.Name; + _logger.Trace("Vuze output file: {0}", outputPath); + } return outputPath; } @@ -50,4 +62,4 @@ namespace NzbDrone.Core.Download.Clients.Vuze public override string Name => "Vuze"; } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Core/Download/Clients/rTorrent/RTorrent.cs b/src/NzbDrone.Core/Download/Clients/rTorrent/RTorrent.cs index bd44f7bf7..00ffd0ec6 100644 --- a/src/NzbDrone.Core/Download/Clients/rTorrent/RTorrent.cs +++ b/src/NzbDrone.Core/Download/Clients/rTorrent/RTorrent.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Linq; using System.Collections.Generic; using System.Threading; @@ -81,57 +81,60 @@ namespace NzbDrone.Core.Download.Clients.RTorrent public override IEnumerable GetItems() { - try + var torrents = _proxy.GetTorrents(Settings); + + _logger.Debug("Retrieved metadata of {0} torrents in client", torrents.Count); + + var items = new List(); + foreach (RTorrentTorrent torrent in torrents) { - var torrents = _proxy.GetTorrents(Settings); + // Don't concern ourselves with categories other than specified + if (torrent.Category != Settings.TvCategory) continue; - _logger.Debug("Retrieved metadata of {0} torrents in client", torrents.Count); + if (torrent.Path.StartsWith(".")) + { + throw new DownloadClientException("Download paths paths must be absolute. Please specify variable \"directory\" in rTorrent."); + } - var items = new List(); - foreach (RTorrentTorrent torrent in torrents) + var item = new DownloadClientItem(); + item.DownloadClient = Definition.Name; + item.Title = torrent.Name; + item.DownloadId = torrent.Hash; + item.OutputPath = _remotePathMappingService.RemapRemoteToLocal(Settings.Host, new OsPath(torrent.Path)); + item.TotalSize = torrent.TotalSize; + item.RemainingSize = torrent.RemainingSize; + item.Category = torrent.Category; + + if (torrent.DownRate > 0) + { + var secondsLeft = torrent.RemainingSize / torrent.DownRate; + item.RemainingTime = TimeSpan.FromSeconds(secondsLeft); + } + else { - // Don't concern ourselves with categories other than specified - if (torrent.Category != Settings.TvCategory) continue; - - if (torrent.Path.StartsWith(".")) - { - throw new DownloadClientException("Download paths paths must be absolute. Please specify variable \"directory\" in rTorrent."); - } - - var item = new DownloadClientItem(); - item.DownloadClient = Definition.Name; - item.Title = torrent.Name; - item.DownloadId = torrent.Hash; - item.OutputPath = _remotePathMappingService.RemapRemoteToLocal(Settings.Host, new OsPath(torrent.Path)); - item.TotalSize = torrent.TotalSize; - item.RemainingSize = torrent.RemainingSize; - item.Category = torrent.Category; - - if (torrent.DownRate > 0) { - var secondsLeft = torrent.RemainingSize / torrent.DownRate; - item.RemainingTime = TimeSpan.FromSeconds(secondsLeft); - } else { - item.RemainingTime = TimeSpan.Zero; - } - - if (torrent.IsFinished) item.Status = DownloadItemStatus.Completed; - else if (torrent.IsActive) item.Status = DownloadItemStatus.Downloading; - else if (!torrent.IsActive) item.Status = DownloadItemStatus.Paused; - - // No stop ratio data is present, so do not delete - item.IsReadOnly = true; - - items.Add(item); + item.RemainingTime = TimeSpan.Zero; } - return items; - } - catch (DownloadClientException ex) - { - _logger.Error(ex); - return Enumerable.Empty(); + if (torrent.IsFinished) + { + item.Status = DownloadItemStatus.Completed; + } + else if (torrent.IsActive) + { + item.Status = DownloadItemStatus.Downloading; + } + else if (!torrent.IsActive) + { + item.Status = DownloadItemStatus.Paused; + } + + // No stop ratio data is present, so do not delete + item.CanMoveFiles = item.CanBeRemoved = false; + + items.Add(item); } + return items; } public override void RemoveItem(string downloadId, bool deleteData) @@ -144,11 +147,11 @@ namespace NzbDrone.Core.Download.Clients.RTorrent _proxy.RemoveTorrent(downloadId, Settings); } - public override DownloadClientStatus GetStatus() + public override DownloadClientInfo GetStatus() { // XXX: This function's correctness has not been considered - var status = new DownloadClientStatus + var status = new DownloadClientInfo { IsLocalhost = Settings.Host == "127.0.0.1" || Settings.Host == "localhost" }; @@ -177,7 +180,7 @@ namespace NzbDrone.Core.Download.Clients.RTorrent } catch (Exception ex) { - _logger.Error(ex); + _logger.Error(ex, "Failed to test rTorrent"); return new NzbDroneValidationFailure(string.Empty, "Unknown exception: " + ex.Message); } @@ -192,7 +195,7 @@ namespace NzbDrone.Core.Download.Clients.RTorrent } catch (Exception ex) { - _logger.Error(ex); + _logger.Error(ex, "Failed to get torrents"); return new NzbDroneValidationFailure(string.Empty, "Failed to get the list of torrents: " + ex.Message); } diff --git a/src/NzbDrone.Core/Download/Clients/rTorrent/RTorrentDirectoryValidator.cs b/src/NzbDrone.Core/Download/Clients/rTorrent/RTorrentDirectoryValidator.cs index 3cb2d6a8b..45a3c39c5 100644 --- a/src/NzbDrone.Core/Download/Clients/rTorrent/RTorrentDirectoryValidator.cs +++ b/src/NzbDrone.Core/Download/Clients/rTorrent/RTorrentDirectoryValidator.cs @@ -1,4 +1,4 @@ -using FluentValidation; +using FluentValidation; using FluentValidation.Results; using NzbDrone.Common.Extensions; using NzbDrone.Core.Download.Clients.RTorrent; @@ -15,13 +15,11 @@ namespace NzbDrone.Core.Download.Clients.rTorrent { public RTorrentDirectoryValidator(RootFolderValidator rootFolderValidator, PathExistsValidator pathExistsValidator, - DroneFactoryValidator droneFactoryValidator, MappedNetworkDriveValidator mappedNetworkDriveValidator) { RuleFor(c => c.TvDirectory).Cascade(CascadeMode.StopOnFirstFailure) .IsValidPath() .SetValidator(rootFolderValidator) - .SetValidator(droneFactoryValidator) .SetValidator(mappedNetworkDriveValidator) .SetValidator(pathExistsValidator) .When(c => c.TvDirectory.IsNotNullOrWhiteSpace()) diff --git a/src/NzbDrone.Core/Download/Clients/rTorrent/RTorrentProxy.cs b/src/NzbDrone.Core/Download/Clients/rTorrent/RTorrentProxy.cs index 749a68d7a..c00df292e 100644 --- a/src/NzbDrone.Core/Download/Clients/rTorrent/RTorrentProxy.cs +++ b/src/NzbDrone.Core/Download/Clients/rTorrent/RTorrentProxy.cs @@ -1,7 +1,9 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Net; +using System.Net.Sockets; +using System.Runtime.InteropServices.ComTypes; using NLog; using NzbDrone.Common.Extensions; using CookComputing.XmlRpc; @@ -54,8 +56,7 @@ namespace NzbDrone.Core.Download.Clients.RTorrent _logger.Debug("Executing remote method: system.client_version"); var client = BuildClient(settings); - - var version = client.GetVersion(); + var version = ExecuteRequest(() => client.GetVersion()); return version; } @@ -65,20 +66,22 @@ namespace NzbDrone.Core.Download.Clients.RTorrent _logger.Debug("Executing remote method: d.multicall2"); var client = BuildClient(settings); - var ret = client.TorrentMulticall("", "", - "d.name=", // string - "d.hash=", // string - "d.base_path=", // string - "d.custom1=", // string (label) - "d.size_bytes=", // long - "d.left_bytes=", // long - "d.down.rate=", // long (in bytes / s) - "d.ratio=", // long - "d.is_open=", // long - "d.is_active=", // long - "d.complete="); //long + var ret = ExecuteRequest(() => client.TorrentMulticall("", "", + "d.name=", // string + "d.hash=", // string + "d.base_path=", // string + "d.custom1=", // string (label) + "d.size_bytes=", // long + "d.left_bytes=", // long + "d.down.rate=", // long (in bytes / s) + "d.ratio=", // long + "d.is_open=", // long + "d.is_active=", // long + "d.complete=") //long + ); var items = new List(); + foreach (object[] torrent in ret) { var labelDecoded = System.Web.HttpUtility.UrlDecode((string) torrent[3]); @@ -107,8 +110,8 @@ namespace NzbDrone.Core.Download.Clients.RTorrent _logger.Debug("Executing remote method: load.normal"); var client = BuildClient(settings); + var response = ExecuteRequest(() => client.LoadStart("", torrentUrl, GetCommands(label, priority, directory))); - var response = client.LoadStart("", torrentUrl, GetCommands(label, priority, directory)); if (response != 0) { throw new DownloadClientException("Could not add torrent: {0}.", torrentUrl); @@ -120,8 +123,8 @@ namespace NzbDrone.Core.Download.Clients.RTorrent _logger.Debug("Executing remote method: load.raw"); var client = BuildClient(settings); + var response = ExecuteRequest(() => client.LoadRawStart("", fileContent, GetCommands(label, priority, directory))); - var response = client.LoadRawStart("", fileContent, GetCommands(label, priority, directory)); if (response != 0) { throw new DownloadClientException("Could not add torrent: {0}.", fileName); @@ -133,14 +136,39 @@ namespace NzbDrone.Core.Download.Clients.RTorrent _logger.Debug("Executing remote method: d.erase"); var client = BuildClient(settings); + var response = ExecuteRequest(() => client.Remove(hash)); - var response = client.Remove(hash); if (response != 0) { throw new DownloadClientException("Could not remove torrent: {0}.", hash); } } + public bool HasHashTorrent(string hash, RTorrentSettings settings) + { + _logger.Debug("Executing remote method: d.name"); + + var client = BuildClient(settings); + + try + { + var name = ExecuteRequest(() => client.GetName(hash)); + + if (name.IsNullOrWhiteSpace()) + { + return false; + } + + var metaTorrent = name == (hash + ".meta"); + + return !metaTorrent; + } + catch (Exception) + { + return false; + } + } + private string[] GetCommands(string label, RTorrentPriority priority, string directory) { var result = new List(); @@ -163,25 +191,6 @@ namespace NzbDrone.Core.Download.Clients.RTorrent return result.ToArray(); } - public bool HasHashTorrent(string hash, RTorrentSettings settings) - { - _logger.Debug("Executing remote method: d.name"); - - var client = BuildClient(settings); - - try - { - var name = client.GetName(hash); - if (name.IsNullOrWhiteSpace()) return false; - bool metaTorrent = name == (hash + ".meta"); - return !metaTorrent; - } - catch (Exception) - { - return false; - } - } - private IRTorrent BuildClient(RTorrentSettings settings) { var client = XmlRpcProxyGen.Create(); @@ -201,5 +210,21 @@ namespace NzbDrone.Core.Download.Clients.RTorrent return client; } + + private T ExecuteRequest(Func task) + { + try + { + return task(); + } + catch (XmlRpcServerException ex) + { + throw new DownloadClientException("Unable to connect to rTorrent, please check your settings", ex); + } + catch (WebException ex) + { + throw new DownloadClientUnavailableException("Unable to connect to rTorrent, please check your settings", ex); + } + } } } diff --git a/src/NzbDrone.Core/Download/Clients/uTorrent/UTorrent.cs b/src/NzbDrone.Core/Download/Clients/uTorrent/UTorrent.cs index a565d747a..d82178e81 100644 --- a/src/NzbDrone.Core/Download/Clients/uTorrent/UTorrent.cs +++ b/src/NzbDrone.Core/Download/Clients/uTorrent/UTorrent.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Linq; using System.Collections.Generic; using NzbDrone.Common.Disk; @@ -49,6 +49,8 @@ namespace NzbDrone.Core.Download.Clients.UTorrent _proxy.MoveTorrentToTopInQueue(hash, Settings); } + _proxy.SetState(hash, (UTorrentState)Settings.IntialState, Settings); + return hash; } @@ -65,6 +67,8 @@ namespace NzbDrone.Core.Download.Clients.UTorrent _proxy.MoveTorrentToTopInQueue(hash, Settings); } + _proxy.SetState(hash, (UTorrentState)Settings.IntialState, Settings); + return hash; } @@ -72,42 +76,7 @@ namespace NzbDrone.Core.Download.Clients.UTorrent public override IEnumerable GetItems() { - List torrents; - - try - { - var cacheKey = string.Format("{0}:{1}:{2}", Settings.Host, Settings.Port, Settings.TvCategory); - var cache = _torrentCache.Find(cacheKey); - - var response = _proxy.GetTorrents(cache == null ? null : cache.CacheID, Settings); - - if (cache != null && response.Torrents == null) - { - var removedAndUpdated = new HashSet(response.TorrentsChanged.Select(v => v.Hash).Concat(response.TorrentsRemoved)); - - torrents = cache.Torrents - .Where(v => !removedAndUpdated.Contains(v.Hash)) - .Concat(response.TorrentsChanged) - .ToList(); - } - else - { - torrents = response.Torrents; - } - - cache = new UTorrentTorrentCache - { - CacheID = response.CacheNumber, - Torrents = torrents - }; - - _torrentCache.Set(cacheKey, cache, TimeSpan.FromMinutes(15)); - } - catch (DownloadClientException ex) - { - _logger.Error(ex); - return Enumerable.Empty(); - } + var torrents = GetTorrents(); var queueItems = new List(); @@ -165,7 +134,7 @@ namespace NzbDrone.Core.Download.Clients.UTorrent } // 'Started' without 'Queued' is when the torrent is 'forced seeding' - item.IsReadOnly = torrent.Status.HasFlag(UTorrentTorrentStatus.Queued) || torrent.Status.HasFlag(UTorrentTorrentStatus.Started); + item.CanMoveFiles = item.CanBeRemoved = (!torrent.Status.HasFlag(UTorrentTorrentStatus.Queued) && !torrent.Status.HasFlag(UTorrentTorrentStatus.Started)); queueItems.Add(item); } @@ -173,12 +142,46 @@ namespace NzbDrone.Core.Download.Clients.UTorrent return queueItems; } + private List GetTorrents() + { + List torrents; + + var cacheKey = string.Format("{0}:{1}:{2}", Settings.Host, Settings.Port, Settings.TvCategory); + var cache = _torrentCache.Find(cacheKey); + + var response = _proxy.GetTorrents(cache == null ? null : cache.CacheID, Settings); + + if (cache != null && response.Torrents == null) + { + var removedAndUpdated = new HashSet(response.TorrentsChanged.Select(v => v.Hash).Concat(response.TorrentsRemoved)); + + torrents = cache.Torrents + .Where(v => !removedAndUpdated.Contains(v.Hash)) + .Concat(response.TorrentsChanged) + .ToList(); + } + else + { + torrents = response.Torrents; + } + + cache = new UTorrentTorrentCache + { + CacheID = response.CacheNumber, + Torrents = torrents + }; + + _torrentCache.Set(cacheKey, cache, TimeSpan.FromMinutes(15)); + + return torrents; + } + public override void RemoveItem(string downloadId, bool deleteData) { _proxy.RemoveTorrent(downloadId, deleteData, Settings); } - public override DownloadClientStatus GetStatus() + public override DownloadClientInfo GetStatus() { var config = _proxy.GetConfig(Settings); @@ -199,7 +202,7 @@ namespace NzbDrone.Core.Download.Clients.UTorrent } } - var status = new DownloadClientStatus + var status = new DownloadClientInfo { IsLocalhost = Settings.Host == "127.0.0.1" || Settings.Host == "localhost" }; @@ -232,7 +235,7 @@ namespace NzbDrone.Core.Download.Clients.UTorrent } catch (DownloadClientAuthenticationException ex) { - _logger.Error(ex); + _logger.Error(ex, ex.Message); return new NzbDroneValidationFailure("Username", "Authentication failure") { DetailedDescription = "Please verify your username and password." @@ -240,7 +243,7 @@ namespace NzbDrone.Core.Download.Clients.UTorrent } catch (WebException ex) { - _logger.Error(ex); + _logger.Error(ex, "Unable to connect to uTorrent"); if (ex.Status == WebExceptionStatus.ConnectFailure) { return new NzbDroneValidationFailure("Host", "Unable to connect") @@ -252,7 +255,7 @@ namespace NzbDrone.Core.Download.Clients.UTorrent } catch (Exception ex) { - _logger.Error(ex); + _logger.Error(ex, "Failed to test uTorrent"); return new NzbDroneValidationFailure(string.Empty, "Unknown exception: " + ex.Message); } @@ -267,7 +270,7 @@ namespace NzbDrone.Core.Download.Clients.UTorrent } catch (Exception ex) { - _logger.Error(ex); + _logger.Error(ex, "Failed to get torrents"); return new NzbDroneValidationFailure(string.Empty, "Failed to get the list of torrents: " + ex.Message); } diff --git a/src/NzbDrone.Core/Download/Clients/uTorrent/UTorrentProxy.cs b/src/NzbDrone.Core/Download/Clients/uTorrent/UTorrentProxy.cs index 64117f328..c594ebb58 100644 --- a/src/NzbDrone.Core/Download/Clients/uTorrent/UTorrentProxy.cs +++ b/src/NzbDrone.Core/Download/Clients/uTorrent/UTorrentProxy.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Net; using NLog; @@ -22,6 +22,7 @@ namespace NzbDrone.Core.Download.Clients.UTorrent void RemoveTorrent(string hash, bool removeData, UTorrentSettings settings); void SetTorrentLabel(string hash, string label, UTorrentSettings settings); void MoveTorrentToTopInQueue(string hash, UTorrentSettings settings); + void SetState(string hash, UTorrentState state, UTorrentSettings settings); } public class UTorrentProxy : IUTorrentProxy @@ -157,6 +158,15 @@ namespace NzbDrone.Core.Download.Clients.UTorrent ProcessRequest(requestBuilder, settings); } + public void SetState(string hash, UTorrentState state, UTorrentSettings settings) + { + var requestBuilder = BuildRequest(settings) + .AddQueryParam("action", state.ToString().ToLowerInvariant()) + .AddQueryParam("hash", hash); + + ProcessRequest(requestBuilder, settings); + } + private HttpRequestBuilder BuildRequest(UTorrentSettings settings) { var requestBuilder = new HttpRequestBuilder(false, settings.Host, settings.Port) @@ -244,7 +254,7 @@ namespace NzbDrone.Core.Download.Clients.UTorrent } catch (WebException ex) { - throw new DownloadClientException("Unable to connect to uTorrent, please check your settings", ex); + throw new DownloadClientUnavailableException("Unable to connect to uTorrent, please check your settings", ex); } cookies = response.GetCookies(); diff --git a/src/NzbDrone.Core/Download/Clients/uTorrent/UTorrentSettings.cs b/src/NzbDrone.Core/Download/Clients/uTorrent/UTorrentSettings.cs index 103bec26e..cd03090f4 100644 --- a/src/NzbDrone.Core/Download/Clients/uTorrent/UTorrentSettings.cs +++ b/src/NzbDrone.Core/Download/Clients/uTorrent/UTorrentSettings.cs @@ -1,4 +1,4 @@ -using FluentValidation; +using FluentValidation; using NzbDrone.Core.Annotations; using NzbDrone.Core.ThingiProvider; using NzbDrone.Core.Validation; @@ -22,7 +22,7 @@ namespace NzbDrone.Core.Download.Clients.UTorrent public UTorrentSettings() { Host = "localhost"; - Port = 9091; + Port = 8080; TvCategory = "lidarr"; } @@ -47,6 +47,9 @@ namespace NzbDrone.Core.Download.Clients.UTorrent [FieldDefinition(6, Label = "Older Priority", Type = FieldType.Select, SelectOptions = typeof(UTorrentPriority), HelpText = "Priority to use when grabbing albums released over 14 days ago")] public int OlderTvPriority { get; set; } + [FieldDefinition(7, Label = "Initial State", Type = FieldType.Select, SelectOptions = typeof(UTorrentState), HelpText = "Initial state for torrents added to uTorrent")] + public int IntialState { get; set; } + public NzbDroneValidationResult Validate() { return new NzbDroneValidationResult(Validator.Validate(this)); diff --git a/src/NzbDrone.Core/Download/Clients/uTorrent/UtorrentState.cs b/src/NzbDrone.Core/Download/Clients/uTorrent/UtorrentState.cs new file mode 100644 index 000000000..db5d3da87 --- /dev/null +++ b/src/NzbDrone.Core/Download/Clients/uTorrent/UtorrentState.cs @@ -0,0 +1,10 @@ +namespace NzbDrone.Core.Download.Clients.UTorrent +{ + public enum UTorrentState + { + Start = 0, + ForceStart = 1, + Pause = 2, + Stop = 3 + } +} diff --git a/src/NzbDrone.Core/Download/CompletedDownloadService.cs b/src/NzbDrone.Core/Download/CompletedDownloadService.cs index 9f56132b5..6e889c771 100644 --- a/src/NzbDrone.Core/Download/CompletedDownloadService.cs +++ b/src/NzbDrone.Core/Download/CompletedDownloadService.cs @@ -11,7 +11,7 @@ using NzbDrone.Core.History; using NzbDrone.Core.MediaFiles; using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Parser; -using NzbDrone.Core.Tv; +using NzbDrone.Core.Music; using NzbDrone.Core.MediaFiles.TrackImport; namespace NzbDrone.Core.Download @@ -26,26 +26,26 @@ namespace NzbDrone.Core.Download private readonly IConfigService _configService; private readonly IEventAggregator _eventAggregator; private readonly IHistoryService _historyService; - private readonly IDownloadedEpisodesImportService _downloadedEpisodesImportService; + private readonly IDownloadedTracksImportService _downloadedTracksImportService; private readonly IParsingService _parsingService; private readonly Logger _logger; - private readonly ISeriesService _seriesService; + private readonly IArtistService _artistService; public CompletedDownloadService(IConfigService configService, IEventAggregator eventAggregator, IHistoryService historyService, - IDownloadedEpisodesImportService downloadedEpisodesImportService, + IDownloadedTracksImportService downloadedTracksImportService, IParsingService parsingService, - ISeriesService seriesService, + IArtistService artistService, Logger logger) { _configService = configService; _eventAggregator = eventAggregator; _historyService = historyService; - _downloadedEpisodesImportService = downloadedEpisodesImportService; + _downloadedTracksImportService = downloadedTracksImportService; _parsingService = parsingService; _logger = logger; - _seriesService = seriesService; + _artistService = artistService; } public void Process(TrackedDownload trackedDownload, bool ignoreWarnings = false) @@ -80,26 +80,18 @@ namespace NzbDrone.Core.Download return; } - var downloadedEpisodesFolder = new OsPath(_configService.DownloadedAlbumsFolder); + var artist = trackedDownload.RemoteAlbum.Artist; - if (downloadedEpisodesFolder.Contains(downloadItemOutputPath)) - { - trackedDownload.Warn("Intermediate Download path inside drone factory, Skipping."); - return; - } - - var series = _parsingService.GetSeries(trackedDownload.DownloadItem.Title); - - if (series == null) + if (artist == null) { if (historyItem != null) { - series = _seriesService.GetSeries(historyItem.ArtistId); + artist = _artistService.GetArtist(historyItem.ArtistId); } - if (series == null) + if (artist == null) { - trackedDownload.Warn("Series title mismatch, automatic import is not possible."); + trackedDownload.Warn("Artist name mismatch, automatic import is not possible."); return; } } @@ -111,7 +103,7 @@ namespace NzbDrone.Core.Download private void Import(TrackedDownload trackedDownload) { var outputPath = trackedDownload.DownloadItem.OutputPath.FullPath; - var importResults = _downloadedEpisodesImportService.ProcessPath(outputPath, ImportMode.Auto, trackedDownload.RemoteEpisode.Series, trackedDownload.DownloadItem); + var importResults = _downloadedTracksImportService.ProcessPath(outputPath, ImportMode.Auto, trackedDownload.RemoteAlbum.Artist, trackedDownload.DownloadItem); if (importResults.Empty()) { @@ -119,7 +111,7 @@ namespace NzbDrone.Core.Download return; } - if (importResults.Count(c => c.Result == ImportResultType.Imported) >= Math.Max(1, trackedDownload.RemoteEpisode.Episodes.Count)) + if (importResults.Count(c => c.Result == ImportResultType.Imported) >= Math.Max(1, trackedDownload.RemoteAlbum.Albums.Count)) { trackedDownload.State = TrackedDownloadStage.Imported; _eventAggregator.PublishEvent(new DownloadCompletedEvent(trackedDownload)); diff --git a/src/NzbDrone.Core/Download/DownloadClientBase.cs b/src/NzbDrone.Core/Download/DownloadClientBase.cs index e557f735e..e2b3f7d10 100644 --- a/src/NzbDrone.Core/Download/DownloadClientBase.cs +++ b/src/NzbDrone.Core/Download/DownloadClientBase.cs @@ -60,7 +60,7 @@ namespace NzbDrone.Core.Download public abstract string Download(RemoteAlbum remoteAlbum); public abstract IEnumerable GetItems(); public abstract void RemoveItem(string downloadId, bool deleteData); - public abstract DownloadClientStatus GetStatus(); + public abstract DownloadClientInfo GetStatus(); protected virtual void DeleteItemData(string downloadId) { diff --git a/src/NzbDrone.Core/Download/DownloadClientFactory.cs b/src/NzbDrone.Core/Download/DownloadClientFactory.cs index dc0f218b5..909b48ed6 100644 --- a/src/NzbDrone.Core/Download/DownloadClientFactory.cs +++ b/src/NzbDrone.Core/Download/DownloadClientFactory.cs @@ -1,5 +1,7 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Linq; +using FluentValidation.Results; using NLog; using NzbDrone.Common.Composition; using NzbDrone.Core.Messaging.Events; @@ -9,17 +11,24 @@ namespace NzbDrone.Core.Download { public interface IDownloadClientFactory : IProviderFactory { - + List DownloadHandlingEnabled(bool filterBlockedClients = true); } public class DownloadClientFactory : ProviderFactory, IDownloadClientFactory { - private readonly IDownloadClientRepository _providerRepository; + private readonly IDownloadClientStatusService _downloadClientStatusService; + private readonly Logger _logger; - public DownloadClientFactory(IDownloadClientRepository providerRepository, IEnumerable providers, IContainer container, IEventAggregator eventAggregator, Logger logger) + public DownloadClientFactory(IDownloadClientStatusService downloadClientStatusService, + IDownloadClientRepository providerRepository, + IEnumerable providers, + IContainer container, + IEventAggregator eventAggregator, + Logger logger) : base(providerRepository, providers, container, eventAggregator, logger) { - _providerRepository = providerRepository; + _downloadClientStatusService = downloadClientStatusService; + _logger = logger; } protected override List Active() @@ -33,5 +42,46 @@ namespace NzbDrone.Core.Download definition.Protocol = provider.Protocol; } + + public List DownloadHandlingEnabled(bool filterBlockedClients = true) + { + var enabledClients = GetAvailableProviders(); + + if (filterBlockedClients) + { + return FilterBlockedClients(enabledClients).ToList(); + } + + return enabledClients.ToList(); + } + + private IEnumerable FilterBlockedClients(IEnumerable clients) + { + var blockedIndexers = _downloadClientStatusService.GetBlockedProviders().ToDictionary(v => v.ProviderId, v => v); + + foreach (var client in clients) + { + DownloadClientStatus downloadClientStatus; + if (blockedIndexers.TryGetValue(client.Definition.Id, out downloadClientStatus)) + { + _logger.Debug("Temporarily ignoring download client {0} till {1} due to recent failures.", client.Definition.Name, downloadClientStatus.DisabledTill.Value.ToLocalTime()); + continue; + } + + yield return client; + } + } + + public override ValidationResult Test(DownloadClientDefinition definition) + { + var result = base.Test(definition); + + if ((result == null || result.IsValid) && definition.Id != 0) + { + _downloadClientStatusService.RecordSuccess(definition.Id); + } + + return result; + } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Core/Download/DownloadClientInfo.cs b/src/NzbDrone.Core/Download/DownloadClientInfo.cs new file mode 100644 index 000000000..cf586ab64 --- /dev/null +++ b/src/NzbDrone.Core/Download/DownloadClientInfo.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; +using NzbDrone.Common.Disk; + +namespace NzbDrone.Core.Download +{ + public class DownloadClientInfo + { + public bool IsLocalhost { get; set; } + public List OutputRootFolders { get; set; } + } +} diff --git a/src/NzbDrone.Core/Download/DownloadClientItem.cs b/src/NzbDrone.Core/Download/DownloadClientItem.cs index 2e0533e50..eab9e431e 100644 --- a/src/NzbDrone.Core/Download/DownloadClientItem.cs +++ b/src/NzbDrone.Core/Download/DownloadClientItem.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Diagnostics; using NzbDrone.Common.Disk; @@ -21,7 +21,9 @@ namespace NzbDrone.Core.Download public DownloadItemStatus Status { get; set; } public bool IsEncrypted { get; set; } - public bool IsReadOnly { get; set; } + + public bool CanMoveFiles { get; set; } + public bool CanBeRemoved { get; set; } public bool Removed { get; set; } } diff --git a/src/NzbDrone.Core/Download/DownloadClientProvider.cs b/src/NzbDrone.Core/Download/DownloadClientProvider.cs index 5cb899806..7ed7cd5b9 100644 --- a/src/NzbDrone.Core/Download/DownloadClientProvider.cs +++ b/src/NzbDrone.Core/Download/DownloadClientProvider.cs @@ -1,4 +1,4 @@ -using System.Linq; +using System.Linq; using System.Collections.Generic; using NzbDrone.Core.Indexers; @@ -27,19 +27,12 @@ namespace NzbDrone.Core.Download public IEnumerable GetDownloadClients() { - return _downloadClientFactory.GetAvailableProviders();//.Select(MapDownloadClient); + return _downloadClientFactory.GetAvailableProviders(); } public IDownloadClient Get(int id) { return _downloadClientFactory.GetAvailableProviders().Single(d => d.Definition.Id == id); } - - public IDownloadClient MapDownloadClient(IDownloadClient downloadClient) - { - _downloadClientFactory.SetProviderCharacteristics(downloadClient, (DownloadClientDefinition)downloadClient.Definition); - - return downloadClient; - } } } diff --git a/src/NzbDrone.Core/Download/DownloadClientStatus.cs b/src/NzbDrone.Core/Download/DownloadClientStatus.cs index a092fd8de..f4d819424 100644 --- a/src/NzbDrone.Core/Download/DownloadClientStatus.cs +++ b/src/NzbDrone.Core/Download/DownloadClientStatus.cs @@ -1,11 +1,9 @@ -using System.Collections.Generic; -using NzbDrone.Common.Disk; +using NzbDrone.Core.ThingiProvider.Status; namespace NzbDrone.Core.Download { - public class DownloadClientStatus + public class DownloadClientStatus : ProviderStatusBase { - public bool IsLocalhost { get; set; } - public List OutputRootFolders { get; set; } + } } diff --git a/src/NzbDrone.Core/Download/DownloadClientStatusRepository.cs b/src/NzbDrone.Core/Download/DownloadClientStatusRepository.cs new file mode 100644 index 000000000..ac6cfc0b9 --- /dev/null +++ b/src/NzbDrone.Core/Download/DownloadClientStatusRepository.cs @@ -0,0 +1,19 @@ +using NzbDrone.Core.Datastore; +using NzbDrone.Core.Messaging.Events; +using NzbDrone.Core.ThingiProvider.Status; + +namespace NzbDrone.Core.Download +{ + public interface IDownloadClientStatusRepository : IProviderStatusRepository + { + + } + + public class DownloadClientStatusRepository : ProviderStatusRepository, IDownloadClientStatusRepository + { + public DownloadClientStatusRepository(IMainDatabase database, IEventAggregator eventAggregator) + : base(database, eventAggregator) + { + } + } +} diff --git a/src/NzbDrone.Core/Download/DownloadClientStatusService.cs b/src/NzbDrone.Core/Download/DownloadClientStatusService.cs new file mode 100644 index 000000000..ba3360dcf --- /dev/null +++ b/src/NzbDrone.Core/Download/DownloadClientStatusService.cs @@ -0,0 +1,22 @@ +using System; +using NLog; +using NzbDrone.Core.Messaging.Events; +using NzbDrone.Core.ThingiProvider.Status; + +namespace NzbDrone.Core.Download +{ + public interface IDownloadClientStatusService : IProviderStatusServiceBase + { + + } + + public class DownloadClientStatusService : ProviderStatusServiceBase, IDownloadClientStatusService + { + public DownloadClientStatusService(IDownloadClientStatusRepository providerStatusRepository, IEventAggregator eventAggregator, Logger logger) + : base(providerStatusRepository, eventAggregator, logger) + { + MinimumTimeSinceInitialFailure = TimeSpan.FromMinutes(5); + MaximumEscalationLevel = 5; + } + } +} diff --git a/src/NzbDrone.Core/Download/DownloadEventHub.cs b/src/NzbDrone.Core/Download/DownloadEventHub.cs index f738f5c2e..e0e3bd56e 100644 --- a/src/NzbDrone.Core/Download/DownloadEventHub.cs +++ b/src/NzbDrone.Core/Download/DownloadEventHub.cs @@ -1,4 +1,4 @@ -using System; +using System; using NLog; using NzbDrone.Common.Messaging; using NzbDrone.Core.Configuration; @@ -37,7 +37,7 @@ namespace NzbDrone.Core.Download { if (!_configService.RemoveCompletedDownloads || message.TrackedDownload.DownloadItem.Removed || - message.TrackedDownload.DownloadItem.IsReadOnly || + !message.TrackedDownload.DownloadItem.CanBeRemoved || message.TrackedDownload.DownloadItem.Status == DownloadItemStatus.Downloading) { return; @@ -50,7 +50,7 @@ namespace NzbDrone.Core.Download { var trackedDownload = message.TrackedDownload; - if (trackedDownload == null || trackedDownload.DownloadItem.IsReadOnly || _configService.RemoveFailedDownloads == false) + if (trackedDownload == null || !trackedDownload.DownloadItem.CanBeRemoved || _configService.RemoveFailedDownloads == false) { return; } @@ -78,4 +78,4 @@ namespace NzbDrone.Core.Download } } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Core/Download/DownloadFailedEvent.cs b/src/NzbDrone.Core/Download/DownloadFailedEvent.cs index 680000d8f..c22dd9548 100644 --- a/src/NzbDrone.Core/Download/DownloadFailedEvent.cs +++ b/src/NzbDrone.Core/Download/DownloadFailedEvent.cs @@ -1,7 +1,8 @@ -using System.Collections.Generic; +using System.Collections.Generic; using NzbDrone.Common.Messaging; using NzbDrone.Core.Download.TrackedDownloads; using NzbDrone.Core.Qualities; +using NzbDrone.Core.Languages; namespace NzbDrone.Core.Download { @@ -21,5 +22,6 @@ namespace NzbDrone.Core.Download public string Message { get; set; } public Dictionary Data { get; set; } public TrackedDownload TrackedDownload { get; set; } + public Language Language { get; set; } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Core/Download/DownloadService.cs b/src/NzbDrone.Core/Download/DownloadService.cs index c4f1e155e..450836a4a 100644 --- a/src/NzbDrone.Core/Download/DownloadService.cs +++ b/src/NzbDrone.Core/Download/DownloadService.cs @@ -1,10 +1,11 @@ -using System; +using System; using NLog; using NzbDrone.Common.EnsureThat; using NzbDrone.Common.Extensions; using NzbDrone.Common.Http; using NzbDrone.Common.Instrumentation.Extensions; using NzbDrone.Common.TPL; +using NzbDrone.Core.Download.Clients; using NzbDrone.Core.Exceptions; using NzbDrone.Core.Indexers; using NzbDrone.Core.Messaging.Events; @@ -20,18 +21,21 @@ namespace NzbDrone.Core.Download public class DownloadService : IDownloadService { private readonly IProvideDownloadClient _downloadClientProvider; + private readonly IDownloadClientStatusService _downloadClientStatusService; private readonly IIndexerStatusService _indexerStatusService; private readonly IRateLimitService _rateLimitService; private readonly IEventAggregator _eventAggregator; private readonly Logger _logger; public DownloadService(IProvideDownloadClient downloadClientProvider, - IIndexerStatusService indexerStatusService, - IRateLimitService rateLimitService, - IEventAggregator eventAggregator, - Logger logger) + IDownloadClientStatusService downloadClientStatusService, + IIndexerStatusService indexerStatusService, + IRateLimitService rateLimitService, + IEventAggregator eventAggregator, + Logger logger) { _downloadClientProvider = downloadClientProvider; + _downloadClientStatusService = downloadClientStatusService; _indexerStatusService = indexerStatusService; _rateLimitService = rateLimitService; _eventAggregator = eventAggregator; @@ -48,8 +52,12 @@ namespace NzbDrone.Core.Download if (downloadClient == null) { - _logger.Warn("{0} Download client isn't configured yet.", remoteAlbum.Release.DownloadProtocol); - return; + throw new DownloadClientUnavailableException($"{remoteAlbum.Release.DownloadProtocol} Download client isn't configured yet"); + } + + if (_downloadClientStatusService.IsDisabled(downloadClient.Definition.Id)) + { + throw new DownloadClientUnavailableException($"{downloadClient.Name} is disabled due to recent failues"); } // Limit grabs to 2 per second. @@ -63,6 +71,7 @@ namespace NzbDrone.Core.Download try { downloadClientId = downloadClient.Download(remoteAlbum); + _downloadClientStatusService.RecordSuccess(downloadClient.Definition.Id); _indexerStatusService.RecordSuccess(remoteAlbum.Release.IndexerId); } catch (ReleaseDownloadException ex) @@ -91,4 +100,4 @@ namespace NzbDrone.Core.Download _eventAggregator.PublishEvent(albumGrabbedEvent); } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Core/Download/EpisodeGrabbedEvent.cs b/src/NzbDrone.Core/Download/EpisodeGrabbedEvent.cs deleted file mode 100644 index b7861b8d7..000000000 --- a/src/NzbDrone.Core/Download/EpisodeGrabbedEvent.cs +++ /dev/null @@ -1,17 +0,0 @@ -using NzbDrone.Common.Messaging; -using NzbDrone.Core.Parser.Model; - -namespace NzbDrone.Core.Download -{ - public class EpisodeGrabbedEvent : IEvent - { - public RemoteEpisode Episode { get; private set; } - public string DownloadClient { get; set; } - public string DownloadId { get; set; } - - public EpisodeGrabbedEvent(RemoteEpisode episode) - { - Episode = episode; - } - } -} \ No newline at end of file diff --git a/src/NzbDrone.Core/Download/FailedDownloadService.cs b/src/NzbDrone.Core/Download/FailedDownloadService.cs index 935401864..bc2fec122 100644 --- a/src/NzbDrone.Core/Download/FailedDownloadService.cs +++ b/src/NzbDrone.Core/Download/FailedDownloadService.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using NzbDrone.Common.Extensions; using NzbDrone.Core.Download.TrackedDownloads; @@ -95,7 +95,8 @@ namespace NzbDrone.Core.Download DownloadId = historyItem.DownloadId, Message = message, Data = historyItem.Data, - TrackedDownload = trackedDownload + TrackedDownload = trackedDownload, + Language = historyItem.Language }; _eventAggregator.PublishEvent(downloadFailedEvent); diff --git a/src/NzbDrone.Core/Download/IDownloadClient.cs b/src/NzbDrone.Core/Download/IDownloadClient.cs index b16555dcf..cf0d1e419 100644 --- a/src/NzbDrone.Core/Download/IDownloadClient.cs +++ b/src/NzbDrone.Core/Download/IDownloadClient.cs @@ -12,6 +12,6 @@ namespace NzbDrone.Core.Download string Download(RemoteAlbum remoteAlbum); IEnumerable GetItems(); void RemoveItem(string downloadId, bool deleteData); - DownloadClientStatus GetStatus(); + DownloadClientInfo GetStatus(); } } diff --git a/src/NzbDrone.Core/Download/Pending/PendingRelease.cs b/src/NzbDrone.Core/Download/Pending/PendingRelease.cs index 976d9ae38..a9273ec2e 100644 --- a/src/NzbDrone.Core/Download/Pending/PendingRelease.cs +++ b/src/NzbDrone.Core/Download/Pending/PendingRelease.cs @@ -1,4 +1,4 @@ -using System; +using System; using NzbDrone.Core.Datastore; using NzbDrone.Core.Parser.Model; @@ -11,9 +11,9 @@ namespace NzbDrone.Core.Download.Pending public DateTime Added { get; set; } public ParsedAlbumInfo ParsedAlbumInfo { get; set; } public ReleaseInfo Release { get; set; } + public PendingReleaseReason Reason { get; set; } //Not persisted - public RemoteEpisode RemoteEpisode { get; set; } public RemoteAlbum RemoteAlbum { get; set; } } } diff --git a/src/NzbDrone.Core/Download/Pending/PendingReleaseReason.cs b/src/NzbDrone.Core/Download/Pending/PendingReleaseReason.cs new file mode 100644 index 000000000..ba83714b7 --- /dev/null +++ b/src/NzbDrone.Core/Download/Pending/PendingReleaseReason.cs @@ -0,0 +1,9 @@ +namespace NzbDrone.Core.Download.Pending +{ + public enum PendingReleaseReason + { + Delay = 0, + DownloadClientUnavailable = 1, + Fallback = 2 + } +} diff --git a/src/NzbDrone.Core/Download/Pending/PendingReleaseService.cs b/src/NzbDrone.Core/Download/Pending/PendingReleaseService.cs index d978b759d..65cefcc13 100644 --- a/src/NzbDrone.Core/Download/Pending/PendingReleaseService.cs +++ b/src/NzbDrone.Core/Download/Pending/PendingReleaseService.cs @@ -1,4 +1,4 @@ -using NLog; +using NLog; using NzbDrone.Common.Crypto; using NzbDrone.Common.Extensions; using NzbDrone.Core.Configuration; @@ -20,8 +20,7 @@ namespace NzbDrone.Core.Download.Pending { public interface IPendingReleaseService { - void Add(DownloadDecision decision); - + void Add(DownloadDecision decision, PendingReleaseReason reason); List GetPending(); List GetPendingRemoteAlbums(int artistId); List GetPendingQueue(); @@ -67,7 +66,7 @@ namespace NzbDrone.Core.Download.Pending } - public void Add(DownloadDecision decision) + public void Add(DownloadDecision decision, PendingReleaseReason reason) { var alreadyPending = GetPendingReleases(); @@ -77,14 +76,29 @@ namespace NzbDrone.Core.Download.Pending .Intersect(albumIds) .Any()); - if (existingReports.Any(MatchingReleasePredicate(decision.RemoteAlbum.Release))) + var matchingReports = existingReports.Where(MatchingReleasePredicate(decision.RemoteAlbum.Release)).ToList(); + + if (matchingReports.Any()) { - _logger.Debug("This release is already pending, not adding again"); - return; + var sameReason = true; + + foreach (var matchingReport in matchingReports) + { + if (matchingReport.Reason != reason) + { + _logger.Debug("The release {0} is already pending with reason {1}, changing to {2}", decision.RemoteAlbum, matchingReport.Reason, reason); matchingReport.Reason = reason; + _repository.Update(matchingReport); + sameReason = false; + } + } + + if (sameReason) + { + _logger.Debug("The release {0} is already pending with reason {1}, not adding again", decision.RemoteAlbum, reason); return; + } } - _logger.Debug("Adding release to pending releases"); - Insert(decision); + _logger.Debug("Adding release {0} to pending releases with reason {1}", decision.RemoteAlbum, reason); Insert(decision, reason); } public List GetPending() @@ -101,7 +115,7 @@ namespace NzbDrone.Core.Download.Pending private List FilterBlockedIndexers(List releases) { - var blockedIndexers = new HashSet(_indexerStatusService.GetBlockedIndexers().Select(v => v.IndexerId)); + var blockedIndexers = new HashSet(_indexerStatusService.GetBlockedProviders().Select(v => v.ProviderId)); return releases.Where(release => !blockedIndexers.Contains(release.IndexerId)).ToList(); } @@ -117,7 +131,7 @@ namespace NzbDrone.Core.Download.Pending var nextRssSync = new Lazy(() => _taskManager.GetNextExecution(typeof(RssSyncCommand))); - foreach (var pendingRelease in GetPendingReleases()) + foreach (var pendingRelease in GetPendingReleases().Where(p => p.Reason != PendingReleaseReason.Fallback)) { foreach (var album in pendingRelease.RemoteAlbum.Albums) { @@ -132,6 +146,13 @@ namespace NzbDrone.Core.Download.Pending ect = ect.AddMinutes(_configService.RssSyncInterval); } + var timeleft = ect.Subtract(DateTime.UtcNow); + + if (timeleft.TotalSeconds < 0) + { + timeleft = TimeSpan.Zero; + } + var queue = new Queue.Queue { Id = GetQueueId(pendingRelease, album), @@ -142,11 +163,13 @@ namespace NzbDrone.Core.Download.Pending Size = pendingRelease.RemoteAlbum.Release.Size, Sizeleft = pendingRelease.RemoteAlbum.Release.Size, RemoteAlbum = pendingRelease.RemoteAlbum, - Timeleft = ect.Subtract(DateTime.UtcNow), + Timeleft = timeleft, EstimatedCompletionTime = ect, - Status = "Pending", - Protocol = pendingRelease.RemoteAlbum.Release.DownloadProtocol - }; + Status = pendingRelease.Reason.ToString(), + Protocol = pendingRelease.RemoteAlbum.Release.DownloadProtocol, + Indexer = pendingRelease.RemoteAlbum.Release.Indexer + }; + queued.Add(queue); } } @@ -229,7 +252,7 @@ namespace NzbDrone.Core.Download.Pending }; } - private void Insert(DownloadDecision decision) + private void Insert(DownloadDecision decision, PendingReleaseReason reason) { _repository.Insert(new PendingRelease { @@ -237,7 +260,8 @@ namespace NzbDrone.Core.Download.Pending ParsedAlbumInfo = decision.RemoteAlbum.ParsedAlbumInfo, Release = decision.RemoteAlbum.Release, Title = decision.RemoteAlbum.Release.Title, - Added = DateTime.UtcNow + Added = DateTime.UtcNow, + Reason = reason }); _eventAggregator.PublishEvent(new PendingReleasesUpdatedEvent()); diff --git a/src/NzbDrone.Core/Download/ProcessDownloadDecisions.cs b/src/NzbDrone.Core/Download/ProcessDownloadDecisions.cs index 1c82f3253..d2338732c 100644 --- a/src/NzbDrone.Core/Download/ProcessDownloadDecisions.cs +++ b/src/NzbDrone.Core/Download/ProcessDownloadDecisions.cs @@ -1,9 +1,12 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; +using System.Net; using NLog; using NzbDrone.Core.DecisionEngine; +using NzbDrone.Core.Download.Clients; using NzbDrone.Core.Download.Pending; +using NzbDrone.Core.Indexers; namespace NzbDrone.Core.Download { @@ -36,36 +39,33 @@ namespace NzbDrone.Core.Download var prioritizedDecisions = _prioritizeDownloadDecision.PrioritizeDecisions(qualifiedReports); var grabbed = new List(); var pending = new List(); + var failed = new List(); + + var usenetFailed = false; + var torrentFailed = false; foreach (var report in prioritizedDecisions) { var remoteAlbum = report.RemoteAlbum; - - var albumIds = remoteAlbum.Albums.Select(e => e.Id).ToList(); + var downloadProtocol = report.RemoteAlbum.Release.DownloadProtocol; //Skip if already grabbed - if (grabbed.SelectMany(r => r.RemoteAlbum.Albums) - .Select(e => e.Id) - .ToList() - .Intersect(albumIds) - .Any()) + if (IsAlbumProcessed(grabbed, report)) { continue; } if (report.TemporarilyRejected) { - _pendingReleaseService.Add(report); + _pendingReleaseService.Add(report, PendingReleaseReason.Delay); pending.Add(report); continue; } - if (pending.SelectMany(r => r.RemoteAlbum.Albums) - .Select(e => e.Id) - .ToList() - .Intersect(albumIds) - .Any()) + if (downloadProtocol == DownloadProtocol.Usenet && usenetFailed || + downloadProtocol == DownloadProtocol.Torrent && torrentFailed) { + failed.Add(report); continue; } @@ -74,14 +74,31 @@ namespace NzbDrone.Core.Download _downloadService.DownloadReport(remoteAlbum); grabbed.Add(report); } - catch (Exception e) + catch (Exception ex) { - //TODO: support for store & forward - //We'll need to differentiate between a download client error and an indexer error - _logger.Warn(e, "Couldn't add report to download queue. " + remoteAlbum); + if (ex is DownloadClientUnavailableException || ex is DownloadClientAuthenticationException) + { + _logger.Debug(ex, "Failed to send release to download client, storing until later. " + remoteAlbum); + failed.Add(report); + + if (downloadProtocol == DownloadProtocol.Usenet) + { + usenetFailed = true; + } + else if (downloadProtocol == DownloadProtocol.Torrent) + { + torrentFailed = true; + } + } + else + { + _logger.Warn(ex, "Couldn't add report to download queue. " + remoteAlbum); + } } } + pending.AddRange(ProcessFailedGrabs(grabbed, failed)); + return new ProcessedDecisions(grabbed, pending, decisions.Where(d => d.Rejected).ToList()); } @@ -90,5 +107,50 @@ namespace NzbDrone.Core.Download //Process both approved and temporarily rejected return decisions.Where(c => (c.Approved || c.TemporarilyRejected) && c.RemoteAlbum.Albums.Any()).ToList(); } + + private bool IsAlbumProcessed(List decisions, DownloadDecision report) + { + var albumIds = report.RemoteAlbum.Albums.Select(e => e.Id).ToList(); + + return decisions.SelectMany(r => r.RemoteAlbum.Albums) + .Select(e => e.Id) + .ToList() + .Intersect(albumIds) + .Any(); + } + + private List ProcessFailedGrabs(List grabbed, List failed) + { + var pending = new List(); + var stored = new List(); + + foreach (var report in failed) + { + // If a release was already grabbed with matching albums we should store it as a fallback + // and filter it out the next time it is processed incase a higher quality release failed to + // add to the download client, but a lower quality release was sent to another client + // If the release wasn't grabbed already, but was already stored, store it as a fallback, + // otherwise store it as DownloadClientUnavailable. + + if (IsAlbumProcessed(grabbed, report)) + { + _pendingReleaseService.Add(report, PendingReleaseReason.Fallback); + pending.Add(report); + } + else if (IsAlbumProcessed(stored, report)) + { + _pendingReleaseService.Add(report, PendingReleaseReason.Fallback); + pending.Add(report); + } + else + { + _pendingReleaseService.Add(report, PendingReleaseReason.DownloadClientUnavailable); + pending.Add(report); + stored.Add(report); + } + } + + return pending; + } } } diff --git a/src/NzbDrone.Core/Download/TrackedDownloads/DownloadMonitoringService.cs b/src/NzbDrone.Core/Download/TrackedDownloads/DownloadMonitoringService.cs index a760b71fe..b1c27d869 100644 --- a/src/NzbDrone.Core/Download/TrackedDownloads/DownloadMonitoringService.cs +++ b/src/NzbDrone.Core/Download/TrackedDownloads/DownloadMonitoringService.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using NLog; @@ -13,9 +13,12 @@ namespace NzbDrone.Core.Download.TrackedDownloads { public class DownloadMonitoringService : IExecute, IHandle, - IHandle + IHandle, + IHandle + { - private readonly IProvideDownloadClient _downloadClientProvider; + private readonly IDownloadClientStatusService _downloadClientStatusService; + private readonly IDownloadClientFactory _downloadClientFactory; private readonly IEventAggregator _eventAggregator; private readonly IManageCommandQueue _manageCommandQueue; private readonly IConfigService _configService; @@ -25,16 +28,18 @@ namespace NzbDrone.Core.Download.TrackedDownloads private readonly Logger _logger; private readonly Debouncer _refreshDebounce; - public DownloadMonitoringService(IProvideDownloadClient downloadClientProvider, - IEventAggregator eventAggregator, - IManageCommandQueue manageCommandQueue, - IConfigService configService, - IFailedDownloadService failedDownloadService, - ICompletedDownloadService completedDownloadService, - ITrackedDownloadService trackedDownloadService, - Logger logger) + public DownloadMonitoringService(IDownloadClientStatusService downloadClientStatusService, + IDownloadClientFactory downloadClientFactory, + IEventAggregator eventAggregator, + IManageCommandQueue manageCommandQueue, + IConfigService configService, + IFailedDownloadService failedDownloadService, + ICompletedDownloadService completedDownloadService, + ITrackedDownloadService trackedDownloadService, + Logger logger) { - _downloadClientProvider = downloadClientProvider; + _downloadClientStatusService = downloadClientStatusService; + _downloadClientFactory = downloadClientFactory; _eventAggregator = eventAggregator; _manageCommandQueue = manageCommandQueue; _configService = configService; @@ -56,7 +61,7 @@ namespace NzbDrone.Core.Download.TrackedDownloads _refreshDebounce.Pause(); try { - var downloadClients = _downloadClientProvider.GetDownloadClients(); + var downloadClients = _downloadClientFactory.DownloadHandlingEnabled(); var trackedDownloads = new List(); @@ -64,10 +69,10 @@ namespace NzbDrone.Core.Download.TrackedDownloads { var clientTrackedDownloads = ProcessClientDownloads(downloadClient); - // Only track completed downloads if trackedDownloads.AddRange(clientTrackedDownloads.Where(DownloadIsTrackable)); } + _trackedDownloadService.UpdateTrackable(trackedDownloads); _eventAggregator.PublishEvent(new TrackedDownloadRefreshedEvent(trackedDownloads)); } finally @@ -84,9 +89,12 @@ namespace NzbDrone.Core.Download.TrackedDownloads try { downloadClientHistory = downloadClient.GetItems().ToList(); + + _downloadClientStatusService.RecordSuccess(downloadClient.Definition.Id); } catch (Exception ex) { + _downloadClientStatusService.RecordFailure(downloadClient.Definition.Id); _logger.Warn(ex, "Unable to retrieve queue and history items from " + downloadClient.Definition.Name); } @@ -107,7 +115,7 @@ namespace NzbDrone.Core.Download.TrackedDownloads private void RemoveCompletedDownloads(List trackedDownloads) { - foreach (var trackedDownload in trackedDownloads.Where(c => !c.DownloadItem.IsReadOnly && c.State == TrackedDownloadStage.Imported)) + foreach (var trackedDownload in trackedDownloads.Where(c => c.DownloadItem.CanBeRemoved && c.State == TrackedDownloadStage.Imported)) { _eventAggregator.PublishEvent(new DownloadCompletedEvent(trackedDownload)); } @@ -168,9 +176,16 @@ namespace NzbDrone.Core.Download.TrackedDownloads _refreshDebounce.Execute(); } - public void Handle(EpisodeImportedEvent message) + public void Handle(TrackImportedEvent message) { _refreshDebounce.Execute(); } + + public void Handle(TrackedDownloadsRemovedEvent message) + { + var trackedDownloads = _trackedDownloadService.GetTrackedDownloads().Where(t => t.IsTrackable && DownloadIsTrackable(t)).ToList(); + + _eventAggregator.PublishEvent(new TrackedDownloadRefreshedEvent(trackedDownloads)); + } } } diff --git a/src/NzbDrone.Core/Download/TrackedDownloads/TrackedDownload.cs b/src/NzbDrone.Core/Download/TrackedDownloads/TrackedDownload.cs index ac9ec462f..560ed0e01 100644 --- a/src/NzbDrone.Core/Download/TrackedDownloads/TrackedDownload.cs +++ b/src/NzbDrone.Core/Download/TrackedDownloads/TrackedDownload.cs @@ -1,4 +1,4 @@ -using NzbDrone.Core.Indexers; +using NzbDrone.Core.Indexers; using NzbDrone.Core.Parser.Model; namespace NzbDrone.Core.Download.TrackedDownloads @@ -9,10 +9,11 @@ namespace NzbDrone.Core.Download.TrackedDownloads public DownloadClientItem DownloadItem { get; set; } public TrackedDownloadStage State { get; set; } public TrackedDownloadStatus Status { get; private set; } - public RemoteEpisode RemoteEpisode { get; set; } public RemoteAlbum RemoteAlbum { get; set; } public TrackedDownloadStatusMessage[] StatusMessages { get; private set; } public DownloadProtocol Protocol { get; set; } + public string Indexer { get; set; } + public bool IsTrackable { get; set; } public TrackedDownload() { diff --git a/src/NzbDrone.Core/Download/TrackedDownloads/TrackedDownloadService.cs b/src/NzbDrone.Core/Download/TrackedDownloads/TrackedDownloadService.cs index b1e8189d4..434b67166 100644 --- a/src/NzbDrone.Core/Download/TrackedDownloads/TrackedDownloadService.cs +++ b/src/NzbDrone.Core/Download/TrackedDownloads/TrackedDownloadService.cs @@ -1,9 +1,11 @@ -using System; +using System; +using System.Collections.Generic; using System.Linq; using NLog; using NzbDrone.Common.Cache; using NzbDrone.Common.Extensions; using NzbDrone.Core.History; +using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Parser; namespace NzbDrone.Core.Download.TrackedDownloads @@ -11,23 +13,30 @@ namespace NzbDrone.Core.Download.TrackedDownloads public interface ITrackedDownloadService { TrackedDownload Find(string downloadId); + void StopTracking(string downloadId); + void StopTracking(List downloadIds); TrackedDownload TrackDownload(DownloadClientDefinition downloadClient, DownloadClientItem downloadItem); + List GetTrackedDownloads(); + void UpdateTrackable(List trackedDownloads); } public class TrackedDownloadService : ITrackedDownloadService { private readonly IParsingService _parsingService; private readonly IHistoryService _historyService; + private readonly IEventAggregator _eventAggregator; private readonly Logger _logger; private readonly ICached _cache; public TrackedDownloadService(IParsingService parsingService, - ICacheManager cacheManager, - IHistoryService historyService, - Logger logger) + ICacheManager cacheManager, + IHistoryService historyService, + IEventAggregator eventAggregator, + Logger logger) { _parsingService = parsingService; _historyService = historyService; + _eventAggregator = eventAggregator; _cache = cacheManager.GetCache(GetType()); _logger = logger; } @@ -37,13 +46,39 @@ namespace NzbDrone.Core.Download.TrackedDownloads return _cache.Find(downloadId); } + public void StopTracking(string downloadId) + { + var trackedDownload = _cache.Find(downloadId); + + _cache.Remove(downloadId); + _eventAggregator.PublishEvent(new TrackedDownloadsRemovedEvent(new List { trackedDownload })); + } + + public void StopTracking(List downloadIds) + { + var trackedDownloads = new List(); + + foreach (var downloadId in downloadIds) + { + var trackedDownload = _cache.Find(downloadId); + _cache.Remove(downloadId); + trackedDownloads.Add(trackedDownload); + } + + _eventAggregator.PublishEvent(new TrackedDownloadsRemovedEvent(trackedDownloads)); + } + public TrackedDownload TrackDownload(DownloadClientDefinition downloadClient, DownloadClientItem downloadItem) { var existingItem = Find(downloadItem.DownloadId); if (existingItem != null && existingItem.State != TrackedDownloadStage.Downloading) { + LogItemChange(existingItem, existingItem.DownloadItem, downloadItem); + existingItem.DownloadItem = downloadItem; + existingItem.IsTrackable = true; + return existingItem; } @@ -51,7 +86,8 @@ namespace NzbDrone.Core.Download.TrackedDownloads { DownloadClient = downloadClient.Id, DownloadItem = downloadItem, - Protocol = downloadClient.Protocol + Protocol = downloadClient.Protocol, + IsTrackable = true }; try @@ -69,6 +105,10 @@ namespace NzbDrone.Core.Download.TrackedDownloads var firstHistoryItem = historyItems.OrderByDescending(h => h.Date).First(); trackedDownload.State = GetStateFromHistory(firstHistoryItem.EventType); + var grabbedEvent = historyItems.FirstOrDefault(v => v.EventType == HistoryEventType.Grabbed); + trackedDownload.Indexer = grabbedEvent?.Data["indexer"]; + + if (parsedAlbumInfo == null || trackedDownload.RemoteAlbum == null || trackedDownload.RemoteAlbum.Artist == null || @@ -87,6 +127,7 @@ namespace NzbDrone.Core.Download.TrackedDownloads if (trackedDownload.RemoteAlbum == null) { + _logger.Trace("No Album found for download '{0}', not tracking.", trackedDownload.DownloadItem.Title); return null; } } @@ -96,10 +137,45 @@ namespace NzbDrone.Core.Download.TrackedDownloads return null; } + LogItemChange(trackedDownload, existingItem?.DownloadItem, trackedDownload.DownloadItem); + _cache.Set(trackedDownload.DownloadItem.DownloadId, trackedDownload); return trackedDownload; } + public List GetTrackedDownloads() + { + return _cache.Values.ToList(); + } + + public void UpdateTrackable(List trackedDownloads) + { + var untrackable = GetTrackedDownloads().ExceptBy(t => t.DownloadItem.DownloadId, trackedDownloads, t => t.DownloadItem.DownloadId, StringComparer.CurrentCulture).ToList(); + + foreach (var trackedDownload in untrackable) + { + trackedDownload.IsTrackable = false; + } + } + + private void LogItemChange(TrackedDownload trackedDownload, DownloadClientItem existingItem, DownloadClientItem downloadItem) + { + if (existingItem == null || + existingItem.Status != downloadItem.Status || + existingItem.CanBeRemoved != downloadItem.CanBeRemoved || + existingItem.CanMoveFiles != downloadItem.CanMoveFiles) + { + _logger.Debug("Tracking '{0}:{1}': ClientState={2}{3} SonarrStage={4} Episode='{5}' OutputPath={6}.", + downloadItem.DownloadClient, downloadItem.Title, + downloadItem.Status, downloadItem.CanBeRemoved ? "" : + downloadItem.CanMoveFiles ? " (busy)" : " (readonly)", + trackedDownload.State, + trackedDownload.RemoteAlbum?.ParsedAlbumInfo, + downloadItem.OutputPath); + } + } + + private static TrackedDownloadStage GetStateFromHistory(HistoryEventType eventType) { switch (eventType) diff --git a/src/NzbDrone.Core/Download/TrackedDownloads/TrackedDownloadsRemovedEvent.cs b/src/NzbDrone.Core/Download/TrackedDownloads/TrackedDownloadsRemovedEvent.cs new file mode 100644 index 000000000..76c926d5a --- /dev/null +++ b/src/NzbDrone.Core/Download/TrackedDownloads/TrackedDownloadsRemovedEvent.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; +using NzbDrone.Common.Messaging; + +namespace NzbDrone.Core.Download.TrackedDownloads +{ + public class TrackedDownloadsRemovedEvent : IEvent + { + public List TrackedDownloads { get; private set; } + + public TrackedDownloadsRemovedEvent(List trackedDownloads) + { + TrackedDownloads = trackedDownloads; + } + } +} diff --git a/src/NzbDrone.Core/Extras/ExistingExtraFileService.cs b/src/NzbDrone.Core/Extras/ExistingExtraFileService.cs index 8e7716e6a..14dfb622a 100644 --- a/src/NzbDrone.Core/Extras/ExistingExtraFileService.cs +++ b/src/NzbDrone.Core/Extras/ExistingExtraFileService.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.IO; using System.Linq; using NLog; @@ -41,21 +41,19 @@ namespace NzbDrone.Core.Extras _logger.Debug("Looking for existing extra files in {0}", artist.Path); var filesOnDisk = _diskScanService.GetNonAudioFiles(artist.Path); - var possibleExtraFiles = _diskScanService.FilterFiles(artist, filesOnDisk); + var possibleExtraFiles = _diskScanService.FilterFiles(artist.Path, filesOnDisk); var filteredFiles = possibleExtraFiles; var importedFiles = new List(); foreach (var existingExtraFileImporter in _existingExtraFileImporters) { - // TODO Implement existingExtraFileImporter for Audio Files + var imported = existingExtraFileImporter.ProcessFiles(artist, filteredFiles, importedFiles); - //var imported = existingExtraFileImporter.ProcessFiles(artist, filteredFiles, importedFiles); - - //importedFiles.AddRange(imported.Select(f => Path.Combine(artist.Path, f.RelativePath))); + importedFiles.AddRange(imported.Select(f => Path.Combine(artist.Path, f.RelativePath))); } _logger.Info("Found {0} extra files", extraFiles.Count); } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Core/Extras/ExtraService.cs b/src/NzbDrone.Core/Extras/ExtraService.cs index 7447e162e..0c9851b9d 100644 --- a/src/NzbDrone.Core/Extras/ExtraService.cs +++ b/src/NzbDrone.Core/Extras/ExtraService.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.IO; using System.Linq; @@ -12,50 +12,55 @@ using NzbDrone.Core.MediaFiles; using NzbDrone.Core.MediaFiles.Events; using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Parser.Model; -using NzbDrone.Core.Tv; +using NzbDrone.Core.Music; namespace NzbDrone.Core.Extras { - // NOTE: Majora: ExtraService can be reserved for Music Videos, lyric files, etc for Plex. TODO: Implement Extras for Music public interface IExtraService { - void ImportExtraFiles(LocalEpisode localEpisode, EpisodeFile episodeFile, bool isReadOnly); + void ImportExtraFiles(LocalTrack localEpisode, TrackFile episodeFile, bool isReadOnly); } public class ExtraService : IExtraService, IHandle, - IHandle, - IHandle + IHandle, + IHandle { private readonly IMediaFileService _mediaFileService; - private readonly IEpisodeService _episodeService; + //private readonly IEpisodeService _episodeService; + private readonly IAlbumService _albumService; + private readonly ITrackService _trackService; private readonly IDiskProvider _diskProvider; private readonly IConfigService _configService; private readonly List _extraFileManagers; private readonly Logger _logger; public ExtraService(IMediaFileService mediaFileService, - IEpisodeService episodeService, + //IEpisodeService episodeService, + IAlbumService albumService, + ITrackService trackService, IDiskProvider diskProvider, IConfigService configService, List extraFileManagers, Logger logger) { _mediaFileService = mediaFileService; - _episodeService = episodeService; + //_episodeService = episodeService; + _albumService = albumService; + _trackService = trackService; _diskProvider = diskProvider; _configService = configService; _extraFileManagers = extraFileManagers.OrderBy(e => e.Order).ToList(); _logger = logger; } - public void ImportExtraFiles(LocalEpisode localEpisode, EpisodeFile episodeFile, bool isReadOnly) + public void ImportExtraFiles(LocalTrack localTrack, TrackFile trackFile, bool isReadOnly) { - var series = localEpisode.Series; + var artist = localTrack.Artist; foreach (var extraFileManager in _extraFileManagers) { - extraFileManager.CreateAfterEpisodeImport(series, episodeFile); + extraFileManager.CreateAfterTrackImport(artist, trackFile); } if (!_configService.ImportExtraFiles) @@ -63,7 +68,7 @@ namespace NzbDrone.Core.Extras return; } - var sourcePath = localEpisode.Path; + var sourcePath = localTrack.Path; var sourceFolder = _diskProvider.GetParentFolder(sourcePath); var sourceFileName = Path.GetFileNameWithoutExtension(sourcePath); var files = _diskProvider.GetFiles(sourceFolder, SearchOption.TopDirectoryOnly); @@ -72,7 +77,7 @@ namespace NzbDrone.Core.Extras .Select(e => e.Trim(' ', '.')) .ToList(); - var matchingFilenames = files.Where(f => Path.GetFileNameWithoutExtension(f).StartsWith(sourceFileName)); + var matchingFilenames = files.Where(f => Path.GetFileNameWithoutExtension(f).StartsWith(sourceFileName, StringComparison.InvariantCultureIgnoreCase)); foreach (var matchingFilename in matchingFilenames) { @@ -88,7 +93,7 @@ namespace NzbDrone.Core.Extras foreach (var extraFileManager in _extraFileManagers) { var extension = Path.GetExtension(matchingFilename); - var extraFile = extraFileManager.Import(series, episodeFile, matchingFilename, extension, isReadOnly); + var extraFile = extraFileManager.Import(artist, trackFile, matchingFilename, extension, isReadOnly); if (extraFile != null) { @@ -105,49 +110,49 @@ namespace NzbDrone.Core.Extras public void Handle(MediaCoversUpdatedEvent message) { - var series = message.Series; - var episodeFiles = GetEpisodeFiles(series.Id); + var artist = message.Artist; + var albums = _albumService.GetAlbumsByArtist(artist.Id); + var trackFiles = GetTrackFiles(artist.Id); foreach (var extraFileManager in _extraFileManagers) { - extraFileManager.CreateAfterSeriesScan(series, episodeFiles); + extraFileManager.CreateAfterArtistScan(artist, albums, trackFiles); } } - public void Handle(EpisodeFolderCreatedEvent message) + public void Handle(TrackFolderCreatedEvent message) { - var series = message.Series; + var artist = message.Artist; foreach (var extraFileManager in _extraFileManagers) { - extraFileManager.CreateAfterEpisodeImport(series, message.SeriesFolder, message.SeasonFolder); + extraFileManager.CreateAfterTrackImport(artist, message.ArtistFolder, message.AlbumFolder); } } - public void Handle(SeriesRenamedEvent message) + public void Handle(ArtistRenamedEvent message) { - var series = message.Series; - var episodeFiles = GetEpisodeFiles(series.Id); + var artist = message.Artist; + var trackFiles = GetTrackFiles(artist.Id); foreach (var extraFileManager in _extraFileManagers) { - extraFileManager.MoveFilesAfterRename(series, episodeFiles); + extraFileManager.MoveFilesAfterRename(artist, trackFiles); } } - private List GetEpisodeFiles(int seriesId) + private List GetTrackFiles(int artistId) { - //var episodeFiles = _mediaFileService.GetFilesBySeries(seriesId); - //var episodes = _episodeService.GetEpisodeBySeries(seriesId); + var trackFiles = _mediaFileService.GetFilesByArtist(artistId); + var tracks = _trackService.GetTracksByArtist(artistId); - //foreach (var episodeFile in episodeFiles) - //{ - // var localEpisodeFile = episodeFile; - // episodeFile.Episodes = new LazyList(episodes.Where(e => e.EpisodeFileId == localEpisodeFile.Id)); - //} + foreach (var trackFile in trackFiles) + { + var localTrackFile = trackFile; + trackFile.Tracks = new LazyList(tracks.Where(e => e.TrackFileId == localTrackFile.Id)); + } - //return episodeFiles; - return new List(); + return trackFiles; } } } diff --git a/src/NzbDrone.Core/Extras/Files/ExtraFile.cs b/src/NzbDrone.Core/Extras/Files/ExtraFile.cs index 036eaec33..1e3c2b8bf 100644 --- a/src/NzbDrone.Core/Extras/Files/ExtraFile.cs +++ b/src/NzbDrone.Core/Extras/Files/ExtraFile.cs @@ -1,13 +1,13 @@ -using System; +using System; using NzbDrone.Core.Datastore; namespace NzbDrone.Core.Extras.Files { public abstract class ExtraFile : ModelBase { - public int SeriesId { get; set; } - public int? EpisodeFileId { get; set; } - public int? SeasonNumber { get; set; } + public int ArtistId { get; set; } + public int? TrackFileId { get; set; } + public int? AlbumId { get; set; } public string RelativePath { get; set; } public DateTime Added { get; set; } public DateTime LastUpdated { get; set; } diff --git a/src/NzbDrone.Core/Extras/Files/ExtraFileManager.cs b/src/NzbDrone.Core/Extras/Files/ExtraFileManager.cs index f21e989aa..b0599bff6 100644 --- a/src/NzbDrone.Core/Extras/Files/ExtraFileManager.cs +++ b/src/NzbDrone.Core/Extras/Files/ExtraFileManager.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.IO; using System.Text; @@ -7,18 +7,18 @@ using NzbDrone.Common.Disk; using NzbDrone.Common.Extensions; using NzbDrone.Core.Configuration; using NzbDrone.Core.MediaFiles; -using NzbDrone.Core.Tv; +using NzbDrone.Core.Music; namespace NzbDrone.Core.Extras.Files { public interface IManageExtraFiles { int Order { get; } - IEnumerable CreateAfterSeriesScan(Series series, List episodeFiles); - IEnumerable CreateAfterEpisodeImport(Series series, EpisodeFile episodeFile); - IEnumerable CreateAfterEpisodeImport(Series series, string seriesFolder, string seasonFolder); - IEnumerable MoveFilesAfterRename(Series series, List episodeFiles); - ExtraFile Import(Series series, EpisodeFile episodeFile, string path, string extension, bool readOnly); + IEnumerable CreateAfterArtistScan(Artist artist, List albums, List trackFiles); + IEnumerable CreateAfterTrackImport(Artist artist, TrackFile trackFile); + IEnumerable CreateAfterTrackImport(Artist artist, string artistFolder, string albumFolder); + IEnumerable MoveFilesAfterRename(Artist artist, List trackFiles); + ExtraFile Import(Artist artist, TrackFile trackFile, string path, string extension, bool readOnly); } public abstract class ExtraFileManager : IManageExtraFiles @@ -42,16 +42,16 @@ namespace NzbDrone.Core.Extras.Files } public abstract int Order { get; } - public abstract IEnumerable CreateAfterSeriesScan(Series series, List episodeFiles); - public abstract IEnumerable CreateAfterEpisodeImport(Series series, EpisodeFile episodeFile); - public abstract IEnumerable CreateAfterEpisodeImport(Series series, string seriesFolder, string seasonFolder); - public abstract IEnumerable MoveFilesAfterRename(Series series, List episodeFiles); - public abstract ExtraFile Import(Series series, EpisodeFile episodeFile, string path, string extension, bool readOnly); + public abstract IEnumerable CreateAfterArtistScan(Artist artist, List albums, List trackFiles); + public abstract IEnumerable CreateAfterTrackImport(Artist artist, TrackFile trackFile); + public abstract IEnumerable CreateAfterTrackImport(Artist artist, string artistFolder, string albumFolder); + public abstract IEnumerable MoveFilesAfterRename(Artist artist, List trackFiles); + public abstract ExtraFile Import(Artist artist, TrackFile trackFile, string path, string extension, bool readOnly); - protected TExtraFile ImportFile(Series series, EpisodeFile episodeFile, string path, bool readOnly, string extension, string fileNameSuffix = null) + protected TExtraFile ImportFile(Artist artist, TrackFile trackFile, string path, bool readOnly, string extension, string fileNameSuffix = null) { - var newFolder = Path.GetDirectoryName(Path.Combine(series.Path, episodeFile.RelativePath)); - var filenameBuilder = new StringBuilder(Path.GetFileNameWithoutExtension(episodeFile.RelativePath)); + var newFolder = Path.GetDirectoryName(Path.Combine(artist.Path, trackFile.RelativePath)); + var filenameBuilder = new StringBuilder(Path.GetFileNameWithoutExtension(trackFile.RelativePath)); if (fileNameSuffix.IsNotNullOrWhiteSpace()) { @@ -72,28 +72,27 @@ namespace NzbDrone.Core.Extras.Files return new TExtraFile { - SeriesId = series.Id, - SeasonNumber = episodeFile.SeasonNumber, - EpisodeFileId = episodeFile.Id, - RelativePath = series.Path.GetRelativePath(newFileName), + ArtistId = artist.Id, + AlbumId = trackFile.AlbumId, + TrackFileId = trackFile.Id, + RelativePath = artist.Path.GetRelativePath(newFileName), Extension = extension }; } - protected TExtraFile MoveFile(Series series, EpisodeFile episodeFile, TExtraFile extraFile, string fileNameSuffix = null) + protected TExtraFile MoveFile(Artist artist, TrackFile trackFile, TExtraFile extraFile, string fileNameSuffix = null) { - var newFolder = Path.GetDirectoryName(Path.Combine(series.Path, episodeFile.RelativePath)); - var filenameBuilder = new StringBuilder(Path.GetFileNameWithoutExtension(episodeFile.RelativePath)); + var newFolder = Path.GetDirectoryName(Path.Combine(artist.Path, trackFile.RelativePath)); + var filenameBuilder = new StringBuilder(Path.GetFileNameWithoutExtension(trackFile.RelativePath)); if (fileNameSuffix.IsNotNullOrWhiteSpace()) { filenameBuilder.Append(fileNameSuffix); } - filenameBuilder.Append("."); filenameBuilder.Append(extraFile.Extension); - var existingFileName = Path.Combine(series.Path, extraFile.RelativePath); + var existingFileName = Path.Combine(artist.Path, extraFile.RelativePath); var newFileName = Path.Combine(newFolder, filenameBuilder.ToString()); if (newFileName.PathNotEquals(existingFileName)) @@ -101,7 +100,7 @@ namespace NzbDrone.Core.Extras.Files try { _diskProvider.MoveFile(existingFileName, newFileName); - extraFile.RelativePath = series.Path.GetRelativePath(newFileName); + extraFile.RelativePath = artist.Path.GetRelativePath(newFileName); return extraFile; } diff --git a/src/NzbDrone.Core/Extras/Files/ExtraFileRepository.cs b/src/NzbDrone.Core/Extras/Files/ExtraFileRepository.cs index 7cb4644c3..5e3a900a6 100644 --- a/src/NzbDrone.Core/Extras/Files/ExtraFileRepository.cs +++ b/src/NzbDrone.Core/Extras/Files/ExtraFileRepository.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using NzbDrone.Core.Datastore; using NzbDrone.Core.Messaging.Events; @@ -7,12 +7,12 @@ namespace NzbDrone.Core.Extras.Files { public interface IExtraFileRepository : IBasicRepository where TExtraFile : ExtraFile, new() { - void DeleteForSeries(int seriesId); - void DeleteForSeason(int seriesId, int seasonNumber); - void DeleteForEpisodeFile(int episodeFileId); - List GetFilesBySeries(int seriesId); - List GetFilesBySeason(int seriesId, int seasonNumber); - List GetFilesByEpisodeFile(int episodeFileId); + void DeleteForArtist(int artistId); + void DeleteForAlbum(int artistId, int albumId); + void DeleteForTrackFile(int trackFileId); + List GetFilesByArtist(int artistId); + List GetFilesByAlbum(int artistId, int albumId); + List GetFilesByTrackFile(int trackFileId); TExtraFile FindByPath(string path); } @@ -24,34 +24,34 @@ namespace NzbDrone.Core.Extras.Files { } - public void DeleteForSeries(int seriesId) + public void DeleteForArtist(int artistId) { - Delete(c => c.SeriesId == seriesId); + Delete(c => c.ArtistId == artistId); } - public void DeleteForSeason(int seriesId, int seasonNumber) + public void DeleteForAlbum(int artistId, int albumId) { - Delete(c => c.SeriesId == seriesId && c.SeasonNumber == seasonNumber); + Delete(c => c.ArtistId == artistId && c.AlbumId == albumId); } - public void DeleteForEpisodeFile(int episodeFileId) + public void DeleteForTrackFile(int trackFileId) { - Delete(c => c.EpisodeFileId == episodeFileId); + Delete(c => c.TrackFileId == trackFileId); } - public List GetFilesBySeries(int seriesId) + public List GetFilesByArtist(int artistId) { - return Query.Where(c => c.SeriesId == seriesId); + return Query.Where(c => c.ArtistId == artistId); } - public List GetFilesBySeason(int seriesId, int seasonNumber) + public List GetFilesByAlbum(int artistId, int albumId) { - return Query.Where(c => c.SeriesId == seriesId && c.SeasonNumber == seasonNumber); + return Query.Where(c => c.ArtistId == artistId && c.AlbumId == albumId); } - public List GetFilesByEpisodeFile(int episodeFileId) + public List GetFilesByTrackFile(int trackFileId) { - return Query.Where(c => c.EpisodeFileId == episodeFileId); + return Query.Where(c => c.TrackFileId == trackFileId); } public TExtraFile FindByPath(string path) diff --git a/src/NzbDrone.Core/Extras/Files/ExtraFileService.cs b/src/NzbDrone.Core/Extras/Files/ExtraFileService.cs index ac30f6536..0a8bdbb10 100644 --- a/src/NzbDrone.Core/Extras/Files/ExtraFileService.cs +++ b/src/NzbDrone.Core/Extras/Files/ExtraFileService.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.IO; using System.Linq; @@ -8,16 +8,16 @@ using NzbDrone.Common.Extensions; using NzbDrone.Core.MediaFiles; using NzbDrone.Core.MediaFiles.Events; using NzbDrone.Core.Messaging.Events; -using NzbDrone.Core.Tv; -using NzbDrone.Core.Tv.Events; +using NzbDrone.Core.Music; +using NzbDrone.Core.Music.Events; namespace NzbDrone.Core.Extras.Files { public interface IExtraFileService where TExtraFile : ExtraFile, new() { - List GetFilesBySeries(int seriesId); - List GetFilesByEpisodeFile(int episodeFileId); + List GetFilesByArtist(int artistId); + List GetFilesByTrackFile(int trackFileId); TExtraFile FindByPath(string path); void Upsert(TExtraFile extraFile); void Upsert(List extraFiles); @@ -26,24 +26,24 @@ namespace NzbDrone.Core.Extras.Files } public abstract class ExtraFileService : IExtraFileService, - IHandleAsync, - IHandleAsync + IHandleAsync, + IHandleAsync where TExtraFile : ExtraFile, new() { private readonly IExtraFileRepository _repository; - private readonly ISeriesService _seriesService; + private readonly IArtistService _artistService; private readonly IDiskProvider _diskProvider; private readonly IRecycleBinProvider _recycleBinProvider; private readonly Logger _logger; public ExtraFileService(IExtraFileRepository repository, - ISeriesService seriesService, + IArtistService artistService, IDiskProvider diskProvider, IRecycleBinProvider recycleBinProvider, Logger logger) { _repository = repository; - _seriesService = seriesService; + _artistService = artistService; _diskProvider = diskProvider; _recycleBinProvider = recycleBinProvider; _logger = logger; @@ -51,14 +51,14 @@ namespace NzbDrone.Core.Extras.Files public virtual bool PermanentlyDelete => false; - public List GetFilesBySeries(int seriesId) + public List GetFilesByArtist(int artistId) { - return _repository.GetFilesBySeries(seriesId); + return _repository.GetFilesByArtist(artistId); } - public List GetFilesByEpisodeFile(int episodeFileId) + public List GetFilesByTrackFile(int trackFileId) { - return _repository.GetFilesByEpisodeFile(episodeFileId); + return _repository.GetFilesByTrackFile(trackFileId); } public TExtraFile FindByPath(string path) @@ -97,28 +97,28 @@ namespace NzbDrone.Core.Extras.Files _repository.DeleteMany(ids); } - public void HandleAsync(SeriesDeletedEvent message) + public void HandleAsync(ArtistDeletedEvent message) { - _logger.Debug("Deleting Extra from database for series: {0}", message.Series); - _repository.DeleteForSeries(message.Series.Id); + _logger.Debug("Deleting Extra from database for artist: {0}", message.Artist); + _repository.DeleteForArtist(message.Artist.Id); } - public void HandleAsync(EpisodeFileDeletedEvent message) + public void HandleAsync(TrackFileDeletedEvent message) { - var episodeFile = message.EpisodeFile; + var trackFile = message.TrackFile; if (message.Reason == DeleteMediaFileReason.NoLinkedEpisodes) { - _logger.Debug("Removing episode file from DB as part of cleanup routine, not deleting extra files from disk."); + _logger.Debug("Removing track file from DB as part of cleanup routine, not deleting extra files from disk."); } else { - var series = _seriesService.GetSeries(message.EpisodeFile.SeriesId); + var artist = _artistService.GetArtist(message.TrackFile.ArtistId); - foreach (var extra in _repository.GetFilesByEpisodeFile(episodeFile.Id)) + foreach (var extra in _repository.GetFilesByTrackFile(trackFile.Id)) { - var path = Path.Combine(series.Path, extra.RelativePath); + var path = Path.Combine(artist.Path, extra.RelativePath); if (_diskProvider.FileExists(path)) { @@ -130,15 +130,15 @@ namespace NzbDrone.Core.Extras.Files else { // Send extra files to the recycling bin so they can be recovered if necessary - var subfolder = _diskProvider.GetParentFolder(series.Path).GetRelativePath(_diskProvider.GetParentFolder(path)); + var subfolder = _diskProvider.GetParentFolder(artist.Path).GetRelativePath(_diskProvider.GetParentFolder(path)); _recycleBinProvider.DeleteFile(path, subfolder); } } } } - _logger.Debug("Deleting Extra from database for episode file: {0}", episodeFile); - _repository.DeleteForEpisodeFile(episodeFile.Id); + _logger.Debug("Deleting Extra from database for track file: {0}", trackFile); + _repository.DeleteForTrackFile(trackFile.Id); } } } diff --git a/src/NzbDrone.Core/Extras/IImportExistingExtraFiles.cs b/src/NzbDrone.Core/Extras/IImportExistingExtraFiles.cs index ad14b60a5..cb5a7dcff 100644 --- a/src/NzbDrone.Core/Extras/IImportExistingExtraFiles.cs +++ b/src/NzbDrone.Core/Extras/IImportExistingExtraFiles.cs @@ -1,12 +1,12 @@ -using System.Collections.Generic; +using System.Collections.Generic; using NzbDrone.Core.Extras.Files; -using NzbDrone.Core.Tv; +using NzbDrone.Core.Music; namespace NzbDrone.Core.Extras { public interface IImportExistingExtraFiles { int Order { get; } - IEnumerable ProcessFiles(Series series, List filesOnDisk, List importedFiles); + IEnumerable ProcessFiles(Artist artist, List filesOnDisk, List importedFiles); } } diff --git a/src/NzbDrone.Core/Extras/ImportExistingExtraFilesBase.cs b/src/NzbDrone.Core/Extras/ImportExistingExtraFilesBase.cs index a2dddaa69..c232259c4 100644 --- a/src/NzbDrone.Core/Extras/ImportExistingExtraFilesBase.cs +++ b/src/NzbDrone.Core/Extras/ImportExistingExtraFilesBase.cs @@ -1,10 +1,10 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.IO; using System.Linq; using NzbDrone.Common; using NzbDrone.Common.Extensions; using NzbDrone.Core.Extras.Files; -using NzbDrone.Core.Tv; +using NzbDrone.Core.Music; namespace NzbDrone.Core.Extras { @@ -19,21 +19,21 @@ namespace NzbDrone.Core.Extras } public abstract int Order { get; } - public abstract IEnumerable ProcessFiles(Series series, List filesOnDisk, List importedFiles); + public abstract IEnumerable ProcessFiles(Artist artist, List filesOnDisk, List importedFiles); - public virtual ImportExistingExtraFileFilterResult FilterAndClean(Series series, List filesOnDisk, List importedFiles) + public virtual ImportExistingExtraFileFilterResult FilterAndClean(Artist artist, List filesOnDisk, List importedFiles) { - var seriesFiles = _extraFileService.GetFilesBySeries(series.Id); + var artistFiles = _extraFileService.GetFilesByArtist(artist.Id); - Clean(series, filesOnDisk, importedFiles, seriesFiles); + Clean(artist, filesOnDisk, importedFiles, artistFiles); - return Filter(series, filesOnDisk, importedFiles, seriesFiles); + return Filter(artist, filesOnDisk, importedFiles, artistFiles); } - private ImportExistingExtraFileFilterResult Filter(Series series, List filesOnDisk, List importedFiles, List seriesFiles) + private ImportExistingExtraFileFilterResult Filter(Artist artist, List filesOnDisk, List importedFiles, List artistFiles) { - var previouslyImported = seriesFiles.IntersectBy(s => Path.Combine(series.Path, s.RelativePath), filesOnDisk, f => f, PathEqualityComparer.Instance).ToList(); - var filteredFiles = filesOnDisk.Except(previouslyImported.Select(f => Path.Combine(series.Path, f.RelativePath)).ToList(), PathEqualityComparer.Instance) + var previouslyImported = artistFiles.IntersectBy(s => Path.Combine(artist.Path, s.RelativePath), filesOnDisk, f => f, PathEqualityComparer.Instance).ToList(); + var filteredFiles = filesOnDisk.Except(previouslyImported.Select(f => Path.Combine(artist.Path, f.RelativePath)).ToList(), PathEqualityComparer.Instance) .Except(importedFiles, PathEqualityComparer.Instance) .ToList(); @@ -42,12 +42,12 @@ namespace NzbDrone.Core.Extras return new ImportExistingExtraFileFilterResult(previouslyImported, filteredFiles); } - private void Clean(Series series, List filesOnDisk, List importedFiles, List seriesFiles) + private void Clean(Artist artist, List filesOnDisk, List importedFiles, List artistFiles) { - var alreadyImportedFileIds = seriesFiles.IntersectBy(f => Path.Combine(series.Path, f.RelativePath), importedFiles, i => i, PathEqualityComparer.Instance) + var alreadyImportedFileIds = artistFiles.IntersectBy(f => Path.Combine(artist.Path, f.RelativePath), importedFiles, i => i, PathEqualityComparer.Instance) .Select(f => f.Id); - var deletedFiles = seriesFiles.ExceptBy(f => Path.Combine(series.Path, f.RelativePath), filesOnDisk, i => i, PathEqualityComparer.Instance) + var deletedFiles = artistFiles.ExceptBy(f => Path.Combine(artist.Path, f.RelativePath), filesOnDisk, i => i, PathEqualityComparer.Instance) .Select(f => f.Id); _extraFileService.DeleteMany(alreadyImportedFileIds); diff --git a/src/NzbDrone.Core/Extras/Lyrics/ExistingLyricImporter.cs b/src/NzbDrone.Core/Extras/Lyrics/ExistingLyricImporter.cs new file mode 100644 index 000000000..7bf9f2998 --- /dev/null +++ b/src/NzbDrone.Core/Extras/Lyrics/ExistingLyricImporter.cs @@ -0,0 +1,86 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; +using NLog; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.Extras.Files; +using NzbDrone.Core.Parser; +using NzbDrone.Core.Music; + +namespace NzbDrone.Core.Extras.Lyrics +{ + public class ExistingLyricImporter : ImportExistingExtraFilesBase + { + private readonly IExtraFileService _lyricFileService; + private readonly IParsingService _parsingService; + private readonly Logger _logger; + + public ExistingLyricImporter(IExtraFileService lyricFileService, + IParsingService parsingService, + Logger logger) + : base (lyricFileService) + { + _lyricFileService = lyricFileService; + _parsingService = parsingService; + _logger = logger; + } + + public override int Order => 1; + + public override IEnumerable ProcessFiles(Artist artist, List filesOnDisk, List importedFiles) + { + _logger.Debug("Looking for existing lyrics files in {0}", artist.Path); + + var subtitleFiles = new List(); + var filterResult = FilterAndClean(artist, filesOnDisk, importedFiles); + + foreach (var possibleSubtitleFile in filterResult.FilesOnDisk) + { + var extension = Path.GetExtension(possibleSubtitleFile); + + if (LyricFileExtensions.Extensions.Contains(extension)) + { + var localTrack = _parsingService.GetLocalTrack(possibleSubtitleFile, artist); + + if (localTrack == null) + { + _logger.Debug("Unable to parse lyric file: {0}", possibleSubtitleFile); + continue; + } + + if (localTrack.Tracks.Empty()) + { + _logger.Debug("Cannot find related tracks for: {0}", possibleSubtitleFile); + continue; + } + + if (localTrack.Tracks.DistinctBy(e => e.TrackFileId).Count() > 1) + { + _logger.Debug("Lyric file: {0} does not match existing files.", possibleSubtitleFile); + continue; + } + + var subtitleFile = new LyricFile + { + ArtistId = artist.Id, + AlbumId = localTrack.Album.Id, + TrackFileId = localTrack.Tracks.First().TrackFileId, + RelativePath = artist.Path.GetRelativePath(possibleSubtitleFile), + Language = LanguageParser.ParseSubtitleLanguage(possibleSubtitleFile), + Extension = extension + }; + + subtitleFiles.Add(subtitleFile); + } + } + + _logger.Info("Found {0} existing lyric files", subtitleFiles.Count); + _lyricFileService.Upsert(subtitleFiles); + + // Return files that were just imported along with files that were + // previously imported so previously imported files aren't imported twice + + return subtitleFiles.Concat(filterResult.PreviouslyImported); + } + } +} diff --git a/src/NzbDrone.Core/Extras/Lyrics/ImportedLyricFiles.cs b/src/NzbDrone.Core/Extras/Lyrics/ImportedLyricFiles.cs new file mode 100644 index 000000000..abbaa4c74 --- /dev/null +++ b/src/NzbDrone.Core/Extras/Lyrics/ImportedLyricFiles.cs @@ -0,0 +1,17 @@ +using System.Collections.Generic; +using NzbDrone.Core.Extras.Files; + +namespace NzbDrone.Core.Extras.Lyrics +{ + public class ImportedLyricFiles + { + public List SourceFiles { get; set; } + public List LyricFiles { get; set; } + + public ImportedLyricFiles() + { + SourceFiles = new List(); + LyricFiles = new List(); + } + } +} diff --git a/src/NzbDrone.Core/Extras/Lyrics/LyricFile.cs b/src/NzbDrone.Core/Extras/Lyrics/LyricFile.cs new file mode 100644 index 000000000..82bd50c27 --- /dev/null +++ b/src/NzbDrone.Core/Extras/Lyrics/LyricFile.cs @@ -0,0 +1,10 @@ +using NzbDrone.Core.Extras.Files; +using NzbDrone.Core.Languages; + +namespace NzbDrone.Core.Extras.Lyrics +{ + public class LyricFile : ExtraFile + { + public Language Language { get; set; } + } +} diff --git a/src/NzbDrone.Core/Extras/Lyrics/LyricFileExtensions.cs b/src/NzbDrone.Core/Extras/Lyrics/LyricFileExtensions.cs new file mode 100644 index 000000000..1d47fc0b1 --- /dev/null +++ b/src/NzbDrone.Core/Extras/Lyrics/LyricFileExtensions.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; + +namespace NzbDrone.Core.Extras.Lyrics +{ + public static class LyricFileExtensions + { + private static HashSet _fileExtensions; + + static LyricFileExtensions() + { + _fileExtensions = new HashSet(StringComparer.OrdinalIgnoreCase) + { + ".lrc", + ".txt", + ".utf", + ".utf8", + ".utf-8" + }; + } + + public static HashSet Extensions => _fileExtensions; + } +} diff --git a/src/NzbDrone.Core/Extras/Lyrics/LyricFileRepository.cs b/src/NzbDrone.Core/Extras/Lyrics/LyricFileRepository.cs new file mode 100644 index 000000000..60fe50f45 --- /dev/null +++ b/src/NzbDrone.Core/Extras/Lyrics/LyricFileRepository.cs @@ -0,0 +1,18 @@ +using NzbDrone.Core.Datastore; +using NzbDrone.Core.Extras.Files; +using NzbDrone.Core.Messaging.Events; + +namespace NzbDrone.Core.Extras.Lyrics +{ + public interface ILyricFileRepository : IExtraFileRepository + { + } + + public class LyricFileRepository : ExtraFileRepository, ILyricFileRepository + { + public LyricFileRepository(IMainDatabase database, IEventAggregator eventAggregator) + : base(database, eventAggregator) + { + } + } +} diff --git a/src/NzbDrone.Core/Extras/Lyrics/LyricFileService.cs b/src/NzbDrone.Core/Extras/Lyrics/LyricFileService.cs new file mode 100644 index 000000000..4d2935ce5 --- /dev/null +++ b/src/NzbDrone.Core/Extras/Lyrics/LyricFileService.cs @@ -0,0 +1,20 @@ +using NLog; +using NzbDrone.Common.Disk; +using NzbDrone.Core.Extras.Files; +using NzbDrone.Core.MediaFiles; +using NzbDrone.Core.Music; + +namespace NzbDrone.Core.Extras.Lyrics +{ + public interface ILyricFileService : IExtraFileService + { + } + + public class LyricFileService : ExtraFileService, ILyricFileService + { + public LyricFileService(IExtraFileRepository repository, IArtistService artistService, IDiskProvider diskProvider, IRecycleBinProvider recycleBinProvider, Logger logger) + : base(repository, artistService, diskProvider, recycleBinProvider, logger) + { + } + } +} diff --git a/src/NzbDrone.Core/Extras/Lyrics/LyricService.cs b/src/NzbDrone.Core/Extras/Lyrics/LyricService.cs new file mode 100644 index 000000000..679157989 --- /dev/null +++ b/src/NzbDrone.Core/Extras/Lyrics/LyricService.cs @@ -0,0 +1,122 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using NLog; +using NzbDrone.Common.Disk; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.Configuration; +using NzbDrone.Core.Extras.Files; +using NzbDrone.Core.MediaFiles; +using NzbDrone.Core.Parser; +using NzbDrone.Core.Music; +using NzbDrone.Core.Languages; + +namespace NzbDrone.Core.Extras.Lyrics +{ + public class LyricService : ExtraFileManager + { + private readonly ILyricFileService _lyricFileService; + private readonly Logger _logger; + + public LyricService(IConfigService configService, + IDiskProvider diskProvider, + IDiskTransferService diskTransferService, + ILyricFileService lyricFileService, + Logger logger) + : base(configService, diskProvider, diskTransferService, logger) + { + _lyricFileService = lyricFileService; + _logger = logger; + } + + public override int Order => 1; + + public override IEnumerable CreateAfterArtistScan(Artist artist, List albums, List trackFiles) + { + return Enumerable.Empty(); + } + + public override IEnumerable CreateAfterTrackImport(Artist artist, TrackFile trackFile) + { + return Enumerable.Empty(); + } + + public override IEnumerable CreateAfterTrackImport(Artist artist, string artistFolder, string albumFolder) + { + return Enumerable.Empty(); + } + + public override IEnumerable MoveFilesAfterRename(Artist artist, List trackFiles) + { + var subtitleFiles = _lyricFileService.GetFilesByArtist(artist.Id); + + var movedFiles = new List(); + + foreach (var trackFile in trackFiles) + { + var groupedExtraFilesForTrackFile = subtitleFiles.Where(m => m.TrackFileId == trackFile.Id) + .GroupBy(s => s.Language + s.Extension).ToList(); + + foreach (var group in groupedExtraFilesForTrackFile) + { + var groupCount = group.Count(); + var copy = 1; + + if (groupCount > 1) + { + _logger.Warn("Multiple lyric files found with the same language and extension for {0}", Path.Combine(artist.Path, trackFile.RelativePath)); + } + + foreach (var subtitleFile in group) + { + var suffix = GetSuffix(subtitleFile.Language, copy, groupCount > 1); + movedFiles.AddIfNotNull(MoveFile(artist, trackFile, subtitleFile, suffix)); + + copy++; + } + } + } + + _lyricFileService.Upsert(movedFiles); + + return movedFiles; + } + + public override ExtraFile Import(Artist artist, TrackFile trackFile, string path, string extension, bool readOnly) + { + if (LyricFileExtensions.Extensions.Contains(Path.GetExtension(path))) + { + var language = LanguageParser.ParseSubtitleLanguage(path); + var suffix = GetSuffix(language, 1, false); + var subtitleFile = ImportFile(artist, trackFile, path, readOnly, extension, suffix); + subtitleFile.Language = language; + + _lyricFileService.Upsert(subtitleFile); + + return subtitleFile; + } + + return null; + } + + private string GetSuffix(Language language, int copy, bool multipleCopies = false) + { + var suffixBuilder = new StringBuilder(); + + if (multipleCopies) + { + suffixBuilder.Append("."); + suffixBuilder.Append(copy); + } + + if (language != Language.Unknown) + { + suffixBuilder.Append("."); + suffixBuilder.Append(IsoLanguages.Get(language).TwoLetterCode); + } + + return suffixBuilder.ToString(); + } + } +} diff --git a/src/NzbDrone.Core/Extras/Metadata/Consumers/MediaBrowser/MediaBrowserMetadata.cs b/src/NzbDrone.Core/Extras/Metadata/Consumers/MediaBrowser/MediaBrowserMetadata.cs index d2ea82bae..9c1dd86a7 100644 --- a/src/NzbDrone.Core/Extras/Metadata/Consumers/MediaBrowser/MediaBrowserMetadata.cs +++ b/src/NzbDrone.Core/Extras/Metadata/Consumers/MediaBrowser/MediaBrowserMetadata.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.IO; using System.Linq; @@ -9,7 +9,7 @@ using NLog; using NzbDrone.Common.Extensions; using NzbDrone.Core.Extras.Metadata.Files; using NzbDrone.Core.MediaFiles; -using NzbDrone.Core.Tv; +using NzbDrone.Core.Music; namespace NzbDrone.Core.Extras.Metadata.Consumers.MediaBrowser { @@ -25,7 +25,7 @@ namespace NzbDrone.Core.Extras.Metadata.Consumers.MediaBrowser public override string Name => "Emby (Legacy)"; - public override MetadataFile FindMetadataFile(Series series, string path) + public override MetadataFile FindMetadataFile(Artist artist, string path) { var filename = Path.GetFileName(path); @@ -33,28 +33,28 @@ namespace NzbDrone.Core.Extras.Metadata.Consumers.MediaBrowser var metadata = new MetadataFile { - SeriesId = series.Id, + ArtistId = artist.Id, Consumer = GetType().Name, - RelativePath = series.Path.GetRelativePath(path) + RelativePath = artist.Path.GetRelativePath(path) }; - if (filename.Equals("series.xml", StringComparison.InvariantCultureIgnoreCase)) + if (filename.Equals("artist.xml", StringComparison.InvariantCultureIgnoreCase)) { - metadata.Type = MetadataType.SeriesMetadata; + metadata.Type = MetadataType.ArtistMetadata; return metadata; } return null; } - public override MetadataFileResult SeriesMetadata(Series series) + public override MetadataFileResult ArtistMetadata(Artist artist) { - if (!Settings.SeriesMetadata) + if (!Settings.ArtistMetadata) { return null; } - _logger.Debug("Generating series.xml for: {0}", series.Title); + _logger.Debug("Generating artist.xml for: {0}", artist.Name); var sb = new StringBuilder(); var xws = new XmlWriterSettings(); xws.OmitXmlDeclaration = true; @@ -62,85 +62,73 @@ namespace NzbDrone.Core.Extras.Metadata.Consumers.MediaBrowser using (var xw = XmlWriter.Create(sb, xws)) { - var tvShow = new XElement("Series"); + var artistElement = new XElement("Artist"); - tvShow.Add(new XElement("id", series.TvdbId)); - tvShow.Add(new XElement("Status", series.Status)); - tvShow.Add(new XElement("Network", series.Network)); - tvShow.Add(new XElement("Airs_Time", series.AirTime)); + artistElement.Add(new XElement("id", artist.ForeignArtistId)); + artistElement.Add(new XElement("Status", artist.Status)); - if (series.FirstAired.HasValue) - { - tvShow.Add(new XElement("FirstAired", series.FirstAired.Value.ToString("yyyy-MM-dd"))); - } - - tvShow.Add(new XElement("ContentRating", series.Certification)); - tvShow.Add(new XElement("Added", series.Added.ToString("MM/dd/yyyy HH:mm:ss tt"))); - tvShow.Add(new XElement("LockData", "false")); - tvShow.Add(new XElement("Overview", series.Overview)); - tvShow.Add(new XElement("LocalTitle", series.Title)); - - if (series.FirstAired.HasValue) - { - tvShow.Add(new XElement("PremiereDate", series.FirstAired.Value.ToString("yyyy-MM-dd"))); - } + artistElement.Add(new XElement("Added", artist.Added.ToString("MM/dd/yyyy HH:mm:ss tt"))); + artistElement.Add(new XElement("LockData", "false")); + artistElement.Add(new XElement("Overview", artist.Overview)); + artistElement.Add(new XElement("LocalTitle", artist.Name)); - tvShow.Add(new XElement("Rating", series.Ratings.Value)); - tvShow.Add(new XElement("ProductionYear", series.Year)); - tvShow.Add(new XElement("RunningTime", series.Runtime)); - tvShow.Add(new XElement("IMDB", series.ImdbId)); - tvShow.Add(new XElement("TVRageId", series.TvRageId)); - tvShow.Add(new XElement("Genres", series.Genres.Select(genre => new XElement("Genre", genre)))); + artistElement.Add(new XElement("Rating", artist.Ratings.Value)); + artistElement.Add(new XElement("Genres", artist.Genres.Select(genre => new XElement("Genre", genre)))); var persons = new XElement("Persons"); - foreach (var person in series.Actors) + foreach (var person in artist.Members) { persons.Add(new XElement("Person", new XElement("Name", person.Name), new XElement("Type", "Actor"), - new XElement("Role", person.Character) + new XElement("Role", person.Instrument) )); } - tvShow.Add(persons); + artistElement.Add(persons); - var doc = new XDocument(tvShow); + var doc = new XDocument(artistElement); doc.Save(xw); - _logger.Debug("Saving series.xml for {0}", series.Title); + _logger.Debug("Saving artist.xml for {0}", artist.Name); - return new MetadataFileResult("series.xml", doc.ToString()); + return new MetadataFileResult("artist.xml", doc.ToString()); } } - - public override MetadataFileResult EpisodeMetadata(Series series, EpisodeFile episodeFile) + + public override MetadataFileResult AlbumMetadata(Artist artist, Album album) + { + return null; + } + + public override MetadataFileResult TrackMetadata(Artist artist, TrackFile trackFile) { return null; } - public override List SeriesImages(Series series) + public override List ArtistImages(Artist artist) { return new List(); } - public override List SeasonImages(Series series, Season season) + public override List AlbumImages(Artist artist, Album season) { return new List(); } - public override List EpisodeImages(Series series, EpisodeFile episodeFile) + public override List TrackImages(Artist artist, TrackFile trackFile) { return new List(); } - private IEnumerable ProcessSeriesImages(Series series) + private IEnumerable ProcessArtistImages(Artist artist) { return new List(); } - private IEnumerable ProcessSeasonImages(Series series, Season season) + private IEnumerable ProcessAlbumImages(Artist artist, Album album) { return new List(); } @@ -155,4 +143,4 @@ namespace NzbDrone.Core.Extras.Metadata.Consumers.MediaBrowser return null; } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Core/Extras/Metadata/Consumers/MediaBrowser/MediaBrowserMetadataSettings.cs b/src/NzbDrone.Core/Extras/Metadata/Consumers/MediaBrowser/MediaBrowserMetadataSettings.cs index 11899124f..c81e4924b 100644 --- a/src/NzbDrone.Core/Extras/Metadata/Consumers/MediaBrowser/MediaBrowserMetadataSettings.cs +++ b/src/NzbDrone.Core/Extras/Metadata/Consumers/MediaBrowser/MediaBrowserMetadataSettings.cs @@ -1,4 +1,4 @@ -using FluentValidation; +using FluentValidation; using NzbDrone.Core.Annotations; using NzbDrone.Core.ThingiProvider; using NzbDrone.Core.Validation; @@ -18,11 +18,11 @@ namespace NzbDrone.Core.Extras.Metadata.Consumers.MediaBrowser public MediaBrowserMetadataSettings() { - SeriesMetadata = true; + ArtistMetadata = true; } - [FieldDefinition(0, Label = "Series Metadata", Type = FieldType.Checkbox)] - public bool SeriesMetadata { get; set; } + [FieldDefinition(0, Label = "Artist Metadata", Type = FieldType.Checkbox, HelpText = "artist.xml")] + public bool ArtistMetadata { get; set; } public bool IsValid => true; diff --git a/src/NzbDrone.Core/Extras/Metadata/Consumers/Roksbox/RoksboxMetadata.cs b/src/NzbDrone.Core/Extras/Metadata/Consumers/Roksbox/RoksboxMetadata.cs index cf5d5e61d..e2229cc07 100644 --- a/src/NzbDrone.Core/Extras/Metadata/Consumers/Roksbox/RoksboxMetadata.cs +++ b/src/NzbDrone.Core/Extras/Metadata/Consumers/Roksbox/RoksboxMetadata.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.IO; using System.Linq; @@ -12,7 +12,7 @@ using NzbDrone.Common.Extensions; using NzbDrone.Core.Extras.Metadata.Files; using NzbDrone.Core.MediaCover; using NzbDrone.Core.MediaFiles; -using NzbDrone.Core.Tv; +using NzbDrone.Core.Music; namespace NzbDrone.Core.Extras.Metadata.Consumers.Roksbox { @@ -36,25 +36,25 @@ namespace NzbDrone.Core.Extras.Metadata.Consumers.Roksbox public override string Name => "Roksbox"; - public override string GetFilenameAfterMove(Series series, EpisodeFile episodeFile, MetadataFile metadataFile) + public override string GetFilenameAfterMove(Artist artist, TrackFile trackFile, MetadataFile metadataFile) { - var episodeFilePath = Path.Combine(series.Path, episodeFile.RelativePath); + var trackFilePath = Path.Combine(artist.Path, trackFile.RelativePath); - if (metadataFile.Type == MetadataType.EpisodeImage) + if (metadataFile.Type == MetadataType.TrackImage) { - return GetEpisodeImageFilename(episodeFilePath); + return GetTrackImageFilename(trackFilePath); } - if (metadataFile.Type == MetadataType.EpisodeMetadata) + if (metadataFile.Type == MetadataType.TrackMetadata) { - return GetEpisodeMetadataFilename(episodeFilePath); + return GetTrackMetadataFilename(trackFilePath); } - _logger.Debug("Unknown episode file metadata: {0}", metadataFile.RelativePath); - return Path.Combine(series.Path, metadataFile.RelativePath); + _logger.Debug("Unknown track file metadata: {0}", metadataFile.RelativePath); + return Path.Combine(artist.Path, metadataFile.RelativePath); } - public override MetadataFile FindMetadataFile(Series series, string path) + public override MetadataFile FindMetadataFile(Artist artist, string path) { var filename = Path.GetFileName(path); @@ -63,9 +63,9 @@ namespace NzbDrone.Core.Extras.Metadata.Consumers.Roksbox var metadata = new MetadataFile { - SeriesId = series.Id, + ArtistId = artist.Id, Consumer = GetType().Name, - RelativePath = series.Path.GetRelativePath(path) + RelativePath = artist.Path.GetRelativePath(path) }; //Series and season images are both named folder.jpg, only season ones sit in season folders @@ -75,35 +75,34 @@ namespace NzbDrone.Core.Extras.Metadata.Consumers.Roksbox if (seasonMatch.Success) { - metadata.Type = MetadataType.SeasonImage; + metadata.Type = MetadataType.AlbumImage; if (seasonMatch.Groups["specials"].Success) { - metadata.SeasonNumber = 0; + metadata.AlbumId = 0; } else { - metadata.SeasonNumber = Convert.ToInt32(seasonMatch.Groups["season"].Value); + metadata.AlbumId = Convert.ToInt32(seasonMatch.Groups["season"].Value); } return metadata; } - metadata.Type = MetadataType.SeriesImage; + metadata.Type = MetadataType.ArtistImage; return metadata; } - var parseResult = Parser.Parser.ParseTitle(filename); + var parseResult = Parser.Parser.ParseMusicTitle(filename); - if (parseResult != null && - !parseResult.FullSeason) + if (parseResult != null) { var extension = Path.GetExtension(filename).ToLowerInvariant(); if (extension == ".xml") { - metadata.Type = MetadataType.EpisodeMetadata; + metadata.Type = MetadataType.TrackMetadata; return metadata; } @@ -111,7 +110,7 @@ namespace NzbDrone.Core.Extras.Metadata.Consumers.Roksbox { if (!Path.GetFileNameWithoutExtension(filename).EndsWith("-thumb")) { - metadata.Type = MetadataType.EpisodeImage; + metadata.Type = MetadataType.TrackImage; return metadata; } } @@ -120,23 +119,28 @@ namespace NzbDrone.Core.Extras.Metadata.Consumers.Roksbox return null; } - public override MetadataFileResult SeriesMetadata(Series series) + public override MetadataFileResult ArtistMetadata(Artist artist) { - //Series metadata is not supported + //Artist metadata is not supported return null; } - public override MetadataFileResult EpisodeMetadata(Series series, EpisodeFile episodeFile) + public override MetadataFileResult AlbumMetadata(Artist artist, Album album) + { + return null; + } + + public override MetadataFileResult TrackMetadata(Artist artist, TrackFile trackFile) { if (!Settings.EpisodeMetadata) { return null; } - _logger.Debug("Generating Episode Metadata for: {0}", episodeFile.RelativePath); + _logger.Debug("Generating Track Metadata for: {0}", trackFile.RelativePath); var xmlResult = string.Empty; - foreach (var episode in episodeFile.Episodes.Value) + foreach (var track in trackFile.Tracks.Value) { var sb = new StringBuilder(); var xws = new XmlWriterSettings(); @@ -148,24 +152,10 @@ namespace NzbDrone.Core.Extras.Metadata.Consumers.Roksbox var doc = new XDocument(); var details = new XElement("video"); - details.Add(new XElement("title", string.Format("{0} - {1}x{2} - {3}", series.Title, episode.SeasonNumber, episode.EpisodeNumber, episode.Title))); - details.Add(new XElement("year", episode.AirDate)); - details.Add(new XElement("genre", string.Join(" / ", series.Genres))); - var actors = string.Join(" , ", series.Actors.ConvertAll(c => c.Name + " - " + c.Character).GetRange(0, Math.Min(3, series.Actors.Count))); + details.Add(new XElement("title", string.Format("{0} - {1} - {2}", artist.Name, track.TrackNumber, track.Title))); + details.Add(new XElement("genre", string.Join(" / ", artist.Genres))); + var actors = string.Join(" , ", artist.Members.ConvertAll(c => c.Name + " - " + c.Instrument).GetRange(0, Math.Min(3, artist.Members.Count))); details.Add(new XElement("actors", actors)); - details.Add(new XElement("description", episode.Overview)); - details.Add(new XElement("length", series.Runtime)); - - if (series.Certification.IsNotNullOrWhiteSpace() && - ValidCertification.Contains(series.Certification.ToUpperInvariant())) - { - details.Add(new XElement("mpaa", series.Certification.ToUpperInvariant())); - } - - else - { - details.Add(new XElement("mpaa", "UNRATED")); - } doc.Add(details); doc.Save(xw); @@ -175,77 +165,93 @@ namespace NzbDrone.Core.Extras.Metadata.Consumers.Roksbox } } - return new MetadataFileResult(GetEpisodeMetadataFilename(episodeFile.RelativePath), xmlResult.Trim(Environment.NewLine.ToCharArray())); + return new MetadataFileResult(GetTrackMetadataFilename(trackFile.RelativePath), xmlResult.Trim(Environment.NewLine.ToCharArray())); } - public override List SeriesImages(Series series) + public override List ArtistImages(Artist artist) { - var image = series.Images.SingleOrDefault(c => c.CoverType == MediaCoverTypes.Poster) ?? series.Images.FirstOrDefault(); + if (!Settings.AlbumImages) + { + return new List(); + } + + var image = artist.Images.SingleOrDefault(c => c.CoverType == MediaCoverTypes.Poster) ?? artist.Images.FirstOrDefault(); if (image == null) { - _logger.Trace("Failed to find suitable Series image for series {0}.", series.Title); + _logger.Trace("Failed to find suitable Artist image for artist {0}.", artist.Name); return null; } - var source = _mediaCoverService.GetCoverPath(series.Id, image.CoverType); - var destination = Path.GetFileName(series.Path) + Path.GetExtension(source); + var source = _mediaCoverService.GetCoverPath(artist.Id, image.CoverType); + var destination = Path.GetFileName(artist.Path) + Path.GetExtension(source); return new List{ new ImageFileResult(destination, source) }; } - public override List SeasonImages(Series series, Season season) + public override List AlbumImages(Artist artist, Album album) { - var seasonFolders = GetSeasonFolders(series); + if (!Settings.AlbumImages) + { + return new List(); + } - string seasonFolder; - if (!seasonFolders.TryGetValue(season.SeasonNumber, out seasonFolder)) + var albumFolders = GetAlbumFolders(artist); + + string albumFolder; + if (!albumFolders.TryGetValue(album.ArtistId, out albumFolder)) { - _logger.Trace("Failed to find season folder for series {0}, season {1}.", series.Title, season.SeasonNumber); + _logger.Trace("Failed to find album folder for artit {0}, album {1}.", artist.Name, album.Title); return new List(); } //Roksbox only supports one season image, so first of all try for poster otherwise just use whatever is first in the collection - var image = season.Images.SingleOrDefault(c => c.CoverType == MediaCoverTypes.Poster) ?? season.Images.FirstOrDefault(); + var image = album.Images.SingleOrDefault(c => c.CoverType == MediaCoverTypes.Poster) ?? album.Images.FirstOrDefault(); if (image == null) { - _logger.Trace("Failed to find suitable season image for series {0}, season {1}.", series.Title, season.SeasonNumber); + _logger.Trace("Failed to find suitable album image for artist {0}, album {1}.", artist.Name, album.Title); return new List(); } - var filename = Path.GetFileName(seasonFolder) + ".jpg"; - var path = series.Path.GetRelativePath(Path.Combine(series.Path, seasonFolder, filename)); + var filename = Path.GetFileName(albumFolder) + ".jpg"; + var path = artist.Path.GetRelativePath(Path.Combine(artist.Path, albumFolder, filename)); return new List { new ImageFileResult(path, image.Url) }; } - public override List EpisodeImages(Series series, EpisodeFile episodeFile) + public override List TrackImages(Artist artist, TrackFile trackFile) { - var screenshot = episodeFile.Episodes.Value.First().Images.SingleOrDefault(i => i.CoverType == MediaCoverTypes.Screenshot); + //if (!Settings.EpisodeImages) + //{ + // return new List(); + //} - if (screenshot == null) - { - _logger.Trace("Episode screenshot not available"); - return new List(); - } + //var screenshot = episodeFile.Tracks.Value.First().Images.SingleOrDefault(i => i.CoverType == MediaCoverTypes.Screenshot); + + //if (screenshot == null) + //{ + // _logger.Trace("Episode screenshot not available"); + // return new List(); + //} - return new List {new ImageFileResult(GetEpisodeImageFilename(episodeFile.RelativePath), screenshot.Url)}; + //return new List {new ImageFileResult(GetEpisodeImageFilename(episodeFile.RelativePath), screenshot.Url)}; + return new List(); } - private string GetEpisodeMetadataFilename(string episodeFilePath) + private string GetTrackMetadataFilename(string trackFilePath) { - return Path.ChangeExtension(episodeFilePath, "xml"); + return Path.ChangeExtension(trackFilePath, "xml"); } - private string GetEpisodeImageFilename(string episodeFilePath) + private string GetTrackImageFilename(string trackFilePath) { - return Path.ChangeExtension(episodeFilePath, "jpg"); + return Path.ChangeExtension(trackFilePath, "jpg"); } - private Dictionary GetSeasonFolders(Series series) + private Dictionary GetAlbumFolders(Artist artist) { var seasonFolderMap = new Dictionary(); - foreach (var folder in _diskProvider.GetDirectories(series.Path)) + foreach (var folder in _diskProvider.GetDirectories(artist.Path)) { var directoryinfo = new DirectoryInfo(folder); var seasonMatch = SeasonImagesRegex.Match(directoryinfo.Name); @@ -267,13 +273,13 @@ namespace NzbDrone.Core.Extras.Metadata.Consumers.Roksbox } else { - _logger.Debug("Failed to parse season number from {0} for series {1}.", folder, series.Title); + _logger.Debug("Failed to parse season number from {0} for artist {1}.", folder, artist.Name); } } } else { - _logger.Debug("Rejecting folder {0} for series {1}.", Path.GetDirectoryName(folder), series.Title); + _logger.Debug("Rejecting folder {0} for artist {1}.", Path.GetDirectoryName(folder), artist.Name); } } diff --git a/src/NzbDrone.Core/Extras/Metadata/Consumers/Roksbox/RoksboxMetadataSettings.cs b/src/NzbDrone.Core/Extras/Metadata/Consumers/Roksbox/RoksboxMetadataSettings.cs index f0da481bf..9e5f18eaf 100644 --- a/src/NzbDrone.Core/Extras/Metadata/Consumers/Roksbox/RoksboxMetadataSettings.cs +++ b/src/NzbDrone.Core/Extras/Metadata/Consumers/Roksbox/RoksboxMetadataSettings.cs @@ -1,4 +1,4 @@ -using FluentValidation; +using FluentValidation; using NzbDrone.Core.Annotations; using NzbDrone.Core.ThingiProvider; using NzbDrone.Core.Validation; @@ -19,21 +19,21 @@ namespace NzbDrone.Core.Extras.Metadata.Consumers.Roksbox public RoksboxMetadataSettings() { EpisodeMetadata = true; - SeriesImages = true; - SeasonImages = true; + ArtistImages = true; + AlbumImages = true; EpisodeImages = true; } - [FieldDefinition(0, Label = "Episode Metadata", Type = FieldType.Checkbox)] + [FieldDefinition(0, Label = "Episode Metadata", Type = FieldType.Checkbox, HelpText = "Season##\\filename.xml")] public bool EpisodeMetadata { get; set; } - [FieldDefinition(1, Label = "Series Images", Type = FieldType.Checkbox)] - public bool SeriesImages { get; set; } + [FieldDefinition(1, Label = "Artist Images", Type = FieldType.Checkbox, HelpText = "Artist Title.jpg")] + public bool ArtistImages { get; set; } - [FieldDefinition(2, Label = "Season Images", Type = FieldType.Checkbox)] - public bool SeasonImages { get; set; } + [FieldDefinition(2, Label = "Album Images", Type = FieldType.Checkbox, HelpText = "Album Title.jpg")] + public bool AlbumImages { get; set; } - [FieldDefinition(3, Label = "Episode Images", Type = FieldType.Checkbox)] + [FieldDefinition(3, Label = "Episode Images", Type = FieldType.Checkbox, HelpText = "Season##\\filename.jpg")] public bool EpisodeImages { get; set; } public bool IsValid => true; diff --git a/src/NzbDrone.Core/Extras/Metadata/Consumers/Wdtv/WdtvMetadata.cs b/src/NzbDrone.Core/Extras/Metadata/Consumers/Wdtv/WdtvMetadata.cs index d1846c963..78505cd39 100644 --- a/src/NzbDrone.Core/Extras/Metadata/Consumers/Wdtv/WdtvMetadata.cs +++ b/src/NzbDrone.Core/Extras/Metadata/Consumers/Wdtv/WdtvMetadata.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.IO; using System.Linq; @@ -12,7 +12,7 @@ using NzbDrone.Common.Extensions; using NzbDrone.Core.Extras.Metadata.Files; using NzbDrone.Core.MediaCover; using NzbDrone.Core.MediaFiles; -using NzbDrone.Core.Tv; +using NzbDrone.Core.Music; namespace NzbDrone.Core.Extras.Metadata.Consumers.Wdtv { @@ -35,26 +35,26 @@ namespace NzbDrone.Core.Extras.Metadata.Consumers.Wdtv public override string Name => "WDTV"; - public override string GetFilenameAfterMove(Series series, EpisodeFile episodeFile, MetadataFile metadataFile) + public override string GetFilenameAfterMove(Artist artist, TrackFile trackFile, MetadataFile metadataFile) { - var episodeFilePath = Path.Combine(series.Path, episodeFile.RelativePath); + var trackFilePath = Path.Combine(artist.Path, trackFile.RelativePath); - if (metadataFile.Type == MetadataType.EpisodeImage) + if (metadataFile.Type == MetadataType.TrackImage) { - return GetEpisodeImageFilename(episodeFilePath); + return GetTrackImageFilename(trackFilePath); } - if (metadataFile.Type == MetadataType.EpisodeMetadata) + if (metadataFile.Type == MetadataType.TrackMetadata) { - return GetEpisodeMetadataFilename(episodeFilePath); + return GetTrackMetadataFilename(trackFilePath); } - _logger.Debug("Unknown episode file metadata: {0}", metadataFile.RelativePath); - return Path.Combine(series.Path, metadataFile.RelativePath); + _logger.Debug("Unknown track file metadata: {0}", metadataFile.RelativePath); + return Path.Combine(artist.Path, metadataFile.RelativePath); } - public override MetadataFile FindMetadataFile(Series series, string path) + public override MetadataFile FindMetadataFile(Artist artist, string path) { var filename = Path.GetFileName(path); @@ -62,9 +62,9 @@ namespace NzbDrone.Core.Extras.Metadata.Consumers.Wdtv var metadata = new MetadataFile { - SeriesId = series.Id, + ArtistId = artist.Id, Consumer = GetType().Name, - RelativePath = series.Path.GetRelativePath(path) + RelativePath = artist.Path.GetRelativePath(path) }; //Series and season images are both named folder.jpg, only season ones sit in season folders @@ -74,37 +74,36 @@ namespace NzbDrone.Core.Extras.Metadata.Consumers.Wdtv var seasonMatch = SeasonImagesRegex.Match(parentdir.Name); if (seasonMatch.Success) { - metadata.Type = MetadataType.SeasonImage; + metadata.Type = MetadataType.AlbumImage; if (seasonMatch.Groups["specials"].Success) { - metadata.SeasonNumber = 0; + metadata.AlbumId = 0; } else { - metadata.SeasonNumber = Convert.ToInt32(seasonMatch.Groups["season"].Value); + metadata.AlbumId = Convert.ToInt32(seasonMatch.Groups["season"].Value); } return metadata; } - metadata.Type = MetadataType.SeriesImage; + metadata.Type = MetadataType.ArtistImage; return metadata; } - var parseResult = Parser.Parser.ParseTitle(filename); + var parseResult = Parser.Parser.ParseMusicTitle(filename); - if (parseResult != null && - !parseResult.FullSeason) + if (parseResult != null) { switch (Path.GetExtension(filename).ToLowerInvariant()) { case ".xml": - metadata.Type = MetadataType.EpisodeMetadata; + metadata.Type = MetadataType.TrackMetadata; return metadata; case ".metathumb": - metadata.Type = MetadataType.EpisodeImage; + metadata.Type = MetadataType.TrackImage; return metadata; } @@ -113,23 +112,28 @@ namespace NzbDrone.Core.Extras.Metadata.Consumers.Wdtv return null; } - public override MetadataFileResult SeriesMetadata(Series series) + public override MetadataFileResult ArtistMetadata(Artist artist) { - //Series metadata is not supported + //Artist metadata is not supported return null; } - public override MetadataFileResult EpisodeMetadata(Series series, EpisodeFile episodeFile) + public override MetadataFileResult AlbumMetadata(Artist artist, Album album) + { + return null; + } + + public override MetadataFileResult TrackMetadata(Artist artist, TrackFile trackFile) { if (!Settings.EpisodeMetadata) { return null; } - _logger.Debug("Generating Episode Metadata for: {0}", Path.Combine(series.Path, episodeFile.RelativePath)); + _logger.Debug("Generating Track Metadata for: {0}", Path.Combine(artist.Path, trackFile.RelativePath)); var xmlResult = string.Empty; - foreach (var episode in episodeFile.Episodes.Value) + foreach (var track in trackFile.Tracks.Value) { var sb = new StringBuilder(); var xws = new XmlWriterSettings(); @@ -141,16 +145,13 @@ namespace NzbDrone.Core.Extras.Metadata.Consumers.Wdtv var doc = new XDocument(); var details = new XElement("details"); - details.Add(new XElement("id", series.Id)); - details.Add(new XElement("title", string.Format("{0} - {1}x{2:00} - {3}", series.Title, episode.SeasonNumber, episode.EpisodeNumber, episode.Title))); - details.Add(new XElement("series_name", series.Title)); - details.Add(new XElement("episode_name", episode.Title)); - details.Add(new XElement("season_number", episode.SeasonNumber.ToString("00"))); - details.Add(new XElement("episode_number", episode.EpisodeNumber.ToString("00"))); - details.Add(new XElement("firstaired", episode.AirDate)); - details.Add(new XElement("genre", string.Join(" / ", series.Genres))); - details.Add(new XElement("actor", string.Join(" / ", series.Actors.ConvertAll(c => c.Name + " - " + c.Character)))); - details.Add(new XElement("overview", episode.Overview)); + details.Add(new XElement("id", artist.Id)); + details.Add(new XElement("title", string.Format("{0} - {1} - {2}", artist.Name, track.TrackNumber, track.Title))); + details.Add(new XElement("artist_name", artist.Name)); + details.Add(new XElement("track_name", track.Title)); + details.Add(new XElement("track_number", track.TrackNumber.ToString("00"))); + details.Add(new XElement("genre", string.Join(" / ", artist.Genres))); + details.Add(new XElement("member", string.Join(" / ", artist.Members.ConvertAll(c => c.Name + " - " + c.Instrument)))); //Todo: get guest stars, writer and director @@ -165,27 +166,27 @@ namespace NzbDrone.Core.Extras.Metadata.Consumers.Wdtv } } - var filename = GetEpisodeMetadataFilename(episodeFile.RelativePath); + var filename = GetTrackMetadataFilename(trackFile.RelativePath); return new MetadataFileResult(filename, xmlResult.Trim(Environment.NewLine.ToCharArray())); } - public override List SeriesImages(Series series) + public override List ArtistImages(Artist artist) { - if (!Settings.SeriesImages) + if (!Settings.ArtistImages) { return new List(); } //Because we only support one image, attempt to get the Poster type, then if that fails grab the first - var image = series.Images.SingleOrDefault(c => c.CoverType == MediaCoverTypes.Poster) ?? series.Images.FirstOrDefault(); + var image = artist.Images.SingleOrDefault(c => c.CoverType == MediaCoverTypes.Poster) ?? artist.Images.FirstOrDefault(); if (image == null) { - _logger.Trace("Failed to find suitable Series image for series {0}.", series.Title); + _logger.Trace("Failed to find suitable Artist image for artist {0}.", artist.Name); return new List(); } - var source = _mediaCoverService.GetCoverPath(series.Id, image.CoverType); + var source = _mediaCoverService.GetCoverPath(artist.Id, image.CoverType); var destination = "folder" + Path.GetExtension(source); return new List @@ -194,28 +195,28 @@ namespace NzbDrone.Core.Extras.Metadata.Consumers.Wdtv }; } - public override List SeasonImages(Series series, Season season) + public override List AlbumImages(Artist artist, Album album) { - if (!Settings.SeasonImages) + if (!Settings.AlbumImages) { return new List(); } - var seasonFolders = GetSeasonFolders(series); + var seasonFolders = GetAlbumFolders(artist); //Work out the path to this season - if we don't have a matching path then skip this season. string seasonFolder; - if (!seasonFolders.TryGetValue(season.SeasonNumber, out seasonFolder)) + if (!seasonFolders.TryGetValue(album.Id, out seasonFolder)) { - _logger.Trace("Failed to find season folder for series {0}, season {1}.", series.Title, season.SeasonNumber); + _logger.Trace("Failed to find album folder for artist {0}, album {1}.", artist.Name, album.Title); return new List(); } //WDTV only supports one season image, so first of all try for poster otherwise just use whatever is first in the collection - var image = season.Images.SingleOrDefault(c => c.CoverType == MediaCoverTypes.Poster) ?? season.Images.FirstOrDefault(); + var image = album.Images.SingleOrDefault(c => c.CoverType == MediaCoverTypes.Poster) ?? album.Images.FirstOrDefault(); if (image == null) { - _logger.Trace("Failed to find suitable season image for series {0}, season {1}.", series.Title, season.SeasonNumber); + _logger.Trace("Failed to find suitable album image for artist {0}, album {1}.", artist.Name, album.Title); return new List(); } @@ -224,39 +225,27 @@ namespace NzbDrone.Core.Extras.Metadata.Consumers.Wdtv return new List{ new ImageFileResult(path, image.Url) }; } - public override List EpisodeImages(Series series, EpisodeFile episodeFile) + public override List TrackImages(Artist artist, TrackFile trackFile) { - if (!Settings.EpisodeImages) - { - return new List(); - } - - var screenshot = episodeFile.Episodes.Value.First().Images.SingleOrDefault(i => i.CoverType == MediaCoverTypes.Screenshot); - - if (screenshot == null) - { - _logger.Trace("Episode screenshot not available"); - return new List(); - } - return new List{ new ImageFileResult(GetEpisodeImageFilename(episodeFile.RelativePath), screenshot.Url) }; + return new List(); } - private string GetEpisodeMetadataFilename(string episodeFilePath) + private string GetTrackMetadataFilename(string trackFilePath) { - return Path.ChangeExtension(episodeFilePath, "xml"); + return Path.ChangeExtension(trackFilePath, "xml"); } - private string GetEpisodeImageFilename(string episodeFilePath) + private string GetTrackImageFilename(string trackFilePath) { - return Path.ChangeExtension(episodeFilePath, "metathumb"); + return Path.ChangeExtension(trackFilePath, "metathumb"); } - private Dictionary GetSeasonFolders(Series series) + private Dictionary GetAlbumFolders(Artist artist) { var seasonFolderMap = new Dictionary(); - foreach (var folder in _diskProvider.GetDirectories(series.Path)) + foreach (var folder in _diskProvider.GetDirectories(artist.Path)) { var directoryinfo = new DirectoryInfo(folder); var seasonMatch = SeasonImagesRegex.Match(directoryinfo.Name); @@ -278,14 +267,14 @@ namespace NzbDrone.Core.Extras.Metadata.Consumers.Wdtv } else { - _logger.Debug("Failed to parse season number from {0} for series {1}.", folder, series.Title); + _logger.Debug("Failed to parse season number from {0} for artist {1}.", folder, artist.Name); } } } else { - _logger.Debug("Rejecting folder {0} for series {1}.", Path.GetDirectoryName(folder), series.Title); + _logger.Debug("Rejecting folder {0} for artist {1}.", Path.GetDirectoryName(folder), artist.Name); } } diff --git a/src/NzbDrone.Core/Extras/Metadata/Consumers/Wdtv/WdtvMetadataSettings.cs b/src/NzbDrone.Core/Extras/Metadata/Consumers/Wdtv/WdtvMetadataSettings.cs index e010ff7e5..8b954c653 100644 --- a/src/NzbDrone.Core/Extras/Metadata/Consumers/Wdtv/WdtvMetadataSettings.cs +++ b/src/NzbDrone.Core/Extras/Metadata/Consumers/Wdtv/WdtvMetadataSettings.cs @@ -1,4 +1,4 @@ -using FluentValidation; +using FluentValidation; using NzbDrone.Core.Annotations; using NzbDrone.Core.ThingiProvider; using NzbDrone.Core.Validation; @@ -19,19 +19,19 @@ namespace NzbDrone.Core.Extras.Metadata.Consumers.Wdtv public WdtvMetadataSettings() { EpisodeMetadata = true; - SeriesImages = true; - SeasonImages = true; + ArtistImages = true; + AlbumImages = true; EpisodeImages = true; } [FieldDefinition(0, Label = "Episode Metadata", Type = FieldType.Checkbox)] public bool EpisodeMetadata { get; set; } - [FieldDefinition(1, Label = "Series Images", Type = FieldType.Checkbox)] - public bool SeriesImages { get; set; } + [FieldDefinition(1, Label = "Artist Images", Type = FieldType.Checkbox)] + public bool ArtistImages { get; set; } - [FieldDefinition(2, Label = "Season Images", Type = FieldType.Checkbox)] - public bool SeasonImages { get; set; } + [FieldDefinition(2, Label = "Album Images", Type = FieldType.Checkbox)] + public bool AlbumImages { get; set; } [FieldDefinition(3, Label = "Episode Images", Type = FieldType.Checkbox)] public bool EpisodeImages { get; set; } diff --git a/src/NzbDrone.Core/Extras/Metadata/Consumers/Xbmc/XbmcMetadata.cs b/src/NzbDrone.Core/Extras/Metadata/Consumers/Xbmc/XbmcMetadata.cs index 99e384cb9..983754fa7 100644 --- a/src/NzbDrone.Core/Extras/Metadata/Consumers/Xbmc/XbmcMetadata.cs +++ b/src/NzbDrone.Core/Extras/Metadata/Consumers/Xbmc/XbmcMetadata.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.IO; using System.Linq; @@ -11,7 +11,7 @@ using NzbDrone.Common.Extensions; using NzbDrone.Core.Extras.Metadata.Files; using NzbDrone.Core.MediaCover; using NzbDrone.Core.MediaFiles; -using NzbDrone.Core.Tv; +using NzbDrone.Core.Music; namespace NzbDrone.Core.Extras.Metadata.Consumers.Xbmc { @@ -27,31 +27,31 @@ namespace NzbDrone.Core.Extras.Metadata.Consumers.Xbmc _logger = logger; } - private static readonly Regex SeriesImagesRegex = new Regex(@"^(?poster|banner|fanart)\.(?:png|jpg)", RegexOptions.Compiled | RegexOptions.IgnoreCase); - private static readonly Regex SeasonImagesRegex = new Regex(@"^season(?\d{2,}|-all|-specials)-(?poster|banner|fanart)\.(?:png|jpg)", RegexOptions.Compiled | RegexOptions.IgnoreCase); + private static readonly Regex ArtistImagesRegex = new Regex(@"^(?poster|banner|fanart|logo)\.(?:png|jpg)", RegexOptions.Compiled | RegexOptions.IgnoreCase); + private static readonly Regex AlbumImagesRegex = new Regex(@"^season(?\d{2,}|-all|-specials)-(?poster|banner|fanart|cover)\.(?:png|jpg)", RegexOptions.Compiled | RegexOptions.IgnoreCase); private static readonly Regex EpisodeImageRegex = new Regex(@"-thumb\.(?:png|jpg)", RegexOptions.Compiled | RegexOptions.IgnoreCase); public override string Name => "Kodi (XBMC) / Emby"; - public override string GetFilenameAfterMove(Series series, EpisodeFile episodeFile, MetadataFile metadataFile) + public override string GetFilenameAfterMove(Artist artist, TrackFile trackFile, MetadataFile metadataFile) { - var episodeFilePath = Path.Combine(series.Path, episodeFile.RelativePath); + var trackFilePath = Path.Combine(artist.Path, trackFile.RelativePath); - if (metadataFile.Type == MetadataType.EpisodeImage) + if (metadataFile.Type == MetadataType.TrackImage) { - return GetEpisodeImageFilename(episodeFilePath); + return GetEpisodeImageFilename(trackFilePath); } - if (metadataFile.Type == MetadataType.EpisodeMetadata) + if (metadataFile.Type == MetadataType.TrackMetadata) { - return GetEpisodeMetadataFilename(episodeFilePath); + return GetEpisodeMetadataFilename(trackFilePath); } _logger.Debug("Unknown episode file metadata: {0}", metadataFile.RelativePath); - return Path.Combine(series.Path, metadataFile.RelativePath); + return Path.Combine(artist.Path, metadataFile.RelativePath); } - public override MetadataFile FindMetadataFile(Series series, string path) + public override MetadataFile FindMetadataFile(Artist artist, string path) { var filename = Path.GetFileName(path); @@ -59,34 +59,34 @@ namespace NzbDrone.Core.Extras.Metadata.Consumers.Xbmc var metadata = new MetadataFile { - SeriesId = series.Id, + ArtistId = artist.Id, Consumer = GetType().Name, - RelativePath = series.Path.GetRelativePath(path) + RelativePath = artist.Path.GetRelativePath(path) }; - if (SeriesImagesRegex.IsMatch(filename)) + if (ArtistImagesRegex.IsMatch(filename)) { - metadata.Type = MetadataType.SeriesImage; + metadata.Type = MetadataType.ArtistImage; return metadata; } - var seasonMatch = SeasonImagesRegex.Match(filename); + var seasonMatch = AlbumImagesRegex.Match(filename); if (seasonMatch.Success) { - metadata.Type = MetadataType.SeasonImage; + metadata.Type = MetadataType.AlbumImage; var seasonNumberMatch = seasonMatch.Groups["season"].Value; int seasonNumber; if (seasonNumberMatch.Contains("specials")) { - metadata.SeasonNumber = 0; + metadata.AlbumId = 0; } else if (int.TryParse(seasonNumberMatch, out seasonNumber)) { - metadata.SeasonNumber = seasonNumber; + metadata.AlbumId = seasonNumber; } else @@ -99,107 +99,141 @@ namespace NzbDrone.Core.Extras.Metadata.Consumers.Xbmc if (EpisodeImageRegex.IsMatch(filename)) { - metadata.Type = MetadataType.EpisodeImage; + metadata.Type = MetadataType.TrackImage; return metadata; } - if (filename.Equals("tvshow.nfo", StringComparison.InvariantCultureIgnoreCase)) + if (filename.Equals("artist.nfo", StringComparison.OrdinalIgnoreCase)) { - metadata.Type = MetadataType.SeriesMetadata; + metadata.Type = MetadataType.ArtistMetadata; return metadata; } - var parseResult = Parser.Parser.ParseTitle(filename); + if (filename.Equals("album.nfo", StringComparison.OrdinalIgnoreCase)) + { + metadata.Type = MetadataType.AlbumMetadata; + return metadata; + } + + var parseResult = Parser.Parser.ParseMusicTitle(filename); if (parseResult != null && - !parseResult.FullSeason && - Path.GetExtension(filename) == ".nfo") + Path.GetExtension(filename).Equals(".nfo", StringComparison.OrdinalIgnoreCase)) { - metadata.Type = MetadataType.EpisodeMetadata; + metadata.Type = MetadataType.TrackMetadata; return metadata; } return null; } - public override MetadataFileResult SeriesMetadata(Series series) + public override MetadataFileResult ArtistMetadata(Artist artist) { - if (!Settings.SeriesMetadata) + if (!Settings.ArtistMetadata) { return null; } - _logger.Debug("Generating tvshow.nfo for: {0}", series.Title); + _logger.Debug("Generating artist.nfo for: {0}", artist.Name); var sb = new StringBuilder(); var xws = new XmlWriterSettings(); xws.OmitXmlDeclaration = true; xws.Indent = false; - var episodeGuideUrl = string.Format("http://www.thetvdb.com/api/1D62F2F90030C444/series/{0}/all/en.zip", series.TvdbId); - using (var xw = XmlWriter.Create(sb, xws)) { - var tvShow = new XElement("tvshow"); + var artistElement = new XElement("artist"); - tvShow.Add(new XElement("title", series.Title)); + artistElement.Add(new XElement("title", artist.Name)); - if (series.Ratings != null && series.Ratings.Votes > 0) + if (artist.Ratings != null && artist.Ratings.Votes > 0) { - tvShow.Add(new XElement("rating", series.Ratings.Value)); + artistElement.Add(new XElement("rating", artist.Ratings.Value)); } - tvShow.Add(new XElement("plot", series.Overview)); - tvShow.Add(new XElement("episodeguide", new XElement("url", episodeGuideUrl))); - tvShow.Add(new XElement("episodeguideurl", episodeGuideUrl)); - tvShow.Add(new XElement("mpaa", series.Certification)); - tvShow.Add(new XElement("id", series.TvdbId)); + artistElement.Add(new XElement("musicbrainzartistid", artist.ForeignArtistId)); + artistElement.Add(new XElement("biography", artist.Overview)); + artistElement.Add(new XElement("outline", artist.Overview)); + //tvShow.Add(new XElement("episodeguide", new XElement("url", episodeGuideUrl))); + //tvShow.Add(new XElement("episodeguideurl", episodeGuideUrl)); + + //foreach (var genre in artist.Genres) + //{ + // tvShow.Add(new XElement("genre", genre)); + //} + + + //foreach (var actor in artist.Members) + //{ + // var xmlActor = new XElement("actor", + // new XElement("name", actor.Name), + // new XElement("role", actor.Instrument)); + + // if (actor.Images.Any()) + // { + // xmlActor.Add(new XElement("thumb", actor.Images.First().Url)); + // } + + // tvShow.Add(xmlActor); + //} + + var doc = new XDocument(artistElement); + doc.Save(xw); - foreach (var genre in series.Genres) - { - tvShow.Add(new XElement("genre", genre)); - } + _logger.Debug("Saving artist.nfo for {0}", artist.Name); - if (series.FirstAired.HasValue) - { - tvShow.Add(new XElement("premiered", series.FirstAired.Value.ToString("yyyy-MM-dd"))); - } + return new MetadataFileResult("artist.nfo", doc.ToString()); + } + } - tvShow.Add(new XElement("studio", series.Network)); + public override MetadataFileResult AlbumMetadata(Artist artist, Album album) + { + if (!Settings.AlbumMetadata) + { + return null; + } - foreach (var actor in series.Actors) - { - var xmlActor = new XElement("actor", - new XElement("name", actor.Name), - new XElement("role", actor.Character)); + _logger.Debug("Generating album.nfo for: {0}", album.Title); + var sb = new StringBuilder(); + var xws = new XmlWriterSettings(); + xws.OmitXmlDeclaration = true; + xws.Indent = false; - if (actor.Images.Any()) - { - xmlActor.Add(new XElement("thumb", actor.Images.First().Url)); - } + using (var xw = XmlWriter.Create(sb, xws)) + { + var albumElement = new XElement("album"); - tvShow.Add(xmlActor); + albumElement.Add(new XElement("title", album.Title)); + + if (album.Ratings != null && album.Ratings.Votes > 0) + { + albumElement.Add(new XElement("rating", album.Ratings.Value)); } - var doc = new XDocument(tvShow); + albumElement.Add(new XElement("musicbrainzalbumid", album.ForeignAlbumId)); + albumElement.Add(new XElement("artistdesc", artist.Overview)); + albumElement.Add(new XElement("releasedate", album.ReleaseDate.Value.ToShortDateString())); + + var doc = new XDocument(albumElement); doc.Save(xw); - _logger.Debug("Saving tvshow.nfo for {0}", series.Title); + _logger.Debug("Saving album.nfo for {0}", artist.Name); - return new MetadataFileResult("tvshow.nfo", doc.ToString()); + return new MetadataFileResult("album.nfo", doc.ToString()); } } - public override MetadataFileResult EpisodeMetadata(Series series, EpisodeFile episodeFile) + public override MetadataFileResult TrackMetadata(Artist artist, TrackFile trackFile) { - if (!Settings.EpisodeMetadata) + if (!Settings.TrackMetadata) { return null; } - _logger.Debug("Generating Episode Metadata for: {0}", Path.Combine(series.Path, episodeFile.RelativePath)); + _logger.Debug("Generating Track Metadata for: {0}", Path.Combine(artist.Path, trackFile.RelativePath)); var xmlResult = string.Empty; - foreach (var episode in episodeFile.Episodes.Value) + foreach (var episode in trackFile.Tracks.Value) { var sb = new StringBuilder(); var xws = new XmlWriterSettings(); @@ -209,29 +243,15 @@ namespace NzbDrone.Core.Extras.Metadata.Consumers.Xbmc using (var xw = XmlWriter.Create(sb, xws)) { var doc = new XDocument(); - var image = episode.Images.SingleOrDefault(i => i.CoverType == MediaCoverTypes.Screenshot); var details = new XElement("episodedetails"); details.Add(new XElement("title", episode.Title)); - details.Add(new XElement("season", episode.SeasonNumber)); - details.Add(new XElement("episode", episode.EpisodeNumber)); - details.Add(new XElement("aired", episode.AirDate)); - details.Add(new XElement("plot", episode.Overview)); + details.Add(new XElement("episode", episode.TrackNumber)); //If trakt ever gets airs before information for specials we should add set it details.Add(new XElement("displayseason")); details.Add(new XElement("displayepisode")); - if (image == null) - { - details.Add(new XElement("thumb")); - } - - else - { - details.Add(new XElement("thumb", image.Url)); - } - details.Add(new XElement("watched", "false")); if (episode.Ratings != null && episode.Ratings.Votes > 0) @@ -239,39 +259,39 @@ namespace NzbDrone.Core.Extras.Metadata.Consumers.Xbmc details.Add(new XElement("rating", episode.Ratings.Value)); } - if (episodeFile.MediaInfo != null) + if (trackFile.MediaInfo != null) { var fileInfo = new XElement("fileinfo"); var streamDetails = new XElement("streamdetails"); var video = new XElement("video"); - video.Add(new XElement("aspect", (float)episodeFile.MediaInfo.Width / (float)episodeFile.MediaInfo.Height)); - video.Add(new XElement("bitrate", episodeFile.MediaInfo.VideoBitrate)); - video.Add(new XElement("codec", episodeFile.MediaInfo.VideoCodec)); - video.Add(new XElement("framerate", episodeFile.MediaInfo.VideoFps)); - video.Add(new XElement("height", episodeFile.MediaInfo.Height)); - video.Add(new XElement("scantype", episodeFile.MediaInfo.ScanType)); - video.Add(new XElement("width", episodeFile.MediaInfo.Height)); - - if (episodeFile.MediaInfo.RunTime != null) + video.Add(new XElement("aspect", (float)trackFile.MediaInfo.Width / (float)trackFile.MediaInfo.Height)); + video.Add(new XElement("bitrate", trackFile.MediaInfo.VideoBitrate)); + video.Add(new XElement("codec", trackFile.MediaInfo.VideoCodec)); + video.Add(new XElement("framerate", trackFile.MediaInfo.VideoFps)); + video.Add(new XElement("height", trackFile.MediaInfo.Height)); + video.Add(new XElement("scantype", trackFile.MediaInfo.ScanType)); + video.Add(new XElement("width", trackFile.MediaInfo.Width)); + + if (trackFile.MediaInfo.RunTime != null) { - video.Add(new XElement("duration", episodeFile.MediaInfo.RunTime.TotalMinutes)); - video.Add(new XElement("durationinseconds", episodeFile.MediaInfo.RunTime.TotalSeconds)); + video.Add(new XElement("duration", trackFile.MediaInfo.RunTime.TotalMinutes)); + video.Add(new XElement("durationinseconds", trackFile.MediaInfo.RunTime.TotalSeconds)); } streamDetails.Add(video); var audio = new XElement("audio"); - audio.Add(new XElement("bitrate", episodeFile.MediaInfo.AudioBitrate)); - audio.Add(new XElement("channels", episodeFile.MediaInfo.AudioChannels)); - audio.Add(new XElement("codec", GetAudioCodec(episodeFile.MediaInfo.AudioFormat))); - audio.Add(new XElement("language", episodeFile.MediaInfo.AudioLanguages)); + audio.Add(new XElement("bitrate", trackFile.MediaInfo.AudioBitrate)); + audio.Add(new XElement("channels", trackFile.MediaInfo.AudioChannels)); + audio.Add(new XElement("codec", GetAudioCodec(trackFile.MediaInfo.AudioFormat))); + audio.Add(new XElement("language", trackFile.MediaInfo.AudioLanguages)); streamDetails.Add(audio); - if (episodeFile.MediaInfo.Subtitles != null && episodeFile.MediaInfo.Subtitles.Length > 0) + if (trackFile.MediaInfo.Subtitles != null && trackFile.MediaInfo.Subtitles.Length > 0) { var subtitle = new XElement("subtitle"); - subtitle.Add(new XElement("language", episodeFile.MediaInfo.Subtitles)); + subtitle.Add(new XElement("language", trackFile.MediaInfo.Subtitles)); streamDetails.Add(subtitle); } @@ -291,80 +311,52 @@ namespace NzbDrone.Core.Extras.Metadata.Consumers.Xbmc } } - return new MetadataFileResult(GetEpisodeMetadataFilename(episodeFile.RelativePath), xmlResult.Trim(Environment.NewLine.ToCharArray())); + return new MetadataFileResult(GetEpisodeMetadataFilename(trackFile.RelativePath), xmlResult.Trim(Environment.NewLine.ToCharArray())); } - public override List SeriesImages(Series series) + public override List ArtistImages(Artist artist) { - if (!Settings.SeriesImages) + if (!Settings.ArtistImages) { return new List(); } - return ProcessSeriesImages(series).ToList(); + return ProcessArtistImages(artist).ToList(); } - public override List SeasonImages(Series series, Season season) + public override List AlbumImages(Artist artist, Album album) { - if (!Settings.SeasonImages) + if (!Settings.AlbumImages) { return new List(); } - return ProcessSeasonImages(series, season).ToList(); + return ProcessAlbumImages(artist, album).ToList(); } - public override List EpisodeImages(Series series, EpisodeFile episodeFile) + public override List TrackImages(Artist artist, TrackFile trackFile) { - if (!Settings.EpisodeImages) - { - return new List(); - } - try - { - var screenshot = episodeFile.Episodes.Value.First().Images.SingleOrDefault(i => i.CoverType == MediaCoverTypes.Screenshot); - - if (screenshot == null) - { - _logger.Debug("Episode screenshot not available"); - return new List(); - } - - return new List - { - new ImageFileResult(GetEpisodeImageFilename(episodeFile.RelativePath), screenshot.Url) - }; - } - catch (Exception ex) - { - _logger.Error(ex, "Unable to process episode image for file: {0}", Path.Combine(series.Path, episodeFile.RelativePath)); - - return new List(); - } + return new List(); } - private IEnumerable ProcessSeriesImages(Series series) + private IEnumerable ProcessArtistImages(Artist artist) { - foreach (var image in series.Images) + foreach (var image in artist.Images) { - var source = _mediaCoverService.GetCoverPath(series.Id, image.CoverType); + var source = _mediaCoverService.GetCoverPath(artist.Id, image.CoverType); var destination = image.CoverType.ToString().ToLowerInvariant() + Path.GetExtension(source); yield return new ImageFileResult(destination, source); } } - private IEnumerable ProcessSeasonImages(Series series, Season season) + private IEnumerable ProcessAlbumImages(Artist artist, Album album) { - foreach (var image in season.Images) + foreach (var image in album.Images) { - var filename = string.Format("season{0:00}-{1}.jpg", season.SeasonNumber, image.CoverType.ToString().ToLower()); - - if (season.SeasonNumber == 0) - { - filename = string.Format("season-specials-{0}.jpg", image.CoverType.ToString().ToLower()); - } + var destination = Path.GetFileName(album.Path); + var filename = string.Format("{0}\\{1}{2}", destination, image.CoverType.ToString().ToLower(), Path.GetExtension(image.Url)); yield return new ImageFileResult(filename, image.Url); } diff --git a/src/NzbDrone.Core/Extras/Metadata/Consumers/Xbmc/XbmcMetadataSettings.cs b/src/NzbDrone.Core/Extras/Metadata/Consumers/Xbmc/XbmcMetadataSettings.cs index cd4b833ae..f093f82ca 100644 --- a/src/NzbDrone.Core/Extras/Metadata/Consumers/Xbmc/XbmcMetadataSettings.cs +++ b/src/NzbDrone.Core/Extras/Metadata/Consumers/Xbmc/XbmcMetadataSettings.cs @@ -1,4 +1,4 @@ -using FluentValidation; +using FluentValidation; using NzbDrone.Core.Annotations; using NzbDrone.Core.ThingiProvider; using NzbDrone.Core.Validation; @@ -18,28 +18,32 @@ namespace NzbDrone.Core.Extras.Metadata.Consumers.Xbmc public XbmcMetadataSettings() { - SeriesMetadata = true; - EpisodeMetadata = true; - SeriesImages = true; - SeasonImages = true; + ArtistMetadata = true; + AlbumMetadata = true; + TrackMetadata = true; + ArtistImages = true; + AlbumImages = true; EpisodeImages = true; } - [FieldDefinition(0, Label = "Series Metadata", Type = FieldType.Checkbox)] - public bool SeriesMetadata { get; set; } + [FieldDefinition(0, Label = "Artist Metadata", Type = FieldType.Checkbox)] + public bool ArtistMetadata { get; set; } - [FieldDefinition(1, Label = "Episode Metadata", Type = FieldType.Checkbox)] - public bool EpisodeMetadata { get; set; } + [FieldDefinition(1, Label = "Album Metadata", Type = FieldType.Checkbox)] + public bool AlbumMetadata { get; set; } - [FieldDefinition(2, Label = "Series Images", Type = FieldType.Checkbox)] - public bool SeriesImages { get; set; } + [FieldDefinition(2, Label = "Track Metadata", Type = FieldType.Checkbox)] + public bool TrackMetadata { get; set; } - [FieldDefinition(3, Label = "Season Images", Type = FieldType.Checkbox)] - public bool SeasonImages { get; set; } + [FieldDefinition(3, Label = "Artist Images", Type = FieldType.Checkbox)] + public bool ArtistImages { get; set; } - [FieldDefinition(4, Label = "Episode Images", Type = FieldType.Checkbox)] + [FieldDefinition(4, Label = "Album Images", Type = FieldType.Checkbox)] + public bool AlbumImages { get; set; } + + [FieldDefinition(5, Label = "Episode Images", Type = FieldType.Checkbox)] public bool EpisodeImages { get; set; } - + public bool IsValid => true; public NzbDroneValidationResult Validate() diff --git a/src/NzbDrone.Core/Extras/Metadata/ExistingMetadataImporter.cs b/src/NzbDrone.Core/Extras/Metadata/ExistingMetadataImporter.cs index fa271f575..be52758d0 100644 --- a/src/NzbDrone.Core/Extras/Metadata/ExistingMetadataImporter.cs +++ b/src/NzbDrone.Core/Extras/Metadata/ExistingMetadataImporter.cs @@ -1,13 +1,13 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.IO; using System.Linq; using NLog; using NzbDrone.Common.Extensions; using NzbDrone.Core.Extras.Files; using NzbDrone.Core.Extras.Metadata.Files; -using NzbDrone.Core.Extras.Subtitles; +using NzbDrone.Core.Extras.Lyrics; using NzbDrone.Core.Parser; -using NzbDrone.Core.Tv; +using NzbDrone.Core.Music; namespace NzbDrone.Core.Extras.Metadata { @@ -32,56 +32,56 @@ namespace NzbDrone.Core.Extras.Metadata public override int Order => 0; - public override IEnumerable ProcessFiles(Series series, List filesOnDisk, List importedFiles) + public override IEnumerable ProcessFiles(Artist artist, List filesOnDisk, List importedFiles) { - _logger.Debug("Looking for existing metadata in {0}", series.Path); + _logger.Debug("Looking for existing metadata in {0}", artist.Path); var metadataFiles = new List(); - var filterResult = FilterAndClean(series, filesOnDisk, importedFiles); + var filterResult = FilterAndClean(artist, filesOnDisk, importedFiles); foreach (var possibleMetadataFile in filterResult.FilesOnDisk) { // Don't process files that have known Subtitle file extensions (saves a bit of unecessary processing) - if (SubtitleFileExtensions.Extensions.Contains(Path.GetExtension(possibleMetadataFile))) + if (LyricFileExtensions.Extensions.Contains(Path.GetExtension(possibleMetadataFile))) { continue; } foreach (var consumer in _consumers) { - var metadata = consumer.FindMetadataFile(series, possibleMetadataFile); + var metadata = consumer.FindMetadataFile(artist, possibleMetadataFile); if (metadata == null) { continue; } - if (metadata.Type == MetadataType.EpisodeImage || - metadata.Type == MetadataType.EpisodeMetadata) + if (metadata.Type == MetadataType.TrackImage || + metadata.Type == MetadataType.TrackMetadata) { - var localEpisode = _parsingService.GetLocalEpisode(possibleMetadataFile, series); + var localTrack = _parsingService.GetLocalTrack(possibleMetadataFile, artist); - if (localEpisode == null) + if (localTrack == null) { _logger.Debug("Unable to parse extra file: {0}", possibleMetadataFile); continue; } - if (localEpisode.Episodes.Empty()) + if (localTrack.Tracks.Empty()) { _logger.Debug("Cannot find related episodes for: {0}", possibleMetadataFile); continue; } - if (localEpisode.Episodes.DistinctBy(e => e.EpisodeFileId).Count() > 1) + if (localTrack.Tracks.DistinctBy(e => e.TrackFileId).Count() > 1) { _logger.Debug("Extra file: {0} does not match existing files.", possibleMetadataFile); continue; } - metadata.SeasonNumber = localEpisode.SeasonNumber; - metadata.EpisodeFileId = localEpisode.Episodes.First().EpisodeFileId; + metadata.AlbumId = localTrack.Album.Id; + metadata.TrackFileId = localTrack.Tracks.First().TrackFileId; } metadata.Extension = Path.GetExtension(possibleMetadataFile); diff --git a/src/NzbDrone.Core/Extras/Metadata/Files/CleanMetadataFileService.cs b/src/NzbDrone.Core/Extras/Metadata/Files/CleanMetadataFileService.cs index 6166ae20b..1fe7ea41a 100644 --- a/src/NzbDrone.Core/Extras/Metadata/Files/CleanMetadataFileService.cs +++ b/src/NzbDrone.Core/Extras/Metadata/Files/CleanMetadataFileService.cs @@ -1,13 +1,13 @@ -using System.IO; +using System.IO; using NLog; using NzbDrone.Common.Disk; -using NzbDrone.Core.Tv; +using NzbDrone.Core.Music; namespace NzbDrone.Core.Extras.Metadata.Files { public interface ICleanMetadataService { - void Clean(Series series); + void Clean(Artist artist); } public class CleanExtraFileService : ICleanMetadataService @@ -25,15 +25,15 @@ namespace NzbDrone.Core.Extras.Metadata.Files _logger = logger; } - public void Clean(Series series) + public void Clean(Artist artist) { - _logger.Debug("Cleaning missing metadata files for series: {0}", series.Title); + _logger.Debug("Cleaning missing metadata files for artist: {0}", artist.Name); - var metadataFiles = _metadataFileService.GetFilesBySeries(series.Id); + var metadataFiles = _metadataFileService.GetFilesByArtist(artist.Id); foreach (var metadataFile in metadataFiles) { - if (!_diskProvider.FileExists(Path.Combine(series.Path, metadataFile.RelativePath))) + if (!_diskProvider.FileExists(Path.Combine(artist.Path, metadataFile.RelativePath))) { _logger.Debug("Deleting metadata file from database: {0}", metadataFile.RelativePath); _metadataFileService.Delete(metadataFile.Id); diff --git a/src/NzbDrone.Core/Extras/Metadata/Files/MetadataFileService.cs b/src/NzbDrone.Core/Extras/Metadata/Files/MetadataFileService.cs index f5fc2ba69..571fec828 100644 --- a/src/NzbDrone.Core/Extras/Metadata/Files/MetadataFileService.cs +++ b/src/NzbDrone.Core/Extras/Metadata/Files/MetadataFileService.cs @@ -1,8 +1,8 @@ -using NLog; +using NLog; using NzbDrone.Common.Disk; using NzbDrone.Core.Extras.Files; using NzbDrone.Core.MediaFiles; -using NzbDrone.Core.Tv; +using NzbDrone.Core.Music; namespace NzbDrone.Core.Extras.Metadata.Files { @@ -12,8 +12,8 @@ namespace NzbDrone.Core.Extras.Metadata.Files public class MetadataFileService : ExtraFileService, IMetadataFileService { - public MetadataFileService(IExtraFileRepository repository, ISeriesService seriesService, IDiskProvider diskProvider, IRecycleBinProvider recycleBinProvider, Logger logger) - : base(repository, seriesService, diskProvider, recycleBinProvider, logger) + public MetadataFileService(IExtraFileRepository repository, IArtistService artistService, IDiskProvider diskProvider, IRecycleBinProvider recycleBinProvider, Logger logger) + : base(repository, artistService, diskProvider, recycleBinProvider, logger) { } diff --git a/src/NzbDrone.Core/Extras/Metadata/IMetadata.cs b/src/NzbDrone.Core/Extras/Metadata/IMetadata.cs index b631425e6..31c3cc17f 100644 --- a/src/NzbDrone.Core/Extras/Metadata/IMetadata.cs +++ b/src/NzbDrone.Core/Extras/Metadata/IMetadata.cs @@ -1,19 +1,20 @@ -using System.Collections.Generic; +using System.Collections.Generic; using NzbDrone.Core.Extras.Metadata.Files; using NzbDrone.Core.MediaFiles; using NzbDrone.Core.ThingiProvider; -using NzbDrone.Core.Tv; +using NzbDrone.Core.Music; namespace NzbDrone.Core.Extras.Metadata { public interface IMetadata : IProvider { - string GetFilenameAfterMove(Series series, EpisodeFile episodeFile, MetadataFile metadataFile); - MetadataFile FindMetadataFile(Series series, string path); - MetadataFileResult SeriesMetadata(Series series); - MetadataFileResult EpisodeMetadata(Series series, EpisodeFile episodeFile); - List SeriesImages(Series series); - List SeasonImages(Series series, Season season); - List EpisodeImages(Series series, EpisodeFile episodeFile); + string GetFilenameAfterMove(Artist artist, TrackFile trackFile, MetadataFile metadataFile); + MetadataFile FindMetadataFile(Artist artist, string path); + MetadataFileResult ArtistMetadata(Artist artist); + MetadataFileResult AlbumMetadata(Artist artist, Album album); + MetadataFileResult TrackMetadata(Artist artist, TrackFile trackFile); + List ArtistImages(Artist artist); + List AlbumImages(Artist artist, Album album); + List TrackImages(Artist artist, TrackFile trackFile); } } diff --git a/src/NzbDrone.Core/Extras/Metadata/MetadataBase.cs b/src/NzbDrone.Core/Extras/Metadata/MetadataBase.cs index f60928703..4897d2cdf 100644 --- a/src/NzbDrone.Core/Extras/Metadata/MetadataBase.cs +++ b/src/NzbDrone.Core/Extras/Metadata/MetadataBase.cs @@ -1,11 +1,11 @@ -using System; +using System; using System.Collections.Generic; using System.IO; using FluentValidation.Results; using NzbDrone.Core.Extras.Metadata.Files; using NzbDrone.Core.MediaFiles; using NzbDrone.Core.ThingiProvider; -using NzbDrone.Core.Tv; +using NzbDrone.Core.Music; namespace NzbDrone.Core.Extras.Metadata { @@ -26,22 +26,23 @@ namespace NzbDrone.Core.Extras.Metadata return new ValidationResult(); } - public virtual string GetFilenameAfterMove(Series series, EpisodeFile episodeFile, MetadataFile metadataFile) + public virtual string GetFilenameAfterMove(Artist artist, TrackFile trackFile, MetadataFile metadataFile) { - var existingFilename = Path.Combine(series.Path, metadataFile.RelativePath); + var existingFilename = Path.Combine(artist.Path, metadataFile.RelativePath); var extension = Path.GetExtension(existingFilename).TrimStart('.'); - var newFileName = Path.ChangeExtension(Path.Combine(series.Path, episodeFile.RelativePath), extension); + var newFileName = Path.ChangeExtension(Path.Combine(artist.Path, trackFile.RelativePath), extension); return newFileName; } - public abstract MetadataFile FindMetadataFile(Series series, string path); + public abstract MetadataFile FindMetadataFile(Artist artist, string path); - public abstract MetadataFileResult SeriesMetadata(Series series); - public abstract MetadataFileResult EpisodeMetadata(Series series, EpisodeFile episodeFile); - public abstract List SeriesImages(Series series); - public abstract List SeasonImages(Series series, Season season); - public abstract List EpisodeImages(Series series, EpisodeFile episodeFile); + public abstract MetadataFileResult ArtistMetadata(Artist artist); + public abstract MetadataFileResult AlbumMetadata(Artist artist, Album album); + public abstract MetadataFileResult TrackMetadata(Artist artist, TrackFile trackFile); + public abstract List ArtistImages(Artist artist); + public abstract List AlbumImages(Artist artist, Album album); + public abstract List TrackImages(Artist artist, TrackFile trackFile); public virtual object RequestAction(string action, IDictionary query) { return null; } diff --git a/src/NzbDrone.Core/Extras/Metadata/MetadataService.cs b/src/NzbDrone.Core/Extras/Metadata/MetadataService.cs index dbe4c1fba..003350eaf 100644 --- a/src/NzbDrone.Core/Extras/Metadata/MetadataService.cs +++ b/src/NzbDrone.Core/Extras/Metadata/MetadataService.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.IO; using System.Linq; @@ -11,7 +11,7 @@ using NzbDrone.Core.Configuration; using NzbDrone.Core.Extras.Files; using NzbDrone.Core.Extras.Metadata.Files; using NzbDrone.Core.MediaFiles; -using NzbDrone.Core.Tv; +using NzbDrone.Core.Music; namespace NzbDrone.Core.Extras.Metadata { @@ -24,6 +24,7 @@ namespace NzbDrone.Core.Extras.Metadata private readonly IHttpClient _httpClient; private readonly IMediaFileAttributeService _mediaFileAttributeService; private readonly IMetadataFileService _metadataFileService; + private readonly IAlbumService _albumService; private readonly Logger _logger; public MetadataService(IConfigService configService, @@ -34,6 +35,7 @@ namespace NzbDrone.Core.Extras.Metadata IHttpClient httpClient, IMediaFileAttributeService mediaFileAttributeService, IMetadataFileService metadataFileService, + IAlbumService albumService, Logger logger) : base(configService, diskProvider, diskTransferService, logger) { @@ -44,19 +46,20 @@ namespace NzbDrone.Core.Extras.Metadata _httpClient = httpClient; _mediaFileAttributeService = mediaFileAttributeService; _metadataFileService = metadataFileService; + _albumService = albumService; _logger = logger; } public override int Order => 0; - public override IEnumerable CreateAfterSeriesScan(Series series, List episodeFiles) + public override IEnumerable CreateAfterArtistScan(Artist artist, List albums, List trackFiles) { - var metadataFiles = _metadataFileService.GetFilesBySeries(series.Id); - _cleanMetadataService.Clean(series); + var metadataFiles = _metadataFileService.GetFilesByArtist(artist.Id); + _cleanMetadataService.Clean(artist); - if (!_diskProvider.FolderExists(series.Path)) + if (!_diskProvider.FolderExists(artist.Path)) { - _logger.Info("Series folder does not exist, skipping metadata creation"); + _logger.Info("Artist folder does not exist, skipping metadata creation"); return Enumerable.Empty(); } @@ -66,14 +69,20 @@ namespace NzbDrone.Core.Extras.Metadata { var consumerFiles = GetMetadataFilesForConsumer(consumer, metadataFiles); - files.AddIfNotNull(ProcessSeriesMetadata(consumer, series, consumerFiles)); - files.AddRange(ProcessSeriesImages(consumer, series, consumerFiles)); - files.AddRange(ProcessSeasonImages(consumer, series, consumerFiles)); + files.AddIfNotNull(ProcessArtistMetadata(consumer, artist, consumerFiles)); + files.AddRange(ProcessArtistImages(consumer, artist, consumerFiles)); + files.AddRange(ProcessAlbumImages(consumer, artist, consumerFiles)); - foreach (var episodeFile in episodeFiles) + foreach (var album in albums) { - files.AddIfNotNull(ProcessEpisodeMetadata(consumer, series, episodeFile, consumerFiles)); - files.AddRange(ProcessEpisodeImages(consumer, series, episodeFile, consumerFiles)); + album.Artist = artist; + files.AddIfNotNull(ProcessAlbumMetadata(consumer, album, consumerFiles)); + } + + foreach (var trackFile in trackFiles) + { + files.AddIfNotNull(ProcessEpisodeMetadata(consumer, artist, trackFile, consumerFiles)); + files.AddRange(ProcessEpisodeImages(consumer, artist, trackFile, consumerFiles)); } } @@ -82,15 +91,15 @@ namespace NzbDrone.Core.Extras.Metadata return files; } - public override IEnumerable CreateAfterEpisodeImport(Series series, EpisodeFile episodeFile) + public override IEnumerable CreateAfterTrackImport(Artist artist, TrackFile trackFile) { var files = new List(); foreach (var consumer in _metadataFactory.Enabled()) { - files.AddIfNotNull(ProcessEpisodeMetadata(consumer, series, episodeFile, new List())); - files.AddRange(ProcessEpisodeImages(consumer, series, episodeFile, new List())); + files.AddIfNotNull(ProcessEpisodeMetadata(consumer, artist, trackFile, new List())); + files.AddRange(ProcessEpisodeImages(consumer, artist, trackFile, new List())); } _metadataFileService.Upsert(files); @@ -98,11 +107,11 @@ namespace NzbDrone.Core.Extras.Metadata return files; } - public override IEnumerable CreateAfterEpisodeImport(Series series, string seriesFolder, string seasonFolder) + public override IEnumerable CreateAfterTrackImport(Artist artist, string artistFolder, string albumFolder) { - var metadataFiles = _metadataFileService.GetFilesBySeries(series.Id); + var metadataFiles = _metadataFileService.GetFilesByArtist(artist.Id); - if (seriesFolder.IsNullOrWhiteSpace() && seasonFolder.IsNullOrWhiteSpace()) + if (artistFolder.IsNullOrWhiteSpace() && albumFolder.IsNullOrWhiteSpace()) { return new List(); } @@ -113,15 +122,15 @@ namespace NzbDrone.Core.Extras.Metadata { var consumerFiles = GetMetadataFilesForConsumer(consumer, metadataFiles); - if (seriesFolder.IsNotNullOrWhiteSpace()) + if (artistFolder.IsNotNullOrWhiteSpace()) { - files.AddIfNotNull(ProcessSeriesMetadata(consumer, series, consumerFiles)); - files.AddRange(ProcessSeriesImages(consumer, series, consumerFiles)); + files.AddIfNotNull(ProcessArtistMetadata(consumer, artist, consumerFiles)); + files.AddRange(ProcessArtistImages(consumer, artist, consumerFiles)); } - if (seasonFolder.IsNotNullOrWhiteSpace()) + if (albumFolder.IsNotNullOrWhiteSpace()) { - files.AddRange(ProcessSeasonImages(consumer, series, consumerFiles)); + files.AddRange(ProcessAlbumImages(consumer, artist, consumerFiles)); } } @@ -130,9 +139,9 @@ namespace NzbDrone.Core.Extras.Metadata return files; } - public override IEnumerable MoveFilesAfterRename(Series series, List episodeFiles) + public override IEnumerable MoveFilesAfterRename(Artist artist, List trackFiles) { - var metadataFiles = _metadataFileService.GetFilesBySeries(series.Id); + var metadataFiles = _metadataFileService.GetFilesByArtist(artist.Id); var movedFiles = new List(); // TODO: Move EpisodeImage and EpisodeMetadata metadata files, instead of relying on consumers to do it @@ -140,21 +149,21 @@ namespace NzbDrone.Core.Extras.Metadata foreach (var consumer in _metadataFactory.GetAvailableProviders()) { - foreach (var episodeFile in episodeFiles) + foreach (var trackFile in trackFiles) { - var metadataFilesForConsumer = GetMetadataFilesForConsumer(consumer, metadataFiles).Where(m => m.EpisodeFileId == episodeFile.Id).ToList(); + var metadataFilesForConsumer = GetMetadataFilesForConsumer(consumer, metadataFiles).Where(m => m.TrackFileId == trackFile.Id).ToList(); foreach (var metadataFile in metadataFilesForConsumer) { - var newFileName = consumer.GetFilenameAfterMove(series, episodeFile, metadataFile); - var existingFileName = Path.Combine(series.Path, metadataFile.RelativePath); + var newFileName = consumer.GetFilenameAfterMove(artist, trackFile, metadataFile); + var existingFileName = Path.Combine(artist.Path, metadataFile.RelativePath); if (newFileName.PathNotEquals(existingFileName)) { try { _diskProvider.MoveFile(existingFileName, newFileName); - metadataFile.RelativePath = series.Path.GetRelativePath(newFileName); + metadataFile.RelativePath = artist.Path.GetRelativePath(newFileName); movedFiles.Add(metadataFile); } catch (Exception ex) @@ -171,40 +180,84 @@ namespace NzbDrone.Core.Extras.Metadata return movedFiles; } - public override ExtraFile Import(Series series, EpisodeFile episodeFile, string path, string extension, bool readOnly) + public override ExtraFile Import(Artist artist, TrackFile trackFile, string path, string extension, bool readOnly) { return null; } - private List GetMetadataFilesForConsumer(IMetadata consumer, List seriesMetadata) + private List GetMetadataFilesForConsumer(IMetadata consumer, List artistMetadata) { - return seriesMetadata.Where(c => c.Consumer == consumer.GetType().Name).ToList(); + return artistMetadata.Where(c => c.Consumer == consumer.GetType().Name).ToList(); + } + + private MetadataFile ProcessArtistMetadata(IMetadata consumer, Artist artist, List existingMetadataFiles) + { + var artistMetadata = consumer.ArtistMetadata(artist); + + if (artistMetadata == null) + { + return null; + } + + var hash = artistMetadata.Contents.SHA256Hash(); + + var metadata = GetMetadataFile(artist, existingMetadataFiles, e => e.Type == MetadataType.ArtistMetadata) ?? + new MetadataFile + { + ArtistId = artist.Id, + Consumer = consumer.GetType().Name, + Type = MetadataType.ArtistMetadata + }; + + if (hash == metadata.Hash) + { + if (artistMetadata.RelativePath != metadata.RelativePath) + { + metadata.RelativePath = artistMetadata.RelativePath; + + return metadata; + } + + return null; + } + + var fullPath = Path.Combine(artist.Path, artistMetadata.RelativePath); + + _logger.Debug("Writing Artist Metadata to: {0}", fullPath); + SaveMetadataFile(fullPath, artistMetadata.Contents); + + metadata.Hash = hash; + metadata.RelativePath = artistMetadata.RelativePath; + metadata.Extension = Path.GetExtension(fullPath); + + return metadata; } - private MetadataFile ProcessSeriesMetadata(IMetadata consumer, Series series, List existingMetadataFiles) + private MetadataFile ProcessAlbumMetadata(IMetadata consumer, Album album, List existingMetadataFiles) { - var seriesMetadata = consumer.SeriesMetadata(series); + var albumMetadata = consumer.AlbumMetadata(album.Artist, album); - if (seriesMetadata == null) + if (albumMetadata == null) { return null; } - var hash = seriesMetadata.Contents.SHA256Hash(); + var hash = albumMetadata.Contents.SHA256Hash(); - var metadata = GetMetadataFile(series, existingMetadataFiles, e => e.Type == MetadataType.SeriesMetadata) ?? + var metadata = GetMetadataFile(album.Artist, existingMetadataFiles, e => e.Type == MetadataType.AlbumMetadata && e.AlbumId == album.Id) ?? new MetadataFile { - SeriesId = series.Id, + ArtistId = album.ArtistId, + AlbumId = album.Id, Consumer = consumer.GetType().Name, - Type = MetadataType.SeriesMetadata + Type = MetadataType.AlbumMetadata }; if (hash == metadata.Hash) { - if (seriesMetadata.RelativePath != metadata.RelativePath) + if (albumMetadata.RelativePath != metadata.RelativePath) { - metadata.RelativePath = seriesMetadata.RelativePath; + metadata.RelativePath = albumMetadata.RelativePath; return metadata; } @@ -212,35 +265,35 @@ namespace NzbDrone.Core.Extras.Metadata return null; } - var fullPath = Path.Combine(series.Path, seriesMetadata.RelativePath); + var fullPath = Path.Combine(album.Path, albumMetadata.RelativePath); - _logger.Debug("Writing Series Metadata to: {0}", fullPath); - SaveMetadataFile(fullPath, seriesMetadata.Contents); + _logger.Debug("Writing Album Metadata to: {0}", fullPath); + SaveMetadataFile(fullPath, albumMetadata.Contents); metadata.Hash = hash; - metadata.RelativePath = seriesMetadata.RelativePath; + metadata.RelativePath = albumMetadata.RelativePath; metadata.Extension = Path.GetExtension(fullPath); return metadata; } - private MetadataFile ProcessEpisodeMetadata(IMetadata consumer, Series series, EpisodeFile episodeFile, List existingMetadataFiles) + private MetadataFile ProcessEpisodeMetadata(IMetadata consumer, Artist artist, TrackFile trackFile, List existingMetadataFiles) { - var episodeMetadata = consumer.EpisodeMetadata(series, episodeFile); + var episodeMetadata = consumer.TrackMetadata(artist, trackFile); if (episodeMetadata == null) { return null; } - var fullPath = Path.Combine(series.Path, episodeMetadata.RelativePath); + var fullPath = Path.Combine(artist.Path, episodeMetadata.RelativePath); - var existingMetadata = GetMetadataFile(series, existingMetadataFiles, c => c.Type == MetadataType.EpisodeMetadata && - c.EpisodeFileId == episodeFile.Id); + var existingMetadata = GetMetadataFile(artist, existingMetadataFiles, c => c.Type == MetadataType.TrackMetadata && + c.TrackFileId == trackFile.Id); if (existingMetadata != null) { - var existingFullPath = Path.Combine(series.Path, existingMetadata.RelativePath); + var existingFullPath = Path.Combine(artist.Path, existingMetadata.RelativePath); if (fullPath.PathNotEquals(existingFullPath)) { _diskTransferService.TransferFile(existingFullPath, fullPath, TransferMode.Move); @@ -253,11 +306,11 @@ namespace NzbDrone.Core.Extras.Metadata var metadata = existingMetadata ?? new MetadataFile { - SeriesId = series.Id, - SeasonNumber = episodeFile.SeasonNumber, - EpisodeFileId = episodeFile.Id, + ArtistId = artist.Id, + AlbumId = trackFile.AlbumId, + TrackFileId = trackFile.Id, Consumer = consumer.GetType().Name, - Type = MetadataType.EpisodeMetadata, + Type = MetadataType.TrackMetadata, RelativePath = episodeMetadata.RelativePath, Extension = Path.GetExtension(fullPath) }; @@ -267,7 +320,7 @@ namespace NzbDrone.Core.Extras.Metadata return null; } - _logger.Debug("Writing Episode Metadata to: {0}", fullPath); + _logger.Debug("Writing Track Metadata to: {0}", fullPath); SaveMetadataFile(fullPath, episodeMetadata.Contents); metadata.Hash = hash; @@ -275,32 +328,32 @@ namespace NzbDrone.Core.Extras.Metadata return metadata; } - private List ProcessSeriesImages(IMetadata consumer, Series series, List existingMetadataFiles) + private List ProcessArtistImages(IMetadata consumer, Artist artist, List existingMetadataFiles) { var result = new List(); - foreach (var image in consumer.SeriesImages(series)) + foreach (var image in consumer.ArtistImages(artist)) { - var fullPath = Path.Combine(series.Path, image.RelativePath); + var fullPath = Path.Combine(artist.Path, image.RelativePath); if (_diskProvider.FileExists(fullPath)) { - _logger.Debug("Series image already exists: {0}", fullPath); + _logger.Debug("Artist image already exists: {0}", fullPath); continue; } - var metadata = GetMetadataFile(series, existingMetadataFiles, c => c.Type == MetadataType.SeriesImage && + var metadata = GetMetadataFile(artist, existingMetadataFiles, c => c.Type == MetadataType.ArtistImage && c.RelativePath == image.RelativePath) ?? new MetadataFile { - SeriesId = series.Id, + ArtistId = artist.Id, Consumer = consumer.GetType().Name, - Type = MetadataType.SeriesImage, + Type = MetadataType.ArtistImage, RelativePath = image.RelativePath, Extension = Path.GetExtension(fullPath) }; - DownloadImage(series, image); + DownloadImage(artist, image); result.Add(metadata); } @@ -308,36 +361,38 @@ namespace NzbDrone.Core.Extras.Metadata return result; } - private List ProcessSeasonImages(IMetadata consumer, Series series, List existingMetadataFiles) + private List ProcessAlbumImages(IMetadata consumer, Artist artist, List existingMetadataFiles) { var result = new List(); - foreach (var season in series.Seasons) + var albums = _albumService.GetAlbumsByArtist(artist.Id); + + foreach (var album in albums) { - foreach (var image in consumer.SeasonImages(series, season)) + foreach (var image in consumer.AlbumImages(artist, album)) { - var fullPath = Path.Combine(series.Path, image.RelativePath); + var fullPath = Path.Combine(artist.Path, image.RelativePath); if (_diskProvider.FileExists(fullPath)) { - _logger.Debug("Season image already exists: {0}", fullPath); + _logger.Debug("Album image already exists: {0}", fullPath); continue; } - var metadata = GetMetadataFile(series, existingMetadataFiles, c => c.Type == MetadataType.SeasonImage && - c.SeasonNumber == season.SeasonNumber && + var metadata = GetMetadataFile(artist, existingMetadataFiles, c => c.Type == MetadataType.AlbumImage && + c.AlbumId == album.Id && c.RelativePath == image.RelativePath) ?? new MetadataFile { - SeriesId = series.Id, - SeasonNumber = season.SeasonNumber, + ArtistId = artist.Id, + AlbumId = album.Id, Consumer = consumer.GetType().Name, - Type = MetadataType.SeasonImage, + Type = MetadataType.AlbumImage, RelativePath = image.RelativePath, Extension = Path.GetExtension(fullPath) }; - DownloadImage(series, image); + DownloadImage(artist, image); result.Add(metadata); } @@ -346,26 +401,26 @@ namespace NzbDrone.Core.Extras.Metadata return result; } - private List ProcessEpisodeImages(IMetadata consumer, Series series, EpisodeFile episodeFile, List existingMetadataFiles) + private List ProcessEpisodeImages(IMetadata consumer, Artist artist, TrackFile trackFile, List existingMetadataFiles) { var result = new List(); - foreach (var image in consumer.EpisodeImages(series, episodeFile)) + foreach (var image in consumer.TrackImages(artist, trackFile)) { - var fullPath = Path.Combine(series.Path, image.RelativePath); + var fullPath = Path.Combine(artist.Path, image.RelativePath); if (_diskProvider.FileExists(fullPath)) { - _logger.Debug("Episode image already exists: {0}", fullPath); + _logger.Debug("Track image already exists: {0}", fullPath); continue; } - var existingMetadata = GetMetadataFile(series, existingMetadataFiles, c => c.Type == MetadataType.EpisodeImage && - c.EpisodeFileId == episodeFile.Id); + var existingMetadata = GetMetadataFile(artist, existingMetadataFiles, c => c.Type == MetadataType.TrackImage && + c.TrackFileId == trackFile.Id); if (existingMetadata != null) { - var existingFullPath = Path.Combine(series.Path, existingMetadata.RelativePath); + var existingFullPath = Path.Combine(artist.Path, existingMetadata.RelativePath); if (fullPath.PathNotEquals(existingFullPath)) { _diskTransferService.TransferFile(existingFullPath, fullPath, TransferMode.Move); @@ -378,16 +433,16 @@ namespace NzbDrone.Core.Extras.Metadata var metadata = existingMetadata ?? new MetadataFile { - SeriesId = series.Id, - SeasonNumber = episodeFile.SeasonNumber, - EpisodeFileId = episodeFile.Id, + ArtistId = artist.Id, + AlbumId = trackFile.AlbumId, + TrackFileId = trackFile.Id, Consumer = consumer.GetType().Name, - Type = MetadataType.EpisodeImage, + Type = MetadataType.TrackImage, RelativePath = image.RelativePath, Extension = Path.GetExtension(fullPath) }; - DownloadImage(series, image); + DownloadImage(artist, image); result.Add(metadata); } @@ -395,9 +450,9 @@ namespace NzbDrone.Core.Extras.Metadata return result; } - private void DownloadImage(Series series, ImageFileResult image) + private void DownloadImage(Artist artist, ImageFileResult image) { - var fullPath = Path.Combine(series.Path, image.RelativePath); + var fullPath = Path.Combine(artist.Path, image.RelativePath); try { @@ -413,11 +468,11 @@ namespace NzbDrone.Core.Extras.Metadata } catch (WebException ex) { - _logger.Warn(ex, "Couldn't download image {0} for {1}. {2}", image.Url, series, ex.Message); + _logger.Warn(ex, "Couldn't download image {0} for {1}. {2}", image.Url, artist, ex.Message); } catch (Exception ex) { - _logger.Error(ex, "Couldn't download image {0} for {1}. {2}", image.Url, series, ex.Message); + _logger.Error(ex, "Couldn't download image {0} for {1}. {2}", image.Url, artist, ex.Message); } } @@ -427,7 +482,7 @@ namespace NzbDrone.Core.Extras.Metadata _mediaFileAttributeService.SetFilePermissions(path); } - private MetadataFile GetMetadataFile(Series series, List existingMetadataFiles, Func predicate) + private MetadataFile GetMetadataFile(Artist artist, List existingMetadataFiles, Func predicate) { var matchingMetadataFiles = existingMetadataFiles.Where(predicate).ToList(); @@ -439,7 +494,7 @@ namespace NzbDrone.Core.Extras.Metadata //Remove duplicate metadata files from DB and disk foreach (var file in matchingMetadataFiles.Skip(1)) { - var path = Path.Combine(series.Path, file.RelativePath); + var path = Path.Combine(artist.Path, file.RelativePath); _logger.Debug("Removing duplicate Metadata file: {0}", path); diff --git a/src/NzbDrone.Core/Extras/Metadata/MetadataType.cs b/src/NzbDrone.Core/Extras/Metadata/MetadataType.cs index 849bc31dd..2a827b48e 100644 --- a/src/NzbDrone.Core/Extras/Metadata/MetadataType.cs +++ b/src/NzbDrone.Core/Extras/Metadata/MetadataType.cs @@ -1,12 +1,13 @@ -namespace NzbDrone.Core.Extras.Metadata +namespace NzbDrone.Core.Extras.Metadata { public enum MetadataType { Unknown = 0, - SeriesMetadata = 1, - EpisodeMetadata = 2, - SeriesImage = 3, - SeasonImage = 4, - EpisodeImage = 5 + ArtistMetadata = 1, + TrackMetadata = 2, + ArtistImage = 3, + AlbumImage = 4, + TrackImage = 5, + AlbumMetadata = 6 } } diff --git a/src/NzbDrone.Core/Extras/Others/ExistingOtherExtraImporter.cs b/src/NzbDrone.Core/Extras/Others/ExistingOtherExtraImporter.cs index 05afb5645..95abfebdc 100644 --- a/src/NzbDrone.Core/Extras/Others/ExistingOtherExtraImporter.cs +++ b/src/NzbDrone.Core/Extras/Others/ExistingOtherExtraImporter.cs @@ -1,11 +1,11 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.IO; using System.Linq; using NLog; using NzbDrone.Common.Extensions; using NzbDrone.Core.Extras.Files; using NzbDrone.Core.Parser; -using NzbDrone.Core.Tv; +using NzbDrone.Core.Music; namespace NzbDrone.Core.Extras.Others { @@ -27,12 +27,12 @@ namespace NzbDrone.Core.Extras.Others public override int Order => 2; - public override IEnumerable ProcessFiles(Series series, List filesOnDisk, List importedFiles) + public override IEnumerable ProcessFiles(Artist artist, List filesOnDisk, List importedFiles) { - _logger.Debug("Looking for existing extra files in {0}", series.Path); + _logger.Debug("Looking for existing extra files in {0}", artist.Path); var extraFiles = new List(); - var filterResult = FilterAndClean(series, filesOnDisk, importedFiles); + var filterResult = FilterAndClean(artist, filesOnDisk, importedFiles); foreach (var possibleExtraFile in filterResult.FilesOnDisk) { @@ -44,21 +44,21 @@ namespace NzbDrone.Core.Extras.Others continue; } - var localEpisode = _parsingService.GetLocalEpisode(possibleExtraFile, series); + var localTrack = _parsingService.GetLocalTrack(possibleExtraFile, artist); - if (localEpisode == null) + if (localTrack == null) { _logger.Debug("Unable to parse extra file: {0}", possibleExtraFile); continue; } - if (localEpisode.Episodes.Empty()) + if (localTrack.Tracks.Empty()) { - _logger.Debug("Cannot find related episodes for: {0}", possibleExtraFile); + _logger.Debug("Cannot find related tracks for: {0}", possibleExtraFile); continue; } - if (localEpisode.Episodes.DistinctBy(e => e.EpisodeFileId).Count() > 1) + if (localTrack.Tracks.DistinctBy(e => e.TrackFileId).Count() > 1) { _logger.Debug("Extra file: {0} does not match existing files.", possibleExtraFile); continue; @@ -66,10 +66,10 @@ namespace NzbDrone.Core.Extras.Others var extraFile = new OtherExtraFile { - SeriesId = series.Id, - SeasonNumber = localEpisode.SeasonNumber, - EpisodeFileId = localEpisode.Episodes.First().EpisodeFileId, - RelativePath = series.Path.GetRelativePath(possibleExtraFile), + ArtistId = artist.Id, + AlbumId = localTrack.Album.Id, + TrackFileId = localTrack.Tracks.First().TrackFileId, + RelativePath = artist.Path.GetRelativePath(possibleExtraFile), Extension = extension }; diff --git a/src/NzbDrone.Core/Extras/Others/OtherExtraFileService.cs b/src/NzbDrone.Core/Extras/Others/OtherExtraFileService.cs index ceeb15ff8..063d75f86 100644 --- a/src/NzbDrone.Core/Extras/Others/OtherExtraFileService.cs +++ b/src/NzbDrone.Core/Extras/Others/OtherExtraFileService.cs @@ -1,8 +1,8 @@ -using NLog; +using NLog; using NzbDrone.Common.Disk; using NzbDrone.Core.Extras.Files; using NzbDrone.Core.MediaFiles; -using NzbDrone.Core.Tv; +using NzbDrone.Core.Music; namespace NzbDrone.Core.Extras.Others { @@ -12,8 +12,8 @@ namespace NzbDrone.Core.Extras.Others public class OtherExtraFileService : ExtraFileService, IOtherExtraFileService { - public OtherExtraFileService(IExtraFileRepository repository, ISeriesService seriesService, IDiskProvider diskProvider, IRecycleBinProvider recycleBinProvider, Logger logger) - : base(repository, seriesService, diskProvider, recycleBinProvider, logger) + public OtherExtraFileService(IExtraFileRepository repository, IArtistService artistService, IDiskProvider diskProvider, IRecycleBinProvider recycleBinProvider, Logger logger) + : base(repository, artistService, diskProvider, recycleBinProvider, logger) { } } diff --git a/src/NzbDrone.Core/Extras/Others/OtherExtraService.cs b/src/NzbDrone.Core/Extras/Others/OtherExtraService.cs index f3e331c30..664d212d5 100644 --- a/src/NzbDrone.Core/Extras/Others/OtherExtraService.cs +++ b/src/NzbDrone.Core/Extras/Others/OtherExtraService.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.IO; using System.Linq; using NLog; @@ -7,7 +8,7 @@ using NzbDrone.Common.Extensions; using NzbDrone.Core.Configuration; using NzbDrone.Core.Extras.Files; using NzbDrone.Core.MediaFiles; -using NzbDrone.Core.Tv; +using NzbDrone.Core.Music; namespace NzbDrone.Core.Extras.Others { @@ -27,33 +28,33 @@ namespace NzbDrone.Core.Extras.Others public override int Order => 2; - public override IEnumerable CreateAfterSeriesScan(Series series, List episodeFiles) + public override IEnumerable CreateAfterArtistScan(Artist artist, List albums, List trackFiles) { return Enumerable.Empty(); } - public override IEnumerable CreateAfterEpisodeImport(Series series, EpisodeFile episodeFile) + public override IEnumerable CreateAfterTrackImport(Artist artist, TrackFile trackFile) { return Enumerable.Empty(); } - public override IEnumerable CreateAfterEpisodeImport(Series series, string seriesFolder, string seasonFolder) + public override IEnumerable CreateAfterTrackImport(Artist artist, string artistFolder, string albumFolder) { return Enumerable.Empty(); } - public override IEnumerable MoveFilesAfterRename(Series series, List episodeFiles) + public override IEnumerable MoveFilesAfterRename(Artist artist, List trackFiles) { - var extraFiles = _otherExtraFileService.GetFilesBySeries(series.Id); + var extraFiles = _otherExtraFileService.GetFilesByArtist(artist.Id); var movedFiles = new List(); - foreach (var episodeFile in episodeFiles) + foreach (var trackFile in trackFiles) { - var extraFilesForEpisodeFile = extraFiles.Where(m => m.EpisodeFileId == episodeFile.Id).ToList(); + var extraFilesForTrackFile = extraFiles.Where(m => m.TrackFileId == trackFile.Id).ToList(); - foreach (var extraFile in extraFilesForEpisodeFile) + foreach (var extraFile in extraFilesForTrackFile) { - movedFiles.AddIfNotNull(MoveFile(series, episodeFile, extraFile)); + movedFiles.AddIfNotNull(MoveFile(artist, trackFile, extraFile)); } } @@ -62,15 +63,15 @@ namespace NzbDrone.Core.Extras.Others return movedFiles; } - public override ExtraFile Import(Series series, EpisodeFile episodeFile, string path, string extension, bool readOnly) + public override ExtraFile Import(Artist artist, TrackFile trackFile, string path, string extension, bool readOnly) { // If the extension is .nfo we need to change it to .nfo-orig - if (Path.GetExtension(path).Equals(".nfo")) + if (Path.GetExtension(path).Equals(".nfo", StringComparison.OrdinalIgnoreCase)) { extension += "-orig"; } - var extraFile = ImportFile(series, episodeFile, path, readOnly, extension, null); + var extraFile = ImportFile(artist, trackFile, path, readOnly, extension, null); _otherExtraFileService.Upsert(extraFile); diff --git a/src/NzbDrone.Core/Extras/Subtitles/ExistingSubtitleImporter.cs b/src/NzbDrone.Core/Extras/Subtitles/ExistingSubtitleImporter.cs deleted file mode 100644 index d3ae8d46b..000000000 --- a/src/NzbDrone.Core/Extras/Subtitles/ExistingSubtitleImporter.cs +++ /dev/null @@ -1,86 +0,0 @@ -using System.Collections.Generic; -using System.IO; -using System.Linq; -using NLog; -using NzbDrone.Common.Extensions; -using NzbDrone.Core.Extras.Files; -using NzbDrone.Core.Parser; -using NzbDrone.Core.Tv; - -namespace NzbDrone.Core.Extras.Subtitles -{ - public class ExistingSubtitleImporter : ImportExistingExtraFilesBase - { - private readonly IExtraFileService _subtitleFileService; - private readonly IParsingService _parsingService; - private readonly Logger _logger; - - public ExistingSubtitleImporter(IExtraFileService subtitleFileService, - IParsingService parsingService, - Logger logger) - : base (subtitleFileService) - { - _subtitleFileService = subtitleFileService; - _parsingService = parsingService; - _logger = logger; - } - - public override int Order => 1; - - public override IEnumerable ProcessFiles(Series series, List filesOnDisk, List importedFiles) - { - _logger.Debug("Looking for existing subtitle files in {0}", series.Path); - - var subtitleFiles = new List(); - var filterResult = FilterAndClean(series, filesOnDisk, importedFiles); - - foreach (var possibleSubtitleFile in filterResult.FilesOnDisk) - { - var extension = Path.GetExtension(possibleSubtitleFile); - - if (SubtitleFileExtensions.Extensions.Contains(extension)) - { - var localEpisode = _parsingService.GetLocalEpisode(possibleSubtitleFile, series); - - if (localEpisode == null) - { - _logger.Debug("Unable to parse subtitle file: {0}", possibleSubtitleFile); - continue; - } - - if (localEpisode.Episodes.Empty()) - { - _logger.Debug("Cannot find related episodes for: {0}", possibleSubtitleFile); - continue; - } - - if (localEpisode.Episodes.DistinctBy(e => e.EpisodeFileId).Count() > 1) - { - _logger.Debug("Subtitle file: {0} does not match existing files.", possibleSubtitleFile); - continue; - } - - var subtitleFile = new SubtitleFile - { - SeriesId = series.Id, - SeasonNumber = localEpisode.SeasonNumber, - EpisodeFileId = localEpisode.Episodes.First().EpisodeFileId, - RelativePath = series.Path.GetRelativePath(possibleSubtitleFile), - Language = LanguageParser.ParseSubtitleLanguage(possibleSubtitleFile), - Extension = extension - }; - - subtitleFiles.Add(subtitleFile); - } - } - - _logger.Info("Found {0} existing subtitle files", subtitleFiles.Count); - _subtitleFileService.Upsert(subtitleFiles); - - // Return files that were just imported along with files that were - // previously imported so previously imported files aren't imported twice - - return subtitleFiles.Concat(filterResult.PreviouslyImported); - } - } -} diff --git a/src/NzbDrone.Core/Extras/Subtitles/ImportedSubtitleFiles.cs b/src/NzbDrone.Core/Extras/Subtitles/ImportedSubtitleFiles.cs deleted file mode 100644 index 287ebdb68..000000000 --- a/src/NzbDrone.Core/Extras/Subtitles/ImportedSubtitleFiles.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System.Collections.Generic; -using NzbDrone.Core.Extras.Files; - -namespace NzbDrone.Core.Extras.Subtitles -{ - public class ImportedSubtitleFiles - { - public List SourceFiles { get; set; } - public List SubtitleFiles { get; set; } - - public ImportedSubtitleFiles() - { - SourceFiles = new List(); - SubtitleFiles = new List(); - } - } -} diff --git a/src/NzbDrone.Core/Extras/Subtitles/SubtitleFile.cs b/src/NzbDrone.Core/Extras/Subtitles/SubtitleFile.cs deleted file mode 100644 index 0ccd3ede6..000000000 --- a/src/NzbDrone.Core/Extras/Subtitles/SubtitleFile.cs +++ /dev/null @@ -1,10 +0,0 @@ -using NzbDrone.Core.Extras.Files; -using NzbDrone.Core.Parser; - -namespace NzbDrone.Core.Extras.Subtitles -{ - public class SubtitleFile : ExtraFile - { - public Language Language { get; set; } - } -} diff --git a/src/NzbDrone.Core/Extras/Subtitles/SubtitleFileExtensions.cs b/src/NzbDrone.Core/Extras/Subtitles/SubtitleFileExtensions.cs deleted file mode 100644 index 423d14656..000000000 --- a/src/NzbDrone.Core/Extras/Subtitles/SubtitleFileExtensions.cs +++ /dev/null @@ -1,32 +0,0 @@ -using System.Collections.Generic; - -namespace NzbDrone.Core.Extras.Subtitles -{ - public static class SubtitleFileExtensions - { - private static HashSet _fileExtensions; - - static SubtitleFileExtensions() - { - _fileExtensions = new HashSet - { - ".aqt", - ".ass", - ".idx", - ".jss", - ".psb", - ".rt", - ".smi", - ".srt", - ".ssa", - ".sub", - ".txt", - ".utf", - ".utf8", - ".utf-8" - }; - } - - public static HashSet Extensions => _fileExtensions; - } -} diff --git a/src/NzbDrone.Core/Extras/Subtitles/SubtitleFileRepository.cs b/src/NzbDrone.Core/Extras/Subtitles/SubtitleFileRepository.cs deleted file mode 100644 index 9b87fa9e0..000000000 --- a/src/NzbDrone.Core/Extras/Subtitles/SubtitleFileRepository.cs +++ /dev/null @@ -1,18 +0,0 @@ -using NzbDrone.Core.Datastore; -using NzbDrone.Core.Extras.Files; -using NzbDrone.Core.Messaging.Events; - -namespace NzbDrone.Core.Extras.Subtitles -{ - public interface ISubtitleFileRepository : IExtraFileRepository - { - } - - public class SubtitleFileRepository : ExtraFileRepository, ISubtitleFileRepository - { - public SubtitleFileRepository(IMainDatabase database, IEventAggregator eventAggregator) - : base(database, eventAggregator) - { - } - } -} \ No newline at end of file diff --git a/src/NzbDrone.Core/Extras/Subtitles/SubtitleFileService.cs b/src/NzbDrone.Core/Extras/Subtitles/SubtitleFileService.cs deleted file mode 100644 index ac7d4da2b..000000000 --- a/src/NzbDrone.Core/Extras/Subtitles/SubtitleFileService.cs +++ /dev/null @@ -1,20 +0,0 @@ -using NLog; -using NzbDrone.Common.Disk; -using NzbDrone.Core.Extras.Files; -using NzbDrone.Core.MediaFiles; -using NzbDrone.Core.Tv; - -namespace NzbDrone.Core.Extras.Subtitles -{ - public interface ISubtitleFileService : IExtraFileService - { - } - - public class SubtitleFileService : ExtraFileService, ISubtitleFileService - { - public SubtitleFileService(IExtraFileRepository repository, ISeriesService seriesService, IDiskProvider diskProvider, IRecycleBinProvider recycleBinProvider, Logger logger) - : base(repository, seriesService, diskProvider, recycleBinProvider, logger) - { - } - } -} diff --git a/src/NzbDrone.Core/Extras/Subtitles/SubtitleService.cs b/src/NzbDrone.Core/Extras/Subtitles/SubtitleService.cs deleted file mode 100644 index 52910a285..000000000 --- a/src/NzbDrone.Core/Extras/Subtitles/SubtitleService.cs +++ /dev/null @@ -1,121 +0,0 @@ -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text; -using NLog; -using NzbDrone.Common.Disk; -using NzbDrone.Common.Extensions; -using NzbDrone.Core.Configuration; -using NzbDrone.Core.Extras.Files; -using NzbDrone.Core.MediaFiles; -using NzbDrone.Core.Parser; -using NzbDrone.Core.Tv; - -namespace NzbDrone.Core.Extras.Subtitles -{ - public class SubtitleService : ExtraFileManager - { - private readonly ISubtitleFileService _subtitleFileService; - private readonly Logger _logger; - - public SubtitleService(IConfigService configService, - IDiskProvider diskProvider, - IDiskTransferService diskTransferService, - ISubtitleFileService subtitleFileService, - Logger logger) - : base(configService, diskProvider, diskTransferService, logger) - { - _subtitleFileService = subtitleFileService; - _logger = logger; - } - - public override int Order => 1; - - public override IEnumerable CreateAfterSeriesScan(Series series, List episodeFiles) - { - return Enumerable.Empty(); - } - - public override IEnumerable CreateAfterEpisodeImport(Series series, EpisodeFile episodeFile) - { - return Enumerable.Empty(); - } - - public override IEnumerable CreateAfterEpisodeImport(Series series, string seriesFolder, string seasonFolder) - { - return Enumerable.Empty(); - } - - public override IEnumerable MoveFilesAfterRename(Series series, List episodeFiles) - { - var subtitleFiles = _subtitleFileService.GetFilesBySeries(series.Id); - - var movedFiles = new List(); - - foreach (var episodeFile in episodeFiles) - { - var groupedExtraFilesForEpisodeFile = subtitleFiles.Where(m => m.EpisodeFileId == episodeFile.Id) - .GroupBy(s => s.Language + s.Extension).ToList(); - - foreach (var group in groupedExtraFilesForEpisodeFile) - { - var groupCount = group.Count(); - var copy = 1; - - if (groupCount > 1) - { - _logger.Warn("Multiple subtitle files found with the same language and extension for {0}", Path.Combine(series.Path, episodeFile.RelativePath)); - } - - foreach (var subtitleFile in group) - { - var suffix = GetSuffix(subtitleFile.Language, copy, groupCount > 1); - movedFiles.AddIfNotNull(MoveFile(series, episodeFile, subtitleFile, suffix)); - - copy++; - } - } - } - - _subtitleFileService.Upsert(movedFiles); - - return movedFiles; - } - - public override ExtraFile Import(Series series, EpisodeFile episodeFile, string path, string extension, bool readOnly) - { - if (SubtitleFileExtensions.Extensions.Contains(Path.GetExtension(path))) - { - var language = LanguageParser.ParseSubtitleLanguage(path); - var suffix = GetSuffix(language, 1, false); - var subtitleFile = ImportFile(series, episodeFile, path, readOnly, extension, suffix); - subtitleFile.Language = language; - - _subtitleFileService.Upsert(subtitleFile); - - return subtitleFile; - } - - return null; - } - - private string GetSuffix(Language language, int copy, bool multipleCopies = false) - { - var suffixBuilder = new StringBuilder(); - - if (multipleCopies) - { - suffixBuilder.Append("."); - suffixBuilder.Append(copy); - } - - if (language != Language.Unknown) - { - suffixBuilder.Append("."); - suffixBuilder.Append(IsoLanguages.Get(language).TwoLetterCode); - } - - return suffixBuilder.ToString(); - } - } -} diff --git a/src/NzbDrone.Core/HealthCheck/CheckOnAttribute.cs b/src/NzbDrone.Core/HealthCheck/CheckOnAttribute.cs new file mode 100644 index 000000000..a40aee50e --- /dev/null +++ b/src/NzbDrone.Core/HealthCheck/CheckOnAttribute.cs @@ -0,0 +1,16 @@ +using System; +using NzbDrone.Common.Messaging; + +namespace NzbDrone.Core.HealthCheck +{ + [AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] + public class CheckOnAttribute : Attribute + { + public Type EventType { get; set; } + + public CheckOnAttribute(Type eventType) + { + EventType = eventType; + } + } +} diff --git a/src/NzbDrone.Core/HealthCheck/Checks/AppDataLocationCheck.cs b/src/NzbDrone.Core/HealthCheck/Checks/AppDataLocationCheck.cs index ad4f2db9e..d4f9a7cc7 100644 --- a/src/NzbDrone.Core/HealthCheck/Checks/AppDataLocationCheck.cs +++ b/src/NzbDrone.Core/HealthCheck/Checks/AppDataLocationCheck.cs @@ -1,5 +1,6 @@ -using NzbDrone.Common.EnvironmentInfo; +using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.Extensions; +using NzbDrone.Core.Configuration.Events; namespace NzbDrone.Core.HealthCheck.Checks { @@ -22,7 +23,6 @@ namespace NzbDrone.Core.HealthCheck.Checks return new HealthCheck(GetType()); } - - public override bool CheckOnConfigChange => false; + } } diff --git a/src/NzbDrone.Core/HealthCheck/Checks/DownloadClientCheck.cs b/src/NzbDrone.Core/HealthCheck/Checks/DownloadClientCheck.cs index d99eed1a3..ebe520960 100644 --- a/src/NzbDrone.Core/HealthCheck/Checks/DownloadClientCheck.cs +++ b/src/NzbDrone.Core/HealthCheck/Checks/DownloadClientCheck.cs @@ -1,10 +1,13 @@ -using System; +using System; using System.Linq; using NLog; using NzbDrone.Core.Download; +using NzbDrone.Core.ThingiProvider.Events; namespace NzbDrone.Core.HealthCheck.Checks { + [CheckOn(typeof(ProviderUpdatedEvent))] + [CheckOn(typeof(ProviderDeletedEvent))] public class DownloadClientCheck : HealthCheckBase { private readonly IProvideDownloadClient _downloadClientProvider; @@ -33,11 +36,10 @@ namespace NzbDrone.Core.HealthCheck.Checks } catch (Exception ex) { - - _logger.Error(ex, "Unable to communicate with {0}", downloadClient.Definition.Name); + _logger.Debug(ex, "Unable to communicate with {0}", downloadClient.Definition.Name); var message = $"Unable to communicate with {downloadClient.Definition.Name}."; - return new HealthCheck(GetType(), HealthCheckResult.Error, $"{message} {ex.Message}"); + return new HealthCheck(GetType(), HealthCheckResult.Error, $"{message} {ex.Message}", "#unable-to-communicate-with-download-client"); } } diff --git a/src/NzbDrone.Core/HealthCheck/Checks/DownloadClientStatusCheck.cs b/src/NzbDrone.Core/HealthCheck/Checks/DownloadClientStatusCheck.cs new file mode 100644 index 000000000..91b9b122d --- /dev/null +++ b/src/NzbDrone.Core/HealthCheck/Checks/DownloadClientStatusCheck.cs @@ -0,0 +1,45 @@ +using System; +using System.Linq; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.Download; +using NzbDrone.Core.ThingiProvider.Events; + +namespace NzbDrone.Core.HealthCheck.Checks +{ + [CheckOn(typeof(ProviderUpdatedEvent))] + [CheckOn(typeof(ProviderDeletedEvent))] + [CheckOn(typeof(ProviderStatusChangedEvent))] + public class DownloadClientStatusCheck : HealthCheckBase + { + private readonly IDownloadClientFactory _providerFactory; + private readonly IDownloadClientStatusService _providerStatusService; + + public DownloadClientStatusCheck(IDownloadClientFactory providerFactory, IDownloadClientStatusService providerStatusService) + { + _providerFactory = providerFactory; + _providerStatusService = providerStatusService; + } + + public override HealthCheck Check() + { + var enabledProviders = _providerFactory.GetAvailableProviders(); + var backOffProviders = enabledProviders.Join(_providerStatusService.GetBlockedProviders(), + i => i.Definition.Id, + s => s.ProviderId, + (i, s) => new { Provider = i, Status = s }) + .ToList(); + + if (backOffProviders.Empty()) + { + return new HealthCheck(GetType()); + } + + if (backOffProviders.Count == enabledProviders.Count) + { + return new HealthCheck(GetType(), HealthCheckResult.Error, "All download clients are unavailable due to failures", "#download-clients-are-unavailable-due-to-failures"); + } + + return new HealthCheck(GetType(), HealthCheckResult.Warning, string.Format("Download clients unavailable due to failures: {0}", string.Join(", ", backOffProviders.Select(v => v.Provider.Definition.Name))), "#download-clients-are-unavailable-due-to-failures"); + } + } +} diff --git a/src/NzbDrone.Core/HealthCheck/Checks/DroneFactoryCheck.cs b/src/NzbDrone.Core/HealthCheck/Checks/DroneFactoryCheck.cs deleted file mode 100644 index af78455fd..000000000 --- a/src/NzbDrone.Core/HealthCheck/Checks/DroneFactoryCheck.cs +++ /dev/null @@ -1,42 +0,0 @@ -using NzbDrone.Common.Disk; -using NzbDrone.Common.Extensions; -using NzbDrone.Core.Configuration; - -namespace NzbDrone.Core.HealthCheck.Checks -{ - public class DroneFactoryCheck : HealthCheckBase - { - private readonly IConfigService _configService; - private readonly IDiskProvider _diskProvider; - - public DroneFactoryCheck(IConfigService configService, IDiskProvider diskProvider) - { - _configService = configService; - _diskProvider = diskProvider; - } - - public override HealthCheck Check() - { - var droneFactoryFolder = _configService.DownloadedAlbumsFolder; - - if (droneFactoryFolder.IsNullOrWhiteSpace()) - { - return new HealthCheck(GetType()); - } - - if (!_diskProvider.FolderExists(droneFactoryFolder)) - { - return new HealthCheck(GetType(), HealthCheckResult.Error, "Drone factory folder does not exist"); - } - - if (!_diskProvider.FolderWritable(droneFactoryFolder)) - { - return new HealthCheck(GetType(), HealthCheckResult.Error, "Unable to write to drone factory folder"); - } - - //Todo: Unable to import one or more files/folders from - - return new HealthCheck(GetType()); - } - } -} diff --git a/src/NzbDrone.Core/HealthCheck/Checks/ImportMechanismCheck.cs b/src/NzbDrone.Core/HealthCheck/Checks/ImportMechanismCheck.cs index 233144a8b..29172e0d8 100644 --- a/src/NzbDrone.Core/HealthCheck/Checks/ImportMechanismCheck.cs +++ b/src/NzbDrone.Core/HealthCheck/Checks/ImportMechanismCheck.cs @@ -1,15 +1,19 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; -using NzbDrone.Common.Disk; using NzbDrone.Core.Configuration; +using NzbDrone.Core.Configuration.Events; using NzbDrone.Core.Download; using NzbDrone.Core.Download.Clients; using NzbDrone.Core.Download.Clients.Nzbget; using NzbDrone.Core.Download.Clients.Sabnzbd; +using NzbDrone.Core.ThingiProvider.Events; namespace NzbDrone.Core.HealthCheck.Checks { + [CheckOn(typeof(ProviderUpdatedEvent))] + [CheckOn(typeof(ProviderDeletedEvent))] + [CheckOn(typeof(ConfigSavedEvent))] public class ImportMechanismCheck : HealthCheckBase { private readonly IConfigService _configService; @@ -24,7 +28,6 @@ namespace NzbDrone.Core.HealthCheck.Checks public override HealthCheck Check() { - var droneFactoryFolder = new OsPath(_configService.DownloadedAlbumsFolder); List downloadClients; try @@ -35,16 +38,13 @@ namespace NzbDrone.Core.HealthCheck.Checks Status = v.GetStatus() }).ToList(); } - catch (DownloadClientException) + catch (Exception) { // One or more download clients failed, assume the health is okay and verify later return new HealthCheck(GetType()); } var downloadClientIsLocalHost = downloadClients.All(v => v.Status.IsLocalhost); - var downloadClientOutputInDroneFactory = !droneFactoryFolder.IsEmpty && - downloadClients.Any(v => v.Status.OutputRootFolders != null && - v.Status.OutputRootFolders.Any(droneFactoryFolder.Contains)); if (!_configService.IsDefined("EnableCompletedDownloadHandling")) { @@ -56,32 +56,20 @@ namespace NzbDrone.Core.HealthCheck.Checks if (downloadClients.All(v => v.DownloadClient is Sabnzbd)) { - // With Sabnzbd we can check if the category should be changed. - if (downloadClientOutputInDroneFactory) - { - return new HealthCheck(GetType(), HealthCheckResult.Warning, "Enable Completed Download Handling if possible (Sabnzbd - Conflicting Category)", "Migrating-to-Completed-Download-Handling#sabnzbd-conflicting-download-client-category"); - } - return new HealthCheck(GetType(), HealthCheckResult.Warning, "Enable Completed Download Handling if possible (Sabnzbd)", "Migrating-to-Completed-Download-Handling#sabnzbd-enable-completed-download-handling"); } if (downloadClients.All(v => v.DownloadClient is Nzbget)) { - // With Nzbget we can check if the category should be changed. - if (downloadClientOutputInDroneFactory) - { - return new HealthCheck(GetType(), HealthCheckResult.Warning, "Enable Completed Download Handling if possible (Nzbget - Conflicting Category)", "Migrating-to-Completed-Download-Handling#nzbget-conflicting-download-client-category"); - } - return new HealthCheck(GetType(), HealthCheckResult.Warning, "Enable Completed Download Handling if possible (Nzbget)", "Migrating-to-Completed-Download-Handling#nzbget-enable-completed-download-handling"); } return new HealthCheck(GetType(), HealthCheckResult.Warning, "Enable Completed Download Handling if possible", "Migrating-to-Completed-Download-Handling"); } - if (!_configService.EnableCompletedDownloadHandling && droneFactoryFolder.IsEmpty) + if (!_configService.EnableCompletedDownloadHandling) { - return new HealthCheck(GetType(), HealthCheckResult.Warning, "Enable Completed Download Handling or configure Drone factory"); + return new HealthCheck(GetType(), HealthCheckResult.Warning, "Enable Completed Download Handling"); } return new HealthCheck(GetType()); @@ -91,6 +79,6 @@ namespace NzbDrone.Core.HealthCheck.Checks public class ImportMechanismCheckStatus { public IDownloadClient DownloadClient { get; set; } - public DownloadClientStatus Status { get; set; } + public DownloadClientInfo Status { get; set; } } } diff --git a/src/NzbDrone.Core/HealthCheck/Checks/IndexerRssCheck.cs b/src/NzbDrone.Core/HealthCheck/Checks/IndexerRssCheck.cs index 1d9ebf37e..557e07dce 100644 --- a/src/NzbDrone.Core/HealthCheck/Checks/IndexerRssCheck.cs +++ b/src/NzbDrone.Core/HealthCheck/Checks/IndexerRssCheck.cs @@ -1,9 +1,13 @@ -using System.Linq; +using System.Linq; using NzbDrone.Common.Extensions; using NzbDrone.Core.Indexers; +using NzbDrone.Core.ThingiProvider.Events; namespace NzbDrone.Core.HealthCheck.Checks { + [CheckOn(typeof(ProviderUpdatedEvent))] + [CheckOn(typeof(ProviderDeletedEvent))] + [CheckOn(typeof(ProviderStatusChangedEvent))] public class IndexerRssCheck : HealthCheckBase { private readonly IIndexerFactory _indexerFactory; diff --git a/src/NzbDrone.Core/HealthCheck/Checks/IndexerSearchCheck.cs b/src/NzbDrone.Core/HealthCheck/Checks/IndexerSearchCheck.cs index f02e5de29..220c08c0e 100644 --- a/src/NzbDrone.Core/HealthCheck/Checks/IndexerSearchCheck.cs +++ b/src/NzbDrone.Core/HealthCheck/Checks/IndexerSearchCheck.cs @@ -1,9 +1,13 @@ -using System.Linq; +using System.Linq; using NzbDrone.Common.Extensions; using NzbDrone.Core.Indexers; +using NzbDrone.Core.ThingiProvider.Events; namespace NzbDrone.Core.HealthCheck.Checks { + [CheckOn(typeof(ProviderUpdatedEvent))] + [CheckOn(typeof(ProviderDeletedEvent))] + [CheckOn(typeof(ProviderStatusChangedEvent))] public class IndexerSearchCheck : HealthCheckBase { private readonly IIndexerFactory _indexerFactory; diff --git a/src/NzbDrone.Core/HealthCheck/Checks/IndexerStatusCheck.cs b/src/NzbDrone.Core/HealthCheck/Checks/IndexerStatusCheck.cs index 29eadb180..d608686a2 100644 --- a/src/NzbDrone.Core/HealthCheck/Checks/IndexerStatusCheck.cs +++ b/src/NzbDrone.Core/HealthCheck/Checks/IndexerStatusCheck.cs @@ -1,42 +1,45 @@ -using System; +using System; using System.Linq; using NzbDrone.Common.Extensions; using NzbDrone.Core.Indexers; +using NzbDrone.Core.ThingiProvider.Events; namespace NzbDrone.Core.HealthCheck.Checks { + [CheckOn(typeof(ProviderUpdatedEvent))] + [CheckOn(typeof(ProviderDeletedEvent))] + [CheckOn(typeof(ProviderStatusChangedEvent))] public class IndexerStatusCheck : HealthCheckBase { - private readonly IIndexerFactory _indexerFactory; - private readonly IIndexerStatusService _indexerStatusService; + private readonly IIndexerFactory _providerFactory; + private readonly IIndexerStatusService _providerStatusService; - public IndexerStatusCheck(IIndexerFactory indexerFactory, IIndexerStatusService indexerStatusService) + public IndexerStatusCheck(IIndexerFactory providerFactory, IIndexerStatusService providerStatusService) { - _indexerFactory = indexerFactory; - _indexerStatusService = indexerStatusService; + _providerFactory = providerFactory; + _providerStatusService = providerStatusService; } public override HealthCheck Check() { - var enabledIndexers = _indexerFactory.GetAvailableProviders(); - var backOffIndexers = enabledIndexers.Join(_indexerStatusService.GetBlockedIndexers(), + var enabledProviders = _providerFactory.GetAvailableProviders(); + var backOffProviders = enabledProviders.Join(_providerStatusService.GetBlockedProviders(), i => i.Definition.Id, - s => s.IndexerId, + s => s.ProviderId, (i, s) => new { Indexer = i, Status = s }) - .Where(v => (v.Status.MostRecentFailure - v.Status.InitialFailure) > TimeSpan.FromHours(1)) .ToList(); - if (backOffIndexers.Empty()) + if (backOffProviders.Empty()) { return new HealthCheck(GetType()); } - if (backOffIndexers.Count == enabledIndexers.Count) + if (backOffProviders.Count == enabledProviders.Count) { return new HealthCheck(GetType(), HealthCheckResult.Error, "All indexers are unavailable due to failures", "#indexers-are-unavailable-due-to-failures"); } - return new HealthCheck(GetType(), HealthCheckResult.Warning, string.Format("Indexers unavailable due to failures: {0}", string.Join(", ", backOffIndexers.Select(v => v.Indexer.Definition.Name))), "#indexers-are-unavailable-due-to-failures"); + return new HealthCheck(GetType(), HealthCheckResult.Warning, string.Format("Indexers unavailable due to failures: {0}", string.Join(", ", backOffProviders.Select(v => v.Indexer.Definition.Name))), "#indexers-are-unavailable-due-to-failures"); } } } diff --git a/src/NzbDrone.Core/HealthCheck/Checks/MediaInfoDllCheck.cs b/src/NzbDrone.Core/HealthCheck/Checks/MediaInfoDllCheck.cs index 5b5a9f3f4..badc522f4 100644 --- a/src/NzbDrone.Core/HealthCheck/Checks/MediaInfoDllCheck.cs +++ b/src/NzbDrone.Core/HealthCheck/Checks/MediaInfoDllCheck.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Runtime.CompilerServices; using NzbDrone.Core.MediaFiles.MediaInfo; @@ -20,7 +20,5 @@ namespace NzbDrone.Core.HealthCheck.Checks return new HealthCheck(GetType()); } - - public override bool CheckOnConfigChange => false; } } diff --git a/src/NzbDrone.Core/HealthCheck/Checks/MonoTlsCheck.cs b/src/NzbDrone.Core/HealthCheck/Checks/MonoTlsCheck.cs new file mode 100644 index 000000000..a3a0226a6 --- /dev/null +++ b/src/NzbDrone.Core/HealthCheck/Checks/MonoTlsCheck.cs @@ -0,0 +1,41 @@ +using System; +using System.Linq; +using System.Reflection; +using NLog; +using NzbDrone.Common.EnvironmentInfo; + +namespace NzbDrone.Core.HealthCheck.Checks +{ + public class MonoTlsCheck : HealthCheckBase + { + private readonly IPlatformInfo _platformInfo; + private readonly Logger _logger; + + public MonoTlsCheck(IPlatformInfo platformInfo, Logger logger) + { + _platformInfo = platformInfo; + _logger = logger; + } + + public override HealthCheck Check() + { + if (!PlatformInfo.IsMono) + { + return new HealthCheck(GetType()); + } + + var monoVersion = _platformInfo.Version; + + if (monoVersion >= new Version("5.0.0") && Environment.GetEnvironmentVariable("MONO_TLS_PROVIDER") == "legacy") + { + // Mono 5.0 still has issues in combination with libmediainfo, so disabling this check for now. + //_logger.Debug("Mono version 5.0.0 or higher and legacy TLS provider is selected, recommending user to switch to btls."); + //return new HealthCheck(GetType(), HealthCheckResult.Warning, "Sonarr now supports Mono 5.x with btls enabled, consider removing MONO_TLS_PROVIDER=legacy option"); + } + + return new HealthCheck(GetType()); + } + + public override bool CheckOnSchedule => false; + } +} diff --git a/src/NzbDrone.Core/HealthCheck/Checks/MonoVersionCheck.cs b/src/NzbDrone.Core/HealthCheck/Checks/MonoVersionCheck.cs index 2033b9d87..8a485396c 100644 --- a/src/NzbDrone.Core/HealthCheck/Checks/MonoVersionCheck.cs +++ b/src/NzbDrone.Core/HealthCheck/Checks/MonoVersionCheck.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Linq; using System.Reflection; using NLog; @@ -26,58 +26,22 @@ namespace NzbDrone.Core.HealthCheck.Checks var monoVersion = _platformInfo.Version; - if (monoVersion == new Version("3.4.0") && HasMonoBug18599()) - { - _logger.Debug("Mono version 3.4.0, checking for Mono bug #18599 returned positive."); - return new HealthCheck(GetType(), HealthCheckResult.Error, "You are running an old and unsupported version of Mono with a known bug. You should upgrade to a higher version"); - } - if (monoVersion == new Version("4.4.0") || monoVersion == new Version("4.4.1")) { _logger.Debug("Mono version {0}", monoVersion); - return new HealthCheck(GetType(), HealthCheckResult.Error, $"Your Mono version {monoVersion} has a bug that causes issues connecting to indexers/download clients. You should upgrade to a higher version"); + return new HealthCheck(GetType(), HealthCheckResult.Error, $"Your Mono version {monoVersion} has a bug that causes issues connecting to indexers/download clients. You should upgrade to a higher version"); } - if (monoVersion >= new Version("3.10")) + if (monoVersion >= new Version("4.4")) { - _logger.Debug("Mono version is 3.10 or better: {0}", monoVersion); + _logger.Debug("Mono version is 4.6 or better: {0}", monoVersion); return new HealthCheck(GetType()); } return new HealthCheck(GetType(), HealthCheckResult.Warning, "You are running an old and unsupported version of Mono. Please upgrade Mono for improved stability."); } - public override bool CheckOnConfigChange => false; - public override bool CheckOnSchedule => false; - private bool HasMonoBug18599() - { - _logger.Debug("mono version 3.4.0, checking for mono bug #18599."); - var numberFormatterType = Type.GetType("System.NumberFormatter"); - - if (numberFormatterType == null) - { - _logger.Debug("Couldn't find System.NumberFormatter. Aborting test."); - return false; - } - - var fieldInfo = numberFormatterType.GetField("userFormatProvider", - BindingFlags.Static | BindingFlags.NonPublic); - - if (fieldInfo == null) - { - _logger.Debug("userFormatProvider field not found, version likely preceeds the official v3.4.0."); - return false; - } - - if (fieldInfo.GetCustomAttributes(false).Any(v => v is ThreadStaticAttribute)) - { - _logger.Debug("userFormatProvider field doesn't contain the ThreadStatic Attribute, version is affected by the critical bug #18599."); - return true; - } - - return false; - } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Core/HealthCheck/Checks/MountCheck.cs b/src/NzbDrone.Core/HealthCheck/Checks/MountCheck.cs new file mode 100644 index 000000000..91129d1c8 --- /dev/null +++ b/src/NzbDrone.Core/HealthCheck/Checks/MountCheck.cs @@ -0,0 +1,36 @@ +using System.Linq; +using NzbDrone.Common.Disk; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.Music; + +namespace NzbDrone.Core.HealthCheck.Checks +{ + public class MountCheck : HealthCheckBase + { + private readonly IDiskProvider _diskProvider; + private readonly IArtistService _artistService; + + public MountCheck(IDiskProvider diskProvider, IArtistService artistService) + { + _diskProvider = diskProvider; + _artistService = artistService; + } + + public override HealthCheck Check() + { + // Not best for optimization but due to possible symlinks and junctions, we get mounts based on series path so internals can handle mount resolution. + var mounts = _artistService.GetAllArtists() + .Select(artist => _diskProvider.GetMount(artist.Path)) + .Where(m => m != null && m.MountOptions != null && m.MountOptions.IsReadOnly) + .DistinctBy(m => m.RootDirectory) + .ToList(); + + if (mounts.Any()) + { + return new HealthCheck(GetType(), HealthCheckResult.Error, "Mount containing a artist path is mounted read-only: " + string.Join(",", mounts.Select(m => m.Name)), "#artist-mount-ro"); + } + + return new HealthCheck(GetType()); + } + } +} diff --git a/src/NzbDrone.Core/HealthCheck/Checks/ProxyCheck.cs b/src/NzbDrone.Core/HealthCheck/Checks/ProxyCheck.cs index 346a12b1a..71ededd18 100644 --- a/src/NzbDrone.Core/HealthCheck/Checks/ProxyCheck.cs +++ b/src/NzbDrone.Core/HealthCheck/Checks/ProxyCheck.cs @@ -1,13 +1,15 @@ -using NLog; +using NLog; using NzbDrone.Common.Http; using NzbDrone.Core.Configuration; using System; using System.Linq; using System.Net; using NzbDrone.Common.Cloud; +using NzbDrone.Core.Configuration.Events; namespace NzbDrone.Core.HealthCheck.Checks { + [CheckOn(typeof(ConfigSavedEvent))] public class ProxyCheck : HealthCheckBase { private readonly Logger _logger; @@ -43,7 +45,7 @@ namespace NzbDrone.Core.HealthCheck.Checks { var response = _client.Execute(request); - // We only care about 400 responses, other error codes can be ignored + // We only care about 400 responses, other error codes can be ignored if (response.StatusCode == HttpStatusCode.BadRequest) { _logger.Error("Proxy Health Check failed: {0}", response.StatusCode); diff --git a/src/NzbDrone.Core/HealthCheck/Checks/RootFolderCheck.cs b/src/NzbDrone.Core/HealthCheck/Checks/RootFolderCheck.cs index d7cb3f7d1..86a8fc4ef 100644 --- a/src/NzbDrone.Core/HealthCheck/Checks/RootFolderCheck.cs +++ b/src/NzbDrone.Core/HealthCheck/Checks/RootFolderCheck.cs @@ -1,23 +1,26 @@ -using System.Linq; +using System.Linq; using NzbDrone.Common.Disk; -using NzbDrone.Core.Tv; +using NzbDrone.Core.Music; +using NzbDrone.Core.Music.Events; namespace NzbDrone.Core.HealthCheck.Checks { + [CheckOn(typeof(ArtistDeletedEvent))] + [CheckOn(typeof(ArtistMovedEvent))] public class RootFolderCheck : HealthCheckBase { - private readonly ISeriesService _seriesService; + private readonly IArtistService _artistService; private readonly IDiskProvider _diskProvider; - public RootFolderCheck(ISeriesService seriesService, IDiskProvider diskProvider) + public RootFolderCheck(IArtistService artistService, IDiskProvider diskProvider) { - _seriesService = seriesService; + _artistService = artistService; _diskProvider = diskProvider; } public override HealthCheck Check() { - var missingRootFolders = _seriesService.GetAllSeries() + var missingRootFolders = _artistService.GetAllArtists() .Select(s => _diskProvider.GetParentFolder(s.Path)) .Distinct() .Where(s => !_diskProvider.FolderExists(s)) @@ -36,7 +39,5 @@ namespace NzbDrone.Core.HealthCheck.Checks return new HealthCheck(GetType()); } - - public override bool CheckOnConfigChange => false; } } diff --git a/src/NzbDrone.Core/HealthCheck/Checks/UpdateCheck.cs b/src/NzbDrone.Core/HealthCheck/Checks/UpdateCheck.cs index c0d7a5c31..d474b503c 100644 --- a/src/NzbDrone.Core/HealthCheck/Checks/UpdateCheck.cs +++ b/src/NzbDrone.Core/HealthCheck/Checks/UpdateCheck.cs @@ -1,13 +1,15 @@ -using System; +using System; using System.IO; using NzbDrone.Common.Disk; using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.Extensions; using NzbDrone.Core.Configuration; +using NzbDrone.Core.Configuration.Events; using NzbDrone.Core.Update; namespace NzbDrone.Core.HealthCheck.Checks { + [CheckOn(typeof(ConfigFileSavedEvent))] public class UpdateCheck : HealthCheckBase { private readonly IDiskProvider _diskProvider; @@ -66,7 +68,5 @@ namespace NzbDrone.Core.HealthCheck.Checks return new HealthCheck(GetType()); } - - public override bool CheckOnConfigChange => false; } } diff --git a/src/NzbDrone.Core/HealthCheck/HealthCheckBase.cs b/src/NzbDrone.Core/HealthCheck/HealthCheckBase.cs index 5e1700ac6..d9c715ca9 100644 --- a/src/NzbDrone.Core/HealthCheck/HealthCheckBase.cs +++ b/src/NzbDrone.Core/HealthCheck/HealthCheckBase.cs @@ -1,4 +1,4 @@ -namespace NzbDrone.Core.HealthCheck +namespace NzbDrone.Core.HealthCheck { public abstract class HealthCheckBase : IProvideHealthCheck { @@ -6,8 +6,6 @@ public virtual bool CheckOnStartup => true; - public virtual bool CheckOnConfigChange => true; - public virtual bool CheckOnSchedule => true; } } diff --git a/src/NzbDrone.Core/HealthCheck/HealthCheckService.cs b/src/NzbDrone.Core/HealthCheck/HealthCheckService.cs index 56789b8a1..b6487b492 100644 --- a/src/NzbDrone.Core/HealthCheck/HealthCheckService.cs +++ b/src/NzbDrone.Core/HealthCheck/HealthCheckService.cs @@ -1,8 +1,11 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using NLog; using NzbDrone.Common.Cache; +using NzbDrone.Common.Extensions; +using NzbDrone.Common.Messaging; +using NzbDrone.Common.Reflection; using NzbDrone.Core.Configuration.Events; using NzbDrone.Core.Download; using NzbDrone.Core.Indexers; @@ -21,13 +24,12 @@ namespace NzbDrone.Core.HealthCheck public class HealthCheckService : IHealthCheckService, IExecute, IHandleAsync, - IHandleAsync, - IHandleAsync>, - IHandleAsync>, - IHandleAsync>, - IHandleAsync> + IHandleAsync { - private readonly IEnumerable _healthChecks; + private readonly IProvideHealthCheck[] _healthChecks; + private readonly IProvideHealthCheck[] _startupHealthChecks; + private readonly IProvideHealthCheck[] _scheduledHealthChecks; + private readonly Dictionary _eventDrivenHealthChecks; private readonly IEventAggregator _eventAggregator; private readonly ICacheManager _cacheManager; private readonly Logger _logger; @@ -39,12 +41,16 @@ namespace NzbDrone.Core.HealthCheck ICacheManager cacheManager, Logger logger) { - _healthChecks = healthChecks; + _healthChecks = healthChecks.ToArray(); _eventAggregator = eventAggregator; _cacheManager = cacheManager; _logger = logger; _healthCheckResults = _cacheManager.GetCache(GetType()); + + _startupHealthChecks = _healthChecks.Where(v => v.CheckOnStartup).ToArray(); + _scheduledHealthChecks = _healthChecks.Where(v => v.CheckOnSchedule).ToArray(); + _eventDrivenHealthChecks = GetEventDrivenHealthChecks(); } public List Results() @@ -52,11 +58,18 @@ namespace NzbDrone.Core.HealthCheck return _healthCheckResults.Values.ToList(); } - private void PerformHealthCheck(Func predicate) + private Dictionary GetEventDrivenHealthChecks() + { + return _healthChecks + .SelectMany(h => h.GetType().GetAttributes().Select(a => Tuple.Create(a.EventType, h))) + .GroupBy(t => t.Item1, t => t.Item2) + .ToDictionary(g => g.Key, g => g.ToArray()); + } + + private void PerformHealthCheck(IProvideHealthCheck[] healthChecks) { - var results = _healthChecks.Where(predicate) - .Select(c => c.Check()) - .ToList(); + var results = healthChecks.Select(c => c.Check()) + .ToList(); foreach (var result in results) { @@ -76,37 +89,37 @@ namespace NzbDrone.Core.HealthCheck public void Execute(CheckHealthCommand message) { - PerformHealthCheck(c => message.Trigger == CommandTrigger.Manual || c.CheckOnSchedule); + if (message.Trigger == CommandTrigger.Manual) + { + PerformHealthCheck(_healthChecks); + } + else + { + PerformHealthCheck(_scheduledHealthChecks); + } } public void HandleAsync(ApplicationStartedEvent message) { - PerformHealthCheck(c => c.CheckOnStartup); - } - - public void HandleAsync(ConfigSavedEvent message) - { - PerformHealthCheck(c => c.CheckOnConfigChange); + PerformHealthCheck(_startupHealthChecks); } - public void HandleAsync(ProviderUpdatedEvent message) + public void HandleAsync(IEvent message) { - PerformHealthCheck(c => c.CheckOnConfigChange); - } + if (message is HealthCheckCompleteEvent) + { + return; + } - public void HandleAsync(ProviderDeletedEvent message) - { - PerformHealthCheck(c => c.CheckOnConfigChange); - } + IProvideHealthCheck[] checks; + if (!_eventDrivenHealthChecks.TryGetValue(message.GetType(), out checks)) + { + return; + } - public void HandleAsync(ProviderUpdatedEvent message) - { - PerformHealthCheck(c => c.CheckOnConfigChange); - } + // TODO: Add debounce - public void HandleAsync(ProviderDeletedEvent message) - { - PerformHealthCheck(c => c.CheckOnConfigChange); + PerformHealthCheck(checks); } } } diff --git a/src/NzbDrone.Core/HealthCheck/IProvideHealthCheck.cs b/src/NzbDrone.Core/HealthCheck/IProvideHealthCheck.cs index ece0b7952..d71f2653f 100644 --- a/src/NzbDrone.Core/HealthCheck/IProvideHealthCheck.cs +++ b/src/NzbDrone.Core/HealthCheck/IProvideHealthCheck.cs @@ -1,10 +1,9 @@ -namespace NzbDrone.Core.HealthCheck +namespace NzbDrone.Core.HealthCheck { public interface IProvideHealthCheck { HealthCheck Check(); bool CheckOnStartup { get; } - bool CheckOnConfigChange { get; } bool CheckOnSchedule { get; } } } diff --git a/src/NzbDrone.Core/History/History.cs b/src/NzbDrone.Core/History/History.cs index 5c017a71c..ffc274db0 100644 --- a/src/NzbDrone.Core/History/History.cs +++ b/src/NzbDrone.Core/History/History.cs @@ -1,8 +1,9 @@ -using System; +using System; using System.Collections.Generic; using NzbDrone.Core.Datastore; using NzbDrone.Core.Qualities; using NzbDrone.Core.Music; +using NzbDrone.Core.Languages; namespace NzbDrone.Core.History { @@ -14,7 +15,8 @@ namespace NzbDrone.Core.History { Data = new Dictionary(); } - + + public int TrackId { get; set; } public int AlbumId { get; set; } public int ArtistId { get; set; } public string SourceTitle { get; set; } @@ -22,8 +24,10 @@ namespace NzbDrone.Core.History public DateTime Date { get; set; } public Album Album { get; set; } public Artist Artist { get; set; } + public Track Track { get; set; } public HistoryEventType EventType { get; set; } public Dictionary Data { get; set; } + public Language Language { get; set; } public string DownloadId { get; set; } @@ -36,6 +40,7 @@ namespace NzbDrone.Core.History SeriesFolderImported = 2, DownloadFolderImported = 3, DownloadFailed = 4, - EpisodeFileDeleted = 5 + TrackFileDeleted = 5, + TrackFileRenamed = 6 } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Core/History/HistoryRepository.cs b/src/NzbDrone.Core/History/HistoryRepository.cs index 68cd40ae7..9cc0049bf 100644 --- a/src/NzbDrone.Core/History/HistoryRepository.cs +++ b/src/NzbDrone.Core/History/HistoryRepository.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using Marr.Data.QGen; using NzbDrone.Core.Datastore; @@ -10,7 +10,6 @@ namespace NzbDrone.Core.History { public interface IHistoryRepository : IBasicRepository { - List GetBestQualityInHistory(int albumId); History MostRecentForAlbum(int albumId); History MostRecentForDownloadId(string downloadId); List FindByDownloadId(string downloadId); @@ -27,13 +26,6 @@ namespace NzbDrone.Core.History } - public List GetBestQualityInHistory(int albumId) - { - var history = Query.Where(c => c.AlbumId == albumId); - - return history.Select(h => h.Quality).ToList(); - } - public History MostRecentForAlbum(int albumId) { return Query.Where(h => h.AlbumId == albumId) @@ -72,9 +64,10 @@ namespace NzbDrone.Core.History protected override SortBuilder GetPagedQuery(QueryBuilder query, PagingSpec pagingSpec) { var baseQuery = query.Join(JoinType.Inner, h => h.Artist, (h, s) => h.ArtistId == s.Id) - .Join(JoinType.Inner, h => h.Album, (h, e) => h.AlbumId == e.Id); + .Join(JoinType.Inner, h => h.Album, (h, e) => h.AlbumId == e.Id) + .Join(JoinType.Left, h => h.Track, (h, e) => h.TrackId == e.Id); return base.GetPagedQuery(baseQuery, pagingSpec); } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Core/History/HistoryService.cs b/src/NzbDrone.Core/History/HistoryService.cs index 2f5707346..c9dba532a 100644 --- a/src/NzbDrone.Core/History/HistoryService.cs +++ b/src/NzbDrone.Core/History/HistoryService.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.IO; using System.Linq; @@ -10,7 +10,9 @@ using NzbDrone.Core.MediaFiles; using NzbDrone.Core.MediaFiles.Events; using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Parser.Model; -using NzbDrone.Core.Profiles; +using NzbDrone.Core.Profiles.Qualities; +using NzbDrone.Core.Profiles.Languages; +using NzbDrone.Core.Languages; using NzbDrone.Core.Qualities; using NzbDrone.Core.Music.Events; @@ -18,7 +20,6 @@ namespace NzbDrone.Core.History { public interface IHistoryService { - QualityModel GetBestQualityInHistory(Profile profile, int episodeId); PagingSpec Paged(PagingSpec pagingSpec); History MostRecentForAlbum(int episodeId); History MostRecentForDownloadId(string downloadId); @@ -29,9 +30,10 @@ namespace NzbDrone.Core.History public class HistoryService : IHistoryService, IHandle, - IHandle, + IHandle, IHandle, - IHandle, + IHandle, + IHandle, IHandle { private readonly IHistoryRepository _historyRepository; @@ -73,22 +75,13 @@ namespace NzbDrone.Core.History return _historyRepository.FindByDownloadId(downloadId); } - public QualityModel GetBestQualityInHistory(Profile profile, int albumId) + private string FindDownloadId(TrackImportedEvent trackedDownload) { - var comparer = new QualityModelComparer(profile); - return _historyRepository.GetBestQualityInHistory(albumId) - .OrderByDescending(q => q, comparer) - .FirstOrDefault(); - } - - [Obsolete("Used for Sonarr, not Lidarr")] - private string FindDownloadId(EpisodeImportedEvent trackedDownload) - { - _logger.Debug("Trying to find downloadId for {0} from history", trackedDownload.ImportedEpisode.Path); + _logger.Debug("Trying to find downloadId for {0} from history", trackedDownload.ImportedTrack.Path); - var albumIds = trackedDownload.EpisodeInfo.Episodes.Select(c => c.Id).ToList(); + var albumIds = trackedDownload.TrackInfo.Tracks.Select(c => c.AlbumId).ToList(); - var allHistory = _historyRepository.FindDownloadHistory(trackedDownload.EpisodeInfo.Series.Id, trackedDownload.ImportedEpisode.Quality); + var allHistory = _historyRepository.FindDownloadHistory(trackedDownload.TrackInfo.Artist.Id, trackedDownload.ImportedTrack.Quality); //Find download related items for these episdoes @@ -104,7 +97,7 @@ namespace NzbDrone.Core.History if (stillDownloading.Any()) { - foreach (var matchingHistory in trackedDownload.EpisodeInfo.Episodes.Select(e => stillDownloading.Where(c => c.AlbumId == e.Id).ToList())) + foreach (var matchingHistory in trackedDownload.TrackInfo.Tracks.Select(e => stillDownloading.Where(c => c.AlbumId == e.AlbumId).ToList())) { if (matchingHistory.Count != 1) { @@ -139,7 +132,8 @@ namespace NzbDrone.Core.History SourceTitle = message.Album.Release.Title, ArtistId = album.ArtistId, AlbumId = album.Id, - DownloadId = message.DownloadId + DownloadId = message.DownloadId, + Language = message.Album.ParsedAlbumInfo.Language }; history.Data.Add("Indexer", message.Album.Release.Indexer); @@ -171,8 +165,7 @@ namespace NzbDrone.Core.History } } - [Obsolete("Used for Sonarr, not Lidarr")] - public void Handle(EpisodeImportedEvent message) + public void Handle(TrackImportedEvent message) { if (!message.NewDownload) { @@ -186,23 +179,25 @@ namespace NzbDrone.Core.History downloadId = FindDownloadId(message); } - foreach (var episode in message.EpisodeInfo.Episodes) + foreach (var track in message.TrackInfo.Tracks) { var history = new History { EventType = HistoryEventType.DownloadFolderImported, Date = DateTime.UtcNow, - Quality = message.EpisodeInfo.Quality, - SourceTitle = message.ImportedEpisode.SceneName ?? Path.GetFileNameWithoutExtension(message.EpisodeInfo.Path), - ArtistId = message.ImportedEpisode.SeriesId, - AlbumId = episode.Id, - DownloadId = downloadId - }; + Quality = message.TrackInfo.Quality, + SourceTitle = message.ImportedTrack.SceneName ?? Path.GetFileNameWithoutExtension(message.TrackInfo.Path), + ArtistId = message.ImportedTrack.ArtistId, + AlbumId = message.ImportedTrack.AlbumId, + TrackId = track.Id, + DownloadId = downloadId, + Language = message.TrackInfo.Language + }; //Won't have a value since we publish this event before saving to DB. //history.Data.Add("FileId", message.ImportedEpisode.Id.ToString()); - history.Data.Add("DroppedPath", message.EpisodeInfo.Path); - history.Data.Add("ImportedPath", Path.Combine(message.EpisodeInfo.Series.Path, message.ImportedEpisode.RelativePath)); + history.Data.Add("DroppedPath", message.TrackInfo.Path); + history.Data.Add("ImportedPath", Path.Combine(message.TrackInfo.Artist.Path, message.ImportedTrack.RelativePath)); history.Data.Add("DownloadClient", message.DownloadClient); _historyRepository.Insert(history); @@ -221,7 +216,8 @@ namespace NzbDrone.Core.History SourceTitle = message.SourceTitle, ArtistId = message.ArtistId, AlbumId = albumId, - DownloadId = message.DownloadId + DownloadId = message.DownloadId, + Language = message.Language }; history.Data.Add("DownloadClient", message.DownloadClient); @@ -231,25 +227,25 @@ namespace NzbDrone.Core.History } } - [Obsolete("Used for Sonarr, not Lidarr")] - public void Handle(EpisodeFileDeletedEvent message) + public void Handle(TrackFileDeletedEvent message) { if (message.Reason == DeleteMediaFileReason.NoLinkedEpisodes) { - _logger.Debug("Removing episode file from DB as part of cleanup routine, not creating history event."); + _logger.Debug("Removing track file from DB as part of cleanup routine, not creating history event."); return; } - foreach (var episode in message.EpisodeFile.Episodes.Value) + foreach (var track in message.TrackFile.Tracks.Value) { var history = new History { - EventType = HistoryEventType.EpisodeFileDeleted, + EventType = HistoryEventType.TrackFileDeleted, Date = DateTime.UtcNow, - Quality = message.EpisodeFile.Quality, - SourceTitle = message.EpisodeFile.Path, - ArtistId = message.EpisodeFile.SeriesId, - AlbumId = episode.Id, + Quality = message.TrackFile.Quality, + SourceTitle = message.TrackFile.Path, + ArtistId = message.TrackFile.ArtistId, + AlbumId = message.TrackFile.AlbumId, + TrackId = track.Id, }; history.Data.Add("Reason", message.Reason.ToString()); @@ -258,9 +254,38 @@ namespace NzbDrone.Core.History } } + public void Handle(TrackFileRenamedEvent message) + { + var sourcePath = message.OriginalPath; + var sourceRelativePath = message.Artist.Path.GetRelativePath(message.OriginalPath); + var path = Path.Combine(message.Artist.Path, message.TrackFile.RelativePath); + var relativePath = message.TrackFile.RelativePath; + + foreach (var track in message.TrackFile.Tracks.Value) + { + var history = new History + { + EventType = HistoryEventType.TrackFileRenamed, + Date = DateTime.UtcNow, + Quality = message.TrackFile.Quality, + SourceTitle = message.OriginalPath, + ArtistId = message.TrackFile.ArtistId, + AlbumId = message.TrackFile.AlbumId, + TrackId = track.Id, + }; + + history.Data.Add("SourcePath", sourcePath); + history.Data.Add("SourceRelativePath", sourceRelativePath); + history.Data.Add("Path", path); + history.Data.Add("RelativePath", relativePath); + + _historyRepository.Insert(history); + } + } + public void Handle(ArtistDeletedEvent message) { _historyRepository.DeleteForArtist(message.Artist.Id); } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupDownloadClientUnavailablePendingReleases.cs b/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupDownloadClientUnavailablePendingReleases.cs new file mode 100644 index 000000000..eb3073a1f --- /dev/null +++ b/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupDownloadClientUnavailablePendingReleases.cs @@ -0,0 +1,32 @@ +using System; +using NzbDrone.Core.Datastore; +using NzbDrone.Core.Download.Pending; + +namespace NzbDrone.Core.Housekeeping.Housekeepers +{ + public class CleanupDownloadClientUnavailablePendingReleases : IHousekeepingTask + { + private readonly IMainDatabase _database; + + public CleanupDownloadClientUnavailablePendingReleases(IMainDatabase database) + { + _database = database; + } + + public void Clean() + { + var mapper = _database.GetDataMapper(); + var twoWeeksAgo = DateTime.UtcNow.AddDays(-14); + + mapper.Delete(p => p.Added < twoWeeksAgo && + (p.Reason == PendingReleaseReason.DownloadClientUnavailable || + p.Reason == PendingReleaseReason.Fallback)); + + // mapper.AddParameter("twoWeeksAgo", $"{DateTime.UtcNow.AddDays(-14).ToString("s")}Z"); + + // mapper.ExecuteNonQuery(@"DELETE FROM PendingReleases + // WHERE Added < @twoWeeksAgo + // AND (Reason = 'DownloadClientUnavailable' OR Reason = 'Fallback')"); + } + } +} diff --git a/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupDuplicateMetadataFiles.cs b/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupDuplicateMetadataFiles.cs index e65a117a1..5f21924ed 100644 --- a/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupDuplicateMetadataFiles.cs +++ b/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupDuplicateMetadataFiles.cs @@ -1,4 +1,4 @@ -using NzbDrone.Core.Datastore; +using NzbDrone.Core.Datastore; namespace NzbDrone.Core.Housekeeping.Housekeepers { @@ -13,12 +13,12 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers public void Clean() { - DeleteDuplicateSeriesMetadata(); - DeleteDuplicateEpisodeMetadata(); - DeleteDuplicateEpisodeImages(); + DeleteDuplicateArtistMetadata(); + DeleteDuplicateTrackMetadata(); + DeleteDuplicateTrackImages(); } - private void DeleteDuplicateSeriesMetadata() + private void DeleteDuplicateArtistMetadata() { var mapper = _database.GetDataMapper(); @@ -26,12 +26,25 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers WHERE Id IN ( SELECT Id FROM MetadataFiles WHERE Type = 1 - GROUP BY SeriesId, Consumer - HAVING COUNT(SeriesId) > 1 + GROUP BY ArtistId, Consumer + HAVING COUNT(ArtistId) > 1 )"); } - private void DeleteDuplicateEpisodeMetadata() + private void DeleteDuplicateAlbumMetadata() + { + var mapper = _database.GetDataMapper(); + + mapper.ExecuteNonQuery(@"DELETE FROM MetadataFiles + WHERE Id IN ( + SELECT Id FROM MetadataFiles + WHERE Type = 6 + GROUP BY AlbumId, Consumer + HAVING COUNT(AlbumId) > 1 + )"); + } + + private void DeleteDuplicateTrackMetadata() { var mapper = _database.GetDataMapper(); @@ -39,12 +52,12 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers WHERE Id IN ( SELECT Id FROM MetadataFiles WHERE Type = 2 - GROUP BY EpisodeFileId, Consumer - HAVING COUNT(EpisodeFileId) > 1 + GROUP BY TrackFileId, Consumer + HAVING COUNT(TrackFileId) > 1 )"); } - private void DeleteDuplicateEpisodeImages() + private void DeleteDuplicateTrackImages() { var mapper = _database.GetDataMapper(); @@ -52,8 +65,8 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers WHERE Id IN ( SELECT Id FROM MetadataFiles WHERE Type = 5 - GROUP BY EpisodeFileId, Consumer - HAVING COUNT(EpisodeFileId) > 1 + GROUP BY TrackFileId, Consumer + HAVING COUNT(TrackFileId) > 1 )"); } } diff --git a/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupOrphanedDownloadClientStatus.cs b/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupOrphanedDownloadClientStatus.cs new file mode 100644 index 000000000..258a51ab9 --- /dev/null +++ b/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupOrphanedDownloadClientStatus.cs @@ -0,0 +1,26 @@ +using NzbDrone.Core.Datastore; + +namespace NzbDrone.Core.Housekeeping.Housekeepers +{ + public class CleanupOrphanedDownloadClientStatus : IHousekeepingTask + { + private readonly IMainDatabase _database; + + public CleanupOrphanedDownloadClientStatus(IMainDatabase database) + { + _database = database; + } + + public void Clean() + { + var mapper = _database.GetDataMapper(); + + mapper.ExecuteNonQuery(@"DELETE FROM DownloadClientStatus + WHERE Id IN ( + SELECT DownloadClientStatus.Id FROM DownloadClientStatus + LEFT OUTER JOIN DownloadClients + ON DownloadClientStatus.ProviderId = DownloadClients.Id + WHERE DownloadClients.Id IS NULL)"); + } + } +} diff --git a/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupOrphanedIndexerStatus.cs b/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupOrphanedIndexerStatus.cs index b3cf47027..4df6f502b 100644 --- a/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupOrphanedIndexerStatus.cs +++ b/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupOrphanedIndexerStatus.cs @@ -1,4 +1,4 @@ -using NzbDrone.Core.Datastore; +using NzbDrone.Core.Datastore; namespace NzbDrone.Core.Housekeeping.Housekeepers { @@ -19,7 +19,7 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers WHERE Id IN ( SELECT IndexerStatus.Id FROM IndexerStatus LEFT OUTER JOIN Indexers - ON IndexerStatus.IndexerId = Indexers.Id + ON IndexerStatus.ProviderId = Indexers.Id WHERE Indexers.Id IS NULL)"); } } diff --git a/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupOrphanedMetadataFiles.cs b/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupOrphanedMetadataFiles.cs index 05ab54ea1..20fca61b7 100644 --- a/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupOrphanedMetadataFiles.cs +++ b/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupOrphanedMetadataFiles.cs @@ -1,4 +1,4 @@ -using NzbDrone.Core.Datastore; +using NzbDrone.Core.Datastore; namespace NzbDrone.Core.Housekeeping.Housekeepers { @@ -13,37 +13,51 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers public void Clean() { - DeleteOrphanedBySeries(); - DeleteOrphanedByEpisodeFile(); - DeleteWhereEpisodeFileIsZero(); + DeleteOrphanedByArtist(); + DeleteOrphanedByAlbum(); + DeleteOrphanedByTrackFile(); + DeleteWhereTrackFileIsZero(); } - private void DeleteOrphanedBySeries() + private void DeleteOrphanedByArtist() { var mapper = _database.GetDataMapper(); mapper.ExecuteNonQuery(@"DELETE FROM MetadataFiles WHERE Id IN ( SELECT MetadataFiles.Id FROM MetadataFiles - LEFT OUTER JOIN Series - ON MetadataFiles.SeriesId = Series.Id - WHERE Series.Id IS NULL)"); + LEFT OUTER JOIN Artists + ON MetadataFiles.ArtistId = Artists.Id + WHERE Artists.Id IS NULL)"); } - private void DeleteOrphanedByEpisodeFile() + private void DeleteOrphanedByAlbum() { var mapper = _database.GetDataMapper(); mapper.ExecuteNonQuery(@"DELETE FROM MetadataFiles WHERE Id IN ( SELECT MetadataFiles.Id FROM MetadataFiles - LEFT OUTER JOIN EpisodeFiles - ON MetadataFiles.EpisodeFileId = EpisodeFiles.Id - WHERE MetadataFiles.EpisodeFileId > 0 - AND EpisodeFiles.Id IS NULL)"); + LEFT OUTER JOIN Albums + ON MetadataFiles.AlbumId = Albums.Id + WHERE MetadataFiles.AlbumId > 0 + AND Albums.Id IS NULL)"); } - private void DeleteWhereEpisodeFileIsZero() + private void DeleteOrphanedByTrackFile() + { + var mapper = _database.GetDataMapper(); + + mapper.ExecuteNonQuery(@"DELETE FROM MetadataFiles + WHERE Id IN ( + SELECT MetadataFiles.Id FROM MetadataFiles + LEFT OUTER JOIN TrackFiles + ON MetadataFiles.TrackFileId = TrackFiles.Id + WHERE MetadataFiles.TrackFileId > 0 + AND TrackFiles.Id IS NULL)"); + } + + private void DeleteWhereTrackFileIsZero() { var mapper = _database.GetDataMapper(); @@ -51,7 +65,7 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers WHERE Id IN ( SELECT Id FROM MetadataFiles WHERE Type IN (2, 5) - AND EpisodeFileId = 0)"); + AND TrackFileId = 0)"); } } } diff --git a/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupUnusedTags.cs b/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupUnusedTags.cs index 63debb4b7..1eb50ba2b 100644 --- a/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupUnusedTags.cs +++ b/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupUnusedTags.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using Marr.Data; using NzbDrone.Common.Serializer; @@ -19,7 +19,7 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers { var mapper = _database.GetDataMapper(); - var usedTags = new[] { "Series", "Notifications", "DelayProfiles", "Restrictions" } + var usedTags = new[] { "Artists", "Notifications", "DelayProfiles", "Restrictions" } .SelectMany(v => GetUsedTags(v, mapper)) .Distinct() .ToArray(); diff --git a/src/NzbDrone.Core/Housekeeping/Housekeepers/DeleteBadMediaCovers.cs b/src/NzbDrone.Core/Housekeeping/Housekeepers/DeleteBadMediaCovers.cs index 0bd74614b..d61df11b3 100644 --- a/src/NzbDrone.Core/Housekeeping/Housekeepers/DeleteBadMediaCovers.cs +++ b/src/NzbDrone.Core/Housekeeping/Housekeepers/DeleteBadMediaCovers.cs @@ -1,30 +1,30 @@ -using System; +using System; using System.IO; using System.Linq; using NLog; using NzbDrone.Common.Disk; using NzbDrone.Core.Configuration; using NzbDrone.Core.Extras.Metadata.Files; -using NzbDrone.Core.Tv; +using NzbDrone.Core.Music; namespace NzbDrone.Core.Housekeeping.Housekeepers { public class DeleteBadMediaCovers : IHousekeepingTask { private readonly IMetadataFileService _metaFileService; - private readonly ISeriesService _seriesService; + private readonly IArtistService _artistService; private readonly IDiskProvider _diskProvider; private readonly IConfigService _configService; private readonly Logger _logger; public DeleteBadMediaCovers(IMetadataFileService metaFileService, - ISeriesService seriesService, + IArtistService artistService, IDiskProvider diskProvider, IConfigService configService, Logger logger) { _metaFileService = metaFileService; - _seriesService = seriesService; + _artistService = artistService; _diskProvider = diskProvider; _configService = configService; _logger = logger; @@ -34,18 +34,18 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers { if (!_configService.CleanupMetadataImages) return; - var series = _seriesService.GetAllSeries(); + var artists = _artistService.GetAllArtists(); - foreach (var show in series) + foreach (var artist in artists) { - var images = _metaFileService.GetFilesBySeries(show.Id) + var images = _metaFileService.GetFilesByArtist(artist.Id) .Where(c => c.LastUpdated > new DateTime(2014, 12, 27) && c.RelativePath.EndsWith(".jpg", StringComparison.InvariantCultureIgnoreCase)); foreach (var image in images) { try { - var path = Path.Combine(show.Path, image.RelativePath); + var path = Path.Combine(artist.Path, image.RelativePath); if (!IsValid(path)) { _logger.Debug("Deleting invalid image file " + path); @@ -84,4 +84,4 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers return !text.ToLowerInvariant().Contains("html"); } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Core/Housekeeping/Housekeepers/FixFutureDownloadClientStatusTimes.cs b/src/NzbDrone.Core/Housekeeping/Housekeepers/FixFutureDownloadClientStatusTimes.cs new file mode 100644 index 000000000..58361e0b7 --- /dev/null +++ b/src/NzbDrone.Core/Housekeeping/Housekeepers/FixFutureDownloadClientStatusTimes.cs @@ -0,0 +1,12 @@ +using NzbDrone.Core.Download; + +namespace NzbDrone.Core.Housekeeping.Housekeepers +{ + public class FixFutureDownloadClientStatusTimes : FixFutureProviderStatusTimes, IHousekeepingTask + { + public FixFutureDownloadClientStatusTimes(IDownloadClientStatusRepository downloadClientStatusRepository) + : base(downloadClientStatusRepository) + { + } + } +} diff --git a/src/NzbDrone.Core/Housekeeping/Housekeepers/FixFutureIndexerStatusTimes.cs b/src/NzbDrone.Core/Housekeeping/Housekeepers/FixFutureIndexerStatusTimes.cs new file mode 100644 index 000000000..f635698d5 --- /dev/null +++ b/src/NzbDrone.Core/Housekeeping/Housekeepers/FixFutureIndexerStatusTimes.cs @@ -0,0 +1,12 @@ +using NzbDrone.Core.Indexers; + +namespace NzbDrone.Core.Housekeeping.Housekeepers +{ + public class FixFutureIndexerStatusTimes : FixFutureProviderStatusTimes, IHousekeepingTask + { + public FixFutureIndexerStatusTimes(IIndexerStatusRepository indexerStatusRepository) + : base(indexerStatusRepository) + { + } + } +} diff --git a/src/NzbDrone.Core/Housekeeping/Housekeepers/FixFutureProviderStatusTimes.cs b/src/NzbDrone.Core/Housekeeping/Housekeepers/FixFutureProviderStatusTimes.cs new file mode 100644 index 000000000..80bf5c8b9 --- /dev/null +++ b/src/NzbDrone.Core/Housekeeping/Housekeepers/FixFutureProviderStatusTimes.cs @@ -0,0 +1,56 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using NzbDrone.Core.ThingiProvider.Status; + +namespace NzbDrone.Core.Housekeeping.Housekeepers +{ + public abstract class FixFutureProviderStatusTimes where TModel : ProviderStatusBase, new() + { + private readonly IProviderStatusRepository _repo; + + protected FixFutureProviderStatusTimes(IProviderStatusRepository repo) + { + _repo = repo; + } + + public void Clean() + { + var now = DateTime.UtcNow; + var statuses = _repo.All().ToList(); + var toUpdate = new List(); + + foreach (var status in statuses) + { + var updated = false; + var escalationDelay = EscalationBackOff.Periods[status.EscalationLevel]; + var disabledTill = now.AddMinutes(escalationDelay); + + if (status.DisabledTill > disabledTill) + { + status.DisabledTill = disabledTill; + updated = true; + } + + if (status.InitialFailure > now) + { + status.InitialFailure = now; + updated = true; + } + + if (status.MostRecentFailure > now) + { + status.MostRecentFailure = now; + updated = true; + } + + if (updated) + { + toUpdate.Add(status); + } + } + + _repo.UpdateMany(toUpdate); + } + } +} diff --git a/src/NzbDrone.Core/Housekeeping/Housekeepers/UpdateCleanTitleForArtist.cs b/src/NzbDrone.Core/Housekeeping/Housekeepers/UpdateCleanTitleForArtist.cs new file mode 100644 index 000000000..04dce6e36 --- /dev/null +++ b/src/NzbDrone.Core/Housekeeping/Housekeepers/UpdateCleanTitleForArtist.cs @@ -0,0 +1,27 @@ +using System.Linq; +using NzbDrone.Core.Parser; +using NzbDrone.Core.Music; + +namespace NzbDrone.Core.Housekeeping.Housekeepers +{ + public class UpdateCleanTitleForArtist : IHousekeepingTask + { + private readonly IArtistRepository _artistRepository; + + public UpdateCleanTitleForArtist(IArtistRepository artistRepository) + { + _artistRepository = artistRepository; + } + + public void Clean() + { + var artists = _artistRepository.All().ToList(); + + artists.ForEach(s => + { + s.CleanName = s.CleanName.CleanArtistName(); + _artistRepository.Update(s); + }); + } + } +} diff --git a/src/NzbDrone.Core/Housekeeping/Housekeepers/UpdateCleanTitleForSeries.cs b/src/NzbDrone.Core/Housekeeping/Housekeepers/UpdateCleanTitleForSeries.cs deleted file mode 100644 index 16b19c505..000000000 --- a/src/NzbDrone.Core/Housekeeping/Housekeepers/UpdateCleanTitleForSeries.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System.Linq; -using NzbDrone.Core.Parser; -using NzbDrone.Core.Tv; - -namespace NzbDrone.Core.Housekeeping.Housekeepers -{ - public class UpdateCleanTitleForSeries : IHousekeepingTask - { - private readonly ISeriesRepository _seriesRepository; - - public UpdateCleanTitleForSeries(ISeriesRepository seriesRepository) - { - _seriesRepository = seriesRepository; - } - - public void Clean() - { - var series = _seriesRepository.All().ToList(); - - series.ForEach(s => - { - s.CleanTitle = s.CleanTitle.CleanSeriesTitle(); - _seriesRepository.Update(s); - }); - } - } -} diff --git a/src/NzbDrone.Core/IndexerSearch/AlbumSearchService.cs b/src/NzbDrone.Core/IndexerSearch/AlbumSearchService.cs index 70a5be751..f34a5422b 100644 --- a/src/NzbDrone.Core/IndexerSearch/AlbumSearchService.cs +++ b/src/NzbDrone.Core/IndexerSearch/AlbumSearchService.cs @@ -1,25 +1,59 @@ -using NLog; +using System; +using System.Collections.Generic; +using System.Linq; +using NLog; +using NzbDrone.Common.Extensions; using NzbDrone.Common.Instrumentation.Extensions; +using NzbDrone.Core.Datastore; +using NzbDrone.Core.DecisionEngine; using NzbDrone.Core.Download; using NzbDrone.Core.Messaging.Commands; +using NzbDrone.Core.Queue; +using NzbDrone.Core.Music; namespace NzbDrone.Core.IndexerSearch { - class AlbumSearchService : IExecute + class AlbumSearchService : IExecute, + IExecute + //IExecute { private readonly ISearchForNzb _nzbSearchService; + private readonly IAlbumService _albumService; + private readonly IQueueService _queueService; private readonly IProcessDownloadDecisions _processDownloadDecisions; private readonly Logger _logger; public AlbumSearchService(ISearchForNzb nzbSearchService, + IAlbumService albumService, + IQueueService queueService, IProcessDownloadDecisions processDownloadDecisions, Logger logger) { _nzbSearchService = nzbSearchService; + _albumService = albumService; + _queueService = queueService; _processDownloadDecisions = processDownloadDecisions; _logger = logger; } + private void SearchForMissingAlbums(List albums, bool userInvokedSearch) + { + _logger.ProgressInfo("Performing missing search for {0} albums", albums.Count); + var downloadedCount = 0; + + foreach (var album in albums) + { + List decisions; + decisions = _nzbSearchService.AlbumSearch(album.Id, false, userInvokedSearch); + var processed = _processDownloadDecisions.ProcessDecisions(decisions); + + downloadedCount += processed.Grabbed.Count; + } + + _logger.ProgressInfo("Completed missing search for {0} albums. {1} reports downloaded.", albums.Count, downloadedCount); + } + + public void Execute(AlbumSearchCommand message) { foreach (var albumId in message.AlbumIds) @@ -31,5 +65,47 @@ namespace NzbDrone.Core.IndexerSearch _logger.ProgressInfo("Album search completed. {0} reports downloaded.", processed.Grabbed.Count); } } + + public void Execute(MissingAlbumSearchCommand message) + { + List albums; + + if (message.ArtistId.HasValue) + { + int artistId = message.ArtistId.Value; + + albums = _albumService.AlbumsWithoutFiles(new PagingSpec + { + Page = 1, + PageSize = 100000, + SortDirection = SortDirection.Ascending, + SortKey = "Id", + FilterExpression = + v => + v.Monitored == true && + v.Artist.Monitored == true + }).Records.Where(e => e.ArtistId.Equals(artistId)).ToList(); + } + + else + { + albums = _albumService.AlbumsWithoutFiles(new PagingSpec + { + Page = 1, + PageSize = 100000, + SortDirection = SortDirection.Ascending, + SortKey = "Id", + FilterExpression = + v => + v.Monitored == true && + v.Artist.Monitored == true + }).Records.ToList(); + } + + var queue = _queueService.GetQueue().Select(q => q.Album.Id); + var missing = albums.Where(e => !queue.Contains(e.Id)).ToList(); + + SearchForMissingAlbums(missing, message.Trigger == CommandTrigger.Manual); + } } } diff --git a/src/NzbDrone.Core/IndexerSearch/CutoffUnmetAlbumSearchCommand.cs b/src/NzbDrone.Core/IndexerSearch/CutoffUnmetAlbumSearchCommand.cs new file mode 100644 index 000000000..65173f037 --- /dev/null +++ b/src/NzbDrone.Core/IndexerSearch/CutoffUnmetAlbumSearchCommand.cs @@ -0,0 +1,26 @@ +using NzbDrone.Core.Messaging.Commands; + +namespace NzbDrone.Core.IndexerSearch +{ + public class CutoffUnmetAlbumSearchCommand : Command + { + public int? SeriesId { get; set; } + + public override bool SendUpdatesToClient + { + get + { + return true; + } + } + + public CutoffUnmetAlbumSearchCommand() + { + } + + public CutoffUnmetAlbumSearchCommand(int seriesId) + { + SeriesId = seriesId; + } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Core/IndexerSearch/Definitions/SearchCriteriaBase.cs b/src/NzbDrone.Core/IndexerSearch/Definitions/SearchCriteriaBase.cs index 5b2a51097..965b5d87c 100644 --- a/src/NzbDrone.Core/IndexerSearch/Definitions/SearchCriteriaBase.cs +++ b/src/NzbDrone.Core/IndexerSearch/Definitions/SearchCriteriaBase.cs @@ -1,9 +1,8 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; using NzbDrone.Common.EnsureThat; using NzbDrone.Common.Extensions; -using NzbDrone.Core.Tv; using NzbDrone.Core.Music; namespace NzbDrone.Core.IndexerSearch.Definitions @@ -14,12 +13,9 @@ namespace NzbDrone.Core.IndexerSearch.Definitions private static readonly Regex NonWord = new Regex(@"[\W]", RegexOptions.IgnoreCase | RegexOptions.Compiled); private static readonly Regex BeginningThe = new Regex(@"^the\s", RegexOptions.IgnoreCase | RegexOptions.Compiled); - [System.Obsolete("Sonarr TV Stuff -- Shouldn't be needed for Lidarr")] - public Series Series { get; set; } [System.Obsolete("Sonarr TV Stuff -- Shouldn't be needed for Lidarr")] public List SceneTitles { get; set; } - [System.Obsolete("Sonarr TV Stuff -- Shouldn't be needed for Lidarr")] - public List Episodes { get; set; } + public virtual bool MonitoredEpisodesOnly { get; set; } public virtual bool UserInvokedSearch { get; set; } @@ -45,4 +41,4 @@ namespace NzbDrone.Core.IndexerSearch.Definitions return cleanTitle.Trim('+', ' '); } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Core/IndexerSearch/MissingAlbumSearchCommand.cs b/src/NzbDrone.Core/IndexerSearch/MissingAlbumSearchCommand.cs new file mode 100644 index 000000000..1f4ccb7b8 --- /dev/null +++ b/src/NzbDrone.Core/IndexerSearch/MissingAlbumSearchCommand.cs @@ -0,0 +1,20 @@ +using NzbDrone.Core.Messaging.Commands; + +namespace NzbDrone.Core.IndexerSearch +{ + public class MissingAlbumSearchCommand : Command + { + public int? ArtistId { get; set; } + + public override bool SendUpdatesToClient => true; + + public MissingAlbumSearchCommand() + { + } + + public MissingAlbumSearchCommand(int artistId) + { + ArtistId = artistId; + } + } +} diff --git a/src/NzbDrone.Core/IndexerSearch/MissingEpisodeSearchCommand.cs b/src/NzbDrone.Core/IndexerSearch/MissingEpisodeSearchCommand.cs deleted file mode 100644 index 3e2097be3..000000000 --- a/src/NzbDrone.Core/IndexerSearch/MissingEpisodeSearchCommand.cs +++ /dev/null @@ -1,20 +0,0 @@ -using NzbDrone.Core.Messaging.Commands; - -namespace NzbDrone.Core.IndexerSearch -{ - public class MissingEpisodeSearchCommand : Command - { - public int? SeriesId { get; set; } - - public override bool SendUpdatesToClient => true; - - public MissingEpisodeSearchCommand() - { - } - - public MissingEpisodeSearchCommand(int seriesId) - { - SeriesId = seriesId; - } - } -} \ No newline at end of file diff --git a/src/NzbDrone.Core/IndexerSearch/NzbSearchService.cs b/src/NzbDrone.Core/IndexerSearch/NzbSearchService.cs index 9a318ea1a..e129db351 100644 --- a/src/NzbDrone.Core/IndexerSearch/NzbSearchService.cs +++ b/src/NzbDrone.Core/IndexerSearch/NzbSearchService.cs @@ -4,12 +4,10 @@ using System.Globalization; using System.Threading.Tasks; using NLog; using NzbDrone.Common.Instrumentation.Extensions; -using NzbDrone.Core.DataAugmentation.Scene; using NzbDrone.Core.DecisionEngine; using NzbDrone.Core.IndexerSearch.Definitions; using NzbDrone.Core.Indexers; using NzbDrone.Core.Parser.Model; -using NzbDrone.Core.Tv; using System.Linq; using NzbDrone.Common.TPL; using NzbDrone.Core.Music; diff --git a/src/NzbDrone.Core/Indexers/Fanzub/FanzubSettings.cs b/src/NzbDrone.Core/Indexers/Fanzub/FanzubSettings.cs index 1f9f25028..a0760b5d8 100644 --- a/src/NzbDrone.Core/Indexers/Fanzub/FanzubSettings.cs +++ b/src/NzbDrone.Core/Indexers/Fanzub/FanzubSettings.cs @@ -1,4 +1,4 @@ -using FluentValidation; +using FluentValidation; using NzbDrone.Core.Annotations; using NzbDrone.Core.ThingiProvider; using NzbDrone.Core.Validation; @@ -13,7 +13,7 @@ namespace NzbDrone.Core.Indexers.Fanzub } } - public class FanzubSettings : IProviderConfig + public class FanzubSettings : IIndexerSettings { private static readonly FanzubSettingsValidator Validator = new FanzubSettingsValidator(); diff --git a/src/NzbDrone.Core/Indexers/Gazelle/Gazelle.cs b/src/NzbDrone.Core/Indexers/Gazelle/Gazelle.cs new file mode 100644 index 000000000..603d7ed85 --- /dev/null +++ b/src/NzbDrone.Core/Indexers/Gazelle/Gazelle.cs @@ -0,0 +1,81 @@ +using NLog; +using NzbDrone.Common.Cache; +using NzbDrone.Common.Http; +using NzbDrone.Core.Configuration; +using NzbDrone.Core.Parser; +using NzbDrone.Core.ThingiProvider; +using System.Collections.Generic; + +namespace NzbDrone.Core.Indexers.Gazelle +{ + public class Gazelle : HttpIndexerBase + { + public override string Name => "Gazelle API"; + public override DownloadProtocol Protocol => DownloadProtocol.Torrent; + public override bool SupportsRss => true; + public override bool SupportsSearch => true; + public override int PageSize => 50; + + private readonly ICached> _authCookieCache; + private readonly IHttpClient _httpClient; + private readonly Logger _logger; + + public Gazelle(IHttpClient httpClient, ICacheManager cacheManager, IIndexerStatusService indexerStatusService, + IConfigService configService, IParsingService parsingService, Logger logger) + : base(httpClient, indexerStatusService, configService, parsingService, logger) + { + _httpClient = httpClient; + _logger = logger; + _authCookieCache = cacheManager.GetCache>(GetType(), "authCookies"); + } + + public override IIndexerRequestGenerator GetRequestGenerator() + { + return new GazelleRequestGenerator() + { + Settings = Settings, + HttpClient = _httpClient, + Logger = _logger, + AuthCookieCache = _authCookieCache + }; + } + + public override IParseIndexerResponse GetParser() + { + return new GazelleParser(Settings); + } + + public override IEnumerable DefaultDefinitions + { + get + { + yield return GetDefinition("Apollo.Rip", GetSettings("https://apollo.rip")); + yield return GetDefinition("REDacted", GetSettings("https://redacted.ch")); + yield return GetDefinition("Not What CD", GetSettings("https://notwhat.cd")); + + } + } + + private IndexerDefinition GetDefinition(string name, GazelleSettings settings) + { + return new IndexerDefinition + { + EnableRss = false, + EnableSearch = false, + Name = name, + Implementation = GetType().Name, + Settings = settings, + Protocol = DownloadProtocol.Torrent, + SupportsRss = SupportsRss, + SupportsSearch = SupportsSearch + }; + } + + private GazelleSettings GetSettings(string url) + { + var settings = new GazelleSettings { BaseUrl = url }; + + return settings; + } + } +} diff --git a/src/NzbDrone.Core/Indexers/Gazelle/GazelleApi.cs b/src/NzbDrone.Core/Indexers/Gazelle/GazelleApi.cs new file mode 100644 index 000000000..77318f24f --- /dev/null +++ b/src/NzbDrone.Core/Indexers/Gazelle/GazelleApi.cs @@ -0,0 +1,88 @@ +using System; +using Newtonsoft.Json; +using System.Collections.Generic; + +namespace NzbDrone.Core.Indexers.Gazelle +{ + public class GazelleArtist + { + public string Name { get; set; } + public string Id { get; set; } + public string Aliasid { get; set; } + } + + public class GazelleTorrent + { + public int TorrentId { get; set; } + public int EditionId { get; set; } + public List Artists { get; set; } + public bool Remastered { get; set; } + public string RemasterYear { get; set; } + public string RemasterTitle { get; set; } + public string Media { get; set; } + public string Encoding { get; set; } + public string Format { get; set; } + public bool HasLog { get; set; } + public int LogScore { get; set; } + public bool HasQueue { get; set; } + public bool Scene { get; set; } + public bool VanityHouse { get; set; } + public int FileCount { get; set; } + public DateTime Time { get; set; } + public string Size { get; set; } + public string Snatches { get; set; } + public string Seeders { get; set; } + public string Leechers { get; set; } + public bool IsFreeLeech { get; set; } + public bool IsNeutralLeech { get; set; } + public bool IsPersonalFreeLeech { get; set; } + public bool CanUseToken { get; set; } + } + + public class GazelleRelease + { + public string GroupId { get; set; } + public string GroupName { get; set; } + public string Artist { get; set; } + public string GroupYear { get; set; } + public string Cover { get; set; } + public List Tags { get; set; } + public string ReleaseType { get; set; } + public int TotalLeechers { get; set; } + public int TotalSeeders { get; set; } + public int TotalSnatched { get; set; } + public long MaxSize { get; set; } + public string GroupTime { get; set; } + public List Torrents { get; set; } + } + + public class GazelleResponse + { + public string Status { get; set; } + public GazelleBrowseResponse Response { get; set; } + } + + public class GazelleBrowseResponse + { + public List Results { get; set; } + public string CurrentPage { get; set; } + public string Pages { get; set; } + } + + public class GazelleAuthResponse + { + public string Status { get; set; } + public GazelleIndexResponse Response { get; set; } + + } + + public class GazelleIndexResponse + { + public string Username { get; set; } + public string Id { get; set; } + public string Authkey { get; set; } + public string Passkey { get; set; } + + } + +} diff --git a/src/NzbDrone.Core/Indexers/Gazelle/GazelleInfo.cs b/src/NzbDrone.Core/Indexers/Gazelle/GazelleInfo.cs new file mode 100644 index 000000000..84915f605 --- /dev/null +++ b/src/NzbDrone.Core/Indexers/Gazelle/GazelleInfo.cs @@ -0,0 +1,13 @@ +using NzbDrone.Core.Parser.Model; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace NzbDrone.Core.Indexers.Gazelle +{ + public class GazelleInfo : TorrentInfo + { + public bool? Scene { get; set; } + } +} diff --git a/src/NzbDrone.Core/Indexers/Gazelle/GazelleParser.cs b/src/NzbDrone.Core/Indexers/Gazelle/GazelleParser.cs new file mode 100644 index 000000000..f60695486 --- /dev/null +++ b/src/NzbDrone.Core/Indexers/Gazelle/GazelleParser.cs @@ -0,0 +1,112 @@ +using System.Collections.Generic; +using System.Net; +using Newtonsoft.Json; +using NzbDrone.Common.Http; +using NzbDrone.Core.Indexers.Exceptions; +using NzbDrone.Core.Parser.Model; +using System.Linq; +using NzbDrone.Common.Cache; +using NzbDrone.Common.Extensions; + +namespace NzbDrone.Core.Indexers.Gazelle +{ + public class GazelleParser : IParseIndexerResponse + { + private readonly GazelleSettings _settings; + public ICached> AuthCookieCache { get; set; } + + public GazelleParser(GazelleSettings settings) + { + _settings = settings; + } + + public IList ParseResponse(IndexerResponse indexerResponse) + { + var torrentInfos = new List(); + + if (indexerResponse.HttpResponse.StatusCode != HttpStatusCode.OK) + { + // Remove cookie cache + AuthCookieCache.Remove(_settings.BaseUrl.Trim().TrimEnd('/')); + + throw new IndexerException(indexerResponse, $"Unexpected response status {indexerResponse.HttpResponse.StatusCode} code from API request"); + } + + if (!indexerResponse.HttpResponse.Headers.ContentType.Contains(HttpAccept.Json.Value)) + { + // Remove cookie cache + AuthCookieCache.Remove(_settings.BaseUrl.Trim().TrimEnd('/')); + + throw new IndexerException(indexerResponse, $"Unexpected response header {indexerResponse.HttpResponse.Headers.ContentType} from API request, expected {HttpAccept.Json.Value}"); + } + + var jsonResponse = JsonConvert.DeserializeObject(indexerResponse.Content); + if (jsonResponse.Status != "success" || + jsonResponse.Status.IsNullOrWhiteSpace() || + jsonResponse.Response == null) + { + return torrentInfos; + } + + + foreach (var result in jsonResponse.Response.Results) + { + if (result.Torrents != null) + { + foreach (var torrent in result.Torrents) + { + var id = torrent.TorrentId; + + torrentInfos.Add(new GazelleInfo() + { + Guid = string.Format("Gazelle-{0}", id), + Artist = result.Artist, + // Splice Title from info to avoid calling API again for every torrent. + Title = result.Artist + " - " + result.GroupName + " (" + result.GroupYear +") (" + torrent.Format + " " + torrent.Encoding + ")", + Album = result.GroupName, + Container = torrent.Encoding, + Codec = torrent.Format, + Size = long.Parse(torrent.Size), + DownloadUrl = GetDownloadUrl(id, _settings.AuthKey, _settings.PassKey), + InfoUrl = GetInfoUrl(result.GroupId, id), + Seeders = int.Parse(torrent.Seeders), + Peers = int.Parse(torrent.Leechers) + int.Parse(torrent.Seeders), + PublishDate = torrent.Time.ToUniversalTime(), + Scene = torrent.Scene, + }); + } + } + } + + var torr = torrentInfos; + // order by date + return + torrentInfos + .OrderByDescending(o => o.PublishDate) + .ToArray(); + + } + + private string GetDownloadUrl(int torrentId, string authKey, string passKey) + { + var url = new HttpUri(_settings.BaseUrl) + .CombinePath("/torrents.php") + .AddQueryParam("action", "download") + .AddQueryParam("id", torrentId) + .AddQueryParam("authkey", authKey) + .AddQueryParam("torrent_pass", passKey); + + return url.FullUri; + } + + private string GetInfoUrl(string groupId, int torrentId) + { + var url = new HttpUri(_settings.BaseUrl) + .CombinePath("/torrents.php") + .AddQueryParam("id", groupId) + .AddQueryParam("torrentid", torrentId); + + return url.FullUri; + } + } +} diff --git a/src/NzbDrone.Core/Indexers/Gazelle/GazelleRequestGenerator.cs b/src/NzbDrone.Core/Indexers/Gazelle/GazelleRequestGenerator.cs new file mode 100644 index 000000000..c27403193 --- /dev/null +++ b/src/NzbDrone.Core/Indexers/Gazelle/GazelleRequestGenerator.cs @@ -0,0 +1,141 @@ +using System; +using System.Collections.Generic; +using NzbDrone.Common.Http; +using NzbDrone.Core.IndexerSearch.Definitions; +using NzbDrone.Common.Cache; +using NLog; +using NzbDrone.Common.Serializer; + +namespace NzbDrone.Core.Indexers.Gazelle +{ + public class GazelleRequestGenerator : IIndexerRequestGenerator + { + + public GazelleSettings Settings { get; set; } + + public ICached> AuthCookieCache { get; set; } + public IHttpClient HttpClient { get; set; } + public Logger Logger { get; set; } + + public virtual IndexerPageableRequestChain GetRecentRequests() + { + var pageableRequests = new IndexerPageableRequestChain(); + + pageableRequests.Add(GetRequest(null)); + + return pageableRequests; + } + + public IndexerPageableRequestChain GetSearchRequests(AlbumSearchCriteria searchCriteria) + { + var pageableRequests = new IndexerPageableRequestChain(); + pageableRequests.Add(GetRequest(string.Format("&artistname={0}&groupname={1}", searchCriteria.Artist.Name, searchCriteria.AlbumTitle))); + return pageableRequests; + } + + public IndexerPageableRequestChain GetSearchRequests(ArtistSearchCriteria searchCriteria) + { + var pageableRequests = new IndexerPageableRequestChain(); + pageableRequests.Add(GetRequest(string.Format("&artistname={0}",searchCriteria.Artist.Name))); + return pageableRequests; + } + + private IEnumerable GetRequest(string searchParameters) + { + Authenticate(); + + var filter = ""; + if (searchParameters == null) + { + + } + + var request = + new IndexerRequest( + $"{Settings.BaseUrl.Trim().TrimEnd('/')}/ajax.php?action=browse&searchstr={searchParameters}{filter}", + HttpAccept.Json); + + var cookies = AuthCookieCache.Find(Settings.BaseUrl.Trim().TrimEnd('/')); + foreach (var cookie in cookies) + { + request.HttpRequest.Cookies[cookie.Key] = cookie.Value; + } + + yield return request; + } + + private GazelleAuthResponse GetIndex(Dictionary cookies) + { + var indexRequestBuilder = new HttpRequestBuilder($"{Settings.BaseUrl.Trim().TrimEnd('/')}") + { + LogResponseContent = true + }; + + indexRequestBuilder.SetCookies(cookies); + indexRequestBuilder.Method = HttpMethod.GET; + indexRequestBuilder.Resource("ajax.php?action=index"); + + var authIndexRequest = indexRequestBuilder + .Accept(HttpAccept.Json) + .Build(); + + var indexResponse = HttpClient.Execute(authIndexRequest); + + var result = Json.Deserialize(indexResponse.Content); + + return result; + } + + private void Authenticate() + { + + var requestBuilder = new HttpRequestBuilder($"{Settings.BaseUrl.Trim().TrimEnd('/')}") + { + LogResponseContent = true + }; + + requestBuilder.Method = HttpMethod.POST; + requestBuilder.Resource("login.php"); + requestBuilder.PostProcess += r => r.RequestTimeout = TimeSpan.FromSeconds(15); + + var authKey = Settings.BaseUrl.Trim().TrimEnd('/'); + var cookies = AuthCookieCache.Find(authKey); + + if (cookies == null) + { + AuthCookieCache.Remove(authKey); + var authLoginRequest = requestBuilder + .AddFormParameter("username", Settings.Username) + .AddFormParameter("password", Settings.Password) + .AddFormParameter("keeplogged", "1") + .SetHeader("Content-Type", "multipart/form-data") + .Accept(HttpAccept.Json) + .Build(); + + var response = HttpClient.Execute(authLoginRequest); + + cookies = response.GetCookies(); + AuthCookieCache.Set(authKey, cookies, new TimeSpan(7, 0, 0, 0, 0)); // re-auth every 7 days + requestBuilder.SetCookies(cookies); + } + else + { + requestBuilder.SetCookies(cookies); + } + + var index = GetIndex(cookies); + + if (index.Status != "success" || string.IsNullOrWhiteSpace(index.Status)) + { + Logger.Debug("Gazelle authentication failed."); + throw new Exception("Failed to authenticate with Gazelle."); + } + + Logger.Debug("Gazelle authentication succeeded."); + + Settings.AuthKey = index.Response.Authkey; + Settings.PassKey = index.Response.Passkey; + + } + } +} diff --git a/src/NzbDrone.Core/Indexers/Gazelle/GazelleSettings.cs b/src/NzbDrone.Core/Indexers/Gazelle/GazelleSettings.cs new file mode 100644 index 000000000..428a99b9e --- /dev/null +++ b/src/NzbDrone.Core/Indexers/Gazelle/GazelleSettings.cs @@ -0,0 +1,47 @@ +using FluentValidation; +using NzbDrone.Core.Annotations; +using NzbDrone.Core.Validation; +using System.Text.RegularExpressions; + +namespace NzbDrone.Core.Indexers.Gazelle +{ + public class GazelleSettingsValidator : AbstractValidator + { + public GazelleSettingsValidator() + { + RuleFor(c => c.BaseUrl).ValidRootUrl(); + RuleFor(c => c.Username).NotEmpty(); + RuleFor(c => c.Password).NotEmpty(); + } + } + + public class GazelleSettings : ITorrentIndexerSettings + { + private static readonly GazelleSettingsValidator Validator = new GazelleSettingsValidator(); + + public GazelleSettings() + { + MinimumSeeders = IndexerDefaults.MINIMUM_SEEDERS; + } + + public string AuthKey; + public string PassKey; + + [FieldDefinition(0, Label = "URL", Advanced = true, HelpText = "Do not change this unless you know what you're doing. Since your cookie will be sent to that host.")] + public string BaseUrl { get; set; } + + [FieldDefinition(1, Label = "Username", HelpText = "Username")] + public string Username { get; set; } + + [FieldDefinition(2, Label = "Password", Type = FieldType.Password, HelpText = "Password")] + public string Password { get; set; } + + [FieldDefinition(3, Type = FieldType.Textbox, Label = "Minimum Seeders", HelpText = "Minimum number of seeders required.", Advanced = true)] + public int MinimumSeeders { get; set; } + + public NzbDroneValidationResult Validate() + { + return new NzbDroneValidationResult(Validator.Validate(this)); + } + } +} diff --git a/src/NzbDrone.Core/Indexers/HttpIndexerBase.cs b/src/NzbDrone.Core/Indexers/HttpIndexerBase.cs index bc76f3f83..df6877aca 100644 --- a/src/NzbDrone.Core/Indexers/HttpIndexerBase.cs +++ b/src/NzbDrone.Core/Indexers/HttpIndexerBase.cs @@ -17,7 +17,7 @@ using NzbDrone.Core.ThingiProvider; namespace NzbDrone.Core.Indexers { public abstract class HttpIndexerBase : IndexerBase - where TSettings : IProviderConfig, new() + where TSettings : IIndexerSettings, new() { protected const int MaxNumResultsPerQuery = 1000; @@ -46,9 +46,7 @@ namespace NzbDrone.Core.Indexers return new List(); } - var generator = GetRequestGenerator(); - - return FetchReleases(generator.GetRecentRequests(), true); + return FetchReleases(g => g.GetRecentRequests(), true); } public override IList Fetch(AlbumSearchCriteria searchCriteria) @@ -58,9 +56,7 @@ namespace NzbDrone.Core.Indexers return new List(); } - var generator = GetRequestGenerator(); - - return FetchReleases(generator.GetSearchRequests(searchCriteria)); + return FetchReleases(g => g.GetSearchRequests(searchCriteria)); } public override IList Fetch(ArtistSearchCriteria searchCriteria) @@ -70,20 +66,22 @@ namespace NzbDrone.Core.Indexers return new List(); } - var generator = GetRequestGenerator(); - - return FetchReleases(generator.GetSearchRequests(searchCriteria)); + return FetchReleases(g => g.GetSearchRequests(searchCriteria)); } - protected virtual IList FetchReleases(IndexerPageableRequestChain pageableRequestChain, bool isRecent = false) + protected virtual IList FetchReleases(Func pageableRequestChainSelector, bool isRecent = false) { var releases = new List(); var url = string.Empty; - var parser = GetParser(); - try { + var generator = GetRequestGenerator(); + var parser = GetParser(); + + var pageableRequestChain = pageableRequestChainSelector(generator); + + var fullyUpdated = false; ReleaseInfo lastReleaseInfo = null; if (isRecent) @@ -139,7 +137,7 @@ namespace NzbDrone.Core.Indexers } } - releases.AddRange(pagedReleases); + releases.AddRange(pagedReleases.Where(IsValidRelease)); } if (releases.Any()) @@ -186,18 +184,22 @@ namespace NzbDrone.Core.Indexers _logger.Warn("{0} {1} {2}", this, url, webException.Message); } } - catch (HttpException httpException) + catch (TooManyRequestsException ex) { - if ((int)httpException.Response.StatusCode == 429) + if (ex.RetryAfter != TimeSpan.Zero) { - _indexerStatusService.RecordFailure(Definition.Id, TimeSpan.FromHours(1)); - _logger.Warn("API Request Limit reached for {0}", this); + _indexerStatusService.RecordFailure(Definition.Id, ex.RetryAfter); } else { - _indexerStatusService.RecordFailure(Definition.Id); - _logger.Warn("{0} {1}", this, httpException.Message); + _indexerStatusService.RecordFailure(Definition.Id, TimeSpan.FromHours(1)); } + _logger.Warn("API Request Limit reached for {0}", this); + } + catch (HttpException ex) + { + _indexerStatusService.RecordFailure(Definition.Id); + _logger.Warn("{0} {1}", this, ex.Message); } catch (RequestLimitReachedException) { @@ -237,6 +239,16 @@ namespace NzbDrone.Core.Indexers return CleanupReleases(releases); } + protected virtual bool IsValidRelease(ReleaseInfo release) + { + if (release.DownloadUrl.IsNullOrWhiteSpace()) + { + return false; + } + + return true; + } + protected virtual bool IsFullPage(IList page) { return PageSize != 0 && page.Count >= PageSize; diff --git a/src/NzbDrone.Core/Indexers/IIndexerSettings.cs b/src/NzbDrone.Core/Indexers/IIndexerSettings.cs new file mode 100644 index 000000000..46f6bbae8 --- /dev/null +++ b/src/NzbDrone.Core/Indexers/IIndexerSettings.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using NzbDrone.Core.ThingiProvider; + +namespace NzbDrone.Core.Indexers +{ + public interface IIndexerSettings : IProviderConfig + { + string BaseUrl { get; set; } + } +} diff --git a/src/NzbDrone.Core/Indexers/IPTorrents/IPTorrentsRequestGenerator.cs b/src/NzbDrone.Core/Indexers/IPTorrents/IPTorrentsRequestGenerator.cs index a3ce1ae24..b5d95dfb7 100644 --- a/src/NzbDrone.Core/Indexers/IPTorrents/IPTorrentsRequestGenerator.cs +++ b/src/NzbDrone.Core/Indexers/IPTorrents/IPTorrentsRequestGenerator.cs @@ -29,7 +29,7 @@ namespace NzbDrone.Core.Indexers.IPTorrents private IEnumerable GetRssRequests() { - yield return new IndexerRequest(Settings.Url, HttpAccept.Rss); + yield return new IndexerRequest(Settings.BaseUrl, HttpAccept.Rss); } } } diff --git a/src/NzbDrone.Core/Indexers/IPTorrents/IPTorrentsSettings.cs b/src/NzbDrone.Core/Indexers/IPTorrents/IPTorrentsSettings.cs index 4b82353a2..a99b9940b 100644 --- a/src/NzbDrone.Core/Indexers/IPTorrents/IPTorrentsSettings.cs +++ b/src/NzbDrone.Core/Indexers/IPTorrents/IPTorrentsSettings.cs @@ -2,7 +2,6 @@ using System.Text.RegularExpressions; using FluentValidation; using NzbDrone.Common.Extensions; using NzbDrone.Core.Annotations; -using NzbDrone.Core.ThingiProvider; using NzbDrone.Core.Validation; namespace NzbDrone.Core.Indexers.IPTorrents @@ -11,26 +10,30 @@ namespace NzbDrone.Core.Indexers.IPTorrents { public IPTorrentsSettingsValidator() { - RuleFor(c => c.Url).ValidRootUrl(); + RuleFor(c => c.BaseUrl).ValidRootUrl(); - RuleFor(c => c.Url).Matches(@"/rss\?.+$"); + RuleFor(c => c.BaseUrl).Matches(@"/rss\?.+$"); - RuleFor(c => c.Url).Matches(@"/rss\?.+;download(?:;|$)") + RuleFor(c => c.BaseUrl).Matches(@"/rss\?.+;download(?:;|$)") .WithMessage("Use Direct Download Url (;download)") - .When(v => v.Url.IsNotNullOrWhiteSpace() && Regex.IsMatch(v.Url, @"/rss\?.+$")); + .When(v => v.BaseUrl.IsNotNullOrWhiteSpace() && Regex.IsMatch(v.BaseUrl, @"/rss\?.+$")); } } - public class IPTorrentsSettings : IProviderConfig + public class IPTorrentsSettings : ITorrentIndexerSettings { private static readonly IPTorrentsSettingsValidator Validator = new IPTorrentsSettingsValidator(); public IPTorrentsSettings() { + MinimumSeeders = IndexerDefaults.MINIMUM_SEEDERS; } [FieldDefinition(0, Label = "Feed URL", HelpText = "The full RSS feed url generated by IPTorrents, using only the categories you selected (HD, SD, x264, etc ...)")] - public string Url { get; set; } + public string BaseUrl { get; set; } + + [FieldDefinition(1, Type = FieldType.Textbox, Label = "Minimum Seeders", HelpText = "Minimum number of seeders required.", Advanced = true)] + public int MinimumSeeders { get; set; } public NzbDroneValidationResult Validate() { diff --git a/src/NzbDrone.Core/Indexers/ITorrentIndexerSettings.cs b/src/NzbDrone.Core/Indexers/ITorrentIndexerSettings.cs new file mode 100644 index 000000000..150f63424 --- /dev/null +++ b/src/NzbDrone.Core/Indexers/ITorrentIndexerSettings.cs @@ -0,0 +1,7 @@ +namespace NzbDrone.Core.Indexers +{ + public interface ITorrentIndexerSettings : IIndexerSettings + { + int MinimumSeeders { get; set; } + } +} diff --git a/src/NzbDrone.Core/Indexers/IndexerBase.cs b/src/NzbDrone.Core/Indexers/IndexerBase.cs index 2a0515ad2..6894b4af3 100644 --- a/src/NzbDrone.Core/Indexers/IndexerBase.cs +++ b/src/NzbDrone.Core/Indexers/IndexerBase.cs @@ -13,7 +13,7 @@ using NzbDrone.Core.ThingiProvider; namespace NzbDrone.Core.Indexers { public abstract class IndexerBase : IIndexer - where TSettings : IProviderConfig, new() + where TSettings : IIndexerSettings, new() { protected readonly IIndexerStatusService _indexerStatusService; protected readonly IConfigService _configService; @@ -72,6 +72,7 @@ namespace NzbDrone.Core.Indexers result.ForEach(c => { + c.Guid = string.Concat(Definition.Id, "_", c.Guid); c.IndexerId = Definition.Id; c.Indexer = Definition.Name; c.DownloadProtocol = Protocol; @@ -94,11 +95,6 @@ namespace NzbDrone.Core.Indexers failures.Add(new ValidationFailure(string.Empty, "Test was aborted due to an error: " + ex.Message)); } - if (Definition.Id != 0) - { - _indexerStatusService.RecordSuccess(Definition.Id); - } - return new ValidationResult(failures); } diff --git a/src/NzbDrone.Core/Indexers/IndexerDefaults.cs b/src/NzbDrone.Core/Indexers/IndexerDefaults.cs new file mode 100644 index 000000000..ba33fedfd --- /dev/null +++ b/src/NzbDrone.Core/Indexers/IndexerDefaults.cs @@ -0,0 +1,7 @@ +namespace NzbDrone.Core.Indexers +{ + public static class IndexerDefaults + { + public const int MINIMUM_SEEDERS = 1; + } +} diff --git a/src/NzbDrone.Core/Indexers/IndexerFactory.cs b/src/NzbDrone.Core/Indexers/IndexerFactory.cs index c4903c9c7..ba1c9a347 100644 --- a/src/NzbDrone.Core/Indexers/IndexerFactory.cs +++ b/src/NzbDrone.Core/Indexers/IndexerFactory.cs @@ -1,5 +1,6 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; +using FluentValidation.Results; using NLog; using NzbDrone.Common.Composition; using NzbDrone.Core.Messaging.Events; @@ -21,7 +22,7 @@ namespace NzbDrone.Core.Indexers public IndexerFactory(IIndexerStatusService indexerStatusService, IIndexerRepository providerRepository, IEnumerable providers, - IContainer container, + IContainer container, IEventAggregator eventAggregator, Logger logger) : base(providerRepository, providers, container, eventAggregator, logger) @@ -70,7 +71,7 @@ namespace NzbDrone.Core.Indexers private IEnumerable FilterBlockedIndexers(IEnumerable indexers) { - var blockedIndexers = _indexerStatusService.GetBlockedIndexers().ToDictionary(v => v.IndexerId, v => v); + var blockedIndexers = _indexerStatusService.GetBlockedProviders().ToDictionary(v => v.ProviderId, v => v); foreach (var indexer in indexers) { @@ -84,5 +85,17 @@ namespace NzbDrone.Core.Indexers yield return indexer; } } + + public override ValidationResult Test(IndexerDefinition definition) + { + var result = base.Test(definition); + + if ((result == null || result.IsValid) && definition.Id != 0) + { + _indexerStatusService.RecordSuccess(definition.Id); + } + + return result; + } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Core/Indexers/IndexerStatus.cs b/src/NzbDrone.Core/Indexers/IndexerStatus.cs index 662c9de64..72546da7c 100644 --- a/src/NzbDrone.Core/Indexers/IndexerStatus.cs +++ b/src/NzbDrone.Core/Indexers/IndexerStatus.cs @@ -1,23 +1,10 @@ -using System; -using NzbDrone.Core.Datastore; using NzbDrone.Core.Parser.Model; +using NzbDrone.Core.ThingiProvider.Status; namespace NzbDrone.Core.Indexers { - public class IndexerStatus : ModelBase + public class IndexerStatus : ProviderStatusBase { - public int IndexerId { get; set; } - - public DateTime? InitialFailure { get; set; } - public DateTime? MostRecentFailure { get; set; } - public int EscalationLevel { get; set; } - public DateTime? DisabledTill { get; set; } - public ReleaseInfo LastRssSyncReleaseInfo { get; set; } - - public bool IsDisabled() - { - return DisabledTill.HasValue && DisabledTill.Value > DateTime.UtcNow; - } } } diff --git a/src/NzbDrone.Core/Indexers/IndexerStatusRepository.cs b/src/NzbDrone.Core/Indexers/IndexerStatusRepository.cs index 8a70b790a..02a656125 100644 --- a/src/NzbDrone.Core/Indexers/IndexerStatusRepository.cs +++ b/src/NzbDrone.Core/Indexers/IndexerStatusRepository.cs @@ -1,26 +1,20 @@ -using System.Linq; using NzbDrone.Core.Datastore; using NzbDrone.Core.Messaging.Events; -using NzbDrone.Core.ThingiProvider; +using NzbDrone.Core.ThingiProvider.Status; namespace NzbDrone.Core.Indexers { - public interface IIndexerStatusRepository : IProviderRepository + public interface IIndexerStatusRepository : IProviderStatusRepository { - IndexerStatus FindByIndexerId(int indexerId); - } + } + + public class IndexerStatusRepository : ProviderStatusRepository, IIndexerStatusRepository - public class IndexerStatusRepository : ProviderRepository, IIndexerStatusRepository { public IndexerStatusRepository(IMainDatabase database, IEventAggregator eventAggregator) : base(database, eventAggregator) { } - - public IndexerStatus FindByIndexerId(int indexerId) - { - return Query.Where(c => c.IndexerId == indexerId).SingleOrDefault(); - } } } diff --git a/src/NzbDrone.Core/Indexers/IndexerStatusService.cs b/src/NzbDrone.Core/Indexers/IndexerStatusService.cs index 8e1bd1fe5..a9c1275bd 100644 --- a/src/NzbDrone.Core/Indexers/IndexerStatusService.cs +++ b/src/NzbDrone.Core/Indexers/IndexerStatusService.cs @@ -1,131 +1,27 @@ -using System; -using System.Collections.Generic; -using System.Linq; using NLog; using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Parser.Model; -using NzbDrone.Core.ThingiProvider.Events; +using NzbDrone.Core.ThingiProvider.Status; namespace NzbDrone.Core.Indexers { - public interface IIndexerStatusService + public interface IIndexerStatusService : IProviderStatusServiceBase { - List GetBlockedIndexers(); ReleaseInfo GetLastRssSyncReleaseInfo(int indexerId); - void RecordSuccess(int indexerId); - void RecordFailure(int indexerId, TimeSpan minimumBackOff = default(TimeSpan)); - void RecordConnectionFailure(int indexerId); void UpdateRssSyncStatus(int indexerId, ReleaseInfo releaseInfo); } - public class IndexerStatusService : IIndexerStatusService, IHandleAsync> + public class IndexerStatusService : ProviderStatusServiceBase, IIndexerStatusService { - private static readonly int[] EscalationBackOffPeriods = { - 0, - 5 * 60, - 15 * 60, - 30 * 60, - 60 * 60, - 3 * 60 * 60, - 6 * 60 * 60, - 12 * 60 * 60, - 24 * 60 * 60 - }; - private static readonly int MaximumEscalationLevel = EscalationBackOffPeriods.Length - 1; - - private static readonly object _syncRoot = new object(); - - private readonly IIndexerStatusRepository _indexerStatusRepository; - private readonly Logger _logger; - - public IndexerStatusService(IIndexerStatusRepository indexerStatusRepository, Logger logger) - { - _indexerStatusRepository = indexerStatusRepository; - _logger = logger; - } - - public List GetBlockedIndexers() + public IndexerStatusService(IIndexerStatusRepository providerStatusRepository, IEventAggregator eventAggregator, Logger logger) + : base(providerStatusRepository, eventAggregator, logger) { - return _indexerStatusRepository.All().Where(v => v.IsDisabled()).ToList(); } public ReleaseInfo GetLastRssSyncReleaseInfo(int indexerId) { - return GetIndexerStatus(indexerId).LastRssSyncReleaseInfo; - } - - private IndexerStatus GetIndexerStatus(int indexerId) - { - return _indexerStatusRepository.FindByIndexerId(indexerId) ?? new IndexerStatus { IndexerId = indexerId }; - } - - private TimeSpan CalculateBackOffPeriod(IndexerStatus status) - { - var level = Math.Min(MaximumEscalationLevel, status.EscalationLevel); - - return TimeSpan.FromSeconds(EscalationBackOffPeriods[level]); - } - - public void RecordSuccess(int indexerId) - { - lock (_syncRoot) - { - var status = GetIndexerStatus(indexerId); - - if (status.EscalationLevel == 0) - { - return; - } - - status.EscalationLevel--; - status.DisabledTill = null; - - _indexerStatusRepository.Upsert(status); - } - } - - protected void RecordFailure(int indexerId, TimeSpan minimumBackOff, bool escalate) - { - lock (_syncRoot) - { - var status = GetIndexerStatus(indexerId); - - var now = DateTime.UtcNow; - - if (status.EscalationLevel == 0) - { - status.InitialFailure = now; - } - - status.MostRecentFailure = now; - if (escalate) - { - status.EscalationLevel = Math.Min(MaximumEscalationLevel, status.EscalationLevel + 1); - } - - if (minimumBackOff != TimeSpan.Zero) - { - while (status.EscalationLevel < MaximumEscalationLevel && CalculateBackOffPeriod(status) < minimumBackOff) - { - status.EscalationLevel++; - } - } - - status.DisabledTill = now + CalculateBackOffPeriod(status); - - _indexerStatusRepository.Upsert(status); - } - } - - public void RecordFailure(int indexerId, TimeSpan minimumBackOff = default(TimeSpan)) - { - RecordFailure(indexerId, minimumBackOff, true); - } - - public void RecordConnectionFailure(int indexerId) - { - RecordFailure(indexerId, default(TimeSpan), false); + return GetProviderStatus(indexerId).LastRssSyncReleaseInfo; } @@ -133,21 +29,11 @@ namespace NzbDrone.Core.Indexers { lock (_syncRoot) { - var status = GetIndexerStatus(indexerId); + var status = GetProviderStatus(indexerId); status.LastRssSyncReleaseInfo = releaseInfo; - _indexerStatusRepository.Upsert(status); - } - } - - public void HandleAsync(ProviderDeletedEvent message) - { - var indexerStatus = _indexerStatusRepository.FindByIndexerId(message.ProviderId); - - if (indexerStatus != null) - { - _indexerStatusRepository.Delete(indexerStatus); + _providerStatusRepository.Upsert(status); } } } diff --git a/src/NzbDrone.Core/Indexers/Newznab/Newznab.cs b/src/NzbDrone.Core/Indexers/Newznab/Newznab.cs index a985891a6..bd16cc4d5 100644 --- a/src/NzbDrone.Core/Indexers/Newznab/Newznab.cs +++ b/src/NzbDrone.Core/Indexers/Newznab/Newznab.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using FluentValidation.Results; @@ -78,7 +78,7 @@ namespace NzbDrone.Core.Indexers.Newznab private NewznabSettings GetSettings(string url, params int[] categories) { - var settings = new NewznabSettings { Url = url }; + var settings = new NewznabSettings { BaseUrl = url }; if (categories.Any()) { @@ -92,6 +92,7 @@ namespace NzbDrone.Core.Indexers.Newznab { base.Test(failures); + if (failures.Any()) return; failures.AddIfNotNull(TestCapabilities()); } diff --git a/src/NzbDrone.Core/Indexers/Newznab/NewznabCapabilitiesProvider.cs b/src/NzbDrone.Core/Indexers/Newznab/NewznabCapabilitiesProvider.cs index e9397e4c7..a9c8fd9c0 100644 --- a/src/NzbDrone.Core/Indexers/Newznab/NewznabCapabilitiesProvider.cs +++ b/src/NzbDrone.Core/Indexers/Newznab/NewznabCapabilitiesProvider.cs @@ -41,7 +41,7 @@ namespace NzbDrone.Core.Indexers.Newznab { var capabilities = new NewznabCapabilities(); - var url = string.Format("{0}/api?t=caps", indexerSettings.Url.TrimEnd('/')); + var url = string.Format("{0}{1}?t=caps", indexerSettings.BaseUrl.TrimEnd('/'), indexerSettings.ApiPath.TrimEnd('/')); if (indexerSettings.ApiKey.IsNotNullOrWhiteSpace()) { @@ -58,7 +58,7 @@ namespace NzbDrone.Core.Indexers.Newznab } catch (Exception ex) { - _logger.Debug(ex, "Failed to get newznab api capabilities from {0}", indexerSettings.Url); + _logger.Debug(ex, "Failed to get newznab api capabilities from {0}", indexerSettings.BaseUrl); throw; } @@ -68,13 +68,13 @@ namespace NzbDrone.Core.Indexers.Newznab } catch (XmlException ex) { - _logger.Debug(ex, "Failed to parse newznab api capabilities for {0}.", indexerSettings.Url); + _logger.Debug(ex, "Failed to parse newznab api capabilities for {0}", indexerSettings.BaseUrl); ex.WithData(response); throw; } catch (Exception ex) { - _logger.Error(ex, "Failed to determine newznab api capabilities for {0}, using the defaults instead till Lidarr restarts.", indexerSettings.Url); + _logger.Error(ex, "Failed to determine newznab api capabilities for {0}, using the defaults instead till Lidarr restarts", indexerSettings.BaseUrl); } return capabilities; @@ -84,7 +84,19 @@ namespace NzbDrone.Core.Indexers.Newznab { var capabilities = new NewznabCapabilities(); - var xmlRoot = XDocument.Parse(response.Content).Element("caps"); + var xDoc = XDocument.Parse(response.Content); + + if (xDoc == null) + { + throw new XmlException("Invalid XML"); + } + + var xmlRoot = xDoc.Element("caps"); + + if (xmlRoot == null) + { + throw new XmlException("Unexpected XML"); + } var xmlLimits = xmlRoot.Element("limits"); if (xmlLimits != null) diff --git a/src/NzbDrone.Core/Indexers/Newznab/NewznabRequestGenerator.cs b/src/NzbDrone.Core/Indexers/Newznab/NewznabRequestGenerator.cs index e08e527e9..ad3676fc5 100644 --- a/src/NzbDrone.Core/Indexers/Newznab/NewznabRequestGenerator.cs +++ b/src/NzbDrone.Core/Indexers/Newznab/NewznabRequestGenerator.cs @@ -55,6 +55,10 @@ namespace NzbDrone.Core.Indexers.Newznab { pageableRequests.Add(GetPagedRequests(MaxPages, Settings.Categories, "music", "")); } + else if (capabilities.SupportedSearchParameters != null) + { + pageableRequests.Add(GetPagedRequests(MaxPages, Settings.Categories, "search", "")); + } return pageableRequests; } @@ -63,10 +67,25 @@ namespace NzbDrone.Core.Indexers.Newznab { var pageableRequests = new IndexerPageableRequestChain(); - AddAudioPageableRequests(pageableRequests, - string.Format("&artist={0}&album={1}", - searchCriteria.Artist.Name, - searchCriteria.AlbumTitle)); + if (SupportsAudioSearch) + { + AddAudioPageableRequests(pageableRequests, searchCriteria, + string.Format("&artist={0}&album={1}", + NewsnabifyTitle(searchCriteria.Artist.Name), + NewsnabifyTitle(searchCriteria.AlbumTitle))); + } + + if (SupportsSearch) + { + pageableRequests.AddTier(); + + pageableRequests.Add(GetPagedRequests(MaxPages, Settings.Categories, "search", + string.Format("&q={0}", + NewsnabifyTitle(string.Format("{0} - {1}", + searchCriteria.Artist.Name, + searchCriteria.AlbumTitle))))); + + } return pageableRequests; } @@ -75,25 +94,35 @@ namespace NzbDrone.Core.Indexers.Newznab { var pageableRequests = new IndexerPageableRequestChain(); - AddAudioPageableRequests(pageableRequests, - string.Format("&artist={0}", - searchCriteria.Artist.Name)); - - return pageableRequests; - } - private void AddAudioPageableRequests(IndexerPageableRequestChain chain, string parameters) - { if (SupportsAudioSearch) { - chain.AddTier(); - - chain.Add(GetPagedRequests(MaxPages, Settings.Categories, "music", + AddAudioPageableRequests(pageableRequests, searchCriteria, + string.Format("&artist={0}", + NewsnabifyTitle(searchCriteria.Artist.Name))); + } + + if (SupportsSearch) + { + pageableRequests.AddTier(); + + pageableRequests.Add(GetPagedRequests(MaxPages, Settings.Categories, "search", string.Format("&q={0}", - parameters))); - + NewsnabifyTitle(searchCriteria.Artist.Name)))); + } + + return pageableRequests; + } + + private void AddAudioPageableRequests(IndexerPageableRequestChain chain, SearchCriteriaBase searchCriteria, string parameters) + { + chain.AddTier(); + + chain.Add(GetPagedRequests(MaxPages, Settings.Categories, "music", + string.Format("&q={0}", + parameters))); } private IEnumerable GetPagedRequests(int maxPages, IEnumerable categories, string searchType, string parameters) @@ -105,7 +134,7 @@ namespace NzbDrone.Core.Indexers.Newznab var categoriesQuery = string.Join(",", categories.Distinct()); - var baseUrl = string.Format("{0}/api?t={1}&cat={2}&extended=1{3}", Settings.Url.TrimEnd('/'), searchType, categoriesQuery, Settings.AdditionalParameters); + var baseUrl = string.Format("{0}{1}?t={2}&cat={3}&extended=1{4}", Settings.BaseUrl.TrimEnd('/'), Settings.ApiPath.TrimEnd('/'), searchType, categoriesQuery, Settings.AdditionalParameters); if (Settings.ApiKey.IsNotNullOrWhiteSpace()) { diff --git a/src/NzbDrone.Core/Indexers/Newznab/NewznabRssParser.cs b/src/NzbDrone.Core/Indexers/Newznab/NewznabRssParser.cs index a0d647fcc..b9f1efa54 100644 --- a/src/NzbDrone.Core/Indexers/Newznab/NewznabRssParser.cs +++ b/src/NzbDrone.Core/Indexers/Newznab/NewznabRssParser.cs @@ -1,4 +1,5 @@ -using System; +using System; +using System.Collections.Generic; using System.Linq; using System.Xml.Linq; using NzbDrone.Common.Extensions; @@ -13,7 +14,8 @@ namespace NzbDrone.Core.Indexers.Newznab public NewznabRssParser() { - PreferredEnclosureMimeType = "application/x-nzb"; + PreferredEnclosureMimeTypes = UsenetEnclosureMimeTypes; + UseEnclosureUrl = true; } protected override bool PreProcess(IndexerResponse indexerResponse) @@ -45,6 +47,24 @@ namespace NzbDrone.Core.Indexers.Newznab throw new NewznabException(indexerResponse, errorMessage); } + protected override bool PostProcess(IndexerResponse indexerResponse, List items, List releases) + { + var enclosureTypes = items.SelectMany(GetEnclosures).Select(v => v.Type).Distinct().ToArray(); + if (enclosureTypes.Any() && enclosureTypes.Intersect(PreferredEnclosureMimeTypes).Empty()) + { + if (enclosureTypes.Intersect(TorrentEnclosureMimeTypes).Any()) + { + _logger.Warn("Feed does not contain {0}, found {1}, did you intend to add a Torznab indexer?", NzbEnclosureMimeType, enclosureTypes[0]); + } + else + { + _logger.Warn("Feed does not contain {0}, found {1}.", NzbEnclosureMimeType, enclosureTypes[0]); + } + } + + return true; + } + protected override ReleaseInfo ProcessItem(XElement item, ReleaseInfo releaseInfo) { releaseInfo = base.ProcessItem(item, releaseInfo); @@ -55,17 +75,6 @@ namespace NzbDrone.Core.Indexers.Newznab return releaseInfo; } - protected override ReleaseInfo PostProcess(XElement item, ReleaseInfo releaseInfo) - { - var enclosureType = GetEnclosure(item).Attribute("type").Value; - if (enclosureType.Contains("application/x-bittorrent")) - { - throw new UnsupportedFeedException("Feed contains {0}, did you intend to add a Torznab indexer?", enclosureType); - } - - return base.PostProcess(item, releaseInfo); - } - protected override string GetInfoUrl(XElement item) { return ParseUrl(item.TryGetValue("comments").TrimEnd("#comments")); @@ -102,18 +111,6 @@ namespace NzbDrone.Core.Indexers.Newznab return base.GetPublishDate(item); } - protected override string GetDownloadUrl(XElement item) - { - var url = base.GetDownloadUrl(item); - - if (!Uri.IsWellFormedUriString(url, UriKind.Absolute)) - { - url = ParseUrl((string)item.Element("enclosure").Attribute("url")); - } - - return url; - } - protected virtual string GetArtist(XElement item) { var artistString = TryGetNewznabAttribute(item, "artist"); @@ -140,11 +137,14 @@ namespace NzbDrone.Core.Indexers.Newznab protected string TryGetNewznabAttribute(XElement item, string key, string defaultValue = "") { - var attr = item.Elements(ns + "attr").FirstOrDefault(e => e.Attribute("name").Value.Equals(key, StringComparison.CurrentCultureIgnoreCase)); - - if (attr != null) + var attrElement = item.Elements(ns + "attr").FirstOrDefault(e => e.Attribute("name").Value.Equals(key, StringComparison.OrdinalIgnoreCase)); + if (attrElement != null) { - return attr.Attribute("value").Value; + var attrValue = attrElement.Attribute("value"); + if (attrValue != null) + { + return attrValue.Value; + } } return defaultValue; diff --git a/src/NzbDrone.Core/Indexers/Newznab/NewznabSettings.cs b/src/NzbDrone.Core/Indexers/Newznab/NewznabSettings.cs index 466ebb6e4..0412d0c64 100644 --- a/src/NzbDrone.Core/Indexers/Newznab/NewznabSettings.cs +++ b/src/NzbDrone.Core/Indexers/Newznab/NewznabSettings.cs @@ -5,7 +5,6 @@ using FluentValidation; using FluentValidation.Results; using NzbDrone.Common.Extensions; using NzbDrone.Core.Annotations; -using NzbDrone.Core.ThingiProvider; using NzbDrone.Core.Validation; namespace NzbDrone.Core.Indexers.Newznab @@ -25,12 +24,12 @@ namespace NzbDrone.Core.Indexers.Newznab private static bool ShouldHaveApiKey(NewznabSettings settings) { - if (settings.Url == null) + if (settings.BaseUrl == null) { return false; } - return ApiKeyWhiteList.Any(c => settings.Url.ToLowerInvariant().Contains(c)); + return ApiKeyWhiteList.Any(c => settings.BaseUrl.ToLowerInvariant().Contains(c)); } private static readonly Regex AdditionalParametersRegex = new Regex(@"(&.+?\=.+?)+", RegexOptions.Compiled); @@ -39,49 +38,53 @@ namespace NzbDrone.Core.Indexers.Newznab { Custom(newznab => { - if (newznab.Categories.Empty() && newznab.AnimeCategories.Empty()) + if (newznab.Categories.Empty()) { - return new ValidationFailure("", "Either 'Categories' or 'Anime Categories' must be provided"); + return new ValidationFailure("", "'Categories' must be provided"); } return null; }); - RuleFor(c => c.Url).ValidRootUrl(); + RuleFor(c => c.BaseUrl).ValidRootUrl(); + RuleFor(c => c.ApiPath).ValidUrlBase("/api"); RuleFor(c => c.ApiKey).NotEmpty().When(ShouldHaveApiKey); RuleFor(c => c.AdditionalParameters).Matches(AdditionalParametersRegex) .When(c => !c.AdditionalParameters.IsNullOrWhiteSpace()); } } - public class NewznabSettings : IProviderConfig + public class NewznabSettings : IIndexerSettings { private static readonly NewznabSettingsValidator Validator = new NewznabSettingsValidator(); public NewznabSettings() { + ApiPath = "/api"; Categories = new[] { 3000, 3010, 3020, 3030, 3040 }; - AnimeCategories = Enumerable.Empty(); } [FieldDefinition(0, Label = "URL")] - public string Url { get; set; } + public string BaseUrl { get; set; } - [FieldDefinition(1, Label = "API Key")] + [FieldDefinition(1, Label = "API Path", HelpText = "Path to the api, usually /api", Advanced = true)] + public string ApiPath { get; set; } + + [FieldDefinition(2, Label = "API Key")] public string ApiKey { get; set; } - [FieldDefinition(2, Label = "Categories", HelpText = "Comma Separated list, leave blank to disable standard/daily shows", Advanced = true)] + [FieldDefinition(3, Label = "Categories", HelpText = "Comma Separated list, leave blank to disable standard/daily shows", Advanced = true)] public IEnumerable Categories { get; set; } - [FieldDefinition(3, Label = "Anime Categories", HelpText = "Comma Separated list, leave blank to disable anime", Advanced = true)] - public IEnumerable AnimeCategories { get; set; } - [FieldDefinition(4, Label = "Additional Parameters", HelpText = "Additional Newznab parameters", Advanced = true)] public string AdditionalParameters { get; set; } + // Field 5 is used by TorznabSettings MinimumSeeders + // If you need to add another field here, update TorznabSettings as well and this comment + public virtual NzbDroneValidationResult Validate() { return new NzbDroneValidationResult(Validator.Validate(this)); } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Core/Indexers/Nyaa/NyaaSettings.cs b/src/NzbDrone.Core/Indexers/Nyaa/NyaaSettings.cs index 5977c2782..6b997021c 100644 --- a/src/NzbDrone.Core/Indexers/Nyaa/NyaaSettings.cs +++ b/src/NzbDrone.Core/Indexers/Nyaa/NyaaSettings.cs @@ -1,6 +1,5 @@ -using FluentValidation; +using FluentValidation; using NzbDrone.Core.Annotations; -using NzbDrone.Core.ThingiProvider; using NzbDrone.Core.Validation; using System.Text.RegularExpressions; namespace NzbDrone.Core.Indexers.Nyaa @@ -14,14 +13,15 @@ namespace NzbDrone.Core.Indexers.Nyaa } } - public class NyaaSettings : IProviderConfig + public class NyaaSettings : ITorrentIndexerSettings { private static readonly NyaaSettingsValidator Validator = new NyaaSettingsValidator(); public NyaaSettings() { - BaseUrl = "https://www.nyaa.se"; + BaseUrl = ""; AdditionalParameters = "&cats=1_37&filter=1"; + MinimumSeeders = IndexerDefaults.MINIMUM_SEEDERS; } [FieldDefinition(0, Label = "Website URL")] @@ -30,9 +30,12 @@ namespace NzbDrone.Core.Indexers.Nyaa [FieldDefinition(1, Label = "Additional Parameters", Advanced = true, HelpText = "Please note if you change the category you will have to add required/restricted rules about the subgroups to avoid foreign language releases.")] public string AdditionalParameters { get; set; } + [FieldDefinition(2, Type = FieldType.Textbox, Label = "Minimum Seeders", HelpText = "Minimum number of seeders required.", Advanced = true)] + public int MinimumSeeders { get; set; } + public NzbDroneValidationResult Validate() { return new NzbDroneValidationResult(Validator.Validate(this)); } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Core/Indexers/Omgwtfnzbs/OmgwtfnzbsSettings.cs b/src/NzbDrone.Core/Indexers/Omgwtfnzbs/OmgwtfnzbsSettings.cs index fe6217361..429d0e390 100644 --- a/src/NzbDrone.Core/Indexers/Omgwtfnzbs/OmgwtfnzbsSettings.cs +++ b/src/NzbDrone.Core/Indexers/Omgwtfnzbs/OmgwtfnzbsSettings.cs @@ -1,4 +1,4 @@ -using FluentValidation; +using FluentValidation; using NzbDrone.Core.Annotations; using NzbDrone.Core.ThingiProvider; using NzbDrone.Core.Validation; @@ -15,7 +15,7 @@ namespace NzbDrone.Core.Indexers.Omgwtfnzbs } } - public class OmgwtfnzbsSettings : IProviderConfig + public class OmgwtfnzbsSettings : IIndexerSettings { private static readonly OmgwtfnzbsSettingsValidator Validator = new OmgwtfnzbsSettingsValidator(); @@ -24,6 +24,9 @@ namespace NzbDrone.Core.Indexers.Omgwtfnzbs Delay = 30; } + // Unused since Omg has a hardcoded url. + public string BaseUrl { get; set; } + [FieldDefinition(0, Label = "Username")] public string Username { get; set; } diff --git a/src/NzbDrone.Core/Indexers/Rarbg/RarbgSettings.cs b/src/NzbDrone.Core/Indexers/Rarbg/RarbgSettings.cs index c60616b27..33ca45404 100644 --- a/src/NzbDrone.Core/Indexers/Rarbg/RarbgSettings.cs +++ b/src/NzbDrone.Core/Indexers/Rarbg/RarbgSettings.cs @@ -1,6 +1,5 @@ using FluentValidation; using NzbDrone.Core.Annotations; -using NzbDrone.Core.ThingiProvider; using NzbDrone.Core.Validation; namespace NzbDrone.Core.Indexers.Rarbg @@ -13,7 +12,7 @@ namespace NzbDrone.Core.Indexers.Rarbg } } - public class RarbgSettings : IProviderConfig + public class RarbgSettings : ITorrentIndexerSettings { private static readonly RarbgSettingsValidator Validator = new RarbgSettingsValidator(); @@ -21,6 +20,7 @@ namespace NzbDrone.Core.Indexers.Rarbg { BaseUrl = "https://torrentapi.org"; RankedOnly = false; + MinimumSeeders = IndexerDefaults.MINIMUM_SEEDERS; } [FieldDefinition(0, Label = "API URL", HelpText = "URL to Rarbg api, not the website.")] @@ -32,9 +32,12 @@ namespace NzbDrone.Core.Indexers.Rarbg [FieldDefinition(2, Type = FieldType.Captcha, Label = "CAPTCHA Token", HelpText = "CAPTCHA Clearance token used to handle CloudFlare Anti-DDOS measures on shared-ip VPNs.")] public string CaptchaToken { get; set; } + [FieldDefinition(3, Type = FieldType.Textbox, Label = "Minimum Seeders", HelpText = "Minimum number of seeders required.", Advanced = true)] + public int MinimumSeeders { get; set; } + public NzbDroneValidationResult Validate() { return new NzbDroneValidationResult(Validator.Validate(this)); } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Core/Indexers/RssEnclosure.cs b/src/NzbDrone.Core/Indexers/RssEnclosure.cs new file mode 100644 index 000000000..6c0a59c37 --- /dev/null +++ b/src/NzbDrone.Core/Indexers/RssEnclosure.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace NzbDrone.Core.Indexers +{ + public class RssEnclosure + { + public string Url { get; set; } + public string Type { get; set; } + public long Length { get; set; } + } +} diff --git a/src/NzbDrone.Core/Indexers/RssParser.cs b/src/NzbDrone.Core/Indexers/RssParser.cs index fb7e60d0e..a77563236 100644 --- a/src/NzbDrone.Core/Indexers/RssParser.cs +++ b/src/NzbDrone.Core/Indexers/RssParser.cs @@ -19,6 +19,11 @@ namespace NzbDrone.Core.Indexers public class RssParser : IParseIndexerResponse { private static readonly Regex ReplaceEntities = new Regex("&[a-z]+;", RegexOptions.Compiled | RegexOptions.IgnoreCase); + public const string NzbEnclosureMimeType = "application/x-nzb"; + public const string TorrentEnclosureMimeType = "application/x-bittorrent"; + public const string MagnetEnclosureMimeType = "application/x-bittorrent;x-scheme-handler/magnet"; + public static readonly string[] UsenetEnclosureMimeTypes = new[] { NzbEnclosureMimeType }; + public static readonly string[] TorrentEnclosureMimeTypes = new[] { TorrentEnclosureMimeType, MagnetEnclosureMimeType }; protected readonly Logger _logger; @@ -32,7 +37,7 @@ namespace NzbDrone.Core.Indexers // Parse "Size: 1.3 GB" or "1.3 GB" parts in the description element and use that as Size. public bool ParseSizeInDescription { get; set; } - public string PreferredEnclosureMimeType { get; set; } + public string[] PreferredEnclosureMimeTypes { get; set; } private IndexerResponse _indexerResponse; @@ -53,7 +58,7 @@ namespace NzbDrone.Core.Indexers } var document = LoadXmlDocument(indexerResponse); - var items = GetItems(document); + var items = GetItems(document).ToList(); foreach (var item in items) { @@ -77,6 +82,11 @@ namespace NzbDrone.Core.Indexers } } + if (!PostProcess(indexerResponse, items, releases)) + { + return new List(); + } + return releases; } @@ -124,6 +134,11 @@ namespace NzbDrone.Core.Indexers return true; } + protected virtual bool PostProcess(IndexerResponse indexerResponse, List elements, List releases) + { + return true; + } + protected ReleaseInfo ProcessItem(XElement item) { var releaseInfo = CreateNewReleaseInfo(); @@ -132,7 +147,7 @@ namespace NzbDrone.Core.Indexers _logger.Trace("Parsed: {0}", releaseInfo.Title); - return PostProcess(item, releaseInfo); + return PostProcessItem(item, releaseInfo); } protected virtual ReleaseInfo ProcessItem(XElement item, ReleaseInfo releaseInfo) @@ -156,7 +171,7 @@ namespace NzbDrone.Core.Indexers return releaseInfo; } - protected virtual ReleaseInfo PostProcess(XElement item, ReleaseInfo releaseInfo) + protected virtual ReleaseInfo PostProcessItem(XElement item, ReleaseInfo releaseInfo) { return releaseInfo; } @@ -187,7 +202,8 @@ namespace NzbDrone.Core.Indexers { if (UseEnclosureUrl) { - return ParseUrl((string)GetEnclosure(item).Attribute("url")); + var enclosure = GetEnclosure(item); + return enclosure != null ? ParseUrl(enclosure.Url) : null; } return ParseUrl((string)item.Element("link")); @@ -228,37 +244,60 @@ namespace NzbDrone.Core.Indexers if (enclosure != null) { - return (long)enclosure.Attribute("length"); + return enclosure.Length; } return 0; } - protected virtual XElement GetEnclosure(XElement item) + protected virtual RssEnclosure[] GetEnclosures(XElement item) + { + var enclosures = item.Elements("enclosure") + .Select(v => new RssEnclosure + { + Url = v.Attribute("url").Value, + Type = v.Attribute("type").Value, + Length = (long)v.Attribute("length") + }) + .ToArray(); + + return enclosures; + } + + protected RssEnclosure GetEnclosure(XElement item, bool enforceMimeType = true) + { + var enclosures = GetEnclosures(item); + + return GetEnclosure(enclosures, enforceMimeType); + } + + protected virtual RssEnclosure GetEnclosure(RssEnclosure[] enclosures, bool enforceMimeType = true) { - var enclosures = item.Elements("enclosure").ToArray(); if (enclosures.Length == 0) { return null; } - if (enclosures.Length == 1) + if (PreferredEnclosureMimeTypes != null) { - return enclosures.First(); - } + foreach (var preferredEnclosureType in PreferredEnclosureMimeTypes) + { + var preferredEnclosure = enclosures.FirstOrDefault(v => v.Type == preferredEnclosureType); - if (PreferredEnclosureMimeType != null) - { - var preferredEnclosure = enclosures.FirstOrDefault(v => v.Attribute("type").Value == PreferredEnclosureMimeType); + if (preferredEnclosure != null) + { + return preferredEnclosure; + } + } - if (preferredEnclosure != null) + if (enforceMimeType) { - return preferredEnclosure; + return null; } } - return item.Elements("enclosure").SingleOrDefault(); + return enclosures.SingleOrDefault(); } protected IEnumerable GetItems(XDocument document) @@ -305,6 +344,11 @@ namespace NzbDrone.Core.Indexers public static long ParseSize(string sizeString, bool defaultToBinaryPrefix) { + if (sizeString.IsNullOrWhiteSpace()) + { + return 0; + } + if (sizeString.All(char.IsDigit)) { return long.Parse(sizeString); diff --git a/src/NzbDrone.Core/Indexers/TorrentRss/TorrentRssIndexerSettings.cs b/src/NzbDrone.Core/Indexers/TorrentRss/TorrentRssIndexerSettings.cs index ef2b74f9a..7edcc1680 100644 --- a/src/NzbDrone.Core/Indexers/TorrentRss/TorrentRssIndexerSettings.cs +++ b/src/NzbDrone.Core/Indexers/TorrentRss/TorrentRssIndexerSettings.cs @@ -1,6 +1,5 @@ using FluentValidation; using NzbDrone.Core.Annotations; -using NzbDrone.Core.ThingiProvider; using NzbDrone.Core.Validation; namespace NzbDrone.Core.Indexers.TorrentRss @@ -13,7 +12,7 @@ namespace NzbDrone.Core.Indexers.TorrentRss } } - public class TorrentRssIndexerSettings : IProviderConfig + public class TorrentRssIndexerSettings : ITorrentIndexerSettings { private static readonly TorrentRssIndexerSettingsValidator validator = new TorrentRssIndexerSettingsValidator(); @@ -21,6 +20,7 @@ namespace NzbDrone.Core.Indexers.TorrentRss { BaseUrl = string.Empty; AllowZeroSize = false; + MinimumSeeders = IndexerDefaults.MINIMUM_SEEDERS; } [FieldDefinition(0, Label = "Full RSS Feed URL")] @@ -32,9 +32,12 @@ namespace NzbDrone.Core.Indexers.TorrentRss [FieldDefinition(2, Type = FieldType.Checkbox, Label = "Allow Zero Size", HelpText="Enabling this will allow you to use feeds that don't specify release size, but be careful, size related checks will not be performed.")] public bool AllowZeroSize { get; set; } + [FieldDefinition(3, Type = FieldType.Textbox, Label = "Minimum Seeders", HelpText = "Minimum number of seeders required.", Advanced = true)] + public int MinimumSeeders { get; set; } + public NzbDroneValidationResult Validate() { return new NzbDroneValidationResult(validator.Validate(this)); } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Core/Indexers/TorrentRssParser.cs b/src/NzbDrone.Core/Indexers/TorrentRssParser.cs index b77022540..df55ae6a8 100644 --- a/src/NzbDrone.Core/Indexers/TorrentRssParser.cs +++ b/src/NzbDrone.Core/Indexers/TorrentRssParser.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Text.RegularExpressions; using System.Xml.Linq; using NzbDrone.Common.Extensions; @@ -16,7 +16,7 @@ namespace NzbDrone.Core.Indexers public TorrentRssParser() { - PreferredEnclosureMimeType = "application/x-bittorrent"; + PreferredEnclosureMimeTypes = TorrentEnclosureMimeTypes; } public IEnumerable GetItems(IndexerResponse indexerResponse) diff --git a/src/NzbDrone.Core/Indexers/Torrentleech/TorrentleechSettings.cs b/src/NzbDrone.Core/Indexers/Torrentleech/TorrentleechSettings.cs index 957bfc3ed..a76690478 100644 --- a/src/NzbDrone.Core/Indexers/Torrentleech/TorrentleechSettings.cs +++ b/src/NzbDrone.Core/Indexers/Torrentleech/TorrentleechSettings.cs @@ -1,6 +1,5 @@ using FluentValidation; using NzbDrone.Core.Annotations; -using NzbDrone.Core.ThingiProvider; using NzbDrone.Core.Validation; namespace NzbDrone.Core.Indexers.Torrentleech @@ -14,13 +13,14 @@ namespace NzbDrone.Core.Indexers.Torrentleech } } - public class TorrentleechSettings : IProviderConfig + public class TorrentleechSettings : ITorrentIndexerSettings { private static readonly TorrentleechSettingsValidator Validator = new TorrentleechSettingsValidator(); public TorrentleechSettings() { BaseUrl = "http://rss.torrentleech.org"; + MinimumSeeders = IndexerDefaults.MINIMUM_SEEDERS; } [FieldDefinition(0, Label = "Website URL")] @@ -29,9 +29,12 @@ namespace NzbDrone.Core.Indexers.Torrentleech [FieldDefinition(1, Label = "API Key")] public string ApiKey { get; set; } + [FieldDefinition(2, Type = FieldType.Textbox, Label = "Minimum Seeders", HelpText = "Minimum number of seeders required.", Advanced = true)] + public int MinimumSeeders { get; set; } + public NzbDroneValidationResult Validate() { return new NzbDroneValidationResult(Validator.Validate(this)); } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Core/Indexers/Torznab/Torznab.cs b/src/NzbDrone.Core/Indexers/Torznab/Torznab.cs index 8d2649c2d..6dc9591d1 100644 --- a/src/NzbDrone.Core/Indexers/Torznab/Torznab.cs +++ b/src/NzbDrone.Core/Indexers/Torznab/Torznab.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using FluentValidation.Results; @@ -66,7 +66,7 @@ namespace NzbDrone.Core.Indexers.Torznab private TorznabSettings GetSettings(string url, params int[] categories) { - var settings = new TorznabSettings { Url = url }; + var settings = new TorznabSettings { BaseUrl = url }; if (categories.Any()) { @@ -80,6 +80,7 @@ namespace NzbDrone.Core.Indexers.Torznab { base.Test(failures); + if (failures.Any()) return; failures.AddIfNotNull(TestCapabilities()); } @@ -94,9 +95,8 @@ namespace NzbDrone.Core.Indexers.Torznab return null; } - if (capabilities.SupportedTvSearchParameters != null && - new[] { "q", "tvdbid", "rid" }.Any(v => capabilities.SupportedTvSearchParameters.Contains(v)) && - new[] { "season", "ep" }.All(v => capabilities.SupportedTvSearchParameters.Contains(v))) + if (capabilities.SupportedAudioSearchParameters != null && + new[] { "artist", "album" }.All(v => capabilities.SupportedAudioSearchParameters.Contains(v))) { return null; } diff --git a/src/NzbDrone.Core/Indexers/Torznab/TorznabRssParser.cs b/src/NzbDrone.Core/Indexers/Torznab/TorznabRssParser.cs index 038785214..46bd46533 100644 --- a/src/NzbDrone.Core/Indexers/Torznab/TorznabRssParser.cs +++ b/src/NzbDrone.Core/Indexers/Torznab/TorznabRssParser.cs @@ -1,4 +1,5 @@ -using System; +using System; +using System.Collections.Generic; using System.Linq; using System.Xml.Linq; using NzbDrone.Common.Extensions; @@ -11,6 +12,11 @@ namespace NzbDrone.Core.Indexers.Torznab { public const string ns = "{http://torznab.com/schemas/2015/feed}"; + public TorznabRssParser() + { + UseEnclosureUrl = true; + } + protected override bool PreProcess(IndexerResponse indexerResponse) { var xdoc = LoadXmlDocument(indexerResponse); @@ -36,15 +42,22 @@ namespace NzbDrone.Core.Indexers.Torznab throw new TorznabException("Torznab error detected: {0}", errorMessage); } - protected override ReleaseInfo PostProcess(XElement item, ReleaseInfo releaseInfo) + protected override bool PostProcess(IndexerResponse indexerResponse, List items, List releases) { - var enclosureType = item.Element("enclosure").Attribute("type").Value; - if (!enclosureType.Contains("application/x-bittorrent")) + var enclosureTypes = items.SelectMany(GetEnclosures).Select(v => v.Type).Distinct().ToArray(); + if (enclosureTypes.Any() && enclosureTypes.Intersect(PreferredEnclosureMimeTypes).Empty()) { - throw new UnsupportedFeedException("Feed contains {0} instead of application/x-bittorrent", enclosureType); + if (enclosureTypes.Intersect(UsenetEnclosureMimeTypes).Any()) + { + _logger.Warn("Feed does not contain {0}, found {1}, did you intend to add a Newznab indexer?", TorrentEnclosureMimeType, enclosureTypes[0]); + } + else + { + _logger.Warn("Feed does not contain {0}, found {1}.", TorrentEnclosureMimeType, enclosureTypes[0]); + } } - return base.PostProcess(item, releaseInfo); + return true; } @@ -134,11 +147,14 @@ namespace NzbDrone.Core.Indexers.Torznab protected string TryGetTorznabAttribute(XElement item, string key, string defaultValue = "") { - var attr = item.Elements(ns + "attr").FirstOrDefault(e => e.Attribute("name").Value.Equals(key, StringComparison.CurrentCultureIgnoreCase)); - - if (attr != null) + var attrElement = item.Elements(ns + "attr").FirstOrDefault(e => e.Attribute("name").Value.Equals(key, StringComparison.OrdinalIgnoreCase)); + if (attrElement != null) { - return attr.Attribute("value").Value; + var attrValue = attrElement.Attribute("value"); + if (attrValue != null) + { + return attrValue.Value; + } } return defaultValue; diff --git a/src/NzbDrone.Core/Indexers/Torznab/TorznabSettings.cs b/src/NzbDrone.Core/Indexers/Torznab/TorznabSettings.cs index 86d7be1a1..0c2cda826 100644 --- a/src/NzbDrone.Core/Indexers/Torznab/TorznabSettings.cs +++ b/src/NzbDrone.Core/Indexers/Torznab/TorznabSettings.cs @@ -3,6 +3,7 @@ using System.Text.RegularExpressions; using FluentValidation; using FluentValidation.Results; using NzbDrone.Common.Extensions; +using NzbDrone.Core.Annotations; using NzbDrone.Core.Indexers.Newznab; using NzbDrone.Core.Validation; @@ -17,12 +18,12 @@ namespace NzbDrone.Core.Indexers.Torznab private static bool ShouldHaveApiKey(TorznabSettings settings) { - if (settings.Url == null) + if (settings.BaseUrl == null) { return false; } - return ApiKeyWhiteList.Any(c => settings.Url.ToLowerInvariant().Contains(c)); + return ApiKeyWhiteList.Any(c => settings.BaseUrl.ToLowerInvariant().Contains(c)); } private static readonly Regex AdditionalParametersRegex = new Regex(@"(&.+?\=.+?)+", RegexOptions.Compiled); @@ -31,28 +32,37 @@ namespace NzbDrone.Core.Indexers.Torznab { Custom(newznab => { - if (newznab.Categories.Empty() && newznab.AnimeCategories.Empty()) + if (newznab.Categories.Empty()) { - return new ValidationFailure("", "Either 'Categories' or 'Anime Categories' must be provided"); + return new ValidationFailure("", "'Categories' must be provided"); } return null; }); - RuleFor(c => c.Url).ValidRootUrl(); + RuleFor(c => c.BaseUrl).ValidRootUrl(); + RuleFor(c => c.ApiPath).ValidUrlBase("/api"); RuleFor(c => c.ApiKey).NotEmpty().When(ShouldHaveApiKey); RuleFor(c => c.AdditionalParameters).Matches(AdditionalParametersRegex) .When(c => !c.AdditionalParameters.IsNullOrWhiteSpace()); } } - public class TorznabSettings : NewznabSettings + public class TorznabSettings : NewznabSettings, ITorrentIndexerSettings { private static readonly TorznabSettingsValidator Validator = new TorznabSettingsValidator(); + public TorznabSettings() + { + MinimumSeeders = IndexerDefaults.MINIMUM_SEEDERS; + } + + [FieldDefinition(5, Type = FieldType.Textbox, Label = "Minimum Seeders", HelpText = "Minimum number of seeders required.", Advanced = true)] + public int MinimumSeeders { get; set; } + public override NzbDroneValidationResult Validate() { return new NzbDroneValidationResult(Validator.Validate(this)); } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Core/Indexers/Waffles/WafflesSettings.cs b/src/NzbDrone.Core/Indexers/Waffles/WafflesSettings.cs index df5f5aa42..70ef5eba4 100644 --- a/src/NzbDrone.Core/Indexers/Waffles/WafflesSettings.cs +++ b/src/NzbDrone.Core/Indexers/Waffles/WafflesSettings.cs @@ -1,7 +1,6 @@ -using System.Text.RegularExpressions; +using System.Text.RegularExpressions; using FluentValidation; using NzbDrone.Core.Annotations; -using NzbDrone.Core.ThingiProvider; using NzbDrone.Core.Validation; namespace NzbDrone.Core.Indexers.Waffles @@ -16,13 +15,14 @@ namespace NzbDrone.Core.Indexers.Waffles } } - public class WafflesSettings : IProviderConfig + public class WafflesSettings : ITorrentIndexerSettings { private static readonly WafflesSettingsValidator Validator = new WafflesSettingsValidator(); public WafflesSettings() { BaseUrl = "https://www.waffles.ch"; + MinimumSeeders = IndexerDefaults.MINIMUM_SEEDERS; } [FieldDefinition(0, Label = "Website URL")] @@ -34,10 +34,13 @@ namespace NzbDrone.Core.Indexers.Waffles [FieldDefinition(2, Label = "RSS Passkey")] public string RssPasskey { get; set; } + [FieldDefinition(3, Type = FieldType.Textbox, Label = "Minimum Seeders", HelpText = "Minimum number of seeders required.", Advanced = true)] + public int MinimumSeeders { get; set; } + public NzbDroneValidationResult Validate() { return new NzbDroneValidationResult(Validator.Validate(this)); } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Core/Instrumentation/ReconfigureLogging.cs b/src/NzbDrone.Core/Instrumentation/ReconfigureLogging.cs index e53bea79a..1fd197da6 100644 --- a/src/NzbDrone.Core/Instrumentation/ReconfigureLogging.cs +++ b/src/NzbDrone.Core/Instrumentation/ReconfigureLogging.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using NLog; using NLog.Config; @@ -24,7 +24,7 @@ namespace NzbDrone.Core.Instrumentation var rules = LogManager.Configuration.LoggingRules; //Console - SetMinimumLogLevel(rules, "consoleLogger", minimumLogLevel); + SetMinimumLogLevel(rules, "consoleLogger", LogLevel.Trace); //Log Files SetMinimumLogLevel(rules, "appFileInfo", minimumLogLevel <= LogLevel.Info ? LogLevel.Info : LogLevel.Off); diff --git a/src/NzbDrone.Core/Jobs/TaskManager.cs b/src/NzbDrone.Core/Jobs/TaskManager.cs index 646f9a249..fbc63abbd 100644 --- a/src/NzbDrone.Core/Jobs/TaskManager.cs +++ b/src/NzbDrone.Core/Jobs/TaskManager.cs @@ -5,16 +5,13 @@ using NLog; using NzbDrone.Core.Backup; using NzbDrone.Core.Configuration; using NzbDrone.Core.Configuration.Events; -using NzbDrone.Core.DataAugmentation.Scene; using NzbDrone.Core.Download; using NzbDrone.Core.HealthCheck; using NzbDrone.Core.Housekeeping; using NzbDrone.Core.Indexers; using NzbDrone.Core.Lifecycle; -using NzbDrone.Core.MediaFiles.Commands; using NzbDrone.Core.Messaging.Commands; using NzbDrone.Core.Messaging.Events; -using NzbDrone.Core.Tv.Commands; using NzbDrone.Core.Update.Commands; using NzbDrone.Core.Music.Commands; @@ -74,13 +71,7 @@ namespace NzbDrone.Core.Jobs { Interval = GetRssSyncInterval(), TypeName = typeof(RssSyncCommand).FullName - }, - - new ScheduledTask - { - Interval = _configService.DownloadedAlbumsScanInterval, - TypeName = typeof(DownloadedAlbumsScanCommand).FullName - }, + } }; var currentTasks = _scheduledTaskRepository.All().ToList(); @@ -144,10 +135,7 @@ namespace NzbDrone.Core.Jobs var rss = _scheduledTaskRepository.GetDefinition(typeof(RssSyncCommand)); rss.Interval = _configService.RssSyncInterval; - var downloadedAlbums = _scheduledTaskRepository.GetDefinition(typeof(DownloadedAlbumsScanCommand)); - downloadedAlbums.Interval = _configService.DownloadedAlbumsScanInterval; - - _scheduledTaskRepository.UpdateMany(new List { rss, downloadedAlbums }); + _scheduledTaskRepository.Update(rss); } } } diff --git a/src/NzbDrone.Core/Languages/Language.cs b/src/NzbDrone.Core/Languages/Language.cs new file mode 100644 index 000000000..bb252ce07 --- /dev/null +++ b/src/NzbDrone.Core/Languages/Language.cs @@ -0,0 +1,158 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using NzbDrone.Core.Datastore; + +namespace NzbDrone.Core.Languages +{ + public class Language : IEmbeddedDocument, IEquatable + { + public int Id { get; set; } + public string Name { get; set; } + + public Language() + { + } + + private Language(int id, string name) + { + Id = id; + Name = name; + } + + public override string ToString() + { + return Name; + } + + public override int GetHashCode() + { + return Id.GetHashCode(); + } + + public bool Equals(Language other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + return Id.Equals(other.Id); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + + return Equals(obj as Language); + } + + public static bool operator ==(Language left, Language right) + { + return Equals(left, right); + } + + public static bool operator !=(Language left, Language right) + { + return !Equals(left, right); + } + + public static Language Unknown { get { return new Language(0, "Unknown"); } } + public static Language English { get { return new Language(1, "English"); } } + public static Language French { get { return new Language(2, "French"); } } + public static Language Spanish { get { return new Language(3, "Spanish"); } } + public static Language German { get { return new Language(4, "German"); } } + public static Language Italian { get { return new Language(5, "Italian"); } } + public static Language Danish { get { return new Language(6, "Danish"); } } + public static Language Dutch { get { return new Language(7, "Dutch"); } } + public static Language Japanese { get { return new Language(8, "Japanese"); } } + public static Language Cantonese { get { return new Language(9, "Cantonese"); } } + public static Language Mandarin { get { return new Language(10, "Mandarin"); } } + public static Language Russian { get { return new Language(11, "Russian"); } } + public static Language Polish { get { return new Language(12, "Polish"); } } + public static Language Vietnamese { get { return new Language(13, "Vietnamese"); } } + public static Language Swedish { get { return new Language(14, "Swedish"); } } + public static Language Norwegian { get { return new Language(15, "Norwegian"); } } + public static Language Finnish { get { return new Language(16, "Finnish"); } } + public static Language Turkish { get { return new Language(17, "Turkish"); } } + public static Language Portuguese { get { return new Language(18, "Portuguese"); } } + public static Language Flemish { get { return new Language(19, "Flemish"); } } + public static Language Greek { get { return new Language(20, "Greek"); } } + public static Language Korean { get { return new Language(21, "Korean"); } } + public static Language Hungarian { get { return new Language(22, "Hungarian"); } } + public static Language Hebrew { get { return new Language(23, "Hebrew"); } } + public static Language Lithuanian { get { return new Language(24, "Lithuanian"); } } + public static Language Czech { get { return new Language(25, "Czech"); } } + + + public static List All + { + get + { + return new List + { + Unknown, + English, + French, + Spanish, + German, + Italian, + Danish, + Dutch, + Japanese, + Cantonese, + Mandarin, + Russian, + Polish, + Vietnamese, + Swedish, + Norwegian, + Finnish, + Turkish, + Portuguese, + Flemish, + Greek, + Korean, + Hungarian, + Hebrew, + Lithuanian, + Czech + }; + } + } + + public static Language FindById(int id) + { + if (id == 0) return Unknown; + + Language language = All.FirstOrDefault(v => v.Id == id); + + if (language == null) + { + throw new ArgumentException("ID does not match a known language", nameof(id)); + } + + return language; + } + + public static explicit operator Language(int id) + { + return FindById(id); + } + + public static explicit operator int(Language language) + { + return language.Id; + } + + public static explicit operator Language(string lang) + { + var language = All.FirstOrDefault(v => v.Name.Equals(lang, StringComparison.InvariantCultureIgnoreCase)); + + if (language == null) + { + throw new ArgumentException("Language does not match a known language", nameof(lang)); + } + + return language; + } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Core/Languages/LanguageComparer.cs b/src/NzbDrone.Core/Languages/LanguageComparer.cs new file mode 100644 index 000000000..e286aa681 --- /dev/null +++ b/src/NzbDrone.Core/Languages/LanguageComparer.cs @@ -0,0 +1,27 @@ +using System.Collections.Generic; +using NzbDrone.Common.EnsureThat; +using NzbDrone.Core.Profiles.Languages; + +namespace NzbDrone.Core.Languages +{ + public class LanguageComparer : IComparer + { + private readonly LanguageProfile _profile; + + public LanguageComparer(LanguageProfile profile) + { + Ensure.That(profile, () => profile).IsNotNull(); + Ensure.That(profile.Languages, () => profile.Languages).HasItems(); + + _profile = profile; + } + + public int Compare(Language left, Language right) + { + int leftIndex = _profile.Languages.FindIndex(v => v.Language == left); + int rightIndex = _profile.Languages.FindIndex(v => v.Language == right); + + return leftIndex.CompareTo(rightIndex); + } + } +} diff --git a/src/NzbDrone.Core/Languages/LanguagesBelowCutoff.cs b/src/NzbDrone.Core/Languages/LanguagesBelowCutoff.cs new file mode 100644 index 000000000..3b04f024a --- /dev/null +++ b/src/NzbDrone.Core/Languages/LanguagesBelowCutoff.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; + +namespace NzbDrone.Core.Languages +{ + public class LanguagesBelowCutoff + { + public int ProfileId { get; set; } + public IEnumerable LanguageIds { get; set; } + + public LanguagesBelowCutoff(int profileId, IEnumerable languageIds) + { + ProfileId = profileId; + LanguageIds = languageIds; + } + } +} diff --git a/src/NzbDrone.Core/NzbDrone.Core.dll.config b/src/NzbDrone.Core/Lidarr.Core.dll.config similarity index 100% rename from src/NzbDrone.Core/NzbDrone.Core.dll.config rename to src/NzbDrone.Core/Lidarr.Core.dll.config diff --git a/src/NzbDrone.Core/Lifecycle/LifecycleService.cs b/src/NzbDrone.Core/Lifecycle/LifecycleService.cs index a5c0c2a94..e4d727b0d 100644 --- a/src/NzbDrone.Core/Lifecycle/LifecycleService.cs +++ b/src/NzbDrone.Core/Lifecycle/LifecycleService.cs @@ -40,7 +40,7 @@ namespace NzbDrone.Core.Lifecycle if (_runtimeInfo.IsWindowsService) { - _serviceProvider.Stop(ServiceProvider.NZBDRONE_SERVICE_NAME); + _serviceProvider.Stop(ServiceProvider.SERVICE_NAME); } } @@ -52,7 +52,7 @@ namespace NzbDrone.Core.Lifecycle if (_runtimeInfo.IsWindowsService) { - _serviceProvider.Restart(ServiceProvider.NZBDRONE_SERVICE_NAME); + _serviceProvider.Restart(ServiceProvider.SERVICE_NAME); } } diff --git a/src/NzbDrone.Core/MediaCover/MediaCover.cs b/src/NzbDrone.Core/MediaCover/MediaCover.cs index 3808e9e20..ed832e003 100644 --- a/src/NzbDrone.Core/MediaCover/MediaCover.cs +++ b/src/NzbDrone.Core/MediaCover/MediaCover.cs @@ -1,4 +1,4 @@ -using NzbDrone.Core.Datastore; +using NzbDrone.Core.Datastore; namespace NzbDrone.Core.MediaCover { @@ -12,7 +12,8 @@ namespace NzbDrone.Core.MediaCover Screenshot = 4, Headshot = 5, Cover = 6, - Disc = 7 + Disc = 7, + Logo = 8 } public class MediaCover : IEmbeddedDocument @@ -30,4 +31,4 @@ namespace NzbDrone.Core.MediaCover Url = url; } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Core/MediaCover/MediaCoverService.cs b/src/NzbDrone.Core/MediaCover/MediaCoverService.cs index f8e7b652b..b477f0e85 100644 --- a/src/NzbDrone.Core/MediaCover/MediaCoverService.cs +++ b/src/NzbDrone.Core/MediaCover/MediaCoverService.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.IO; using System.Net; @@ -9,23 +9,24 @@ using NzbDrone.Common.Extensions; using NzbDrone.Common.Http; using NzbDrone.Core.Configuration; using NzbDrone.Core.Messaging.Events; -using NzbDrone.Core.Tv; -using NzbDrone.Core.Tv.Events; +using NzbDrone.Core.Music; +using NzbDrone.Core.Music.Events; namespace NzbDrone.Core.MediaCover { public interface IMapCoversToLocal { - void ConvertToLocalUrls(int seriesId, IEnumerable covers); - string GetCoverPath(int seriesId, MediaCoverTypes mediaCoverTypes, int? height = null); + void ConvertToLocalUrls(int artistId, IEnumerable covers, int? albumId = null); + string GetCoverPath(int artistId, MediaCoverTypes mediaCoverTypes, int? height = null, int? albumId = null); } public class MediaCoverService : - IHandleAsync, - IHandleAsync, + IHandleAsync, + IHandleAsync, IMapCoversToLocal { private readonly IImageResizer _resizer; + private readonly IAlbumService _albumService; private readonly IHttpClient _httpClient; private readonly IDiskProvider _diskProvider; private readonly ICoverExistsSpecification _coverExistsSpecification; @@ -36,6 +37,7 @@ namespace NzbDrone.Core.MediaCover private readonly string _coverRootFolder; public MediaCoverService(IImageResizer resizer, + IAlbumService albumService, IHttpClient httpClient, IDiskProvider diskProvider, IAppFolderInfo appFolderInfo, @@ -45,6 +47,7 @@ namespace NzbDrone.Core.MediaCover Logger logger) { _resizer = resizer; + _albumService = albumService; _httpClient = httpClient; _diskProvider = diskProvider; _coverExistsSpecification = coverExistsSpecification; @@ -55,20 +58,32 @@ namespace NzbDrone.Core.MediaCover _coverRootFolder = appFolderInfo.GetMediaCoverPath(); } - public string GetCoverPath(int seriesId, MediaCoverTypes coverTypes, int? height = null) + public string GetCoverPath(int artistId, MediaCoverTypes coverTypes, int? height = null, int? albumId = null) { var heightSuffix = height.HasValue ? "-" + height.ToString() : ""; - return Path.Combine(GetSeriesCoverPath(seriesId), coverTypes.ToString().ToLower() + heightSuffix + ".jpg"); + if (albumId.HasValue) + { + return Path.Combine(GetAlbumCoverPath(artistId, albumId.Value), coverTypes.ToString().ToLower() + heightSuffix + ".jpg"); + } + + return Path.Combine(GetArtistCoverPath(artistId), coverTypes.ToString().ToLower() + heightSuffix + ".jpg"); } - public void ConvertToLocalUrls(int seriesId, IEnumerable covers) + public void ConvertToLocalUrls(int artistId, IEnumerable covers, int? albumId = null) { foreach (var mediaCover in covers) { - var filePath = GetCoverPath(seriesId, mediaCover.CoverType); + var filePath = GetCoverPath(artistId, mediaCover.CoverType, null, albumId); - mediaCover.Url = _configFileProvider.UrlBase + @"/MediaCover/" + seriesId + "/" + mediaCover.CoverType.ToString().ToLower() + ".jpg"; + if (albumId.HasValue) + { + mediaCover.Url = _configFileProvider.UrlBase + @"/MediaCover/" + artistId + "/" + albumId + "/" + mediaCover.CoverType.ToString().ToLower() + ".jpg"; + } + else + { + mediaCover.Url = _configFileProvider.UrlBase + @"/MediaCover/" + artistId + "/" + mediaCover.CoverType.ToString().ToLower() + ".jpg"; + } if (_diskProvider.FileExists(filePath)) { @@ -78,47 +93,87 @@ namespace NzbDrone.Core.MediaCover } } - private string GetSeriesCoverPath(int seriesId) + private string GetArtistCoverPath(int artistId) + { + return Path.Combine(_coverRootFolder, artistId.ToString()); + } + + private string GetAlbumCoverPath(int artistId, int albumId) { - return Path.Combine(_coverRootFolder, seriesId.ToString()); + return Path.Combine(_coverRootFolder, artistId.ToString(), albumId.ToString()); } - private void EnsureCovers(Series series) + private void EnsureCovers(Artist artist) { - foreach (var cover in series.Images) + foreach (var cover in artist.Images) { - var fileName = GetCoverPath(series.Id, cover.CoverType); + var fileName = GetCoverPath(artist.Id, cover.CoverType); var alreadyExists = false; try { alreadyExists = _coverExistsSpecification.AlreadyExists(cover.Url, fileName); if (!alreadyExists) { - DownloadCover(series, cover); + DownloadCover(artist, cover); } } catch (WebException e) { - _logger.Warn("Couldn't download media cover for {0}. {1}", series, e.Message); + _logger.Warn("Couldn't download media cover for {0}. {1}", artist, e.Message); } catch (Exception e) { - _logger.Error(e, "Couldn't download media cover for {0}", series); + _logger.Error(e, "Couldn't download media cover for {0}", artist); } - EnsureResizedCovers(series, cover, !alreadyExists); + EnsureResizedCovers(artist, cover, !alreadyExists); } } - private void DownloadCover(Series series, MediaCover cover) + private void EnsureAlbumCovers(Album album) { - var fileName = GetCoverPath(series.Id, cover.CoverType); + foreach (var cover in album.Images) + { + var fileName = GetCoverPath(album.ArtistId, cover.CoverType, null, album.Id); + var alreadyExists = false; + try + { + alreadyExists = _coverExistsSpecification.AlreadyExists(cover.Url, fileName); + if (!alreadyExists) + { + DownloadAlbumCover(album, cover); + } + } + catch (WebException e) + { + _logger.Warn("Couldn't download media cover for {0}. {1}", album, e.Message); + } + catch (Exception e) + { + _logger.Error(e, "Couldn't download media cover for {0}", album); + } + + EnsureResizedCovers(album.Artist, cover, !alreadyExists, album); + } + } + + private void DownloadCover(Artist artist, MediaCover cover) + { + var fileName = GetCoverPath(artist.Id, cover.CoverType); + + _logger.Info("Downloading {0} for {1} {2}", cover.CoverType, artist, cover.Url); + _httpClient.DownloadFile(cover.Url, fileName); + } + + private void DownloadAlbumCover(Album album, MediaCover cover) + { + var fileName = GetCoverPath(album.ArtistId, cover.CoverType, null, album.Id); - _logger.Info("Downloading {0} for {1} {2}", cover.CoverType, series, cover.Url); + _logger.Info("Downloading {0} for {1} {2}", cover.CoverType, album, cover.Url); _httpClient.DownloadFile(cover.Url, fileName); } - private void EnsureResizedCovers(Series series, MediaCover cover, bool forceResize) + private void EnsureResizedCovers(Artist artist, MediaCover cover, bool forceResize, Album album = null) { int[] heights; @@ -128,6 +183,9 @@ namespace NzbDrone.Core.MediaCover return; case MediaCoverTypes.Poster: + case MediaCoverTypes.Cover: + case MediaCoverTypes.Disc: + case MediaCoverTypes.Logo: case MediaCoverTypes.Headshot: heights = new[] { 500, 250 }; break; @@ -141,37 +199,72 @@ namespace NzbDrone.Core.MediaCover heights = new[] { 360, 180 }; break; } + - foreach (var height in heights) + if (album == null) { - var mainFileName = GetCoverPath(series.Id, cover.CoverType); - var resizeFileName = GetCoverPath(series.Id, cover.CoverType, height); - - if (forceResize || !_diskProvider.FileExists(resizeFileName) || _diskProvider.GetFileSize(resizeFileName) == 0) + foreach (var height in heights) { - _logger.Debug("Resizing {0}-{1} for {2}", cover.CoverType, height, series); + var mainFileName = GetCoverPath(artist.Id, cover.CoverType); + var resizeFileName = GetCoverPath(artist.Id, cover.CoverType, height); - try + if (forceResize || !_diskProvider.FileExists(resizeFileName) || _diskProvider.GetFileSize(resizeFileName) == 0) { - _resizer.Resize(mainFileName, resizeFileName, height); + _logger.Debug("Resizing {0}-{1} for {2}", cover.CoverType, height, artist); + + try + { + _resizer.Resize(mainFileName, resizeFileName, height); + } + catch + { + _logger.Debug("Couldn't resize media cover {0}-{1} for {2}, using full size image instead.", cover.CoverType, height, artist); + } } - catch + } + } + else + { + foreach (var height in heights) + { + var mainFileName = GetCoverPath(album.ArtistId, cover.CoverType, null, album.Id); + var resizeFileName = GetCoverPath(album.ArtistId, cover.CoverType, height, album.Id); + + if (forceResize || !_diskProvider.FileExists(resizeFileName) || _diskProvider.GetFileSize(resizeFileName) == 0) { - _logger.Debug("Couldn't resize media cover {0}-{1} for {2}, using full size image instead.", cover.CoverType, height, series); + _logger.Debug("Resizing {0}-{1} for {2}", cover.CoverType, height, artist); + + try + { + _resizer.Resize(mainFileName, resizeFileName, height); + } + catch + { + _logger.Debug("Couldn't resize media cover {0}-{1} for {2}, using full size image instead.", cover.CoverType, height, album); + } } } } } - public void HandleAsync(SeriesUpdatedEvent message) + public void HandleAsync(ArtistUpdatedEvent message) { - EnsureCovers(message.Series); - _eventAggregator.PublishEvent(new MediaCoversUpdatedEvent(message.Series)); + EnsureCovers(message.Artist); + + //Turn off for now, not using album images + + //var albums = _albumService.GetAlbumsByArtist(message.Artist.Id); + //foreach (Album album in albums) + //{ + // EnsureAlbumCovers(album); + //} + + _eventAggregator.PublishEvent(new MediaCoversUpdatedEvent(message.Artist)); } - public void HandleAsync(SeriesDeletedEvent message) + public void HandleAsync(ArtistDeletedEvent message) { - var path = GetSeriesCoverPath(message.Series.Id); + var path = GetArtistCoverPath(message.Artist.Id); if (_diskProvider.FolderExists(path)) { _diskProvider.DeleteFolder(path, true); diff --git a/src/NzbDrone.Core/MediaCover/MediaCoversUpdatedEvent.cs b/src/NzbDrone.Core/MediaCover/MediaCoversUpdatedEvent.cs index 7335f7f9b..65ce089a1 100644 --- a/src/NzbDrone.Core/MediaCover/MediaCoversUpdatedEvent.cs +++ b/src/NzbDrone.Core/MediaCover/MediaCoversUpdatedEvent.cs @@ -1,15 +1,21 @@ -using NzbDrone.Common.Messaging; -using NzbDrone.Core.Tv; +using NzbDrone.Common.Messaging; +using NzbDrone.Core.Music; namespace NzbDrone.Core.MediaCover { public class MediaCoversUpdatedEvent : IEvent { - public Series Series { get; set; } + public Artist Artist { get; set; } + public Album Album { get; set; } - public MediaCoversUpdatedEvent(Series series) + public MediaCoversUpdatedEvent(Artist artist) { - Series = series; + Artist = artist; + } + + public MediaCoversUpdatedEvent(Album album) + { + Album = album; } } } diff --git a/src/NzbDrone.Core/MediaFiles/Commands/DownloadedAlbumsScanCommand.cs b/src/NzbDrone.Core/MediaFiles/Commands/DownloadedAlbumsScanCommand.cs index a06e39379..52742bd81 100644 --- a/src/NzbDrone.Core/MediaFiles/Commands/DownloadedAlbumsScanCommand.cs +++ b/src/NzbDrone.Core/MediaFiles/Commands/DownloadedAlbumsScanCommand.cs @@ -5,10 +5,6 @@ namespace NzbDrone.Core.MediaFiles.Commands { public class DownloadedAlbumsScanCommand : Command { - public override bool SendUpdatesToClient => SendUpdates; - - public bool SendUpdates { get; set; } - // Properties used by third-party apps, do not modify. public string Path { get; set; } public string DownloadClientId { get; set; } diff --git a/src/NzbDrone.Core/MediaFiles/Commands/RenameSeriesCommand.cs b/src/NzbDrone.Core/MediaFiles/Commands/RenameSeriesCommand.cs deleted file mode 100644 index a2bcda88c..000000000 --- a/src/NzbDrone.Core/MediaFiles/Commands/RenameSeriesCommand.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System.Collections.Generic; -using NzbDrone.Core.Messaging.Commands; - -namespace NzbDrone.Core.MediaFiles.Commands -{ - public class RenameSeriesCommand : Command - { - public List SeriesIds { get; set; } - - public override bool SendUpdatesToClient => true; - - public RenameSeriesCommand() - { - } - } -} \ No newline at end of file diff --git a/src/NzbDrone.Core/MediaFiles/Commands/RescanArtistCommand.cs b/src/NzbDrone.Core/MediaFiles/Commands/RescanArtistCommand.cs index 904350d5a..5e17c0cd7 100644 --- a/src/NzbDrone.Core/MediaFiles/Commands/RescanArtistCommand.cs +++ b/src/NzbDrone.Core/MediaFiles/Commands/RescanArtistCommand.cs @@ -1,24 +1,18 @@ -using NzbDrone.Core.Messaging.Commands; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; +using NzbDrone.Core.Messaging.Commands; -namespace NzbDrone.Core.MediaFiles.Events +namespace NzbDrone.Core.MediaFiles.Commands { public class RescanArtistCommand : Command { - - public string ArtistId { get; set; } + public int? ArtistId { get; set; } public override bool SendUpdatesToClient => true; public RescanArtistCommand() { - ArtistId = ""; } - public RescanArtistCommand(string artistId) + public RescanArtistCommand(int artistId) { ArtistId = artistId; } diff --git a/src/NzbDrone.Core/MediaFiles/Commands/RescanSeriesCommand.cs b/src/NzbDrone.Core/MediaFiles/Commands/RescanSeriesCommand.cs deleted file mode 100644 index 6330574ab..000000000 --- a/src/NzbDrone.Core/MediaFiles/Commands/RescanSeriesCommand.cs +++ /dev/null @@ -1,20 +0,0 @@ -using NzbDrone.Core.Messaging.Commands; - -namespace NzbDrone.Core.MediaFiles.Commands -{ - public class RescanSeriesCommand : Command - { - public int? SeriesId { get; set; } - - public override bool SendUpdatesToClient => true; - - public RescanSeriesCommand() - { - } - - public RescanSeriesCommand(int seriesId) - { - SeriesId = seriesId; - } - } -} \ No newline at end of file diff --git a/src/NzbDrone.Core/MediaFiles/DiskScanService.cs b/src/NzbDrone.Core/MediaFiles/DiskScanService.cs index 32b52f9e7..8a198802b 100644 --- a/src/NzbDrone.Core/MediaFiles/DiskScanService.cs +++ b/src/NzbDrone.Core/MediaFiles/DiskScanService.cs @@ -13,7 +13,6 @@ using NzbDrone.Core.MediaFiles.Commands; using NzbDrone.Core.MediaFiles.Events; using NzbDrone.Core.Messaging.Commands; using NzbDrone.Core.Messaging.Events; -using NzbDrone.Core.Tv; using NzbDrone.Core.Music; using NzbDrone.Core.Music.Events; using NzbDrone.Core.MediaFiles.TrackImport; @@ -25,7 +24,7 @@ namespace NzbDrone.Core.MediaFiles void Scan(Artist artist); string[] GetAudioFiles(string path, bool allDirectories = true); string[] GetNonAudioFiles(string path, bool allDirectories = true); - List FilterFiles(Artist artist, IEnumerable files); + List FilterFiles(string basePath, IEnumerable files); } public class DiskScanService : @@ -60,9 +59,8 @@ namespace NzbDrone.Core.MediaFiles _eventAggregator = eventAggregator; _logger = logger; } - - private static readonly Regex ExcludedSubFoldersRegex = new Regex(@"(?:\\|\/|^)(extras|@eadir|extrafanart|plex\sversions|\..+)(?:\\|\/)", RegexOptions.Compiled | RegexOptions.IgnoreCase); - private static readonly Regex ExcludedFilesRegex = new Regex(@"^\._|Thumbs\.db", RegexOptions.Compiled | RegexOptions.IgnoreCase); + private static readonly Regex ExcludedSubFoldersRegex = new Regex(@"(?:\\|\/|^)(?:extras|@eadir|extrafanart|plex versions|\.[^\\/]+)(?:\\|\/)", RegexOptions.Compiled | RegexOptions.IgnoreCase); + private static readonly Regex ExcludedFilesRegex = new Regex(@"^\._|^Thumbs\.db$", RegexOptions.Compiled | RegexOptions.IgnoreCase); public void Scan(Artist artist) { @@ -82,7 +80,7 @@ namespace NzbDrone.Core.MediaFiles return; } - _logger.ProgressInfo("Scanning disk for {0}", artist.Name); + _logger.ProgressInfo("Scanning {0}", artist.Name); if (!_diskProvider.FolderExists(artist.Path)) { @@ -102,7 +100,7 @@ namespace NzbDrone.Core.MediaFiles } var musicFilesStopwatch = Stopwatch.StartNew(); - var mediaFileList = FilterFiles(artist, GetAudioFiles(artist.Path)).ToList(); + var mediaFileList = FilterFiles(artist.Path, GetAudioFiles(artist.Path)).ToList(); musicFilesStopwatch.Stop(); _logger.Trace("Finished getting track files for: {0} [{1}]", artist, musicFilesStopwatch.Elapsed); @@ -136,7 +134,7 @@ namespace NzbDrone.Core.MediaFiles var searchOption = allDirectories ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly; var filesOnDisk = _diskProvider.GetFiles(path, searchOption).ToList(); - var mediaFileList = filesOnDisk.Where(file => MediaFileExtensions.Extensions.Contains(Path.GetExtension(file).ToLower())) + var mediaFileList = filesOnDisk.Where(file => MediaFileExtensions.Extensions.Contains(Path.GetExtension(file))) .ToList(); _logger.Trace("{0} files were found in {1}", filesOnDisk.Count, path); @@ -151,7 +149,7 @@ namespace NzbDrone.Core.MediaFiles var searchOption = allDirectories ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly; var filesOnDisk = _diskProvider.GetFiles(path, searchOption).ToList(); - var mediaFileList = filesOnDisk.Where(file => !MediaFileExtensions.Extensions.Contains(Path.GetExtension(file).ToLower())) + var mediaFileList = filesOnDisk.Where(file => !MediaFileExtensions.Extensions.Contains(Path.GetExtension(file))) .ToList(); _logger.Trace("{0} files were found in {1}", filesOnDisk.Count, path); @@ -159,9 +157,9 @@ namespace NzbDrone.Core.MediaFiles return mediaFileList.ToArray(); } - public List FilterFiles(Artist artist, IEnumerable files) + public List FilterFiles(string basePath, IEnumerable files) { - return files.Where(file => !ExcludedSubFoldersRegex.IsMatch(artist.Path.GetRelativePath(file))) + return files.Where(file => !ExcludedSubFoldersRegex.IsMatch(basePath.GetRelativePath(file))) .Where(file => !ExcludedFilesRegex.IsMatch(Path.GetFileName(file))) .ToList(); } @@ -194,9 +192,9 @@ namespace NzbDrone.Core.MediaFiles public void Execute(RescanArtistCommand message) { - if (message.ArtistId.IsNotNullOrWhiteSpace()) + if (message.ArtistId.HasValue) { - var artist = _artistService.FindById(message.ArtistId); + var artist = _artistService.GetArtist(message.ArtistId.Value); Scan(artist); } diff --git a/src/NzbDrone.Core/MediaFiles/DownloadedAlbumsCommandService.cs b/src/NzbDrone.Core/MediaFiles/DownloadedAlbumsCommandService.cs index 41bfd2739..e117b6b1f 100644 --- a/src/NzbDrone.Core/MediaFiles/DownloadedAlbumsCommandService.cs +++ b/src/NzbDrone.Core/MediaFiles/DownloadedAlbumsCommandService.cs @@ -1,5 +1,5 @@ using System.Collections.Generic; -using System.IO; +using System; using System.Linq; using NLog; using NzbDrone.Common.Disk; @@ -9,49 +9,28 @@ using NzbDrone.Core.Download.TrackedDownloads; using NzbDrone.Core.MediaFiles.Commands; using NzbDrone.Core.Messaging.Commands; using NzbDrone.Core.MediaFiles.TrackImport; +using NzbDrone.Common.Instrumentation.Extensions; namespace NzbDrone.Core.MediaFiles { public class DownloadedAlbumsCommandService : IExecute { - private readonly IDownloadedEpisodesImportService _downloadedEpisodesImportService; + private readonly IDownloadedTracksImportService _downloadedTracksImportService; private readonly ITrackedDownloadService _trackedDownloadService; private readonly IDiskProvider _diskProvider; - private readonly IConfigService _configService; private readonly Logger _logger; - public DownloadedAlbumsCommandService(IDownloadedEpisodesImportService downloadedEpisodesImportService, + public DownloadedAlbumsCommandService(IDownloadedTracksImportService downloadedTracksImportService, ITrackedDownloadService trackedDownloadService, IDiskProvider diskProvider, - IConfigService configService, Logger logger) { - _downloadedEpisodesImportService = downloadedEpisodesImportService; + _downloadedTracksImportService = downloadedTracksImportService; _trackedDownloadService = trackedDownloadService; _diskProvider = diskProvider; - _configService = configService; _logger = logger; } - private List ProcessDroneFactoryFolder() - { - var downloadedAlbumsFolder = _configService.DownloadedAlbumsFolder; - - if (string.IsNullOrEmpty(downloadedAlbumsFolder)) - { - _logger.Trace("Drone Factory folder is not configured"); - return new List(); - } - - if (!_diskProvider.FolderExists(downloadedAlbumsFolder)) - { - _logger.Warn("Drone Factory folder [{0}] doesn't exist.", downloadedAlbumsFolder); - return new List(); - } - - return _downloadedEpisodesImportService.ProcessRootFolder(new DirectoryInfo(downloadedAlbumsFolder)); - } - private List ProcessPath(DownloadedAlbumsScanCommand message) { if (!_diskProvider.FolderExists(message.Path) && !_diskProvider.FileExists(message.Path)) @@ -68,17 +47,17 @@ namespace NzbDrone.Core.MediaFiles { _logger.Debug("External directory scan request for known download {0}. [{1}]", message.DownloadClientId, message.Path); - return _downloadedEpisodesImportService.ProcessPath(message.Path, message.ImportMode, trackedDownload.RemoteEpisode.Series, trackedDownload.DownloadItem); + return _downloadedTracksImportService.ProcessPath(message.Path, message.ImportMode, trackedDownload.RemoteAlbum.Artist, trackedDownload.DownloadItem); } else { _logger.Warn("External directory scan request for unknown download {0}, attempting normal import. [{1}]", message.DownloadClientId, message.Path); - return _downloadedEpisodesImportService.ProcessPath(message.Path, message.ImportMode); + return _downloadedTracksImportService.ProcessPath(message.Path, message.ImportMode); } } - return _downloadedEpisodesImportService.ProcessPath(message.Path, message.ImportMode); + return _downloadedTracksImportService.ProcessPath(message.Path, message.ImportMode); } public void Execute(DownloadedAlbumsScanCommand message) @@ -91,7 +70,7 @@ namespace NzbDrone.Core.MediaFiles } else { - importResults = ProcessDroneFactoryFolder(); + throw new ArgumentException("A path must be provided", "path"); } if (importResults == null || importResults.All(v => v.Result != ImportResultType.Imported)) diff --git a/src/NzbDrone.Core/MediaFiles/DownloadedEpisodesImportService.cs b/src/NzbDrone.Core/MediaFiles/DownloadedEpisodesImportService.cs deleted file mode 100644 index 583d50708..000000000 --- a/src/NzbDrone.Core/MediaFiles/DownloadedEpisodesImportService.cs +++ /dev/null @@ -1,270 +0,0 @@ -using System.Collections.Generic; -using System.IO; -using System.Linq; -using NLog; -using NzbDrone.Common.Disk; -using NzbDrone.Core.DecisionEngine; -using NzbDrone.Core.Parser; -using NzbDrone.Core.Tv; -using NzbDrone.Core.Download; -using NzbDrone.Core.Parser.Model; -using NzbDrone.Core.MediaFiles.TrackImport; - -namespace NzbDrone.Core.MediaFiles -{ - public interface IDownloadedEpisodesImportService - { - List ProcessRootFolder(DirectoryInfo directoryInfo); - List ProcessPath(string path, ImportMode importMode = ImportMode.Auto, Series series = null, DownloadClientItem downloadClientItem = null); - bool ShouldDeleteFolder(DirectoryInfo directoryInfo, Series series); - } - - public class DownloadedEpisodesImportService : IDownloadedEpisodesImportService - { - private readonly IDiskProvider _diskProvider; - private readonly IDiskScanService _diskScanService; - private readonly ISeriesService _seriesService; - private readonly IParsingService _parsingService; - private readonly IMakeImportDecision _importDecisionMaker; - private readonly IImportApprovedEpisodes _importApprovedEpisodes; - private readonly IDetectSample _detectSample; - private readonly Logger _logger; - - public DownloadedEpisodesImportService(IDiskProvider diskProvider, - IDiskScanService diskScanService, - ISeriesService seriesService, - IParsingService parsingService, - IMakeImportDecision importDecisionMaker, - IImportApprovedEpisodes importApprovedEpisodes, - IDetectSample detectSample, - Logger logger) - { - _diskProvider = diskProvider; - _diskScanService = diskScanService; - _seriesService = seriesService; - _parsingService = parsingService; - _importDecisionMaker = importDecisionMaker; - _importApprovedEpisodes = importApprovedEpisodes; - _detectSample = detectSample; - _logger = logger; - } - - public List ProcessRootFolder(DirectoryInfo directoryInfo) - { - var results = new List(); - - foreach (var subFolder in _diskProvider.GetDirectories(directoryInfo.FullName)) - { - var folderResults = ProcessFolder(new DirectoryInfo(subFolder), ImportMode.Auto, null); - results.AddRange(folderResults); - } - - foreach (var videoFile in _diskScanService.GetNonAudioFiles(directoryInfo.FullName, false)) - { - var fileResults = ProcessFile(new FileInfo(videoFile), ImportMode.Auto, null); - results.AddRange(fileResults); - } - - return results; - } - - public List ProcessPath(string path, ImportMode importMode = ImportMode.Auto, Series series = null, DownloadClientItem downloadClientItem = null) - { - if (_diskProvider.FolderExists(path)) - { - var directoryInfo = new DirectoryInfo(path); - - if (series == null) - { - return ProcessFolder(directoryInfo, importMode, downloadClientItem); - } - - return ProcessFolder(directoryInfo, importMode, series, downloadClientItem); - } - - if (_diskProvider.FileExists(path)) - { - var fileInfo = new FileInfo(path); - - if (series == null) - { - return ProcessFile(fileInfo, importMode, downloadClientItem); - } - - return ProcessFile(fileInfo, importMode, series, downloadClientItem); - } - - _logger.Error("Import failed, path does not exist or is not accessible by Lidarr: {0}", path); - return new List(); - } - - public bool ShouldDeleteFolder(DirectoryInfo directoryInfo, Series series) - { - var videoFiles = _diskScanService.GetNonAudioFiles(directoryInfo.FullName); - var rarFiles = _diskProvider.GetFiles(directoryInfo.FullName, SearchOption.AllDirectories).Where(f => Path.GetExtension(f) == ".rar"); - - foreach (var videoFile in videoFiles) - { - var episodeParseResult = Parser.Parser.ParseTitle(Path.GetFileName(videoFile)); - - if (episodeParseResult == null) - { - _logger.Warn("Unable to parse file on import: [{0}]", videoFile); - return false; - } - - var size = _diskProvider.GetFileSize(videoFile); - var quality = QualityParser.ParseQuality(videoFile); - - if (!_detectSample.IsSample(series, quality, videoFile, size, episodeParseResult.IsPossibleSpecialEpisode)) - { - _logger.Warn("Non-sample file detected: [{0}]", videoFile); - return false; - } - } - - if (rarFiles.Any(f => _diskProvider.GetFileSize(f) > 10.Megabytes())) - { - _logger.Warn("RAR file detected, will require manual cleanup"); - return false; - } - - return true; - } - - private List ProcessFolder(DirectoryInfo directoryInfo, ImportMode importMode, DownloadClientItem downloadClientItem) - { - var cleanedUpName = GetCleanedUpFolderName(directoryInfo.Name); - var series = _parsingService.GetSeries(cleanedUpName); - - if (series == null) - { - _logger.Debug("Unknown Series {0}", cleanedUpName); - - return new List - { - UnknownSeriesResult("Unknown Series") - }; - } - - return ProcessFolder(directoryInfo, importMode, series, downloadClientItem); - } - - private List ProcessFolder(DirectoryInfo directoryInfo, ImportMode importMode, Series series, DownloadClientItem downloadClientItem) - { - throw new System.NotImplementedException("Will be removed"); - - //if (_seriesService.SeriesPathExists(directoryInfo.FullName)) - //{ - // _logger.Warn("Unable to process folder that is mapped to an existing show"); - // return new List(); - //} - - //var cleanedUpName = GetCleanedUpFolderName(directoryInfo.Name); - //var folderInfo = Parser.Parser.ParseTitle(directoryInfo.Name); - - //if (folderInfo != null) - //{ - // _logger.Debug("{0} folder quality: {1}", cleanedUpName, folderInfo.Quality); - //} - - //var videoFiles = _diskScanService.GetVideoFiles(directoryInfo.FullName); - - //if (downloadClientItem == null) - //{ - // foreach (var videoFile in videoFiles) - // { - // if (_diskProvider.IsFileLocked(videoFile)) - // { - // return new List - // { - // FileIsLockedResult(videoFile) - // }; - // } - // } - //} - - //var decisions = _importDecisionMaker.GetImportDecisions(videoFiles.ToList(), series, folderInfo, true); - //var importResults = _importApprovedEpisodes.Import(decisions, true, downloadClientItem, importMode); - - //if ((downloadClientItem == null || !downloadClientItem.IsReadOnly) && - // importResults.Any(i => i.Result == ImportResultType.Imported) && - // ShouldDeleteFolder(directoryInfo, series)) - //{ - // _logger.Debug("Deleting folder after importing valid files"); - // _diskProvider.DeleteFolder(directoryInfo.FullName, true); - //} - - //return importResults; - } - - private List ProcessFile(FileInfo fileInfo, ImportMode importMode, DownloadClientItem downloadClientItem) - { - var series = _parsingService.GetSeries(Path.GetFileNameWithoutExtension(fileInfo.Name)); - - if (series == null) - { - _logger.Debug("Unknown Series for file: {0}", fileInfo.Name); - - return new List - { - UnknownSeriesResult(string.Format("Unknown Series for file: {0}", fileInfo.Name), fileInfo.FullName) - }; - } - - return ProcessFile(fileInfo, importMode, series, downloadClientItem); - } - - private List ProcessFile(FileInfo fileInfo, ImportMode importMode, Series series, DownloadClientItem downloadClientItem) - { - throw new System.NotImplementedException("Will be removed"); - //if (Path.GetFileNameWithoutExtension(fileInfo.Name).StartsWith("._")) - //{ - // _logger.Debug("[{0}] starts with '._', skipping", fileInfo.FullName); - - // return new List - // { - // new ImportResult(new ImportDecision(new LocalTrack { Path = fileInfo.FullName }, new Rejection("Invalid music file, filename starts with '._'")), "Invalid music file, filename starts with '._'") - // }; - //} - - //if (downloadClientItem == null) - //{ - // if (_diskProvider.IsFileLocked(fileInfo.FullName)) - // { - // return new List - // { - // FileIsLockedResult(fileInfo.FullName) - // }; - // } - //} - - //var decisions = _importDecisionMaker.GetImportDecisions(new List() { fileInfo.FullName }, series, null, true); - - //return _importApprovedEpisodes.Import(decisions, true, downloadClientItem, importMode); - } - - private string GetCleanedUpFolderName(string folder) - { - folder = folder.Replace("_UNPACK_", "") - .Replace("_FAILED_", ""); - - return folder; - } - - private ImportResult FileIsLockedResult(string videoFile) - { - throw new System.NotImplementedException("Will be removed"); - //_logger.Debug("[{0}] is currently locked by another process, skipping", videoFile); - //return new ImportResult(new ImportDecision(new LocalEpisode { Path = videoFile }, new Rejection("Locked file, try again later")), "Locked file, try again later"); - } - - private ImportResult UnknownSeriesResult(string message, string videoFile = null) - { - throw new System.NotImplementedException("Will be removed"); - //var localEpisode = videoFile == null ? null : new LocalEpisode { Path = videoFile }; - - //return new ImportResult(new ImportDecision(localEpisode, new Rejection("Unknown Series")), message); - } - } -} diff --git a/src/NzbDrone.Core/MediaFiles/DownloadedTracksImportService.cs b/src/NzbDrone.Core/MediaFiles/DownloadedTracksImportService.cs new file mode 100644 index 000000000..5312b655c --- /dev/null +++ b/src/NzbDrone.Core/MediaFiles/DownloadedTracksImportService.cs @@ -0,0 +1,282 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using NLog; +using NzbDrone.Common.Disk; +using NzbDrone.Core.DecisionEngine; +using NzbDrone.Core.Download; +using NzbDrone.Core.MediaFiles.TrackImport; +using NzbDrone.Core.Music; +using NzbDrone.Core.Parser; +using NzbDrone.Core.Parser.Model; + +namespace NzbDrone.Core.MediaFiles +{ + public interface IDownloadedTracksImportService + { + List ProcessRootFolder(DirectoryInfo directoryInfo); + List ProcessPath(string path, ImportMode importMode = ImportMode.Auto, Artist artist = null, DownloadClientItem downloadClientItem = null); + bool ShouldDeleteFolder(DirectoryInfo directoryInfo, Artist artist); + } + + public class DownloadedTracksImportService : IDownloadedTracksImportService + { + private readonly IDiskProvider _diskProvider; + private readonly IDiskScanService _diskScanService; + private readonly IArtistService _artistService; + private readonly IParsingService _parsingService; + private readonly IMakeImportDecision _importDecisionMaker; + private readonly IImportApprovedTracks _importApprovedTracks; + private readonly Logger _logger; + + public DownloadedTracksImportService(IDiskProvider diskProvider, + IDiskScanService diskScanService, + IArtistService artistService, + IParsingService parsingService, + IMakeImportDecision importDecisionMaker, + IImportApprovedTracks importApprovedTracks, + Logger logger) + { + _diskProvider = diskProvider; + _diskScanService = diskScanService; + _artistService = artistService; + _parsingService = parsingService; + _importDecisionMaker = importDecisionMaker; + _importApprovedTracks = importApprovedTracks; + _logger = logger; + } + + public List ProcessRootFolder(DirectoryInfo directoryInfo) + { + var results = new List(); + + foreach (var subFolder in _diskProvider.GetDirectories(directoryInfo.FullName)) + { + var folderResults = ProcessFolder(new DirectoryInfo(subFolder), ImportMode.Auto, null); + results.AddRange(folderResults); + } + + foreach (var videoFile in _diskScanService.GetNonAudioFiles(directoryInfo.FullName, false)) + { + var fileResults = ProcessFile(new FileInfo(videoFile), ImportMode.Auto, null); + results.AddRange(fileResults); + } + + return results; + } + + public List ProcessPath(string path, ImportMode importMode = ImportMode.Auto, Artist artist = null, DownloadClientItem downloadClientItem = null) + { + if (_diskProvider.FolderExists(path)) + { + var directoryInfo = new DirectoryInfo(path); + + if (artist == null) + { + return ProcessFolder(directoryInfo, importMode, downloadClientItem); + } + + return ProcessFolder(directoryInfo, importMode, artist, downloadClientItem); + } + + if (_diskProvider.FileExists(path)) + { + var fileInfo = new FileInfo(path); + + if (artist == null) + { + return ProcessFile(fileInfo, importMode, downloadClientItem); + } + + return ProcessFile(fileInfo, importMode, artist, downloadClientItem); + } + + _logger.Error("Import failed, path does not exist or is not accessible by Lidarr: {0}", path); + return new List(); + } + + public bool ShouldDeleteFolder(DirectoryInfo directoryInfo, Artist artist) + { + var audioFiles = _diskScanService.GetNonAudioFiles(directoryInfo.FullName); + var rarFiles = _diskProvider.GetFiles(directoryInfo.FullName, SearchOption.AllDirectories).Where(f => Path.GetExtension(f).Equals(".rar", StringComparison.OrdinalIgnoreCase)); + + foreach (var audioFile in audioFiles) + { + var albumParseResult = Parser.Parser.ParseMusicTitle(Path.GetFileName(audioFile)); + + if (albumParseResult == null) + { + _logger.Warn("Unable to parse file on import: [{0}]", audioFile); + return false; + } + + var size = _diskProvider.GetFileSize(audioFile); + var quality = QualityParser.ParseQuality(audioFile); + + //if (!_detectSample.IsSample(artist, quality, audioFile, size, albumParseResult.IsPossibleSpecialEpisode)) + //{ + // _logger.Warn("Non-sample file detected: [{0}]", audioFile); + // return false; + //} + } + + if (rarFiles.Any(f => _diskProvider.GetFileSize(f) > 10.Megabytes())) + { + _logger.Warn("RAR file detected, will require manual cleanup"); + return false; + } + + return true; + } + + private List ProcessFolder(DirectoryInfo directoryInfo, ImportMode importMode, DownloadClientItem downloadClientItem) + { + var cleanedUpName = GetCleanedUpFolderName(directoryInfo.Name); + + var files = _diskScanService.GetAudioFiles(directoryInfo.FullName); + var artist = _parsingService.GetArtist(files.First()); + + if (artist == null) + { + _logger.Debug("Unknown Artist {0}", cleanedUpName); + + return new List + { + UnknownArtistResult("Unknown Artist") + }; + } + + return ProcessFolder(directoryInfo, importMode, artist, downloadClientItem); + } + + private List ProcessFolder(DirectoryInfo directoryInfo, ImportMode importMode, Artist artist, DownloadClientItem downloadClientItem) + { + if (_artistService.ArtistPathExists(directoryInfo.FullName)) + { + _logger.Warn("Unable to process folder that is mapped to an existing artist"); + return new List(); + } + + var cleanedUpName = GetCleanedUpFolderName(directoryInfo.Name); + var folderInfo = Parser.Parser.ParseAlbumTitle(directoryInfo.Name); + var trackInfo = new ParsedTrackInfo { }; + + if (folderInfo != null) + { + _logger.Debug("{0} folder quality: {1}", cleanedUpName, folderInfo.Quality); + + trackInfo = new ParsedTrackInfo + { + AlbumTitle = folderInfo.AlbumTitle, + ArtistTitle = folderInfo.ArtistName, + Quality = folderInfo.Quality, + ReleaseGroup = folderInfo.ReleaseGroup, + ReleaseHash = folderInfo.ReleaseHash, + }; + } + + var audioFiles = _diskScanService.FilterFiles(directoryInfo.FullName, _diskScanService.GetAudioFiles(directoryInfo.FullName)); + + if (downloadClientItem == null) + { + foreach (var audioFile in audioFiles) + { + if (_diskProvider.IsFileLocked(audioFile)) + { + return new List + { + FileIsLockedResult(audioFile) + }; + } + } + } + + + + var decisions = _importDecisionMaker.GetImportDecisions(audioFiles.ToList(), artist, trackInfo); + var importResults = _importApprovedTracks.Import(decisions, true, downloadClientItem, importMode); + + if (importMode == ImportMode.Auto) + { + importMode = (downloadClientItem == null || downloadClientItem.CanMoveFiles) ? ImportMode.Move : ImportMode.Copy; + } + + if (importMode == ImportMode.Move && + importResults.Any(i => i.Result == ImportResultType.Imported) && + ShouldDeleteFolder(directoryInfo, artist)) + { + _logger.Debug("Deleting folder after importing valid files"); + _diskProvider.DeleteFolder(directoryInfo.FullName, true); + } + + return importResults; + } + + private List ProcessFile(FileInfo fileInfo, ImportMode importMode, DownloadClientItem downloadClientItem) + { + var artist = _parsingService.GetArtist(Path.GetFileNameWithoutExtension(fileInfo.Name)); + + if (artist == null) + { + _logger.Debug("Unknown Artist for file: {0}", fileInfo.Name); + + return new List + { + UnknownArtistResult(string.Format("Unknown Artist for file: {0}", fileInfo.Name), fileInfo.FullName) + }; + } + + return ProcessFile(fileInfo, importMode, artist, downloadClientItem); + } + + private List ProcessFile(FileInfo fileInfo, ImportMode importMode, Artist artist, DownloadClientItem downloadClientItem) + { + if (Path.GetFileNameWithoutExtension(fileInfo.Name).StartsWith("._")) + { + _logger.Debug("[{0}] starts with '._', skipping", fileInfo.FullName); + + return new List + { + new ImportResult(new ImportDecision(new LocalTrack { Path = fileInfo.FullName }, new Rejection("Invalid music file, filename starts with '._'")), "Invalid music file, filename starts with '._'") + }; + } + + if (downloadClientItem == null) + { + if (_diskProvider.IsFileLocked(fileInfo.FullName)) + { + return new List + { + FileIsLockedResult(fileInfo.FullName) + }; + } + } + + var decisions = _importDecisionMaker.GetImportDecisions(new List() { fileInfo.FullName }, artist, null); + + return _importApprovedTracks.Import(decisions, true, downloadClientItem, importMode); + } + + private string GetCleanedUpFolderName(string folder) + { + folder = folder.Replace("_UNPACK_", "") + .Replace("_FAILED_", ""); + + return folder; + } + + private ImportResult FileIsLockedResult(string audioFile) + { + _logger.Debug("[{0}] is currently locked by another process, skipping", audioFile); + return new ImportResult(new ImportDecision(new LocalTrack { Path = audioFile }, new Rejection("Locked file, try again later")), "Locked file, try again later"); + } + + private ImportResult UnknownArtistResult(string message, string audioFile = null) + { + var localTrack = audioFile == null ? null : new LocalTrack { Path = audioFile }; + + return new ImportResult(new ImportDecision(localTrack, new Rejection("Unknown Artist")), message); + } + } +} diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeFile.cs b/src/NzbDrone.Core/MediaFiles/EpisodeFile.cs deleted file mode 100644 index ecce449b4..000000000 --- a/src/NzbDrone.Core/MediaFiles/EpisodeFile.cs +++ /dev/null @@ -1,31 +0,0 @@ -using System; -using System.Collections.Generic; -using Marr.Data; -using NzbDrone.Core.Datastore; -using NzbDrone.Core.Qualities; -using NzbDrone.Core.Tv; -using NzbDrone.Core.MediaFiles.MediaInfo; - -namespace NzbDrone.Core.MediaFiles -{ - public class EpisodeFile : ModelBase - { - public int SeriesId { get; set; } - public int SeasonNumber { get; set; } - public string RelativePath { get; set; } - public string Path { get; set; } - public long Size { get; set; } - public DateTime DateAdded { get; set; } - public string SceneName { get; set; } - public string ReleaseGroup { get; set; } - public QualityModel Quality { get; set; } - public MediaInfoModel MediaInfo { get; set; } - public LazyLoaded> Episodes { get; set; } - public LazyLoaded Series { get; set; } - - public override string ToString() - { - return string.Format("[{0}] {1}", Id, RelativePath); - } - } -} \ No newline at end of file diff --git a/src/NzbDrone.Core/MediaFiles/Events/EpisodeDownloadedEvent.cs b/src/NzbDrone.Core/MediaFiles/Events/EpisodeDownloadedEvent.cs deleted file mode 100644 index af22b63fb..000000000 --- a/src/NzbDrone.Core/MediaFiles/Events/EpisodeDownloadedEvent.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System.Collections.Generic; -using NzbDrone.Common.Messaging; -using NzbDrone.Core.Parser.Model; - -namespace NzbDrone.Core.MediaFiles.Events -{ - public class EpisodeDownloadedEvent : IEvent - { - public LocalEpisode Episode { get; private set; } - public EpisodeFile EpisodeFile { get; private set; } - public List OldFiles { get; private set; } - - public EpisodeDownloadedEvent(LocalEpisode episode, EpisodeFile episodeFile, List oldFiles) - { - Episode = episode; - EpisodeFile = episodeFile; - OldFiles = oldFiles; - } - } -} \ No newline at end of file diff --git a/src/NzbDrone.Core/MediaFiles/Events/EpisodeFileAddedEvent.cs b/src/NzbDrone.Core/MediaFiles/Events/EpisodeFileAddedEvent.cs deleted file mode 100644 index 83ea2a908..000000000 --- a/src/NzbDrone.Core/MediaFiles/Events/EpisodeFileAddedEvent.cs +++ /dev/null @@ -1,14 +0,0 @@ -using NzbDrone.Common.Messaging; - -namespace NzbDrone.Core.MediaFiles.Events -{ - public class EpisodeFileAddedEvent : IEvent - { - public EpisodeFile EpisodeFile { get; private set; } - - public EpisodeFileAddedEvent(EpisodeFile episodeFile) - { - EpisodeFile = episodeFile; - } - } -} \ No newline at end of file diff --git a/src/NzbDrone.Core/MediaFiles/Events/EpisodeFileDeletedEvent.cs b/src/NzbDrone.Core/MediaFiles/Events/EpisodeFileDeletedEvent.cs deleted file mode 100644 index 2cbc177a2..000000000 --- a/src/NzbDrone.Core/MediaFiles/Events/EpisodeFileDeletedEvent.cs +++ /dev/null @@ -1,16 +0,0 @@ -using NzbDrone.Common.Messaging; - -namespace NzbDrone.Core.MediaFiles.Events -{ - public class EpisodeFileDeletedEvent : IEvent - { - public EpisodeFile EpisodeFile { get; private set; } - public DeleteMediaFileReason Reason { get; private set; } - - public EpisodeFileDeletedEvent(EpisodeFile episodeFile, DeleteMediaFileReason reason) - { - EpisodeFile = episodeFile; - Reason = reason; - } - } -} \ No newline at end of file diff --git a/src/NzbDrone.Core/MediaFiles/Events/EpisodeFolderCreatedEvent.cs b/src/NzbDrone.Core/MediaFiles/Events/EpisodeFolderCreatedEvent.cs deleted file mode 100644 index 126b21222..000000000 --- a/src/NzbDrone.Core/MediaFiles/Events/EpisodeFolderCreatedEvent.cs +++ /dev/null @@ -1,20 +0,0 @@ -using NzbDrone.Common.Messaging; -using NzbDrone.Core.Tv; - -namespace NzbDrone.Core.MediaFiles.Events -{ - public class EpisodeFolderCreatedEvent : IEvent - { - public Series Series { get; private set; } - public EpisodeFile EpisodeFile { get; private set; } - public string SeriesFolder { get; set; } - public string SeasonFolder { get; set; } - public string EpisodeFolder { get; set; } - - public EpisodeFolderCreatedEvent(Series series, EpisodeFile episodeFile) - { - Series = series; - EpisodeFile = episodeFile; - } - } -} diff --git a/src/NzbDrone.Core/MediaFiles/Events/EpisodeImportedEvent.cs b/src/NzbDrone.Core/MediaFiles/Events/EpisodeImportedEvent.cs deleted file mode 100644 index 518132857..000000000 --- a/src/NzbDrone.Core/MediaFiles/Events/EpisodeImportedEvent.cs +++ /dev/null @@ -1,32 +0,0 @@ -using NzbDrone.Common.Messaging; -using NzbDrone.Core.Parser.Model; - -namespace NzbDrone.Core.MediaFiles.Events -{ - public class EpisodeImportedEvent : IEvent - { - public LocalEpisode EpisodeInfo { get; private set; } - public EpisodeFile ImportedEpisode { get; private set; } - public bool NewDownload { get; private set; } - public string DownloadClient { get; private set; } - public string DownloadId { get; private set; } - public bool IsReadOnly { get; set; } - - public EpisodeImportedEvent(LocalEpisode episodeInfo, EpisodeFile importedEpisode, bool newDownload) - { - EpisodeInfo = episodeInfo; - ImportedEpisode = importedEpisode; - NewDownload = newDownload; - } - - public EpisodeImportedEvent(LocalEpisode episodeInfo, EpisodeFile importedEpisode, bool newDownload, string downloadClient, string downloadId, bool isReadOnly) - { - EpisodeInfo = episodeInfo; - ImportedEpisode = importedEpisode; - NewDownload = newDownload; - DownloadClient = downloadClient; - DownloadId = downloadId; - IsReadOnly = isReadOnly; - } - } -} \ No newline at end of file diff --git a/src/NzbDrone.Core/MediaFiles/Events/SeriesRenamedEvent.cs b/src/NzbDrone.Core/MediaFiles/Events/SeriesRenamedEvent.cs deleted file mode 100644 index 8cfe96b89..000000000 --- a/src/NzbDrone.Core/MediaFiles/Events/SeriesRenamedEvent.cs +++ /dev/null @@ -1,15 +0,0 @@ -using NzbDrone.Common.Messaging; -using NzbDrone.Core.Tv; - -namespace NzbDrone.Core.MediaFiles.Events -{ - public class SeriesRenamedEvent : IEvent - { - public Series Series { get; private set; } - - public SeriesRenamedEvent(Series series) - { - Series = series; - } - } -} \ No newline at end of file diff --git a/src/NzbDrone.Core/MediaFiles/Events/SeriesScanSkippedEvent.cs b/src/NzbDrone.Core/MediaFiles/Events/SeriesScanSkippedEvent.cs deleted file mode 100644 index 765207bd5..000000000 --- a/src/NzbDrone.Core/MediaFiles/Events/SeriesScanSkippedEvent.cs +++ /dev/null @@ -1,23 +0,0 @@ -using NzbDrone.Common.Messaging; -using NzbDrone.Core.Tv; - -namespace NzbDrone.Core.MediaFiles.Events -{ - public class SeriesScanSkippedEvent : IEvent - { - public Series Series { get; private set; } - public SeriesScanSkippedReason Reason { get; set; } - - public SeriesScanSkippedEvent(Series series, SeriesScanSkippedReason reason) - { - Series = series; - Reason = reason; - } - } - - public enum SeriesScanSkippedReason - { - RootFolderDoesNotExist, - RootFolderIsEmpty - } -} diff --git a/src/NzbDrone.Core/MediaFiles/Events/SeriesScannedEvent.cs b/src/NzbDrone.Core/MediaFiles/Events/SeriesScannedEvent.cs deleted file mode 100644 index f82de5214..000000000 --- a/src/NzbDrone.Core/MediaFiles/Events/SeriesScannedEvent.cs +++ /dev/null @@ -1,15 +0,0 @@ -using NzbDrone.Common.Messaging; -using NzbDrone.Core.Tv; - -namespace NzbDrone.Core.MediaFiles.Events -{ - public class SeriesScannedEvent : IEvent - { - public Series Series { get; private set; } - - public SeriesScannedEvent(Series series) - { - Series = series; - } - } -} \ No newline at end of file diff --git a/src/NzbDrone.Core/MediaFiles/Events/TrackDownloadedEvent.cs b/src/NzbDrone.Core/MediaFiles/Events/TrackDownloadedEvent.cs deleted file mode 100644 index 82ea9f788..000000000 --- a/src/NzbDrone.Core/MediaFiles/Events/TrackDownloadedEvent.cs +++ /dev/null @@ -1,23 +0,0 @@ -using NzbDrone.Common.Messaging; -using NzbDrone.Core.Parser.Model; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; - -namespace NzbDrone.Core.MediaFiles.Events -{ - public class TrackDownloadedEvent : IEvent - { - public LocalTrack Track { get; private set; } - public TrackFile TrackFile { get; private set; } - public List OldFiles { get; private set; } - - public TrackDownloadedEvent(LocalTrack track, TrackFile trackFile, List oldFiles) - { - Track = track; - TrackFile = trackFile; - OldFiles = oldFiles; - } - } -} diff --git a/src/NzbDrone.Core/MediaFiles/Events/TrackFileRenamedEvent.cs b/src/NzbDrone.Core/MediaFiles/Events/TrackFileRenamedEvent.cs new file mode 100644 index 000000000..a4c1847fc --- /dev/null +++ b/src/NzbDrone.Core/MediaFiles/Events/TrackFileRenamedEvent.cs @@ -0,0 +1,19 @@ +using NzbDrone.Common.Messaging; +using NzbDrone.Core.Music; + +namespace NzbDrone.Core.MediaFiles.Events +{ + public class TrackFileRenamedEvent : IEvent + { + public Artist Artist { get; private set; } + public TrackFile TrackFile { get; private set; } + public string OriginalPath { get; private set; } + + public TrackFileRenamedEvent(Artist artist, TrackFile trackFile, string originalPath) + { + Artist = artist; + TrackFile = trackFile; + OriginalPath = originalPath; + } + } +} diff --git a/src/NzbDrone.Core/MediaFiles/Events/TrackImportedEvent.cs b/src/NzbDrone.Core/MediaFiles/Events/TrackImportedEvent.cs index 812b2ae78..eeb00398b 100644 --- a/src/NzbDrone.Core/MediaFiles/Events/TrackImportedEvent.cs +++ b/src/NzbDrone.Core/MediaFiles/Events/TrackImportedEvent.cs @@ -1,9 +1,7 @@ -using NzbDrone.Common.Messaging; -using NzbDrone.Core.Parser.Model; -using System; using System.Collections.Generic; -using System.Linq; -using System.Text; +using NzbDrone.Common.Messaging; +using NzbDrone.Core.Download; +using NzbDrone.Core.Parser.Model; namespace NzbDrone.Core.MediaFiles.Events { @@ -11,26 +9,24 @@ namespace NzbDrone.Core.MediaFiles.Events { public LocalTrack TrackInfo { get; private set; } public TrackFile ImportedTrack { get; private set; } + public List OldFiles { get; private set; } public bool NewDownload { get; private set; } public string DownloadClient { get; private set; } public string DownloadId { get; private set; } - public bool IsReadOnly { get; set; } - public TrackImportedEvent(LocalTrack trackInfo, TrackFile importedTrack, bool newDownload) + public TrackImportedEvent(LocalTrack trackInfo, TrackFile importedTrack, List oldFiles, bool newDownload, DownloadClientItem downloadClientItem) { TrackInfo = trackInfo; ImportedTrack = importedTrack; + OldFiles = oldFiles; NewDownload = newDownload; - } - public TrackImportedEvent(LocalTrack trackInfo, TrackFile importedTrack, bool newDownload, string downloadClient, string downloadId, bool isReadOnly) - { - TrackInfo = trackInfo; - ImportedTrack = importedTrack; - NewDownload = newDownload; - DownloadClient = downloadClient; - DownloadId = downloadId; - IsReadOnly = isReadOnly; + if (downloadClientItem != null) + { + DownloadClient = downloadClientItem.DownloadClient; + DownloadId = downloadClientItem.DownloadId; + } + } } } diff --git a/src/NzbDrone.Core/MediaFiles/MediaFileDeletionService.cs b/src/NzbDrone.Core/MediaFiles/MediaFileDeletionService.cs new file mode 100644 index 000000000..e5d274020 --- /dev/null +++ b/src/NzbDrone.Core/MediaFiles/MediaFileDeletionService.cs @@ -0,0 +1,84 @@ +using System; +using System.IO; +using System.Net; +using NLog; +using NzbDrone.Common.Disk; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.Exceptions; +using NzbDrone.Core.Messaging.Events; +using NzbDrone.Core.Music; +using NzbDrone.Core.Music.Events; + +namespace NzbDrone.Core.MediaFiles +{ + public interface IDeleteMediaFiles + { + void DeleteTrackFile(Artist artist, TrackFile trackFile); + } + + public class MediaFileDeletionService : IDeleteMediaFiles, IHandleAsync + { + private readonly IDiskProvider _diskProvider; + private readonly IRecycleBinProvider _recycleBinProvider; + private readonly IMediaFileService _mediaFileService; + private readonly Logger _logger; + + public MediaFileDeletionService(IDiskProvider diskProvider, + IRecycleBinProvider recycleBinProvider, + IMediaFileService mediaFileService, + Logger logger) + { + _diskProvider = diskProvider; + _recycleBinProvider = recycleBinProvider; + _mediaFileService = mediaFileService; + _logger = logger; + } + + public void DeleteTrackFile(Artist artist, TrackFile trackFile) + { + var fullPath = Path.Combine(artist.Path, trackFile.RelativePath); + var rootFolder = _diskProvider.GetParentFolder(artist.Path); + + if (!_diskProvider.FolderExists(rootFolder)) + { + throw new NzbDroneClientException(HttpStatusCode.Conflict, "Artist's root folder ({0}) doesn't exist.", rootFolder); + } + + if (_diskProvider.GetDirectories(rootFolder).Empty()) + { + throw new NzbDroneClientException(HttpStatusCode.Conflict, "Artist's root folder ({0}) is empty.", rootFolder); + } + + if (_diskProvider.FolderExists(artist.Path) && _diskProvider.FileExists(fullPath)) + { + _logger.Info("Deleting track file: {0}", fullPath); + + var subfolder = _diskProvider.GetParentFolder(artist.Path).GetRelativePath(_diskProvider.GetParentFolder(fullPath)); + + try + { + _recycleBinProvider.DeleteFile(fullPath, subfolder); + } + catch (Exception e) + { + _logger.Error(e, "Unable to delete track file"); + throw new NzbDroneClientException(HttpStatusCode.InternalServerError, "Unable to delete track file"); + } + } + + // Delete the track file from the database to clean it up even if the file was already deleted + _mediaFileService.Delete(trackFile, DeleteMediaFileReason.Manual); + } + + public void HandleAsync(ArtistDeletedEvent message) + { + if (message.DeleteFiles) + { + if (_diskProvider.FolderExists(message.Artist.Path)) + { + _recycleBinProvider.DeleteFolder(message.Artist.Path); + } + } + } + } +} diff --git a/src/NzbDrone.Core/MediaFiles/MediaFileExtensions.cs b/src/NzbDrone.Core/MediaFiles/MediaFileExtensions.cs index 20944d36d..a2ec33ef1 100644 --- a/src/NzbDrone.Core/MediaFiles/MediaFileExtensions.cs +++ b/src/NzbDrone.Core/MediaFiles/MediaFileExtensions.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using NzbDrone.Core.Qualities; @@ -10,7 +11,7 @@ namespace NzbDrone.Core.MediaFiles static MediaFileExtensions() { - _fileExtensions = new Dictionary + _fileExtensions = new Dictionary(StringComparer.OrdinalIgnoreCase) { { ".mp3", Quality.Unknown }, { ".m4a", Quality.Unknown }, @@ -19,7 +20,7 @@ namespace NzbDrone.Core.MediaFiles }; } - public static HashSet Extensions => new HashSet(_fileExtensions.Keys); + public static HashSet Extensions => new HashSet(_fileExtensions.Keys, StringComparer.OrdinalIgnoreCase); public static Quality GetQualityForExtension(string extension) { diff --git a/src/NzbDrone.Core/MediaFiles/MediaFileService.cs b/src/NzbDrone.Core/MediaFiles/MediaFileService.cs index e485344ca..d7b52422e 100644 --- a/src/NzbDrone.Core/MediaFiles/MediaFileService.cs +++ b/src/NzbDrone.Core/MediaFiles/MediaFileService.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.IO; using System.Linq; using NLog; @@ -15,9 +15,11 @@ namespace NzbDrone.Core.MediaFiles { TrackFile Add(TrackFile trackFile); void Update(TrackFile trackFile); + void Update(List trackFile); void Delete(TrackFile trackFile, DeleteMediaFileReason reason); List GetFilesByArtist(int artistId); List GetFilesByAlbum(int artistId, int albumId); + List GetFiles(IEnumerable ids); List GetFilesWithoutMediaInfo(); List FilterExistingFiles(List files, Artist artist); TrackFile Get(int id); @@ -50,6 +52,12 @@ namespace NzbDrone.Core.MediaFiles _mediaFileRepository.Update(trackFile); } + public void Update(List trackFiles) + { + _mediaFileRepository.UpdateMany(trackFiles); + } + + public void Delete(TrackFile trackFile, DeleteMediaFileReason reason) { //Little hack so we have the tracks and artist attached for the event consumers @@ -60,6 +68,11 @@ namespace NzbDrone.Core.MediaFiles _eventAggregator.PublishEvent(new TrackFileDeletedEvent(trackFile, reason)); } + public List GetFiles(IEnumerable ids) + { + return _mediaFileRepository.Get(ids).ToList(); + } + public List GetFilesWithoutMediaInfo() { @@ -101,4 +114,4 @@ namespace NzbDrone.Core.MediaFiles return _mediaFileRepository.GetFilesByAlbum(albumId); } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Core/MediaFiles/MediaFileTableCleanupService.cs b/src/NzbDrone.Core/MediaFiles/MediaFileTableCleanupService.cs index 70cc1fdde..39b3ee700 100644 --- a/src/NzbDrone.Core/MediaFiles/MediaFileTableCleanupService.cs +++ b/src/NzbDrone.Core/MediaFiles/MediaFileTableCleanupService.cs @@ -1,10 +1,9 @@ -using System; +using System; using System.Collections.Generic; using System.IO; using NLog; using NzbDrone.Common; using NzbDrone.Common.Extensions; -using NzbDrone.Core.Tv; using NzbDrone.Core.Music; namespace NzbDrone.Core.MediaFiles @@ -77,4 +76,4 @@ namespace NzbDrone.Core.MediaFiles } } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Core/MediaFiles/MediaInfo/MediaInfoFormatter.cs b/src/NzbDrone.Core/MediaFiles/MediaInfo/MediaInfoFormatter.cs index 7efd2cb73..8804768dc 100644 --- a/src/NzbDrone.Core/MediaFiles/MediaInfo/MediaInfoFormatter.cs +++ b/src/NzbDrone.Core/MediaFiles/MediaInfo/MediaInfoFormatter.cs @@ -35,8 +35,14 @@ namespace NzbDrone.Core.MediaFiles.MediaInfo return mediaInfo.AudioChannelPositionsText.ContainsIgnoreCase("LFE") ? audioChannels - 1 + 0.1m : audioChannels; } + if (audioChannelPositions.Contains("+")) + { + return audioChannelPositions.Split('+') + .Sum(s => decimal.Parse(s.Trim(), CultureInfo.InvariantCulture)); + } + return audioChannelPositions.Replace("Object Based / ", "") - .Split(new [] { " / " }, StringSplitOptions.None) + .Split(new[] { " / " }, StringSplitOptions.RemoveEmptyEntries) .First() .Split('/') .Sum(s => decimal.Parse(s, CultureInfo.InvariantCulture)); diff --git a/src/NzbDrone.Core/MediaFiles/RecycleBinProvider.cs b/src/NzbDrone.Core/MediaFiles/RecycleBinProvider.cs index 540164a7c..fabb4d3fd 100644 --- a/src/NzbDrone.Core/MediaFiles/RecycleBinProvider.cs +++ b/src/NzbDrone.Core/MediaFiles/RecycleBinProvider.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.IO; using NLog; using NzbDrone.Common.Disk; @@ -8,7 +8,7 @@ using NzbDrone.Core.Configuration; using NzbDrone.Core.MediaFiles.Commands; using NzbDrone.Core.Messaging.Commands; using NzbDrone.Core.Messaging.Events; -using NzbDrone.Core.Tv.Events; +using NzbDrone.Core.Music.Events; namespace NzbDrone.Core.MediaFiles { @@ -20,7 +20,7 @@ namespace NzbDrone.Core.MediaFiles void Cleanup(); } - public class RecycleBinProvider : IHandleAsync, IExecute, IRecycleBinProvider + public class RecycleBinProvider : IExecute, IRecycleBinProvider { private readonly IDiskTransferService _diskTransferService; private readonly IDiskProvider _diskProvider; @@ -62,11 +62,7 @@ namespace NzbDrone.Core.MediaFiles _diskProvider.FolderSetLastWriteTime(destination, DateTime.UtcNow); foreach (var file in _diskProvider.GetFiles(destination, SearchOption.AllDirectories)) { - if (OsInfo.IsWindows) - { - //TODO: Better fix than this for non-Windows? - _diskProvider.FileSetLastWriteTime(file, DateTime.UtcNow); - } + SetLastWriteTime(file, DateTime.UtcNow); } _logger.Debug("Folder has been moved to the recycling bin: {0}", destination); @@ -123,12 +119,8 @@ namespace NzbDrone.Core.MediaFiles _logger.Error(e, "Unable to move '{0}' to the recycling bin: '{1}'", path, destination); throw; } - - //TODO: Better fix than this for non-Windows? - if (OsInfo.IsWindows) - { - _diskProvider.FileSetLastWriteTime(destination, DateTime.UtcNow); - } + + SetLastWriteTime(destination, DateTime.UtcNow); _logger.Debug("File has been moved to the recycling bin: {0}", destination); } @@ -192,14 +184,15 @@ namespace NzbDrone.Core.MediaFiles _logger.Debug("Recycling Bin has been cleaned up."); } - public void HandleAsync(SeriesDeletedEvent message) + private void SetLastWriteTime(string file, DateTime dateTime) { - if (message.DeleteFiles) + // Swallow any IOException that may be thrown due to "Invalid parameter" + try + { + _diskProvider.FileSetLastWriteTime(file, dateTime); + } + catch (IOException) { - if (_diskProvider.FolderExists(message.Series.Path)) - { - DeleteFolder(message.Series.Path); - } } } diff --git a/src/NzbDrone.Core/MediaFiles/RenameTrackFileService.cs b/src/NzbDrone.Core/MediaFiles/RenameTrackFileService.cs index bea565262..0a59441e2 100644 --- a/src/NzbDrone.Core/MediaFiles/RenameTrackFileService.cs +++ b/src/NzbDrone.Core/MediaFiles/RenameTrackFileService.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.IO; using System.Linq; @@ -71,7 +71,7 @@ namespace NzbDrone.Core.MediaFiles { var artist = _artistService.GetArtist(artistId); - var tracks = _trackService.GetTracksByAlbum(artistId, albumId); + var tracks = _trackService.GetTracksByAlbum(albumId); var files = _mediaFileService.GetFilesByAlbum(artistId, albumId); return GetPreviews(artist, tracks, files) @@ -129,6 +129,8 @@ namespace NzbDrone.Core.MediaFiles renamed.Add(trackFile); _logger.Debug("Renamed track file: {0}", trackFile); + + _eventAggregator.PublishEvent(new TrackFileRenamedEvent(artist, trackFile, trackFilePath)); } catch (SameFilenameException ex) { diff --git a/src/NzbDrone.Core/MediaFiles/TrackFile.cs b/src/NzbDrone.Core/MediaFiles/TrackFile.cs index 982089ef0..7a861a932 100644 --- a/src/NzbDrone.Core/MediaFiles/TrackFile.cs +++ b/src/NzbDrone.Core/MediaFiles/TrackFile.cs @@ -1,4 +1,4 @@ -using Marr.Data; +using Marr.Data; using NzbDrone.Core.Datastore; using NzbDrone.Core.MediaFiles.MediaInfo; using NzbDrone.Core.Music; @@ -7,6 +7,7 @@ using System; using System.Collections.Generic; using System.Linq; using System.Text; +using NzbDrone.Core.Languages; namespace NzbDrone.Core.MediaFiles { @@ -27,6 +28,7 @@ namespace NzbDrone.Core.MediaFiles //public LazyLoaded> Episodes { get; set; } public LazyLoaded Artist { get; set; } public LazyLoaded> Tracks { get; set; } + public Language Language { get; set; } public override string ToString() { diff --git a/src/NzbDrone.Core/MediaFiles/TrackFileMovingService.cs b/src/NzbDrone.Core/MediaFiles/TrackFileMovingService.cs index 2107fcd79..70f9a2b6b 100644 --- a/src/NzbDrone.Core/MediaFiles/TrackFileMovingService.cs +++ b/src/NzbDrone.Core/MediaFiles/TrackFileMovingService.cs @@ -1,4 +1,5 @@ using NLog; +using NzbDrone.Common; using NzbDrone.Common.Disk; using NzbDrone.Common.EnsureThat; using NzbDrone.Common.Extensions; @@ -8,7 +9,6 @@ using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Music; using NzbDrone.Core.Organizer; using NzbDrone.Core.Parser.Model; -using NzbDrone.Core.Tv; using System; using System.Collections.Generic; using System.IO; @@ -166,7 +166,7 @@ namespace NzbDrone.Core.MediaFiles if (!_diskProvider.FolderExists(rootFolder)) { - throw new DirectoryNotFoundException(string.Format("Root folder '{0}' was not found.", rootFolder)); + throw new TrackImport.RootFolderNotFoundException(string.Format("Root folder '{0}' was not found.", rootFolder)); } var changed = false; diff --git a/src/NzbDrone.Core/MediaFiles/TrackImport/DetectSample.cs b/src/NzbDrone.Core/MediaFiles/TrackImport/DetectSample.cs deleted file mode 100644 index d93839701..000000000 --- a/src/NzbDrone.Core/MediaFiles/TrackImport/DetectSample.cs +++ /dev/null @@ -1,119 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using NLog; -using NzbDrone.Core.MediaFiles.MediaInfo; -using NzbDrone.Core.Qualities; -using NzbDrone.Core.Tv; - -namespace NzbDrone.Core.MediaFiles.TrackImport -{ - public interface IDetectSample - { - bool IsSample(Series series, QualityModel quality, string path, long size, bool isSpecial); - } - - public class DetectSample : IDetectSample - { - private readonly IVideoFileInfoReader _videoFileInfoReader; - private readonly Logger _logger; - - private static List _largeSampleSizeQualities = new List { Quality.FLAC }; - - public DetectSample(IVideoFileInfoReader videoFileInfoReader, Logger logger) - { - _videoFileInfoReader = videoFileInfoReader; - _logger = logger; - } - - public static long SampleSizeLimit => 70.Megabytes(); - - public bool IsSample(Series series, QualityModel quality, string path, long size, bool isSpecial) - { - if (isSpecial) - { - _logger.Debug("Special, skipping sample check"); - return false; - } - - var extension = Path.GetExtension(path); - - if (extension != null && extension.Equals(".flv", StringComparison.InvariantCultureIgnoreCase)) - { - _logger.Debug("Skipping sample check for .flv file"); - return false; - } - - if (extension != null && extension.Equals(".strm", StringComparison.InvariantCultureIgnoreCase)) - { - _logger.Debug("Skipping sample check for .strm file"); - return false; - } - - try - { - var runTime = _videoFileInfoReader.GetRunTime(path); - var minimumRuntime = GetMinimumAllowedRuntime(series); - - if (runTime.TotalMinutes.Equals(0)) - { - _logger.Error("[{0}] has a runtime of 0, is it a valid video file?", path); - return true; - } - - if (runTime.TotalSeconds < minimumRuntime) - { - _logger.Debug("[{0}] appears to be a sample. Runtime: {1} seconds. Expected at least: {2} seconds", path, runTime, minimumRuntime); - return true; - } - } - - catch (DllNotFoundException) - { - _logger.Debug("Falling back to file size detection"); - - return CheckSize(size, quality); - } - - _logger.Debug("Runtime is over 90 seconds"); - return false; - } - - private bool CheckSize(long size, QualityModel quality) - { - { - if (size < SampleSizeLimit * 2) - { - _logger.Debug("1080p file is less than sample limit"); - return true; - } - } - - if (size < SampleSizeLimit) - { - _logger.Debug("File is less than sample limit"); - return true; - } - - return false; - } - - private int GetMinimumAllowedRuntime(Series series) - { - //Webisodes - 90 seconds - if (series.Runtime <= 10) - { - return 90; - } - - //30 minute episodes - 5 minutes - if (series.Runtime <= 30) - { - return 300; - } - - //60 minute episodes - 10 minutes - return 600; - } - } -} diff --git a/src/NzbDrone.Core/MediaFiles/TrackImport/ImportApprovedEpisodes.cs b/src/NzbDrone.Core/MediaFiles/TrackImport/ImportApprovedEpisodes.cs deleted file mode 100644 index 685df77d5..000000000 --- a/src/NzbDrone.Core/MediaFiles/TrackImport/ImportApprovedEpisodes.cs +++ /dev/null @@ -1,77 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using NLog; -using NzbDrone.Common.Disk; -using NzbDrone.Common.Extensions; -using NzbDrone.Core.MediaFiles.Events; -using NzbDrone.Core.Messaging.Events; -using NzbDrone.Core.Parser; -using NzbDrone.Core.Parser.Model; -using NzbDrone.Core.Qualities; -using NzbDrone.Core.Download; -using NzbDrone.Core.Extras; - -namespace NzbDrone.Core.MediaFiles.TrackImport -{ - [Obsolete("Used by Sonarr, not by Lidarr")] - public interface IImportApprovedEpisodes - { - List Import(List decisions, bool newDownload, DownloadClientItem downloadClientItem = null, ImportMode importMode = ImportMode.Auto); - } - - public class ImportApprovedEpisodes : IImportApprovedEpisodes - { - private readonly IUpgradeMediaFiles _episodeFileUpgrader; - private readonly IMediaFileService _mediaFileService; - private readonly IExtraService _extraService; - private readonly IDiskProvider _diskProvider; - private readonly IEventAggregator _eventAggregator; - private readonly Logger _logger; - - public ImportApprovedEpisodes(IUpgradeMediaFiles episodeFileUpgrader, - IMediaFileService mediaFileService, - IExtraService extraService, - IDiskProvider diskProvider, - IEventAggregator eventAggregator, - Logger logger) - { - _episodeFileUpgrader = episodeFileUpgrader; - _mediaFileService = mediaFileService; - _extraService = extraService; - _diskProvider = diskProvider; - _eventAggregator = eventAggregator; - _logger = logger; - } - - public List Import(List decisions, bool newDownload, DownloadClientItem downloadClientItem = null, ImportMode importMode = ImportMode.Auto) - { - throw new NotImplementedException("This will be removed"); - } - - private string GetSceneName(DownloadClientItem downloadClientItem, LocalEpisode localEpisode) - { - if (downloadClientItem != null) - { - var title = Parser.Parser.RemoveFileExtension(downloadClientItem.Title); - - var parsedTitle = Parser.Parser.ParseTitle(title); - - if (parsedTitle != null && !parsedTitle.FullSeason) - { - return title; - } - } - - var fileName = Path.GetFileNameWithoutExtension(localEpisode.Path.CleanFilePath()); - - if (SceneChecker.IsSceneTitle(fileName)) - { - return fileName; - } - - return null; - } - } -} diff --git a/src/NzbDrone.Core/MediaFiles/TrackImport/ImportApprovedTracks.cs b/src/NzbDrone.Core/MediaFiles/TrackImport/ImportApprovedTracks.cs index e652e09e7..301ffe40b 100644 --- a/src/NzbDrone.Core/MediaFiles/TrackImport/ImportApprovedTracks.cs +++ b/src/NzbDrone.Core/MediaFiles/TrackImport/ImportApprovedTracks.cs @@ -11,6 +11,7 @@ using System.Collections.Generic; using System.Linq; using System.Text; using NzbDrone.Core.Music; +using NzbDrone.Core.Languages; namespace NzbDrone.Core.MediaFiles.TrackImport { @@ -51,6 +52,7 @@ namespace NzbDrone.Core.MediaFiles.TrackImport var qualifiedImports = decisions.Where(c => c.Approved) .GroupBy(c => c.LocalTrack.Artist.Id, (i, s) => s .OrderByDescending(c => c.LocalTrack.Quality, new QualityModelComparer(s.First().LocalTrack.Artist.Profile)) + .ThenByDescending(c => c.LocalTrack.Language, new LanguageComparer(s.First().LocalTrack.Artist.LanguageProfile)) .ThenByDescending(c => c.LocalTrack.Size)) .SelectMany(c => c) .ToList(); @@ -82,16 +84,17 @@ namespace NzbDrone.Core.MediaFiles.TrackImport trackFile.Size = _diskProvider.GetFileSize(localTrack.Path); trackFile.Quality = localTrack.Quality; trackFile.MediaInfo = localTrack.MediaInfo; - trackFile.AlbumId = _albumRepository.FindByArtistAndName(localTrack.Artist.Name, Parser.Parser.CleanArtistTitle(localTrack.ParsedTrackInfo.AlbumTitle)).Id; + trackFile.AlbumId = localTrack.Album.Id; trackFile.ReleaseGroup = localTrack.ParsedTrackInfo.ReleaseGroup; trackFile.Tracks = localTrack.Tracks; + trackFile.Language = localTrack.Language; bool copyOnly; switch (importMode) { default: case ImportMode.Auto: - copyOnly = downloadClientItem != null && downloadClientItem.IsReadOnly; + copyOnly = downloadClientItem != null && !downloadClientItem.CanMoveFiles; break; case ImportMode.Move: copyOnly = false; @@ -121,19 +124,18 @@ namespace NzbDrone.Core.MediaFiles.TrackImport // _extraService.ImportExtraFiles(localTrack, trackFile, copyOnly); // TODO: Import Music Extras //} - if (downloadClientItem != null) - { - _eventAggregator.PublishEvent(new TrackImportedEvent(localTrack, trackFile, newDownload, downloadClientItem.DownloadClient, downloadClientItem.DownloadId, downloadClientItem.IsReadOnly)); - } - else - { - _eventAggregator.PublishEvent(new TrackImportedEvent(localTrack, trackFile, newDownload)); - } + _eventAggregator.PublishEvent(new TrackImportedEvent(localTrack, trackFile, oldFiles, newDownload, downloadClientItem)); - if (newDownload) - { - _eventAggregator.PublishEvent(new TrackDownloadedEvent(localTrack, trackFile, oldFiles)); - } + } + catch (RootFolderNotFoundException e) + { + _logger.Warn(e, "Couldn't import track " + localTrack); + importResults.Add(new ImportResult(importDecision, "Failed to import track, Root folder missing.")); + } + catch (DestinationAlreadyExistsException e) + { + _logger.Warn(e, "Couldn't import track " + localTrack); + importResults.Add(new ImportResult(importDecision, "Failed to import track, Destination already exists.")); } catch (Exception e) { diff --git a/src/NzbDrone.Core/MediaFiles/TrackImport/ImportDecisionMaker.cs b/src/NzbDrone.Core/MediaFiles/TrackImport/ImportDecisionMaker.cs index 8fa2b2f3f..30e916bd4 100644 --- a/src/NzbDrone.Core/MediaFiles/TrackImport/ImportDecisionMaker.cs +++ b/src/NzbDrone.Core/MediaFiles/TrackImport/ImportDecisionMaker.cs @@ -11,6 +11,7 @@ using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Qualities; using NzbDrone.Core.MediaFiles.MediaInfo; using NzbDrone.Core.Music; +using NzbDrone.Core.Languages; namespace NzbDrone.Core.MediaFiles.TrackImport { @@ -27,7 +28,6 @@ namespace NzbDrone.Core.MediaFiles.TrackImport private readonly IMediaFileService _mediaFileService; private readonly IDiskProvider _diskProvider; private readonly IVideoFileInfoReader _videoFileInfoReader; - private readonly IDetectSample _detectSample; private readonly Logger _logger; public ImportDecisionMaker(IEnumerable specifications, @@ -35,7 +35,6 @@ namespace NzbDrone.Core.MediaFiles.TrackImport IMediaFileService mediaFileService, IDiskProvider diskProvider, IVideoFileInfoReader videoFileInfoReader, - IDetectSample detectSample, Logger logger) { _specifications = specifications; @@ -43,7 +42,6 @@ namespace NzbDrone.Core.MediaFiles.TrackImport _mediaFileService = mediaFileService; _diskProvider = diskProvider; _videoFileInfoReader = videoFileInfoReader; - _detectSample = detectSample; _logger = logger; } @@ -80,6 +78,7 @@ namespace NzbDrone.Core.MediaFiles.TrackImport if (localTrack != null) { localTrack.Quality = GetQuality(folderInfo, localTrack.Quality, artist); + localTrack.Language = GetLanguage(folderInfo, localTrack.Language, artist); localTrack.Size = _diskProvider.GetFileSize(file); _logger.Debug("Size: {0}", localTrack.Size); @@ -112,6 +111,19 @@ namespace NzbDrone.Core.MediaFiles.TrackImport decision = new ImportDecision(localTrack, new Rejection("Unexpected error processing file")); } + if (decision == null) + { + _logger.Error("Unable to make a decision on {0}", file); + } + else if (decision.Rejections.Any()) + { + _logger.Debug("File rejected for the following reasons: {0}", string.Join(", ", decision.Rejections)); + } + else + { + _logger.Debug("File accepted"); + } + return decision; } @@ -188,6 +200,37 @@ namespace NzbDrone.Core.MediaFiles.TrackImport return fileQuality; } + private Language GetLanguage(ParsedTrackInfo folderInfo, Language fileLanguage, Artist artist) + { + if (UseFolderLanguage(folderInfo, fileLanguage, artist)) + { + _logger.Debug("Using language from folder: {0}", folderInfo.Language); + return folderInfo.Language; + } + + return fileLanguage; + } + + private bool UseFolderLanguage(ParsedTrackInfo folderInfo, Language fileLanguage, Artist artist) + { + if (folderInfo == null) + { + return false; + } + + if (folderInfo.Language == Language.Unknown) + { + return false; + } + + if (new LanguageComparer(artist.LanguageProfile).Compare(folderInfo.Language, fileLanguage) > 0) + { + return true; + } + + return false; + } + private bool UseFolderQuality(ParsedTrackInfo folderInfo, QualityModel fileQuality, Artist artist) { if (folderInfo == null) diff --git a/src/NzbDrone.Core/MediaFiles/TrackImport/Manual/ManualImportFile.cs b/src/NzbDrone.Core/MediaFiles/TrackImport/Manual/ManualImportFile.cs index 44eb491c3..ed784afbd 100644 --- a/src/NzbDrone.Core/MediaFiles/TrackImport/Manual/ManualImportFile.cs +++ b/src/NzbDrone.Core/MediaFiles/TrackImport/Manual/ManualImportFile.cs @@ -1,14 +1,17 @@ using System.Collections.Generic; using NzbDrone.Core.Qualities; +using NzbDrone.Core.Languages; namespace NzbDrone.Core.MediaFiles.TrackImport.Manual { public class ManualImportFile { public string Path { get; set; } - public int SeriesId { get; set; } - public List EpisodeIds { get; set; } + public int ArtistId { get; set; } + public int AlbumId { get; set; } + public List TrackIds { get; set; } public QualityModel Quality { get; set; } + public Language Language { get; set; } public string DownloadId { get; set; } } } diff --git a/src/NzbDrone.Core/MediaFiles/TrackImport/Manual/ManualImportItem.cs b/src/NzbDrone.Core/MediaFiles/TrackImport/Manual/ManualImportItem.cs index 9d335f258..2a5ed6b02 100644 --- a/src/NzbDrone.Core/MediaFiles/TrackImport/Manual/ManualImportItem.cs +++ b/src/NzbDrone.Core/MediaFiles/TrackImport/Manual/ManualImportItem.cs @@ -1,7 +1,8 @@ using System.Collections.Generic; using NzbDrone.Core.DecisionEngine; using NzbDrone.Core.Qualities; -using NzbDrone.Core.Tv; +using NzbDrone.Core.Languages; +using NzbDrone.Core.Music; namespace NzbDrone.Core.MediaFiles.TrackImport.Manual { @@ -11,10 +12,11 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Manual public string RelativePath { get; set; } public string Name { get; set; } public long Size { get; set; } - public Series Series { get; set; } - public int? SeasonNumber { get; set; } - public List Episodes { get; set; } + public Artist Artist { get; set; } + public Album Album { get; set; } + public List Tracks { get; set; } public QualityModel Quality { get; set; } + public Language Language { get; set; } public string DownloadId { get; set; } public IEnumerable Rejections { get; set; } } diff --git a/src/NzbDrone.Core/MediaFiles/TrackImport/Manual/ManualImportService.cs b/src/NzbDrone.Core/MediaFiles/TrackImport/Manual/ManualImportService.cs index b259d7745..6a32984b9 100644 --- a/src/NzbDrone.Core/MediaFiles/TrackImport/Manual/ManualImportService.cs +++ b/src/NzbDrone.Core/MediaFiles/TrackImport/Manual/ManualImportService.cs @@ -14,7 +14,7 @@ using NzbDrone.Core.Messaging.Commands; using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Parser; using NzbDrone.Core.Parser.Model; -using NzbDrone.Core.Tv; +using NzbDrone.Core.Music; namespace NzbDrone.Core.MediaFiles.TrackImport.Manual { @@ -29,12 +29,13 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Manual private readonly IParsingService _parsingService; private readonly IDiskScanService _diskScanService; private readonly IMakeImportDecision _importDecisionMaker; - private readonly ISeriesService _seriesService; - private readonly IEpisodeService _episodeService; + private readonly IArtistService _artistService; + private readonly IAlbumService _albumService; + private readonly ITrackService _trackService; private readonly IVideoFileInfoReader _videoFileInfoReader; - private readonly IImportApprovedEpisodes _importApprovedEpisodes; + private readonly IImportApprovedTracks _importApprovedTracks; private readonly ITrackedDownloadService _trackedDownloadService; - private readonly IDownloadedEpisodesImportService _downloadedEpisodesImportService; + private readonly IDownloadedTracksImportService _downloadedEpisodesImportService; private readonly IEventAggregator _eventAggregator; private readonly Logger _logger; @@ -42,12 +43,13 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Manual IParsingService parsingService, IDiskScanService diskScanService, IMakeImportDecision importDecisionMaker, - ISeriesService seriesService, - IEpisodeService episodeService, + IArtistService artistService, + IAlbumService albumService, + ITrackService trackService, IVideoFileInfoReader videoFileInfoReader, - IImportApprovedEpisodes importApprovedEpisodes, + IImportApprovedTracks importApprovedTracks, ITrackedDownloadService trackedDownloadService, - IDownloadedEpisodesImportService downloadedEpisodesImportService, + IDownloadedTracksImportService downloadedEpisodesImportService, IEventAggregator eventAggregator, Logger logger) { @@ -55,10 +57,11 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Manual _parsingService = parsingService; _diskScanService = diskScanService; _importDecisionMaker = importDecisionMaker; - _seriesService = seriesService; - _episodeService = episodeService; + _artistService = artistService; + _albumService = albumService; + _trackService = trackService; _videoFileInfoReader = videoFileInfoReader; - _importApprovedEpisodes = importApprovedEpisodes; + _importApprovedTracks = importApprovedTracks; _trackedDownloadService = trackedDownloadService; _downloadedEpisodesImportService = downloadedEpisodesImportService; _eventAggregator = eventAggregator; @@ -94,179 +97,194 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Manual private List ProcessFolder(string folder, string downloadId) { - throw new System.NotImplementedException("TODO: This will be rewritten for Music"); - //var directoryInfo = new DirectoryInfo(folder); - //var series = _parsingService.GetSeries(directoryInfo.Name); + var directoryInfo = new DirectoryInfo(folder); + var artist = _parsingService.GetArtist(directoryInfo.Name); - //if (series == null && downloadId.IsNotNullOrWhiteSpace()) - //{ - // var trackedDownload = _trackedDownloadService.Find(downloadId); - // series = trackedDownload.RemoteEpisode.Series; - //} + if (artist == null && downloadId.IsNotNullOrWhiteSpace()) + { + var trackedDownload = _trackedDownloadService.Find(downloadId); + artist = trackedDownload.RemoteAlbum.Artist; + } - //if (series == null) - //{ - // var files = _diskScanService.GetVideoFiles(folder); + if (artist == null) + { + var files = _diskScanService.FilterFiles(folder, _diskScanService.GetAudioFiles(folder)); - // return files.Select(file => ProcessFile(file, downloadId, folder)).Where(i => i != null).ToList(); - //} + return files.Select(file => ProcessFile(file, downloadId, folder)).Where(i => i != null).ToList(); + } - //var folderInfo = Parser.Parser.ParseTitle(directoryInfo.Name); - //var seriesFiles = _diskScanService.GetVideoFiles(folder).ToList(); - //var decisions = _importDecisionMaker.GetImportDecisions(seriesFiles, series, folderInfo, SceneSource(series, folder)); + var folderInfo = Parser.Parser.ParseMusicTitle(directoryInfo.Name); + var artistFiles = _diskScanService.GetAudioFiles(folder).ToList(); + var decisions = _importDecisionMaker.GetImportDecisions(artistFiles, artist, folderInfo); - //return decisions.Select(decision => MapItem(decision, folder, downloadId)).ToList(); + return decisions.Select(decision => MapItem(decision, folder, downloadId)).ToList(); } private ManualImportItem ProcessFile(string file, string downloadId, string folder = null) { - throw new System.NotImplementedException("TODO: This will be rewritten for Music"); - //if (folder.IsNullOrWhiteSpace()) - //{ - // folder = new FileInfo(file).Directory.FullName; - //} + if (folder.IsNullOrWhiteSpace()) + { + folder = new FileInfo(file).Directory.FullName; + } - //var relativeFile = folder.GetRelativePath(file); + var relativeFile = folder.GetRelativePath(file); - //var series = _parsingService.GetSeries(relativeFile.Split('\\', '/')[0]); + var artist = _parsingService.GetArtist(relativeFile.Split('\\', '/')[0]); - //if (series == null) - //{ - // series = _parsingService.GetSeries(relativeFile); - //} + if (artist == null) + { + artist = _parsingService.GetArtistFromTag(file); + } - //if (series == null && downloadId.IsNotNullOrWhiteSpace()) - //{ - // var trackedDownload = _trackedDownloadService.Find(downloadId); - // series = trackedDownload.RemoteEpisode.Series; - //} + if (artist == null && downloadId.IsNotNullOrWhiteSpace()) + { + var trackedDownload = _trackedDownloadService.Find(downloadId); + artist = trackedDownload.RemoteAlbum.Artist; + } - //if (series == null) - //{ - // var localEpisode = new LocalEpisode(); - // localEpisode.Path = file; - // localEpisode.Quality = QualityParser.ParseQuality(file); - // localEpisode.Size = _diskProvider.GetFileSize(file); + if (artist == null) + { + var localTrack = new LocalTrack(); + localTrack.Path = file; + localTrack.Quality = QualityParser.ParseQuality(file); + localTrack.Language = LanguageParser.ParseLanguage(file); + localTrack.Size = _diskProvider.GetFileSize(file); - // return MapItem(new ImportDecision(localEpisode, new Rejection("Unknown Series")), folder, downloadId); - //} + return MapItem(new ImportDecision(localTrack, new Rejection("Unknown Artist")), folder, downloadId); + } - //var importDecisions = _importDecisionMaker.GetImportDecisions(new List {file}, - // series, null, SceneSource(series, folder)); + var importDecisions = _importDecisionMaker.GetImportDecisions(new List { file }, + artist, null); - //return importDecisions.Any() ? MapItem(importDecisions.First(), folder, downloadId) : null; + return importDecisions.Any() ? MapItem(importDecisions.First(), folder, downloadId) : new ManualImportItem + { + DownloadId = downloadId, + Path = file, + RelativePath = folder.GetRelativePath(file), + Name = Path.GetFileNameWithoutExtension(file), + Rejections = new List + { + new Rejection("Unable to process file") + } + }; } - private bool SceneSource(Series series, string folder) + private bool SceneSource(Artist artist, string folder) { - return !(series.Path.PathEquals(folder) || series.Path.IsParentPath(folder)); + return !(artist.Path.PathEquals(folder) || artist.Path.IsParentPath(folder)); } private ManualImportItem MapItem(ImportDecision decision, string folder, string downloadId) { - throw new System.NotImplementedException("TODO: This will be rewritten for Music"); - //var item = new ManualImportItem(); - - //item.Path = decision.LocalEpisode.Path; - //item.RelativePath = folder.GetRelativePath(decision.LocalEpisode.Path); - //item.Name = Path.GetFileNameWithoutExtension(decision.LocalEpisode.Path); - //item.DownloadId = downloadId; - - //if (decision.LocalEpisode.Series != null) - //{ - // item.Series = decision.LocalEpisode.Series; - //} - - //if (decision.LocalEpisode.Episodes.Any()) - //{ - // item.SeasonNumber = decision.LocalEpisode.SeasonNumber; - // item.Episodes = decision.LocalEpisode.Episodes; - //} - - //item.Quality = decision.LocalEpisode.Quality; - //item.Size = _diskProvider.GetFileSize(decision.LocalEpisode.Path); - //item.Rejections = decision.Rejections; - - //return item; + var item = new ManualImportItem(); + + item.Path = decision.LocalTrack.Path; + item.RelativePath = folder.GetRelativePath(decision.LocalTrack.Path); + item.Name = Path.GetFileNameWithoutExtension(decision.LocalTrack.Path); + item.DownloadId = downloadId; + + if (decision.LocalTrack.Artist != null) + { + item.Artist = decision.LocalTrack.Artist; + } + + if (decision.LocalTrack.Album != null) + { + item.Album = decision.LocalTrack.Album; + } + + if (decision.LocalTrack.Tracks.Any()) + { + item.Tracks = decision.LocalTrack.Tracks; + } + + item.Quality = decision.LocalTrack.Quality; + item.Language = decision.LocalTrack.Language; + item.Size = _diskProvider.GetFileSize(decision.LocalTrack.Path); + item.Rejections = decision.Rejections; + + return item; } public void Execute(ManualImportCommand message) { _logger.ProgressTrace("Manually importing {0} files using mode {1}", message.Files.Count, message.ImportMode); - throw new System.NotImplementedException("TODO: This will be rewritten for Music"); - - //var imported = new List(); - //var importedTrackedDownload = new List(); - - //for (int i = 0; i < message.Files.Count; i++) - //{ - // _logger.ProgressTrace("Processing file {0} of {1}", i + 1, message.Files.Count); - - // var file = message.Files[i]; - // var series = _seriesService.GetSeries(file.SeriesId); - // var episodes = _episodeService.GetEpisodes(file.EpisodeIds); - // var parsedEpisodeInfo = Parser.Parser.ParsePath(file.Path) ?? new ParsedEpisodeInfo(); - // var mediaInfo = _videoFileInfoReader.GetMediaInfo(file.Path); - // var existingFile = series.Path.IsParentPath(file.Path); - - // var localEpisode = new LocalEpisode - // { - // ExistingFile = false, - // Episodes = episodes, - // MediaInfo = mediaInfo, - // ParsedEpisodeInfo = parsedEpisodeInfo, - // Path = file.Path, - // Quality = file.Quality, - // Series = series, - // Size = 0 - // }; - - // //TODO: Cleanup non-tracked downloads - - // var importDecision = new ImportDecision(localEpisode); - - // if (file.DownloadId.IsNullOrWhiteSpace()) - // { - // imported.AddRange(_importApprovedEpisodes.Import(new List { importDecision }, !existingFile, null, message.ImportMode)); - // } - - // else - // { - // var trackedDownload = _trackedDownloadService.Find(file.DownloadId); - // var importResult = _importApprovedEpisodes.Import(new List { importDecision }, true, trackedDownload.DownloadItem, message.ImportMode).First(); - - // imported.Add(importResult); - - // importedTrackedDownload.Add(new ManuallyImportedFile - // { - // TrackedDownload = trackedDownload, - // ImportResult = importResult - // }); - // } - //} - - //_logger.ProgressTrace("Manually imported {0} files", imported.Count); - - //foreach (var groupedTrackedDownload in importedTrackedDownload.GroupBy(i => i.TrackedDownload.DownloadItem.DownloadId).ToList()) - //{ - // var trackedDownload = groupedTrackedDownload.First().TrackedDownload; - - // if (_diskProvider.FolderExists(trackedDownload.DownloadItem.OutputPath.FullPath)) - // { - // if (_downloadedEpisodesImportService.ShouldDeleteFolder( - // new DirectoryInfo(trackedDownload.DownloadItem.OutputPath.FullPath), - // trackedDownload.RemoteEpisode.Series) && !trackedDownload.DownloadItem.IsReadOnly) - // { - // _diskProvider.DeleteFolder(trackedDownload.DownloadItem.OutputPath.FullPath, true); - // } - // } - - // if (groupedTrackedDownload.Select(c => c.ImportResult).Count(c => c.Result == ImportResultType.Imported) >= Math.Max(1, trackedDownload.RemoteEpisode.Episodes.Count)) - // { - // trackedDownload.State = TrackedDownloadStage.Imported; - // _eventAggregator.PublishEvent(new DownloadCompletedEvent(trackedDownload)); - // } - //} + + var imported = new List(); + var importedTrackedDownload = new List(); + + for (int i = 0; i < message.Files.Count; i++) + { + _logger.ProgressTrace("Processing file {0} of {1}", i + 1, message.Files.Count); + + var file = message.Files[i]; + var artist = _artistService.GetArtist(file.ArtistId); + var album = _albumService.GetAlbum(file.AlbumId); + var tracks = _trackService.GetTracks(file.TrackIds); + var parsedTrackInfo = Parser.Parser.ParseMusicPath(file.Path) ?? new ParsedTrackInfo(); + var mediaInfo = _videoFileInfoReader.GetMediaInfo(file.Path); + var existingFile = artist.Path.IsParentPath(file.Path); + + var localTrack = new LocalTrack + { + ExistingFile = false, + Tracks = tracks, + MediaInfo = mediaInfo, + ParsedTrackInfo = parsedTrackInfo, + Path = file.Path, + Quality = file.Quality, + Language = file.Language, + Artist = artist, + Album = album, + Size = 0 + }; + + //TODO: Cleanup non-tracked downloads + + var importDecision = new ImportDecision(localTrack); + + if (file.DownloadId.IsNullOrWhiteSpace()) + { + imported.AddRange(_importApprovedTracks.Import(new List { importDecision }, !existingFile, null, message.ImportMode)); + } + + else + { + var trackedDownload = _trackedDownloadService.Find(file.DownloadId); + var importResult = _importApprovedTracks.Import(new List { importDecision }, true, trackedDownload.DownloadItem, message.ImportMode).First(); + + imported.Add(importResult); + + importedTrackedDownload.Add(new ManuallyImportedFile + { + TrackedDownload = trackedDownload, + ImportResult = importResult + }); + } + } + + _logger.ProgressTrace("Manually imported {0} files", imported.Count); + + foreach (var groupedTrackedDownload in importedTrackedDownload.GroupBy(i => i.TrackedDownload.DownloadItem.DownloadId).ToList()) + { + var trackedDownload = groupedTrackedDownload.First().TrackedDownload; + + if (_diskProvider.FolderExists(trackedDownload.DownloadItem.OutputPath.FullPath)) + { + if (_downloadedEpisodesImportService.ShouldDeleteFolder( + new DirectoryInfo(trackedDownload.DownloadItem.OutputPath.FullPath), + trackedDownload.RemoteAlbum.Artist) && trackedDownload.DownloadItem.CanMoveFiles) + { + _diskProvider.DeleteFolder(trackedDownload.DownloadItem.OutputPath.FullPath, true); + } + } + + if (groupedTrackedDownload.Select(c => c.ImportResult).Count(c => c.Result == ImportResultType.Imported) >= Math.Max(1, trackedDownload.RemoteAlbum.Albums.Count)) + { + trackedDownload.State = TrackedDownloadStage.Imported; + _eventAggregator.PublishEvent(new DownloadCompletedEvent(trackedDownload)); + } + } } } } diff --git a/src/NzbDrone.Core/MediaFiles/TrackImport/RootFolderNotFoundException.cs b/src/NzbDrone.Core/MediaFiles/TrackImport/RootFolderNotFoundException.cs new file mode 100644 index 000000000..15c3f997c --- /dev/null +++ b/src/NzbDrone.Core/MediaFiles/TrackImport/RootFolderNotFoundException.cs @@ -0,0 +1,25 @@ +using System; +using System.IO; +using System.Runtime.Serialization; + +namespace NzbDrone.Core.MediaFiles.TrackImport +{ + public class RootFolderNotFoundException : DirectoryNotFoundException + { + public RootFolderNotFoundException() + { + } + + public RootFolderNotFoundException(string message) : base(message) + { + } + + public RootFolderNotFoundException(string message, Exception innerException) : base(message, innerException) + { + } + + protected RootFolderNotFoundException(SerializationInfo info, StreamingContext context) : base(info, context) + { + } + } +} diff --git a/src/NzbDrone.Core/MediaFiles/TrackImport/Specifications/SameFileSpecification.cs b/src/NzbDrone.Core/MediaFiles/TrackImport/Specifications/SameFileSpecification.cs new file mode 100644 index 000000000..1ef2f8df6 --- /dev/null +++ b/src/NzbDrone.Core/MediaFiles/TrackImport/Specifications/SameFileSpecification.cs @@ -0,0 +1,43 @@ +using System.Linq; +using NLog; +using NzbDrone.Core.DecisionEngine; +using NzbDrone.Core.Download; +using NzbDrone.Core.Parser.Model; + +namespace NzbDrone.Core.MediaFiles.TrackImport.Specifications +{ + public class SameFileSpecification : IImportDecisionEngineSpecification + { + private readonly Logger _logger; + + public SameFileSpecification(Logger logger) + { + _logger = logger; + } + + public Decision IsSatisfiedBy(LocalTrack localTrack) + { + var trackFiles = localTrack.Tracks.Where(e => e.TrackFileId != 0).Select(e => e.TrackFile).ToList(); + + if (trackFiles.Count == 0) + { + _logger.Debug("No existing track file, skipping"); + return Decision.Accept(); + } + + if (trackFiles.Count > 1) + { + _logger.Debug("More than one existing track file, skipping."); + return Decision.Accept(); + } + + if (trackFiles.First().Value.Size == localTrack.Size) + { + _logger.Debug("'{0}' Has the same filesize as existing file", localTrack.Path); + return Decision.Reject("Has the same filesize as existing file"); + } + + return Decision.Accept(); + } + } +} diff --git a/src/NzbDrone.Core/MediaFiles/TrackImport/Specifications/UpgradeSpecification.cs b/src/NzbDrone.Core/MediaFiles/TrackImport/Specifications/UpgradeSpecification.cs index 5e1ec2f74..581e0c72b 100644 --- a/src/NzbDrone.Core/MediaFiles/TrackImport/Specifications/UpgradeSpecification.cs +++ b/src/NzbDrone.Core/MediaFiles/TrackImport/Specifications/UpgradeSpecification.cs @@ -4,6 +4,9 @@ using NLog; using NzbDrone.Core.DecisionEngine; using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Qualities; +using NzbDrone.Core.Languages; +using NzbDrone.Core.Profiles.Qualities; +using System.Collections.Generic; namespace NzbDrone.Core.MediaFiles.TrackImport.Specifications { @@ -19,7 +22,12 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Specifications public Decision IsSatisfiedBy(LocalTrack localTrack) { var qualityComparer = new QualityModelComparer(localTrack.Artist.Profile); - if (localTrack.Tracks.Any(e => e.TrackFileId != 0 && qualityComparer.Compare(e.TrackFile.Value.Quality, localTrack.Quality) > 0)) + var languageComparer = new LanguageComparer(localTrack.Artist.LanguageProfile); + var profile = localTrack.Artist.Profile.Value; + + if (localTrack.Tracks.Any(e => e.TrackFileId != 0 && + languageComparer.Compare(e.TrackFile.Value.Language, localTrack.Language) > 0 && + qualityComparer.Compare(e.TrackFile.Value.Quality, localTrack.Quality) == 0)) { _logger.Debug("This file isn't an upgrade for all tracks. Skipping {0}", localTrack.Path); return Decision.Reject("Not an upgrade for existing track file(s)"); diff --git a/src/NzbDrone.Core/Messaging/Commands/Command.cs b/src/NzbDrone.Core/Messaging/Commands/Command.cs index 20becd1f0..2eb164c03 100644 --- a/src/NzbDrone.Core/Messaging/Commands/Command.cs +++ b/src/NzbDrone.Core/Messaging/Commands/Command.cs @@ -4,7 +4,20 @@ namespace NzbDrone.Core.Messaging.Commands { public abstract class Command { - public virtual bool SendUpdatesToClient => false; + private bool _sendUpdatesToClient; + + public virtual bool SendUpdatesToClient + { + get + { + return _sendUpdatesToClient; + } + + set + { + _sendUpdatesToClient = value; + } + } public virtual bool UpdateScheduledTask => true; @@ -13,6 +26,7 @@ namespace NzbDrone.Core.Messaging.Commands public string Name { get; private set; } public DateTime? LastExecutionTime { get; set; } public CommandTrigger Trigger { get; set; } + public bool SuppressMessages { get; set; } public Command() { diff --git a/src/NzbDrone.Core/Messaging/Commands/CommandExecutor.cs b/src/NzbDrone.Core/Messaging/Commands/CommandExecutor.cs index 15843ef7b..f2f727180 100644 --- a/src/NzbDrone.Core/Messaging/Commands/CommandExecutor.cs +++ b/src/NzbDrone.Core/Messaging/Commands/CommandExecutor.cs @@ -48,9 +48,13 @@ namespace NzbDrone.Core.Messaging.Commands } catch (ThreadAbortException ex) { - _logger.Error(ex); + _logger.Error(ex, "Thread aborted"); Thread.ResetAbort(); } + catch (OperationCanceledException ex) + { + _logger.Trace("Stopped one command execution pipeline"); + } catch (Exception ex) { _logger.Error(ex, "Unknown error in thread"); @@ -76,7 +80,7 @@ namespace NzbDrone.Core.Messaging.Commands handler.Execute(command); - _commandQueueManager.Complete(commandModel, command.CompletionMessage); + _commandQueueManager.Complete(commandModel, command.CompletionMessage ?? commandModel.Message); } catch (CommandFailedException ex) { diff --git a/src/NzbDrone.Core/Messaging/Commands/CommandQueueManager.cs b/src/NzbDrone.Core/Messaging/Commands/CommandQueueManager.cs index d45547b8f..5fb2eb02a 100644 --- a/src/NzbDrone.Core/Messaging/Commands/CommandQueueManager.cs +++ b/src/NzbDrone.Core/Messaging/Commands/CommandQueueManager.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; @@ -15,11 +15,12 @@ namespace NzbDrone.Core.Messaging.Commands { public interface IManageCommandQueue { + List PushMany(List commands) where TCommand : Command; CommandModel Push(TCommand command, CommandPriority priority = CommandPriority.Normal, CommandTrigger trigger = CommandTrigger.Unspecified) where TCommand : Command; CommandModel Push(string commandName, DateTime? lastExecutionTime, CommandPriority priority = CommandPriority.Normal, CommandTrigger trigger = CommandTrigger.Unspecified); IEnumerable Queue(CancellationToken cancellationToken); CommandModel Get(int id); - List GetStarted(); + List GetStarted(); void SetMessage(CommandModel command, string message); void Start(CommandModel command); void Complete(CommandModel command, string message); @@ -35,9 +36,9 @@ namespace NzbDrone.Core.Messaging.Commands private readonly Logger _logger; private readonly ICached _commandCache; - private readonly BlockingCollection _commandQueue; + private readonly BlockingCollection _commandQueue; - public CommandQueueManager(ICommandRepository repo, + public CommandQueueManager(ICommandRepository repo, IServiceFactory serviceFactory, ICacheManager cacheManager, Logger logger) @@ -50,6 +51,49 @@ namespace NzbDrone.Core.Messaging.Commands _commandQueue = new BlockingCollection(new CommandQueue()); } + public List PushMany(List commands) where TCommand : Command + { + _logger.Trace("Publishing {0} commands", commands.Count); + + var commandModels = new List(); + var existingCommands = _commandCache.Values.Where(q => q.Status == CommandStatus.Queued || + q.Status == CommandStatus.Started).ToList(); + + foreach (var command in commands) + { + var existing = existingCommands.SingleOrDefault(c => c.Name == command.Name && CommandEqualityComparer.Instance.Equals(c.Body, command)); + + if (existing != null) + { + continue; + } + + var commandModel = new CommandModel + + { + Name = command.Name, + Body = command, + QueuedAt = DateTime.UtcNow, + Trigger = CommandTrigger.Unspecified, + Priority = CommandPriority.Normal, + Status = CommandStatus.Queued + }; + + commandModels.Add(commandModel); + } + + _repo.InsertMany(commandModels); + + foreach (var commandModel in commandModels) + { + _commandCache.Set(commandModel.Id.ToString(), commandModel); + _commandQueue.Add(commandModel); + } + + return commandModels; + } + + public CommandModel Push(TCommand command, CommandPriority priority = CommandPriority.Normal, CommandTrigger trigger = CommandTrigger.Unspecified) where TCommand : Command { Ensure.That(command, () => command).IsNotNull(); @@ -123,7 +167,7 @@ namespace NzbDrone.Core.Messaging.Commands command.Status = CommandStatus.Started; _logger.Trace("Marking command as started: {0}", command.Name); - _commandCache.Set(command.Id.ToString(), command); + _commandCache.Set(command.Id.ToString(), command); _repo.Start(command); } @@ -135,7 +179,7 @@ namespace NzbDrone.Core.Messaging.Commands public void Fail(CommandModel command, string message, Exception e) { command.Exception = e.ToString(); - + Update(command, CommandStatus.Failed, message); } @@ -150,7 +194,7 @@ namespace NzbDrone.Core.Messaging.Commands public void CleanCommands() { _logger.Trace("Cleaning up old commands"); - + var old = _commandCache.Values.Where(c => c.EndedAt < DateTime.UtcNow.AddMinutes(-5)); foreach (var command in old) diff --git a/src/NzbDrone.Core/Messaging/Events/EventAggregator.cs b/src/NzbDrone.Core/Messaging/Events/EventAggregator.cs index a66d22c2c..0111a7342 100644 --- a/src/NzbDrone.Core/Messaging/Events/EventAggregator.cs +++ b/src/NzbDrone.Core/Messaging/Events/EventAggregator.cs @@ -62,6 +62,17 @@ namespace NzbDrone.Core.Messaging.Events } } + foreach (var handler in _serviceFactory.BuildAll>()) + { + var handlerLocal = handler; + + _taskFactory.StartNew(() => + { + handlerLocal.HandleAsync(@event); + }, TaskCreationOptions.PreferFairness) + .LogExceptions(); + } + foreach (var handler in _serviceFactory.BuildAll>()) { var handlerLocal = handler; diff --git a/src/NzbDrone.Core/MetadataSource/IProvideArtistInfo.cs b/src/NzbDrone.Core/MetadataSource/IProvideArtistInfo.cs index 48240cbc6..f4f67652d 100644 --- a/src/NzbDrone.Core/MetadataSource/IProvideArtistInfo.cs +++ b/src/NzbDrone.Core/MetadataSource/IProvideArtistInfo.cs @@ -1,4 +1,4 @@ -using NzbDrone.Core.Music; +using NzbDrone.Core.Music; using System; using System.Collections.Generic; @@ -6,6 +6,6 @@ namespace NzbDrone.Core.MetadataSource { public interface IProvideArtistInfo { - Tuple> GetArtistInfo(string lidarrId); + Tuple> GetArtistInfo(string lidarrId, List primaryAlbumTypes, List secondaryAlbumTypes); } } diff --git a/src/NzbDrone.Core/MetadataSource/IProvideSeriesInfo.cs b/src/NzbDrone.Core/MetadataSource/IProvideSeriesInfo.cs deleted file mode 100644 index f2ab03336..000000000 --- a/src/NzbDrone.Core/MetadataSource/IProvideSeriesInfo.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System; -using System.Collections.Generic; -using NzbDrone.Core.Tv; - -namespace NzbDrone.Core.MetadataSource -{ - public interface IProvideSeriesInfo - { - Tuple> GetSeriesInfo(int tvdbSeriesId); - } -} \ No newline at end of file diff --git a/src/NzbDrone.Core/MetadataSource/SearchArtistComparer.cs b/src/NzbDrone.Core/MetadataSource/SearchArtistComparer.cs new file mode 100644 index 000000000..f01e8107f --- /dev/null +++ b/src/NzbDrone.Core/MetadataSource/SearchArtistComparer.cs @@ -0,0 +1,87 @@ +using System; +using System.Collections.Generic; +using System.Text.RegularExpressions; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.Music; + +namespace NzbDrone.Core.MetadataSource +{ + public class SearchArtistComparer : IComparer + { + private static readonly Regex RegexCleanPunctuation = new Regex("[-._:]", RegexOptions.Compiled); + private static readonly Regex RegexCleanCountryYearPostfix = new Regex(@"(?<=.+)( \([A-Z]{2}\)| \(\d{4}\)| \([A-Z]{2}\) \(\d{4}\))$", RegexOptions.Compiled); + private static readonly Regex ArticleRegex = new Regex(@"^(a|an|the)\s", RegexOptions.IgnoreCase | RegexOptions.Compiled); + + public string SearchQuery { get; private set; } + + private readonly string _searchQueryWithoutYear; + private int? _year; + + public SearchArtistComparer(string searchQuery) + { + SearchQuery = searchQuery; + + var match = Regex.Match(SearchQuery, @"^(?.+)\s+(?:\((?\d{4})\)|(?\d{4}))$"); + if (match.Success) + { + _searchQueryWithoutYear = match.Groups["query"].Value.ToLowerInvariant(); + _year = int.Parse(match.Groups["year"].Value); + } + else + { + _searchQueryWithoutYear = searchQuery.ToLowerInvariant(); + } + } + + public int Compare(Artist x, Artist y) + { + int result = 0; + + // Prefer exact matches + result = Compare(x, y, s => CleanPunctuation(s.Name).Equals(CleanPunctuation(SearchQuery))); + if (result != 0) return -result; + + // Remove Articles (a/an/the) + result = Compare(x, y, s => CleanArticles(s.Name).Equals(CleanArticles(SearchQuery))); + if (result != 0) return -result; + + // Prefer close matches + result = Compare(x, y, s => CleanPunctuation(s.Name).LevenshteinDistance(CleanPunctuation(SearchQuery)) <= 1); + if (result != 0) return -result; + + return Compare(x, y, s => SearchQuery.LevenshteinDistanceClean(s.Name)); + } + + public int Compare(Artist x, Artist y, Func keySelector) + where T : IComparable + { + var keyX = keySelector(x); + var keyY = keySelector(y); + + return keyX.CompareTo(keyY); + } + + private string CleanPunctuation(string title) + { + title = RegexCleanPunctuation.Replace(title, ""); + + return title.ToLowerInvariant(); + } + + private string CleanTitle(string title) + { + title = RegexCleanPunctuation.Replace(title, ""); + title = RegexCleanCountryYearPostfix.Replace(title, ""); + + return title.ToLowerInvariant(); + } + + private string CleanArticles(string title) + { + title = ArticleRegex.Replace(title, ""); + + return title.Trim().ToLowerInvariant(); + } + + } +} diff --git a/src/NzbDrone.Core/MetadataSource/SearchSeriesComparer.cs b/src/NzbDrone.Core/MetadataSource/SearchSeriesComparer.cs deleted file mode 100644 index 05d9a1223..000000000 --- a/src/NzbDrone.Core/MetadataSource/SearchSeriesComparer.cs +++ /dev/null @@ -1,127 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text.RegularExpressions; -using NzbDrone.Common.Extensions; -using NzbDrone.Core.Tv; - -namespace NzbDrone.Core.MetadataSource -{ - public class SearchSeriesComparer : IComparer - { - private static readonly Regex RegexCleanPunctuation = new Regex("[-._:]", RegexOptions.Compiled); - private static readonly Regex RegexCleanCountryYearPostfix = new Regex(@"(?<=.+)( \([A-Z]{2}\)| \(\d{4}\)| \([A-Z]{2}\) \(\d{4}\))$", RegexOptions.Compiled); - private static readonly Regex ArticleRegex = new Regex(@"^(a|an|the)\s", RegexOptions.IgnoreCase | RegexOptions.Compiled); - - public string SearchQuery { get; private set; } - - private readonly string _searchQueryWithoutYear; - private int? _year; - - public SearchSeriesComparer(string searchQuery) - { - SearchQuery = searchQuery; - - var match = Regex.Match(SearchQuery, @"^(?.+)\s+(?:\((?\d{4})\)|(?\d{4}))$"); - if (match.Success) - { - _searchQueryWithoutYear = match.Groups["query"].Value.ToLowerInvariant(); - _year = int.Parse(match.Groups["year"].Value); - } - else - { - _searchQueryWithoutYear = searchQuery.ToLowerInvariant(); - } - } - - public int Compare(Series x, Series y) - { - int result = 0; - - // Prefer exact matches - result = Compare(x, y, s => CleanPunctuation(s.Title).Equals(CleanPunctuation(SearchQuery))); - if (result != 0) return -result; - - // Remove Articles (a/an/the) - result = Compare(x, y, s => CleanArticles(s.Title).Equals(CleanArticles(SearchQuery))); - if (result != 0) return -result; - - // Prefer close matches - result = Compare(x, y, s => CleanPunctuation(s.Title).LevenshteinDistance(CleanPunctuation(SearchQuery)) <= 1); - if (result != 0) return -result; - - // Compare clean matches by year "Battlestar Galactica 1978" - result = CompareWithYear(x, y, s => CleanTitle(s.Title).LevenshteinDistance(_searchQueryWithoutYear) <= 1); - if (result != 0) return -result; - - // Compare prefix matches by year "(CSI: ..." - result = CompareWithYear(x, y, s => s.Title.ToLowerInvariant().StartsWith(_searchQueryWithoutYear + ":")); - if (result != 0) return -result; - - return Compare(x, y, s => SearchQuery.LevenshteinDistanceClean(s.Title) - GetYearFactor(s)); - } - - public int Compare(Series x, Series y, Func keySelector) - where T : IComparable - { - var keyX = keySelector(x); - var keyY = keySelector(y); - - return keyX.CompareTo(keyY); - } - - public int CompareWithYear(Series x, Series y, Predicate canMatch) - { - var matchX = canMatch(x); - var matchY = canMatch(y); - - if (matchX && matchY) - { - if (_year.HasValue) - { - var result = Compare(x, y, s => s.Year == _year.Value); - if (result != 0) return result; - } - - return Compare(x, y, s => s.Year); - } - - return matchX.CompareTo(matchY); - } - - private string CleanPunctuation(string title) - { - title = RegexCleanPunctuation.Replace(title, ""); - - return title.ToLowerInvariant(); - } - - private string CleanTitle(string title) - { - title = RegexCleanPunctuation.Replace(title, ""); - title = RegexCleanCountryYearPostfix.Replace(title, ""); - - return title.ToLowerInvariant(); - } - - private string CleanArticles(string title) - { - title = ArticleRegex.Replace(title, ""); - - return title.Trim().ToLowerInvariant(); - } - - private int GetYearFactor(Series series) - { - if (_year.HasValue) - { - var offset = Math.Abs(series.Year - _year.Value); - if (offset <= 1) - { - return 20 - 10 * offset; - } - } - - return 0; - } - } -} diff --git a/src/NzbDrone.Core/MetadataSource/SkyHook/Resource/ActorResource.cs b/src/NzbDrone.Core/MetadataSource/SkyHook/Resource/ActorResource.cs deleted file mode 100644 index 180933387..000000000 --- a/src/NzbDrone.Core/MetadataSource/SkyHook/Resource/ActorResource.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace NzbDrone.Core.MetadataSource.SkyHook.Resource -{ - public class ActorResource - { - public string Name { get; set; } - public string Character { get; set; } - public string Image { get; set; } - } -} \ No newline at end of file diff --git a/src/NzbDrone.Core/MetadataSource/SkyHook/Resource/AlbumResource.cs b/src/NzbDrone.Core/MetadataSource/SkyHook/Resource/AlbumResource.cs index 5e5a1f13d..0c701a546 100644 --- a/src/NzbDrone.Core/MetadataSource/SkyHook/Resource/AlbumResource.cs +++ b/src/NzbDrone.Core/MetadataSource/SkyHook/Resource/AlbumResource.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Text; @@ -20,7 +20,7 @@ namespace NzbDrone.Core.MetadataSource.SkyHook.Resource public string Title { get; set; } // In case of a takedown, this may be empty public string Overview { get; set; } public List Genres { get; set; } - public string Label { get; set; } + public List Label { get; set; } public string Type { get; set; } public List Tracks { get; set; } } diff --git a/src/NzbDrone.Core/MetadataSource/SkyHook/Resource/ArtistInfoResource.cs b/src/NzbDrone.Core/MetadataSource/SkyHook/Resource/ArtistInfoResource.cs deleted file mode 100644 index 8a81d873f..000000000 --- a/src/NzbDrone.Core/MetadataSource/SkyHook/Resource/ArtistInfoResource.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; - -namespace NzbDrone.Core.MetadataSource.SkyHook.Resource -{ - public class ArtistInfoResource - { - public ArtistInfoResource() { } - - public List Genres { get; set; } - public string AristUrl { get; set; } - public string Overview { get; set; } - public string Id { get; set; } - public List Images { get; set; } - public string ArtistName { get; set; } - } -} diff --git a/src/NzbDrone.Core/MetadataSource/SkyHook/Resource/ArtistResource.cs b/src/NzbDrone.Core/MetadataSource/SkyHook/Resource/ArtistResource.cs index f2ab72f94..85fc14ea8 100644 --- a/src/NzbDrone.Core/MetadataSource/SkyHook/Resource/ArtistResource.cs +++ b/src/NzbDrone.Core/MetadataSource/SkyHook/Resource/ArtistResource.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Text; @@ -14,10 +14,15 @@ namespace NzbDrone.Core.MetadataSource.SkyHook.Resource public List Genres { get; set; } public string AristUrl { get; set; } public string Overview { get; set; } + public string Type { get; set; } + public string Disambiguation { get; set; } public string Id { get; set; } public List Images { get; set; } + public List Links { get; set; } public string ArtistName { get; set; } public List Albums { get; set; } - + public string Status { get; set; } + public RatingResource Rating { get; set; } + } } diff --git a/src/NzbDrone.Core/MetadataSource/SkyHook/Resource/EpisodeResource.cs b/src/NzbDrone.Core/MetadataSource/SkyHook/Resource/EpisodeResource.cs deleted file mode 100644 index acaffe418..000000000 --- a/src/NzbDrone.Core/MetadataSource/SkyHook/Resource/EpisodeResource.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System; - -namespace NzbDrone.Core.MetadataSource.SkyHook.Resource -{ - public class EpisodeResource - { - public int SeasonNumber { get; set; } - public int EpisodeNumber { get; set; } - public int? AbsoluteEpisodeNumber { get; set; } - public string Title { get; set; } - public string AirDate { get; set; } - public DateTime? AirDateUtc { get; set; } - public RatingResource Rating { get; set; } - public string Overview { get; set; } - public string Image { get; set; } - } -} \ No newline at end of file diff --git a/src/NzbDrone.Core/MetadataSource/SkyHook/Resource/LinkResource.cs b/src/NzbDrone.Core/MetadataSource/SkyHook/Resource/LinkResource.cs new file mode 100644 index 000000000..3021fbdfe --- /dev/null +++ b/src/NzbDrone.Core/MetadataSource/SkyHook/Resource/LinkResource.cs @@ -0,0 +1,8 @@ +namespace NzbDrone.Core.MetadataSource.SkyHook.Resource +{ + public class LinkResource + { + public string Target { get; set; } + public string Type { get; set; } + } +} diff --git a/src/NzbDrone.Core/MetadataSource/SkyHook/Resource/MemberResource.cs b/src/NzbDrone.Core/MetadataSource/SkyHook/Resource/MemberResource.cs new file mode 100644 index 000000000..1f79b6221 --- /dev/null +++ b/src/NzbDrone.Core/MetadataSource/SkyHook/Resource/MemberResource.cs @@ -0,0 +1,9 @@ +namespace NzbDrone.Core.MetadataSource.SkyHook.Resource +{ + public class MemberResource + { + public string Name { get; set; } + public string Instrument { get; set; } + public string Image { get; set; } + } +} diff --git a/src/NzbDrone.Core/MetadataSource/SkyHook/Resource/SeasonResource.cs b/src/NzbDrone.Core/MetadataSource/SkyHook/Resource/SeasonResource.cs deleted file mode 100644 index 55ce6ccf9..000000000 --- a/src/NzbDrone.Core/MetadataSource/SkyHook/Resource/SeasonResource.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System.Collections.Generic; - -namespace NzbDrone.Core.MetadataSource.SkyHook.Resource -{ - public class SeasonResource - { - public SeasonResource() - { - Images = new List(); - } - - public int SeasonNumber { get; set; } - public List Images { get; set; } - } -} \ No newline at end of file diff --git a/src/NzbDrone.Core/MetadataSource/SkyHook/Resource/ShowResource.cs b/src/NzbDrone.Core/MetadataSource/SkyHook/Resource/ShowResource.cs deleted file mode 100644 index fd442100d..000000000 --- a/src/NzbDrone.Core/MetadataSource/SkyHook/Resource/ShowResource.cs +++ /dev/null @@ -1,43 +0,0 @@ -using System.Collections.Generic; - -namespace NzbDrone.Core.MetadataSource.SkyHook.Resource -{ - public class ShowResource - { - public ShowResource() - { - Actors = new List(); - Genres = new List(); - Images = new List(); - Seasons = new List(); - Episodes = new List(); - } - - public int TvdbId { get; set; } - public string Title { get; set; } - public string Overview { get; set; } - //public string Language { get; set; } - public string Slug { get; set; } - public string FirstAired { get; set; } - public int? TvRageId { get; set; } - public int? TvMazeId { get; set; } - - public string Status { get; set; } - public int? Runtime { get; set; } - public TimeOfDayResource TimeOfDay { get; set; } - - public string Network { get; set; } - public string ImdbId { get; set; } - - public List Actors { get; set; } - public List Genres { get; set; } - - public string ContentRating { get; set; } - - public RatingResource Rating { get; set; } - - public List Images { get; set; } - public List Seasons { get; set; } - public List Episodes { get; set; } - } -} \ No newline at end of file diff --git a/src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookProxy.cs b/src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookProxy.cs index 8652e54d2..4eb93c459 100644 --- a/src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookProxy.cs +++ b/src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookProxy.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Net; @@ -9,49 +9,61 @@ using NzbDrone.Common.Http; using NzbDrone.Core.Exceptions; using NzbDrone.Core.MediaCover; using NzbDrone.Core.MetadataSource.SkyHook.Resource; -using NzbDrone.Core.Tv; using Newtonsoft.Json.Linq; using NzbDrone.Core.Music; using Newtonsoft.Json; +using NzbDrone.Core.Configuration; namespace NzbDrone.Core.MetadataSource.SkyHook { - public class SkyHookProxy : IProvideSeriesInfo, IProvideArtistInfo, ISearchForNewArtist + public class SkyHookProxy : IProvideArtistInfo, ISearchForNewArtist { private readonly IHttpClient _httpClient; private readonly Logger _logger; + private readonly IArtistService _artistService; private readonly IHttpRequestBuilderFactory _requestBuilder; + private readonly IConfigService _configService; - public SkyHookProxy(IHttpClient httpClient, ILidarrCloudRequestBuilder requestBuilder, Logger logger) + private IHttpRequestBuilderFactory customerRequestBuilder; + + public SkyHookProxy(IHttpClient httpClient, ILidarrCloudRequestBuilder requestBuilder, IArtistService artistService, Logger logger, IConfigService configService) { _httpClient = httpClient; - _requestBuilder = requestBuilder.Search; + _configService = configService; + _requestBuilder = requestBuilder.Search; + _artistService = artistService; _logger = logger; } - public Tuple> GetSeriesInfo(int tvdbSeriesId) - { - throw new NotImplementedException(); - } - - public Tuple> GetArtistInfo(string foreignArtistId) + public Tuple> GetArtistInfo(string foreignArtistId, List primaryAlbumTypes, List secondaryAlbumTypes) { _logger.Debug("Getting Artist with LidarrAPI.MetadataID of {0}", foreignArtistId); - // We need to perform a direct lookup of the artist - var httpRequest = _requestBuilder.Create() + SetCustomProvider(); + + if (primaryAlbumTypes == null) + { + primaryAlbumTypes = new List(); + } + + if (secondaryAlbumTypes == null) + { + secondaryAlbumTypes = new List(); + } + + var httpRequest = customerRequestBuilder.Create() .SetSegment("route", "artists/" + foreignArtistId) + .AddQueryParam("primTypes", string.Join("|",primaryAlbumTypes)) + .AddQueryParam("secTypes", string.Join("|", secondaryAlbumTypes)) .Build(); - - httpRequest.AllowAutoRedirect = true; httpRequest.SuppressHttpError = true; var httpResponse = _httpClient.Get(httpRequest); - + if (httpResponse.HasHttpError) { @@ -89,7 +101,7 @@ namespace NzbDrone.Core.MetadataSource.SkyHook try { - return new List { GetArtistInfo(slug).Item1 }; + return new List { GetArtistInfo(slug, new List{"Album"}, new List{"Studio"}).Item1 }; } catch (ArtistNotFoundException) { @@ -97,7 +109,9 @@ namespace NzbDrone.Core.MetadataSource.SkyHook } } - var httpRequest = _requestBuilder.Create() + SetCustomProvider(); + + var httpRequest = customerRequestBuilder.Create() .SetSegment("route", "search") .AddQueryParam("type", "artist") .AddQueryParam("query", title.ToLower().Trim()) @@ -106,18 +120,30 @@ namespace NzbDrone.Core.MetadataSource.SkyHook var httpResponse = _httpClient.Get>(httpRequest); - - return httpResponse.Resource.SelectList(MapArtist); + + return httpResponse.Resource.SelectList(MapSearhResult); } catch (HttpException) { - throw new SkyHookException("Search for '{0}' failed. Unable to communicate with SkyHook.", title); + throw new SkyHookException("Search for '{0}' failed. Unable to communicate with LidarrAPI.", title); } catch (Exception ex) { _logger.Warn(ex, ex.Message); - throw new SkyHookException("Search for '{0}' failed. Invalid response received from SkyHook.", title); + throw new SkyHookException("Search for '{0}' failed. Invalid response received from LidarrAPI.", title); + } + } + + private Artist MapSearhResult(ArtistResource resource) + { + var artist = _artistService.FindById(resource.Id); + + if (artist == null) + { + artist = MapArtist(resource); } + + return artist; } private static Album MapAlbum(AlbumResource resource) @@ -126,13 +152,14 @@ namespace NzbDrone.Core.MetadataSource.SkyHook album.Title = resource.Title; album.ForeignAlbumId = resource.Id; album.ReleaseDate = resource.ReleaseDate; - album.CleanTitle = Parser.Parser.CleanArtistTitle(album.Title); + album.CleanTitle = Parser.Parser.CleanArtistName(album.Title); album.AlbumType = resource.Type; album.Images = resource.Images.Select(MapImage).ToList(); + album.Label = resource.Label; var tracks = resource.Tracks.Select(MapTrack); album.Tracks = tracks.ToList(); - + return album; } @@ -149,58 +176,68 @@ namespace NzbDrone.Core.MetadataSource.SkyHook private static Artist MapArtist(ArtistResource resource) { - + Artist artist = new Artist(); artist.Name = resource.ArtistName; artist.ForeignArtistId = resource.Id; artist.Genres = resource.Genres; artist.Overview = resource.Overview; - artist.NameSlug = Parser.Parser.CleanArtistTitle(artist.Name); - artist.CleanName = Parser.Parser.CleanArtistTitle(artist.Name); - artist.SortName = SeriesTitleNormalizer.Normalize(artist.Name,0); + artist.NameSlug = Parser.Parser.CleanArtistName(artist.Name) + "-" + resource.Id.Substring(resource.Id.Length - 6); + artist.CleanName = Parser.Parser.CleanArtistName(artist.Name); + artist.SortName = Parser.Parser.NormalizeTitle(artist.Name); + artist.Disambiguation = resource.Disambiguation; + artist.ArtistType = resource.Type; artist.Images = resource.Images.Select(MapImage).ToList(); + artist.Status = MapArtistStatus(resource.Status); + artist.Ratings = MapRatings(resource.Rating); + artist.Links = resource.Links.Select(MapLink).ToList(); return artist; } - private static Actor MapActors(ActorResource arg) + private static Member MapMembers(MemberResource arg) { - var newActor = new Actor + var newMember = new Member { Name = arg.Name, - Character = arg.Character + Instrument = arg.Instrument }; if (arg.Image != null) { - newActor.Images = new List + newMember.Images = new List { new MediaCover.MediaCover(MediaCoverTypes.Headshot, arg.Image) }; } - return newActor; + return newMember; } - private static SeriesStatusType MapArtistStatus(string status) + private static ArtistStatusType MapArtistStatus(string status) { + if (status == null) + { + return ArtistStatusType.Continuing; + } + if (status.Equals("ended", StringComparison.InvariantCultureIgnoreCase)) { - return SeriesStatusType.Ended; + return ArtistStatusType.Ended; } - return SeriesStatusType.Continuing; + return ArtistStatusType.Continuing; } - private static Core.Music.Ratings MapRatings(RatingResource rating) + private static Music.Ratings MapRatings(RatingResource rating) { if (rating == null) { - return new Core.Music.Ratings(); + return new Music.Ratings(); } - return new Core.Music.Ratings + return new Music.Ratings { Votes = rating.Count, Value = rating.Value @@ -216,6 +253,15 @@ namespace NzbDrone.Core.MetadataSource.SkyHook }; } + private static Music.Links MapLink(LinkResource arg) + { + return new Music.Links + { + Url = arg.Target, + Name = arg.Type + }; + } + private static MediaCoverTypes MapCoverType(string coverType) { switch (coverType.ToLower()) @@ -230,9 +276,23 @@ namespace NzbDrone.Core.MetadataSource.SkyHook return MediaCoverTypes.Cover; case "disc": return MediaCoverTypes.Disc; + case "logo": + return MediaCoverTypes.Logo; default: return MediaCoverTypes.Unknown; } } + + private void SetCustomProvider() + { + if (_configService.MetadataSource.IsNotNullOrWhiteSpace()) + { + customerRequestBuilder = new HttpRequestBuilder(_configService.MetadataSource.TrimEnd("/") + "/{route}/").CreateFactory(); + } + else + { + customerRequestBuilder = _requestBuilder; + } + } } } diff --git a/src/NzbDrone.Core/Music/AddArtistOptions.cs b/src/NzbDrone.Core/Music/AddArtistOptions.cs index 5f83e1c72..2e2c198c1 100644 --- a/src/NzbDrone.Core/Music/AddArtistOptions.cs +++ b/src/NzbDrone.Core/Music/AddArtistOptions.cs @@ -1,4 +1,3 @@ -using NzbDrone.Core.Tv; using System; using System.Collections.Generic; using System.Linq; diff --git a/src/NzbDrone.Core/Music/AddArtistService.cs b/src/NzbDrone.Core/Music/AddArtistService.cs index 1ed528c99..5c0eaf04e 100644 --- a/src/NzbDrone.Core/Music/AddArtistService.cs +++ b/src/NzbDrone.Core/Music/AddArtistService.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.IO; using System.Linq; @@ -53,7 +53,7 @@ namespace NzbDrone.Core.Music newArtist.Path = Path.Combine(newArtist.RootFolderPath, folderName); } - newArtist.CleanName = newArtist.Name.CleanArtistTitle(); + newArtist.CleanName = newArtist.Name.CleanArtistName(); newArtist.SortName = ArtistNameNormalizer.Normalize(newArtist.Name, newArtist.ForeignArtistId); // There is no Sort Title newArtist.Added = DateTime.UtcNow; @@ -86,7 +86,7 @@ namespace NzbDrone.Core.Music try { - tuple = _artistInfo.GetArtistInfo(newArtist.ForeignArtistId); + tuple = _artistInfo.GetArtistInfo(newArtist.ForeignArtistId, newArtist.PrimaryAlbumTypes, newArtist.SecondaryAlbumTypes); } catch (ArtistNotFoundException) { diff --git a/src/NzbDrone.Core/Music/AddArtistValidator.cs b/src/NzbDrone.Core/Music/AddArtistValidator.cs index bc860f09e..4a8f84967 100644 --- a/src/NzbDrone.Core/Music/AddArtistValidator.cs +++ b/src/NzbDrone.Core/Music/AddArtistValidator.cs @@ -1,4 +1,4 @@ -using FluentValidation; +using FluentValidation; using FluentValidation.Results; using NzbDrone.Core.Validation.Paths; using System; @@ -16,17 +16,15 @@ namespace NzbDrone.Core.Music public class AddArtistValidator : AbstractValidator, IAddArtistValidator { public AddArtistValidator(RootFolderValidator rootFolderValidator, - SeriesPathValidator seriesPathValidator, - DroneFactoryValidator droneFactoryValidator, - SeriesAncestorValidator seriesAncestorValidator, + ArtistPathValidator artistPathValidator, + ArtistAncestorValidator artistAncestorValidator, ArtistSlugValidator artistTitleSlugValidator) { RuleFor(c => c.Path).Cascade(CascadeMode.StopOnFirstFailure) .IsValidPath() .SetValidator(rootFolderValidator) - .SetValidator(seriesPathValidator) - .SetValidator(droneFactoryValidator) - .SetValidator(seriesAncestorValidator); + .SetValidator(artistPathValidator) + .SetValidator(artistAncestorValidator); RuleFor(c => c.NameSlug).SetValidator(artistTitleSlugValidator);// TODO: Check if we are going to use a slug or artistName } diff --git a/src/NzbDrone.Core/Music/Album.cs b/src/NzbDrone.Core/Music/Album.cs index e46a70ceb..85b5f4d04 100644 --- a/src/NzbDrone.Core/Music/Album.cs +++ b/src/NzbDrone.Core/Music/Album.cs @@ -1,6 +1,5 @@ -using NzbDrone.Common.Extensions; +using NzbDrone.Common.Extensions; using NzbDrone.Core.Datastore; -using NzbDrone.Core.Tv; using System; using System.Collections.Generic; using System.Linq; @@ -22,7 +21,7 @@ namespace NzbDrone.Core.Music public string Title { get; set; } public string CleanTitle { get; set; } public DateTime? ReleaseDate { get; set; } - public string Label { get; set; } + public List Label { get; set; } //public int TrackCount { get; set; } public string Path { get; set; } public int ProfileId { get; set; } @@ -39,7 +38,7 @@ namespace NzbDrone.Core.Music public String AlbumType { get; set; } // TODO: Turn this into a type similar to Series Type in TV //public string ArtworkUrl { get; set; } //public string Explicitness { get; set; } - public AddSeriesOptions AddOptions { get; set; } + public AddArtistOptions AddOptions { get; set; } public Artist Artist { get; set; } public Ratings Ratings { get; set; } diff --git a/src/NzbDrone.Core/Music/AlbumCutoffService.cs b/src/NzbDrone.Core/Music/AlbumCutoffService.cs new file mode 100644 index 000000000..58eb2cf05 --- /dev/null +++ b/src/NzbDrone.Core/Music/AlbumCutoffService.cs @@ -0,0 +1,65 @@ +using System.Collections.Generic; +using System.Linq; +using NLog; +using NzbDrone.Core.Datastore; +using NzbDrone.Core.Languages; +using NzbDrone.Core.Profiles.Qualities; +using NzbDrone.Core.Profiles.Languages; +using NzbDrone.Core.Qualities; + +namespace NzbDrone.Core.Music +{ + public interface IAlbumCutoffService + { + PagingSpec AlbumsWhereCutoffUnmet(PagingSpec pagingSpec); + } + + public class AlbumCutoffService : IAlbumCutoffService + { + private readonly IAlbumRepository _albumRepository; + private readonly IProfileService _profileService; + private readonly ILanguageProfileService _languageProfileService; + private readonly Logger _logger; + + public AlbumCutoffService(IAlbumRepository albumRepository, IProfileService profileService, ILanguageProfileService languageProfileService, Logger logger) + { + _albumRepository = albumRepository; + _profileService = profileService; + _languageProfileService = languageProfileService; + _logger = logger; + } + + public PagingSpec AlbumsWhereCutoffUnmet(PagingSpec pagingSpec) + { + var qualitiesBelowCutoff = new List(); + var languagesBelowCutoff = new List(); + var profiles = _profileService.All(); + var languageProfiles = _languageProfileService.All(); + + //Get all items less than the cutoff + foreach (var profile in profiles) + { + var cutoffIndex = profile.Items.FindIndex(v => v.Quality == profile.Cutoff); + var belowCutoff = profile.Items.Take(cutoffIndex).ToList(); + + if (belowCutoff.Any()) + { + qualitiesBelowCutoff.Add(new QualitiesBelowCutoff(profile.Id, belowCutoff.Select(i => i.Quality.Id))); + } + } + + foreach (var profile in languageProfiles) + { + var languageCutoffIndex = profile.Languages.FindIndex(v => v.Language == profile.Cutoff); + var belowLanguageCutoff = profile.Languages.Take(languageCutoffIndex).ToList(); + + if (belowLanguageCutoff.Any()) + { + languagesBelowCutoff.Add(new LanguagesBelowCutoff(profile.Id, belowLanguageCutoff.Select(l => l.Language.Id))); + } + } + + return _albumRepository.AlbumsWhereCutoffUnmet(pagingSpec, qualitiesBelowCutoff, languagesBelowCutoff); + } + } +} diff --git a/src/NzbDrone.Core/Music/AlbumMonitoredService.cs b/src/NzbDrone.Core/Music/AlbumMonitoredService.cs index c19965f41..6ffb30971 100644 --- a/src/NzbDrone.Core/Music/AlbumMonitoredService.cs +++ b/src/NzbDrone.Core/Music/AlbumMonitoredService.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using NLog; @@ -57,7 +57,7 @@ namespace NzbDrone.Core.Music foreach (var album in albums) { album.Monitored = monitored; - var tracks = _trackService.GetTracksByAlbum(album.ArtistId, album.Id); + var tracks = _trackService.GetTracksByAlbum(album.Id); foreach (var track in tracks) { track.Monitored = monitored; diff --git a/src/NzbDrone.Core/Music/AlbumRepository.cs b/src/NzbDrone.Core/Music/AlbumRepository.cs index e7c113322..439e9c698 100644 --- a/src/NzbDrone.Core/Music/AlbumRepository.cs +++ b/src/NzbDrone.Core/Music/AlbumRepository.cs @@ -1,10 +1,12 @@ -using System; +using System; using System.Linq; using Marr.Data.QGen; using NzbDrone.Core.Datastore; using NzbDrone.Core.Datastore.Extensions; using System.Collections.Generic; using NzbDrone.Core.Messaging.Events; +using NzbDrone.Core.Languages; +using NzbDrone.Core.Qualities; using Marr.Data.QGen; namespace NzbDrone.Core.Music @@ -18,17 +20,22 @@ namespace NzbDrone.Core.Music Album FindByArtistAndName(string artistName, string cleanTitle); Album FindById(string spotifyId); PagingSpec AlbumsWithoutFiles(PagingSpec pagingSpec); + PagingSpec AlbumsWhereCutoffUnmet(PagingSpec pagingSpec, List qualitiesBelowCutoff, List languagesBelowCutoff); List AlbumsBetweenDates(DateTime startDate, DateTime endDate, bool includeUnmonitored); void SetMonitoredFlat(Album album, bool monitored); + void SetMonitored(IEnumerable ids, bool monitored); } public class AlbumRepository : BasicRepository, IAlbumRepository { + private readonly IMainDatabase _database; + public AlbumRepository(IMainDatabase database, IEventAggregator eventAggregator) : base(database, eventAggregator) { + _database = database; } - + public bool AlbumPathExists(string path) { @@ -57,6 +64,15 @@ namespace NzbDrone.Core.Music return pagingSpec; } + public PagingSpec AlbumsWhereCutoffUnmet(PagingSpec pagingSpec, List qualitiesBelowCutoff, List languagesBelowCutoff) + { + + pagingSpec.TotalRecords = GetCutOffAlbumsQueryCount(pagingSpec, qualitiesBelowCutoff, languagesBelowCutoff); + pagingSpec.Records = GetCutOffAlbumsQuery(pagingSpec, qualitiesBelowCutoff, languagesBelowCutoff).ToList(); + + return pagingSpec; + } + public List AlbumsBetweenDates(DateTime startDate, DateTime endDate, bool includeUnmonitored) { var query = Query.Join(JoinType.Inner, e => e.Artist, (e, s) => e.ArtistId == s.Id) @@ -76,35 +92,39 @@ namespace NzbDrone.Core.Music private QueryBuilder GetMissingAlbumsQuery(PagingSpec pagingSpec, DateTime currentTime) { string sortKey; - string monitored = "([t0].[Monitored] = 0) OR ([t1].[Monitored] = 0)"; + string monitored = "(Albums.[Monitored] = 0) OR (Artists.[Monitored] = 0)"; if (pagingSpec.FilterExpression.ToString().Contains("True")) { - monitored = "([t0].[Monitored] = 1) AND ([t1].[Monitored] = 1)"; + monitored = "(Albums.[Monitored] = 1) AND (Artists.[Monitored] = 1)"; } if (pagingSpec.SortKey == "releaseDate") { - sortKey = "[t0]." + pagingSpec.SortKey; + sortKey = "Albums." + pagingSpec.SortKey; } else if (pagingSpec.SortKey == "artist.sortName") { - sortKey = "[t1]." + pagingSpec.SortKey.Split('.').Last(); + sortKey = "Artists." + pagingSpec.SortKey.Split('.').Last(); } else { - sortKey = "[t0].releaseDate"; + sortKey = "Albums.releaseDate"; } - string query = string.Format("SELECT * FROM Albums [t0] INNER JOIN Artists [t1] ON ([t0].[ArtistId] = [t1].[Id])" + - "WHERE ({0}) AND {1}" + - " AND NOT EXISTS (SELECT 1 from Tracks [t2] WHERE [t2].albumId = [t0].id AND [t2].trackFileId <> 0) ORDER BY {2} {3} LIMIT {4} OFFSET {5}", - monitored, BuildReleaseDateCutoffWhereClause(currentTime), sortKey, pagingSpec.ToSortDirection(), pagingSpec.PageSize, pagingSpec.PagingOffset()); + string query = string.Format("SELECT Albums.* FROM (SELECT Tracks.AlbumId, COUNT(*) AS TotalTrackCount," + "" + + "SUM(CASE WHEN TrackFileId > 0 THEN 1 ELSE 0 END) AS AvailableTrackCount FROM Tracks GROUP BY Tracks.ArtistId, Tracks.AlbumId) as Tracks" + + " LEFT OUTER JOIN Albums ON Tracks.AlbumId == Albums.Id" + + " LEFT OUTER JOIN Artists ON Albums.ArtistId == Artists.Id" + + " WHERE Tracks.TotalTrackCount != Tracks.AvailableTrackCount AND ({0}) AND {1}" + + " GROUP BY Tracks.AlbumId" + + " ORDER BY {2} {3} LIMIT {4} OFFSET {5}", + monitored, BuildReleaseDateCutoffWhereClause(currentTime), sortKey, pagingSpec.ToSortDirection(), pagingSpec.PageSize, pagingSpec.PagingOffset()); - return Query.QueryText(query); + return Query.QueryText(query); //Use Manual Query until we find a way to "NOT EXIST(SELECT 1 from Tracks WHERE [t2].trackFileId <> 0)" - + //return Query.Join(JoinType.Inner, e => e.Artist, (e, s) => e.ArtistId == s.Id) // .Where(pagingSpec.FilterExpression) // .AndWhere(BuildReleaseDateCutoffWhereClause(currentTime)) @@ -122,10 +142,13 @@ namespace NzbDrone.Core.Music { monitored = 1; } - - string query = string.Format("SELECT * FROM Albums [t0] INNER JOIN Artists [t1] ON ([t0].[ArtistId] = [t1].[Id])" + - "WHERE (([t0].[Monitored] = {0}) AND ([t1].[Monitored] = {0})) AND {1}" + - " AND NOT EXISTS (SELECT 1 from Tracks [t2] WHERE [t2].albumId = [t0].id AND [t2].trackFileId <> 0)", + + string query = string.Format("SELECT Albums.* FROM (SELECT Tracks.AlbumId, COUNT(*) AS TotalTrackCount," + + " SUM(CASE WHEN TrackFileId > 0 THEN 1 ELSE 0 END) AS AvailableTrackCount FROM Tracks GROUP BY Tracks.ArtistId, Tracks.AlbumId) as Tracks" + + " LEFT OUTER JOIN Albums ON Tracks.AlbumId == Albums.Id" + + " LEFT OUTER JOIN Artists ON Albums.ArtistId == Artists.Id" + + " WHERE Tracks.TotalTrackCount != Tracks.AvailableTrackCount AND ({0}) AND {1}" + + " GROUP BY Tracks.AlbumId", monitored, BuildReleaseDateCutoffWhereClause(currentTime)); return Query.QueryText(query).Count(); @@ -133,16 +156,116 @@ namespace NzbDrone.Core.Music private string BuildReleaseDateCutoffWhereClause(DateTime currentTime) { - return string.Format("datetime(strftime('%s', [t0].[ReleaseDate]), 'unixepoch') <= '{0}'", + return string.Format("datetime(strftime('%s', Albums.[ReleaseDate]), 'unixepoch') <= '{0}'", currentTime.ToString("yyyy-MM-dd HH:mm:ss")); } + private QueryBuilder GetCutOffAlbumsQuery(PagingSpec pagingSpec, List qualitiesBelowCutoff, List languagesBelowCutoff) + { + string sortKey; + string monitored = "(Albums.[Monitored] = 0) OR (Artists.[Monitored] = 0)"; + + if (pagingSpec.FilterExpression.ToString().Contains("True")) + { + monitored = "(Albums.[Monitored] = 1) AND (Artists.[Monitored] = 1)"; + } + + if (pagingSpec.SortKey == "releaseDate") + { + sortKey = "Albums." + pagingSpec.SortKey; + } + else if (pagingSpec.SortKey == "artist.sortName") + { + sortKey = "Artists." + pagingSpec.SortKey.Split('.').Last(); + } + else + { + sortKey = "Albums.releaseDate"; + } + + string query = string.Format("SELECT Albums.* FROM(SELECT TrackFiles.AlbumId, TrackFiles.Language, COUNT(*) AS FileCount, " + + " MIN(Quality) AS MinQuality FROM TrackFiles GROUP BY TrackFiles.ArtistId, TrackFiles.AlbumId) as TrackFiles" + + " LEFT OUTER JOIN Albums ON TrackFiles.AlbumId == Albums.Id" + + " LEFT OUTER JOIN Artists ON Albums.ArtistId == Artists.Id" + + " WHERE ({0}) AND ({1} OR {2})" + + " GROUP BY TrackFiles.AlbumId" + + " ORDER BY {3} {4} LIMIT {5} OFFSET {6}", + monitored, BuildQualityCutoffWhereClause(qualitiesBelowCutoff), BuildLanguageCutoffWhereClause(languagesBelowCutoff), sortKey, pagingSpec.ToSortDirection(), pagingSpec.PageSize, pagingSpec.PagingOffset()); + + return Query.QueryText(query); + + } + + private int GetCutOffAlbumsQueryCount(PagingSpec pagingSpec, List qualitiesBelowCutoff, List languagesBelowCutoff) + { + var monitored = 0; + + if (pagingSpec.FilterExpression.ToString().Contains("True")) + { + monitored = 1; + } + + string query = string.Format("SELECT Albums.* FROM (SELECT TrackFiles.AlbumId, TrackFiles.Language, COUNT(*) AS FileCount," + + " MIN(Quality) AS MinQuality FROM TrackFiles GROUP BY TrackFiles.ArtistId, TrackFiles.AlbumId) as TrackFiles" + + " LEFT OUTER JOIN Albums ON TrackFiles.AlbumId == Albums.Id" + + " LEFT OUTER JOIN Artists ON Albums.ArtistId == Artists.Id" + + " WHERE ({0}) AND ({1} OR {2})" + + " GROUP BY TrackFiles.AlbumId", + monitored, BuildQualityCutoffWhereClause(qualitiesBelowCutoff), BuildLanguageCutoffWhereClause(languagesBelowCutoff)); + + return Query.QueryText(query).Count(); + } + + + private string BuildLanguageCutoffWhereClause(List languagesBelowCutoff) + { + var clauses = new List(); + + foreach (var language in languagesBelowCutoff) + { + foreach (var belowCutoff in language.LanguageIds) + { + clauses.Add(String.Format("(Artists.[LanguageProfileId] = {0} AND TrackFiles.[Language] = {1})", language.ProfileId, belowCutoff)); + } + } + + return String.Format("({0})", String.Join(" OR ", clauses)); + } + + private string BuildQualityCutoffWhereClause(List qualitiesBelowCutoff) + { + var clauses = new List(); + + foreach (var profile in qualitiesBelowCutoff) + { + foreach (var belowCutoff in profile.QualityIds) + { + clauses.Add(string.Format("(Artists.[ProfileId] = {0} AND TrackFiles.MinQuality LIKE '%_quality_: {1},%')", profile.ProfileId, belowCutoff)); + } + } + + return string.Format("({0})", string.Join(" OR ", clauses)); + } + public void SetMonitoredFlat(Album album, bool monitored) { album.Monitored = monitored; SetFields(album, p => p.Monitored); } + public void SetMonitored(IEnumerable ids, bool monitored) + { + var mapper = _database.GetDataMapper(); + + mapper.AddParameter("monitored", monitored); + + var sql = "UPDATE Albums " + + "SET Monitored = @monitored " + + $"WHERE Id IN ({string.Join(", ", ids)})"; + + mapper.ExecuteNonQuery(sql); + } + public Album FindByName(string cleanTitle) { cleanTitle = cleanTitle.ToLowerInvariant(); @@ -153,21 +276,21 @@ namespace NzbDrone.Core.Music public Album FindByTitle(int artistId, string title) { - title = Parser.Parser.CleanArtistTitle(title); + title = Parser.Parser.CleanArtistName(title); return Query.Where(s => s.CleanTitle == title) .AndWhere(s => s.ArtistId == artistId) - .SingleOrDefault(); + .FirstOrDefault(); } public Album FindByArtistAndName(string artistName, string cleanTitle) { - var cleanArtistName = Parser.Parser.CleanArtistTitle(artistName); + var cleanArtistName = Parser.Parser.CleanArtistName(artistName); cleanTitle = cleanTitle.ToLowerInvariant(); var query = Query.Join(JoinType.Inner, album => album.Artist, (album, artist) => album.ArtistId == artist.Id) .Where(artist => artist.CleanName == cleanArtistName) .Where(album => album.CleanTitle == cleanTitle); - return Query.Join(JoinType.Inner, album => album.Artist, (album, artist) => album.ArtistId == artist.Id ) + return Query.Join(JoinType.Inner, album => album.Artist, (album, artist) => album.ArtistId == artist.Id) .Where(artist => artist.CleanName == cleanArtistName) .Where(album => album.CleanTitle == cleanTitle) .SingleOrDefault(); diff --git a/src/NzbDrone.Core/Music/AlbumService.cs b/src/NzbDrone.Core/Music/AlbumService.cs index c6017ea06..0e233968d 100644 --- a/src/NzbDrone.Core/Music/AlbumService.cs +++ b/src/NzbDrone.Core/Music/AlbumService.cs @@ -1,4 +1,4 @@ -using NLog; +using NLog; using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Music.Events; using NzbDrone.Core.Organizer; @@ -27,6 +27,7 @@ namespace NzbDrone.Core.Music Album UpdateAlbum(Album album); List UpdateAlbums(List album); void SetAlbumMonitored(int albumId, bool monitored); + void SetMonitored(IEnumerable ids, bool monitored); PagingSpec AlbumsWithoutFiles(PagingSpec pagingSpec); List AlbumsBetweenDates(DateTime start, DateTime end, bool includeUnmonitored); void InsertMany(List albums); @@ -163,9 +164,30 @@ namespace NzbDrone.Core.Music var album = _albumRepository.Get(albumId); _albumRepository.SetMonitoredFlat(album, monitored); + var tracks = _trackService.GetTracksByAlbum(albumId); + foreach (var track in tracks) + { + track.Monitored = monitored; + } + _trackService.UpdateTracks(tracks); + _logger.Debug("Monitored flag for Album:{0} was set to {1}", albumId, monitored); } + public void SetMonitored(IEnumerable ids, bool monitored) + { + _albumRepository.SetMonitored(ids, monitored); + foreach (var id in ids) + { + var tracks = _trackService.GetTracksByAlbum(id); + foreach (var track in tracks) + { + track.Monitored = monitored; + } + _trackService.UpdateTracks(tracks); + } + } + public List UpdateAlbums(List album) { _logger.Debug("Updating {0} album", album.Count); @@ -181,10 +203,5 @@ namespace NzbDrone.Core.Music var albums = GetAlbumsByArtist(message.Artist.Id); _albumRepository.DeleteMany(albums); } - - public void Handle(ArtistDeletedEvent message) - { - throw new NotImplementedException(); - } } } diff --git a/src/NzbDrone.Core/Music/Artist.cs b/src/NzbDrone.Core/Music/Artist.cs index 7e803bd70..bd7be5695 100644 --- a/src/NzbDrone.Core/Music/Artist.cs +++ b/src/NzbDrone.Core/Music/Artist.cs @@ -1,7 +1,8 @@ -using Marr.Data; +using Marr.Data; using NzbDrone.Common.Extensions; using NzbDrone.Core.Datastore; -using NzbDrone.Core.Profiles; +using NzbDrone.Core.Profiles.Qualities; +using NzbDrone.Core.Profiles.Languages; using NzbDrone.Core.Music; using System; using System.Collections.Generic; @@ -19,6 +20,7 @@ namespace NzbDrone.Core.Music Members = new List(); Albums = new List(); Tags = new HashSet(); + Links = new List(); } @@ -32,18 +34,25 @@ namespace NzbDrone.Core.Music public string CleanName { get; set; } public string SortName { get; set; } public string Overview { get; set; } + public string Disambiguation { get; set; } + public string ArtistType { get; set; } + public List PrimaryAlbumTypes { get; set; } + public List SecondaryAlbumTypes { get; set; } public bool Monitored { get; set; } public bool AlbumFolder { get; set; } public DateTime? LastInfoSync { get; set; } public DateTime? LastDiskSync { get; set; } - public int Status { get; set; } // TODO: Figure out what this is, do we need it? + public ArtistStatusType Status { get; set; } public string Path { get; set; } public List Images { get; set; } + public List Links { get; set; } public List Genres { get; set; } public string RootFolderPath { get; set; } public DateTime Added { get; set; } public LazyLoaded Profile { get; set; } + public LazyLoaded LanguageProfile { get; set; } public int ProfileId { get; set; } + public int LanguageProfileId { get; set; } public List Albums { get; set; } public HashSet Tags { get; set; } public AddArtistOptions AddOptions { get; set; } @@ -63,8 +72,11 @@ namespace NzbDrone.Core.Music Path = otherArtist.Path; Profile = otherArtist.Profile; + LanguageProfileId = otherArtist.LanguageProfileId; Albums = otherArtist.Albums; + PrimaryAlbumTypes = otherArtist.PrimaryAlbumTypes; + SecondaryAlbumTypes = otherArtist.SecondaryAlbumTypes; ProfileId = otherArtist.ProfileId; Tags = otherArtist.Tags; diff --git a/src/NzbDrone.Core/Music/ArtistEditedService.cs b/src/NzbDrone.Core/Music/ArtistEditedService.cs new file mode 100644 index 000000000..cc7d062ba --- /dev/null +++ b/src/NzbDrone.Core/Music/ArtistEditedService.cs @@ -0,0 +1,26 @@ +using NzbDrone.Core.Messaging.Commands; +using NzbDrone.Core.Messaging.Events; +using NzbDrone.Core.Music.Commands; +using NzbDrone.Core.Music.Events; + +namespace NzbDrone.Core.Music +{ + public class ArtistEditedService : IHandle + { + private readonly IManageCommandQueue _commandQueueManager; + + public ArtistEditedService(IManageCommandQueue commandQueueManager) + { + _commandQueueManager = commandQueueManager; + } + + public void Handle(ArtistEditedEvent message) + { + // Refresh Artist is we change AlbumType Preferences + if (message.Artist.PrimaryAlbumTypes != message.OldArtist.PrimaryAlbumTypes || message.Artist.SecondaryAlbumTypes != message.OldArtist.SecondaryAlbumTypes) + { + _commandQueueManager.Push(new RefreshArtistCommand(message.Artist.Id)); + } + } + } +} diff --git a/src/NzbDrone.Core/Music/ArtistScannedHandler.cs b/src/NzbDrone.Core/Music/ArtistScannedHandler.cs new file mode 100644 index 000000000..f1493c2d8 --- /dev/null +++ b/src/NzbDrone.Core/Music/ArtistScannedHandler.cs @@ -0,0 +1,62 @@ +using NLog; +using NzbDrone.Core.IndexerSearch; +using NzbDrone.Core.MediaFiles.Events; +using NzbDrone.Core.Messaging.Commands; +using NzbDrone.Core.Messaging.Events; + +namespace NzbDrone.Core.Music +{ + public class ArtistScannedHandler : IHandle, + IHandle + { + private readonly IAlbumMonitoredService _albumMonitoredService; + private readonly IArtistService _artistService; + private readonly IManageCommandQueue _commandQueueManager; + //private readonly IEpisodeAddedService _episodeAddedService; + + private readonly Logger _logger; + + public ArtistScannedHandler(IAlbumMonitoredService albumMonitoredService, + IArtistService artistService, + IManageCommandQueue commandQueueManager, + //IEpisodeAddedService episodeAddedService, + Logger logger) + { + _albumMonitoredService = albumMonitoredService; + _artistService = artistService; + _commandQueueManager = commandQueueManager; + //_episodeAddedService = episodeAddedService; + _logger = logger; + } + + private void HandleScanEvents(Artist artist) + { + if (artist.AddOptions == null) + { + //_episodeAddedService.SearchForRecentlyAdded(series.Id); + return; + } + + _logger.Info("[{0}] was recently added, performing post-add actions", artist.Name); + _albumMonitoredService.SetAlbumMonitoredStatus(artist, artist.AddOptions); + + if (artist.AddOptions.SearchForMissingTracks) + { + _commandQueueManager.Push(new MissingAlbumSearchCommand(artist.Id)); + } + + artist.AddOptions = null; + _artistService.RemoveAddOptions(artist); + } + + public void Handle(ArtistScannedEvent message) + { + HandleScanEvents(message.Artist); + } + + public void Handle(ArtistScanSkippedEvent message) + { + HandleScanEvents(message.Artist); + } + } +} diff --git a/src/NzbDrone.Core/Music/ArtistService.cs b/src/NzbDrone.Core/Music/ArtistService.cs index 1297a42e9..9bd021192 100644 --- a/src/NzbDrone.Core/Music/ArtistService.cs +++ b/src/NzbDrone.Core/Music/ArtistService.cs @@ -1,4 +1,4 @@ -using NLog; +using NLog; using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Music.Events; using NzbDrone.Core.Organizer; @@ -22,6 +22,7 @@ namespace NzbDrone.Core.Music Artist FindByTitleInexact(string title); void DeleteArtist(int artistId, bool deleteFiles); List GetAllArtists(); + List AllForTag(int tagId); Artist UpdateArtist(Artist artist); List UpdateArtists(List artist); bool ArtistPathExists(string folder); @@ -76,7 +77,7 @@ namespace NzbDrone.Core.Music public Artist FindByName(string title) { - return _artistRepository.FindByName(title.CleanArtistTitle()); + return _artistRepository.FindByName(title.CleanArtistName()); } public Artist FindByTitleInexact(string title) @@ -89,6 +90,12 @@ namespace NzbDrone.Core.Music return _artistRepository.All().ToList(); } + public List AllForTag(int tagId) + { + return GetAllArtists().Where(s => s.Tags.Contains(tagId)) + .ToList(); + } + public Artist GetArtist(int artistDBId) { return _artistRepository.Get(artistDBId); diff --git a/src/NzbDrone.Core/Music/ArtistSlugValidator.cs b/src/NzbDrone.Core/Music/ArtistSlugValidator.cs index 1d894fee2..bbaa0c24b 100644 --- a/src/NzbDrone.Core/Music/ArtistSlugValidator.cs +++ b/src/NzbDrone.Core/Music/ArtistSlugValidator.cs @@ -1,7 +1,8 @@ -using FluentValidation.Validators; +using System.Linq; +using FluentValidation.Validators; +using NzbDrone.Common.Extensions; using System; using System.Collections.Generic; -using System.Linq; using System.Text; namespace NzbDrone.Core.Music @@ -11,7 +12,7 @@ namespace NzbDrone.Core.Music private readonly IArtistService _artistService; public ArtistSlugValidator(IArtistService artistService) - : base("Title slug is in use by another artist with a similar name") + : base("Name slug '{slug}' is in use by artist '{artistName}'") { _artistService = artistService; } @@ -22,8 +23,22 @@ namespace NzbDrone.Core.Music dynamic instance = context.ParentContext.InstanceToValidate; var instanceId = (int)instance.Id; + var slug = context.PropertyValue.ToString(); + + var conflictingArtist = _artistService.GetAllArtists() + .FirstOrDefault(s => s.NameSlug.IsNotNullOrWhiteSpace() && + s.NameSlug.Equals(context.PropertyValue.ToString()) && + s.Id != instanceId); + + if (conflictingArtist == null) + { + return true; + } + + context.MessageFormatter.AppendArgument("slug", slug); + context.MessageFormatter.AppendArgument("artistName", conflictingArtist.Name); - return !_artistService.GetAllArtists().Exists(s => s.NameSlug.Equals(context.PropertyValue.ToString()) && s.Id != instanceId); + return false; } } } diff --git a/src/NzbDrone.Core/Music/ArtistStatusType.cs b/src/NzbDrone.Core/Music/ArtistStatusType.cs new file mode 100644 index 000000000..478959016 --- /dev/null +++ b/src/NzbDrone.Core/Music/ArtistStatusType.cs @@ -0,0 +1,8 @@ +namespace NzbDrone.Core.Music +{ + public enum ArtistStatusType + { + Continuing = 0, + Ended = 1 + } +} diff --git a/src/NzbDrone.Core/Music/Commands/RefreshArtistCommand.cs b/src/NzbDrone.Core/Music/Commands/RefreshArtistCommand.cs index fdf3e56d6..be4556ffa 100644 --- a/src/NzbDrone.Core/Music/Commands/RefreshArtistCommand.cs +++ b/src/NzbDrone.Core/Music/Commands/RefreshArtistCommand.cs @@ -1,8 +1,4 @@ -using NzbDrone.Core.Messaging.Commands; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; +using NzbDrone.Core.Messaging.Commands; namespace NzbDrone.Core.Music.Commands { diff --git a/src/NzbDrone.Core/Music/Links.cs b/src/NzbDrone.Core/Music/Links.cs new file mode 100644 index 000000000..abf47eafc --- /dev/null +++ b/src/NzbDrone.Core/Music/Links.cs @@ -0,0 +1,10 @@ +using NzbDrone.Core.Datastore; + +namespace NzbDrone.Core.Music +{ + public class Links : IEmbeddedDocument + { + public string Url { get; set; } + public string Name { get; set; } + } +} diff --git a/src/NzbDrone.Core/Music/RefreshAlbumService.cs b/src/NzbDrone.Core/Music/RefreshAlbumService.cs index 4fddf1d6b..d9c6a349e 100644 --- a/src/NzbDrone.Core/Music/RefreshAlbumService.cs +++ b/src/NzbDrone.Core/Music/RefreshAlbumService.cs @@ -1,4 +1,4 @@ -using NLog; +using NLog; using NzbDrone.Common.Extensions; using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Music.Events; @@ -76,10 +76,11 @@ namespace NzbDrone.Core.Music albumToUpdate.LastInfoSync = DateTime.UtcNow; albumToUpdate.CleanTitle = album.CleanTitle; albumToUpdate.Title = album.Title ?? "Unknown"; - albumToUpdate.CleanTitle = Parser.Parser.CleanArtistTitle(albumToUpdate.Title); + albumToUpdate.CleanTitle = Parser.Parser.CleanArtistName(albumToUpdate.Title); albumToUpdate.ArtistId = artist.Id; albumToUpdate.AlbumType = album.AlbumType; albumToUpdate.Genres = album.Genres; + albumToUpdate.Label = album.Label; albumToUpdate.Images = album.Images; albumToUpdate.ReleaseDate = album.ReleaseDate; albumToUpdate.Duration = album.Tracks.Sum(track => track.Duration); diff --git a/src/NzbDrone.Core/Music/RefreshArtistService.cs b/src/NzbDrone.Core/Music/RefreshArtistService.cs index 83a6856b3..d765e1e61 100644 --- a/src/NzbDrone.Core/Music/RefreshArtistService.cs +++ b/src/NzbDrone.Core/Music/RefreshArtistService.cs @@ -1,4 +1,4 @@ -using NLog; +using NLog; using NzbDrone.Common.Extensions; using NzbDrone.Common.Instrumentation.Extensions; using NzbDrone.Core.Exceptions; @@ -54,7 +54,7 @@ namespace NzbDrone.Core.Music try { - tuple = _artistInfo.GetArtistInfo(artist.ForeignArtistId); + tuple = _artistInfo.GetArtistInfo(artist.ForeignArtistId, artist.PrimaryAlbumTypes, artist.SecondaryAlbumTypes); } catch (ArtistNotFoundException) { @@ -79,6 +79,9 @@ namespace NzbDrone.Core.Music artist.LastInfoSync = DateTime.UtcNow; artist.Images = artistInfo.Images; artist.Genres = artistInfo.Genres; + artist.Links = artistInfo.Links; + artist.Disambiguation = artistInfo.Disambiguation; + artist.ArtistType = artistInfo.ArtistType; try { diff --git a/src/NzbDrone.Core/Music/RefreshTrackService.cs b/src/NzbDrone.Core/Music/RefreshTrackService.cs index e84ba318a..6e024c880 100644 --- a/src/NzbDrone.Core/Music/RefreshTrackService.cs +++ b/src/NzbDrone.Core/Music/RefreshTrackService.cs @@ -1,4 +1,4 @@ -using NLog; +using NLog; using NzbDrone.Common.Extensions; using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Music.Events; @@ -37,7 +37,7 @@ namespace NzbDrone.Core.Music album = _albumService.FindById(album.ForeignAlbumId); - var existingTracks = _trackService.GetTracksByAlbum(album.ArtistId, album.Id); + var existingTracks = _trackService.GetTracksByAlbum(album.Id); var updateList = new List(); var newList = new List(); diff --git a/src/NzbDrone.Core/Music/ShouldRefreshArtist.cs b/src/NzbDrone.Core/Music/ShouldRefreshArtist.cs index f38ccaaf5..de252678c 100644 --- a/src/NzbDrone.Core/Music/ShouldRefreshArtist.cs +++ b/src/NzbDrone.Core/Music/ShouldRefreshArtist.cs @@ -1,4 +1,4 @@ -using NLog; +using NLog; using System; using System.Collections.Generic; using System.Linq; @@ -11,14 +11,14 @@ namespace NzbDrone.Core.Music bool ShouldRefresh(Artist artist); } - public class CheckIfArtistShouldBeRefreshed : ICheckIfArtistShouldBeRefreshed + public class ShouldRefreshArtist : ICheckIfArtistShouldBeRefreshed { - private readonly ITrackService _trackService; + private readonly IAlbumService _albumService; private readonly Logger _logger; - public CheckIfArtistShouldBeRefreshed(ITrackService trackService, Logger logger) + public ShouldRefreshArtist(IAlbumService albumService, Logger logger) { - _trackService = trackService; + _albumService = albumService; _logger = logger; } @@ -36,7 +36,21 @@ namespace NzbDrone.Core.Music return false; } - //_logger.Trace("Artist {0} ended long ago, should not be refreshed.", artist.Title); + if (artist.Status == ArtistStatusType.Continuing) + { + _logger.Trace("Artist {0} is continuing, should refresh.", artist.Name); + return true; + } + + var lastAlbum = _albumService.GetAlbumsByArtist(artist.Id).OrderByDescending(e => e.ReleaseDate).FirstOrDefault(); + + if (lastAlbum != null && lastAlbum.ReleaseDate > DateTime.UtcNow.AddDays(-30)) + { + _logger.Trace("Last album in {0} aired less than 30 days ago, should refresh.", artist.Name); + return true; + } + + _logger.Trace("Artist {0} ended long ago, should not be refreshed.", artist.Name); return false; } } diff --git a/src/NzbDrone.Core/Music/TrackRepository.cs b/src/NzbDrone.Core/Music/TrackRepository.cs index b260056fe..408c01598 100644 --- a/src/NzbDrone.Core/Music/TrackRepository.cs +++ b/src/NzbDrone.Core/Music/TrackRepository.cs @@ -1,4 +1,4 @@ -using NzbDrone.Core.Datastore; +using NzbDrone.Core.Datastore; using System.Collections.Generic; using System.Linq; using NLog; @@ -15,7 +15,7 @@ namespace NzbDrone.Core.Music { Track Find(int artistId, int albumId, int trackNumber); List GetTracks(int artistId); - List GetTracks(int artistId, int albumId); + List GetTracksByAlbum(int albumId); List GetTracksByFileId(int fileId); List TracksWithFiles(int artistId); PagingSpec TracksWithoutFiles(PagingSpec pagingSpec); @@ -51,10 +51,9 @@ namespace NzbDrone.Core.Music return Query.Where(s => s.ArtistId == artistId).ToList(); } - public List GetTracks(int artistId, int albumId) + public List GetTracksByAlbum(int albumId) { - return Query.Where(s => s.ArtistId == artistId) - .AndWhere(s => s.AlbumId == albumId) + return Query.Where(s => s.AlbumId == albumId) .ToList(); } diff --git a/src/NzbDrone.Core/Music/TrackService.cs b/src/NzbDrone.Core/Music/TrackService.cs index 76fc7a5ca..d58eb6fbf 100644 --- a/src/NzbDrone.Core/Music/TrackService.cs +++ b/src/NzbDrone.Core/Music/TrackService.cs @@ -1,4 +1,4 @@ -using NLog; +using NLog; using NzbDrone.Core.Configuration; using NzbDrone.Core.Datastore; using NzbDrone.Core.MediaFiles; @@ -19,7 +19,7 @@ namespace NzbDrone.Core.Music Track FindTrack(int artistId, int albumId, int trackNumber); Track FindTrackByTitle(int artistId, int albumId, string releaseTitle); List GetTracksByArtist(int artistId); - List GetTracksByAlbum(int artistId, int albumId); + List GetTracksByAlbum(int albumId); //List GetTracksByAlbumTitle(string artistId, string albumTitle); List TracksWithFiles(int artistId); //PagingSpec TracksWithoutFiles(PagingSpec pagingSpec); @@ -70,16 +70,16 @@ namespace NzbDrone.Core.Music return _trackRepository.GetTracks(artistId).ToList(); } - public List GetTracksByAlbum(int artistId, int albumId) + public List GetTracksByAlbum(int albumId) { - return _trackRepository.GetTracks(artistId, albumId); + return _trackRepository.GetTracksByAlbum(albumId); } public Track FindTrackByTitle(int artistId, int albumId, string releaseTitle) { // TODO: can replace this search mechanism with something smarter/faster/better var normalizedReleaseTitle = Parser.Parser.NormalizeEpisodeTitle(releaseTitle).Replace(".", " "); - var tracks = _trackRepository.GetTracks(artistId, albumId); + var tracks = _trackRepository.GetTracksByAlbum(albumId); var matches = tracks.Select( track => new diff --git a/src/NzbDrone.Core/Notifications/Boxcar/Boxcar.cs b/src/NzbDrone.Core/Notifications/Boxcar/Boxcar.cs index f5bc2b40c..b38172f43 100644 --- a/src/NzbDrone.Core/Notifications/Boxcar/Boxcar.cs +++ b/src/NzbDrone.Core/Notifications/Boxcar/Boxcar.cs @@ -18,12 +18,12 @@ namespace NzbDrone.Core.Notifications.Boxcar public override void OnGrab(GrabMessage grabMessage) { - _proxy.SendNotification(EPISODE_GRABBED_TITLE, grabMessage.Message, Settings); + _proxy.SendNotification(ALBUM_GRABBED_TITLE, grabMessage.Message, Settings); } public override void OnDownload(DownloadMessage message) { - _proxy.SendNotification(EPISODE_DOWNLOADED_TITLE , message.Message, Settings); + _proxy.SendNotification(TRACK_DOWNLOADED_TITLE , message.Message, Settings); } public override ValidationResult Test() diff --git a/src/NzbDrone.Core/Notifications/CustomScript/CustomScript.cs b/src/NzbDrone.Core/Notifications/CustomScript/CustomScript.cs index 0a73f11ff..87894ceb6 100644 --- a/src/NzbDrone.Core/Notifications/CustomScript/CustomScript.cs +++ b/src/NzbDrone.Core/Notifications/CustomScript/CustomScript.cs @@ -6,7 +6,7 @@ using FluentValidation.Results; using NLog; using NzbDrone.Common.Disk; using NzbDrone.Common.Processes; -using NzbDrone.Core.Tv; +using NzbDrone.Core.Music; using NzbDrone.Core.Validation; namespace NzbDrone.Core.Notifications.CustomScript @@ -30,81 +30,80 @@ namespace NzbDrone.Core.Notifications.CustomScript public override void OnGrab(GrabMessage message) { - var series = message.Series; - var remoteEpisode = message.Episode; - var releaseGroup = remoteEpisode.ParsedEpisodeInfo.ReleaseGroup; + var artist = message.Artist; + var remoteAlbum = message.Album; + var releaseGroup = remoteAlbum.ParsedAlbumInfo.ReleaseGroup; var environmentVariables = new StringDictionary(); environmentVariables.Add("Lidarr_EventType", "Grab"); - environmentVariables.Add("Lidarr_Series_Id", series.Id.ToString()); - environmentVariables.Add("Lidarr_Series_Title", series.Title); - environmentVariables.Add("Lidarr_Series_TvdbId", series.TvdbId.ToString()); - environmentVariables.Add("Lidarr_Series_Type", series.SeriesType.ToString()); - environmentVariables.Add("Lidarr_Release_EpisodeCount", remoteEpisode.Episodes.Count.ToString()); - environmentVariables.Add("Lidarr_Release_SeasonNumber", remoteEpisode.ParsedEpisodeInfo.SeasonNumber.ToString()); - environmentVariables.Add("Lidarr_Release_EpisodeNumbers", string.Join(",", remoteEpisode.Episodes.Select(e => e.EpisodeNumber))); - environmentVariables.Add("Lidarr_Release_EpisodeAirDates", string.Join(",", remoteEpisode.Episodes.Select(e => e.AirDate))); - environmentVariables.Add("Lidarr_Release_EpisodeAirDatesUtc", string.Join(",", remoteEpisode.Episodes.Select(e => e.AirDateUtc))); - environmentVariables.Add("Lidarr_Release_EpisodeTitles", string.Join("|", remoteEpisode.Episodes.Select(e => e.Title))); - environmentVariables.Add("Lidarr_Release_Title", remoteEpisode.Release.Title); - environmentVariables.Add("Lidarr_Release_Indexer", remoteEpisode.Release.Indexer); - environmentVariables.Add("Lidarr_Release_Size", remoteEpisode.Release.Size.ToString()); - environmentVariables.Add("Lidarr_Release_Quality", remoteEpisode.ParsedEpisodeInfo.Quality.Quality.Name); - environmentVariables.Add("Lidarr_Release_QualityVersion", remoteEpisode.ParsedEpisodeInfo.Quality.Revision.Version.ToString()); - environmentVariables.Add("Lidarr_Release_ReleaseGroup", releaseGroup); + environmentVariables.Add("Lidarr_Artist_Id", artist.Id.ToString()); + environmentVariables.Add("Lidarr_Artist_Name", artist.Name); + environmentVariables.Add("Lidarr_Artist_MBId", artist.ForeignArtistId.ToString()); + //environmentVariables.Add("Lidarr_Artist_Type", artist.SeriesType.ToString()); + environmentVariables.Add("Lidarr_Release_AlbumCount", remoteAlbum.Albums.Count.ToString()); + environmentVariables.Add("Lidarr_Release_AlbumReleaseDates", string.Join(",", remoteAlbum.Albums.Select(e => e.ReleaseDate))); + environmentVariables.Add("Lidarr_Release_AlbumTitles", string.Join("|", remoteAlbum.Albums.Select(e => e.Title))); + environmentVariables.Add("Lidarr_Release_Title", remoteAlbum.Release.Title); + environmentVariables.Add("Lidarr_Release_Indexer", remoteAlbum.Release.Indexer); + environmentVariables.Add("Lidarr_Release_Size", remoteAlbum.Release.Size.ToString()); + environmentVariables.Add("Lidarr_Release_Quality", remoteAlbum.ParsedAlbumInfo.Quality.Quality.Name); + environmentVariables.Add("Lidarr_Release_QualityVersion", remoteAlbum.ParsedAlbumInfo.Quality.Revision.Version.ToString()); + environmentVariables.Add("Lidarr_Release_ReleaseGroup", releaseGroup ?? string.Empty); + environmentVariables.Add("Lidarr_Download_Client", message.DownloadClient ?? string.Empty); + environmentVariables.Add("Lidarr_Download_Id", message.DownloadId ?? string.Empty); ExecuteScript(environmentVariables); } public override void OnDownload(DownloadMessage message) { - var series = message.Series; - var episodeFile = message.EpisodeFile; + var artist = message.Artist; + var trackFile = message.TrackFile; var sourcePath = message.SourcePath; var environmentVariables = new StringDictionary(); environmentVariables.Add("Lidarr_EventType", "Download"); environmentVariables.Add("LIdarr_IsUpgrade", message.OldFiles.Any().ToString()); - environmentVariables.Add("Lidarr_Series_Id", series.Id.ToString()); - environmentVariables.Add("Lidarr_Series_Title", series.Title); - environmentVariables.Add("Lidarr_Series_Path", series.Path); - environmentVariables.Add("Lidarr_Series_TvdbId", series.TvdbId.ToString()); - environmentVariables.Add("Lidarr_Series_Type", series.SeriesType.ToString()); - environmentVariables.Add("Lidarr_EpisodeFile_Id", episodeFile.Id.ToString()); - environmentVariables.Add("Lidarr_EpisodeFile_EpisodeCount", episodeFile.Episodes.Value.Count.ToString()); - environmentVariables.Add("Lidarr_EpisodeFile_RelativePath", episodeFile.RelativePath); - environmentVariables.Add("Lidarr_EpisodeFile_Path", Path.Combine(series.Path, episodeFile.RelativePath)); - environmentVariables.Add("Lidarr_EpisodeFile_SeasonNumber", episodeFile.SeasonNumber.ToString()); - environmentVariables.Add("Lidarr_EpisodeFile_EpisodeNumbers", string.Join(",", episodeFile.Episodes.Value.Select(e => e.EpisodeNumber))); - environmentVariables.Add("Lidarr_EpisodeFile_EpisodeAirDates", string.Join(",", episodeFile.Episodes.Value.Select(e => e.AirDate))); - environmentVariables.Add("Lidarr_EpisodeFile_EpisodeAirDatesUtc", string.Join(",", episodeFile.Episodes.Value.Select(e => e.AirDateUtc))); - environmentVariables.Add("Lidarr_EpisodeFile_EpisodeTitles", string.Join("|", episodeFile.Episodes.Value.Select(e => e.Title))); - environmentVariables.Add("Lidarr_EpisodeFile_Quality", episodeFile.Quality.Quality.Name); - environmentVariables.Add("Lidarr_EpisodeFile_QualityVersion", episodeFile.Quality.Revision.Version.ToString()); - environmentVariables.Add("Lidarr_EpisodeFile_ReleaseGroup", episodeFile.ReleaseGroup ?? string.Empty); - environmentVariables.Add("Lidarr_EpisodeFile_SceneName", episodeFile.SceneName ?? string.Empty); - environmentVariables.Add("Lidarr_EpisodeFile_SourcePath", sourcePath); - environmentVariables.Add("Lidarr_EpisodeFile_SourceFolder", Path.GetDirectoryName(sourcePath)); + environmentVariables.Add("Lidarr_Artist_Id", artist.Id.ToString()); + environmentVariables.Add("Lidarr_Artist_Name", artist.Name); + environmentVariables.Add("Lidarr_Artist_Path", artist.Path); + environmentVariables.Add("Lidarr_Artist_MBId", artist.ForeignArtistId.ToString()); + //environmentVariables.Add("Lidarr_Artist_Type", artist.SeriesType.ToString()); + environmentVariables.Add("Lidarr_TrackFile_Id", trackFile.Id.ToString()); + environmentVariables.Add("Lidarr_TrackFile_EpisodeCount", trackFile.Tracks.Value.Count.ToString()); + environmentVariables.Add("Lidarr_TrackFile_RelativePath", trackFile.RelativePath); + environmentVariables.Add("Lidarr_TrackFile_Path", Path.Combine(artist.Path, trackFile.RelativePath)); + environmentVariables.Add("Lidarr_TrackFile_TrackNumbers", string.Join(",", trackFile.Tracks.Value.Select(e => e.TrackNumber))); + environmentVariables.Add("Lidarr_TrackFile_TrackReleaseDates", string.Join(",", trackFile.Tracks.Value.Select(e => e.Album.ReleaseDate))); + environmentVariables.Add("Lidarr_TrackFile_TrackTitles", string.Join("|", trackFile.Tracks.Value.Select(e => e.Title))); + environmentVariables.Add("Lidarr_TrackFile_Quality", trackFile.Quality.Quality.Name); + environmentVariables.Add("Lidarr_TrackFile_QualityVersion", trackFile.Quality.Revision.Version.ToString()); + environmentVariables.Add("Lidarr_TrackFile_ReleaseGroup", trackFile.ReleaseGroup ?? string.Empty); + environmentVariables.Add("Lidarr_TrackFile_SceneName", trackFile.SceneName ?? string.Empty); + environmentVariables.Add("Lidarr_TrackFile_SourcePath", sourcePath); + environmentVariables.Add("Lidarr_TrackFile_SourceFolder", Path.GetDirectoryName(sourcePath)); + environmentVariables.Add("Lidarr_Download_Client", message.DownloadClient ?? string.Empty); + environmentVariables.Add("Lidarr_Download_Id", message.DownloadId ?? string.Empty); if (message.OldFiles.Any()) { environmentVariables.Add("Lidarr_DeletedRelativePaths", string.Join("|", message.OldFiles.Select(e => e.RelativePath))); - environmentVariables.Add("Lidarr_DeletedPaths", string.Join("|", message.OldFiles.Select(e => Path.Combine(series.Path, e.RelativePath)))); + environmentVariables.Add("Lidarr_DeletedPaths", string.Join("|", message.OldFiles.Select(e => Path.Combine(artist.Path, e.RelativePath)))); } ExecuteScript(environmentVariables); } - public override void OnRename(Series series) + public override void OnRename(Artist artist) { var environmentVariables = new StringDictionary(); environmentVariables.Add("Lidarr_EventType", "Rename"); - environmentVariables.Add("Lidarr_Series_Id", series.Id.ToString()); - environmentVariables.Add("Lidarr_Series_Title", series.Title); - environmentVariables.Add("Lidarr_Series_Path", series.Path); - environmentVariables.Add("Lidarr_Series_TvdbId", series.TvdbId.ToString()); - environmentVariables.Add("Lidarr_Series_Type", series.SeriesType.ToString()); + environmentVariables.Add("Lidarr_Artist_Id", artist.Id.ToString()); + environmentVariables.Add("Lidarr_Artist_Title", artist.Name); + environmentVariables.Add("Lidarr_Artist_Path", artist.Path); + environmentVariables.Add("Lidarr_Artist_TvdbId", artist.ForeignArtistId.ToString()); + //environmentVariables.Add("Lidarr_Artist_Type", artist.SeriesType.ToString()); ExecuteScript(environmentVariables); } diff --git a/src/NzbDrone.Core/Notifications/DownloadMessage.cs b/src/NzbDrone.Core/Notifications/DownloadMessage.cs index a16ecea80..b88cb4694 100644 --- a/src/NzbDrone.Core/Notifications/DownloadMessage.cs +++ b/src/NzbDrone.Core/Notifications/DownloadMessage.cs @@ -1,16 +1,18 @@ -using System.Collections.Generic; +using System.Collections.Generic; using NzbDrone.Core.MediaFiles; -using NzbDrone.Core.Tv; +using NzbDrone.Core.Music; namespace NzbDrone.Core.Notifications { public class DownloadMessage { public string Message { get; set; } - public Series Series { get; set; } - public EpisodeFile EpisodeFile { get; set; } - public List OldFiles { get; set; } + public Artist Artist { get; set; } + public TrackFile TrackFile { get; set; } + public List OldFiles { get; set; } public string SourcePath { get; set; } + public string DownloadClient { get; set; } + public string DownloadId { get; set; } public override string ToString() { diff --git a/src/NzbDrone.Core/Notifications/Email/Email.cs b/src/NzbDrone.Core/Notifications/Email/Email.cs index 52597861d..69158599b 100644 --- a/src/NzbDrone.Core/Notifications/Email/Email.cs +++ b/src/NzbDrone.Core/Notifications/Email/Email.cs @@ -1,7 +1,6 @@ -using System.Collections.Generic; +using System.Collections.Generic; using FluentValidation.Results; using NzbDrone.Common.Extensions; -using NzbDrone.Core.Tv; namespace NzbDrone.Core.Notifications.Email { @@ -23,14 +22,14 @@ namespace NzbDrone.Core.Notifications.Email { var body = $"{grabMessage.Message} sent to queue."; - _emailService.SendEmail(Settings, EPISODE_GRABBED_TITLE_BRANDED, body); + _emailService.SendEmail(Settings, ALBUM_GRABBED_TITLE_BRANDED, body); } public override void OnDownload(DownloadMessage message) { var body = $"{message.Message} Downloaded and sorted."; - _emailService.SendEmail(Settings, EPISODE_DOWNLOADED_TITLE_BRANDED, body); + _emailService.SendEmail(Settings, TRACK_DOWNLOADED_TITLE_BRANDED, body); } diff --git a/src/NzbDrone.Core/Notifications/GrabMessage.cs b/src/NzbDrone.Core/Notifications/GrabMessage.cs index e62dbe701..b9eda36cd 100644 --- a/src/NzbDrone.Core/Notifications/GrabMessage.cs +++ b/src/NzbDrone.Core/Notifications/GrabMessage.cs @@ -1,15 +1,17 @@ -using NzbDrone.Core.Parser.Model; +using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Qualities; -using NzbDrone.Core.Tv; +using NzbDrone.Core.Music; namespace NzbDrone.Core.Notifications { public class GrabMessage { public string Message { get; set; } - public Series Series { get; set; } - public RemoteEpisode Episode { get; set; } - public QualityModel Quality { get; set; } + public Artist Artist { get; set; } + public RemoteAlbum Album { get; set; } + public QualityModel Quality { get; set; } + public string DownloadClient { get; set; } + public string DownloadId { get; set; } public override string ToString() { diff --git a/src/NzbDrone.Core/Notifications/Growl/Growl.cs b/src/NzbDrone.Core/Notifications/Growl/Growl.cs index 97232be70..e6ff72463 100644 --- a/src/NzbDrone.Core/Notifications/Growl/Growl.cs +++ b/src/NzbDrone.Core/Notifications/Growl/Growl.cs @@ -1,7 +1,6 @@ -using System.Collections.Generic; +using System.Collections.Generic; using FluentValidation.Results; using NzbDrone.Common.Extensions; -using NzbDrone.Core.Tv; namespace NzbDrone.Core.Notifications.Growl { @@ -21,12 +20,12 @@ namespace NzbDrone.Core.Notifications.Growl public override void OnGrab(GrabMessage grabMessage) { - _growlService.SendNotification(EPISODE_GRABBED_TITLE, grabMessage.Message, "GRAB", Settings.Host, Settings.Port, Settings.Password); + _growlService.SendNotification(ALBUM_GRABBED_TITLE, grabMessage.Message, "GRAB", Settings.Host, Settings.Port, Settings.Password); } public override void OnDownload(DownloadMessage message) { - _growlService.SendNotification(EPISODE_DOWNLOADED_TITLE, message.Message, "DOWNLOAD", Settings.Host, Settings.Port, Settings.Password); + _growlService.SendNotification(TRACK_DOWNLOADED_TITLE, message.Message, "DOWNLOAD", Settings.Host, Settings.Port, Settings.Password); } diff --git a/src/NzbDrone.Core/Notifications/INotification.cs b/src/NzbDrone.Core/Notifications/INotification.cs index 7c4e105b9..36b01e8db 100644 --- a/src/NzbDrone.Core/Notifications/INotification.cs +++ b/src/NzbDrone.Core/Notifications/INotification.cs @@ -1,5 +1,5 @@ -using NzbDrone.Core.ThingiProvider; -using NzbDrone.Core.Tv; +using NzbDrone.Core.ThingiProvider; +using NzbDrone.Core.Music; namespace NzbDrone.Core.Notifications { @@ -9,7 +9,7 @@ namespace NzbDrone.Core.Notifications void OnGrab(GrabMessage grabMessage); void OnDownload(DownloadMessage message); - void OnRename(Series series); + void OnRename(Artist artist); bool SupportsOnGrab { get; } bool SupportsOnDownload { get; } bool SupportsOnUpgrade { get; } diff --git a/src/NzbDrone.Core/Notifications/Join/Join.cs b/src/NzbDrone.Core/Notifications/Join/Join.cs index b8122e7f5..c47b4f993 100644 --- a/src/NzbDrone.Core/Notifications/Join/Join.cs +++ b/src/NzbDrone.Core/Notifications/Join/Join.cs @@ -19,12 +19,12 @@ namespace NzbDrone.Core.Notifications.Join public override void OnGrab(GrabMessage message) { - _proxy.SendNotification(EPISODE_GRABBED_TITLE_BRANDED, message.Message, Settings); + _proxy.SendNotification(ALBUM_GRABBED_TITLE_BRANDED, message.Message, Settings); } public override void OnDownload(DownloadMessage message) { - _proxy.SendNotification(EPISODE_DOWNLOADED_TITLE_BRANDED, message.Message, Settings); + _proxy.SendNotification(TRACK_DOWNLOADED_TITLE_BRANDED, message.Message, Settings); } public override ValidationResult Test() diff --git a/src/NzbDrone.Core/Notifications/MediaBrowser/MediaBrowser.cs b/src/NzbDrone.Core/Notifications/MediaBrowser/MediaBrowser.cs index b13409dc7..baa150246 100644 --- a/src/NzbDrone.Core/Notifications/MediaBrowser/MediaBrowser.cs +++ b/src/NzbDrone.Core/Notifications/MediaBrowser/MediaBrowser.cs @@ -1,7 +1,7 @@ -using System.Collections.Generic; +using System.Collections.Generic; using FluentValidation.Results; using NzbDrone.Common.Extensions; -using NzbDrone.Core.Tv; +using NzbDrone.Core.Music; namespace NzbDrone.Core.Notifications.Emby { @@ -22,7 +22,7 @@ namespace NzbDrone.Core.Notifications.Emby { if (Settings.Notify) { - _mediaBrowserService.Notify(Settings, EPISODE_GRABBED_TITLE_BRANDED, grabMessage.Message); + _mediaBrowserService.Notify(Settings, ALBUM_GRABBED_TITLE_BRANDED, grabMessage.Message); } } @@ -30,20 +30,20 @@ namespace NzbDrone.Core.Notifications.Emby { if (Settings.Notify) { - _mediaBrowserService.Notify(Settings, EPISODE_DOWNLOADED_TITLE_BRANDED, message.Message); + _mediaBrowserService.Notify(Settings, TRACK_DOWNLOADED_TITLE_BRANDED, message.Message); } if (Settings.UpdateLibrary) { - _mediaBrowserService.Update(Settings, message.Series); + _mediaBrowserService.Update(Settings, message.Artist); } } - public override void OnRename(Series series) + public override void OnRename(Artist artist) { if (Settings.UpdateLibrary) { - _mediaBrowserService.Update(Settings, series); + _mediaBrowserService.Update(Settings, artist); } } diff --git a/src/NzbDrone.Core/Notifications/MediaBrowser/MediaBrowserProxy.cs b/src/NzbDrone.Core/Notifications/MediaBrowser/MediaBrowserProxy.cs index b5e1da15a..4df933054 100644 --- a/src/NzbDrone.Core/Notifications/MediaBrowser/MediaBrowserProxy.cs +++ b/src/NzbDrone.Core/Notifications/MediaBrowser/MediaBrowserProxy.cs @@ -1,4 +1,4 @@ -using NLog; +using NLog; using NzbDrone.Common.Http; using NzbDrone.Common.Serializer; @@ -31,9 +31,9 @@ namespace NzbDrone.Core.Notifications.Emby ProcessRequest(request, settings); } - public void Update(MediaBrowserSettings settings, int tvdbId) + public void Update(MediaBrowserSettings settings, string mbId) { - var path = string.Format("/Library/Series/Updated?tvdbid={0}", tvdbId); + var path = string.Format("/Library/Artist/Updated?tvdbid={0}", mbId); //TODO: Get Emby to add a new Library Route var request = BuildRequest(path, settings); request.Headers.Add("Content-Length", "0"); diff --git a/src/NzbDrone.Core/Notifications/MediaBrowser/MediaBrowserService.cs b/src/NzbDrone.Core/Notifications/MediaBrowser/MediaBrowserService.cs index ad1b8fba9..c39b5d1c6 100644 --- a/src/NzbDrone.Core/Notifications/MediaBrowser/MediaBrowserService.cs +++ b/src/NzbDrone.Core/Notifications/MediaBrowser/MediaBrowserService.cs @@ -1,16 +1,16 @@ -using System; +using System; using System.Net; using FluentValidation.Results; using NLog; using NzbDrone.Core.Rest; -using NzbDrone.Core.Tv; +using NzbDrone.Core.Music; namespace NzbDrone.Core.Notifications.Emby { public interface IMediaBrowserService { void Notify(MediaBrowserSettings settings, string title, string message); - void Update(MediaBrowserSettings settings, Series series); + void Update(MediaBrowserSettings settings, Artist artist); ValidationFailure Test(MediaBrowserSettings settings); } @@ -30,9 +30,9 @@ namespace NzbDrone.Core.Notifications.Emby _proxy.Notify(settings, title, message); } - public void Update(MediaBrowserSettings settings, Series series) + public void Update(MediaBrowserSettings settings, Artist artist) { - _proxy.Update(settings, series.TvdbId); + _proxy.Update(settings, artist.ForeignArtistId); } public ValidationFailure Test(MediaBrowserSettings settings) diff --git a/src/NzbDrone.Core/Notifications/NotificationBase.cs b/src/NzbDrone.Core/Notifications/NotificationBase.cs index 46cc76f60..915d1d4bf 100644 --- a/src/NzbDrone.Core/Notifications/NotificationBase.cs +++ b/src/NzbDrone.Core/Notifications/NotificationBase.cs @@ -1,18 +1,18 @@ -using System; +using System; using System.Collections.Generic; using FluentValidation.Results; using NzbDrone.Core.ThingiProvider; -using NzbDrone.Core.Tv; +using NzbDrone.Core.Music; namespace NzbDrone.Core.Notifications { public abstract class NotificationBase : INotification where TSettings : IProviderConfig, new() { - protected const string EPISODE_GRABBED_TITLE = "Episode Grabbed"; - protected const string EPISODE_DOWNLOADED_TITLE = "Episode Downloaded"; + protected const string ALBUM_GRABBED_TITLE = "Album Grabbed"; + protected const string TRACK_DOWNLOADED_TITLE = "Track Downloaded"; - protected const string EPISODE_GRABBED_TITLE_BRANDED = "Lidarr - " + EPISODE_GRABBED_TITLE; - protected const string EPISODE_DOWNLOADED_TITLE_BRANDED = "Lidarr - " + EPISODE_DOWNLOADED_TITLE; + protected const string ALBUM_GRABBED_TITLE_BRANDED = "Lidarr - " + ALBUM_GRABBED_TITLE; + protected const string TRACK_DOWNLOADED_TITLE_BRANDED = "Lidarr - " + TRACK_DOWNLOADED_TITLE; public abstract string Name { get; } @@ -37,7 +37,7 @@ namespace NzbDrone.Core.Notifications } - public virtual void OnRename(Series series) + public virtual void OnRename(Artist series) { } diff --git a/src/NzbDrone.Core/Notifications/NotificationDefinition.cs b/src/NzbDrone.Core/Notifications/NotificationDefinition.cs index 5c2d10045..4673a63a9 100644 --- a/src/NzbDrone.Core/Notifications/NotificationDefinition.cs +++ b/src/NzbDrone.Core/Notifications/NotificationDefinition.cs @@ -1,14 +1,9 @@ -using System.Collections.Generic; using NzbDrone.Core.ThingiProvider; namespace NzbDrone.Core.Notifications { public class NotificationDefinition : ProviderDefinition { - public NotificationDefinition() - { - Tags = new HashSet(); - } public bool OnGrab { get; set; } public bool OnDownload { get; set; } @@ -18,7 +13,6 @@ namespace NzbDrone.Core.Notifications public bool SupportsOnDownload { get; set; } public bool SupportsOnUpgrade { get; set; } public bool SupportsOnRename { get; set; } - public HashSet Tags { get; set; } public override bool Enable => OnGrab || OnDownload || (OnDownload && OnUpgrade); } diff --git a/src/NzbDrone.Core/Notifications/NotificationService.cs b/src/NzbDrone.Core/Notifications/NotificationService.cs index 985126f19..47f1a4210 100644 --- a/src/NzbDrone.Core/Notifications/NotificationService.cs +++ b/src/NzbDrone.Core/Notifications/NotificationService.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using NLog; @@ -8,14 +8,14 @@ using NzbDrone.Core.MediaFiles.Events; using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Qualities; using NzbDrone.Core.ThingiProvider; -using NzbDrone.Core.Tv; +using NzbDrone.Core.Music; namespace NzbDrone.Core.Notifications { public class NotificationService - : IHandle, - IHandle, - IHandle + : IHandle, + IHandle, + IHandle { private readonly INotificationFactory _notificationFactory; private readonly Logger _logger; @@ -26,83 +26,78 @@ namespace NzbDrone.Core.Notifications _logger = logger; } - private string GetMessage(Series series, List episodes, QualityModel quality) + private string GetMessage(Artist artist, List albums, QualityModel quality) { var qualityString = quality.Quality.ToString(); if (quality.Revision.Version > 1) { - if (series.SeriesType == SeriesTypes.Anime) - { - qualityString += " v" + quality.Revision.Version; - } - - else - { - qualityString += " Proper"; - } + qualityString += " Proper"; } - if (series.SeriesType == SeriesTypes.Daily) - { - var episode = episodes.First(); - return string.Format("{0} - {1} - {2} [{3}]", - series.Title, - episode.AirDate, - episode.Title, - qualityString); + var albumTitles = string.Join(" + ", albums.Select(e => e.Title)); + + return string.Format("{0} - {1} - [{2}]", + artist.Name, + albumTitles, + qualityString); + } + + private string GetTrackMessage(Artist artist, List tracks, QualityModel quality) + { + var qualityString = quality.Quality.ToString(); + + if (quality.Revision.Version > 1) + { + qualityString += " Proper"; } - var episodeNumbers = string.Concat(episodes.Select(e => e.EpisodeNumber) - .Select(i => string.Format("x{0:00}", i))); - var episodeTitles = string.Join(" + ", episodes.Select(e => e.Title)); + var trackTitles = string.Join(" + ", tracks.Select(e => e.Title)); - return string.Format("{0} - {1}{2} - {3} [{4}]", - series.Title, - episodes.First().SeasonNumber, - episodeNumbers, - episodeTitles, + return string.Format("{0} - {1} - [{2}]", + artist.Name, + trackTitles, qualityString); } - private bool ShouldHandleSeries(ProviderDefinition definition, Series series) + private bool ShouldHandleArtist(ProviderDefinition definition, Artist artist) { - var notificationDefinition = (NotificationDefinition)definition; - - if (notificationDefinition.Tags.Empty()) + if (definition.Tags.Empty()) { _logger.Debug("No tags set for this notification."); return true; } - if (notificationDefinition.Tags.Intersect(series.Tags).Any()) + if (definition.Tags.Intersect(artist.Tags).Any()) { - _logger.Debug("Notification and series have one or more matching tags."); + _logger.Debug("Notification and artist have one or more intersecting tags."); return true; } //TODO: this message could be more clear - _logger.Debug("{0} does not have any tags that match {1}'s tags", notificationDefinition.Name, series.Title); + _logger.Debug("{0} does not have any intersecting tags with {1}. Notification will not be sent.", definition.Name, artist.Name); return false; } - public void Handle(EpisodeGrabbedEvent message) + public void Handle(AlbumGrabbedEvent message) { var grabMessage = new GrabMessage { - Message = GetMessage(message.Episode.Series, message.Episode.Episodes, message.Episode.ParsedEpisodeInfo.Quality), - Series = message.Episode.Series, - Quality = message.Episode.ParsedEpisodeInfo.Quality, - Episode = message.Episode + Message = GetMessage(message.Album.Artist, message.Album.Albums, message.Album.ParsedAlbumInfo.Quality), + Artist = message.Album.Artist, + Quality = message.Album.ParsedAlbumInfo.Quality, + Album = message.Album, + DownloadClient = message.DownloadClient, + DownloadId = message.DownloadId }; foreach (var notification in _notificationFactory.OnGrabEnabled()) { try { - if (!ShouldHandleSeries(notification.Definition, message.Episode.Series)) continue; + if (!ShouldHandleArtist(notification.Definition, message.Album.Artist)) continue; notification.OnGrab(grabMessage); } @@ -113,20 +108,30 @@ namespace NzbDrone.Core.Notifications } } - public void Handle(EpisodeDownloadedEvent message) + public void Handle(TrackImportedEvent message) { - var downloadMessage = new DownloadMessage(); - downloadMessage.Message = GetMessage(message.Episode.Series, message.Episode.Episodes, message.Episode.Quality); - downloadMessage.Series = message.Episode.Series; - downloadMessage.EpisodeFile = message.EpisodeFile; - downloadMessage.OldFiles = message.OldFiles; - downloadMessage.SourcePath = message.Episode.Path; + if (!message.NewDownload) + { + return; + } + + var downloadMessage = new DownloadMessage + + { + Message = GetTrackMessage(message.TrackInfo.Artist, message.TrackInfo.Tracks, message.TrackInfo.Quality), + Artist = message.TrackInfo.Artist, + TrackFile = message.ImportedTrack, + OldFiles = message.OldFiles, + SourcePath = message.TrackInfo.Path, + DownloadClient = message.DownloadClient, + DownloadId = message.DownloadId + }; foreach (var notification in _notificationFactory.OnDownloadEnabled()) { try { - if (ShouldHandleSeries(notification.Definition, message.Episode.Series)) + if (ShouldHandleArtist(notification.Definition, message.TrackInfo.Artist)) { if (downloadMessage.OldFiles.Empty() || ((NotificationDefinition)notification.Definition).OnUpgrade) { @@ -142,15 +147,15 @@ namespace NzbDrone.Core.Notifications } } - public void Handle(SeriesRenamedEvent message) + public void Handle(ArtistRenamedEvent message) { foreach (var notification in _notificationFactory.OnRenameEnabled()) { try { - if (ShouldHandleSeries(notification.Definition, message.Series)) + if (ShouldHandleArtist(notification.Definition, message.Artist)) { - notification.OnRename(message.Series); + notification.OnRename(message.Artist); } } diff --git a/src/NzbDrone.Core/Notifications/NotifyMyAndroid/NotifyMyAndroid.cs b/src/NzbDrone.Core/Notifications/NotifyMyAndroid/NotifyMyAndroid.cs index 693cb6537..995d994ac 100644 --- a/src/NzbDrone.Core/Notifications/NotifyMyAndroid/NotifyMyAndroid.cs +++ b/src/NzbDrone.Core/Notifications/NotifyMyAndroid/NotifyMyAndroid.cs @@ -18,12 +18,12 @@ namespace NzbDrone.Core.Notifications.NotifyMyAndroid public override void OnGrab(GrabMessage grabMessage) { - _proxy.SendNotification(EPISODE_GRABBED_TITLE, grabMessage.Message, Settings.ApiKey, (NotifyMyAndroidPriority)Settings.Priority); + _proxy.SendNotification(ALBUM_GRABBED_TITLE, grabMessage.Message, Settings.ApiKey, (NotifyMyAndroidPriority)Settings.Priority); } public override void OnDownload(DownloadMessage message) { - _proxy.SendNotification(EPISODE_DOWNLOADED_TITLE, message.Message, Settings.ApiKey, (NotifyMyAndroidPriority)Settings.Priority); + _proxy.SendNotification(TRACK_DOWNLOADED_TITLE, message.Message, Settings.ApiKey, (NotifyMyAndroidPriority)Settings.Priority); } public override ValidationResult Test() diff --git a/src/NzbDrone.Core/Notifications/Plex/Models/PlexSection.cs b/src/NzbDrone.Core/Notifications/Plex/Models/PlexSection.cs index 71aab1988..74f5dd4db 100644 --- a/src/NzbDrone.Core/Notifications/Plex/Models/PlexSection.cs +++ b/src/NzbDrone.Core/Notifications/Plex/Models/PlexSection.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using Newtonsoft.Json; namespace NzbDrone.Core.Notifications.Plex.Models @@ -11,6 +11,11 @@ namespace NzbDrone.Core.Notifications.Plex.Models public class PlexSection { + public PlexSection() + { + Locations = new List(); + } + [JsonProperty("key")] public int Id { get; set; } @@ -23,6 +28,11 @@ namespace NzbDrone.Core.Notifications.Plex.Models public class PlexSectionsContainer { + public PlexSectionsContainer() + { + Sections = new List(); + } + [JsonProperty("Directory")] public List Sections { get; set; } } diff --git a/src/NzbDrone.Core/Notifications/Plex/PlexClient.cs b/src/NzbDrone.Core/Notifications/Plex/PlexClient.cs index 1294c6a40..3641ed416 100644 --- a/src/NzbDrone.Core/Notifications/Plex/PlexClient.cs +++ b/src/NzbDrone.Core/Notifications/Plex/PlexClient.cs @@ -1,7 +1,6 @@ -using System.Collections.Generic; +using System.Collections.Generic; using FluentValidation.Results; using NzbDrone.Common.Extensions; -using NzbDrone.Core.Tv; namespace NzbDrone.Core.Notifications.Plex { @@ -19,12 +18,12 @@ namespace NzbDrone.Core.Notifications.Plex public override void OnGrab(GrabMessage grabMessage) { - _plexClientService.Notify(Settings, EPISODE_GRABBED_TITLE_BRANDED, grabMessage.Message); + _plexClientService.Notify(Settings, ALBUM_GRABBED_TITLE_BRANDED, grabMessage.Message); } public override void OnDownload(DownloadMessage message) { - _plexClientService.Notify(Settings, EPISODE_DOWNLOADED_TITLE_BRANDED, message.Message); + _plexClientService.Notify(Settings, TRACK_DOWNLOADED_TITLE_BRANDED, message.Message); } diff --git a/src/NzbDrone.Core/Notifications/Plex/PlexHomeTheater.cs b/src/NzbDrone.Core/Notifications/Plex/PlexHomeTheater.cs index c90473471..c0ca2766a 100644 --- a/src/NzbDrone.Core/Notifications/Plex/PlexHomeTheater.cs +++ b/src/NzbDrone.Core/Notifications/Plex/PlexHomeTheater.cs @@ -1,10 +1,9 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Net.Sockets; using FluentValidation.Results; using NLog; using NzbDrone.Common.Extensions; using NzbDrone.Core.Notifications.Xbmc; -using NzbDrone.Core.Tv; namespace NzbDrone.Core.Notifications.Plex { @@ -24,12 +23,12 @@ namespace NzbDrone.Core.Notifications.Plex public override void OnGrab(GrabMessage grabMessage) { - Notify(Settings, EPISODE_GRABBED_TITLE_BRANDED, grabMessage.Message); + Notify(Settings, ALBUM_GRABBED_TITLE_BRANDED, grabMessage.Message); } public override void OnDownload(DownloadMessage message) { - Notify(Settings, EPISODE_DOWNLOADED_TITLE_BRANDED, message.Message); + Notify(Settings, TRACK_DOWNLOADED_TITLE_BRANDED, message.Message); } public override ValidationResult Test() diff --git a/src/NzbDrone.Core/Notifications/Plex/PlexServer.cs b/src/NzbDrone.Core/Notifications/Plex/PlexServer.cs index b691ae282..4d34cba3c 100644 --- a/src/NzbDrone.Core/Notifications/Plex/PlexServer.cs +++ b/src/NzbDrone.Core/Notifications/Plex/PlexServer.cs @@ -1,7 +1,7 @@ -using System.Collections.Generic; +using System.Collections.Generic; using FluentValidation.Results; using NzbDrone.Common.Extensions; -using NzbDrone.Core.Tv; +using NzbDrone.Core.Music; namespace NzbDrone.Core.Notifications.Plex { @@ -19,19 +19,19 @@ namespace NzbDrone.Core.Notifications.Plex public override void OnDownload(DownloadMessage message) { - UpdateIfEnabled(message.Series); + UpdateIfEnabled(message.Artist); } - public override void OnRename(Series series) + public override void OnRename(Artist artist) { - UpdateIfEnabled(series); + UpdateIfEnabled(artist); } - private void UpdateIfEnabled(Series series) + private void UpdateIfEnabled(Artist artist) { if (Settings.UpdateLibrary) { - _plexServerService.UpdateLibrary(series, Settings); + _plexServerService.UpdateLibrary(artist, Settings); } } diff --git a/src/NzbDrone.Core/Notifications/Plex/PlexServerProxy.cs b/src/NzbDrone.Core/Notifications/Plex/PlexServerProxy.cs index f9206efc0..f47771395 100644 --- a/src/NzbDrone.Core/Notifications/Plex/PlexServerProxy.cs +++ b/src/NzbDrone.Core/Notifications/Plex/PlexServerProxy.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using System.Net; using Newtonsoft.Json.Linq; @@ -21,7 +21,7 @@ namespace NzbDrone.Core.Notifications.Plex void UpdateSeries(int metadataId, PlexServerSettings settings); string Version(PlexServerSettings settings); List Preferences(PlexServerSettings settings); - int? GetMetadataId(int sectionId, int tvdbId, string language, PlexServerSettings settings); + int? GetMetadataId(int sectionId, string mdId, string language, PlexServerSettings settings); } public class PlexServerProxy : IPlexServerProxy @@ -128,9 +128,9 @@ namespace NzbDrone.Core.Notifications.Plex .Preferences; } - public int? GetMetadataId(int sectionId, int tvdbId, string language, PlexServerSettings settings) + public int? GetMetadataId(int sectionId, string mbId, string language, PlexServerSettings settings) { - var guid = string.Format("com.plexapp.agents.thetvdb://{0}?lang={1}", tvdbId, language); + var guid = string.Format("com.plexapp.agents.lastfm://{0}?lang={1}", mbId, language); // TODO Plex Route for MB? LastFM? var resource = string.Format("library/sections/{0}/all?guid={1}", sectionId, System.Web.HttpUtility.UrlEncode(guid)); var request = GetPlexServerRequest(resource, Method.GET, settings); var client = GetPlexServerClient(settings); diff --git a/src/NzbDrone.Core/Notifications/Plex/PlexServerService.cs b/src/NzbDrone.Core/Notifications/Plex/PlexServerService.cs index 62d8c4e79..b5d71416a 100644 --- a/src/NzbDrone.Core/Notifications/Plex/PlexServerService.cs +++ b/src/NzbDrone.Core/Notifications/Plex/PlexServerService.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; @@ -7,13 +7,13 @@ using NLog; using NzbDrone.Common.Cache; using NzbDrone.Common.Extensions; using NzbDrone.Core.Notifications.Plex.Models; -using NzbDrone.Core.Tv; +using NzbDrone.Core.Music; namespace NzbDrone.Core.Notifications.Plex { public interface IPlexServerService { - void UpdateLibrary(Series series, PlexServerSettings settings); + void UpdateLibrary(Artist artist, PlexServerSettings settings); ValidationFailure Test(PlexServerSettings settings); } @@ -32,7 +32,7 @@ namespace NzbDrone.Core.Notifications.Plex _logger = logger; } - public void UpdateLibrary(Series series, PlexServerSettings settings) + public void UpdateLibrary(Artist artist, PlexServerSettings settings) { try { @@ -46,7 +46,7 @@ namespace NzbDrone.Core.Notifications.Plex if (partialUpdates) { - UpdatePartialSection(series, sections, settings); + UpdatePartialSection(artist, sections, settings); } else @@ -130,17 +130,17 @@ namespace NzbDrone.Core.Notifications.Plex _plexServerProxy.Update(sectionId, settings); } - private void UpdatePartialSection(Series series, List sections, PlexServerSettings settings) + private void UpdatePartialSection(Artist artist, List sections, PlexServerSettings settings) { var partiallyUpdated = false; foreach (var section in sections) { - var metadataId = GetMetadataId(section.Id, series, section.Language, settings); + var metadataId = GetMetadataId(section.Id, artist, section.Language, settings); if (metadataId.HasValue) { - _logger.Debug("Updating Plex host: {0}, Section: {1}, Series: {2}", settings.Host, section.Id, series); + _logger.Debug("Updating Plex host: {0}, Section: {1}, Artist: {2}", settings.Host, section.Id, artist); _plexServerProxy.UpdateSeries(metadataId.Value, settings); partiallyUpdated = true; @@ -150,16 +150,16 @@ namespace NzbDrone.Core.Notifications.Plex // Only update complete sections if all partial updates failed if (!partiallyUpdated) { - _logger.Debug("Unable to update partial section, updating all TV sections"); + _logger.Debug("Unable to update partial section, updating all Music sections"); sections.ForEach(s => UpdateSection(s.Id, settings)); } } - private int? GetMetadataId(int sectionId, Series series, string language, PlexServerSettings settings) + private int? GetMetadataId(int sectionId, Artist artist, string language, PlexServerSettings settings) { - _logger.Debug("Getting metadata from Plex host: {0} for series: {1}", settings.Host, series); + _logger.Debug("Getting metadata from Plex host: {0} for series: {1}", settings.Host, artist); - return _plexServerProxy.GetMetadataId(sectionId, series.TvdbId, language, settings); + return _plexServerProxy.GetMetadataId(sectionId, artist.ForeignArtistId, language, settings); } public ValidationFailure Test(PlexServerSettings settings) @@ -170,7 +170,7 @@ namespace NzbDrone.Core.Notifications.Plex if (sections.Empty()) { - return new ValidationFailure("Host", "At least one TV library is required"); + return new ValidationFailure("Host", "At least one Music library is required"); } } catch(PlexAuthenticationException ex) diff --git a/src/NzbDrone.Core/Notifications/Prowl/Prowl.cs b/src/NzbDrone.Core/Notifications/Prowl/Prowl.cs index 2b6352073..6d2876d4e 100644 --- a/src/NzbDrone.Core/Notifications/Prowl/Prowl.cs +++ b/src/NzbDrone.Core/Notifications/Prowl/Prowl.cs @@ -1,7 +1,6 @@ -using System.Collections.Generic; +using System.Collections.Generic; using FluentValidation.Results; using NzbDrone.Common.Extensions; -using NzbDrone.Core.Tv; using Prowlin; namespace NzbDrone.Core.Notifications.Prowl @@ -20,12 +19,12 @@ namespace NzbDrone.Core.Notifications.Prowl public override void OnGrab(GrabMessage grabMessage) { - _prowlService.SendNotification(EPISODE_GRABBED_TITLE, grabMessage.Message, Settings.ApiKey, (NotificationPriority)Settings.Priority); + _prowlService.SendNotification(ALBUM_GRABBED_TITLE, grabMessage.Message, Settings.ApiKey, (NotificationPriority)Settings.Priority); } public override void OnDownload(DownloadMessage message) { - _prowlService.SendNotification(EPISODE_DOWNLOADED_TITLE, message.Message, Settings.ApiKey, (NotificationPriority)Settings.Priority); + _prowlService.SendNotification(TRACK_DOWNLOADED_TITLE, message.Message, Settings.ApiKey, (NotificationPriority)Settings.Priority); } public override ValidationResult Test() diff --git a/src/NzbDrone.Core/Notifications/PushBullet/PushBullet.cs b/src/NzbDrone.Core/Notifications/PushBullet/PushBullet.cs index 066cd6f57..aee178a50 100644 --- a/src/NzbDrone.Core/Notifications/PushBullet/PushBullet.cs +++ b/src/NzbDrone.Core/Notifications/PushBullet/PushBullet.cs @@ -19,12 +19,12 @@ namespace NzbDrone.Core.Notifications.PushBullet public override void OnGrab(GrabMessage grabMessage) { - _proxy.SendNotification(EPISODE_GRABBED_TITLE_BRANDED, grabMessage.Message, Settings); + _proxy.SendNotification(ALBUM_GRABBED_TITLE_BRANDED, grabMessage.Message, Settings); } public override void OnDownload(DownloadMessage message) { - _proxy.SendNotification(EPISODE_DOWNLOADED_TITLE_BRANDED, message.Message, Settings); + _proxy.SendNotification(TRACK_DOWNLOADED_TITLE_BRANDED, message.Message, Settings); } public override ValidationResult Test() diff --git a/src/NzbDrone.Core/Notifications/Pushalot/Pushalot.cs b/src/NzbDrone.Core/Notifications/Pushalot/Pushalot.cs index c505034b8..aa5985dd2 100644 --- a/src/NzbDrone.Core/Notifications/Pushalot/Pushalot.cs +++ b/src/NzbDrone.Core/Notifications/Pushalot/Pushalot.cs @@ -1,7 +1,6 @@ -using System.Collections.Generic; +using System.Collections.Generic; using FluentValidation.Results; using NzbDrone.Common.Extensions; -using NzbDrone.Core.Tv; namespace NzbDrone.Core.Notifications.Pushalot { @@ -19,12 +18,12 @@ namespace NzbDrone.Core.Notifications.Pushalot public override void OnGrab(GrabMessage grabMessage) { - _proxy.SendNotification(EPISODE_GRABBED_TITLE, grabMessage.Message, Settings); + _proxy.SendNotification(ALBUM_GRABBED_TITLE, grabMessage.Message, Settings); } public override void OnDownload(DownloadMessage message) { - _proxy.SendNotification(EPISODE_DOWNLOADED_TITLE, message.Message, Settings); + _proxy.SendNotification(TRACK_DOWNLOADED_TITLE, message.Message, Settings); } diff --git a/src/NzbDrone.Core/Notifications/Pushover/Pushover.cs b/src/NzbDrone.Core/Notifications/Pushover/Pushover.cs index 0b7349d71..6755d77c0 100644 --- a/src/NzbDrone.Core/Notifications/Pushover/Pushover.cs +++ b/src/NzbDrone.Core/Notifications/Pushover/Pushover.cs @@ -18,12 +18,12 @@ namespace NzbDrone.Core.Notifications.Pushover public override void OnGrab(GrabMessage grabMessage) { - _proxy.SendNotification(EPISODE_GRABBED_TITLE, grabMessage.Message, Settings); + _proxy.SendNotification(ALBUM_GRABBED_TITLE, grabMessage.Message, Settings); } public override void OnDownload(DownloadMessage message) { - _proxy.SendNotification(EPISODE_DOWNLOADED_TITLE, message.Message, Settings); + _proxy.SendNotification(TRACK_DOWNLOADED_TITLE, message.Message, Settings); } public override ValidationResult Test() diff --git a/src/NzbDrone.Core/Notifications/Slack/Payloads/SlackPayload.cs b/src/NzbDrone.Core/Notifications/Slack/Payloads/SlackPayload.cs index a2c64b737..c67f4c55a 100644 --- a/src/NzbDrone.Core/Notifications/Slack/Payloads/SlackPayload.cs +++ b/src/NzbDrone.Core/Notifications/Slack/Payloads/SlackPayload.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using Newtonsoft.Json; namespace NzbDrone.Core.Notifications.Slack.Payloads @@ -12,6 +12,9 @@ namespace NzbDrone.Core.Notifications.Slack.Payloads [JsonProperty("icon_emoji")] public string IconEmoji { get; set; } + [JsonProperty("icon_url")] + public string IconUrl { get; set; } + public List Attachments { get; set; } } } diff --git a/src/NzbDrone.Core/Notifications/Slack/Slack.cs b/src/NzbDrone.Core/Notifications/Slack/Slack.cs index 2d4b5ae43..44614c0ca 100644 --- a/src/NzbDrone.Core/Notifications/Slack/Slack.cs +++ b/src/NzbDrone.Core/Notifications/Slack/Slack.cs @@ -1,11 +1,13 @@ -using System; +using System; using System.Collections.Generic; using FluentValidation.Results; using NLog; using NzbDrone.Common.Extensions; +using NzbDrone.Common.Http; +using NzbDrone.Common.Serializer; using NzbDrone.Core.Notifications.Slack.Payloads; using NzbDrone.Core.Rest; -using NzbDrone.Core.Tv; +using NzbDrone.Core.Music; using NzbDrone.Core.Validation; using RestSharp; @@ -14,10 +16,12 @@ namespace NzbDrone.Core.Notifications.Slack { public class Slack : NotificationBase { + private readonly ISlackProxy _proxy; private readonly Logger _logger; - public Slack(Logger logger) + public Slack(ISlackProxy proxy, Logger logger) { + _proxy = proxy; _logger = logger; } @@ -26,65 +30,51 @@ namespace NzbDrone.Core.Notifications.Slack public override void OnGrab(GrabMessage message) { - var payload = new SlackPayload - { - IconEmoji = Settings.Icon, - Username = Settings.Username, - Text = $"Grabbed: {message.Message}", - Attachments = new List - { - new Attachment - { - Fallback = message.Message, - Title = message.Series.Title, - Text = message.Message, - Color = "warning" - } - } - }; - - NotifySlack(payload); + var attachments = new List + { + new Attachment + { + Fallback = message.Message, + Title = message.Artist.Name, + Text = message.Message, + Color = "warning" + } + }; + var payload = CreatePayload($"Grabbed: {message.Message}", attachments); + + _proxy.SendPayload(payload, Settings); } public override void OnDownload(DownloadMessage message) { - var payload = new SlackPayload - { - IconEmoji = Settings.Icon, - Username = Settings.Username, - Text = $"Imported: {message.Message}", - Attachments = new List - { - new Attachment - { - Fallback = message.Message, - Title = message.Series.Title, - Text = message.Message, - Color = "good" - } - } - }; - - NotifySlack(payload); + var attachments = new List + { + new Attachment + { + Fallback = message.Message, + Title = message.Artist.Name, + Text = message.Message, + Color = "good" + } + }; + var payload = CreatePayload($"Imported: {message.Message}", attachments); + + _proxy.SendPayload(payload, Settings); } - public override void OnRename(Series series) + public override void OnRename(Artist artist) { - var payload = new SlackPayload - { - IconEmoji = Settings.Icon, - Username = Settings.Username, - Text = "Renamed", - Attachments = new List - { - new Attachment - { - Title = series.Title, - } - } - }; + var attachments = new List + { + new Attachment + { + Title = artist.Name, + } + }; + + var payload = CreatePayload("Renamed", attachments); - NotifySlack(payload); + _proxy.SendPayload(payload, Settings); } public override ValidationResult Test() @@ -101,14 +91,9 @@ namespace NzbDrone.Core.Notifications.Slack try { var message = $"Test message from Lidarr posted at {DateTime.Now}"; - var payload = new SlackPayload - { - IconEmoji = Settings.Icon, - Username = Settings.Username, - Text = message - }; + var payload = CreatePayload(message); - NotifySlack(payload); + _proxy.SendPayload(payload, Settings); } catch (SlackExeption ex) @@ -119,24 +104,31 @@ namespace NzbDrone.Core.Notifications.Slack return null; } - private void NotifySlack(SlackPayload payload) + private SlackPayload CreatePayload(string message, List attachments = null) { - try + var icon = Settings.Icon; + + var payload = new SlackPayload { - var client = RestClientFactory.BuildClient(Settings.WebHookUrl); - var request = new RestRequest(Method.POST) - { - RequestFormat = DataFormat.Json, - JsonSerializer = new JsonNetSerializer() - }; - request.AddBody(payload); - client.ExecuteAndValidate(request); - } - catch (RestException ex) + Username = Settings.Username, + Text = message, + Attachments = attachments + }; + + if (icon.IsNotNullOrWhiteSpace()) { - _logger.Error(ex, "Unable to post payload {0}", payload); - throw new SlackExeption("Unable to post payload", ex); + // Set the correct icon based on the value + if (icon.StartsWith(":") && icon.EndsWith(":")) + { + payload.IconEmoji = icon; + } + else + { + payload.IconUrl = icon; + } } + + return payload; } } } diff --git a/src/NzbDrone.Core/Notifications/Slack/SlackProxy.cs b/src/NzbDrone.Core/Notifications/Slack/SlackProxy.cs new file mode 100644 index 000000000..f510464d3 --- /dev/null +++ b/src/NzbDrone.Core/Notifications/Slack/SlackProxy.cs @@ -0,0 +1,46 @@ +using NLog; +using NzbDrone.Common.Http; +using NzbDrone.Common.Serializer; +using NzbDrone.Core.Notifications.Slack.Payloads; +using NzbDrone.Core.Rest; + +namespace NzbDrone.Core.Notifications.Slack +{ + public interface ISlackProxy + { + void SendPayload(SlackPayload payload, SlackSettings settings); + } + + public class SlackProxy : ISlackProxy + { + private readonly IHttpClient _httpClient; + private readonly Logger _logger; + + public SlackProxy(IHttpClient httpClient, Logger logger) + { + _httpClient = httpClient; + _logger = logger; + } + + public void SendPayload(SlackPayload payload, SlackSettings settings) + { + try + { + var request = new HttpRequestBuilder(settings.WebHookUrl) + .Accept(HttpAccept.Json) + .Build(); + + request.Method = HttpMethod.POST; + request.Headers.ContentType = "application/json"; + request.SetContent(payload.ToJson()); + + _httpClient.Execute(request); + } + catch (RestException ex) + { + _logger.Error(ex, "Unable to post payload {0}", payload); + throw new SlackExeption("Unable to post payload", ex); + } + } + } +} diff --git a/src/NzbDrone.Core/Notifications/Slack/SlackSettings.cs b/src/NzbDrone.Core/Notifications/Slack/SlackSettings.cs index f64daddb5..80fbfce0e 100644 --- a/src/NzbDrone.Core/Notifications/Slack/SlackSettings.cs +++ b/src/NzbDrone.Core/Notifications/Slack/SlackSettings.cs @@ -1,4 +1,4 @@ -using FluentValidation; +using FluentValidation; using NzbDrone.Core.Annotations; using NzbDrone.Core.ThingiProvider; using NzbDrone.Core.Validation; @@ -24,7 +24,7 @@ namespace NzbDrone.Core.Notifications.Slack [FieldDefinition(1, Label = "Username", HelpText = "Choose the username that this integration will post as", Type = FieldType.Textbox)] public string Username { get; set; } - [FieldDefinition(2, Label = "Icon", HelpText = "Change the icon that is used for messages from this integration", Type = FieldType.Textbox, HelpLink = "http://www.emoji-cheat-sheet.com/")] + [FieldDefinition(2, Label = "Icon", HelpText = "Change the icon that is used for messages from this integration (Emoji or URL)", Type = FieldType.Textbox, HelpLink = "http://www.emoji-cheat-sheet.com/")] public string Icon { get; set; } public NzbDroneValidationResult Validate() diff --git a/src/NzbDrone.Core/Notifications/Synology/SynologyIndexer.cs b/src/NzbDrone.Core/Notifications/Synology/SynologyIndexer.cs index 7006b8d29..8631f030d 100644 --- a/src/NzbDrone.Core/Notifications/Synology/SynologyIndexer.cs +++ b/src/NzbDrone.Core/Notifications/Synology/SynologyIndexer.cs @@ -1,9 +1,9 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.IO; using FluentValidation.Results; using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.Extensions; -using NzbDrone.Core.Tv; +using NzbDrone.Core.Music; namespace NzbDrone.Core.Notifications.Synology { @@ -26,24 +26,24 @@ namespace NzbDrone.Core.Notifications.Synology { foreach (var oldFile in message.OldFiles) { - var fullPath = Path.Combine(message.Series.Path, oldFile.RelativePath); + var fullPath = Path.Combine(message.Artist.Path, oldFile.RelativePath); _indexerProxy.DeleteFile(fullPath); } { - var fullPath = Path.Combine(message.Series.Path, message.EpisodeFile.RelativePath); + var fullPath = Path.Combine(message.Artist.Path, message.TrackFile.RelativePath); _indexerProxy.AddFile(fullPath); } } } - public override void OnRename(Series series) + public override void OnRename(Artist artist) { if (Settings.UpdateLibrary) { - _indexerProxy.UpdateFolder(series.Path); + _indexerProxy.UpdateFolder(artist.Path); } } diff --git a/src/NzbDrone.Core/Notifications/Telegram/Telegram.cs b/src/NzbDrone.Core/Notifications/Telegram/Telegram.cs index 83648e9f2..e44308bb7 100644 --- a/src/NzbDrone.Core/Notifications/Telegram/Telegram.cs +++ b/src/NzbDrone.Core/Notifications/Telegram/Telegram.cs @@ -18,12 +18,12 @@ namespace NzbDrone.Core.Notifications.Telegram public override void OnGrab(GrabMessage grabMessage) { - _proxy.SendNotification(EPISODE_GRABBED_TITLE, grabMessage.Message, Settings); + _proxy.SendNotification(ALBUM_GRABBED_TITLE, grabMessage.Message, Settings); } public override void OnDownload(DownloadMessage message) { - _proxy.SendNotification(EPISODE_DOWNLOADED_TITLE, message.Message, Settings); + _proxy.SendNotification(TRACK_DOWNLOADED_TITLE, message.Message, Settings); } public override ValidationResult Test() diff --git a/src/NzbDrone.Core/Notifications/Twitter/TwitterSettings.cs b/src/NzbDrone.Core/Notifications/Twitter/TwitterSettings.cs index 470ed317a..44d176675 100644 --- a/src/NzbDrone.Core/Notifications/Twitter/TwitterSettings.cs +++ b/src/NzbDrone.Core/Notifications/Twitter/TwitterSettings.cs @@ -1,4 +1,4 @@ -using FluentValidation; +using FluentValidation; using NzbDrone.Common.Extensions; using NzbDrone.Core.Annotations; using NzbDrone.Core.ThingiProvider; @@ -34,7 +34,7 @@ namespace NzbDrone.Core.Notifications.Twitter public TwitterSettings() { DirectMessage = true; - AuthorizeNotification = "step1"; + AuthorizeNotification = "startOAuth"; } [FieldDefinition(0, Label = "Consumer Key", HelpText = "Consumer key from a Twitter application", HelpLink = "https://github.com/Lidarr/Lidarr/wiki/Twitter-Notifications")] @@ -55,7 +55,7 @@ namespace NzbDrone.Core.Notifications.Twitter [FieldDefinition(5, Label = "Direct Message", Type = FieldType.Checkbox, HelpText = "Send a direct message instead of a public message")] public bool DirectMessage { get; set; } - [FieldDefinition(6, Label = "Connect to twitter", Type = FieldType.Action)] + [FieldDefinition(6, Label = "Connect to Twitter", Type = FieldType.OAuth)] public string AuthorizeNotification { get; set; } public NzbDroneValidationResult Validate() diff --git a/src/NzbDrone.Core/Notifications/Webhook/Webhook.cs b/src/NzbDrone.Core/Notifications/Webhook/Webhook.cs index 15f4ff00e..252503071 100644 --- a/src/NzbDrone.Core/Notifications/Webhook/Webhook.cs +++ b/src/NzbDrone.Core/Notifications/Webhook/Webhook.cs @@ -1,35 +1,78 @@ - using System.Collections.Generic; +using System.Linq; using FluentValidation.Results; -using NzbDrone.Core.Tv; +using NzbDrone.Core.Music; using NzbDrone.Common.Extensions; +using NzbDrone.Core.Validation; namespace NzbDrone.Core.Notifications.Webhook { public class Webhook : NotificationBase { - private readonly IWebhookService _service; + private readonly IWebhookProxy _proxy; - public Webhook(IWebhookService service) + public Webhook(IWebhookProxy proxy) { - _service = service; + _proxy = proxy; } public override string Link => "https://github.com/Lidarr/Lidarr/wiki/Webhook"; public override void OnGrab(GrabMessage message) { - _service.OnGrab(message.Series, message.Episode, message.Quality, Settings); + var remoteAlbum = message.Album; + var quality = message.Quality; + + var payload = new WebhookGrabPayload + + { + EventType = "Grab", + Artist = new WebhookArtist(message.Artist), + Albums = remoteAlbum.Albums.ConvertAll(x => new WebhookAlbum(x) + { + // TODO: Stop passing these parameters inside an album v3 + Quality = quality.Quality.Name, + QualityVersion = quality.Revision.Version, + ReleaseGroup = remoteAlbum.ParsedAlbumInfo.ReleaseGroup + }), + Release = new WebhookRelease(quality, remoteAlbum) + }; + + _proxy.SendWebhook(payload, Settings); } public override void OnDownload(DownloadMessage message) { - _service.OnDownload(message.Series, message.EpisodeFile, Settings); + var trackFile = message.TrackFile; + + var payload = new WebhookImportPayload + + { + EventType = "Download", + Artist = new WebhookArtist(message.Artist), + Tracks = trackFile.Tracks.Value.ConvertAll(x => new WebhookTrack(x) + { + // TODO: Stop passing these parameters inside an episode v3 + Quality = trackFile.Quality.Quality.Name, + QualityVersion = trackFile.Quality.Revision.Version, + ReleaseGroup = trackFile.ReleaseGroup + }), + TrackFile = new WebhookTrackFile(trackFile), + IsUpgrade = message.OldFiles.Any() + }; + + _proxy.SendWebhook(payload, Settings); } - public override void OnRename(Series series) + public override void OnRename(Artist artist) { - _service.OnRename(series, Settings); + var payload = new WebhookPayload + { + EventType = "Rename", + Artist = new WebhookArtist(artist) + }; + + _proxy.SendWebhook(payload, Settings); } public override string Name => "Webhook"; @@ -38,9 +81,42 @@ namespace NzbDrone.Core.Notifications.Webhook { var failures = new List(); - failures.AddIfNotNull(_service.Test(Settings)); + failures.AddIfNotNull(SendWebhookTest()); return new ValidationResult(failures); } + + private ValidationFailure SendWebhookTest() + { + try + { + var payload = new WebhookGrabPayload + { + EventType = "Test", + Artist = new WebhookArtist() + { + Id = 1, + Name = "Test Name", + Path = "C:\\testpath", + MBId = "aaaaa-aaa-aaaa-aaaaaa" + }, + Albums = new List() { + new WebhookAlbum() + { + Id = 123, + Title = "Test title" + } + } + }; + + _proxy.SendWebhook(payload, Settings); + } + catch (WebhookException ex) + { + return new NzbDroneValidationFailure("Url", ex.Message); + } + + return null; + } } } diff --git a/src/NzbDrone.Core/Notifications/Webhook/WebhookAlbum.cs b/src/NzbDrone.Core/Notifications/Webhook/WebhookAlbum.cs new file mode 100644 index 000000000..433bb7370 --- /dev/null +++ b/src/NzbDrone.Core/Notifications/Webhook/WebhookAlbum.cs @@ -0,0 +1,26 @@ +using NzbDrone.Core.Music; +using System; + +namespace NzbDrone.Core.Notifications.Webhook +{ + public class WebhookAlbum + { + public WebhookAlbum() { } + + public WebhookAlbum(Album album) + { + Id = album.Id; + Title = album.Title; + ReleaseDate = album.ReleaseDate; + } + + public int Id { get; set; } + public string Title { get; set; } + public DateTime? ReleaseDate { get; set; } + + public string Quality { get; set; } + public int QualityVersion { get; set; } + public string ReleaseGroup { get; set; } + public string SceneName { get; set; } + } +} diff --git a/src/NzbDrone.Core/Notifications/Webhook/WebhookArtist.cs b/src/NzbDrone.Core/Notifications/Webhook/WebhookArtist.cs new file mode 100644 index 000000000..5a699d26d --- /dev/null +++ b/src/NzbDrone.Core/Notifications/Webhook/WebhookArtist.cs @@ -0,0 +1,22 @@ +using NzbDrone.Core.Music; + +namespace NzbDrone.Core.Notifications.Webhook +{ + public class WebhookArtist + { + public int Id { get; set; } + public string Name { get; set; } + public string Path { get; set; } + public string MBId { get; set; } + + public WebhookArtist() { } + + public WebhookArtist(Artist artist) + { + Id = artist.Id; + Name = artist.Name; + Path = artist.Path; + MBId = artist.ForeignArtistId; + } + } +} diff --git a/src/NzbDrone.Core/Notifications/Webhook/WebhookEpisode.cs b/src/NzbDrone.Core/Notifications/Webhook/WebhookEpisode.cs deleted file mode 100644 index a7979b726..000000000 --- a/src/NzbDrone.Core/Notifications/Webhook/WebhookEpisode.cs +++ /dev/null @@ -1,32 +0,0 @@ -using NzbDrone.Core.Tv; -using System; - -namespace NzbDrone.Core.Notifications.Webhook -{ - public class WebhookEpisode - { - public WebhookEpisode() { } - - public WebhookEpisode(Episode episode) - { - Id = episode.Id; - SeasonNumber = episode.SeasonNumber; - EpisodeNumber = episode.EpisodeNumber; - Title = episode.Title; - AirDate = episode.AirDate; - AirDateUtc = episode.AirDateUtc; - } - - public int Id { get; set; } - public int EpisodeNumber { get; set; } - public int SeasonNumber { get; set; } - public string Title { get; set; } - public string AirDate { get; set; } - public DateTime? AirDateUtc { get; set; } - - public string Quality { get; set; } - public int QualityVersion { get; set; } - public string ReleaseGroup { get; set; } - public string SceneName { get; set; } - } -} \ No newline at end of file diff --git a/src/NzbDrone.Core/Notifications/Webhook/WebhookGrabPayload.cs b/src/NzbDrone.Core/Notifications/Webhook/WebhookGrabPayload.cs new file mode 100644 index 000000000..4cb7c868b --- /dev/null +++ b/src/NzbDrone.Core/Notifications/Webhook/WebhookGrabPayload.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; + +namespace NzbDrone.Core.Notifications.Webhook +{ + public class WebhookGrabPayload : WebhookPayload + { + public List Albums { get; set; } + public WebhookRelease Release { get; set; } + } +} diff --git a/src/NzbDrone.Core/Notifications/Webhook/WebhookImportPayload.cs b/src/NzbDrone.Core/Notifications/Webhook/WebhookImportPayload.cs new file mode 100644 index 000000000..d3cfd0ed3 --- /dev/null +++ b/src/NzbDrone.Core/Notifications/Webhook/WebhookImportPayload.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; + +namespace NzbDrone.Core.Notifications.Webhook +{ + public class WebhookImportPayload : WebhookPayload + { + public List Tracks { get; set; } + public WebhookTrackFile TrackFile { get; set; } + public bool IsUpgrade { get; set; } + } +} diff --git a/src/NzbDrone.Core/Notifications/Webhook/WebhookMethod.cs b/src/NzbDrone.Core/Notifications/Webhook/WebhookMethod.cs index 42c080e00..5d6e859a6 100644 --- a/src/NzbDrone.Core/Notifications/Webhook/WebhookMethod.cs +++ b/src/NzbDrone.Core/Notifications/Webhook/WebhookMethod.cs @@ -1,8 +1,10 @@ -namespace NzbDrone.Core.Notifications.Webhook +using NzbDrone.Common.Http; + +namespace NzbDrone.Core.Notifications.Webhook { public enum WebhookMethod { - POST = RestSharp.Method.POST, - PUT = RestSharp.Method.PUT + POST = HttpMethod.POST, + PUT = HttpMethod.PUT } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Core/Notifications/Webhook/WebhookPayload.cs b/src/NzbDrone.Core/Notifications/Webhook/WebhookPayload.cs index 41009a695..3beff4105 100644 --- a/src/NzbDrone.Core/Notifications/Webhook/WebhookPayload.cs +++ b/src/NzbDrone.Core/Notifications/Webhook/WebhookPayload.cs @@ -1,11 +1,8 @@ -using System.Collections.Generic; - namespace NzbDrone.Core.Notifications.Webhook { public class WebhookPayload { public string EventType { get; set; } - public WebhookSeries Series { get; set; } - public List Episodes { get; set; } + public WebhookArtist Artist { get; set; } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Core/Notifications/Webhook/WebhookProxy.cs b/src/NzbDrone.Core/Notifications/Webhook/WebhookProxy.cs new file mode 100644 index 000000000..050c5527f --- /dev/null +++ b/src/NzbDrone.Core/Notifications/Webhook/WebhookProxy.cs @@ -0,0 +1,41 @@ +using NzbDrone.Common.Http; +using NzbDrone.Common.Serializer; +using NzbDrone.Core.Rest; + +namespace NzbDrone.Core.Notifications.Webhook +{ + public interface IWebhookProxy + { + void SendWebhook(WebhookPayload payload, WebhookSettings settings); + } + + public class WebhookProxy : IWebhookProxy + { + private readonly IHttpClient _httpClient; + + public WebhookProxy(IHttpClient httpClient) + { + _httpClient = httpClient; + } + + public void SendWebhook(WebhookPayload body, WebhookSettings settings) + { + try + { + var request = new HttpRequestBuilder(settings.Url) + .Accept(HttpAccept.Json) + .Build(); + + request.Method = (HttpMethod)settings.Method; + request.Headers.ContentType = "application/json"; + request.SetContent(body.ToJson()); + + _httpClient.Execute(request); + } + catch (RestException ex) + { + throw new WebhookException("Unable to post to webhook: {0}", ex, ex.Message); + } + } + } +} diff --git a/src/NzbDrone.Core/Notifications/Webhook/WebhookRelease.cs b/src/NzbDrone.Core/Notifications/Webhook/WebhookRelease.cs new file mode 100644 index 000000000..60ff931da --- /dev/null +++ b/src/NzbDrone.Core/Notifications/Webhook/WebhookRelease.cs @@ -0,0 +1,27 @@ +using NzbDrone.Core.Parser.Model; +using NzbDrone.Core.Qualities; + +namespace NzbDrone.Core.Notifications.Webhook +{ + public class WebhookRelease + { + public WebhookRelease() { } + + public WebhookRelease(QualityModel quality, RemoteAlbum remoteAlbum) + { + Quality = quality.Quality.Name; + QualityVersion = quality.Revision.Version; + ReleaseGroup = remoteAlbum.ParsedAlbumInfo.ReleaseGroup; + ReleaseTitle = remoteAlbum.Release.Title; + Indexer = remoteAlbum.Release.Indexer; + Size = remoteAlbum.Release.Size; + } + + public string Quality { get; set; } + public int QualityVersion { get; set; } + public string ReleaseGroup { get; set; } + public string ReleaseTitle { get; set; } + public string Indexer { get; set; } + public long Size { get; set; } + } +} diff --git a/src/NzbDrone.Core/Notifications/Webhook/WebhookSeries.cs b/src/NzbDrone.Core/Notifications/Webhook/WebhookSeries.cs deleted file mode 100644 index 222f9eebb..000000000 --- a/src/NzbDrone.Core/Notifications/Webhook/WebhookSeries.cs +++ /dev/null @@ -1,22 +0,0 @@ -using NzbDrone.Core.Tv; - -namespace NzbDrone.Core.Notifications.Webhook -{ - public class WebhookSeries - { - public int Id { get; set; } - public string Title { get; set; } - public string Path { get; set; } - public int TvdbId { get; set; } - - public WebhookSeries() { } - - public WebhookSeries(Series series) - { - Id = series.Id; - Title = series.Title; - Path = series.Path; - TvdbId = series.TvdbId; - } - } -} \ No newline at end of file diff --git a/src/NzbDrone.Core/Notifications/Webhook/WebhookService.cs b/src/NzbDrone.Core/Notifications/Webhook/WebhookService.cs deleted file mode 100644 index b04efa168..000000000 --- a/src/NzbDrone.Core/Notifications/Webhook/WebhookService.cs +++ /dev/null @@ -1,118 +0,0 @@ -using FluentValidation.Results; -using NzbDrone.Core.MediaFiles; -using NzbDrone.Core.Tv; -using NzbDrone.Core.Validation; -using NzbDrone.Core.Rest; -using RestSharp; -using NzbDrone.Core.Qualities; -using NzbDrone.Core.Parser.Model; -using System.Collections.Generic; - -namespace NzbDrone.Core.Notifications.Webhook -{ - public interface IWebhookService - { - void OnDownload(Series series, EpisodeFile episodeFile, WebhookSettings settings); - void OnRename(Series series, WebhookSettings settings); - void OnGrab(Series series, RemoteEpisode episode, QualityModel quality, WebhookSettings settings); - ValidationFailure Test(WebhookSettings settings); - } - - public class WebhookService : IWebhookService - { - public void OnDownload(Series series, EpisodeFile episodeFile, WebhookSettings settings) - { - var payload = new WebhookPayload - { - EventType = "Download", - Series = new WebhookSeries(series), - Episodes = episodeFile.Episodes.Value.ConvertAll(x => new WebhookEpisode(x) { - Quality = episodeFile.Quality.Quality.Name, - QualityVersion = episodeFile.Quality.Revision.Version, - ReleaseGroup = episodeFile.ReleaseGroup, - SceneName = episodeFile.SceneName - }) - }; - - NotifyWebhook(payload, settings); - } - - public void OnRename(Series series, WebhookSettings settings) - { - var payload = new WebhookPayload - { - EventType = "Rename", - Series = new WebhookSeries(series) - }; - - NotifyWebhook(payload, settings); - } - - public void OnGrab(Series series, RemoteEpisode episode, QualityModel quality, WebhookSettings settings) - { - var payload = new WebhookPayload - { - EventType = "Grab", - Series = new WebhookSeries(series), - Episodes = episode.Episodes.ConvertAll(x => new WebhookEpisode(x) - { - Quality = quality.Quality.Name, - QualityVersion = quality.Revision.Version, - ReleaseGroup = episode.ParsedEpisodeInfo.ReleaseGroup - }) - }; - NotifyWebhook(payload, settings); - } - - public void NotifyWebhook(WebhookPayload body, WebhookSettings settings) - { - try { - var client = RestClientFactory.BuildClient(settings.Url); - var request = new RestRequest((Method) settings.Method); - request.RequestFormat = DataFormat.Json; - request.AddBody(body); - client.ExecuteAndValidate(request); - } - catch (RestException ex) - { - throw new WebhookException("Unable to post to webhook: {0}", ex, ex.Message); - } - } - - public ValidationFailure Test(WebhookSettings settings) - { - try - { - NotifyWebhook( - new WebhookPayload - { - EventType = "Test", - Series = new WebhookSeries() - { - Id = 1, - Title = "Test Title", - Path = "C:\\testpath", - TvdbId = 1234 - }, - Episodes = new List() { - new WebhookEpisode() - { - Id = 123, - EpisodeNumber = 1, - SeasonNumber = 1, - Title = "Test title" - } - } - }, - settings - ); - } - catch (WebhookException ex) - { - return new NzbDroneValidationFailure("Url", ex.Message); - } - - return null; - } - } -} diff --git a/src/NzbDrone.Core/Notifications/Webhook/WebhookTrack.cs b/src/NzbDrone.Core/Notifications/Webhook/WebhookTrack.cs new file mode 100644 index 000000000..3d03fda2c --- /dev/null +++ b/src/NzbDrone.Core/Notifications/Webhook/WebhookTrack.cs @@ -0,0 +1,26 @@ +using NzbDrone.Core.Music; +using System; + +namespace NzbDrone.Core.Notifications.Webhook +{ + public class WebhookTrack + { + public WebhookTrack() { } + + public WebhookTrack(Track track) + { + Id = track.Id; + Title = track.Title; + TrackNumber = track.TrackNumber; + + } + + public int Id { get; set; } + public string Title { get; set; } + public int TrackNumber { get; set; } + + public string Quality { get; set; } + public int QualityVersion { get; set; } + public string ReleaseGroup { get; set; } + } +} diff --git a/src/NzbDrone.Core/Notifications/Webhook/WebhookTrackFile.cs b/src/NzbDrone.Core/Notifications/Webhook/WebhookTrackFile.cs new file mode 100644 index 000000000..a51bd520a --- /dev/null +++ b/src/NzbDrone.Core/Notifications/Webhook/WebhookTrackFile.cs @@ -0,0 +1,28 @@ +using NzbDrone.Core.MediaFiles; + +namespace NzbDrone.Core.Notifications.Webhook +{ + public class WebhookTrackFile + { + public WebhookTrackFile() { } + + public WebhookTrackFile(TrackFile trackFile) + { + Id = trackFile.Id; + RelativePath = trackFile.RelativePath; + Path = trackFile.Path; + Quality = trackFile.Quality.Quality.Name; + QualityVersion = trackFile.Quality.Revision.Version; + ReleaseGroup = trackFile.ReleaseGroup; + SceneName = trackFile.SceneName; + } + + public int Id { get; set; } + public string RelativePath { get; set; } + public string Path { get; set; } + public string Quality { get; set; } + public int QualityVersion { get; set; } + public string ReleaseGroup { get; set; } + public string SceneName { get; set; } + } +} diff --git a/src/NzbDrone.Core/Notifications/Xbmc/HttpApiProvider.cs b/src/NzbDrone.Core/Notifications/Xbmc/HttpApiProvider.cs index e1481fdae..564cdc397 100644 --- a/src/NzbDrone.Core/Notifications/Xbmc/HttpApiProvider.cs +++ b/src/NzbDrone.Core/Notifications/Xbmc/HttpApiProvider.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.IO; using System.Linq; @@ -6,7 +6,7 @@ using System.Xml.Linq; using NLog; using NzbDrone.Common.Http; using NzbDrone.Core.Notifications.Xbmc.Model; -using NzbDrone.Core.Tv; +using NzbDrone.Core.Music; namespace NzbDrone.Core.Notifications.Xbmc { @@ -34,7 +34,7 @@ namespace NzbDrone.Core.Notifications.Xbmc SendCommand(settings, command); } - public void Update(XbmcSettings settings, Series series) + public void Update(XbmcSettings settings, Artist artist) { if (!settings.AlwaysUpdate) { @@ -48,7 +48,7 @@ namespace NzbDrone.Core.Notifications.Xbmc } } - UpdateLibrary(settings, series); + UpdateLibrary(settings, artist); } public void Clean(XbmcSettings settings) @@ -80,12 +80,12 @@ namespace NzbDrone.Core.Notifications.Xbmc return new List(); } - internal string GetSeriesPath(XbmcSettings settings, Series series) + internal string GetSeriesPath(XbmcSettings settings, Artist artist) { var query = string.Format( "select path.strPath from path, tvshow, tvshowlinkpath where tvshow.c12 = {0} and tvshowlinkpath.idShow = tvshow.idShow and tvshowlinkpath.idPath = path.idPath", - series.TvdbId); + artist.ForeignArtistId); var command = string.Format("QueryVideoDatabase({0})", query); const string setResponseCommand = @@ -137,17 +137,17 @@ namespace NzbDrone.Core.Notifications.Xbmc return false; } - private void UpdateLibrary(XbmcSettings settings, Series series) + private void UpdateLibrary(XbmcSettings settings, Artist artist) { try { _logger.Debug("Sending Update DB Request to XBMC Host: {0}", settings.Address); - var xbmcSeriesPath = GetSeriesPath(settings, series); + var xbmcSeriesPath = GetSeriesPath(settings, artist); //If the path is found update it, else update the whole library if (!string.IsNullOrEmpty(xbmcSeriesPath)) { - _logger.Debug("Updating series [{0}] on XBMC host: {1}", series, settings.Address); + _logger.Debug("Updating artist [{0}] on XBMC host: {1}", artist, settings.Address); var command = BuildExecBuiltInCommand(string.Format("UpdateLibrary(video,{0})", xbmcSeriesPath)); SendCommand(settings, command); } @@ -155,7 +155,7 @@ namespace NzbDrone.Core.Notifications.Xbmc else { //Update the entire library - _logger.Debug("Series [{0}] doesn't exist on XBMC host: {1}, Updating Entire Library", series, settings.Address); + _logger.Debug("Artist [{0}] doesn't exist on XBMC host: {1}, Updating Entire Library", artist, settings.Address); var command = BuildExecBuiltInCommand("UpdateLibrary(video)"); SendCommand(settings, command); } diff --git a/src/NzbDrone.Core/Notifications/Xbmc/IApiProvider.cs b/src/NzbDrone.Core/Notifications/Xbmc/IApiProvider.cs index bf250edc3..4c93ac123 100644 --- a/src/NzbDrone.Core/Notifications/Xbmc/IApiProvider.cs +++ b/src/NzbDrone.Core/Notifications/Xbmc/IApiProvider.cs @@ -1,12 +1,12 @@ -using NzbDrone.Core.Notifications.Xbmc.Model; -using NzbDrone.Core.Tv; +using NzbDrone.Core.Notifications.Xbmc.Model; +using NzbDrone.Core.Music; namespace NzbDrone.Core.Notifications.Xbmc { public interface IApiProvider { void Notify(XbmcSettings settings, string title, string message); - void Update(XbmcSettings settings, Series series); + void Update(XbmcSettings settings, Artist artist); void Clean(XbmcSettings settings); bool CanHandle(XbmcVersion version); } diff --git a/src/NzbDrone.Core/Notifications/Xbmc/JsonApiProvider.cs b/src/NzbDrone.Core/Notifications/Xbmc/JsonApiProvider.cs index 1a0674908..b61830ffc 100644 --- a/src/NzbDrone.Core/Notifications/Xbmc/JsonApiProvider.cs +++ b/src/NzbDrone.Core/Notifications/Xbmc/JsonApiProvider.cs @@ -1,9 +1,9 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using NLog; using NzbDrone.Core.Notifications.Xbmc.Model; -using NzbDrone.Core.Tv; +using NzbDrone.Core.Music; namespace NzbDrone.Core.Notifications.Xbmc { @@ -28,7 +28,7 @@ namespace NzbDrone.Core.Notifications.Xbmc _proxy.Notify(settings, title, message); } - public void Update(XbmcSettings settings, Series series) + public void Update(XbmcSettings settings, Artist artist) { if (!settings.AlwaysUpdate) { @@ -42,7 +42,7 @@ namespace NzbDrone.Core.Notifications.Xbmc } } - UpdateLibrary(settings, series); + UpdateLibrary(settings, artist); } public void Clean(XbmcSettings settings) @@ -55,22 +55,22 @@ namespace NzbDrone.Core.Notifications.Xbmc return _proxy.GetActivePlayers(settings); } - public string GetSeriesPath(XbmcSettings settings, Series series) + public string GetSeriesPath(XbmcSettings settings, Artist artist) { - var allSeries = _proxy.GetSeries(settings); + var allSeries = _proxy.GetArtist(settings); if (!allSeries.Any()) { - _logger.Debug("No TV shows returned from XBMC"); + _logger.Debug("No Artists returned from XBMC"); return null; } var matchingSeries = allSeries.FirstOrDefault(s => { - var tvdbId = 0; - int.TryParse(s.ImdbNumber, out tvdbId); + var tvdbId = "0"; + //int.TryParse(s.ImdbNumber, out tvdbId); - return tvdbId == series.TvdbId || s.Label == series.Title; + return tvdbId == artist.ForeignArtistId || s.Label == artist.Name; }); if (matchingSeries != null) return matchingSeries.File; @@ -78,20 +78,20 @@ namespace NzbDrone.Core.Notifications.Xbmc return null; } - private void UpdateLibrary(XbmcSettings settings, Series series) + private void UpdateLibrary(XbmcSettings settings, Artist artist) { try { - var seriesPath = GetSeriesPath(settings, series); + var seriesPath = GetSeriesPath(settings, artist); if (seriesPath != null) { - _logger.Debug("Updating series {0} (Path: {1}) on XBMC host: {2}", series, seriesPath, settings.Address); + _logger.Debug("Updating artist {0} (Path: {1}) on XBMC host: {2}", artist, seriesPath, settings.Address); } else { - _logger.Debug("Series {0} doesn't exist on XBMC host: {1}, Updating Entire Library", series, + _logger.Debug("Artist {0} doesn't exist on XBMC host: {1}, Updating Entire Library", artist, settings.Address); } diff --git a/src/NzbDrone.Core/Notifications/Xbmc/Xbmc.cs b/src/NzbDrone.Core/Notifications/Xbmc/Xbmc.cs index 9bb87f474..e5aaf6b44 100644 --- a/src/NzbDrone.Core/Notifications/Xbmc/Xbmc.cs +++ b/src/NzbDrone.Core/Notifications/Xbmc/Xbmc.cs @@ -1,10 +1,10 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using System.Net.Sockets; using FluentValidation.Results; using NLog; using NzbDrone.Common.Extensions; -using NzbDrone.Core.Tv; +using NzbDrone.Core.Music; namespace NzbDrone.Core.Notifications.Xbmc { @@ -33,12 +33,12 @@ namespace NzbDrone.Core.Notifications.Xbmc const string header = "Lidarr - Downloaded"; Notify(Settings, header, message.Message); - UpdateAndClean(message.Series, message.OldFiles.Any()); + UpdateAndClean(message.Artist, message.OldFiles.Any()); } - public override void OnRename(Series series) + public override void OnRename(Artist artist) { - UpdateAndClean(series); + UpdateAndClean(artist); } public override string Name => "Kodi (XBMC)"; @@ -68,13 +68,13 @@ namespace NzbDrone.Core.Notifications.Xbmc } } - private void UpdateAndClean(Series series, bool clean = true) + private void UpdateAndClean(Artist artist, bool clean = true) { try { if (Settings.UpdateLibrary) { - _xbmcService.Update(Settings, series); + _xbmcService.Update(Settings, artist); } if (clean && Settings.CleanLibrary) diff --git a/src/NzbDrone.Core/Notifications/Xbmc/XbmcJsonApiProxy.cs b/src/NzbDrone.Core/Notifications/Xbmc/XbmcJsonApiProxy.cs index a4ee40549..5d5f42386 100644 --- a/src/NzbDrone.Core/Notifications/Xbmc/XbmcJsonApiProxy.cs +++ b/src/NzbDrone.Core/Notifications/Xbmc/XbmcJsonApiProxy.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using NLog; using NzbDrone.Common.Extensions; using NzbDrone.Common.Serializer; @@ -16,7 +16,7 @@ namespace NzbDrone.Core.Notifications.Xbmc string UpdateLibrary(XbmcSettings settings, string path); void CleanLibrary(XbmcSettings settings); List GetActivePlayers(XbmcSettings settings); - List GetSeries(XbmcSettings settings); + List GetArtist(XbmcSettings settings); } public class XbmcJsonApiProxy : IXbmcJsonApiProxy @@ -79,7 +79,7 @@ namespace NzbDrone.Core.Notifications.Xbmc return Json.Deserialize(response).Result; } - public List GetSeries(XbmcSettings settings) + public List GetArtist(XbmcSettings settings) { var request = new RestRequest(); var parameters = new Dictionary(); diff --git a/src/NzbDrone.Core/Notifications/Xbmc/XbmcService.cs b/src/NzbDrone.Core/Notifications/Xbmc/XbmcService.cs index d9cacf8f8..040b1a269 100644 --- a/src/NzbDrone.Core/Notifications/Xbmc/XbmcService.cs +++ b/src/NzbDrone.Core/Notifications/Xbmc/XbmcService.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using FluentValidation.Results; @@ -7,14 +7,14 @@ using NLog; using NzbDrone.Common.Cache; using NzbDrone.Common.Serializer; using NzbDrone.Core.Notifications.Xbmc.Model; -using NzbDrone.Core.Tv; +using NzbDrone.Core.Music; namespace NzbDrone.Core.Notifications.Xbmc { public interface IXbmcService { void Notify(XbmcSettings settings, string title, string message); - void Update(XbmcSettings settings, Series series); + void Update(XbmcSettings settings, Artist artist); void Clean(XbmcSettings settings); ValidationFailure Test(XbmcSettings settings, string message); } @@ -45,10 +45,10 @@ namespace NzbDrone.Core.Notifications.Xbmc provider.Notify(settings, title, message); } - public void Update(XbmcSettings settings, Series series) + public void Update(XbmcSettings settings, Artist artist) { var provider = GetApiProvider(settings); - provider.Update(settings, series); + provider.Update(settings, artist); } public void Clean(XbmcSettings settings) @@ -122,4 +122,4 @@ namespace NzbDrone.Core.Notifications.Xbmc return null; } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Core/NzbDrone.Core.csproj b/src/NzbDrone.Core/NzbDrone.Core.csproj index 534145063..01e53521a 100644 --- a/src/NzbDrone.Core/NzbDrone.Core.csproj +++ b/src/NzbDrone.Core/NzbDrone.Core.csproj @@ -1,5 +1,5 @@  - + Debug x86 @@ -9,8 +9,8 @@ Library Properties NzbDrone.Core - NzbDrone.Core - v4.0 + Lidarr.Core + v4.6.1 512 @@ -41,6 +41,7 @@ DEBUG;TRACE prompt 4 + false x86 @@ -50,19 +51,20 @@ TRACE prompt 4 + false + + ..\packages\xmlrpcnet.2.5.0\lib\net20\CookComputing.XmlRpcV2.dll + ..\packages\FluentMigrator.1.6.2\lib\40\FluentMigrator.dll - True ..\packages\FluentMigrator.Runner.1.6.2\lib\40\FluentMigrator.Runner.dll - True - ..\packages\FluentValidation.6.2.1.0\lib\portable-net40+sl50+wp80+win8+wpa81\FluentValidation.dll - True + ..\packages\FluentValidation.6.2.1.0\lib\Net45\FluentValidation.dll False @@ -74,29 +76,25 @@ ..\packages\ImageResizer.3.4.3\lib\ImageResizer.dll - True - ..\packages\Newtonsoft.Json.9.0.1\lib\net40\Newtonsoft.Json.dll - True + ..\packages\Newtonsoft.Json.9.0.1\lib\net45\Newtonsoft.Json.dll - ..\packages\NLog.4.4.3\lib\net40\NLog.dll + ..\packages\NLog.4.4.12\lib\net45\NLog.dll - + ..\packages\OAuth.1.0.3\lib\net40\OAuth.dll - - False - ..\packages\xmlrpcnet.2.5.0\lib\net20\CookComputing.XmlRpcV2.dll + + ..\packages\Prowlin.0.9.4456.26422\lib\net40\Prowlin.dll ..\packages\taglib.2.1.0.0\lib\policy.2.0.taglib-sharp.dll True - ..\packages\RestSharp.105.2.3\lib\net4\RestSharp.dll - True + ..\packages\RestSharp.105.2.3\lib\net46\RestSharp.dll @@ -108,9 +106,6 @@ - - ..\packages\Prowlin.0.9.4456.26422\lib\net40\Prowlin.dll - ..\Libraries\Sqlite\System.Data.SQLite.dll @@ -132,6 +127,7 @@ + @@ -148,25 +144,13 @@ - - - - - - - - - - - - - + @@ -187,119 +171,7 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Code - - - - - - - + @@ -324,13 +196,13 @@ - - + + @@ -342,6 +214,7 @@ + @@ -350,10 +223,11 @@ - + + @@ -371,6 +245,7 @@ + @@ -446,6 +321,7 @@ + @@ -497,15 +373,21 @@ + + + + + + @@ -516,12 +398,11 @@ - + - @@ -555,18 +436,21 @@ - - - - - - - + + + + + + + + - + + + @@ -588,9 +472,11 @@ + + @@ -599,9 +485,12 @@ + + + - + @@ -614,8 +503,19 @@ + + + + + + + + + + + @@ -692,7 +592,7 @@ - + @@ -708,6 +608,9 @@ + + + @@ -723,9 +626,9 @@ - + - + @@ -733,41 +636,31 @@ - - Code - + Code - - - + + - - - - - - - @@ -816,21 +709,17 @@ - - + - - + - - - + @@ -851,11 +740,16 @@ + + + + + @@ -918,6 +812,7 @@ + @@ -929,14 +824,19 @@ - + + + - - + + + + + @@ -954,10 +854,17 @@ - + + + + + + + + @@ -1061,22 +968,17 @@ - - - - - - - - + + + - + @@ -1110,16 +1012,14 @@ - - - - + + @@ -1130,44 +1030,11 @@ + + + + - - - - - - - - - - - Code - - - - - - - - - - - - - - - - - - - - Code - - - - - - @@ -1183,23 +1050,21 @@ - + + - - - - + @@ -1228,7 +1093,7 @@ - + Always diff --git a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs index b8374f866..b834726e8 100644 --- a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs +++ b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs @@ -11,7 +11,6 @@ using NzbDrone.Common.Extensions; using NzbDrone.Core.MediaFiles; using NzbDrone.Core.MediaFiles.MediaInfo; using NzbDrone.Core.Qualities; -using NzbDrone.Core.Tv; using NzbDrone.Core.Music; namespace NzbDrone.Core.Organizer @@ -37,15 +36,9 @@ namespace NzbDrone.Core.Organizer private static readonly Regex TitleRegex = new Regex(@"\{(?[- ._\[(]*)(?(?:[a-z0-9]+)(?:(?[- ._]+)(?:[a-z0-9]+))?)(?::(?[a-z0-9]+))?(?[- ._)\]]*)\}", RegexOptions.Compiled | RegexOptions.IgnoreCase); - private static readonly Regex EpisodeRegex = new Regex(@"(?\{episode(?:\:0+)?})", - RegexOptions.Compiled | RegexOptions.IgnoreCase); - private static readonly Regex TrackRegex = new Regex(@"(?\{track(?:\:0+)?})", RegexOptions.Compiled | RegexOptions.IgnoreCase); - private static readonly Regex SeasonRegex = new Regex(@"(?\{season(?:\:0+)?})", - RegexOptions.Compiled | RegexOptions.IgnoreCase); - private static readonly Regex AbsoluteEpisodeRegex = new Regex(@"(?\{absolute(?:\:0+)?})", RegexOptions.Compiled | RegexOptions.IgnoreCase); @@ -60,10 +53,10 @@ namespace NzbDrone.Core.Organizer public static readonly Regex SeriesTitleRegex = new Regex(@"(?\{(?:Series)(?[- ._])(Clean)?Title\})", RegexOptions.Compiled | RegexOptions.IgnoreCase); - public static readonly Regex ArtistNameRegex = new Regex(@"(?\{(?:Artist)(?[- ._])(Clean)?Name\})", + public static readonly Regex ArtistNameRegex = new Regex(@"(?\{(?:Artist)(?[- ._])(Clean)?Name(The)?\})", RegexOptions.Compiled | RegexOptions.IgnoreCase); - public static readonly Regex AlbumTitleRegex = new Regex(@"(?\{(?:Album)(?[- ._])(Clean)?Title\})", + public static readonly Regex AlbumTitleRegex = new Regex(@"(?\{(?:Album)(?[- ._])(Clean)?Title(The)?\})", RegexOptions.Compiled | RegexOptions.IgnoreCase); private static readonly Regex FileNameCleanupRegex = new Regex(@"([- ._])(\1)+", RegexOptions.Compiled); @@ -77,6 +70,8 @@ namespace NzbDrone.Core.Organizer private static readonly char[] EpisodeTitleTrimCharacters = new[] { ' ', '.', '?' }; + private static readonly Regex TitlePrefixRegex = new Regex(@"^(The|An|A) (.*?)((?: *\([^)]+\))*)$", RegexOptions.Compiled | RegexOptions.IgnoreCase); + public FileNameBuilder(INamingConfigService namingConfigService, IQualityDefinitionService qualityDefinitionService, ICacheManager cacheManager, @@ -110,10 +105,10 @@ namespace NzbDrone.Core.Organizer var tokenHandlers = new Dictionary>(FileNameBuilderTokenEqualityComparer.Instance); tracks = tracks.OrderBy(e => e.AlbumId).ThenBy(e => e.TrackNumber).ToList(); - + pattern = FormatTrackNumberTokens(pattern, "", tracks); //pattern = AddAbsoluteNumberingTokens(pattern, tokenHandlers, series, episodes, namingConfig); - + AddArtistTokens(tokenHandlers, artist); AddAlbumTokens(tokenHandlers, album); AddTrackTokens(tokenHandlers, tracks); @@ -143,13 +138,13 @@ namespace NzbDrone.Core.Organizer if (artist.AlbumFolder) { - + var albumFolder = GetAlbumFolder(artist, album); albumFolder = CleanFileName(albumFolder); path = Path.Combine(path, albumFolder); - + } return path; @@ -165,9 +160,9 @@ namespace NzbDrone.Core.Organizer } var basicNamingConfig = new BasicNamingConfig - { - Separator = trackFormat.Separator - }; + { + Separator = trackFormat.Separator + }; var titleTokens = TitleRegex.Matches(nameSpec.StandardTrackFormat); @@ -238,6 +233,11 @@ namespace NzbDrone.Core.Organizer return title; } + public static string TitleThe(string title) + { + return TitlePrefixRegex.Replace(title, "$2, $1$3"); + } + public static string CleanFileName(string name, bool replace = true) { string result = name; @@ -262,12 +262,14 @@ namespace NzbDrone.Core.Organizer { tokenHandlers["{Artist Name}"] = m => artist.Name; tokenHandlers["{Artist CleanName}"] = m => CleanTitle(artist.Name); + tokenHandlers["{Artist NameThe}"] = m => TitleThe(artist.Name); } private void AddAlbumTokens(Dictionary> tokenHandlers, Album album) { tokenHandlers["{Album Title}"] = m => album.Title; tokenHandlers["{Album CleanTitle}"] = m => CleanTitle(album.Title); + tokenHandlers["{Album TitleThe}"] = m => TitleThe(album.Title); if (album.ReleaseDate.HasValue) { tokenHandlers["{Release Year}"] = m => album.ReleaseDate.Value.Year.ToString(); @@ -291,18 +293,6 @@ namespace NzbDrone.Core.Organizer tokenHandlers["{Release Group}"] = m => trackFile.ReleaseGroup ?? m.DefaultValue("Lidarr"); } - private void AddQualityTokens(Dictionary> tokenHandlers, Series series, EpisodeFile episodeFile) - { - var qualityTitle = _qualityDefinitionService.Get(episodeFile.Quality.Quality).Title; - var qualityProper = GetQualityProper(series, episodeFile.Quality); - var qualityReal = GetQualityReal(series, episodeFile.Quality); - - tokenHandlers["{Quality Full}"] = m => String.Format("{0} {1} {2}", qualityTitle, qualityProper, qualityReal); - tokenHandlers["{Quality Title}"] = m => qualityTitle; - tokenHandlers["{Quality Proper}"] = m => qualityProper; - tokenHandlers["{Quality Real}"] = m => qualityReal; - } - private void AddQualityTokens(Dictionary> tokenHandlers, Artist artist, TrackFile trackFile) { var qualityTitle = _qualityDefinitionService.Get(trackFile.Quality.Quality).Title; @@ -321,7 +311,7 @@ namespace NzbDrone.Core.Organizer { return; } - + var audioCodec = MediaInfoFormatter.FormatAudioCodec(trackFile.MediaInfo); var audioChannels = MediaInfoFormatter.FormatAudioChannels(trackFile.MediaInfo); @@ -468,7 +458,7 @@ namespace NzbDrone.Core.Organizer private AbsoluteTrackFormat[] GetAbsoluteFormat(string pattern) { - return _absoluteTrackFormatCache.Get(pattern, () => AbsoluteEpisodePatternRegex.Matches(pattern).OfType() + return _absoluteTrackFormatCache.Get(pattern, () => AbsoluteEpisodePatternRegex.Matches(pattern).OfType() .Select(match => new AbsoluteTrackFormat { Separator = match.Groups["separator"].Value.IsNotNullOrWhiteSpace() ? match.Groups["separator"].Value : "-", @@ -506,40 +496,31 @@ namespace NzbDrone.Core.Organizer return MultiPartCleanupRegex.Replace(title, string.Empty).Trim(); } - private string GetQualityProper(Series series, QualityModel quality) - { - if (quality.Revision.Version > 1) - { - if (series.SeriesType == SeriesTypes.Anime) - { - return "v" + quality.Revision.Version; - } - - return "Proper"; - } - - return String.Empty; - } + // TODO: DO WE NEED FOR MUSIC? + //private string GetQualityProper(Series series, QualityModel quality) + //{ + // if (quality.Revision.Version > 1) + // { + // if (series.SeriesType == SeriesTypes.Anime) + // { + // return "v" + quality.Revision.Version; + // } - private string GetQualityReal(Series series, QualityModel quality) - { - if (quality.Revision.Real > 0) - { - return "REAL"; - } + // return "Proper"; + // } - return string.Empty; - } + // return String.Empty; + //} - private string GetOriginalTitle(EpisodeFile episodeFile) - { - if (episodeFile.SceneName.IsNullOrWhiteSpace()) - { - return GetOriginalFileName(episodeFile); - } + //private string GetQualityReal(Series series, QualityModel quality) + //{ + // if (quality.Revision.Real > 0) + // { + // return "REAL"; + // } - return episodeFile.SceneName; - } + // return string.Empty; + //} private string GetOriginalTitle(TrackFile trackFile) { @@ -551,16 +532,6 @@ namespace NzbDrone.Core.Organizer return trackFile.SceneName; } - private string GetOriginalFileName(EpisodeFile episodeFile) - { - if (episodeFile.RelativePath.IsNullOrWhiteSpace()) - { - return Path.GetFileNameWithoutExtension(episodeFile.Path); - } - - return Path.GetFileNameWithoutExtension(episodeFile.RelativePath); - } - private string GetOriginalFileName(TrackFile trackFile) { if (trackFile.RelativePath.IsNullOrWhiteSpace()) diff --git a/src/NzbDrone.Core/Organizer/FileNameSampleService.cs b/src/NzbDrone.Core/Organizer/FileNameSampleService.cs index 1ef6af4c9..13dc05942 100644 --- a/src/NzbDrone.Core/Organizer/FileNameSampleService.cs +++ b/src/NzbDrone.Core/Organizer/FileNameSampleService.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using NzbDrone.Core.MediaFiles; using NzbDrone.Core.Qualities; using NzbDrone.Core.Music; @@ -30,12 +30,12 @@ namespace NzbDrone.Core.Organizer _standardArtist = new Artist { - Name = "Artist Name" + Name = "The Artist Name" }; _standardAlbum = new Album { - Title = "Album Title", + Title = "The Album Title", ReleaseDate = System.DateTime.Today }; diff --git a/src/NzbDrone.Core/Organizer/FileNameValidation.cs b/src/NzbDrone.Core/Organizer/FileNameValidation.cs index 5231f1fe6..33f397331 100644 --- a/src/NzbDrone.Core/Organizer/FileNameValidation.cs +++ b/src/NzbDrone.Core/Organizer/FileNameValidation.cs @@ -1,4 +1,4 @@ -using System.Text.RegularExpressions; +using System.Text.RegularExpressions; using FluentValidation; using FluentValidation.Validators; @@ -56,7 +56,7 @@ namespace NzbDrone.Core.Organizer public static IRuleBuilderOptions ValidAlbumFolderFormat(this IRuleBuilder ruleBuilder) { ruleBuilder.SetValidator(new NotEmptyValidator(null)); - return ruleBuilder.SetValidator(new RegularExpressionValidator(FileNameBuilder.AlbumTitleRegex)).WithMessage("Must contain Album name"); + return ruleBuilder.SetValidator(new RegularExpressionValidator(FileNameBuilder.AlbumTitleRegex)).WithMessage("Must contain Album title"); } } diff --git a/src/NzbDrone.Core/Organizer/FileNameValidationService.cs b/src/NzbDrone.Core/Organizer/FileNameValidationService.cs index a26b619c8..324940453 100644 --- a/src/NzbDrone.Core/Organizer/FileNameValidationService.cs +++ b/src/NzbDrone.Core/Organizer/FileNameValidationService.cs @@ -1,41 +1,19 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using FluentValidation.Results; using NzbDrone.Core.Parser.Model; -using NzbDrone.Core.Tv; namespace NzbDrone.Core.Organizer { public interface IFilenameValidationService { - ValidationFailure ValidateStandardFilename(SampleResult sampleResult); ValidationFailure ValidateTrackFilename(SampleResult sampleResult); - ValidationFailure ValidateDailyFilename(SampleResult sampleResult); - ValidationFailure ValidateAnimeFilename(SampleResult sampleResult); } public class FileNameValidationService : IFilenameValidationService { private const string ERROR_MESSAGE = "Produces invalid file names"; - public ValidationFailure ValidateStandardFilename(SampleResult sampleResult) - { - var validationFailure = new ValidationFailure("StandardEpisodeFormat", ERROR_MESSAGE); - var parsedEpisodeInfo = Parser.Parser.ParseTitle(sampleResult.FileName); - - if (parsedEpisodeInfo == null) - { - return validationFailure; - } - - if (!ValidateSeasonAndEpisodeNumbers(sampleResult.Episodes, parsedEpisodeInfo)) - { - return validationFailure; - } - - return null; - } - public ValidationFailure ValidateTrackFilename(SampleResult sampleResult) { var validationFailure = new ValidationFailure("StandardTrackFormat", ERROR_MESSAGE); @@ -57,71 +35,5 @@ namespace NzbDrone.Core.Organizer return null; } - public ValidationFailure ValidateDailyFilename(SampleResult sampleResult) - { - var validationFailure = new ValidationFailure("DailyEpisodeFormat", ERROR_MESSAGE); - var parsedEpisodeInfo = Parser.Parser.ParseTitle(sampleResult.FileName); - - if (parsedEpisodeInfo == null) - { - return validationFailure; - } - - if (parsedEpisodeInfo.IsDaily) - { - if (!parsedEpisodeInfo.AirDate.Equals(sampleResult.Episodes.Single().AirDate)) - { - return validationFailure; - } - - return null; - } - - if (!ValidateSeasonAndEpisodeNumbers(sampleResult.Episodes, parsedEpisodeInfo)) - { - return validationFailure; - } - - return null; - } - - public ValidationFailure ValidateAnimeFilename(SampleResult sampleResult) - { - var validationFailure = new ValidationFailure("AnimeEpisodeFormat", ERROR_MESSAGE); - var parsedEpisodeInfo = Parser.Parser.ParseTitle(sampleResult.FileName); - - if (parsedEpisodeInfo == null) - { - return validationFailure; - } - - if (parsedEpisodeInfo.AbsoluteEpisodeNumbers.Any()) - { - if (!parsedEpisodeInfo.AbsoluteEpisodeNumbers.First().Equals(sampleResult.Episodes.First().AbsoluteEpisodeNumber)) - { - return validationFailure; - } - - return null; - } - - if (!ValidateSeasonAndEpisodeNumbers(sampleResult.Episodes, parsedEpisodeInfo)) - { - return validationFailure; - } - - return null; - } - - private bool ValidateSeasonAndEpisodeNumbers(List episodes, ParsedEpisodeInfo parsedEpisodeInfo) - { - if (parsedEpisodeInfo.SeasonNumber != episodes.First().SeasonNumber || - !parsedEpisodeInfo.EpisodeNumbers.OrderBy(e => e).SequenceEqual(episodes.Select(e => e.EpisodeNumber).OrderBy(e => e))) - { - return false; - } - - return true; - } } } diff --git a/src/NzbDrone.Core/Organizer/SampleResult.cs b/src/NzbDrone.Core/Organizer/SampleResult.cs index 3075032ce..fb85df202 100644 --- a/src/NzbDrone.Core/Organizer/SampleResult.cs +++ b/src/NzbDrone.Core/Organizer/SampleResult.cs @@ -1,6 +1,5 @@ -using System.Collections.Generic; +using System.Collections.Generic; using NzbDrone.Core.MediaFiles; -using NzbDrone.Core.Tv; using NzbDrone.Core.Music; namespace NzbDrone.Core.Organizer @@ -8,11 +7,8 @@ namespace NzbDrone.Core.Organizer public class SampleResult { public string FileName { get; set; } - public Series Series { get; set; } public Artist Artist { get; set; } public Album Album { get; set; } - public List Episodes { get; set; } - public EpisodeFile EpisodeFile { get; set; } public List Tracks { get; set; } public TrackFile TrackFile { get; set; } } diff --git a/src/NzbDrone.Core/Parser/IsoLanguage.cs b/src/NzbDrone.Core/Parser/IsoLanguage.cs index 1bd198e50..c21ba27a7 100644 --- a/src/NzbDrone.Core/Parser/IsoLanguage.cs +++ b/src/NzbDrone.Core/Parser/IsoLanguage.cs @@ -1,4 +1,6 @@ -namespace NzbDrone.Core.Parser +using NzbDrone.Core.Languages; + +namespace NzbDrone.Core.Parser { public class IsoLanguage { diff --git a/src/NzbDrone.Core/Parser/IsoLanguages.cs b/src/NzbDrone.Core/Parser/IsoLanguages.cs index ddbbe74c2..d82fd3c80 100644 --- a/src/NzbDrone.Core/Parser/IsoLanguages.cs +++ b/src/NzbDrone.Core/Parser/IsoLanguages.cs @@ -1,5 +1,6 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; +using NzbDrone.Core.Languages; namespace NzbDrone.Core.Parser { diff --git a/src/NzbDrone.Core/Parser/Language.cs b/src/NzbDrone.Core/Parser/Language.cs deleted file mode 100644 index f85281dd1..000000000 --- a/src/NzbDrone.Core/Parser/Language.cs +++ /dev/null @@ -1,29 +0,0 @@ -namespace NzbDrone.Core.Parser -{ - public enum Language - { - Unknown = 0, - English = 1, - French = 2, - Spanish = 3, - German = 4, - Italian = 5, - Danish = 6, - Dutch = 7, - Japanese = 8, - Cantonese = 9, - Mandarin = 10, - Russian = 11, - Polish = 12, - Vietnamese = 13, - Swedish = 14, - Norwegian = 15, - Finnish = 16, - Turkish = 17, - Portuguese = 18, - Flemish = 19, - Greek = 20, - Korean = 21, - Hungarian = 22 - } -} diff --git a/src/NzbDrone.Core/Parser/LanguageParser.cs b/src/NzbDrone.Core/Parser/LanguageParser.cs index a2de40b84..2fd2f2935 100644 --- a/src/NzbDrone.Core/Parser/LanguageParser.cs +++ b/src/NzbDrone.Core/Parser/LanguageParser.cs @@ -1,9 +1,10 @@ -using System; +using System; using System.IO; using System.Linq; using System.Text.RegularExpressions; using NLog; using NzbDrone.Common.Instrumentation; +using NzbDrone.Core.Languages; namespace NzbDrone.Core.Parser { diff --git a/src/NzbDrone.Core/Parser/Model/LocalEpisode.cs b/src/NzbDrone.Core/Parser/Model/LocalEpisode.cs deleted file mode 100644 index 67ec2d873..000000000 --- a/src/NzbDrone.Core/Parser/Model/LocalEpisode.cs +++ /dev/null @@ -1,40 +0,0 @@ -using System.Linq; -using System.Collections.Generic; -using NzbDrone.Core.Qualities; -using NzbDrone.Core.Tv; -using NzbDrone.Core.MediaFiles.MediaInfo; - -namespace NzbDrone.Core.Parser.Model -{ - public class LocalEpisode - { - public LocalEpisode() - { - Episodes = new List(); - } - - public string Path { get; set; } - public long Size { get; set; } - public ParsedEpisodeInfo ParsedEpisodeInfo { get; set; } - public Series Series { get; set; } - public List Episodes { get; set; } - public QualityModel Quality { get; set; } - public MediaInfoModel MediaInfo { get; set; } - public bool ExistingFile { get; set; } - - public int SeasonNumber - { - get - { - return Episodes.Select(c => c.SeasonNumber).Distinct().Single(); - } - } - - public bool IsSpecial => SeasonNumber == 0; - - public override string ToString() - { - return Path; - } - } -} \ No newline at end of file diff --git a/src/NzbDrone.Core/Parser/Model/LocalTrack.cs b/src/NzbDrone.Core/Parser/Model/LocalTrack.cs index 359ce6285..eb45514c2 100644 --- a/src/NzbDrone.Core/Parser/Model/LocalTrack.cs +++ b/src/NzbDrone.Core/Parser/Model/LocalTrack.cs @@ -1,10 +1,11 @@ -using NzbDrone.Core.MediaFiles.MediaInfo; +using NzbDrone.Core.MediaFiles.MediaInfo; using NzbDrone.Core.Music; using NzbDrone.Core.Qualities; using System; using System.Collections.Generic; using System.Linq; using System.Text; +using NzbDrone.Core.Languages; namespace NzbDrone.Core.Parser.Model { @@ -22,6 +23,7 @@ namespace NzbDrone.Core.Parser.Model public Album Album { get; set; } public List Tracks { get; set; } public QualityModel Quality { get; set; } + public Language Language { get; set; } public MediaInfoModel MediaInfo { get; set; } public bool ExistingFile { get; set; } diff --git a/src/NzbDrone.Core/Parser/Model/ParsedAlbumInfo.cs b/src/NzbDrone.Core/Parser/Model/ParsedAlbumInfo.cs index 7e8ff08b4..c116bec37 100644 --- a/src/NzbDrone.Core/Parser/Model/ParsedAlbumInfo.cs +++ b/src/NzbDrone.Core/Parser/Model/ParsedAlbumInfo.cs @@ -1,9 +1,10 @@ -using NzbDrone.Common.Extensions; +using NzbDrone.Common.Extensions; using NzbDrone.Core.Qualities; using System; using System.Collections.Generic; using System.Linq; using System.Text; +using NzbDrone.Core.Languages; namespace NzbDrone.Core.Parser.Model { diff --git a/src/NzbDrone.Core/Parser/Model/ParsedEpisodeInfo.cs b/src/NzbDrone.Core/Parser/Model/ParsedEpisodeInfo.cs deleted file mode 100644 index 7adf609e8..000000000 --- a/src/NzbDrone.Core/Parser/Model/ParsedEpisodeInfo.cs +++ /dev/null @@ -1,93 +0,0 @@ -using System.Linq; -using NzbDrone.Common.Extensions; -using NzbDrone.Core.Qualities; - -namespace NzbDrone.Core.Parser.Model -{ - // TODO: This model needs to module music, not TV series - public class ParsedEpisodeInfo - { - public string SeriesTitle { get; set; } - public SeriesTitleInfo SeriesTitleInfo { get; set; } - public QualityModel Quality { get; set; } - public int SeasonNumber { get; set; } - public int[] EpisodeNumbers { get; set; } - public int[] AbsoluteEpisodeNumbers { get; set; } - public string AirDate { get; set; } - public Language Language { get; set; } - public bool FullSeason { get; set; } - public bool Special { get; set; } - public string ReleaseGroup { get; set; } - public string ReleaseHash { get; set; } - - public ParsedEpisodeInfo() - { - EpisodeNumbers = new int[0]; - AbsoluteEpisodeNumbers = new int[0]; - } - - public bool IsDaily - { - get - { - return !string.IsNullOrWhiteSpace(AirDate); - } - - //This prevents manually downloading a release from blowing up in mono - //TODO: Is there a better way? - private set { } - } - - public bool IsAbsoluteNumbering - { - get - { - return AbsoluteEpisodeNumbers.Any(); - } - - //This prevents manually downloading a release from blowing up in mono - //TODO: Is there a better way? - private set { } - } - - public bool IsPossibleSpecialEpisode - { - get - { - // if we don't have eny episode numbers we are likely a special episode and need to do a search by episode title - return (AirDate.IsNullOrWhiteSpace() && - SeriesTitle.IsNullOrWhiteSpace() && - (EpisodeNumbers.Length == 0 || SeasonNumber == 0) || - !SeriesTitle.IsNullOrWhiteSpace() && Special); - } - - //This prevents manually downloading a release from blowing up in mono - //TODO: Is there a better way? - private set {} - } - - public override string ToString() - { - string episodeString = "[Unknown Episode]"; - - if (IsDaily && EpisodeNumbers.Empty()) - { - episodeString = string.Format("{0}", AirDate); - } - else if (FullSeason) - { - episodeString = string.Format("Season {0:00}", SeasonNumber); - } - else if (EpisodeNumbers != null && EpisodeNumbers.Any()) - { - episodeString = string.Format("S{0:00}E{1}", SeasonNumber, string.Join("-", EpisodeNumbers.Select(c => c.ToString("00")))); - } - else if (AbsoluteEpisodeNumbers != null && AbsoluteEpisodeNumbers.Any()) - { - episodeString = string.Format("{0}", string.Join("-", AbsoluteEpisodeNumbers.Select(c => c.ToString("000")))); - } - - return string.Format("{0} - {1} {2}", SeriesTitle, episodeString, Quality); - } - } -} \ No newline at end of file diff --git a/src/NzbDrone.Core/Parser/Model/ParsedTrackInfo.cs b/src/NzbDrone.Core/Parser/Model/ParsedTrackInfo.cs index 6466e6634..cdf1593ce 100644 --- a/src/NzbDrone.Core/Parser/Model/ParsedTrackInfo.cs +++ b/src/NzbDrone.Core/Parser/Model/ParsedTrackInfo.cs @@ -1,9 +1,10 @@ -using NzbDrone.Common.Extensions; +using NzbDrone.Common.Extensions; using NzbDrone.Core.Qualities; using System; using System.Collections.Generic; using System.Linq; using System.Text; +using NzbDrone.Core.Languages; namespace NzbDrone.Core.Parser.Model { @@ -14,9 +15,12 @@ namespace NzbDrone.Core.Parser.Model public string ArtistTitle { get; set; } public string AlbumTitle { get; set; } public ArtistTitleInfo ArtistTitleInfo { get; set; } + public string ArtistMBId { get; set; } + public string AlbumMBId { get; set; } + public string TrackMBId { get; set; } public QualityModel Quality { get; set; } public int[] TrackNumbers { get; set; } - //public Language Language { get; set; } + public Language Language { get; set; } public string ReleaseGroup { get; set; } public string ReleaseHash { get; set; } @@ -25,21 +29,18 @@ namespace NzbDrone.Core.Parser.Model TrackNumbers = new int[0]; } - - - public override string ToString() { - string episodeString = "[Unknown Track]"; + string trackString = "[Unknown Track]"; if (TrackNumbers != null && TrackNumbers.Any()) { - episodeString = string.Format("T{0}", string.Join("-", TrackNumbers.Select(c => c.ToString("00")))); + trackString = string.Format("T{0}", string.Join("-", TrackNumbers.Select(c => c.ToString("00")))); } - return string.Format("{0} - {1} {2}", ArtistTitle, episodeString, Quality); + return string.Format("{0} - {1} {2}", ArtistTitle, trackString, Quality); } } } diff --git a/src/NzbDrone.Core/Parser/Model/RemoteEpisode.cs b/src/NzbDrone.Core/Parser/Model/RemoteEpisode.cs deleted file mode 100644 index 319606781..000000000 --- a/src/NzbDrone.Core/Parser/Model/RemoteEpisode.cs +++ /dev/null @@ -1,26 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using NzbDrone.Core.Tv; - -namespace NzbDrone.Core.Parser.Model -{ - public class RemoteEpisode - { - public ReleaseInfo Release { get; set; } - public ParsedEpisodeInfo ParsedEpisodeInfo { get; set; } - public Series Series { get; set; } - public List Episodes { get; set; } - public bool DownloadAllowed { get; set; } - - public bool IsRecentEpisode() - { - return Episodes.Any(e => e.AirDateUtc >= DateTime.UtcNow.Date.AddDays(-14)); - } - - public override string ToString() - { - return Release.Title; - } - } -} \ No newline at end of file diff --git a/src/NzbDrone.Core/Parser/Model/SeriesTitleInfo.cs b/src/NzbDrone.Core/Parser/Model/SeriesTitleInfo.cs deleted file mode 100644 index e9befbf39..000000000 --- a/src/NzbDrone.Core/Parser/Model/SeriesTitleInfo.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace NzbDrone.Core.Parser.Model -{ - public class SeriesTitleInfo - { - public string Title { get; set; } - public string TitleWithoutYear { get; set; } - public int Year { get; set; } - } -} diff --git a/src/NzbDrone.Core/Parser/Parser.cs b/src/NzbDrone.Core/Parser/Parser.cs index 25e274916..c5872eb28 100644 --- a/src/NzbDrone.Core/Parser/Parser.cs +++ b/src/NzbDrone.Core/Parser/Parser.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.IO; using System.Linq; @@ -8,7 +8,7 @@ using NzbDrone.Common.Extensions; using NzbDrone.Common.Instrumentation; using NzbDrone.Core.MediaFiles.MediaInfo; using NzbDrone.Core.Parser.Model; -using NzbDrone.Core.Tv; +using NzbDrone.Core.Languages; namespace NzbDrone.Core.Parser { @@ -60,6 +60,22 @@ namespace NzbDrone.Core.Parser //Artist Discography new Regex(@"^(?.+?)\W*(?Discograghy|Discografia).+(?\d{4}).+(?\d{4})", RegexOptions.IgnoreCase | RegexOptions.Compiled), + + //Artist - Album (Year) Strict + new Regex(@"^(?:(?.+?)(?:-)+)(?.+?)\W*(?:\(|\[).+?(?\d{4})", + RegexOptions.IgnoreCase | RegexOptions.Compiled), + + //Artist - Album (Year) + new Regex(@"^(?:(?.+?)(?:-)+)(?.+?)\W*(?:\(|\[)(?\d{4})", + RegexOptions.IgnoreCase | RegexOptions.Compiled), + + //Artist - Album + new Regex(@"^(?:(?.+?)(?:-)+)(?.+?)\W*(?:\(|\[)", + RegexOptions.IgnoreCase | RegexOptions.Compiled), + + //Artist - Album Year + new Regex(@"^(?:(?.+?)(?:-)+)(?.+?)\W*(\d{4}|\d{3})", + RegexOptions.IgnoreCase | RegexOptions.Compiled), }; private static readonly Regex[] ReportTitleRegex = new[] @@ -300,7 +316,7 @@ namespace NzbDrone.Core.Parser private static readonly Regex SixDigitAirDateRegex = new Regex(@"(?<=[_.-])(?(?[1-9]\d{1})(?[0-1][0-9])(?[0-3][0-9]))(?=[_.-])", RegexOptions.IgnoreCase | RegexOptions.Compiled); - private static readonly Regex CleanReleaseGroupRegex = new Regex(@"^(.*?[-._ ](S\d+E\d+)[-._ ])|(-(RP|1|NZBGeek|Obfuscated|sample))+$", + private static readonly Regex CleanReleaseGroupRegex = new Regex(@"^(.*?[-._ ](S\d+E\d+)[-._ ])|(-(RP|1|NZBGeek|Obfuscated|Scrambled|sample))+$", RegexOptions.IgnoreCase | RegexOptions.Compiled); private static readonly Regex CleanTorrentSuffixRegex = new Regex(@"\[(?:ettv|rartv|rarbg|cttv)\]$", @@ -312,6 +328,9 @@ namespace NzbDrone.Core.Parser private static readonly Regex AnimeReleaseGroupRegex = new Regex(@"^(?:\[(?(?!\s).+?(?\b(?:ita|italian)\b)|(?german\b|videomann)|(?flemish)|(?greek)|(?(?:\W|_)(?:FR|VOSTFR)(?:\W|_))|(?\brus\b)|(?nl\W?subs?)|(?\b(?:HUNDUB|HUN)\b)|(?\b(?:español|castellano)\b)", + RegexOptions.IgnoreCase | RegexOptions.Compiled); + private static readonly Regex YearInTitleRegex = new Regex(@"^(?.+?)(?:\W|_)?(?<year>\d{4})", RegexOptions.IgnoreCase | RegexOptions.Compiled); @@ -329,30 +348,37 @@ namespace NzbDrone.Core.Parser { var fileInfo = new FileInfo(path); var file = TagLib.File.Create(path); - var trackName = file.Tag.Title; var trackNumber = file.Tag.Track; + var trackTitle = file.Tag.Title; + + var artist = file.Tag.FirstAlbumArtist; + + if (artist.IsNullOrWhiteSpace()) + { + artist = file.Tag.FirstPerformer; + } var artistTitleInfo = new ArtistTitleInfo { - Title = file.Tag.Title, - Year = (int) file.Tag.Year + Title = artist, + Year = (int)file.Tag.Year }; var temp = new int[1]; - temp[0] = (int) trackNumber; + temp[0] = (int)trackNumber; var result = new ParsedTrackInfo { + Language = Language.English, //TODO Parse from Tag/Mediainfo AlbumTitle = file.Tag.Album, - ArtistTitle = file.Tag.FirstAlbumArtist, - Quality = QualityParser.ParseQuality(trackName), + ArtistTitle = artist, + ArtistMBId = file.Tag.MusicBrainzArtistId, + AlbumMBId = file.Tag.MusicBrainzReleaseId, + TrackMBId = file.Tag.MusicBrainzReleaseType, TrackNumbers = temp, ArtistTitleInfo = artistTitleInfo, - Title = file.Tag.Title + Title = trackTitle }; - - Logger.Debug("Quality parsed: {0}", file.Tag.BeatsPerMinute); - foreach (TagLib.ICodec codec in file.Properties.Codecs) { TagLib.IAudioCodec acodec = codec as TagLib.IAudioCodec; @@ -366,6 +392,7 @@ namespace NzbDrone.Core.Parser Logger.Debug("Channels: " + acodec.AudioChannels + "\n"); result.Quality = QualityParser.ParseQuality(acodec.Description, acodec.AudioBitrate, acodec.AudioSampleRate); + Logger.Debug("Quality parsed: {0}", result.Quality); } @@ -390,27 +417,6 @@ namespace NzbDrone.Core.Parser return result; } - public static ParsedEpisodeInfo ParsePath(string path) - { - var fileInfo = new FileInfo(path); - - var result = ParseTitle(fileInfo.Name); - - if (result == null) - { - Logger.Debug("Attempting to parse episode info using directory and file names. {0}", fileInfo.Directory.Name); - result = ParseTitle(fileInfo.Directory.Name + " " + fileInfo.Name); - } - - if (result == null) - { - Logger.Debug("Attempting to parse episode info using directory name. {0}", fileInfo.Directory.Name); - result = ParseTitle(fileInfo.Directory.Name + fileInfo.Extension); - } - - return result; - } - public static ParsedTrackInfo ParseMusicTitle(string title) { try @@ -429,9 +435,9 @@ namespace NzbDrone.Core.Parser Logger.Debug("Reversed name detected. Converted to '{0}'", title); } - var simpleTitle = SimpleTitleRegex.Replace(title, string.Empty); + var releaseTitle = RemoveFileExtension(title); - simpleTitle = RemoveFileExtension(simpleTitle); + var simpleTitle = SimpleTitleRegex.Replace(releaseTitle, string.Empty); // TODO: Quick fix stripping [url] - prefixes. simpleTitle = WebsitePrefixRegex.Replace(simpleTitle, string.Empty); @@ -540,9 +546,9 @@ namespace NzbDrone.Core.Parser Logger.Debug("Reversed name detected. Converted to '{0}'", title); } - var simpleTitle = SimpleTitleRegex.Replace(title, string.Empty); + var releaseTitle = RemoveFileExtension(title); - simpleTitle = RemoveFileExtension(simpleTitle); + var simpleTitle = SimpleTitleRegex.Replace(releaseTitle, string.Empty); // TODO: Quick fix stripping [url] - prefixes. simpleTitle = WebsitePrefixRegex.Replace(simpleTitle, string.Empty); @@ -583,13 +589,13 @@ namespace NzbDrone.Core.Parser if (result != null) { - result.Language = LanguageParser.ParseLanguage(title); + result.Language = LanguageParser.ParseLanguage(releaseTitle); Logger.Debug("Language parsed: {0}", result.Language); result.Quality = QualityParser.ParseQuality(title); Logger.Debug("Quality parsed: {0}", result.Quality); - result.ReleaseGroup = ParseReleaseGroup(title); + result.ReleaseGroup = ParseReleaseGroup(releaseTitle); var subGroup = GetSubGroup(match); if (!subGroup.IsNullOrWhiteSpace()) @@ -626,130 +632,6 @@ namespace NzbDrone.Core.Parser return null; } - public static ParsedEpisodeInfo ParseTitle(string title) - { - try - { - if (!ValidateBeforeParsing(title)) return null; - - Logger.Debug("Parsing string '{0}'", title); - - if (ReversedTitleRegex.IsMatch(title)) - { - var titleWithoutExtension = RemoveFileExtension(title).ToCharArray(); - Array.Reverse(titleWithoutExtension); - - title = new string(titleWithoutExtension) + title.Substring(titleWithoutExtension.Length); - - Logger.Debug("Reversed name detected. Converted to '{0}'", title); - } - - var simpleTitle = SimpleTitleRegex.Replace(title, string.Empty); - - simpleTitle = RemoveFileExtension(simpleTitle); - - // TODO: Quick fix stripping [url] - prefixes. - simpleTitle = WebsitePrefixRegex.Replace(simpleTitle, string.Empty); - - simpleTitle = CleanTorrentSuffixRegex.Replace(simpleTitle, string.Empty); - - var airDateMatch = AirDateRegex.Match(simpleTitle); - if (airDateMatch.Success) - { - simpleTitle = airDateMatch.Groups[1].Value + airDateMatch.Groups["airyear"].Value + "." + airDateMatch.Groups["airmonth"].Value + "." + airDateMatch.Groups["airday"].Value; - } - - var sixDigitAirDateMatch = SixDigitAirDateRegex.Match(simpleTitle); - if (sixDigitAirDateMatch.Success) - { - var airYear = sixDigitAirDateMatch.Groups["airyear"].Value; - var airMonth = sixDigitAirDateMatch.Groups["airmonth"].Value; - var airDay = sixDigitAirDateMatch.Groups["airday"].Value; - - if (airMonth != "00" || airDay != "00") - { - var fixedDate = string.Format("20{0}.{1}.{2}", airYear, airMonth, airDay); - - simpleTitle = simpleTitle.Replace(sixDigitAirDateMatch.Groups["airdate"].Value, fixedDate); - } - } - - foreach (var regex in ReportTitleRegex) - { - var match = regex.Matches(simpleTitle); - - if (match.Count != 0) - { - Logger.Trace(regex); - try - { - var result = ParseMatchCollection(match); - - if (result != null) - { - if (result.FullSeason && title.ContainsIgnoreCase("Special")) - { - result.FullSeason = false; - result.Special = true; - } - - result.Language = LanguageParser.ParseLanguage(title); - Logger.Debug("Language parsed: {0}", result.Language); - - result.Quality = QualityParser.ParseQuality(title); - Logger.Debug("Quality parsed: {0}", result.Quality); - - result.ReleaseGroup = ParseReleaseGroup(title); - - var subGroup = GetSubGroup(match); - if (!subGroup.IsNullOrWhiteSpace()) - { - result.ReleaseGroup = subGroup; - } - - Logger.Debug("Release Group parsed: {0}", result.ReleaseGroup); - - result.ReleaseHash = GetReleaseHash(match); - if (!result.ReleaseHash.IsNullOrWhiteSpace()) - { - Logger.Debug("Release Hash parsed: {0}", result.ReleaseHash); - } - - return result; - } - } - catch (InvalidDateException ex) - { - Logger.Debug(ex, ex.Message); - break; - } - } - } - } - catch (Exception e) - { - if (!title.ToLower().Contains("password") && !title.ToLower().Contains("yenc")) - Logger.Error(e, "An error has occurred while trying to parse {0}", title); - } - - Logger.Debug("Unable to parse {0}", title); - return null; - } - - public static string ParseSeriesName(string title) - { - Logger.Debug("Parsing string '{0}'", title); - - var parseResult = ParseTitle(title); - - if (parseResult == null) - { - return CleanSeriesTitle(title); - } - - return parseResult.SeriesTitle; - } - public static string CleanSeriesTitle(this string title) { long number = 0; @@ -761,15 +643,15 @@ namespace NzbDrone.Core.Parser return NormalizeRegex.Replace(title, string.Empty).ToLower().RemoveAccent(); } - public static string CleanArtistTitle(this string title) + public static string CleanArtistName(this string name) { long number = 0; //If Title only contains numbers return it as is. - if (long.TryParse(title, out number)) - return title; + if (long.TryParse(name, out number)) + return name; - return NormalizeRegex.Replace(title, string.Empty).ToLower().RemoveAccent(); + return NormalizeRegex.Replace(name, string.Empty).ToLower().RemoveAccent(); } public static string NormalizeEpisodeTitle(string title) @@ -840,25 +722,97 @@ namespace NzbDrone.Core.Parser return title; } - private static SeriesTitleInfo GetSeriesTitleInfo(string title) + public static Language ParseLanguage(string title) { - var seriesTitleInfo = new SeriesTitleInfo(); - seriesTitleInfo.Title = title; + var lowerTitle = title.ToLower(); - var match = YearInTitleRegex.Match(title); + if (lowerTitle.Contains("english")) + return Language.English; - if (!match.Success) - { - seriesTitleInfo.TitleWithoutYear = title; - } + if (lowerTitle.Contains("french")) + return Language.French; - else - { - seriesTitleInfo.TitleWithoutYear = match.Groups["title"].Value; - seriesTitleInfo.Year = Convert.ToInt32(match.Groups["year"].Value); - } + if (lowerTitle.Contains("spanish")) + return Language.Spanish; + + if (lowerTitle.Contains("danish")) + return Language.Danish; + + if (lowerTitle.Contains("dutch")) + return Language.Dutch; + + if (lowerTitle.Contains("japanese")) + return Language.Japanese; + + if (lowerTitle.Contains("cantonese")) + return Language.Cantonese; + + if (lowerTitle.Contains("mandarin")) + return Language.Mandarin; + + if (lowerTitle.Contains("korean")) + return Language.Korean; + + if (lowerTitle.Contains("russian")) + return Language.Russian; + + if (lowerTitle.Contains("polish")) + return Language.Polish; - return seriesTitleInfo; + if (lowerTitle.Contains("vietnamese")) + return Language.Vietnamese; + + if (lowerTitle.Contains("swedish")) + return Language.Swedish; + + if (lowerTitle.Contains("norwegian")) + return Language.Norwegian; + + if (lowerTitle.Contains("nordic")) + return Language.Norwegian; + + if (lowerTitle.Contains("finnish")) + return Language.Finnish; + + if (lowerTitle.Contains("turkish")) + return Language.Turkish; + + if (lowerTitle.Contains("portuguese")) + return Language.Portuguese; + + if (lowerTitle.Contains("hungarian")) + return Language.Hungarian; + + var match = LanguageRegex.Match(title); + + if (match.Groups["italian"].Captures.Cast<Capture>().Any()) + return Language.Italian; + + if (match.Groups["german"].Captures.Cast<Capture>().Any()) + return Language.German; + + if (match.Groups["flemish"].Captures.Cast<Capture>().Any()) + return Language.Flemish; + + if (match.Groups["greek"].Captures.Cast<Capture>().Any()) + return Language.Greek; + + if (match.Groups["spanish"].Captures.Cast<Capture>().Any()) + return Language.Spanish; + + if (match.Groups["french"].Success) + return Language.French; + + if (match.Groups["russian"].Success) + return Language.Russian; + + if (match.Groups["dutch"].Success) + return Language.Dutch; + + if (match.Groups["hungarian"].Success) + return Language.Hungarian; + + return Language.English; } private static ParsedTrackInfo ParseMatchMusicCollection(MatchCollection matchCollection) @@ -868,7 +822,7 @@ namespace NzbDrone.Core.Parser // Coppied from Radarr (https://github.com/Radarr/Radarr/blob/develop/src/NzbDrone.Core/Parser/Parser.cs) // TODO: Split into separate method and write unit tests for. - var parts = artistName.Split('.'); + var parts = artistName.Split('.'); artistName = ""; int n = 0; bool previousAcronym = false; @@ -936,6 +890,20 @@ namespace NzbDrone.Core.Parser return artistTitleInfo; } + public static string ParseArtistName(string title) + { + Logger.Debug("Parsing string '{0}'", title); + + var parseResult = ParseAlbumTitle(title); + + if (parseResult == null) + { + return CleanSeriesTitle(title); + } + + return parseResult.ArtistName; + } + private static ParsedAlbumInfo ParseAlbumMatchCollection(MatchCollection matchCollection) { var artistName = matchCollection[0].Groups["artist"].Value.Replace('.', ' ').Replace('_', ' '); @@ -959,141 +927,6 @@ namespace NzbDrone.Core.Parser return result; } - - private static ParsedEpisodeInfo ParseMatchCollection(MatchCollection matchCollection) - { - var seriesName = matchCollection[0].Groups["title"].Value.Replace('.', ' ').Replace('_', ' '); - seriesName = RequestInfoRegex.Replace(seriesName, "").Trim(' '); - - int airYear; - int.TryParse(matchCollection[0].Groups["airyear"].Value, out airYear); - - ParsedEpisodeInfo result; - - if (airYear < 1900) - { - var seasons = new List<int>(); - - foreach (Capture seasonCapture in matchCollection[0].Groups["season"].Captures) - { - int parsedSeason; - if (int.TryParse(seasonCapture.Value, out parsedSeason)) - seasons.Add(parsedSeason); - } - - //If no season was found it should be treated as a mini series and season 1 - if (seasons.Count == 0) seasons.Add(1); - - //If more than 1 season was parsed go to the next REGEX (A multi-season release is unlikely) - if (seasons.Distinct().Count() > 1) return null; - - result = new ParsedEpisodeInfo - { - SeasonNumber = seasons.First(), - EpisodeNumbers = new int[0], - AbsoluteEpisodeNumbers = new int[0] - }; - - foreach (Match matchGroup in matchCollection) - { - var episodeCaptures = matchGroup.Groups["episode"].Captures.Cast<Capture>().ToList(); - var absoluteEpisodeCaptures = matchGroup.Groups["absoluteepisode"].Captures.Cast<Capture>().ToList(); - - //Allows use to return a list of 0 episodes (We can handle that as a full season release) - if (episodeCaptures.Any()) - { - var first = ParseNumber(episodeCaptures.First().Value); - var last = ParseNumber(episodeCaptures.Last().Value); - - if (first > last) - { - return null; - } - - var count = last - first + 1; - result.EpisodeNumbers = Enumerable.Range(first, count).ToArray(); - } - - if (absoluteEpisodeCaptures.Any()) - { - var first = Convert.ToInt32(absoluteEpisodeCaptures.First().Value); - var last = Convert.ToInt32(absoluteEpisodeCaptures.Last().Value); - - if (first > last) - { - return null; - } - - var count = last - first + 1; - result.AbsoluteEpisodeNumbers = Enumerable.Range(first, count).ToArray(); - - if (matchGroup.Groups["special"].Success) - { - result.Special = true; - } - } - - if (!episodeCaptures.Any() && !absoluteEpisodeCaptures.Any()) - { - //Check to see if this is an "Extras" or "SUBPACK" release, if it is, return NULL - //Todo: Set a "Extras" flag in EpisodeParseResult if we want to download them ever - if (!matchCollection[0].Groups["extras"].Value.IsNullOrWhiteSpace()) return null; - - result.FullSeason = true; - } - } - - if (result.AbsoluteEpisodeNumbers.Any() && !result.EpisodeNumbers.Any()) - { - result.SeasonNumber = 0; - } - } - - else - { - //Try to Parse as a daily show - var airmonth = Convert.ToInt32(matchCollection[0].Groups["airmonth"].Value); - var airday = Convert.ToInt32(matchCollection[0].Groups["airday"].Value); - - //Swap day and month if month is bigger than 12 (scene fail) - if (airmonth > 12) - { - var tempDay = airday; - airday = airmonth; - airmonth = tempDay; - } - - DateTime airDate; - - try - { - airDate = new DateTime(airYear, airmonth, airday); - } - catch (Exception) - { - throw new InvalidDateException("Invalid date found: {0}-{1}-{2}", airYear, airmonth, airday); - } - - //Check if episode is in the future (most likely a parse error) - if (airDate > DateTime.Now.AddDays(1).Date || airDate < new DateTime(1970, 1, 1)) - { - throw new InvalidDateException("Invalid date found: {0}", airDate); - } - - result = new ParsedEpisodeInfo - { - AirDate = airDate.ToString(Episode.AIR_DATE_FORMAT), - }; - } - - result.SeriesTitle = seriesName; - result.SeriesTitleInfo = GetSeriesTitleInfo(result.SeriesTitle); - - Logger.Debug("Episode Parsed. {0}", result); - - return result; - } - private static bool ValidateBeforeParsing(string title) { if (title.ToLower().Contains("password") && title.ToLower().Contains("yenc")) diff --git a/src/NzbDrone.Core/Parser/ParsingService.cs b/src/NzbDrone.Core/Parser/ParsingService.cs index 1e048aeff..3f270deea 100644 --- a/src/NzbDrone.Core/Parser/ParsingService.cs +++ b/src/NzbDrone.Core/Parser/ParsingService.cs @@ -1,13 +1,11 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.IO; using System.Linq; using NLog; using NzbDrone.Common.Extensions; -using NzbDrone.Core.DataAugmentation.Scene; using NzbDrone.Core.IndexerSearch.Definitions; using NzbDrone.Core.MediaFiles; using NzbDrone.Core.Parser.Model; -using NzbDrone.Core.Tv; using NzbDrone.Core.Music; using System; @@ -15,16 +13,11 @@ namespace NzbDrone.Core.Parser { public interface IParsingService { - LocalEpisode GetLocalEpisode(string filename, Series series); - LocalEpisode GetLocalEpisode(string filename, Series series, ParsedEpisodeInfo folderInfo, bool sceneSource); - Series GetSeries(string title); - RemoteEpisode Map(ParsedEpisodeInfo parsedEpisodeInfo, int tvdbId, int tvRageId, SearchCriteriaBase searchCriteria = null); - RemoteEpisode Map(ParsedEpisodeInfo parsedEpisodeInfo, int seriesId, IEnumerable<int> episodeIds); + Artist GetArtist(string title); + Artist GetArtistFromTag(string file); RemoteAlbum Map(ParsedAlbumInfo parsedAlbumInfo, SearchCriteriaBase searchCriteria = null); RemoteAlbum Map(ParsedAlbumInfo parsedAlbumInfo, int artistId, IEnumerable<int> albumIds); - List<Episode> GetEpisodes(ParsedEpisodeInfo parsedEpisodeInfo, Series series, bool sceneSource, SearchCriteriaBase searchCriteria = null); List<Album> GetAlbums(ParsedAlbumInfo parsedAlbumInfo, Artist artist, SearchCriteriaBase searchCriteria = null); - ParsedEpisodeInfo ParseSpecialEpisodeTitle(string title, int tvdbId, int tvRageId, SearchCriteriaBase searchCriteria = null); // Music stuff here LocalTrack GetLocalTrack(string filename, Artist artist); @@ -34,27 +27,17 @@ namespace NzbDrone.Core.Parser public class ParsingService : IParsingService { - private readonly IEpisodeService _episodeService; - private readonly ISeriesService _seriesService; - - private readonly IAlbumRepository _albumRepository; private readonly IArtistService _artistService; private readonly IAlbumService _albumService; private readonly ITrackService _trackService; private readonly Logger _logger; - public ParsingService(IEpisodeService episodeService, - ISeriesService seriesService, - ITrackService trackService, + public ParsingService(ITrackService trackService, IArtistService artistService, - IAlbumRepository albumRepository, IAlbumService albumService, // ISceneMappingService sceneMappingService, Logger logger) { - _episodeService = episodeService; - _seriesService = seriesService; - _albumRepository = albumRepository; _albumService = albumService; _artistService = artistService; // _sceneMappingService = sceneMappingService; @@ -62,99 +45,42 @@ namespace NzbDrone.Core.Parser _logger = logger; } - public LocalEpisode GetLocalEpisode(string filename, Series series) - { - return GetLocalEpisode(filename, series, null, false); - } - - public LocalEpisode GetLocalEpisode(string filename, Series series, ParsedEpisodeInfo folderInfo, bool sceneSource) + public Artist GetArtist(string title) { - ParsedEpisodeInfo parsedEpisodeInfo; - - if (folderInfo != null) + var parsedAlbumInfo = Parser.ParseAlbumTitle(title); + + if (parsedAlbumInfo == null || parsedAlbumInfo.ArtistName.IsNullOrWhiteSpace()) { - parsedEpisodeInfo = folderInfo.JsonClone(); - parsedEpisodeInfo.Quality = QualityParser.ParseQuality(Path.GetFileName(filename)); + return _artistService.FindByName(title); } - else - { - parsedEpisodeInfo = Parser.ParsePath(filename); - } - - if (parsedEpisodeInfo == null || parsedEpisodeInfo.IsPossibleSpecialEpisode) - { - var title = Path.GetFileNameWithoutExtension(filename); - var specialEpisodeInfo = ParseSpecialEpisodeTitle(title, series); - - if (specialEpisodeInfo != null) - { - parsedEpisodeInfo = specialEpisodeInfo; - } - } - - if (parsedEpisodeInfo == null) - { - if (MediaFileExtensions.Extensions.Contains(Path.GetExtension(filename))) - { - _logger.Warn("Unable to parse episode info from path {0}", filename); - } - - return null; - } - - var episodes = GetEpisodes(parsedEpisodeInfo, series, sceneSource); - - return new LocalEpisode - { - Series = series, - Quality = parsedEpisodeInfo.Quality, - Episodes = episodes, - Path = filename, - ParsedEpisodeInfo = parsedEpisodeInfo, - ExistingFile = series.Path.IsParentPath(filename) - }; + return _artistService.FindByName(parsedAlbumInfo.ArtistName); + } - public Series GetSeries(string title) + public Artist GetArtistFromTag(string file) { - var parsedEpisodeInfo = Parser.ParseTitle(title); + var parsedTrackInfo = Parser.ParseMusicPath(file); - if (parsedEpisodeInfo == null) - { - return _seriesService.FindByTitle(title); - } - - var series = _seriesService.FindByTitle(parsedEpisodeInfo.SeriesTitle); + var artist = new Artist(); - if (series == null) + if (parsedTrackInfo.ArtistMBId.IsNotNullOrWhiteSpace()) { - series = _seriesService.FindByTitle(parsedEpisodeInfo.SeriesTitleInfo.TitleWithoutYear, - parsedEpisodeInfo.SeriesTitleInfo.Year); - } + artist = _artistService.FindById(parsedTrackInfo.ArtistMBId); - return series; - } - - [System.Obsolete("Used for sonarr, not lidarr")] - public RemoteEpisode Map(ParsedEpisodeInfo parsedEpisodeInfo, int tvdbId, int tvRageId, SearchCriteriaBase searchCriteria = null) - { - var remoteEpisode = new RemoteEpisode + if (artist != null) { - ParsedEpisodeInfo = parsedEpisodeInfo, - }; - - var series = GetSeries(parsedEpisodeInfo, tvdbId, tvRageId, searchCriteria); + return artist; + } + } - if (series == null) + if (parsedTrackInfo == null || parsedTrackInfo.ArtistTitle.IsNullOrWhiteSpace()) { - return remoteEpisode; + return null; } - remoteEpisode.Series = series; - remoteEpisode.Episodes = GetEpisodes(parsedEpisodeInfo, series, true, searchCriteria); + return _artistService.FindByName(parsedTrackInfo.ArtistTitle); - return remoteEpisode; } public RemoteAlbum Map(ParsedAlbumInfo parsedAlbumInfo, SearchCriteriaBase searchCriteria = null) @@ -214,17 +140,6 @@ namespace NzbDrone.Core.Parser } - [System.Obsolete("Used for sonarr, not lidarr")] - public RemoteEpisode Map(ParsedEpisodeInfo parsedEpisodeInfo, int seriesId, IEnumerable<int> episodeIds) - { - return new RemoteEpisode - { - ParsedEpisodeInfo = parsedEpisodeInfo, - Series = _seriesService.GetSeries(seriesId), - Episodes = _episodeService.GetEpisodes(episodeIds) - }; - } - public RemoteAlbum Map(ParsedAlbumInfo parsedAlbumInfo, int artistId, IEnumerable<int> albumIds) { return new RemoteAlbum @@ -235,110 +150,6 @@ namespace NzbDrone.Core.Parser }; } - public List<Episode> GetEpisodes(ParsedEpisodeInfo parsedEpisodeInfo, Series series, bool sceneSource, SearchCriteriaBase searchCriteria = null) - { - if (parsedEpisodeInfo.FullSeason) - { - return _episodeService.GetEpisodesBySeason(series.Id, parsedEpisodeInfo.SeasonNumber); - } - - if (parsedEpisodeInfo.IsDaily) - { - if (series.SeriesType == SeriesTypes.Standard) - { - _logger.Warn("Found daily-style episode for non-daily series: {0}.", series); - return new List<Episode>(); - } - - var episodeInfo = GetDailyEpisode(series, parsedEpisodeInfo.AirDate, searchCriteria); - - if (episodeInfo != null) - { - return new List<Episode> { episodeInfo }; - } - - return new List<Episode>(); - } - - if (parsedEpisodeInfo.IsAbsoluteNumbering) - { - return GetAnimeEpisodes(series, parsedEpisodeInfo, sceneSource); - } - - return GetStandardEpisodes(series, parsedEpisodeInfo, sceneSource, searchCriteria); - } - - public ParsedEpisodeInfo ParseSpecialEpisodeTitle(string title, int tvdbId, int tvRageId, SearchCriteriaBase searchCriteria = null) - { - if (searchCriteria != null) - { - if (tvdbId == 0) - tvdbId = 0; // _sceneMappingService.FindTvdbId(title) ?? 0; - - if (tvdbId != 0 && tvdbId == searchCriteria.Series.TvdbId) - { - return ParseSpecialEpisodeTitle(title, searchCriteria.Series); - } - - if (tvRageId != 0 && tvRageId == searchCriteria.Series.TvRageId) - { - return ParseSpecialEpisodeTitle(title, searchCriteria.Series); - } - } - - var series = GetSeries(title); - - if (series == null) - { - series = _seriesService.FindByTitleInexact(title); - } - - if (series == null && tvdbId > 0) - { - series = _seriesService.FindByTvdbId(tvdbId); - } - - if (series == null && tvRageId > 0) - { - series = _seriesService.FindByTvRageId(tvRageId); - } - - if (series == null) - { - _logger.Debug("No matching series {0}", title); - return null; - } - - return ParseSpecialEpisodeTitle(title, series); - } - - private ParsedEpisodeInfo ParseSpecialEpisodeTitle(string title, Series series) - { - // find special episode in series season 0 - var episode = _episodeService.FindEpisodeByTitle(series.Id, 0, title); - - if (episode != null) - { - // create parsed info from tv episode - var info = new ParsedEpisodeInfo(); - info.SeriesTitle = series.Title; - info.SeriesTitleInfo = new SeriesTitleInfo(); - info.SeriesTitleInfo.Title = info.SeriesTitle; - info.SeasonNumber = episode.SeasonNumber; - info.EpisodeNumbers = new int[1] { episode.EpisodeNumber }; - info.FullSeason = false; - info.Quality = QualityParser.ParseQuality(title); - info.ReleaseGroup = Parser.ParseReleaseGroup(title); - info.Language = LanguageParser.ParseLanguage(title); - info.Special = true; - - _logger.Debug("Found special episode {0} for title '{1}'", info, title); - return info; - } - - return null; - } - private Artist GetArtist(ParsedAlbumInfo parsedAlbumInfo, SearchCriteriaBase searchCriteria) { Artist artist = null; @@ -355,283 +166,13 @@ namespace NzbDrone.Core.Parser if (artist == null) { - _logger.Debug("No matching series {0}", parsedAlbumInfo.ArtistName); + _logger.Debug("No matching artist {0}", parsedAlbumInfo.ArtistName); return null; } return artist; } - - private Series GetSeries(ParsedEpisodeInfo parsedEpisodeInfo, int tvdbId, int tvRageId, SearchCriteriaBase searchCriteria) - { - Series series = null; - - //var sceneMappingTvdbId = _sceneMappingService.FindTvdbId(parsedEpisodeInfo.SeriesTitle); - //if (sceneMappingTvdbId.HasValue) - //{ - // if (searchCriteria != null && searchCriteria.Series.TvdbId == sceneMappingTvdbId.Value) - // { - // return searchCriteria.Series; - // } - - // series = _seriesService.FindByTvdbId(sceneMappingTvdbId.Value); - - // if (series == null) - // { - // _logger.Debug("No matching series {0}", parsedEpisodeInfo.SeriesTitle); - // return null; - // } - - // return series; - //} - - if (searchCriteria != null) - { - if (searchCriteria.Series.CleanTitle == parsedEpisodeInfo.SeriesTitle.CleanSeriesTitle()) - { - return searchCriteria.Series; - } - - if (tvdbId > 0 && tvdbId == searchCriteria.Series.TvdbId) - { - //TODO: If series is found by TvdbId, we should report it as a scene naming exception, since it will fail to import - return searchCriteria.Series; - } - - if (tvRageId > 0 && tvRageId == searchCriteria.Series.TvRageId) - { - //TODO: If series is found by TvRageId, we should report it as a scene naming exception, since it will fail to import - return searchCriteria.Series; - } - } - - series = _seriesService.FindByTitle(parsedEpisodeInfo.SeriesTitle); - - if (series == null && parsedEpisodeInfo.SeriesTitleInfo.Year > 0) - { - series = _seriesService.FindByTitle(parsedEpisodeInfo.SeriesTitleInfo.TitleWithoutYear, parsedEpisodeInfo.SeriesTitleInfo.Year); - } - - if (series == null && tvdbId > 0) - { - //TODO: If series is found by TvdbId, we should report it as a scene naming exception, since it will fail to import - series = _seriesService.FindByTvdbId(tvdbId); - } - - if (series == null && tvRageId > 0) - { - //TODO: If series is found by TvRageId, we should report it as a scene naming exception, since it will fail to import - series = _seriesService.FindByTvRageId(tvRageId); - } - - if (series == null) - { - _logger.Debug("No matching series {0}", parsedEpisodeInfo.SeriesTitle); - return null; - } - - return series; - } - - private Episode GetDailyEpisode(Series series, string airDate, SearchCriteriaBase searchCriteria) - { - Episode episodeInfo = null; - - if (searchCriteria != null) - { - episodeInfo = searchCriteria.Episodes.SingleOrDefault( - e => e.AirDate == airDate); - } - - if (episodeInfo == null) - { - episodeInfo = _episodeService.FindEpisode(series.Id, airDate); - } - - return episodeInfo; - } - - private List<Episode> GetAnimeEpisodes(Series series, ParsedEpisodeInfo parsedEpisodeInfo, bool sceneSource) - { - var result = new List<Episode>(); - -// var sceneSeasonNumber = _sceneMappingService.GetSceneSeasonNumber(parsedEpisodeInfo.SeriesTitle); - - foreach (var absoluteEpisodeNumber in parsedEpisodeInfo.AbsoluteEpisodeNumbers) - { - Episode episode = null; - - if (parsedEpisodeInfo.Special) - { - episode = _episodeService.FindEpisode(series.Id, 0, absoluteEpisodeNumber); - } - - //else if (sceneSource) - //{ - // // Is there a reason why we excluded season 1 from this handling before? - // // Might have something to do with the scene name to season number check - // // If this needs to be reverted tests will need to be added - // if (sceneSeasonNumber.HasValue) - // { - // var episodes = _episodeService.FindEpisodesBySceneNumbering(series.Id, sceneSeasonNumber.Value, absoluteEpisodeNumber); - - // if (episodes.Count == 1) - // { - // episode = episodes.First(); - // } - - // if (episode == null) - // { - // episode = _episodeService.FindEpisode(series.Id, sceneSeasonNumber.Value, absoluteEpisodeNumber); - // } - // } - - // else - // { - // episode = _episodeService.FindEpisodeBySceneNumbering(series.Id, absoluteEpisodeNumber); - // } - //} - - if (episode == null) - { - episode = _episodeService.FindEpisode(series.Id, absoluteEpisodeNumber); - } - - if (episode != null) - { - _logger.Debug("Using absolute episode number {0} for: {1} - TVDB: {2}x{3:00}", - absoluteEpisodeNumber, - series.Title, - episode.SeasonNumber, - episode.EpisodeNumber); - - result.Add(episode); - } - } - - return result; - } - - //private List<Track> GetStandardTracks(Artist artist, ParsedTrackInfo parsedTrackInfo, SearchCriteriaBase searchCriteria) - //{ - // var result = new List<Track>(); - - // if (parsedTrackInfo.TrackNumbers == null) - // { - // return result; - // } - - // foreach (var trackNumber in parsedTrackInfo.TrackNumbers) - // { - // Track trackInfo = null; - - // if (searchCriteria != null) - // { - // trackInfo = searchCriteria.Tracks.SingleOrDefault(e => e.TrackNumber == trackNumber); //e => e.SeasonNumber == seasonNumber && e.TrackNumber == trackNumber - // } - - // if (trackInfo == null) - // { - // // TODO: [ParsingService]: FindTrack by artistID and trackNumber (or albumID and trackNumber if we change db schema to album as base) - // _logger.Debug("TrackInfo is null, we will not add as FindTrack(artistId, trackNumber) is not implemented"); - // //trackInfo = _trackService.FindTrack(artist.SpotifyId, trackNumber); - // } - - // if (trackInfo != null) - // { - // result.Add(trackInfo); - // } - - // else - // { - // _logger.Debug("Unable to find {0}", parsedTrackInfo); - // } - // } - - - - // return result; - //} - - private List<Episode> GetStandardEpisodes(Series series, ParsedEpisodeInfo parsedEpisodeInfo, bool sceneSource, SearchCriteriaBase searchCriteria) - { - var result = new List<Episode>(); - var seasonNumber = parsedEpisodeInfo.SeasonNumber; - - //if (sceneSource) - //{ - // var sceneMapping = _sceneMappingService.FindSceneMapping(parsedEpisodeInfo.SeriesTitle); - - // if (sceneMapping != null && sceneMapping.SeasonNumber.HasValue && sceneMapping.SeasonNumber.Value >= 0 && - // sceneMapping.SceneSeasonNumber == seasonNumber) - // { - // seasonNumber = sceneMapping.SeasonNumber.Value; - // } - //} - - if (parsedEpisodeInfo.EpisodeNumbers == null) - { - return new List<Episode>(); - } - - foreach (var episodeNumber in parsedEpisodeInfo.EpisodeNumbers) - { - if (series.UseSceneNumbering && sceneSource) - { - List<Episode> episodes = new List<Episode>(); - - if (searchCriteria != null) - { - episodes = searchCriteria.Episodes.Where(e => e.SceneSeasonNumber == parsedEpisodeInfo.SeasonNumber && - e.SceneEpisodeNumber == episodeNumber).ToList(); - } - - if (!episodes.Any()) - { - episodes = _episodeService.FindEpisodesBySceneNumbering(series.Id, seasonNumber, episodeNumber); - } - - if (episodes != null && episodes.Any()) - { - _logger.Debug("Using Scene to TVDB Mapping for: {0} - Scene: {1}x{2:00} - TVDB: {3}", - series.Title, - episodes.First().SceneSeasonNumber, - episodes.First().SceneEpisodeNumber, - string.Join(", ", episodes.Select(e => string.Format("{0}x{1:00}", e.SeasonNumber, e.EpisodeNumber)))); - - result.AddRange(episodes); - continue; - } - } - - Episode episodeInfo = null; - - if (searchCriteria != null) - { - episodeInfo = searchCriteria.Episodes.SingleOrDefault(e => e.SeasonNumber == seasonNumber && e.EpisodeNumber == episodeNumber); - } - - if (episodeInfo == null) - { - episodeInfo = _episodeService.FindEpisode(series.Id, seasonNumber, episodeNumber); - } - - if (episodeInfo != null) - { - result.Add(episodeInfo); - } - - else - { - _logger.Debug("Unable to find {0}", parsedEpisodeInfo); - } - } - - return result; - } - - public LocalTrack GetLocalTrack(string filename, Artist artist) { return GetLocalTrack(filename, artist, null); @@ -653,12 +194,7 @@ namespace NzbDrone.Core.Parser parsedTrackInfo = Parser.ParseMusicPath(filename); } - //if (parsedTrackInfo == null) - //{ - // var title = Path.GetFileNameWithoutExtension(filename); - //} - - if (parsedTrackInfo == null) + if (parsedTrackInfo == null || parsedTrackInfo.AlbumTitle.IsNullOrWhiteSpace()) { if (MediaFileExtensions.Extensions.Contains(Path.GetExtension(filename))) { @@ -668,12 +204,15 @@ namespace NzbDrone.Core.Parser return null; } - var tracks = GetTracks(parsedTrackInfo, artist); + var tracks = GetTracks(artist, parsedTrackInfo); + var album = _albumService.FindByTitle(artist.Id, parsedTrackInfo.AlbumTitle); return new LocalTrack { Artist = artist, + Album = album, Quality = parsedTrackInfo.Quality, + Language = parsedTrackInfo.Language, Tracks = tracks, Path = filename, ParsedTrackInfo = parsedTrackInfo, @@ -681,42 +220,57 @@ namespace NzbDrone.Core.Parser }; } - private List<Track> GetTracks(ParsedTrackInfo parsedTrackInfo, Artist artist) - { - return GetStandardTracks(artist, parsedTrackInfo); - } - - private List<Track> GetStandardTracks(Artist artist, ParsedTrackInfo parsedTrackInfo) + private List<Track> GetTracks(Artist artist, ParsedTrackInfo parsedTrackInfo) { var result = new List<Track>(); - if (parsedTrackInfo.TrackNumbers == null) + if (parsedTrackInfo.AlbumTitle.IsNullOrWhiteSpace()) { + _logger.Debug("Album title could not be parsed for {0}", parsedTrackInfo); return new List<Track>(); } - foreach (var trackNumber in parsedTrackInfo.TrackNumbers) + var album = _albumService.FindByTitle(artist.Id, parsedTrackInfo.AlbumTitle); + _logger.Debug("Album {0} selected for {1}", album, parsedTrackInfo); + + if (album == null) { - Track trackInfo = null; + _logger.Debug("Parsed album title not found in Db for {0}", parsedTrackInfo); + return new List<Track>(); + } - //if (searchCriteria != null) - //{ - // trackInfo = searchCriteria.Episodes.SingleOrDefault(e => e.SeasonNumber == seasonNumber && e.EpisodeNumber == trackNumber); - //} + Track trackInfo = null; - if (trackInfo == null) - { - var album = _albumRepository.FindByArtistAndName(parsedTrackInfo.ArtistTitle, Parser.CleanArtistTitle(parsedTrackInfo.AlbumTitle)); - if (album == null) - { - return new List<Track>(); - } - trackInfo = _trackService.FindTrack(artist.Id, album.Id, trackNumber); - } + if (parsedTrackInfo.Title.IsNotNullOrWhiteSpace()) + { + trackInfo = _trackService.FindTrackByTitle(artist.Id, album.Id, parsedTrackInfo.Title); + _logger.Debug("Track {0} selected for {1}", trackInfo, parsedTrackInfo); if (trackInfo != null) { result.Add(trackInfo); + return result; + } + } + + _logger.Debug("Track title search unsuccessful, falling back to track number for {1}", trackInfo, parsedTrackInfo); + + if (parsedTrackInfo.TrackNumbers == null) + { + _logger.Debug("Track has no track numbers: {1}", trackInfo, parsedTrackInfo); + return new List<Track>(); + } + + foreach (var trackNumber in parsedTrackInfo.TrackNumbers) + { + Track trackInfoByNumber = null; + + trackInfoByNumber = _trackService.FindTrack(artist.Id, album.Id, trackNumber); + _logger.Debug("Track {0} selected for {1}", trackInfoByNumber, parsedTrackInfo); + + if (trackInfoByNumber != null) + { + result.Add(trackInfoByNumber); } else diff --git a/src/NzbDrone.Core/Parser/SceneChecker.cs b/src/NzbDrone.Core/Parser/SceneChecker.cs index 188027153..36dcb78ca 100644 --- a/src/NzbDrone.Core/Parser/SceneChecker.cs +++ b/src/NzbDrone.Core/Parser/SceneChecker.cs @@ -1,4 +1,4 @@ -namespace NzbDrone.Core.Parser +namespace NzbDrone.Core.Parser { public static class SceneChecker { @@ -9,12 +9,12 @@ if (!title.Contains(".")) return false; if (title.Contains(" ")) return false; - var parsedTitle = Parser.ParseTitle(title); + var parsedTitle = Parser.ParseMusicTitle(title); if (parsedTitle == null || parsedTitle.ReleaseGroup == null || parsedTitle.Quality.Quality == Qualities.Quality.Unknown || - string.IsNullOrWhiteSpace(parsedTitle.SeriesTitle)) + string.IsNullOrWhiteSpace(parsedTitle.ArtistTitle)) { return false; } diff --git a/src/NzbDrone.Core/Profiles/Delay/DelayProfileService.cs b/src/NzbDrone.Core/Profiles/Delay/DelayProfileService.cs index a367ce4eb..6c3db01a7 100644 --- a/src/NzbDrone.Core/Profiles/Delay/DelayProfileService.cs +++ b/src/NzbDrone.Core/Profiles/Delay/DelayProfileService.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using NzbDrone.Common.Extensions; @@ -11,8 +11,10 @@ namespace NzbDrone.Core.Profiles.Delay void Delete(int id); List<DelayProfile> All(); DelayProfile Get(int id); + List<DelayProfile> AllForTag(int tagId); List<DelayProfile> AllForTags(HashSet<int> tagIds); DelayProfile BestForTags(HashSet<int> tagIds); + List<DelayProfile> Reorder(int id, int? afterId); } public class DelayProfileService : IDelayProfileService @@ -26,6 +28,8 @@ namespace NzbDrone.Core.Profiles.Delay public DelayProfile Add(DelayProfile profile) { + profile.Order = _repo.Count(); + return _repo.Insert(profile); } @@ -60,9 +64,15 @@ namespace NzbDrone.Core.Profiles.Delay return _repo.Get(id); } + public List<DelayProfile> AllForTag(int tagId) + { + return All().Where(r => r.Tags.Contains(tagId)) + .ToList(); + } + public List<DelayProfile> AllForTags(HashSet<int> tagIds) { - return _repo.All().Where(r => r.Tags.Intersect(tagIds).Any() || r.Tags.Empty()).ToList(); + return All().Where(r => r.Tags.Intersect(tagIds).Any() || r.Tags.Empty()).ToList(); } public DelayProfile BestForTags(HashSet<int> tagIds) @@ -70,5 +80,72 @@ namespace NzbDrone.Core.Profiles.Delay return _repo.All().Where(r => r.Tags.Intersect(tagIds).Any() || r.Tags.Empty()) .OrderBy(d => d.Order).First(); } + + public List<DelayProfile> Reorder(int id, int? afterId) + { + var all = All().OrderBy(d => d.Order) + .ToList(); + + var moving = all.SingleOrDefault(d => d.Id == id); + var after = afterId.HasValue ? all.SingleOrDefault(d => d.Id == afterId) : null; + + if (moving == null) + { + // TODO: This should throw + return all; + } + + var afterOrder = GetAfterOrder(moving, after); + var afterCount = afterOrder + 2; + var movingOrder = moving.Order; + + foreach (var delayProfile in all) + { + if (delayProfile.Id == 1) + { + continue; + } + + if (delayProfile.Id == id) + { + delayProfile.Order = afterOrder + 1; + } + + else if (delayProfile.Id == after?.Id) + { + delayProfile.Order = afterOrder; + } + + else if (delayProfile.Order > afterOrder) + { + delayProfile.Order = afterCount; + afterCount++; + } + + else if (delayProfile.Order > movingOrder) + { + delayProfile.Order--; + } + } + + _repo.UpdateMany(all); + + return All(); + } + + private int GetAfterOrder(DelayProfile moving, DelayProfile after) + { + if (after == null) + { + return 0; + } + + if (moving.Order < after.Order) + { + return after.Order - 1; + } + + return after.Order; + } } } diff --git a/src/NzbDrone.Core/Profiles/Languages/LanguageProfile.cs b/src/NzbDrone.Core/Profiles/Languages/LanguageProfile.cs new file mode 100644 index 000000000..ff07eed25 --- /dev/null +++ b/src/NzbDrone.Core/Profiles/Languages/LanguageProfile.cs @@ -0,0 +1,19 @@ +using System.Collections.Generic; +using System.Linq; +using NzbDrone.Core.Datastore; +using NzbDrone.Core.Languages; + +namespace NzbDrone.Core.Profiles.Languages +{ + public class LanguageProfile : ModelBase + { + public string Name { get; set; } + public List<ProfileLanguageItem> Languages { get; set; } + public Language Cutoff { get; set; } + + public Language LastAllowedLanguage() + { + return Languages.Last(q => q.Allowed).Language; + } + } +} diff --git a/src/NzbDrone.Core/Profiles/Languages/LanguageProfileInUseException.cs b/src/NzbDrone.Core/Profiles/Languages/LanguageProfileInUseException.cs new file mode 100644 index 000000000..bfba8045d --- /dev/null +++ b/src/NzbDrone.Core/Profiles/Languages/LanguageProfileInUseException.cs @@ -0,0 +1,13 @@ +using NzbDrone.Common.Exceptions; + +namespace NzbDrone.Core.Profiles.Languages +{ + public class LanguageProfileInUseException : NzbDroneException + { + public LanguageProfileInUseException(int profileId) + : base("Language profile [{0}] is in use.", profileId) + { + + } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Core/Profiles/Languages/LanguageProfileRepository.cs b/src/NzbDrone.Core/Profiles/Languages/LanguageProfileRepository.cs new file mode 100644 index 000000000..797c14efe --- /dev/null +++ b/src/NzbDrone.Core/Profiles/Languages/LanguageProfileRepository.cs @@ -0,0 +1,23 @@ +using NzbDrone.Core.Datastore; +using NzbDrone.Core.Messaging.Events; + +namespace NzbDrone.Core.Profiles.Languages +{ + public interface ILanguageProfileRepository : IBasicRepository<LanguageProfile> + { + bool Exists(int id); + } + + public class LanguageProfileRepository : BasicRepository<LanguageProfile>, ILanguageProfileRepository + { + public LanguageProfileRepository(IMainDatabase database, IEventAggregator eventAggregator) + : base(database, eventAggregator) + { + } + + public bool Exists(int id) + { + return DataMapper.Query<LanguageProfile>().Where(p => p.Id == id).GetRowCount() == 1; + } + } +} diff --git a/src/NzbDrone.Core/Profiles/Languages/LanguageProfileService.cs b/src/NzbDrone.Core/Profiles/Languages/LanguageProfileService.cs new file mode 100644 index 000000000..dfab045bd --- /dev/null +++ b/src/NzbDrone.Core/Profiles/Languages/LanguageProfileService.cs @@ -0,0 +1,95 @@ +using System.Collections.Generic; +using System.Linq; +using NLog; +using NzbDrone.Core.Lifecycle; +using NzbDrone.Core.Messaging.Events; +using NzbDrone.Core.Languages; +using NzbDrone.Core.Music; + +namespace NzbDrone.Core.Profiles.Languages +{ + public interface ILanguageProfileService + { + LanguageProfile Add(LanguageProfile profile); + void Update(LanguageProfile profile); + void Delete(int id); + List<LanguageProfile> All(); + LanguageProfile Get(int id); + bool Exists(int id); + } + + public class LanguageProfileService : ILanguageProfileService, IHandle<ApplicationStartedEvent> + { + private readonly ILanguageProfileRepository _profileRepository; + private readonly IArtistService _artistService; + private readonly Logger _logger; + + public LanguageProfileService(ILanguageProfileRepository profileRepository, IArtistService artistService, Logger logger) + { + _profileRepository = profileRepository; + _artistService = artistService; + _logger = logger; + } + + public LanguageProfile Add(LanguageProfile profile) + { + return _profileRepository.Insert(profile); + } + + public void Update(LanguageProfile profile) + { + _profileRepository.Update(profile); + } + + public void Delete(int id) + { + if (_artistService.GetAllArtists().Any(c => c.LanguageProfileId == id)) + { + throw new LanguageProfileInUseException(id); + } + + _profileRepository.Delete(id); + } + + public List<LanguageProfile> All() + { + return _profileRepository.All().ToList(); + } + + public LanguageProfile Get(int id) + { + return _profileRepository.Get(id); + } + + public bool Exists(int id) + { + return _profileRepository.Exists(id); + } + + private LanguageProfile AddDefaultProfile(string name, Language cutoff, params Language[] allowed) + { + var languages = Language.All + .OrderByDescending(l => l.Name) + .Select(v => new ProfileLanguageItem { Language = v, Allowed = allowed.Contains(v) }) + .ToList(); + + var profile = new LanguageProfile + { + Name = name, + Cutoff = cutoff, + Languages = languages, + }; + + return Add(profile); + } + + public void Handle(ApplicationStartedEvent message) + { + if (All().Any()) return; + + _logger.Info("Setting up default language profiles"); + + AddDefaultProfile("English", Language.English, Language.English); + } + } +} diff --git a/src/NzbDrone.Core/Profiles/Languages/ProfileLanguageItem.cs b/src/NzbDrone.Core/Profiles/Languages/ProfileLanguageItem.cs new file mode 100644 index 000000000..a25ea2257 --- /dev/null +++ b/src/NzbDrone.Core/Profiles/Languages/ProfileLanguageItem.cs @@ -0,0 +1,11 @@ +using NzbDrone.Core.Datastore; +using NzbDrone.Core.Languages; + +namespace NzbDrone.Core.Profiles.Languages +{ + public class ProfileLanguageItem : IEmbeddedDocument + { + public Language Language { get; set; } + public bool Allowed { get; set; } + } +} diff --git a/src/NzbDrone.Core/Profiles/Profile.cs b/src/NzbDrone.Core/Profiles/Profile.cs deleted file mode 100644 index 6215e9474..000000000 --- a/src/NzbDrone.Core/Profiles/Profile.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using NzbDrone.Core.Datastore; -using NzbDrone.Core.Parser; -using NzbDrone.Core.Qualities; - -namespace NzbDrone.Core.Profiles -{ - public class Profile : ModelBase - { - public string Name { get; set; } - public Quality Cutoff { get; set; } - public List<ProfileQualityItem> Items { get; set; } - public Language Language { get; set; } - - public Quality LastAllowedQuality() - { - return Items.Last(q => q.Allowed).Quality; - } - } -} diff --git a/src/NzbDrone.Core/Profiles/ProfileInUseException.cs b/src/NzbDrone.Core/Profiles/ProfileInUseException.cs deleted file mode 100644 index d55523d9a..000000000 --- a/src/NzbDrone.Core/Profiles/ProfileInUseException.cs +++ /dev/null @@ -1,13 +0,0 @@ -using NzbDrone.Common.Exceptions; - -namespace NzbDrone.Core.Profiles -{ - public class ProfileInUseException : NzbDroneException - { - public ProfileInUseException(int profileId) - : base("Profile [{0}] is in use.", profileId) - { - - } - } -} \ No newline at end of file diff --git a/src/NzbDrone.Core/Profiles/ProfileQualityItem.cs b/src/NzbDrone.Core/Profiles/ProfileQualityItem.cs deleted file mode 100644 index 35c9ce360..000000000 --- a/src/NzbDrone.Core/Profiles/ProfileQualityItem.cs +++ /dev/null @@ -1,11 +0,0 @@ -using NzbDrone.Core.Datastore; -using NzbDrone.Core.Qualities; - -namespace NzbDrone.Core.Profiles -{ - public class ProfileQualityItem : IEmbeddedDocument - { - public Quality Quality { get; set; } - public bool Allowed { get; set; } - } -} diff --git a/src/NzbDrone.Core/Profiles/Quality/Profile.cs b/src/NzbDrone.Core/Profiles/Quality/Profile.cs new file mode 100644 index 000000000..d453d83d9 --- /dev/null +++ b/src/NzbDrone.Core/Profiles/Quality/Profile.cs @@ -0,0 +1,19 @@ +using System.Collections.Generic; +using System.Linq; +using NzbDrone.Core.Datastore; +using NzbDrone.Core.Qualities; + +namespace NzbDrone.Core.Profiles.Qualities +{ + public class Profile : ModelBase + { + public string Name { get; set; } + public Quality Cutoff { get; set; } + public List<ProfileQualityItem> Items { get; set; } + + public Quality LastAllowedQuality() + { + return Items.Last(q => q.Allowed).Quality; + } + } +} diff --git a/src/NzbDrone.Core/Profiles/Quality/ProfileInUseException.cs b/src/NzbDrone.Core/Profiles/Quality/ProfileInUseException.cs new file mode 100644 index 000000000..b56c9f1db --- /dev/null +++ b/src/NzbDrone.Core/Profiles/Quality/ProfileInUseException.cs @@ -0,0 +1,13 @@ +using NzbDrone.Common.Exceptions; + +namespace NzbDrone.Core.Profiles.Qualities +{ + public class ProfileInUseException : NzbDroneException + { + public ProfileInUseException(int profileId) + : base("Profile [{0}] is in use.", profileId) + { + + } + } +} diff --git a/src/NzbDrone.Core/Profiles/Quality/ProfileQualityItem.cs b/src/NzbDrone.Core/Profiles/Quality/ProfileQualityItem.cs new file mode 100644 index 000000000..338654854 --- /dev/null +++ b/src/NzbDrone.Core/Profiles/Quality/ProfileQualityItem.cs @@ -0,0 +1,11 @@ +using NzbDrone.Core.Datastore; +using NzbDrone.Core.Qualities; + +namespace NzbDrone.Core.Profiles.Qualities +{ + public class ProfileQualityItem : IEmbeddedDocument + { + public Quality Quality { get; set; } + public bool Allowed { get; set; } + } +} diff --git a/src/NzbDrone.Core/Profiles/ProfileRepository.cs b/src/NzbDrone.Core/Profiles/Quality/ProfileRepository.cs similarity index 88% rename from src/NzbDrone.Core/Profiles/ProfileRepository.cs rename to src/NzbDrone.Core/Profiles/Quality/ProfileRepository.cs index 4e071a0cf..1724d7a4f 100644 --- a/src/NzbDrone.Core/Profiles/ProfileRepository.cs +++ b/src/NzbDrone.Core/Profiles/Quality/ProfileRepository.cs @@ -1,7 +1,7 @@ -using NzbDrone.Core.Datastore; +using NzbDrone.Core.Datastore; using NzbDrone.Core.Messaging.Events; -namespace NzbDrone.Core.Profiles +namespace NzbDrone.Core.Profiles.Qualities { public interface IProfileRepository : IBasicRepository<Profile> { diff --git a/src/NzbDrone.Core/Profiles/ProfileService.cs b/src/NzbDrone.Core/Profiles/Quality/ProfileService.cs similarity index 92% rename from src/NzbDrone.Core/Profiles/ProfileService.cs rename to src/NzbDrone.Core/Profiles/Quality/ProfileService.cs index 671d4127f..a1be66b2a 100644 --- a/src/NzbDrone.Core/Profiles/ProfileService.cs +++ b/src/NzbDrone.Core/Profiles/Quality/ProfileService.cs @@ -1,14 +1,12 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using NLog; using NzbDrone.Core.Lifecycle; using NzbDrone.Core.Messaging.Events; -using NzbDrone.Core.Parser; using NzbDrone.Core.Qualities; -using NzbDrone.Core.Tv; using NzbDrone.Core.Music; -namespace NzbDrone.Core.Profiles +namespace NzbDrone.Core.Profiles.Qualities { public interface IProfileService { @@ -75,7 +73,7 @@ namespace NzbDrone.Core.Profiles .Select(v => new ProfileQualityItem { Quality = v.Quality, Allowed = allowed.Contains(v.Quality) }) .ToList(); - var profile = new Profile { Name = name, Cutoff = cutoff, Items = items, Language = Language.English }; + var profile = new Profile { Name = name, Cutoff = cutoff, Items = items}; return Add(profile); } @@ -87,6 +85,8 @@ namespace NzbDrone.Core.Profiles _logger.Info("Setting up default quality profiles"); AddDefaultProfile("Any", + Quality.Unknown, + Quality.Unknown, Quality.MP3_192, Quality.MP3_256, Quality.MP3_320, @@ -95,12 +95,14 @@ namespace NzbDrone.Core.Profiles Quality.FLAC); AddDefaultProfile("Lossless", + Quality.FLAC, Quality.FLAC); AddDefaultProfile("Standard", + Quality.MP3_192, Quality.MP3_192, Quality.MP3_256, Quality.MP3_320); } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Core/Properties/AssemblyInfo.cs b/src/NzbDrone.Core/Properties/AssemblyInfo.cs index 4593d015a..352317239 100644 --- a/src/NzbDrone.Core/Properties/AssemblyInfo.cs +++ b/src/NzbDrone.Core/Properties/AssemblyInfo.cs @@ -1,4 +1,4 @@ -using System.Reflection; +using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; @@ -6,11 +6,9 @@ using System.Runtime.InteropServices; // set of attributes. Change these attribute values to modify the information // associated with an assembly. -[assembly: AssemblyTitle("NzbDrone.Core")] +[assembly: AssemblyTitle("Lidarr.Core")] // The following GUID is for the ID of the typelib if this project is exposed to COM [assembly: Guid("3C29FEF7-4B07-49ED-822E-1C29DC49BFAB")] -[assembly: AssemblyVersion("10.0.0.*")] - -[assembly: InternalsVisibleTo("NzbDrone.Core.Test")] +[assembly: InternalsVisibleTo("Lidarr.Core.Test")] diff --git a/src/NzbDrone.Core/Qualities/QualityDefinitionService.cs b/src/NzbDrone.Core/Qualities/QualityDefinitionService.cs index d2fc46e3c..edff516eb 100644 --- a/src/NzbDrone.Core/Qualities/QualityDefinitionService.cs +++ b/src/NzbDrone.Core/Qualities/QualityDefinitionService.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using NLog; using NzbDrone.Core.Lifecycle; @@ -11,6 +11,7 @@ namespace NzbDrone.Core.Qualities public interface IQualityDefinitionService { void Update(QualityDefinition qualityDefinition); + void UpdateMany(List<QualityDefinition> qualityDefinitions); List<QualityDefinition> All(); QualityDefinition GetById(int id); QualityDefinition Get(Quality quality); @@ -41,6 +42,11 @@ namespace NzbDrone.Core.Qualities _cache.Clear(); } + public void UpdateMany(List<QualityDefinition> qualityDefinitions) + { + _repo.UpdateMany(qualityDefinitions); + } + public List<QualityDefinition> All() { return GetAll().Values.OrderBy(d => d.Weight).ToList(); @@ -50,17 +56,17 @@ namespace NzbDrone.Core.Qualities { return GetAll().Values.Single(v => v.Id == id); } - + public QualityDefinition Get(Quality quality) { return GetAll()[quality]; } - + private void InsertMissingDefinitions() { List<QualityDefinition> insertList = new List<QualityDefinition>(); List<QualityDefinition> updateList = new List<QualityDefinition>(); - + var allDefinitions = Quality.DefaultQualityDefinitions.OrderBy(d => d.Weight).ToList(); var existingDefinitions = _repo.All().ToList(); @@ -83,7 +89,7 @@ namespace NzbDrone.Core.Qualities _repo.InsertMany(insertList); _repo.UpdateMany(updateList); _repo.DeleteMany(existingDefinitions); - + _cache.Clear(); } diff --git a/src/NzbDrone.Core/Qualities/QualityModel.cs b/src/NzbDrone.Core/Qualities/QualityModel.cs index a483d22c2..b5658415b 100644 --- a/src/NzbDrone.Core/Qualities/QualityModel.cs +++ b/src/NzbDrone.Core/Qualities/QualityModel.cs @@ -1,17 +1,18 @@ -using System; +using System; +using System.Linq; using Newtonsoft.Json; using NzbDrone.Core.Datastore; namespace NzbDrone.Core.Qualities { - public class QualityModel : IEmbeddedDocument, IEquatable<QualityModel> + public class QualityModel : IEmbeddedDocument, IEquatable<QualityModel>, IComparable { public Quality Quality { get; set; } public Revision Revision { get; set; } [JsonIgnore] public QualitySource QualitySource { get; set; } - + public QualityModel() : this(Quality.Unknown, new Revision()) { @@ -40,6 +41,45 @@ namespace NzbDrone.Core.Qualities } } + public int CompareTo(object obj) + { + var other = (QualityModel)obj; + var definition = Quality.DefaultQualityDefinitions.First(q => q.Quality == Quality); + var otherDefinition = Quality.DefaultQualityDefinitions.First(q => q.Quality == other.Quality); + + if (definition.Weight > otherDefinition.Weight) + { + return 1; + } + + if (definition.Weight < otherDefinition.Weight) + { + return -1; + } + + if (Revision.Real > other.Revision.Real) + { + return 1; + } + + if (Revision.Real < other.Revision.Real) + { + return -1; + } + + if (Revision.Version > other.Revision.Version) + { + return 1; + } + + if (Revision.Version < other.Revision.Version) + { + return -1; + } + + return 0; + } + public bool Equals(QualityModel other) { if (ReferenceEquals(null, other)) return false; diff --git a/src/NzbDrone.Core/Qualities/QualityModelComparer.cs b/src/NzbDrone.Core/Qualities/QualityModelComparer.cs index 64f1939b8..029089030 100644 --- a/src/NzbDrone.Core/Qualities/QualityModelComparer.cs +++ b/src/NzbDrone.Core/Qualities/QualityModelComparer.cs @@ -1,6 +1,6 @@ -using System.Collections.Generic; +using System.Collections.Generic; using NzbDrone.Common.EnsureThat; -using NzbDrone.Core.Profiles; +using NzbDrone.Core.Profiles.Qualities; namespace NzbDrone.Core.Qualities { diff --git a/src/NzbDrone.Core/Queue/EstimatedCompletionTimeComparer.cs b/src/NzbDrone.Core/Queue/EstimatedCompletionTimeComparer.cs new file mode 100644 index 000000000..e8c52e1ab --- /dev/null +++ b/src/NzbDrone.Core/Queue/EstimatedCompletionTimeComparer.cs @@ -0,0 +1,38 @@ +using System; +using System.Collections.Generic; + +namespace NzbDrone.Core.Queue +{ + public class EstimatedCompletionTimeComparer : IComparer<DateTime?> + { + public int Compare(DateTime? x, DateTime? y) + { + if (!x.HasValue && !y.HasValue) + { + return 0; + } + + if (!x.HasValue && y.HasValue) + { + return 1; + } + + if (x.HasValue && !y.HasValue) + { + return -1; + } + + if (x.Value > y.Value) + { + return 1; + } + + if (x.Value < y.Value) + { + return -1; + } + + return 0; + } + } +} diff --git a/src/NzbDrone.Core/Queue/Queue.cs b/src/NzbDrone.Core/Queue/Queue.cs index 54c949baa..2bdddbe22 100644 --- a/src/NzbDrone.Core/Queue/Queue.cs +++ b/src/NzbDrone.Core/Queue/Queue.cs @@ -1,11 +1,10 @@ -using System; +using System; using System.Collections.Generic; using NzbDrone.Core.Datastore; using NzbDrone.Core.Download.TrackedDownloads; using NzbDrone.Core.Indexers; using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Qualities; -using NzbDrone.Core.Tv; using NzbDrone.Core.Music; namespace NzbDrone.Core.Queue @@ -14,7 +13,6 @@ namespace NzbDrone.Core.Queue { public Artist Artist { get; set; } public Album Album { get; set; } - public Episode Episode { get; set; } public QualityModel Quality { get; set; } public decimal Size { get; set; } public string Title { get; set; } @@ -27,5 +25,8 @@ namespace NzbDrone.Core.Queue public string DownloadId { get; set; } public RemoteAlbum RemoteAlbum { get; set; } public DownloadProtocol Protocol { get; set; } + public string DownloadClient { get; set; } + public string Indexer { get; set; } + public string ErrorMessage { get; set; } } } diff --git a/src/NzbDrone.Core/Queue/QueueService.cs b/src/NzbDrone.Core/Queue/QueueService.cs index e86ebee29..2b3a1ce7a 100644 --- a/src/NzbDrone.Core/Queue/QueueService.cs +++ b/src/NzbDrone.Core/Queue/QueueService.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using NzbDrone.Common.Crypto; @@ -12,6 +12,7 @@ namespace NzbDrone.Core.Queue { List<Queue> GetQueue(); Queue Find(int id); + void Remove(int id); } public class QueueService : IQueueService, IHandle<TrackedDownloadRefreshedEvent> @@ -34,6 +35,11 @@ namespace NzbDrone.Core.Queue return _queue.SingleOrDefault(q => q.Id == id); } + public void Remove(int id) + { + _queue.Remove(Find(id)); + } + public void Handle(TrackedDownloadRefreshedEvent message) { _queue = message.TrackedDownloads.OrderBy(c => c.DownloadItem.RemainingTime).SelectMany(MapQueue) @@ -72,9 +78,12 @@ namespace NzbDrone.Core.Queue Status = trackedDownload.DownloadItem.Status.ToString(), TrackedDownloadStatus = trackedDownload.Status.ToString(), StatusMessages = trackedDownload.StatusMessages.ToList(), + ErrorMessage = trackedDownload.DownloadItem.Message, RemoteAlbum = trackedDownload.RemoteAlbum, DownloadId = trackedDownload.DownloadItem.DownloadId, - Protocol = trackedDownload.Protocol + Protocol = trackedDownload.Protocol, + DownloadClient = trackedDownload.DownloadItem.DownloadClient, + Indexer = trackedDownload.Indexer }; if (queue.Timeleft.HasValue) diff --git a/src/NzbDrone.Core/Queue/TimeleftComparer.cs b/src/NzbDrone.Core/Queue/TimeleftComparer.cs new file mode 100644 index 000000000..2c051deeb --- /dev/null +++ b/src/NzbDrone.Core/Queue/TimeleftComparer.cs @@ -0,0 +1,38 @@ +using System; +using System.Collections.Generic; + +namespace NzbDrone.Core.Queue +{ + public class TimeleftComparer : IComparer<TimeSpan?> + { + public int Compare(TimeSpan? x, TimeSpan? y) + { + if (!x.HasValue && !y.HasValue) + { + return 0; + } + + if (!x.HasValue && y.HasValue) + { + return 1; + } + + if (x.HasValue && !y.HasValue) + { + return -1; + } + + if (x.Value > y.Value) + { + return 1; + } + + if (x.Value < y.Value) + { + return -1; + } + + return 0; + } + } +} diff --git a/src/NzbDrone.Core/Restrictions/RestrictionService.cs b/src/NzbDrone.Core/Restrictions/RestrictionService.cs index 5d7cfba8d..90b9d4a6f 100644 --- a/src/NzbDrone.Core/Restrictions/RestrictionService.cs +++ b/src/NzbDrone.Core/Restrictions/RestrictionService.cs @@ -34,7 +34,7 @@ namespace NzbDrone.Core.Restrictions public List<Restriction> AllForTag(int tagId) { - return _repo.All().Where(r => r.Tags.Contains(tagId) || r.Tags.Empty()).ToList(); + return _repo.All().Where(r => r.Tags.Contains(tagId)).ToList(); } public List<Restriction> AllForTags(HashSet<int> tagIds) diff --git a/src/NzbDrone.Core/RootFolders/RootFolderService.cs b/src/NzbDrone.Core/RootFolders/RootFolderService.cs index 84bb9f472..4ee118524 100644 --- a/src/NzbDrone.Core/RootFolders/RootFolderService.cs +++ b/src/NzbDrone.Core/RootFolders/RootFolderService.cs @@ -1,4 +1,4 @@ -using System.Linq; +using System.Linq; using System; using System.Collections.Generic; using System.IO; @@ -7,7 +7,7 @@ using NzbDrone.Common; using NzbDrone.Common.Disk; using NzbDrone.Common.Extensions; using NzbDrone.Core.Configuration; -using NzbDrone.Core.Tv; +using NzbDrone.Core.Music; namespace NzbDrone.Core.RootFolders { @@ -24,7 +24,7 @@ namespace NzbDrone.Core.RootFolders { private readonly IRootFolderRepository _rootFolderRepository; private readonly IDiskProvider _diskProvider; - private readonly ISeriesRepository _seriesRepository; + private readonly IArtistRepository _artistRepository; private readonly IConfigService _configService; private readonly Logger _logger; @@ -44,13 +44,13 @@ namespace NzbDrone.Core.RootFolders public RootFolderService(IRootFolderRepository rootFolderRepository, IDiskProvider diskProvider, - ISeriesRepository seriesRepository, + IArtistRepository artistRepository, IConfigService configService, Logger logger) { _rootFolderRepository = rootFolderRepository; _diskProvider = diskProvider; - _seriesRepository = seriesRepository; + _artistRepository = artistRepository; _configService = configService; _logger = logger; } @@ -107,11 +107,6 @@ namespace NzbDrone.Core.RootFolders throw new InvalidOperationException("Recent directory already exists."); } - if (_configService.DownloadedAlbumsFolder.IsNotNullOrWhiteSpace() && _configService.DownloadedAlbumsFolder.PathEquals(rootFolder.Path)) - { - throw new InvalidOperationException("Drone Factory folder cannot be used."); - } - if (!_diskProvider.FolderWritable(rootFolder.Path)) { throw new UnauthorizedAccessException(string.Format("Root folder path '{0}' is not writable by user '{1}'", rootFolder.Path, Environment.UserName)); @@ -139,7 +134,7 @@ namespace NzbDrone.Core.RootFolders } var results = new List<UnmappedFolder>(); - var series = _seriesRepository.All().ToList(); + var artist = _artistRepository.All().ToList(); if (!_diskProvider.FolderExists(path)) { @@ -147,8 +142,8 @@ namespace NzbDrone.Core.RootFolders return results; } - var possibleSeriesFolders = _diskProvider.GetDirectories(path).ToList(); - var unmappedFolders = possibleSeriesFolders.Except(series.Select(s => s.Path), PathEqualityComparer.Instance).ToList(); + var possibleArtistFolders = _diskProvider.GetDirectories(path).ToList(); + var unmappedFolders = possibleArtistFolders.Except(artist.Select(s => s.Path), PathEqualityComparer.Instance).ToList(); foreach (string unmappedFolder in unmappedFolders) { @@ -171,4 +166,4 @@ namespace NzbDrone.Core.RootFolders return rootFolder; } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Core/SeriesStats/SeasonStatistics.cs b/src/NzbDrone.Core/SeriesStats/SeasonStatistics.cs deleted file mode 100644 index 3b3731ed5..000000000 --- a/src/NzbDrone.Core/SeriesStats/SeasonStatistics.cs +++ /dev/null @@ -1,41 +0,0 @@ -using System; -using NzbDrone.Core.Datastore; - -namespace NzbDrone.Core.SeriesStats -{ - public class SeasonStatistics : ResultSet - { - public int SeriesId { get; set; } - public int SeasonNumber { get; set; } - public string NextAiringString { get; set; } - public string PreviousAiringString { get; set; } - public int EpisodeFileCount { get; set; } - public int EpisodeCount { get; set; } - public int TotalEpisodeCount { get; set; } - public long SizeOnDisk { get; set; } - - public DateTime? NextAiring - { - get - { - DateTime nextAiring; - - if (!DateTime.TryParse(NextAiringString, out nextAiring)) return null; - - return nextAiring; - } - } - - public DateTime? PreviousAiring - { - get - { - DateTime previousAiring; - - if (!DateTime.TryParse(PreviousAiringString, out previousAiring)) return null; - - return previousAiring; - } - } - } -} diff --git a/src/NzbDrone.Core/SeriesStats/SeriesStatistics.cs b/src/NzbDrone.Core/SeriesStats/SeriesStatistics.cs deleted file mode 100644 index 25a82d68f..000000000 --- a/src/NzbDrone.Core/SeriesStats/SeriesStatistics.cs +++ /dev/null @@ -1,42 +0,0 @@ -using System; -using System.Collections.Generic; -using NzbDrone.Core.Datastore; - -namespace NzbDrone.Core.SeriesStats -{ - public class SeriesStatistics : ResultSet - { - public int SeriesId { get; set; } - public string NextAiringString { get; set; } - public string PreviousAiringString { get; set; } - public int EpisodeFileCount { get; set; } - public int EpisodeCount { get; set; } - public int TotalEpisodeCount { get; set; } - public long SizeOnDisk { get; set; } - public List<SeasonStatistics> SeasonStatistics { get; set; } - - public DateTime? NextAiring - { - get - { - DateTime nextAiring; - - if (!DateTime.TryParse(NextAiringString, out nextAiring)) return null; - - return nextAiring; - } - } - - public DateTime? PreviousAiring - { - get - { - DateTime previousAiring; - - if (!DateTime.TryParse(PreviousAiringString, out previousAiring)) return null; - - return previousAiring; - } - } - } -} diff --git a/src/NzbDrone.Core/SeriesStats/SeriesStatisticsRepository.cs b/src/NzbDrone.Core/SeriesStats/SeriesStatisticsRepository.cs deleted file mode 100644 index 73e4e8b4b..000000000 --- a/src/NzbDrone.Core/SeriesStats/SeriesStatisticsRepository.cs +++ /dev/null @@ -1,82 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; -using NzbDrone.Core.Datastore; - -namespace NzbDrone.Core.SeriesStats -{ - public interface ISeriesStatisticsRepository - { - List<SeasonStatistics> SeriesStatistics(); - List<SeasonStatistics> SeriesStatistics(int seriesId); - } - - public class SeriesStatisticsRepository : ISeriesStatisticsRepository - { - private readonly IMainDatabase _database; - - public SeriesStatisticsRepository(IMainDatabase database) - { - _database = database; - } - - public List<SeasonStatistics> SeriesStatistics() - { - var mapper = _database.GetDataMapper(); - - mapper.AddParameter("currentDate", DateTime.UtcNow); - - var sb = new StringBuilder(); - sb.AppendLine(GetSelectClause()); - sb.AppendLine(GetEpisodeFilesJoin()); - sb.AppendLine(GetGroupByClause()); - var queryText = sb.ToString(); - - return mapper.Query<SeasonStatistics>(queryText); - } - - public List<SeasonStatistics> SeriesStatistics(int seriesId) - { - var mapper = _database.GetDataMapper(); - - mapper.AddParameter("currentDate", DateTime.UtcNow); - mapper.AddParameter("seriesId", seriesId); - - var sb = new StringBuilder(); - sb.AppendLine(GetSelectClause()); - sb.AppendLine(GetEpisodeFilesJoin()); - sb.AppendLine("WHERE Episodes.SeriesId = @seriesId"); - sb.AppendLine(GetGroupByClause()); - var queryText = sb.ToString(); - - return mapper.Query<SeasonStatistics>(queryText); - } - - private string GetSelectClause() - { - return @"SELECT Episodes.*, SUM(EpisodeFiles.Size) as SizeOnDisk FROM - (SELECT - Episodes.SeriesId, - Episodes.SeasonNumber, - SUM(CASE WHEN AirdateUtc <= @currentDate OR EpisodeFileId > 0 THEN 1 ELSE 0 END) AS TotalEpisodeCount, - SUM(CASE WHEN (Monitored = 1 AND AirdateUtc <= @currentDate) OR EpisodeFileId > 0 THEN 1 ELSE 0 END) AS EpisodeCount, - SUM(CASE WHEN EpisodeFileId > 0 THEN 1 ELSE 0 END) AS EpisodeFileCount, - MIN(CASE WHEN AirDateUtc < @currentDate OR EpisodeFileId > 0 OR Monitored = 0 THEN NULL ELSE AirDateUtc END) AS NextAiringString, - MAX(CASE WHEN AirDateUtc >= @currentDate OR EpisodeFileId = 0 AND Monitored = 0 THEN NULL ELSE AirDateUtc END) AS PreviousAiringString - FROM Episodes - GROUP BY Episodes.SeriesId, Episodes.SeasonNumber) as Episodes"; - } - - private string GetGroupByClause() - { - return "GROUP BY Episodes.SeriesId, Episodes.SeasonNumber"; - } - - private string GetEpisodeFilesJoin() - { - return @"LEFT OUTER JOIN EpisodeFiles - ON EpisodeFiles.SeriesId = Episodes.SeriesId - AND EpisodeFiles.SeasonNumber = Episodes.SeasonNumber"; - } - } -} diff --git a/src/NzbDrone.Core/SeriesStats/SeriesStatisticsService.cs b/src/NzbDrone.Core/SeriesStats/SeriesStatisticsService.cs deleted file mode 100644 index b273f84ce..000000000 --- a/src/NzbDrone.Core/SeriesStats/SeriesStatisticsService.cs +++ /dev/null @@ -1,63 +0,0 @@ -using System.Collections.Generic; -using System.Linq; - -namespace NzbDrone.Core.SeriesStats -{ - public interface ISeriesStatisticsService - { - List<SeriesStatistics> SeriesStatistics(); - SeriesStatistics SeriesStatistics(int seriesId); - } - - public class SeriesStatisticsService : ISeriesStatisticsService - { - private readonly ISeriesStatisticsRepository _seriesStatisticsRepository; - - public SeriesStatisticsService(ISeriesStatisticsRepository seriesStatisticsRepository) - { - _seriesStatisticsRepository = seriesStatisticsRepository; - } - - public List<SeriesStatistics> SeriesStatistics() - { - var seasonStatistics = _seriesStatisticsRepository.SeriesStatistics(); - - return seasonStatistics.GroupBy(s => s.SeriesId).Select(s => MapSeriesStatistics(s.ToList())).ToList(); - } - - public SeriesStatistics SeriesStatistics(int seriesId) - { - var stats = _seriesStatisticsRepository.SeriesStatistics(seriesId); - - if (stats == null || stats.Count == 0) return new SeriesStatistics(); - - return MapSeriesStatistics(stats); - } - - private SeriesStatistics MapSeriesStatistics(List<SeasonStatistics> seasonStatistics) - { - var seriesStatistics = new SeriesStatistics - { - SeasonStatistics = seasonStatistics, - SeriesId = seasonStatistics.First().SeriesId, - EpisodeFileCount = seasonStatistics.Sum(s => s.EpisodeFileCount), - EpisodeCount = seasonStatistics.Sum(s => s.EpisodeCount), - TotalEpisodeCount = seasonStatistics.Sum(s => s.TotalEpisodeCount), - SizeOnDisk = seasonStatistics.Sum(s => s.SizeOnDisk) - }; - - var nextAiring = seasonStatistics.Where(s => s.NextAiring != null) - .OrderBy(s => s.NextAiring) - .FirstOrDefault(); - - var previousAiring = seasonStatistics.Where(s => s.PreviousAiring != null) - .OrderBy(s => s.PreviousAiring) - .LastOrDefault(); - - seriesStatistics.NextAiringString = nextAiring != null ? nextAiring.NextAiringString : null; - seriesStatistics.PreviousAiringString = previousAiring != null ? previousAiring.PreviousAiringString : null; - - return seriesStatistics; - } - } -} diff --git a/src/NzbDrone.Core/Tags/TagDetails.cs b/src/NzbDrone.Core/Tags/TagDetails.cs new file mode 100644 index 000000000..39df71be0 --- /dev/null +++ b/src/NzbDrone.Core/Tags/TagDetails.cs @@ -0,0 +1,18 @@ +using System.Collections.Generic; +using NzbDrone.Core.Datastore; +using NzbDrone.Core.Notifications; +using NzbDrone.Core.Profiles.Delay; +using NzbDrone.Core.Restrictions; +using NzbDrone.Core.Music; + +namespace NzbDrone.Core.Tags +{ + public class TagDetails : ModelBase + { + public string Label { get; set; } + public List<Artist> Artist { get; set; } + public List<NotificationDefinition> Notifications { get; set; } + public List<Restriction> Restrictions { get; set; } + public List<DelayProfile> DelayProfiles { get; set; } + } +} diff --git a/src/NzbDrone.Core/Tags/TagRepository.cs b/src/NzbDrone.Core/Tags/TagRepository.cs index 500502843..2921ca7c8 100644 --- a/src/NzbDrone.Core/Tags/TagRepository.cs +++ b/src/NzbDrone.Core/Tags/TagRepository.cs @@ -8,6 +8,7 @@ namespace NzbDrone.Core.Tags public interface ITagRepository : IBasicRepository<Tag> { Tag GetByLabel(string label); + Tag FindByLabel(string label); } public class TagRepository : BasicRepository<Tag>, ITagRepository @@ -28,5 +29,10 @@ namespace NzbDrone.Core.Tags return model; } + + public Tag FindByLabel(string label) + { + return Query.Where(c => c.Label == label).SingleOrDefault(); + } } } diff --git a/src/NzbDrone.Core/Tags/TagService.cs b/src/NzbDrone.Core/Tags/TagService.cs index b7637a6f8..d7ee7b28d 100644 --- a/src/NzbDrone.Core/Tags/TagService.cs +++ b/src/NzbDrone.Core/Tags/TagService.cs @@ -1,6 +1,10 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using NzbDrone.Core.Messaging.Events; +using NzbDrone.Core.Notifications; +using NzbDrone.Core.Profiles.Delay; +using NzbDrone.Core.Restrictions; +using NzbDrone.Core.Music; namespace NzbDrone.Core.Tags { @@ -8,6 +12,7 @@ namespace NzbDrone.Core.Tags { Tag GetTag(int tagId); Tag GetTag(string tag); + TagDetails Details(int tagId); List<Tag> All(); Tag Add(Tag tag); Tag Update(Tag tag); @@ -18,11 +23,24 @@ namespace NzbDrone.Core.Tags { private readonly ITagRepository _repo; private readonly IEventAggregator _eventAggregator; + private readonly IDelayProfileService _delayProfileService; + private readonly INotificationFactory _notificationFactory; + private readonly IRestrictionService _restrictionService; + private readonly IArtistService _artistService; - public TagService(ITagRepository repo, IEventAggregator eventAggregator) + public TagService(ITagRepository repo, + IEventAggregator eventAggregator, + IDelayProfileService delayProfileService, + INotificationFactory notificationFactory, + IRestrictionService restrictionService, + IArtistService artistService) { _repo = repo; _eventAggregator = eventAggregator; + _delayProfileService = delayProfileService; + _notificationFactory = notificationFactory; + _restrictionService = restrictionService; + _artistService = artistService; } public Tag GetTag(int tagId) @@ -42,6 +60,25 @@ namespace NzbDrone.Core.Tags } } + public TagDetails Details(int tagId) + { + var tag = GetTag(tagId); + var delayProfiles = _delayProfileService.AllForTag(tagId); + var notifications = _notificationFactory.AllForTag(tagId); + var restrictions = _restrictionService.AllForTag(tagId); + var artist = _artistService.AllForTag(tagId); + + return new TagDetails + { + Id = tagId, + Label = tag.Label, + DelayProfiles = delayProfiles, + Notifications = notifications, + Restrictions = restrictions, + Artist = artist + }; + } + public List<Tag> All() { return _repo.All().OrderBy(t => t.Label).ToList(); @@ -49,7 +86,12 @@ namespace NzbDrone.Core.Tags public Tag Add(Tag tag) { - //TODO: check for duplicate tag by label and return that tag instead? + var existingTag = _repo.FindByLabel(tag.Label); + + if (existingTag != null) + { + return existingTag; + } tag.Label = tag.Label.ToLowerInvariant(); diff --git a/src/NzbDrone.Core/ThingiProvider/Events/ProviderStatusChangedEvent.cs b/src/NzbDrone.Core/ThingiProvider/Events/ProviderStatusChangedEvent.cs new file mode 100644 index 000000000..8def1f0c7 --- /dev/null +++ b/src/NzbDrone.Core/ThingiProvider/Events/ProviderStatusChangedEvent.cs @@ -0,0 +1,18 @@ +using NzbDrone.Common.Messaging; +using NzbDrone.Core.ThingiProvider.Status; + +namespace NzbDrone.Core.ThingiProvider.Events +{ + public class ProviderStatusChangedEvent<TProvider> : IEvent + { + public int ProviderId { get; private set; } + + public ProviderStatusBase Status { get; private set; } + + public ProviderStatusChangedEvent(int id, ProviderStatusBase status) + { + ProviderId = id; + Status = status; + } + } +} diff --git a/src/NzbDrone.Core/ThingiProvider/IProviderFactory.cs b/src/NzbDrone.Core/ThingiProvider/IProviderFactory.cs index ce6519e1b..2627cec14 100644 --- a/src/NzbDrone.Core/ThingiProvider/IProviderFactory.cs +++ b/src/NzbDrone.Core/ThingiProvider/IProviderFactory.cs @@ -20,5 +20,6 @@ namespace NzbDrone.Core.ThingiProvider TProvider GetInstance(TProviderDefinition definition); ValidationResult Test(TProviderDefinition definition); object RequestAction(TProviderDefinition definition, string action, IDictionary<string, string> query); + List<TProviderDefinition> AllForTag(int tagId); } } \ No newline at end of file diff --git a/src/NzbDrone.Core/ThingiProvider/ProviderDefinition.cs b/src/NzbDrone.Core/ThingiProvider/ProviderDefinition.cs index 45bd5a25a..d83c7dfda 100644 --- a/src/NzbDrone.Core/ThingiProvider/ProviderDefinition.cs +++ b/src/NzbDrone.Core/ThingiProvider/ProviderDefinition.cs @@ -1,9 +1,15 @@ -using NzbDrone.Core.Datastore; +using System.Collections.Generic; +using NzbDrone.Core.Datastore; namespace NzbDrone.Core.ThingiProvider { public abstract class ProviderDefinition : ModelBase { + protected ProviderDefinition() + { + Tags = new HashSet<int>(); + } + private IProviderConfig _settings; public string Name { get; set; } @@ -12,6 +18,7 @@ namespace NzbDrone.Core.ThingiProvider public string ConfigContract { get; set; } public virtual bool Enable { get; set; } public ProviderMessage Message { get; set; } + public HashSet<int> Tags { get; set; } public IProviderConfig Settings { diff --git a/src/NzbDrone.Core/ThingiProvider/ProviderFactory.cs b/src/NzbDrone.Core/ThingiProvider/ProviderFactory.cs index 0c64aa994..750f0cf3d 100644 --- a/src/NzbDrone.Core/ThingiProvider/ProviderFactory.cs +++ b/src/NzbDrone.Core/ThingiProvider/ProviderFactory.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using FluentValidation.Results; @@ -76,7 +76,7 @@ namespace NzbDrone.Core.ThingiProvider return definitions; } - public ValidationResult Test(TProviderDefinition definition) + public virtual ValidationResult Test(TProviderDefinition definition) { return GetInstance(definition).Test(); } @@ -167,5 +167,11 @@ namespace NzbDrone.Core.ThingiProvider _providerRepository.Delete(invalidDefinition); } } + + public List<TProviderDefinition> AllForTag(int tagId) + { + return All().Where(p => p.Tags.Contains(tagId)) + .ToList(); + } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Core/ThingiProvider/Status/EscalationBackOff.cs b/src/NzbDrone.Core/ThingiProvider/Status/EscalationBackOff.cs new file mode 100644 index 000000000..304613d58 --- /dev/null +++ b/src/NzbDrone.Core/ThingiProvider/Status/EscalationBackOff.cs @@ -0,0 +1,18 @@ +namespace NzbDrone.Core.ThingiProvider.Status +{ + public static class EscalationBackOff + { + public static readonly int[] Periods = + { + 0, + 5 * 60, + 15 * 60, + 30 * 60, + 60 * 60, + 3 * 60 * 60, + 6 * 60 * 60, + 12 * 60 * 60, + 24 * 60 * 60 + }; + } +} diff --git a/src/NzbDrone.Core/ThingiProvider/Status/ProviderStatusBase.cs b/src/NzbDrone.Core/ThingiProvider/Status/ProviderStatusBase.cs new file mode 100644 index 000000000..395a43efd --- /dev/null +++ b/src/NzbDrone.Core/ThingiProvider/Status/ProviderStatusBase.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using NzbDrone.Core.Datastore; + +namespace NzbDrone.Core.ThingiProvider.Status +{ + public abstract class ProviderStatusBase : ModelBase + { + public int ProviderId { get; set; } + + public DateTime? InitialFailure { get; set; } + public DateTime? MostRecentFailure { get; set; } + public int EscalationLevel { get; set; } + public DateTime? DisabledTill { get; set; } + + public virtual bool IsDisabled() + { + return DisabledTill.HasValue && DisabledTill.Value > DateTime.UtcNow; + } + } +} diff --git a/src/NzbDrone.Core/ThingiProvider/Status/ProviderStatusRepository.cs b/src/NzbDrone.Core/ThingiProvider/Status/ProviderStatusRepository.cs new file mode 100644 index 000000000..c2782b409 --- /dev/null +++ b/src/NzbDrone.Core/ThingiProvider/Status/ProviderStatusRepository.cs @@ -0,0 +1,30 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using NzbDrone.Core.Datastore; +using NzbDrone.Core.Messaging.Events; + +namespace NzbDrone.Core.ThingiProvider.Status +{ + public interface IProviderStatusRepository<TModel> : IBasicRepository<TModel> + where TModel : ProviderStatusBase, new() + { + TModel FindByProviderId(int providerId); + } + + public class ProviderStatusRepository<TModel> : BasicRepository<TModel>, IProviderStatusRepository<TModel> + where TModel : ProviderStatusBase, new() + + { + public ProviderStatusRepository(IMainDatabase database, IEventAggregator eventAggregator) + : base(database, eventAggregator) + { + } + + public TModel FindByProviderId(int providerId) + { + return Query.Where(c => c.ProviderId == providerId).SingleOrDefault(); + } + } +} diff --git a/src/NzbDrone.Core/ThingiProvider/Status/ProviderStatusServiceBase.cs b/src/NzbDrone.Core/ThingiProvider/Status/ProviderStatusServiceBase.cs new file mode 100644 index 000000000..58089c007 --- /dev/null +++ b/src/NzbDrone.Core/ThingiProvider/Status/ProviderStatusServiceBase.cs @@ -0,0 +1,145 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using NLog; +using NzbDrone.Core.Messaging.Events; +using NzbDrone.Core.ThingiProvider.Events; + +namespace NzbDrone.Core.ThingiProvider.Status +{ + public interface IProviderStatusServiceBase<TModel> + where TModel : ProviderStatusBase, new() + { + bool IsDisabled(int providerId); + List<TModel> GetBlockedProviders(); + void RecordSuccess(int providerId); + void RecordFailure(int providerId, TimeSpan minimumBackOff = default(TimeSpan)); + void RecordConnectionFailure(int providerId); + } + + public abstract class ProviderStatusServiceBase<TProvider, TModel> : IProviderStatusServiceBase<TModel>, IHandleAsync<ProviderDeletedEvent<TProvider>> + where TProvider : IProvider + where TModel : ProviderStatusBase, new() + { + + protected readonly object _syncRoot = new object(); + + protected readonly IProviderStatusRepository<TModel> _providerStatusRepository; + protected readonly IEventAggregator _eventAggregator; + protected readonly Logger _logger; + + protected int MaximumEscalationLevel { get; set; } = EscalationBackOff.Periods.Length - 1; + protected TimeSpan MinimumTimeSinceInitialFailure { get; set; } = TimeSpan.Zero; + + public ProviderStatusServiceBase(IProviderStatusRepository<TModel> providerStatusRepository, IEventAggregator eventAggregator, Logger logger) + { + _providerStatusRepository = providerStatusRepository; + _eventAggregator = eventAggregator; + _logger = logger; + } + + public bool IsDisabled(int providerId) + { + return GetProviderStatus(providerId).IsDisabled(); + } + + public virtual List<TModel> GetBlockedProviders() + { + return _providerStatusRepository.All().Where(v => v.IsDisabled()).ToList(); + } + + protected virtual TModel GetProviderStatus(int providerId) + { + return _providerStatusRepository.FindByProviderId(providerId) ?? new TModel { ProviderId = providerId }; + } + + protected virtual TimeSpan CalculateBackOffPeriod(TModel status) + { + var level = Math.Min(MaximumEscalationLevel, status.EscalationLevel); + + return TimeSpan.FromSeconds(EscalationBackOff.Periods[level]); + } + + public virtual void RecordSuccess(int providerId) + { + lock (_syncRoot) + { + var status = GetProviderStatus(providerId); + + if (status.EscalationLevel == 0) + { + return; + } + + status.EscalationLevel--; + status.DisabledTill = null; + + _providerStatusRepository.Upsert(status); + + _eventAggregator.PublishEvent(new ProviderStatusChangedEvent<TProvider>(providerId, status)); + } + } + + protected virtual void RecordFailure(int providerId, TimeSpan minimumBackOff, bool escalate) + { + lock (_syncRoot) + { + var status = GetProviderStatus(providerId); + + var now = DateTime.UtcNow; + status.MostRecentFailure = now; + + if (status.EscalationLevel == 0) + { + status.InitialFailure = now; + status.EscalationLevel = 1; + escalate = false; + } + + var inGracePeriod = (status.InitialFailure.Value + MinimumTimeSinceInitialFailure) > now; + + if (escalate && !inGracePeriod) + { + status.EscalationLevel = Math.Min(MaximumEscalationLevel, status.EscalationLevel + 1); + } + + if (minimumBackOff != TimeSpan.Zero) + { + while (status.EscalationLevel < MaximumEscalationLevel && CalculateBackOffPeriod(status) < minimumBackOff) + { + status.EscalationLevel++; + } + } + + if (!inGracePeriod || minimumBackOff != TimeSpan.Zero) + { + status.DisabledTill = now + CalculateBackOffPeriod(status); + } + + _providerStatusRepository.Upsert(status); + + _eventAggregator.PublishEvent(new ProviderStatusChangedEvent<TProvider>(providerId, status)); + } + } + + public virtual void RecordFailure(int providerId, TimeSpan minimumBackOff = default(TimeSpan)) + { + RecordFailure(providerId, minimumBackOff, true); + } + + public virtual void RecordConnectionFailure(int providerId) + { + RecordFailure(providerId, default(TimeSpan), false); + } + + public virtual void HandleAsync(ProviderDeletedEvent<TProvider> message) + { + var providerStatus = _providerStatusRepository.FindByProviderId(message.ProviderId); + + if (providerStatus != null) + { + _providerStatusRepository.Delete(providerStatus); + } + } + } +} diff --git a/src/NzbDrone.Core/Tv/Actor.cs b/src/NzbDrone.Core/Tv/Actor.cs deleted file mode 100644 index cfc8a0bbd..000000000 --- a/src/NzbDrone.Core/Tv/Actor.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System.Collections.Generic; -using NzbDrone.Core.Datastore; - -namespace NzbDrone.Core.Tv -{ - public class Actor : IEmbeddedDocument - { - public Actor() - { - Images = new List<MediaCover.MediaCover>(); - } - - public string Name { get; set; } - public string Character { get; set; } - public List<MediaCover.MediaCover> Images { get; set; } - } -} diff --git a/src/NzbDrone.Core/Tv/AddSeriesOptions.cs b/src/NzbDrone.Core/Tv/AddSeriesOptions.cs deleted file mode 100644 index d325076d8..000000000 --- a/src/NzbDrone.Core/Tv/AddSeriesOptions.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace NzbDrone.Core.Tv -{ - public class AddSeriesOptions : MonitoringOptions - { - public bool SearchForMissingEpisodes { get; set; } - - } -} diff --git a/src/NzbDrone.Core/Tv/AddSeriesService.cs b/src/NzbDrone.Core/Tv/AddSeriesService.cs deleted file mode 100644 index 45d70e734..000000000 --- a/src/NzbDrone.Core/Tv/AddSeriesService.cs +++ /dev/null @@ -1,99 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using FluentValidation; -using FluentValidation.Results; -using NLog; -using NzbDrone.Common.EnsureThat; -using NzbDrone.Core.Exceptions; -using NzbDrone.Core.MetadataSource; -using NzbDrone.Core.Organizer; -using NzbDrone.Core.Parser; - -namespace NzbDrone.Core.Tv -{ - public interface IAddSeriesService - { - Series AddSeries(Series newSeries); - } - - public class AddSeriesService : IAddSeriesService - { - private readonly ISeriesService _seriesService; - private readonly IProvideSeriesInfo _seriesInfo; - private readonly IBuildFileNames _fileNameBuilder; - private readonly IAddSeriesValidator _addSeriesValidator; - private readonly Logger _logger; - - public AddSeriesService(ISeriesService seriesService, - IProvideSeriesInfo seriesInfo, - IBuildFileNames fileNameBuilder, - IAddSeriesValidator addSeriesValidator, - Logger logger) - { - _seriesService = seriesService; - _seriesInfo = seriesInfo; - _fileNameBuilder = fileNameBuilder; - _addSeriesValidator = addSeriesValidator; - _logger = logger; - } - - public Series AddSeries(Series newSeries) - { - Ensure.That(newSeries, () => newSeries).IsNotNull(); - - newSeries = AddSkyhookData(newSeries); - - if (string.IsNullOrWhiteSpace(newSeries.Path)) - { - //var folderName = _fileNameBuilder.GetSeriesFolder(newSeries); - //newSeries.Path = Path.Combine(newSeries.RootFolderPath, folderName); - } - - newSeries.CleanTitle = newSeries.Title.CleanSeriesTitle(); - newSeries.SortTitle = SeriesTitleNormalizer.Normalize(newSeries.Title, newSeries.TvdbId); - newSeries.Added = DateTime.UtcNow; - - var validationResult = _addSeriesValidator.Validate(newSeries); - - if (!validationResult.IsValid) - { - throw new ValidationException(validationResult.Errors); - } - - _logger.Info("Adding Series {0} Path: [{1}]", newSeries, newSeries.Path); - _seriesService.AddSeries(newSeries); - - return newSeries; - } - - private Series AddSkyhookData(Series newSeries) - { - Tuple<Series, List<Episode>> tuple; - - try - { - tuple = _seriesInfo.GetSeriesInfo(newSeries.TvdbId); - } - catch (SeriesNotFoundException) - { - _logger.Error("tvdbid {1} was not found, it may have been removed from TheTVDB.", newSeries.TvdbId); - - throw new ValidationException(new List<ValidationFailure> - { - new ValidationFailure("TvdbId", "A series with this ID was not found", newSeries.TvdbId) - }); - } - - var series = tuple.Item1; - - // If seasons were passed in on the new series use them, otherwise use the seasons from Skyhook - newSeries.Seasons = newSeries.Seasons != null && newSeries.Seasons.Any() ? newSeries.Seasons : series.Seasons; - - series.ApplyChanges(newSeries); - - return series; - } - } -} diff --git a/src/NzbDrone.Core/Tv/AddSeriesValidator.cs b/src/NzbDrone.Core/Tv/AddSeriesValidator.cs deleted file mode 100644 index 418815be6..000000000 --- a/src/NzbDrone.Core/Tv/AddSeriesValidator.cs +++ /dev/null @@ -1,30 +0,0 @@ -using FluentValidation; -using FluentValidation.Results; -using NzbDrone.Core.Validation.Paths; - -namespace NzbDrone.Core.Tv -{ - public interface IAddSeriesValidator - { - ValidationResult Validate(Series instance); - } - - public class AddSeriesValidator : AbstractValidator<Series>, IAddSeriesValidator - { - public AddSeriesValidator(RootFolderValidator rootFolderValidator, - SeriesPathValidator seriesPathValidator, - DroneFactoryValidator droneFactoryValidator, - SeriesAncestorValidator seriesAncestorValidator, - SeriesTitleSlugValidator seriesTitleSlugValidator) - { - RuleFor(c => c.Path).Cascade(CascadeMode.StopOnFirstFailure) - .IsValidPath() - .SetValidator(rootFolderValidator) - .SetValidator(seriesPathValidator) - .SetValidator(droneFactoryValidator) - .SetValidator(seriesAncestorValidator); - - RuleFor(c => c.TitleSlug).SetValidator(seriesTitleSlugValidator); - } - } -} diff --git a/src/NzbDrone.Core/Tv/Commands/RefreshSeriesCommand.cs b/src/NzbDrone.Core/Tv/Commands/RefreshSeriesCommand.cs deleted file mode 100644 index 4cae630cd..000000000 --- a/src/NzbDrone.Core/Tv/Commands/RefreshSeriesCommand.cs +++ /dev/null @@ -1,22 +0,0 @@ -using NzbDrone.Core.Messaging.Commands; - -namespace NzbDrone.Core.Tv.Commands -{ - public class RefreshSeriesCommand : Command - { - public int? SeriesId { get; set; } - - public RefreshSeriesCommand() - { - } - - public RefreshSeriesCommand(int? seriesId) - { - SeriesId = seriesId; - } - - public override bool SendUpdatesToClient => true; - - public override bool UpdateScheduledTask => !SeriesId.HasValue; - } -} \ No newline at end of file diff --git a/src/NzbDrone.Core/Tv/Episode.cs b/src/NzbDrone.Core/Tv/Episode.cs deleted file mode 100644 index dcb95069e..000000000 --- a/src/NzbDrone.Core/Tv/Episode.cs +++ /dev/null @@ -1,49 +0,0 @@ -using System; -using System.Collections.Generic; -using Marr.Data; -using NzbDrone.Common.Extensions; -using NzbDrone.Core.Datastore; -using NzbDrone.Core.MediaFiles; - -namespace NzbDrone.Core.Tv -{ - public class Episode : ModelBase - { - public Episode() - { - Images = new List<MediaCover.MediaCover>(); - } - - public const string AIR_DATE_FORMAT = "yyyy-MM-dd"; - - public int SeriesId { get; set; } - public int EpisodeFileId { get; set; } - public int SeasonNumber { get; set; } - public int EpisodeNumber { get; set; } - public string Title { get; set; } - public string AirDate { get; set; } - public DateTime? AirDateUtc { get; set; } - public string Overview { get; set; } - public bool Monitored { get; set; } - public int? AbsoluteEpisodeNumber { get; set; } - public int? SceneAbsoluteEpisodeNumber { get; set; } - public int? SceneSeasonNumber { get; set; } - public int? SceneEpisodeNumber { get; set; } - public bool UnverifiedSceneNumbering { get; set; } - public Ratings Ratings { get; set; } - public List<MediaCover.MediaCover> Images { get; set; } - - public string SeriesTitle { get; private set; } - - public LazyLoaded<EpisodeFile> EpisodeFile { get; set; } - - public Series Series { get; set; } - - public bool HasFile => EpisodeFileId > 0; - - public override string ToString() - { - return string.Format("[{0}]{1}", Id, Title.NullSafe()); - } - } -} \ No newline at end of file diff --git a/src/NzbDrone.Core/Tv/EpisodeAddedService.cs b/src/NzbDrone.Core/Tv/EpisodeAddedService.cs deleted file mode 100644 index 1eba25720..000000000 --- a/src/NzbDrone.Core/Tv/EpisodeAddedService.cs +++ /dev/null @@ -1,88 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using NLog; -using NzbDrone.Common.Cache; -using NzbDrone.Common.Extensions; -using NzbDrone.Core.IndexerSearch; -using NzbDrone.Core.Messaging.Commands; -using NzbDrone.Core.Messaging.Events; -using NzbDrone.Core.Tv.Events; - -namespace NzbDrone.Core.Tv -{ - public interface IEpisodeAddedService - { - void SearchForRecentlyAdded(int seriesId); - } - - public class EpisodeAddedService : IHandle<EpisodeInfoRefreshedEvent>, IEpisodeAddedService - { - private readonly IManageCommandQueue _commandQueueManager; - private readonly IEpisodeService _episodeService; - private readonly Logger _logger; - private readonly ICached<List<int>> _addedEpisodesCache; - - public EpisodeAddedService(ICacheManager cacheManager, - IManageCommandQueue commandQueueManager, - IEpisodeService episodeService, - Logger logger) - { - _commandQueueManager = commandQueueManager; - _episodeService = episodeService; - _logger = logger; - _addedEpisodesCache = cacheManager.GetCache<List<int>>(GetType()); - } - - public void SearchForRecentlyAdded(int seriesId) - { - var previouslyAired = _addedEpisodesCache.Find(seriesId.ToString()); - - if (previouslyAired != null && previouslyAired.Any()) - { - var missing = previouslyAired.Select(e => _episodeService.GetEpisode(e)).Where(e => !e.HasFile).ToList(); - - if (missing.Any()) - { - //_commandQueueManager.Push(new EpisodeSearchCommand(missing.Select(e => e.Id).ToList())); - } - } - - _addedEpisodesCache.Remove(seriesId.ToString()); - } - - public void Handle(EpisodeInfoRefreshedEvent message) - { - if (message.Series.AddOptions == null) - { - if (!message.Series.Monitored) - { - _logger.Debug("Series is not monitored"); - return; - } - - if (message.Added.Empty()) - { - _logger.Debug("No new episodes, skipping search"); - return; - } - - if (message.Added.None(a => a.AirDateUtc.HasValue)) - { - _logger.Debug("No new episodes have an air date"); - return; - } - - var previouslyAired = message.Added.Where(a => a.AirDateUtc.HasValue && a.AirDateUtc.Value.Before(DateTime.UtcNow.AddDays(1)) && a.Monitored).ToList(); - - if (previouslyAired.Empty()) - { - _logger.Debug("Newly added episodes all air in the future"); - return; - } - - _addedEpisodesCache.Set(message.Series.Id.ToString(), previouslyAired.Select(e => e.Id).ToList()); - } - } - } -} diff --git a/src/NzbDrone.Core/Tv/EpisodeCutoffService.cs b/src/NzbDrone.Core/Tv/EpisodeCutoffService.cs deleted file mode 100644 index 6747aa87e..000000000 --- a/src/NzbDrone.Core/Tv/EpisodeCutoffService.cs +++ /dev/null @@ -1,48 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using NLog; -using NzbDrone.Core.Datastore; -using NzbDrone.Core.Profiles; -using NzbDrone.Core.Qualities; - -namespace NzbDrone.Core.Tv -{ - public interface IEpisodeCutoffService - { - PagingSpec<Episode> EpisodesWhereCutoffUnmet(PagingSpec<Episode> pagingSpec); - } - - public class EpisodeCutoffService : IEpisodeCutoffService - { - private readonly IEpisodeRepository _episodeRepository; - private readonly IProfileService _profileService; - private readonly Logger _logger; - - public EpisodeCutoffService(IEpisodeRepository episodeRepository, IProfileService profileService, Logger logger) - { - _episodeRepository = episodeRepository; - _profileService = profileService; - _logger = logger; - } - - public PagingSpec<Episode> EpisodesWhereCutoffUnmet(PagingSpec<Episode> pagingSpec) - { - var qualitiesBelowCutoff = new List<QualitiesBelowCutoff>(); - var profiles = _profileService.All(); - - //Get all items less than the cutoff - foreach (var profile in profiles) - { - var cutoffIndex = profile.Items.FindIndex(v => v.Quality == profile.Cutoff); - var belowCutoff = profile.Items.Take(cutoffIndex).ToList(); - - if (belowCutoff.Any()) - { - qualitiesBelowCutoff.Add(new QualitiesBelowCutoff(profile.Id, belowCutoff.Select(i => i.Quality.Id))); - } - } - - return _episodeRepository.EpisodesWhereCutoffUnmet(pagingSpec, qualitiesBelowCutoff, false); - } - } -} diff --git a/src/NzbDrone.Core/Tv/EpisodeMonitoredService.cs b/src/NzbDrone.Core/Tv/EpisodeMonitoredService.cs deleted file mode 100644 index b15c130be..000000000 --- a/src/NzbDrone.Core/Tv/EpisodeMonitoredService.cs +++ /dev/null @@ -1,109 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using NLog; -using NzbDrone.Common.Extensions; - -namespace NzbDrone.Core.Tv -{ - public interface IEpisodeMonitoredService - { - void SetEpisodeMonitoredStatus(Series series, MonitoringOptions monitoringOptions); - } - - public class EpisodeMonitoredService : IEpisodeMonitoredService - { - private readonly ISeriesService _seriesService; - private readonly IEpisodeService _episodeService; - private readonly Logger _logger; - - public EpisodeMonitoredService(ISeriesService seriesService, IEpisodeService episodeService, Logger logger) - { - _seriesService = seriesService; - _episodeService = episodeService; - _logger = logger; - } - - public void SetEpisodeMonitoredStatus(Series series, MonitoringOptions monitoringOptions) - { - if (monitoringOptions != null) - { - _logger.Debug("[{0}] Setting episode monitored status.", series.Title); - - var episodes = _episodeService.GetEpisodeBySeries(series.Id); - - if (monitoringOptions.IgnoreEpisodesWithFiles) - { - _logger.Debug("Ignoring Episodes with Files"); - ToggleEpisodesMonitoredState(episodes.Where(e => e.HasFile), false); - } - - else - { - _logger.Debug("Monitoring Episodes with Files"); - ToggleEpisodesMonitoredState(episodes.Where(e => e.HasFile), true); - } - - if (monitoringOptions.IgnoreEpisodesWithoutFiles) - { - _logger.Debug("Ignoring Episodes without Files"); - ToggleEpisodesMonitoredState(episodes.Where(e => !e.HasFile && e.AirDateUtc.HasValue && e.AirDateUtc.Value.Before(DateTime.UtcNow)), false); - } - - else - { - _logger.Debug("Monitoring Episodes without Files"); - ToggleEpisodesMonitoredState(episodes.Where(e => !e.HasFile && e.AirDateUtc.HasValue && e.AirDateUtc.Value.Before(DateTime.UtcNow)), true); - } - - var lastSeason = series.Seasons.Select(s => s.SeasonNumber).MaxOrDefault(); - - foreach (var s in series.Seasons) - { - var season = s; - - if (season.Monitored) - { - if (!monitoringOptions.IgnoreEpisodesWithFiles && !monitoringOptions.IgnoreEpisodesWithoutFiles) - { - ToggleEpisodesMonitoredState(episodes.Where(e => e.SeasonNumber == season.SeasonNumber), true); - } - } - - else - { - if (!monitoringOptions.IgnoreEpisodesWithFiles && !monitoringOptions.IgnoreEpisodesWithoutFiles) - { - ToggleEpisodesMonitoredState(episodes.Where(e => e.SeasonNumber == season.SeasonNumber), false); - } - - else if (season.SeasonNumber == 0) - { - ToggleEpisodesMonitoredState(episodes.Where(e => e.SeasonNumber == season.SeasonNumber), false); - } - } - - if (season.SeasonNumber < lastSeason) - { - if (episodes.Where(e => e.SeasonNumber == season.SeasonNumber).All(e => !e.Monitored)) - { - season.Monitored = false; - } - } - } - - _episodeService.UpdateEpisodes(episodes); - } - - _seriesService.UpdateSeries(series); - } - - private void ToggleEpisodesMonitoredState(IEnumerable<Episode> episodes, bool monitored) - { - foreach (var episode in episodes) - { - episode.Monitored = monitored; - } - } - } -} diff --git a/src/NzbDrone.Core/Tv/EpisodeRepository.cs b/src/NzbDrone.Core/Tv/EpisodeRepository.cs deleted file mode 100644 index 5a1f413ad..000000000 --- a/src/NzbDrone.Core/Tv/EpisodeRepository.cs +++ /dev/null @@ -1,266 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Marr.Data.QGen; -using NLog; -using NzbDrone.Common.Extensions; -using NzbDrone.Core.Datastore; -using NzbDrone.Core.Datastore.Extensions; -using NzbDrone.Core.Messaging.Events; -using NzbDrone.Core.MediaFiles; -using NzbDrone.Core.Qualities; - -namespace NzbDrone.Core.Tv -{ - public interface IEpisodeRepository : IBasicRepository<Episode> - { - Episode Find(int seriesId, int season, int episodeNumber); - Episode Find(int seriesId, int absoluteEpisodeNumber); - Episode Get(int seriesId, string date); - Episode Find(int seriesId, string date); - List<Episode> GetEpisodes(int seriesId); - List<Episode> GetEpisodes(int seriesId, int seasonNumber); - List<Episode> GetEpisodeByFileId(int fileId); - List<Episode> EpisodesWithFiles(int seriesId); - PagingSpec<Episode> EpisodesWithoutFiles(PagingSpec<Episode> pagingSpec, bool includeSpecials); - PagingSpec<Episode> EpisodesWhereCutoffUnmet(PagingSpec<Episode> pagingSpec, List<QualitiesBelowCutoff> qualitiesBelowCutoff, bool includeSpecials); - List<Episode> FindEpisodesBySceneNumbering(int seriesId, int seasonNumber, int episodeNumber); - Episode FindEpisodeBySceneNumbering(int seriesId, int sceneAbsoluteEpisodeNumber); - List<Episode> EpisodesBetweenDates(DateTime startDate, DateTime endDate, bool includeUnmonitored); - void SetMonitoredFlat(Episode episode, bool monitored); - void SetMonitoredBySeason(int seriesId, int seasonNumber, bool monitored); - void SetFileId(int episodeId, int fileId); - } - - public class EpisodeRepository : BasicRepository<Episode>, IEpisodeRepository - { - private readonly IMainDatabase _database; - private readonly Logger _logger; - - public EpisodeRepository(IMainDatabase database, IEventAggregator eventAggregator, Logger logger) - : base(database, eventAggregator) - { - _database = database; - _logger = logger; - } - - public Episode Find(int seriesId, int season, int episodeNumber) - { - return Query.Where(s => s.SeriesId == seriesId) - .AndWhere(s => s.SeasonNumber == season) - .AndWhere(s => s.EpisodeNumber == episodeNumber) - .SingleOrDefault(); - } - - public Episode Find(int seriesId, int absoluteEpisodeNumber) - { - return Query.Where(s => s.SeriesId == seriesId) - .AndWhere(s => s.AbsoluteEpisodeNumber == absoluteEpisodeNumber) - .SingleOrDefault(); - } - - public Episode Get(int seriesId, string date) - { - var episode = FindOneByAirDate(seriesId, date); - - if (episode == null) - { - throw new InvalidOperationException("Expected at one episode"); - } - - return episode; - } - - public Episode Find(int seriesId, string date) - { - return FindOneByAirDate(seriesId, date); - } - - public List<Episode> GetEpisodes(int seriesId) - { - return Query.Where(s => s.SeriesId == seriesId).ToList(); - } - - public List<Episode> GetEpisodes(int seriesId, int seasonNumber) - { - return Query.Where(s => s.SeriesId == seriesId) - .AndWhere(s => s.SeasonNumber == seasonNumber) - .ToList(); - } - - public List<Episode> GetEpisodeByFileId(int fileId) - { - return Query.Where(e => e.EpisodeFileId == fileId).ToList(); - } - - public List<Episode> EpisodesWithFiles(int seriesId) - { - return Query.Join<Episode, EpisodeFile>(JoinType.Inner, e => e.EpisodeFile, (e, ef) => e.EpisodeFileId == ef.Id) - .Where(e => e.SeriesId == seriesId); - } - - public PagingSpec<Episode> EpisodesWithoutFiles(PagingSpec<Episode> pagingSpec, bool includeSpecials) - { - var currentTime = DateTime.UtcNow; - var startingSeasonNumber = 1; - - if (includeSpecials) - { - startingSeasonNumber = 0; - } - - pagingSpec.TotalRecords = GetMissingEpisodesQuery(pagingSpec, currentTime, startingSeasonNumber).GetRowCount(); - pagingSpec.Records = GetMissingEpisodesQuery(pagingSpec, currentTime, startingSeasonNumber).ToList(); - - return pagingSpec; - } - - public PagingSpec<Episode> EpisodesWhereCutoffUnmet(PagingSpec<Episode> pagingSpec, List<QualitiesBelowCutoff> qualitiesBelowCutoff, bool includeSpecials) - { - var startingSeasonNumber = 1; - - if (includeSpecials) - { - startingSeasonNumber = 0; - } - - pagingSpec.TotalRecords = EpisodesWhereCutoffUnmetQuery(pagingSpec, qualitiesBelowCutoff, startingSeasonNumber).GetRowCount(); - pagingSpec.Records = EpisodesWhereCutoffUnmetQuery(pagingSpec, qualitiesBelowCutoff, startingSeasonNumber).ToList(); - - return pagingSpec; - } - - public List<Episode> FindEpisodesBySceneNumbering(int seriesId, int seasonNumber, int episodeNumber) - { - return Query.Where(s => s.SeriesId == seriesId) - .AndWhere(s => s.SceneSeasonNumber == seasonNumber) - .AndWhere(s => s.SceneEpisodeNumber == episodeNumber); - } - - public Episode FindEpisodeBySceneNumbering(int seriesId, int sceneAbsoluteEpisodeNumber) - { - var episodes = Query.Where(s => s.SeriesId == seriesId) - .AndWhere(s => s.SceneAbsoluteEpisodeNumber == sceneAbsoluteEpisodeNumber) - .ToList(); - - if (episodes.Empty() || episodes.Count > 1) - { - return null; - } - - return episodes.Single(); - } - - public List<Episode> EpisodesBetweenDates(DateTime startDate, DateTime endDate, bool includeUnmonitored) - { - var query = Query.Join<Episode, Series>(JoinType.Inner, e => e.Series, (e, s) => e.SeriesId == s.Id) - .Where<Episode>(e => e.AirDateUtc >= startDate) - .AndWhere(e => e.AirDateUtc <= endDate); - - - if (!includeUnmonitored) - { - query.AndWhere(e => e.Monitored) - .AndWhere(e => e.Series.Monitored); - } - - return query.ToList(); - } - - public void SetMonitoredFlat(Episode episode, bool monitored) - { - episode.Monitored = monitored; - SetFields(episode, p => p.Monitored); - } - - public void SetMonitoredBySeason(int seriesId, int seasonNumber, bool monitored) - { - var mapper = _database.GetDataMapper(); - - mapper.AddParameter("seriesId", seriesId); - mapper.AddParameter("seasonNumber", seasonNumber); - mapper.AddParameter("monitored", monitored); - - const string sql = "UPDATE Episodes " + - "SET Monitored = @monitored " + - "WHERE SeriesId = @seriesId " + - "AND SeasonNumber = @seasonNumber"; - - mapper.ExecuteNonQuery(sql); - } - - public void SetFileId(int episodeId, int fileId) - { - SetFields(new Episode { Id = episodeId, EpisodeFileId = fileId }, episode => episode.EpisodeFileId); - } - - private SortBuilder<Episode> GetMissingEpisodesQuery(PagingSpec<Episode> pagingSpec, DateTime currentTime, int startingSeasonNumber) - { - return Query.Join<Episode, Series>(JoinType.Inner, e => e.Series, (e, s) => e.SeriesId == s.Id) - .Where(pagingSpec.FilterExpression) - .AndWhere(e => e.EpisodeFileId == 0) - .AndWhere(e => e.SeasonNumber >= startingSeasonNumber) - .AndWhere(BuildAirDateUtcCutoffWhereClause(currentTime)) - .OrderBy(pagingSpec.OrderByClause(), pagingSpec.ToSortDirection()) - .Skip(pagingSpec.PagingOffset()) - .Take(pagingSpec.PageSize); - } - - private SortBuilder<Episode> EpisodesWhereCutoffUnmetQuery(PagingSpec<Episode> pagingSpec, List<QualitiesBelowCutoff> qualitiesBelowCutoff, int startingSeasonNumber) - { - return Query.Join<Episode, Series>(JoinType.Inner, e => e.Series, (e, s) => e.SeriesId == s.Id) - .Join<Episode, EpisodeFile>(JoinType.Left, e => e.EpisodeFile, (e, s) => e.EpisodeFileId == s.Id) - .Where(pagingSpec.FilterExpression) - .AndWhere(e => e.EpisodeFileId != 0) - .AndWhere(e => e.SeasonNumber >= startingSeasonNumber) - .AndWhere(BuildQualityCutoffWhereClause(qualitiesBelowCutoff)) - .OrderBy(pagingSpec.OrderByClause(), pagingSpec.ToSortDirection()) - .Skip(pagingSpec.PagingOffset()) - .Take(pagingSpec.PageSize); - } - - private string BuildAirDateUtcCutoffWhereClause(DateTime currentTime) - { - return string.Format("WHERE datetime(strftime('%s', [t0].[AirDateUtc]) + [t1].[RunTime] * 60, 'unixepoch') <= '{0}'", - currentTime.ToString("yyyy-MM-dd HH:mm:ss")); - } - - private string BuildQualityCutoffWhereClause(List<QualitiesBelowCutoff> qualitiesBelowCutoff) - { - var clauses = new List<string>(); - - foreach (var profile in qualitiesBelowCutoff) - { - foreach (var belowCutoff in profile.QualityIds) - { - clauses.Add(string.Format("([t1].[ProfileId] = {0} AND [t2].[Quality] LIKE '%_quality_: {1},%')", profile.ProfileId, belowCutoff)); - } - } - - return string.Format("({0})", string.Join(" OR ", clauses)); - } - - private Episode FindOneByAirDate(int seriesId, string date) - { - var episodes = Query.Where(s => s.SeriesId == seriesId) - .AndWhere(s => s.AirDate == date) - .ToList(); - - if (!episodes.Any()) return null; - - if (episodes.Count == 1) return episodes.First(); - - _logger.Debug("Multiple episodes with the same air date were found, will exclude specials"); - - var regularEpisodes = episodes.Where(e => e.SeasonNumber > 0).ToList(); - - if (regularEpisodes.Count == 1) - { - _logger.Debug("Left with one episode after excluding specials"); - return regularEpisodes.First(); - } - - throw new InvalidOperationException("Multiple episodes with the same air date found"); - } - } -} diff --git a/src/NzbDrone.Core/Tv/EpisodeService.cs b/src/NzbDrone.Core/Tv/EpisodeService.cs deleted file mode 100644 index 5a117ab11..000000000 --- a/src/NzbDrone.Core/Tv/EpisodeService.cs +++ /dev/null @@ -1,225 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using NLog; -using NzbDrone.Core.Configuration; -using NzbDrone.Core.Datastore; -using NzbDrone.Core.MediaFiles; -using NzbDrone.Core.MediaFiles.Events; -using NzbDrone.Core.Messaging.Events; -using NzbDrone.Core.Tv.Events; - -namespace NzbDrone.Core.Tv -{ - public interface IEpisodeService - { - Episode GetEpisode(int id); - List<Episode> GetEpisodes(IEnumerable<int> ids); - Episode FindEpisode(int seriesId, int seasonNumber, int episodeNumber); - Episode FindEpisode(int seriesId, int absoluteEpisodeNumber); - Episode FindEpisodeByTitle(int seriesId, int seasonNumber, string releaseTitle); - List<Episode> FindEpisodesBySceneNumbering(int seriesId, int seasonNumber, int episodeNumber); - Episode FindEpisodeBySceneNumbering(int seriesId, int sceneAbsoluteEpisodeNumber); - Episode GetEpisode(int seriesId, string date); - Episode FindEpisode(int seriesId, string date); - List<Episode> GetEpisodeBySeries(int seriesId); - List<Episode> GetEpisodesBySeason(int seriesId, int seasonNumber); - List<Episode> EpisodesWithFiles(int seriesId); - PagingSpec<Episode> EpisodesWithoutFiles(PagingSpec<Episode> pagingSpec); - List<Episode> GetEpisodesByFileId(int episodeFileId); - void UpdateEpisode(Episode episode); - void SetEpisodeMonitored(int episodeId, bool monitored); - void UpdateEpisodes(List<Episode> episodes); - List<Episode> EpisodesBetweenDates(DateTime start, DateTime end, bool includeUnmonitored); - void InsertMany(List<Episode> episodes); - void UpdateMany(List<Episode> episodes); - void DeleteMany(List<Episode> episodes); - void SetEpisodeMonitoredBySeason(int seriesId, int seasonNumber, bool monitored); - } - - public class EpisodeService : IEpisodeService, - IHandle<EpisodeFileDeletedEvent>, - IHandle<EpisodeFileAddedEvent>, - IHandleAsync<SeriesDeletedEvent> - { - private readonly IEpisodeRepository _episodeRepository; - private readonly IConfigService _configService; - private readonly Logger _logger; - - public EpisodeService(IEpisodeRepository episodeRepository, IConfigService configService, Logger logger) - { - _episodeRepository = episodeRepository; - _configService = configService; - _logger = logger; - } - - public Episode GetEpisode(int id) - { - return _episodeRepository.Get(id); - } - - public List<Episode> GetEpisodes(IEnumerable<int> ids) - { - return _episodeRepository.Get(ids).ToList(); - } - - public Episode FindEpisode(int seriesId, int seasonNumber, int episodeNumber) - { - return _episodeRepository.Find(seriesId, seasonNumber, episodeNumber); - } - - public Episode FindEpisode(int seriesId, int absoluteEpisodeNumber) - { - return _episodeRepository.Find(seriesId, absoluteEpisodeNumber); - } - - public List<Episode> FindEpisodesBySceneNumbering(int seriesId, int seasonNumber, int episodeNumber) - { - return _episodeRepository.FindEpisodesBySceneNumbering(seriesId, seasonNumber, episodeNumber); - } - - public Episode FindEpisodeBySceneNumbering(int seriesId, int sceneAbsoluteEpisodeNumber) - { - return _episodeRepository.FindEpisodeBySceneNumbering(seriesId, sceneAbsoluteEpisodeNumber); - } - - public Episode GetEpisode(int seriesId, string date) - { - return _episodeRepository.Get(seriesId, date); - } - - public Episode FindEpisode(int seriesId, string date) - { - return _episodeRepository.Find(seriesId, date); - } - - public List<Episode> GetEpisodeBySeries(int seriesId) - { - return _episodeRepository.GetEpisodes(seriesId).ToList(); - } - - public List<Episode> GetEpisodesBySeason(int seriesId, int seasonNumber) - { - return _episodeRepository.GetEpisodes(seriesId, seasonNumber); - } - - public Episode FindEpisodeByTitle(int seriesId, int seasonNumber, string releaseTitle) - { - // TODO: can replace this search mechanism with something smarter/faster/better - var normalizedReleaseTitle = Parser.Parser.NormalizeEpisodeTitle(releaseTitle).Replace(".", " "); - var episodes = _episodeRepository.GetEpisodes(seriesId, seasonNumber); - - var matches = episodes.Select( - episode => new - { - Position = normalizedReleaseTitle.IndexOf(Parser.Parser.NormalizeEpisodeTitle(episode.Title), StringComparison.CurrentCultureIgnoreCase), - Length = Parser.Parser.NormalizeEpisodeTitle(episode.Title).Length, - Episode = episode - }) - .Where(e => e.Episode.Title.Length > 0 && e.Position >= 0) - .OrderBy(e => e.Position) - .ThenByDescending(e => e.Length) - .ToList(); - - if (matches.Any()) - { - return matches.First().Episode; - } - - return null; - } - - public List<Episode> EpisodesWithFiles(int seriesId) - { - return _episodeRepository.EpisodesWithFiles(seriesId); - } - - public PagingSpec<Episode> EpisodesWithoutFiles(PagingSpec<Episode> pagingSpec) - { - var episodeResult = _episodeRepository.EpisodesWithoutFiles(pagingSpec, true); - - return episodeResult; - } - - public List<Episode> GetEpisodesByFileId(int episodeFileId) - { - return _episodeRepository.GetEpisodeByFileId(episodeFileId); - } - - public void UpdateEpisode(Episode episode) - { - _episodeRepository.Update(episode); - } - - public void SetEpisodeMonitored(int episodeId, bool monitored) - { - var episode = _episodeRepository.Get(episodeId); - _episodeRepository.SetMonitoredFlat(episode, monitored); - - _logger.Debug("Monitored flag for Episode:{0} was set to {1}", episodeId, monitored); - } - - public void SetEpisodeMonitoredBySeason(int seriesId, int seasonNumber, bool monitored) - { - _episodeRepository.SetMonitoredBySeason(seriesId, seasonNumber, monitored); - } - - public void UpdateEpisodes(List<Episode> episodes) - { - _episodeRepository.UpdateMany(episodes); - } - - public List<Episode> EpisodesBetweenDates(DateTime start, DateTime end, bool includeUnmonitored) - { - var episodes = _episodeRepository.EpisodesBetweenDates(start.ToUniversalTime(), end.ToUniversalTime(), includeUnmonitored); - - return episodes; - } - - public void InsertMany(List<Episode> episodes) - { - _episodeRepository.InsertMany(episodes); - } - - public void UpdateMany(List<Episode> episodes) - { - _episodeRepository.UpdateMany(episodes); - } - - public void DeleteMany(List<Episode> episodes) - { - _episodeRepository.DeleteMany(episodes); - } - - public void HandleAsync(SeriesDeletedEvent message) - { - var episodes = GetEpisodeBySeries(message.Series.Id); - _episodeRepository.DeleteMany(episodes); - } - - public void Handle(EpisodeFileDeletedEvent message) - { - foreach (var episode in GetEpisodesByFileId(message.EpisodeFile.Id)) - { - _logger.Debug("Detaching episode {0} from file.", episode.Id); - episode.EpisodeFileId = 0; - - if (message.Reason != DeleteMediaFileReason.Upgrade && _configService.AutoUnmonitorPreviouslyDownloadedTracks) - { - episode.Monitored = false; - } - - UpdateEpisode(episode); - } - } - - public void Handle(EpisodeFileAddedEvent message) - { - foreach (var episode in message.EpisodeFile.Episodes.Value) - { - _episodeRepository.SetFileId(episode.Id, message.EpisodeFile.Id); - _logger.Debug("Linking [{0}] > [{1}]", message.EpisodeFile.RelativePath, episode); - } - } - } -} \ No newline at end of file diff --git a/src/NzbDrone.Core/Tv/Events/EpisodeInfoRefreshedEvent.cs b/src/NzbDrone.Core/Tv/Events/EpisodeInfoRefreshedEvent.cs deleted file mode 100644 index 4eded3b79..000000000 --- a/src/NzbDrone.Core/Tv/Events/EpisodeInfoRefreshedEvent.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System.Collections.Generic; -using System.Collections.ObjectModel; -using NzbDrone.Common.Messaging; - -namespace NzbDrone.Core.Tv.Events -{ - public class EpisodeInfoRefreshedEvent : IEvent - { - public Series Series { get; set; } - public ReadOnlyCollection<Episode> Added { get; private set; } - public ReadOnlyCollection<Episode> Updated { get; private set; } - - public EpisodeInfoRefreshedEvent(Series series, IList<Episode> added, IList<Episode> updated) - { - Series = series; - Added = new ReadOnlyCollection<Episode>(added); - Updated = new ReadOnlyCollection<Episode>(updated); - } - } -} \ No newline at end of file diff --git a/src/NzbDrone.Core/Tv/Events/SeriesAddedEvent.cs b/src/NzbDrone.Core/Tv/Events/SeriesAddedEvent.cs deleted file mode 100644 index 1a18c2b8d..000000000 --- a/src/NzbDrone.Core/Tv/Events/SeriesAddedEvent.cs +++ /dev/null @@ -1,14 +0,0 @@ -using NzbDrone.Common.Messaging; - -namespace NzbDrone.Core.Tv.Events -{ - public class SeriesAddedEvent : IEvent - { - public Series Series { get; private set; } - - public SeriesAddedEvent(Series series) - { - Series = series; - } - } -} diff --git a/src/NzbDrone.Core/Tv/Events/SeriesDeletedEvent.cs b/src/NzbDrone.Core/Tv/Events/SeriesDeletedEvent.cs deleted file mode 100644 index e04d8f60e..000000000 --- a/src/NzbDrone.Core/Tv/Events/SeriesDeletedEvent.cs +++ /dev/null @@ -1,16 +0,0 @@ -using NzbDrone.Common.Messaging; - -namespace NzbDrone.Core.Tv.Events -{ - public class SeriesDeletedEvent : IEvent - { - public Series Series { get; private set; } - public bool DeleteFiles { get; private set; } - - public SeriesDeletedEvent(Series series, bool deleteFiles) - { - Series = series; - DeleteFiles = deleteFiles; - } - } -} \ No newline at end of file diff --git a/src/NzbDrone.Core/Tv/Events/SeriesEditedEvent.cs b/src/NzbDrone.Core/Tv/Events/SeriesEditedEvent.cs deleted file mode 100644 index a37a6c902..000000000 --- a/src/NzbDrone.Core/Tv/Events/SeriesEditedEvent.cs +++ /dev/null @@ -1,16 +0,0 @@ -using NzbDrone.Common.Messaging; - -namespace NzbDrone.Core.Tv.Events -{ - public class SeriesEditedEvent : IEvent - { - public Series Series { get; private set; } - public Series OldSeries { get; private set; } - - public SeriesEditedEvent(Series series, Series oldSeries) - { - Series = series; - OldSeries = oldSeries; - } - } -} \ No newline at end of file diff --git a/src/NzbDrone.Core/Tv/Events/SeriesRefreshStartingEvent.cs b/src/NzbDrone.Core/Tv/Events/SeriesRefreshStartingEvent.cs deleted file mode 100644 index e330b0004..000000000 --- a/src/NzbDrone.Core/Tv/Events/SeriesRefreshStartingEvent.cs +++ /dev/null @@ -1,14 +0,0 @@ -using NzbDrone.Common.Messaging; - -namespace NzbDrone.Core.Tv.Events -{ - public class SeriesRefreshStartingEvent : IEvent - { - public bool ManualTrigger { get; set; } - - public SeriesRefreshStartingEvent(bool manualTrigger) - { - ManualTrigger = manualTrigger; - } - } -} \ No newline at end of file diff --git a/src/NzbDrone.Core/Tv/Events/SeriesUpdatedEvent.cs b/src/NzbDrone.Core/Tv/Events/SeriesUpdatedEvent.cs deleted file mode 100644 index 8dafe0563..000000000 --- a/src/NzbDrone.Core/Tv/Events/SeriesUpdatedEvent.cs +++ /dev/null @@ -1,14 +0,0 @@ -using NzbDrone.Common.Messaging; - -namespace NzbDrone.Core.Tv.Events -{ - public class SeriesUpdatedEvent : IEvent - { - public Series Series { get; private set; } - - public SeriesUpdatedEvent(Series series) - { - Series = series; - } - } -} \ No newline at end of file diff --git a/src/NzbDrone.Core/Tv/MonitoringOptions.cs b/src/NzbDrone.Core/Tv/MonitoringOptions.cs deleted file mode 100644 index 2cda68b1c..000000000 --- a/src/NzbDrone.Core/Tv/MonitoringOptions.cs +++ /dev/null @@ -1,10 +0,0 @@ -using NzbDrone.Core.Datastore; - -namespace NzbDrone.Core.Tv -{ - public class MonitoringOptions : IEmbeddedDocument - { - public bool IgnoreEpisodesWithFiles { get; set; } - public bool IgnoreEpisodesWithoutFiles { get; set; } - } -} diff --git a/src/NzbDrone.Core/Tv/Ratings.cs b/src/NzbDrone.Core/Tv/Ratings.cs deleted file mode 100644 index 6c66fbb7e..000000000 --- a/src/NzbDrone.Core/Tv/Ratings.cs +++ /dev/null @@ -1,10 +0,0 @@ -using NzbDrone.Core.Datastore; - -namespace NzbDrone.Core.Tv -{ - public class Ratings : IEmbeddedDocument - { - public int Votes { get; set; } - public decimal Value { get; set; } - } -} diff --git a/src/NzbDrone.Core/Tv/RefreshEpisodeService.cs b/src/NzbDrone.Core/Tv/RefreshEpisodeService.cs deleted file mode 100644 index b81292219..000000000 --- a/src/NzbDrone.Core/Tv/RefreshEpisodeService.cs +++ /dev/null @@ -1,199 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using NLog; -using NzbDrone.Common.Extensions; -using NzbDrone.Core.Messaging.Events; -using NzbDrone.Core.Tv.Events; - -namespace NzbDrone.Core.Tv -{ - public interface IRefreshEpisodeService - { - void RefreshEpisodeInfo(Series series, IEnumerable<Episode> remoteEpisodes); - } - - public class RefreshEpisodeService : IRefreshEpisodeService - { - private readonly IEpisodeService _episodeService; - private readonly IEventAggregator _eventAggregator; - private readonly Logger _logger; - - public RefreshEpisodeService(IEpisodeService episodeService, IEventAggregator eventAggregator, Logger logger) - { - _episodeService = episodeService; - _eventAggregator = eventAggregator; - _logger = logger; - } - - public void RefreshEpisodeInfo(Series series, IEnumerable<Episode> remoteEpisodes) - { - _logger.Info("Starting episode info refresh for: {0}", series); - var successCount = 0; - var failCount = 0; - - var existingEpisodes = _episodeService.GetEpisodeBySeries(series.Id); - var seasons = series.Seasons; - - var updateList = new List<Episode>(); - var newList = new List<Episode>(); - var dupeFreeRemoteEpisodes = remoteEpisodes.DistinctBy(m => new { m.SeasonNumber, m.EpisodeNumber }).ToList(); - - if (series.SeriesType == SeriesTypes.Anime) - { - dupeFreeRemoteEpisodes = MapAbsoluteEpisodeNumbers(dupeFreeRemoteEpisodes); - } - - foreach (var episode in OrderEpisodes(series, dupeFreeRemoteEpisodes)) - { - try - { - var episodeToUpdate = GetEpisodeToUpdate(series, episode, existingEpisodes); - - if (episodeToUpdate != null) - { - existingEpisodes.Remove(episodeToUpdate); - updateList.Add(episodeToUpdate); - } - else - { - episodeToUpdate = new Episode(); - episodeToUpdate.Monitored = GetMonitoredStatus(episode, seasons); - newList.Add(episodeToUpdate); - } - - episodeToUpdate.SeriesId = series.Id; - episodeToUpdate.EpisodeNumber = episode.EpisodeNumber; - episodeToUpdate.SeasonNumber = episode.SeasonNumber; - episodeToUpdate.AbsoluteEpisodeNumber = episode.AbsoluteEpisodeNumber; - episodeToUpdate.Title = episode.Title ?? "TBA"; - episodeToUpdate.Overview = episode.Overview; - episodeToUpdate.AirDate = episode.AirDate; - episodeToUpdate.AirDateUtc = episode.AirDateUtc; - episodeToUpdate.Ratings = episode.Ratings; - episodeToUpdate.Images = episode.Images; - - successCount++; - } - catch (Exception e) - { - _logger.Fatal(e, "An error has occurred while updating episode info for series {0}. {1}", series, episode); - failCount++; - } - } - - var allEpisodes = new List<Episode>(); - allEpisodes.AddRange(newList); - allEpisodes.AddRange(updateList); - - AdjustMultiEpisodeAirTime(series, allEpisodes); - AdjustDirectToDvdAirDate(series, allEpisodes); - - _episodeService.DeleteMany(existingEpisodes); - _episodeService.UpdateMany(updateList); - _episodeService.InsertMany(newList); - - _eventAggregator.PublishEvent(new EpisodeInfoRefreshedEvent(series, newList, updateList)); - - if (failCount != 0) - { - _logger.Info("Finished episode refresh for series: {0}. Successful: {1} - Failed: {2} ", - series.Title, successCount, failCount); - } - else - { - _logger.Info("Finished episode refresh for series: {0}.", series); - } - } - - private bool GetMonitoredStatus(Episode episode, IEnumerable<Season> seasons) - { - if (episode.EpisodeNumber == 0 && episode.SeasonNumber != 1) - { - return false; - } - - var season = seasons.SingleOrDefault(c => c.SeasonNumber == episode.SeasonNumber); - return season == null || season.Monitored; - } - - private void AdjustMultiEpisodeAirTime(Series series, IEnumerable<Episode> allEpisodes) - { - if (series.Network == "Netflix") - { - _logger.Debug("Not adjusting episode air times for Netflix series {0}", series.Title); - return; - } - - var groups = allEpisodes.Where(c => c.AirDateUtc.HasValue) - .GroupBy(e => new { e.SeasonNumber, e.AirDate }) - .Where(g => g.Count() > 1) - .ToList(); - - foreach (var group in groups) - { - var episodeCount = 0; - - foreach (var episode in group.OrderBy(e => e.SeasonNumber).ThenBy(e => e.EpisodeNumber)) - { - episode.AirDateUtc = episode.AirDateUtc.Value.AddMinutes(series.Runtime * episodeCount); - episodeCount++; - } - } - } - - private void AdjustDirectToDvdAirDate(Series series, IEnumerable<Episode> allEpisodes) - { - if (series.Status == SeriesStatusType.Ended && allEpisodes.All(v => !v.AirDateUtc.HasValue) && series.FirstAired.HasValue) - { - foreach (var episode in allEpisodes) - { - episode.AirDateUtc = series.FirstAired; - episode.AirDate = series.FirstAired.Value.ToString("yyyy-MM-dd"); - } - } - } - - private List<Episode> MapAbsoluteEpisodeNumbers(List<Episode> remoteEpisodes) - { - //Return all episodes with no abs number, but distinct for those with abs number - return remoteEpisodes.Where(e => e.AbsoluteEpisodeNumber.HasValue) - .OrderByDescending(e => e.SeasonNumber) - .DistinctBy(e => e.AbsoluteEpisodeNumber.Value) - .Concat(remoteEpisodes.Where(e => !e.AbsoluteEpisodeNumber.HasValue)) - .ToList(); - } - - private Episode GetEpisodeToUpdate(Series series, Episode episode, List<Episode> existingEpisodes) - { - if (series.SeriesType == SeriesTypes.Anime) - { - if (episode.AbsoluteEpisodeNumber.HasValue) - { - var matchingEpisode = existingEpisodes.FirstOrDefault(e => e.AbsoluteEpisodeNumber == episode.AbsoluteEpisodeNumber); - - if (matchingEpisode != null) return matchingEpisode; - } - } - - return existingEpisodes.FirstOrDefault(e => e.SeasonNumber == episode.SeasonNumber && e.EpisodeNumber == episode.EpisodeNumber); - } - - private IEnumerable<Episode> OrderEpisodes(Series series, List<Episode> episodes) - { - if (series.SeriesType == SeriesTypes.Anime) - { - var withAbs = episodes.Where(e => e.AbsoluteEpisodeNumber.HasValue) - .OrderBy(e => e.AbsoluteEpisodeNumber); - - var withoutAbs = episodes.Where(e => !e.AbsoluteEpisodeNumber.HasValue) - .OrderBy(e => e.SeasonNumber) - .ThenBy(e => e.EpisodeNumber); - - return withAbs.Concat(withoutAbs); - } - - return episodes.OrderBy(e => e.SeasonNumber).ThenBy(e => e.EpisodeNumber); - } - } -} \ No newline at end of file diff --git a/src/NzbDrone.Core/Tv/RefreshSeriesService.cs b/src/NzbDrone.Core/Tv/RefreshSeriesService.cs deleted file mode 100644 index 39e32dbe7..000000000 --- a/src/NzbDrone.Core/Tv/RefreshSeriesService.cs +++ /dev/null @@ -1,190 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using NLog; -using NzbDrone.Common.Extensions; -using NzbDrone.Common.Instrumentation.Extensions; -using NzbDrone.Core.DataAugmentation.DailySeries; -using NzbDrone.Core.Exceptions; -using NzbDrone.Core.MediaFiles; -using NzbDrone.Core.Messaging.Commands; -using NzbDrone.Core.Messaging.Events; -using NzbDrone.Core.MetadataSource; -using NzbDrone.Core.Tv.Commands; -using NzbDrone.Core.Tv.Events; - -namespace NzbDrone.Core.Tv -{ - public class RefreshSeriesService : IExecute<RefreshSeriesCommand> - { - private readonly IProvideSeriesInfo _seriesInfo; - private readonly ISeriesService _seriesService; - private readonly IRefreshEpisodeService _refreshEpisodeService; - private readonly IEventAggregator _eventAggregator; - private readonly IDailySeriesService _dailySeriesService; - private readonly IDiskScanService _diskScanService; - private readonly ICheckIfSeriesShouldBeRefreshed _checkIfSeriesShouldBeRefreshed; - private readonly Logger _logger; - - public RefreshSeriesService(IProvideSeriesInfo seriesInfo, - ISeriesService seriesService, - IRefreshEpisodeService refreshEpisodeService, - IEventAggregator eventAggregator, - IDailySeriesService dailySeriesService, - IDiskScanService diskScanService, - ICheckIfSeriesShouldBeRefreshed checkIfSeriesShouldBeRefreshed, - Logger logger) - { - _seriesInfo = seriesInfo; - _seriesService = seriesService; - _refreshEpisodeService = refreshEpisodeService; - _eventAggregator = eventAggregator; - _dailySeriesService = dailySeriesService; - _diskScanService = diskScanService; - _checkIfSeriesShouldBeRefreshed = checkIfSeriesShouldBeRefreshed; - _logger = logger; - } - - private void RefreshSeriesInfo(Series series) - { - _logger.ProgressInfo("Updating Info for {0}", series.Title); - - Tuple<Series, List<Episode>> tuple; - - try - { - tuple = _seriesInfo.GetSeriesInfo(series.TvdbId); - } - catch (SeriesNotFoundException) - { - _logger.Error("Series '{0}' (tvdbid {1}) was not found, it may have been removed from TheTVDB.", series.Title, series.TvdbId); - return; - } - - var seriesInfo = tuple.Item1; - - if (series.TvdbId != seriesInfo.TvdbId) - { - _logger.Warn("Series '{0}' (tvdbid {1}) was replaced with '{2}' (tvdbid {3}), because the original was a duplicate.", series.Title, series.TvdbId, seriesInfo.Title, seriesInfo.TvdbId); - series.TvdbId = seriesInfo.TvdbId; - } - - series.Title = seriesInfo.Title; - series.TitleSlug = seriesInfo.TitleSlug; - series.TvRageId = seriesInfo.TvRageId; - series.TvMazeId = seriesInfo.TvMazeId; - series.ImdbId = seriesInfo.ImdbId; - series.AirTime = seriesInfo.AirTime; - series.Overview = seriesInfo.Overview; - series.Status = seriesInfo.Status; - series.CleanTitle = seriesInfo.CleanTitle; - series.SortTitle = seriesInfo.SortTitle; - series.LastInfoSync = DateTime.UtcNow; - series.Runtime = seriesInfo.Runtime; - series.Images = seriesInfo.Images; - series.Network = seriesInfo.Network; - series.FirstAired = seriesInfo.FirstAired; - series.Ratings = seriesInfo.Ratings; - series.Actors = seriesInfo.Actors; - series.Genres = seriesInfo.Genres; - series.Certification = seriesInfo.Certification; - - if (_dailySeriesService.IsDailySeries(series.TvdbId)) - { - series.SeriesType = SeriesTypes.Daily; - } - - try - { - series.Path = new DirectoryInfo(series.Path).FullName; - series.Path = series.Path.GetActualCasing(); - } - catch (Exception e) - { - _logger.Warn(e, "Couldn't update series path for " + series.Path); - } - - series.Seasons = UpdateSeasons(series, seriesInfo); - - _seriesService.UpdateSeries(series); - _refreshEpisodeService.RefreshEpisodeInfo(series, tuple.Item2); - - _logger.Debug("Finished series refresh for {0}", series.Title); - _eventAggregator.PublishEvent(new SeriesUpdatedEvent(series)); - } - - private List<Season> UpdateSeasons(Series series, Series seriesInfo) - { - var seasons = seriesInfo.Seasons.DistinctBy(s => s.SeasonNumber).ToList(); - - foreach (var season in seasons) - { - var existingSeason = series.Seasons.FirstOrDefault(s => s.SeasonNumber == season.SeasonNumber); - - //Todo: Should this should use the previous season's monitored state? - if (existingSeason == null) - { - if (season.SeasonNumber == 0) - { - season.Monitored = false; - continue; - } - - _logger.Debug("New season ({0}) for series: [{1}] {2}, setting monitored to true", season.SeasonNumber, series.TvdbId, series.Title); - season.Monitored = true; - } - - else - { - season.Monitored = existingSeason.Monitored; - } - } - - return seasons; - } - - public void Execute(RefreshSeriesCommand message) - { - _eventAggregator.PublishEvent(new SeriesRefreshStartingEvent(message.Trigger == CommandTrigger.Manual)); - - if (message.SeriesId.HasValue) - { - var series = _seriesService.GetSeries(message.SeriesId.Value); - RefreshSeriesInfo(series); - } - else - { - var allSeries = _seriesService.GetAllSeries().OrderBy(c => c.SortTitle).ToList(); - - foreach (var series in allSeries) - { - if (message.Trigger == CommandTrigger.Manual || _checkIfSeriesShouldBeRefreshed.ShouldRefresh(series)) - { - try - { - RefreshSeriesInfo(series); - } - catch (Exception e) - { - _logger.Error(e, "Couldn't refresh info for {0}", series); - } - } - - else - { - try - { - _logger.Info("Skipping refresh of series: {0}", series.Title); - //_diskScanService.Scan(series); - } - catch (Exception e) - { - _logger.Error(e, "Couldn't rescan series {0}", series); - } - } - } - } - } - } -} diff --git a/src/NzbDrone.Core/Tv/Season.cs b/src/NzbDrone.Core/Tv/Season.cs deleted file mode 100644 index e233c734f..000000000 --- a/src/NzbDrone.Core/Tv/Season.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System.Collections.Generic; -using NzbDrone.Core.Datastore; - -namespace NzbDrone.Core.Tv -{ - public class Season : IEmbeddedDocument - { - public Season() - { - Images = new List<MediaCover.MediaCover>(); - } - - public int SeasonNumber { get; set; } - public bool Monitored { get; set; } - public List<MediaCover.MediaCover> Images { get; set; } - } -} \ No newline at end of file diff --git a/src/NzbDrone.Core/Tv/Series.cs b/src/NzbDrone.Core/Tv/Series.cs deleted file mode 100644 index 8542d183b..000000000 --- a/src/NzbDrone.Core/Tv/Series.cs +++ /dev/null @@ -1,78 +0,0 @@ -using System; -using System.Collections.Generic; -using Marr.Data; -using NzbDrone.Common.Extensions; -using NzbDrone.Core.Datastore; -using NzbDrone.Core.Profiles; - -namespace NzbDrone.Core.Tv -{ - public class Series : ModelBase - { - public Series() - { - Images = new List<MediaCover.MediaCover>(); - Genres = new List<string>(); - Actors = new List<Actor>(); - Seasons = new List<Season>(); - Tags = new HashSet<int>(); - } - - public int TvdbId { get; set; } - public int TvRageId { get; set; } - public int TvMazeId { get; set; } - public string ImdbId { get; set; } - public string Title { get; set; } - public string CleanTitle { get; set; } - public string SortTitle { get; set; } - public SeriesStatusType Status { get; set; } - public string Overview { get; set; } - public string AirTime { get; set; } - public bool Monitored { get; set; } - public int ProfileId { get; set; } - public bool SeasonFolder { get; set; } - public DateTime? LastInfoSync { get; set; } - public int Runtime { get; set; } - public List<MediaCover.MediaCover> Images { get; set; } - public SeriesTypes SeriesType { get; set; } - public string Network { get; set; } - public bool UseSceneNumbering { get; set; } - public string TitleSlug { get; set; } - public string Path { get; set; } - public int Year { get; set; } - public Ratings Ratings { get; set; } - public List<string> Genres { get; set; } - public List<Actor> Actors { get; set; } - public string Certification { get; set; } - public string RootFolderPath { get; set; } - public DateTime Added { get; set; } - public DateTime? FirstAired { get; set; } - public LazyLoaded<Profile> Profile { get; set; } - - public List<Season> Seasons { get; set; } - public HashSet<int> Tags { get; set; } - public AddSeriesOptions AddOptions { get; set; } - - public override string ToString() - { - return string.Format("[{0}][{1}]", TvdbId, Title.NullSafe()); - } - - public void ApplyChanges(Series otherSeries) - { - TvdbId = otherSeries.TvdbId; - - Seasons = otherSeries.Seasons; - Path = otherSeries.Path; - ProfileId = otherSeries.ProfileId; - - SeasonFolder = otherSeries.SeasonFolder; - Monitored = otherSeries.Monitored; - - SeriesType = otherSeries.SeriesType; - RootFolderPath = otherSeries.RootFolderPath; - Tags = otherSeries.Tags; - AddOptions = otherSeries.AddOptions; - } - } -} \ No newline at end of file diff --git a/src/NzbDrone.Core/Tv/SeriesAddedHandler.cs b/src/NzbDrone.Core/Tv/SeriesAddedHandler.cs deleted file mode 100644 index 2e7ee8005..000000000 --- a/src/NzbDrone.Core/Tv/SeriesAddedHandler.cs +++ /dev/null @@ -1,22 +0,0 @@ -using NzbDrone.Core.Messaging.Commands; -using NzbDrone.Core.Messaging.Events; -using NzbDrone.Core.Tv.Commands; -using NzbDrone.Core.Tv.Events; - -namespace NzbDrone.Core.Tv -{ - public class SeriesAddedHandler : IHandle<SeriesAddedEvent> - { - private readonly IManageCommandQueue _commandQueueManager; - - public SeriesAddedHandler(IManageCommandQueue commandQueueManager) - { - _commandQueueManager = commandQueueManager; - } - - public void Handle(SeriesAddedEvent message) - { - _commandQueueManager.Push(new RefreshSeriesCommand(message.Series.Id)); - } - } -} diff --git a/src/NzbDrone.Core/Tv/SeriesEditedService.cs b/src/NzbDrone.Core/Tv/SeriesEditedService.cs deleted file mode 100644 index 063537f18..000000000 --- a/src/NzbDrone.Core/Tv/SeriesEditedService.cs +++ /dev/null @@ -1,25 +0,0 @@ -using NzbDrone.Core.Messaging.Commands; -using NzbDrone.Core.Messaging.Events; -using NzbDrone.Core.Tv.Commands; -using NzbDrone.Core.Tv.Events; - -namespace NzbDrone.Core.Tv -{ - public class SeriesEditedService : IHandle<SeriesEditedEvent> - { - private readonly IManageCommandQueue _commandQueueManager; - - public SeriesEditedService(IManageCommandQueue commandQueueManager) - { - _commandQueueManager = commandQueueManager; - } - - public void Handle(SeriesEditedEvent message) - { - if (message.Series.SeriesType != message.OldSeries.SeriesType) - { - _commandQueueManager.Push(new RefreshSeriesCommand(message.Series.Id)); - } - } - } -} diff --git a/src/NzbDrone.Core/Tv/SeriesRepository.cs b/src/NzbDrone.Core/Tv/SeriesRepository.cs deleted file mode 100644 index d5bc343ff..000000000 --- a/src/NzbDrone.Core/Tv/SeriesRepository.cs +++ /dev/null @@ -1,56 +0,0 @@ -using System.Linq; -using NzbDrone.Core.Datastore; -using NzbDrone.Core.Messaging.Events; - - -namespace NzbDrone.Core.Tv -{ - public interface ISeriesRepository : IBasicRepository<Series> - { - bool SeriesPathExists(string path); - Series FindByTitle(string cleanTitle); - Series FindByTitle(string cleanTitle, int year); - Series FindByTvdbId(int tvdbId); - Series FindByTvRageId(int tvRageId); - } - - public class SeriesRepository : BasicRepository<Series>, ISeriesRepository - { - public SeriesRepository(IMainDatabase database, IEventAggregator eventAggregator) - : base(database, eventAggregator) - { - } - - public bool SeriesPathExists(string path) - { - return Query.Where(c => c.Path == path).Any(); - } - - public Series FindByTitle(string cleanTitle) - { - cleanTitle = cleanTitle.ToLowerInvariant(); - - return Query.Where(s => s.CleanTitle == cleanTitle) - .SingleOrDefault(); - } - - public Series FindByTitle(string cleanTitle, int year) - { - cleanTitle = cleanTitle.ToLowerInvariant(); - - return Query.Where(s => s.CleanTitle == cleanTitle) - .AndWhere(s => s.Year == year) - .SingleOrDefault(); - } - - public Series FindByTvdbId(int tvdbId) - { - return Query.Where(s => s.TvdbId == tvdbId).SingleOrDefault(); - } - - public Series FindByTvRageId(int tvRageId) - { - return Query.Where(s => s.TvRageId == tvRageId).SingleOrDefault(); - } - } -} \ No newline at end of file diff --git a/src/NzbDrone.Core/Tv/SeriesScannedHandler.cs b/src/NzbDrone.Core/Tv/SeriesScannedHandler.cs deleted file mode 100644 index 9d208c764..000000000 --- a/src/NzbDrone.Core/Tv/SeriesScannedHandler.cs +++ /dev/null @@ -1,62 +0,0 @@ -using NLog; -using NzbDrone.Core.IndexerSearch; -using NzbDrone.Core.MediaFiles.Events; -using NzbDrone.Core.Messaging.Commands; -using NzbDrone.Core.Messaging.Events; - -namespace NzbDrone.Core.Tv -{ - public class SeriesScannedHandler : IHandle<SeriesScannedEvent>, - IHandle<SeriesScanSkippedEvent> - { - private readonly IEpisodeMonitoredService _episodeMonitoredService; - private readonly ISeriesService _seriesService; - private readonly IManageCommandQueue _commandQueueManager; - private readonly IEpisodeAddedService _episodeAddedService; - - private readonly Logger _logger; - - public SeriesScannedHandler(IEpisodeMonitoredService episodeMonitoredService, - ISeriesService seriesService, - IManageCommandQueue commandQueueManager, - IEpisodeAddedService episodeAddedService, - Logger logger) - { - _episodeMonitoredService = episodeMonitoredService; - _seriesService = seriesService; - _commandQueueManager = commandQueueManager; - _episodeAddedService = episodeAddedService; - _logger = logger; - } - - private void HandleScanEvents(Series series) - { - if (series.AddOptions == null) - { - _episodeAddedService.SearchForRecentlyAdded(series.Id); - return; - } - - _logger.Info("[{0}] was recently added, performing post-add actions", series.Title); - _episodeMonitoredService.SetEpisodeMonitoredStatus(series, series.AddOptions); - - if (series.AddOptions.SearchForMissingEpisodes) - { - _commandQueueManager.Push(new MissingEpisodeSearchCommand(series.Id)); - } - - series.AddOptions = null; - _seriesService.RemoveAddOptions(series); - } - - public void Handle(SeriesScannedEvent message) - { - HandleScanEvents(message.Series); - } - - public void Handle(SeriesScanSkippedEvent message) - { - HandleScanEvents(message.Series); - } - } -} diff --git a/src/NzbDrone.Core/Tv/SeriesService.cs b/src/NzbDrone.Core/Tv/SeriesService.cs deleted file mode 100644 index ad17301ab..000000000 --- a/src/NzbDrone.Core/Tv/SeriesService.cs +++ /dev/null @@ -1,212 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using NLog; -using NzbDrone.Common.EnsureThat; -using NzbDrone.Common.Extensions; -using NzbDrone.Core.DataAugmentation.Scene; -using NzbDrone.Core.Messaging.Events; -using NzbDrone.Core.Organizer; -using NzbDrone.Core.Parser; -using NzbDrone.Core.Tv.Events; - -namespace NzbDrone.Core.Tv -{ - public interface ISeriesService - { - Series GetSeries(int seriesId); - List<Series> GetSeries(IEnumerable<int> seriesIds); - Series AddSeries(Series newSeries); - Series FindByTvdbId(int tvdbId); - Series FindByTvRageId(int tvRageId); - Series FindByTitle(string title); - Series FindByTitle(string title, int year); - Series FindByTitleInexact(string title); - void DeleteSeries(int seriesId, bool deleteFiles); - List<Series> GetAllSeries(); - Series UpdateSeries(Series series); - List<Series> UpdateSeries(List<Series> series); - bool SeriesPathExists(string folder); - void RemoveAddOptions(Series series); - } - - public class SeriesService : ISeriesService - { - private readonly ISeriesRepository _seriesRepository; - private readonly IEventAggregator _eventAggregator; - // private readonly ISceneMappingService _sceneMappingService; - private readonly IEpisodeService _episodeService; - private readonly IBuildFileNames _fileNameBuilder; - private readonly Logger _logger; - - public SeriesService(ISeriesRepository seriesRepository, - IEventAggregator eventAggregator, - // ISceneMappingService sceneMappingService, - IEpisodeService episodeService, - IBuildFileNames fileNameBuilder, - Logger logger) - { - _seriesRepository = seriesRepository; - _eventAggregator = eventAggregator; - //_sceneMappingService = sceneMappingService; - _episodeService = episodeService; - _fileNameBuilder = fileNameBuilder; - _logger = logger; - } - - public Series GetSeries(int seriesId) - { - return _seriesRepository.Get(seriesId); - } - - public List<Series> GetSeries(IEnumerable<int> seriesIds) - { - return _seriesRepository.Get(seriesIds).ToList(); - } - - public Series AddSeries(Series newSeries) - { - _seriesRepository.Insert(newSeries); - _eventAggregator.PublishEvent(new SeriesAddedEvent(GetSeries(newSeries.Id))); - - return newSeries; - } - - public Series FindByTvdbId(int tvRageId) - { - return _seriesRepository.FindByTvdbId(tvRageId); - } - - public Series FindByTvRageId(int tvRageId) - { - return _seriesRepository.FindByTvRageId(tvRageId); - } - - public Series FindByTitle(string title) - { - //var tvdbId = _sceneMappingService.FindTvdbId(title); - - //if (tvdbId.HasValue) - //{ - // return _seriesRepository.FindByTvdbId(tvdbId.Value); - //} - - return _seriesRepository.FindByTitle(title.CleanSeriesTitle()); - } - - public Series FindByTitleInexact(string title) - { - // find any series clean title within the provided release title - string cleanTitle = title.CleanSeriesTitle(); - var list = _seriesRepository.All().Where(s => cleanTitle.Contains(s.CleanTitle)).ToList(); - if (!list.Any()) - { - // no series matched - return null; - } - if (list.Count == 1) - { - // return the first series if there is only one - return list.Single(); - } - // build ordered list of series by position in the search string - var query = - list.Select(series => new - { - position = cleanTitle.IndexOf(series.CleanTitle), - length = series.CleanTitle.Length, - series = series - }) - .Where(s => (s.position>=0)) - .ToList() - .OrderBy(s => s.position) - .ThenByDescending(s => s.length) - .ToList(); - - // get the leftmost series that is the longest - // series are usually the first thing in release title, so we select the leftmost and longest match - var match = query.First().series; - - _logger.Debug("Multiple series matched {0} from title {1}", match.Title, title); - foreach (var entry in list) - { - _logger.Debug("Multiple series match candidate: {0} cleantitle: {1}", entry.Title, entry.CleanTitle); - } - - return match; - } - - public Series FindByTitle(string title, int year) - { - return _seriesRepository.FindByTitle(title.CleanSeriesTitle(), year); - } - - public void DeleteSeries(int seriesId, bool deleteFiles) - { - var series = _seriesRepository.Get(seriesId); - _seriesRepository.Delete(seriesId); - _eventAggregator.PublishEvent(new SeriesDeletedEvent(series, deleteFiles)); - } - - public List<Series> GetAllSeries() - { - return _seriesRepository.All().ToList(); - } - - public Series UpdateSeries(Series series) - { - var storedSeries = GetSeries(series.Id); - - foreach (var season in series.Seasons) - { - var storedSeason = storedSeries.Seasons.SingleOrDefault(s => s.SeasonNumber == season.SeasonNumber); - - if (storedSeason != null && season.Monitored != storedSeason.Monitored) - { - _episodeService.SetEpisodeMonitoredBySeason(series.Id, season.SeasonNumber, season.Monitored); - } - } - - var updatedSeries = _seriesRepository.Update(series); - _eventAggregator.PublishEvent(new SeriesEditedEvent(updatedSeries, storedSeries)); - - return updatedSeries; - } - - public List<Series> UpdateSeries(List<Series> series) - { - _logger.Debug("Updating {0} series", series.Count); - foreach (var s in series) - { - _logger.Trace("Updating: {0}", s.Title); - if (!s.RootFolderPath.IsNullOrWhiteSpace()) - { - var folderName = new DirectoryInfo(s.Path).Name; - s.Path = Path.Combine(s.RootFolderPath, folderName); - _logger.Trace("Changing path for {0} to {1}", s.Title, s.Path); - } - - else - { - _logger.Trace("Not changing path for: {0}", s.Title); - } - } - - _seriesRepository.UpdateMany(series); - _logger.Debug("{0} series updated", series.Count); - - return series; - } - - public bool SeriesPathExists(string folder) - { - return _seriesRepository.SeriesPathExists(folder); - } - - public void RemoveAddOptions(Series series) - { - _seriesRepository.SetFields(series, s => s.AddOptions); - } - } -} diff --git a/src/NzbDrone.Core/Tv/SeriesStatusType.cs b/src/NzbDrone.Core/Tv/SeriesStatusType.cs deleted file mode 100644 index acc9fbf81..000000000 --- a/src/NzbDrone.Core/Tv/SeriesStatusType.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace NzbDrone.Core.Tv -{ - public enum SeriesStatusType - { - Continuing = 0, - Ended = 1 - } -} diff --git a/src/NzbDrone.Core/Tv/SeriesTitleNormalizer.cs b/src/NzbDrone.Core/Tv/SeriesTitleNormalizer.cs deleted file mode 100644 index 9fc2c5933..000000000 --- a/src/NzbDrone.Core/Tv/SeriesTitleNormalizer.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System.Collections.Generic; - -namespace NzbDrone.Core.Tv -{ - public static class SeriesTitleNormalizer - { - private readonly static Dictionary<int, string> PreComputedTitles = new Dictionary<int, string> - { - { 281588, "a to z" }, - { 266757, "ad trials triumph early church" }, - { 289260, "ad bible continues"} - }; - - public static string Normalize(string title, int tvdbId) - { - if (PreComputedTitles.ContainsKey(tvdbId)) - { - return PreComputedTitles[tvdbId]; - } - - return Parser.Parser.NormalizeTitle(title).ToLower(); - } - } -} diff --git a/src/NzbDrone.Core/Tv/SeriesTitleSlugValidator.cs b/src/NzbDrone.Core/Tv/SeriesTitleSlugValidator.cs deleted file mode 100644 index 97ff29095..000000000 --- a/src/NzbDrone.Core/Tv/SeriesTitleSlugValidator.cs +++ /dev/null @@ -1,25 +0,0 @@ -using FluentValidation.Validators; - -namespace NzbDrone.Core.Tv -{ - public class SeriesTitleSlugValidator : PropertyValidator - { - private readonly ISeriesService _seriesService; - - public SeriesTitleSlugValidator(ISeriesService seriesService) - : base("Title slug is in use by another series with a similar name") - { - _seriesService = seriesService; - } - - protected override bool IsValid(PropertyValidatorContext context) - { - if (context.PropertyValue == null) return true; - - dynamic instance = context.ParentContext.InstanceToValidate; - var instanceId = (int)instance.Id; - - return !_seriesService.GetAllSeries().Exists(s => s.TitleSlug.Equals(context.PropertyValue.ToString()) && s.Id != instanceId); - } - } -} diff --git a/src/NzbDrone.Core/Tv/SeriesTypes.cs b/src/NzbDrone.Core/Tv/SeriesTypes.cs deleted file mode 100644 index 176ff7655..000000000 --- a/src/NzbDrone.Core/Tv/SeriesTypes.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace NzbDrone.Core.Tv -{ - public enum SeriesTypes - { - Standard = 0, - Daily = 1, - Anime = 2, - } -} \ No newline at end of file diff --git a/src/NzbDrone.Core/Tv/ShouldRefreshSeries.cs b/src/NzbDrone.Core/Tv/ShouldRefreshSeries.cs deleted file mode 100644 index bbf48cbb8..000000000 --- a/src/NzbDrone.Core/Tv/ShouldRefreshSeries.cs +++ /dev/null @@ -1,55 +0,0 @@ -using System; -using System.Linq; -using NLog; - -namespace NzbDrone.Core.Tv -{ - public interface ICheckIfSeriesShouldBeRefreshed - { - bool ShouldRefresh(Series series); - } - - public class ShouldRefreshSeries : ICheckIfSeriesShouldBeRefreshed - { - private readonly IEpisodeService _episodeService; - private readonly Logger _logger; - - public ShouldRefreshSeries(IEpisodeService episodeService, Logger logger) - { - _episodeService = episodeService; - _logger = logger; - } - - public bool ShouldRefresh(Series series) - { - if (series.LastInfoSync < DateTime.UtcNow.AddDays(-30)) - { - _logger.Trace("Series {0} last updated more than 30 days ago, should refresh.", series.Title); - return true; - } - - if (series.LastInfoSync >= DateTime.UtcNow.AddHours(-6)) - { - _logger.Trace("Series {0} last updated less than 6 hours ago, should not be refreshed.", series.Title); - return false; - } - - if (series.Status == SeriesStatusType.Continuing) - { - _logger.Trace("Series {0} is continuing, should refresh.", series.Title); - return true; - } - - var lastEpisode = _episodeService.GetEpisodeBySeries(series.Id).OrderByDescending(e => e.AirDateUtc).FirstOrDefault(); - - if (lastEpisode != null && lastEpisode.AirDateUtc > DateTime.UtcNow.AddDays(-30)) - { - _logger.Trace("Last episode in {0} aired less than 30 days ago, should refresh.", series.Title); - return true; - } - - _logger.Trace("Series {0} ended long ago, should not be refreshed.", series.Title); - return false; - } - } -} diff --git a/src/NzbDrone.Core/Update/Commands/ApplicationUpdateCommand.cs b/src/NzbDrone.Core/Update/Commands/ApplicationUpdateCommand.cs index 7c4604a7c..f3b920b08 100644 --- a/src/NzbDrone.Core/Update/Commands/ApplicationUpdateCommand.cs +++ b/src/NzbDrone.Core/Update/Commands/ApplicationUpdateCommand.cs @@ -1,4 +1,4 @@ -using NzbDrone.Core.Messaging.Commands; +using NzbDrone.Core.Messaging.Commands; namespace NzbDrone.Core.Update.Commands { @@ -6,6 +6,6 @@ namespace NzbDrone.Core.Update.Commands { public override bool SendUpdatesToClient => true; - public override string CompletionMessage => "Restarting Lidarr to apply updates"; + public override string CompletionMessage => null; } } diff --git a/src/NzbDrone.Core/Update/InstallUpdateService.cs b/src/NzbDrone.Core/Update/InstallUpdateService.cs index d9490ba2e..4219d7c4f 100644 --- a/src/NzbDrone.Core/Update/InstallUpdateService.cs +++ b/src/NzbDrone.Core/Update/InstallUpdateService.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.IO; using NLog; @@ -167,7 +167,7 @@ namespace NzbDrone.Core.Update throw new UpdateFailedException("Update Script: '{0}' does not exist", scriptPath); } - _logger.Info("Removing NzbDrone.Update"); + _logger.Info("Removing Lidarr.Update"); _diskProvider.DeleteFolder(_appFolderInfo.GetUpdateClientFolder(), true); _logger.ProgressInfo("Starting update script: {0}", _configFileProvider.UpdateScriptPath); @@ -200,19 +200,20 @@ namespace NzbDrone.Core.Update if (latestAvailable == null) { - _logger.ProgressDebug("No update available."); + _logger.ProgressDebug("No update available"); return; } if (OsInfo.IsNotWindows && !_configFileProvider.UpdateAutomatically && message.Trigger != CommandTrigger.Manual) { - _logger.ProgressDebug("Auto-update not enabled, not installing available update."); + _logger.ProgressDebug("Auto-update not enabled, not installing available update"); return; } try { InstallUpdate(latestAvailable); + _logger.ProgressDebug("Restarting Lidarr to apply updates"); } catch (UpdateFolderNotWritableException ex) { diff --git a/src/NzbDrone.Core/Validation/LanguageProfileExistsValidator.cs b/src/NzbDrone.Core/Validation/LanguageProfileExistsValidator.cs new file mode 100644 index 000000000..90450e3e5 --- /dev/null +++ b/src/NzbDrone.Core/Validation/LanguageProfileExistsValidator.cs @@ -0,0 +1,23 @@ +using FluentValidation.Validators; +using NzbDrone.Core.Profiles.Languages; + +namespace NzbDrone.Core.Validation +{ + public class LanguageProfileExistsValidator : PropertyValidator + { + private readonly ILanguageProfileService _profileService; + + public LanguageProfileExistsValidator(ILanguageProfileService profileService) + : base("Language profile does not exist") + { + _profileService = profileService; + } + + protected override bool IsValid(PropertyValidatorContext context) + { + if (context.PropertyValue == null) return true; + + return _profileService.Exists((int)context.PropertyValue); + } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Core/Validation/LanguageValidator.cs b/src/NzbDrone.Core/Validation/LanguageValidator.cs deleted file mode 100644 index 9edfc9085..000000000 --- a/src/NzbDrone.Core/Validation/LanguageValidator.cs +++ /dev/null @@ -1,21 +0,0 @@ -using FluentValidation.Validators; - -namespace NzbDrone.Core.Validation -{ - public class LanguageValidator : PropertyValidator - { - public LanguageValidator() - : base("Unknown Language") - { - } - - protected override bool IsValid(PropertyValidatorContext context) - { - if (context.PropertyValue == null) return false; - - if ((int) context.PropertyValue == 0) return false; - - return true; - } - } -} diff --git a/src/NzbDrone.Core/Validation/Paths/ArtistAncestorValidator.cs b/src/NzbDrone.Core/Validation/Paths/ArtistAncestorValidator.cs new file mode 100644 index 000000000..6ba36edfe --- /dev/null +++ b/src/NzbDrone.Core/Validation/Paths/ArtistAncestorValidator.cs @@ -0,0 +1,25 @@ +using System.Linq; +using FluentValidation.Validators; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.Music; + +namespace NzbDrone.Core.Validation.Paths +{ + public class ArtistAncestorValidator : PropertyValidator + { + private readonly IArtistService _artistService; + + public ArtistAncestorValidator(IArtistService artistService) + : base("Path is an ancestor of an existing path") + { + _artistService = artistService; + } + + protected override bool IsValid(PropertyValidatorContext context) + { + if (context.PropertyValue == null) return true; + + return !_artistService.GetAllArtists().Any(s => context.PropertyValue.ToString().IsParentPath(s.Path)); + } + } +} diff --git a/src/NzbDrone.Core/Validation/Paths/DroneFactoryValidator.cs b/src/NzbDrone.Core/Validation/Paths/DroneFactoryValidator.cs deleted file mode 100644 index 7637b74d2..000000000 --- a/src/NzbDrone.Core/Validation/Paths/DroneFactoryValidator.cs +++ /dev/null @@ -1,28 +0,0 @@ -using FluentValidation.Validators; -using NzbDrone.Common.Extensions; -using NzbDrone.Core.Configuration; - -namespace NzbDrone.Core.Validation.Paths -{ - public class DroneFactoryValidator : PropertyValidator - { - private readonly IConfigService _configService; - - public DroneFactoryValidator(IConfigService configService) - : base("Path is already used for drone factory") - { - _configService = configService; - } - - protected override bool IsValid(PropertyValidatorContext context) - { - if (context.PropertyValue == null) return false; - - var droneFactory = _configService.DownloadedAlbumsFolder; - - if (string.IsNullOrWhiteSpace(droneFactory)) return true; - - return !droneFactory.PathEquals(context.PropertyValue.ToString()); - } - } -} \ No newline at end of file diff --git a/src/NzbDrone.Core/Validation/Paths/SeriesAncestorValidator.cs b/src/NzbDrone.Core/Validation/Paths/SeriesAncestorValidator.cs deleted file mode 100644 index c91560873..000000000 --- a/src/NzbDrone.Core/Validation/Paths/SeriesAncestorValidator.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System.Linq; -using FluentValidation.Validators; -using NzbDrone.Common.Extensions; -using NzbDrone.Core.Tv; - -namespace NzbDrone.Core.Validation.Paths -{ - public class SeriesAncestorValidator : PropertyValidator - { - private readonly ISeriesService _seriesService; - - public SeriesAncestorValidator(ISeriesService seriesService) - : base("Path is an ancestor of an existing path") - { - _seriesService = seriesService; - } - - protected override bool IsValid(PropertyValidatorContext context) - { - if (context.PropertyValue == null) return true; - - return !_seriesService.GetAllSeries().Any(s => context.PropertyValue.ToString().IsParentPath(s.Path)); - } - } -} \ No newline at end of file diff --git a/src/NzbDrone.Core/Validation/Paths/SeriesExistsValidator.cs b/src/NzbDrone.Core/Validation/Paths/SeriesExistsValidator.cs deleted file mode 100644 index 21e4ea629..000000000 --- a/src/NzbDrone.Core/Validation/Paths/SeriesExistsValidator.cs +++ /dev/null @@ -1,26 +0,0 @@ -using System; -using FluentValidation.Validators; -using NzbDrone.Core.Tv; - -namespace NzbDrone.Core.Validation.Paths -{ - public class SeriesExistsValidator : PropertyValidator - { - private readonly ISeriesService _seriesService; - - public SeriesExistsValidator(ISeriesService seriesService) - : base("This series has already been added") - { - _seriesService = seriesService; - } - - protected override bool IsValid(PropertyValidatorContext context) - { - if (context.PropertyValue == null) return true; - - var tvdbId = Convert.ToInt32(context.PropertyValue.ToString()); - - return (!_seriesService.GetAllSeries().Exists(s => s.TvdbId == tvdbId)); - } - } -} \ No newline at end of file diff --git a/src/NzbDrone.Core/Validation/Paths/SeriesPathValidator.cs b/src/NzbDrone.Core/Validation/Paths/SeriesPathValidator.cs deleted file mode 100644 index fa4d8fa59..000000000 --- a/src/NzbDrone.Core/Validation/Paths/SeriesPathValidator.cs +++ /dev/null @@ -1,27 +0,0 @@ -using FluentValidation.Validators; -using NzbDrone.Common.Extensions; -using NzbDrone.Core.Tv; - -namespace NzbDrone.Core.Validation.Paths -{ - public class SeriesPathValidator : PropertyValidator - { - private readonly ISeriesService _seriesService; - - public SeriesPathValidator(ISeriesService seriesService) - : base("Path is already configured for another series") - { - _seriesService = seriesService; - } - - protected override bool IsValid(PropertyValidatorContext context) - { - if (context.PropertyValue == null) return true; - - dynamic instance = context.ParentContext.InstanceToValidate; - var instanceId = (int)instance.Id; - - return (!_seriesService.GetAllSeries().Exists(s => s.Path.PathEquals(context.PropertyValue.ToString()) && s.Id != instanceId)); - } - } -} \ No newline at end of file diff --git a/src/NzbDrone.Core/Validation/ProfileExistsValidator.cs b/src/NzbDrone.Core/Validation/ProfileExistsValidator.cs index e7ff62b67..0eb5293dd 100644 --- a/src/NzbDrone.Core/Validation/ProfileExistsValidator.cs +++ b/src/NzbDrone.Core/Validation/ProfileExistsValidator.cs @@ -1,5 +1,5 @@ using FluentValidation.Validators; -using NzbDrone.Core.Profiles; +using NzbDrone.Core.Profiles.Qualities; namespace NzbDrone.Core.Validation { diff --git a/src/NzbDrone.Core/Validation/RuleBuilderExtensions.cs b/src/NzbDrone.Core/Validation/RuleBuilderExtensions.cs index fe4d97860..fb4f14c32 100644 --- a/src/NzbDrone.Core/Validation/RuleBuilderExtensions.cs +++ b/src/NzbDrone.Core/Validation/RuleBuilderExtensions.cs @@ -1,4 +1,4 @@ -using System.Text.RegularExpressions; +using System.Text.RegularExpressions; using FluentValidation; using FluentValidation.Validators; using NzbDrone.Core.Parser; @@ -34,9 +34,9 @@ namespace NzbDrone.Core.Validation return ruleBuilder.SetValidator(new RegularExpressionValidator("^https?://[-_a-z0-9.]+", RegexOptions.IgnoreCase)).WithMessage("must be valid URL that starts with http(s)://"); } - public static IRuleBuilderOptions<T, string> ValidUrlBase<T>(this IRuleBuilder<T, string> ruleBuilder) + public static IRuleBuilderOptions<T, string> ValidUrlBase<T>(this IRuleBuilder<T, string> ruleBuilder, string example = "/lidarr") { - return ruleBuilder.SetValidator(new RegularExpressionValidator(@"^(?!\/?https?://[-_a-z0-9.]+)", RegexOptions.IgnoreCase)).WithMessage("Must be a valid URL path (ie: '/Lidarr')"); + return ruleBuilder.SetValidator(new RegularExpressionValidator(@"^(?!\/?https?://[-_a-z0-9.]+)", RegexOptions.IgnoreCase)).WithMessage($"Must be a valid URL path (ie: '{example}')"); } public static IRuleBuilderOptions<T, int> ValidPort<T>(this IRuleBuilder<T, int> ruleBuilder) @@ -58,14 +58,9 @@ namespace NzbDrone.Core.Validation }); } - public static IRuleBuilderOptions<T, Language> ValidLanguage<T>(this IRuleBuilder<T, Language> ruleBuilder) - { - return ruleBuilder.SetValidator(new LanguageValidator()); - } - public static IRuleBuilderOptions<T, TProp> AsWarning<T, TProp>(this IRuleBuilderOptions<T, TProp> ruleBuilder) { return ruleBuilder.WithState(v => NzbDroneValidationState.Warning); } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Core/packages.config b/src/NzbDrone.Core/packages.config index 6b185ed7d..ec02cf109 100644 --- a/src/NzbDrone.Core/packages.config +++ b/src/NzbDrone.Core/packages.config @@ -1,15 +1,15 @@ <?xml version="1.0" encoding="utf-8"?> <packages> - <package id="FluentMigrator" version="1.6.2" targetFramework="net40" /> - <package id="FluentMigrator.Runner" version="1.6.2" targetFramework="net40" /> - <package id="FluentValidation" version="6.2.1.0" targetFramework="net40" /> - <package id="ImageResizer" version="3.4.3" targetFramework="net40" /> - <package id="Newtonsoft.Json" version="9.0.1" targetFramework="net40" /> - <package id="NLog" version="4.4.3" targetFramework="net40" /> - <package id="OAuth" version="1.0.3" targetFramework="net40" /> - <package id="Prowlin" version="0.9.4456.26422" targetFramework="net40" /> - <package id="RestSharp" version="105.2.3" targetFramework="net40" /> - <package id="taglib" version="2.1.0.0" targetFramework="net40" /> - <package id="TinyTwitter" version="1.1.2" targetFramework="net40" /> - <package id="xmlrpcnet" version="2.5.0" targetFramework="net40" /> + <package id="FluentMigrator" version="1.6.2" targetFramework="net461" /> + <package id="FluentMigrator.Runner" version="1.6.2" targetFramework="net461" /> + <package id="FluentValidation" version="6.2.1.0" targetFramework="net461" /> + <package id="ImageResizer" version="3.4.3" targetFramework="net461" /> + <package id="Newtonsoft.Json" version="9.0.1" targetFramework="net461" /> + <package id="NLog" version="4.4.12" targetFramework="net461" /> + <package id="OAuth" version="1.0.3" targetFramework="net461" /> + <package id="Prowlin" version="0.9.4456.26422" targetFramework="net461" /> + <package id="RestSharp" version="105.2.3" targetFramework="net461" /> + <package id="taglib" version="2.1.0.0" targetFramework="net461" /> + <package id="TinyTwitter" version="1.1.2" targetFramework="net461" /> + <package id="xmlrpcnet" version="2.5.0" targetFramework="net461" /> </packages> \ No newline at end of file diff --git a/src/NzbDrone.Host/AccessControl/UrlAclAdapter.cs b/src/NzbDrone.Host/AccessControl/UrlAclAdapter.cs index fd483479b..35c2dee83 100644 --- a/src/NzbDrone.Host/AccessControl/UrlAclAdapter.cs +++ b/src/NzbDrone.Host/AccessControl/UrlAclAdapter.cs @@ -32,7 +32,7 @@ namespace NzbDrone.Host.AccessControl } private List<UrlAcl> InternalUrls { get; } - private List<UrlAcl> RegisteredUrls { get; } + private List<UrlAcl> RegisteredUrls { get; set; } private static readonly Regex UrlAclRegex = new Regex(@"(?<scheme>https?)\:\/\/(?<address>.+?)\:(?<port>\d+)/(?<urlbase>.+)?", RegexOptions.Compiled | RegexOptions.IgnoreCase); @@ -49,11 +49,16 @@ namespace NzbDrone.Host.AccessControl _logger = logger; InternalUrls = new List<UrlAcl>(); - RegisteredUrls = GetRegisteredUrls(); + RegisteredUrls = new List<UrlAcl>(); } public void ConfigureUrls() { + if (RegisteredUrls.Empty()) + { + GetRegisteredUrls(); + } + var localHostHttpUrls = BuildUrlAcls("http", "localhost", _configFileProvider.Port); var interfaceHttpUrls = BuildUrlAcls("http", _configFileProvider.BindAddress, _configFileProvider.Port); @@ -128,19 +133,24 @@ namespace NzbDrone.Host.AccessControl c.UrlBase == urlAcl.UrlBase); } - private List<UrlAcl> GetRegisteredUrls() + private void GetRegisteredUrls() { if (OsInfo.IsNotWindows) { - return new List<UrlAcl>(); + return; + } + + if (RegisteredUrls.Any()) + { + return; } var arguments = string.Format("http show urlacl"); var output = _netshProvider.Run(arguments); - if (output == null || !output.Standard.Any()) return new List<UrlAcl>(); + if (output == null || !output.Standard.Any()) return; - return output.Standard.Select(line => + RegisteredUrls = output.Standard.Select(line => { var match = UrlAclRegex.Match(line.Content); diff --git a/src/NzbDrone.Host/ApplicationServer.cs b/src/NzbDrone.Host/ApplicationServer.cs index 29d56304e..b884a9cb8 100644 --- a/src/NzbDrone.Host/ApplicationServer.cs +++ b/src/NzbDrone.Host/ApplicationServer.cs @@ -1,8 +1,10 @@ -using System; +using System; using System.ServiceProcess; using NLog; +using NzbDrone.Common.Composition; using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Core.Configuration; +using NzbDrone.Core.Datastore; using NzbDrone.Core.Lifecycle; using NzbDrone.Core.Messaging.Events; using NzbDrone.Host.Owin; @@ -22,6 +24,7 @@ namespace NzbDrone.Host private readonly IHostController _hostController; private readonly IStartupContext _startupContext; private readonly IBrowserService _browserService; + private readonly IContainer _container; private readonly Logger _logger; public NzbDroneServiceFactory(IConfigFileProvider configFileProvider, @@ -29,6 +32,7 @@ namespace NzbDrone.Host IRuntimeInfo runtimeInfo, IStartupContext startupContext, IBrowserService browserService, + IContainer container, Logger logger) { _configFileProvider = configFileProvider; @@ -36,6 +40,7 @@ namespace NzbDrone.Host _runtimeInfo = runtimeInfo; _startupContext = startupContext; _browserService = browserService; + _container = container; _logger = logger; } @@ -52,6 +57,7 @@ namespace NzbDrone.Host } _runtimeInfo.IsExiting = false; + DbFactory.RegisterDatabase(_container); _hostController.StartServer(); if (!_startupContext.Flags.Contains(StartupContext.NO_BROWSER) @@ -93,4 +99,4 @@ namespace NzbDrone.Host } } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Host/Bootstrap.cs b/src/NzbDrone.Host/Bootstrap.cs index 849c8e13f..69c15dd16 100644 --- a/src/NzbDrone.Host/Bootstrap.cs +++ b/src/NzbDrone.Host/Bootstrap.cs @@ -1,13 +1,14 @@ -using System; +using System; using System.Reflection; using System.Threading; using NLog; using NzbDrone.Common.Composition; using NzbDrone.Common.EnvironmentInfo; +using NzbDrone.Common.Exceptions; using NzbDrone.Common.Instrumentation; using NzbDrone.Common.Processes; using NzbDrone.Common.Security; -using NzbDrone.Core.Datastore; +using NzbDrone.Core.Configuration; using NzbDrone.Core.Instrumentation; namespace NzbDrone.Host @@ -49,9 +50,13 @@ namespace NzbDrone.Host SpinToExit(appMode); } } - catch (TerminateApplicationException e) + catch (InvalidConfigFileException ex) { - Logger.Info(e.Message); + throw new LidarrStartupException(ex); + } + catch (TerminateApplicationException ex) + { + Logger.Info(ex.Message); LogManager.Configuration = null; } } @@ -70,7 +75,6 @@ namespace NzbDrone.Host EnsureSingleInstance(applicationModes == ApplicationModes.Service, startupContext); } - DbFactory.RegisterDatabase(_container); _container.Resolve<Router>().Route(applicationModes); } @@ -88,11 +92,15 @@ namespace NzbDrone.Host { var instancePolicy = _container.Resolve<ISingleInstancePolicy>(); - if (isService) + if (startupContext.Flags.Contains(StartupContext.TERMINATE)) { instancePolicy.KillAllOtherInstance(); } - else if (startupContext.Flags.Contains(StartupContext.TERMINATE)) + else if (startupContext.Args.ContainsKey(StartupContext.APPDATA)) + { + instancePolicy.WarnIfAlreadyRunning(); + } + else if (isService) { instancePolicy.KillAllOtherInstance(); } diff --git a/src/NzbDrone.Host/MainAppContainerBuilder.cs b/src/NzbDrone.Host/MainAppContainerBuilder.cs index 23ba6a0dd..86dfc9b93 100644 --- a/src/NzbDrone.Host/MainAppContainerBuilder.cs +++ b/src/NzbDrone.Host/MainAppContainerBuilder.cs @@ -1,6 +1,6 @@ -using System.Collections.Generic; +using System.Collections.Generic; using Nancy.Bootstrapper; -using NzbDrone.Api; +using Lidarr.Http; using NzbDrone.Common.Composition; using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.Http.Dispatchers; @@ -14,10 +14,11 @@ namespace NzbDrone.Host { var assemblies = new List<string> { - "NzbDrone.Host", - "NzbDrone.Core", - "NzbDrone.Api", - "NzbDrone.SignalR" + "Lidarr.Host", + "Lidarr.Core", + "Lidarr.SignalR", + "Lidarr.Api.V3", + "Lidarr.Http" }; return new MainAppContainerBuilder(args, assemblies).Container; @@ -28,8 +29,8 @@ namespace NzbDrone.Host { AutoRegisterImplementations<NzbDronePersistentConnection>(); - Container.Register<INancyBootstrapper, NancyBootstrapper>(); + Container.Register<INancyBootstrapper, LidarrBootstrapper>(); Container.Register<IHttpDispatcher, FallbackHttpDispatcher>(); } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Host/NzbDrone.Host.csproj b/src/NzbDrone.Host/NzbDrone.Host.csproj index 6a203bae9..64b28955f 100644 --- a/src/NzbDrone.Host/NzbDrone.Host.csproj +++ b/src/NzbDrone.Host/NzbDrone.Host.csproj @@ -1,5 +1,5 @@ <?xml version="1.0" encoding="utf-8"?> -<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> +<Project ToolsVersion="12.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> <PropertyGroup> <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration> <Platform Condition=" '$(Platform)' == '' ">x86</Platform> @@ -9,8 +9,8 @@ <OutputType>Library</OutputType> <AppDesignerFolder>Properties</AppDesignerFolder> <RootNamespace>NzbDrone.Host</RootNamespace> - <AssemblyName>NzbDrone.Host</AssemblyName> - <TargetFrameworkVersion>v4.0</TargetFrameworkVersion> + <AssemblyName>Lidarr.Host</AssemblyName> + <TargetFrameworkVersion>v4.6.1</TargetFrameworkVersion> <FileAlignment>512</FileAlignment> <TargetFrameworkProfile> </TargetFrameworkProfile> @@ -43,6 +43,7 @@ <WarningLevel>4</WarningLevel> <UseVSHostingProcess>true</UseVSHostingProcess> <CodeAnalysisRuleSet>BasicCorrectnessRules.ruleset</CodeAnalysisRuleSet> + <Prefer32Bit>false</Prefer32Bit> </PropertyGroup> <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|x86' "> <PlatformTarget>x86</PlatformTarget> @@ -52,37 +53,50 @@ <DefineConstants>TRACE</DefineConstants> <ErrorReport>prompt</ErrorReport> <WarningLevel>4</WarningLevel> + <Prefer32Bit>false</Prefer32Bit> </PropertyGroup> <PropertyGroup> <RunPostBuildEvent>OnOutputUpdated</RunPostBuildEvent> </PropertyGroup> <ItemGroup> - <Reference Include="Microsoft.Owin, Version=2.1.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL"> - <SpecificVersion>False</SpecificVersion> - <HintPath>..\packages\Microsoft.Owin.2.1.0\lib\net40\Microsoft.Owin.dll</HintPath> + <Reference Include="Microsoft.AspNet.SignalR.Core, Version=2.2.2.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL"> + <HintPath>..\packages\Microsoft.AspNet.SignalR.Core.2.2.2\lib\net45\Microsoft.AspNet.SignalR.Core.dll</HintPath> </Reference> - <Reference Include="Microsoft.Owin.Host.HttpListener, Version=2.1.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL"> - <SpecificVersion>False</SpecificVersion> - <HintPath>..\packages\Microsoft.Owin.Host.HttpListener.2.1.0\lib\net40\Microsoft.Owin.Host.HttpListener.dll</HintPath> + <Reference Include="Microsoft.AspNet.SignalR.SystemWeb, Version=2.2.2.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL"> + <HintPath>..\packages\Microsoft.AspNet.SignalR.SystemWeb.2.2.2\lib\net45\Microsoft.AspNet.SignalR.SystemWeb.dll</HintPath> </Reference> - <Reference Include="Microsoft.Owin.Hosting, Version=2.1.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL"> - <SpecificVersion>False</SpecificVersion> - <HintPath>..\packages\Microsoft.Owin.Hosting.2.1.0\lib\net40\Microsoft.Owin.Hosting.dll</HintPath> + <Reference Include="Microsoft.Owin, Version=3.1.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL"> + <HintPath>..\packages\Microsoft.Owin.3.1.0\lib\net45\Microsoft.Owin.dll</HintPath> </Reference> - <Reference Include="Nancy, Version=1.4.2.0, Culture=neutral, processorArchitecture=MSIL"> - <HintPath>..\packages\Nancy.1.4.3\lib\net40\Nancy.dll</HintPath> - <Private>True</Private> + <Reference Include="Microsoft.Owin.Diagnostics, Version=3.1.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL"> + <HintPath>..\packages\Microsoft.Owin.Diagnostics.3.1.0\lib\net45\Microsoft.Owin.Diagnostics.dll</HintPath> + </Reference> + <Reference Include="Microsoft.Owin.Host.HttpListener, Version=3.1.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL"> + <HintPath>..\packages\Microsoft.Owin.Host.HttpListener.3.1.0\lib\net45\Microsoft.Owin.Host.HttpListener.dll</HintPath> + </Reference> + <Reference Include="Microsoft.Owin.Host.SystemWeb, Version=3.1.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL"> + <HintPath>..\packages\Microsoft.Owin.Host.SystemWeb.3.1.0\lib\net45\Microsoft.Owin.Host.SystemWeb.dll</HintPath> + </Reference> + <Reference Include="Microsoft.Owin.Hosting, Version=3.1.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL"> + <HintPath>..\packages\Microsoft.Owin.Hosting.3.1.0\lib\net45\Microsoft.Owin.Hosting.dll</HintPath> + </Reference> + <Reference Include="Microsoft.Owin.Security, Version=3.1.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL"> + <HintPath>..\packages\Microsoft.Owin.Security.3.1.0\lib\net45\Microsoft.Owin.Security.dll</HintPath> + </Reference> + <Reference Include="Nancy, Version=1.4.4.0, Culture=neutral, processorArchitecture=MSIL"> + <HintPath>..\packages\Nancy.1.4.4\lib\net40\Nancy.dll</HintPath> </Reference> <Reference Include="Nancy.Owin, Version=1.4.1.0, Culture=neutral, processorArchitecture=MSIL"> <HintPath>..\packages\Nancy.Owin.1.4.1\lib\net40\Nancy.Owin.dll</HintPath> - <Private>True</Private> </Reference> <Reference Include="Newtonsoft.Json, Version=9.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL"> - <HintPath>..\packages\Newtonsoft.Json.9.0.1\lib\net40\Newtonsoft.Json.dll</HintPath> - <Private>True</Private> + <HintPath>..\packages\Newtonsoft.Json.9.0.1\lib\net45\Newtonsoft.Json.dll</HintPath> </Reference> <Reference Include="NLog, Version=4.0.0.0, Culture=neutral, PublicKeyToken=5120e14c03d0593c, processorArchitecture=MSIL"> - <HintPath>..\packages\NLog.4.4.3\lib\net40\NLog.dll</HintPath> + <HintPath>..\packages\NLog.4.4.12\lib\net45\NLog.dll</HintPath> + </Reference> + <Reference Include="Owin, Version=1.0.0.0, Culture=neutral, PublicKeyToken=f0ebd12fd5e55cc5, processorArchitecture=MSIL"> + <HintPath>..\packages\Owin.1.0\lib\net40\Owin.dll</HintPath> </Reference> <Reference Include="System" /> <Reference Include="System.Core" /> @@ -91,9 +105,6 @@ <HintPath>..\Libraries\Interop.NetFwTypeLib.dll</HintPath> <EmbedInteropTypes>True</EmbedInteropTypes> </Reference> - <Reference Include="Owin"> - <HintPath>..\packages\Owin.1.0\lib\net40\Owin.dll</HintPath> - </Reference> </ItemGroup> <ItemGroup> <Compile Include="..\NzbDrone.Common\Properties\SharedAssemblyInfo.cs"> @@ -156,18 +167,6 @@ </BootstrapperPackage> </ItemGroup> <ItemGroup> - <ProjectReference Include="..\Microsoft.AspNet.SignalR.Core\Microsoft.AspNet.SignalR.Core.csproj"> - <Project>{1B9A82C4-BCA1-4834-A33E-226F17BE070B}</Project> - <Name>Microsoft.AspNet.SignalR.Core</Name> - </ProjectReference> - <ProjectReference Include="..\Microsoft.AspNet.SignalR.Owin\Microsoft.AspNet.SignalR.Owin.csproj"> - <Project>{2B8C6DAD-4D85-41B1-83FD-248D9F347522}</Project> - <Name>Microsoft.AspNet.SignalR.Owin</Name> - </ProjectReference> - <ProjectReference Include="..\NzbDrone.Api\NzbDrone.Api.csproj"> - <Project>{FD286DF8-2D3A-4394-8AD5-443FADE55FB2}</Project> - <Name>NzbDrone.Api</Name> - </ProjectReference> <ProjectReference Include="..\NzbDrone.Common\NzbDrone.Common.csproj"> <Project>{F2BE0FDF-6E47-4827-A420-DD4EF82407F8}</Project> <Name>NzbDrone.Common</Name> @@ -180,6 +179,14 @@ <Project>{7C2CC69F-5CA0-4E5C-85CB-983F9F6C3B36}</Project> <Name>NzbDrone.SignalR</Name> </ProjectReference> + <ProjectReference Include="..\Lidarr.Api.V3\Lidarr.Api.V3.csproj"> + <Project>{7140ff1f-79be-492f-9188-b21a050bf708}</Project> + <Name>Lidarr.Api.V3</Name> + </ProjectReference> + <ProjectReference Include="..\Lidarr.Http\Lidarr.Http.csproj"> + <Project>{5370bff7-1bd7-46bc-af06-7d9ea5cda1d6}</Project> + <Name>Lidarr.Http</Name> + </ProjectReference> </ItemGroup> <ItemGroup> <Content Include="NzbDrone.ico" /> diff --git a/src/NzbDrone.Host/Owin/MiddleWare/NzbDroneVersionMiddleWare.cs b/src/NzbDrone.Host/Owin/MiddleWare/NzbDroneVersionMiddleWare.cs index a74d9b1d3..5ef687b74 100644 --- a/src/NzbDrone.Host/Owin/MiddleWare/NzbDroneVersionMiddleWare.cs +++ b/src/NzbDrone.Host/Owin/MiddleWare/NzbDroneVersionMiddleWare.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Threading.Tasks; using Microsoft.Owin; using NzbDrone.Common.EnvironmentInfo; @@ -26,10 +26,10 @@ namespace NzbDrone.Host.Owin.MiddleWare _versionHeader = new KeyValuePair<string, string[]>("X-ApplicationVersion", new[] { BuildInfo.Version.ToString() }); } - public override Task Invoke(IOwinContext context) + public override async Task Invoke(IOwinContext context) { context.Response.Headers.Add(_versionHeader); - return Next.Invoke(context); + await Next.Invoke(context); } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Host/Owin/MiddleWare/SignalRMiddleWare.cs b/src/NzbDrone.Host/Owin/MiddleWare/SignalRMiddleWare.cs index 0df60a326..8134b6928 100644 --- a/src/NzbDrone.Host/Owin/MiddleWare/SignalRMiddleWare.cs +++ b/src/NzbDrone.Host/Owin/MiddleWare/SignalRMiddleWare.cs @@ -12,14 +12,17 @@ namespace NzbDrone.Host.Owin.MiddleWare public SignalRMiddleWare(IContainer container) { - SignalrDependencyResolver.Register(container); + SignalRDependencyResolver.Register(container); + SignalRJsonSerializer.Register(); + // Half the default time (110s) to get under nginx's default 60 proxy_read_timeout + GlobalHost.Configuration.ConnectionTimeout = TimeSpan.FromSeconds(55); GlobalHost.Configuration.DisconnectTimeout = TimeSpan.FromMinutes(3); } public void Attach(IAppBuilder appBuilder) { - appBuilder.MapConnection("signalr", typeof(NzbDronePersistentConnection), new ConnectionConfiguration { EnableCrossDomain = true }); + appBuilder.MapConnection("/signalr", typeof(NzbDronePersistentConnection), new ConnectionConfiguration ()); } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Host/Properties/AssemblyInfo.cs b/src/NzbDrone.Host/Properties/AssemblyInfo.cs index 4ed833932..0be525adf 100644 --- a/src/NzbDrone.Host/Properties/AssemblyInfo.cs +++ b/src/NzbDrone.Host/Properties/AssemblyInfo.cs @@ -8,4 +8,3 @@ using System.Runtime.InteropServices; [assembly: AssemblyTitle("Lidarr.exe")] [assembly: Guid("C2172AF4-F9A6-4D91-BAEE-C2E4EE680613")] -[assembly: AssemblyVersion("10.0.0.*")] diff --git a/src/NzbDrone.Host/Router.cs b/src/NzbDrone.Host/Router.cs index 72d1c8f67..f6f5f7a4d 100644 --- a/src/NzbDrone.Host/Router.cs +++ b/src/NzbDrone.Host/Router.cs @@ -1,5 +1,10 @@ -using NLog; +using System; +using NLog; using NzbDrone.Common; +using NzbDrone.Common.EnvironmentInfo; +using NzbDrone.Common.Processes; +using IServiceProvider = NzbDrone.Common.IServiceProvider; + namespace NzbDrone.Host { @@ -8,14 +13,22 @@ namespace NzbDrone.Host private readonly INzbDroneServiceFactory _nzbDroneServiceFactory; private readonly IServiceProvider _serviceProvider; private readonly IConsoleService _consoleService; + private readonly IRuntimeInfo _runtimeInfo; + private readonly IProcessProvider _processProvider; private readonly Logger _logger; - public Router(INzbDroneServiceFactory nzbDroneServiceFactory, IServiceProvider serviceProvider, - IConsoleService consoleService, Logger logger) + public Router(INzbDroneServiceFactory nzbDroneServiceFactory, + IServiceProvider serviceProvider, + IConsoleService consoleService, + IRuntimeInfo runtimeInfo, + IProcessProvider processProvider, + Logger logger) { _nzbDroneServiceFactory = nzbDroneServiceFactory; _serviceProvider = serviceProvider; _consoleService = consoleService; + _runtimeInfo = runtimeInfo; + _processProvider = processProvider; _logger = logger; } @@ -34,34 +47,38 @@ namespace NzbDrone.Host case ApplicationModes.Interactive: { - _logger.Debug("Console selected"); + _logger.Debug(_runtimeInfo.IsWindowsTray ? "Tray selected" : "Console selected"); _nzbDroneServiceFactory.Start(); break; } case ApplicationModes.InstallService: { _logger.Debug("Install Service selected"); - if (_serviceProvider.ServiceExist(ServiceProvider.NZBDRONE_SERVICE_NAME)) + if (_serviceProvider.ServiceExist(ServiceProvider.SERVICE_NAME)) { _consoleService.PrintServiceAlreadyExist(); } else { - _serviceProvider.Install(ServiceProvider.NZBDRONE_SERVICE_NAME); - _serviceProvider.Start(ServiceProvider.NZBDRONE_SERVICE_NAME); + _serviceProvider.Install(ServiceProvider.SERVICE_NAME); + _serviceProvider.SetPermissions(ServiceProvider.SERVICE_NAME); + + // Start the service and exit. + // Ensures that there isn't an instance of Sonarr already running that the service account cannot stop. + _processProvider.SpawnNewProcess("sc.exe", $"start {ServiceProvider.SERVICE_NAME}", null, true); } break; } case ApplicationModes.UninstallService: { _logger.Debug("Uninstall Service selected"); - if (!_serviceProvider.ServiceExist(ServiceProvider.NZBDRONE_SERVICE_NAME)) + if (!_serviceProvider.ServiceExist(ServiceProvider.SERVICE_NAME)) { _consoleService.PrintServiceDoesNotExist(); } else { - _serviceProvider.UnInstall(ServiceProvider.NZBDRONE_SERVICE_NAME); + _serviceProvider.Uninstall(ServiceProvider.SERVICE_NAME); } break; diff --git a/src/NzbDrone.Host/SingleInstancePolicy.cs b/src/NzbDrone.Host/SingleInstancePolicy.cs index 6242aac2d..0235b9845 100644 --- a/src/NzbDrone.Host/SingleInstancePolicy.cs +++ b/src/NzbDrone.Host/SingleInstancePolicy.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using NLog; @@ -10,6 +10,7 @@ namespace NzbDrone.Host { void PreventStartIfAlreadyRunning(); void KillAllOtherInstance(); + void WarnIfAlreadyRunning(); } public class SingleInstancePolicy : ISingleInstancePolicy @@ -45,6 +46,14 @@ namespace NzbDrone.Host } } + public void WarnIfAlreadyRunning() + { + if (IsAlreadyRunning()) + { + _logger.Debug("Another instance of Lidarr is already running."); + } + } + private bool IsAlreadyRunning() { return GetOtherNzbDroneProcessIds().Any(); @@ -56,8 +65,8 @@ namespace NzbDrone.Host { var currentId = _processProvider.GetCurrentProcess().Id; - var otherProcesses = _processProvider.FindProcessByName(ProcessProvider.NZB_DRONE_CONSOLE_PROCESS_NAME) - .Union(_processProvider.FindProcessByName(ProcessProvider.NZB_DRONE_PROCESS_NAME)) + var otherProcesses = _processProvider.FindProcessByName(ProcessProvider.LIDARR_CONSOLE_PROCESS_NAME) + .Union(_processProvider.FindProcessByName(ProcessProvider.LIDARR_PROCESS_NAME)) .Select(c => c.Id) .Except(new[] { currentId }) .ToList(); @@ -76,4 +85,4 @@ namespace NzbDrone.Host } } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Host/app.config b/src/NzbDrone.Host/app.config index 885d99232..2c6b0faf8 100644 --- a/src/NzbDrone.Host/app.config +++ b/src/NzbDrone.Host/app.config @@ -6,7 +6,7 @@ </connectionManagement> </system.net> <startup useLegacyV2RuntimeActivationPolicy="true"> - <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.0" /> + <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.6.1" /> </startup> <runtime> <loadFromRemoteSources enabled="true" /> @@ -30,7 +30,11 @@ </dependentAssembly> <dependentAssembly> <assemblyIdentity name="Microsoft.Owin" publicKeyToken="31bf3856ad364e35" culture="neutral" /> - <bindingRedirect oldVersion="0.0.0.0-2.1.0.0" newVersion="2.1.0.0" /> + <bindingRedirect oldVersion="0.0.0.0-3.1.0.0" newVersion="3.1.0.0" /> + </dependentAssembly> + <dependentAssembly> + <assemblyIdentity name="Microsoft.Owin.Security" publicKeyToken="31bf3856ad364e35" culture="neutral" /> + <bindingRedirect oldVersion="0.0.0.0-3.1.0.0" newVersion="3.1.0.0" /> </dependentAssembly> </assemblyBinding> </runtime> diff --git a/src/NzbDrone.Host/packages.config b/src/NzbDrone.Host/packages.config index cd7fd0969..ef5bb1111 100644 --- a/src/NzbDrone.Host/packages.config +++ b/src/NzbDrone.Host/packages.config @@ -1,11 +1,18 @@ <?xml version="1.0" encoding="utf-8"?> <packages> - <package id="Microsoft.Owin" version="2.1.0" targetFramework="net40" /> - <package id="Microsoft.Owin.Host.HttpListener" version="2.1.0" targetFramework="net40" /> - <package id="Microsoft.Owin.Hosting" version="2.1.0" targetFramework="net40" /> - <package id="Nancy" version="1.4.3" targetFramework="net40" /> - <package id="Nancy.Owin" version="1.4.1" targetFramework="net40" /> - <package id="Newtonsoft.Json" version="9.0.1" targetFramework="net40" /> - <package id="NLog" version="4.4.3" targetFramework="net40" /> - <package id="Owin" version="1.0" targetFramework="net40" /> + <package id="Microsoft.AspNet.SignalR.Core" version="2.2.2" targetFramework="net461" /> + <package id="Microsoft.AspNet.SignalR.SelfHost" version="2.2.2" targetFramework="net461" /> + <package id="Microsoft.AspNet.SignalR.SystemWeb" version="2.2.2" targetFramework="net461" /> + <package id="Microsoft.Owin" version="3.1.0" targetFramework="net461" /> + <package id="Microsoft.Owin.Diagnostics" version="3.1.0" targetFramework="net461" /> + <package id="Microsoft.Owin.Host.HttpListener" version="3.1.0" targetFramework="net461" /> + <package id="Microsoft.Owin.Host.SystemWeb" version="3.1.0" targetFramework="net461" /> + <package id="Microsoft.Owin.Hosting" version="3.1.0" targetFramework="net461" /> + <package id="Microsoft.Owin.Security" version="3.1.0" targetFramework="net461" /> + <package id="Microsoft.Owin.SelfHost" version="3.1.0" targetFramework="net461" /> + <package id="Nancy" version="1.4.4" targetFramework="net461" /> + <package id="Nancy.Owin" version="1.4.1" targetFramework="net461" /> + <package id="Newtonsoft.Json" version="9.0.1" targetFramework="net461" /> + <package id="NLog" version="4.4.12" targetFramework="net461" /> + <package id="Owin" version="1.0" targetFramework="net461" /> </packages> \ No newline at end of file diff --git a/src/NzbDrone.Integration.Test/ApiTests/ArtistEditorFixture.cs b/src/NzbDrone.Integration.Test/ApiTests/ArtistEditorFixture.cs index 8cda79bf1..e4363ad12 100644 --- a/src/NzbDrone.Integration.Test/ApiTests/ArtistEditorFixture.cs +++ b/src/NzbDrone.Integration.Test/ApiTests/ArtistEditorFixture.cs @@ -1,5 +1,6 @@ -using FluentAssertions; +using FluentAssertions; using NUnit.Framework; +using Lidarr.Api.V3.Artist; using System.Linq; using NzbDrone.Test.Common; @@ -10,11 +11,12 @@ namespace NzbDrone.Integration.Test.ApiTests { private void GivenExistingArtist() { - foreach (var name in new[] { "90210", "Dexter" }) + foreach (var name in new[] { "Alien Ant Farm", "Kiss" }) { var newArtist = Artist.Lookup(name).First(); - newArtist.ProfileId = 1; + newArtist.QualityProfileId = 1; + newArtist.LanguageProfileId = 1; newArtist.Path = string.Format(@"C:\Test\{0}", name).AsOsAgnostic(); Artist.Post(newArtist); @@ -28,15 +30,16 @@ namespace NzbDrone.Integration.Test.ApiTests var artist = Artist.All(); - foreach (var s in artist) + var artistEditor = new ArtistEditorResource { - s.ProfileId = 2; - } + QualityProfileId = 2, + ArtistIds = artist.Select(o => o.Id).ToList() + }; - var result = Artist.Editor(artist); + var result = Artist.Editor(artistEditor); result.Should().HaveCount(2); - result.TrueForAll(s => s.ProfileId == 2).Should().BeTrue(); + result.TrueForAll(s => s.QualityProfileId == 2).Should().BeTrue(); } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Integration.Test/ApiTests/ArtistFixture.cs b/src/NzbDrone.Integration.Test/ApiTests/ArtistFixture.cs index b51db599e..71aba2717 100644 --- a/src/NzbDrone.Integration.Test/ApiTests/ArtistFixture.cs +++ b/src/NzbDrone.Integration.Test/ApiTests/ArtistFixture.cs @@ -1,4 +1,4 @@ -using FluentAssertions; +using FluentAssertions; using NUnit.Framework; using System.Linq; using System.IO; @@ -12,13 +12,14 @@ namespace NzbDrone.Integration.Test.ApiTests [Test, Order(0)] public void add_artist_with_tags_should_store_them() { - EnsureNoArtist("266189", "Alien Ant Farm"); + EnsureNoArtist("f59c5520-5f46-4d2c-b2c4-822eabf53419", "Linkin Park"); var tag = EnsureTag("abc"); - var artist = Artist.Lookup("lidarr:266189").Single(); + var artist = Artist.Lookup("lidarr:f59c5520-5f46-4d2c-b2c4-822eabf53419").Single(); - artist.ProfileId = 1; - artist.Path = Path.Combine(ArtistRootFolder, artist.Name); + artist.QualityProfileId = 1; + artist.LanguageProfileId = 1; + artist.Path = Path.Combine(ArtistRootFolder, artist.ArtistName); artist.Tags = new HashSet<int>(); artist.Tags.Add(tag.Id); @@ -31,11 +32,11 @@ namespace NzbDrone.Integration.Test.ApiTests [Test, Order(0)] public void add_artist_without_profileid_should_return_badrequest() { - EnsureNoArtist("266189", "Alien Ant Farm"); + EnsureNoArtist("f59c5520-5f46-4d2c-b2c4-822eabf53419", "Linkin Park"); - var artist = Artist.Lookup("lidarr:266189").Single(); + var artist = Artist.Lookup("lidarr:f59c5520-5f46-4d2c-b2c4-822eabf53419").Single(); - artist.Path = Path.Combine(ArtistRootFolder, artist.Name); + artist.Path = Path.Combine(ArtistRootFolder, artist.ArtistName); Artist.InvalidPost(artist); } @@ -43,11 +44,12 @@ namespace NzbDrone.Integration.Test.ApiTests [Test, Order(0)] public void add_artist_without_path_should_return_badrequest() { - EnsureNoArtist("266189", "Alien Ant Farm"); + EnsureNoArtist("f59c5520-5f46-4d2c-b2c4-822eabf53419", "Linkin Park"); - var artist = Artist.Lookup("lidarr:266189").Single(); + var artist = Artist.Lookup("lidarr:f59c5520-5f46-4d2c-b2c4-822eabf53419").Single(); - artist.ProfileId = 1; + artist.QualityProfileId = 1; + artist.LanguageProfileId = 1; Artist.InvalidPost(artist); } @@ -55,41 +57,45 @@ namespace NzbDrone.Integration.Test.ApiTests [Test, Order(1)] public void add_artist() { - EnsureNoArtist("266189", "Alien Ant Farm"); + EnsureNoArtist("f59c5520-5f46-4d2c-b2c4-822eabf53419", "Linkin Park"); - var artist = Artist.Lookup("lidarr:266189").Single(); + var artist = Artist.Lookup("lidarr:f59c5520-5f46-4d2c-b2c4-822eabf53419").Single(); - artist.ProfileId = 1; - artist.Path = Path.Combine(ArtistRootFolder, artist.Name); + artist.QualityProfileId = 1; + artist.LanguageProfileId = 1; + artist.Path = Path.Combine(ArtistRootFolder, artist.ArtistName); var result = Artist.Post(artist); result.Should().NotBeNull(); result.Id.Should().NotBe(0); - result.ProfileId.Should().Be(1); - result.Path.Should().Be(Path.Combine(ArtistRootFolder, artist.Name)); + result.QualityProfileId.Should().Be(1); + result.LanguageProfileId.Should().Be(1); + result.Path.Should().Be(Path.Combine(ArtistRootFolder, artist.ArtistName)); } [Test, Order(2)] public void get_all_artist() { - EnsureArtist("266189", "Alien Ant Farm"); - EnsureArtist("73065", "Coldplay"); + EnsureArtist("8ac6cc32-8ddf-43b1-9ac4-4b04f9053176", "Alien Ant Farm"); + EnsureArtist("cc197bad-dc9c-440d-a5b5-d52ba2e14234", "Coldplay"); - Artist.All().Should().NotBeNullOrEmpty(); - Artist.All().Should().Contain(v => v.ForeignArtistId == "73065"); - Artist.All().Should().Contain(v => v.ForeignArtistId == "266189"); + var artists = Artist.All(); + + artists.Should().NotBeNullOrEmpty(); + artists.Should().Contain(v => v.ForeignArtistId == "8ac6cc32-8ddf-43b1-9ac4-4b04f9053176"); + artists.Should().Contain(v => v.ForeignArtistId == "cc197bad-dc9c-440d-a5b5-d52ba2e14234"); } [Test, Order(2)] public void get_artist_by_id() { - var artist = EnsureArtist("266189", "Alien Ant Farm"); + var artist = EnsureArtist("8ac6cc32-8ddf-43b1-9ac4-4b04f9053176", "Alien Ant Farm"); var result = Artist.Get(artist.Id); - result.ForeignArtistId.Should().Be("266189"); + result.ForeignArtistId.Should().Be("8ac6cc32-8ddf-43b1-9ac4-4b04f9053176"); } [Test] @@ -101,25 +107,26 @@ namespace NzbDrone.Integration.Test.ApiTests [Test, Order(2)] public void update_artist_profile_id() { - var artist = EnsureArtist("266189", "Alien Ant Farm"); + var artist = EnsureArtist("8ac6cc32-8ddf-43b1-9ac4-4b04f9053176", "Alien Ant Farm"); var profileId = 1; - if (artist.ProfileId == profileId) + if (artist.QualityProfileId == profileId) { profileId = 2; } - artist.ProfileId = profileId; + artist.QualityProfileId = profileId; + artist.LanguageProfileId = profileId; var result = Artist.Put(artist); - Artist.Get(artist.Id).ProfileId.Should().Be(profileId); + Artist.Get(artist.Id).QualityProfileId.Should().Be(profileId); } [Test, Order(3)] public void update_artist_monitored() { - var artist = EnsureArtist("266189", "Alien Ant Farm", false); + var artist = EnsureArtist("f59c5520-5f46-4d2c-b2c4-822eabf53419", "Linkin Park", false); artist.Monitored.Should().BeFalse(); //artist.Seasons.First().Monitored.Should().BeFalse(); @@ -139,7 +146,7 @@ namespace NzbDrone.Integration.Test.ApiTests [Test, Order(3)] public void update_artist_tags() { - var artist = EnsureArtist("266189", "Alien Ant Farm"); + var artist = EnsureArtist("8ac6cc32-8ddf-43b1-9ac4-4b04f9053176", "Alien Ant Farm"); var tag = EnsureTag("abc"); if (artist.Tags.Contains(tag.Id)) @@ -161,13 +168,13 @@ namespace NzbDrone.Integration.Test.ApiTests [Test, Order(4)] public void delete_artist() { - var artist = EnsureArtist("266189", "Alien Ant Farm"); + var artist = EnsureArtist("8ac6cc32-8ddf-43b1-9ac4-4b04f9053176", "Alien Ant Farm"); Artist.Get(artist.Id).Should().NotBeNull(); Artist.Delete(artist.Id); - Artist.All().Should().NotContain(v => v.ForeignArtistId == "266189"); + Artist.All().Should().NotContain(v => v.ForeignArtistId == "8ac6cc32-8ddf-43b1-9ac4-4b04f9053176"); } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Integration.Test/ApiTests/ArtistLookupFixture.cs b/src/NzbDrone.Integration.Test/ApiTests/ArtistLookupFixture.cs index 1c36a509c..af78cd1b5 100644 --- a/src/NzbDrone.Integration.Test/ApiTests/ArtistLookupFixture.cs +++ b/src/NzbDrone.Integration.Test/ApiTests/ArtistLookupFixture.cs @@ -1,4 +1,4 @@ -using FluentAssertions; +using FluentAssertions; using NUnit.Framework; namespace NzbDrone.Integration.Test.ApiTests @@ -6,32 +6,32 @@ namespace NzbDrone.Integration.Test.ApiTests [TestFixture] public class ArtistLookupFixture : IntegrationTest { - [TestCase("archer", "Archer (2009)")] - [TestCase("90210", "90210")] + [TestCase("Kiss", "Kiss")] + [TestCase("Linkin Park", "Linkin Park")] public void lookup_new_artist_by_name(string term, string name) { var artist = Artist.Lookup(term); artist.Should().NotBeEmpty(); - artist.Should().Contain(c => c.Name == name); + artist.Should().Contain(c => c.ArtistName == name); } [Test] - public void lookup_new_series_by_tvdbid() + public void lookup_new_artist_by_mbid() { - var artist = Artist.Lookup("lidarr:266189"); + var artist = Artist.Lookup("lidarr:f59c5520-5f46-4d2c-b2c4-822eabf53419"); artist.Should().NotBeEmpty(); - artist.Should().Contain(c => c.Name == "The Blacklist"); + artist.Should().Contain(c => c.ArtistName == "Linkin Park"); } [Test] [Ignore("Unreliable")] - public void lookup_random_series_using_asterix() + public void lookup_random_artist_using_asterix() { var artist = Artist.Lookup("*"); artist.Should().NotBeEmpty(); } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Integration.Test/ApiTests/BlacklistFixture.cs b/src/NzbDrone.Integration.Test/ApiTests/BlacklistFixture.cs index af2bf2c40..dfd4cc580 100644 --- a/src/NzbDrone.Integration.Test/ApiTests/BlacklistFixture.cs +++ b/src/NzbDrone.Integration.Test/ApiTests/BlacklistFixture.cs @@ -1,6 +1,7 @@ -using FluentAssertions; +using FluentAssertions; using NUnit.Framework; -using NzbDrone.Api.Music; +using Lidarr.Api.V3.Artist; +using Lidarr.Api.V3.Blacklist; namespace NzbDrone.Integration.Test.ApiTests { @@ -15,7 +16,7 @@ namespace NzbDrone.Integration.Test.ApiTests { _artist = EnsureArtist("8ac6cc32-8ddf-43b1-9ac4-4b04f9053176", "Alien Ant Farm"); - Blacklist.Post(new Api.Blacklist.BlacklistResource + Blacklist.Post(new BlacklistResource { ArtistId = _artist.Id, SourceTitle = "Blacklist - Album 1 [2015 FLAC]" diff --git a/src/NzbDrone.Integration.Test/ApiTests/CalendarFixture.cs b/src/NzbDrone.Integration.Test/ApiTests/CalendarFixture.cs index e1bc9980f..c38ed9f7f 100644 --- a/src/NzbDrone.Integration.Test/ApiTests/CalendarFixture.cs +++ b/src/NzbDrone.Integration.Test/ApiTests/CalendarFixture.cs @@ -1,6 +1,6 @@ -using FluentAssertions; +using FluentAssertions; using NUnit.Framework; -using NzbDrone.Api.Albums; +using Lidarr.Api.V3.Albums; using NzbDrone.Integration.Test.Client; using System; using System.Collections.Generic; diff --git a/src/NzbDrone.Integration.Test/ApiTests/CommandFixture.cs b/src/NzbDrone.Integration.Test/ApiTests/CommandFixture.cs index bf17b6d12..a1a0e597d 100644 --- a/src/NzbDrone.Integration.Test/ApiTests/CommandFixture.cs +++ b/src/NzbDrone.Integration.Test/ApiTests/CommandFixture.cs @@ -1,6 +1,6 @@ -using FluentAssertions; +using FluentAssertions; using NUnit.Framework; -using NzbDrone.Api.Commands; +using Lidarr.Api.V3.Commands; namespace NzbDrone.Integration.Test.ApiTests { @@ -16,4 +16,4 @@ namespace NzbDrone.Integration.Test.ApiTests response.Id.Should().NotBe(0); } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Integration.Test/ApiTests/DiskSpaceFixture.cs b/src/NzbDrone.Integration.Test/ApiTests/DiskSpaceFixture.cs index 527f18346..a64dba210 100644 --- a/src/NzbDrone.Integration.Test/ApiTests/DiskSpaceFixture.cs +++ b/src/NzbDrone.Integration.Test/ApiTests/DiskSpaceFixture.cs @@ -1,7 +1,7 @@ -using System.Linq; +using System.Linq; using FluentAssertions; using NUnit.Framework; -using NzbDrone.Api.DiskSpace; +using Lidarr.Api.V3.DiskSpace; using NzbDrone.Integration.Test.Client; namespace NzbDrone.Integration.Test.ApiTests diff --git a/src/NzbDrone.Integration.Test/ApiTests/ReleaseFixture.cs b/src/NzbDrone.Integration.Test/ApiTests/ReleaseFixture.cs index fc53ac47f..64c7ea250 100644 --- a/src/NzbDrone.Integration.Test/ApiTests/ReleaseFixture.cs +++ b/src/NzbDrone.Integration.Test/ApiTests/ReleaseFixture.cs @@ -1,6 +1,6 @@ -using FluentAssertions; +using FluentAssertions; using NUnit.Framework; -using NzbDrone.Api.Indexers; +using Lidarr.Api.V3.Indexers; using System.Linq; using System.Net; diff --git a/src/NzbDrone.Integration.Test/ApiTests/ReleasePushFixture.cs b/src/NzbDrone.Integration.Test/ApiTests/ReleasePushFixture.cs new file mode 100644 index 000000000..f43576ba7 --- /dev/null +++ b/src/NzbDrone.Integration.Test/ApiTests/ReleasePushFixture.cs @@ -0,0 +1,31 @@ +using FluentAssertions; +using NUnit.Framework; +using Lidarr.Api.V3.Indexers; +using System.Linq; +using System.Net; +using System.Collections.Generic; +using System; +using System.Globalization; + +namespace NzbDrone.Integration.Test.ApiTests +{ + [TestFixture] + public class ReleasePushFixture : IntegrationTest + { + [Test] + public void should_have_utc_date() + { + var body = new Dictionary<string, object>(); + body.Add("guid", "sdfsdfsdf"); + body.Add("title", "The Artist - The Album"); + body.Add("publishDate", DateTime.UtcNow.ToString("yyyy-MM-dd HH:mm:ssZ", CultureInfo.InvariantCulture)); + + var request = ReleasePush.BuildRequest(); + request.AddBody(body); + var result = ReleasePush.Post<ReleaseResource>(request, HttpStatusCode.OK); + + result.Should().NotBeNull(); + result.AgeHours.Should().BeApproximately(0, 0.1); + } + } +} diff --git a/src/NzbDrone.Integration.Test/ApiTests/RootFolderFixture.cs b/src/NzbDrone.Integration.Test/ApiTests/RootFolderFixture.cs index 5133a5da7..e7f2527d6 100644 --- a/src/NzbDrone.Integration.Test/ApiTests/RootFolderFixture.cs +++ b/src/NzbDrone.Integration.Test/ApiTests/RootFolderFixture.cs @@ -1,7 +1,7 @@ -using System; +using System; using FluentAssertions; using NUnit.Framework; -using NzbDrone.Api.RootFolders; +using Lidarr.Api.V3.RootFolders; namespace NzbDrone.Integration.Test.ApiTests { @@ -53,4 +53,4 @@ namespace NzbDrone.Integration.Test.ApiTests postResponse.Should().NotBeNull(); } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Integration.Test/ApiTests/TrackFixture.cs b/src/NzbDrone.Integration.Test/ApiTests/TrackFixture.cs index 88215ea3c..afe070bbf 100644 --- a/src/NzbDrone.Integration.Test/ApiTests/TrackFixture.cs +++ b/src/NzbDrone.Integration.Test/ApiTests/TrackFixture.cs @@ -1,8 +1,8 @@ -using System.Threading; +using System.Threading; using FluentAssertions; using NUnit.Framework; -using NzbDrone.Api.Series; -using NzbDrone.Api.Music; +using Lidarr.Api.V3.Series; +using Lidarr.Api.V3.Artist; using System.Linq; using NzbDrone.Test.Common; @@ -23,7 +23,7 @@ namespace NzbDrone.Integration.Test.ApiTests { var newArtist = Artist.Lookup("archer").Single(c => c.ForeignArtistId == "110381"); - newArtist.ProfileId = 1; + newArtist.QualityProfileId = 1; newArtist.Path = @"C:\Test\Archer".AsOsAgnostic(); newArtist = Artist.Post(newArtist); diff --git a/src/NzbDrone.Integration.Test/ApiTests/WantedFixture.cs b/src/NzbDrone.Integration.Test/ApiTests/WantedFixture.cs index 3d12398a3..ae699e66b 100644 --- a/src/NzbDrone.Integration.Test/ApiTests/WantedFixture.cs +++ b/src/NzbDrone.Integration.Test/ApiTests/WantedFixture.cs @@ -1,4 +1,4 @@ -using System.Linq; +using System.Linq; using FluentAssertions; using NUnit.Framework; using NzbDrone.Core.Qualities; @@ -11,7 +11,7 @@ namespace NzbDrone.Integration.Test.ApiTests [Test, Order(0)] public void missing_should_be_empty() { - EnsureNoArtist("266189", "The Blacklist"); + EnsureNoArtist("8ac6cc32-8ddf-43b1-9ac4-4b04f9053176", "Alien Ant Farm"); var result = WantedMissing.GetPaged(0, 15, "releaseDate", "desc"); @@ -21,7 +21,7 @@ namespace NzbDrone.Integration.Test.ApiTests [Test, Order(1)] public void missing_should_have_monitored_items() { - EnsureArtist("266189", "The Blacklist", true); + EnsureArtist("8ac6cc32-8ddf-43b1-9ac4-4b04f9053176", "Alien Ant Farm", true); var result = WantedMissing.GetPaged(0, 15, "releaseDate", "desc"); @@ -31,19 +31,19 @@ namespace NzbDrone.Integration.Test.ApiTests [Test, Order(1)] public void missing_should_have_artist() { - EnsureArtist("266189", "The Blacklist", true); + EnsureArtist("8ac6cc32-8ddf-43b1-9ac4-4b04f9053176", "Alien Ant Farm", true); var result = WantedMissing.GetPaged(0, 15, "releaseDate", "desc"); - result.Records.First().Series.Should().NotBeNull(); - result.Records.First().Series.Title.Should().Be("The Blacklist"); + result.Records.First().Artist.Should().NotBeNull(); + result.Records.First().Artist.ArtistName.Should().Be("The Blacklist"); } [Test, Order(1)] public void cutoff_should_have_monitored_items() { EnsureProfileCutoff(1, Quality.MP3_256); - var artist = EnsureArtist("266189", "The Blacklist", true); + var artist = EnsureArtist("8ac6cc32-8ddf-43b1-9ac4-4b04f9053176", "Alien Ant Farm", true); EnsureTrackFile(artist, 1, 1, Quality.MP3_192); var result = WantedCutoffUnmet.GetPaged(0, 15, "releaseDate", "desc"); @@ -54,7 +54,7 @@ namespace NzbDrone.Integration.Test.ApiTests [Test, Order(1)] public void missing_should_not_have_unmonitored_items() { - EnsureArtist("266189", "The Blacklist", false); + EnsureArtist("8ac6cc32-8ddf-43b1-9ac4-4b04f9053176", "Alien Ant Farm", false); var result = WantedMissing.GetPaged(0, 15, "releaseDate", "desc"); @@ -65,7 +65,7 @@ namespace NzbDrone.Integration.Test.ApiTests public void cutoff_should_not_have_unmonitored_items() { EnsureProfileCutoff(1, Quality.MP3_256); - var artist = EnsureArtist("266189", "The Blacklist", false); + var artist = EnsureArtist("8ac6cc32-8ddf-43b1-9ac4-4b04f9053176", "Alien Ant Farm", false); EnsureTrackFile(artist, 1, 1, Quality.MP3_192); var result = WantedCutoffUnmet.GetPaged(0, 15, "releaseDate", "desc"); @@ -77,19 +77,19 @@ namespace NzbDrone.Integration.Test.ApiTests public void cutoff_should_have_artist() { EnsureProfileCutoff(1, Quality.MP3_256); - var artist = EnsureArtist("266189", "The Blacklist", true); + var artist = EnsureArtist("8ac6cc32-8ddf-43b1-9ac4-4b04f9053176", "Alien Ant Farm", true); EnsureTrackFile(artist, 1, 1, Quality.MP3_192); var result = WantedCutoffUnmet.GetPaged(0, 15, "releaseDate", "desc"); - result.Records.First().Series.Should().NotBeNull(); - result.Records.First().Series.Title.Should().Be("The Blacklist"); + result.Records.First().Artist.Should().NotBeNull(); + result.Records.First().Artist.ArtistName.Should().Be("Alien Ant Farm"); } [Test, Order(2)] public void missing_should_have_unmonitored_items() { - EnsureArtist("266189", "The Blacklist", false); + EnsureArtist("8ac6cc32-8ddf-43b1-9ac4-4b04f9053176", "Alien Ant Farm", false); var result = WantedMissing.GetPaged(0, 15, "releaseDate", "desc", "monitored", "false"); @@ -100,7 +100,7 @@ namespace NzbDrone.Integration.Test.ApiTests public void cutoff_should_have_unmonitored_items() { EnsureProfileCutoff(1, Quality.MP3_256); - var artist = EnsureArtist("266189", "The Blacklist", false); + var artist = EnsureArtist("8ac6cc32-8ddf-43b1-9ac4-4b04f9053176", "Alien Ant Farm", false); EnsureTrackFile(artist, 1, 1, Quality.MP3_192); var result = WantedCutoffUnmet.GetPaged(0, 15, "releaseDate", "desc", "monitored", "false"); diff --git a/src/NzbDrone.Integration.Test/Client/AlbumClient.cs b/src/NzbDrone.Integration.Test/Client/AlbumClient.cs new file mode 100644 index 000000000..e6d9975c1 --- /dev/null +++ b/src/NzbDrone.Integration.Test/Client/AlbumClient.cs @@ -0,0 +1,20 @@ +using System.Collections.Generic; +using Lidarr.Api.V3.Albums; +using RestSharp; + +namespace NzbDrone.Integration.Test.Client +{ + public class AlbumClient : ClientBase<AlbumResource> + { + public AlbumClient(IRestClient restClient, string apiKey) + : base(restClient, apiKey, "album") + { + } + + public List<AlbumResource> GetAlbumsInArtist(int artistId) + { + var request = BuildRequest("?artistId=" + artistId.ToString()); + return Get<List<AlbumResource>>(request); + } + } +} diff --git a/src/NzbDrone.Integration.Test/Client/ArtistClient.cs b/src/NzbDrone.Integration.Test/Client/ArtistClient.cs index 254726baa..c06e96ab4 100644 --- a/src/NzbDrone.Integration.Test/Client/ArtistClient.cs +++ b/src/NzbDrone.Integration.Test/Client/ArtistClient.cs @@ -1,6 +1,6 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Net; -using NzbDrone.Api.Music; +using Lidarr.Api.V3.Artist; using RestSharp; namespace NzbDrone.Integration.Test.Client @@ -19,10 +19,10 @@ namespace NzbDrone.Integration.Test.Client return Get<List<ArtistResource>>(request); } - public List<ArtistResource> Editor(List<ArtistResource> series) + public List<ArtistResource> Editor(ArtistEditorResource artist) { var request = BuildRequest("editor"); - request.AddBody(series); + request.AddBody(artist); return Put<List<ArtistResource>>(request); } diff --git a/src/NzbDrone.Integration.Test/Client/ClientBase.cs b/src/NzbDrone.Integration.Test/Client/ClientBase.cs index 884fe992a..d2bdcc8f2 100644 --- a/src/NzbDrone.Integration.Test/Client/ClientBase.cs +++ b/src/NzbDrone.Integration.Test/Client/ClientBase.cs @@ -1,12 +1,13 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Net; using FluentAssertions; using NLog; -using NzbDrone.Api; -using NzbDrone.Api.REST; +using Lidarr.Api.V3; +using Lidarr.Http.REST; using NzbDrone.Common.Serializer; using RestSharp; using System.Linq; +using Lidarr.Http; namespace NzbDrone.Integration.Test.Client { @@ -176,4 +177,4 @@ namespace NzbDrone.Integration.Test.Client Execute<object>(request, statusCode); } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Integration.Test/Client/CommandClient.cs b/src/NzbDrone.Integration.Test/Client/CommandClient.cs index 047427a98..67e409cc9 100644 --- a/src/NzbDrone.Integration.Test/Client/CommandClient.cs +++ b/src/NzbDrone.Integration.Test/Client/CommandClient.cs @@ -1,4 +1,4 @@ -using NzbDrone.Api.Commands; +using Lidarr.Api.V3.Commands; using RestSharp; using NzbDrone.Core.Messaging.Commands; using FluentAssertions; diff --git a/src/NzbDrone.Integration.Test/Client/DownloadClientClient.cs b/src/NzbDrone.Integration.Test/Client/DownloadClientClient.cs index e31e38748..0a975af25 100644 --- a/src/NzbDrone.Integration.Test/Client/DownloadClientClient.cs +++ b/src/NzbDrone.Integration.Test/Client/DownloadClientClient.cs @@ -1,5 +1,5 @@ -using System.Collections.Generic; -using NzbDrone.Api.DownloadClient; +using System.Collections.Generic; +using Lidarr.Api.V3.DownloadClient; using RestSharp; namespace NzbDrone.Integration.Test.Client @@ -17,4 +17,4 @@ namespace NzbDrone.Integration.Test.Client return Get<List<DownloadClientResource>>(request); } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Integration.Test/Client/EpisodeClient.cs b/src/NzbDrone.Integration.Test/Client/EpisodeClient.cs deleted file mode 100644 index 46d0b8e03..000000000 --- a/src/NzbDrone.Integration.Test/Client/EpisodeClient.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System.Collections.Generic; -using NzbDrone.Api.Episodes; -using RestSharp; - -namespace NzbDrone.Integration.Test.Client -{ - public class EpisodeClient : ClientBase<EpisodeResource> - { - public EpisodeClient(IRestClient restClient, string apiKey) - : base(restClient, apiKey, "episode") - { - } - - public List<EpisodeResource> GetEpisodesInSeries(int seriesId) - { - var request = BuildRequest("?seriesId=" + seriesId.ToString()); - return Get<List<EpisodeResource>>(request); - } - } -} diff --git a/src/NzbDrone.Integration.Test/Client/IndexerClient.cs b/src/NzbDrone.Integration.Test/Client/IndexerClient.cs index 9d6f9b974..b3cc265c2 100644 --- a/src/NzbDrone.Integration.Test/Client/IndexerClient.cs +++ b/src/NzbDrone.Integration.Test/Client/IndexerClient.cs @@ -1,4 +1,4 @@ -using NzbDrone.Api.Indexers; +using Lidarr.Api.V3.Indexers; using RestSharp; namespace NzbDrone.Integration.Test.Client @@ -10,4 +10,4 @@ namespace NzbDrone.Integration.Test.Client { } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Integration.Test/Client/NotificationClient.cs b/src/NzbDrone.Integration.Test/Client/NotificationClient.cs index 6f0f06eb5..b056ce5f7 100644 --- a/src/NzbDrone.Integration.Test/Client/NotificationClient.cs +++ b/src/NzbDrone.Integration.Test/Client/NotificationClient.cs @@ -1,5 +1,5 @@ -using System.Collections.Generic; -using NzbDrone.Api.Notifications; +using System.Collections.Generic; +using Lidarr.Api.V3.Notifications; using RestSharp; namespace NzbDrone.Integration.Test.Client @@ -17,4 +17,4 @@ namespace NzbDrone.Integration.Test.Client return Get<List<NotificationResource>>(request); } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Integration.Test/Client/ReleaseClient.cs b/src/NzbDrone.Integration.Test/Client/ReleaseClient.cs index 46a6db839..bda2ccb03 100644 --- a/src/NzbDrone.Integration.Test/Client/ReleaseClient.cs +++ b/src/NzbDrone.Integration.Test/Client/ReleaseClient.cs @@ -1,4 +1,4 @@ -using NzbDrone.Api.Indexers; +using Lidarr.Api.V3.Indexers; using RestSharp; namespace NzbDrone.Integration.Test.Client diff --git a/src/NzbDrone.Integration.Test/Client/ReleasePushClient.cs b/src/NzbDrone.Integration.Test/Client/ReleasePushClient.cs new file mode 100644 index 000000000..5b660dfda --- /dev/null +++ b/src/NzbDrone.Integration.Test/Client/ReleasePushClient.cs @@ -0,0 +1,13 @@ +using Lidarr.Api.V3.Indexers; +using RestSharp; + +namespace NzbDrone.Integration.Test.Client +{ + public class ReleasePushClient : ClientBase<ReleaseResource> + { + public ReleasePushClient(IRestClient restClient, string apiKey) + : base(restClient, apiKey, "release/push") + { + } + } +} diff --git a/src/NzbDrone.Integration.Test/Client/TrackClient.cs b/src/NzbDrone.Integration.Test/Client/TrackClient.cs index 585910dc7..ab51399b7 100644 --- a/src/NzbDrone.Integration.Test/Client/TrackClient.cs +++ b/src/NzbDrone.Integration.Test/Client/TrackClient.cs @@ -1,5 +1,5 @@ -using System.Collections.Generic; -using NzbDrone.Api.Tracks; +using System.Collections.Generic; +using Lidarr.Api.V3.Tracks; using RestSharp; namespace NzbDrone.Integration.Test.Client diff --git a/src/NzbDrone.Integration.Test/CorsFixture.cs b/src/NzbDrone.Integration.Test/CorsFixture.cs index 2d9d8ac4f..d27539ba8 100644 --- a/src/NzbDrone.Integration.Test/CorsFixture.cs +++ b/src/NzbDrone.Integration.Test/CorsFixture.cs @@ -1,6 +1,6 @@ -using FluentAssertions; +using FluentAssertions; using NUnit.Framework; -using NzbDrone.Api.Extensions; +using Lidarr.Http.Extensions; using RestSharp; namespace NzbDrone.Integration.Test @@ -10,7 +10,7 @@ namespace NzbDrone.Integration.Test { private RestRequest BuildRequest() { - var request = new RestRequest("series"); + var request = new RestRequest("artist"); request.AddHeader(AccessControlHeaders.RequestMethod, "POST"); return request; diff --git a/src/NzbDrone.Integration.Test/HttpLogFixture.cs b/src/NzbDrone.Integration.Test/HttpLogFixture.cs index bf187eb4d..52e28670e 100644 --- a/src/NzbDrone.Integration.Test/HttpLogFixture.cs +++ b/src/NzbDrone.Integration.Test/HttpLogFixture.cs @@ -1,4 +1,4 @@ -using System.IO; +using System.IO; using System.Linq; using FluentAssertions; using NUnit.Framework; @@ -18,7 +18,7 @@ namespace NzbDrone.Integration.Test var logFile = Path.Combine(_runner.AppData, "logs", "Lidarr.trace.txt"); var logLines = File.ReadAllLines(logFile); - var result = Artist.InvalidPost(new Api.Music.ArtistResource()); + var result = Artist.InvalidPost(new Lidarr.Api.V3.Artist.ArtistResource()); logLines = File.ReadAllLines(logFile).Skip(logLines.Length).ToArray(); diff --git a/src/NzbDrone.Integration.Test/IntegrationTest.cs b/src/NzbDrone.Integration.Test/IntegrationTest.cs index 488a081c6..80d2612af 100644 --- a/src/NzbDrone.Integration.Test/IntegrationTest.cs +++ b/src/NzbDrone.Integration.Test/IntegrationTest.cs @@ -1,7 +1,7 @@ -using System.Collections.Generic; using NLog; using NzbDrone.Core.Indexers.Newznab; using NzbDrone.Test.Common; +using Lidarr.Http.ClientSchema; namespace NzbDrone.Integration.Test { @@ -25,7 +25,7 @@ namespace NzbDrone.Integration.Test protected override void InitializeTestTarget() { - Indexers.Post(new Api.Indexers.IndexerResource + Indexers.Post(new Lidarr.Api.V3.Indexers.IndexerResource { EnableRss = false, EnableSearch = false, @@ -33,7 +33,7 @@ namespace NzbDrone.Integration.Test Implementation = nameof(Newznab), Name = "NewznabTest", Protocol = Core.Indexers.DownloadProtocol.Usenet, - Fields = Api.ClientSchema.SchemaBuilder.ToSchema(new NewznabSettings()) + Fields = SchemaBuilder.ToSchema(new NewznabSettings()) }); } diff --git a/src/NzbDrone.Integration.Test/IntegrationTestBase.cs b/src/NzbDrone.Integration.Test/IntegrationTestBase.cs index de5ca1e1c..964c5e4c8 100644 --- a/src/NzbDrone.Integration.Test/IntegrationTestBase.cs +++ b/src/NzbDrone.Integration.Test/IntegrationTestBase.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.IO; using System.Linq; @@ -10,18 +10,18 @@ using NLog; using NLog.Config; using NLog.Targets; using NUnit.Framework; -using NzbDrone.Api.Blacklist; -using NzbDrone.Api.Commands; -using NzbDrone.Api.Config; -using NzbDrone.Api.DownloadClient; -using NzbDrone.Api.TrackFiles; -using NzbDrone.Api.Episodes; -using NzbDrone.Api.History; -using NzbDrone.Api.Profiles; -using NzbDrone.Api.RootFolders; -using NzbDrone.Api.Music; -using NzbDrone.Api.Albums; -using NzbDrone.Api.Tags; +using Lidarr.Api.V3.Blacklist; +using Lidarr.Api.V3.Commands; +using Lidarr.Api.V3.Config; +using Lidarr.Api.V3.DownloadClient; +using Lidarr.Api.V3.TrackFiles; +using Lidarr.Api.V3.History; +using Lidarr.Api.V3.Profiles.Quality; +using Lidarr.Api.V3.RootFolders; +using Lidarr.Api.V3.Artist; +using Lidarr.Api.V3.Albums; +using Lidarr.Api.V3.Tracks; +using Lidarr.Api.V3.Tags; using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.Serializer; using NzbDrone.Core.Qualities; @@ -41,20 +41,21 @@ namespace NzbDrone.Integration.Test public ClientBase<BlacklistResource> Blacklist; public CommandClient Commands; public DownloadClientClient DownloadClients; - public EpisodeClient Episodes; + public AlbumClient Albums; public TrackClient Tracks; public ClientBase<HistoryResource> History; public ClientBase<HostConfigResource> HostConfig; public IndexerClient Indexers; public ClientBase<NamingConfigResource> NamingConfig; public NotificationClient Notifications; - public ClientBase<ProfileResource> Profiles; + public ClientBase<QualityProfileResource> Profiles; public ReleaseClient Releases; + public ReleasePushClient ReleasePush; public ClientBase<RootFolderResource> RootFolders; public ArtistClient Artist; public ClientBase<TagResource> Tags; - public ClientBase<EpisodeResource> WantedMissing; - public ClientBase<EpisodeResource> WantedCutoffUnmet; + public ClientBase<AlbumResource> WantedMissing; + public ClientBase<AlbumResource> WantedCutoffUnmet; private List<SignalRMessage> _signalRReceived; private Connection _signalrConnection; @@ -95,26 +96,28 @@ namespace NzbDrone.Integration.Test protected virtual void InitRestClients() { - RestClient = new RestClient(RootUrl + "api/"); + RestClient = new RestClient(RootUrl + "api/v3/"); RestClient.AddDefaultHeader("Authentication", ApiKey); RestClient.AddDefaultHeader("X-Api-Key", ApiKey); Blacklist = new ClientBase<BlacklistResource>(RestClient, ApiKey); Commands = new CommandClient(RestClient, ApiKey); DownloadClients = new DownloadClientClient(RestClient, ApiKey); - Episodes = new EpisodeClient(RestClient, ApiKey); + Albums = new AlbumClient(RestClient, ApiKey); + Tracks = new TrackClient(RestClient, ApiKey); History = new ClientBase<HistoryResource>(RestClient, ApiKey); HostConfig = new ClientBase<HostConfigResource>(RestClient, ApiKey, "config/host"); Indexers = new IndexerClient(RestClient, ApiKey); NamingConfig = new ClientBase<NamingConfigResource>(RestClient, ApiKey, "config/naming"); Notifications = new NotificationClient(RestClient, ApiKey); - Profiles = new ClientBase<ProfileResource>(RestClient, ApiKey); + Profiles = new ClientBase<QualityProfileResource>(RestClient, ApiKey); Releases = new ReleaseClient(RestClient, ApiKey); + ReleasePush = new ReleasePushClient(RestClient, ApiKey); RootFolders = new ClientBase<RootFolderResource>(RestClient, ApiKey); Artist = new ArtistClient(RestClient, ApiKey); Tags = new ClientBase<TagResource>(RestClient, ApiKey); - WantedMissing = new ClientBase<EpisodeResource>(RestClient, ApiKey, "wanted/missing"); - WantedCutoffUnmet = new ClientBase<EpisodeResource>(RestClient, ApiKey, "wanted/cutoff"); + WantedMissing = new ClientBase<AlbumResource>(RestClient, ApiKey, "wanted/missing"); + WantedCutoffUnmet = new ClientBase<AlbumResource>(RestClient, ApiKey, "wanted/cutoff"); } [OneTimeTearDown] @@ -212,8 +215,9 @@ namespace NzbDrone.Integration.Test { var lookup = Artist.Lookup("lidarr:" + lidarrId); var artist = lookup.First(); - artist.ProfileId = 1; - artist.Path = Path.Combine(ArtistRootFolder, artist.Name); + artist.QualityProfileId = 1; + artist.LanguageProfileId = 1; + artist.Path = Path.Combine(ArtistRootFolder, artist.ArtistName); artist.Monitored = true; artist.AddOptions = new Core.Music.AddArtistOptions(); Directory.CreateDirectory(artist.Path); @@ -267,12 +271,12 @@ namespace NzbDrone.Integration.Test if (result.TrackFile == null) { - var path = Path.Combine(ArtistRootFolder, artist.Name, string.Format("{0} - {1} - Track.mp3", track, artist.Name)); + var path = Path.Combine(ArtistRootFolder, artist.ArtistName, string.Format("{0} - {1} - Track.mp3", track, artist.ArtistName)); Directory.CreateDirectory(Path.GetDirectoryName(path)); File.WriteAllText(path, "Fake Track"); - Commands.PostAndWait(new CommandResource { Name = "refreshseries", Body = new RefreshArtistCommand(artist.Id) }); + Commands.PostAndWait(new CommandResource { Name = "refreshartist", Body = new RefreshArtistCommand(artist.Id) }); Commands.WaitAll(); result = Tracks.GetTracksInArtist(artist.Id).Single(v => v.AlbumId == albumId && v.TrackNumber == track); @@ -283,7 +287,7 @@ namespace NzbDrone.Integration.Test return result.TrackFile; } - public ProfileResource EnsureProfileCutoff(int profileId, Quality cutoff) + public QualityProfileResource EnsureProfileCutoff(int profileId, Quality cutoff) { var profile = Profiles.Get(profileId); diff --git a/src/NzbDrone.Integration.Test/NzbDrone.Integration.Test.csproj b/src/NzbDrone.Integration.Test/NzbDrone.Integration.Test.csproj index 52c43a3c6..509f762eb 100644 --- a/src/NzbDrone.Integration.Test/NzbDrone.Integration.Test.csproj +++ b/src/NzbDrone.Integration.Test/NzbDrone.Integration.Test.csproj @@ -1,5 +1,5 @@ <?xml version="1.0" encoding="utf-8"?> -<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> +<Project ToolsVersion="12.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> <Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" /> <PropertyGroup> <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration> @@ -8,13 +8,14 @@ <OutputType>Library</OutputType> <AppDesignerFolder>Properties</AppDesignerFolder> <RootNamespace>NzbDrone.Integration.Test</RootNamespace> - <AssemblyName>NzbDrone.Integration.Test</AssemblyName> - <TargetFrameworkVersion>v4.0</TargetFrameworkVersion> + <AssemblyName>Lidarr.Integration.Test</AssemblyName> + <TargetFrameworkVersion>v4.6.1</TargetFrameworkVersion> <FileAlignment>512</FileAlignment> <SolutionDir Condition="$(SolutionDir) == '' Or $(SolutionDir) == '*Undefined*'">..\</SolutionDir> <RestorePackages>true</RestorePackages> <ProductVersion>12.0.0</ProductVersion> <SchemaVersion>2.0</SchemaVersion> + <TargetFrameworkProfile /> </PropertyGroup> <PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x86'"> <DebugSymbols>true</DebugSymbols> @@ -26,6 +27,7 @@ <CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet> <WarningLevel>4</WarningLevel> <Optimize>false</Optimize> + <Prefer32Bit>false</Prefer32Bit> </PropertyGroup> <PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x86'"> <OutputPath>bin\x86\Release\</OutputPath> @@ -36,53 +38,53 @@ <ErrorReport>prompt</ErrorReport> <CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet> <WarningLevel>4</WarningLevel> + <Prefer32Bit>false</Prefer32Bit> </PropertyGroup> <ItemGroup> <Reference Include="FluentAssertions, Version=4.19.0.0, Culture=neutral, PublicKeyToken=33f2691a05b67b6a, processorArchitecture=MSIL"> - <HintPath>..\packages\FluentAssertions.4.19.0\lib\net40\FluentAssertions.dll</HintPath> + <HintPath>..\packages\FluentAssertions.4.19.0\lib\net45\FluentAssertions.dll</HintPath> </Reference> <Reference Include="FluentAssertions.Core, Version=4.19.0.0, Culture=neutral, PublicKeyToken=33f2691a05b67b6a, processorArchitecture=MSIL"> - <HintPath>..\packages\FluentAssertions.4.19.0\lib\net40\FluentAssertions.Core.dll</HintPath> + <HintPath>..\packages\FluentAssertions.4.19.0\lib\net45\FluentAssertions.Core.dll</HintPath> </Reference> <Reference Include="FluentValidation, Version=6.2.1.0, Culture=neutral, processorArchitecture=MSIL"> - <HintPath>..\packages\FluentValidation.6.2.1.0\lib\portable-net40+sl50+wp80+win8+wpa81\FluentValidation.dll</HintPath> - <Private>True</Private> + <HintPath>..\packages\FluentValidation.6.2.1.0\lib\Net45\FluentValidation.dll</HintPath> </Reference> - <Reference Include="Microsoft.AspNet.SignalR.Client, Version=1.2.2.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL"> - <HintPath>..\packages\Microsoft.AspNet.SignalR.Client.1.2.2\lib\net40\Microsoft.AspNet.SignalR.Client.dll</HintPath> + <Reference Include="Microsoft.AspNet.SignalR.Client, Version=2.2.2.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL"> + <HintPath>..\packages\Microsoft.AspNet.SignalR.Client.2.2.2\lib\net45\Microsoft.AspNet.SignalR.Client.dll</HintPath> </Reference> - <Reference Include="Microsoft.Owin, Version=2.1.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL"> - <SpecificVersion>False</SpecificVersion> - <HintPath>..\packages\Microsoft.Owin.2.1.0\lib\net40\Microsoft.Owin.dll</HintPath> + <Reference Include="Microsoft.Owin, Version=3.1.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL"> + <HintPath>..\packages\Microsoft.Owin.3.1.0\lib\net45\Microsoft.Owin.dll</HintPath> </Reference> - <Reference Include="Microsoft.Owin.Host.HttpListener"> - <HintPath>..\packages\Microsoft.Owin.Host.HttpListener.2.1.0\lib\net40\Microsoft.Owin.Host.HttpListener.dll</HintPath> + <Reference Include="Microsoft.Owin.Host.HttpListener, Version=3.1.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL"> + <HintPath>..\packages\Microsoft.Owin.Host.HttpListener.3.1.0\lib\net45\Microsoft.Owin.Host.HttpListener.dll</HintPath> </Reference> - <Reference Include="Microsoft.Owin.Hosting, Version=2.1.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL"> - <SpecificVersion>False</SpecificVersion> - <HintPath>..\packages\Microsoft.Owin.Hosting.2.1.0\lib\net40\Microsoft.Owin.Hosting.dll</HintPath> + <Reference Include="Microsoft.Owin.Hosting, Version=3.1.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL"> + <HintPath>..\packages\Microsoft.Owin.Hosting.3.1.0\lib\net45\Microsoft.Owin.Hosting.dll</HintPath> </Reference> - <Reference Include="Nancy, Version=1.4.2.0, Culture=neutral, processorArchitecture=MSIL"> - <HintPath>..\packages\Nancy.1.4.3\lib\net40\Nancy.dll</HintPath> - <Private>True</Private> + <Reference Include="Moq, Version=4.0.10827.0, Culture=neutral, PublicKeyToken=69f491c39445e920, processorArchitecture=MSIL"> + <HintPath>..\packages\Moq.4.0.10827\lib\NET40\Moq.dll</HintPath> + </Reference> + <Reference Include="Nancy, Version=1.4.4.0, Culture=neutral, processorArchitecture=MSIL"> + <HintPath>..\packages\Nancy.1.4.4\lib\net40\Nancy.dll</HintPath> </Reference> <Reference Include="Nancy.Owin, Version=1.4.1.0, Culture=neutral, processorArchitecture=MSIL"> <HintPath>..\packages\Nancy.Owin.1.4.1\lib\net40\Nancy.Owin.dll</HintPath> - <Private>True</Private> </Reference> <Reference Include="Newtonsoft.Json, Version=9.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL"> - <HintPath>..\packages\Newtonsoft.Json.9.0.1\lib\net40\Newtonsoft.Json.dll</HintPath> - <Private>True</Private> + <HintPath>..\packages\Newtonsoft.Json.9.0.1\lib\net45\Newtonsoft.Json.dll</HintPath> </Reference> <Reference Include="NLog, Version=4.0.0.0, Culture=neutral, PublicKeyToken=5120e14c03d0593c, processorArchitecture=MSIL"> - <HintPath>..\packages\NLog.4.4.3\lib\net40\NLog.dll</HintPath> + <HintPath>..\packages\NLog.4.4.12\lib\net45\NLog.dll</HintPath> </Reference> <Reference Include="nunit.framework, Version=3.6.0.0, Culture=neutral, PublicKeyToken=2638cd05610744eb, processorArchitecture=MSIL"> - <HintPath>..\packages\NUnit.3.6.0\lib\net40\nunit.framework.dll</HintPath> + <HintPath>..\packages\NUnit.3.6.0\lib\net45\nunit.framework.dll</HintPath> + </Reference> + <Reference Include="Owin, Version=1.0.0.0, Culture=neutral, PublicKeyToken=f0ebd12fd5e55cc5, processorArchitecture=MSIL"> + <HintPath>..\packages\Owin.1.0\lib\net40\Owin.dll</HintPath> </Reference> <Reference Include="RestSharp, Version=105.2.3.0, Culture=neutral, processorArchitecture=MSIL"> - <HintPath>..\packages\RestSharp.105.2.3\lib\net4\RestSharp.dll</HintPath> - <Private>True</Private> + <HintPath>..\packages\RestSharp.105.2.3\lib\net46\RestSharp.dll</HintPath> </Reference> <Reference Include="System" /> <Reference Include="System.Core" /> @@ -91,26 +93,22 @@ <Reference Include="System.Xml" /> <Reference Include="System.Xml.Linq" /> <Reference Include="Microsoft.CSharp" /> - <Reference Include="Moq"> - <HintPath>..\packages\Moq.4.0.10827\lib\NET40\Moq.dll</HintPath> - </Reference> - <Reference Include="Owin"> - <HintPath>..\packages\Owin.1.0\lib\net40\Owin.dll</HintPath> - </Reference> </ItemGroup> <ItemGroup> <Compile Include="ApiTests\DiskSpaceFixture.cs" /> <Compile Include="ApiTests\CalendarFixture.cs" /> <Compile Include="ApiTests\BlacklistFixture.cs" /> <Compile Include="ApiTests\DownloadClientFixture.cs" /> + <Compile Include="ApiTests\ReleasePushFixture.cs" /> <Compile Include="ApiTests\TrackFileFixture.cs" /> <Compile Include="ApiTests\FileSystemFixture.cs" /> <Compile Include="ApiTests\ArtistFixture.cs" /> <Compile Include="ApiTests\ArtistLookupFixture.cs" /> <Compile Include="ApiTests\WantedFixture.cs" /> <Compile Include="Client\ClientBase.cs" /> + <Compile Include="Client\ReleasePushClient.cs" /> <Compile Include="Client\TrackClient.cs" /> - <Compile Include="Client\EpisodeClient.cs" /> + <Compile Include="Client\AlbumClient.cs" /> <Compile Include="Client\IndexerClient.cs" /> <Compile Include="Client\DownloadClientClient.cs" /> <Compile Include="Client\NotificationClient.cs" /> @@ -140,9 +138,9 @@ <None Include="packages.config" /> </ItemGroup> <ItemGroup> - <ProjectReference Include="..\NzbDrone.Api\NzbDrone.Api.csproj"> - <Project>{FD286DF8-2D3A-4394-8AD5-443FADE55FB2}</Project> - <Name>NzbDrone.Api</Name> + <ProjectReference Include="..\Lidarr.Api.V3\Lidarr.Api.V3.csproj"> + <Project>{7140ff1f-79be-492f-9188-b21a050bf708}</Project> + <Name>Lidarr.Api.V3</Name> </ProjectReference> <ProjectReference Include="..\NzbDrone.Common\NzbDrone.Common.csproj"> <Project>{F2BE0FDF-6E47-4827-A420-DD4EF82407F8}</Project> @@ -164,6 +162,10 @@ <Project>{CADDFCE0-7509-4430-8364-2074E1EEFCA2}</Project> <Name>NzbDrone.Test.Common</Name> </ProjectReference> + <ProjectReference Include="..\Lidarr.Http\Lidarr.Http.csproj"> + <Project>{5370bff7-1bd7-46bc-af06-7d9ea5cda1d6}</Project> + <Name>Lidarr.Http</Name> + </ProjectReference> </ItemGroup> <ItemGroup> <Content Include="..\Libraries\Sqlite\sqlite3.dll"> diff --git a/src/NzbDrone.Integration.Test/Properties/AssemblyInfo.cs b/src/NzbDrone.Integration.Test/Properties/AssemblyInfo.cs index 5183f6f7e..a53b560c0 100644 --- a/src/NzbDrone.Integration.Test/Properties/AssemblyInfo.cs +++ b/src/NzbDrone.Integration.Test/Properties/AssemblyInfo.cs @@ -1,14 +1,14 @@ -using System.Reflection; +using System.Reflection; using System.Runtime.InteropServices; // General Information about an assembly is controlled through the following // set of attributes. Change these attribute values to modify the information // associated with an assembly. -[assembly: AssemblyTitle("NzbDrone.Smoke.Test")] +[assembly: AssemblyTitle("Lidarr.Smoke.Test")] [assembly: AssemblyDescription("")] [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("NzbDrone.Smoke.Test")] +[assembly: AssemblyProduct("Lidarr.Smoke.Test")] [assembly: AssemblyCopyright("Copyright © 2013")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] @@ -21,4 +21,3 @@ using System.Runtime.InteropServices; // The following GUID is for the ID of the typelib if this project is exposed to COM [assembly: Guid("8a49cb1d-87ac-42f9-a582-607365a6bd79")] -[assembly: AssemblyVersion("10.0.0.*")] diff --git a/src/NzbDrone.Integration.Test/packages.config b/src/NzbDrone.Integration.Test/packages.config index e82b24d7f..e45b2f78c 100644 --- a/src/NzbDrone.Integration.Test/packages.config +++ b/src/NzbDrone.Integration.Test/packages.config @@ -1,17 +1,17 @@ <?xml version="1.0" encoding="utf-8"?> <packages> - <package id="FluentAssertions" version="4.19.0" targetFramework="net40" /> - <package id="FluentValidation" version="6.2.1.0" targetFramework="net40" /> - <package id="Microsoft.AspNet.SignalR.Client" version="1.2.2" targetFramework="net40" /> - <package id="Microsoft.Owin" version="2.1.0" targetFramework="net40" /> - <package id="Microsoft.Owin.Host.HttpListener" version="2.1.0" targetFramework="net40" /> - <package id="Microsoft.Owin.Hosting" version="2.1.0" targetFramework="net40" /> - <package id="Moq" version="4.0.10827" targetFramework="net40" /> - <package id="Nancy" version="1.4.3" targetFramework="net40" /> - <package id="Nancy.Owin" version="1.4.1" targetFramework="net40" /> - <package id="Newtonsoft.Json" version="9.0.1" targetFramework="net40" /> - <package id="NLog" version="4.4.3" targetFramework="net40" /> - <package id="NUnit" version="3.6.0" targetFramework="net40" /> - <package id="Owin" version="1.0" targetFramework="net40" /> - <package id="RestSharp" version="105.2.3" targetFramework="net40" /> + <package id="FluentAssertions" version="4.19.0" targetFramework="net461" /> + <package id="FluentValidation" version="6.2.1.0" targetFramework="net461" /> + <package id="Microsoft.AspNet.SignalR.Client" version="2.2.2" targetFramework="net461" /> + <package id="Microsoft.Owin" version="3.1.0" targetFramework="net461" /> + <package id="Microsoft.Owin.Host.HttpListener" version="3.1.0" targetFramework="net461" /> + <package id="Microsoft.Owin.Hosting" version="3.1.0" targetFramework="net461" /> + <package id="Moq" version="4.0.10827" targetFramework="net461" /> + <package id="Nancy" version="1.4.4" targetFramework="net461" /> + <package id="Nancy.Owin" version="1.4.1" targetFramework="net461" /> + <package id="Newtonsoft.Json" version="9.0.1" targetFramework="net461" /> + <package id="NLog" version="4.4.12" targetFramework="net461" /> + <package id="NUnit" version="3.6.0" targetFramework="net461" /> + <package id="Owin" version="1.0" targetFramework="net461" /> + <package id="RestSharp" version="105.2.3" targetFramework="net461" /> </packages> \ No newline at end of file diff --git a/src/NzbDrone.Libraries.Test/NzbDrone.Libraries.Test.csproj b/src/NzbDrone.Libraries.Test/NzbDrone.Libraries.Test.csproj index b16c163ce..2d9d78b03 100644 --- a/src/NzbDrone.Libraries.Test/NzbDrone.Libraries.Test.csproj +++ b/src/NzbDrone.Libraries.Test/NzbDrone.Libraries.Test.csproj @@ -1,5 +1,5 @@ <?xml version="1.0" encoding="utf-8"?> -<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> +<Project ToolsVersion="12.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> <Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" /> <PropertyGroup> <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration> @@ -8,13 +8,14 @@ <OutputType>Library</OutputType> <AppDesignerFolder>Properties</AppDesignerFolder> <RootNamespace>NzbDrone.Libraries.Test</RootNamespace> - <AssemblyName>NzbDrone.Libraries.Test</AssemblyName> - <TargetFrameworkVersion>v4.0</TargetFrameworkVersion> + <AssemblyName>Lidarr.Libraries.Test</AssemblyName> + <TargetFrameworkVersion>v4.6.1</TargetFrameworkVersion> <FileAlignment>512</FileAlignment> <SolutionDir Condition="$(SolutionDir) == '' Or $(SolutionDir) == '*Undefined*'">..\</SolutionDir> <RestorePackages>true</RestorePackages> <ProductVersion>12.0.0</ProductVersion> <SchemaVersion>2.0</SchemaVersion> + <TargetFrameworkProfile /> </PropertyGroup> <PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x86'"> <DebugSymbols>true</DebugSymbols> @@ -26,6 +27,7 @@ <CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet> <WarningLevel>4</WarningLevel> <Optimize>false</Optimize> + <Prefer32Bit>false</Prefer32Bit> </PropertyGroup> <PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x86'"> <OutputPath>bin\x86\Release\</OutputPath> @@ -36,20 +38,20 @@ <ErrorReport>prompt</ErrorReport> <CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet> <WarningLevel>4</WarningLevel> + <Prefer32Bit>false</Prefer32Bit> </PropertyGroup> <ItemGroup> <Reference Include="FluentAssertions, Version=4.19.0.0, Culture=neutral, PublicKeyToken=33f2691a05b67b6a, processorArchitecture=MSIL"> - <HintPath>..\packages\FluentAssertions.4.19.0\lib\net40\FluentAssertions.dll</HintPath> + <HintPath>..\packages\FluentAssertions.4.19.0\lib\net45\FluentAssertions.dll</HintPath> </Reference> <Reference Include="FluentAssertions.Core, Version=4.19.0.0, Culture=neutral, PublicKeyToken=33f2691a05b67b6a, processorArchitecture=MSIL"> - <HintPath>..\packages\FluentAssertions.4.19.0\lib\net40\FluentAssertions.Core.dll</HintPath> + <HintPath>..\packages\FluentAssertions.4.19.0\lib\net45\FluentAssertions.Core.dll</HintPath> </Reference> <Reference Include="Newtonsoft.Json, Version=9.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL"> - <HintPath>..\packages\Newtonsoft.Json.9.0.1\lib\net40\Newtonsoft.Json.dll</HintPath> - <Private>True</Private> + <HintPath>..\packages\Newtonsoft.Json.9.0.1\lib\net45\Newtonsoft.Json.dll</HintPath> </Reference> <Reference Include="nunit.framework, Version=3.6.0.0, Culture=neutral, PublicKeyToken=2638cd05610744eb, processorArchitecture=MSIL"> - <HintPath>..\packages\NUnit.3.6.0\lib\net40\nunit.framework.dll</HintPath> + <HintPath>..\packages\NUnit.3.6.0\lib\net45\nunit.framework.dll</HintPath> </Reference> <Reference Include="System" /> <Reference Include="System.Core" /> diff --git a/src/NzbDrone.Libraries.Test/Properties/AssemblyInfo.cs b/src/NzbDrone.Libraries.Test/Properties/AssemblyInfo.cs index 8d91461ae..7e777db60 100644 --- a/src/NzbDrone.Libraries.Test/Properties/AssemblyInfo.cs +++ b/src/NzbDrone.Libraries.Test/Properties/AssemblyInfo.cs @@ -1,14 +1,14 @@ -using System.Reflection; +using System.Reflection; using System.Runtime.InteropServices; // General Information about an assembly is controlled through the following // set of attributes. Change these attribute values to modify the information // associated with an assembly. -[assembly: AssemblyTitle("NzbDrone.Libraries.Test")] +[assembly: AssemblyTitle("Lidarr.Libraries.Test")] [assembly: AssemblyDescription("")] [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("NzbDrone.Libraries.Test")] +[assembly: AssemblyProduct("Lidarr.Libraries.Test")] [assembly: AssemblyCopyright("Copyright © 2013")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] @@ -21,4 +21,3 @@ using System.Runtime.InteropServices; // The following GUID is for the ID of the typelib if this project is exposed to COM [assembly: Guid("32ec29e2-40ba-4050-917d-e295d85d4969")] -[assembly: AssemblyVersion("10.0.0.*")] diff --git a/src/NzbDrone.Libraries.Test/app.config b/src/NzbDrone.Libraries.Test/app.config index c1684a7be..077a6a061 100644 --- a/src/NzbDrone.Libraries.Test/app.config +++ b/src/NzbDrone.Libraries.Test/app.config @@ -10,6 +10,10 @@ <assemblyIdentity name="FluentMigrator" publicKeyToken="aacfc7de5acabf05" culture="neutral" /> <bindingRedirect oldVersion="0.0.0.0-1.3.1.0" newVersion="1.3.1.0" /> </dependentAssembly> + <dependentAssembly> + <assemblyIdentity name="Microsoft.Practices.ServiceLocation" publicKeyToken="31bf3856ad364e35" culture="neutral" /> + <bindingRedirect oldVersion="0.0.0.0-1.3.0.0" newVersion="1.3.0.0" /> + </dependentAssembly> </assemblyBinding> </runtime> -</configuration> \ No newline at end of file +<startup><supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.6.1" /></startup></configuration> diff --git a/src/NzbDrone.Libraries.Test/packages.config b/src/NzbDrone.Libraries.Test/packages.config index 413eb1612..1ebfe9722 100644 --- a/src/NzbDrone.Libraries.Test/packages.config +++ b/src/NzbDrone.Libraries.Test/packages.config @@ -1,6 +1,6 @@ <?xml version="1.0" encoding="utf-8"?> <packages> - <package id="FluentAssertions" version="4.19.0" targetFramework="net40" /> - <package id="Newtonsoft.Json" version="9.0.1" targetFramework="net40" /> - <package id="NUnit" version="3.6.0" targetFramework="net40" /> + <package id="FluentAssertions" version="4.19.0" targetFramework="net461" /> + <package id="Newtonsoft.Json" version="9.0.1" targetFramework="net461" /> + <package id="NUnit" version="3.6.0" targetFramework="net461" /> </packages> \ No newline at end of file diff --git a/src/NzbDrone.Mono.Test/NzbDrone.Mono.Test.csproj b/src/NzbDrone.Mono.Test/NzbDrone.Mono.Test.csproj index 9e78587a3..1caca6658 100644 --- a/src/NzbDrone.Mono.Test/NzbDrone.Mono.Test.csproj +++ b/src/NzbDrone.Mono.Test/NzbDrone.Mono.Test.csproj @@ -1,5 +1,5 @@ <?xml version="1.0" encoding="utf-8"?> -<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> +<Project ToolsVersion="12.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> <Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" /> <PropertyGroup> <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration> @@ -8,11 +8,12 @@ <OutputType>Library</OutputType> <AppDesignerFolder>Properties</AppDesignerFolder> <RootNamespace>NzbDrone.Mono.Test</RootNamespace> - <AssemblyName>NzbDrone.Mono.Test</AssemblyName> - <TargetFrameworkVersion>v4.0</TargetFrameworkVersion> + <AssemblyName>Lidarr.Mono.Test</AssemblyName> + <TargetFrameworkVersion>v4.6.1</TargetFrameworkVersion> <FileAlignment>512</FileAlignment> <SolutionDir Condition="$(SolutionDir) == '' Or $(SolutionDir) == '*Undefined*'">..\</SolutionDir> <RestorePackages>true</RestorePackages> + <TargetFrameworkProfile /> </PropertyGroup> <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' "> <DebugSymbols>true</DebugSymbols> @@ -22,6 +23,7 @@ <DefineConstants>DEBUG;TRACE</DefineConstants> <ErrorReport>prompt</ErrorReport> <WarningLevel>4</WarningLevel> + <Prefer32Bit>false</Prefer32Bit> </PropertyGroup> <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' "> <DebugType>pdbonly</DebugType> @@ -30,6 +32,7 @@ <DefineConstants>TRACE</DefineConstants> <ErrorReport>prompt</ErrorReport> <WarningLevel>4</WarningLevel> + <Prefer32Bit>false</Prefer32Bit> </PropertyGroup> <PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x86'"> <DebugSymbols>true</DebugSymbols> @@ -39,6 +42,7 @@ <PlatformTarget>x86</PlatformTarget> <ErrorReport>prompt</ErrorReport> <CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet> + <Prefer32Bit>false</Prefer32Bit> </PropertyGroup> <PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x86'"> <OutputPath>bin\x86\Release\</OutputPath> @@ -48,13 +52,14 @@ <PlatformTarget>x86</PlatformTarget> <ErrorReport>prompt</ErrorReport> <CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet> + <Prefer32Bit>false</Prefer32Bit> </PropertyGroup> <ItemGroup> <Reference Include="FluentAssertions, Version=4.19.0.0, Culture=neutral, PublicKeyToken=33f2691a05b67b6a, processorArchitecture=MSIL"> - <HintPath>..\packages\FluentAssertions.4.19.0\lib\net40\FluentAssertions.dll</HintPath> + <HintPath>..\packages\FluentAssertions.4.19.0\lib\net45\FluentAssertions.dll</HintPath> </Reference> <Reference Include="FluentAssertions.Core, Version=4.19.0.0, Culture=neutral, PublicKeyToken=33f2691a05b67b6a, processorArchitecture=MSIL"> - <HintPath>..\packages\FluentAssertions.4.19.0\lib\net40\FluentAssertions.Core.dll</HintPath> + <HintPath>..\packages\FluentAssertions.4.19.0\lib\net45\FluentAssertions.Core.dll</HintPath> </Reference> <Reference Include="Mono.Posix, Version=4.0.0.0, Culture=neutral, PublicKeyToken=0738eb9f132ed756, processorArchitecture=MSIL"> <SpecificVersion>False</SpecificVersion> @@ -62,10 +67,9 @@ </Reference> <Reference Include="Moq, Version=4.0.10827.0, Culture=neutral, PublicKeyToken=69f491c39445e920, processorArchitecture=MSIL"> <HintPath>..\packages\Moq.4.0.10827\lib\NET40\Moq.dll</HintPath> - <Private>True</Private> </Reference> <Reference Include="nunit.framework, Version=3.6.0.0, Culture=neutral, PublicKeyToken=2638cd05610744eb, processorArchitecture=MSIL"> - <HintPath>..\packages\NUnit.3.6.0\lib\net40\nunit.framework.dll</HintPath> + <HintPath>..\packages\NUnit.3.6.0\lib\net45\nunit.framework.dll</HintPath> </Reference> <Reference Include="System" /> <Reference Include="System.Core" /> diff --git a/src/NzbDrone.Mono.Test/Properties/AssemblyInfo.cs b/src/NzbDrone.Mono.Test/Properties/AssemblyInfo.cs index 012007b52..9bacc9f2f 100644 --- a/src/NzbDrone.Mono.Test/Properties/AssemblyInfo.cs +++ b/src/NzbDrone.Mono.Test/Properties/AssemblyInfo.cs @@ -1,14 +1,14 @@ -using System.Reflection; +using System.Reflection; using System.Runtime.InteropServices; // General Information about an assembly is controlled through the following // set of attributes. Change these attribute values to modify the information // associated with an assembly. -[assembly: AssemblyTitle("NzbDrone.Mono.Test")] +[assembly: AssemblyTitle("Lidarr.Mono.Test")] [assembly: AssemblyDescription("")] [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("NzbDrone.Mono.Test")] +[assembly: AssemblyProduct("Lidarr.Mono.Test")] [assembly: AssemblyCopyright("Copyright © 2014")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] diff --git a/src/NzbDrone.Mono.Test/app.config b/src/NzbDrone.Mono.Test/app.config index b6d9543c5..8e850db4b 100644 --- a/src/NzbDrone.Mono.Test/app.config +++ b/src/NzbDrone.Mono.Test/app.config @@ -4,7 +4,7 @@ <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1"> <dependentAssembly> <assemblyIdentity name="Microsoft.Owin" publicKeyToken="31bf3856ad364e35" culture="neutral" /> - <bindingRedirect oldVersion="0.0.0.0-2.1.0.0" newVersion="2.1.0.0" /> + <bindingRedirect oldVersion="0.0.0.0-3.1.0.0" newVersion="3.1.0.0" /> </dependentAssembly> <dependentAssembly> <assemblyIdentity name="Newtonsoft.Json" publicKeyToken="30ad4fe6b2a6aeed" culture="neutral" /> @@ -14,6 +14,14 @@ <assemblyIdentity name="FluentMigrator" publicKeyToken="aacfc7de5acabf05" culture="neutral" /> <bindingRedirect oldVersion="0.0.0.0-1.3.1.0" newVersion="1.3.1.0" /> </dependentAssembly> + <dependentAssembly> + <assemblyIdentity name="Microsoft.Practices.ServiceLocation" publicKeyToken="31bf3856ad364e35" culture="neutral" /> + <bindingRedirect oldVersion="0.0.0.0-1.3.0.0" newVersion="1.3.0.0" /> + </dependentAssembly> + <dependentAssembly> + <assemblyIdentity name="Microsoft.Owin.Security" publicKeyToken="31bf3856ad364e35" culture="neutral" /> + <bindingRedirect oldVersion="0.0.0.0-3.1.0.0" newVersion="3.1.0.0" /> + </dependentAssembly> </assemblyBinding> </runtime> -</configuration> \ No newline at end of file +<startup><supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.6.1" /></startup></configuration> diff --git a/src/NzbDrone.Mono.Test/packages.config b/src/NzbDrone.Mono.Test/packages.config index 337af9af9..2b0cc4306 100644 --- a/src/NzbDrone.Mono.Test/packages.config +++ b/src/NzbDrone.Mono.Test/packages.config @@ -1,6 +1,6 @@ <?xml version="1.0" encoding="utf-8"?> <packages> - <package id="FluentAssertions" version="4.19.0" targetFramework="net40" /> - <package id="Moq" version="4.0.10827" targetFramework="net40" /> - <package id="NUnit" version="3.6.0" targetFramework="net40" /> + <package id="FluentAssertions" version="4.19.0" targetFramework="net461" /> + <package id="Moq" version="4.0.10827" targetFramework="net461" /> + <package id="NUnit" version="3.6.0" targetFramework="net461" /> </packages> \ No newline at end of file diff --git a/src/NzbDrone.Mono/Disk/DiskProvider.cs b/src/NzbDrone.Mono/Disk/DiskProvider.cs index d9f3cba29..bc47532fb 100644 --- a/src/NzbDrone.Mono/Disk/DiskProvider.cs +++ b/src/NzbDrone.Mono/Disk/DiskProvider.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.IO; using System.Linq; @@ -40,24 +40,15 @@ namespace NzbDrone.Mono.Disk { Ensure.That(path, () => path).IsValidPath(); - try - { - var mount = GetMount(path); - - if (mount == null) - { - Logger.Debug("Unable to get free space for '{0}', unable to find suitable drive", path); - return null; - } + var mount = GetMount(path); - return mount.AvailableFreeSpace; - } - catch (InvalidOperationException ex) + if (mount == null) { - Logger.Error(ex, "Couldn't get free space for {0}", path); + Logger.Debug("Unable to get free space for '{0}', unable to find suitable drive", path); + return null; } - return null; + return mount.AvailableFreeSpace; } public override void InheritFolderPermissions(string filename) @@ -86,31 +77,23 @@ namespace NzbDrone.Mono.Disk public override List<IMount> GetMounts() { - return GetDriveInfoMounts().Select(d => new DriveInfoMount(d, FindDriveType.Find(d.DriveFormat))) - .Where(d => d.DriveType == DriveType.Fixed || d.DriveType == DriveType.Network || d.DriveType == DriveType.Removable) - .Concat(_procMountProvider.GetMounts()) - .DistinctBy(v => v.RootDirectory) - .ToList(); + return _procMountProvider.GetMounts() + .Concat(GetDriveInfoMounts() + .Select(d => new DriveInfoMount(d, FindDriveType.Find(d.DriveFormat))) + .Where(d => d.DriveType == DriveType.Fixed || + d.DriveType == DriveType.Network || d.DriveType == + DriveType.Removable)) + .DistinctBy(v => v.RootDirectory) + .ToList(); } public override long? GetTotalSize(string path) { Ensure.That(path, () => path).IsValidPath(); - try - { - var mount = GetMount(path); - - if (mount == null) return null; - - return mount.TotalSize; - } - catch (InvalidOperationException e) - { - Logger.Error(e, "Couldn't get total space for {0}", path); - } + var mount = GetMount(path); - return null; + return mount?.TotalSize; } public override bool TryCreateHardLink(string source, string destination) diff --git a/src/NzbDrone.Mono/Disk/ProcMount.cs b/src/NzbDrone.Mono/Disk/ProcMount.cs index 87e428112..5d7160440 100644 --- a/src/NzbDrone.Mono/Disk/ProcMount.cs +++ b/src/NzbDrone.Mono/Disk/ProcMount.cs @@ -10,12 +10,13 @@ namespace NzbDrone.Mono.Disk { private readonly UnixDriveInfo _unixDriveInfo; - public ProcMount(DriveType driveType, string name, string mount, string type, Dictionary<string, string> options) + public ProcMount(DriveType driveType, string name, string mount, string type, MountOptions mountOptions) { DriveType = driveType; Name = name; RootDirectory = mount; DriveFormat = type; + MountOptions = mountOptions; _unixDriveInfo = new UnixDriveInfo(mount); } @@ -28,6 +29,8 @@ namespace NzbDrone.Mono.Disk public bool IsReady => _unixDriveInfo.IsReady; + public MountOptions MountOptions { get; private set; } + public string Name { get; private set; } public string RootDirectory { get; private set; } @@ -42,7 +45,7 @@ namespace NzbDrone.Mono.Disk { get { - if (VolumeLabel.IsNullOrWhiteSpace()) + if (VolumeLabel.IsNullOrWhiteSpace() || VolumeLabel.StartsWith("UUID=") || Name == VolumeLabel) { return Name; } diff --git a/src/NzbDrone.Mono/Disk/ProcMountProvider.cs b/src/NzbDrone.Mono/Disk/ProcMountProvider.cs index caa9cc467..ded2d2b36 100644 --- a/src/NzbDrone.Mono/Disk/ProcMountProvider.cs +++ b/src/NzbDrone.Mono/Disk/ProcMountProvider.cs @@ -130,7 +130,7 @@ namespace NzbDrone.Mono.Disk driveType = DriveType.Network; } - return new ProcMount(driveType, name, mount, type, options); + return new ProcMount(driveType, name, mount, type, new MountOptions(options)); } private Dictionary<string, string> ParseOptions(string options) diff --git a/src/NzbDrone.Mono/NzbDrone.Mono.csproj b/src/NzbDrone.Mono/NzbDrone.Mono.csproj index c8532b6f0..34516784d 100644 --- a/src/NzbDrone.Mono/NzbDrone.Mono.csproj +++ b/src/NzbDrone.Mono/NzbDrone.Mono.csproj @@ -1,5 +1,5 @@ <?xml version="1.0" encoding="utf-8"?> -<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> +<Project ToolsVersion="12.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> <Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" /> <PropertyGroup> <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration> @@ -8,8 +8,8 @@ <OutputType>Library</OutputType> <AppDesignerFolder>Properties</AppDesignerFolder> <RootNamespace>NzbDrone.Mono</RootNamespace> - <AssemblyName>NzbDrone.Mono</AssemblyName> - <TargetFrameworkVersion>v4.0</TargetFrameworkVersion> + <AssemblyName>Lidarr.Mono</AssemblyName> + <TargetFrameworkVersion>v4.6.1</TargetFrameworkVersion> <FileAlignment>512</FileAlignment> <TargetFrameworkProfile /> <SolutionDir Condition="$(SolutionDir) == '' Or $(SolutionDir) == '*Undefined*'">..\</SolutionDir> @@ -23,6 +23,7 @@ <DefineConstants>DEBUG;TRACE</DefineConstants> <ErrorReport>prompt</ErrorReport> <WarningLevel>4</WarningLevel> + <Prefer32Bit>false</Prefer32Bit> </PropertyGroup> <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' "> <DebugType>pdbonly</DebugType> @@ -31,6 +32,7 @@ <DefineConstants>TRACE</DefineConstants> <ErrorReport>prompt</ErrorReport> <WarningLevel>4</WarningLevel> + <Prefer32Bit>false</Prefer32Bit> </PropertyGroup> <PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x86'"> <DebugSymbols>true</DebugSymbols> @@ -40,6 +42,7 @@ <PlatformTarget>x86</PlatformTarget> <ErrorReport>prompt</ErrorReport> <CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet> + <Prefer32Bit>false</Prefer32Bit> </PropertyGroup> <PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x86'"> <OutputPath>..\..\_output\</OutputPath> @@ -49,10 +52,11 @@ <PlatformTarget>x86</PlatformTarget> <ErrorReport>prompt</ErrorReport> <CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet> + <Prefer32Bit>false</Prefer32Bit> </PropertyGroup> <ItemGroup> <Reference Include="NLog, Version=4.0.0.0, Culture=neutral, PublicKeyToken=5120e14c03d0593c, processorArchitecture=MSIL"> - <HintPath>..\packages\NLog.4.4.3\lib\net40\NLog.dll</HintPath> + <HintPath>..\packages\NLog.4.4.12\lib\net45\NLog.dll</HintPath> </Reference> <Reference Include="System" /> <Reference Include="System.Core" /> diff --git a/src/NzbDrone.Mono/Properties/AssemblyInfo.cs b/src/NzbDrone.Mono/Properties/AssemblyInfo.cs index f78631ed8..f9d51f7d9 100644 --- a/src/NzbDrone.Mono/Properties/AssemblyInfo.cs +++ b/src/NzbDrone.Mono/Properties/AssemblyInfo.cs @@ -1,14 +1,14 @@ -using System.Reflection; +using System.Reflection; using System.Runtime.InteropServices; // General Information about an assembly is controlled through the following // set of attributes. Change these attribute values to modify the information // associated with an assembly. -[assembly: AssemblyTitle("NzbDrone.Mono")] +[assembly: AssemblyTitle("Lidarr.Mono")] [assembly: AssemblyDescription("")] [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("NzbDrone.Mono")] +[assembly: AssemblyProduct("Lidarr.Mono")] [assembly: AssemblyCopyright("Copyright © 2014")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] @@ -21,4 +21,3 @@ using System.Runtime.InteropServices; // The following GUID is for the ID of the typelib if this project is exposed to COM [assembly: Guid("01493ea5-494f-43bf-be18-8ae4d0708fc6")] -[assembly: AssemblyVersion("10.0.0.*")] diff --git a/src/NzbDrone.Mono/app.config b/src/NzbDrone.Mono/app.config index 8460dd432..9566343db 100644 --- a/src/NzbDrone.Mono/app.config +++ b/src/NzbDrone.Mono/app.config @@ -8,4 +8,4 @@ </dependentAssembly> </assemblyBinding> </runtime> -</configuration> \ No newline at end of file +<startup><supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.6.1" /></startup></configuration> diff --git a/src/NzbDrone.Mono/packages.config b/src/NzbDrone.Mono/packages.config index a14101dce..80757f37a 100644 --- a/src/NzbDrone.Mono/packages.config +++ b/src/NzbDrone.Mono/packages.config @@ -1,4 +1,4 @@ <?xml version="1.0" encoding="utf-8"?> <packages> - <package id="NLog" version="4.4.3" targetFramework="net40" /> + <package id="NLog" version="4.4.12" targetFramework="net461" /> </packages> \ No newline at end of file diff --git a/src/NzbDrone.SignalR/LidarrPerformanceCounterManager.cs b/src/NzbDrone.SignalR/LidarrPerformanceCounterManager.cs new file mode 100644 index 000000000..1576b5e28 --- /dev/null +++ b/src/NzbDrone.SignalR/LidarrPerformanceCounterManager.cs @@ -0,0 +1,58 @@ +using System.Threading; +using Microsoft.AspNet.SignalR.Infrastructure; + +namespace NzbDrone.SignalR +{ + public class LidarrPerformanceCounterManager : IPerformanceCounterManager + { + private readonly IPerformanceCounter _counter = new NoOpPerformanceCounter(); + + public void Initialize(string instanceName, CancellationToken hostShutdownToken) + { + + } + + public IPerformanceCounter LoadCounter(string categoryName, string counterName, string instanceName, bool isReadOnly) + { + return _counter; + } + + public IPerformanceCounter ConnectionsConnected => _counter; + public IPerformanceCounter ConnectionsReconnected => _counter; + public IPerformanceCounter ConnectionsDisconnected => _counter; + public IPerformanceCounter ConnectionsCurrent => _counter; + public IPerformanceCounter ConnectionMessagesReceivedTotal => _counter; + public IPerformanceCounter ConnectionMessagesSentTotal => _counter; + public IPerformanceCounter ConnectionMessagesReceivedPerSec => _counter; + public IPerformanceCounter ConnectionMessagesSentPerSec => _counter; + public IPerformanceCounter MessageBusMessagesReceivedTotal => _counter; + public IPerformanceCounter MessageBusMessagesReceivedPerSec => _counter; + public IPerformanceCounter ScaleoutMessageBusMessagesReceivedPerSec => _counter; + public IPerformanceCounter MessageBusMessagesPublishedTotal => _counter; + public IPerformanceCounter MessageBusMessagesPublishedPerSec => _counter; + public IPerformanceCounter MessageBusSubscribersCurrent => _counter; + public IPerformanceCounter MessageBusSubscribersTotal => _counter; + public IPerformanceCounter MessageBusSubscribersPerSec => _counter; + public IPerformanceCounter MessageBusAllocatedWorkers => _counter; + public IPerformanceCounter MessageBusBusyWorkers => _counter; + public IPerformanceCounter MessageBusTopicsCurrent => _counter; + public IPerformanceCounter ErrorsAllTotal => _counter; + public IPerformanceCounter ErrorsAllPerSec => _counter; + public IPerformanceCounter ErrorsHubResolutionTotal => _counter; + public IPerformanceCounter ErrorsHubResolutionPerSec => _counter; + public IPerformanceCounter ErrorsHubInvocationTotal => _counter; + public IPerformanceCounter ErrorsHubInvocationPerSec => _counter; + public IPerformanceCounter ErrorsTransportTotal => _counter; + public IPerformanceCounter ErrorsTransportPerSec => _counter; + public IPerformanceCounter ScaleoutStreamCountTotal => _counter; + public IPerformanceCounter ScaleoutStreamCountOpen => _counter; + public IPerformanceCounter ScaleoutStreamCountBuffering => _counter; + public IPerformanceCounter ScaleoutErrorsTotal => _counter; + public IPerformanceCounter ScaleoutErrorsPerSec => _counter; + public IPerformanceCounter ScaleoutSendQueueLength => _counter; + public IPerformanceCounter ConnectionsCurrentForeverFrame => _counter; + public IPerformanceCounter ConnectionsCurrentLongPolling => _counter; + public IPerformanceCounter ConnectionsCurrentServerSentEvents => _counter; + public IPerformanceCounter ConnectionsCurrentWebSockets => _counter; + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.SignalR.Core/Infrastructure/NoOpPerformanceCounter.cs b/src/NzbDrone.SignalR/NoOpPerformanceCounter.cs similarity index 79% rename from src/Microsoft.AspNet.SignalR.Core/Infrastructure/NoOpPerformanceCounter.cs rename to src/NzbDrone.SignalR/NoOpPerformanceCounter.cs index cf45a92fa..301d89138 100644 --- a/src/Microsoft.AspNet.SignalR.Core/Infrastructure/NoOpPerformanceCounter.cs +++ b/src/NzbDrone.SignalR/NoOpPerformanceCounter.cs @@ -1,8 +1,7 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - using System.Diagnostics; +using Microsoft.AspNet.SignalR.Infrastructure; -namespace Microsoft.AspNet.SignalR.Infrastructure +namespace NzbDrone.SignalR { public class NoOpPerformanceCounter : IPerformanceCounter { @@ -42,7 +41,7 @@ namespace Microsoft.AspNet.SignalR.Infrastructure public void RemoveInstance() { - + } public CounterSample NextSample() diff --git a/src/NzbDrone.SignalR/NzbDrone.SignalR.csproj b/src/NzbDrone.SignalR/NzbDrone.SignalR.csproj index 0cabc3c01..e05444021 100644 --- a/src/NzbDrone.SignalR/NzbDrone.SignalR.csproj +++ b/src/NzbDrone.SignalR/NzbDrone.SignalR.csproj @@ -1,5 +1,5 @@ <?xml version="1.0" encoding="utf-8"?> -<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> +<Project ToolsVersion="12.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> <Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" /> <PropertyGroup> <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration> @@ -8,13 +8,14 @@ <OutputType>Library</OutputType> <AppDesignerFolder>Properties</AppDesignerFolder> <RootNamespace>NzbDrone.SignalR</RootNamespace> - <AssemblyName>NzbDrone.SignalR</AssemblyName> - <TargetFrameworkVersion>v4.0</TargetFrameworkVersion> + <AssemblyName>Lidarr.SignalR</AssemblyName> + <TargetFrameworkVersion>v4.6.1</TargetFrameworkVersion> <FileAlignment>512</FileAlignment> <SolutionDir Condition="$(SolutionDir) == '' Or $(SolutionDir) == '*Undefined*'">..\</SolutionDir> <RestorePackages>true</RestorePackages> <ProductVersion>12.0.0</ProductVersion> <SchemaVersion>2.0</SchemaVersion> + <TargetFrameworkProfile /> </PropertyGroup> <PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x86'"> <DebugSymbols>true</DebugSymbols> @@ -26,6 +27,7 @@ <CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet> <WarningLevel>4</WarningLevel> <Optimize>false</Optimize> + <Prefer32Bit>false</Prefer32Bit> </PropertyGroup> <PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x86'"> <OutputPath>..\..\_output\</OutputPath> @@ -36,11 +38,38 @@ <ErrorReport>prompt</ErrorReport> <CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet> <WarningLevel>4</WarningLevel> + <Prefer32Bit>false</Prefer32Bit> </PropertyGroup> <ItemGroup> + <Reference Include="Microsoft.AspNet.SignalR.Core, Version=2.2.2.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL"> + <HintPath>..\packages\Microsoft.AspNet.SignalR.Core.2.2.2\lib\net45\Microsoft.AspNet.SignalR.Core.dll</HintPath> + </Reference> + <Reference Include="Microsoft.AspNet.SignalR.SystemWeb, Version=2.2.2.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL"> + <HintPath>..\packages\Microsoft.AspNet.SignalR.SystemWeb.2.2.2\lib\net45\Microsoft.AspNet.SignalR.SystemWeb.dll</HintPath> + </Reference> + <Reference Include="Microsoft.Owin, Version=3.1.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL"> + <HintPath>..\packages\Microsoft.Owin.3.1.0\lib\net45\Microsoft.Owin.dll</HintPath> + </Reference> + <Reference Include="Microsoft.Owin.Diagnostics, Version=3.1.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL"> + <HintPath>..\packages\Microsoft.Owin.Diagnostics.3.1.0\lib\net45\Microsoft.Owin.Diagnostics.dll</HintPath> + </Reference> + <Reference Include="Microsoft.Owin.Host.HttpListener, Version=3.1.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL"> + <HintPath>..\packages\Microsoft.Owin.Host.HttpListener.3.1.0\lib\net45\Microsoft.Owin.Host.HttpListener.dll</HintPath> + </Reference> + <Reference Include="Microsoft.Owin.Host.SystemWeb, Version=3.1.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL"> + <HintPath>..\packages\Microsoft.Owin.Host.SystemWeb.3.1.0\lib\net45\Microsoft.Owin.Host.SystemWeb.dll</HintPath> + </Reference> + <Reference Include="Microsoft.Owin.Hosting, Version=3.1.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL"> + <HintPath>..\packages\Microsoft.Owin.Hosting.3.1.0\lib\net45\Microsoft.Owin.Hosting.dll</HintPath> + </Reference> + <Reference Include="Microsoft.Owin.Security, Version=3.1.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL"> + <HintPath>..\packages\Microsoft.Owin.Security.3.1.0\lib\net45\Microsoft.Owin.Security.dll</HintPath> + </Reference> <Reference Include="Newtonsoft.Json, Version=9.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL"> - <HintPath>..\packages\Newtonsoft.Json.9.0.1\lib\net40\Newtonsoft.Json.dll</HintPath> - <Private>True</Private> + <HintPath>..\packages\Newtonsoft.Json.9.0.1\lib\net45\Newtonsoft.Json.dll</HintPath> + </Reference> + <Reference Include="Owin, Version=1.0.0.0, Culture=neutral, PublicKeyToken=f0ebd12fd5e55cc5, processorArchitecture=MSIL"> + <HintPath>..\packages\Owin.1.0\lib\net40\Owin.dll</HintPath> </Reference> <Reference Include="System" /> <Reference Include="System.Core" /> @@ -50,18 +79,16 @@ <Compile Include="..\NzbDrone.Common\Properties\SharedAssemblyInfo.cs"> <Link>Properties\SharedAssemblyInfo.cs</Link> </Compile> + <Compile Include="NoOpPerformanceCounter.cs" /> <Compile Include="NzbDronePersistentConnection.cs" /> <Compile Include="Properties\AssemblyInfo.cs" /> - <Compile Include="Serializer.cs" /> + <Compile Include="SignalRContractResolver.cs" /> <Compile Include="SignalrDependencyResolver.cs" /> + <Compile Include="SignalRJsonSerializer.cs" /> <Compile Include="SignalRMessage.cs" /> - <Compile Include="SonarrPerformanceCounterManager.cs" /> + <Compile Include="LidarrPerformanceCounterManager.cs" /> </ItemGroup> <ItemGroup> - <ProjectReference Include="..\Microsoft.AspNet.SignalR.Core\Microsoft.AspNet.SignalR.Core.csproj"> - <Project>{1B9A82C4-BCA1-4834-A33E-226F17BE070B}</Project> - <Name>Microsoft.AspNet.SignalR.Core</Name> - </ProjectReference> <ProjectReference Include="..\NzbDrone.Common\NzbDrone.Common.csproj"> <Project>{F2BE0FDF-6E47-4827-A420-DD4EF82407F8}</Project> <Name>NzbDrone.Common</Name> diff --git a/src/NzbDrone.SignalR/NzbDronePersistentConnection.cs b/src/NzbDrone.SignalR/NzbDronePersistentConnection.cs index b3342ffba..ff1a2dca8 100644 --- a/src/NzbDrone.SignalR/NzbDronePersistentConnection.cs +++ b/src/NzbDrone.SignalR/NzbDronePersistentConnection.cs @@ -1,7 +1,12 @@ -using Microsoft.AspNet.SignalR; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.AspNet.SignalR; using Microsoft.AspNet.SignalR.Infrastructure; +using NzbDrone.Common.EnvironmentInfo; +using NzbDrone.Common.Serializer; using NzbDrone.Common.Extensions; using NzbDrone.Core.Configuration; +using NzbDrone.Core.Datastore.Events; namespace NzbDrone.SignalR { @@ -15,17 +20,31 @@ namespace NzbDrone.SignalR private IPersistentConnectionContext Context => ((ConnectionManager)GlobalHost.ConnectionManager).GetConnection(GetType()); private static string API_KEY; + private readonly Dictionary<string, string> _messageHistory; public NzbDronePersistentConnection(IConfigFileProvider configFileProvider) { API_KEY = configFileProvider.ApiKey; + _messageHistory = new Dictionary<string, string>(); } + public void BroadcastMessage(SignalRMessage message) { + string lastMessage; + if (_messageHistory.TryGetValue(message.Name, out lastMessage)) + { + if (message.Action == ModelAction.Updated && message.Body.ToJson() == lastMessage) + { + return; + } + } + + _messageHistory[message.Name] = message.Body.ToJson(); + Context.Connection.Broadcast(message); } - + protected override bool AuthorizeRequest(IRequest request) { var apiKey = request.QueryString["apiKey"]; @@ -37,5 +56,27 @@ namespace NzbDrone.SignalR return false; } + + protected override Task OnConnected(IRequest request, string connectionId) + { + return SendVersion(connectionId); + } + + protected override Task OnReconnected(IRequest request, string connectionId) + { + return SendVersion(connectionId); + } + + private Task SendVersion(string connectionId) + { + return Context.Connection.Send(connectionId, new SignalRMessage + { + Name = "version", + Body = new + { + Version = BuildInfo.Version.ToString() + } + }); + } } } \ No newline at end of file diff --git a/src/NzbDrone.SignalR/Properties/AssemblyInfo.cs b/src/NzbDrone.SignalR/Properties/AssemblyInfo.cs index 7d5972415..9bdb0da6c 100644 --- a/src/NzbDrone.SignalR/Properties/AssemblyInfo.cs +++ b/src/NzbDrone.SignalR/Properties/AssemblyInfo.cs @@ -1,10 +1,9 @@ -using System.Reflection; +using System.Reflection; using System.Runtime.InteropServices; // General Information about an assembly is controlled through the following // set of attributes. Change these attribute values to modify the information // associated with an assembly. -[assembly: AssemblyTitle("NzbDrone.SignalR")] +[assembly: AssemblyTitle("Lidarr.SignalR")] [assembly: Guid("98bd985a-4f23-4201-8ed3-f6f3d7f2a5fe")] -[assembly: AssemblyVersion("10.0.0.*")] diff --git a/src/NzbDrone.SignalR/Serializer.cs b/src/NzbDrone.SignalR/Serializer.cs deleted file mode 100644 index e631ef146..000000000 --- a/src/NzbDrone.SignalR/Serializer.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System; -using System.IO; -using Microsoft.AspNet.SignalR.Json; -using NzbDrone.Common.Serializer; - -namespace NzbDrone.SignalR -{ - public class Serializer : IJsonSerializer - { - private readonly JsonNetSerializer _signalRSerializer = new JsonNetSerializer(); - - public void Serialize(object value, TextWriter writer) - { - if (value.GetType().FullName.StartsWith("NzbDrone")) - { - Json.Serialize(value, writer); - } - else - { - _signalRSerializer.Serialize(value, writer); - } - } - - public object Parse(TextReader reader, Type targetType) - { - return Json.Deserialize(reader.ReadToEnd(), targetType); - } - } -} \ No newline at end of file diff --git a/src/NzbDrone.SignalR/SignalRContractResolver.cs b/src/NzbDrone.SignalR/SignalRContractResolver.cs new file mode 100644 index 000000000..0f766d90a --- /dev/null +++ b/src/NzbDrone.SignalR/SignalRContractResolver.cs @@ -0,0 +1,28 @@ +using System; +using Newtonsoft.Json.Serialization; + +namespace NzbDrone.SignalR +{ + public class SignalRContractResolver : IContractResolver + { + private readonly IContractResolver _camelCaseContractResolver; + private readonly IContractResolver _defaultContractSerializer; + + public SignalRContractResolver() + { + _defaultContractSerializer = new DefaultContractResolver(); + _camelCaseContractResolver = new CamelCasePropertyNamesContractResolver(); + } + + public JsonContract ResolveContract(Type type) + { + var fullName = type.FullName; + if (fullName.StartsWith("NzbDrone") || fullName.StartsWith("Lidarr")) + { + return _camelCaseContractResolver.ResolveContract(type); + } + + return _defaultContractSerializer.ResolveContract(type); + } + } +} diff --git a/src/NzbDrone.SignalR/SignalRJsonSerializer.cs b/src/NzbDrone.SignalR/SignalRJsonSerializer.cs new file mode 100644 index 000000000..031a03f89 --- /dev/null +++ b/src/NzbDrone.SignalR/SignalRJsonSerializer.cs @@ -0,0 +1,22 @@ +using Microsoft.AspNet.SignalR; +using Newtonsoft.Json; +using NzbDrone.Common.Serializer; + +namespace NzbDrone.SignalR +{ + public static class SignalRJsonSerializer + { + private static JsonSerializer _serializer; + private static JsonSerializerSettings _serializerSettings; + + public static void Register() + { + _serializerSettings = Json.GetSerializerSettings(); + _serializerSettings.ContractResolver = new SignalRContractResolver(); + + _serializer = JsonSerializer.Create(_serializerSettings); + + GlobalHost.DependencyResolver.Register(typeof(JsonSerializer), () => _serializer); + } + } +} diff --git a/src/NzbDrone.SignalR/SignalRMessage.cs b/src/NzbDrone.SignalR/SignalRMessage.cs index e8993c286..17a7d4187 100644 --- a/src/NzbDrone.SignalR/SignalRMessage.cs +++ b/src/NzbDrone.SignalR/SignalRMessage.cs @@ -1,8 +1,14 @@ +using Newtonsoft.Json; +using NzbDrone.Core.Datastore.Events; + namespace NzbDrone.SignalR { public class SignalRMessage { public object Body { get; set; } public string Name { get; set; } + + [JsonIgnore] + public ModelAction Action { get; set; } } } \ No newline at end of file diff --git a/src/NzbDrone.SignalR/SignalrDependencyResolver.cs b/src/NzbDrone.SignalR/SignalrDependencyResolver.cs index c8a1e7172..2a934ada3 100644 --- a/src/NzbDrone.SignalR/SignalrDependencyResolver.cs +++ b/src/NzbDrone.SignalR/SignalrDependencyResolver.cs @@ -1,20 +1,20 @@ -using System; +using System; using Microsoft.AspNet.SignalR; using Microsoft.AspNet.SignalR.Infrastructure; using NzbDrone.Common.Composition; namespace NzbDrone.SignalR { - public class SignalrDependencyResolver : DefaultDependencyResolver + public class SignalRDependencyResolver : DefaultDependencyResolver { private readonly IContainer _container; public static void Register(IContainer container) { - GlobalHost.DependencyResolver = new SignalrDependencyResolver(container); + GlobalHost.DependencyResolver = new SignalRDependencyResolver(container); } - private SignalrDependencyResolver(IContainer container) + private SignalRDependencyResolver(IContainer container) { _container = container; var performanceCounterManager = new LidarrPerformanceCounterManager(); @@ -23,6 +23,17 @@ namespace NzbDrone.SignalR public override object GetService(Type serviceType) { + // Microsoft.AspNet.SignalR.Infrastructure.AckSubscriber is not registered in our internal contaiiner, + // but it still gets treated like it is (possibly due to being a concrete type). + + var fullName = serviceType.FullName; + + if (fullName == "Microsoft.AspNet.SignalR.Infrastructure.AckSubscriber" || + fullName == "Newtonsoft.Json.JsonSerializer") + { + return base.GetService(serviceType); + } + if (_container.IsTypeRegistered(serviceType)) { return _container.Resolve(serviceType); @@ -31,4 +42,4 @@ namespace NzbDrone.SignalR return base.GetService(serviceType); } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.SignalR/SonarrPerformanceCounterManager.cs b/src/NzbDrone.SignalR/SonarrPerformanceCounterManager.cs deleted file mode 100644 index d75f9afe0..000000000 --- a/src/NzbDrone.SignalR/SonarrPerformanceCounterManager.cs +++ /dev/null @@ -1,54 +0,0 @@ -using System.Threading; -using Microsoft.AspNet.SignalR.Infrastructure; - -namespace NzbDrone.SignalR -{ - public class LidarrPerformanceCounterManager : IPerformanceCounterManager - { - private readonly IPerformanceCounter _counter = new NoOpPerformanceCounter(); - - public void Initialize(string instanceName, CancellationToken hostShutdownToken) - { - - } - - public IPerformanceCounter LoadCounter(string categoryName, string counterName, string instanceName, bool isReadOnly) - { - return _counter; - } - - public IPerformanceCounter ConnectionsConnected => _counter; - public IPerformanceCounter ConnectionsReconnected => _counter; - public IPerformanceCounter ConnectionsDisconnected => _counter; - public IPerformanceCounter ConnectionsCurrent => _counter; - public IPerformanceCounter ConnectionMessagesReceivedTotal => _counter; - public IPerformanceCounter ConnectionMessagesSentTotal => _counter; - public IPerformanceCounter ConnectionMessagesReceivedPerSec => _counter; - public IPerformanceCounter ConnectionMessagesSentPerSec => _counter; - public IPerformanceCounter MessageBusMessagesReceivedTotal => _counter; - public IPerformanceCounter MessageBusMessagesReceivedPerSec => _counter; - public IPerformanceCounter ScaleoutMessageBusMessagesReceivedPerSec => _counter; - public IPerformanceCounter MessageBusMessagesPublishedTotal => _counter; - public IPerformanceCounter MessageBusMessagesPublishedPerSec => _counter; - public IPerformanceCounter MessageBusSubscribersCurrent => _counter; - public IPerformanceCounter MessageBusSubscribersTotal => _counter; - public IPerformanceCounter MessageBusSubscribersPerSec => _counter; - public IPerformanceCounter MessageBusAllocatedWorkers => _counter; - public IPerformanceCounter MessageBusBusyWorkers => _counter; - public IPerformanceCounter MessageBusTopicsCurrent => _counter; - public IPerformanceCounter ErrorsAllTotal => _counter; - public IPerformanceCounter ErrorsAllPerSec => _counter; - public IPerformanceCounter ErrorsHubResolutionTotal => _counter; - public IPerformanceCounter ErrorsHubResolutionPerSec => _counter; - public IPerformanceCounter ErrorsHubInvocationTotal => _counter; - public IPerformanceCounter ErrorsHubInvocationPerSec => _counter; - public IPerformanceCounter ErrorsTransportTotal => _counter; - public IPerformanceCounter ErrorsTransportPerSec => _counter; - public IPerformanceCounter ScaleoutStreamCountTotal => _counter; - public IPerformanceCounter ScaleoutStreamCountOpen => _counter; - public IPerformanceCounter ScaleoutStreamCountBuffering => _counter; - public IPerformanceCounter ScaleoutErrorsTotal => _counter; - public IPerformanceCounter ScaleoutErrorsPerSec => _counter; - public IPerformanceCounter ScaleoutSendQueueLength => _counter; - } -} \ No newline at end of file diff --git a/src/NzbDrone.SignalR/app.config b/src/NzbDrone.SignalR/app.config index c1684a7be..d4d6857aa 100644 --- a/src/NzbDrone.SignalR/app.config +++ b/src/NzbDrone.SignalR/app.config @@ -10,6 +10,14 @@ <assemblyIdentity name="FluentMigrator" publicKeyToken="aacfc7de5acabf05" culture="neutral" /> <bindingRedirect oldVersion="0.0.0.0-1.3.1.0" newVersion="1.3.1.0" /> </dependentAssembly> + <dependentAssembly> + <assemblyIdentity name="Microsoft.Owin" publicKeyToken="31bf3856ad364e35" culture="neutral" /> + <bindingRedirect oldVersion="0.0.0.0-3.1.0.0" newVersion="3.1.0.0" /> + </dependentAssembly> + <dependentAssembly> + <assemblyIdentity name="Microsoft.Owin.Security" publicKeyToken="31bf3856ad364e35" culture="neutral" /> + <bindingRedirect oldVersion="0.0.0.0-3.1.0.0" newVersion="3.1.0.0" /> + </dependentAssembly> </assemblyBinding> </runtime> -</configuration> \ No newline at end of file +<startup><supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.6.1" /></startup></configuration> diff --git a/src/NzbDrone.SignalR/packages.config b/src/NzbDrone.SignalR/packages.config index 7c276ed86..ee54882a5 100644 --- a/src/NzbDrone.SignalR/packages.config +++ b/src/NzbDrone.SignalR/packages.config @@ -1,4 +1,13 @@ <?xml version="1.0" encoding="utf-8"?> <packages> - <package id="Newtonsoft.Json" version="9.0.1" targetFramework="net40" /> + <package id="Microsoft.AspNet.SignalR.Core" version="2.2.2" targetFramework="net461" /> + <package id="Microsoft.AspNet.SignalR.SystemWeb" version="2.2.2" targetFramework="net461" /> + <package id="Microsoft.Owin" version="3.1.0" targetFramework="net461" /> + <package id="Microsoft.Owin.Diagnostics" version="3.1.0" targetFramework="net461" /> + <package id="Microsoft.Owin.Host.HttpListener" version="3.1.0" targetFramework="net461" /> + <package id="Microsoft.Owin.Host.SystemWeb" version="3.1.0" targetFramework="net461" /> + <package id="Microsoft.Owin.Hosting" version="3.1.0" targetFramework="net461" /> + <package id="Microsoft.Owin.Security" version="3.1.0" targetFramework="net461" /> + <package id="Newtonsoft.Json" version="9.0.1" targetFramework="net461" /> + <package id="Owin" version="1.0" targetFramework="net461" /> </packages> \ No newline at end of file diff --git a/src/NzbDrone.Test.Common/App.config b/src/NzbDrone.Test.Common/App.config index c7a8ca18c..a62cc0f65 100644 --- a/src/NzbDrone.Test.Common/App.config +++ b/src/NzbDrone.Test.Common/App.config @@ -25,6 +25,14 @@ <assemblyIdentity name="Microsoft.Practices.ServiceLocation" publicKeyToken="31bf3856ad364e35" culture="neutral" /> <bindingRedirect oldVersion="0.0.0.0-1.3.0.0" newVersion="1.3.0.0" /> </dependentAssembly> + <dependentAssembly> + <assemblyIdentity name="Microsoft.Owin" publicKeyToken="31bf3856ad364e35" culture="neutral" /> + <bindingRedirect oldVersion="0.0.0.0-3.1.0.0" newVersion="3.1.0.0" /> + </dependentAssembly> + <dependentAssembly> + <assemblyIdentity name="Microsoft.Owin.Security" publicKeyToken="31bf3856ad364e35" culture="neutral" /> + <bindingRedirect oldVersion="0.0.0.0-3.1.0.0" newVersion="3.1.0.0" /> + </dependentAssembly> </assemblyBinding> </runtime> -</configuration> \ No newline at end of file +<startup><supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.6.1" /></startup></configuration> diff --git a/src/NzbDrone.Test.Common/AutoMoq/AutoMoqer.cs b/src/NzbDrone.Test.Common/AutoMoq/AutoMoqer.cs index 5cf6805a8..d8ef41b9a 100644 --- a/src/NzbDrone.Test.Common/AutoMoq/AutoMoqer.cs +++ b/src/NzbDrone.Test.Common/AutoMoq/AutoMoqer.cs @@ -171,11 +171,11 @@ namespace NzbDrone.Test.Common.AutoMoq private void RegisterPlatformLibrary(IUnityContainer container) { - var assemblyName = "NzbDrone.Windows"; + var assemblyName = "Lidarr.Windows"; if (OsInfo.IsNotWindows) { - assemblyName = "NzbDrone.Mono"; + assemblyName = "Lidarr.Mono"; } if (!File.Exists(assemblyName + ".dll")) @@ -188,4 +188,4 @@ namespace NzbDrone.Test.Common.AutoMoq #endregion } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Test.Common/LoggingTest.cs b/src/NzbDrone.Test.Common/LoggingTest.cs index b8aba6dcd..b2d783761 100644 --- a/src/NzbDrone.Test.Common/LoggingTest.cs +++ b/src/NzbDrone.Test.Common/LoggingTest.cs @@ -1,6 +1,8 @@ using NLog; using NLog.Config; using NLog.Targets; +using System; +using System.IO; using NUnit.Framework; using NUnit.Framework.Interfaces; using NzbDrone.Common.EnvironmentInfo; @@ -20,9 +22,19 @@ namespace NzbDrone.Test.Common if (LogManager.Configuration == null || LogManager.Configuration.AllTargets.None(c => c is ExceptionVerification)) { LogManager.Configuration = new LoggingConfiguration(); - var consoleTarget = new ConsoleTarget { Layout = "${level}: ${message} ${exception}" }; - LogManager.Configuration.AddTarget(consoleTarget.GetType().Name, consoleTarget); - LogManager.Configuration.LoggingRules.Add(new LoggingRule("*", LogLevel.Trace, consoleTarget)); + + var logOutput = TestLogOutput.Console; + Enum.TryParse<TestLogOutput>(Environment.GetEnvironmentVariable("LIDARR_TESTS_LOG_OUTPUT"), out logOutput); + + switch (logOutput) + { + case TestLogOutput.Console: + RegisterConsoleLogger(); + break; + case TestLogOutput.File: + RegisterFileLogger(); + break; + } RegisterExceptionVerification(); @@ -30,6 +42,32 @@ namespace NzbDrone.Test.Common } } + private static void RegisterConsoleLogger() + { + var consoleTarget = new ConsoleTarget { Layout = "${level}: ${message} ${exception}" }; + LogManager.Configuration.AddTarget(consoleTarget.GetType().Name, consoleTarget); + LogManager.Configuration.LoggingRules.Add(new LoggingRule("*", LogLevel.Trace, consoleTarget)); + } + + private static void RegisterFileLogger() + { + const string layout = @"${level}|${message}${onexception:inner=${newline}${newline}${exception:format=ToString}${newline}}"; + + var fileTarget = new FileTarget(); + + fileTarget.Name = "Test File Logger"; + fileTarget.FileName = Path.Combine(TestContext.CurrentContext.WorkDirectory, "TestLog.txt"); + fileTarget.AutoFlush = false; + fileTarget.KeepFileOpen = true; + fileTarget.ConcurrentWrites = true; + fileTarget.ConcurrentWriteAttemptDelay = 50; + fileTarget.ConcurrentWriteAttempts = 10; + fileTarget.Layout = layout; + + LogManager.Configuration.AddTarget(fileTarget.GetType().Name, fileTarget); + LogManager.Configuration.LoggingRules.Add(new LoggingRule("*", LogLevel.Trace, fileTarget)); + } + private static void RegisterExceptionVerification() { var exceptionVerification = new ExceptionVerification(); @@ -42,6 +80,7 @@ namespace NzbDrone.Test.Common { InitLogging(); ExceptionVerification.Reset(); + TestLogger.Info("--- Start: {0} ---", TestContext.CurrentContext.Test.FullName); } [TearDown] @@ -53,6 +92,8 @@ namespace NzbDrone.Test.Common { ExceptionVerification.AssertNoUnexpectedLogs(); } + + TestLogger.Info("--- End: {0} ---", TestContext.CurrentContext.Test.FullName); } } } diff --git a/src/NzbDrone.Test.Common/NzbDrone.Test.Common.csproj b/src/NzbDrone.Test.Common/NzbDrone.Test.Common.csproj index 66b5682af..03d7d1d15 100644 --- a/src/NzbDrone.Test.Common/NzbDrone.Test.Common.csproj +++ b/src/NzbDrone.Test.Common/NzbDrone.Test.Common.csproj @@ -1,5 +1,5 @@ <?xml version="1.0" encoding="utf-8"?> -<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> +<Project ToolsVersion="12.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> <PropertyGroup> <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration> <Platform Condition=" '$(Platform)' == '' ">x86</Platform> @@ -9,11 +9,12 @@ <OutputType>Library</OutputType> <AppDesignerFolder>Properties</AppDesignerFolder> <RootNamespace>NzbDrone.Test.Common</RootNamespace> - <AssemblyName>NzbDrone.Test.Common</AssemblyName> - <TargetFrameworkVersion>v4.0</TargetFrameworkVersion> + <AssemblyName>Lidarr.Test.Common</AssemblyName> + <TargetFrameworkVersion>v4.6.1</TargetFrameworkVersion> <FileAlignment>512</FileAlignment> <SolutionDir Condition="$(SolutionDir) == '' Or $(SolutionDir) == '*Undefined*'">..\</SolutionDir> <RestorePackages>true</RestorePackages> + <TargetFrameworkProfile /> </PropertyGroup> <PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x86'"> <DebugSymbols>true</DebugSymbols> @@ -25,6 +26,7 @@ <CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet> <WarningLevel>4</WarningLevel> <Optimize>false</Optimize> + <Prefer32Bit>false</Prefer32Bit> </PropertyGroup> <PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x86'"> <OutputPath>bin\x86\Release\</OutputPath> @@ -35,27 +37,41 @@ <ErrorReport>prompt</ErrorReport> <CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet> <WarningLevel>4</WarningLevel> + <Prefer32Bit>false</Prefer32Bit> </PropertyGroup> <ItemGroup> <Reference Include="FluentAssertions, Version=4.19.0.0, Culture=neutral, PublicKeyToken=33f2691a05b67b6a, processorArchitecture=MSIL"> - <HintPath>..\packages\FluentAssertions.4.19.0\lib\net40\FluentAssertions.dll</HintPath> + <HintPath>..\packages\FluentAssertions.4.19.0\lib\net45\FluentAssertions.dll</HintPath> </Reference> <Reference Include="FluentAssertions.Core, Version=4.19.0.0, Culture=neutral, PublicKeyToken=33f2691a05b67b6a, processorArchitecture=MSIL"> - <HintPath>..\packages\FluentAssertions.4.19.0\lib\net40\FluentAssertions.Core.dll</HintPath> + <HintPath>..\packages\FluentAssertions.4.19.0\lib\net45\FluentAssertions.Core.dll</HintPath> + </Reference> + <Reference Include="FluentValidation, Version=6.2.1.0, Culture=neutral, processorArchitecture=MSIL"> + <HintPath>..\packages\FluentValidation.6.2.1.0\lib\Net45\FluentValidation.dll</HintPath> + </Reference> + <Reference Include="Microsoft.Practices.ServiceLocation, Version=1.3.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL"> + <HintPath>..\packages\CommonServiceLocator.1.3\lib\portable-net4+sl5+netcore45+wpa81+wp8\Microsoft.Practices.ServiceLocation.dll</HintPath> + </Reference> + <Reference Include="Microsoft.Practices.Unity, Version=2.1.505.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL"> + <HintPath>..\packages\Unity.2.1.505.2\lib\NET35\Microsoft.Practices.Unity.dll</HintPath> + </Reference> + <Reference Include="Microsoft.Practices.Unity.Configuration, Version=2.1.505.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL"> + <HintPath>..\packages\Unity.2.1.505.2\lib\NET35\Microsoft.Practices.Unity.Configuration.dll</HintPath> + </Reference> + <Reference Include="Moq, Version=4.0.10827.0, Culture=neutral, PublicKeyToken=69f491c39445e920, processorArchitecture=MSIL"> + <HintPath>..\packages\Moq.4.0.10827\lib\NET40\Moq.dll</HintPath> </Reference> <Reference Include="Newtonsoft.Json, Version=9.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL"> - <HintPath>..\packages\Newtonsoft.Json.9.0.1\lib\net40\Newtonsoft.Json.dll</HintPath> - <Private>True</Private> + <HintPath>..\packages\Newtonsoft.Json.9.0.1\lib\net45\Newtonsoft.Json.dll</HintPath> </Reference> <Reference Include="NLog, Version=4.0.0.0, Culture=neutral, PublicKeyToken=5120e14c03d0593c, processorArchitecture=MSIL"> - <HintPath>..\packages\NLog.4.4.3\lib\net40\NLog.dll</HintPath> + <HintPath>..\packages\NLog.4.4.12\lib\net45\NLog.dll</HintPath> </Reference> <Reference Include="nunit.framework, Version=3.6.0.0, Culture=neutral, PublicKeyToken=2638cd05610744eb, processorArchitecture=MSIL"> - <HintPath>..\packages\NUnit.3.6.0\lib\net40\nunit.framework.dll</HintPath> + <HintPath>..\packages\NUnit.3.6.0\lib\net45\nunit.framework.dll</HintPath> </Reference> <Reference Include="RestSharp, Version=105.2.3.0, Culture=neutral, processorArchitecture=MSIL"> - <HintPath>..\packages\RestSharp.105.2.3\lib\net4\RestSharp.dll</HintPath> - <Private>True</Private> + <HintPath>..\packages\RestSharp.105.2.3\lib\net46\RestSharp.dll</HintPath> </Reference> <Reference Include="System" /> <Reference Include="System.Core" /> @@ -91,6 +107,8 @@ <Compile Include="StringExtensions.cs" /> <Compile Include="TestBase.cs" /> <Compile Include="TestException.cs" /> + <Compile Include="TestLogOutput.cs" /> + <Compile Include="TestValidator.cs" /> </ItemGroup> <ItemGroup> <Content Include="AutoMoq\License.txt" /> diff --git a/src/NzbDrone.Test.Common/NzbDroneRunner.cs b/src/NzbDrone.Test.Common/NzbDroneRunner.cs index 0a686f3e0..a1258d1ce 100644 --- a/src/NzbDrone.Test.Common/NzbDroneRunner.cs +++ b/src/NzbDrone.Test.Common/NzbDroneRunner.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Diagnostics; using System.IO; using System.Threading; @@ -32,12 +32,7 @@ namespace NzbDrone.Test.Common { AppData = Path.Combine(TestContext.CurrentContext.TestDirectory, "_intg_" + DateTime.Now.Ticks); - var nzbdroneConsoleExe = "Lidarr.Console.exe"; - - if (OsInfo.IsNotWindows) - { - nzbdroneConsoleExe = "Lidarr.exe"; - } + var lidarrConsoleExe = OsInfo.IsWindows ? "Lidarr.Console.exe" : "Lidarr.exe"; if (BuildInfo.IsDebug) { @@ -45,7 +40,7 @@ namespace NzbDrone.Test.Common } else { - Start(Path.Combine("bin", nzbdroneConsoleExe)); + Start(Path.Combine("bin", lidarrConsoleExe)); } while (true) @@ -84,8 +79,8 @@ namespace NzbDrone.Test.Common _processProvider.Kill(_nzbDroneProcess.Id); } - _processProvider.KillAll(ProcessProvider.NZB_DRONE_CONSOLE_PROCESS_NAME); - _processProvider.KillAll(ProcessProvider.NZB_DRONE_PROCESS_NAME); + _processProvider.KillAll(ProcessProvider.LIDARR_CONSOLE_PROCESS_NAME); + _processProvider.KillAll(ProcessProvider.LIDARR_PROCESS_NAME); } private void Start(string outputNzbdroneConsoleExe) @@ -134,4 +129,4 @@ namespace NzbDrone.Test.Common } } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Test.Common/Properties/AssemblyInfo.cs b/src/NzbDrone.Test.Common/Properties/AssemblyInfo.cs index d82d940d5..a73dfabf8 100644 --- a/src/NzbDrone.Test.Common/Properties/AssemblyInfo.cs +++ b/src/NzbDrone.Test.Common/Properties/AssemblyInfo.cs @@ -1,14 +1,14 @@ -using System.Reflection; +using System.Reflection; using System.Runtime.InteropServices; // General Information about an assembly is controlled through the following // set of attributes. Change these attribute values to modify the information // associated with an assembly. -[assembly: AssemblyTitle("NzbDrone.Test.Common")] +[assembly: AssemblyTitle("Lidarr.Test.Common")] [assembly: AssemblyDescription("")] [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("Microsoft")] -[assembly: AssemblyProduct("NzbDrone.Test.Common")] +[assembly: AssemblyProduct("Lidarr.Test.Common")] [assembly: AssemblyCopyright("Copyright © Microsoft 2011")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] @@ -21,4 +21,3 @@ using System.Runtime.InteropServices; // The following GUID is for the ID of the typelib if this project is exposed to COM [assembly: Guid("f3e91f6e-d01d-4f20-8255-147cc10f04e3")] -[assembly: AssemblyVersion("10.0.0.*")] diff --git a/src/NzbDrone.Test.Common/TestLogOutput.cs b/src/NzbDrone.Test.Common/TestLogOutput.cs new file mode 100644 index 000000000..91cf2d52d --- /dev/null +++ b/src/NzbDrone.Test.Common/TestLogOutput.cs @@ -0,0 +1,9 @@ +namespace NzbDrone.Test.Common +{ + public enum TestLogOutput + { + Console = 0, + File = 1, + None = 2 + } +} diff --git a/src/NzbDrone.Test.Common/TestValidator.cs b/src/NzbDrone.Test.Common/TestValidator.cs new file mode 100644 index 000000000..e6851b01f --- /dev/null +++ b/src/NzbDrone.Test.Common/TestValidator.cs @@ -0,0 +1,16 @@ +using System; +using FluentValidation; + +namespace NzbDrone.Test.Common +{ + public class TestValidator<T> : InlineValidator<T> + { + public TestValidator(params Action<TestValidator<T>>[] actions) + { + foreach (var action in actions) + { + action(this); + } + } + } +} diff --git a/src/NzbDrone.Test.Common/packages.config b/src/NzbDrone.Test.Common/packages.config index ea89c79fc..4aea1afbd 100644 --- a/src/NzbDrone.Test.Common/packages.config +++ b/src/NzbDrone.Test.Common/packages.config @@ -1,11 +1,12 @@ <?xml version="1.0" encoding="utf-8"?> <packages> - <package id="CommonServiceLocator" version="1.3" targetFramework="net40" /> - <package id="FluentAssertions" version="4.19.0" targetFramework="net40" /> - <package id="Moq" version="4.0.10827" /> - <package id="Newtonsoft.Json" version="9.0.1" targetFramework="net40" /> - <package id="NLog" version="4.4.3" targetFramework="net40" /> - <package id="NUnit" version="3.6.0" targetFramework="net40" /> - <package id="RestSharp" version="105.2.3" targetFramework="net40" /> - <package id="Unity" version="2.1.505.2" targetFramework="net40" /> + <package id="CommonServiceLocator" version="1.3" targetFramework="net461" /> + <package id="FluentAssertions" version="4.19.0" targetFramework="net461" /> + <package id="FluentValidation" version="6.2.1.0" targetFramework="net461" /> + <package id="Moq" version="4.0.10827" targetFramework="net461" /> + <package id="Newtonsoft.Json" version="9.0.1" targetFramework="net461" /> + <package id="NLog" version="4.4.12" targetFramework="net461" /> + <package id="NUnit" version="3.6.0" targetFramework="net461" /> + <package id="RestSharp" version="105.2.3" targetFramework="net461" /> + <package id="Unity" version="2.1.505.2" targetFramework="net461" /> </packages> \ No newline at end of file diff --git a/src/NzbDrone.Test.Dummy/DummyApp.cs b/src/NzbDrone.Test.Dummy/DummyApp.cs index 5176dab6f..37b7f261e 100644 --- a/src/NzbDrone.Test.Dummy/DummyApp.cs +++ b/src/NzbDrone.Test.Dummy/DummyApp.cs @@ -1,11 +1,11 @@ -using System; +using System; using System.Diagnostics; namespace NzbDrone.Test.Dummy { public class DummyApp { - public const string DUMMY_PROCCESS_NAME = "NzbDrone.Test.Dummy"; + public const string DUMMY_PROCCESS_NAME = "Lidarr.Test.Dummy"; static void Main(string[] args) { diff --git a/src/NzbDrone.Test.Dummy/NzbDrone.Test.Dummy.csproj b/src/NzbDrone.Test.Dummy/NzbDrone.Test.Dummy.csproj index fb311dbd5..ebf6a23aa 100644 --- a/src/NzbDrone.Test.Dummy/NzbDrone.Test.Dummy.csproj +++ b/src/NzbDrone.Test.Dummy/NzbDrone.Test.Dummy.csproj @@ -1,5 +1,5 @@ <?xml version="1.0" encoding="utf-8"?> -<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> +<Project ToolsVersion="12.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> <PropertyGroup> <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration> <Platform Condition=" '$(Platform)' == '' ">x86</Platform> @@ -9,8 +9,8 @@ <OutputType>Exe</OutputType> <AppDesignerFolder>Properties</AppDesignerFolder> <RootNamespace>NzbDrone.Test.Dummy</RootNamespace> - <AssemblyName>NzbDrone.Test.Dummy</AssemblyName> - <TargetFrameworkVersion>v4.0</TargetFrameworkVersion> + <AssemblyName>Lidarr.Test.Dummy</AssemblyName> + <TargetFrameworkVersion>v4.6.1</TargetFrameworkVersion> <TargetFrameworkProfile> </TargetFrameworkProfile> <FileAlignment>512</FileAlignment> @@ -26,6 +26,7 @@ <DefineConstants>DEBUG;TRACE</DefineConstants> <ErrorReport>prompt</ErrorReport> <WarningLevel>4</WarningLevel> + <Prefer32Bit>false</Prefer32Bit> </PropertyGroup> <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|x86' "> <PlatformTarget>x86</PlatformTarget> @@ -35,6 +36,7 @@ <DefineConstants>TRACE</DefineConstants> <ErrorReport>prompt</ErrorReport> <WarningLevel>4</WarningLevel> + <Prefer32Bit>false</Prefer32Bit> </PropertyGroup> <ItemGroup> <Reference Include="System" /> @@ -60,4 +62,4 @@ <Target Name="AfterBuild"> </Target> --> -</Project> +</Project> \ No newline at end of file diff --git a/src/NzbDrone.Test.Dummy/Properties/AssemblyInfo.cs b/src/NzbDrone.Test.Dummy/Properties/AssemblyInfo.cs index d2e93dadf..fe958f227 100644 --- a/src/NzbDrone.Test.Dummy/Properties/AssemblyInfo.cs +++ b/src/NzbDrone.Test.Dummy/Properties/AssemblyInfo.cs @@ -1,14 +1,14 @@ -using System.Reflection; +using System.Reflection; using System.Runtime.InteropServices; // General Information about an assembly is controlled through the following // set of attributes. Change these attribute values to modify the information // associated with an assembly. -[assembly: AssemblyTitle("NzbDrone.Test.Dummy")] +[assembly: AssemblyTitle("Lidarr.Test.Dummy")] [assembly: AssemblyDescription("")] [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("Microsoft")] -[assembly: AssemblyProduct("NzbDrone.Test.Dummy")] +[assembly: AssemblyProduct("Lidarr.Test.Dummy")] [assembly: AssemblyCopyright("Copyright © Microsoft 2011")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] @@ -21,4 +21,3 @@ using System.Runtime.InteropServices; // The following GUID is for the ID of the typelib if this project is exposed to COM [assembly: Guid("7b773a86-574d-48c3-9e89-6f2e0dff714b")] -[assembly: AssemblyVersion("10.0.0.*")] diff --git a/src/NzbDrone.Test.Dummy/app.config b/src/NzbDrone.Test.Dummy/app.config index e36560333..0dff63a07 100644 --- a/src/NzbDrone.Test.Dummy/app.config +++ b/src/NzbDrone.Test.Dummy/app.config @@ -1,3 +1,3 @@ <?xml version="1.0"?> <configuration> -<startup><supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.0"/></startup></configuration> +<startup><supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.6.1"/></startup></configuration> diff --git a/src/NzbDrone.Update.Test/NzbDrone.Update.Test.csproj b/src/NzbDrone.Update.Test/NzbDrone.Update.Test.csproj index e3c6dc4de..993138c50 100644 --- a/src/NzbDrone.Update.Test/NzbDrone.Update.Test.csproj +++ b/src/NzbDrone.Update.Test/NzbDrone.Update.Test.csproj @@ -1,5 +1,5 @@ <?xml version="1.0" encoding="utf-8"?> -<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> +<Project ToolsVersion="12.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> <PropertyGroup> <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration> <Platform Condition=" '$(Platform)' == '' ">x86</Platform> @@ -9,8 +9,8 @@ <OutputType>Library</OutputType> <AppDesignerFolder>Properties</AppDesignerFolder> <RootNamespace>NzbDrone.Update.Test</RootNamespace> - <AssemblyName>NzbDrone.Update.Test</AssemblyName> - <TargetFrameworkVersion>v4.0</TargetFrameworkVersion> + <AssemblyName>Lidarr.Update.Test</AssemblyName> + <TargetFrameworkVersion>v4.6.1</TargetFrameworkVersion> <TargetFrameworkProfile> </TargetFrameworkProfile> <FileAlignment>512</FileAlignment> @@ -26,6 +26,7 @@ <DefineConstants>DEBUG;TRACE</DefineConstants> <ErrorReport>prompt</ErrorReport> <WarningLevel>4</WarningLevel> + <Prefer32Bit>false</Prefer32Bit> </PropertyGroup> <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|x86' "> <PlatformTarget>x86</PlatformTarget> @@ -35,23 +36,26 @@ <DefineConstants>TRACE</DefineConstants> <ErrorReport>prompt</ErrorReport> <WarningLevel>4</WarningLevel> + <Prefer32Bit>false</Prefer32Bit> </PropertyGroup> <ItemGroup> <Reference Include="FizzWare.NBuilder, Version=4.0.0.115, Culture=neutral, PublicKeyToken=5651b03e12e42c12, processorArchitecture=MSIL"> <HintPath>..\packages\NBuilder.4.0.0\lib\net40\FizzWare.NBuilder.dll</HintPath> - <Private>True</Private> </Reference> <Reference Include="FluentAssertions, Version=4.19.0.0, Culture=neutral, PublicKeyToken=33f2691a05b67b6a, processorArchitecture=MSIL"> - <HintPath>..\packages\FluentAssertions.4.19.0\lib\net40\FluentAssertions.dll</HintPath> + <HintPath>..\packages\FluentAssertions.4.19.0\lib\net45\FluentAssertions.dll</HintPath> </Reference> <Reference Include="FluentAssertions.Core, Version=4.19.0.0, Culture=neutral, PublicKeyToken=33f2691a05b67b6a, processorArchitecture=MSIL"> - <HintPath>..\packages\FluentAssertions.4.19.0\lib\net40\FluentAssertions.Core.dll</HintPath> + <HintPath>..\packages\FluentAssertions.4.19.0\lib\net45\FluentAssertions.Core.dll</HintPath> + </Reference> + <Reference Include="Moq, Version=4.0.10827.0, Culture=neutral, PublicKeyToken=69f491c39445e920, processorArchitecture=MSIL"> + <HintPath>..\packages\Moq.4.0.10827\lib\NET40\Moq.dll</HintPath> </Reference> <Reference Include="NLog, Version=4.0.0.0, Culture=neutral, PublicKeyToken=5120e14c03d0593c, processorArchitecture=MSIL"> - <HintPath>..\packages\NLog.4.4.3\lib\net40\NLog.dll</HintPath> + <HintPath>..\packages\NLog.4.4.12\lib\net45\NLog.dll</HintPath> </Reference> <Reference Include="nunit.framework, Version=3.6.0.0, Culture=neutral, PublicKeyToken=2638cd05610744eb, processorArchitecture=MSIL"> - <HintPath>..\packages\NUnit.3.6.0\lib\net40\nunit.framework.dll</HintPath> + <HintPath>..\packages\NUnit.3.6.0\lib\net45\nunit.framework.dll</HintPath> </Reference> <Reference Include="System" /> <Reference Include="System.Core" /> @@ -60,9 +64,6 @@ <Reference Include="System.Xml" /> <Reference Include="System.Xml.Linq" /> <Reference Include="Microsoft.CSharp" /> - <Reference Include="Moq"> - <HintPath>..\packages\Moq.4.0.10827\lib\NET40\Moq.dll</HintPath> - </Reference> </ItemGroup> <ItemGroup> <Compile Include="InstallUpdateServiceFixture.cs" /> diff --git a/src/NzbDrone.Update.Test/ProgramFixture.cs b/src/NzbDrone.Update.Test/ProgramFixture.cs index 1e8c9ec3f..7c3688682 100644 --- a/src/NzbDrone.Update.Test/ProgramFixture.cs +++ b/src/NzbDrone.Update.Test/ProgramFixture.cs @@ -1,4 +1,4 @@ -using System; +using System; using Moq; using NUnit.Framework; using NzbDrone.Common.Model; @@ -34,7 +34,7 @@ namespace NzbDrone.Update.Test [Test] public void should_call_update_with_correct_path() { - var ProcessPath = @"C:\NzbDrone\lidarr.exe".AsOsAgnostic(); + var ProcessPath = @"C:\Lidarr\lidarr.exe".AsOsAgnostic(); Mocker.GetMock<IProcessProvider>().Setup(c => c.GetProcessById(12)) .Returns(new ProcessInfo() { StartPath = ProcessPath }); @@ -43,7 +43,7 @@ namespace NzbDrone.Update.Test Subject.Start(new[] { "12", "", ProcessPath }); - Mocker.GetMock<IInstallUpdateService>().Verify(c => c.Start(@"C:\NzbDrone".AsOsAgnostic(), 12), Times.Once()); + Mocker.GetMock<IInstallUpdateService>().Verify(c => c.Start(@"C:\Lidarr".AsOsAgnostic(), 12), Times.Once()); } diff --git a/src/NzbDrone.Update.Test/Properties/AssemblyInfo.cs b/src/NzbDrone.Update.Test/Properties/AssemblyInfo.cs index 35dc227d7..e7a36b7be 100644 --- a/src/NzbDrone.Update.Test/Properties/AssemblyInfo.cs +++ b/src/NzbDrone.Update.Test/Properties/AssemblyInfo.cs @@ -1,14 +1,14 @@ -using System.Reflection; +using System.Reflection; using System.Runtime.InteropServices; // General Information about an assembly is controlled through the following // set of attributes. Change these attribute values to modify the information // associated with an assembly. -[assembly: AssemblyTitle("NzbDrone.Update.Test")] +[assembly: AssemblyTitle("Lidarr.Update.Test")] [assembly: AssemblyDescription("")] [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("Microsoft")] -[assembly: AssemblyProduct("NzbDrone.Update.Test")] +[assembly: AssemblyProduct("Lidarr.Update.Test")] [assembly: AssemblyCopyright("Copyright © Microsoft 2011")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] @@ -21,4 +21,3 @@ using System.Runtime.InteropServices; // The following GUID is for the ID of the typelib if this project is exposed to COM [assembly: Guid("b323e212-2d04-4c7f-9097-c356749ace4d")] -[assembly: AssemblyVersion("10.0.0.*")] diff --git a/src/NzbDrone.Update.Test/StartNzbDroneService.cs b/src/NzbDrone.Update.Test/StartNzbDroneService.cs index 3ed9a9ee7..087cecb16 100644 --- a/src/NzbDrone.Update.Test/StartNzbDroneService.cs +++ b/src/NzbDrone.Update.Test/StartNzbDroneService.cs @@ -1,4 +1,4 @@ -using System; +using System; using Moq; using NUnit.Framework; using NzbDrone.Common; @@ -16,23 +16,23 @@ namespace NzbDrone.Update.Test [Test] public void should_start_service_if_app_type_was_serivce() { - const string targetFolder = "c:\\NzbDrone\\"; + const string targetFolder = "c:\\Lidarr\\"; Subject.Start(AppType.Service, targetFolder); - Mocker.GetMock<IServiceProvider>().Verify(c => c.Start(ServiceProvider.NZBDRONE_SERVICE_NAME), Times.Once()); + Mocker.GetMock<IServiceProvider>().Verify(c => c.Start(ServiceProvider.SERVICE_NAME), Times.Once()); } [Test] public void should_start_console_if_app_type_was_service_but_start_failed_because_of_permissions() { - const string targetFolder = "c:\\NzbDrone\\"; + const string targetFolder = "c:\\Lidarr\\"; - Mocker.GetMock<IServiceProvider>().Setup(c => c.Start(ServiceProvider.NZBDRONE_SERVICE_NAME)).Throws(new InvalidOperationException()); + Mocker.GetMock<IServiceProvider>().Setup(c => c.Start(ServiceProvider.SERVICE_NAME)).Throws(new InvalidOperationException()); Subject.Start(AppType.Service, targetFolder); - Mocker.GetMock<IProcessProvider>().Verify(c => c.SpawnNewProcess("c:\\NzbDrone\\Lidarr.Console.exe", "/" + StartupContext.NO_BROWSER, null), Times.Once()); + Mocker.GetMock<IProcessProvider>().Verify(c => c.SpawnNewProcess("c:\\Lidarr\\Lidarr.Console.exe", "/" + StartupContext.NO_BROWSER, null, false), Times.Once()); ExceptionVerification.ExpectedWarns(1); } diff --git a/src/NzbDrone.Update.Test/packages.config b/src/NzbDrone.Update.Test/packages.config index dc7aef2ad..045dc1d94 100644 --- a/src/NzbDrone.Update.Test/packages.config +++ b/src/NzbDrone.Update.Test/packages.config @@ -1,8 +1,8 @@ <?xml version="1.0" encoding="utf-8"?> <packages> - <package id="FluentAssertions" version="4.19.0" targetFramework="net40" /> - <package id="Moq" version="4.0.10827" /> - <package id="NBuilder" version="4.0.0" targetFramework="net40" /> - <package id="NLog" version="4.4.3" targetFramework="net40" /> - <package id="NUnit" version="3.6.0" targetFramework="net40" /> + <package id="FluentAssertions" version="4.19.0" targetFramework="net461" /> + <package id="Moq" version="4.0.10827" targetFramework="net461" /> + <package id="NBuilder" version="4.0.0" targetFramework="net461" /> + <package id="NLog" version="4.4.12" targetFramework="net461" /> + <package id="NUnit" version="3.6.0" targetFramework="net461" /> </packages> \ No newline at end of file diff --git a/src/NzbDrone.Update/NzbDrone.Update.csproj b/src/NzbDrone.Update/NzbDrone.Update.csproj index d4e0a5e41..96a4f72a5 100644 --- a/src/NzbDrone.Update/NzbDrone.Update.csproj +++ b/src/NzbDrone.Update/NzbDrone.Update.csproj @@ -1,5 +1,5 @@ <?xml version="1.0" encoding="utf-8"?> -<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> +<Project ToolsVersion="12.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> <PropertyGroup> <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration> <Platform Condition=" '$(Platform)' == '' ">x86</Platform> @@ -9,8 +9,8 @@ <OutputType>WinExe</OutputType> <AppDesignerFolder>Properties</AppDesignerFolder> <RootNamespace>NzbDrone.Update</RootNamespace> - <AssemblyName>NzbDrone.Update</AssemblyName> - <TargetFrameworkVersion>v4.0</TargetFrameworkVersion> + <AssemblyName>Lidarr.Update</AssemblyName> + <TargetFrameworkVersion>v4.6.1</TargetFrameworkVersion> <TargetFrameworkProfile> </TargetFrameworkProfile> <FileAlignment>512</FileAlignment> @@ -22,30 +22,31 @@ <DebugSymbols>true</DebugSymbols> <DebugType>full</DebugType> <Optimize>false</Optimize> - <OutputPath>..\..\_output\NzbDrone.Update\</OutputPath> + <OutputPath>..\..\_output\Lidarr.Update\</OutputPath> <DefineConstants>DEBUG;TRACE</DefineConstants> <ErrorReport>prompt</ErrorReport> <WarningLevel>4</WarningLevel> + <Prefer32Bit>false</Prefer32Bit> </PropertyGroup> <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|x86' "> <PlatformTarget>x86</PlatformTarget> <DebugType>pdbonly</DebugType> <Optimize>true</Optimize> - <OutputPath>..\..\_output\NzbDrone.Update\</OutputPath> + <OutputPath>..\..\_output\Lidarr.Update\</OutputPath> <DefineConstants>TRACE</DefineConstants> <ErrorReport>prompt</ErrorReport> <WarningLevel>4</WarningLevel> + <Prefer32Bit>false</Prefer32Bit> </PropertyGroup> <PropertyGroup> <ApplicationManifest>app.manifest</ApplicationManifest> </PropertyGroup> <ItemGroup> <Reference Include="Newtonsoft.Json, Version=9.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL"> - <HintPath>..\packages\Newtonsoft.Json.9.0.1\lib\net40\Newtonsoft.Json.dll</HintPath> - <Private>True</Private> + <HintPath>..\packages\Newtonsoft.Json.9.0.1\lib\net45\Newtonsoft.Json.dll</HintPath> </Reference> <Reference Include="NLog, Version=4.0.0.0, Culture=neutral, PublicKeyToken=5120e14c03d0593c, processorArchitecture=MSIL"> - <HintPath>..\packages\NLog.4.4.3\lib\net40\NLog.dll</HintPath> + <HintPath>..\packages\NLog.4.4.12\lib\net45\NLog.dll</HintPath> </Reference> <Reference Include="System" /> <Reference Include="System.Core" /> diff --git a/src/NzbDrone.Update/Properties/AssemblyInfo.cs b/src/NzbDrone.Update/Properties/AssemblyInfo.cs index 5a577baf3..652646236 100644 --- a/src/NzbDrone.Update/Properties/AssemblyInfo.cs +++ b/src/NzbDrone.Update/Properties/AssemblyInfo.cs @@ -1,12 +1,11 @@ -using System.Reflection; +using System.Reflection; using System.Runtime.InteropServices; // General Information about an assembly is controlled through the following // set of attributes. Change these attribute values to modify the information // associated with an assembly. -[assembly: AssemblyTitle("NzbDrone.Update")] +[assembly: AssemblyTitle("Lidarr.Update")] // The following GUID is for the ID of the typelib if this project is exposed to COM [assembly: Guid("e4560a3d-8053-4d57-a260-bfe52f4cc357")] -[assembly: AssemblyVersion("10.0.0.*")] diff --git a/src/NzbDrone.Update/UpdateEngine/BackupAppData.cs b/src/NzbDrone.Update/UpdateEngine/BackupAppData.cs index a93bca1f5..0031b55cb 100644 --- a/src/NzbDrone.Update/UpdateEngine/BackupAppData.cs +++ b/src/NzbDrone.Update/UpdateEngine/BackupAppData.cs @@ -47,7 +47,7 @@ namespace NzbDrone.Update.UpdateEngine try { _diskTransferService.TransferFile(_appFolderInfo.GetConfigPath(), _appFolderInfo.GetUpdateBackupConfigFile(), TransferMode.Copy); - _diskTransferService.TransferFile(_appFolderInfo.GetNzbDroneDatabase(), _appFolderInfo.GetUpdateBackupDatabase(), TransferMode.Copy); + _diskTransferService.TransferFile(_appFolderInfo.GetDatabase(), _appFolderInfo.GetUpdateBackupDatabase(), TransferMode.Copy); } catch (Exception e) { diff --git a/src/NzbDrone.Update/UpdateEngine/DetectApplicationType.cs b/src/NzbDrone.Update/UpdateEngine/DetectApplicationType.cs index 7df590a66..23434ec51 100644 --- a/src/NzbDrone.Update/UpdateEngine/DetectApplicationType.cs +++ b/src/NzbDrone.Update/UpdateEngine/DetectApplicationType.cs @@ -28,13 +28,13 @@ namespace NzbDrone.Update.UpdateEngine return AppType.Normal; } - if (_serviceProvider.ServiceExist(ServiceProvider.NZBDRONE_SERVICE_NAME) - && _serviceProvider.IsServiceRunning(ServiceProvider.NZBDRONE_SERVICE_NAME)) + if (_serviceProvider.ServiceExist(ServiceProvider.SERVICE_NAME) + && _serviceProvider.IsServiceRunning(ServiceProvider.SERVICE_NAME)) { return AppType.Service; } - if (_processProvider.Exists(ProcessProvider.NZB_DRONE_CONSOLE_PROCESS_NAME)) + if (_processProvider.Exists(ProcessProvider.LIDARR_CONSOLE_PROCESS_NAME)) { return AppType.Console; } diff --git a/src/NzbDrone.Update/UpdateEngine/DetectExistingVersion.cs b/src/NzbDrone.Update/UpdateEngine/DetectExistingVersion.cs index d27190f17..88665ce82 100644 --- a/src/NzbDrone.Update/UpdateEngine/DetectExistingVersion.cs +++ b/src/NzbDrone.Update/UpdateEngine/DetectExistingVersion.cs @@ -22,7 +22,7 @@ namespace NzbDrone.Update.UpdateEngine { try { - var targetExecutable = Path.Combine(targetFolder, "NzbDrone.exe"); + var targetExecutable = Path.Combine(targetFolder, "Lidarr.exe"); if (File.Exists(targetExecutable)) { diff --git a/src/NzbDrone.Update/UpdateEngine/InstallUpdateService.cs b/src/NzbDrone.Update/UpdateEngine/InstallUpdateService.cs index 875b0e007..9b7240ab2 100644 --- a/src/NzbDrone.Update/UpdateEngine/InstallUpdateService.cs +++ b/src/NzbDrone.Update/UpdateEngine/InstallUpdateService.cs @@ -86,8 +86,8 @@ namespace NzbDrone.Update.UpdateEngine var appType = _detectApplicationType.GetAppType(); - _processProvider.FindProcessByName(ProcessProvider.NZB_DRONE_CONSOLE_PROCESS_NAME); - _processProvider.FindProcessByName(ProcessProvider.NZB_DRONE_PROCESS_NAME); + _processProvider.FindProcessByName(ProcessProvider.LIDARR_CONSOLE_PROCESS_NAME); + _processProvider.FindProcessByName(ProcessProvider.LIDARR_PROCESS_NAME); if (OsInfo.IsWindows) { @@ -101,7 +101,7 @@ namespace NzbDrone.Update.UpdateEngine if (OsInfo.IsWindows) { - if (_processProvider.Exists(ProcessProvider.NZB_DRONE_CONSOLE_PROCESS_NAME) || _processProvider.Exists(ProcessProvider.NZB_DRONE_PROCESS_NAME)) + if (_processProvider.Exists(ProcessProvider.LIDARR_CONSOLE_PROCESS_NAME) || _processProvider.Exists(ProcessProvider.LIDARR_PROCESS_NAME)) { _logger.Error("Lidarr was restarted prematurely by external process."); return; @@ -141,14 +141,14 @@ namespace NzbDrone.Update.UpdateEngine { System.Threading.Thread.Sleep(1000); - if (_processProvider.Exists(ProcessProvider.NZB_DRONE_PROCESS_NAME)) + if (_processProvider.Exists(ProcessProvider.LIDARR_PROCESS_NAME)) { _logger.Info("Lidarr was restarted by external process."); break; } } - if (!_processProvider.Exists(ProcessProvider.NZB_DRONE_PROCESS_NAME)) + if (!_processProvider.Exists(ProcessProvider.LIDARR_PROCESS_NAME)) { _startNzbDrone.Start(appType, installationFolder); } diff --git a/src/NzbDrone.Update/UpdateEngine/StartNzbDrone.cs b/src/NzbDrone.Update/UpdateEngine/StartNzbDrone.cs index 5372a9680..cadd1ac15 100644 --- a/src/NzbDrone.Update/UpdateEngine/StartNzbDrone.cs +++ b/src/NzbDrone.Update/UpdateEngine/StartNzbDrone.cs @@ -57,7 +57,7 @@ namespace NzbDrone.Update.UpdateEngine private void StartService() { _logger.Info("Starting Lidarr service"); - _serviceProvider.Start(ServiceProvider.NZBDRONE_SERVICE_NAME); + _serviceProvider.Start(ServiceProvider.SERVICE_NAME); } private void StartWinform(string installationFolder) diff --git a/src/NzbDrone.Update/UpdateEngine/TerminateNzbDrone.cs b/src/NzbDrone.Update/UpdateEngine/TerminateNzbDrone.cs index 45a584919..7ad7173c3 100644 --- a/src/NzbDrone.Update/UpdateEngine/TerminateNzbDrone.cs +++ b/src/NzbDrone.Update/UpdateEngine/TerminateNzbDrone.cs @@ -31,13 +31,13 @@ namespace NzbDrone.Update.UpdateEngine { _logger.Info("Stopping all running services"); - if (_serviceProvider.ServiceExist(ServiceProvider.NZBDRONE_SERVICE_NAME) - && _serviceProvider.IsServiceRunning(ServiceProvider.NZBDRONE_SERVICE_NAME)) + if (_serviceProvider.ServiceExist(ServiceProvider.SERVICE_NAME) + && _serviceProvider.IsServiceRunning(ServiceProvider.SERVICE_NAME)) { try { _logger.Info("NzbDrone Service is installed and running"); - _serviceProvider.Stop(ServiceProvider.NZBDRONE_SERVICE_NAME); + _serviceProvider.Stop(ServiceProvider.SERVICE_NAME); } catch (Exception e) { @@ -47,15 +47,15 @@ namespace NzbDrone.Update.UpdateEngine _logger.Info("Killing all running processes"); - _processProvider.KillAll(ProcessProvider.NZB_DRONE_CONSOLE_PROCESS_NAME); - _processProvider.KillAll(ProcessProvider.NZB_DRONE_PROCESS_NAME); + _processProvider.KillAll(ProcessProvider.LIDARR_CONSOLE_PROCESS_NAME); + _processProvider.KillAll(ProcessProvider.LIDARR_PROCESS_NAME); } else { _logger.Info("Killing all running processes"); - _processProvider.KillAll(ProcessProvider.NZB_DRONE_CONSOLE_PROCESS_NAME); - _processProvider.KillAll(ProcessProvider.NZB_DRONE_PROCESS_NAME); + _processProvider.KillAll(ProcessProvider.LIDARR_CONSOLE_PROCESS_NAME); + _processProvider.KillAll(ProcessProvider.LIDARR_PROCESS_NAME); _processProvider.Kill(processId); } diff --git a/src/NzbDrone.Update/app.config b/src/NzbDrone.Update/app.config index 76f6c07e9..65f7b09db 100644 --- a/src/NzbDrone.Update/app.config +++ b/src/NzbDrone.Update/app.config @@ -1,7 +1,7 @@ <?xml version="1.0" encoding="utf-8"?> <configuration> <startup> - <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.0" /> + <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.6.1" /> </startup> <runtime> <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1"> @@ -15,4 +15,4 @@ </dependentAssembly> </assemblyBinding> </runtime> -</configuration> \ No newline at end of file +</configuration> diff --git a/src/NzbDrone.Update/packages.config b/src/NzbDrone.Update/packages.config index 96f5ba1e0..4386ebaa4 100644 --- a/src/NzbDrone.Update/packages.config +++ b/src/NzbDrone.Update/packages.config @@ -1,5 +1,5 @@ <?xml version="1.0" encoding="utf-8"?> <packages> - <package id="Newtonsoft.Json" version="9.0.1" targetFramework="net40" /> - <package id="NLog" version="4.4.3" targetFramework="net40" /> + <package id="Newtonsoft.Json" version="9.0.1" targetFramework="net461" /> + <package id="NLog" version="4.4.12" targetFramework="net461" /> </packages> \ No newline at end of file diff --git a/src/NzbDrone.Windows.Test/NzbDrone.Windows.Test.csproj b/src/NzbDrone.Windows.Test/NzbDrone.Windows.Test.csproj index 9eb35ad5e..c70c85c06 100644 --- a/src/NzbDrone.Windows.Test/NzbDrone.Windows.Test.csproj +++ b/src/NzbDrone.Windows.Test/NzbDrone.Windows.Test.csproj @@ -1,5 +1,5 @@ <?xml version="1.0" encoding="utf-8"?> -<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> +<Project ToolsVersion="12.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> <Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" /> <PropertyGroup> <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration> @@ -8,11 +8,12 @@ <OutputType>Library</OutputType> <AppDesignerFolder>Properties</AppDesignerFolder> <RootNamespace>NzbDrone.Windows.Test</RootNamespace> - <AssemblyName>NzbDrone.Windows.Test</AssemblyName> - <TargetFrameworkVersion>v4.0</TargetFrameworkVersion> + <AssemblyName>Lidarr.Windows.Test</AssemblyName> + <TargetFrameworkVersion>v4.6.1</TargetFrameworkVersion> <FileAlignment>512</FileAlignment> <SolutionDir Condition="$(SolutionDir) == '' Or $(SolutionDir) == '*Undefined*'">..\</SolutionDir> <RestorePackages>true</RestorePackages> + <TargetFrameworkProfile /> </PropertyGroup> <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' "> <DebugSymbols>true</DebugSymbols> @@ -22,6 +23,7 @@ <DefineConstants>DEBUG;TRACE</DefineConstants> <ErrorReport>prompt</ErrorReport> <WarningLevel>4</WarningLevel> + <Prefer32Bit>false</Prefer32Bit> </PropertyGroup> <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' "> <DebugType>pdbonly</DebugType> @@ -30,6 +32,7 @@ <DefineConstants>TRACE</DefineConstants> <ErrorReport>prompt</ErrorReport> <WarningLevel>4</WarningLevel> + <Prefer32Bit>false</Prefer32Bit> </PropertyGroup> <PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x86'"> <DebugSymbols>true</DebugSymbols> @@ -39,6 +42,7 @@ <PlatformTarget>x86</PlatformTarget> <ErrorReport>prompt</ErrorReport> <CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet> + <Prefer32Bit>false</Prefer32Bit> </PropertyGroup> <PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x86'"> <OutputPath>bin\x86\Release\</OutputPath> @@ -48,16 +52,17 @@ <PlatformTarget>x86</PlatformTarget> <ErrorReport>prompt</ErrorReport> <CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet> + <Prefer32Bit>false</Prefer32Bit> </PropertyGroup> <ItemGroup> <Reference Include="FluentAssertions, Version=4.19.0.0, Culture=neutral, PublicKeyToken=33f2691a05b67b6a, processorArchitecture=MSIL"> - <HintPath>..\packages\FluentAssertions.4.19.0\lib\net40\FluentAssertions.dll</HintPath> + <HintPath>..\packages\FluentAssertions.4.19.0\lib\net45\FluentAssertions.dll</HintPath> </Reference> <Reference Include="FluentAssertions.Core, Version=4.19.0.0, Culture=neutral, PublicKeyToken=33f2691a05b67b6a, processorArchitecture=MSIL"> - <HintPath>..\packages\FluentAssertions.4.19.0\lib\net40\FluentAssertions.Core.dll</HintPath> + <HintPath>..\packages\FluentAssertions.4.19.0\lib\net45\FluentAssertions.Core.dll</HintPath> </Reference> <Reference Include="nunit.framework, Version=3.6.0.0, Culture=neutral, PublicKeyToken=2638cd05610744eb, processorArchitecture=MSIL"> - <HintPath>..\packages\NUnit.3.6.0\lib\net40\nunit.framework.dll</HintPath> + <HintPath>..\packages\NUnit.3.6.0\lib\net45\nunit.framework.dll</HintPath> </Reference> <Reference Include="System" /> <Reference Include="System.Core" /> diff --git a/src/NzbDrone.Windows.Test/Properties/AssemblyInfo.cs b/src/NzbDrone.Windows.Test/Properties/AssemblyInfo.cs index c881ae54e..e4c165eb6 100644 --- a/src/NzbDrone.Windows.Test/Properties/AssemblyInfo.cs +++ b/src/NzbDrone.Windows.Test/Properties/AssemblyInfo.cs @@ -1,14 +1,14 @@ -using System.Reflection; +using System.Reflection; using System.Runtime.InteropServices; // General Information about an assembly is controlled through the following // set of attributes. Change these attribute values to modify the information // associated with an assembly. -[assembly: AssemblyTitle("NzbDrone.Windows.Test")] +[assembly: AssemblyTitle("Lidarr.Windows.Test")] [assembly: AssemblyDescription("")] [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("NzbDrone.Windows.Test")] +[assembly: AssemblyProduct("Lidarr.Windows.Test")] [assembly: AssemblyCopyright("Copyright © 2014")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] diff --git a/src/NzbDrone.Windows.Test/app.config b/src/NzbDrone.Windows.Test/app.config index b6d9543c5..8e850db4b 100644 --- a/src/NzbDrone.Windows.Test/app.config +++ b/src/NzbDrone.Windows.Test/app.config @@ -4,7 +4,7 @@ <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1"> <dependentAssembly> <assemblyIdentity name="Microsoft.Owin" publicKeyToken="31bf3856ad364e35" culture="neutral" /> - <bindingRedirect oldVersion="0.0.0.0-2.1.0.0" newVersion="2.1.0.0" /> + <bindingRedirect oldVersion="0.0.0.0-3.1.0.0" newVersion="3.1.0.0" /> </dependentAssembly> <dependentAssembly> <assemblyIdentity name="Newtonsoft.Json" publicKeyToken="30ad4fe6b2a6aeed" culture="neutral" /> @@ -14,6 +14,14 @@ <assemblyIdentity name="FluentMigrator" publicKeyToken="aacfc7de5acabf05" culture="neutral" /> <bindingRedirect oldVersion="0.0.0.0-1.3.1.0" newVersion="1.3.1.0" /> </dependentAssembly> + <dependentAssembly> + <assemblyIdentity name="Microsoft.Practices.ServiceLocation" publicKeyToken="31bf3856ad364e35" culture="neutral" /> + <bindingRedirect oldVersion="0.0.0.0-1.3.0.0" newVersion="1.3.0.0" /> + </dependentAssembly> + <dependentAssembly> + <assemblyIdentity name="Microsoft.Owin.Security" publicKeyToken="31bf3856ad364e35" culture="neutral" /> + <bindingRedirect oldVersion="0.0.0.0-3.1.0.0" newVersion="3.1.0.0" /> + </dependentAssembly> </assemblyBinding> </runtime> -</configuration> \ No newline at end of file +<startup><supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.6.1" /></startup></configuration> diff --git a/src/NzbDrone.Windows.Test/packages.config b/src/NzbDrone.Windows.Test/packages.config index a4dcb9206..fd0af0f89 100644 --- a/src/NzbDrone.Windows.Test/packages.config +++ b/src/NzbDrone.Windows.Test/packages.config @@ -1,5 +1,5 @@ <?xml version="1.0" encoding="utf-8"?> <packages> - <package id="FluentAssertions" version="4.19.0" targetFramework="net40" /> - <package id="NUnit" version="3.6.0" targetFramework="net40" /> + <package id="FluentAssertions" version="4.19.0" targetFramework="net461" /> + <package id="NUnit" version="3.6.0" targetFramework="net461" /> </packages> \ No newline at end of file diff --git a/src/NzbDrone.Windows/NzbDrone.Windows.csproj b/src/NzbDrone.Windows/NzbDrone.Windows.csproj index 2ee8e6c5a..20b8e9c28 100644 --- a/src/NzbDrone.Windows/NzbDrone.Windows.csproj +++ b/src/NzbDrone.Windows/NzbDrone.Windows.csproj @@ -1,5 +1,5 @@ <?xml version="1.0" encoding="utf-8"?> -<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> +<Project ToolsVersion="12.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> <Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" /> <PropertyGroup> <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration> @@ -8,11 +8,12 @@ <OutputType>Library</OutputType> <AppDesignerFolder>Properties</AppDesignerFolder> <RootNamespace>NzbDrone.Windows</RootNamespace> - <AssemblyName>NzbDrone.Windows</AssemblyName> - <TargetFrameworkVersion>v4.0</TargetFrameworkVersion> + <AssemblyName>Lidarr.Windows</AssemblyName> + <TargetFrameworkVersion>v4.6.1</TargetFrameworkVersion> <FileAlignment>512</FileAlignment> <SolutionDir Condition="$(SolutionDir) == '' Or $(SolutionDir) == '*Undefined*'">..\</SolutionDir> <RestorePackages>true</RestorePackages> + <TargetFrameworkProfile /> </PropertyGroup> <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' "> <DebugSymbols>true</DebugSymbols> @@ -23,6 +24,7 @@ <ErrorReport>prompt</ErrorReport> <WarningLevel>4</WarningLevel> <PlatformTarget>x86</PlatformTarget> + <Prefer32Bit>false</Prefer32Bit> </PropertyGroup> <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' "> <DebugType>pdbonly</DebugType> @@ -31,6 +33,7 @@ <DefineConstants>TRACE</DefineConstants> <ErrorReport>prompt</ErrorReport> <WarningLevel>4</WarningLevel> + <Prefer32Bit>false</Prefer32Bit> </PropertyGroup> <PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x86'"> <DebugSymbols>true</DebugSymbols> @@ -40,6 +43,7 @@ <PlatformTarget>x86</PlatformTarget> <ErrorReport>prompt</ErrorReport> <CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet> + <Prefer32Bit>false</Prefer32Bit> </PropertyGroup> <PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x86'"> <OutputPath>..\..\_output\</OutputPath> @@ -49,10 +53,11 @@ <PlatformTarget>x86</PlatformTarget> <ErrorReport>prompt</ErrorReport> <CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet> + <Prefer32Bit>false</Prefer32Bit> </PropertyGroup> <ItemGroup> <Reference Include="NLog, Version=4.0.0.0, Culture=neutral, PublicKeyToken=5120e14c03d0593c, processorArchitecture=MSIL"> - <HintPath>..\packages\NLog.4.4.3\lib\net40\NLog.dll</HintPath> + <HintPath>..\packages\NLog.4.4.12\lib\net45\NLog.dll</HintPath> </Reference> <Reference Include="System" /> <Reference Include="System.Core" /> diff --git a/src/NzbDrone.Windows/Properties/AssemblyInfo.cs b/src/NzbDrone.Windows/Properties/AssemblyInfo.cs index bbeee6014..1ea78db56 100644 --- a/src/NzbDrone.Windows/Properties/AssemblyInfo.cs +++ b/src/NzbDrone.Windows/Properties/AssemblyInfo.cs @@ -1,14 +1,14 @@ -using System.Reflection; +using System.Reflection; using System.Runtime.InteropServices; // General Information about an assembly is controlled through the following // set of attributes. Change these attribute values to modify the information // associated with an assembly. -[assembly: AssemblyTitle("NzbDrone.Windows")] +[assembly: AssemblyTitle("Lidarr.Windows")] [assembly: AssemblyDescription("")] [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("NzbDrone.Windows")] +[assembly: AssemblyProduct("Lidarr.Windows")] [assembly: AssemblyCopyright("Copyright © 2014")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] diff --git a/src/NzbDrone.Windows/app.config b/src/NzbDrone.Windows/app.config index 8460dd432..9566343db 100644 --- a/src/NzbDrone.Windows/app.config +++ b/src/NzbDrone.Windows/app.config @@ -8,4 +8,4 @@ </dependentAssembly> </assemblyBinding> </runtime> -</configuration> \ No newline at end of file +<startup><supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.6.1" /></startup></configuration> diff --git a/src/NzbDrone.Windows/packages.config b/src/NzbDrone.Windows/packages.config index a14101dce..80757f37a 100644 --- a/src/NzbDrone.Windows/packages.config +++ b/src/NzbDrone.Windows/packages.config @@ -1,4 +1,4 @@ <?xml version="1.0" encoding="utf-8"?> <packages> - <package id="NLog" version="4.4.3" targetFramework="net40" /> + <package id="NLog" version="4.4.12" targetFramework="net461" /> </packages> \ No newline at end of file diff --git a/src/NzbDrone.sln b/src/NzbDrone.sln deleted file mode 100644 index 8b4a72d40..000000000 --- a/src/NzbDrone.sln +++ /dev/null @@ -1,613 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 14 -VisualStudioVersion = 14.0.25420.1 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{57A04B72-8088-4F75-A582-1158CF8291F7}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Test.Common", "Test.Common", "{47697CDB-27B6-4B05-B4F8-0CBE6F6EDF97}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NzbDrone.Test.Dummy", "NzbDrone.Test.Dummy\NzbDrone.Test.Dummy.csproj", "{FAFB5948-A222-4CF6-AD14-026BE7564802}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NzbDrone.Test.Common", "NzbDrone.Test.Common\NzbDrone.Test.Common.csproj", "{CADDFCE0-7509-4430-8364-2074E1EEFCA2}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NzbDrone.Core.Test", "NzbDrone.Core.Test\NzbDrone.Core.Test.csproj", "{193ADD3B-792B-4173-8E4C-5A3F8F0237F0}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NzbDrone.Host.Test", "NzbDrone.App.Test\NzbDrone.Host.Test.csproj", "{C0EA1A40-91AD-4EEB-BD16-2DDDEBD20AE5}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NzbDrone.Update.Test", "NzbDrone.Update.Test\NzbDrone.Update.Test.csproj", "{35388E8E-0CDB-4A84-AD16-E4B6EFDA5D97}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NzbDrone.Common.Test", "NzbDrone.Common.Test\NzbDrone.Common.Test.csproj", "{BEC74619-DDBB-4FBA-B517-D3E20AFC9997}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NzbDrone.Api.Test", "NzbDrone.Api.Test\NzbDrone.Api.Test.csproj", "{D18A5DEB-5102-4775-A1AF-B75DAAA8907B}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NzbDrone.Libraries.Test", "NzbDrone.Libraries.Test\NzbDrone.Libraries.Test.csproj", "{CBF6B8B0-A015-413A-8C86-01238BB45770}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NzbDrone.Integration.Test", "NzbDrone.Integration.Test\NzbDrone.Integration.Test.csproj", "{8CEFECD0-A6C2-498F-98B1-3FBE5820F9AB}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NzbDrone.Automation.Test", "NzbDrone.Automation.Test\NzbDrone.Automation.Test.csproj", "{CC26800D-F67E-464B-88DE-8EB1A0C227A3}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "WindowsServiceHelpers", "WindowsServiceHelpers", "{F9E67978-5CD6-4A5F-827B-4249711C0B02}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ServiceInstall", "ServiceHelpers\ServiceInstall\ServiceInstall.csproj", "{6BCE712F-846D-4846-9D1B-A66B858DA755}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ServiceUninstall", "ServiceHelpers\ServiceUninstall\ServiceUninstall.csproj", "{700D0B95-95CD-43F3-B6C9-FAA0FC1358D4}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NzbDrone.Core", "NzbDrone.Core\NzbDrone.Core.csproj", "{FF5EE3B6-913B-47CE-9CEB-11C51B4E1205}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NzbDrone.Update", "NzbDrone.Update\NzbDrone.Update.csproj", "{4CCC53CD-8D5E-4CC4-97D2-5C9312AC2BD7}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NzbDrone.Common", "NzbDrone.Common\NzbDrone.Common.csproj", "{F2BE0FDF-6E47-4827-A420-DD4EF82407F8}" - ProjectSection(ProjectDependencies) = postProject - {9DC31DE3-79FF-47A8-96B4-6BA18F6BB1CB} = {9DC31DE3-79FF-47A8-96B4-6BA18F6BB1CB} - EndProjectSection -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".nuget", ".nuget", "{1E6B3CBE-1578-41C1-9BF9-78D818740BE9}" - ProjectSection(SolutionItems) = preProject - .nuget\NuGet.exe = .nuget\NuGet.exe - EndProjectSection -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NzbDrone.Api", "NzbDrone.Api\NzbDrone.Api.csproj", "{FD286DF8-2D3A-4394-8AD5-443FADE55FB2}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Host", "Host", "{486ADF86-DD89-4E19-B805-9D94F19800D9}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NzbDrone.Console", "NzbDrone.Console\NzbDrone.Console.csproj", "{3DCA7B58-B8B3-49AC-9D9E-56F4A0460976}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NzbDrone.Host", "NzbDrone.Host\NzbDrone.Host.csproj", "{95C11A9E-56ED-456A-8447-2C89C1139266}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NzbDrone", "NzbDrone\NzbDrone.csproj", "{D12F7F2F-8A3C-415F-88FA-6DD061A84869}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NzbDrone.SignalR", "NzbDrone.SignalR\NzbDrone.SignalR.csproj", "{7C2CC69F-5CA0-4E5C-85CB-983F9F6C3B36}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "External", "External", "{F6E3A728-AE77-4D02-BAC8-82FBC1402DDA}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNet.SignalR.Core", "Microsoft.AspNet.SignalR.Core\Microsoft.AspNet.SignalR.Core.csproj", "{1B9A82C4-BCA1-4834-A33E-226F17BE070B}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNet.SignalR.Owin", "Microsoft.AspNet.SignalR.Owin\Microsoft.AspNet.SignalR.Owin.csproj", "{2B8C6DAD-4D85-41B1-83FD-248D9F347522}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Marr.Data", "Marr.Data\Marr.Data.csproj", "{F6FC6BE7-0847-4817-A1ED-223DC647C3D7}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NzbDrone.Mono", "NzbDrone.Mono\NzbDrone.Mono.csproj", "{15AD7579-A314-4626-B556-663F51D97CD1}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NzbDrone.Windows", "NzbDrone.Windows\NzbDrone.Windows.csproj", "{911284D3-F130-459E-836C-2430B6FBF21D}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Platform", "Platform", "{0F0D4998-8F5D-4467-A909-BB192C4B3B4B}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Platform", "Platform", "{4EACDBBC-BCD7-4765-A57B-3E08331E4749}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NzbDrone.Windows.Test", "NzbDrone.Windows.Test\NzbDrone.Windows.Test.csproj", "{80B51429-7A0E-46D6-BEE3-C80DCB1C4EAA}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NzbDrone.Mono.Test", "NzbDrone.Mono.Test\NzbDrone.Mono.Test.csproj", "{40D72824-7D02-4A77-9106-8FE0EEA2B997}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MonoTorrent", "MonoTorrent\MonoTorrent.csproj", "{411A9E0E-FDC6-4E25-828A-0C2CD1CD96F8}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LogentriesCore", "LogentriesCore\LogentriesCore.csproj", "{90D6E9FC-7B88-4E1B-B018-8FA742274558}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LogentriesNLog", "LogentriesNLog\LogentriesNLog.csproj", "{9DC31DE3-79FF-47A8-96B4-6BA18F6BB1CB}" - ProjectSection(ProjectDependencies) = postProject - {90D6E9FC-7B88-4E1B-B018-8FA742274558} = {90D6E9FC-7B88-4E1B-B018-8FA742274558} - EndProjectSection -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CurlSharp", "ExternalModules\CurlSharp\CurlSharp\CurlSharp.csproj", "{74420A79-CC16-442C-8B1E-7C1B913844F0}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Debug|x86 = Debug|x86 - Mono|Any CPU = Mono|Any CPU - Mono|x86 = Mono|x86 - Release|Any CPU = Release|Any CPU - Release|x86 = Release|x86 - Travis|Any CPU = Travis|Any CPU - Travis|x86 = Travis|x86 - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {FAFB5948-A222-4CF6-AD14-026BE7564802}.Debug|Any CPU.ActiveCfg = Debug|x86 - {FAFB5948-A222-4CF6-AD14-026BE7564802}.Debug|Any CPU.Build.0 = Debug|x86 - {FAFB5948-A222-4CF6-AD14-026BE7564802}.Debug|x86.ActiveCfg = Debug|x86 - {FAFB5948-A222-4CF6-AD14-026BE7564802}.Debug|x86.Build.0 = Debug|x86 - {FAFB5948-A222-4CF6-AD14-026BE7564802}.Mono|Any CPU.ActiveCfg = Release|x86 - {FAFB5948-A222-4CF6-AD14-026BE7564802}.Mono|Any CPU.Build.0 = Release|x86 - {FAFB5948-A222-4CF6-AD14-026BE7564802}.Mono|x86.ActiveCfg = Release|x86 - {FAFB5948-A222-4CF6-AD14-026BE7564802}.Mono|x86.Build.0 = Release|x86 - {FAFB5948-A222-4CF6-AD14-026BE7564802}.Release|Any CPU.ActiveCfg = Release|x86 - {FAFB5948-A222-4CF6-AD14-026BE7564802}.Release|x86.ActiveCfg = Release|x86 - {FAFB5948-A222-4CF6-AD14-026BE7564802}.Release|x86.Build.0 = Release|x86 - {FAFB5948-A222-4CF6-AD14-026BE7564802}.Travis|Any CPU.ActiveCfg = Release|x86 - {FAFB5948-A222-4CF6-AD14-026BE7564802}.Travis|Any CPU.Build.0 = Release|x86 - {FAFB5948-A222-4CF6-AD14-026BE7564802}.Travis|x86.ActiveCfg = Release|x86 - {FAFB5948-A222-4CF6-AD14-026BE7564802}.Travis|x86.Build.0 = Release|x86 - {CADDFCE0-7509-4430-8364-2074E1EEFCA2}.Debug|Any CPU.ActiveCfg = Debug|x86 - {CADDFCE0-7509-4430-8364-2074E1EEFCA2}.Debug|Any CPU.Build.0 = Debug|x86 - {CADDFCE0-7509-4430-8364-2074E1EEFCA2}.Debug|x86.ActiveCfg = Debug|x86 - {CADDFCE0-7509-4430-8364-2074E1EEFCA2}.Debug|x86.Build.0 = Debug|x86 - {CADDFCE0-7509-4430-8364-2074E1EEFCA2}.Mono|Any CPU.ActiveCfg = Release|x86 - {CADDFCE0-7509-4430-8364-2074E1EEFCA2}.Mono|Any CPU.Build.0 = Release|x86 - {CADDFCE0-7509-4430-8364-2074E1EEFCA2}.Mono|x86.ActiveCfg = Debug|x86 - {CADDFCE0-7509-4430-8364-2074E1EEFCA2}.Mono|x86.Build.0 = Debug|x86 - {CADDFCE0-7509-4430-8364-2074E1EEFCA2}.Release|Any CPU.ActiveCfg = Release|x86 - {CADDFCE0-7509-4430-8364-2074E1EEFCA2}.Release|x86.ActiveCfg = Release|x86 - {CADDFCE0-7509-4430-8364-2074E1EEFCA2}.Release|x86.Build.0 = Release|x86 - {CADDFCE0-7509-4430-8364-2074E1EEFCA2}.Travis|Any CPU.ActiveCfg = Release|x86 - {CADDFCE0-7509-4430-8364-2074E1EEFCA2}.Travis|Any CPU.Build.0 = Release|x86 - {CADDFCE0-7509-4430-8364-2074E1EEFCA2}.Travis|x86.ActiveCfg = Release|x86 - {CADDFCE0-7509-4430-8364-2074E1EEFCA2}.Travis|x86.Build.0 = Release|x86 - {193ADD3B-792B-4173-8E4C-5A3F8F0237F0}.Debug|Any CPU.ActiveCfg = Debug|x86 - {193ADD3B-792B-4173-8E4C-5A3F8F0237F0}.Debug|Any CPU.Build.0 = Debug|x86 - {193ADD3B-792B-4173-8E4C-5A3F8F0237F0}.Debug|x86.ActiveCfg = Debug|x86 - {193ADD3B-792B-4173-8E4C-5A3F8F0237F0}.Debug|x86.Build.0 = Debug|x86 - {193ADD3B-792B-4173-8E4C-5A3F8F0237F0}.Mono|Any CPU.ActiveCfg = Release|x86 - {193ADD3B-792B-4173-8E4C-5A3F8F0237F0}.Mono|Any CPU.Build.0 = Release|x86 - {193ADD3B-792B-4173-8E4C-5A3F8F0237F0}.Mono|x86.ActiveCfg = Debug|x86 - {193ADD3B-792B-4173-8E4C-5A3F8F0237F0}.Mono|x86.Build.0 = Debug|x86 - {193ADD3B-792B-4173-8E4C-5A3F8F0237F0}.Release|Any CPU.ActiveCfg = Release|x86 - {193ADD3B-792B-4173-8E4C-5A3F8F0237F0}.Release|x86.ActiveCfg = Release|x86 - {193ADD3B-792B-4173-8E4C-5A3F8F0237F0}.Release|x86.Build.0 = Release|x86 - {193ADD3B-792B-4173-8E4C-5A3F8F0237F0}.Travis|Any CPU.ActiveCfg = Release|x86 - {193ADD3B-792B-4173-8E4C-5A3F8F0237F0}.Travis|Any CPU.Build.0 = Release|x86 - {193ADD3B-792B-4173-8E4C-5A3F8F0237F0}.Travis|x86.ActiveCfg = Release|x86 - {193ADD3B-792B-4173-8E4C-5A3F8F0237F0}.Travis|x86.Build.0 = Release|x86 - {C0EA1A40-91AD-4EEB-BD16-2DDDEBD20AE5}.Debug|Any CPU.ActiveCfg = Debug|x86 - {C0EA1A40-91AD-4EEB-BD16-2DDDEBD20AE5}.Debug|Any CPU.Build.0 = Debug|x86 - {C0EA1A40-91AD-4EEB-BD16-2DDDEBD20AE5}.Debug|x86.ActiveCfg = Debug|x86 - {C0EA1A40-91AD-4EEB-BD16-2DDDEBD20AE5}.Debug|x86.Build.0 = Debug|x86 - {C0EA1A40-91AD-4EEB-BD16-2DDDEBD20AE5}.Mono|Any CPU.ActiveCfg = Release|x86 - {C0EA1A40-91AD-4EEB-BD16-2DDDEBD20AE5}.Mono|Any CPU.Build.0 = Release|x86 - {C0EA1A40-91AD-4EEB-BD16-2DDDEBD20AE5}.Mono|x86.ActiveCfg = Debug|x86 - {C0EA1A40-91AD-4EEB-BD16-2DDDEBD20AE5}.Mono|x86.Build.0 = Debug|x86 - {C0EA1A40-91AD-4EEB-BD16-2DDDEBD20AE5}.Release|Any CPU.ActiveCfg = Release|x86 - {C0EA1A40-91AD-4EEB-BD16-2DDDEBD20AE5}.Release|x86.ActiveCfg = Release|x86 - {C0EA1A40-91AD-4EEB-BD16-2DDDEBD20AE5}.Release|x86.Build.0 = Release|x86 - {C0EA1A40-91AD-4EEB-BD16-2DDDEBD20AE5}.Travis|Any CPU.ActiveCfg = Release|x86 - {C0EA1A40-91AD-4EEB-BD16-2DDDEBD20AE5}.Travis|Any CPU.Build.0 = Release|x86 - {C0EA1A40-91AD-4EEB-BD16-2DDDEBD20AE5}.Travis|x86.ActiveCfg = Release|x86 - {C0EA1A40-91AD-4EEB-BD16-2DDDEBD20AE5}.Travis|x86.Build.0 = Release|x86 - {35388E8E-0CDB-4A84-AD16-E4B6EFDA5D97}.Debug|Any CPU.ActiveCfg = Debug|x86 - {35388E8E-0CDB-4A84-AD16-E4B6EFDA5D97}.Debug|Any CPU.Build.0 = Debug|x86 - {35388E8E-0CDB-4A84-AD16-E4B6EFDA5D97}.Debug|x86.ActiveCfg = Debug|x86 - {35388E8E-0CDB-4A84-AD16-E4B6EFDA5D97}.Debug|x86.Build.0 = Debug|x86 - {35388E8E-0CDB-4A84-AD16-E4B6EFDA5D97}.Mono|Any CPU.ActiveCfg = Release|x86 - {35388E8E-0CDB-4A84-AD16-E4B6EFDA5D97}.Mono|Any CPU.Build.0 = Release|x86 - {35388E8E-0CDB-4A84-AD16-E4B6EFDA5D97}.Mono|x86.ActiveCfg = Debug|x86 - {35388E8E-0CDB-4A84-AD16-E4B6EFDA5D97}.Mono|x86.Build.0 = Debug|x86 - {35388E8E-0CDB-4A84-AD16-E4B6EFDA5D97}.Release|Any CPU.ActiveCfg = Release|x86 - {35388E8E-0CDB-4A84-AD16-E4B6EFDA5D97}.Release|x86.ActiveCfg = Release|x86 - {35388E8E-0CDB-4A84-AD16-E4B6EFDA5D97}.Release|x86.Build.0 = Release|x86 - {35388E8E-0CDB-4A84-AD16-E4B6EFDA5D97}.Travis|Any CPU.ActiveCfg = Release|x86 - {35388E8E-0CDB-4A84-AD16-E4B6EFDA5D97}.Travis|Any CPU.Build.0 = Release|x86 - {35388E8E-0CDB-4A84-AD16-E4B6EFDA5D97}.Travis|x86.ActiveCfg = Release|x86 - {35388E8E-0CDB-4A84-AD16-E4B6EFDA5D97}.Travis|x86.Build.0 = Release|x86 - {BEC74619-DDBB-4FBA-B517-D3E20AFC9997}.Debug|Any CPU.ActiveCfg = Debug|x86 - {BEC74619-DDBB-4FBA-B517-D3E20AFC9997}.Debug|Any CPU.Build.0 = Debug|x86 - {BEC74619-DDBB-4FBA-B517-D3E20AFC9997}.Debug|x86.ActiveCfg = Debug|x86 - {BEC74619-DDBB-4FBA-B517-D3E20AFC9997}.Debug|x86.Build.0 = Debug|x86 - {BEC74619-DDBB-4FBA-B517-D3E20AFC9997}.Mono|Any CPU.ActiveCfg = Release|x86 - {BEC74619-DDBB-4FBA-B517-D3E20AFC9997}.Mono|Any CPU.Build.0 = Release|x86 - {BEC74619-DDBB-4FBA-B517-D3E20AFC9997}.Mono|x86.ActiveCfg = Debug|x86 - {BEC74619-DDBB-4FBA-B517-D3E20AFC9997}.Mono|x86.Build.0 = Debug|x86 - {BEC74619-DDBB-4FBA-B517-D3E20AFC9997}.Release|Any CPU.ActiveCfg = Release|x86 - {BEC74619-DDBB-4FBA-B517-D3E20AFC9997}.Release|x86.ActiveCfg = Release|x86 - {BEC74619-DDBB-4FBA-B517-D3E20AFC9997}.Release|x86.Build.0 = Release|x86 - {BEC74619-DDBB-4FBA-B517-D3E20AFC9997}.Travis|Any CPU.ActiveCfg = Release|x86 - {BEC74619-DDBB-4FBA-B517-D3E20AFC9997}.Travis|Any CPU.Build.0 = Release|x86 - {BEC74619-DDBB-4FBA-B517-D3E20AFC9997}.Travis|x86.ActiveCfg = Release|x86 - {BEC74619-DDBB-4FBA-B517-D3E20AFC9997}.Travis|x86.Build.0 = Release|x86 - {D18A5DEB-5102-4775-A1AF-B75DAAA8907B}.Debug|Any CPU.ActiveCfg = Debug|x86 - {D18A5DEB-5102-4775-A1AF-B75DAAA8907B}.Debug|Any CPU.Build.0 = Debug|x86 - {D18A5DEB-5102-4775-A1AF-B75DAAA8907B}.Debug|x86.ActiveCfg = Debug|x86 - {D18A5DEB-5102-4775-A1AF-B75DAAA8907B}.Debug|x86.Build.0 = Debug|x86 - {D18A5DEB-5102-4775-A1AF-B75DAAA8907B}.Mono|Any CPU.ActiveCfg = Release|x86 - {D18A5DEB-5102-4775-A1AF-B75DAAA8907B}.Mono|Any CPU.Build.0 = Release|x86 - {D18A5DEB-5102-4775-A1AF-B75DAAA8907B}.Mono|x86.ActiveCfg = Release|x86 - {D18A5DEB-5102-4775-A1AF-B75DAAA8907B}.Mono|x86.Build.0 = Release|x86 - {D18A5DEB-5102-4775-A1AF-B75DAAA8907B}.Release|Any CPU.ActiveCfg = Release|x86 - {D18A5DEB-5102-4775-A1AF-B75DAAA8907B}.Release|x86.ActiveCfg = Release|x86 - {D18A5DEB-5102-4775-A1AF-B75DAAA8907B}.Release|x86.Build.0 = Release|x86 - {D18A5DEB-5102-4775-A1AF-B75DAAA8907B}.Travis|Any CPU.ActiveCfg = Release|x86 - {D18A5DEB-5102-4775-A1AF-B75DAAA8907B}.Travis|Any CPU.Build.0 = Release|x86 - {D18A5DEB-5102-4775-A1AF-B75DAAA8907B}.Travis|x86.ActiveCfg = Release|x86 - {D18A5DEB-5102-4775-A1AF-B75DAAA8907B}.Travis|x86.Build.0 = Release|x86 - {CBF6B8B0-A015-413A-8C86-01238BB45770}.Debug|Any CPU.ActiveCfg = Debug|x86 - {CBF6B8B0-A015-413A-8C86-01238BB45770}.Debug|Any CPU.Build.0 = Debug|x86 - {CBF6B8B0-A015-413A-8C86-01238BB45770}.Debug|x86.ActiveCfg = Debug|x86 - {CBF6B8B0-A015-413A-8C86-01238BB45770}.Debug|x86.Build.0 = Debug|x86 - {CBF6B8B0-A015-413A-8C86-01238BB45770}.Mono|Any CPU.ActiveCfg = Release|x86 - {CBF6B8B0-A015-413A-8C86-01238BB45770}.Mono|Any CPU.Build.0 = Release|x86 - {CBF6B8B0-A015-413A-8C86-01238BB45770}.Mono|x86.ActiveCfg = Debug|x86 - {CBF6B8B0-A015-413A-8C86-01238BB45770}.Mono|x86.Build.0 = Debug|x86 - {CBF6B8B0-A015-413A-8C86-01238BB45770}.Release|Any CPU.ActiveCfg = Release|x86 - {CBF6B8B0-A015-413A-8C86-01238BB45770}.Release|x86.ActiveCfg = Release|x86 - {CBF6B8B0-A015-413A-8C86-01238BB45770}.Release|x86.Build.0 = Release|x86 - {CBF6B8B0-A015-413A-8C86-01238BB45770}.Travis|Any CPU.ActiveCfg = Release|x86 - {CBF6B8B0-A015-413A-8C86-01238BB45770}.Travis|Any CPU.Build.0 = Release|x86 - {CBF6B8B0-A015-413A-8C86-01238BB45770}.Travis|x86.ActiveCfg = Release|x86 - {CBF6B8B0-A015-413A-8C86-01238BB45770}.Travis|x86.Build.0 = Release|x86 - {8CEFECD0-A6C2-498F-98B1-3FBE5820F9AB}.Debug|Any CPU.ActiveCfg = Debug|x86 - {8CEFECD0-A6C2-498F-98B1-3FBE5820F9AB}.Debug|Any CPU.Build.0 = Debug|x86 - {8CEFECD0-A6C2-498F-98B1-3FBE5820F9AB}.Debug|x86.ActiveCfg = Debug|x86 - {8CEFECD0-A6C2-498F-98B1-3FBE5820F9AB}.Debug|x86.Build.0 = Debug|x86 - {8CEFECD0-A6C2-498F-98B1-3FBE5820F9AB}.Mono|Any CPU.ActiveCfg = Release|x86 - {8CEFECD0-A6C2-498F-98B1-3FBE5820F9AB}.Mono|Any CPU.Build.0 = Release|x86 - {8CEFECD0-A6C2-498F-98B1-3FBE5820F9AB}.Mono|x86.ActiveCfg = Debug|x86 - {8CEFECD0-A6C2-498F-98B1-3FBE5820F9AB}.Mono|x86.Build.0 = Debug|x86 - {8CEFECD0-A6C2-498F-98B1-3FBE5820F9AB}.Release|Any CPU.ActiveCfg = Release|x86 - {8CEFECD0-A6C2-498F-98B1-3FBE5820F9AB}.Release|x86.ActiveCfg = Release|x86 - {8CEFECD0-A6C2-498F-98B1-3FBE5820F9AB}.Release|x86.Build.0 = Release|x86 - {8CEFECD0-A6C2-498F-98B1-3FBE5820F9AB}.Travis|Any CPU.ActiveCfg = Release|x86 - {8CEFECD0-A6C2-498F-98B1-3FBE5820F9AB}.Travis|Any CPU.Build.0 = Release|x86 - {8CEFECD0-A6C2-498F-98B1-3FBE5820F9AB}.Travis|x86.ActiveCfg = Release|x86 - {8CEFECD0-A6C2-498F-98B1-3FBE5820F9AB}.Travis|x86.Build.0 = Release|x86 - {CC26800D-F67E-464B-88DE-8EB1A0C227A3}.Debug|Any CPU.ActiveCfg = Debug|x86 - {CC26800D-F67E-464B-88DE-8EB1A0C227A3}.Debug|Any CPU.Build.0 = Debug|x86 - {CC26800D-F67E-464B-88DE-8EB1A0C227A3}.Debug|x86.ActiveCfg = Debug|x86 - {CC26800D-F67E-464B-88DE-8EB1A0C227A3}.Debug|x86.Build.0 = Debug|x86 - {CC26800D-F67E-464B-88DE-8EB1A0C227A3}.Mono|Any CPU.ActiveCfg = Release|x86 - {CC26800D-F67E-464B-88DE-8EB1A0C227A3}.Mono|Any CPU.Build.0 = Release|x86 - {CC26800D-F67E-464B-88DE-8EB1A0C227A3}.Mono|x86.ActiveCfg = Debug|x86 - {CC26800D-F67E-464B-88DE-8EB1A0C227A3}.Mono|x86.Build.0 = Debug|x86 - {CC26800D-F67E-464B-88DE-8EB1A0C227A3}.Release|Any CPU.ActiveCfg = Release|x86 - {CC26800D-F67E-464B-88DE-8EB1A0C227A3}.Release|x86.ActiveCfg = Release|x86 - {CC26800D-F67E-464B-88DE-8EB1A0C227A3}.Release|x86.Build.0 = Release|x86 - {CC26800D-F67E-464B-88DE-8EB1A0C227A3}.Travis|Any CPU.ActiveCfg = Release|x86 - {CC26800D-F67E-464B-88DE-8EB1A0C227A3}.Travis|Any CPU.Build.0 = Release|x86 - {CC26800D-F67E-464B-88DE-8EB1A0C227A3}.Travis|x86.ActiveCfg = Release|x86 - {CC26800D-F67E-464B-88DE-8EB1A0C227A3}.Travis|x86.Build.0 = Release|x86 - {6BCE712F-846D-4846-9D1B-A66B858DA755}.Debug|Any CPU.ActiveCfg = Debug|x86 - {6BCE712F-846D-4846-9D1B-A66B858DA755}.Debug|Any CPU.Build.0 = Debug|x86 - {6BCE712F-846D-4846-9D1B-A66B858DA755}.Debug|x86.ActiveCfg = Debug|x86 - {6BCE712F-846D-4846-9D1B-A66B858DA755}.Debug|x86.Build.0 = Debug|x86 - {6BCE712F-846D-4846-9D1B-A66B858DA755}.Mono|Any CPU.ActiveCfg = Release|x86 - {6BCE712F-846D-4846-9D1B-A66B858DA755}.Mono|Any CPU.Build.0 = Release|x86 - {6BCE712F-846D-4846-9D1B-A66B858DA755}.Mono|x86.ActiveCfg = Debug|x86 - {6BCE712F-846D-4846-9D1B-A66B858DA755}.Release|Any CPU.ActiveCfg = Release|x86 - {6BCE712F-846D-4846-9D1B-A66B858DA755}.Release|x86.ActiveCfg = Release|x86 - {6BCE712F-846D-4846-9D1B-A66B858DA755}.Release|x86.Build.0 = Release|x86 - {6BCE712F-846D-4846-9D1B-A66B858DA755}.Travis|Any CPU.ActiveCfg = Release|x86 - {6BCE712F-846D-4846-9D1B-A66B858DA755}.Travis|Any CPU.Build.0 = Release|x86 - {6BCE712F-846D-4846-9D1B-A66B858DA755}.Travis|x86.ActiveCfg = Release|x86 - {6BCE712F-846D-4846-9D1B-A66B858DA755}.Travis|x86.Build.0 = Release|x86 - {700D0B95-95CD-43F3-B6C9-FAA0FC1358D4}.Debug|Any CPU.ActiveCfg = Debug|x86 - {700D0B95-95CD-43F3-B6C9-FAA0FC1358D4}.Debug|Any CPU.Build.0 = Debug|x86 - {700D0B95-95CD-43F3-B6C9-FAA0FC1358D4}.Debug|x86.ActiveCfg = Debug|x86 - {700D0B95-95CD-43F3-B6C9-FAA0FC1358D4}.Debug|x86.Build.0 = Debug|x86 - {700D0B95-95CD-43F3-B6C9-FAA0FC1358D4}.Mono|Any CPU.ActiveCfg = Release|x86 - {700D0B95-95CD-43F3-B6C9-FAA0FC1358D4}.Mono|Any CPU.Build.0 = Release|x86 - {700D0B95-95CD-43F3-B6C9-FAA0FC1358D4}.Mono|x86.ActiveCfg = Debug|x86 - {700D0B95-95CD-43F3-B6C9-FAA0FC1358D4}.Release|Any CPU.ActiveCfg = Release|x86 - {700D0B95-95CD-43F3-B6C9-FAA0FC1358D4}.Release|x86.ActiveCfg = Release|x86 - {700D0B95-95CD-43F3-B6C9-FAA0FC1358D4}.Release|x86.Build.0 = Release|x86 - {700D0B95-95CD-43F3-B6C9-FAA0FC1358D4}.Travis|Any CPU.ActiveCfg = Release|x86 - {700D0B95-95CD-43F3-B6C9-FAA0FC1358D4}.Travis|Any CPU.Build.0 = Release|x86 - {700D0B95-95CD-43F3-B6C9-FAA0FC1358D4}.Travis|x86.ActiveCfg = Release|x86 - {700D0B95-95CD-43F3-B6C9-FAA0FC1358D4}.Travis|x86.Build.0 = Release|x86 - {FF5EE3B6-913B-47CE-9CEB-11C51B4E1205}.Debug|Any CPU.ActiveCfg = Debug|x86 - {FF5EE3B6-913B-47CE-9CEB-11C51B4E1205}.Debug|Any CPU.Build.0 = Debug|x86 - {FF5EE3B6-913B-47CE-9CEB-11C51B4E1205}.Debug|x86.ActiveCfg = Debug|x86 - {FF5EE3B6-913B-47CE-9CEB-11C51B4E1205}.Debug|x86.Build.0 = Debug|x86 - {FF5EE3B6-913B-47CE-9CEB-11C51B4E1205}.Mono|Any CPU.ActiveCfg = Release|x86 - {FF5EE3B6-913B-47CE-9CEB-11C51B4E1205}.Mono|Any CPU.Build.0 = Release|x86 - {FF5EE3B6-913B-47CE-9CEB-11C51B4E1205}.Mono|x86.ActiveCfg = Release|x86 - {FF5EE3B6-913B-47CE-9CEB-11C51B4E1205}.Mono|x86.Build.0 = Release|x86 - {FF5EE3B6-913B-47CE-9CEB-11C51B4E1205}.Release|Any CPU.ActiveCfg = Release|x86 - {FF5EE3B6-913B-47CE-9CEB-11C51B4E1205}.Release|x86.ActiveCfg = Release|x86 - {FF5EE3B6-913B-47CE-9CEB-11C51B4E1205}.Release|x86.Build.0 = Release|x86 - {FF5EE3B6-913B-47CE-9CEB-11C51B4E1205}.Travis|Any CPU.ActiveCfg = Release|x86 - {FF5EE3B6-913B-47CE-9CEB-11C51B4E1205}.Travis|Any CPU.Build.0 = Release|x86 - {FF5EE3B6-913B-47CE-9CEB-11C51B4E1205}.Travis|x86.ActiveCfg = Release|x86 - {FF5EE3B6-913B-47CE-9CEB-11C51B4E1205}.Travis|x86.Build.0 = Release|x86 - {4CCC53CD-8D5E-4CC4-97D2-5C9312AC2BD7}.Debug|Any CPU.ActiveCfg = Debug|x86 - {4CCC53CD-8D5E-4CC4-97D2-5C9312AC2BD7}.Debug|Any CPU.Build.0 = Debug|x86 - {4CCC53CD-8D5E-4CC4-97D2-5C9312AC2BD7}.Debug|x86.ActiveCfg = Debug|x86 - {4CCC53CD-8D5E-4CC4-97D2-5C9312AC2BD7}.Debug|x86.Build.0 = Debug|x86 - {4CCC53CD-8D5E-4CC4-97D2-5C9312AC2BD7}.Mono|Any CPU.ActiveCfg = Release|x86 - {4CCC53CD-8D5E-4CC4-97D2-5C9312AC2BD7}.Mono|Any CPU.Build.0 = Release|x86 - {4CCC53CD-8D5E-4CC4-97D2-5C9312AC2BD7}.Mono|x86.ActiveCfg = Debug|x86 - {4CCC53CD-8D5E-4CC4-97D2-5C9312AC2BD7}.Mono|x86.Build.0 = Debug|x86 - {4CCC53CD-8D5E-4CC4-97D2-5C9312AC2BD7}.Release|Any CPU.ActiveCfg = Release|x86 - {4CCC53CD-8D5E-4CC4-97D2-5C9312AC2BD7}.Release|x86.ActiveCfg = Release|x86 - {4CCC53CD-8D5E-4CC4-97D2-5C9312AC2BD7}.Release|x86.Build.0 = Release|x86 - {4CCC53CD-8D5E-4CC4-97D2-5C9312AC2BD7}.Travis|Any CPU.ActiveCfg = Release|x86 - {4CCC53CD-8D5E-4CC4-97D2-5C9312AC2BD7}.Travis|Any CPU.Build.0 = Release|x86 - {4CCC53CD-8D5E-4CC4-97D2-5C9312AC2BD7}.Travis|x86.ActiveCfg = Release|x86 - {4CCC53CD-8D5E-4CC4-97D2-5C9312AC2BD7}.Travis|x86.Build.0 = Release|x86 - {F2BE0FDF-6E47-4827-A420-DD4EF82407F8}.Debug|Any CPU.ActiveCfg = Debug|x86 - {F2BE0FDF-6E47-4827-A420-DD4EF82407F8}.Debug|Any CPU.Build.0 = Debug|x86 - {F2BE0FDF-6E47-4827-A420-DD4EF82407F8}.Debug|x86.ActiveCfg = Debug|x86 - {F2BE0FDF-6E47-4827-A420-DD4EF82407F8}.Debug|x86.Build.0 = Debug|x86 - {F2BE0FDF-6E47-4827-A420-DD4EF82407F8}.Mono|Any CPU.ActiveCfg = Release|x86 - {F2BE0FDF-6E47-4827-A420-DD4EF82407F8}.Mono|Any CPU.Build.0 = Release|x86 - {F2BE0FDF-6E47-4827-A420-DD4EF82407F8}.Mono|x86.ActiveCfg = Release|x86 - {F2BE0FDF-6E47-4827-A420-DD4EF82407F8}.Mono|x86.Build.0 = Release|x86 - {F2BE0FDF-6E47-4827-A420-DD4EF82407F8}.Release|Any CPU.ActiveCfg = Release|x86 - {F2BE0FDF-6E47-4827-A420-DD4EF82407F8}.Release|x86.ActiveCfg = Release|x86 - {F2BE0FDF-6E47-4827-A420-DD4EF82407F8}.Release|x86.Build.0 = Release|x86 - {F2BE0FDF-6E47-4827-A420-DD4EF82407F8}.Travis|Any CPU.ActiveCfg = Release|x86 - {F2BE0FDF-6E47-4827-A420-DD4EF82407F8}.Travis|Any CPU.Build.0 = Release|x86 - {F2BE0FDF-6E47-4827-A420-DD4EF82407F8}.Travis|x86.ActiveCfg = Release|x86 - {F2BE0FDF-6E47-4827-A420-DD4EF82407F8}.Travis|x86.Build.0 = Release|x86 - {FD286DF8-2D3A-4394-8AD5-443FADE55FB2}.Debug|Any CPU.ActiveCfg = Debug|x86 - {FD286DF8-2D3A-4394-8AD5-443FADE55FB2}.Debug|Any CPU.Build.0 = Debug|x86 - {FD286DF8-2D3A-4394-8AD5-443FADE55FB2}.Debug|x86.ActiveCfg = Debug|x86 - {FD286DF8-2D3A-4394-8AD5-443FADE55FB2}.Debug|x86.Build.0 = Debug|x86 - {FD286DF8-2D3A-4394-8AD5-443FADE55FB2}.Mono|Any CPU.ActiveCfg = Release|x86 - {FD286DF8-2D3A-4394-8AD5-443FADE55FB2}.Mono|Any CPU.Build.0 = Release|x86 - {FD286DF8-2D3A-4394-8AD5-443FADE55FB2}.Mono|x86.ActiveCfg = Release|x86 - {FD286DF8-2D3A-4394-8AD5-443FADE55FB2}.Mono|x86.Build.0 = Release|x86 - {FD286DF8-2D3A-4394-8AD5-443FADE55FB2}.Release|Any CPU.ActiveCfg = Release|x86 - {FD286DF8-2D3A-4394-8AD5-443FADE55FB2}.Release|x86.ActiveCfg = Release|x86 - {FD286DF8-2D3A-4394-8AD5-443FADE55FB2}.Release|x86.Build.0 = Release|x86 - {FD286DF8-2D3A-4394-8AD5-443FADE55FB2}.Travis|Any CPU.ActiveCfg = Release|x86 - {FD286DF8-2D3A-4394-8AD5-443FADE55FB2}.Travis|Any CPU.Build.0 = Release|x86 - {FD286DF8-2D3A-4394-8AD5-443FADE55FB2}.Travis|x86.ActiveCfg = Release|x86 - {FD286DF8-2D3A-4394-8AD5-443FADE55FB2}.Travis|x86.Build.0 = Release|x86 - {3DCA7B58-B8B3-49AC-9D9E-56F4A0460976}.Debug|Any CPU.ActiveCfg = Debug|x86 - {3DCA7B58-B8B3-49AC-9D9E-56F4A0460976}.Debug|Any CPU.Build.0 = Debug|x86 - {3DCA7B58-B8B3-49AC-9D9E-56F4A0460976}.Debug|x86.ActiveCfg = Debug|x86 - {3DCA7B58-B8B3-49AC-9D9E-56F4A0460976}.Debug|x86.Build.0 = Debug|x86 - {3DCA7B58-B8B3-49AC-9D9E-56F4A0460976}.Mono|Any CPU.ActiveCfg = Release|x86 - {3DCA7B58-B8B3-49AC-9D9E-56F4A0460976}.Mono|Any CPU.Build.0 = Release|x86 - {3DCA7B58-B8B3-49AC-9D9E-56F4A0460976}.Mono|x86.ActiveCfg = Debug|x86 - {3DCA7B58-B8B3-49AC-9D9E-56F4A0460976}.Mono|x86.Build.0 = Debug|x86 - {3DCA7B58-B8B3-49AC-9D9E-56F4A0460976}.Release|Any CPU.ActiveCfg = Release|x86 - {3DCA7B58-B8B3-49AC-9D9E-56F4A0460976}.Release|x86.ActiveCfg = Release|x86 - {3DCA7B58-B8B3-49AC-9D9E-56F4A0460976}.Release|x86.Build.0 = Release|x86 - {3DCA7B58-B8B3-49AC-9D9E-56F4A0460976}.Travis|Any CPU.ActiveCfg = Release|x86 - {3DCA7B58-B8B3-49AC-9D9E-56F4A0460976}.Travis|Any CPU.Build.0 = Release|x86 - {3DCA7B58-B8B3-49AC-9D9E-56F4A0460976}.Travis|x86.ActiveCfg = Release|x86 - {3DCA7B58-B8B3-49AC-9D9E-56F4A0460976}.Travis|x86.Build.0 = Release|x86 - {95C11A9E-56ED-456A-8447-2C89C1139266}.Debug|Any CPU.ActiveCfg = Debug|x86 - {95C11A9E-56ED-456A-8447-2C89C1139266}.Debug|Any CPU.Build.0 = Debug|x86 - {95C11A9E-56ED-456A-8447-2C89C1139266}.Debug|x86.ActiveCfg = Debug|x86 - {95C11A9E-56ED-456A-8447-2C89C1139266}.Debug|x86.Build.0 = Debug|x86 - {95C11A9E-56ED-456A-8447-2C89C1139266}.Mono|Any CPU.ActiveCfg = Release|x86 - {95C11A9E-56ED-456A-8447-2C89C1139266}.Mono|Any CPU.Build.0 = Release|x86 - {95C11A9E-56ED-456A-8447-2C89C1139266}.Mono|x86.ActiveCfg = Debug|x86 - {95C11A9E-56ED-456A-8447-2C89C1139266}.Mono|x86.Build.0 = Debug|x86 - {95C11A9E-56ED-456A-8447-2C89C1139266}.Release|Any CPU.ActiveCfg = Release|x86 - {95C11A9E-56ED-456A-8447-2C89C1139266}.Release|x86.ActiveCfg = Release|x86 - {95C11A9E-56ED-456A-8447-2C89C1139266}.Release|x86.Build.0 = Release|x86 - {95C11A9E-56ED-456A-8447-2C89C1139266}.Travis|Any CPU.ActiveCfg = Release|x86 - {95C11A9E-56ED-456A-8447-2C89C1139266}.Travis|Any CPU.Build.0 = Release|x86 - {95C11A9E-56ED-456A-8447-2C89C1139266}.Travis|x86.ActiveCfg = Release|x86 - {95C11A9E-56ED-456A-8447-2C89C1139266}.Travis|x86.Build.0 = Release|x86 - {D12F7F2F-8A3C-415F-88FA-6DD061A84869}.Debug|Any CPU.ActiveCfg = Debug|x86 - {D12F7F2F-8A3C-415F-88FA-6DD061A84869}.Debug|Any CPU.Build.0 = Debug|x86 - {D12F7F2F-8A3C-415F-88FA-6DD061A84869}.Debug|x86.ActiveCfg = Debug|x86 - {D12F7F2F-8A3C-415F-88FA-6DD061A84869}.Debug|x86.Build.0 = Debug|x86 - {D12F7F2F-8A3C-415F-88FA-6DD061A84869}.Mono|Any CPU.ActiveCfg = Release|x86 - {D12F7F2F-8A3C-415F-88FA-6DD061A84869}.Mono|Any CPU.Build.0 = Release|x86 - {D12F7F2F-8A3C-415F-88FA-6DD061A84869}.Mono|x86.ActiveCfg = Release|x86 - {D12F7F2F-8A3C-415F-88FA-6DD061A84869}.Release|Any CPU.ActiveCfg = Release|x86 - {D12F7F2F-8A3C-415F-88FA-6DD061A84869}.Release|x86.ActiveCfg = Release|x86 - {D12F7F2F-8A3C-415F-88FA-6DD061A84869}.Release|x86.Build.0 = Release|x86 - {D12F7F2F-8A3C-415F-88FA-6DD061A84869}.Travis|Any CPU.ActiveCfg = Release|x86 - {D12F7F2F-8A3C-415F-88FA-6DD061A84869}.Travis|Any CPU.Build.0 = Release|x86 - {D12F7F2F-8A3C-415F-88FA-6DD061A84869}.Travis|x86.ActiveCfg = Release|x86 - {D12F7F2F-8A3C-415F-88FA-6DD061A84869}.Travis|x86.Build.0 = Release|x86 - {7C2CC69F-5CA0-4E5C-85CB-983F9F6C3B36}.Debug|Any CPU.ActiveCfg = Debug|x86 - {7C2CC69F-5CA0-4E5C-85CB-983F9F6C3B36}.Debug|Any CPU.Build.0 = Debug|x86 - {7C2CC69F-5CA0-4E5C-85CB-983F9F6C3B36}.Debug|x86.ActiveCfg = Debug|x86 - {7C2CC69F-5CA0-4E5C-85CB-983F9F6C3B36}.Debug|x86.Build.0 = Debug|x86 - {7C2CC69F-5CA0-4E5C-85CB-983F9F6C3B36}.Mono|Any CPU.ActiveCfg = Release|x86 - {7C2CC69F-5CA0-4E5C-85CB-983F9F6C3B36}.Mono|Any CPU.Build.0 = Release|x86 - {7C2CC69F-5CA0-4E5C-85CB-983F9F6C3B36}.Mono|x86.ActiveCfg = Debug|x86 - {7C2CC69F-5CA0-4E5C-85CB-983F9F6C3B36}.Mono|x86.Build.0 = Debug|x86 - {7C2CC69F-5CA0-4E5C-85CB-983F9F6C3B36}.Release|Any CPU.ActiveCfg = Release|x86 - {7C2CC69F-5CA0-4E5C-85CB-983F9F6C3B36}.Release|x86.ActiveCfg = Release|x86 - {7C2CC69F-5CA0-4E5C-85CB-983F9F6C3B36}.Release|x86.Build.0 = Release|x86 - {7C2CC69F-5CA0-4E5C-85CB-983F9F6C3B36}.Travis|Any CPU.ActiveCfg = Release|x86 - {7C2CC69F-5CA0-4E5C-85CB-983F9F6C3B36}.Travis|Any CPU.Build.0 = Release|x86 - {7C2CC69F-5CA0-4E5C-85CB-983F9F6C3B36}.Travis|x86.ActiveCfg = Release|x86 - {7C2CC69F-5CA0-4E5C-85CB-983F9F6C3B36}.Travis|x86.Build.0 = Release|x86 - {1B9A82C4-BCA1-4834-A33E-226F17BE070B}.Debug|Any CPU.ActiveCfg = Debug|x86 - {1B9A82C4-BCA1-4834-A33E-226F17BE070B}.Debug|Any CPU.Build.0 = Debug|x86 - {1B9A82C4-BCA1-4834-A33E-226F17BE070B}.Debug|x86.ActiveCfg = Debug|x86 - {1B9A82C4-BCA1-4834-A33E-226F17BE070B}.Debug|x86.Build.0 = Debug|x86 - {1B9A82C4-BCA1-4834-A33E-226F17BE070B}.Mono|Any CPU.ActiveCfg = Release|x86 - {1B9A82C4-BCA1-4834-A33E-226F17BE070B}.Mono|Any CPU.Build.0 = Release|x86 - {1B9A82C4-BCA1-4834-A33E-226F17BE070B}.Mono|x86.ActiveCfg = Debug|x86 - {1B9A82C4-BCA1-4834-A33E-226F17BE070B}.Mono|x86.Build.0 = Debug|x86 - {1B9A82C4-BCA1-4834-A33E-226F17BE070B}.Release|Any CPU.ActiveCfg = Release|x86 - {1B9A82C4-BCA1-4834-A33E-226F17BE070B}.Release|x86.ActiveCfg = Release|x86 - {1B9A82C4-BCA1-4834-A33E-226F17BE070B}.Release|x86.Build.0 = Release|x86 - {1B9A82C4-BCA1-4834-A33E-226F17BE070B}.Travis|Any CPU.ActiveCfg = Release|x86 - {1B9A82C4-BCA1-4834-A33E-226F17BE070B}.Travis|Any CPU.Build.0 = Release|x86 - {1B9A82C4-BCA1-4834-A33E-226F17BE070B}.Travis|x86.ActiveCfg = Release|x86 - {1B9A82C4-BCA1-4834-A33E-226F17BE070B}.Travis|x86.Build.0 = Release|x86 - {2B8C6DAD-4D85-41B1-83FD-248D9F347522}.Debug|Any CPU.ActiveCfg = Debug|x86 - {2B8C6DAD-4D85-41B1-83FD-248D9F347522}.Debug|Any CPU.Build.0 = Debug|x86 - {2B8C6DAD-4D85-41B1-83FD-248D9F347522}.Debug|x86.ActiveCfg = Debug|x86 - {2B8C6DAD-4D85-41B1-83FD-248D9F347522}.Debug|x86.Build.0 = Debug|x86 - {2B8C6DAD-4D85-41B1-83FD-248D9F347522}.Mono|Any CPU.ActiveCfg = Release|x86 - {2B8C6DAD-4D85-41B1-83FD-248D9F347522}.Mono|Any CPU.Build.0 = Release|x86 - {2B8C6DAD-4D85-41B1-83FD-248D9F347522}.Mono|x86.ActiveCfg = Release|x86 - {2B8C6DAD-4D85-41B1-83FD-248D9F347522}.Mono|x86.Build.0 = Release|x86 - {2B8C6DAD-4D85-41B1-83FD-248D9F347522}.Release|Any CPU.ActiveCfg = Release|x86 - {2B8C6DAD-4D85-41B1-83FD-248D9F347522}.Release|x86.ActiveCfg = Release|x86 - {2B8C6DAD-4D85-41B1-83FD-248D9F347522}.Release|x86.Build.0 = Release|x86 - {2B8C6DAD-4D85-41B1-83FD-248D9F347522}.Travis|Any CPU.ActiveCfg = Release|x86 - {2B8C6DAD-4D85-41B1-83FD-248D9F347522}.Travis|Any CPU.Build.0 = Release|x86 - {2B8C6DAD-4D85-41B1-83FD-248D9F347522}.Travis|x86.ActiveCfg = Release|x86 - {2B8C6DAD-4D85-41B1-83FD-248D9F347522}.Travis|x86.Build.0 = Release|x86 - {F6FC6BE7-0847-4817-A1ED-223DC647C3D7}.Debug|Any CPU.ActiveCfg = Debug|x86 - {F6FC6BE7-0847-4817-A1ED-223DC647C3D7}.Debug|Any CPU.Build.0 = Debug|x86 - {F6FC6BE7-0847-4817-A1ED-223DC647C3D7}.Debug|x86.ActiveCfg = Debug|x86 - {F6FC6BE7-0847-4817-A1ED-223DC647C3D7}.Debug|x86.Build.0 = Debug|x86 - {F6FC6BE7-0847-4817-A1ED-223DC647C3D7}.Mono|Any CPU.ActiveCfg = Release|x86 - {F6FC6BE7-0847-4817-A1ED-223DC647C3D7}.Mono|Any CPU.Build.0 = Release|x86 - {F6FC6BE7-0847-4817-A1ED-223DC647C3D7}.Mono|x86.ActiveCfg = Release|x86 - {F6FC6BE7-0847-4817-A1ED-223DC647C3D7}.Mono|x86.Build.0 = Release|x86 - {F6FC6BE7-0847-4817-A1ED-223DC647C3D7}.Release|Any CPU.ActiveCfg = Release|x86 - {F6FC6BE7-0847-4817-A1ED-223DC647C3D7}.Release|x86.ActiveCfg = Release|x86 - {F6FC6BE7-0847-4817-A1ED-223DC647C3D7}.Release|x86.Build.0 = Release|x86 - {F6FC6BE7-0847-4817-A1ED-223DC647C3D7}.Travis|Any CPU.ActiveCfg = Release|x86 - {F6FC6BE7-0847-4817-A1ED-223DC647C3D7}.Travis|Any CPU.Build.0 = Release|x86 - {F6FC6BE7-0847-4817-A1ED-223DC647C3D7}.Travis|x86.ActiveCfg = Release|x86 - {F6FC6BE7-0847-4817-A1ED-223DC647C3D7}.Travis|x86.Build.0 = Release|x86 - {15AD7579-A314-4626-B556-663F51D97CD1}.Debug|Any CPU.ActiveCfg = Debug|x86 - {15AD7579-A314-4626-B556-663F51D97CD1}.Debug|Any CPU.Build.0 = Debug|x86 - {15AD7579-A314-4626-B556-663F51D97CD1}.Debug|x86.ActiveCfg = Debug|x86 - {15AD7579-A314-4626-B556-663F51D97CD1}.Debug|x86.Build.0 = Debug|x86 - {15AD7579-A314-4626-B556-663F51D97CD1}.Mono|Any CPU.ActiveCfg = Release|Any CPU - {15AD7579-A314-4626-B556-663F51D97CD1}.Mono|Any CPU.Build.0 = Release|Any CPU - {15AD7579-A314-4626-B556-663F51D97CD1}.Mono|x86.ActiveCfg = Release|x86 - {15AD7579-A314-4626-B556-663F51D97CD1}.Release|Any CPU.ActiveCfg = Release|Any CPU - {15AD7579-A314-4626-B556-663F51D97CD1}.Release|Any CPU.Build.0 = Release|Any CPU - {15AD7579-A314-4626-B556-663F51D97CD1}.Release|x86.ActiveCfg = Release|x86 - {15AD7579-A314-4626-B556-663F51D97CD1}.Release|x86.Build.0 = Release|x86 - {15AD7579-A314-4626-B556-663F51D97CD1}.Travis|Any CPU.ActiveCfg = Release|Any CPU - {15AD7579-A314-4626-B556-663F51D97CD1}.Travis|Any CPU.Build.0 = Release|Any CPU - {15AD7579-A314-4626-B556-663F51D97CD1}.Travis|x86.ActiveCfg = Release|x86 - {15AD7579-A314-4626-B556-663F51D97CD1}.Travis|x86.Build.0 = Release|x86 - {911284D3-F130-459E-836C-2430B6FBF21D}.Debug|Any CPU.ActiveCfg = Debug|x86 - {911284D3-F130-459E-836C-2430B6FBF21D}.Debug|Any CPU.Build.0 = Debug|x86 - {911284D3-F130-459E-836C-2430B6FBF21D}.Debug|x86.ActiveCfg = Debug|x86 - {911284D3-F130-459E-836C-2430B6FBF21D}.Debug|x86.Build.0 = Debug|x86 - {911284D3-F130-459E-836C-2430B6FBF21D}.Mono|Any CPU.ActiveCfg = Release|Any CPU - {911284D3-F130-459E-836C-2430B6FBF21D}.Mono|Any CPU.Build.0 = Release|Any CPU - {911284D3-F130-459E-836C-2430B6FBF21D}.Mono|x86.ActiveCfg = Release|x86 - {911284D3-F130-459E-836C-2430B6FBF21D}.Release|Any CPU.ActiveCfg = Release|Any CPU - {911284D3-F130-459E-836C-2430B6FBF21D}.Release|Any CPU.Build.0 = Release|Any CPU - {911284D3-F130-459E-836C-2430B6FBF21D}.Release|x86.ActiveCfg = Release|x86 - {911284D3-F130-459E-836C-2430B6FBF21D}.Release|x86.Build.0 = Release|x86 - {911284D3-F130-459E-836C-2430B6FBF21D}.Travis|Any CPU.ActiveCfg = Release|Any CPU - {911284D3-F130-459E-836C-2430B6FBF21D}.Travis|Any CPU.Build.0 = Release|Any CPU - {911284D3-F130-459E-836C-2430B6FBF21D}.Travis|x86.ActiveCfg = Release|x86 - {911284D3-F130-459E-836C-2430B6FBF21D}.Travis|x86.Build.0 = Release|x86 - {80B51429-7A0E-46D6-BEE3-C80DCB1C4EAA}.Debug|Any CPU.ActiveCfg = Debug|x86 - {80B51429-7A0E-46D6-BEE3-C80DCB1C4EAA}.Debug|Any CPU.Build.0 = Debug|x86 - {80B51429-7A0E-46D6-BEE3-C80DCB1C4EAA}.Debug|x86.ActiveCfg = Debug|x86 - {80B51429-7A0E-46D6-BEE3-C80DCB1C4EAA}.Debug|x86.Build.0 = Debug|x86 - {80B51429-7A0E-46D6-BEE3-C80DCB1C4EAA}.Mono|Any CPU.ActiveCfg = Release|Any CPU - {80B51429-7A0E-46D6-BEE3-C80DCB1C4EAA}.Mono|Any CPU.Build.0 = Release|Any CPU - {80B51429-7A0E-46D6-BEE3-C80DCB1C4EAA}.Mono|x86.ActiveCfg = Release|x86 - {80B51429-7A0E-46D6-BEE3-C80DCB1C4EAA}.Release|Any CPU.ActiveCfg = Release|Any CPU - {80B51429-7A0E-46D6-BEE3-C80DCB1C4EAA}.Release|Any CPU.Build.0 = Release|Any CPU - {80B51429-7A0E-46D6-BEE3-C80DCB1C4EAA}.Release|x86.ActiveCfg = Release|x86 - {80B51429-7A0E-46D6-BEE3-C80DCB1C4EAA}.Release|x86.Build.0 = Release|x86 - {80B51429-7A0E-46D6-BEE3-C80DCB1C4EAA}.Travis|Any CPU.ActiveCfg = Release|Any CPU - {80B51429-7A0E-46D6-BEE3-C80DCB1C4EAA}.Travis|Any CPU.Build.0 = Release|Any CPU - {80B51429-7A0E-46D6-BEE3-C80DCB1C4EAA}.Travis|x86.ActiveCfg = Release|x86 - {80B51429-7A0E-46D6-BEE3-C80DCB1C4EAA}.Travis|x86.Build.0 = Release|x86 - {40D72824-7D02-4A77-9106-8FE0EEA2B997}.Debug|Any CPU.ActiveCfg = Debug|x86 - {40D72824-7D02-4A77-9106-8FE0EEA2B997}.Debug|Any CPU.Build.0 = Debug|x86 - {40D72824-7D02-4A77-9106-8FE0EEA2B997}.Debug|x86.ActiveCfg = Debug|x86 - {40D72824-7D02-4A77-9106-8FE0EEA2B997}.Debug|x86.Build.0 = Debug|x86 - {40D72824-7D02-4A77-9106-8FE0EEA2B997}.Mono|Any CPU.ActiveCfg = Release|Any CPU - {40D72824-7D02-4A77-9106-8FE0EEA2B997}.Mono|Any CPU.Build.0 = Release|Any CPU - {40D72824-7D02-4A77-9106-8FE0EEA2B997}.Mono|x86.ActiveCfg = Release|x86 - {40D72824-7D02-4A77-9106-8FE0EEA2B997}.Release|Any CPU.ActiveCfg = Release|Any CPU - {40D72824-7D02-4A77-9106-8FE0EEA2B997}.Release|Any CPU.Build.0 = Release|Any CPU - {40D72824-7D02-4A77-9106-8FE0EEA2B997}.Release|x86.ActiveCfg = Release|x86 - {40D72824-7D02-4A77-9106-8FE0EEA2B997}.Release|x86.Build.0 = Release|x86 - {40D72824-7D02-4A77-9106-8FE0EEA2B997}.Travis|Any CPU.ActiveCfg = Release|Any CPU - {40D72824-7D02-4A77-9106-8FE0EEA2B997}.Travis|Any CPU.Build.0 = Release|Any CPU - {40D72824-7D02-4A77-9106-8FE0EEA2B997}.Travis|x86.ActiveCfg = Release|x86 - {40D72824-7D02-4A77-9106-8FE0EEA2B997}.Travis|x86.Build.0 = Release|x86 - {411A9E0E-FDC6-4E25-828A-0C2CD1CD96F8}.Debug|Any CPU.ActiveCfg = Debug|x86 - {411A9E0E-FDC6-4E25-828A-0C2CD1CD96F8}.Debug|Any CPU.Build.0 = Debug|x86 - {411A9E0E-FDC6-4E25-828A-0C2CD1CD96F8}.Debug|x86.ActiveCfg = Debug|x86 - {411A9E0E-FDC6-4E25-828A-0C2CD1CD96F8}.Debug|x86.Build.0 = Debug|x86 - {411A9E0E-FDC6-4E25-828A-0C2CD1CD96F8}.Mono|Any CPU.ActiveCfg = Release|x86 - {411A9E0E-FDC6-4E25-828A-0C2CD1CD96F8}.Mono|Any CPU.Build.0 = Release|x86 - {411A9E0E-FDC6-4E25-828A-0C2CD1CD96F8}.Mono|x86.ActiveCfg = Release|x86 - {411A9E0E-FDC6-4E25-828A-0C2CD1CD96F8}.Mono|x86.Build.0 = Release|x86 - {411A9E0E-FDC6-4E25-828A-0C2CD1CD96F8}.Release|Any CPU.ActiveCfg = Release|x86 - {411A9E0E-FDC6-4E25-828A-0C2CD1CD96F8}.Release|x86.ActiveCfg = Release|x86 - {411A9E0E-FDC6-4E25-828A-0C2CD1CD96F8}.Release|x86.Build.0 = Release|x86 - {411A9E0E-FDC6-4E25-828A-0C2CD1CD96F8}.Travis|Any CPU.ActiveCfg = Release|x86 - {411A9E0E-FDC6-4E25-828A-0C2CD1CD96F8}.Travis|Any CPU.Build.0 = Release|x86 - {411A9E0E-FDC6-4E25-828A-0C2CD1CD96F8}.Travis|x86.ActiveCfg = Release|x86 - {411A9E0E-FDC6-4E25-828A-0C2CD1CD96F8}.Travis|x86.Build.0 = Release|x86 - {90D6E9FC-7B88-4E1B-B018-8FA742274558}.Debug|Any CPU.ActiveCfg = Debug|x86 - {90D6E9FC-7B88-4E1B-B018-8FA742274558}.Debug|Any CPU.Build.0 = Debug|x86 - {90D6E9FC-7B88-4E1B-B018-8FA742274558}.Debug|x86.ActiveCfg = Debug|x86 - {90D6E9FC-7B88-4E1B-B018-8FA742274558}.Debug|x86.Build.0 = Debug|x86 - {90D6E9FC-7B88-4E1B-B018-8FA742274558}.Mono|Any CPU.ActiveCfg = Release|Any CPU - {90D6E9FC-7B88-4E1B-B018-8FA742274558}.Mono|Any CPU.Build.0 = Release|Any CPU - {90D6E9FC-7B88-4E1B-B018-8FA742274558}.Mono|x86.ActiveCfg = Release|x86 - {90D6E9FC-7B88-4E1B-B018-8FA742274558}.Mono|x86.Build.0 = Release|x86 - {90D6E9FC-7B88-4E1B-B018-8FA742274558}.Release|Any CPU.ActiveCfg = Release|Any CPU - {90D6E9FC-7B88-4E1B-B018-8FA742274558}.Release|Any CPU.Build.0 = Release|Any CPU - {90D6E9FC-7B88-4E1B-B018-8FA742274558}.Release|x86.ActiveCfg = Release|x86 - {90D6E9FC-7B88-4E1B-B018-8FA742274558}.Release|x86.Build.0 = Release|x86 - {90D6E9FC-7B88-4E1B-B018-8FA742274558}.Travis|Any CPU.ActiveCfg = Release|Any CPU - {90D6E9FC-7B88-4E1B-B018-8FA742274558}.Travis|Any CPU.Build.0 = Release|Any CPU - {90D6E9FC-7B88-4E1B-B018-8FA742274558}.Travis|x86.ActiveCfg = Release|x86 - {90D6E9FC-7B88-4E1B-B018-8FA742274558}.Travis|x86.Build.0 = Release|x86 - {9DC31DE3-79FF-47A8-96B4-6BA18F6BB1CB}.Debug|Any CPU.ActiveCfg = Debug|x86 - {9DC31DE3-79FF-47A8-96B4-6BA18F6BB1CB}.Debug|Any CPU.Build.0 = Debug|x86 - {9DC31DE3-79FF-47A8-96B4-6BA18F6BB1CB}.Debug|x86.ActiveCfg = Debug|x86 - {9DC31DE3-79FF-47A8-96B4-6BA18F6BB1CB}.Debug|x86.Build.0 = Debug|x86 - {9DC31DE3-79FF-47A8-96B4-6BA18F6BB1CB}.Mono|Any CPU.ActiveCfg = Release|Any CPU - {9DC31DE3-79FF-47A8-96B4-6BA18F6BB1CB}.Mono|Any CPU.Build.0 = Release|Any CPU - {9DC31DE3-79FF-47A8-96B4-6BA18F6BB1CB}.Mono|x86.ActiveCfg = Release|x86 - {9DC31DE3-79FF-47A8-96B4-6BA18F6BB1CB}.Mono|x86.Build.0 = Release|x86 - {9DC31DE3-79FF-47A8-96B4-6BA18F6BB1CB}.Release|Any CPU.ActiveCfg = Release|Any CPU - {9DC31DE3-79FF-47A8-96B4-6BA18F6BB1CB}.Release|Any CPU.Build.0 = Release|Any CPU - {9DC31DE3-79FF-47A8-96B4-6BA18F6BB1CB}.Release|x86.ActiveCfg = Release|x86 - {9DC31DE3-79FF-47A8-96B4-6BA18F6BB1CB}.Release|x86.Build.0 = Release|x86 - {9DC31DE3-79FF-47A8-96B4-6BA18F6BB1CB}.Travis|Any CPU.ActiveCfg = Release|Any CPU - {9DC31DE3-79FF-47A8-96B4-6BA18F6BB1CB}.Travis|Any CPU.Build.0 = Release|Any CPU - {9DC31DE3-79FF-47A8-96B4-6BA18F6BB1CB}.Travis|x86.ActiveCfg = Release|x86 - {9DC31DE3-79FF-47A8-96B4-6BA18F6BB1CB}.Travis|x86.Build.0 = Release|x86 - {74420A79-CC16-442C-8B1E-7C1B913844F0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {74420A79-CC16-442C-8B1E-7C1B913844F0}.Debug|Any CPU.Build.0 = Debug|Any CPU - {74420A79-CC16-442C-8B1E-7C1B913844F0}.Debug|x86.ActiveCfg = Debug|Any CPU - {74420A79-CC16-442C-8B1E-7C1B913844F0}.Debug|x86.Build.0 = Debug|Any CPU - {74420A79-CC16-442C-8B1E-7C1B913844F0}.Mono|Any CPU.ActiveCfg = Release|Any CPU - {74420A79-CC16-442C-8B1E-7C1B913844F0}.Mono|Any CPU.Build.0 = Release|Any CPU - {74420A79-CC16-442C-8B1E-7C1B913844F0}.Mono|x86.ActiveCfg = Release|Any CPU - {74420A79-CC16-442C-8B1E-7C1B913844F0}.Mono|x86.Build.0 = Release|Any CPU - {74420A79-CC16-442C-8B1E-7C1B913844F0}.Release|Any CPU.ActiveCfg = Release|Any CPU - {74420A79-CC16-442C-8B1E-7C1B913844F0}.Release|Any CPU.Build.0 = Release|Any CPU - {74420A79-CC16-442C-8B1E-7C1B913844F0}.Release|x86.ActiveCfg = Release|Any CPU - {74420A79-CC16-442C-8B1E-7C1B913844F0}.Release|x86.Build.0 = Release|Any CPU - {74420A79-CC16-442C-8B1E-7C1B913844F0}.Travis|Any CPU.ActiveCfg = Release|Any CPU - {74420A79-CC16-442C-8B1E-7C1B913844F0}.Travis|Any CPU.Build.0 = Release|Any CPU - {74420A79-CC16-442C-8B1E-7C1B913844F0}.Travis|x86.ActiveCfg = Release|Any CPU - {74420A79-CC16-442C-8B1E-7C1B913844F0}.Travis|x86.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(NestedProjects) = preSolution - {47697CDB-27B6-4B05-B4F8-0CBE6F6EDF97} = {57A04B72-8088-4F75-A582-1158CF8291F7} - {FAFB5948-A222-4CF6-AD14-026BE7564802} = {47697CDB-27B6-4B05-B4F8-0CBE6F6EDF97} - {CADDFCE0-7509-4430-8364-2074E1EEFCA2} = {47697CDB-27B6-4B05-B4F8-0CBE6F6EDF97} - {193ADD3B-792B-4173-8E4C-5A3F8F0237F0} = {57A04B72-8088-4F75-A582-1158CF8291F7} - {C0EA1A40-91AD-4EEB-BD16-2DDDEBD20AE5} = {57A04B72-8088-4F75-A582-1158CF8291F7} - {35388E8E-0CDB-4A84-AD16-E4B6EFDA5D97} = {57A04B72-8088-4F75-A582-1158CF8291F7} - {BEC74619-DDBB-4FBA-B517-D3E20AFC9997} = {57A04B72-8088-4F75-A582-1158CF8291F7} - {D18A5DEB-5102-4775-A1AF-B75DAAA8907B} = {57A04B72-8088-4F75-A582-1158CF8291F7} - {CBF6B8B0-A015-413A-8C86-01238BB45770} = {57A04B72-8088-4F75-A582-1158CF8291F7} - {8CEFECD0-A6C2-498F-98B1-3FBE5820F9AB} = {57A04B72-8088-4F75-A582-1158CF8291F7} - {CC26800D-F67E-464B-88DE-8EB1A0C227A3} = {57A04B72-8088-4F75-A582-1158CF8291F7} - {6BCE712F-846D-4846-9D1B-A66B858DA755} = {F9E67978-5CD6-4A5F-827B-4249711C0B02} - {700D0B95-95CD-43F3-B6C9-FAA0FC1358D4} = {F9E67978-5CD6-4A5F-827B-4249711C0B02} - {3DCA7B58-B8B3-49AC-9D9E-56F4A0460976} = {486ADF86-DD89-4E19-B805-9D94F19800D9} - {95C11A9E-56ED-456A-8447-2C89C1139266} = {486ADF86-DD89-4E19-B805-9D94F19800D9} - {D12F7F2F-8A3C-415F-88FA-6DD061A84869} = {486ADF86-DD89-4E19-B805-9D94F19800D9} - {1B9A82C4-BCA1-4834-A33E-226F17BE070B} = {F6E3A728-AE77-4D02-BAC8-82FBC1402DDA} - {2B8C6DAD-4D85-41B1-83FD-248D9F347522} = {F6E3A728-AE77-4D02-BAC8-82FBC1402DDA} - {F6FC6BE7-0847-4817-A1ED-223DC647C3D7} = {F6E3A728-AE77-4D02-BAC8-82FBC1402DDA} - {15AD7579-A314-4626-B556-663F51D97CD1} = {0F0D4998-8F5D-4467-A909-BB192C4B3B4B} - {911284D3-F130-459E-836C-2430B6FBF21D} = {0F0D4998-8F5D-4467-A909-BB192C4B3B4B} - {4EACDBBC-BCD7-4765-A57B-3E08331E4749} = {57A04B72-8088-4F75-A582-1158CF8291F7} - {80B51429-7A0E-46D6-BEE3-C80DCB1C4EAA} = {4EACDBBC-BCD7-4765-A57B-3E08331E4749} - {40D72824-7D02-4A77-9106-8FE0EEA2B997} = {4EACDBBC-BCD7-4765-A57B-3E08331E4749} - {411A9E0E-FDC6-4E25-828A-0C2CD1CD96F8} = {F6E3A728-AE77-4D02-BAC8-82FBC1402DDA} - {90D6E9FC-7B88-4E1B-B018-8FA742274558} = {F6E3A728-AE77-4D02-BAC8-82FBC1402DDA} - {9DC31DE3-79FF-47A8-96B4-6BA18F6BB1CB} = {F6E3A728-AE77-4D02-BAC8-82FBC1402DDA} - {74420A79-CC16-442C-8B1E-7C1B913844F0} = {F6E3A728-AE77-4D02-BAC8-82FBC1402DDA} - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - EnterpriseLibraryConfigurationToolBinariesPath = packages\Unity.2.1.505.0\lib\NET35;packages\Unity.2.1.505.2\lib\NET35 - EndGlobalSection - GlobalSection(MonoDevelopProperties) = preSolution - StartupItem = NzbDrone.Console\NzbDrone.Console.csproj - EndGlobalSection - GlobalSection(JSLint) = preSolution - SolutionConfigurationLocation = JSLintOptions.xml - EndGlobalSection -EndGlobal diff --git a/src/NzbDrone.sln.DotSettings b/src/NzbDrone.sln.DotSettings deleted file mode 100644 index 1d351d0ce..000000000 --- a/src/NzbDrone.sln.DotSettings +++ /dev/null @@ -1,287 +0,0 @@ -<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation"> - <s:Int64 x:Key="/Default/CodeEditing/Intellisense/CodeCompletion/KeywordCompletionMinLength/@EntryValue">0</s:Int64> - <s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=CheckNamespace/@EntryIndexedValue">ERROR</s:String> - <s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=ClassNeverInstantiated_002EGlobal/@EntryIndexedValue">DO_NOT_SHOW</s:String> - <s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=ConvertIfStatementToReturnStatement/@EntryIndexedValue">DO_NOT_SHOW</s:String> - <s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=ConvertNullableToShortForm/@EntryIndexedValue">DO_NOT_SHOW</s:String> - <s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=FormatStringProblem/@EntryIndexedValue">ERROR</s:String> - <s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=FunctionRecursiveOnAllPaths/@EntryIndexedValue">ERROR</s:String> - <s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=InvokeAsExtensionMethod/@EntryIndexedValue">ERROR</s:String> - <s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=LocalizableElement/@EntryIndexedValue">DO_NOT_SHOW</s:String> - <s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=NUnit_002ENonPublicMethodWithTestAttribute/@EntryIndexedValue">ERROR</s:String> - <s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=PossibleIntendedRethrow/@EntryIndexedValue">ERROR</s:String> - <s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=ReturnTypeCanBeEnumerable_002EGlobal/@EntryIndexedValue">HINT</s:String> - <s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=StringLiteralTypo/@EntryIndexedValue">WARNING</s:String> - <s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=TestClassNameDoesNotMatchFileNameWarning/@EntryIndexedValue">WARNING</s:String> - <s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=TestClassNameSuffixWarning/@EntryIndexedValue">WARNING</s:String> - <s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=UnusedParameter_002ELocal/@EntryIndexedValue">WARNING</s:String> - <s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=UseObjectOrCollectionInitializer/@EntryIndexedValue">HINT</s:String> - <s:Boolean x:Key="/Default/CodeInspection/TestFileAnalysis/SeachForOrphanedProjectFiles/@EntryValue">True</s:Boolean> - <s:String x:Key="/Default/CodeInspection/TestFileAnalysis/TestClassSuffix/@EntryValue">Fixture</s:String> - <s:String x:Key="/Default/CodeInspection/TestFileAnalysis/TestProjectToCodeProjectNameSpaceRegEx/@EntryValue">^(.*)\.Test$</s:String> - <s:String x:Key="/Default/CodeStyle/CodeCleanup/Profiles/=NzbDrone/@EntryIndexedValue"><?xml version="1.0" encoding="utf-16"?><Profile name="NzbDrone"><CSArrangeThisQualifier>True</CSArrangeThisQualifier><RemoveCodeRedundancies>True</RemoveCodeRedundancies><CSUseAutoProperty>True</CSUseAutoProperty><CSMakeFieldReadonly>True</CSMakeFieldReadonly><CSUseVar><BehavourStyle>CAN_CHANGE_TO_IMPLICIT</BehavourStyle><LocalVariableStyle>IMPLICIT_EXCEPT_SIMPLE_TYPES</LocalVariableStyle><ForeachVariableStyle>ALWAYS_IMPLICIT</ForeachVariableStyle></CSUseVar><CSUpdateFileHeader>True</CSUpdateFileHeader><CSOptimizeUsings><OptimizeUsings>True</OptimizeUsings><EmbraceInRegion>False</EmbraceInRegion><RegionName></RegionName></CSOptimizeUsings><CSShortenReferences>True</CSShortenReferences><CSReorderTypeMembers>True</CSReorderTypeMembers><XAMLCollapseEmptyTags>False</XAMLCollapseEmptyTags></Profile></s:String> - <s:String x:Key="/Default/CodeStyle/CodeCleanup/SilentCleanupProfile/@EntryValue">NzbDrone</s:String> - <s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/FORCE_CHOP_COMPOUND_WHILE_EXPRESSION/@EntryValue">True</s:Boolean> - <s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/FORCE_FIXED_BRACES_STYLE/@EntryValue">ALWAYS_ADD</s:String> - <s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/FORCE_FOR_BRACES_STYLE/@EntryValue">ALWAYS_ADD</s:String> - <s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/FORCE_FOREACH_BRACES_STYLE/@EntryValue">ALWAYS_ADD</s:String> - <s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/FORCE_IFELSE_BRACES_STYLE/@EntryValue">ALWAYS_ADD</s:String> - <s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/FORCE_USING_BRACES_STYLE/@EntryValue">ALWAYS_ADD</s:String> - <s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/FORCE_WHILE_BRACES_STYLE/@EntryValue">ALWAYS_ADD</s:String> - <s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/INDENT_NESTED_FIXED_STMT/@EntryValue">True</s:Boolean> - <s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/INDENT_NESTED_USINGS_STMT/@EntryValue">True</s:Boolean> - <s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/PLACE_FIELD_ATTRIBUTE_ON_SAME_LINE/@EntryValue">False</s:Boolean> - <s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/PLACE_SIMPLE_ACCESSOR_ATTRIBUTE_ON_SAME_LINE/@EntryValue">False</s:Boolean> - <s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/PLACE_SIMPLE_ACCESSOR_ON_SINGLE_LINE/@EntryValue">False</s:Boolean> - <s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/SIMPLE_EMBEDDED_STATEMENT_STYLE/@EntryValue">ON_SINGLE_LINE</s:String> - <s:Boolean x:Key="/Default/CodeStyle/CSharpUsing/AllowAlias/@EntryValue">False</s:Boolean> - <s:Boolean x:Key="/Default/CodeStyle/CSharpUsing/CanUseGlobalAlias/@EntryValue">False</s:Boolean> - <s:Boolean x:Key="/Default/CodeStyle/CSharpUsing/PreferQualifiedReference/@EntryValue">False</s:Boolean> - <s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/PredefinedNamingRules/=Constants/@EntryIndexedValue"><Policy Inspect="True" Prefix="" Suffix="" Style="AA_BB" /></s:String> - <s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/PredefinedNamingRules/=PrivateConstants/@EntryIndexedValue"><Policy Inspect="True" Prefix="" Suffix="" Style="AA_BB" /></s:String> - <s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/UserRules/=6658173a_002Dfe71_002D4efa_002D9d9e_002Da36d4499375e/@EntryIndexedValue"><Policy><Descriptor Staticness="Static, Instance" AccessRightKinds="Public" Description="Test Methods"><ElementKinds><Kind Name="TEST_MEMBER" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="should_" Suffix="" Style="aa_bb" /></Policy></s:String> - <s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/UserRules/=f5dc62ff_002De860_002D4dc4_002Dacef_002Dd674121c2124/@EntryIndexedValue"><Policy><Descriptor Staticness="Static, Instance" AccessRightKinds="Public" Description="Test Fixtures"><ElementKinds><Kind Name="TEST_TYPE" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="Fixture" Style="AaBb" /></Policy></s:String> - <s:String x:Key="/Default/CodeStyle/Naming/JavaScriptNaming/UserRules/=JS_005FPARAMETER/@EntryIndexedValue"><Policy Inspect="True" Prefix="" Suffix="" Style="aaBb"><ExtraRule Prefix="" Suffix="" Style="AaBb" /></Policy></s:String> - <s:String x:Key="/Default/Environment/Editor/MatchingBraceHighlighting/Position/@EntryValue">BOTH_SIDES</s:String> - <s:String x:Key="/Default/Environment/Editor/MatchingBraceHighlighting/Style/@EntryValue">COLOR</s:String> - <s:Boolean x:Key="/Default/Environment/ExternalSources/Decompiler/ReorderMembers/@EntryValue">True</s:Boolean> - <s:String x:Key="/Default/Environment/Hierarchy/PsiConfigurationSettingsKey/LocationType/@EntryValue">SOLUTION_FOLDER</s:String> - - - - <s:Boolean x:Key="/Default/Environment/InjectedLayers/FileInjectedLayer/=5C7F3FB135E52A44B9447C48B2EEEE92/@KeyIndexDefined">True</s:Boolean> - <s:String x:Key="/Default/Environment/InjectedLayers/FileInjectedLayer/=5C7F3FB135E52A44B9447C48B2EEEE92/AbsolutePath/@EntryValue">C:\Dropbox\Git\NzbDrone\NzbDrone.sln.DotSettings</s:String> - <s:Boolean x:Key="/Default/Environment/InjectedLayers/FileInjectedLayer/=EAB6F2886783AB41B46249432F57475A/@KeyIndexDefined">True</s:Boolean> - <s:String x:Key="/Default/Environment/InjectedLayers/FileInjectedLayer/=EAB6F2886783AB41B46249432F57475A/AbsolutePath/@EntryValue">C:\Dropbox\Git\NzbDrone\src\Microsoft.AspNet.SignalR.Core\Microsoft.AspNet.SignalR.Core.csproj.DotSettings</s:String> - <s:String x:Key="/Default/Environment/InjectedLayers/FileInjectedLayer/=EAB6F2886783AB41B46249432F57475A/RelativePath/@EntryValue">..\Microsoft.AspNet.SignalR.Core\Microsoft.AspNet.SignalR.Core.csproj.DotSettings</s:String> - - - - - - <s:Boolean x:Key="/Default/Environment/InjectedLayers/InjectedLayerCustomization/=File5C7F3FB135E52A44B9447C48B2EEEE92/@KeyIndexDefined">True</s:Boolean> - <s:Boolean x:Key="/Default/Environment/InjectedLayers/InjectedLayerCustomization/=File5C7F3FB135E52A44B9447C48B2EEEE92/IsOn/@EntryValue">False</s:Boolean> - <s:Double x:Key="/Default/Environment/InjectedLayers/InjectedLayerCustomization/=File5C7F3FB135E52A44B9447C48B2EEEE92/RelativePriority/@EntryValue">1</s:Double> - <s:Boolean x:Key="/Default/Environment/InjectedLayers/InjectedLayerCustomization/=FileEAB6F2886783AB41B46249432F57475A/@KeyIndexDefined">True</s:Boolean> - <s:Boolean x:Key="/Default/Environment/InjectedLayers/InjectedLayerCustomization/=FileEAB6F2886783AB41B46249432F57475A/IsOn/@EntryValue">False</s:Boolean> - <s:Double x:Key="/Default/Environment/InjectedLayers/InjectedLayerCustomization/=FileEAB6F2886783AB41B46249432F57475A/RelativePriority/@EntryValue">3</s:Double> - - - - <s:Boolean x:Key="/Default/Environment/InjectedLayers/InjectedLayerCustomization/=File_003A_003AC_003A_005CDropbox_005CGit_005CNzbDrone_005CNzbDrone_002Esln_002EDotSettings/@KeyIndexDefined">True</s:Boolean> - <s:Double x:Key="/Default/Environment/InjectedLayers/InjectedLayerCustomization/=File_003A_003AC_003A_005CDropbox_005CGit_005CNzbDrone_005CNzbDrone_002Esln_002EDotSettings/RelativePriority/@EntryValue">2</s:Double> - <s:Boolean x:Key="/Default/Environment/MemoryUsageIndicator/IsVisible/@EntryValue">False</s:Boolean> - <s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002EAddAccessorOwnerDeclarationBracesMigration/@EntryIndexedValue">True</s:Boolean> - <s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002EMigrateBlankLinesAroundFieldToBlankLinesAroundProperty/@EntryIndexedValue">True</s:Boolean> - <s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002EMigrateThisQualifierSettings/@EntryIndexedValue">True</s:Boolean> - <s:Boolean x:Key="/Default/Environment/TextControl/HighlightCurrentLine/@EntryValue">True</s:Boolean> - <s:Boolean x:Key="/Default/Environment/UnitTesting/DisabledProviders/=Jasmine/@EntryIndexedValue">True</s:Boolean> - <s:Boolean x:Key="/Default/Environment/UnitTesting/DisabledProviders/=MSTest/@EntryIndexedValue">True</s:Boolean> - <s:Boolean x:Key="/Default/Environment/UnitTesting/DisabledProviders/=QUnit/@EntryIndexedValue">True</s:Boolean> - <s:Int64 x:Key="/Default/Environment/UnitTesting/ParallelProcessesCount/@EntryValue">4</s:Int64> - <s:Boolean x:Key="/Default/Environment/UnitTesting/SeparateAppDomainPerAssembly/@EntryValue">True</s:Boolean> - <s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/QuickList/=26E712D4B91E2E49A0E92C0AFE6FF57E/Entry/=38860059D7978D4DAF1997C7CBC46A78/EntryName/@EntryValue">Backbone model</s:String> - <s:Int64 x:Key="/Default/PatternsAndTemplates/LiveTemplates/QuickList/=26E712D4B91E2E49A0E92C0AFE6FF57E/Entry/=38860059D7978D4DAF1997C7CBC46A78/Position/@EntryValue">5</s:Int64> - <s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=09B531154963914B9AB9E2A05E1F2B44/@KeyIndexDefined">True</s:Boolean> - <s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=09B531154963914B9AB9E2A05E1F2B44/Applicability/=Live/@EntryIndexedValue">True</s:Boolean> - <s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=09B531154963914B9AB9E2A05E1F2B44/Description/@EntryValue">Nunit Test</s:String> - <s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=09B531154963914B9AB9E2A05E1F2B44/Field/=testname/@KeyIndexDefined">True</s:Boolean> - <s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=09B531154963914B9AB9E2A05E1F2B44/Field/=testname/Expression/@EntryValue">spacestounderstrokes(testname)</s:String> - <s:Int64 x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=09B531154963914B9AB9E2A05E1F2B44/Field/=testname/Order/@EntryValue">0</s:Int64> - <s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=09B531154963914B9AB9E2A05E1F2B44/Reformat/@EntryValue">True</s:Boolean> - <s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=09B531154963914B9AB9E2A05E1F2B44/Scope/=CE6825B6B50BCB44A4991BEC7FBA3363/@KeyIndexDefined">True</s:Boolean> - <s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=09B531154963914B9AB9E2A05E1F2B44/Scope/=CE6825B6B50BCB44A4991BEC7FBA3363/CustomProperties/=minimumLanguageVersion/@EntryIndexedValue">4.0</s:String> - <s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=09B531154963914B9AB9E2A05E1F2B44/Scope/=CE6825B6B50BCB44A4991BEC7FBA3363/Type/@EntryValue">InCSharpQuery</s:String> - <s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=09B531154963914B9AB9E2A05E1F2B44/Shortcut/@EntryValue">test</s:String> - <s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=09B531154963914B9AB9E2A05E1F2B44/ShortenQualifiedReferences/@EntryValue">True</s:Boolean> - <s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=09B531154963914B9AB9E2A05E1F2B44/Text/@EntryValue">[Test] -public void $testname$() -{ - - -}</s:String> - <s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=4BC8C4E3B166924493A3914451281686/@KeyIndexDefined">True</s:Boolean> - <s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=4BC8C4E3B166924493A3914451281686/Applicability/=Live/@EntryIndexedValue">True</s:Boolean> - <s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=4BC8C4E3B166924493A3914451281686/Description/@EntryValue">Nunit Setup</s:String> - <s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=4BC8C4E3B166924493A3914451281686/Reformat/@EntryValue">True</s:Boolean> - <s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=4BC8C4E3B166924493A3914451281686/Scope/=C3001E7C0DA78E4487072B7E050D86C5/@KeyIndexDefined">True</s:Boolean> - <s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=4BC8C4E3B166924493A3914451281686/Scope/=C3001E7C0DA78E4487072B7E050D86C5/CustomProperties/=minimumLanguageVersion/@EntryIndexedValue">2.0</s:String> - <s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=4BC8C4E3B166924493A3914451281686/Scope/=C3001E7C0DA78E4487072B7E050D86C5/Type/@EntryValue">InCSharpFile</s:String> - <s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=4BC8C4E3B166924493A3914451281686/Shortcut/@EntryValue">Setup</s:String> - <s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=4BC8C4E3B166924493A3914451281686/ShortenQualifiedReferences/@EntryValue">True</s:Boolean> - <s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=4BC8C4E3B166924493A3914451281686/Text/@EntryValue"> [SetUp] - public void Setup() - { - - }</s:String> - <s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=4F6A0CA32FE60746A73308F7E39A63C1/@KeyIndexDefined">True</s:Boolean> - <s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=4F6A0CA32FE60746A73308F7E39A63C1/Applicability/=File/@EntryIndexedValue">True</s:Boolean> - <s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=4F6A0CA32FE60746A73308F7E39A63C1/CustomProperties/=Extension/@EntryIndexedValue">js</s:String> - <s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=4F6A0CA32FE60746A73308F7E39A63C1/CustomProperties/=FileName/@EntryIndexedValue">Model</s:String> - <s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=4F6A0CA32FE60746A73308F7E39A63C1/CustomProperties/=ValidateFileName/@EntryIndexedValue">True</s:String> - <s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=4F6A0CA32FE60746A73308F7E39A63C1/Description/@EntryValue">Backbone Model</s:String> - <s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=4F6A0CA32FE60746A73308F7E39A63C1/Field/=ModelName/@KeyIndexDefined">True</s:Boolean> - <s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=4F6A0CA32FE60746A73308F7E39A63C1/Field/=ModelName/Expression/@EntryValue">getFileNameWithoutExtension()</s:String> - <s:Int64 x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=4F6A0CA32FE60746A73308F7E39A63C1/Field/=ModelName/Order/@EntryValue">0</s:Int64> - <s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=4F6A0CA32FE60746A73308F7E39A63C1/Field/=resource/@KeyIndexDefined">True</s:Boolean> - <s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=4F6A0CA32FE60746A73308F7E39A63C1/Field/=resource/Expression/@EntryValue">getFileNameWithoutExtension()</s:String> - <s:Int64 x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=4F6A0CA32FE60746A73308F7E39A63C1/Field/=resource/InitialRange/@EntryValue">-1</s:Int64> - <s:Int64 x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=4F6A0CA32FE60746A73308F7E39A63C1/Field/=resource/Order/@EntryValue">1</s:Int64> - <s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=4F6A0CA32FE60746A73308F7E39A63C1/Reformat/@EntryValue">True</s:Boolean> - <s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=4F6A0CA32FE60746A73308F7E39A63C1/Scope/=0A12C11AC0ACCA4E921B6B2CEE180C57/@KeyIndexDefined">True</s:Boolean> - <s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=4F6A0CA32FE60746A73308F7E39A63C1/Scope/=0A12C11AC0ACCA4E921B6B2CEE180C57/Type/@EntryValue">InAnyWebProject</s:String> - <s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=4F6A0CA32FE60746A73308F7E39A63C1/ShortenQualifiedReferences/@EntryValue">True</s:Boolean> - <s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=4F6A0CA32FE60746A73308F7E39A63C1/Text/@EntryValue">NzbDrone.$ModelName$Model = Backbone.Model.extend({ - -}); - - -$ModelName$Collection = Backbone.Collection.extend({ - - model: NzbDrone.$ModelName$Model, - url: NzbDrone.Constants.ApiRoot + '/$resource$', - -}); -</s:String> - <s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=5FF6ECA7884F0F45BAB165819AF9DA75/@KeyIndexDefined">True</s:Boolean> - <s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=5FF6ECA7884F0F45BAB165819AF9DA75/Applicability/=Live/@EntryIndexedValue">True</s:Boolean> - <s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=5FF6ECA7884F0F45BAB165819AF9DA75/Description/@EntryValue">Create a new Method</s:String> - <s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=5FF6ECA7884F0F45BAB165819AF9DA75/Field/=name/@KeyIndexDefined">True</s:Boolean> - <s:Int64 x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=5FF6ECA7884F0F45BAB165819AF9DA75/Field/=name/Order/@EntryValue">0</s:Int64> - <s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=5FF6ECA7884F0F45BAB165819AF9DA75/Reformat/@EntryValue">True</s:Boolean> - <s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=5FF6ECA7884F0F45BAB165819AF9DA75/Scope/=FFA15E6CFCBE90499C572A859225B012/@KeyIndexDefined">True</s:Boolean> - <s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=5FF6ECA7884F0F45BAB165819AF9DA75/Scope/=FFA15E6CFCBE90499C572A859225B012/Type/@EntryValue">InJavaScriptFile</s:String> - <s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=5FF6ECA7884F0F45BAB165819AF9DA75/Shortcut/@EntryValue">func</s:String> - <s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=5FF6ECA7884F0F45BAB165819AF9DA75/ShortenQualifiedReferences/@EntryValue">True</s:Boolean> - <s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=5FF6ECA7884F0F45BAB165819AF9DA75/Text/@EntryValue"> $name$: function () { - - },</s:String> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - <s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=686B0D0C738CD1449F9389FEB5A34944/@KeyIndexDefined">True</s:Boolean> - <s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=686B0D0C738CD1449F9389FEB5A34944/Applicability/=Live/@EntryIndexedValue">True</s:Boolean> - <s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=686B0D0C738CD1449F9389FEB5A34944/Description/@EntryValue">Backbone Model</s:String> - <s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=686B0D0C738CD1449F9389FEB5A34944/Field/=ModelName/@KeyIndexDefined">True</s:Boolean> - <s:Int64 x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=686B0D0C738CD1449F9389FEB5A34944/Field/=ModelName/Order/@EntryValue">0</s:Int64> - <s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=686B0D0C738CD1449F9389FEB5A34944/Reformat/@EntryValue">True</s:Boolean> - <s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=686B0D0C738CD1449F9389FEB5A34944/Scope/=FFA15E6CFCBE90499C572A859225B012/@KeyIndexDefined">True</s:Boolean> - <s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=686B0D0C738CD1449F9389FEB5A34944/Scope/=FFA15E6CFCBE90499C572A859225B012/Type/@EntryValue">InJavaScriptFile</s:String> - <s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=686B0D0C738CD1449F9389FEB5A34944/Shortcut/@EntryValue">model</s:String> - <s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=686B0D0C738CD1449F9389FEB5A34944/ShortenQualifiedReferences/@EntryValue">True</s:Boolean> - <s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=686B0D0C738CD1449F9389FEB5A34944/Text/@EntryValue">$ModelName$ = Backbone.M</s:String> - <s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=8EC91D4AC875274D9804299C81802FB3/@KeyIndexDefined">True</s:Boolean> - <s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=8EC91D4AC875274D9804299C81802FB3/Applicability/=Live/@EntryIndexedValue">True</s:Boolean> - <s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=8EC91D4AC875274D9804299C81802FB3/Description/@EntryValue">Subscribe to event</s:String> - <s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=8EC91D4AC875274D9804299C81802FB3/Field/=Event/@KeyIndexDefined">True</s:Boolean> - <s:Int64 x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=8EC91D4AC875274D9804299C81802FB3/Field/=Event/Order/@EntryValue">1</s:Int64> - <s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=8EC91D4AC875274D9804299C81802FB3/Field/=Handler/@KeyIndexDefined">True</s:Boolean> - <s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=8EC91D4AC875274D9804299C81802FB3/Field/=Handler/Expression/@EntryValue">typeMember()</s:String> - <s:Int64 x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=8EC91D4AC875274D9804299C81802FB3/Field/=Handler/Order/@EntryValue">2</s:Int64> - <s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=8EC91D4AC875274D9804299C81802FB3/Field/=Target/@KeyIndexDefined">True</s:Boolean> - <s:Int64 x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=8EC91D4AC875274D9804299C81802FB3/Field/=Target/Order/@EntryValue">0</s:Int64> - <s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=8EC91D4AC875274D9804299C81802FB3/Reformat/@EntryValue">True</s:Boolean> - <s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=8EC91D4AC875274D9804299C81802FB3/Scope/=FFA15E6CFCBE90499C572A859225B012/@KeyIndexDefined">True</s:Boolean> - <s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=8EC91D4AC875274D9804299C81802FB3/Scope/=FFA15E6CFCBE90499C572A859225B012/Type/@EntryValue">InJavaScriptFile</s:String> - <s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=8EC91D4AC875274D9804299C81802FB3/Shortcut/@EntryValue">vent</s:String> - <s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=8EC91D4AC875274D9804299C81802FB3/ShortenQualifiedReferences/@EntryValue">True</s:Boolean> - <s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=8EC91D4AC875274D9804299C81802FB3/Text/@EntryValue">NzbDrone.vent.listenTo($Target$, '$Event$', this.$Handler$, this); -</s:String> - <s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=CB2236C947CEAB4B90BDEB514C88F7B9/@KeyIndexDefined">True</s:Boolean> - <s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=CB2236C947CEAB4B90BDEB514C88F7B9/Applicability/=Live/@EntryIndexedValue">True</s:Boolean> - <s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=CB2236C947CEAB4B90BDEB514C88F7B9/Field/=event/@KeyIndexDefined">True</s:Boolean> - <s:Int64 x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=CB2236C947CEAB4B90BDEB514C88F7B9/Field/=event/Order/@EntryValue">0</s:Int64> - <s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=CB2236C947CEAB4B90BDEB514C88F7B9/Field/=handler/@KeyIndexDefined">True</s:Boolean> - <s:Int64 x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=CB2236C947CEAB4B90BDEB514C88F7B9/Field/=handler/Order/@EntryValue">2</s:Int64> - <s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=CB2236C947CEAB4B90BDEB514C88F7B9/Field/=selector/@KeyIndexDefined">True</s:Boolean> - <s:Int64 x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=CB2236C947CEAB4B90BDEB514C88F7B9/Field/=selector/Order/@EntryValue">1</s:Int64> - <s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=CB2236C947CEAB4B90BDEB514C88F7B9/Reformat/@EntryValue">True</s:Boolean> - <s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=CB2236C947CEAB4B90BDEB514C88F7B9/Scope/=FFA15E6CFCBE90499C572A859225B012/@KeyIndexDefined">True</s:Boolean> - <s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=CB2236C947CEAB4B90BDEB514C88F7B9/Scope/=FFA15E6CFCBE90499C572A859225B012/Type/@EntryValue">InJavaScriptFile</s:String> - <s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=CB2236C947CEAB4B90BDEB514C88F7B9/Shortcut/@EntryValue">events</s:String> - <s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=CB2236C947CEAB4B90BDEB514C88F7B9/ShortenQualifiedReferences/@EntryValue">True</s:Boolean> - <s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=CB2236C947CEAB4B90BDEB514C88F7B9/Text/@EntryValue"> events: { - '$event$ .x-$selector$': '$handler$' - },</s:String> - <s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=D02CEEFCB5BA1E4C8660DD8D7D09D183/@KeyIndexDefined">True</s:Boolean> - <s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=D02CEEFCB5BA1E4C8660DD8D7D09D183/Applicability/=Live/@EntryIndexedValue">True</s:Boolean> - <s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=D02CEEFCB5BA1E4C8660DD8D7D09D183/Description/@EntryValue">Add Initialize Method</s:String> - <s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=D02CEEFCB5BA1E4C8660DD8D7D09D183/Reformat/@EntryValue">True</s:Boolean> - <s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=D02CEEFCB5BA1E4C8660DD8D7D09D183/Scope/=FFA15E6CFCBE90499C572A859225B012/@KeyIndexDefined">True</s:Boolean> - <s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=D02CEEFCB5BA1E4C8660DD8D7D09D183/Scope/=FFA15E6CFCBE90499C572A859225B012/Type/@EntryValue">InJavaScriptFile</s:String> - <s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=D02CEEFCB5BA1E4C8660DD8D7D09D183/Shortcut/@EntryValue">init</s:String> - <s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=D02CEEFCB5BA1E4C8660DD8D7D09D183/ShortenQualifiedReferences/@EntryValue">True</s:Boolean> - <s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=D02CEEFCB5BA1E4C8660DD8D7D09D183/Text/@EntryValue"> initialize: function () { - - },</s:String> - <s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=DFB2B8E186019749B105D91A01D4D269/@KeyIndexDefined">True</s:Boolean> - <s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=DFB2B8E186019749B105D91A01D4D269/Applicability/=File/@EntryIndexedValue">True</s:Boolean> - <s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=DFB2B8E186019749B105D91A01D4D269/CustomProperties/=Extension/@EntryIndexedValue">cs</s:String> - <s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=DFB2B8E186019749B105D91A01D4D269/CustomProperties/=FileName/@EntryIndexedValue">$ServiceName$Fixture</s:String> - <s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=DFB2B8E186019749B105D91A01D4D269/CustomProperties/=ValidateFileName/@EntryIndexedValue">True</s:String> - <s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=DFB2B8E186019749B105D91A01D4D269/Description/@EntryValue">TestFixture</s:String> - <s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=DFB2B8E186019749B105D91A01D4D269/Field/=ServiceName/@KeyIndexDefined">True</s:Boolean> - <s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=DFB2B8E186019749B105D91A01D4D269/Field/=ServiceName/Expression/@EntryValue">completeType()</s:String> - <s:Int64 x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=DFB2B8E186019749B105D91A01D4D269/Field/=ServiceName/Order/@EntryValue">0</s:Int64> - <s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=DFB2B8E186019749B105D91A01D4D269/Field/=TestBase/@KeyIndexDefined">True</s:Boolean> - <s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=DFB2B8E186019749B105D91A01D4D269/Field/=TestBase/Expression/@EntryValue">list("CoreTest, ObjectDbTest")</s:String> - <s:Int64 x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=DFB2B8E186019749B105D91A01D4D269/Field/=TestBase/Order/@EntryValue">1</s:Int64> - <s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=DFB2B8E186019749B105D91A01D4D269/Reformat/@EntryValue">True</s:Boolean> - <s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=DFB2B8E186019749B105D91A01D4D269/Scope/=E8F0594528C33E45BBFEC6CFE851095D/@KeyIndexDefined">True</s:Boolean> - <s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=DFB2B8E186019749B105D91A01D4D269/Scope/=E8F0594528C33E45BBFEC6CFE851095D/Type/@EntryValue">InCSharpProjectFile</s:String> - <s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=DFB2B8E186019749B105D91A01D4D269/ShortenQualifiedReferences/@EntryValue">True</s:Boolean> - <s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=DFB2B8E186019749B105D91A01D4D269/Text/@EntryValue">using System; -using System.Collections.Generic; -using System.Linq; -using FizzWare.NBuilder; -using FluentAssertions; -using NUnit.Framework; -using NzbDrone.Core.Datastore; -using NzbDrone.Core.Repository; -using NzbDrone.Core.Test.Framework; - -namespace NzbDrone.Core.Test.Datastore -{ - [TestFixture] - public class $ServiceName$Fixture: $TestBase$<$ServiceName$> - { - - } -}</s:String> - - - - - - - - - - - </wpf:ResourceDictionary> \ No newline at end of file diff --git a/src/NzbDrone/NzbDrone.csproj b/src/NzbDrone/NzbDrone.csproj index f2dedfe32..40be937b3 100644 --- a/src/NzbDrone/NzbDrone.csproj +++ b/src/NzbDrone/NzbDrone.csproj @@ -1,5 +1,5 @@ <?xml version="1.0" encoding="utf-8"?> -<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> +<Project ToolsVersion="12.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> <PropertyGroup> <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration> <Platform Condition=" '$(Platform)' == '' ">x86</Platform> @@ -10,7 +10,7 @@ <AppDesignerFolder>Properties</AppDesignerFolder> <RootNamespace>NzbDrone</RootNamespace> <AssemblyName>Lidarr</AssemblyName> - <TargetFrameworkVersion>v4.0</TargetFrameworkVersion> + <TargetFrameworkVersion>v4.6.1</TargetFrameworkVersion> <FileAlignment>512</FileAlignment> <TargetFrameworkProfile> </TargetFrameworkProfile> @@ -43,6 +43,7 @@ <WarningLevel>4</WarningLevel> <UseVSHostingProcess>true</UseVSHostingProcess> <CodeAnalysisRuleSet>BasicCorrectnessRules.ruleset</CodeAnalysisRuleSet> + <Prefer32Bit>false</Prefer32Bit> </PropertyGroup> <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|x86' "> <PlatformTarget>x86</PlatformTarget> @@ -52,6 +53,7 @@ <DefineConstants>TRACE</DefineConstants> <ErrorReport>prompt</ErrorReport> <WarningLevel>4</WarningLevel> + <Prefer32Bit>false</Prefer32Bit> </PropertyGroup> <PropertyGroup> <ApplicationIcon>..\NzbDrone.Host\NzbDrone.ico</ApplicationIcon> @@ -66,28 +68,25 @@ <ApplicationManifest>app.manifest</ApplicationManifest> </PropertyGroup> <ItemGroup> - <Reference Include="Microsoft.Owin, Version=2.1.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL"> - <SpecificVersion>False</SpecificVersion> - <HintPath>..\packages\Microsoft.Owin.2.1.0\lib\net40\Microsoft.Owin.dll</HintPath> + <Reference Include="Microsoft.Owin, Version=3.1.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL"> + <HintPath>..\packages\Microsoft.Owin.3.1.0\lib\net45\Microsoft.Owin.dll</HintPath> </Reference> - <Reference Include="Microsoft.Owin.Hosting, Version=1.1.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL"> - <SpecificVersion>False</SpecificVersion> - <HintPath>..\packages\Microsoft.Owin.Hosting.2.1.0\lib\net40\Microsoft.Owin.Hosting.dll</HintPath> + <Reference Include="Microsoft.Owin.Hosting, Version=3.1.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL"> + <HintPath>..\packages\Microsoft.Owin.Hosting.3.1.0\lib\net45\Microsoft.Owin.Hosting.dll</HintPath> </Reference> <Reference Include="Newtonsoft.Json, Version=9.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL"> - <HintPath>..\packages\Newtonsoft.Json.9.0.1\lib\net40\Newtonsoft.Json.dll</HintPath> - <Private>True</Private> + <HintPath>..\packages\Newtonsoft.Json.9.0.1\lib\net45\Newtonsoft.Json.dll</HintPath> </Reference> <Reference Include="NLog, Version=4.0.0.0, Culture=neutral, PublicKeyToken=5120e14c03d0593c, processorArchitecture=MSIL"> - <HintPath>..\packages\NLog.4.4.3\lib\net40\NLog.dll</HintPath> + <HintPath>..\packages\NLog.4.4.12\lib\net45\NLog.dll</HintPath> + </Reference> + <Reference Include="Owin, Version=1.0.0.0, Culture=neutral, PublicKeyToken=f0ebd12fd5e55cc5, processorArchitecture=MSIL"> + <HintPath>..\packages\Owin.1.0\lib\net40\Owin.dll</HintPath> </Reference> <Reference Include="System" /> <Reference Include="System.Core" /> <Reference Include="System.Drawing" /> <Reference Include="System.Windows.Forms" /> - <Reference Include="Owin"> - <HintPath>..\packages\Owin.1.0\lib\net40\Owin.dll</HintPath> - </Reference> </ItemGroup> <ItemGroup> <Compile Include="..\NzbDrone.Common\Properties\SharedAssemblyInfo.cs"> @@ -128,14 +127,6 @@ </BootstrapperPackage> </ItemGroup> <ItemGroup> - <ProjectReference Include="..\Microsoft.AspNet.SignalR.Core\Microsoft.AspNet.SignalR.Core.csproj"> - <Project>{1B9A82C4-BCA1-4834-A33E-226F17BE070B}</Project> - <Name>Microsoft.AspNet.SignalR.Core</Name> - </ProjectReference> - <ProjectReference Include="..\Microsoft.AspNet.SignalR.Owin\Microsoft.AspNet.SignalR.Owin.csproj"> - <Project>{2B8C6DAD-4D85-41B1-83FD-248D9F347522}</Project> - <Name>Microsoft.AspNet.SignalR.Owin</Name> - </ProjectReference> <ProjectReference Include="..\NzbDrone.Common\NzbDrone.Common.csproj"> <Project>{F2BE0FDF-6E47-4827-A420-DD4EF82407F8}</Project> <Name>NzbDrone.Common</Name> diff --git a/src/NzbDrone/Properties/AssemblyInfo.cs b/src/NzbDrone/Properties/AssemblyInfo.cs index 4385d79f2..132121d17 100644 --- a/src/NzbDrone/Properties/AssemblyInfo.cs +++ b/src/NzbDrone/Properties/AssemblyInfo.cs @@ -8,4 +8,3 @@ using System.Runtime.InteropServices; [assembly: AssemblyTitle("Lidarr.exe")] [assembly: Guid("67AADCD9-89AA-4D95-8281-3193740E70E5")] -[assembly: AssemblyVersion("10.0.0.*")] diff --git a/src/NzbDrone/Properties/Resources.Designer.cs b/src/NzbDrone/Properties/Resources.Designer.cs index 65584111d..dfbfeb594 100644 --- a/src/NzbDrone/Properties/Resources.Designer.cs +++ b/src/NzbDrone/Properties/Resources.Designer.cs @@ -1,7 +1,7 @@ //------------------------------------------------------------------------------ // <auto-generated> // This code was generated by a tool. -// Runtime Version:4.0.30319.32559 +// Runtime Version:4.0.30319.42000 // // Changes to this file may cause incorrect behavior and will be lost if // the code is regenerated. @@ -9,6 +9,9 @@ //------------------------------------------------------------------------------ namespace NzbDrone.Properties { + using System; + + /// <summary> /// A strongly-typed resource class, for looking up localized strings, etc. /// </summary> diff --git a/src/NzbDrone/packages.config b/src/NzbDrone/packages.config index 11b77285e..8c5ab74a0 100644 --- a/src/NzbDrone/packages.config +++ b/src/NzbDrone/packages.config @@ -1,8 +1,8 @@ <?xml version="1.0" encoding="utf-8"?> <packages> - <package id="Microsoft.Owin" version="2.1.0" targetFramework="net40" /> - <package id="Microsoft.Owin.Hosting" version="2.1.0" targetFramework="net40" /> - <package id="Newtonsoft.Json" version="9.0.1" targetFramework="net40" /> - <package id="NLog" version="4.4.3" targetFramework="net40" /> - <package id="Owin" version="1.0" targetFramework="net40" /> + <package id="Microsoft.Owin" version="3.1.0" targetFramework="net461" /> + <package id="Microsoft.Owin.Hosting" version="3.1.0" targetFramework="net461" /> + <package id="Newtonsoft.Json" version="9.0.1" targetFramework="net461" /> + <package id="NLog" version="4.4.12" targetFramework="net461" /> + <package id="Owin" version="1.0" targetFramework="net461" /> </packages> \ No newline at end of file diff --git a/src/ServiceHelpers/ServiceInstall/Properties/AssemblyInfo.cs b/src/ServiceHelpers/ServiceInstall/Properties/AssemblyInfo.cs index 63a2e4bc0..29ef7e071 100644 --- a/src/ServiceHelpers/ServiceInstall/Properties/AssemblyInfo.cs +++ b/src/ServiceHelpers/ServiceInstall/Properties/AssemblyInfo.cs @@ -7,4 +7,3 @@ using System.Runtime.InteropServices; // The following GUID is for the ID of the typelib if this project is exposed to COM [assembly: Guid("13976baa-e5ba-42b2-8ad7-8d568b68a53b")] -[assembly: AssemblyVersion("10.0.0.*")] diff --git a/src/ServiceHelpers/ServiceInstall/ServiceHelper.cs b/src/ServiceHelpers/ServiceInstall/ServiceHelper.cs index 77aff8ee2..265b86c43 100644 --- a/src/ServiceHelpers/ServiceInstall/ServiceHelper.cs +++ b/src/ServiceHelpers/ServiceInstall/ServiceHelper.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Diagnostics; using System.IO; using System.Reflection; @@ -8,7 +8,7 @@ namespace ServiceInstall { public static class ServiceHelper { - private static string NzbDroneExe => Path.Combine(new FileInfo(Assembly.GetExecutingAssembly().Location).Directory.FullName, "Lidarr.Console.exe"); + private static string LidarrExe => Path.Combine(new FileInfo(Assembly.GetExecutingAssembly().Location).Directory.FullName, "Lidarr.Console.exe"); private static bool IsAnAdministrator() { @@ -18,7 +18,7 @@ namespace ServiceInstall public static void Run(string arg) { - if (!File.Exists(NzbDroneExe)) + if (!File.Exists(LidarrExe)) { Console.WriteLine("Unable to find Lidarr.Console.exe in the current directory."); return; @@ -32,7 +32,7 @@ namespace ServiceInstall var startInfo = new ProcessStartInfo { - FileName = NzbDroneExe, + FileName = LidarrExe, Arguments = arg, UseShellExecute = false, RedirectStandardOutput = true, @@ -50,13 +50,11 @@ namespace ServiceInstall process.BeginOutputReadLine(); process.WaitForExit(); - } private static void OnDataReceived(object sender, DataReceivedEventArgs e) { Console.WriteLine(e.Data); } - } -} \ No newline at end of file +} diff --git a/src/ServiceHelpers/ServiceInstall/ServiceInstall.csproj b/src/ServiceHelpers/ServiceInstall/ServiceInstall.csproj index 5334a144e..64a646a01 100644 --- a/src/ServiceHelpers/ServiceInstall/ServiceInstall.csproj +++ b/src/ServiceHelpers/ServiceInstall/ServiceInstall.csproj @@ -1,5 +1,5 @@ -<?xml version="1.0" encoding="utf-8"?> -<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> +<?xml version="1.0" encoding="utf-8"?> +<Project ToolsVersion="12.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> <PropertyGroup> <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration> <Platform Condition=" '$(Platform)' == '' ">x86</Platform> @@ -10,7 +10,7 @@ <AppDesignerFolder>Properties</AppDesignerFolder> <RootNamespace>ServiceInstall</RootNamespace> <AssemblyName>ServiceInstall</AssemblyName> - <TargetFrameworkVersion>v4.0</TargetFrameworkVersion> + <TargetFrameworkVersion>v4.6.1</TargetFrameworkVersion> <TargetFrameworkProfile> </TargetFrameworkProfile> <FileAlignment>512</FileAlignment> @@ -26,6 +26,7 @@ <DefineConstants>DEBUG;TRACE</DefineConstants> <ErrorReport>prompt</ErrorReport> <WarningLevel>4</WarningLevel> + <Prefer32Bit>false</Prefer32Bit> </PropertyGroup> <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|x86' "> <PlatformTarget>x86</PlatformTarget> @@ -35,6 +36,7 @@ <DefineConstants>TRACE</DefineConstants> <ErrorReport>prompt</ErrorReport> <WarningLevel>4</WarningLevel> + <Prefer32Bit>false</Prefer32Bit> </PropertyGroup> <PropertyGroup> <ApplicationManifest>app.manifest</ApplicationManifest> @@ -77,4 +79,4 @@ <Target Name="AfterBuild"> </Target> --> -</Project> +</Project> \ No newline at end of file diff --git a/src/ServiceHelpers/ServiceInstall/app.config b/src/ServiceHelpers/ServiceInstall/app.config index e33d6f761..99216b8f8 100644 --- a/src/ServiceHelpers/ServiceInstall/app.config +++ b/src/ServiceHelpers/ServiceInstall/app.config @@ -1,6 +1,6 @@ <?xml version="1.0"?> <configuration> <startup useLegacyV2RuntimeActivationPolicy="true"> - <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.0"/> + <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.6.1"/> </startup> </configuration> diff --git a/src/ServiceHelpers/ServiceUninstall/Properties/AssemblyInfo.cs b/src/ServiceHelpers/ServiceUninstall/Properties/AssemblyInfo.cs index c5e087a13..2a6d7f070 100644 --- a/src/ServiceHelpers/ServiceUninstall/Properties/AssemblyInfo.cs +++ b/src/ServiceHelpers/ServiceUninstall/Properties/AssemblyInfo.cs @@ -4,4 +4,3 @@ using System.Runtime.InteropServices; [assembly: AssemblyTitle("UninstallService")] [assembly: Guid("0a964b21-9de9-40b3-9378-0474fd5f21a8")] -[assembly: AssemblyVersion("10.0.0.*")] diff --git a/src/ServiceHelpers/ServiceUninstall/ServiceHelper.cs b/src/ServiceHelpers/ServiceUninstall/ServiceHelper.cs index 531674dc4..9ad0944ee 100644 --- a/src/ServiceHelpers/ServiceUninstall/ServiceHelper.cs +++ b/src/ServiceHelpers/ServiceUninstall/ServiceHelper.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Diagnostics; using System.IO; using System.Reflection; @@ -8,7 +8,7 @@ namespace ServiceUninstall { public static class ServiceHelper { - private static string NzbDroneExe => Path.Combine(new FileInfo(Assembly.GetExecutingAssembly().Location).Directory.FullName, "Lidarr.Console.exe"); + private static string LidarrExe => Path.Combine(new FileInfo(Assembly.GetExecutingAssembly().Location).Directory.FullName, "Lidarr.Console.exe"); private static bool IsAnAdministrator() { @@ -18,7 +18,7 @@ namespace ServiceUninstall public static void Run(string arg) { - if (!File.Exists(NzbDroneExe)) + if (!File.Exists(LidarrExe)) { Console.WriteLine("Unable to find Lidarr.exe in the current directory."); return; @@ -32,7 +32,7 @@ namespace ServiceUninstall var startInfo = new ProcessStartInfo { - FileName = NzbDroneExe, + FileName = LidarrExe, Arguments = arg, UseShellExecute = false, RedirectStandardOutput = true, @@ -50,13 +50,11 @@ namespace ServiceUninstall process.BeginOutputReadLine(); process.WaitForExit(); - } private static void OnDataReceived(object sender, DataReceivedEventArgs e) { Console.WriteLine(e.Data); } - } -} \ No newline at end of file +} diff --git a/src/ServiceHelpers/ServiceUninstall/ServiceUninstall.csproj b/src/ServiceHelpers/ServiceUninstall/ServiceUninstall.csproj index 5905c1dbd..cf036844f 100644 --- a/src/ServiceHelpers/ServiceUninstall/ServiceUninstall.csproj +++ b/src/ServiceHelpers/ServiceUninstall/ServiceUninstall.csproj @@ -1,5 +1,5 @@ -<?xml version="1.0" encoding="utf-8"?> -<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> +<?xml version="1.0" encoding="utf-8"?> +<Project ToolsVersion="12.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> <PropertyGroup> <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration> <Platform Condition=" '$(Platform)' == '' ">x86</Platform> @@ -10,7 +10,7 @@ <AppDesignerFolder>Properties</AppDesignerFolder> <RootNamespace>ServiceUninstall</RootNamespace> <AssemblyName>ServiceUninstall</AssemblyName> - <TargetFrameworkVersion>v4.0</TargetFrameworkVersion> + <TargetFrameworkVersion>v4.6.1</TargetFrameworkVersion> <TargetFrameworkProfile> </TargetFrameworkProfile> <FileAlignment>512</FileAlignment> @@ -26,6 +26,7 @@ <DefineConstants>DEBUG;TRACE</DefineConstants> <ErrorReport>prompt</ErrorReport> <WarningLevel>4</WarningLevel> + <Prefer32Bit>false</Prefer32Bit> </PropertyGroup> <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|x86' "> <PlatformTarget>x86</PlatformTarget> @@ -35,6 +36,7 @@ <DefineConstants>TRACE</DefineConstants> <ErrorReport>prompt</ErrorReport> <WarningLevel>4</WarningLevel> + <Prefer32Bit>false</Prefer32Bit> </PropertyGroup> <PropertyGroup> <StartupObject>ServiceUninstall.Program</StartupObject> @@ -77,4 +79,4 @@ <Target Name="AfterBuild"> </Target> --> -</Project> +</Project> \ No newline at end of file diff --git a/src/ServiceHelpers/ServiceUninstall/app.config b/src/ServiceHelpers/ServiceUninstall/app.config index e33d6f761..99216b8f8 100644 --- a/src/ServiceHelpers/ServiceUninstall/app.config +++ b/src/ServiceHelpers/ServiceUninstall/app.config @@ -1,6 +1,6 @@ <?xml version="1.0"?> <configuration> <startup useLegacyV2RuntimeActivationPolicy="true"> - <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.0"/> + <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.6.1"/> </startup> </configuration> diff --git a/src/UI/.idea/.name b/src/UI/.idea/.name deleted file mode 100644 index 78ec2c0fe..000000000 --- a/src/UI/.idea/.name +++ /dev/null @@ -1 +0,0 @@ -NzbDrone.UI \ No newline at end of file diff --git a/src/UI/.idea/NzbDrone.UI.iml b/src/UI/.idea/NzbDrone.UI.iml deleted file mode 100644 index 2184ad470..000000000 --- a/src/UI/.idea/NzbDrone.UI.iml +++ /dev/null @@ -1,11 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<module type="WEB_MODULE" version="4"> - <component name="NewModuleRootManager"> - <content url="file://$MODULE_DIR$" /> - <orderEntry type="inheritedJdk" /> - <orderEntry type="sourceFolder" forTests="false" /> - <orderEntry type="library" name="jQuery-1.9.1" level="application" /> - <orderEntry type="library" name="backbone.backgrid.filter.js" level="project" /> - </component> -</module> - diff --git a/src/UI/.idea/codeStyleSettings.xml b/src/UI/.idea/codeStyleSettings.xml deleted file mode 100644 index 7598f4c8e..000000000 --- a/src/UI/.idea/codeStyleSettings.xml +++ /dev/null @@ -1,59 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<project version="4"> - <component name="ProjectCodeStyleSettingsManager"> - <option name="PER_PROJECT_SETTINGS"> - <value> - <option name="LINE_SEPARATOR" value=" " /> - <option name="RIGHT_MARGIN" value="190" /> - <option name="HTML_ATTRIBUTE_WRAP" value="0" /> - <option name="HTML_KEEP_LINE_BREAKS" value="false" /> - <option name="HTML_KEEP_BLANK_LINES" value="1" /> - <option name="HTML_ALIGN_ATTRIBUTES" value="false" /> - <option name="HTML_INLINE_ELEMENTS" value="" /> - <option name="HTML_DONT_ADD_BREAKS_IF_INLINE_CONTENT" value="" /> - <CssCodeStyleSettings> - <option name="HEX_COLOR_LOWER_CASE" value="true" /> - <option name="HEX_COLOR_LONG_FORMAT" value="true" /> - <option name="VALUE_ALIGNMENT" value="1" /> - </CssCodeStyleSettings> - <JSCodeStyleSettings> - <option name="SPACE_BEFORE_PROPERTY_COLON" value="true" /> - <option name="ALIGN_OBJECT_PROPERTIES" value="2" /> - <option name="SPACE_BEFORE_FUNCTION_LEFT_PARENTH" value="false" /> - <option name="OBJECT_LITERAL_WRAP" value="2" /> - <option name="SPACES_WITHIN_OBJECT_LITERAL_BRACES" value="true" /> - </JSCodeStyleSettings> - <XML> - <option name="XML_LEGACY_SETTINGS_IMPORTED" value="true" /> - </XML> - <codeStyleSettings language="CSS"> - <indentOptions> - <option name="SMART_TABS" value="true" /> - </indentOptions> - </codeStyleSettings> - <codeStyleSettings language="JavaScript"> - <option name="LINE_COMMENT_AT_FIRST_COLUMN" value="true" /> - <option name="KEEP_LINE_BREAKS" value="false" /> - <option name="KEEP_FIRST_COLUMN_COMMENT" value="false" /> - <option name="KEEP_BLANK_LINES_IN_CODE" value="1" /> - <option name="CATCH_ON_NEW_LINE" value="true" /> - <option name="FINALLY_ON_NEW_LINE" value="true" /> - <option name="ALIGN_MULTILINE_PARAMETERS" value="false" /> - <option name="ALIGN_MULTILINE_BINARY_OPERATION" value="true" /> - <option name="SPACE_BEFORE_METHOD_PARENTHESES" value="true" /> - <option name="CALL_PARAMETERS_WRAP" value="1" /> - <option name="BINARY_OPERATION_WRAP" value="1" /> - <option name="TERNARY_OPERATION_SIGNS_ON_NEXT_LINE" value="true" /> - <option name="ARRAY_INITIALIZER_WRAP" value="2" /> - <option name="ARRAY_INITIALIZER_LBRACE_ON_NEXT_LINE" value="true" /> - <option name="ARRAY_INITIALIZER_RBRACE_ON_NEXT_LINE" value="true" /> - <option name="IF_BRACE_FORCE" value="3" /> - <option name="DOWHILE_BRACE_FORCE" value="3" /> - <option name="WHILE_BRACE_FORCE" value="3" /> - <option name="FOR_BRACE_FORCE" value="3" /> - </codeStyleSettings> - </value> - </option> - <option name="USE_PER_PROJECT_SETTINGS" value="true" /> - </component> -</project> \ No newline at end of file diff --git a/src/UI/.idea/dictionaries/Keivan.xml b/src/UI/.idea/dictionaries/Keivan.xml deleted file mode 100644 index fb034b3af..000000000 --- a/src/UI/.idea/dictionaries/Keivan.xml +++ /dev/null @@ -1,20 +0,0 @@ -<component name="ProjectDictionaryState"> - <dictionary name="Keivan"> - <words> - <w>deps</w> - <w>mixins</w> - <w>nzbdrone</w> - <w>rootdir</w> - <w>rootfolder</w> - <w>rootfolders</w> - <w>signalr</w> - <w>lidarr</w> - <w>templated</w> - <w>thetvdb</w> - <w>trakt</w> - <w>tvdb</w> - <w>xlarge</w> - <w>yyyy</w> - </words> - </dictionary> -</component> \ No newline at end of file diff --git a/src/UI/.idea/dictionaries/Keivan_Beigi.xml b/src/UI/.idea/dictionaries/Keivan_Beigi.xml deleted file mode 100644 index 00d8e4cec..000000000 --- a/src/UI/.idea/dictionaries/Keivan_Beigi.xml +++ /dev/null @@ -1,13 +0,0 @@ -<component name="ProjectDictionaryState"> - <dictionary name="Keivan.Beigi"> - <words> - <w>backgrid</w> - <w>bnzbd</w> - <w>clickable</w> - <w>couldn</w> - <w>mouseenter</w> - <w>mouseleave</w> - <w>navbar</w> - </words> - </dictionary> -</component> \ No newline at end of file diff --git a/src/UI/.idea/dictionaries/Mark.xml b/src/UI/.idea/dictionaries/Mark.xml deleted file mode 100644 index ecbbe884c..000000000 --- a/src/UI/.idea/dictionaries/Mark.xml +++ /dev/null @@ -1,3 +0,0 @@ -<component name="ProjectDictionaryState"> - <dictionary name="Mark" /> -</component> \ No newline at end of file diff --git a/src/UI/.idea/encodings.xml b/src/UI/.idea/encodings.xml deleted file mode 100644 index e55d06786..000000000 --- a/src/UI/.idea/encodings.xml +++ /dev/null @@ -1,7 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<project version="4"> - <component name="Encoding" useUTFGuessing="true" native2AsciiForPropertiesFiles="false"> - <file url="file://$PROJECT_DIR$/System/Logs/Files/LogFileModel.js" charset="UTF-8" /> - <file url="PROJECT" charset="UTF-8" /> - </component> -</project> \ No newline at end of file diff --git a/src/UI/.idea/inspectionProfiles/Project_Default.xml b/src/UI/.idea/inspectionProfiles/Project_Default.xml deleted file mode 100644 index 7aba4e3c2..000000000 --- a/src/UI/.idea/inspectionProfiles/Project_Default.xml +++ /dev/null @@ -1,117 +0,0 @@ -<component name="InspectionProjectProfileManager"> - <profile version="1.0" is_locked="false"> - <option name="myName" value="Project Default" /> - <option name="myLocal" value="false" /> - <inspection_tool class="AssignmentResultUsedJS" enabled="true" level="WARNING" enabled_by_default="true" /> - <inspection_tool class="AssignmentToForLoopParameterJS" enabled="true" level="ERROR" enabled_by_default="true" /> - <inspection_tool class="AssignmentToFunctionParameterJS" enabled="false" level="WEAK WARNING" enabled_by_default="false" /> - <inspection_tool class="BadExpressionStatementJS" enabled="true" level="ERROR" enabled_by_default="true" /> - <inspection_tool class="BreakStatementJS" enabled="true" level="SERVER PROBLEM" enabled_by_default="true" /> - <inspection_tool class="BreakStatementWithLabelJS" enabled="true" level="ERROR" enabled_by_default="true" /> - <inspection_tool class="ChainedEqualityJS" enabled="true" level="WARNING" enabled_by_default="true" /> - <inspection_tool class="CheckEmptyScriptTag" enabled="false" level="WARNING" enabled_by_default="false" /> - <inspection_tool class="ConditionalExpressionWithIdenticalBranchesJS" enabled="true" level="ERROR" enabled_by_default="true" /> - <inspection_tool class="ConstantIfStatementJS" enabled="true" level="ERROR" enabled_by_default="true" /> - <inspection_tool class="ConstantOnLHSOfComparisonJS" enabled="true" level="ERROR" enabled_by_default="true" /> - <inspection_tool class="ContinueStatementWithLabelJS" enabled="true" level="ERROR" enabled_by_default="true" /> - <inspection_tool class="CssMissingSemicolonInspection" enabled="true" level="WARNING" enabled_by_default="true" /> - <inspection_tool class="CyclomaticComplexityJS" enabled="true" level="WARNING" enabled_by_default="true"> - <option name="m_limit" value="10" /> - </inspection_tool> - <inspection_tool class="DefaultNotLastCaseInSwitchJS" enabled="true" level="ERROR" enabled_by_default="true" /> - <inspection_tool class="DuplicateCaseLabelJS" enabled="true" level="ERROR" enabled_by_default="true" /> - <inspection_tool class="DuplicateConditionJS" enabled="true" level="WARNING" enabled_by_default="true" /> - <inspection_tool class="DynamicallyGeneratedCodeJS" enabled="true" level="WARNING" enabled_by_default="true" /> - <inspection_tool class="EmptyTryBlockJS" enabled="true" level="ERROR" enabled_by_default="true" /> - <inspection_tool class="ExceptionCaughtLocallyJS" enabled="true" level="ERROR" enabled_by_default="true" /> - <inspection_tool class="ForLoopReplaceableByWhileJS" enabled="true" level="INFO" enabled_by_default="true"> - <option name="m_ignoreLoopsWithoutConditions" value="false" /> - </inspection_tool> - <inspection_tool class="FunctionNamingConventionJS" enabled="true" level="WARNING" enabled_by_default="true"> - <option name="m_regex" value="[a-z][A-Za-z]*" /> - <option name="m_minLength" value="4" /> - <option name="m_maxLength" value="32" /> - </inspection_tool> - <inspection_tool class="FunctionWithInconsistentReturnsJS" enabled="true" level="ERROR" enabled_by_default="true" /> - <inspection_tool class="HtmlFormInputWithoutLabel" enabled="false" level="WEAK WARNING" enabled_by_default="false" /> - <inspection_tool class="HtmlPresentationalElement" enabled="false" level="WEAK WARNING" enabled_by_default="false" /> - <inspection_tool class="HtmlUnknownAttribute" enabled="true" level="WARNING" enabled_by_default="true"> - <option name="myValues"> - <value> - <list size="2"> - <item index="0" class="java.lang.String" itemvalue="name" /> - <item index="1" class="java.lang.String" itemvalue="validation-name" /> - </list> - </value> - </option> - <option name="myCustomValuesEnabled" value="true" /> - </inspection_tool> - <inspection_tool class="HtmlUnknownTag" enabled="true" level="WARNING" enabled_by_default="true"> - <option name="myValues"> - <value> - <list size="8"> - <item index="0" class="java.lang.String" itemvalue="nobr" /> - <item index="1" class="java.lang.String" itemvalue="noembed" /> - <item index="2" class="java.lang.String" itemvalue="comment" /> - <item index="3" class="java.lang.String" itemvalue="noscript" /> - <item index="4" class="java.lang.String" itemvalue="embed" /> - <item index="5" class="java.lang.String" itemvalue="script" /> - <item index="6" class="java.lang.String" itemvalue="icon" /> - <item index="7" class="java.lang.String" itemvalue="p" /> - </list> - </value> - </option> - <option name="myCustomValuesEnabled" value="true" /> - </inspection_tool> - <inspection_tool class="IfStatementWithIdenticalBranchesJS" enabled="true" level="ERROR" enabled_by_default="true" /> - <inspection_tool class="IfStatementWithTooManyBranchesJS" enabled="true" level="ERROR" enabled_by_default="true"> - <option name="m_limit" value="3" /> - </inspection_tool> - <inspection_tool class="JSDuplicatedDeclaration" enabled="true" level="ERROR" enabled_by_default="true" /> - <inspection_tool class="JSHint" enabled="true" level="ERROR" enabled_by_default="true" /> - <inspection_tool class="JSLastCommaInArrayLiteral" enabled="true" level="ERROR" enabled_by_default="true" /> - <inspection_tool class="JSPotentiallyInvalidUsageOfThis" enabled="true" level="SERVER PROBLEM" enabled_by_default="true" /> - <inspection_tool class="JSUndeclaredVariable" enabled="true" level="ERROR" enabled_by_default="true" /> - <inspection_tool class="JSUnnecessarySemicolon" enabled="true" level="ERROR" enabled_by_default="true" /> - <inspection_tool class="JSUnresolvedFunction" enabled="true" level="WARNING" enabled_by_default="true" /> - <inspection_tool class="JSUnresolvedVariable" enabled="true" level="WARNING" enabled_by_default="true" /> - <inspection_tool class="JSUnusedGlobalSymbols" enabled="true" level="WARNING" enabled_by_default="true"> - <option name="myReportUnusedDefinitions" value="true" /> - <option name="myReportUnusedProperties" value="true" /> - </inspection_tool> - <inspection_tool class="LabeledStatementJS" enabled="true" level="ERROR" enabled_by_default="true" /> - <inspection_tool class="LocalVariableNamingConventionJS" enabled="true" level="WARNING" enabled_by_default="true"> - <option name="m_regex" value="[a-z][A-Za-z]*" /> - <option name="m_minLength" value="1" /> - <option name="m_maxLength" value="32" /> - </inspection_tool> - <inspection_tool class="NegatedIfStatementJS" enabled="true" level="WARNING" enabled_by_default="true" /> - <inspection_tool class="NestedAssignmentJS" enabled="true" level="WARNING" enabled_by_default="true" /> - <inspection_tool class="NestedFunctionCallJS" enabled="false" level="ERROR" enabled_by_default="false" /> - <inspection_tool class="NestedSwitchStatementJS" enabled="true" level="WARNING" enabled_by_default="true" /> - <inspection_tool class="NestingDepthJS" enabled="true" level="WARNING" enabled_by_default="true"> - <option name="m_limit" value="5" /> - </inspection_tool> - <inspection_tool class="NonBlockStatementBodyJS" enabled="true" level="ERROR" enabled_by_default="true" /> - <inspection_tool class="ParameterNamingConventionJS" enabled="true" level="WARNING" enabled_by_default="true"> - <option name="m_regex" value="[a-z][A-Za-z]*" /> - <option name="m_minLength" value="1" /> - <option name="m_maxLength" value="32" /> - </inspection_tool> - <inspection_tool class="ParametersPerFunctionJS" enabled="true" level="WARNING" enabled_by_default="true"> - <option name="m_limit" value="4" /> - </inspection_tool> - <inspection_tool class="ReservedWordUsedAsNameJS" enabled="true" level="ERROR" enabled_by_default="true" /> - <inspection_tool class="ReuseOfLocalVariableJS" enabled="true" level="WARNING" enabled_by_default="true" /> - <inspection_tool class="StatementsPerFunctionJS" enabled="true" level="WARNING" enabled_by_default="true"> - <option name="m_limit" value="30" /> - </inspection_tool> - <inspection_tool class="SwitchStatementWithNoDefaultBranchJS" enabled="true" level="WARNING" enabled_by_default="true" /> - <inspection_tool class="TailRecursionJS" enabled="true" level="WARNING" enabled_by_default="true" /> - <inspection_tool class="ThisExpressionReferencesGlobalObjectJS" enabled="true" level="ERROR" enabled_by_default="true" /> - <inspection_tool class="ThreeNegationsPerFunctionJS" enabled="true" level="WARNING" enabled_by_default="true" /> - <inspection_tool class="UnterminatedStatementJS" enabled="true" level="ERROR" enabled_by_default="true"> - <option name="ignoreSemicolonAtEndOfBlock" value="true" /> - </inspection_tool> - </profile> -</component> \ No newline at end of file diff --git a/src/UI/.idea/inspectionProfiles/profiles_settings.xml b/src/UI/.idea/inspectionProfiles/profiles_settings.xml deleted file mode 100644 index 3b312839b..000000000 --- a/src/UI/.idea/inspectionProfiles/profiles_settings.xml +++ /dev/null @@ -1,7 +0,0 @@ -<component name="InspectionProjectProfileManager"> - <settings> - <option name="PROJECT_PROFILE" value="Project Default" /> - <option name="USE_PROJECT_PROFILE" value="true" /> - <version value="1.0" /> - </settings> -</component> \ No newline at end of file diff --git a/src/UI/.idea/jsLibraryMappings.xml b/src/UI/.idea/jsLibraryMappings.xml deleted file mode 100644 index 62c621f94..000000000 --- a/src/UI/.idea/jsLibraryMappings.xml +++ /dev/null @@ -1,8 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<project version="4"> - <component name="JavaScriptLibraryMappings"> - <excludedPredefinedLibrary name="HTML" /> - <excludedPredefinedLibrary name="HTML5 / EcmaScript 5" /> - </component> -</project> - diff --git a/src/UI/.idea/jsLinters/jshint.xml b/src/UI/.idea/jsLinters/jshint.xml deleted file mode 100644 index 0b5c0e41e..000000000 --- a/src/UI/.idea/jsLinters/jshint.xml +++ /dev/null @@ -1,72 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<project version="4"> - <component name="JSHintConfiguration" version="2.6.0" use-config-file="false"> - <option asi="false" /> - <option bitwise="true" /> - <option boss="false" /> - <option browser="true" /> - <option camelcase="true" /> - <option couch="false" /> - <option curly="true" /> - <option debug="false" /> - <option devel="true" /> - <option dojo="false" /> - <option eqeqeq="true" /> - <option eqnull="false" /> - <option es3="false" /> - <option esnext="false" /> - <option evil="false" /> - <option expr="false" /> - <option forin="true" /> - <option freeze="false" /> - <option funcscope="false" /> - <option gcl="false" /> - <option globalstrict="true" /> - <option immed="true" /> - <option iterator="false" /> - <option jquery="false" /> - <option lastsemic="false" /> - <option latedef="true" /> - <option laxbreak="false" /> - <option laxcomma="false" /> - <option loopfunc="false" /> - <option maxdepth="3" /> - <option maxerr="50" /> - <option mootools="false" /> - <option moz="false" /> - <option multistr="false" /> - <option newcap="true" /> - <option noarg="true" /> - <option node="true" /> - <option noempty="false" /> - <option nomen="false" /> - <option nonbsp="false" /> - <option nonew="true" /> - <option nonstandard="false" /> - <option notypeof="false" /> - <option noyield="false" /> - <option onevar="false" /> - <option passfail="false" /> - <option phantom="false" /> - <option plusplus="false" /> - <option predef="window, define, require, module" /> - <option proto="false" /> - <option prototypejs="false" /> - <option quotmark="single" /> - <option rhino="false" /> - <option scripturl="false" /> - <option shadow="false" /> - <option smarttabs="false" /> - <option strict="false" /> - <option sub="false" /> - <option supernew="false" /> - <option trailing="false" /> - <option undef="true" /> - <option unused="true" /> - <option validthis="false" /> - <option white="false" /> - <option worker="false" /> - <option wsh="false" /> - <option yui="false" /> - </component> -</project> \ No newline at end of file diff --git a/src/UI/.idea/jsLinters/jslint.xml b/src/UI/.idea/jsLinters/jslint.xml deleted file mode 100644 index 822a7aa5e..000000000 --- a/src/UI/.idea/jsLinters/jslint.xml +++ /dev/null @@ -1,13 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<project version="4"> - <component name="JSLintConfiguration" html="true" json="true"> - <option browser="true" /> - <option devel="true" /> - <option indent="4" /> - <option maxerr="50" /> - <option plusplus="true" /> - <option todo="true" /> - <option white="true" /> - </component> -</project> - diff --git a/src/UI/.idea/misc.xml b/src/UI/.idea/misc.xml deleted file mode 100644 index e9e9ba1c3..000000000 --- a/src/UI/.idea/misc.xml +++ /dev/null @@ -1,6 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<project version="4"> - <component name="ProjectKey"> - <option name="state" value="git@github.com:NzbDrone/NzbDrone.git" /> - </component> -</project> \ No newline at end of file diff --git a/src/UI/.idea/modules.xml b/src/UI/.idea/modules.xml deleted file mode 100644 index ab774833e..000000000 --- a/src/UI/.idea/modules.xml +++ /dev/null @@ -1,8 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<project version="4"> - <component name="ProjectModuleManager"> - <modules> - <module fileurl="file://$PROJECT_DIR$/.idea/NzbDrone.UI.iml" filepath="$PROJECT_DIR$/.idea/NzbDrone.UI.iml" /> - </modules> - </component> -</project> \ No newline at end of file diff --git a/src/UI/.idea/runConfigurations/Debug___Chrome.xml b/src/UI/.idea/runConfigurations/Debug___Chrome.xml deleted file mode 100644 index 2ff8dbf6b..000000000 --- a/src/UI/.idea/runConfigurations/Debug___Chrome.xml +++ /dev/null @@ -1,23 +0,0 @@ -<component name="ProjectRunConfigurationManager"> - <configuration default="false" name="Debug - Chrome" type="JavascriptDebugType" factoryName="JavaScript Debug" singleton="true" uri="http://localhost:8686"> - <mapping url="http://localhost:8686/Calendar" local-file="$PROJECT_DIR$/Calendar" /> - <mapping url="http://localhost:8686/MainMenuView.js" local-file="$PROJECT_DIR$/MainMenuView.js" /> - <mapping url="http://localhost:8686/Settings" local-file="$PROJECT_DIR$/Settings" /> - <mapping url="http://localhost:8686/Upcoming" local-file="$PROJECT_DIR$/Upcoming" /> - <mapping url="http://localhost:8686/app.js" local-file="$PROJECT_DIR$/app.js" /> - <mapping url="http://localhost:8686/Mixins" local-file="$PROJECT_DIR$/Mixins" /> - <mapping url="http://localhost:8686/Wanted" local-file="$PROJECT_DIR$/Wanted" /> - <mapping url="http://localhost:8686/Quality" local-file="$PROJECT_DIR$/Quality" /> - <mapping url="http://localhost:8686/Config.js" local-file="$PROJECT_DIR$/Config.js" /> - <mapping url="http://localhost:8686/Shared" local-file="$PROJECT_DIR$/Shared" /> - <mapping url="http://localhost:8686/AddArtist" local-file="$PROJECT_DIR$/AddArtist" /> - <mapping url="http://localhost:8686/HeaderView.js" local-file="$PROJECT_DIR$/HeaderView.js" /> - <mapping url="http://localhost:8686" local-file="$PROJECT_DIR$" /> - <mapping url="http://localhost:8686/Routing.js" local-file="$PROJECT_DIR$/Routing.js" /> - <mapping url="http://localhost:8686/Controller.js" local-file="$PROJECT_DIR$/Controller.js" /> - <mapping url="http://localhost:8686/Series" local-file="$PROJECT_DIR$/Series" /> - <RunnerSettings RunnerId="JavascriptDebugRunner" /> - <ConfigurationWrapper RunnerId="JavascriptDebugRunner" /> - <method /> - </configuration> -</component> \ No newline at end of file diff --git a/src/UI/.idea/runConfigurations/Debug___Firefox.xml b/src/UI/.idea/runConfigurations/Debug___Firefox.xml deleted file mode 100644 index dbbdebbe4..000000000 --- a/src/UI/.idea/runConfigurations/Debug___Firefox.xml +++ /dev/null @@ -1,23 +0,0 @@ -<component name="ProjectRunConfigurationManager"> - <configuration default="false" name="Debug - Firefox" type="JavascriptDebugType" factoryName="JavaScript Debug" singleton="true" engineId="firefox" uri="http://localhost:8686"> - <mapping url="http://localhost:8686/Calendar" local-file="$PROJECT_DIR$/Calendar" /> - <mapping url="http://localhost:8686/MainMenuView.js" local-file="$PROJECT_DIR$/MainMenuView.js" /> - <mapping url="http://localhost:8686/Settings" local-file="$PROJECT_DIR$/Settings" /> - <mapping url="http://localhost:8686/Upcoming" local-file="$PROJECT_DIR$/Upcoming" /> - <mapping url="http://localhost:8686/app.js" local-file="$PROJECT_DIR$/app.js" /> - <mapping url="http://localhost:8686/Mixins" local-file="$PROJECT_DIR$/Mixins" /> - <mapping url="http://localhost:8686/Wanted" local-file="$PROJECT_DIR$/Wanted" /> - <mapping url="http://localhost:8686/Config.js" local-file="$PROJECT_DIR$/Config.js" /> - <mapping url="http://localhost:8686/Quality" local-file="$PROJECT_DIR$/Quality" /> - <mapping url="http://localhost:8686/AddArtist" local-file="$PROJECT_DIR$/AddArtist" /> - <mapping url="http://localhost:8686/Shared" local-file="$PROJECT_DIR$/Shared" /> - <mapping url="http://localhost:8686/HeaderView.js" local-file="$PROJECT_DIR$/HeaderView.js" /> - <mapping url="http://localhost:8686" local-file="$PROJECT_DIR$" /> - <mapping url="http://localhost:8686/Routing.js" local-file="$PROJECT_DIR$/Routing.js" /> - <mapping url="http://localhost:8686/Controller.js" local-file="$PROJECT_DIR$/Controller.js" /> - <mapping url="http://localhost:8686/Series" local-file="$PROJECT_DIR$/Series" /> - <RunnerSettings RunnerId="JavascriptDebugRunner" /> - <ConfigurationWrapper RunnerId="JavascriptDebugRunner" /> - <method /> - </configuration> -</component> \ No newline at end of file diff --git a/src/UI/.idea/scopes/NzbDrone.xml b/src/UI/.idea/scopes/NzbDrone.xml deleted file mode 100644 index 17c1c9c5e..000000000 --- a/src/UI/.idea/scopes/NzbDrone.xml +++ /dev/null @@ -1,3 +0,0 @@ -<component name="DependencyValidationManager"> - <scope name="NzbDrone" pattern="!file:JsLibraries//*" /> -</component> \ No newline at end of file diff --git a/src/UI/.idea/scopes/scope_settings.xml b/src/UI/.idea/scopes/scope_settings.xml deleted file mode 100644 index 922003b84..000000000 --- a/src/UI/.idea/scopes/scope_settings.xml +++ /dev/null @@ -1,5 +0,0 @@ -<component name="DependencyValidationManager"> - <state> - <option name="SKIP_IMPORT_STATEMENTS" value="false" /> - </state> -</component> \ No newline at end of file diff --git a/src/UI/.idea/vcs.xml b/src/UI/.idea/vcs.xml deleted file mode 100644 index 9ab281ac8..000000000 --- a/src/UI/.idea/vcs.xml +++ /dev/null @@ -1,7 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<project version="4"> - <component name="VcsDirectoryMappings"> - <mapping directory="$PROJECT_DIR$/../.." vcs="Git" /> - </component> -</project> - diff --git a/src/UI/.jshintrc b/src/UI/.jshintrc deleted file mode 100644 index 888afe448..000000000 --- a/src/UI/.jshintrc +++ /dev/null @@ -1,19 +0,0 @@ -{ - "-W030": false, - "-W064": false, - "-W097": false, - "-W100": false, - "undef": true, - "curly": true, - "immed": true, - "eqeqeq": true, - "latedef": true, - "globals": { - "module": true, - "require": true, - "define": true, - "window": true, - "document": true, - "console": true - } -} diff --git a/src/UI/Activity/ActivityLayout.js b/src/UI/Activity/ActivityLayout.js deleted file mode 100644 index a8826a714..000000000 --- a/src/UI/Activity/ActivityLayout.js +++ /dev/null @@ -1,84 +0,0 @@ -var Marionette = require('marionette'); -var Backbone = require('backbone'); -var Backgrid = require('backgrid'); -var HistoryLayout = require('./History/HistoryLayout'); -var BlacklistLayout = require('./Blacklist/BlacklistLayout'); -var QueueLayout = require('./Queue/QueueLayout'); - -module.exports = Marionette.Layout.extend({ - template : 'Activity/ActivityLayoutTemplate', - - regions : { - queueRegion : '#queue', - history : '#history', - blacklist : '#blacklist' - }, - - ui : { - queueTab : '.x-queue-tab', - historyTab : '.x-history-tab', - blacklistTab : '.x-blacklist-tab' - }, - - events : { - 'click .x-queue-tab' : '_showQueue', - 'click .x-history-tab' : '_showHistory', - 'click .x-blacklist-tab' : '_showBlacklist' - }, - - initialize : function(options) { - if (options.action) { - this.action = options.action.toLowerCase(); - } - }, - - onShow : function() { - switch (this.action) { - case 'history': - this._showHistory(); - break; - case 'blacklist': - this._showBlacklist(); - break; - default: - this._showQueue(); - } - }, - - _navigate : function(route) { - Backbone.history.navigate(route, { - trigger : false, - replace : true - }); - }, - - _showHistory : function(e) { - if (e) { - e.preventDefault(); - } - - this.history.show(new HistoryLayout()); - this.ui.historyTab.tab('show'); - this._navigate('/activity/history'); - }, - - _showBlacklist : function(e) { - if (e) { - e.preventDefault(); - } - - this.blacklist.show(new BlacklistLayout()); - this.ui.blacklistTab.tab('show'); - this._navigate('/activity/blacklist'); - }, - - _showQueue : function(e) { - if (e) { - e.preventDefault(); - } - - this.queueRegion.show(new QueueLayout()); - this.ui.queueTab.tab('show'); - this._navigate('/activity/queue'); - } -}); \ No newline at end of file diff --git a/src/UI/Activity/ActivityLayoutTemplate.hbs b/src/UI/Activity/ActivityLayoutTemplate.hbs deleted file mode 100644 index c9c08ecf7..000000000 --- a/src/UI/Activity/ActivityLayoutTemplate.hbs +++ /dev/null @@ -1,11 +0,0 @@ -<ul class="nav nav-tabs"> - <li><a href="#queue" class="x-queue-tab no-router">Queue</a></li> - <li><a href="#history" class="x-history-tab no-router">History</a></li> - <li><a href="#blacklist" class="x-blacklist-tab no-router">Blacklist</a></li> -</ul> - -<div class="tab-content"> - <div class="tab-pane" id="queue"></div> - <div class="tab-pane" id="history"></div> - <div class="tab-pane" id="blacklist"></div> -</div> \ No newline at end of file diff --git a/src/UI/Activity/Blacklist/BlacklistActionsCell.js b/src/UI/Activity/Blacklist/BlacklistActionsCell.js deleted file mode 100644 index ed013db1d..000000000 --- a/src/UI/Activity/Blacklist/BlacklistActionsCell.js +++ /dev/null @@ -1,28 +0,0 @@ -var vent = require('vent'); -var NzbDroneCell = require('../../Cells/NzbDroneCell'); -var BlacklistDetailsLayout = require('./Details/BlacklistDetailsLayout'); - -module.exports = NzbDroneCell.extend({ - className : 'blacklist-actions-cell', - - events : { - 'click .x-details' : '_details', - 'click .x-delete' : '_delete' - }, - - render : function() { - this.$el.empty(); - this.$el.html('<i class="icon-lidarr-info x-details"></i>' + - '<i class="icon-lidarr-delete x-delete"></i>'); - - return this; - }, - - _details : function() { - vent.trigger(vent.Commands.OpenModalCommand, new BlacklistDetailsLayout({ model : this.model })); - }, - - _delete : function() { - this.model.destroy(); - } -}); diff --git a/src/UI/Activity/Blacklist/BlacklistCollection.js b/src/UI/Activity/Blacklist/BlacklistCollection.js deleted file mode 100644 index 86b177065..000000000 --- a/src/UI/Activity/Blacklist/BlacklistCollection.js +++ /dev/null @@ -1,47 +0,0 @@ -var BlacklistModel = require('./BlacklistModel'); -var PageableCollection = require('backbone.pageable'); -var AsSortedCollection = require('../../Mixins/AsSortedCollection'); -var AsPersistedStateCollection = require('../../Mixins/AsPersistedStateCollection'); - -var Collection = PageableCollection.extend({ - url : window.NzbDrone.ApiRoot + '/blacklist', - model : BlacklistModel, - - state : { - pageSize : 15, - sortKey : 'date', - order : 1 - }, - - queryParams : { - totalPages : null, - totalRecords : null, - pageSize : 'pageSize', - sortKey : 'sortKey', - order : 'sortDir', - directions : { - '-1' : 'asc', - '1' : 'desc' - } - }, - - sortMappings : { - 'artist' : { sortKey : 'artist.sortName' } - }, - - parseState : function(resp) { - return { totalRecords : resp.totalRecords }; - }, - - parseRecords : function(resp) { - if (resp) { - return resp.records; - } - - return resp; - } -}); -Collection = AsSortedCollection.call(Collection); -Collection = AsPersistedStateCollection.call(Collection); - -module.exports = Collection; \ No newline at end of file diff --git a/src/UI/Activity/Blacklist/BlacklistLayout.js b/src/UI/Activity/Blacklist/BlacklistLayout.js deleted file mode 100644 index 1be8fed68..000000000 --- a/src/UI/Activity/Blacklist/BlacklistLayout.js +++ /dev/null @@ -1,114 +0,0 @@ -var vent = require('vent'); -var Marionette = require('marionette'); -var Backgrid = require('backgrid'); -var BlacklistCollection = require('./BlacklistCollection'); -var ArtistTitleCell = require('../../Cells/ArtistTitleCell'); -var QualityCell = require('../../Cells/QualityCell'); -var RelativeDateCell = require('../../Cells/RelativeDateCell'); -var BlacklistActionsCell = require('./BlacklistActionsCell'); -var GridPager = require('../../Shared/Grid/Pager'); -var LoadingView = require('../../Shared/LoadingView'); -var ToolbarLayout = require('../../Shared/Toolbar/ToolbarLayout'); - -module.exports = Marionette.Layout.extend({ - template : 'Activity/Blacklist/BlacklistLayoutTemplate', - - regions : { - blacklist : '#x-blacklist', - toolbar : '#x-toolbar', - pager : '#x-pager' - }, - - columns : [ - { - name : 'artist', - label : 'Artist', - cell : ArtistTitleCell - }, - { - name : 'sourceTitle', - label : 'Source Title', - cell : 'string' - }, - { - name : 'quality', - label : 'Quality', - cell : QualityCell, - sortable : false - }, - { - name : 'date', - label : 'Date', - cell : RelativeDateCell - }, - { - name : 'this', - label : '', - cell : BlacklistActionsCell, - sortable : false - } - ], - - initialize : function() { - this.collection = new BlacklistCollection({ tableName : 'blacklist' }); - - this.listenTo(this.collection, 'sync', this._showTable); - this.listenTo(vent, vent.Events.CommandComplete, this._commandComplete); - }, - - onShow : function() { - this.blacklist.show(new LoadingView()); - this._showToolbar(); - this.collection.fetch(); - }, - - _showTable : function(collection) { - - this.blacklist.show(new Backgrid.Grid({ - columns : this.columns, - collection : collection, - className : 'table table-hover' - })); - - this.pager.show(new GridPager({ - columns : this.columns, - collection : collection - })); - }, - - _showToolbar : function() { - var leftSideButtons = { - type : 'default', - storeState : false, - items : [ - { - title : 'Clear Blacklist', - icon : 'icon-lidarr-clear', - command : 'clearBlacklist' - } - ] - }; - - this.toolbar.show(new ToolbarLayout({ - left : [ - leftSideButtons - ], - context : this - })); - }, - - _refreshTable : function(buttonContext) { - this.collection.state.currentPage = 1; - var promise = this.collection.fetch({ reset : true }); - - if (buttonContext) { - buttonContext.ui.icon.spinForPromise(promise); - } - }, - - _commandComplete : function(options) { - if (options.command.get('name') === 'clearblacklist') { - this._refreshTable(); - } - } -}); diff --git a/src/UI/Activity/Blacklist/BlacklistLayoutTemplate.hbs b/src/UI/Activity/Blacklist/BlacklistLayoutTemplate.hbs deleted file mode 100644 index 8f78eb0db..000000000 --- a/src/UI/Activity/Blacklist/BlacklistLayoutTemplate.hbs +++ /dev/null @@ -1,11 +0,0 @@ -<div id="x-toolbar"/> -<div class="row"> - <div class="col-md-12"> - <div id="x-blacklist" class="table-responsive"/> - </div> -</div> -<div class="row"> - <div class="col-md-12"> - <div id="x-pager"/> - </div> -</div> diff --git a/src/UI/Activity/Blacklist/BlacklistModel.js b/src/UI/Activity/Blacklist/BlacklistModel.js deleted file mode 100644 index e3c4b5ab0..000000000 --- a/src/UI/Activity/Blacklist/BlacklistModel.js +++ /dev/null @@ -1,17 +0,0 @@ -var Backbone = require('backbone'); -var ArtistCollection = require('../../Artist/ArtistCollection'); - -module.exports = Backbone.Model.extend({ - - //Hack to deal with Backbone 1.0's bug - initialize : function() { - this.url = function() { - return this.collection.url + '/' + this.get('id'); - }; - }, - - parse : function(model) { - model.artist = ArtistCollection.get(model.artistId); - return model; - } -}); \ No newline at end of file diff --git a/src/UI/Activity/Blacklist/Details/BlacklistDetailsLayout.js b/src/UI/Activity/Blacklist/Details/BlacklistDetailsLayout.js deleted file mode 100644 index cdcbf25f0..000000000 --- a/src/UI/Activity/Blacklist/Details/BlacklistDetailsLayout.js +++ /dev/null @@ -1,14 +0,0 @@ -var Marionette = require('marionette'); -var BlacklistDetailsView = require('./BlacklistDetailsView'); - -module.exports = Marionette.Layout.extend({ - template : 'Activity/Blacklist/Details/BlacklistDetailsLayoutTemplate', - - regions : { - bodyRegion : '.modal-body' - }, - - onShow : function() { - this.bodyRegion.show(new BlacklistDetailsView({ model : this.model })); - } -}); \ No newline at end of file diff --git a/src/UI/Activity/Blacklist/Details/BlacklistDetailsLayoutTemplate.hbs b/src/UI/Activity/Blacklist/Details/BlacklistDetailsLayoutTemplate.hbs deleted file mode 100644 index 3cdfa99c7..000000000 --- a/src/UI/Activity/Blacklist/Details/BlacklistDetailsLayoutTemplate.hbs +++ /dev/null @@ -1,18 +0,0 @@ -<div class="modal-content"> - <div class="history-detail-modal"> - <div class="modal-header"> - <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> - - <h3> - Blacklisted - </h3> - - </div> - <div class="modal-body"> - - </div> - <div class="modal-footer"> - <button class="btn" data-dismiss="modal">Close</button> - </div> - </div> -</div> diff --git a/src/UI/Activity/Blacklist/Details/BlacklistDetailsView.js b/src/UI/Activity/Blacklist/Details/BlacklistDetailsView.js deleted file mode 100644 index 1b7bc883d..000000000 --- a/src/UI/Activity/Blacklist/Details/BlacklistDetailsView.js +++ /dev/null @@ -1,5 +0,0 @@ -var Marionette = require('marionette'); - -module.exports = Marionette.ItemView.extend({ - template : 'Activity/Blacklist/Details/BlacklistDetailsViewTemplate' -}); \ No newline at end of file diff --git a/src/UI/Activity/Blacklist/Details/BlacklistDetailsViewTemplate.hbs b/src/UI/Activity/Blacklist/Details/BlacklistDetailsViewTemplate.hbs deleted file mode 100644 index d29a878fc..000000000 --- a/src/UI/Activity/Blacklist/Details/BlacklistDetailsViewTemplate.hbs +++ /dev/null @@ -1,23 +0,0 @@ -<dl class="dl-horizontal info"> - - <dt>Name:</dt> - <dd>{{sourceTitle}}</dd> - - {{#if protocol}} - {{#unless_eq protocol compare="unknown"}} - <dt>Protocol:</dt> - <dd>{{protocol}}</dd> - {{/unless_eq}} - {{/if}} - - {{#if indexer}} - <dt>Indexer:</dt> - <dd>{{indexer}}</dd> - {{/if}} - - - {{#if message}} - <dt>Message:</dt> - <dd>{{message}}</dd> - {{/if}} -</dl> diff --git a/src/UI/Activity/History/Details/HistoryDetailsAge.js b/src/UI/Activity/History/Details/HistoryDetailsAge.js deleted file mode 100644 index a7c40f69a..000000000 --- a/src/UI/Activity/History/Details/HistoryDetailsAge.js +++ /dev/null @@ -1,22 +0,0 @@ -var Handlebars = require('handlebars'); -var FormatHelpers = require('../../../Shared/FormatHelpers'); - -Handlebars.registerHelper('historyAge', function() { - - var age = this.age; - var unit = FormatHelpers.plural(Math.round(age), 'day'); - var ageHours = parseFloat(this.ageHours); - var ageMinutes = this.ageMinutes ? parseFloat(this.ageMinutes) : null; - - if (age < 2) { - age = ageHours.toFixed(1); - unit = FormatHelpers.plural(Math.round(ageHours), 'hour'); - } - - if (age < 2 && ageMinutes) { - age = parseFloat(ageMinutes).toFixed(1); - unit = FormatHelpers.plural(Math.round(ageMinutes), 'minute'); - } - - return new Handlebars.SafeString('<dt>Age (when grabbed):</dt><dd>{0} {1}</dd>'.format(age, unit)); -}); diff --git a/src/UI/Activity/History/Details/HistoryDetailsLayout.js b/src/UI/Activity/History/Details/HistoryDetailsLayout.js deleted file mode 100644 index 5654a3e72..000000000 --- a/src/UI/Activity/History/Details/HistoryDetailsLayout.js +++ /dev/null @@ -1,35 +0,0 @@ -var $ = require('jquery'); -var vent = require('vent'); -var Marionette = require('marionette'); -var HistoryDetailsView = require('./HistoryDetailsView'); - -module.exports = Marionette.Layout.extend({ - template : 'Activity/History/Details/HistoryDetailsLayoutTemplate', - - regions : { - bodyRegion : '.modal-body' - }, - - events : { - 'click .x-mark-as-failed' : '_markAsFailed' - }, - - onShow : function() { - this.bodyRegion.show(new HistoryDetailsView({ model : this.model })); - }, - - _markAsFailed : function() { - var url = window.NzbDrone.ApiRoot + '/history/failed'; - var data = { - id : this.model.get('id') - }; - - $.ajax({ - url : url, - type : 'POST', - data : data - }); - - vent.trigger(vent.Commands.CloseModalCommand); - } -}); \ No newline at end of file diff --git a/src/UI/Activity/History/Details/HistoryDetailsLayoutTemplate.hbs b/src/UI/Activity/History/Details/HistoryDetailsLayoutTemplate.hbs deleted file mode 100644 index e24b3b861..000000000 --- a/src/UI/Activity/History/Details/HistoryDetailsLayoutTemplate.hbs +++ /dev/null @@ -1,22 +0,0 @@ -<div class="modal-content"> - <div class="history-detail-modal"> - <div class="modal-header"> - <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> - - <h3> - {{#if_eq eventType compare="grabbed"}}Grabbed{{/if_eq}} - {{#if_eq eventType compare="downloadFailed"}}Download Failed{{/if_eq}} - {{#if_eq eventType compare="downloadFolderImported"}}Album Imported{{/if_eq}} - {{#if_eq eventType compare="episodeFileDeleted"}}Album Files Deleted{{/if_eq}} - </h3> - - </div> - <div class="modal-body"> - - </div> - <div class="modal-footer"> - {{#if_eq eventType compare="grabbed"}}<button class="btn btn-danger x-mark-as-failed">Mark As Failed</button>{{/if_eq}} - <button class="btn" data-dismiss="modal">Close</button> - </div> - </div> -</div> diff --git a/src/UI/Activity/History/Details/HistoryDetailsView.js b/src/UI/Activity/History/Details/HistoryDetailsView.js deleted file mode 100644 index a883b0cb4..000000000 --- a/src/UI/Activity/History/Details/HistoryDetailsView.js +++ /dev/null @@ -1,6 +0,0 @@ -var Marionette = require('marionette'); -require('./HistoryDetailsAge'); - -module.exports = Marionette.ItemView.extend({ - template : 'Activity/History/Details/HistoryDetailsViewTemplate' -}); \ No newline at end of file diff --git a/src/UI/Activity/History/Details/HistoryDetailsViewTemplate.hbs b/src/UI/Activity/History/Details/HistoryDetailsViewTemplate.hbs deleted file mode 100644 index 147504fc0..000000000 --- a/src/UI/Activity/History/Details/HistoryDetailsViewTemplate.hbs +++ /dev/null @@ -1,103 +0,0 @@ -{{#if_eq eventType compare="grabbed"}} -<dl class="dl-horizontal info"> - - <dt>Name:</dt> - <dd>{{sourceTitle}}</dd> - - {{#with data}} - {{#if indexer}} - <dt>Indexer:</dt> - <dd>{{indexer}}</dd> - {{/if}} - - {{#if releaseGroup}} - <dt>Release Group:</dt> - <dd>{{releaseGroup}}</dd> - {{/if}} - - {{#if nzbInfoUrl}} - <dt>Info:</dt> - <dd><a href="{{nzbInfoUrl}}">{{nzbInfoUrl}}</a></dd> - {{/if}} - - {{#if downloadClient}} - <dt>Download Client:</dt> - <dd>{{downloadClient}}</dd> - {{/if}} - - {{#if downloadId}} - <dt>Grab ID:</dt> - <dd>{{downloadId}}</dd> - {{/if}} - - {{#if age}} - {{historyAge}} - {{/if}} - - {{#if publishedDate}} - <dt>Published Date:</dt> - <dd>{{ShortDate publishedDate}} {{LTS publishedDate}}</dd> - {{/if}} - {{/with}} -</dl> -{{/if_eq}} - -{{#if_eq eventType compare="downloadFailed"}} -<dl class="dl-horizontal"> - - <dt>Name:</dt> - <dd>{{sourceTitle}}</dd> - - {{#with data}} - <dt>Message:</dt> - <dd>{{message}}</dd> - {{/with}} -</dl> -{{/if_eq}} - -{{#if_eq eventType compare="downloadFolderImported"}} -<dl class="dl-horizontal"> - - {{#if sourceTitle}} - <dt>Name:</dt> - <dd>{{sourceTitle}}</dd> - {{/if}} - - {{#with data}} - {{#if droppedPath}} - <dt>Source:</dt> - <dd>{{droppedPath}}</dd> - {{/if}} - - {{#if importedPath}} - <dt>Imported To:</dt> - <dd>{{importedPath}}</dd> - {{/if}} - {{/with}} -</dl> -{{/if_eq}} - -{{#if_eq eventType compare="episodeFileDeleted"}} -<dl class="dl-horizontal"> - - <dt>Path:</dt> - <dd>{{sourceTitle}}</dd> - - {{#with data}} - <dt>Reason:</dt> - <dd> - {{#if_eq reason compare="Manual"}} - File was deleted by via UI - {{/if_eq}} - - {{#if_eq reason compare="MissingFromDisk"}} - Lidarr was unable to find the file on disk so it was removed - {{/if_eq}} - - {{#if_eq reason compare="Upgrade"}} - File was deleted to import an upgrade - {{/if_eq}} - </dd> - {{/with}} -</dl> -{{/if_eq}} \ No newline at end of file diff --git a/src/UI/Activity/History/HistoryCollection.js b/src/UI/Activity/History/HistoryCollection.js deleted file mode 100644 index 93ae84ea4..000000000 --- a/src/UI/Activity/History/HistoryCollection.js +++ /dev/null @@ -1,84 +0,0 @@ -var HistoryModel = require('./HistoryModel'); -var PageableCollection = require('backbone.pageable'); -var AsFilteredCollection = require('../../Mixins/AsFilteredCollection'); -var AsSortedCollection = require('../../Mixins/AsSortedCollection'); -var AsPersistedStateCollection = require('../../Mixins/AsPersistedStateCollection'); - -var Collection = PageableCollection.extend({ - url : window.NzbDrone.ApiRoot + '/history', - model : HistoryModel, - - state : { - pageSize : 15, - sortKey : 'date', - order : 1 - }, - - queryParams : { - totalPages : null, - totalRecords : null, - pageSize : 'pageSize', - sortKey : 'sortKey', - order : 'sortDir', - directions : { - '-1' : 'asc', - '1' : 'desc' - } - }, - - filterModes : { - 'all' : [ - null, - null - ], - 'grabbed' : [ - 'eventType', - '1' - ], - 'imported' : [ - 'eventType', - '3' - ], - 'failed' : [ - 'eventType', - '4' - ], - 'deleted' : [ - 'eventType', - '5' - ] - }, - - sortMappings : { - 'artist' : { sortKey : 'artist.sortName' }, - 'album' : { sortKey : 'album.title' } - }, - - initialize : function(options) { - delete this.queryParams.albumId; - - if (options) { - if (options.albumId) { - this.queryParams.albumId = options.albumId; - } - } - }, - - parseState : function(resp) { - return { totalRecords : resp.totalRecords }; - }, - - parseRecords : function(resp) { - if (resp) { - return resp.records; - } - - return resp; - } -}); - -Collection = AsFilteredCollection.call(Collection); -Collection = AsSortedCollection.call(Collection); -Collection = AsPersistedStateCollection.call(Collection); - -module.exports = Collection; \ No newline at end of file diff --git a/src/UI/Activity/History/HistoryDetailsCell.js b/src/UI/Activity/History/HistoryDetailsCell.js deleted file mode 100644 index 5c8a33b09..000000000 --- a/src/UI/Activity/History/HistoryDetailsCell.js +++ /dev/null @@ -1,21 +0,0 @@ -var vent = require('vent'); -var NzbDroneCell = require('../../Cells/NzbDroneCell'); - -module.exports = NzbDroneCell.extend({ - className : 'history-details-cell', - - events : { - 'click' : '_showDetails' - }, - - render : function() { - this.$el.empty(); - this.$el.html('<i class="icon-lidarr-info"></i>'); - - return this; - }, - - _showDetails : function() { - vent.trigger(vent.Commands.ShowHistoryDetails, { model : this.model }); - } -}); \ No newline at end of file diff --git a/src/UI/Activity/History/HistoryLayout.js b/src/UI/Activity/History/HistoryLayout.js deleted file mode 100644 index 3223885f7..000000000 --- a/src/UI/Activity/History/HistoryLayout.js +++ /dev/null @@ -1,146 +0,0 @@ -var Marionette = require('marionette'); -var Backgrid = require('backgrid'); -var HistoryCollection = require('./HistoryCollection'); -var EventTypeCell = require('../../Cells/EventTypeCell'); -var ArtistTitleCell = require('../../Cells/ArtistTitleCell'); -var AlbumTitleCell = require('../../Cells/AlbumTitleCell'); -var HistoryQualityCell = require('./HistoryQualityCell'); -var RelativeDateCell = require('../../Cells/RelativeDateCell'); -var HistoryDetailsCell = require('./HistoryDetailsCell'); -var GridPager = require('../../Shared/Grid/Pager'); -var ToolbarLayout = require('../../Shared/Toolbar/ToolbarLayout'); -var LoadingView = require('../../Shared/LoadingView'); - -module.exports = Marionette.Layout.extend({ - template : 'Activity/History/HistoryLayoutTemplate', - - regions : { - history : '#x-history', - toolbar : '#x-history-toolbar', - pager : '#x-history-pager' - }, - - columns : [ - { - name : 'eventType', - label : '', - cell : EventTypeCell, - cellValue : 'this' - }, - { - name : 'artist', - label : 'Artist', - cell : ArtistTitleCell - }, - { - name : 'album', - label : 'Album Title', - cell : AlbumTitleCell - }, - { - name : 'this', - label : 'Quality', - cell : HistoryQualityCell, - sortable : false - }, - { - name : 'date', - label : 'Date', - cell : RelativeDateCell - }, - { - name : 'this', - label : '', - cell : HistoryDetailsCell, - sortable : false - } - ], - - initialize : function() { - this.collection = new HistoryCollection({ tableName : 'history' }); - this.listenTo(this.collection, 'sync', this._showTable); - }, - - onShow : function() { - this.history.show(new LoadingView()); - this._showToolbar(); - }, - - _showTable : function(collection) { - - this.history.show(new Backgrid.Grid({ - columns : this.columns, - collection : collection, - className : 'table table-hover' - })); - - this.pager.show(new GridPager({ - columns : this.columns, - collection : collection - })); - }, - - _showToolbar : function() { - var filterOptions = { - type : 'radio', - storeState : true, - menuKey : 'history.filterMode', - defaultAction : 'all', - items : [ - { - key : 'all', - title : '', - tooltip : 'All', - icon : 'icon-lidarr-all', - callback : this._setFilter - }, - { - key : 'grabbed', - title : '', - tooltip : 'Grabbed', - icon : 'icon-lidarr-downloading', - callback : this._setFilter - }, - { - key : 'imported', - title : '', - tooltip : 'Imported', - icon : 'icon-lidarr-imported', - callback : this._setFilter - }, - { - key : 'failed', - title : '', - tooltip : 'Failed', - icon : 'icon-lidarr-download-failed', - callback : this._setFilter - }, - { - key : 'deleted', - title : '', - tooltip : 'Deleted', - icon : 'icon-lidarr-deleted', - callback : this._setFilter - } - ] - }; - - this.toolbar.show(new ToolbarLayout({ - right : [ - filterOptions - ], - context : this - })); - }, - - _setFilter : function(buttonContext) { - var mode = buttonContext.model.get('key'); - - this.collection.state.currentPage = 1; - var promise = this.collection.setFilterMode(mode); - - if (buttonContext) { - buttonContext.ui.icon.spinForPromise(promise); - } - } -}); diff --git a/src/UI/Activity/History/HistoryLayoutTemplate.hbs b/src/UI/Activity/History/HistoryLayoutTemplate.hbs deleted file mode 100644 index bffb274fe..000000000 --- a/src/UI/Activity/History/HistoryLayoutTemplate.hbs +++ /dev/null @@ -1,11 +0,0 @@ -<div id="x-history-toolbar"/> -<div class="row"> - <div class="col-md-12"> - <div id="x-history" class="table-responsive"/> - </div> -</div> -<div class="row"> - <div class="col-md-12"> - <div id="x-history-pager"/> - </div> -</div> diff --git a/src/UI/Activity/History/HistoryModel.js b/src/UI/Activity/History/HistoryModel.js deleted file mode 100644 index e37fbedd5..000000000 --- a/src/UI/Activity/History/HistoryModel.js +++ /dev/null @@ -1,12 +0,0 @@ -var Backbone = require('backbone'); -var ArtistModel = require('../../Artist/ArtistModel'); -var AlbumModel = require('../../Artist/AlbumModel'); - -module.exports = Backbone.Model.extend({ - parse : function(model) { - model.artist = new ArtistModel(model.artist); - model.album = new AlbumModel(model.album); - model.album.set('artist', model.artist); - return model; - } -}); \ No newline at end of file diff --git a/src/UI/Activity/History/HistoryQualityCell.js b/src/UI/Activity/History/HistoryQualityCell.js deleted file mode 100644 index c65aa042b..000000000 --- a/src/UI/Activity/History/HistoryQualityCell.js +++ /dev/null @@ -1,30 +0,0 @@ -var NzbDroneCell = require('../../Cells/NzbDroneCell'); - -module.exports = NzbDroneCell.extend({ - className : 'history-quality-cell', - - render : function() { - - var title = ''; - var quality = this.model.get('quality'); - var revision = quality.revision; - - if (revision.real && revision.real > 0) { - title += ' REAL'; - } - - if (revision.version && revision.version > 1) { - title += ' PROPER'; - } - - title = title.trim(); - - if (this.model.get('qualityCutoffNotMet')) { - this.$el.html('<span class="badge badge-inverse" title="{0}">{1}</span>'.format(title, quality.quality.name)); - } else { - this.$el.html('<span class="badge" title="{0}">{1}</span>'.format(title, quality.quality.name)); - } - - return this; - } -}); \ No newline at end of file diff --git a/src/UI/Activity/Queue/ProgressCell.js b/src/UI/Activity/Queue/ProgressCell.js deleted file mode 100644 index 1f69bf017..000000000 --- a/src/UI/Activity/Queue/ProgressCell.js +++ /dev/null @@ -1,23 +0,0 @@ -var NzbDroneCell = require('../../Cells/NzbDroneCell'); - -module.exports = NzbDroneCell.extend({ - className : 'progress-cell', - - render : function() { - this.$el.empty(); - - if (this.cellValue) { - - var status = this.model.get('status').toLowerCase(); - - if (status === 'downloading') { - var progress = 100 - (this.model.get('sizeleft') / this.model.get('size') * 100); - - this.$el.html('<div class="progress" title="{0}%">'.format(progress.toFixed(1)) + - '<div class="progress-bar progress-bar-purple" style="width: {0}%;"></div></div>'.format(progress)); - } - } - - return this; - } -}); diff --git a/src/UI/Activity/Queue/QueueActionsCell.js b/src/UI/Activity/Queue/QueueActionsCell.js deleted file mode 100644 index 9653a71bd..000000000 --- a/src/UI/Activity/Queue/QueueActionsCell.js +++ /dev/null @@ -1,59 +0,0 @@ -'use strict'; - -var $ = require('jquery'); -var _ = require('underscore'); -var vent = require('../../vent'); -var TemplatedCell = require('../../Cells/TemplatedCell'); -var RemoveFromQueueView = require('./RemoveFromQueueView'); - -module.exports = TemplatedCell.extend({ - - template : 'Activity/Queue/QueueActionsCellTemplate', - className : 'queue-actions-cell', - - events : { - 'click .x-remove' : '_remove', - 'click .x-manual-import' : '_manualImport', - 'click .x-grab' : '_grab' - }, - - ui : { - import : '.x-import', - grab : '.x-grab' - }, - - _remove : function() { - var showBlacklist = this.model.get('status') !== 'Pending'; - - vent.trigger(vent.Commands.OpenModalCommand, new RemoveFromQueueView({ - model : this.model, - showBlacklist : showBlacklist - })); - }, - - _manualImport : function () { - vent.trigger(vent.Commands.ShowManualImport, - { - downloadId: this.model.get('downloadId'), - title: this.model.get('title') - }); - }, - - _grab : function() { - var self = this; - var data = _.omit(this.model.toJSON(), 'artist', 'album'); - - var promise = $.ajax({ - url : window.NzbDrone.ApiRoot + '/queue/grab', - type : 'POST', - data : JSON.stringify(data) - }); - - this.$(this.ui.grab).spinForPromise(promise); - - promise.success(function() { - //find models that have the same series id and episode ids and remove them - self.model.trigger('destroy', self.model); - }); - } -}); diff --git a/src/UI/Activity/Queue/QueueActionsCellTemplate.hbs b/src/UI/Activity/Queue/QueueActionsCellTemplate.hbs deleted file mode 100644 index 0f2301929..000000000 --- a/src/UI/Activity/Queue/QueueActionsCellTemplate.hbs +++ /dev/null @@ -1,12 +0,0 @@ -{{#if_eq status compare="Completed"}} - {{#if_eq trackedDownloadStatus compare="Warning"}} - <i class="icon-lidarr-import-manual x-manual-import" title="Manual import"></i> - {{/if_eq}} -{{/if_eq}} - -{{#if_eq status compare="Pending"}} - <i class="icon-lidarr-download x-grab" title="Add to download queue (Override Delay Profile)"></i> - <i class="icon-lidarr-delete x-remove" title="Remove pending release"></i> -{{else}} - <i class="icon-lidarr-delete x-remove" title="Remove from download client"></i> -{{/if_eq}} diff --git a/src/UI/Activity/Queue/QueueCollection.js b/src/UI/Activity/Queue/QueueCollection.js deleted file mode 100644 index b0e47070a..000000000 --- a/src/UI/Activity/Queue/QueueCollection.js +++ /dev/null @@ -1,79 +0,0 @@ -var _ = require('underscore'); -var PageableCollection = require('backbone.pageable'); -//var PageableCollection = require('../../Shared/Grid/LidarrPageableCollection'); -var QueueModel = require('./QueueModel'); -var FormatHelpers = require('../../Shared/FormatHelpers'); -var AsSortedCollection = require('../../Mixins/AsSortedCollection'); -var AsPageableCollection = require('../../Mixins/AsPageableCollection'); -var moment = require('moment'); - -require('../../Mixins/backbone.signalr.mixin'); - -var QueueCollection = PageableCollection.extend({ - url : window.NzbDrone.ApiRoot + '/queue', - model : QueueModel, - - state : { - pageSize : 15, - sortKey: 'timeleft' - }, - - mode : 'client', - - findEpisode : function(albumId) { - return _.find(this.fullCollection.models, function(queueModel) { - return queueModel.get('album').id === albumId; - }); - }, - - sortMappings : { - artist : { - sortValue : function(model, attr) { - var artist = model.get(attr); - - return artist.get('sortName'); - } - }, - - albumTitle : { - sortValue : function(model, attr) { - var album = model.get('album'); - - return album.get('title'); - } - }, - - timeleft : { - sortValue : function(model, attr) { - var eta = model.get('estimatedCompletionTime'); - - if (eta) { - return moment(eta).unix(); - } - - return Number.MAX_VALUE; - } - }, - - sizeleft : { - sortValue : function(model, attr) { - var size = model.get('size'); - var sizeleft = model.get('sizeleft'); - - if (size && sizeleft) { - return sizeleft / size; - } - - return 0; - } - } - } -}); - -QueueCollection = AsSortedCollection.call(QueueCollection); -QueueCollection = AsPageableCollection.call(QueueCollection); - -var collection = new QueueCollection().bindSignalR(); -collection.fetch(); - -module.exports = collection; \ No newline at end of file diff --git a/src/UI/Activity/Queue/QueueLayout.js b/src/UI/Activity/Queue/QueueLayout.js deleted file mode 100644 index 028bc40bc..000000000 --- a/src/UI/Activity/Queue/QueueLayout.js +++ /dev/null @@ -1,91 +0,0 @@ -var Marionette = require('marionette'); -var Backgrid = require('backgrid'); -var QueueCollection = require('./QueueCollection'); -var ArtistTitleCell = require('../../Cells/ArtistTitleCell'); -var AlbumTitleCell = require('../../Cells/AlbumTitleCell'); -var QualityCell = require('../../Cells/QualityCell'); -var QueueStatusCell = require('./QueueStatusCell'); -var QueueActionsCell = require('./QueueActionsCell'); -var TimeleftCell = require('./TimeleftCell'); -var ProgressCell = require('./ProgressCell'); -var ProtocolCell = require('../../Release/ProtocolCell'); -var GridPager = require('../../Shared/Grid/Pager'); - -module.exports = Marionette.Layout.extend({ - template : 'Activity/Queue/QueueLayoutTemplate', - - regions : { - table : '#x-queue', - pager : '#x-queue-pager' - }, - - columns : [ - { - name : 'status', - label : '', - cell : QueueStatusCell, - cellValue : 'this' - }, - { - name : 'artist', - label : 'Artist', - cell : ArtistTitleCell - }, - { - name : 'albumTitle', - label : 'Album Title', - cell : AlbumTitleCell, - cellValue : 'album' - }, - { - name : 'quality', - label : 'Quality', - cell : QualityCell, - sortable : false - }, - { - name : 'protocol', - label : 'Protocol', - cell : ProtocolCell - }, - { - name : 'timeleft', - label : 'Time Left', - cell : TimeleftCell, - cellValue : 'this' - }, - { - name : 'sizeleft', - label : 'Progress', - cell : ProgressCell, - cellValue : 'this' - }, - { - name : 'status', - label : '', - cell : QueueActionsCell, - cellValue : 'this' - } - ], - - initialize : function() { - this.listenTo(QueueCollection, 'sync', this._showTable); - }, - - onShow : function() { - this._showTable(); - }, - - _showTable : function() { - this.table.show(new Backgrid.Grid({ - columns : this.columns, - collection : QueueCollection, - className : 'table table-hover' - })); - - this.pager.show(new GridPager({ - columns : this.columns, - collection : QueueCollection - })); - } -}); diff --git a/src/UI/Activity/Queue/QueueLayoutTemplate.hbs b/src/UI/Activity/Queue/QueueLayoutTemplate.hbs deleted file mode 100644 index e8e6a3c12..000000000 --- a/src/UI/Activity/Queue/QueueLayoutTemplate.hbs +++ /dev/null @@ -1,11 +0,0 @@ -<div class="row"> - <div class="col-md-12"> - <div id="x-queue" class="queue table-responsive"/> - </div> -</div> - -<div class="row"> - <div class="col-md-12"> - <div id="x-queue-pager"/> - </div> -</div> \ No newline at end of file diff --git a/src/UI/Activity/Queue/QueueModel.js b/src/UI/Activity/Queue/QueueModel.js deleted file mode 100644 index e37fbedd5..000000000 --- a/src/UI/Activity/Queue/QueueModel.js +++ /dev/null @@ -1,12 +0,0 @@ -var Backbone = require('backbone'); -var ArtistModel = require('../../Artist/ArtistModel'); -var AlbumModel = require('../../Artist/AlbumModel'); - -module.exports = Backbone.Model.extend({ - parse : function(model) { - model.artist = new ArtistModel(model.artist); - model.album = new AlbumModel(model.album); - model.album.set('artist', model.artist); - return model; - } -}); \ No newline at end of file diff --git a/src/UI/Activity/Queue/QueueStatusCell.js b/src/UI/Activity/Queue/QueueStatusCell.js deleted file mode 100644 index a345cac3c..000000000 --- a/src/UI/Activity/Queue/QueueStatusCell.js +++ /dev/null @@ -1,81 +0,0 @@ -var Marionette = require('marionette'); -var NzbDroneCell = require('../../Cells/NzbDroneCell'); - -module.exports = NzbDroneCell.extend({ - className : 'queue-status-cell', - template : 'Activity/Queue/QueueStatusCellTemplate', - - render : function() { - this.$el.empty(); - - if (this.cellValue) { - var status = this.cellValue.get('status').toLowerCase(); - var trackedDownloadStatus = this.cellValue.has('trackedDownloadStatus') ? this.cellValue.get('trackedDownloadStatus').toLowerCase() : 'ok'; - var icon = 'icon-lidarr-downloading'; - var title = 'Downloading'; - var itemTitle = this.cellValue.get('title'); - var content = itemTitle; - - if (status === 'paused') { - icon = 'icon-lidarr-paused'; - title = 'Paused'; - } - - if (status === 'queued') { - icon = 'icon-lidarr-queued'; - title = 'Queued'; - } - - if (status === 'completed') { - icon = 'icon-lidarr-downloaded'; - title = 'Downloaded'; - } - - if (status === 'pending') { - icon = 'icon-lidarr-pending'; - title = 'Pending'; - } - - if (status === 'failed') { - icon = 'icon-lidarr-download-failed'; - title = 'Download failed'; - } - - if (status === 'warning') { - icon = 'icon-lidarr-download-warning'; - title = 'Download warning: check download client for more details'; - } - - if (trackedDownloadStatus === 'warning') { - icon += ' icon-lidarr-warning'; - - this.templateFunction = Marionette.TemplateCache.get(this.template); - content = this.templateFunction(this.cellValue.toJSON()); - } - - if (trackedDownloadStatus === 'error') { - if (status === 'completed') { - icon = 'icon-lidarr-import-failed'; - title = 'Import failed: ' + itemTitle; - } else { - icon = 'icon-lidarr-download-failed'; - title = 'Download failed'; - } - - this.templateFunction = Marionette.TemplateCache.get(this.template); - content = this.templateFunction(this.cellValue.toJSON()); - } - - this.$el.html('<i class="{0}"></i>'.format(icon)); - this.$el.popover({ - content : content, - html : true, - trigger : 'hover', - title : title, - placement : 'right', - container : this.$el - }); - } - return this; - } -}); \ No newline at end of file diff --git a/src/UI/Activity/Queue/QueueStatusCellTemplate.hbs b/src/UI/Activity/Queue/QueueStatusCellTemplate.hbs deleted file mode 100644 index 477fdd028..000000000 --- a/src/UI/Activity/Queue/QueueStatusCellTemplate.hbs +++ /dev/null @@ -1,8 +0,0 @@ -{{#each statusMessages}} - {{title}} - <ul> - {{#each messages}} - <li>{{this}}</li> - {{/each}} - </ul> -{{/each}} \ No newline at end of file diff --git a/src/UI/Activity/Queue/QueueView.js b/src/UI/Activity/Queue/QueueView.js deleted file mode 100644 index ccddebbc9..000000000 --- a/src/UI/Activity/Queue/QueueView.js +++ /dev/null @@ -1,40 +0,0 @@ -var _ = require('underscore'); -var Marionette = require('marionette'); -var QueueCollection = require('./QueueCollection'); - -module.exports = Marionette.ItemView.extend({ - tagName : 'span', - - initialize : function() { - this.listenTo(QueueCollection, 'sync', this.render); - QueueCollection.fetch(); - }, - - render : function() { - this.$el.empty(); - - if (QueueCollection.length === 0) { - return this; - } - - var count = QueueCollection.fullCollection.length; - var label = 'label-info'; - - var errors = QueueCollection.fullCollection.some(function(model) { - return model.has('trackedDownloadStatus') && model.get('trackedDownloadStatus').toLowerCase() === 'error'; - }); - - var warnings = QueueCollection.fullCollection.some(function(model) { - return model.has('trackedDownloadStatus') && model.get('trackedDownloadStatus').toLowerCase() === 'warning'; - }); - - if (errors) { - label = 'label-danger'; - } else if (warnings) { - label = 'label-warning'; - } - - this.$el.html('<span class="label {0}">{1}</span>'.format(label, count)); - return this; - } -}); \ No newline at end of file diff --git a/src/UI/Activity/Queue/RemoveFromQueueView.js b/src/UI/Activity/Queue/RemoveFromQueueView.js deleted file mode 100644 index 571738d7a..000000000 --- a/src/UI/Activity/Queue/RemoveFromQueueView.js +++ /dev/null @@ -1,34 +0,0 @@ -var vent = require('../../vent'); -var Marionette = require('marionette'); - -module.exports = Marionette.ItemView.extend({ - template : 'Activity/Queue/RemoveFromQueueViewTemplate', - - events : { - 'click .x-confirm-remove' : 'removeItem' - }, - - ui : { - blacklist : '.x-blacklist', - indicator : '.x-indicator' - }, - - initialize : function(options) { - this.templateHelpers = { - showBlacklist : options.showBlacklist - }; - }, - - removeItem : function() { - var blacklist = this.ui.blacklist.prop('checked') || false; - - this.ui.indicator.show(); - - this.model.destroy({ - data : { 'blacklist' : blacklist }, - wait : true - }).done(function() { - vent.trigger(vent.Commands.CloseModalCommand); - }); - } -}); diff --git a/src/UI/Activity/Queue/RemoveFromQueueViewTemplate.hbs b/src/UI/Activity/Queue/RemoveFromQueueViewTemplate.hbs deleted file mode 100644 index b7853e2fa..000000000 --- a/src/UI/Activity/Queue/RemoveFromQueueViewTemplate.hbs +++ /dev/null @@ -1,49 +0,0 @@ -<div class="modal-content"> - <div class="modal-header"> - <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> - <h3>{{title}}</h3> - </div> - <div class="modal-body remove-from-queue-modal"> - - <div class="row"> - <div class="col-sm-12"> - Are you sure you want to remove '{{title}}'? - </div> - </div> - - {{#if showBlacklist}} - <div class="row"> - <div class="col-sm-12"> - <div class="form-horizontal"> - <div class="form-group"> - <label class="col-sm-4 control-label">Blacklist release</label> - - <div class="col-sm-8"> - <div class="input-group"> - <label class="checkbox toggle well"> - <input type="checkbox" class="x-blacklist"/> - <p> - <span>Yes</span> - <span>No</span> - </p> - - <div class="btn slide-button btn-danger"/> - </label> - - <span class="help-inline-checkbox"> - <i class="icon-lidarr-form-info" title="Do you want to blacklist this release?"/> - </span> - </div> - </div> - </div> - </div> - </div> - </div> - {{/if}} - </div> - <div class="modal-footer"> - <span class="indicator x-indicator"><i class="icon-lidarr-spinner fa-spin"></i></span> - <button class="btn" data-dismiss="modal">Cancel</button> - <button class="btn btn-danger x-confirm-remove">Remove</button> - </div> -</div> diff --git a/src/UI/Activity/Queue/TimeleftCell.js b/src/UI/Activity/Queue/TimeleftCell.js deleted file mode 100644 index 766d9df2d..000000000 --- a/src/UI/Activity/Queue/TimeleftCell.js +++ /dev/null @@ -1,33 +0,0 @@ -var NzbDroneCell = require('../../Cells/NzbDroneCell'); -var moment = require('moment'); -var UiSettingsModel = require('../../Shared/UiSettingsModel'); -var FormatHelpers = require('../../Shared/FormatHelpers'); - -module.exports = NzbDroneCell.extend({ - className : 'timeleft-cell', - - render : function() { - this.$el.empty(); - - if (this.cellValue) { - if (this.cellValue.get('status').toLowerCase() === 'pending') { - var ect = this.cellValue.get('estimatedCompletionTime'); - var time = '{0} at {1}'.format(FormatHelpers.relativeDate(ect), moment(ect).format(UiSettingsModel.time(true, false))); - this.$el.html('<div title="Delaying download till {0}">-</div>'.format(time)); - return this; - } - - var timeleft = this.cellValue.get('timeleft'); - var totalSize = FormatHelpers.bytes(this.cellValue.get('size'), 2); - var remainingSize = FormatHelpers.bytes(this.cellValue.get('sizeleft'), 2); - - if (timeleft === undefined) { - this.$el.html('-'); - } else { - this.$el.html('<span title="{1} / {2}">{0}</span>'.format(timeleft, remainingSize, totalSize)); - } - } - - return this; - } -}); \ No newline at end of file diff --git a/src/UI/Activity/activity.less b/src/UI/Activity/activity.less deleted file mode 100644 index cb4c538cb..000000000 --- a/src/UI/Activity/activity.less +++ /dev/null @@ -1,27 +0,0 @@ - -.queue-status-cell .popover { - max-width : 800px; -} - -.queue { - .protocol-cell { - text-align : center; - width : 80px; - } - - .episode-number-cell { - min-width : 90px; - } -} - -.remove-from-queue-modal { - .form-horizontal { - margin-top : 20px; - } -} - -.history-detail-modal { - .info { - word-wrap: break-word; - } -} diff --git a/src/UI/AddArtist/AddArtistCollection.js b/src/UI/AddArtist/AddArtistCollection.js deleted file mode 100644 index a243649f4..000000000 --- a/src/UI/AddArtist/AddArtistCollection.js +++ /dev/null @@ -1,23 +0,0 @@ -var Backbone = require('backbone'); -var ArtistModel = require('../Artist/ArtistModel'); -var _ = require('underscore'); - -module.exports = Backbone.Collection.extend({ - url : window.NzbDrone.ApiRoot + '/artist/lookup', - model : ArtistModel, - - parse : function(response) { - var self = this; - - _.each(response, function(model) { - model.id = undefined; - - if (self.unmappedFolderModel) { - model.path = self.unmappedFolderModel.get('folder').path; - } - }); - console.log('response: ', response); - - return response; - } -}); \ No newline at end of file diff --git a/src/UI/AddArtist/AddArtistLayout.js b/src/UI/AddArtist/AddArtistLayout.js deleted file mode 100644 index da539c101..000000000 --- a/src/UI/AddArtist/AddArtistLayout.js +++ /dev/null @@ -1,66 +0,0 @@ -var vent = require('vent'); -var AppLayout = require('../AppLayout'); -var Marionette = require('marionette'); -var RootFolderLayout = require('./RootFolders/RootFolderLayout'); -var ExistingArtistCollectionView = require('./Existing/AddExistingArtistCollectionView'); -var AddArtistView = require('./AddArtistView'); -var ProfileCollection = require('../Profile/ProfileCollection'); -var RootFolderCollection = require('./RootFolders/RootFolderCollection'); -var BulkImportView = require('./BulkImport/BulkImportView'); -require('../Artist/ArtistCollection'); - -module.exports = Marionette.Layout.extend({ - template : 'AddArtist/AddArtistLayoutTemplate', - - regions : { - workspace : '#add-artist-workspace' - }, - - events : { - 'click .x-import' : '_importArtist', - 'click .x-bulk-import' : '_bulkImportArtist', - 'click .x-add-new' : '_addArtist' - }, - - attributes : { - id : 'add-artist-screen' - }, - - initialize : function() { - ProfileCollection.fetch(); - RootFolderCollection.fetch().done(function() { - RootFolderCollection.synced = true; - }); - }, - - onShow : function() { - this.workspace.show(new AddArtistView()); - }, - - _folderSelected : function(options) { - vent.trigger(vent.Commands.CloseModalCommand); - - this.workspace.show(new ExistingArtistCollectionView({ model : options.model })); - }, - - _bulkFolderSelected : function(options) { - vent.trigger(vent.Commands.CloseModalCommand); - this.workspace.show(new BulkImportView({ model : options.model})); - }, - - _importArtist : function() { - this.rootFolderLayout = new RootFolderLayout(); - this.listenTo(this.rootFolderLayout, 'folderSelected', this._folderSelected); - AppLayout.modalRegion.show(this.rootFolderLayout); - }, - - _addArtist : function() { - this.workspace.show(new AddArtistView()); - }, - - _bulkImportArtist : function() { - this.bulkRootFolderLayout = new RootFolderLayout(); - this.listenTo(this.bulkRootFolderLayout, 'folderSelected', this._bulkFolderSelected); - AppLayout.modalRegion.show(this.bulkRootFolderLayout); - } -}); \ No newline at end of file diff --git a/src/UI/AddArtist/AddArtistLayoutTemplate.hbs b/src/UI/AddArtist/AddArtistLayoutTemplate.hbs deleted file mode 100644 index 313cccef4..000000000 --- a/src/UI/AddArtist/AddArtistLayoutTemplate.hbs +++ /dev/null @@ -1,18 +0,0 @@ -<div class="row"> - <div class="col-md-12"> - <div class="btn-group add-artist-btn-group btn-group-lg btn-block"> - <button type="button" class="btn btn-default col-md-7 col-xs-4 add-artist-import-btn x-import"> - <i class="icon-lidarr-hdd"/> - Import existing artists on disk - </button> - <button class="btn btn-default col-md-3 col-xs-4 x-bulk-import"><i class="icon-lidarr-view-list hidden-xs"></i> Bulk Import Artists</button> - <button class="btn btn-default col-md-2 col-xs-4 x-add-new"><i class="icon-lidarr-active hidden-xs"></i> Add New Artist</button> - </div> - </div> -</div> -<div class="row"> - <div class="col-md-12"> - <div id="add-artist-workspace"></div> - </div> -</div> - diff --git a/src/UI/AddArtist/AddArtistView.js b/src/UI/AddArtist/AddArtistView.js deleted file mode 100644 index 85e47a02d..000000000 --- a/src/UI/AddArtist/AddArtistView.js +++ /dev/null @@ -1,183 +0,0 @@ -var _ = require('underscore'); -var vent = require('vent'); -var Marionette = require('marionette'); -var AddArtistCollection = require('./AddArtistCollection'); -var SearchResultCollectionView = require('./SearchResultCollectionView'); -var EmptyView = require('./EmptyView'); -var NotFoundView = require('./NotFoundView'); -var ErrorView = require('./ErrorView'); -var LoadingView = require('../Shared/LoadingView'); - -module.exports = Marionette.Layout.extend({ - template : 'AddArtist/AddArtistViewTemplate', - - regions : { - searchResult : '#search-result' - }, - - ui : { - artistSearch : '.x-artist-search', - searchBar : '.x-search-bar', - loadMore : '.x-load-more' - }, - - events : { - 'click .x-load-more' : '_onLoadMore' - }, - - initialize : function(options) { - this.isExisting = options.isExisting; - this.collection = new AddArtistCollection(); - console.log('this.collection:', this.collection); - - if (this.isExisting) { - this.collection.unmappedFolderModel = this.model; - } - - if (this.isExisting) { - this.className = 'existing-artist'; - } else { - this.className = 'new-artist'; - } - - this.listenTo(vent, vent.Events.ArtistAdded, this._onArtistAdded); - this.listenTo(this.collection, 'sync', this._showResults); - - this.resultCollectionView = new SearchResultCollectionView({ - collection : this.collection, - isExisting : this.isExisting - }); - - this.throttledSearch = _.debounce(this.search, 1000, { trailing : true }).bind(this); - }, - - onRender : function() { - var self = this; - - this.$el.addClass(this.className); - - this.ui.artistSearch.keyup(function(e) { - - if (_.contains([ - 9, - 16, - 17, - 18, - 19, - 20, - 33, - 34, - 35, - 36, - 37, - 38, - 39, - 40, - 91, - 92, - 93 - ], e.keyCode)) { - return; - } - - self._abortExistingSearch(); - self.throttledSearch({ - term : self.ui.artistSearch.val() - }); - }); - - this._clearResults(); - - if (this.isExisting) { - this.ui.searchBar.hide(); - } - }, - - onShow : function() { - this.ui.artistSearch.focus(); - }, - - search : function(options) { - var self = this; - - this.collection.reset(); - - if (!options.term || options.term === this.collection.term) { - return Marionette.$.Deferred().resolve(); - } - - this.searchResult.show(new LoadingView()); - this.collection.term = options.term; - this.currentSearchPromise = this.collection.fetch({ - data : { term : options.term } - }); - - this.currentSearchPromise.fail(function() { - self._showError(); - }); - - return this.currentSearchPromise; - }, - - _onArtistAdded : function(options) { - if (this.isExisting && options.artist.get('path') === this.model.get('folder').path) { - this.close(); - } - - else if (!this.isExisting) { - this.collection.term = ''; - this.collection.reset(); - this._clearResults(); - this.ui.artistSearch.val(''); - this.ui.artistSearch.focus(); - } - }, - - _onLoadMore : function() { - var showingAll = this.resultCollectionView.showMore(); - this.ui.searchBar.show(); - - if (showingAll) { - this.ui.loadMore.hide(); - } - }, - - _clearResults : function() { - if (!this.isExisting) { - this.searchResult.show(new EmptyView()); - } else { - this.searchResult.close(); - } - }, - - _showResults : function() { - if (!this.isClosed) { - if (this.collection.length === 0) { - this.ui.searchBar.show(); - this.searchResult.show(new NotFoundView({ term : this.collection.term })); - } else { - this.searchResult.show(this.resultCollectionView); - if (!this.showingAll && this.isExisting) { - this.ui.loadMore.show(); - } - } - } - }, - - _abortExistingSearch : function() { - if (this.currentSearchPromise && this.currentSearchPromise.readyState > 0 && this.currentSearchPromise.readyState < 4) { - console.log('aborting previous pending search request.'); - this.currentSearchPromise.abort(); - } else { - this._clearResults(); - } - }, - - _showError : function() { - if (!this.isClosed) { - this.ui.searchBar.show(); - this.searchResult.show(new ErrorView({ term : this.collection.term })); - this.collection.term = ''; - } - } -}); \ No newline at end of file diff --git a/src/UI/AddArtist/AddArtistViewTemplate.hbs b/src/UI/AddArtist/AddArtistViewTemplate.hbs deleted file mode 100644 index adadf0569..000000000 --- a/src/UI/AddArtist/AddArtistViewTemplate.hbs +++ /dev/null @@ -1,24 +0,0 @@ -{{#if folder.path}} -<div class="unmapped-folder-path"> - <div class="col-md-12"> - {{folder.path}} - </div> -</div>{{/if}} -<div class="x-search-bar"> - <div class="input-group input-group-lg add-artist-search"> - <span class="input-group-addon"><i class="icon-lidarr-search"/></span> - - {{#if folder}} - <input type="text" class="form-control x-artist-search" value="{{folder.name}}"> - {{else}} - <input type="text" class="form-control x-artist-search" placeholder="Start typing the name of an artist or album..."> - {{/if}} - </div> -</div> -<div class="row"> - <div id="search-result" class="result-list col-md-12"/> -</div> -<div class="btn btn-block text-center new-artist-loadmore x-load-more" style="display: none;"> - <i class="icon-lidarr-load-more"/> - more -</div> diff --git a/src/UI/AddArtist/ArtistTypeSelectionPartial.hbs b/src/UI/AddArtist/ArtistTypeSelectionPartial.hbs deleted file mode 100644 index f8cadd547..000000000 --- a/src/UI/AddArtist/ArtistTypeSelectionPartial.hbs +++ /dev/null @@ -1,3 +0,0 @@ -<select class="form-control col-md-2 x-artist-type" name="artistType"> - <option value="standard">Standard</option> -</select> diff --git a/src/UI/AddArtist/BulkImport/ArtistPathCell.js b/src/UI/AddArtist/BulkImport/ArtistPathCell.js deleted file mode 100644 index debe25ae1..000000000 --- a/src/UI/AddArtist/BulkImport/ArtistPathCell.js +++ /dev/null @@ -1,7 +0,0 @@ -var TemplatedCell = require('../../Cells/TemplatedCell'); - -module.exports = TemplatedCell.extend({ - className : 'artist-title-cell', - template : 'AddArtist/BulkImport/ArtistPathTemplate', - -}); diff --git a/src/UI/AddArtist/BulkImport/ArtistPathTemplate.hbs b/src/UI/AddArtist/BulkImport/ArtistPathTemplate.hbs deleted file mode 100644 index 53fa29105..000000000 --- a/src/UI/AddArtist/BulkImport/ArtistPathTemplate.hbs +++ /dev/null @@ -1 +0,0 @@ -{{path}}<br> diff --git a/src/UI/AddArtist/BulkImport/BulkImportArtistNameCell.js b/src/UI/AddArtist/BulkImport/BulkImportArtistNameCell.js deleted file mode 100644 index 5490439e3..000000000 --- a/src/UI/AddArtist/BulkImport/BulkImportArtistNameCell.js +++ /dev/null @@ -1,21 +0,0 @@ -var NzbDroneCell = require('../../Cells/NzbDroneCell'); -var BulkImportCollection = require('./BulkImportCollection'); - -module.exports = NzbDroneCell.extend({ - className : 'artist-title-cell', - - render : function() { - var collection = this.model.collection; - this.listenTo(collection, 'sync', this._renderCell); - - this._renderCell(); - - return this; - }, - - _renderCell : function() { - this.$el.empty(); - - this.$el.html('<a href="https://www.musicbrainz.org/artist/' + this.cellValue.get('foreignArtistId') +'">' + this.cellValue.get('name') +'</a><br><span class="hint">' + this.cellValue.get('overview') + '</span>'); - } -}); diff --git a/src/UI/AddArtist/BulkImport/BulkImportCollection.js b/src/UI/AddArtist/BulkImport/BulkImportCollection.js deleted file mode 100644 index d6fef1faa..000000000 --- a/src/UI/AddArtist/BulkImport/BulkImportCollection.js +++ /dev/null @@ -1,49 +0,0 @@ -var _ = require('underscore'); -var PageableCollection = require('backbone.pageable'); -var ArtistModel = require('../../Artist/ArtistModel'); -var AsSortedCollection = require('../../Mixins/AsSortedCollection'); -var AsPageableCollection = require('../../Mixins/AsPageableCollection'); -var AsPersistedStateCollection = require('../../Mixins/AsPersistedStateCollection'); - -var BulkImportCollection = PageableCollection.extend({ - url : window.NzbDrone.ApiRoot + '/artist/bulkimport', - model : ArtistModel, - tableName : 'bulkimport', - - state : { - pageSize : 100000, - sortKey: 'sortName', - firstPage: 1 - }, - - fetch : function(options) { - - options = options || {}; - - var data = options.data || {}; - - if (!data.id || !data.folder) { - data.id = this.folderId; - data.folder = this.folder; - } - - options.data = data; - return PageableCollection.prototype.fetch.call(this, options); - }, - - parseLinks : function(options) { - - return { - first : this.url, - next: this.url, - last : this.url - }; - } -}); - - -BulkImportCollection = AsSortedCollection.call(BulkImportCollection); -BulkImportCollection = AsPageableCollection.call(BulkImportCollection); -BulkImportCollection = AsPersistedStateCollection.call(BulkImportCollection); - -module.exports = BulkImportCollection; diff --git a/src/UI/AddArtist/BulkImport/BulkImportMonitorCell.js b/src/UI/AddArtist/BulkImport/BulkImportMonitorCell.js deleted file mode 100644 index db9860ffe..000000000 --- a/src/UI/AddArtist/BulkImport/BulkImportMonitorCell.js +++ /dev/null @@ -1,65 +0,0 @@ -var Backgrid = require('backgrid'); -var Config = require('../../Config'); -var _ = require('underscore'); -var vent = require('vent'); -var TemplatedCell = require('../../Cells/TemplatedCell'); -var NzbDroneCell = require('../../Cells/NzbDroneCell'); -var Marionette = require('marionette'); - -module.exports = TemplatedCell.extend({ - className : 'monitor-cell', - template : 'AddArtist/BulkImport/BulkImportMonitorCell', - - _orig : TemplatedCell.prototype.initialize, - _origRender : TemplatedCell.prototype.initialize, - - ui : { - monitor : '.x-monitor', - }, - - events: { 'change .x-monitor' : '_monitorChanged' }, - - initialize : function () { - this._orig.apply(this, arguments); - - this.defaultMonitor = Config.getValue(Config.Keys.MonitorEpisodes, 'all'); - - this.model.set('monitored', this._convertMonitorToBool(this.defaultMonitor)); - - this.$el.find('.x-monitor').val(this._convertBooltoMonitor(this.model.get('monitored'))); - }, - - _convertMonitorToBool : function(monitorString) { - return monitorString === 'all' ? true : false; - }, - - _convertBooltoMonitor : function(monitorBool) { - return monitorBool === true ? 'all' : 'none'; - }, - - _monitorChanged : function() { - Config.setValue(Config.Keys.MonitorEpisodes, this.$el.find('.x-monitor').val()); - this.defaultMonitor = this.$el.find('.x-monitor').val(); - this.model.set('monitored', this._convertMonitorToBool(this.$el.find('.x-monitor').val())); - }, - - render : function() { - var templateName = this.column.get('template') || this.template; - - this.templateFunction = Marionette.TemplateCache.get(templateName); - this.$el.empty(); - - if (this.cellValue) { - var data = this.cellValue.toJSON(); - var html = this.templateFunction(data); - this.$el.html(html); - } - - this.delegateEvents(); - - this.$el.find('.x-monitor').val(this._convertBooltoMonitor(this.model.get('monitored'))); - - return this; - } - -}); diff --git a/src/UI/AddArtist/BulkImport/BulkImportMonitorCellTemplate.hbs b/src/UI/AddArtist/BulkImport/BulkImportMonitorCellTemplate.hbs deleted file mode 100644 index 5ef509ce1..000000000 --- a/src/UI/AddArtist/BulkImport/BulkImportMonitorCellTemplate.hbs +++ /dev/null @@ -1,4 +0,0 @@ -<select class="col-md-2 form-control x-monitor"> - <option value="all">Yes</option> - <option value="none">No</option> -</select> diff --git a/src/UI/AddArtist/BulkImport/BulkImportProfileCell.js b/src/UI/AddArtist/BulkImport/BulkImportProfileCell.js deleted file mode 100644 index 682c475f9..000000000 --- a/src/UI/AddArtist/BulkImport/BulkImportProfileCell.js +++ /dev/null @@ -1,32 +0,0 @@ -var Backgrid = require('backgrid'); -var ProfileCollection = require('../../Profile/ProfileCollection'); -var Config = require('../../Config'); -var _ = require('underscore'); - -module.exports = Backgrid.SelectCell.extend({ - className : 'profile-cell', - - _orig : Backgrid.SelectCell.prototype.initialize, - - initialize : function () { - this._orig.apply(this, arguments); - - this.defaultProfile = Config.getValue(Config.Keys.DefaultProfileId); - if(ProfileCollection.get(this.defaultProfile)) - { - this.profile = this.defaultProfile; - } else { - this.profile = ProfileCollection.get(1); - } - - this.render(); - - }, - - optionValues : function() { - return _.map(ProfileCollection.models, function(model){ - return [model.get('name'), model.get('id')+""]; - }); - } - -}); diff --git a/src/UI/AddArtist/BulkImport/BulkImportProfileCellT.js b/src/UI/AddArtist/BulkImport/BulkImportProfileCellT.js deleted file mode 100644 index 4b0bd168b..000000000 --- a/src/UI/AddArtist/BulkImport/BulkImportProfileCellT.js +++ /dev/null @@ -1,77 +0,0 @@ -var Backgrid = require('backgrid'); -var ProfileCollection = require('../../Profile/ProfileCollection'); -var Config = require('../../Config'); -var _ = require('underscore'); -var vent = require('vent'); -var TemplatedCell = require('../../Cells/TemplatedCell'); -var NzbDroneCell = require('../../Cells/NzbDroneCell'); -var Marionette = require('marionette'); - -module.exports = TemplatedCell.extend({ - className : 'profile-cell', - template : 'AddArtist/BulkImport/BulkImportProfileCell', - - _orig : TemplatedCell.prototype.initialize, - _origRender : TemplatedCell.prototype.initialize, - - ui : { - profile : '.x-profile', - }, - - events: { 'change .x-profile' : '_profileChanged' }, - - initialize : function () { - this._orig.apply(this, arguments); - - this.listenTo(vent, Config.Events.ConfigUpdatedEvent, this._onConfigUpdated); - - this.defaultProfile = Config.getValue(Config.Keys.DefaultProfileId); - - this.profile = this.defaultProfile; - - if(ProfileCollection.get(this.defaultProfile)) - { - this.profile = this.defaultProfile; - this.model.set('profileId', this.defaultProfile); - } else { - this.profile = 1; - this.model.set('profileId', 1); - } - - this.$('.x-profile').val(this.model.get('profileId')); - - this.cellValue = ProfileCollection; - - }, - - _profileChanged : function() { - Config.setValue(Config.Keys.DefaultProfileId, this.$('.x-profile').val()); - this.model.set('profileId', this.$('.x-profile').val()); - }, - - _onConfigUpdated : function(options) { - if (options.key === Config.Keys.DefaultProfileId) { - this.defaultProfile = options.value; - } - }, - - render : function() { - var templateName = this.column.get('template') || this.template; - - this.cellValue = ProfileCollection; - - this.templateFunction = Marionette.TemplateCache.get(templateName); - this.$el.empty(); - - if (this.cellValue) { - var data = this.cellValue.toJSON(); - var html = this.templateFunction(data); - this.$el.html(html); - } - - this.delegateEvents(); - this.$('.x-profile').val(this.model.get('profileId')); - return this; - } - -}); diff --git a/src/UI/AddArtist/BulkImport/BulkImportProfileCellTemplate.hbs b/src/UI/AddArtist/BulkImport/BulkImportProfileCellTemplate.hbs deleted file mode 100644 index 7124319eb..000000000 --- a/src/UI/AddArtist/BulkImport/BulkImportProfileCellTemplate.hbs +++ /dev/null @@ -1,5 +0,0 @@ -<select class="col-md-2 form-control x-profile"> - {{#each this}} - <option value="{{id}}">{{name}}</option> - {{/each}} -</select> diff --git a/src/UI/AddArtist/BulkImport/BulkImportSelectAllCell.js b/src/UI/AddArtist/BulkImport/BulkImportSelectAllCell.js deleted file mode 100644 index d1435dd14..000000000 --- a/src/UI/AddArtist/BulkImport/BulkImportSelectAllCell.js +++ /dev/null @@ -1,54 +0,0 @@ -var $ = require('jquery'); -var _ = require('underscore'); -var SelectAllCell = require('../../Cells/SelectAllCell'); -var Backgrid = require('backgrid'); -var FullArtistCollection = require('../../Artist/ArtistCollection'); - - -module.exports = SelectAllCell.extend({ - _originalRender : SelectAllCell.prototype.render, - - _originalInit : SelectAllCell.prototype.initialize, - - initialize : function() { - this._originalInit.apply(this, arguments); - - this._refreshIsDuplicate(); - - this.listenTo(this.model, 'change', this._refresh); - }, - - onChange : function(e) { - if(!this.isDuplicate) { - var checked = $(e.target).prop('checked'); - this.$el.parent().toggleClass('selected', checked); - this.model.trigger('backgrid:selected', this.model, checked); - } else { - $(e.target).prop('checked', false); - } - }, - - render : function() { - this._originalRender.apply(this, arguments); - - this.$el.children(':first').prop('disabled', this.isDuplicate); - - if (!this.isDuplicate) { - this.$el.children(':first').prop('checked', this.isChecked); - } - - return this; - }, - - _refresh: function() { - this.isChecked = this.$el.children(':first').prop('checked'); - this._refreshIsDuplicate(); - this.render(); - }, - - _refreshIsDuplicate: function() { - var foreignArtistId = this.model.get('foreignArtistId'); - var existingArtist = FullArtistCollection.where({ foreignArtistId: foreignArtistId }); - this.isDuplicate = existingArtist.length > 0 ? true : false; - } -}); diff --git a/src/UI/AddArtist/BulkImport/BulkImportView.js b/src/UI/AddArtist/BulkImport/BulkImportView.js deleted file mode 100644 index 83fcd41f6..000000000 --- a/src/UI/AddArtist/BulkImport/BulkImportView.js +++ /dev/null @@ -1,191 +0,0 @@ -var $ = require('jquery'); -var _ = require('underscore'); -var Marionette = require('marionette'); -var Backgrid = require('backgrid'); -var ArtistNameCell = require('./BulkImportArtistNameCell'); -var BulkImportCollection = require('./BulkImportCollection'); -var ForeignIdCell = require('./ForeignIdCell'); -var GridPager = require('../../Shared/Grid/Pager'); -var SelectAllCell = require('./BulkImportSelectAllCell'); -var ProfileCell = require('./BulkImportProfileCellT'); -var MonitorCell = require('./BulkImportMonitorCell'); -var ArtistPathCell = require('./ArtistPathCell'); -var LoadingView = require('../../Shared/LoadingView'); -var EmptyView = require('./EmptyView'); -var ToolbarLayout = require('../../Shared/Toolbar/ToolbarLayout'); -var CommandController = require('../../Commands/CommandController'); -var Messenger = require('../../Shared/Messenger'); -var ArtistCollection = require('../../Artist/ArtistCollection'); -var ProfileCollection = require('../../Profile/ProfileCollection'); - -require('backgrid.selectall'); -require('../../Mixins/backbone.signalr.mixin'); - -module.exports = Marionette.Layout.extend({ - template : 'AddArtist/BulkImport/BulkImportViewTemplate', - - regions : { - toolbar : '#x-toolbar', - table : '#x-artists-bulk', - }, - - ui : { - addSelectdBtn : '.x-add-selected' - }, - - initialize : function(options) { - ProfileCollection.fetch(); - this.bulkImportCollection = new BulkImportCollection().bindSignalR({ updateOnly : true }); - this.model = options.model; - this.folder = this.model.get('path'); - this.folderId = this.model.get('id'); - this.bulkImportCollection.folderId = this.folderId; - this.bulkImportCollection.folder = this.folder; - this.bulkImportCollection.fetch(); - this.listenTo(this.bulkImportCollection, {'sync' : this._showContent, 'error' : this._showContent, 'backgrid:selected' : this._select}); - }, - - columns : [ - { - name : '', - cell : SelectAllCell, - headerCell : 'select-all', - sortable : false, - cellValue : 'this' - }, - { - name : 'movie', - label : 'Artist', - cell : ArtistNameCell, - cellValue : 'this', - sortable : false - }, - { - name : 'path', - label : 'Path', - cell : ArtistPathCell, - cellValue : 'this', - sortable : false - }, - { - name : 'foreignArtistId', - label : 'MB Id', - cell : ForeignIdCell, - cellValue : 'this', - sortable: false - }, - { - name :'monitor', - label: 'Monitor', - cell : MonitorCell, - cellValue : 'this', - sortable: false - }, - { - name : 'profileId', - label : 'Profile', - cell : ProfileCell, - cellValue : 'this', - sortable: false - } - ], - - _showContent : function() { - this._showToolbar(); - this._showTable(); - }, - - onShow : function() { - this.table.show(new LoadingView()); - }, - - _showToolbar : function() { - var leftSideButtons = { - type : 'default', - storeState: false, - collapse : true, - items : [ - { - title : 'Add Selected', - icon : 'icon-lidarr-add', - callback : this._addSelected, - ownerContext : this, - className : 'x-add-selected' - } - ] - }; - - this.toolbar.show(new ToolbarLayout({ - left : [leftSideButtons], - right : [], - context : this - })); - - $('#x-toolbar').addClass('inline'); - }, - - _addSelected : function() { - var selected = _.filter(this.bulkImportCollection.models, function(elem){ - return elem.selected; - }); - - var promise = ArtistCollection.importFromList(selected); - this.ui.addSelectdBtn.spinForPromise(promise); - this.ui.addSelectdBtn.addClass('disabled'); - - if (selected.length === 0) { - Messenger.show({ - type : 'error', - message : 'No artists selected' - }); - return; - } - - Messenger.show({ - message : 'Importing {0} artists. This can take multiple minutes depending on how many artists should be imported. Don\'t close this browser window until it is finished!'.format(selected.length), - hideOnNavigate : false, - hideAfter : 30, - type : 'error' - }); - - var _this = this; - - promise.done(function() { - Messenger.show({ - message : 'Imported artists from folder.', - hideAfter : 8, - hideOnNavigate : true - }); - - - _.forEach(selected, function(artist) { - artist.destroy(); //update the collection without the added movies - }); - }); - }, - - _handleEvent : function(eventName, data) { - if (eventName === 'sync' || eventName === 'content') { - this._showContent(); - } - }, - - _select : function(model, selected) { - model.selected = selected; - }, - - _showTable : function() { - if (this.bulkImportCollection.length === 0) { - this.table.show(new EmptyView({ folder : this.folder })); - return; - } - - this.importGrid = new Backgrid.Grid({ - columns : this.columns, - collection : this.bulkImportCollection, - className : 'table table-hover' - }); - - this.table.show(this.importGrid); - } -}); diff --git a/src/UI/AddArtist/BulkImport/BulkImportViewTemplate.hbs b/src/UI/AddArtist/BulkImport/BulkImportViewTemplate.hbs deleted file mode 100644 index e07b37e8e..000000000 --- a/src/UI/AddArtist/BulkImport/BulkImportViewTemplate.hbs +++ /dev/null @@ -1,13 +0,0 @@ -<div id="x-toolbar"/> - -<div class="row"> - <div class="col-md-12"> - <span><b>Disabled artists are possible duplicates. If the match is incorrect, update the MB Id cell to import the proper artist.</b><span> - </div> -</div> - -<div class="row"> - <div class="col-md-12"> - <div id="x-artists-bulk" class="queue table-responsive"/> - </div> -</div> diff --git a/src/UI/AddArtist/BulkImport/EmptyView.js b/src/UI/AddArtist/BulkImport/EmptyView.js deleted file mode 100644 index a3c635533..000000000 --- a/src/UI/AddArtist/BulkImport/EmptyView.js +++ /dev/null @@ -1,10 +0,0 @@ -var Marionette = require('marionette'); - -module.exports = Marionette.CompositeView.extend({ - template : 'AddArtist/BulkImport/EmptyViewTemplate', - - initialize : function (options) { - this.templateHelpers = {}; - this.templateHelpers.folder = options.folder; - } -}); diff --git a/src/UI/AddArtist/BulkImport/EmptyViewTemplate.hbs b/src/UI/AddArtist/BulkImport/EmptyViewTemplate.hbs deleted file mode 100644 index b2d80a15c..000000000 --- a/src/UI/AddArtist/BulkImport/EmptyViewTemplate.hbs +++ /dev/null @@ -1,3 +0,0 @@ -<div class="text-center hint col-md-12"> - <span>No artists found in folder {{folder}}. Have you already added all of them?</span> -</div> diff --git a/src/UI/AddArtist/BulkImport/ForeignIdCell.js b/src/UI/AddArtist/BulkImport/ForeignIdCell.js deleted file mode 100644 index 4b22dcd69..000000000 --- a/src/UI/AddArtist/BulkImport/ForeignIdCell.js +++ /dev/null @@ -1,57 +0,0 @@ -var vent = require('vent'); -var _ = require('underscore'); -var $ = require('jquery'); -var NzbDroneCell = require('../../Cells/NzbDroneCell'); -var CommandController = require('../../Commands/CommandController'); - -module.exports = NzbDroneCell.extend({ - className : 'foreignId-cell', - - events : { - 'blur input.foreignId-input' : '_updateId' - }, - - render : function() { - this.$el.empty(); - - this.$el.html('<i class="icon-lidarr-info hidden"></i><input type="text" class="x-foreignId foreignId-input form-control" value="' + this.cellValue.get('foreignArtistId') + '" />'); - - return this; - }, - - _updateId : function() { - var field = this.$el.find('.x-foreignId'); - var data = field.val(); - - var promise = $.ajax({ - url : window.NzbDrone.ApiRoot + '/artist/lookup?term=lidarrid:' + data, - type : 'GET', - }); - - field.prop('disabled', true); - - var icon = this.$('.icon-lidarr-info'); - - icon.removeClass('hidden'); - - icon.spinForPromise(promise); - var _self = this; - var cacheMonitored = this.model.get('monitored'); - var cacheProfile = this.model.get('profileId'); - var cachePath = this.model.get('path'); - var cacheRoot = this.model.get('rootFolderPath'); - - promise.success(function(response) { - _self.model.set(response[0]); - _self.model.set('monitored', cacheMonitored); - _self.model.set('profileId', cacheProfile); - _self.model.set('path', cachePath); - field.prop('disabled', false); - }); - - promise.error(function(request, status, error) { - console.error('Status: ' + status, 'Error: ' + error); - field.prop('disabled', false); - }); - } -}); diff --git a/src/UI/AddArtist/EmptyView.js b/src/UI/AddArtist/EmptyView.js deleted file mode 100644 index e07b1647d..000000000 --- a/src/UI/AddArtist/EmptyView.js +++ /dev/null @@ -1,5 +0,0 @@ -var Marionette = require('marionette'); - -module.exports = Marionette.CompositeView.extend({ - template : 'AddArtist/EmptyViewTemplate' -}); \ No newline at end of file diff --git a/src/UI/AddArtist/EmptyViewTemplate.hbs b/src/UI/AddArtist/EmptyViewTemplate.hbs deleted file mode 100644 index e2a20efdc..000000000 --- a/src/UI/AddArtist/EmptyViewTemplate.hbs +++ /dev/null @@ -1,3 +0,0 @@ -<div class="text-center hint col-md-12"> - <span>You can also search by MusicBrianzID using the MBID: prefixes.</span> -</div> diff --git a/src/UI/AddArtist/ErrorView.js b/src/UI/AddArtist/ErrorView.js deleted file mode 100644 index 9d53fae8c..000000000 --- a/src/UI/AddArtist/ErrorView.js +++ /dev/null @@ -1,13 +0,0 @@ -var Marionette = require('marionette'); - -module.exports = Marionette.CompositeView.extend({ - template : 'AddArtist/ErrorViewTemplate', - - initialize : function(options) { - this.options = options; - }, - - templateHelpers : function() { - return this.options; - } -}); \ No newline at end of file diff --git a/src/UI/AddArtist/ErrorViewTemplate.hbs b/src/UI/AddArtist/ErrorViewTemplate.hbs deleted file mode 100644 index c0b1e3673..000000000 --- a/src/UI/AddArtist/ErrorViewTemplate.hbs +++ /dev/null @@ -1,7 +0,0 @@ -<div class="text-center col-md-12"> - <h3> - There was an error searching for '{{term}}'. - </h3> - - If the artist name contains non-alphanumeric characters try removing them, otherwise try your search again later. -</div> diff --git a/src/UI/AddArtist/Existing/AddExistingArtistCollectionView.js b/src/UI/AddArtist/Existing/AddExistingArtistCollectionView.js deleted file mode 100644 index af57bc1d2..000000000 --- a/src/UI/AddArtist/Existing/AddExistingArtistCollectionView.js +++ /dev/null @@ -1,51 +0,0 @@ -var Marionette = require('marionette'); -var AddArtistView = require('../AddArtistView'); -var UnmappedFolderCollection = require('./UnmappedFolderCollection'); - -module.exports = Marionette.CompositeView.extend({ - itemView : AddArtistView, - itemViewContainer : '.x-loading-folders', - template : 'AddArtist/Existing/AddExistingArtistCollectionViewTemplate', - - ui : { - loadingFolders : '.x-loading-folders' - }, - - initialize : function() { - this.collection = new UnmappedFolderCollection(); - this.collection.importItems(this.model); - }, - - showCollection : function() { - this._showAndSearch(0); - }, - - appendHtml : function(collectionView, itemView, index) { - collectionView.ui.loadingFolders.before(itemView.el); - }, - - _showAndSearch : function(index) { - var self = this; - var model = this.collection.at(index); - - if (model) { - var currentIndex = index; - var folderName = model.get('folder').name; - this.addItemView(model, this.getItemView(), index); - this.children.findByModel(model).search({ term : folderName }).always(function() { - if (!self.isClosed) { - self._showAndSearch(currentIndex + 1); - } - }); - } - - else { - this.ui.loadingFolders.hide(); - } - }, - - itemViewOptions : { - isExisting : true - } - -}); \ No newline at end of file diff --git a/src/UI/AddArtist/Existing/AddExistingArtistCollectionViewTemplate.hbs b/src/UI/AddArtist/Existing/AddExistingArtistCollectionViewTemplate.hbs deleted file mode 100644 index 5acbd1ef0..000000000 --- a/src/UI/AddArtist/Existing/AddExistingArtistCollectionViewTemplate.hbs +++ /dev/null @@ -1,5 +0,0 @@ -<div class="x-existing-folders"> - <div class="loading-folders x-loading-folders"> - Loading search results from server for your artists, this may take a few minutes. - </div> -</div> \ No newline at end of file diff --git a/src/UI/AddArtist/Existing/UnmappedFolderCollection.js b/src/UI/AddArtist/Existing/UnmappedFolderCollection.js deleted file mode 100644 index bd2a83f49..000000000 --- a/src/UI/AddArtist/Existing/UnmappedFolderCollection.js +++ /dev/null @@ -1,20 +0,0 @@ -var Backbone = require('backbone'); -var UnmappedFolderModel = require('./UnmappedFolderModel'); -var _ = require('underscore'); - -module.exports = Backbone.Collection.extend({ - model : UnmappedFolderModel, - - importItems : function(rootFolderModel) { - - this.reset(); - var rootFolder = rootFolderModel; - - _.each(rootFolderModel.get('unmappedFolders'), function(folder) { - this.push(new UnmappedFolderModel({ - rootFolder : rootFolder, - folder : folder - })); - }, this); - } -}); \ No newline at end of file diff --git a/src/UI/AddArtist/Existing/UnmappedFolderModel.js b/src/UI/AddArtist/Existing/UnmappedFolderModel.js deleted file mode 100644 index 3986a5948..000000000 --- a/src/UI/AddArtist/Existing/UnmappedFolderModel.js +++ /dev/null @@ -1,3 +0,0 @@ -var Backbone = require('backbone'); - -module.exports = Backbone.Model.extend({}); \ No newline at end of file diff --git a/src/UI/AddArtist/MonitoringTooltipTemplate.hbs b/src/UI/AddArtist/MonitoringTooltipTemplate.hbs deleted file mode 100644 index 0c795cf12..000000000 --- a/src/UI/AddArtist/MonitoringTooltipTemplate.hbs +++ /dev/null @@ -1,18 +0,0 @@ -<dl class="monitor-tooltip-contents"> - <dt>All</dt> - <dd>Monitor all tracks except specials</dd> - <dt>Future</dt> - <dd>Monitor tracks that have not been released yet</dd> - <dt>Missing</dt> - <dd>Monitor tracks that do not have files or have not aired yet</dd> - <dt>Existing</dt> - <dd>Monitor tracks that have files or have not aired yet</dd> - <dt>First Season</dt> - <dd>Monitor all tracks of the first album. All other albums will be ignored</dd> - <dt>Latest Season</dt> - <dd>Monitor all tracks of the latest album and future albums</dd> - <dt>None</dt> - <dd>No tracks will be monitored.</dd> - <!--<dt>Latest Season</dt>--> - <!--<dd>Monitor all tracks the latest album only, previous albums will be ignored</dd>--> -</dl> \ No newline at end of file diff --git a/src/UI/AddArtist/NotFoundView.js b/src/UI/AddArtist/NotFoundView.js deleted file mode 100644 index d25f339c3..000000000 --- a/src/UI/AddArtist/NotFoundView.js +++ /dev/null @@ -1,13 +0,0 @@ -var Marionette = require('marionette'); - -module.exports = Marionette.CompositeView.extend({ - template : 'AddArtist/NotFoundViewTemplate', - - initialize : function(options) { - this.options = options; - }, - - templateHelpers : function() { - return this.options; - } -}); \ No newline at end of file diff --git a/src/UI/AddArtist/NotFoundViewTemplate.hbs b/src/UI/AddArtist/NotFoundViewTemplate.hbs deleted file mode 100644 index abaca6646..000000000 --- a/src/UI/AddArtist/NotFoundViewTemplate.hbs +++ /dev/null @@ -1,7 +0,0 @@ -<div class="text-center col-md-12"> - <h3> - Sorry. We couldn't find any artist matching '{{term}}' - </h3> - <a href="https://github.com/mattman86/Lidarr/wiki/FAQ#wiki-why-cant-i-add-a-new-show-to-nzbdrone-its-on-thetvdb">Why can't I find my artist?</a> - -</div> diff --git a/src/UI/AddArtist/RootFolders/RootFolderCollection.js b/src/UI/AddArtist/RootFolders/RootFolderCollection.js deleted file mode 100644 index 81050c19d..000000000 --- a/src/UI/AddArtist/RootFolders/RootFolderCollection.js +++ /dev/null @@ -1,10 +0,0 @@ -var Backbone = require('backbone'); -var RootFolderModel = require('./RootFolderModel'); -require('../../Mixins/backbone.signalr.mixin'); - -var RootFolderCollection = Backbone.Collection.extend({ - url : window.NzbDrone.ApiRoot + '/rootfolder', - model : RootFolderModel -}); - -module.exports = new RootFolderCollection(); \ No newline at end of file diff --git a/src/UI/AddArtist/RootFolders/RootFolderCollectionView.js b/src/UI/AddArtist/RootFolders/RootFolderCollectionView.js deleted file mode 100644 index 1029de245..000000000 --- a/src/UI/AddArtist/RootFolders/RootFolderCollectionView.js +++ /dev/null @@ -1,8 +0,0 @@ -var Marionette = require('marionette'); -var RootFolderItemView = require('./RootFolderItemView'); - -module.exports = Marionette.CompositeView.extend({ - template : 'AddArtist/RootFolders/RootFolderCollectionViewTemplate', - itemViewContainer : '.x-root-folders', - itemView : RootFolderItemView -}); \ No newline at end of file diff --git a/src/UI/AddArtist/RootFolders/RootFolderCollectionViewTemplate.hbs b/src/UI/AddArtist/RootFolders/RootFolderCollectionViewTemplate.hbs deleted file mode 100644 index 70755bbca..000000000 --- a/src/UI/AddArtist/RootFolders/RootFolderCollectionViewTemplate.hbs +++ /dev/null @@ -1,13 +0,0 @@ -<table class="table table-hover"> - <thead> - <tr> - <th class="col-md-10 "> - Path - </th> - <th class="col-md-3"> - Free Space - </th> - </tr> - </thead> - <tbody class="x-root-folders"></tbody> -</table> \ No newline at end of file diff --git a/src/UI/AddArtist/RootFolders/RootFolderItemView.js b/src/UI/AddArtist/RootFolders/RootFolderItemView.js deleted file mode 100644 index c22f6fcf7..000000000 --- a/src/UI/AddArtist/RootFolders/RootFolderItemView.js +++ /dev/null @@ -1,28 +0,0 @@ -var Marionette = require('marionette'); - -module.exports = Marionette.ItemView.extend({ - template : 'AddArtist/RootFolders/RootFolderItemViewTemplate', - className : 'recent-folder', - tagName : 'tr', - - initialize : function() { - this.listenTo(this.model, 'change', this.render); - }, - - events : { - 'click .x-delete' : 'removeFolder', - 'click .x-folder' : 'folderSelected' - }, - - removeFolder : function() { - var self = this; - - this.model.destroy().success(function() { - self.close(); - }); - }, - - folderSelected : function() { - this.trigger('folderSelected', this.model); - } -}); \ No newline at end of file diff --git a/src/UI/AddArtist/RootFolders/RootFolderItemViewTemplate.hbs b/src/UI/AddArtist/RootFolders/RootFolderItemViewTemplate.hbs deleted file mode 100644 index c1378207a..000000000 --- a/src/UI/AddArtist/RootFolders/RootFolderItemViewTemplate.hbs +++ /dev/null @@ -1,9 +0,0 @@ -<td class="col-md-10 x-folder folder-path"> - {{path}} -</td> -<td class="col-md-3 x-folder folder-free-space"> - <span>{{Bytes freeSpace}}</span> -</td> -<td class="col-md-1"> - <i class="icon-lidarr-delete x-delete"></i> -</td> diff --git a/src/UI/AddArtist/RootFolders/RootFolderLayout.js b/src/UI/AddArtist/RootFolders/RootFolderLayout.js deleted file mode 100644 index 7b5036689..000000000 --- a/src/UI/AddArtist/RootFolders/RootFolderLayout.js +++ /dev/null @@ -1,80 +0,0 @@ -var Marionette = require('marionette'); -var RootFolderCollectionView = require('./RootFolderCollectionView'); -var RootFolderCollection = require('./RootFolderCollection'); -var RootFolderModel = require('./RootFolderModel'); -var LoadingView = require('../../Shared/LoadingView'); -var AsValidatedView = require('../../Mixins/AsValidatedView'); -require('../../Mixins/FileBrowser'); - -var Layout = Marionette.Layout.extend({ - template : 'AddArtist/RootFolders/RootFolderLayoutTemplate', - - ui : { - pathInput : '.x-path' - }, - - regions : { - currentDirs : '#current-dirs' - }, - - events : { - 'click .x-add' : '_addFolder', - 'keydown .x-path input' : '_keydown' - }, - - initialize : function() { - this.collection = RootFolderCollection; - this.rootfolderListView = null; - }, - - onShow : function() { - this.listenTo(RootFolderCollection, 'sync', this._showCurrentDirs); - this.currentDirs.show(new LoadingView()); - - if (RootFolderCollection.synced) { - this._showCurrentDirs(); - } - - this.ui.pathInput.fileBrowser(); - }, - - _onFolderSelected : function(options) { - this.trigger('folderSelected', options); - }, - - _addFolder : function() { - var self = this; - - var newDir = new RootFolderModel({ - Path : this.ui.pathInput.val() - }); - - this.bindToModelValidation(newDir); - - newDir.save().done(function() { - RootFolderCollection.add(newDir); - self.trigger('folderSelected', { model : newDir }); - }); - }, - - _showCurrentDirs : function() { - if (!this.rootfolderListView) { - this.rootfolderListView = new RootFolderCollectionView({ collection : RootFolderCollection }); - this.currentDirs.show(this.rootfolderListView); - - this.listenTo(this.rootfolderListView, 'itemview:folderSelected', this._onFolderSelected); - } - }, - - _keydown : function(e) { - if (e.keyCode !== 13) { - return; - } - - this._addFolder(); - } -}); - -var Layout = AsValidatedView.apply(Layout); - -module.exports = Layout; diff --git a/src/UI/AddArtist/RootFolders/RootFolderLayoutTemplate.hbs b/src/UI/AddArtist/RootFolders/RootFolderLayoutTemplate.hbs deleted file mode 100644 index efb6eb7c9..000000000 --- a/src/UI/AddArtist/RootFolders/RootFolderLayoutTemplate.hbs +++ /dev/null @@ -1,36 +0,0 @@ -<div class="modal-content"> - <div class="modal-header"> - <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> - <h3>Select Folder</h3> - </div> - <div class="modal-body root-folders-modal"> - <div class="validation-errors"></div> - <div class="alert alert-info">Enter the path that contains some or all of your Music, you will be able to choose which artist you want to import<button type="button" class="close" data-dismiss="alert">×</button></div> - - <div class="row"> - <div class="form-group"> - - <div class="col-md-12"> - - <div class="input-group"> - <span class="input-group-addon"> <i class="icon-lidarr-folder-open"></i></span> - <input class="form-control x-path" type="text" validation-name="path" placeholder="Enter path to folder that contains your music"> - <span class="input-group-btn"><button class="btn btn-success x-add"><i class="icon-lidarr-ok"/></button></span> - </div> - </div> - </div> - </div> - - <div class="row root-folders"> - <div class="col-md-12"> - {{#if items}} - <h4>Recent Folders</h4> - {{/if}} - <div id="current-dirs" class="root-folders-list"></div> - </div> - </div> - </div> - <div class="modal-footer"> - <button class="btn" data-dismiss="modal">Close</button> - </div> -</div> diff --git a/src/UI/AddArtist/RootFolders/RootFolderModel.js b/src/UI/AddArtist/RootFolders/RootFolderModel.js deleted file mode 100644 index 28681768b..000000000 --- a/src/UI/AddArtist/RootFolders/RootFolderModel.js +++ /dev/null @@ -1,8 +0,0 @@ -var Backbone = require('backbone'); - -module.exports = Backbone.Model.extend({ - urlRoot : window.NzbDrone.ApiRoot + '/rootfolder', - defaults : { - freeSpace : 0 - } -}); \ No newline at end of file diff --git a/src/UI/AddArtist/RootFolders/RootFolderSelectionPartial.hbs b/src/UI/AddArtist/RootFolders/RootFolderSelectionPartial.hbs deleted file mode 100644 index 56729b0dd..000000000 --- a/src/UI/AddArtist/RootFolders/RootFolderSelectionPartial.hbs +++ /dev/null @@ -1,11 +0,0 @@ -<select class="col-md-4 form-control x-root-folder" validation-name="RootFolderPath"> - {{#if this}} - {{#each this}} - <option value="{{id}}">{{path}}</option> - {{/each}} - {{else}} - <option value="">Select Path</option> - {{/if}} - <option value="addNew">Add a different path</option> -</select> - diff --git a/src/UI/AddArtist/SearchResultCollectionView.js b/src/UI/AddArtist/SearchResultCollectionView.js deleted file mode 100644 index e533085ac..000000000 --- a/src/UI/AddArtist/SearchResultCollectionView.js +++ /dev/null @@ -1,29 +0,0 @@ -var Marionette = require('marionette'); -var SearchResultView = require('./SearchResultView'); - -module.exports = Marionette.CollectionView.extend({ - itemView : SearchResultView, - - initialize : function(options) { - this.isExisting = options.isExisting; - this.showing = 1; - }, - - showAll : function() { - this.showingAll = true; - this.render(); - }, - - showMore : function() { - this.showing += 5; - this.render(); - - return this.showing >= this.collection.length; - }, - - appendHtml : function(collectionView, itemView, index) { - if (!this.isExisting || index < this.showing || index === 0) { - collectionView.$el.append(itemView.el); - } - } -}); \ No newline at end of file diff --git a/src/UI/AddArtist/SearchResultView.js b/src/UI/AddArtist/SearchResultView.js deleted file mode 100644 index ee8806a0e..000000000 --- a/src/UI/AddArtist/SearchResultView.js +++ /dev/null @@ -1,297 +0,0 @@ -var _ = require('underscore'); -var vent = require('vent'); -var AppLayout = require('../AppLayout'); -var Backbone = require('backbone'); -var Marionette = require('marionette'); -var Profiles = require('../Profile/ProfileCollection'); -var RootFolders = require('./RootFolders/RootFolderCollection'); -var RootFolderLayout = require('./RootFolders/RootFolderLayout'); -var ArtistCollection = require('../Artist/ArtistCollection'); -var Config = require('../Config'); -var Messenger = require('../Shared/Messenger'); -var AsValidatedView = require('../Mixins/AsValidatedView'); - -require('jquery.dotdotdot'); - -var view = Marionette.ItemView.extend({ - - template : 'AddArtist/SearchResultViewTemplate', - - ui : { - profile : '.x-profile', - rootFolder : '.x-root-folder', - albumFolder : '.x-album-folder', - artistType : '.x-artist-type', - monitor : '.x-monitor', - monitorTooltip : '.x-monitor-tooltip', - addButton : '.x-add', - addAlbumButton : '.x-add-album', - addSearchButton : '.x-add-search', - addAlbumSearchButton : '.x-add-album-search', - overview : '.x-overview' - }, - - events : { - 'click .x-add' : '_addWithoutSearch', - 'click .x-add-album' : '_addWithoutSearch', - 'click .x-add-search' : '_addAndSearch', - 'click .x-add-album-search' : '_addAndSearch', - 'change .x-profile' : '_profileChanged', - 'change .x-root-folder' : '_rootFolderChanged', - 'change .x-album-folder' : '_albumFolderChanged', - 'change .x-artist-type' : '_artistTypeChanged', - 'change .x-monitor' : '_monitorChanged' - }, - - initialize : function() { - - if (!this.model) { - throw 'model is required'; - } - - this.templateHelpers = {}; - this._configureTemplateHelpers(); - - this.listenTo(vent, Config.Events.ConfigUpdatedEvent, this._onConfigUpdated); - this.listenTo(this.model, 'change', this.render); - this.listenTo(RootFolders, 'all', this._rootFoldersUpdated); - }, - - onRender : function() { - - var defaultProfile = Config.getValue(Config.Keys.DefaultProfileId); - var defaultRoot = Config.getValue(Config.Keys.DefaultRootFolderId); - var useAlbumFolder = Config.getValueBoolean(Config.Keys.UseAlbumFolder, true); - var defaultArtistType = Config.getValue(Config.Keys.DefaultSeriesType, 'standard'); - var defaultMonitorEpisodes = Config.getValue(Config.Keys.MonitorEpisodes, 'missing'); - - if (Profiles.get(defaultProfile)) { - this.ui.profile.val(defaultProfile); - } - - if (RootFolders.get(defaultRoot)) { - this.ui.rootFolder.val(defaultRoot); - } - - this.ui.albumFolder.prop('checked', useAlbumFolder); - this.ui.artistType.val(defaultArtistType); - this.ui.monitor.val(defaultMonitorEpisodes); - - //TODO: make this work via onRender, FM? - //works with onShow, but stops working after the first render - this.ui.overview.dotdotdot({ - height : 120 - }); - - this.templateFunction = Marionette.TemplateCache.get('AddArtist/MonitoringTooltipTemplate'); - var content = this.templateFunction(); - - this.ui.monitorTooltip.popover({ - content : content, - html : true, - trigger : 'hover', - title : 'Track Monitoring Options', - placement : 'right', - container : this.$el - }); - }, - - _configureTemplateHelpers : function() { - var existingArtist = ArtistCollection.where({ foreignArtistId : this.model.get('foreignArtistId') }); - - if (existingArtist.length > 0) { - this.templateHelpers.existing = existingArtist[0].toJSON(); - } - - this.templateHelpers.profiles = Profiles.toJSON(); - - if (!this.model.get('isExisting')) { - this.templateHelpers.rootFolders = RootFolders.toJSON(); - } - }, - - _onConfigUpdated : function(options) { - if (options.key === Config.Keys.DefaultProfileId) { - this.ui.profile.val(options.value); - } - - else if (options.key === Config.Keys.DefaultRootFolderId) { - this.ui.rootFolder.val(options.value); - } - - else if (options.key === Config.Keys.UseAlbumFolder) { - this.ui.seasonFolder.prop('checked', options.value); - } - - else if (options.key === Config.Keys.DefaultArtistType) { - this.ui.artistType.val(options.value); - } - - else if (options.key === Config.Keys.MonitorEpisodes) { - this.ui.monitor.val(options.value); - } - }, - - _profileChanged : function() { - Config.setValue(Config.Keys.DefaultProfileId, this.ui.profile.val()); - }, - - _albumFolderChanged : function() { - Config.setValue(Config.Keys.UseAlbumFolder, this.ui.albumFolder.prop('checked')); - }, - - _rootFolderChanged : function() { - var rootFolderValue = this.ui.rootFolder.val(); - if (rootFolderValue === 'addNew') { - var rootFolderLayout = new RootFolderLayout(); - this.listenToOnce(rootFolderLayout, 'folderSelected', this._setRootFolder); - AppLayout.modalRegion.show(rootFolderLayout); - } else { - Config.setValue(Config.Keys.DefaultRootFolderId, rootFolderValue); - } - }, - - _artistTypeChanged : function() { - Config.setValue(Config.Keys.DefaultArtistType, this.ui.artistType.val()); - }, - - _monitorChanged : function() { - Config.setValue(Config.Keys.MonitorEpisodes, this.ui.monitor.val()); - }, - - _setRootFolder : function(options) { - vent.trigger(vent.Commands.CloseModalCommand); - this.ui.rootFolder.val(options.model.id); - this._rootFolderChanged(); - }, - - _addWithoutSearch : function(evt) { - console.log(evt); - this._addArtist(false); - }, - - _addAndSearch : function() { - this._addArtist(true); - }, - - _addArtist : function(searchForMissing) { - // TODO: Refactor to handle multiple add buttons/albums - var addButton = this.ui.addButton; - var addSearchButton = this.ui.addSearchButton; - console.log('_addArtist, searchForMissing=', searchForMissing); - - addButton.addClass('disabled'); - addSearchButton.addClass('disabled'); - - var profile = this.ui.profile.val(); - var rootFolderPath = this.ui.rootFolder.children(':selected').text(); - var artistType = this.ui.artistType.val(); // Perhaps make this a differnitator between artist or Album? - var albumFolder = this.ui.albumFolder.prop('checked'); - - var options = this._getAddArtistOptions(); - options.searchForMissing = searchForMissing; - - this.model.set({ - profileId : profile, - rootFolderPath : rootFolderPath, - albumFolder : albumFolder, - artistType : artistType, - addOptions : options, - monitored : true - }, { silent : true }); - - var self = this; - var promise = this.model.save(); - - if (searchForMissing) { - this.ui.addSearchButton.spinForPromise(promise); - } - - else { - this.ui.addButton.spinForPromise(promise); - } - - promise.always(function() { - addButton.removeClass('disabled'); - addSearchButton.removeClass('disabled'); - }); - - promise.done(function() { - console.log('[SearchResultView] _addArtist promise resolve:', self.model); - ArtistCollection.add(self.model); - - self.close(); - - Messenger.show({ - message : 'Added: ' + self.model.get('name'), - actions : { - goToArtist : { - label : 'Go to Artist', - action : function() { - Backbone.history.navigate('/artist/' + self.model.get('nameSlug'), { trigger : true }); - } - } - }, - hideAfter : 8, - hideOnNavigate : true - }); - - vent.trigger(vent.Events.ArtistAdded, { artist : self.model }); - }); - }, - - _rootFoldersUpdated : function() { - this._configureTemplateHelpers(); - this.render(); - }, - - _getAddArtistOptions : function() { - var monitor = this.ui.monitor.val(); - //[TODO]: Refactor for albums - var lastSeason = _.max(this.model.get('seasons'), 'seasonNumber'); - var firstSeason = _.min(_.reject(this.model.get('seasons'), { seasonNumber : 0 }), 'seasonNumber'); - - //this.model.setSeasonPass(firstSeason.seasonNumber); // TODO - - var options = { - ignoreTracksWithFiles : false, - ignoreTracksWithoutFiles : false - }; - - if (monitor === 'all') { - return options; - } - - else if (monitor === 'future') { - options.ignoreTracksWithFiles = true; - options.ignoreTracksWithoutFiles = true; - } - - /*else if (monitor === 'latest') { - this.model.setSeasonPass(lastSeason.seasonNumber); - } - - else if (monitor === 'first') { - this.model.setSeasonPass(lastSeason.seasonNumber + 1); - this.model.setSeasonMonitored(firstSeason.seasonNumber); - }*/ - - else if (monitor === 'missing') { - options.ignoreTracksWithFiles = true; - } - - else if (monitor === 'existing') { - options.ignoreTracksWithoutFiles = true; - } - - /*else if (monitor === 'none') { - this.model.setSeasonPass(lastSeason.seasonNumber + 1); - }*/ - - return options; - } -}); - -AsValidatedView.apply(view); - -module.exports = view; diff --git a/src/UI/AddArtist/SearchResultViewTemplate.hbs b/src/UI/AddArtist/SearchResultViewTemplate.hbs deleted file mode 100644 index f92eb2d5f..000000000 --- a/src/UI/AddArtist/SearchResultViewTemplate.hbs +++ /dev/null @@ -1,146 +0,0 @@ -<div class="search-item {{#unless isExisting}}search-item-new{{/unless}}"> - <div class="row"> - <div class="col-md-10"> - <div class="row"> - <div class="col-md-12"> - <h2 class="artist-title"> - <!--{{titleWithYear}}--> - {{name}} - - <!--<span class="labels"> - <span class="label label-default">{{network}}</span> - {{#unless_eq status compare="continuing"}} - <span class="label label-danger">Ended</span> - {{/unless_eq}} - </span>--> - </h2> - </div> - </div> - <div class="row new-artist-overview x-overview"> - <div class="col-md-12 overview-internal"> - {{overview}} - </div> - </div> - <div class="row"> - {{#unless existing}} - {{#unless path}} - <div class="form-group col-md-4"> - <label>Path</label> - {{> RootFolderSelectionPartial rootFolders}} - </div> - {{/unless}} - - <div class="form-group col-md-2"> - <label>Monitor <i class="icon-lidarr-form-info monitor-tooltip x-monitor-tooltip"></i></label> - <select class="form-control col-md-2 x-monitor"> - <option value="all">All</option> - <option value="future">Future</option> - <option value="missing">Missing</option> - <option value="existing">Existing</option> - <option value="none">None</option> - </select> - </div> - - <div class="form-group col-md-2"> - <label>Profile</label> - {{> ProfileSelectionPartial profiles}} - </div> - - <!--<div class="form-group col-md-2"> - - </div>--> - - <div class="form-group col-md-2"> - <label>Album Folders</label> - - <div class="input-group"> - <label class="checkbox toggle well"> - <input type="checkbox" class="x-album-folder"/> - <p> - <span>Yes</span> - <span>No</span> - </p> - <div class="btn btn-primary slide-button"/> - </label> - </div> - </div> - {{/unless}} - </div> - <div class="row"> - {{#unless existing}} - {{#if name}} - <div class="form-group col-md-2 col-md-offset-10"> - <!--Uncomment if we need to add even more controls to add artist--> - <!--<label style="visibility: hidden">Add</label>--> - <div class="btn-group"> - <button class="btn btn-success add x-add" title="Add" data-artist="{{name}}"> - <i class="icon-lidarr-add"></i> - </button> - - <button class="btn btn-success add x-add-search" title="Add and Search for missing tracks" data-artist="{{name}}"> - <i class="icon-lidarr-search"></i> - </button> - </div> - </div> - {{else}} - <div class="col-md-2 col-md-offset-10"> - <button class="btn add-artist disabled"> - Add - </button> - </div> - {{/if}} - {{else}} - <div class="col-md-2 col-md-offset-10"> - <a class="btn btn-default" href="{{route}}"> - Already Exists - </a> - </div> - {{/unless}} - </div> - </div> - </div> - <div class="row"> - {{#each albums}} - <div class="col-md-12" style="border:1px dashed black;"> - <div class="col-md-2"> - <a href="{{artworkUrl}}" target="_blank"> - <!-- {{poster}} --> - <img class="album-poster" src="{{artworkUrl}}"> - </a> - </div> - <div class="col-md-8"> - <h2>{{albumName}} ({{year}})</h2> - {{#unless existing}} - {{#if albumName}} - <div class="form-group col-md-offset-10"> - <!--Uncomment if we need to add even more controls to add artist--> - <!--<label style="visibility: hidden">Add</label>--> - <div class="btn-group"> - <button class="btn btn-success add x-add-album" title="Add" data-album="{{albumName}}"> - <i class="icon-lidarr-add"></i> - </button> - - <button class="btn btn-success add x-add-album-search" title="Add and Search for missing tracks" data-album="{{albumName}}"> - <i class="icon-lidarr-search"></i> - </button> - </div> - </div> - {{else}} - <div class="col-md-2 col-md-offset-10"> - <button class="btn add-artist disabled"> - Add - </button> - </div> - {{/if}} - {{else}} - <div class="col-md-2 col-md-offset-10"> - <a class="btn btn-default" href="{{route}}"> - Already Exists - </a> - </div> - {{/unless}} - </div> - </div> - {{/each}} - </div> -</div> diff --git a/src/UI/AddArtist/StartingAlbumSelectionPartial.hbs b/src/UI/AddArtist/StartingAlbumSelectionPartial.hbs deleted file mode 100644 index 573599dab..000000000 --- a/src/UI/AddArtist/StartingAlbumSelectionPartial.hbs +++ /dev/null @@ -1,13 +0,0 @@ -<select class="form-control col-md-2 starting-album x-starting-album"> - - - {{#each this}} - {{#if_eq seasonNumber compare="0"}} - <option value="{{seasonNumber}}">Specials</option> - {{else}} - <option value="{{seasonNumber}}">Album {{seasonNumber}}</option> - {{/if_eq}} - {{/each}} - - <option value="5000000">None</option> -</select> diff --git a/src/UI/AddArtist/addArtist.less b/src/UI/AddArtist/addArtist.less deleted file mode 100644 index e53ff8eee..000000000 --- a/src/UI/AddArtist/addArtist.less +++ /dev/null @@ -1,181 +0,0 @@ -@import "../Shared/Styles/card.less"; -@import "../Shared/Styles/clickable.less"; - -#add-artist-screen { - .existing-artist { - - .card(); - margin : 30px 0px; - - .unmapped-folder-path { - padding: 20px; - margin-left : 0px; - font-weight : 100; - font-size : 25px; - text-align : center; - } - - .new-artist-loadmore { - font-size : 30px; - font-weight : 300; - padding-top : 10px; - padding-bottom : 10px; - } - } - - .new-artist { - .search-item { - .card(); - margin : 40px 0px; - } - } - - .add-artist-search { - margin-top : 20px; - margin-bottom : 20px; - } - - .search-item { - - padding-bottom : 20px; - - .artist-title { - margin-top : 5px; - - .labels { - margin-left : 10px; - - .label { - font-size : 12px; - vertical-align : middle; - } - } - - .year { - font-style : italic; - color : #aaaaaa; - } - } - - .new-artist-overview { - overflow : hidden; - height : 103px; - - .overview-internal { - overflow : hidden; - height : 80px; - } - } - - .artist-poster { - min-width : 138px; - min-height : 203px; - max-width : 138px; - max-height : 203px; - margin : 10px; - } - - .album-poster { - min-width : 100px; - min-height : 100px; - max-width : 138px; - max-height : 203px; - margin : 10px; - } - - a { - color : #343434; - } - - a:hover { - text-decoration : none; - } - - select { - font-size : 14px; - } - - .checkbox { - margin-top : 0px; - } - - .add { - i { - &:before { - color : #ffffff; - } - } - } - - .monitor-tooltip { - margin-left : 5px; - } - } - - .loading-folders { - margin : 30px 0px; - text-align: center; - } - - .hint { - color : #999999; - font-style : italic; - } - - .monitor-tooltip-contents { - padding-bottom : 0px; - - dd { - padding-bottom : 8px; - } - } -} - -li.add-new { - .clickable; - - display: block; - padding: 3px 20px; - clear: both; - font-weight: normal; - line-height: 20px; - color: rgb(51, 51, 51); - white-space: nowrap; -} - -li.add-new:hover { - text-decoration: none; - color: rgb(255, 255, 255); - background-color: rgb(0, 129, 194); -} - -.root-folders-modal { - overflow : visible; - - .root-folders-list { - overflow-y : auto; - max-height : 300px; - - i { - .clickable(); - } - } - - .validation-errors { - display : none; - } - - .input-group { - .form-control { - background-color : white; - } - } - - .root-folders { - margin-top : 20px; - } - - .recent-folder { - .clickable(); - } -} diff --git a/src/UI/Album/AlbumDetailsLayout.js b/src/UI/Album/AlbumDetailsLayout.js deleted file mode 100644 index 53bcf941a..000000000 --- a/src/UI/Album/AlbumDetailsLayout.js +++ /dev/null @@ -1,133 +0,0 @@ -var Marionette = require('marionette'); -var SummaryLayout = require('./Summary/AlbumSummaryLayout'); -var SearchLayout = require('./Search/AlbumSearchLayout'); -var AlbumHistoryLayout = require('./History/AlbumHistoryLayout'); -var ArtistCollection = require('../Artist/ArtistCollection'); -var Messenger = require('../Shared/Messenger'); - -module.exports = Marionette.Layout.extend({ - className : 'modal-lg', - template : 'Album/AlbumDetailsLayoutTemplate', - - regions : { - summary : '#album-summary', - history : '#album-history', - search : '#album-search' - }, - - ui : { - summary : '.x-album-summary', - history : '.x-album-history', - search : '.x-album-search', - monitored : '.x-album-monitored' - }, - - events : { - - 'click .x-album-summary' : '_showSummary', - 'click .x-album-history' : '_showHistory', - 'click .x-album-search' : '_showSearch', - 'click .x-album-monitored' : '_toggleMonitored' - }, - - templateHelpers : {}, - - initialize : function(options) { - - this.templateHelpers.hideArtistLink = options.hideArtistLink; - - - this.artist = ArtistCollection.get(this.model.get('artistId')); - - this.templateHelpers.artist = this.artist.toJSON(); - this.openingTab = options.openingTab || 'summary'; - - this.listenTo(this.model, 'sync', this._setMonitoredState); - }, - - onShow : function() { - this.searchLayout = new SearchLayout({ model : this.model }); - - if (this.openingTab === 'search') { - this.searchLayout.startManualSearch = true; - this._showSearch(); - } - - else { - this._showSummary(); - } - - this._setMonitoredState(); - - if (this.artist.get('monitored')) { - this.$el.removeClass('artist-not-monitored'); - } - - else { - this.$el.addClass('artist-not-monitored'); - } - }, - - _showSummary : function(e) { - if (e) { - e.preventDefault(); - } - - this.ui.summary.tab('show'); - this.summary.show(new SummaryLayout({ - model : this.model, - artist : this.artist - })); - }, - - _showHistory : function(e) { - if (e) { - e.preventDefault(); - } - - this.ui.history.tab('show'); - this.history.show(new AlbumHistoryLayout({ - model : this.model, - artist : this.artist - })); - }, - - _showSearch : function(e) { - if (e) { - e.preventDefault(); - } - - this.ui.search.tab('show'); - this.search.show(this.searchLayout); - }, - - _toggleMonitored : function() { - if (!this.series.get('monitored')) { - - Messenger.show({ - message : 'Unable to change monitored state when artist is not monitored', - type : 'error' - }); - - return; - } - - var name = 'monitored'; - this.model.set(name, !this.model.get(name), { silent : true }); - - this.ui.monitored.addClass('icon-lidarr-spinner fa-spin'); - this.model.save(); - }, - - _setMonitoredState : function() { - this.ui.monitored.removeClass('fa-spin icon-lidarr-spinner'); - - if (this.model.get('monitored')) { - this.ui.monitored.addClass('icon-lidarr-monitored'); - this.ui.monitored.removeClass('icon-lidarr-unmonitored'); - } else { - this.ui.monitored.addClass('icon-lidarr-unmonitored'); - this.ui.monitored.removeClass('icon-lidarr-monitored'); - } - } -}); \ No newline at end of file diff --git a/src/UI/Album/AlbumDetailsLayoutTemplate.hbs b/src/UI/Album/AlbumDetailsLayoutTemplate.hbs deleted file mode 100644 index f76caa790..000000000 --- a/src/UI/Album/AlbumDetailsLayoutTemplate.hbs +++ /dev/null @@ -1,35 +0,0 @@ -<div class="modal-content"> - <div class="album-detail-modal"> - <div class="modal-header"> - <span class="hidden-artist-title x-artist-title">{{artist.name}}</span> - <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> - - <h3> - <i class="icon-lidarr-monitored x-album-monitored album-monitored" title="Toggle monitored status" /> - {{title}} ({{albumYear}}) - </h3> - - </div> - <div class="modal-body"> - <ul class="nav nav-tabs" id="myTab"> - <li><a href="#album-summary" class="x-album-summary">Summary</a></li> - <li><a href="#album-history" class="x-album-history">History</a></li> - <li><a href="#album-search" class="x-album-search">Search</a></li> - </ul> - <div class="tab-content"> - <div class="tab-pane" id="album-summary"/> - <div class="tab-pane" id="album-history"/> - <div class="tab-pane" id="album-search"/> - </div> - </div> - <div class="modal-footer"> - {{#unless hideArtistLink}} - {{#with artist}} - <a href="{{route}}" class="btn btn-default pull-left" data-dismiss="modal">Go to Artist</a> - {{/with}} - {{/unless}} - - <button class="btn btn-default" data-dismiss="modal">Close</button> - </div> - </div> -</div> diff --git a/src/UI/Album/History/AlbumHistoryActionsCell.js b/src/UI/Album/History/AlbumHistoryActionsCell.js deleted file mode 100644 index 28c7dc840..000000000 --- a/src/UI/Album/History/AlbumHistoryActionsCell.js +++ /dev/null @@ -1,35 +0,0 @@ -var $ = require('jquery'); -var vent = require('vent'); -var Marionette = require('marionette'); -var NzbDroneCell = require('../../Cells/NzbDroneCell'); - -module.exports = NzbDroneCell.extend({ - className : 'album-actions-cell', - - events : { - 'click .x-failed' : '_markAsFailed' - }, - - render : function() { - this.$el.empty(); - - if (this.model.get('eventType') === 'grabbed') { - this.$el.html('<i class="icon-lidarr-delete x-failed" title="Mark download as failed"></i>'); - } - - return this; - }, - - _markAsFailed : function() { - var url = window.NzbDrone.ApiRoot + '/history/failed'; - var data = { - id : this.model.get('id') - }; - - $.ajax({ - url : url, - type : 'POST', - data : data - }); - } -}); \ No newline at end of file diff --git a/src/UI/Album/History/AlbumHistoryDetailsCell.js b/src/UI/Album/History/AlbumHistoryDetailsCell.js deleted file mode 100644 index 893aacae8..000000000 --- a/src/UI/Album/History/AlbumHistoryDetailsCell.js +++ /dev/null @@ -1,28 +0,0 @@ -var $ = require('jquery'); -var vent = require('vent'); -var Marionette = require('marionette'); -var NzbDroneCell = require('../../Cells/NzbDroneCell'); -var HistoryDetailsView = require('../../Activity/History/Details/HistoryDetailsView'); -require('bootstrap'); - -module.exports = NzbDroneCell.extend({ - className : 'album-history-details-cell', - - render : function() { - this.$el.empty(); - this.$el.html('<i class="icon-lidarr-form-info"></i>'); - - var html = new HistoryDetailsView({ model : this.model }).render().$el; - - this.$el.popover({ - content : html, - html : true, - trigger : 'hover', - title : 'Details', - placement : 'left', - container : this.$el - }); - - return this; - } -}); \ No newline at end of file diff --git a/src/UI/Album/History/AlbumHistoryLayout.js b/src/UI/Album/History/AlbumHistoryLayout.js deleted file mode 100644 index e9a5b7bb1..000000000 --- a/src/UI/Album/History/AlbumHistoryLayout.js +++ /dev/null @@ -1,84 +0,0 @@ -var Marionette = require('marionette'); -var Backgrid = require('backgrid'); -var HistoryCollection = require('../../Activity/History/HistoryCollection'); -var EventTypeCell = require('../../Cells/EventTypeCell'); -var QualityCell = require('../../Cells/QualityCell'); -var RelativeDateCell = require('../../Cells/RelativeDateCell'); -var AlbumHistoryActionsCell = require('./AlbumHistoryActionsCell'); -var AlbumHistoryDetailsCell = require('./AlbumHistoryDetailsCell'); -var NoHistoryView = require('./NoHistoryView'); -var LoadingView = require('../../Shared/LoadingView'); - -module.exports = Marionette.Layout.extend({ - template : 'Album/History/AlbumHistoryLayoutTemplate', - - regions : { - historyTable : '.history-table' - }, - - columns : [ - { - name : 'eventType', - label : '', - cell : EventTypeCell, - cellValue : 'this' - }, - { - name : 'sourceTitle', - label : 'Source Title', - cell : 'string' - }, - { - name : 'quality', - label : 'Quality', - cell : QualityCell - }, - { - name : 'date', - label : 'Date', - cell : RelativeDateCell - }, - { - name : 'this', - label : '', - cell : AlbumHistoryDetailsCell, - sortable : false - }, - { - name : 'this', - label : '', - cell : AlbumHistoryActionsCell, - sortable : false - } - ], - - initialize : function(options) { - this.model = options.model; - this.artist = options.artist; - - this.collection = new HistoryCollection({ - albumId : this.model.id, - tableName : 'albumHistory' - }); - this.collection.fetch(); - this.listenTo(this.collection, 'sync', this._showTable); - }, - - onRender : function() { - this.historyTable.show(new LoadingView()); - }, - - _showTable : function() { - if (this.collection.any()) { - this.historyTable.show(new Backgrid.Grid({ - collection : this.collection, - columns : this.columns, - className : 'table table-hover table-condensed' - })); - } - - else { - this.historyTable.show(new NoHistoryView()); - } - } -}); \ No newline at end of file diff --git a/src/UI/Album/History/AlbumHistoryLayoutTemplate.hbs b/src/UI/Album/History/AlbumHistoryLayoutTemplate.hbs deleted file mode 100644 index 54fb50522..000000000 --- a/src/UI/Album/History/AlbumHistoryLayoutTemplate.hbs +++ /dev/null @@ -1 +0,0 @@ -<div class="history-table table-responsive"></div> \ No newline at end of file diff --git a/src/UI/Album/History/NoHistoryView.js b/src/UI/Album/History/NoHistoryView.js deleted file mode 100644 index 42c272b5b..000000000 --- a/src/UI/Album/History/NoHistoryView.js +++ /dev/null @@ -1,5 +0,0 @@ -var Marionette = require('marionette'); - -module.exports = Marionette.ItemView.extend({ - template : 'Album/History/NoHistoryViewTemplate' -}); \ No newline at end of file diff --git a/src/UI/Album/History/NoHistoryViewTemplate.hbs b/src/UI/Album/History/NoHistoryViewTemplate.hbs deleted file mode 100644 index f7fd00ec8..000000000 --- a/src/UI/Album/History/NoHistoryViewTemplate.hbs +++ /dev/null @@ -1,3 +0,0 @@ -<p class="text-warning"> - No history for this album. -</p> \ No newline at end of file diff --git a/src/UI/Album/Search/AlbumSearchLayout.js b/src/UI/Album/Search/AlbumSearchLayout.js deleted file mode 100644 index e4364279b..000000000 --- a/src/UI/Album/Search/AlbumSearchLayout.js +++ /dev/null @@ -1,82 +0,0 @@ -var vent = require('vent'); -var Marionette = require('marionette'); -var ButtonsView = require('./ButtonsView'); -var ManualSearchLayout = require('./ManualLayout'); -var ReleaseCollection = require('../../Release/ReleaseCollection'); -var CommandController = require('../../Commands/CommandController'); -var LoadingView = require('../../Shared/LoadingView'); -var NoResultsView = require('./NoResultsView'); - -module.exports = Marionette.Layout.extend({ - template : 'Album/Search/AlbumSearchLayoutTemplate', - - regions : { - main : '#album-search-region' - }, - - events : { - 'click .x-search-auto' : '_searchAuto', - 'click .x-search-manual' : '_searchManual', - 'click .x-search-back' : '_showButtons' - }, - - initialize : function() { - this.mainView = new ButtonsView(); - this.releaseCollection = new ReleaseCollection(); - - this.listenTo(this.releaseCollection, 'sync', this._showSearchResults); - }, - - onShow : function() { - if (this.startManualSearch) { - this._searchManual(); - } - - else { - this._showMainView(); - } - }, - - _searchAuto : function(e) { - if (e) { - e.preventDefault(); - } - - CommandController.Execute('albumSearch', { - albumId : this.model.get('id') - }); - - vent.trigger(vent.Commands.CloseModalCommand); - }, - - _searchManual : function(e) { - if (e) { - e.preventDefault(); - } - - this.mainView = new LoadingView(); - this._showMainView(); - this.releaseCollection.fetchAlbumReleases(this.model.id); - }, - - _showMainView : function() { - this.main.show(this.mainView); - }, - - _showButtons : function() { - this.mainView = new ButtonsView(); - this._showMainView(); - }, - - _showSearchResults : function() { - if (this.releaseCollection.length === 0) { - this.mainView = new NoResultsView(); - } - - else { - this.mainView = new ManualSearchLayout({ collection : this.releaseCollection }); - } - - this._showMainView(); - } -}); \ No newline at end of file diff --git a/src/UI/Album/Search/AlbumSearchLayoutTemplate.hbs b/src/UI/Album/Search/AlbumSearchLayoutTemplate.hbs deleted file mode 100644 index 236159842..000000000 --- a/src/UI/Album/Search/AlbumSearchLayoutTemplate.hbs +++ /dev/null @@ -1 +0,0 @@ -<div id="album-search-region"></div> \ No newline at end of file diff --git a/src/UI/Album/Search/ButtonsView.js b/src/UI/Album/Search/ButtonsView.js deleted file mode 100644 index 43c6b5e3a..000000000 --- a/src/UI/Album/Search/ButtonsView.js +++ /dev/null @@ -1,5 +0,0 @@ -var Marionette = require('marionette'); - -module.exports = Marionette.ItemView.extend({ - template : 'Album/Search/ButtonsViewTemplate' -}); \ No newline at end of file diff --git a/src/UI/Album/Search/ButtonsViewTemplate.hbs b/src/UI/Album/Search/ButtonsViewTemplate.hbs deleted file mode 100644 index 9e578f9db..000000000 --- a/src/UI/Album/Search/ButtonsViewTemplate.hbs +++ /dev/null @@ -1,4 +0,0 @@ -<div class="search-buttons"> - <button class="btn btn-lg btn-block x-search-auto"><i class="icon-lidarr-search-automatic"/> Automatic Search</button> - <button class="btn btn-lg btn-block btn-primary x-search-manual"><i class="icon-lidarr-search-manual"/> Manual Search</button> -</div> \ No newline at end of file diff --git a/src/UI/Album/Search/ManualLayout.js b/src/UI/Album/Search/ManualLayout.js deleted file mode 100644 index f1f1683d7..000000000 --- a/src/UI/Album/Search/ManualLayout.js +++ /dev/null @@ -1,86 +0,0 @@ -var Marionette = require('marionette'); -var Backgrid = require('backgrid'); -var ReleaseTitleCell = require('../../Cells/ReleaseTitleCell'); -var FileSizeCell = require('../../Cells/FileSizeCell'); -var QualityCell = require('../../Cells/QualityCell'); -var ApprovalStatusCell = require('../../Cells/ApprovalStatusCell'); -var DownloadReportCell = require('../../Release/DownloadReportCell'); -var AgeCell = require('../../Release/AgeCell'); -var ProtocolCell = require('../../Release/ProtocolCell'); -var PeersCell = require('../../Release/PeersCell'); - -module.exports = Marionette.Layout.extend({ - template : 'Album/Search/ManualLayoutTemplate', - - regions : { - grid : '#album-release-grid' - }, - - columns : [ - { - name : 'protocol', - label : 'Source', - cell : ProtocolCell - }, - { - name : 'age', - label : 'Age', - cell : AgeCell - }, - { - name : 'title', - label : 'Title', - cell : ReleaseTitleCell - }, - { - name : 'indexer', - label : 'Indexer', - cell : Backgrid.StringCell - }, - { - name : 'size', - label : 'Size', - cell : FileSizeCell - }, - { - name : 'seeders', - label : 'Peers', - cell : PeersCell - }, - { - name : 'quality', - label : 'Quality', - cell : QualityCell - }, - { - name : 'rejections', - label : '<i class="icon-lidarr-header-rejections" />', - tooltip : 'Rejections', - cell : ApprovalStatusCell, - sortable : true, - sortType : 'fixed', - direction : 'ascending', - title : 'Release Rejected' - }, - { - name : 'download', - label : '<i class="icon-lidarr-download" />', - tooltip : 'Auto-Search Prioritization', - cell : DownloadReportCell, - sortable : true, - sortType : 'fixed', - direction : 'ascending' - } - ], - - onShow : function() { - if (!this.isClosed) { - this.grid.show(new Backgrid.Grid({ - row : Backgrid.Row, - columns : this.columns, - collection : this.collection, - className : 'table table-hover' - })); - } - } -}); \ No newline at end of file diff --git a/src/UI/Album/Search/ManualLayoutTemplate.hbs b/src/UI/Album/Search/ManualLayoutTemplate.hbs deleted file mode 100644 index 30513dbd2..000000000 --- a/src/UI/Album/Search/ManualLayoutTemplate.hbs +++ /dev/null @@ -1,2 +0,0 @@ -<div id="album-release-grid" class="table-responsive"></div> -<button class="btn x-search-back">Back</button> \ No newline at end of file diff --git a/src/UI/Album/Search/NoResultsView.js b/src/UI/Album/Search/NoResultsView.js deleted file mode 100644 index 4622e235f..000000000 --- a/src/UI/Album/Search/NoResultsView.js +++ /dev/null @@ -1,5 +0,0 @@ -var Marionette = require('marionette'); - -module.exports = Marionette.ItemView.extend({ - template : 'Album/Search/NoResultsViewTemplate' -}); \ No newline at end of file diff --git a/src/UI/Album/Search/NoResultsViewTemplate.hbs b/src/UI/Album/Search/NoResultsViewTemplate.hbs deleted file mode 100644 index 7904e5520..000000000 --- a/src/UI/Album/Search/NoResultsViewTemplate.hbs +++ /dev/null @@ -1 +0,0 @@ -<div>No results found</div> \ No newline at end of file diff --git a/src/UI/Album/Summary/AlbumSummaryLayout.js b/src/UI/Album/Summary/AlbumSummaryLayout.js deleted file mode 100644 index f031de8af..000000000 --- a/src/UI/Album/Summary/AlbumSummaryLayout.js +++ /dev/null @@ -1,119 +0,0 @@ -var reqres = require('../../reqres'); -var Marionette = require('marionette'); -var Backgrid = require('backgrid'); -var TrackFileModel = require('../../Artist/TrackFileModel'); -var TrackFileCollection = require('../../Artist/TrackFileCollection'); -var FileSizeCell = require('../../Cells/FileSizeCell'); -var QualityCell = require('../../Cells/QualityCell'); -var DeleteEpisodeFileCell = require('../../Cells/DeleteEpisodeFileCell'); -var NoFileView = require('./NoFileView'); -var LoadingView = require('../../Shared/LoadingView'); - -module.exports = Marionette.Layout.extend({ - template : 'Album/Summary/AlbumSummaryLayoutTemplate', - - regions : { - overview : '.album-overview', - activity : '.album-file-info' - }, - - columns : [ - { - name : 'path', - label : 'Path', - cell : 'string', - sortable : false - }, - { - name : 'size', - label : 'Size', - cell : FileSizeCell, - sortable : false - }, - { - name : 'quality', - label : 'Quality', - cell : QualityCell, - sortable : false, - editable : true - }, - { - name : 'this', - label : '', - cell : DeleteEpisodeFileCell, - sortable : false - } - ], - - templateHelpers : {}, - - initialize : function(options) { - if (!this.model.artist) { - this.templateHelpers.artist = options.artist.toJSON(); - } - }, - - onShow : function() { - if (this.model.get('hasFile')) { //TODO Refactor for Albums - var episodeFileId = this.model.get('episodeFileId'); - - if (reqres.hasHandler(reqres.Requests.GetEpisodeFileById)) { - var episodeFile = reqres.request(reqres.Requests.GetEpisodeFileById, episodeFileId); - this.trackFileCollection = new TrackFileCollection(episodeFile, { seriesId : this.model.get('seriesId') }); - this.listenTo(episodeFile, 'destroy', this._episodeFileDeleted); - - this._showTable(); - } - - else { - this.activity.show(new LoadingView()); - - var self = this; - var newEpisodeFile = new TrackFileModel({ id : episodeFileId }); - this.episodeFileCollection = new TrackFileCollection(newEpisodeFile, { seriesId : this.model.get('seriesId') }); - var promise = newEpisodeFile.fetch(); - this.listenTo(newEpisodeFile, 'destroy', this._trackFileDeleted); - - promise.done(function() { - self._showTable(); - }); - } - - this.listenTo(this.episodeFileCollection, 'add remove', this._collectionChanged); - } - - else { - this._showNoFileView(); - } - }, - - _showTable : function() { - this.activity.show(new Backgrid.Grid({ - collection : this.trackFileCollection, - columns : this.columns, - className : 'table table-bordered', - emptyText : 'Nothing to see here!' - })); - }, - - _showNoFileView : function() { - this.activity.show(new NoFileView()); - }, - - _collectionChanged : function() { - if (!this.trackFileCollection.any()) { - this._showNoFileView(); - } - - else { - this._showTable(); - } - }, - - _trackFileDeleted : function() { - this.model.set({ - trackFileId : 0, - hasFile : false - }); - } -}); \ No newline at end of file diff --git a/src/UI/Album/Summary/AlbumSummaryLayoutTemplate.hbs b/src/UI/Album/Summary/AlbumSummaryLayoutTemplate.hbs deleted file mode 100644 index b64441561..000000000 --- a/src/UI/Album/Summary/AlbumSummaryLayoutTemplate.hbs +++ /dev/null @@ -1,11 +0,0 @@ -<div class="album-info"> - {{profile profileId}} - <span class="label label-info">{{label}}</span> - <span class="label label-info">{{RelativeDate releaseDate}}</span> -</div> - -<div class="album-overview"> - {{overview}} -</div> - -<div class="album-file-info"></div> diff --git a/src/UI/Album/Summary/NoFileView.js b/src/UI/Album/Summary/NoFileView.js deleted file mode 100644 index facf379d0..000000000 --- a/src/UI/Album/Summary/NoFileView.js +++ /dev/null @@ -1,5 +0,0 @@ -var Marionette = require('marionette'); - -module.exports = Marionette.ItemView.extend({ - template : 'Album/Summary/NoFileViewTemplate' -}); \ No newline at end of file diff --git a/src/UI/Album/Summary/NoFileViewTemplate.hbs b/src/UI/Album/Summary/NoFileViewTemplate.hbs deleted file mode 100644 index 6224d7fa0..000000000 --- a/src/UI/Album/Summary/NoFileViewTemplate.hbs +++ /dev/null @@ -1,3 +0,0 @@ -<p class="text-warning"> - No file(s) available for this album. -</p> \ No newline at end of file diff --git a/src/UI/AlbumStudio/AlbumStudioCollectionView.js b/src/UI/AlbumStudio/AlbumStudioCollectionView.js deleted file mode 100644 index b844743c2..000000000 --- a/src/UI/AlbumStudio/AlbumStudioCollectionView.js +++ /dev/null @@ -1,25 +0,0 @@ -var _ = require('underscore'); -var Marionette = require('marionette'); -var SingleAlbumCell = require('./SingleAlbumCell'); -var AsSortedCollectionView = require('../Mixins/AsSortedCollectionView'); - -var view = Marionette.CollectionView.extend({ - - itemView : SingleAlbumCell, - - initialize : function(options) { - this.albumCollection = options.collection; - this.artist = options.artist; - }, - - itemViewOptions : function() { - return { - albumCollection : this.albumCollection, - artist : this.artist - }; - } -}); - -AsSortedCollectionView.call(view); - -module.exports = view; \ No newline at end of file diff --git a/src/UI/AlbumStudio/AlbumStudioFooterView.js b/src/UI/AlbumStudio/AlbumStudioFooterView.js deleted file mode 100644 index 2726b0803..000000000 --- a/src/UI/AlbumStudio/AlbumStudioFooterView.js +++ /dev/null @@ -1,116 +0,0 @@ -var _ = require('underscore'); -var $ = require('jquery'); -var Marionette = require('marionette'); -var vent = require('vent'); -var RootFolders = require('../AddArtist/RootFolders/RootFolderCollection'); - -module.exports = Marionette.ItemView.extend({ - template : 'AlbumStudio/AlbumStudioFooterViewTemplate', - - ui : { - artistMonitored : '.x-artist-monitored', - monitor : '.x-monitor', - selectedCount : '.x-selected-count', - container : '.artist-editor-footer', - actions : '.x-action', - indicator : '.x-indicator', - indicatorIcon : '.x-indicator-icon' - }, - - events : { - 'click .x-update' : '_update' - }, - - initialize : function(options) { - this.artistCollection = options.collection; - - RootFolders.fetch().done(function() { - RootFolders.synced = true; - }); - - this.editorGrid = options.editorGrid; - this.listenTo(this.artistCollection, 'backgrid:selected', this._updateInfo); - }, - - onRender : function() { - this._updateInfo(); - }, - - _update : function() { - var self = this; - var selected = this.editorGrid.getSelectedModels(); - var artistMonitored = this.ui.artistMonitored.val(); - var monitoringOptions; - - _.each(selected, function(model) { - if (artistMonitored === 'true') { - model.set('monitored', true); - } else if (artistMonitored === 'false') { - model.set('monitored', false); - } - - monitoringOptions = self._getMonitoringOptions(model); - model.set('addOptions', monitoringOptions); - }); - - var promise = $.ajax({ - url : window.NzbDrone.ApiRoot + '/albumstudio', - type : 'POST', - data : JSON.stringify({ - artist : _.map(selected, function (model) { - return model.toJSON(); - }), - monitoringOptions : monitoringOptions - }) - }); - - this.ui.indicator.show(); - - promise.always(function () { - self.ui.indicator.hide(); - }); - - promise.done(function () { - self.artistCollection.trigger('albumstudio:saved'); - }); - }, - - _updateInfo : function() { - var selected = this.editorGrid.getSelectedModels(); - var selectedCount = selected.length; - - this.ui.selectedCount.html('{0} artists selected'.format(selectedCount)); - - if (selectedCount === 0) { - this.ui.actions.attr('disabled', 'disabled'); - } else { - this.ui.actions.removeAttr('disabled'); - } - }, - - _getMonitoringOptions : function(model) { - var monitor = this.ui.monitor.val(); - - if (monitor === 'noChange') { - return null; - } - - model.setAlbumPass(0); - - var options = { - ignoreTracksWithFiles : false, - ignoreTracksWithoutFiles : false, - monitored : true - }; - - if (monitor === 'all') { - return options; - } - - else if (monitor === 'none') { - options.monitored = false; - } - - return options; - } -}); \ No newline at end of file diff --git a/src/UI/AlbumStudio/AlbumStudioFooterViewTemplate.hbs b/src/UI/AlbumStudio/AlbumStudioFooterViewTemplate.hbs deleted file mode 100644 index 35987aa7f..000000000 --- a/src/UI/AlbumStudio/AlbumStudioFooterViewTemplate.hbs +++ /dev/null @@ -1,31 +0,0 @@ -<div class="artist-editor-footer"> - <div class="row"> - <div class="form-group col-md-2"> - <label>Monitor artist</label> - - <select class="form-control x-action x-artist-monitored"> - <option value="noChange">No change</option> - <option value="true">Monitored</option> - <option value="false">Unmonitored</option> - </select> - </div> - - <div class="form-group col-md-2"> - <label>Monitor albums</label> - - <select class="form-control x-action x-monitor"> - <option value="noChange">No change</option> - <option value="all">All</option> - <option value="none">None</option> - </select> - </div> - - <div class="form-group col-md-3 actions"> - <label class="x-selected-count">0 artists selected</label> - <div> - <button class="btn btn-primary x-action x-update">Update Selected Artist</button> - <span class="indicator x-indicator"><i class="icon-lidarr-spinner fa-spin"></i></span> - </div> - </div> - </div> -</div> diff --git a/src/UI/AlbumStudio/AlbumStudioLayout.js b/src/UI/AlbumStudio/AlbumStudioLayout.js deleted file mode 100644 index aba4256c2..000000000 --- a/src/UI/AlbumStudio/AlbumStudioLayout.js +++ /dev/null @@ -1,147 +0,0 @@ -var _ = require('underscore'); -var vent = require('vent'); -var Backgrid = require('backgrid'); -var Marionette = require('marionette'); -var EmptyView = require('../Artist/Index/EmptyView'); -var ArtistCollection = require('../Artist/ArtistCollection'); -var ToolbarLayout = require('../Shared/Toolbar/ToolbarLayout'); -var FooterView = require('./AlbumStudioFooterView'); -var SelectAllCell = require('../Cells/SelectAllCell'); -var ArtistStatusCell = require('../Cells/ArtistStatusCell'); -var ArtistTitleCell = require('../Cells/ArtistTitleCell'); -var ArtistMonitoredCell = require('../Cells/ArtistMonitoredCell'); -var AlbumsCell = require('./AlbumsCell'); -require('../Mixins/backbone.signalr.mixin'); - -module.exports = Marionette.Layout.extend({ - template : 'AlbumStudio/AlbumStudioLayoutTemplate', - - regions : { - toolbar : '#x-toolbar', - artist : '#x-artist' - }, - - columns : [ - { - name : '', - cell : SelectAllCell, - headerCell : 'select-all', - sortable : false - }, - { - name : 'statusWeight', - label : '', - cell : ArtistStatusCell - }, - { - name : 'monitored', - label : 'Artist', - cell : ArtistMonitoredCell, - trueClass : 'icon-lidarr-monitored', - falseClass : 'icon-lidarr-unmonitored', - tooltip : 'Toggle artist monitored status', - sortable : false - }, - { - name : 'albums', - label : 'Albums', - cell : AlbumsCell, - cellValue : 'this' - } - ], - - initialize : function() { - this.artistCollection = ArtistCollection.clone(); - - this.artistCollection.shadowCollection.bindSignalR(); - - this.listenTo(this.artistCollection, 'sync', this.render); - this.listenTo(this.artistCollection, 'albumstudio:saved', this.render); - - this.filteringOptions = { - type : 'radio', - storeState : true, - menuKey : 'albumstudio.filterMode', - defaultAction : 'all', - items : [ - { - key : 'all', - title : '', - tooltip : 'All', - icon : 'icon-lidarr-all', - callback : this._setFilter - }, - { - key : 'monitored', - title : '', - tooltip : 'Monitored Only', - icon : 'icon-lidarr-monitored', - callback : this._setFilter - }, - { - key : 'continuing', - title : '', - tooltip : 'Continuing Only', - icon : 'icon-lidarr-artist-continuing', - callback : this._setFilter - }, - { - key : 'ended', - title : '', - tooltip : 'Ended Only', - icon : 'icon-lidarr-artist-ended', - callback : this._setFilter - } - ] - }; - }, - - onRender : function() { - this._showTable(); - this._showToolbar(); - this._showFooter(); - }, - - onClose : function() { - vent.trigger(vent.Commands.CloseControlPanelCommand); - }, - - _showToolbar : function() { - this.toolbar.show(new ToolbarLayout({ - right : [this.filteringOptions], - context : this - })); - }, - - _showTable : function() { - if (this.artistCollection.shadowCollection.length === 0) { - this.artist.show(new EmptyView()); - this.toolbar.close(); - return; - } - - this.columns[0].sortedCollection = this.artistCollection; - - this.editorGrid = new Backgrid.Grid({ - collection : this.artistCollection, - columns : this.columns, - className : 'table table-hover' - }); - - this.artist.show(this.editorGrid); - this._showFooter(); - }, - - _showFooter : function() { - vent.trigger(vent.Commands.OpenControlPanelCommand, new FooterView({ - editorGrid : this.editorGrid, - collection : this.artistCollection - })); - }, - - _setFilter : function(buttonContext) { - var mode = buttonContext.model.get('key'); - - this.artistCollection.setFilterMode(mode); - } -}); \ No newline at end of file diff --git a/src/UI/AlbumStudio/AlbumStudioLayoutTemplate.hbs b/src/UI/AlbumStudio/AlbumStudioLayoutTemplate.hbs deleted file mode 100644 index 7f552b226..000000000 --- a/src/UI/AlbumStudio/AlbumStudioLayoutTemplate.hbs +++ /dev/null @@ -1,14 +0,0 @@ -<h1>Album Studio</h1> -<div id="x-toolbar"></div> - -<div class="row"> - <div class="col-md-12"> - <div class="alert alert-info">Album Studio allows you to quickly change the monitored status of albums for all your artists in one place</div> - </div> -</div> - -<div class="row"> - <div class="col-md-12"> - <div id="x-artist"></div> - </div> -</div> \ No newline at end of file diff --git a/src/UI/AlbumStudio/AlbumsCell.js b/src/UI/AlbumStudio/AlbumsCell.js deleted file mode 100644 index 0c7d7efc0..000000000 --- a/src/UI/AlbumStudio/AlbumsCell.js +++ /dev/null @@ -1,59 +0,0 @@ -var $ = require('jquery'); -var _ = require('underscore'); -var vent = require('vent'); -var Marionette = require('marionette'); -var TemplatedCell = require('../Cells/TemplatedCell'); -var AlbumCollection = require('../Artist/AlbumCollection'); -var LoadingView = require('../Shared/LoadingView'); -var ArtistCollection = require('../Artist/ArtistCollection'); -var AlbumCollectionView = require('./AlbumStudioCollectionView'); -//require('../Handlebars/Helpers/Numbers'); - -module.exports = Marionette.Layout.extend({ - template : 'AlbumStudio/AlbumsCellTemplate', - tagName : 'td', - - regions : { - albums : '#albums' - }, - - initialize : function() { - this.artistCollection = ArtistCollection.clone(); - this.artistCollection.shadowCollection.bindSignalR(); - - this.listenTo(this.model, 'change:monitored', this._setMonitoredState); - this.listenTo(this.model, 'remove', this._artistRemoved); - this.listenTo(vent, vent.Events.CommandComplete, this._commandComplete); - - this.listenTo(this.model, 'change', function(model, options) { - if (options && options.changeSource === 'signalr') { - this._refresh(); - } - }); - }, - - onRender : function(){ - this._showAlbums(); - }, - - _showAlbums : function() { - var self = this; - - this.albums.show(new LoadingView()); - - this.albumCollection = new AlbumCollection({ artistId : this.model.id }).bindSignalR(); - - $.when(this.albumCollection.fetch()).done(function() { - var albumCollectionView = new AlbumCollectionView({ - collection : self.albumCollection, - artist : self.model - }); - - if (!self.isClosed) { - self.albums.show(albumCollectionView); - } - }); - }, - - -}); \ No newline at end of file diff --git a/src/UI/AlbumStudio/AlbumsCellTemplate.hbs b/src/UI/AlbumStudio/AlbumsCellTemplate.hbs deleted file mode 100644 index b05bd454b..000000000 --- a/src/UI/AlbumStudio/AlbumsCellTemplate.hbs +++ /dev/null @@ -1 +0,0 @@ -<div id="albums" class="artist-albums"></div> \ No newline at end of file diff --git a/src/UI/AlbumStudio/SingleAlbumCell.js b/src/UI/AlbumStudio/SingleAlbumCell.js deleted file mode 100644 index 2553056fd..000000000 --- a/src/UI/AlbumStudio/SingleAlbumCell.js +++ /dev/null @@ -1,64 +0,0 @@ -var vent = require('vent'); -var Marionette = require('marionette'); -var Backgrid = require('backgrid'); -var ToggleCell = require('../Cells/TrackMonitoredCell'); -var CommandController = require('../Commands/CommandController'); -var moment = require('moment'); -var _ = require('underscore'); -var Messenger = require('../Shared/Messenger'); - -module.exports = Marionette.Layout.extend({ - template : 'AlbumStudio/SingleAlbumCellTemplate', - - ui : { - albumMonitored : '.x-album-monitored' - }, - - events : { - 'click .x-album-monitored' : '_albumMonitored' - }, - - - initialize : function(options) { - this.artist = options.artist; - this.listenTo(this.model, 'sync', this._afterAlbumMonitored); - - }, - - onRender : function() { - this._setAlbumMonitoredState(); - }, - - _albumMonitored : function() { - if (!this.artist.get('monitored')) { - - Messenger.show({ - message : 'Unable to change monitored state when artist is not monitored', - type : 'error' - }); - - return; - } - - var savePromise = this.model.save('monitored', !this.model.get('monitored'), { wait : true }); - - this.ui.albumMonitored.spinForPromise(savePromise); - }, - - _afterAlbumMonitored : function() { - this.render(); - }, - - _setAlbumMonitoredState : function() { - this.ui.albumMonitored.removeClass('icon-lidarr-spinner fa-spin'); - - if (this.model.get('monitored')) { - this.ui.albumMonitored.addClass('icon-lidarr-monitored'); - this.ui.albumMonitored.removeClass('icon-lidarr-unmonitored'); - } else { - this.ui.albumMonitored.addClass('icon-lidarr-unmonitored'); - this.ui.albumMonitored.removeClass('icon-lidarr-monitored'); - } - } - -}); \ No newline at end of file diff --git a/src/UI/AlbumStudio/SingleAlbumCellTemplate.hbs b/src/UI/AlbumStudio/SingleAlbumCellTemplate.hbs deleted file mode 100644 index 5296d3b86..000000000 --- a/src/UI/AlbumStudio/SingleAlbumCellTemplate.hbs +++ /dev/null @@ -1,30 +0,0 @@ -{{#if_eq statistics.totalTrackCount compare=0}} -<span class="single-album album-unaired"> -{{else}} -{{#if_eq statistics.percentOfTracks compare=100}} -<span class="single-album album-all"> -{{else}} -<span class="single-album album-partial"> -{{/if_eq}} -{{/if_eq}} - <span class="label"> - <span class="x-album-monitored album-monitored" title="Toggle album monitored status" data-album-number="{{seasonNumber}}"> - - </span> - <span class="album-number">{{Pad2 title}}</span> - </span><span class="label"> - {{#with statistics}} - {{#if_eq trackCount compare=0}} - <span class="album-status" title="No aired tracks"> </span> - {{else}} - {{#if_eq percentOfEpisodes compare=100}} - <span class="album-status" title="{{trackFileCount}}/{{trackCount}} tracks downloaded">{{trackFileCount}}/{{trackCount}}</span> - {{else}} - <span class="album-status" title="{{trackFileCount}}/{{trackCount}} tracks downloaded">{{trackFileCount}}/{{trackCount}}</span> - {{/if_eq}} - {{/if_eq}} - {{else}} - <span class="album-status" title="No aired tracks"> </span> - {{/with}} - </span> -</span> \ No newline at end of file diff --git a/src/UI/AlbumStudio/albumstudio.less b/src/UI/AlbumStudio/albumstudio.less deleted file mode 100644 index efa157d55..000000000 --- a/src/UI/AlbumStudio/albumstudio.less +++ /dev/null @@ -1,61 +0,0 @@ -@import "../Content/badges.less"; -@import "../Shared/Styles/clickable.less"; - -.artist-albums { - div { - display : inline-block; - padding : 4px; - } -} - -.single-album { - display : inline-block; - margin-bottom : 4px; - - .label { - .badge-inverse(); - - display : inline-block; - padding : 4px; - - font-size : 14px; - height : 25px; - } - - .label:first-child { - border-right : 0px; - border-top-right-radius : 0.0em; - border-bottom-right-radius : 0.0em; - color : #777; - background-color : #eee; - } - - .label:last-child { - border-left : 0px; - border-top-left-radius : 0.0em; - border-bottom-left-radius : 0.0em; - color : #999; - background-color : #f7f7f7; - } - - &.album-all .label:last-child { - background-color : #e0ffe0; - } - - .album-monitored { - width : 16px; - - i { - .clickable(); - } - } - - .album-number { - font-size : 12px; - } - - .album-status { - display : inline-block; - vertical-align : baseline !important; - } -} diff --git a/src/UI/AppLayout.js b/src/UI/AppLayout.js deleted file mode 100644 index 862961423..000000000 --- a/src/UI/AppLayout.js +++ /dev/null @@ -1,20 +0,0 @@ -var Marionette = require('marionette'); -var ModalRegion = require('./Shared/Modal/ModalRegion'); -var ModalRegion2 = require('./Shared/Modal/ModalRegion2'); -var ControlPanelRegion = require('./Shared/ControlPanel/ControlPanelRegion'); - -var Layout = Marionette.Layout.extend({ - regions : { - navbarRegion : '#nav-region', - mainRegion : '#main-region' - }, - - initialize : function() { - this.addRegions({ - modalRegion : ModalRegion, - modalRegion2 : ModalRegion2, - controlPanelRegion : ControlPanelRegion - }); - } -}); -module.exports = new Layout({ el : 'body' }); \ No newline at end of file diff --git a/src/UI/Artist/AlbumCollection.js b/src/UI/Artist/AlbumCollection.js deleted file mode 100644 index ac89d58cf..000000000 --- a/src/UI/Artist/AlbumCollection.js +++ /dev/null @@ -1,43 +0,0 @@ -var Backbone = require('backbone'); -var AlbumModel = require('./AlbumModel'); - -module.exports = Backbone.Collection.extend({ - url : window.NzbDrone.ApiRoot + '/album', - model : AlbumModel, - - originalFetch : Backbone.Collection.prototype.fetch, - - initialize : function(options) { - this.artistId = options.artistId; - this.models = []; - }, - - comparator : function(model1, model2) { - var album1 = model1.get('releaseDate'); - var album2 = model2.get('releaseDate'); - - if (album1 > album2) { - return -1; - } - - if (album1 < album2) { - return 1; - } - - return 0; - }, - - fetch : function(options) { - if (!this.artistId) { - throw 'artistId is required'; - } - - if (!options) { - options = {}; - } - - options.data = { artistId : this.artistId }; - - return this.originalFetch.call(this, options); - } -}); \ No newline at end of file diff --git a/src/UI/Artist/AlbumModel.js b/src/UI/Artist/AlbumModel.js deleted file mode 100644 index ecdda2ce4..000000000 --- a/src/UI/Artist/AlbumModel.js +++ /dev/null @@ -1,8 +0,0 @@ -var Backbone = require('backbone'); - -module.exports = Backbone.Model.extend({ - - defaults : { - artistId : 0 - }, -}); \ No newline at end of file diff --git a/src/UI/Artist/ArtistCollection.js b/src/UI/Artist/ArtistCollection.js deleted file mode 100644 index fe4b7d52b..000000000 --- a/src/UI/Artist/ArtistCollection.js +++ /dev/null @@ -1,145 +0,0 @@ -var _ = require('underscore'); -var Backbone = require('backbone'); -var PageableCollection = require('backbone.pageable'); -var ArtistModel = require('./ArtistModel'); -var ApiData = require('../Shared/ApiData'); -var AsFilteredCollection = require('../Mixins/AsFilteredCollection'); -var AsSortedCollection = require('../Mixins/AsSortedCollection'); -var AsPersistedStateCollection = require('../Mixins/AsPersistedStateCollection'); -var moment = require('moment'); -require('../Mixins/backbone.signalr.mixin'); - -var Collection = PageableCollection.extend({ - url : window.NzbDrone.ApiRoot + '/artist', - model : ArtistModel, - tableName : 'artist', - - state : { - sortKey : 'sortName', - order : -1, - pageSize : 100000, - secondarySortKey : 'sortName', - secondarySortOrder : -1 - }, - - mode : 'client', - - importFromList : function(models) { - var self = this; - - var proxy = _.extend(new Backbone.Model(), { - id : '', - - url : self.url + '/import', - - toJSON : function() { - return models; - } - }); - - this.listenTo(proxy, 'sync', function(proxyModel, models) { - this.add(models, { merge : true}); - this.trigger('save', this); - }); - - return proxy.save(); - }, - - save : function() { - var self = this; - - var proxy = _.extend(new Backbone.Model(), { - id : '', - - url : self.url + '/editor', - - toJSON : function() { - return self.filter(function(model) { - return model.edited; - }); - } - }); - - this.listenTo(proxy, 'sync', function(proxyModel, models) { - this.add(models, { merge : true }); - this.trigger('save', this); - }); - - return proxy.save(); - }, - - filterModes : { - 'all' : [ - null, - null - ], - 'continuing' : [ - 'status', - 'continuing' - ], - 'ended' : [ - 'status', - 'ended' - ], - 'monitored' : [ - 'monitored', - true - ], - 'missing' : [ - null, - null, - function(model) { return model.get('trackCount') !== model.get('trackFileCount'); } - ] - }, - - sortMappings : { - title : { - sortKey : 'sortName' - }, - - artistName: { - sortKey : 'name' - }, - - nextAiring : { - sortValue : function(model, attr, order) { - var nextAiring = model.get(attr); - - if (nextAiring) { - return moment(nextAiring).unix(); - } - - if (order === 1) { - return 0; - } - - return Number.MAX_VALUE; - } - }, - - percentOfTracks : { - sortValue : function(model, attr) { - var percentOfTracks = model.get(attr); - var trackCount = model.get('trackCount'); - - return percentOfTracks + trackCount / 1000000; - } - }, - - path : { - sortValue : function(model) { - var path = model.get('path'); - - return path.toLowerCase(); - } - } - } -}); - -Collection = AsFilteredCollection.call(Collection); -Collection = AsSortedCollection.call(Collection); -Collection = AsPersistedStateCollection.call(Collection); - -var data = ApiData.get('artist'); // TOOD: Build backend for artist - -module.exports = new Collection(data, { full : true }).bindSignalR(); diff --git a/src/UI/Artist/ArtistController.js b/src/UI/Artist/ArtistController.js deleted file mode 100644 index 2f54bc2cf..000000000 --- a/src/UI/Artist/ArtistController.js +++ /dev/null @@ -1,36 +0,0 @@ -var NzbDroneController = require('../Shared/NzbDroneController'); -var AppLayout = require('../AppLayout'); -var ArtistCollection = require('./ArtistCollection'); -var ArtistIndexLayout = require('./Index/ArtistIndexLayout'); -var ArtistDetailsLayout = require('./Details/ArtistDetailsLayout'); - -module.exports = NzbDroneController.extend({ - _originalInit : NzbDroneController.prototype.initialize, - - initialize : function() { - this.route('', this.artist); - this.route('artist', this.artist); - this.route('artist/:query', this.artistDetails); - - this._originalInit.apply(this, arguments); - }, - - artist : function() { - this.setTitle('Lidarr'); - this.showMainRegion(new ArtistIndexLayout()); - }, - - artistDetails : function(query) { - var artists = ArtistCollection.where({ nameSlug : query }); - console.log('artistDetails, artists: ', artists); - if (artists.length !== 0) { - var targetArtist = artists[0]; - console.log("[ArtistController] targetArtist: ", targetArtist); - this.setTitle(targetArtist.get('name')); // TODO: Update NzbDroneController - //this.setArtistName(targetSeries.get('artistName')); - this.showMainRegion(new ArtistDetailsLayout({ model : targetArtist })); - } else { - this.showNotFound(); - } - } -}); \ No newline at end of file diff --git a/src/UI/Artist/ArtistModel.js b/src/UI/Artist/ArtistModel.js deleted file mode 100644 index 8f6a225b8..000000000 --- a/src/UI/Artist/ArtistModel.js +++ /dev/null @@ -1,31 +0,0 @@ -var Backbone = require('backbone'); -var _ = require('underscore'); - -module.exports = Backbone.Model.extend({ - urlRoot : window.NzbDrone.ApiRoot + '/artist', - - defaults : { - trackFileCount : 0, - trackCount : 0, - isExisting : false, - status : 0 - }, - - setAlbumsMonitored : function(albumId) { - _.each(this.get('albums'), function(album) { - if (album.albumId === albumId) { - album.monitored = !album.monitored; - } - }); - }, - - setAlbumPass : function(monitored) { - _.each(this.get('albums'), function(album) { - if (monitored === 0) { - album.monitored = true; - } else { - album.monitored = false; - } - }); - } -}); \ No newline at end of file diff --git a/src/UI/Artist/Delete/DeleteArtistTemplate.hbs b/src/UI/Artist/Delete/DeleteArtistTemplate.hbs deleted file mode 100644 index 815140776..000000000 --- a/src/UI/Artist/Delete/DeleteArtistTemplate.hbs +++ /dev/null @@ -1,50 +0,0 @@ -<div class="modal-content"> - <div class="modal-header"> - <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> - <h3>Delete {{name}}</h3> - </div> - <div class="modal-body delete-artist-modal"> - - <div class="row"> - <div class="col-sm-3 hidden-xs"> - {{poster}} - </div> - <div class="col-sm-9"> - <div class="form-horizontal"> - <h3 class="path">{{path}}</h3> - - <div class="form-group"> - <label class="col-sm-4 control-label">Delete all files</label> - - <div class="col-sm-8"> - <div class="input-group"> - <label class="checkbox toggle well"> - <input type="checkbox" class="x-delete-files"/> - <p> - <span>Yes</span> - <span>No</span> - </p> - - <div class="btn slide-button btn-danger"/> - </label> - - <span class="help-inline-checkbox"> - <i class="icon-lidarr-form-info" title="Do you want to delete all files from disk?"/> - <i class="icon-lidarr-form-warning" title="This option is irreversible, use with extreme caution"/> - </span> - </div> - </div> - </div> - <div class="col-md-offset-1 col-md-5 delete-files-info x-delete-files-info"> - {{trackFileCount}} track files will be deleted - </div> - </div> - </div> - </div> - </div> - <div class="modal-footer"> - <span class="indicator x-indicator"><i class="icon-lidarr-spinner fa-spin"></i></span> - <button class="btn" data-dismiss="modal">Cancel</button> - <button class="btn btn-danger x-confirm-delete">Delete</button> - </div> -</div> diff --git a/src/UI/Artist/Delete/DeleteArtistView.js b/src/UI/Artist/Delete/DeleteArtistView.js deleted file mode 100644 index f71c1cfa8..000000000 --- a/src/UI/Artist/Delete/DeleteArtistView.js +++ /dev/null @@ -1,41 +0,0 @@ -var vent = require('vent'); -var Marionette = require('marionette'); - -module.exports = Marionette.ItemView.extend({ - template : 'Artist/Delete/DeleteArtistTemplate', - - events : { - 'click .x-confirm-delete' : 'removeSeries', - 'change .x-delete-files' : 'changeDeletedFiles' - }, - - ui : { - deleteFiles : '.x-delete-files', - deleteFilesInfo : '.x-delete-files-info', - indicator : '.x-indicator' - }, - - removeSeries : function() { - var self = this; - var deleteFiles = this.ui.deleteFiles.prop('checked'); - this.ui.indicator.show(); - - this.model.destroy({ - data : { 'deleteFiles' : deleteFiles }, - wait : true - }).done(function() { - vent.trigger(vent.Events.SeriesDeleted, { series : self.model }); - vent.trigger(vent.Commands.CloseModalCommand); - }); - }, - - changeDeletedFiles : function() { - var deleteFiles = this.ui.deleteFiles.prop('checked'); - - if (deleteFiles) { - this.ui.deleteFilesInfo.show(); - } else { - this.ui.deleteFilesInfo.hide(); - } - } -}); \ No newline at end of file diff --git a/src/UI/Artist/Details/AlbumCollectionView.js b/src/UI/Artist/Details/AlbumCollectionView.js deleted file mode 100644 index ad6b4b6ec..000000000 --- a/src/UI/Artist/Details/AlbumCollectionView.js +++ /dev/null @@ -1,46 +0,0 @@ -var _ = require('underscore'); -var Marionette = require('marionette'); -var AlbumLayout = require('./AlbumLayout'); -var AsSortedCollectionView = require('../../Mixins/AsSortedCollectionView'); - -var view = Marionette.CollectionView.extend({ - - itemView : AlbumLayout, - - initialize : function(options) { - if (!options.trackCollection) { - throw 'trackCollection is needed'; - } - - this.albumCollection = options.collection; - this.trackCollection = options.trackCollection; - this.artist = options.artist; - }, - - itemViewOptions : function() { - return { - albumCollection : this.albumCollection, - trackCollection : this.trackCollection, - artist : this.artist - }; - }, - - onTrackGrabbed : function(message) { - if (message.track.artist.id !== this.trackCollection.artistId) { - return; - } - - var self = this; - - _.each(message.track.tracks, function(track) { - var ep = self.TrackCollection.get(track.id); - ep.set('downloading', true); - }); - - this.render(); - } -}); - -AsSortedCollectionView.call(view); - -module.exports = view; \ No newline at end of file diff --git a/src/UI/Artist/Details/AlbumInfoView.js b/src/UI/Artist/Details/AlbumInfoView.js deleted file mode 100644 index 0ecbe5d29..000000000 --- a/src/UI/Artist/Details/AlbumInfoView.js +++ /dev/null @@ -1,18 +0,0 @@ -var Marionette = require('marionette'); -var FormatHelpers = require('../../Shared/FormatHelpers'); - -module.exports = Marionette.ItemView.extend({ - template : 'Artist/Details/AlbumInfoViewTemplate', - - initialize : function(options) { - - this.listenTo(this.model, 'change', this.render); - }, - - templateHelpers : function() { - return { - durationMin : FormatHelpers.timeMinSec(this.model.get('duration')) - }; - } - -}); \ No newline at end of file diff --git a/src/UI/Artist/Details/AlbumInfoViewTemplate.hbs b/src/UI/Artist/Details/AlbumInfoViewTemplate.hbs deleted file mode 100644 index 2f8aebd67..000000000 --- a/src/UI/Artist/Details/AlbumInfoViewTemplate.hbs +++ /dev/null @@ -1,42 +0,0 @@ -<div class="row"> - <div class="col-md-9"> - {{profile profileId}} - - {{#if label}} - <span class="label label-info">{{label}}</span> - {{/if}} - - <span class="label label-info">{{path}}</span> - - {{#if ratings}} - <span class="label label-info" title="{{ratings.votes}} vote{{#if_gt ratings.votes compare="1"}}s{{/if_gt}}">{{ratings.value}}</span> - {{/if}} - - <span class="label label-info">{{durationMin}} minutes</span> - </div> - <div class="col-md-9"> - <span class="album-info-links"> - <a href="{{MBAlbumUrl}}" class="label label-info">MusicBrainz</a> - - {{#if tadbId}} - <a href="{{TADBAlbumUrl}}" class="label label-info">The AudioDB</a> - {{/if}} - - {{#if discogsId}} - <a href="{{discogsAlbumUrl}}" class="label label-info">Discogs</a> - {{/if}} - - {{#if amId}} - <a href="{{allMusicAlbumUrl}}" class="label label-info">AllMusic</a> - {{/if}} - </span> - </div> -</div> - -{{#if tags}} -<div class="row"> - <div class="col-md-12"> - {{tagDisplay tags}} - </div> -</div> -{{/if}} diff --git a/src/UI/Artist/Details/AlbumLayout.js b/src/UI/Artist/Details/AlbumLayout.js deleted file mode 100644 index 11686631b..000000000 --- a/src/UI/Artist/Details/AlbumLayout.js +++ /dev/null @@ -1,348 +0,0 @@ -var vent = require('vent'); -var Marionette = require('marionette'); -var Backgrid = require('backgrid'); -var ToggleCell = require('../../Cells/TrackMonitoredCell'); -var TrackTitleCell = require('../../Cells/TrackTitleCell'); -var TrackExplicitCell = require('../../Cells/TrackExplicitCell'); -var RelativeDateCell = require('../../Cells/RelativeDateCell'); -var TrackStatusCell = require('../../Cells/TrackStatusCell'); -var TrackActionsCell = require('../../Cells/TrackActionsCell'); -var TrackNumberCell = require('./TrackNumberCell'); -var TrackWarningCell = require('./TrackWarningCell'); -var TrackRatingCell = require('./TrackRatingCell'); -var TrackDurationCell = require('../../Cells/TrackDurationCell'); -var AlbumInfoView = require('./AlbumInfoView'); -var CommandController = require('../../Commands/CommandController'); -//var TrackFileEditorLayout = require('../../TrackFile/Editor/TrackFileEditorLayout'); -var moment = require('moment'); -var _ = require('underscore'); -var Messenger = require('../../Shared/Messenger'); - -module.exports = Marionette.Layout.extend({ - template : 'Artist/Details/AlbumLayoutTemplate', - - ui : { - albumSearch : '.x-album-search', - albumMonitored : '.x-album-monitored', - albumRename : '.x-album-rename', - albumDetails : '.x-album-details', - cover : '.x-album-cover' - }, - - events : { - 'click .x-track-file-editor' : '_openTrackFileEditor', - 'click .x-album-monitored' : '_albumMonitored', - 'click .x-album-search' : '_albumSearch', - 'click .x-album-rename' : '_albumRename', - 'click .x-album-details' : '_albumDetails', - 'click .x-show-hide-tracks' : '_showHideTracks', - 'dblclick .artist-album h2' : '_showHideTracks' - }, - - regions : { - trackGrid : '.x-track-grid', - albumInfo : '#album-info' - }, - - columns : [ - { - name : 'monitored', - label : '', - cell : ToggleCell, - trueClass : 'icon-lidarr-monitored', - falseClass : 'icon-lidarr-unmonitored', - tooltip : 'Toggle monitored status', - sortable : false - }, - { - name : 'trackNumber', - label : '#', - cell : TrackNumberCell - }, - { - name : 'this', - label : '', - cell : TrackWarningCell, - sortable : false, - className : 'track-warning-cell' - }, - { - name : 'this', - label : 'Title', - hideArtistLink : true, - cell : TrackTitleCell, - sortable : false - }, - { - name : 'this', - label : 'Rating', - cell : TrackRatingCell - }, - { - name : 'this', - label : 'Content', - cell : TrackExplicitCell - }, - //{ - // name : 'airDateUtc', - // label : 'Air Date', - // cell : RelativeDateCell - //}, - { - name : 'duration', - label : 'Duration', - cell : TrackDurationCell, - sortable : false - }, - { - name : 'status', - label : 'Status', - cell : TrackStatusCell, - sortable : false - } - //{ - // name : 'this', - // label : '', - // cell : TrackActionsCell, - // sortable : false - //} - ], - - templateHelpers : function() { - var trackCount = this.trackCollection.filter(function(track) { - return track.get('hasFile') || track.get('monitored'); - }).length; - - var trackFileCount = this.trackCollection.where({ hasFile : true }).length; - var percentOfTracks = 100; - - if (trackCount > 0) { - percentOfTracks = trackFileCount / trackCount * 100; - } - - return { - showingTracks : this.showingTracks, - trackCount : trackCount, - trackFileCount : trackFileCount, - percentOfTracks : percentOfTracks - }; - }, - - initialize : function(options) { - if (!options.trackCollection) { - throw 'trackCollection is required'; - } - - this.artist = options.artist; - this.fullTrackCollection = options.trackCollection; - - this.trackCollection = this.fullTrackCollection.byAlbum(this.model.get('id')); - this._updateTrackCollection(); - - this.showingTracks = this._shouldShowTracks(); - - this.listenTo(this.model, 'sync', this._afterAlbumMonitored); - this.listenTo(this.trackCollection, 'sync', this.render); - this.listenTo(this.fullTrackCollection, 'sync', this._refreshTracks); - this.listenTo(this.model, 'change:images', this._updateImages); - }, - - onRender : function() { - if (this.showingTracks) { - this._showTracks(); - } - - this._showAlbumInfo(); - - this._setAlbumMonitoredState(); - - CommandController.bindToCommand({ - element : this.ui.albumSearch, - command : { - name : 'albumSearch', - artistId : this.artist.id, - albumIds : [this.model.get('id')] - } - }); - - CommandController.bindToCommand({ - element : this.ui.albumRename, - command : { - name : 'renameFiles', - artistId : this.artist.id, - albumId : this.model.get('id') - } - }); - }, - - _getImage : function(type) { - var image = _.where(this.model.get('images'), { coverType : type }); - - if (image && image[0]) { - return image[0].url; - } - - return undefined; - }, - - _albumSearch : function() { - CommandController.Execute('albumSearch', { - name : 'albumSearch', - artistId : this.artist.id, - albumIds : [this.model.get('id')] - }); - }, - - _albumRename : function() { - vent.trigger(vent.Commands.ShowRenamePreview, { - artist : this.artist, - albumId : this.model.get('id') - }); - }, - - _albumDetails : function() { - vent.trigger(vent.Commands.ShowAlbumDetails, { album : this.model }); - }, - - _albumMonitored : function() { - if (!this.artist.get('monitored')) { - - Messenger.show({ - message : 'Unable to change monitored state when artist is not monitored', - type : 'error' - }); - - return; - } - - //var savePromise = this.model.save('monitored', !this.model.get('monitored'), { wait : true }); - var savePromise = this.model.save('monitored', !this.model.get('monitored'), { wait : true }); - - this.ui.albumMonitored.spinForPromise(savePromise); - }, - - _afterAlbumMonitored : function() { - - this.render(); - }, - - _setAlbumMonitoredState : function() { - this.ui.albumMonitored.removeClass('icon-lidarr-spinner fa-spin'); - - if (this.model.get('monitored')) { - this.ui.albumMonitored.addClass('icon-lidarr-monitored'); - this.ui.albumMonitored.removeClass('icon-lidarr-unmonitored'); - } else { - this.ui.albumMonitored.addClass('icon-lidarr-unmonitored'); - this.ui.albumMonitored.removeClass('icon-lidarr-monitored'); - } - }, - - _showTracks : function() { - this.trackGrid.show(new Backgrid.Grid({ - columns : this.columns, - collection : this.trackCollection, - className : 'table table-hover track-grid' - })); - }, - - _showAlbumInfo : function() { - this.albumInfo.show(new AlbumInfoView({ - model : this.model - })); - }, - - _shouldShowTracks : function() { - var startDate = moment().add('month', -1); - var endDate = moment().add('year', 1); - return true; - //return this.trackCollection.some(function(track) { - // var airDate = track.get('releasedDate'); - - // if (airDate) { - // var airDateMoment = moment(airDate); - - // if (airDateMoment.isAfter(startDate) && airDateMoment.isBefore(endDate)) { - // return true; - // } - // } - - // return false; - //}); - }, - - _showHideTracks : function() { - if (this.showingTracks) { - this.showingTracks = false; - this.trackGrid.close(); - } else { - this.showingTracks = true; - this._showTracks(); - } - - this.templateHelpers.showingTracks = this.showingTracks; - this.render(); - }, - - _trackMonitoredToggled : function(options) { - var model = options.model; - var shiftKey = options.shiftKey; - - if (!this.trackCollection.get(model.get('id'))) { - return; - } - - if (!shiftKey) { - return; - } - - var lastToggled = this.trackCollection.lastToggled; - - if (!lastToggled) { - return; - } - - var currentIndex = this.trackCollection.indexOf(model); - var lastIndex = this.trackCollection.indexOf(lastToggled); - - var low = Math.min(currentIndex, lastIndex); - var high = Math.max(currentIndex, lastIndex); - var range = _.range(low + 1, high); - - this.trackCollection.lastToggled = model; - }, - - _updateTrackCollection : function() { - var self = this; - - this.trackCollection.add(this.fullTrackCollection.byAlbum(this.model.get('albumId')).models, { merge : true }); - - this.trackCollection.each(function(model) { - model.trackCollection = self.trackCollection; - }); - }, - - _updateImages : function () { - var cover = this._getImage('cover'); - - if (cover) { - this.ui.poster.attr('src', cover); - } - }, - - _refreshTracks : function() { - this._updateTrackCollection(); - this.trackCollection.fullCollection.sort(); - this.render(); - }, - - _openTrackFileEditor : function() { - //var view = new TrackFileEditorLayout({ - // model : this.model, - // artist : this.artist, - // trackCollection : this.trackCollection - //}); - - //vent.trigger(vent.Commands.OpenModalCommand, view); - } -}); \ No newline at end of file diff --git a/src/UI/Artist/Details/AlbumLayoutTemplate.hbs b/src/UI/Artist/Details/AlbumLayoutTemplate.hbs deleted file mode 100644 index 6cd18d5d6..000000000 --- a/src/UI/Artist/Details/AlbumLayoutTemplate.hbs +++ /dev/null @@ -1,69 +0,0 @@ -<div class="artist-album" id="album-{{title}}"> - <div class="visible-lg col-lg-2 poster"> - {{cover}} - </div> - <div class="col-md-12 col-lg-10"> - <h2 class="header-text"> - <i class="x-album-monitored album-monitored clickable" title="Toggle album monitored status"/> - - {{#if title}} - <div class="x-album-details album-details"> - {{title}} ({{albumYear}}) - </div> - {{else}} - Specials - {{/if}} - - - {{#if_eq trackCount compare=0}} - {{#if monitored}} - <span class="badge badge-primary album-status" title="No aired tracks"> </span> - {{else}} - <span class="badge badge-warning album-status" title="Album is not monitored"> </span> - {{/if}} - {{else}} - {{#if_eq percentOfTracks compare=100}} - <span class="badge badge-success album-status" title="{{trackFileCount}}/{{trackCount}} tracks downloaded">{{trackFileCount}} / {{trackCount}} Tracks</span> - {{else}} - <span class="badge badge-danger album-status" title="{{trackFileCount}}/{{trackCount}} tracks downloaded">{{trackFileCount}} / {{trackCount}} Tracks</span> - {{/if_eq}} - {{/if_eq}} - - <span class="album-actions pull-right"> - <div class="x-track-file-editor"> - <i class="icon-lidarr-track-file" title="Modify track files for album"/> - </div> - <div class="x-album-rename"> - <i class="icon-lidarr-rename" title="Preview rename for album {{albumId}}"/> - </div> - <div class="x-album-search"> - <i class="icon-lidarr-search" title="Search for monitored tracks in album {{albumId}}"/> - </div> - </span> - - </h2> - - <div class="album-detail-release"> - Release Date: {{albumReleaseDate}} - </div> - <div class="album-detail-type"> - Type: {{albumType}} - </div> - - <div id="album-info" class="album-info"></div> - </div> - - <div class="show-hide-tracks x-show-hide-tracks"> - <h4> - {{#if showingTracks}} - <i class="icon-lidarr-panel-hide"/> - Hide Tracks - {{else}} - <i class="icon-lidarr-panel-show"/> - Show Tracks - {{/if}} - </h4> - </div> - - <div class="x-track-grid table-responsive"></div> -</div> diff --git a/src/UI/Artist/Details/ArtistDetailsLayout.js b/src/UI/Artist/Details/ArtistDetailsLayout.js deleted file mode 100644 index 1bd338a22..000000000 --- a/src/UI/Artist/Details/ArtistDetailsLayout.js +++ /dev/null @@ -1,246 +0,0 @@ -var $ = require('jquery'); -var _ = require('underscore'); -var vent = require('vent'); -var reqres = require('../../reqres'); -var Marionette = require('marionette'); -var Backbone = require('backbone'); -var ArtistCollection = require('../ArtistCollection'); -var TrackCollection = require('../TrackCollection'); -var TrackFileCollection = require('../TrackFileCollection'); -var AlbumCollection = require('../AlbumCollection'); -var AlbumCollectionView = require('./AlbumCollectionView'); -var InfoView = require('./InfoView'); -var CommandController = require('../../Commands/CommandController'); -var LoadingView = require('../../Shared/LoadingView'); -var TrackFileEditorLayout = require('../../EpisodeFile/Editor/EpisodeFileEditorLayout'); -require('backstrech'); -require('../../Mixins/backbone.signalr.mixin'); - -module.exports = Marionette.Layout.extend({ - itemViewContainer : '.x-artist-albums', - template : 'Artist/Details/ArtistDetailsTemplate', - - regions : { - albums : '#albums', - info : '#info' - }, - - ui : { - header : '.x-header', - monitored : '.x-monitored', - edit : '.x-edit', - refresh : '.x-refresh', - rename : '.x-rename', - search : '.x-search', - poster : '.x-artist-poster' - }, - - events : { - 'click .x-track-file-editor' : '_openTrackFileEditor', - 'click .x-monitored' : '_toggleMonitored', - 'click .x-edit' : '_editArtist', - 'click .x-refresh' : '_refreshArtist', - 'click .x-rename' : '_renameArtist', - 'click .x-search' : '_artistSearch' - }, - - initialize : function() { - this.artistCollection = ArtistCollection.clone(); - this.artistCollection.shadowCollection.bindSignalR(); - - this.listenTo(this.model, 'change:monitored', this._setMonitoredState); - this.listenTo(this.model, 'remove', this._artistRemoved); - this.listenTo(vent, vent.Events.CommandComplete, this._commandComplete); - - this.listenTo(this.model, 'change', function(model, options) { - if (options && options.changeSource === 'signalr') { - this._refresh(); - } - }); - - this.listenTo(this.model, 'change:images', this._updateImages); - }, - - onShow : function() { - this._showBackdrop(); - this._showAlbums(); - this._setMonitoredState(); - this._showInfo(); - }, - - onRender : function() { - CommandController.bindToCommand({ - element : this.ui.refresh, - command : { - name : 'refreshArtist' - } - }); - CommandController.bindToCommand({ - element : this.ui.search, - command : { - name : 'artistSearch' - } - }); - - CommandController.bindToCommand({ - element : this.ui.rename, - command : { - name : 'renameFiles', - artistId : this.model.id, - albumId : -1 - } - }); - }, - - onClose : function() { - if (this._backstrech) { - this._backstrech.destroy(); - delete this._backstrech; - } - - $('body').removeClass('backdrop'); - reqres.removeHandler(reqres.Requests.GetEpisodeFileById); - }, - - _getImage : function(type) { - var image = _.where(this.model.get('images'), { coverType : type }); - - if (image && image[0]) { - return image[0].url; - } - - return undefined; - }, - - _toggleMonitored : function() { - var savePromise = this.model.save('monitored', !this.model.get('monitored'), { wait : true }); - - this.ui.monitored.spinForPromise(savePromise); - }, - - _setMonitoredState : function() { - var monitored = this.model.get('monitored'); - - this.ui.monitored.removeAttr('data-idle-icon'); - this.ui.monitored.removeClass('fa-spin icon-lidarr-spinner'); - - if (monitored) { - this.ui.monitored.addClass('icon-lidarr-monitored'); - this.ui.monitored.removeClass('icon-lidarr-unmonitored'); - this.$el.removeClass('artist-not-monitored'); - } else { - this.ui.monitored.addClass('icon-lidarr-unmonitored'); - this.ui.monitored.removeClass('icon-lidarr-monitored'); - this.$el.addClass('artist-not-monitored'); - } - }, - - _editArtist : function() { - vent.trigger(vent.Commands.EditArtistCommand, { artist : this.model }); - }, - - _refreshArtist : function() { - CommandController.Execute('refreshArtist', { - name : 'refreshArtist', - artistId : this.model.id - }); - }, - - _artistRemoved : function() { - Backbone.history.navigate('/', { trigger : true }); - }, - - _renameArtist : function() { - vent.trigger(vent.Commands.ShowRenamePreview, { artist : this.model }); - }, - - _artistSearch : function() { - console.log('_artistSearch:', this.model); - CommandController.Execute('artistSearch', { - name : 'artistSearch', - artistId : this.model.id - }); - }, - - _showAlbums : function() { - var self = this; - - this.albums.show(new LoadingView()); - - this.albumCollection = new AlbumCollection({ artistId : this.model.id }).bindSignalR(); - - this.trackCollection = new TrackCollection({ artistId : this.model.id }).bindSignalR(); - this.trackFileCollection = new TrackFileCollection({ artistId : this.model.id }).bindSignalR(); - - reqres.setHandler(reqres.Requests.GetEpisodeFileById, function(trackFileId) { - return self.trackFileCollection.get(trackFileId); - }); - - - $.when(this.albumCollection.fetch(), this.trackCollection.fetch(), this.trackFileCollection.fetch()).done(function() { - var albumCollectionView = new AlbumCollectionView({ - collection : self.albumCollection, - trackCollection : self.trackCollection, - artist : self.model - }); - - if (!self.isClosed) { - self.albums.show(albumCollectionView); - } - }); - }, - - _showInfo : function() { - this.info.show(new InfoView({ - model : this.model, - trackFileCollection : this.trackFileCollection - })); - }, - - _commandComplete : function(options) { - if (options.command.get('name') === 'renamefiles') { - if (options.command.get('artistId') === this.model.get('id')) { - this._refresh(); - } - } - }, - - _refresh : function() { - this.albumCollection.fetch(); - this.trackCollection.fetch(); - this.trackFileCollection.fetch(); - - this._setMonitoredState(); - this._showInfo(); - }, - - _openTrackFileEditor : function() { - var view = new TrackFileEditorLayout({ - artist : this.model, - trackCollection : this.trackCollection - }); - - vent.trigger(vent.Commands.OpenModalCommand, view); - }, - - _updateImages : function () { - var poster = this._getImage('poster'); - - if (poster) { - this.ui.poster.attr('src', poster); - } - - this._showBackdrop(); - }, - - _showBackdrop : function () { - $('body').addClass('backdrop'); - var fanArt = this._getImage('fanart'); - - if (fanArt) { - this._backstrech = $.backstretch(fanArt); - } else { - $('body').removeClass('backdrop'); - } - } -}); \ No newline at end of file diff --git a/src/UI/Artist/Details/ArtistDetailsTemplate.hbs b/src/UI/Artist/Details/ArtistDetailsTemplate.hbs deleted file mode 100644 index 2e72f1127..000000000 --- a/src/UI/Artist/Details/ArtistDetailsTemplate.hbs +++ /dev/null @@ -1,35 +0,0 @@ -<div class="row artist-page-header"> - <div class="visible-lg col-lg-2 poster"> - {{poster}} - </div> - <div class="col-md-12 col-lg-10"> - <div> - <h1 class="header-text"> - <i class="x-monitored" title="Toggle monitored state for entire artist"/> - {{name}} - <div class="artist-actions pull-right"> - <div class="x-track-file-editor"> - <i class="icon-lidarr-track-file" title="Modify track files for artist"/> - </div> - <div class="x-refresh"> - <i class="icon-lidarr-refresh icon-can-spin" title="Update artist info and scan disk"/> - </div> - <div class="x-rename"> - <i class="icon-lidarr-rename" title="Preview rename for all albums"/> - </div> - <div class="x-search"> - <i class="icon-lidarr-search" title="Search for monitored albums in this artist"/> - </div> - <div class="x-edit"> - <i class="icon-lidarr-edit" title="Edit artist"/> - </div> - </div> - </h1> - </div> - <div class="artist-detail-overview"> - {{overview}} - </div> - <div id="info" class="artist-info"></div> - </div> -</div> -<div id="albums"></div> diff --git a/src/UI/Artist/Details/InfoView.js b/src/UI/Artist/Details/InfoView.js deleted file mode 100644 index 74ddb2bf4..000000000 --- a/src/UI/Artist/Details/InfoView.js +++ /dev/null @@ -1,18 +0,0 @@ -var Marionette = require('marionette'); - -module.exports = Marionette.ItemView.extend({ - template : 'Artist/Details/InfoViewTemplate', - - initialize : function(options) { - this.trackFileCollection = options.trackFileCollection; - - this.listenTo(this.model, 'change', this.render); - this.listenTo(this.trackFileCollection, 'sync', this.render); - }, - - templateHelpers : function() { - return { - fileCount : this.trackFileCollection.length - }; - } -}); \ No newline at end of file diff --git a/src/UI/Artist/Details/InfoViewTemplate.hbs b/src/UI/Artist/Details/InfoViewTemplate.hbs deleted file mode 100644 index 2075d3d8f..000000000 --- a/src/UI/Artist/Details/InfoViewTemplate.hbs +++ /dev/null @@ -1,70 +0,0 @@ -<div class="row"> - <div class="col-md-9"> - {{profile profileId}} - - {{#if network}} - <span class="label label-info">{{network}}</span> - {{/if}} - - <span class="label label-info">{{path}}</span> - - {{#if ratings}} - <span class="label label-info" title="{{ratings.votes}} vote{{#if_gt ratings.votes compare="1"}}s{{/if_gt}}">{{ratings.value}}</span> - {{/if}} - - <span class="label label-info">{{Bytes sizeOnDisk}}</span> - - {{#if_eq fileCount compare="1"}} - <span class="label label-info"> 1 file</span> - {{else}} - <span class="label label-info"> {{fileCount}} files</span> - {{/if_eq}} - - {{#if_eq status compare="continuing"}} - <span class="label label-info">Continuing</span> - {{else}} - <span class="label label-default">Ended</span> - {{/if_eq}} - </div> - <div class="col-md-3"> - <span class="artist-info-links"> - <a href="{{MBUrl}}" class="label label-info">MusicBrainz</a> - - {{#if tadbId}} - <a href="{{TADBUrl}}" class="label label-info">The AudioDB</a> - {{/if}} - - {{#if discogsId}} - <a href="{{discogsUrl}}" class="label label-info">Discogs</a> - {{/if}} - - {{#if allMusicId}} - <a href="{{allMusicUrl}}" class="label label-info">AllMusic</a> - {{/if}} - </span> - </div> -</div> - -{{#if alternateTitles}} -<div class="row"> - <div class="col-md-12"> - {{#each alternateTitles}} - {{#if_eq seasonNumber compare="-1"}} - <span class="label label-default">{{title}}</span> - {{/if_eq}} - - {{#if_eq sceneSeasonNumber compare="-1"}} - <span class="label label-default">{{title}}</span> - {{/if_eq}} - {{/each}} - </div> -</div> -{{/if}} - -{{#if tags}} -<div class="row"> - <div class="col-md-12"> - {{tagDisplay tags}} - </div> -</div> -{{/if}} diff --git a/src/UI/Artist/Details/TrackNumberCell.js b/src/UI/Artist/Details/TrackNumberCell.js deleted file mode 100644 index c085e1d15..000000000 --- a/src/UI/Artist/Details/TrackNumberCell.js +++ /dev/null @@ -1,43 +0,0 @@ -var Marionette = require('marionette'); -var NzbDroneCell = require('../../Cells/NzbDroneCell'); -var reqres = require('../../reqres'); -var ArtistCollection = require('../ArtistCollection'); - -module.exports = NzbDroneCell.extend({ - className : 'track-number-cell', - template : 'Artist/Details/TrackNumberCellTemplate', - - render : function() { - this.$el.empty(); - this.$el.html(this.model.get('trackNumber')); - - var artist = ArtistCollection.get(this.model.get('artistId')); - - var alternateTitles = []; - - if (reqres.hasHandler(reqres.Requests.GetAlternateNameBySeasonNumber)) { - alternateTitles = reqres.request(reqres.Requests.GetAlternateNameBySeasonNumber, this.model.get('seriesId'), this.model.get('seasonNumber'), this.model.get('sceneSeasonNumber')); - } - - if (this.model.get('sceneSeasonNumber') > 0 || this.model.get('sceneEpisodeNumber') > 0 || this.model.has('sceneAbsoluteEpisodeNumber') || alternateTitles.length > 0) { - this.templateFunction = Marionette.TemplateCache.get(this.template); - - var json = this.model.toJSON(); - json.alternateTitles = alternateTitles; - - var html = this.templateFunction(json); - - this.$el.popover({ - content : html, - html : true, - trigger : 'hover', - title : 'Scene Information', - placement : 'right', - container : this.$el - }); - } - - this.delegateEvents(); - return this; - } -}); \ No newline at end of file diff --git a/src/UI/Artist/Details/TrackNumberCellTemplate.hbs b/src/UI/Artist/Details/TrackNumberCellTemplate.hbs deleted file mode 100644 index a9028a423..000000000 --- a/src/UI/Artist/Details/TrackNumberCellTemplate.hbs +++ /dev/null @@ -1,39 +0,0 @@ -<div class="scene-info"> - {{#if sceneSeasonNumber}} - <div class="row"> - <div class="key">Season</div> - <div class="value">{{sceneSeasonNumber}}</div> - </div> - {{/if}} - - {{#if sceneEpisodeNumber}} - <div class="row"> - <div class="key">Episode</div> - <div class="value">{{sceneEpisodeNumber}}</div> - </div> - {{/if}} - - {{#if sceneAbsoluteEpisodeNumber}} - <div class="row"> - <div class="key">Absolute</div> - <div class="value">{{sceneAbsoluteEpisodeNumber}}</div> - </div> - {{/if}} - - {{#if alternateTitles}} - <div class="row"> - {{#if_gt alternateTitles.length compare="1"}} - <div class="key">Titles</div> - {{else}} - <div class="key">Title</div> - {{/if_gt}} - <div class="value"> - <ul> - {{#each alternateTitles}} - <li>{{title}}</li> - {{/each}} - </ul> - </div> - </div> - {{/if}} -</div> \ No newline at end of file diff --git a/src/UI/Artist/Details/TrackRatingCell.js b/src/UI/Artist/Details/TrackRatingCell.js deleted file mode 100644 index 934ac30fa..000000000 --- a/src/UI/Artist/Details/TrackRatingCell.js +++ /dev/null @@ -1,19 +0,0 @@ -var NzbDroneCell = require('../../Cells/NzbDroneCell'); -var ArtistCollection = require('../ArtistCollection'); - -module.exports = NzbDroneCell.extend({ - className : 'track-rating-cell', - - - render : function() { - this.$el.empty(); - var ratings = this.model.get('ratings'); - - if (ratings) { - this.$el.html(ratings.value + ' (' + ratings.votes + ' votes)'); - } - - this.delegateEvents(); - return this; - } -}); \ No newline at end of file diff --git a/src/UI/Artist/Details/TrackWarningCell.js b/src/UI/Artist/Details/TrackWarningCell.js deleted file mode 100644 index 22098d1a8..000000000 --- a/src/UI/Artist/Details/TrackWarningCell.js +++ /dev/null @@ -1,21 +0,0 @@ -var NzbDroneCell = require('../../Cells/NzbDroneCell'); -var ArtistCollection = require('../ArtistCollection'); - -module.exports = NzbDroneCell.extend({ - className : 'track-warning-cell', - - render : function() { - this.$el.empty(); - - if (this.model.get('unverifiedSceneNumbering')) { - this.$el.html('<i class="icon-lidarr-form-warning" title="Scene number hasn\'t been verified yet."></i>'); - } - - else if (ArtistCollection.get(this.model.get('artistId')).get('artistType') === 'anime' && this.model.get('seasonNumber') > 0 && !this.model.has('absoluteEpisodeNumber')) { - this.$el.html('<i class="icon-lidarr-form-warning" title="Track does not have an absolute track number"></i>'); - } - - this.delegateEvents(); - return this; - } -}); \ No newline at end of file diff --git a/src/UI/Artist/Edit/EditArtistView.js b/src/UI/Artist/Edit/EditArtistView.js deleted file mode 100644 index dc0f8732a..000000000 --- a/src/UI/Artist/Edit/EditArtistView.js +++ /dev/null @@ -1,54 +0,0 @@ -var vent = require('vent'); -var Marionette = require('marionette'); -var Profiles = require('../../Profile/ProfileCollection'); -var AsModelBoundView = require('../../Mixins/AsModelBoundView'); -var AsValidatedView = require('../../Mixins/AsValidatedView'); -var AsEditModalView = require('../../Mixins/AsEditModalView'); -require('../../Mixins/TagInput'); -require('../../Mixins/FileBrowser'); - -var view = Marionette.ItemView.extend({ - template : 'Artist/Edit/EditArtistViewTemplate', - - ui : { - profile : '.x-profile', - path : '.x-path', - tags : '.x-tags' - }, - - events : { - 'click .x-remove' : '_removeArtist' - }, - - initialize : function() { - this.model.set('profiles', Profiles); - }, - - onRender : function() { - this.ui.path.fileBrowser(); - this.ui.tags.tagInput({ - model : this.model, - property : 'tags' - }); - }, - - _onBeforeSave : function() { - var profileId = this.ui.profile.val(); - this.model.set({ profileId : profileId }); - }, - - _onAfterSave : function() { - this.trigger('saved'); - vent.trigger(vent.Commands.CloseModalCommand); - }, - - _removeArtist : function() { - vent.trigger(vent.Commands.DeleteArtistCommand, { artist : this.model }); - } -}); - -AsModelBoundView.call(view); -AsValidatedView.call(view); -AsEditModalView.call(view); - -module.exports = view; \ No newline at end of file diff --git a/src/UI/Artist/Edit/EditArtistViewTemplate.hbs b/src/UI/Artist/Edit/EditArtistViewTemplate.hbs deleted file mode 100644 index 666e9b673..000000000 --- a/src/UI/Artist/Edit/EditArtistViewTemplate.hbs +++ /dev/null @@ -1,104 +0,0 @@ -<div class="modal-content"> - <div class="modal-header"> - <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> - <h3>{{name}}</h3> - </div> - <div class="modal-body edit-artist-modal"> - <div class="row"> - <div class="col-sm-3 hidden-xs"> - {{poster}} - </div> - <div class="col-sm-9"> - <div class="form-horizontal"> - - <div class="form-group"> - <label class="col-sm-4 control-label">Monitored</label> - - <div class="col-sm-8"> - <div class="input-group"> - <label class="checkbox toggle well"> - <input type="checkbox" name="monitored"/> - <p> - <span>Yes</span> - <span>No</span> - </p> - - <div class="btn btn-primary slide-button"/> - </label> - - <span class="help-inline-checkbox"> - <i class="icon-lidarr-form-info" title="Should Lidarr download albums for this artist?"/> - </span> - </div> - </div> - </div> - - <div class="form-group"> - <label class="col-sm-4 control-label">Use Album Folder</label> - - <div class="col-sm-8"> - <div class="input-group"> - <label class="checkbox toggle well"> - <input type="checkbox" name="albumFolder"/> - <p> - <span>Yes</span> - <span>No</span> - </p> - - <div class="btn btn-primary slide-button"/> - </label> - - <span class="help-inline-checkbox"> - <i class="icon-lidarr-form-info" title="Should downloaded albums be stored in album folders?"/> - </span> - </div> - </div> - </div> - - <div class="form-group"> - <label class="col-sm-4 control-label">Profile</label> - - <div class="col-sm-4"> - <select class="form-control x-profile" id="inputProfile" name="profileId"> - {{#each profiles.models}} - <option value="{{id}}">{{attributes.name}}</option> - {{/each}} - </select> - - </div> - </div> - - <div class="form-group"> - <label class="col-sm-4 control-label">Artist Type</label> - <div class="col-sm-4"> - {{> ArtistTypeSelectionPartial}} - </div> - </div> - - <div class="form-group"> - <label class="col-sm-4 control-label">Path</label> - - <div class="col-sm-6"> - <input type="text" class="form-control x-path" placeholder="Path" name="path"> - </div> - </div> - - <div class="form-group"> - <label class="col-sm-4 control-label">Tags</label> - - <div class="col-sm-6"> - <input type="text" class="form-control x-tags"> - </div> - </div> - </div> - </div> - </div> - </div> - <div class="modal-footer"> - <button class="btn btn-danger pull-left x-remove">Delete</button> - - <span class="indicator x-indicator"><i class="icon-lidarr-spinner fa-spin"></i></span> - <button class="btn" data-dismiss="modal">Cancel</button> - <button class="btn btn-primary x-save">Save</button> - </div> -</div> diff --git a/src/UI/Artist/Editor/ArtistEditorFooterView.js b/src/UI/Artist/Editor/ArtistEditorFooterView.js deleted file mode 100644 index b32fdc7cc..000000000 --- a/src/UI/Artist/Editor/ArtistEditorFooterView.js +++ /dev/null @@ -1,126 +0,0 @@ -var _ = require('underscore'); -var Marionette = require('marionette'); -var vent = require('vent'); -var Profiles = require('../../Profile/ProfileCollection'); -var RootFolders = require('../../AddArtist/RootFolders/RootFolderCollection'); -var RootFolderLayout = require('../../AddArtist/RootFolders/RootFolderLayout'); -var UpdateFilesArtistView = require('./Organize/OrganizeFilesView'); -var Config = require('../../Config'); - -module.exports = Marionette.ItemView.extend({ - template : 'Artist/Editor/ArtistEditorFooterViewTemplate', - - ui : { - monitored : '.x-monitored', - profile : '.x-profiles', - albumFolder : '.x-album-folder', - rootFolder : '.x-root-folder', - selectedCount : '.x-selected-count', - container : '.artist-editor-footer', - actions : '.x-action' - }, - - events : { - 'click .x-save' : '_updateAndSave', - 'change .x-root-folder' : '_rootFolderChanged', - 'click .x-organize-files' : '_organizeFiles' - }, - - templateHelpers : function() { - return { - profiles : Profiles, - rootFolders : RootFolders.toJSON() - }; - }, - - initialize : function(options) { - this.artistCollection = options.collection; - - RootFolders.fetch().done(function() { - RootFolders.synced = true; - }); - - this.editorGrid = options.editorGrid; - this.listenTo(this.artistCollection, 'backgrid:selected', this._updateInfo); - this.listenTo(RootFolders, 'all', this.render); - }, - - onRender : function() { - this._updateInfo(); - }, - - _updateAndSave : function() { - var selected = this.editorGrid.getSelectedModels(); - - var monitored = this.ui.monitored.val(); - var profile = this.ui.profile.val(); - var albumFolder = this.ui.albumFolder.val(); - var rootFolder = this.ui.rootFolder.val(); - - _.each(selected, function(model) { - if (monitored === 'true') { - model.set('monitored', true); - } else if (monitored === 'false') { - model.set('monitored', false); - } - - if (profile !== 'noChange') { - model.set('profileId', parseInt(profile, 10)); - } - - if (albumFolder === 'true') { - model.set('albumFolder', true); - } else if (albumFolder === 'false') { - model.set('albumFolder', false); - } - - if (rootFolder !== 'noChange') { - var rootFolderPath = RootFolders.get(parseInt(rootFolder, 10)); - - model.set('rootFolderPath', rootFolderPath.get('path')); - } - - model.edited = true; - }); - - this.artistCollection.save(); - }, - - _updateInfo : function() { - var selected = this.editorGrid.getSelectedModels(); - var selectedCount = selected.length; - - this.ui.selectedCount.html('{0} artist selected'.format(selectedCount)); - - if (selectedCount === 0) { - this.ui.actions.attr('disabled', 'disabled'); - } else { - this.ui.actions.removeAttr('disabled'); - } - }, - - _rootFolderChanged : function() { - var rootFolderValue = this.ui.rootFolder.val(); - if (rootFolderValue === 'addNew') { - var rootFolderLayout = new RootFolderLayout(); - this.listenToOnce(rootFolderLayout, 'folderSelected', this._setRootFolder); - vent.trigger(vent.Commands.OpenModalCommand, rootFolderLayout); - } else { - Config.setValue(Config.Keys.DefaultRootFolderId, rootFolderValue); - } - }, - - _setRootFolder : function(options) { - vent.trigger(vent.Commands.CloseModalCommand); - this.ui.rootFolder.val(options.model.id); - this._rootFolderChanged(); - }, - - _organizeFiles : function() { - var selected = this.editorGrid.getSelectedModels(); - var updateFilesArtistView = new UpdateFilesArtistView({ artist : selected }); - this.listenToOnce(updateFilesArtistView, 'updatingFiles', this._afterSave); - - vent.trigger(vent.Commands.OpenModalCommand, updateFilesArtistView); - } -}); \ No newline at end of file diff --git a/src/UI/Artist/Editor/ArtistEditorFooterViewTemplate.hbs b/src/UI/Artist/Editor/ArtistEditorFooterViewTemplate.hbs deleted file mode 100644 index 6f92ca1a4..000000000 --- a/src/UI/Artist/Editor/ArtistEditorFooterViewTemplate.hbs +++ /dev/null @@ -1,54 +0,0 @@ -<div class="artist-editor-footer"> - <div class="row"> - <div class="form-group col-md-2"> - <label>Monitored</label> - - <select class="form-control x-action x-monitored"> - <option value="noChange">No change</option> - <option value="true">Monitored</option> - <option value="false">Unmonitored</option> - </select> - </div> - - <div class="form-group col-md-2"> - <label>Profile</label> - - <select class="form-control x-action x-profiles"> - <option value="noChange">No change</option> - {{#each profiles.models}} - <option value="{{id}}">{{attributes.name}}</option> - {{/each}} - </select> - </div> - - <div class="form-group col-md-2"> - <label>Album Folder</label> - - <select class="form-control x-action x-album-folder"> - <option value="noChange">No change</option> - <option value="true">Yes</option> - <option value="false">No</option> - </select> - </div> - - <div class="form-group col-md-3"> - <label>Root Folder</label> - - <select class="form-control x-action x-root-folder" validation-name="RootFolderPath"> - <option value="noChange">No change</option> - {{#each rootFolders}} - <option value="{{id}}">{{path}}</option> - {{/each}} - <option value="addNew">Add a different path</option> - </select> - </div> - - <div class="form-group col-md-3 actions"> - <label class="x-selected-count">0 artist selected</label> - <div> - <button class="btn btn-primary x-action x-save">Save</button> - <button class="btn btn-danger x-action x-organize-files" title="Organize and rename track files">Organize</button> - </div> - </div> - </div> -</div> diff --git a/src/UI/Artist/Editor/ArtistEditorLayout.js b/src/UI/Artist/Editor/ArtistEditorLayout.js deleted file mode 100644 index 7325e82a4..000000000 --- a/src/UI/Artist/Editor/ArtistEditorLayout.js +++ /dev/null @@ -1,185 +0,0 @@ -var vent = require('vent'); -var Marionette = require('marionette'); -var Backgrid = require('backgrid'); -var EmptyView = require('../Index/EmptyView'); -var ArtistCollection = require('../ArtistCollection'); -var ArtistTitleCell = require('../../Cells/ArtistTitleCell'); -var ProfileCell = require('../../Cells/ProfileCell'); -var ArtistStatusCell = require('../../Cells/ArtistStatusCell'); -var AlbumFolderCell = require('../../Cells/AlbumFolderCell'); -var SelectAllCell = require('../../Cells/SelectAllCell'); -var ToolbarLayout = require('../../Shared/Toolbar/ToolbarLayout'); -var FooterView = require('./ArtistEditorFooterView'); -require('../../Mixins/backbone.signalr.mixin'); - -module.exports = Marionette.Layout.extend({ - template : 'Artist/Editor/ArtistEditorLayoutTemplate', - - regions : { - artistRegion : '#x-artist-editor', - toolbar : '#x-toolbar' - }, - - ui : { - monitored : '.x-monitored', - profiles : '.x-profiles', - rootFolder : '.x-root-folder', - selectedCount : '.x-selected-count' - }, - - events : { - 'click .x-save' : '_updateAndSave', - 'change .x-root-folder' : '_rootFolderChanged' - }, - - columns : [ - { - name : '', - cell : SelectAllCell, - headerCell : 'select-all', - sortable : false - }, - { - name : 'statusWeight', - label : '', - cell : ArtistStatusCell - }, - { - name : 'name', - label : 'Artist', - cell : ArtistTitleCell, - cellValue : 'this' - }, - { - name : 'profileId', - label : 'Profile', - cell : ProfileCell - }, - { - name : 'albumFolder', - label : 'Album Folder', - cell : AlbumFolderCell - }, - { - name : 'path', - label : 'Path', - cell : 'string' - } - ], - - leftSideButtons : { - type : 'default', - storeState : false, - items : [ - { - title : 'Album Studio', - icon : 'icon-lidarr-monitored', - route : 'albumstudio' - }, - { - title : 'Update Library', - icon : 'icon-lidarr-refresh', - command : 'refreshartist', - successMessage : 'Library was updated!', - errorMessage : 'Library update failed!' - } - ] - }, - - initialize : function() { - this.artistCollection = ArtistCollection.clone(); - this.artistCollection.bindSignalR(); - - this.listenTo(this.artistCollection, 'save', this.render); - - this.filteringOptions = { - type : 'radio', - storeState : true, - menuKey : 'artisteditor.filterMode', - defaultAction : 'all', - items : [ - { - key : 'all', - title : '', - tooltip : 'All', - icon : 'icon-lidarr-all', - callback : this._setFilter - }, - { - key : 'monitored', - title : '', - tooltip : 'Monitored Only', - icon : 'icon-lidarr-monitored', - callback : this._setFilter - }, - { - key : 'continuing', - title : '', - tooltip : 'Continuing Only', - icon : 'icon-lidarr-artist-continuing', - callback : this._setFilter - }, - { - key : 'ended', - title : '', - tooltip : 'Ended Only', - icon : 'icon-lidarr-artist-ended', - callback : this._setFilter - } - ] - }; - }, - - onRender : function() { - this._showToolbar(); - this._showTable(); - }, - - onClose : function() { - vent.trigger(vent.Commands.CloseControlPanelCommand); - }, - - _showTable : function() { - if (this.artistCollection.shadowCollection.length === 0) { - this.artistRegion.show(new EmptyView()); - this.toolbar.close(); - return; - } - - this.columns[0].sortedCollection = this.artistCollection; - - this.editorGrid = new Backgrid.Grid({ - collection : this.artistCollection, - columns : this.columns, - className : 'table table-hover' - }); - - this.artistRegion.show(this.editorGrid); - this._showFooter(); - }, - - _showToolbar : function() { - this.toolbar.show(new ToolbarLayout({ - left : [ - this.leftSideButtons - ], - right : [ - this.filteringOptions - ], - context : this - })); - }, - - _showFooter : function() { - vent.trigger(vent.Commands.OpenControlPanelCommand, new FooterView({ - editorGrid : this.editorGrid, - collection : this.artistCollection - })); - }, - - _setFilter : function(buttonContext) { - var mode = buttonContext.model.get('key'); - - this.artistCollection.setFilterMode(mode); - } -}); \ No newline at end of file diff --git a/src/UI/Artist/Editor/ArtistEditorLayoutTemplate.hbs b/src/UI/Artist/Editor/ArtistEditorLayoutTemplate.hbs deleted file mode 100644 index 17746236f..000000000 --- a/src/UI/Artist/Editor/ArtistEditorLayoutTemplate.hbs +++ /dev/null @@ -1,7 +0,0 @@ -<div id="x-toolbar"></div> - -<div class="row"> - <div class="col-md-12"> - <div id="x-artist-editor" class="table-responsive"></div> - </div> -</div> \ No newline at end of file diff --git a/src/UI/Artist/Editor/Organize/OrganizeFilesView.js b/src/UI/Artist/Editor/Organize/OrganizeFilesView.js deleted file mode 100644 index bdd478c4c..000000000 --- a/src/UI/Artist/Editor/Organize/OrganizeFilesView.js +++ /dev/null @@ -1,33 +0,0 @@ -var _ = require('underscore'); -var vent = require('vent'); -var Backbone = require('backbone'); -var Marionette = require('marionette'); -var CommandController = require('../../../Commands/CommandController'); - -module.exports = Marionette.ItemView.extend({ - template : 'Artist/Editor/Organize/OrganizeFilesViewTemplate', - - events : { - 'click .x-confirm-organize' : '_organize' - }, - - initialize : function(options) { - this.artist = options.artist; - this.templateHelpers = { - numberOfArtists : this.artist.length, - artist : new Backbone.Collection(this.artist).toJSON() - }; - }, - - _organize : function() { - var artistIds = _.pluck(this.artist, 'id'); - - CommandController.Execute('renameArtist', { - name : 'renameArtist', - artistIds : artistIds - }); - - this.trigger('organizingFiles'); - vent.trigger(vent.Commands.CloseModalCommand); - } -}); \ No newline at end of file diff --git a/src/UI/Artist/Editor/Organize/OrganizeFilesViewTemplate.hbs b/src/UI/Artist/Editor/Organize/OrganizeFilesViewTemplate.hbs deleted file mode 100644 index 07d548748..000000000 --- a/src/UI/Artist/Editor/Organize/OrganizeFilesViewTemplate.hbs +++ /dev/null @@ -1,25 +0,0 @@ -<div class="modal-content"> - <div class="modal-header"> - <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> - <h3>Organize of Selected Artist(s)</h3> - </div> - <div class="modal-body update-files-artist-modal"> - <div class="alert alert-info"> - <button type="button" class="close" data-dismiss="alert">×</button> - Tip: To preview a rename... select "Cancel" then any artist title and use the <i data-original-title="" class="icon-lidarr-rename" title=""></i> - </div> - - Are you sure you want to update all files in the {{numberOfArtists}} selected artist(s)? - - {{debug}} - <ul class="selected-artist"> - {{#each artist}} - <li>{{name}}</li> - {{/each}} - </ul> - </div> - <div class="modal-footer"> - <button class="btn" data-dismiss="modal">Cancel</button> - <button class="btn btn-danger x-confirm-organize">Organize</button> - </div> -</div> diff --git a/src/UI/Artist/Index/ArtistIndexItemView.js b/src/UI/Artist/Index/ArtistIndexItemView.js deleted file mode 100644 index f3cf4faee..000000000 --- a/src/UI/Artist/Index/ArtistIndexItemView.js +++ /dev/null @@ -1,35 +0,0 @@ -var vent = require('vent'); -var Marionette = require('marionette'); -var CommandController = require('../../Commands/CommandController'); - -module.exports = Marionette.ItemView.extend({ - ui : { - refresh : '.x-refresh' - }, - - events : { - 'click .x-edit' : '_editArtist', - 'click .x-refresh' : '_refreshArtist' - }, - - onRender : function() { - CommandController.bindToCommand({ - element : this.ui.refresh, - command : { - name : 'refreshArtist', - artistId : this.model.get('id') - } - }); - }, - - _editArtist : function() { - vent.trigger(vent.Commands.EditArtistCommand, { artist : this.model }); - }, - - _refreshArtist : function() { - CommandController.Execute('refreshArtist', { - name : 'refreshArtist', - artistId : this.model.id - }); - } -}); \ No newline at end of file diff --git a/src/UI/Artist/Index/ArtistIndexLayout.js b/src/UI/Artist/Index/ArtistIndexLayout.js deleted file mode 100644 index 19769e564..000000000 --- a/src/UI/Artist/Index/ArtistIndexLayout.js +++ /dev/null @@ -1,360 +0,0 @@ -var _ = require('underscore'); -var Marionette = require('marionette'); -var Backgrid = require('backgrid'); -var PosterCollectionView = require('./Posters/ArtistPostersCollectionView'); -var ListCollectionView = require('./Overview/ArtistOverviewCollectionView'); -var EmptyView = require('./EmptyView'); -var ArtistCollection = require('../ArtistCollection'); -var RelativeDateCell = require('../../Cells/RelativeDateCell'); -var ArtistTitleCell = require('../../Cells/ArtistTitleCell'); -var TemplatedCell = require('../../Cells/TemplatedCell'); -var ProfileCell = require('../../Cells/ProfileCell'); -var TrackProgressCell = require('../../Cells/TrackProgressCell'); -var ArtistActionsCell = require('../../Cells/ArtistActionsCell'); -var ArtistStatusCell = require('../../Cells/ArtistStatusCell'); -var FooterView = require('./FooterView'); -var FooterModel = require('./FooterModel'); -var ToolbarLayout = require('../../Shared/Toolbar/ToolbarLayout'); -require('../../Mixins/backbone.signalr.mixin'); - -module.exports = Marionette.Layout.extend({ - template : 'Artist/Index/ArtistIndexLayoutTemplate', - - - regions : { - artistRegion : '#x-artist', - toolbar : '#x-toolbar', - toolbar2 : '#x-toolbar2', - footer : '#x-artist-footer' - }, - - columns : [ - { - name : 'statusWeight', - label : '', - cell : ArtistStatusCell - }, - { - name : 'title', - label : 'Title', - cell : ArtistTitleCell, - cellValue : 'this', - sortValue : 'sortName' - }, - { - name : 'albumCount', - label : 'Albums', - cell : 'integer' - }, - { - name : 'profileId', - label : 'Profile', - cell : ProfileCell - }, - { - name : 'network', - label : 'Network', - cell : 'string' - }, - { - name : 'nextAiring', - label : 'Next Airing', - cell : RelativeDateCell - }, - { - name : 'percentOfTracks', - label : 'Tracks', - cell : TrackProgressCell, - className : 'track-progress-cell' - }, - { - name : 'this', - label : '', - sortable : false, - cell : ArtistActionsCell - } - ], - - leftSideButtons : { - type : 'default', - storeState : false, - collapse : true, - items : [ - { - title : 'Add Artist', - icon : 'icon-lidarr-add', - route : 'addartist' - }, - { - title : 'Album Studio', - icon : 'icon-lidarr-monitored', - route : 'albumstudio' - }, - { - title : 'Artist Editor', - icon : 'icon-lidarr-edit', - route : 'artisteditor' - }, - { - title : 'RSS Sync', - icon : 'icon-lidarr-rss', - command : 'rsssync', - errorMessage : 'RSS Sync Failed!' - }, - { - title : 'Update Library', - icon : 'icon-lidarr-refresh', - command : 'refreshartist', - successMessage : 'Library was updated!', - errorMessage : 'Library update failed!' - } - ] - }, - - initialize : function() { - this.artistCollection = ArtistCollection.clone(); - this.artistCollection.bindSignalR(); - - this.listenTo(this.artistCollection, 'sync', function(model, collection, options) { - this.artistCollection.fullCollection.resetFiltered(); - this._renderView(); - }); - - this.listenTo(this.artistCollection, 'add', function(model, collection, options) { - this.artistCollection.fullCollection.resetFiltered(); - this._renderView(); - }); - - this.listenTo(this.artistCollection, 'remove', function(model, collection, options) { - this.artistCollection.fullCollection.resetFiltered(); - this._renderView(); - }); - - this.sortingOptions = { - type : 'sorting', - storeState : false, - viewCollection : this.artistCollection, - items : [ - { - title : 'Title', - name : 'title' - }, - { - title : 'Albums', - name : 'albumCount' - }, - { - title : 'Quality', - name : 'profileId' - }, - { - title : 'Network', - name : 'network' - }, - { - title : 'Next Airing', - name : 'nextAiring' - }, - { - title : 'Tracks', - name : 'percentOfTracks' - } - ] - }; - - this.filteringOptions = { - type : 'radio', - storeState : true, - menuKey : 'series.filterMode', - defaultAction : 'all', - items : [ - { - key : 'all', - title : '', - tooltip : 'All', - icon : 'icon-lidarr-all', - callback : this._setFilter - }, - { - key : 'monitored', - title : '', - tooltip : 'Monitored Only', - icon : 'icon-lidarr-monitored', - callback : this._setFilter - }, - { - key : 'continuing', - title : '', - tooltip : 'Continuing Only', - icon : 'icon-lidarr-artist-continuing', - callback : this._setFilter - }, - { - key : 'ended', - title : '', - tooltip : 'Ended Only', - icon : 'icon-lidarr-artist-ended', - callback : this._setFilter - }, - { - key : 'missing', - title : '', - tooltip : 'Missing', - icon : 'icon-lidarr-missing', - callback : this._setFilter - } - ] - }; - - this.viewButtons = { - type : 'radio', - storeState : true, - menuKey : 'seriesViewMode', - defaultAction : 'listView', - items : [ - { - key : 'posterView', - title : '', - tooltip : 'Posters', - icon : 'icon-lidarr-view-poster', - callback : this._showPosters - }, - { - key : 'listView', - title : '', - tooltip : 'Overview List', - icon : 'icon-lidarr-view-list', - callback : this._showList - }, - { - key : 'tableView', - title : '', - tooltip : 'Table', - icon : 'icon-lidarr-view-table', - callback : this._showTable - } - ] - }; - }, - - onShow : function() { - this._showToolbar(); - this._fetchCollection(); - }, - - _showTable : function() { - this.currentView = new Backgrid.Grid({ - collection : this.artistCollection, - columns : this.columns, - className : 'table table-hover' - }); - - this._renderView(); - }, - - _showList : function() { - this.currentView = new ListCollectionView({ - collection : this.artistCollection - }); - - this._renderView(); - }, - - _showPosters : function() { - this.currentView = new PosterCollectionView({ - collection : this.artistCollection - }); - - this._renderView(); - }, - - _renderView : function() { - // Problem is this is calling before artistCollection has updated. Where are the promises with backbone? - if (this.artistCollection.length === 0) { - this.artistRegion.show(new EmptyView()); - - this.toolbar.close(); - this.toolbar2.close(); - } else { - this.artistRegion.show(this.currentView); - - this._showToolbar(); - this._showFooter(); - } - }, - - _fetchCollection : function() { - this.artistCollection.fetch(); - console.log('index page, collection: ', this.artistCollection); - }, - - _setFilter : function(buttonContext) { - var mode = buttonContext.model.get('key'); - - this.artistCollection.setFilterMode(mode); - }, - - _showToolbar : function() { - if (this.toolbar.currentView) { - return; - } - - this.toolbar2.show(new ToolbarLayout({ - right : [ - this.filteringOptions - ], - context : this - })); - - this.toolbar.show(new ToolbarLayout({ - right : [ - this.sortingOptions, - this.viewButtons - ], - left : [ - this.leftSideButtons - ], - context : this - })); - }, - - _showFooter : function() { - var footerModel = new FooterModel(); - var artist = this.artistCollection.models.length; - var albums = 0; - var tracks = 0; - var trackFiles = 0; - var ended = 0; - var continuing = 0; - var monitored = 0; - - _.each(this.artistCollection.models, function(model) { - albums += model.get('albumCount'); - tracks += model.get('trackCount'); // TODO: Refactor to Seasons and Tracks - trackFiles += model.get('trackFileCount'); - - /*if (model.get('status').toLowerCase() === 'ended') { - ended++; - } else { - continuing++; - }*/ - - if (model.get('monitored')) { - monitored++; - } - }); - - footerModel.set({ - artist : artist, - ended : ended, - continuing : continuing, - monitored : monitored, - unmonitored : artist - monitored, - albums : albums, - tracks : tracks, - trackFiles : trackFiles - }); - - this.footer.show(new FooterView({ model : footerModel })); - } -}); diff --git a/src/UI/Artist/Index/ArtistIndexLayoutTemplate.hbs b/src/UI/Artist/Index/ArtistIndexLayoutTemplate.hbs deleted file mode 100644 index ac13a764c..000000000 --- a/src/UI/Artist/Index/ArtistIndexLayoutTemplate.hbs +++ /dev/null @@ -1,12 +0,0 @@ -<div class="toolbars"> - <div id="x-toolbar"></div> - <div id="x-toolbar2"></div> -</div> - -<div class="row"> - <div class="col-md-12"> - <div id="x-artist" class="table-responsive"></div> - </div> -</div> - -<div id="x-artist-footer"></div> \ No newline at end of file diff --git a/src/UI/Artist/Index/EmptyTemplate.hbs b/src/UI/Artist/Index/EmptyTemplate.hbs deleted file mode 100644 index ebb59426b..000000000 --- a/src/UI/Artist/Index/EmptyTemplate.hbs +++ /dev/null @@ -1,16 +0,0 @@ -<div class="no-artist"> - <div class="row"> - <div class="well col-md-12"> - <i class="icon-lidarr-comment"/> - You must be new around here. You should add some music. - </div> - </div> - <div class="row"> - <div class="col-md-4 col-md-offset-4"> - <a href="/addartist" class='btn btn-lg btn-block btn-success x-add-artist'> - <i class='icon-lidarr-add'></i> - Add Music - </a> - </div> - </div> -</div> diff --git a/src/UI/Artist/Index/EmptyView.js b/src/UI/Artist/Index/EmptyView.js deleted file mode 100644 index 4ad54762c..000000000 --- a/src/UI/Artist/Index/EmptyView.js +++ /dev/null @@ -1,5 +0,0 @@ -var Marionette = require('marionette'); - -module.exports = Marionette.CompositeView.extend({ - template : 'Artist/Index/EmptyTemplate' -}); \ No newline at end of file diff --git a/src/UI/Artist/Index/FooterModel.js b/src/UI/Artist/Index/FooterModel.js deleted file mode 100644 index 235552061..000000000 --- a/src/UI/Artist/Index/FooterModel.js +++ /dev/null @@ -1,4 +0,0 @@ -var Backbone = require('backbone'); -var _ = require('underscore'); - -module.exports = Backbone.Model.extend({}); \ No newline at end of file diff --git a/src/UI/Artist/Index/FooterView.js b/src/UI/Artist/Index/FooterView.js deleted file mode 100644 index a84d6d01b..000000000 --- a/src/UI/Artist/Index/FooterView.js +++ /dev/null @@ -1,5 +0,0 @@ -var Marionette = require('marionette'); - -module.exports = Marionette.CompositeView.extend({ - template : 'Artist/Index/FooterViewTemplate' -}); \ No newline at end of file diff --git a/src/UI/Artist/Index/FooterViewTemplate.hbs b/src/UI/Artist/Index/FooterViewTemplate.hbs deleted file mode 100644 index f61baa356..000000000 --- a/src/UI/Artist/Index/FooterViewTemplate.hbs +++ /dev/null @@ -1,49 +0,0 @@ -<div class="row"> - <div class="artist-legend legend col-xs-6 col-sm-4"> - <ul class='legend-labels'> - <li><span class="progress-bar"></span>Continuing (All tracks downloaded)</li> - <li><span class="progress-bar-success"></span>Ended (All tracks downloaded)</li> - <li><span class="progress-bar-danger"></span>Missing Tracks (Artist monitored)</li> - <li><span class="progress-bar-warning"></span>Missing Tracks (Artist not monitored)</li> - </ul> - </div> - <div class="col-xs-5 col-sm-7"> - <div class="row"> - <div class="artist-stats col-sm-4"> - <dl class="dl-horizontal"> - <dt>Artists</dt> - <dd>{{artist}}</dd> - - <dt>Ended</dt> - <dd>{{ended}}</dd> - - <dt>Continuing</dt> - <dd>{{continuing}}</dd> - </dl> - </div> - - <div class="artist-stats col-sm-4"> - <dl class="dl-horizontal"> - <dt>Monitored</dt> - <dd>{{monitored}}</dd> - - <dt>Unmonitored</dt> - <dd>{{unmonitored}}</dd> - </dl> - </div> - - <div class="artist-stats col-sm-4"> - <dl class="dl-horizontal"> - <dt>Albums</dt> - <dd>{{albums}}</dd> - - <dt>Tracks</dt> - <dd>{{tracks}}</dd> - - <dt>Files</dt> - <dd>{{trackFiles}}</dd> - </dl> - </div> - </div> - </div> -</div> diff --git a/src/UI/Artist/Index/Overview/ArtistOverviewCollectionView.js b/src/UI/Artist/Index/Overview/ArtistOverviewCollectionView.js deleted file mode 100644 index 0b879824d..000000000 --- a/src/UI/Artist/Index/Overview/ArtistOverviewCollectionView.js +++ /dev/null @@ -1,8 +0,0 @@ -var Marionette = require('marionette'); -var ListItemView = require('./ArtistOverviewItemView'); - -module.exports = Marionette.CompositeView.extend({ - itemView : ListItemView, - itemViewContainer : '#x-artist-list', - template : 'Artist/Index/Overview/ArtistOverviewCollectionViewTemplate' -}); \ No newline at end of file diff --git a/src/UI/Artist/Index/Overview/ArtistOverviewCollectionViewTemplate.hbs b/src/UI/Artist/Index/Overview/ArtistOverviewCollectionViewTemplate.hbs deleted file mode 100644 index 04687a280..000000000 --- a/src/UI/Artist/Index/Overview/ArtistOverviewCollectionViewTemplate.hbs +++ /dev/null @@ -1 +0,0 @@ -<div id="x-artist-list"/> diff --git a/src/UI/Artist/Index/Overview/ArtistOverviewItemView.js b/src/UI/Artist/Index/Overview/ArtistOverviewItemView.js deleted file mode 100644 index 82b9485da..000000000 --- a/src/UI/Artist/Index/Overview/ArtistOverviewItemView.js +++ /dev/null @@ -1,7 +0,0 @@ -var vent = require('vent'); -var Marionette = require('marionette'); -var ArtistIndexItemView = require('../ArtistIndexItemView'); - -module.exports = ArtistIndexItemView.extend({ - template : 'Artist/Index/Overview/ArtistOverviewItemViewTemplate' -}); \ No newline at end of file diff --git a/src/UI/Artist/Index/Overview/ArtistOverviewItemViewTemplate.hbs b/src/UI/Artist/Index/Overview/ArtistOverviewItemViewTemplate.hbs deleted file mode 100644 index 0f2babb96..000000000 --- a/src/UI/Artist/Index/Overview/ArtistOverviewItemViewTemplate.hbs +++ /dev/null @@ -1,59 +0,0 @@ -<div class="artist-item"> - <div class="row"> - <div class="col-md-2 col-xs-3"> - <a href="{{route}}"> - {{poster}} - </a> - </div> - <div class="col-md-10 col-xs-9"> - <div class="row"> - <div class="col-md-10 col-xs-10"> - <a href="{{route}}" target="_blank"> - <h2>{{name}}</h2> - </a> - </div> - <div class="col-md-2 col-xs-2"> - <div class="pull-right artist-overview-list-actions"> - <i class="icon-lidarr-refresh x-refresh" title="Update artist info and scan disk"/> - <i class="icon-lidarr-edit x-edit" title="Edit Artist"/> - </div> - </div> - </div> - <div class="row"> - <div class="col-md-10 col-xs-12"> - <div> - {{truncate overview 600}} - </div> - </div> - </div> - <div class="row"> - <div class="col-md-12"> -   - </div> - </div> - <div class="row"> - <div class="col-md-10 col-xs-8"> - <!--{{#if_eq status compare="ended"}} - <span class="label label-danger">Ended</span> - {{/if_eq}}--> - - <!-- - NOTE: We can show next drop date of album in future - {{#if nextAiring}} - <span class="label label-default">{{RelativeDate nextAiring}}</span> - {{/if}}--> - - {{albumCountHelper}} - - {{profile profileId}} - </div> - <div class="col-md-2 col-xs-4"> - {{> TrackProgressPartial }} - </div> - <div class="col-md-8 col-xs-10"> - Path {{path}} - </div> - </div> - </div> - </div> -</div> diff --git a/src/UI/Artist/Index/Posters/ArtistPostersCollectionView.js b/src/UI/Artist/Index/Posters/ArtistPostersCollectionView.js deleted file mode 100644 index cc712d9da..000000000 --- a/src/UI/Artist/Index/Posters/ArtistPostersCollectionView.js +++ /dev/null @@ -1,8 +0,0 @@ -var Marionette = require('marionette'); -var PosterItemView = require('./ArtistPostersItemView'); - -module.exports = Marionette.CompositeView.extend({ - itemView : PosterItemView, - itemViewContainer : '#x-artist-posters', - template : 'Artist/Index/Posters/ArtistPostersCollectionViewTemplate' -}); \ No newline at end of file diff --git a/src/UI/Artist/Index/Posters/ArtistPostersCollectionViewTemplate.hbs b/src/UI/Artist/Index/Posters/ArtistPostersCollectionViewTemplate.hbs deleted file mode 100644 index 0ac6a892e..000000000 --- a/src/UI/Artist/Index/Posters/ArtistPostersCollectionViewTemplate.hbs +++ /dev/null @@ -1 +0,0 @@ -<ul id="x-artist-posters" class="artist-posters"></ul> \ No newline at end of file diff --git a/src/UI/Artist/Index/Posters/ArtistPostersItemView.js b/src/UI/Artist/Index/Posters/ArtistPostersItemView.js deleted file mode 100644 index 3dc34cb4f..000000000 --- a/src/UI/Artist/Index/Posters/ArtistPostersItemView.js +++ /dev/null @@ -1,19 +0,0 @@ -var ArtistIndexItemView = require('../ArtistIndexItemView'); - -module.exports = ArtistIndexItemView.extend({ - tagName : 'li', - template : 'Artist/Index/Posters/ArtistPostersItemViewTemplate', - - initialize : function() { - this.events['mouseenter .x-artist-poster-container'] = 'posterHoverAction'; - this.events['mouseleave .x-artist-poster-container'] = 'posterHoverAction'; - - this.ui.controls = '.x-artist-controls'; - this.ui.title = '.x-title'; - }, - - posterHoverAction : function() { - this.ui.controls.slideToggle(); - this.ui.title.slideToggle(); - } -}); \ No newline at end of file diff --git a/src/UI/Artist/Index/Posters/ArtistPostersItemViewTemplate.hbs b/src/UI/Artist/Index/Posters/ArtistPostersItemViewTemplate.hbs deleted file mode 100644 index 62a0a0a10..000000000 --- a/src/UI/Artist/Index/Posters/ArtistPostersItemViewTemplate.hbs +++ /dev/null @@ -1,30 +0,0 @@ -<div class="artist-posters-item"> - <div class="center"> - <div class="artist-poster-container x-artist-poster-container"> - <div class="artist-controls x-artist-controls"> - <i class="icon-lidarr-refresh x-refresh" title="Refresh Artist"/> - <i class="icon-lidarr-edit x-edit" title="Edit Artist"/> - </div> - {{#unless_eq status compare="continuing"}} - <div class="ended-banner">Ended</div> - {{/unless_eq}} - <a href="{{route}}"> - {{poster}} - <div class="center title">{{title}}</div> - </a> - <div class="hidden-title x-title"> - {{title}} - </div> - </div> - </div> - - <div class="center"> - <div class="labels"> - {{> EpisodeProgressPartial }} - - {{#if nextAiring}} - <span class="label label-default">{{RelativeDate nextAiring}}</span> - {{/if}} - </div> - </div> -</div> diff --git a/src/UI/Artist/Index/TrackProgressPartial.hbs b/src/UI/Artist/Index/TrackProgressPartial.hbs deleted file mode 100644 index a9cec28f7..000000000 --- a/src/UI/Artist/Index/TrackProgressPartial.hbs +++ /dev/null @@ -1,4 +0,0 @@ -<div class="progress track-progress"> - <span class="progressbar-back-text">{{trackFileCount}} / {{trackCount}}</span> - <div class="progress-bar {{TrackProgressClass}} track-progress" style="width:{{percentOfTracks}}%"><span class="progressbar-front-text">{{trackFileCount}} / {{trackCount}}</span></div> -</div> \ No newline at end of file diff --git a/src/UI/Artist/TrackCollection.js b/src/UI/Artist/TrackCollection.js deleted file mode 100644 index 502827325..000000000 --- a/src/UI/Artist/TrackCollection.js +++ /dev/null @@ -1,62 +0,0 @@ -var Backbone = require('backbone'); -var PageableCollection = require('backbone.pageable'); -var TrackModel = require('./TrackModel'); -require('./TrackCollection'); - -module.exports = PageableCollection.extend({ - url : window.NzbDrone.ApiRoot + '/track', - model : TrackModel, - - state : { - sortKey : 'trackNumber', - order : -1, - pageSize : 100000 - }, - - mode : 'client', - - originalFetch : Backbone.Collection.prototype.fetch, - - initialize : function(options) { - this.artistId = options.artistId; - }, - - byAlbum : function(album) { - var filtered = this.filter(function(track) { - return track.get('albumId') === album; - }); - - var TrackCollection = require('./TrackCollection'); - - return new TrackCollection(filtered); - }, - - comparator : function(model1, model2) { - var track1 = model1.get('trackNumber'); - var track2 = model2.get('trackNumber'); - - if (track1 < track2) { - return -1; - } - - if (track1 > track2) { - return 1; - } - - return 0; - }, - - fetch : function(options) { - if (!this.artistId) { - throw 'artistId is required'; - } - - if (!options) { - options = {}; - } - - options.data = { artistId : this.artistId }; - - return this.originalFetch.call(this, options); - } -}); \ No newline at end of file diff --git a/src/UI/Artist/TrackFileCollection.js b/src/UI/Artist/TrackFileCollection.js deleted file mode 100644 index 9b9909ce6..000000000 --- a/src/UI/Artist/TrackFileCollection.js +++ /dev/null @@ -1,28 +0,0 @@ -var Backbone = require('backbone'); -var TrackFileModel = require('./TrackFileModel'); - -module.exports = Backbone.Collection.extend({ - url : window.NzbDrone.ApiRoot + '/trackfile', - model : TrackFileModel, - - originalFetch : Backbone.Collection.prototype.fetch, - - initialize : function(options) { - this.artistId = options.artistId; - this.models = []; - }, - - fetch : function(options) { - if (!this.artistId) { - throw 'artistId is required'; - } - - if (!options) { - options = {}; - } - - options.data = { artistId : this.artistId }; - - return this.originalFetch.call(this, options); - } -}); \ No newline at end of file diff --git a/src/UI/Artist/TrackFileModel.js b/src/UI/Artist/TrackFileModel.js deleted file mode 100644 index 3986a5948..000000000 --- a/src/UI/Artist/TrackFileModel.js +++ /dev/null @@ -1,3 +0,0 @@ -var Backbone = require('backbone'); - -module.exports = Backbone.Model.extend({}); \ No newline at end of file diff --git a/src/UI/Artist/TrackModel.js b/src/UI/Artist/TrackModel.js deleted file mode 100644 index 30a2702d7..000000000 --- a/src/UI/Artist/TrackModel.js +++ /dev/null @@ -1,20 +0,0 @@ -var Backbone = require('backbone'); - -module.exports = Backbone.Model.extend({ - defaults : { - albumId : 0, - status : 0 - }, - - methodUrls : { - 'update' : window.NzbDrone.ApiRoot + '/track' - }, - - sync : function(method, model, options) { - if (model.methodUrls && model.methodUrls[method.toLowerCase()]) { - options = options || {}; - options.url = model.methodUrls[method.toLowerCase()]; - } - return Backbone.sync(method, model, options); - } -}); \ No newline at end of file diff --git a/src/UI/Artist/artist.less b/src/UI/Artist/artist.less deleted file mode 100644 index 55769827e..000000000 --- a/src/UI/Artist/artist.less +++ /dev/null @@ -1,534 +0,0 @@ -@import "../Content/Bootstrap/variables"; -@import "../Shared/Styles/card.less"; -@import "../Shared/Styles/clickable.less"; -@import "../Content/prefixer"; - -.artist-poster { - min-width: 56px; - max-width: 100%; -} - -.album-cover { - min-width: 56px; - max-width: 100%; - -} - -.album-details { - display: inline; - color: #428bca; - text-decoration: none; - - &:focus, &:hover { - color: #2a6496; - text-decoration: underline; - cursor: pointer; - } -} - -.truncate { - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -.edit-artist-modal, .delete-artist-modal { - overflow : visible; - - .artist-poster { - padding-left : 20px; - width : 168px; - } - - .album-cover { - padding-left : 20px; - width : 168px; - } - - .form-horizontal { - margin-top : 10px; - } - - .twitter-typeahead { - .form-control[disabled] { - background-color: #ffffff; - } - } -} - -.delete-artist-modal { - .path { - margin-left : 30px; - } - - .delete-files-info { - margin-top : 10px; - display : none; - } -} - -.artist-item { - padding-bottom : 30px; - - :hover { - text-decoration : none; - } - - h2 { - margin-top : 0px; - } - - a { - color : #000000; - } -} - -.artist-page-header { - .card(black); - .opacity(0.9); - background : #000000; - color : #ffffff; - padding : 30px 15px; - margin : 50px 10px; - - .poster { - margin-top : 4px; - } - - .header-text { - margin-top : 0px; - } -} - -.artist-album { - .card; - .opacity(0.9); - margin : 30px 10px; - padding : 30px 25px; - - .show-hide-tracks { - .clickable(); - text-align : center; - - i { - .clickable(); - } - } - .header-text { - margin-top : 0px; - } -} - -.artist-posters { - list-style-type: none; - - @media (max-width: @screen-xs-max) { - padding : 0px; - } - - li { - display : inline-block; - vertical-align : top; - } - - .artist-posters-item { - - .card; - .clickable; - margin-bottom : 20px; - height : 315px; - - .center { - display : block; - margin-left : auto; - margin-right : auto; - text-align : center; - - .progress { - text-align : left; - margin-top : 5px; - left : 0px; - width : 170px; - - .progressbar-front-text, .progressbar-back-text { - width : 170px; - } - } - } - - .labels { - display : inline-block; - .opacity(0.75); - width : 170px; - - :hover { - cursor : default; - } - - .label { - margin-top : 3px; - display : block; - } - - .tooltip { - .opacity(1); - } - } - - @media (max-width: @screen-xs-max) { - height : 235px; - margin : 5px; - padding : 6px 5px; - - .center { - .progress { - width : 125px; - - .progressbar-front-text, .progressbar-back-text { - width : 125px - } - } - } - - .labels { - width: 125px; - } - } - } - - .artist-poster-container { - position : relative; - overflow : hidden; - display : inline-block; - - .placeholder-image ~ .title { - opacity: 1.0; - } - - .title { - position : absolute; - top : 25px; - color : #f5f5f5; - width : 100%; - font-size : 22px; - line-height: 24px; - opacity : 0.0; - font-weight: 100; - } - - .ended-banner { - color : #eeeeee; - background-color : #b94a48; - .box-shadow(2px 2px 20px #888888); - -moz-transform-origin : 50% 50%; - -webkit-transform-origin : 50% 50%; - position : absolute; - width : 320px; - top : 200px; - left : -122px; - text-align : center; - .opacity(0.9); - - .transform(rotate(45deg)); - } - - .artist-controls { - position : absolute;; - top : 0px; - overflow : hidden; - background-color : #eeeeee; - width : 100%; - text-align : right; - padding-right : 10px; - display : none; - .opacity(0.8); - - i { - .clickable(); - } - } - - .hidden-title { - position : absolute;; - bottom : 0px; - overflow : hidden; - background-color : #eeeeee; - width : 100%; - text-align : center; - .opacity(0.8); - display : none; - } - - .artist-poster { - width : 168px; - height : 247px; - display : block; - font-size : 34px; - line-height : 34px; - } - - @media (max-width: @screen-xs-max) { - .artist-poster { - width : 120px; - height : 176px; - } - - .ended-banner { - top : 145px; - left : -137px; - } - } - } -} - -.artist-detail-overview { - margin-bottom : 50px; -} - -.artist-album { - - .track-number-cell { - width : 40px; - white-space: nowrap; - } - .track-air-date-cell { - width : 150px; - } - - .track-status-cell { - width : 100px; - } - .track-rating-cell { - width : 150px; - } - .track-explicit-cell { - width : 100px; - } - .track-duration-cell { - width : 100px; - } - - .track-title-cell { - cursor : pointer; - } -} - -.track-detail-modal { - - .track-info { - margin-bottom : 10px; - } - - .track-overview { - font-style : italic; - } - - .track-file-info { - margin-top : 30px; - font-size : 12px; - } - - .track-history-details-cell .popover { - max-width: 800px; - } - - .hidden-artist-title { - display : none; - } -} - -.track-grid { - .toggle-cell { - width : 28px; - text-align : center; - padding-left : 0px; - padding-right : 0px; - } - - .toggle-cell { - i { - .clickable; - } - } -} - -.album-actions { - width: 100px; -} - -.album-actions, .artist-actions { - - div { - display : inline-block - } - - text-transform : none; - - i { - .clickable(); - font-size : 24px; - margin-left : 5px; - } -} - -.artist-stats { - font-size : 11px; -} - -.artist-legend { - padding-top : 5px; -} - -.albumpass-artist { - .card; - margin : 20px 0px; - - .title { - font-weight : 300; - font-size : 24px; - line-height : 30px; - margin-left : 5px; - } - - .album-select { - margin-bottom : 0px; - } - - .expander { - .clickable; - line-height : 30px; - margin-left : 8px; - width : 16px; - } - - .album-grid { - margin-top : 10px; - } - - .album-pass-button { - display : inline-block; - } - - .artist-monitor-toggle { - font-size : 24px; - margin-top : 3px; - } - - .help-inline { - margin-top : 7px; - display : inline-block; - } -} - -.album-status { - font-size : 11px; - vertical-align : middle !important; -} - -//Overview List -.artist-overview-list-actions { - min-width: 56px; - max-width: 56px; - - i { - .clickable(); - } -} - -//Editor - -.artist-editor-footer { - max-width: 1160px; - color: #f5f5f5; - margin-left: auto; - margin-right: auto; - - .form-group { - padding-top: 0px; - } -} - -.update-files-artist-modal { - .selected-artist { - margin-top: 15px; - } -} - -//artist Details - -.artist-not-monitored { - .album-monitored, .track-monitored { - color: #888888; - cursor: not-allowed; - - i { - cursor: not-allowed; - } - } -} - -.artist-info { - .row { - margin-bottom : 3px; - - .label { - display : inline-block; - margin-bottom : 2px; - padding : 4px 6px 3px 6px; - max-width : 100%; - white-space : normal; - word-wrap : break-word; - } - } - - .artist-info-links { - @media (max-width: @screen-sm-max) { - display : inline-block; - margin-top : 5px; - } - } -} - -.album-info { - .row { - margin-bottom : 3px; - - .label { - display : inline-block; - margin-bottom : 2px; - padding : 4px 6px 3px 6px; - max-width : 100%; - white-space : normal; - word-wrap : break-word; - } - } - - .album-info-links { - @media (max-width: @screen-sm-max) { - display : inline-block; - margin-top : 5px; - } - } -} - -.scene-info { - .key, .value { - display : inline-block; - } - - .key { - width : 80px; - margin-left : 10px; - vertical-align : top; - } - - .value { - margin-right : 10px; - max-width : 170px; - } - - ul { - padding-left : 0px; - list-style-type : none; - } -} diff --git a/src/UI/Calendar/CalendarCollection.js b/src/UI/Calendar/CalendarCollection.js deleted file mode 100644 index a7a0214c5..000000000 --- a/src/UI/Calendar/CalendarCollection.js +++ /dev/null @@ -1,14 +0,0 @@ -var Backbone = require('backbone'); -var AlbumModel = require('../Artist/AlbumModel'); - -module.exports = Backbone.Collection.extend({ - url : window.NzbDrone.ApiRoot + '/calendar', - model : AlbumModel, - tableName : 'calendar', - - comparator : function(model) { - var date = new Date(model.get('releaseDate')); - var time = date.getTime(); - return time; - } -}); \ No newline at end of file diff --git a/src/UI/Calendar/CalendarFeedView.js b/src/UI/Calendar/CalendarFeedView.js deleted file mode 100644 index 5ccbc82e8..000000000 --- a/src/UI/Calendar/CalendarFeedView.js +++ /dev/null @@ -1,60 +0,0 @@ -var Marionette = require('marionette'); -var StatusModel = require('../System/StatusModel'); -require('../Mixins/CopyToClipboard'); -require('../Mixins/TagInput'); - -module.exports = Marionette.Layout.extend({ - template : 'Calendar/CalendarFeedViewTemplate', - - ui : { - includeUnmonitored : '.x-includeUnmonitored', - premiersOnly : '.x-premiersOnly', - asAllDay : '.x-asAllDay', - tags : '.x-tags', - icalUrl : '.x-ical-url', - icalCopy : '.x-ical-copy', - icalWebCal : '.x-ical-webcal' - }, - - events : { - 'click .x-includeUnmonitored' : '_updateUrl', - 'click .x-premiersOnly' : '_updateUrl', - 'click .x-asAllDay' : '_updateUrl', - 'itemAdded .x-tags' : '_updateUrl', - 'itemRemoved .x-tags' : '_updateUrl' - }, - - onShow : function() { - this._updateUrl(); - this.ui.icalCopy.copyToClipboard(this.ui.icalUrl); - this.ui.tags.tagInput({ allowNew: false }); - }, - - _updateUrl : function() { - var icalUrl = window.location.host + StatusModel.get('urlBase') + '/feed/calendar/Lidarr.ics?'; - - if (this.ui.includeUnmonitored.prop('checked')) { - icalUrl += 'unmonitored=true&'; - } - - if (this.ui.premiersOnly.prop('checked')) { - icalUrl += 'premiersOnly=true&'; - } - - if (this.ui.asAllDay.prop('checked')) { - icalUrl += 'asAllDay=true&'; - } - - if (this.ui.tags.val()) { - icalUrl += 'tags=' + this.ui.tags.val() + '&'; - } - - icalUrl += 'apikey=' + window.NzbDrone.ApiKey; - - var icalHttpUrl = window.location.protocol + '//' + icalUrl; - var icalWebCalUrl = 'webcal://' + icalUrl; - - this.ui.icalUrl.attr('value', icalHttpUrl); - this.ui.icalWebCal.attr('href', icalWebCalUrl); - } -}); \ No newline at end of file diff --git a/src/UI/Calendar/CalendarFeedViewTemplate.hbs b/src/UI/Calendar/CalendarFeedViewTemplate.hbs deleted file mode 100644 index 27411e29f..000000000 --- a/src/UI/Calendar/CalendarFeedViewTemplate.hbs +++ /dev/null @@ -1,57 +0,0 @@ -<div class="modal-content"> - <div class="modal-header"> - <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> - <h3>Lidarr Calendar feed</h3> - </div> - <div class="modal-body edit-series-modal"> - <div class="form-horizontal"> - <div class="form-group"> - <label class="col-sm-3 control-label">Include Unmonitored</label> - - <div class="col-sm-4"> - <div class="input-group"> - <label class="checkbox toggle well"> - <input type="checkbox" name="includeUnmonitored" class="form-control x-includeUnmonitored"/> - - <p> - <span>Yes</span> - <span>No</span> - </p> - - <div class="btn btn-primary slide-button"/> - </label> - </div> - </div> - </div> - <div class="form-group"> - <label class="col-sm-3 control-label">Tags</label> - - <div class="col-sm-1 col-sm-push-5 help-inline"> - <i class="icon-lidarr-form-info" title="One or more tags only show matching series" /> - </div> - - <div class="col-sm-5 col-sm-pull-1"> - <input type="text" class="form-control x-tags"> - </div> - </div> - <div class="form-group"> - <label class="col-sm-3 control-label">iCal feed</label> - <div class="col-sm-1 col-sm-push-8 help-inline"> - <i class="icon-lidarr-form-info" title="Copy this url into your clients subscription form or use the subscribe button if your browser support webcal" /> - </div> - <div class="col-sm-8 col-sm-pull-1"> - <div class="input-group ical-url"> - <input type="text" class="form-control x-ical-url" readonly="readonly" /> - <div class="input-group-btn"> - <button class="btn btn-icon-only x-ical-copy"><i class="icon-lidarr-copy"></i></button> - <button class="btn btn-icon-only no-router x-ical-webcal" title="Subscribe" target="_blank"><i class="icon-lidarr-calendar-o"></i></button> - </div> - </div> - </div> - </div> - </div> - </div> - <div class="modal-footer"> - <button class="btn" data-dismiss="modal">Close</button> - </div> -</div> diff --git a/src/UI/Calendar/CalendarLayout.js b/src/UI/Calendar/CalendarLayout.js deleted file mode 100644 index 3cd289d85..000000000 --- a/src/UI/Calendar/CalendarLayout.js +++ /dev/null @@ -1,96 +0,0 @@ -var AppLayout = require('../AppLayout'); -var Marionette = require('marionette'); -var UpcomingCollectionView = require('./UpcomingCollectionView'); -var CalendarView = require('./CalendarView'); -var CalendarFeedView = require('./CalendarFeedView'); -var ToolbarLayout = require('../Shared/Toolbar/ToolbarLayout'); - -module.exports = Marionette.Layout.extend({ - template : 'Calendar/CalendarLayoutTemplate', - - regions : { - upcoming : '#x-upcoming', - calendar : '#x-calendar', - toolbar : '#x-toolbar' - }, - - onShow : function() { - this._showUpcoming(); - this._showCalendar(); - this._showToolbar(); - }, - - _showUpcoming : function() { - this.upcomingView = new UpcomingCollectionView(); - this.upcoming.show(this.upcomingView); - }, - - _showCalendar : function() { - this.calendarView = new CalendarView(); - this.calendar.show(this.calendarView); - }, - - _showiCal : function() { - var view = new CalendarFeedView(); - AppLayout.modalRegion.show(view); - }, - - _showToolbar : function() { - var leftSideButtons = { - type : 'default', - storeState : false, - items : [ - { - title : 'Get iCal Link', - icon : 'icon-lidarr-calendar-o', - callback : this._showiCal, - ownerContext : this - } - ] - }; - - var filterOptions = { - type : 'radio', - storeState : true, - menuKey : 'calendar.show', - defaultAction : 'monitored', - items : [ - { - key : 'all', - title : '', - tooltip : 'All', - icon : 'icon-lidarr-all', - callback : this._setCalendarFilter - }, - { - key : 'monitored', - title : '', - tooltip : 'Monitored Only', - icon : 'icon-lidarr-monitored', - callback : this._setCalendarFilter - } - ] - }; - - this.toolbar.show(new ToolbarLayout({ - left : [leftSideButtons], - right : [filterOptions], - context : this, - floatOnMobile : true - })); - }, - - _setCalendarFilter : function(buttonContext) { - var mode = buttonContext.model.get('key'); - - if (mode === 'all') { - this.calendarView.setShowUnmonitored(true); - this.upcomingView.setShowUnmonitored(true); - } - - else { - this.calendarView.setShowUnmonitored(false); - this.upcomingView.setShowUnmonitored(false); - } - } -}); \ No newline at end of file diff --git a/src/UI/Calendar/CalendarLayoutTemplate.hbs b/src/UI/Calendar/CalendarLayoutTemplate.hbs deleted file mode 100644 index 508545565..000000000 --- a/src/UI/Calendar/CalendarLayoutTemplate.hbs +++ /dev/null @@ -1,21 +0,0 @@ -<div class="row"> - <div class="col-md-3 hidden-xs"> - <div class="pull-left"> - <h4>Upcoming</h4> - </div> - <div id="x-upcoming"/> - </div> - <div class="col-md-9 col-xs-12"> - <div id="x-toolbar" class="calendar-toolbar"/> - <div id="x-calendar" class="calendar"/> - <div class="legend calendar"> - <ul class='legend-labels'> - <li class="legend-label"><span class="primary" title="Album hasn't aired yet"></span>Unreleased</li> - <li class="legend-label"><span class="purple" title="Album is currently downloading"></span>Downloading</li> - <li class="legend-label"><span class="danger" title="Album file has not been found"></span>Missing</li> - <li class="legend-label"><span class="success" title="Album was downloaded and sorted"></span>Downloaded</li> - <li class="legend-label"><span class="unmonitored" title="Album is unmonitored"></span>Unmonitored</li> - </ul> - </div> - </div> -</div> diff --git a/src/UI/Calendar/CalendarView.js b/src/UI/Calendar/CalendarView.js deleted file mode 100644 index 058a19e61..000000000 --- a/src/UI/Calendar/CalendarView.js +++ /dev/null @@ -1,283 +0,0 @@ -var $ = require('jquery'); -var vent = require('vent'); -var Marionette = require('marionette'); -var moment = require('moment'); -var CalendarCollection = require('./CalendarCollection'); -var UiSettings = require('../Shared/UiSettingsModel'); -var QueueCollection = require('../Activity/Queue/QueueCollection'); -var Config = require('../Config'); - -require('../Mixins/backbone.signalr.mixin'); -require('fullcalendar'); -require('jquery.easypiechart'); - -module.exports = Marionette.ItemView.extend({ - storageKey : 'calendar.view', - - initialize : function() { - this.showUnmonitored = Config.getValue('calendar.show', 'monitored') === 'all'; - this.collection = new CalendarCollection().bindSignalR({ updateOnly : true }); - this.listenTo(this.collection, 'change', this._reloadCalendarEvents); - this.listenTo(QueueCollection, 'sync', this._reloadCalendarEvents); - }, - - render : function() { - this.$el.empty().fullCalendar(this._getOptions()); - }, - - onShow : function() { - this.$('.fc-today-button').click(); - }, - - setShowUnmonitored : function (showUnmonitored) { - if (this.showUnmonitored !== showUnmonitored) { - this.showUnmonitored = showUnmonitored; - this._getEvents(this.$el.fullCalendar('getView')); - } - }, - - _viewRender : function(view, element) { - if (Config.getValue(this.storageKey) !== view.name) { - Config.setValue(this.storageKey, view.name); - } - - this._getEvents(view); - element.find('.fc-day-grid-container').css('height', ''); - }, - - _eventRender : function(event, element) { - element.addClass(event.statusLevel); - element.children('.fc-content').addClass(event.statusLevel); - - if (event.downloading) { - var progress = 100 - event.downloading.get('sizeleft') / event.downloading.get('size') * 100; - var releaseTitle = event.downloading.get('title'); - var estimatedCompletionTime = moment(event.downloading.get('estimatedCompletionTime')).fromNow(); - var status = event.downloading.get('status').toLocaleLowerCase(); - var errorMessage = event.downloading.get('errorMessage'); - - if (status === 'pending') { - this._addStatusIcon(element, 'icon-lidarr-pending', 'Release will be processed {0}'.format(estimatedCompletionTime)); - } - - else if (errorMessage) { - if (status === 'completed') { - this._addStatusIcon(element, 'icon-lidarr-import-failed', 'Import failed: {0}'.format(errorMessage)); - } else { - this._addStatusIcon(element, 'icon-lidarr-download-failed', 'Download failed: {0}'.format(errorMessage)); - } - } - - else if (status === 'failed') { - this._addStatusIcon(element, 'icon-lidarr-download-failed', 'Download failed: check download client for more details'); - } - - else if (status === 'warning') { - this._addStatusIcon(element, 'icon-lidarr-download-warning', 'Download warning: check download client for more details'); - } - - else { - element.find('.fc-time').after('<span class="chart pull-right" data-percent="{0}"></span>'.format(progress)); - - element.find('.chart').easyPieChart({ - barColor : '#ffffff', - trackColor : false, - scaleColor : false, - lineWidth : 2, - size : 14, - animate : false - }); - - element.find('.chart').tooltip({ - title : 'Album is downloading - {0}% {1}'.format(progress.toFixed(1), releaseTitle), - container : '.fc' - }); - } - } - - else if (event.model.get('unverifiedSceneNumbering')) { - this._addStatusIcon(element, 'icon-lidarr-form-warning', 'Scene number hasn\'t been verified yet.'); - } - - }, - - _eventAfterAllRender : function () { - if ($(window).width() < 768) { - this.$('.fc-center').show(); - this.$('.calendar-title').remove(); - - var title = this.$('.fc-center').html(); - var titleDiv = '<div class="calendar-title">{0}</div>'.format(title); - - this.$('.fc-toolbar').before(titleDiv); - this.$('.fc-center').hide(); - } - - this._clearScrollBar(); - }, - - _windowResize : function () { - this._clearScrollBar(); - }, - - _getEvents : function(view) { - var start = moment(view.start.toISOString()).toISOString(); - var end = moment(view.end.toISOString()).toISOString(); - - this.$el.fullCalendar('removeEvents'); - - this.collection.fetch({ - data : { - start : start, - end : end, - unmonitored : this.showUnmonitored - }, - success : this._setEventData.bind(this) - }); - }, - - _setEventData : function(collection) { - if (collection.length === 0) { - return; - } - - var events = []; - var self = this; - - collection.each(function(model) { - var albumTitle = model.get('title'); - var artistName = model.get('artist').name; - var start = model.get('releaseDate'); - var runtime = '30'; - var end = moment(start).add('minutes', runtime).toISOString(); - - var event = { - title : artistName + " - " + albumTitle, - start : moment(start), - end : moment(end), - allDay : true, - statusLevel : self._getStatusLevel(model, end), - downloading : QueueCollection.findEpisode(model.get('id')), - model : model, - sortOrder : 0 - }; - - events.push(event); - }); - - this.$el.fullCalendar('addEventSource', events); - }, - - _getStatusLevel : function(element, endTime) { - var hasFile = element.get('hasFile'); - var downloading = QueueCollection.findEpisode(element.get('id')) || element.get('grabbed'); - var currentTime = moment(); - var start = moment(element.get('releaseDate')); - var end = moment(endTime); - var monitored = element.get('artist').monitored && element.get('monitored'); - - var statusLevel = 'primary'; - - if (hasFile) { - statusLevel = 'success'; - } - - else if (downloading) { - statusLevel = 'purple'; - } - - else if (!monitored) { - statusLevel = 'unmonitored'; - } - - else if (currentTime.isAfter(start) && currentTime.isBefore(end)) { - statusLevel = 'warning'; - } - - else if (start.isBefore(currentTime) && !hasFile) { - statusLevel = 'danger'; - } - - else if (element.get('episodeNumber') === 1) { - statusLevel = 'premiere'; - } - - if (end.isBefore(currentTime.startOf('day'))) { - statusLevel += ' past'; - } - - return statusLevel; - }, - - _reloadCalendarEvents : function() { - this.$el.fullCalendar('removeEvents'); - this._setEventData(this.collection); - }, - - _getOptions : function() { - var options = { - allDayDefault : true, - weekMode : 'variable', - firstDay : UiSettings.get('firstDayOfWeek'), - timeFormat : 'h(:mm)t', - viewRender : this._viewRender.bind(this), - eventRender : this._eventRender.bind(this), - eventAfterAllRender : this._eventAfterAllRender.bind(this), - windowResize : this._windowResize.bind(this), - eventClick : function(event) { - vent.trigger(vent.Commands.ShowAlbumDetails, { album : event.model }); - } - }; - - if ($(window).width() < 768) { - options.defaultView = Config.getValue(this.storageKey, 'basicDay'); - - options.header = { - left : 'prev,next today', - center : 'title', - right : 'basicWeek,listYear' - }; - } - - else { - options.defaultView = Config.getValue(this.storageKey, 'basicWeek'); - - options.header = { - left : 'prev,next today', - center : 'title', - right : 'month,basicWeek,listYear' - }; - } - - options.views = { - month: { - titleFormat: 'MMMM YYYY', - columnFormat: 'ddd' - }, - week: { - titleFormat: UiSettings.get('shortDateFormat'), - columnFormat: UiSettings.get('calendarWeekColumnHeader') - } - - - }; - - options.timeFormat = UiSettings.get('timeFormat'); - - return options; - }, - - _addStatusIcon : function(element, icon, tooltip) { - element.find('.fc-time').after('<span class="status pull-right"><i class="{0}"></i></span>'.format(icon)); - element.find('.status').tooltip({ - title : tooltip, - container : '.fc' - }); - }, - - _clearScrollBar : function () { - // Remove height from calendar so we don't have another scroll bar - this.$('.fc-day-grid-container').css('height', ''); - this.$('.fc-row.fc-widget-header').attr('style', ''); - } -}); \ No newline at end of file diff --git a/src/UI/Calendar/UpcomingCollection.js b/src/UI/Calendar/UpcomingCollection.js deleted file mode 100644 index 7409459a4..000000000 --- a/src/UI/Calendar/UpcomingCollection.js +++ /dev/null @@ -1,28 +0,0 @@ -var Backbone = require('backbone'); -var moment = require('moment'); -var AlbumModel = require('../Artist/AlbumModel'); - -module.exports = Backbone.Collection.extend({ - url : window.NzbDrone.ApiRoot + '/calendar', - model : AlbumModel, - - comparator : function(model1, model2) { - var airDate1 = model1.get('releaseDate'); - var date1 = moment(airDate1); - var time1 = date1.unix(); - - var airDate2 = model2.get('releaseDate'); - var date2 = moment(airDate2); - var time2 = date2.unix(); - - if (time1 < time2) { - return -1; - } - - if (time1 > time2) { - return 1; - } - - return 0; - } -}); \ No newline at end of file diff --git a/src/UI/Calendar/UpcomingCollectionView.js b/src/UI/Calendar/UpcomingCollectionView.js deleted file mode 100644 index c4b40e989..000000000 --- a/src/UI/Calendar/UpcomingCollectionView.js +++ /dev/null @@ -1,36 +0,0 @@ -var _ = require('underscore'); -var Marionette = require('marionette'); -var UpcomingCollection = require('./UpcomingCollection'); -var UpcomingItemView = require('./UpcomingItemView'); -var Config = require('../Config'); -require('../Mixins/backbone.signalr.mixin'); - -module.exports = Marionette.CollectionView.extend({ - itemView : UpcomingItemView, - - initialize : function() { - - this.showUnmonitored = Config.getValue('calendar.show', 'monitored') === 'all'; - - this.collection = new UpcomingCollection().bindSignalR({ updateOnly : true }); - this._fetchCollection(); - - this._fetchCollection = _.bind(this._fetchCollection, this); - this.timer = window.setInterval(this._fetchCollection, 60 * 60 * 1000); - }, - - onClose : function() { - window.clearInterval(this.timer); - }, - - setShowUnmonitored : function (showUnmonitored) { - if (this.showUnmonitored !== showUnmonitored) { - this.showUnmonitored = showUnmonitored; - this._fetchCollection(); - } - }, - - _fetchCollection : function() { - this.collection.fetch({ data: { unmonitored : this.showUnmonitored }}); - } -}); \ No newline at end of file diff --git a/src/UI/Calendar/UpcomingItemView.js b/src/UI/Calendar/UpcomingItemView.js deleted file mode 100644 index 9a47a26dc..000000000 --- a/src/UI/Calendar/UpcomingItemView.js +++ /dev/null @@ -1,28 +0,0 @@ -var vent = require('vent'); -var Marionette = require('marionette'); -var moment = require('moment'); - -module.exports = Marionette.ItemView.extend({ - template : 'Calendar/UpcomingItemViewTemplate', - tagName : 'div', - - events : { - 'click .x-album-title' : '_showAlbumDetails' - }, - - initialize : function() { - var start = this.model.get('releaseDate'); - var runtime = '30'; - var end = moment(start).add('minutes', runtime); - - this.model.set({ - end : end.toISOString() - }); - - this.listenTo(this.model, 'change', this.render); - }, - - _showAlbumDetails : function() { - vent.trigger(vent.Commands.ShowAlbumDetails, { album : this.model }); - } -}); \ No newline at end of file diff --git a/src/UI/Calendar/UpcomingItemViewTemplate.hbs b/src/UI/Calendar/UpcomingItemViewTemplate.hbs deleted file mode 100644 index 6c42e0bba..000000000 --- a/src/UI/Calendar/UpcomingItemViewTemplate.hbs +++ /dev/null @@ -1,18 +0,0 @@ -<div class="event"> - <div class="date {{StatusLevel}}"> - <h1>{{Day releaseDate}}</h1> - <h4>{{Month releaseDate}}</h4> - </div> - {{#with artist}} - <a href="{{route}}"> - <h4>{{name}}</h4> - </a> - {{/with}} - <p>{{#unless_today releaseDate}}{{ShortDate releaseDate}}{{/unless_today}}</p> - <p> - <span class="album-title x-album-title"> - {{title}} - </span> - <span class="pull-right">x</span> - </p> -</div> diff --git a/src/UI/Calendar/calendar.less b/src/UI/Calendar/calendar.less deleted file mode 100644 index 6214b4617..000000000 --- a/src/UI/Calendar/calendar.less +++ /dev/null @@ -1,258 +0,0 @@ -@import "../Content/Bootstrap/mixins"; -@import "../Content/Bootstrap/variables"; -@import "../Content/Bootstrap/buttons"; -@import "../Shared/Styles/clickable"; -@import "../Content/variables"; -@import "../Content/mixins"; -@import "../Content/Overrides/bootstrap"; - -.calendar { - width: 100%; - - th, td { - border-color : #eeeeee; - } - - .fc-event-skin { - background-color : #007ccd; - border : 1px solid #007ccd; - border-radius : 4px; - text-align : center; - } - - .fc-event { - .clickable; - - .status { - margin-right : 4px; - } - } - - th { - background-color : #eeeeee; - } - - h2 { - font-size : 17.5px; - } - - .fc-state-highlight { - background : #dbdbdb; - } - - .past { - opacity : 0.8; - } - - .fc-title { - white-space: normal; - } -} - -.event { - display : inline-block; - width : 100%; - margin-bottom : 10px; - border-top : 1px solid #eeeeee; - padding-top : 10px; - - h4 { - font-weight : 500; - color : #008dcd; - margin : 5px 0px; - } - - p { - color : #999999; - margin : 0px; - } - - .date { - text-align : center; - display : inline-block; - border-left : 4px solid #eeeeee; - padding-left : 16px; - float : left; - margin-right : 20px; - - h4 { - line-height : 1em; - color : #555555; - font-weight : 300; - text-transform : uppercase; - } - - h1 { - font-weight : 500; - line-height : 0.8em; - } - } - - .primary { - border-color : @btn-primary-bg; - } - - .info { - border-color : @btn-info-bg; - } - - .inverse { - border-color : @btn-link-disabled-color; - } - - .warning { - border-color : @btn-warning-bg; - } - - .danger { - border-color : @btn-danger-bg; - } - - .success { - border-color : @btn-success-bg; - } - - .purple { - border-color : @nzbdronePurple; - } - - .pink { - border-color : @nzbdronePink; - } - - .premiere { - border-color : @droneTeal; - } - - .unmonitored { - border-color : grey; - } - - .album-title { - .btn-link; - .text-overflow; - color : @link-color; - margin-top : 1px; - display : inline-block; - - @media (max-width: @screen-xs-min) { - width : 140px; - } - - @media (min-width: @screen-md-min) { - width : 135px; - } - } -} - -.calendar { - -// background-position : -160px -128px; - - .primary { - border-color : @btn-primary-bg; - background-color : @btn-primary-bg; - - .color-impaired-background-gradient(90deg, @btn-primary-bg); - } - - .info { - border-color : @btn-info-bg; - background-color : @btn-info-bg; - } - - .inverse { - border-color : @btn-link-disabled-color; - background-color : @btn-link-disabled-color; - } - - .warning { - border-color : @btn-warning-bg; - background-color : @btn-warning-bg; - - .color-impaired-background-gradient(90deg, @btn-warning-bg); - } - - .danger { - border-color : @btn-danger-bg; - background-color : @btn-danger-bg; - - .color-impaired-background-gradient(90deg, @btn-danger-bg); - } - - .success { - border-color : @btn-success-bg; - background-color : @btn-success-bg; - } - - .purple { - border-color : @nzbdronePurple; - background-color : @nzbdronePurple; - } - - .pink { - border-color : @nzbdronePink; - background-color : @nzbdronePink; - } - - .premiere { - border-color : @droneTeal; - background-color : @droneTeal; - - .color-impaired-background-gradient(90deg, @droneTeal); - } - - .unmonitored { - border-color : grey; - background-color : grey; - - .color-impaired-background-gradient(45deg, grey); - } - - .chart { - margin-top : 2px; - margin-right : 2px; - line-height : 12px; - } - - .legend-labels { - max-width : 100%; - width : 500px; - - @media (max-width: @screen-xs-min) { - width : 100%; - } - } - - .legend-label { - display : inline-block; - width : 150px; - } -} - -.ical { - color: @btn-link-disabled-color; - cursor: pointer; -} - -.ical-url { - - input, input[readonly] { - cursor : text; - } -} - -.calendar-title { - text-align : center; - - h2 { - margin-top : 0px; - margin-bottom : 5px; - } -} - -.calendar-toolbar { - .page-toolbar { - margin-bottom : 10px; - } -} diff --git a/src/UI/Cells/AlbumFolderCell.js b/src/UI/Cells/AlbumFolderCell.js deleted file mode 100644 index bb5173a5c..000000000 --- a/src/UI/Cells/AlbumFolderCell.js +++ /dev/null @@ -1,13 +0,0 @@ -var Backgrid = require('backgrid'); - -module.exports = Backgrid.Cell.extend({ - className : 'artist-folder-cell', - - render : function() { - this.$el.empty(); - var albumFolder = this.model.get(this.column.get('name')); - this.$el.html(albumFolder.toString()); - - return this; - } -}); \ No newline at end of file diff --git a/src/UI/Cells/AlbumTitleCell.js b/src/UI/Cells/AlbumTitleCell.js deleted file mode 100644 index ca3d1cc48..000000000 --- a/src/UI/Cells/AlbumTitleCell.js +++ /dev/null @@ -1,29 +0,0 @@ -var vent = require('vent'); -var NzbDroneCell = require('./NzbDroneCell'); - -module.exports = NzbDroneCell.extend({ - className : 'album-title-cell', - - events : { - 'click' : '_showDetails' - }, - - render : function() { - var title = this.cellValue.get('title'); - - if (!title || title === '') { - title = 'TBA'; - } - - this.$el.html(title); - return this; - }, - - _showDetails : function() { - var hideArtistLink = this.column.get('hideArtistLink'); - vent.trigger(vent.Commands.ShowAlbumDetails, { - album : this.cellValue, - hideArtistLink : hideArtistLink - }); - } -}); \ No newline at end of file diff --git a/src/UI/Cells/ApprovalStatusCell.js b/src/UI/Cells/ApprovalStatusCell.js deleted file mode 100644 index 96e2a45a4..000000000 --- a/src/UI/Cells/ApprovalStatusCell.js +++ /dev/null @@ -1,33 +0,0 @@ -var Backgrid = require('backgrid'); -var Marionette = require('marionette'); -require('bootstrap'); - -module.exports = Backgrid.Cell.extend({ - className : 'approval-status-cell', - template : 'Cells/ApprovalStatusCellTemplate', - - render : function() { - - var rejections = this.model.get(this.column.get('name')); - - if (rejections.length === 0) { - return this; - } - - this.templateFunction = Marionette.TemplateCache.get(this.template); - - var html = this.templateFunction(rejections); - this.$el.html('<i class="icon-lidarr-form-danger"/>'); - - this.$el.popover({ - content : html, - html : true, - trigger : 'hover', - title : this.column.get('title'), - placement : 'left', - container : this.$el - }); - - return this; - } -}); \ No newline at end of file diff --git a/src/UI/Cells/ApprovalStatusCellTemplate.hbs b/src/UI/Cells/ApprovalStatusCellTemplate.hbs deleted file mode 100644 index 87f28cbcb..000000000 --- a/src/UI/Cells/ApprovalStatusCellTemplate.hbs +++ /dev/null @@ -1,11 +0,0 @@ -<ul> - {{#each this}} - <li> - {{#if reason}} - {{reason}} - {{else}} - {{this}} - {{/if}} - </li> - {{/each}} -</ul> diff --git a/src/UI/Cells/ArtistActionsCell.js b/src/UI/Cells/ArtistActionsCell.js deleted file mode 100644 index b8332ce1a..000000000 --- a/src/UI/Cells/ArtistActionsCell.js +++ /dev/null @@ -1,45 +0,0 @@ -var vent = require('vent'); -var NzbDroneCell = require('./NzbDroneCell'); -var CommandController = require('../Commands/CommandController'); - -module.exports = NzbDroneCell.extend({ - className : 'artist-actions-cell', - - ui : { - refresh : '.x-refresh' - }, - - events : { - 'click .x-edit' : '_editArtist', - 'click .x-refresh' : '_refreshArtist' - }, - - render : function() { - this.$el.empty(); - - this.$el.html('<i class="icon-lidarr-refresh x-refresh hidden-xs" title="" data-original-title="Update artist info and scan disk"></i> ' + - '<i class="icon-lidarr-edit x-edit" title="" data-original-title="Edit Artist"></i>'); - - CommandController.bindToCommand({ - element : this.$el.find('.x-refresh'), - command : { - name : 'refreshArtist', - seriesId : this.model.get('id') - } - }); - - this.delegateEvents(); - return this; - }, - - _editArtist : function() { - vent.trigger(vent.Commands.EditArtistCommand, { artist : this.model }); - }, - - _refreshArtist : function() { - CommandController.Execute('refreshArtist', { - name : 'refreshArtist', - artistId : this.model.id - }); - } -}); \ No newline at end of file diff --git a/src/UI/Cells/ArtistMonitoredCell.js b/src/UI/Cells/ArtistMonitoredCell.js deleted file mode 100644 index c45af0b85..000000000 --- a/src/UI/Cells/ArtistMonitoredCell.js +++ /dev/null @@ -1,39 +0,0 @@ -var ToggleCell = require('./ToggleCell'); -var Handlebars = require('handlebars'); - -module.exports = ToggleCell.extend({ - className : 'artist-monitored-cell', - - events : { - 'click i' : '_onClick' - }, - - render : function() { - - this.$el.empty(); - this.$el.html('<i /><a href=""><span class="artist-monitored-name"></span></a>'); - - var name = this.column.get('name'); - - if (this.model.get(name)) { - this.$('i').addClass(this.column.get('trueClass')); - } else { - this.$('i').addClass(this.column.get('falseClass')); - } - - var link = '/artist/' + this.model.get('nameSlug'); - var artistName = this.model.get('name'); - - this.$('a').attr('href', link ); - this.$('span').html(artistName); - - var tooltip = this.column.get('tooltip'); - - if (tooltip) { - this.$('i').attr('title', tooltip); - } - - this.delegateEvents(); - return this; - } -}); \ No newline at end of file diff --git a/src/UI/Cells/ArtistStatusCell.js b/src/UI/Cells/ArtistStatusCell.js deleted file mode 100644 index aa53482f1..000000000 --- a/src/UI/Cells/ArtistStatusCell.js +++ /dev/null @@ -1,32 +0,0 @@ -var NzbDroneCell = require('./NzbDroneCell'); - -module.exports = NzbDroneCell.extend({ - className : 'artist-status-cell', - - render : function() { - this.$el.empty(); - var monitored = this.model.get('monitored'); - var status = this.model.get('status'); - - if (status === 'ended') { - this.$el.html('<i class="icon-lidarr-artist-ended grid-icon" title="Ended"></i>'); - this._setStatusWeight(3); - } - - else if (!monitored) { - this.$el.html('<i class="icon-lidarr-artist-unmonitored grid-icon" title="Not Monitored"></i>'); - this._setStatusWeight(2); - } - - else { - this.$el.html('<i class="icon-lidarr-artist-continuing grid-icon" title="Continuing"></i>'); - this._setStatusWeight(1); - } - - return this; - }, - - _setStatusWeight : function(weight) { - this.model.set('statusWeight', weight, { silent : true }); - } -}); \ No newline at end of file diff --git a/src/UI/Cells/ArtistTitleCell.js b/src/UI/Cells/ArtistTitleCell.js deleted file mode 100644 index 5dcb62104..000000000 --- a/src/UI/Cells/ArtistTitleCell.js +++ /dev/null @@ -1,6 +0,0 @@ -var TemplatedCell = require('./TemplatedCell'); - -module.exports = TemplatedCell.extend({ - className : 'artist-title-cell', - template : 'Cells/ArtistTitleTemplate' -}); \ No newline at end of file diff --git a/src/UI/Cells/ArtistTitleTemplate.hbs b/src/UI/Cells/ArtistTitleTemplate.hbs deleted file mode 100644 index cb3146db8..000000000 --- a/src/UI/Cells/ArtistTitleTemplate.hbs +++ /dev/null @@ -1 +0,0 @@ -<a href="{{route}}">{{name}}</a> diff --git a/src/UI/Cells/DeleteEpisodeFileCell.js b/src/UI/Cells/DeleteEpisodeFileCell.js deleted file mode 100644 index 11aa9c4f6..000000000 --- a/src/UI/Cells/DeleteEpisodeFileCell.js +++ /dev/null @@ -1,27 +0,0 @@ -var vent = require('vent'); -var Backgrid = require('backgrid'); - -module.exports = Backgrid.Cell.extend({ - className : 'delete-episode-file-cell', - - events : { - 'click' : '_onClick' - }, - - render : function() { - this.$el.empty(); - this.$el.html('<i class="icon-lidarr-delete" title="Delete episode file from disk"></i>'); - - return this; - }, - - _onClick : function() { - var self = this; - - if (window.confirm('Are you sure you want to delete \'{0}\' from disk?'.format(this.model.get('path')))) { - this.model.destroy().done(function() { - vent.trigger(vent.Events.EpisodeFileDeleted, { episodeFile : self.model }); - }); - } - } -}); \ No newline at end of file diff --git a/src/UI/Cells/Edit/QualityCellEditor.js b/src/UI/Cells/Edit/QualityCellEditor.js deleted file mode 100644 index 00e469d83..000000000 --- a/src/UI/Cells/Edit/QualityCellEditor.js +++ /dev/null @@ -1,74 +0,0 @@ -var _ = require('underscore'); -var Backgrid = require('backgrid'); -var Marionette = require('marionette'); -var ProfileSchemaCollection = require('../../Settings/Profile/ProfileSchemaCollection'); - -module.exports = Backgrid.CellEditor.extend({ - className : 'quality-cell-editor', - template : 'Cells/Edit/QualityCellEditorTemplate', - tagName : 'select', - - events : { - 'change' : 'save', - 'blur' : 'close', - 'keydown' : 'close' - }, - - render : function() { - var self = this; - - var profileSchemaCollection = new ProfileSchemaCollection(); - var promise = profileSchemaCollection.fetch(); - - promise.done(function() { - var templateName = self.template; - self.schema = profileSchemaCollection.first(); - - var selected = _.find(self.schema.get('items'), function(model) { - return model.quality.id === self.model.get(self.column.get('name')).quality.id; - }); - - if (selected) { - selected.quality.selected = true; - } - - self.templateFunction = Marionette.TemplateCache.get(templateName); - var data = self.schema.toJSON(); - var html = self.templateFunction(data); - self.$el.html(html); - }); - - return this; - }, - - save : function(e) { - var model = this.model; - var column = this.column; - var selected = parseInt(this.$el.val(), 10); - - var profileItem = _.find(this.schema.get('items'), function(model) { - return model.quality.id === selected; - }); - - var newQuality = { - quality : profileItem.quality, - revision : { - version : 1, - real : 0 - } - }; - - model.set(column.get('name'), newQuality); - model.save(); - - model.trigger('backgrid:edited', model, column, new Backgrid.Command(e)); - }, - - close : function(e) { - var model = this.model; - var column = this.column; - var command = new Backgrid.Command(e); - - model.trigger('backgrid:edited', model, column, command); - } -}); \ No newline at end of file diff --git a/src/UI/Cells/Edit/QualityCellEditorTemplate.hbs b/src/UI/Cells/Edit/QualityCellEditorTemplate.hbs deleted file mode 100644 index b7039dd44..000000000 --- a/src/UI/Cells/Edit/QualityCellEditorTemplate.hbs +++ /dev/null @@ -1,9 +0,0 @@ -{{#eachReverse items}} - {{#with quality}} - {{#if selected}} - <option value="{{id}}" selected="selected">{{name}}</option> - {{else}} - <option value="{{id}}">{{name}}</option> - {{/if}} - {{/with}} -{{/eachReverse}} \ No newline at end of file diff --git a/src/UI/Cells/EpisodeActionsCell.js b/src/UI/Cells/EpisodeActionsCell.js deleted file mode 100644 index 969c7c9a8..000000000 --- a/src/UI/Cells/EpisodeActionsCell.js +++ /dev/null @@ -1,44 +0,0 @@ -var vent = require('vent'); -var NzbDroneCell = require('./NzbDroneCell'); -var CommandController = require('../Commands/CommandController'); - -module.exports = NzbDroneCell.extend({ - className : 'episode-actions-cell', - - events : { - 'click .x-automatic-search' : '_automaticSearch', - 'click .x-manual-search' : '_manualSearch' - }, - - render : function() { - this.$el.empty(); - - this.$el.html('<i class="icon-lidarr-search x-automatic-search" title="Automatic Search"></i>' + '<i class="icon-lidarr-search-manual x-manual-search" title="Manual Search"></i>'); - - CommandController.bindToCommand({ - element : this.$el.find('.x-automatic-search'), - command : { - name : 'episodeSearch', - episodeIds : [this.model.get('id')] - } - }); - - this.delegateEvents(); - return this; - }, - - _automaticSearch : function() { - CommandController.Execute('episodeSearch', { - name : 'episodeSearch', - episodeIds : [this.model.get('id')] - }); - }, - - _manualSearch : function() { - vent.trigger(vent.Commands.ShowEpisodeDetails, { - episode : this.cellValue, - hideSeriesLink : true, - openingTab : 'search' - }); - } -}); \ No newline at end of file diff --git a/src/UI/Cells/EpisodeFilePathCell.js b/src/UI/Cells/EpisodeFilePathCell.js deleted file mode 100644 index 5f3916ead..000000000 --- a/src/UI/Cells/EpisodeFilePathCell.js +++ /dev/null @@ -1,19 +0,0 @@ -var reqres = require('../reqres'); -var NzbDroneCell = require('./NzbDroneCell'); - -module.exports = NzbDroneCell.extend({ - className : 'episode-file-path-cell', - - render : function() { - this.$el.empty(); - - if (reqres.hasHandler(reqres.Requests.GetEpisodeFileById)) { - var episodeFile = reqres.request(reqres.Requests.GetEpisodeFileById, this.model.get('episodeFileId')); - - this.$el.html(episodeFile.get('relativePath')); - } - - this.delegateEvents(); - return this; - } -}); \ No newline at end of file diff --git a/src/UI/Cells/EpisodeMonitoredCell.js b/src/UI/Cells/EpisodeMonitoredCell.js deleted file mode 100644 index b615d2909..000000000 --- a/src/UI/Cells/EpisodeMonitoredCell.js +++ /dev/null @@ -1,57 +0,0 @@ -var _ = require('underscore'); -var ToggleCell = require('./ToggleCell'); -var SeriesCollection = require('../Series/SeriesCollection'); -var Messenger = require('../Shared/Messenger'); - -module.exports = ToggleCell.extend({ - className : 'toggle-cell episode-monitored', - - _originalOnClick : ToggleCell.prototype._onClick, - - _onClick : function(e) { - - var series = SeriesCollection.get(this.model.get('seriesId')); - - if (!series.get('monitored')) { - - Messenger.show({ - message : 'Unable to change monitored state when series is not monitored', - type : 'error' - }); - - return; - } - - if (e.shiftKey && this.model.episodeCollection.lastToggled) { - this._selectRange(); - - return; - } - - this._originalOnClick.apply(this, arguments); - this.model.episodeCollection.lastToggled = this.model; - }, - - _selectRange : function() { - var episodeCollection = this.model.episodeCollection; - var lastToggled = episodeCollection.lastToggled; - - var currentIndex = episodeCollection.indexOf(this.model); - var lastIndex = episodeCollection.indexOf(lastToggled); - - var low = Math.min(currentIndex, lastIndex); - var high = Math.max(currentIndex, lastIndex); - var range = _.range(low + 1, high); - - _.each(range, function(index) { - var model = episodeCollection.at(index); - - model.set('monitored', lastToggled.get('monitored')); - model.save(); - }); - - this.model.set('monitored', lastToggled.get('monitored')); - this.model.save(); - this.model.episodeCollection.lastToggled = undefined; - } -}); \ No newline at end of file diff --git a/src/UI/Cells/EpisodeNumberCell.js b/src/UI/Cells/EpisodeNumberCell.js deleted file mode 100644 index 6d4d804b2..000000000 --- a/src/UI/Cells/EpisodeNumberCell.js +++ /dev/null @@ -1,71 +0,0 @@ -var NzbDroneCell = require('./NzbDroneCell'); -var FormatHelpers = require('../Shared/FormatHelpers'); -var _ = require('underscore'); - -module.exports = NzbDroneCell.extend({ - className : 'episode-number-cell', - - render : function() { - - this.$el.empty(); - - var airDateField = this.column.get('airDateUtc') || 'airDateUtc'; - var seasonField = this.column.get('seasonNumber') || 'seasonNumber'; - var episodeField = this.column.get('episodes') || 'episodeNumber'; - var absoluteEpisodeField = 'absoluteEpisodeNumber'; - - if (this.model) { - var result = 'Unknown'; - - var airDate = this.model.get(airDateField); - var seasonNumber = this.model.get(seasonField); - var episodes = this.model.get(episodeField); - var absoluteEpisodeNumber = this.model.get(absoluteEpisodeField); - - if (this.cellValue) { - if (!seasonNumber) { - seasonNumber = this.cellValue.get(seasonField); - } - - if (!episodes) { - episodes = this.cellValue.get(episodeField); - } - - if (absoluteEpisodeNumber === undefined) { - absoluteEpisodeNumber = this.cellValue.get(absoluteEpisodeField); - } - - if (!airDate) { - this.model.get(airDateField); - } - } - - if (episodes) { - - var paddedEpisodes; - var paddedAbsoluteEpisode; - - if (episodes.constructor === Array) { - paddedEpisodes = _.map(episodes, function(episodeNumber) { - return FormatHelpers.pad(episodeNumber, 2); - }).join(); - } else { - paddedEpisodes = FormatHelpers.pad(episodes, 2); - paddedAbsoluteEpisode = FormatHelpers.pad(absoluteEpisodeNumber, 2); - } - - result = '{0}x{1}'.format(seasonNumber, paddedEpisodes); - - if (absoluteEpisodeNumber !== undefined && paddedAbsoluteEpisode) { - result += ' ({0})'.format(paddedAbsoluteEpisode); - } - } else if (airDate) { - result = new Date(airDate).toLocaleDateString(); - } - - this.$el.html(result); - } - this.delegateEvents(); - return this; - } -}); \ No newline at end of file diff --git a/src/UI/Cells/EpisodeStatusCell.js b/src/UI/Cells/EpisodeStatusCell.js deleted file mode 100644 index 50de17206..000000000 --- a/src/UI/Cells/EpisodeStatusCell.js +++ /dev/null @@ -1,127 +0,0 @@ -var reqres = require('../reqres'); -var Backbone = require('backbone'); -var NzbDroneCell = require('./NzbDroneCell'); -var QueueCollection = require('../Activity/Queue/QueueCollection'); -var moment = require('moment'); -var FormatHelpers = require('../Shared/FormatHelpers'); - -module.exports = NzbDroneCell.extend({ - className : 'episode-status-cell', - - render : function() { - this.listenTo(QueueCollection, 'sync', this._renderCell); - - this._renderCell(); - - return this; - }, - - _renderCell : function() { - - if (this.episodeFile) { - this.stopListening(this.episodeFile, 'change', this._refresh); - } - - this.$el.empty(); - - if (this.model) { - - var icon; - var tooltip; - - var hasAired = moment(this.model.get('airDateUtc')).isBefore(moment()); - this.episodeFile = this._getFile(); - - if (this.episodeFile) { - this.listenTo(this.episodeFile, 'change', this._refresh); - - var quality = this.episodeFile.get('quality'); - var revision = quality.revision; - var size = FormatHelpers.bytes(this.episodeFile.get('size')); - var title = 'Episode downloaded'; - - if (revision.real && revision.real > 0) { - title += '[REAL]'; - } - - if (revision.version && revision.version > 1) { - title += ' [PROPER]'; - } - - if (size !== '') { - title += ' - {0}'.format(size); - } - - if (this.episodeFile.get('qualityCutoffNotMet')) { - this.$el.html('<span class="badge badge-inverse" title="{0}">{1}</span>'.format(title, quality.quality.name)); - } else { - this.$el.html('<span class="badge" title="{0}">{1}</span>'.format(title, quality.quality.name)); - } - - return; - } - - else { - var model = this.model; - var downloading = QueueCollection.findEpisode(model.get('id')); - - if (downloading) { - var progress = 100 - (downloading.get('sizeleft') / downloading.get('size') * 100); - - if (progress === 0) { - icon = 'icon-lidarr-downloading'; - tooltip = 'Episode is downloading'; - } - - else { - this.$el.html('<div class="progress" title="Episode is downloading - {0}% {1}">'.format(progress.toFixed(1), downloading.get('title')) + - '<div class="progress-bar progress-bar-purple" style="width: {0}%;"></div></div>'.format(progress)); - return; - } - } - - else if (this.model.get('grabbed')) { - icon = 'icon-lidarr-downloading'; - tooltip = 'Episode is downloading'; - } - - else if (!this.model.get('airDateUtc')) { - icon = 'icon-lidarr-tba'; - tooltip = 'TBA'; - } - - else if (hasAired) { - icon = 'icon-lidarr-missing'; - tooltip = 'Episode missing from disk'; - } else { - icon = 'icon-lidarr-not-aired'; - tooltip = 'Episode has not aired'; - } - } - - this.$el.html('<i class="{0}" title="{1}"/>'.format(icon, tooltip)); - } - }, - - _getFile : function() { - var hasFile = this.model.get('hasFile'); - - if (hasFile) { - var episodeFile; - - if (reqres.hasHandler(reqres.Requests.GetEpisodeFileById)) { - episodeFile = reqres.request(reqres.Requests.GetEpisodeFileById, this.model.get('episodeFileId')); - } - - else if (this.model.has('episodeFile')) { - episodeFile = new Backbone.Model(this.model.get('episodeFile')); - } - - if (episodeFile) { - return episodeFile; - } - } - - return undefined; - } -}); \ No newline at end of file diff --git a/src/UI/Cells/EpisodeTitleCell.js b/src/UI/Cells/EpisodeTitleCell.js deleted file mode 100644 index 7dce10ede..000000000 --- a/src/UI/Cells/EpisodeTitleCell.js +++ /dev/null @@ -1,29 +0,0 @@ -var vent = require('vent'); -var NzbDroneCell = require('./NzbDroneCell'); - -module.exports = NzbDroneCell.extend({ - className : 'episode-title-cell', - - events : { - 'click' : '_showDetails' - }, - - render : function() { - var title = this.cellValue.get('title'); - - if (!title || title === '') { - title = 'TBA'; - } - - this.$el.html(title); - return this; - }, - - _showDetails : function() { - var hideSeriesLink = this.column.get('hideSeriesLink'); - vent.trigger(vent.Commands.ShowEpisodeDetails, { - episode : this.cellValue, - hideSeriesLink : hideSeriesLink - }); - } -}); \ No newline at end of file diff --git a/src/UI/Cells/EventTypeCell.js b/src/UI/Cells/EventTypeCell.js deleted file mode 100644 index d9c643795..000000000 --- a/src/UI/Cells/EventTypeCell.js +++ /dev/null @@ -1,44 +0,0 @@ -var NzbDroneCell = require('./NzbDroneCell'); - -module.exports = NzbDroneCell.extend({ - className : 'history-event-type-cell', - - render : function() { - this.$el.empty(); - - if (this.cellValue) { - var icon; - var toolTip; - - switch (this.cellValue.get('eventType')) { - case 'grabbed': - icon = 'icon-lidarr-downloading'; - toolTip = 'Episode grabbed from {0} and sent to download client'.format(this.cellValue.get('data').indexer); - break; - case 'seriesFolderImported': - icon = 'icon-lidarr-hdd'; - toolTip = 'Existing episode file added to library'; - break; - case 'downloadFolderImported': - icon = 'icon-lidarr-imported'; - toolTip = 'Episode downloaded successfully and picked up from download client'; - break; - case 'downloadFailed': - icon = 'icon-lidarr-download-failed'; - toolTip = 'Episode download failed'; - break; - case 'episodeFileDeleted': - icon = 'icon-lidarr-deleted'; - toolTip = 'Episode file deleted'; - break; - default: - icon = 'icon-lidarr-unknown'; - toolTip = 'unknown event'; - } - - this.$el.html('<i class="{0}" title="{1}" data-placement="right"/>'.format(icon, toolTip)); - } - - return this; - } -}); \ No newline at end of file diff --git a/src/UI/Cells/FileSizeCell.js b/src/UI/Cells/FileSizeCell.js deleted file mode 100644 index 586d5f35c..000000000 --- a/src/UI/Cells/FileSizeCell.js +++ /dev/null @@ -1,13 +0,0 @@ -var Backgrid = require('backgrid'); -var FormatHelpers = require('../Shared/FormatHelpers'); - -module.exports = Backgrid.Cell.extend({ - className : 'file-size-cell', - - render : function() { - var size = this.model.get(this.column.get('name')); - this.$el.html(FormatHelpers.bytes(size)); - this.delegateEvents(); - return this; - } -}); \ No newline at end of file diff --git a/src/UI/Cells/IndexerCell.js b/src/UI/Cells/IndexerCell.js deleted file mode 100644 index bbd2e90df..000000000 --- a/src/UI/Cells/IndexerCell.js +++ /dev/null @@ -1,11 +0,0 @@ -var Backgrid = require('backgrid'); - -module.exports = Backgrid.Cell.extend({ - className : 'indexer-cell', - - render : function() { - var indexer = this.model.get(this.column.get('name')); - this.$el.html(indexer); - return this; - } -}); \ No newline at end of file diff --git a/src/UI/Cells/NzbDroneCell.js b/src/UI/Cells/NzbDroneCell.js deleted file mode 100644 index 7bd6125f3..000000000 --- a/src/UI/Cells/NzbDroneCell.js +++ /dev/null @@ -1,61 +0,0 @@ -var Backgrid = require('backgrid'); -var Backbone = require('backbone'); - -module.exports = Backgrid.Cell.extend({ - - _originalInit : Backgrid.Cell.prototype.initialize, - - initialize : function() { - this._originalInit.apply(this, arguments); - this.cellValue = this._getValue(); - - this.listenTo(this.model, 'change', this._refresh); - - if (this._onEdit) { - this.listenTo(this.model, 'backgrid:edit', function(model, column, cell, editor) { - if (column.get('name') === this.column.get('name')) { - this._onEdit(model, column, cell, editor); - } - }); - } - }, - - _refresh : function() { - this.cellValue = this._getValue(); - this.render(); - }, - - _getValue : function() { - - var cellValue = this.column.get('cellValue'); - - if (cellValue) { - if (cellValue === 'this') { - return this.model; - } - - else { - return this.model.get(cellValue); - } - } - - var name = this.column.get('name'); - - if (name === 'this') { - return this.model; - } - - var value = this.model.get(name); - - if (!value) { - return undefined; - } - - //if not a model - if (!value.get && typeof value === 'object') { - value = new Backbone.Model(value); - } - - return value; - } -}); \ No newline at end of file diff --git a/src/UI/Cells/ProfileCell.js b/src/UI/Cells/ProfileCell.js deleted file mode 100644 index d87ee1af9..000000000 --- a/src/UI/Cells/ProfileCell.js +++ /dev/null @@ -1,29 +0,0 @@ -var Backgrid = require('backgrid'); -var ProfileCollection = require('../Profile/ProfileCollection'); -var _ = require('underscore'); - -module.exports = Backgrid.Cell.extend({ - className : 'profile-cell', - - _originalInit : Backgrid.Cell.prototype.initialize, - - initialize : function () { - this._originalInit.apply(this, arguments); - - this.listenTo(ProfileCollection, 'sync', this.render); - }, - - render : function() { - - this.$el.empty(); - var profileId = this.model.get(this.column.get('name')); - - var profile = _.findWhere(ProfileCollection.models, { id : profileId }); - - if (profile) { - this.$el.html(profile.get('name')); - } - - return this; - } -}); \ No newline at end of file diff --git a/src/UI/Cells/QualityCell.js b/src/UI/Cells/QualityCell.js deleted file mode 100644 index 962bd2ab4..000000000 --- a/src/UI/Cells/QualityCell.js +++ /dev/null @@ -1,8 +0,0 @@ -var TemplatedCell = require('./TemplatedCell'); -var QualityCellEditor = require('./Edit/QualityCellEditor'); - -module.exports = TemplatedCell.extend({ - className : 'quality-cell', - template : 'Cells/QualityCellTemplate', - editor : QualityCellEditor -}); \ No newline at end of file diff --git a/src/UI/Cells/QualityCellTemplate.hbs b/src/UI/Cells/QualityCellTemplate.hbs deleted file mode 100644 index 6625ade9b..000000000 --- a/src/UI/Cells/QualityCellTemplate.hbs +++ /dev/null @@ -1,5 +0,0 @@ -{{#if_gt proper compare="1"}} - <span class="badge badge-info" title="PROPER">{{quality.name}}</span> -{{else}} - <span class="badge">{{quality.name}}</span> -{{/if_gt}} \ No newline at end of file diff --git a/src/UI/Cells/RelativeDateCell.js b/src/UI/Cells/RelativeDateCell.js deleted file mode 100644 index eb69fc855..000000000 --- a/src/UI/Cells/RelativeDateCell.js +++ /dev/null @@ -1,34 +0,0 @@ -var NzbDroneCell = require('./NzbDroneCell'); -var moment = require('moment'); -var FormatHelpers = require('../Shared/FormatHelpers'); -var UiSettings = require('../Shared/UiSettingsModel'); - -module.exports = NzbDroneCell.extend({ - className : 'relative-date-cell', - - render : function() { - - var dateStr = this.model.get(this.column.get('name')); - - if (dateStr) { - var date = moment(dateStr); - var diff = date.diff(moment().zone(date.zone()).startOf('day'), 'days', true); - var result = '<span title="{0}">{1}</span>'; - var tooltip = date.format(UiSettings.longDateTime()); - var text; - - if (diff > 0 && diff < 1) { - text = date.format(UiSettings.time(true, false)); - } else { - if (UiSettings.get('showRelativeDates')) { - text = FormatHelpers.relativeDate(dateStr); - } else { - text = date.format(UiSettings.get('shortDateFormat')); - } - } - - this.$el.html(result.format(tooltip, text)); - } - return this; - } -}); \ No newline at end of file diff --git a/src/UI/Cells/RelativeTimeCell.js b/src/UI/Cells/RelativeTimeCell.js deleted file mode 100644 index b0d552bfd..000000000 --- a/src/UI/Cells/RelativeTimeCell.js +++ /dev/null @@ -1,30 +0,0 @@ -var NzbDroneCell = require('./NzbDroneCell'); -var moment = require('moment'); -var FormatHelpers = require('../Shared/FormatHelpers'); -var UiSettings = require('../Shared/UiSettingsModel'); - -module.exports = NzbDroneCell.extend({ - className : 'relative-time-cell', - - render : function() { - - var dateStr = this.model.get(this.column.get('name')); - - if (dateStr) { - var date = moment(dateStr); - var result = '<span title="{0}">{1}</span>'; - var tooltip = date.format(UiSettings.longDateTime()); - var text; - - if (UiSettings.get('showRelativeDates')) { - text = date.fromNow(); - } else { - text = date.format(UiSettings.shortDateTime()); - } - - this.$el.html(result.format(tooltip, text)); - } - - return this; - } -}); \ No newline at end of file diff --git a/src/UI/Cells/ReleaseTitleCell.js b/src/UI/Cells/ReleaseTitleCell.js deleted file mode 100644 index 7d3551e41..000000000 --- a/src/UI/Cells/ReleaseTitleCell.js +++ /dev/null @@ -1,20 +0,0 @@ -var NzbDroneCell = require('./NzbDroneCell'); - -module.exports = NzbDroneCell.extend({ - className : 'release-title-cell', - - render : function() { - this.$el.empty(); - - var title = this.model.get('title'); - var infoUrl = this.model.get('infoUrl'); - - if (infoUrl) { - this.$el.html('<a href="{0}">{1}</a>'.format(infoUrl, title)); - } else { - this.$el.html(title); - } - - return this; - } -}); \ No newline at end of file diff --git a/src/UI/Cells/SeasonFolderCell.js b/src/UI/Cells/SeasonFolderCell.js deleted file mode 100644 index 7a9385b84..000000000 --- a/src/UI/Cells/SeasonFolderCell.js +++ /dev/null @@ -1,14 +0,0 @@ -var Backgrid = require('backgrid'); - -module.exports = Backgrid.Cell.extend({ - className : 'season-folder-cell', - - render : function() { - this.$el.empty(); - - var seasonFolder = this.model.get(this.column.get('name')); - this.$el.html(seasonFolder.toString()); - - return this; - } -}); \ No newline at end of file diff --git a/src/UI/Cells/SelectAllCell.js b/src/UI/Cells/SelectAllCell.js deleted file mode 100644 index e89289f40..000000000 --- a/src/UI/Cells/SelectAllCell.js +++ /dev/null @@ -1,45 +0,0 @@ -var $ = require('jquery'); -var _ = require('underscore'); -var BackgridSelectAll = require('backgrid.selectall'); - -module.exports = BackgridSelectAll.extend({ - enterEditMode : function(e) { - var collection = this.column.get('sortedCollection') || this.model.collection; - - if (e.shiftKey && collection.lastToggled) { - this._selectRange(collection); - } - - var checked = $(e.target).prop('checked'); - - collection.lastToggled = this.model; - collection.checked = checked; - }, - - onChange : function(e) { - var checked = $(e.target).prop('checked'); - this.$el.parent().toggleClass('selected', checked); - this.model.trigger('backgrid:selected', this.model, checked); - }, - - _selectRange : function(collection) { - var lastToggled = collection.lastToggled; - var checked = collection.checked; - - var currentIndex = collection.indexOf(this.model); - var lastIndex = collection.indexOf(lastToggled); - - var low = Math.min(currentIndex, lastIndex); - var high = Math.max(currentIndex, lastIndex); - var range = _.range(low + 1, high); - - _.each(range, function(index) { - var model = collection.at(index); - - model.trigger('backgrid:select', model, checked); - }); - - collection.lastToggled = undefined; - collection.checked = undefined; - } -}); \ No newline at end of file diff --git a/src/UI/Cells/SeriesActionsCell.js b/src/UI/Cells/SeriesActionsCell.js deleted file mode 100644 index 71f7b22a8..000000000 --- a/src/UI/Cells/SeriesActionsCell.js +++ /dev/null @@ -1,45 +0,0 @@ -var vent = require('vent'); -var NzbDroneCell = require('./NzbDroneCell'); -var CommandController = require('../Commands/CommandController'); - -module.exports = NzbDroneCell.extend({ - className : 'series-actions-cell', - - ui : { - refresh : '.x-refresh' - }, - - events : { - 'click .x-edit' : '_editSeries', - 'click .x-refresh' : '_refreshSeries' - }, - - render : function() { - this.$el.empty(); - - this.$el.html('<i class="icon-lidarr-refresh x-refresh hidden-xs" title="" data-original-title="Update series info and scan disk"></i> ' + - '<i class="icon-lidarr-edit x-edit" title="" data-original-title="Edit Series"></i>'); - - CommandController.bindToCommand({ - element : this.$el.find('.x-refresh'), - command : { - name : 'refreshSeries', - seriesId : this.model.get('id') - } - }); - - this.delegateEvents(); - return this; - }, - - _editSeries : function() { - vent.trigger(vent.Commands.EditSeriesCommand, { series : this.model }); - }, - - _refreshSeries : function() { - CommandController.Execute('refreshSeries', { - name : 'refreshSeries', - seriesId : this.model.id - }); - } -}); \ No newline at end of file diff --git a/src/UI/Cells/SeriesStatusCell.js b/src/UI/Cells/SeriesStatusCell.js deleted file mode 100644 index 7e99d8587..000000000 --- a/src/UI/Cells/SeriesStatusCell.js +++ /dev/null @@ -1,32 +0,0 @@ -var NzbDroneCell = require('./NzbDroneCell'); - -module.exports = NzbDroneCell.extend({ - className : 'series-status-cell', - - render : function() { - this.$el.empty(); - var monitored = this.model.get('monitored'); - var status = this.model.get('status'); - - if (status === 'ended') { - this.$el.html('<i class="icon-lidarr-series-ended grid-icon" title="Ended"></i>'); - this._setStatusWeight(3); - } - - else if (!monitored) { - this.$el.html('<i class="icon-lidarr-series-unmonitored grid-icon" title="Not Monitored"></i>'); - this._setStatusWeight(2); - } - - else { - this.$el.html('<i class="icon-lidarr-series-continuing grid-icon" title="Continuing"></i>'); - this._setStatusWeight(1); - } - - return this; - }, - - _setStatusWeight : function(weight) { - this.model.set('statusWeight', weight, { silent : true }); - } -}); \ No newline at end of file diff --git a/src/UI/Cells/SeriesTitleCell.js b/src/UI/Cells/SeriesTitleCell.js deleted file mode 100644 index a516b4e09..000000000 --- a/src/UI/Cells/SeriesTitleCell.js +++ /dev/null @@ -1,6 +0,0 @@ -var TemplatedCell = require('./TemplatedCell'); - -module.exports = TemplatedCell.extend({ - className : 'series-title-cell', - template : 'Cells/SeriesTitleTemplate' -}); \ No newline at end of file diff --git a/src/UI/Cells/SeriesTitleTemplate.hbs b/src/UI/Cells/SeriesTitleTemplate.hbs deleted file mode 100644 index 99205b00a..000000000 --- a/src/UI/Cells/SeriesTitleTemplate.hbs +++ /dev/null @@ -1 +0,0 @@ -<a href="{{route}}">{{title}}</a> diff --git a/src/UI/Cells/TemplatedCell.js b/src/UI/Cells/TemplatedCell.js deleted file mode 100644 index 1299d4e36..000000000 --- a/src/UI/Cells/TemplatedCell.js +++ /dev/null @@ -1,21 +0,0 @@ -var Marionette = require('marionette'); -var NzbDroneCell = require('./NzbDroneCell'); - -module.exports = NzbDroneCell.extend({ - render : function() { - - var templateName = this.column.get('template') || this.template; - - this.templateFunction = Marionette.TemplateCache.get(templateName); - this.$el.empty(); - - if (this.cellValue) { - var data = this.cellValue.toJSON(); - var html = this.templateFunction(data); - this.$el.html(html); - } - - this.delegateEvents(); - return this; - } -}); \ No newline at end of file diff --git a/src/UI/Cells/ToggleCell.js b/src/UI/Cells/ToggleCell.js deleted file mode 100644 index 0de0762fd..000000000 --- a/src/UI/Cells/ToggleCell.js +++ /dev/null @@ -1,48 +0,0 @@ -var Backgrid = require('backgrid'); - -module.exports = Backgrid.Cell.extend({ - className : 'toggle-cell', - - events : { - 'click' : '_onClick' - }, - - _onClick : function() { - - var self = this; - - this.$el.tooltip('hide'); - - var name = this.column.get('name'); - this.model.set(name, !this.model.get(name)); - - var promise = this.model.save(); - - this.$('i').spinForPromise(promise); - - promise.always(function() { - self.render(); - }); - }, - - render : function() { - this.$el.empty(); - this.$el.html('<i />'); - - var name = this.column.get('name'); - - if (this.model.get(name)) { - this.$('i').addClass(this.column.get('trueClass')); - } else { - this.$('i').addClass(this.column.get('falseClass')); - } - - var tooltip = this.column.get('tooltip'); - - if (tooltip) { - this.$('i').attr('title', tooltip); - } - - return this; - } -}); \ No newline at end of file diff --git a/src/UI/Cells/TrackActionsCell.js b/src/UI/Cells/TrackActionsCell.js deleted file mode 100644 index 1bf8baaf4..000000000 --- a/src/UI/Cells/TrackActionsCell.js +++ /dev/null @@ -1,44 +0,0 @@ -var vent = require('vent'); -var NzbDroneCell = require('./NzbDroneCell'); -var CommandController = require('../Commands/CommandController'); - -module.exports = NzbDroneCell.extend({ - className : 'track-actions-cell', - - events : { - 'click .x-automatic-search' : '_automaticSearch', - 'click .x-manual-search' : '_manualSearch' - }, - - render : function() { - this.$el.empty(); - - this.$el.html('<i class="icon-lidarr-search x-automatic-search" title="Automatic Search"></i>' + '<i class="icon-lidarr-search-manual x-manual-search" title="Manual Search"></i>'); - - CommandController.bindToCommand({ - element : this.$el.find('.x-automatic-search'), - command : { - name : 'trackSearch', - trackIds : [this.model.get('id')] - } - }); - - this.delegateEvents(); - return this; - }, - - _automaticSearch : function() { - CommandController.Execute('trackSearch', { - name : 'trackSearch', - trackIds : [this.model.get('id')] - }); - }, - - _manualSearch : function() { - vent.trigger(vent.Commands.ShowTrackDetails, { - track : this.cellValue, - hideSeriesLink : true, - openingTab : 'search' - }); - } -}); \ No newline at end of file diff --git a/src/UI/Cells/TrackDurationCell.js b/src/UI/Cells/TrackDurationCell.js deleted file mode 100644 index d3a9d42ec..000000000 --- a/src/UI/Cells/TrackDurationCell.js +++ /dev/null @@ -1,13 +0,0 @@ -var Backgrid = require('backgrid'); -var FormatHelpers = require('../Shared/FormatHelpers'); - -module.exports = Backgrid.Cell.extend({ - className : 'track-duration-cell', - - render : function() { - var duration = this.model.get(this.column.get('name')); - this.$el.html(FormatHelpers.timeMinSec(duration,'ms')); - this.delegateEvents(); - return this; - } -}); \ No newline at end of file diff --git a/src/UI/Cells/TrackExplicitCell.js b/src/UI/Cells/TrackExplicitCell.js deleted file mode 100644 index 19eb89ea5..000000000 --- a/src/UI/Cells/TrackExplicitCell.js +++ /dev/null @@ -1,20 +0,0 @@ -var vent = require('vent'); -var NzbDroneCell = require('./NzbDroneCell'); - -module.exports = NzbDroneCell.extend({ - className : 'track-explicit-cell', - template : 'Cells/TrackExplicitCellTemplate', - - render : function() { - var explicit = this.cellValue.get('explicit'); - var print = ''; - - if (explicit === true) { - print = 'Explicit'; - } - - this.$el.html(print); - return this; - } - -}); \ No newline at end of file diff --git a/src/UI/Cells/TrackMonitoredCell.js b/src/UI/Cells/TrackMonitoredCell.js deleted file mode 100644 index aa3d987ae..000000000 --- a/src/UI/Cells/TrackMonitoredCell.js +++ /dev/null @@ -1,57 +0,0 @@ -var _ = require('underscore'); -var ToggleCell = require('./ToggleCell'); -var ArtistCollection = require('../Artist/ArtistCollection'); -var Messenger = require('../Shared/Messenger'); - -module.exports = ToggleCell.extend({ - className : 'toggle-cell track-monitored', - - _originalOnClick : ToggleCell.prototype._onClick, - - _onClick : function(e) { - - var artist = ArtistCollection.get(this.model.get('artistId')); - - if (!artist.get('monitored')) { - - Messenger.show({ - message : 'Unable to change monitored state when artist is not monitored', - type : 'error' - }); - - return; - } - - if (e.shiftKey && this.model.trackCollection.lastToggled) { - this._selectRange(); - - return; - } - - this._originalOnClick.apply(this, arguments); - this.model.trackCollection.lastToggled = this.model; - }, - - _selectRange : function() { - var trackCollection = this.model.trackCollection; - var lastToggled = trackCollection.lastToggled; - - var currentIndex = trackCollection.indexOf(this.model); - var lastIndex = trackCollection.indexOf(lastToggled); - - var low = Math.min(currentIndex, lastIndex); - var high = Math.max(currentIndex, lastIndex); - var range = _.range(low + 1, high); - - _.each(range, function(index) { - var model = trackCollection.at(index); - - model.set('monitored', lastToggled.get('monitored')); - model.save(); - }); - - this.model.set('monitored', lastToggled.get('monitored')); - this.model.save(); - this.model.trackCollection.lastToggled = undefined; - } -}); \ No newline at end of file diff --git a/src/UI/Cells/TrackProgressCell.js b/src/UI/Cells/TrackProgressCell.js deleted file mode 100644 index 1b9f93599..000000000 --- a/src/UI/Cells/TrackProgressCell.js +++ /dev/null @@ -1,28 +0,0 @@ -var Marionette = require('marionette'); -var NzbDroneCell = require('./NzbDroneCell'); - -module.exports = NzbDroneCell.extend({ - className : 'track-progress-cell', - template : 'Cells/TrackProgressCellTemplate', - - render : function() { - - var trackCount = this.model.get('trackCount'); - var trackFileCount = this.model.get('trackFileCount'); - - var percent = 100; - - if (trackCount > 0) { - percent = trackFileCount / trackCount * 100; - } - - this.model.set('percentOfTracks', percent); - - this.templateFunction = Marionette.TemplateCache.get(this.template); - var data = this.model.toJSON(); - var html = this.templateFunction(data); - this.$el.html(html); - - return this; - } -}); \ No newline at end of file diff --git a/src/UI/Cells/TrackProgressCellTemplate.hbs b/src/UI/Cells/TrackProgressCellTemplate.hbs deleted file mode 100644 index b4899728f..000000000 --- a/src/UI/Cells/TrackProgressCellTemplate.hbs +++ /dev/null @@ -1 +0,0 @@ -{{> TrackProgressPartial }} \ No newline at end of file diff --git a/src/UI/Cells/TrackStatusCell.js b/src/UI/Cells/TrackStatusCell.js deleted file mode 100644 index c7e9e6362..000000000 --- a/src/UI/Cells/TrackStatusCell.js +++ /dev/null @@ -1,127 +0,0 @@ -var reqres = require('../reqres'); -var Backbone = require('backbone'); -var NzbDroneCell = require('./NzbDroneCell'); -var QueueCollection = require('../Activity/Queue/QueueCollection'); -var moment = require('moment'); -var FormatHelpers = require('../Shared/FormatHelpers'); - -module.exports = NzbDroneCell.extend({ - className : 'track-status-cell', - - render : function() { - this.listenTo(QueueCollection, 'sync', this._renderCell); - - this._renderCell(); - - return this; - }, - - _renderCell : function() { - - if (this.trackFile) { - this.stopListening(this.trackFile, 'change', this._refresh); - } - - this.$el.empty(); - - if (this.model) { - - var icon; - var tooltip; - - var hasAired = moment(this.model.get('airDateUtc')).isBefore(moment()); - this.trackFile = this._getFile(); - - if (this.trackFile) { - this.listenTo(this.trackFile, 'change', this._refresh); - - var quality = this.trackFile.get('quality'); - var revision = quality.revision; - var size = FormatHelpers.bytes(this.trackFile.get('size')); - var title = 'Track downloaded'; - - if (revision.real && revision.real > 0) { - title += '[REAL]'; - } - - if (revision.version && revision.version > 1) { - title += ' [PROPER]'; - } - - if (size !== '') { - title += ' - {0}'.format(size); - } - - if (this.trackFile.get('qualityCutoffNotMet')) { - this.$el.html('<span class="badge badge-inverse" title="{0}">{1}</span>'.format(title, quality.quality.name)); - } else { - this.$el.html('<span class="badge" title="{0}">{1}</span>'.format(title, quality.quality.name)); - } - - return; - } - - else { - var model = this.model; - var downloading = false; //TODO Fix this by adding to QueueCollection - - if (downloading) { - var progress = 100 - (downloading.get('sizeleft') / downloading.get('size') * 100); - - if (progress === 0) { - icon = 'icon-lidarr-downloading'; - tooltip = 'Track is downloading'; - } - - else { - this.$el.html('<div class="progress" title="Track is downloading - {0}% {1}">'.format(progress.toFixed(1), downloading.get('title')) + - '<div class="progress-bar progress-bar-purple" style="width: {0}%;"></div></div>'.format(progress)); - return; - } - } - - else if (this.model.get('grabbed')) { - icon = 'icon-lidarr-downloading'; - tooltip = 'Track is downloading'; - } - - else if (!this.model.get('airDateUtc')) { - icon = 'icon-lidarr-tba'; - tooltip = 'TBA'; - } - - else if (hasAired) { - icon = 'icon-lidarr-missing'; - tooltip = 'Track missing from disk'; - } else { - icon = 'icon-lidarr-not-aired'; - tooltip = 'Track has not aired'; - } - } - - this.$el.html('<i class="{0}" title="{1}"/>'.format(icon, tooltip)); - } - }, - - _getFile : function() { - var hasFile = this.model.get('hasFile'); - - if (hasFile) { - var trackFile; - - if (reqres.hasHandler(reqres.Requests.GetTrackFileById)) { - trackFile = reqres.request(reqres.Requests.GetTrackFileById, this.model.get('trackFileId')); - } - - else if (this.model.has('trackFile')) { - trackFile = new Backbone.Model(this.model.get('trackFile')); - } - - if (trackFile) { - return trackFile; - } - } - - return undefined; - } -}); \ No newline at end of file diff --git a/src/UI/Cells/TrackTitleCell.js b/src/UI/Cells/TrackTitleCell.js deleted file mode 100644 index ab2777e52..000000000 --- a/src/UI/Cells/TrackTitleCell.js +++ /dev/null @@ -1,29 +0,0 @@ -var vent = require('vent'); -var NzbDroneCell = require('./NzbDroneCell'); - -module.exports = NzbDroneCell.extend({ - className : 'track-title-cell', - - events : { - //'click' : '_showDetails' - }, - - render : function() { - var title = this.cellValue.get('title'); - - if (!title || title === '') { - title = 'TBA'; - } - - this.$el.html(title); - return this; - }, - - _showDetails : function() { - var hideArtistLink = this.column.get('hideArtistLink'); - //vent.trigger(vent.Commands.ShowTrackDetails, { //TODO Impelement Track search and screen as well as album? - // track : this.cellValue, - // hideArtistLink : hideArtistLink - //}); - } -}); \ No newline at end of file diff --git a/src/UI/Cells/cells.less b/src/UI/Cells/cells.less deleted file mode 100644 index f873017c9..000000000 --- a/src/UI/Cells/cells.less +++ /dev/null @@ -1,286 +0,0 @@ -@import "../Content/Bootstrap/mixins"; -@import "../Content/Bootstrap/variables"; -@import "../Content/Bootstrap/buttons"; -@import "../Shared/Styles/clickable"; -@import "../Content/mixins"; -@import "../Content/variables"; - -.artist-title-cell { - .text-overflow(); - - max-width: 450px; - - @media @sm { - max-width: 250px - } -} - -.artist-monitored-cell { - .text-overflow(); - - .artist-monitored-name { - padding-left: 10px; - } -} - -.track-title-cell { - .text-overflow(); - - color: #428bca; - text-decoration: none; - - &:focus, &:hover { - color: #2a6496; - text-decoration: underline; - cursor: pointer; - } - - @media @lg { - max-width: 350px; - } - - @media @md { - max-width: 250px; - } - - @media @sm { - max-width: 200px; - } -} - -.album-title-cell { - .text-overflow(); - - color: #428bca; - text-decoration: none; - - &:focus, &:hover { - color: #2a6496; - text-decoration: underline; - cursor: pointer; - } - - @media @lg { - max-width: 350px; - } - - @media @md { - max-width: 250px; - } - - @media @sm { - max-width: 200px; - } -} - -.air-date-cell { - width : 120px; - cursor: default; - .text-overflow(); -} - -.relative-date-cell, .relative-time-cell { - .text-overflow(); - cursor : default; -} - -.relative-date-cell { - width : 150px; -} - -.history-event-type-cell { - width : 10px; -} - -.download-report-cell { - .clickable(); - - width : 32px; - - i { - .clickable(); - } -} - -.toggle-cell{ - .clickable(); - .not-selectable; -} - -.approval-status-cell { - - .popover { - max-width : 400px; - - ul { - margin-left: -25px; - } - } - - i { - color : @brand-danger; - } -} - -td.track-status-cell, td.quality-cell, td.history-quality-cell, td.progress-cell { - text-align: center; - width: 80px; - - .badge { - font-size: 10px; - } - - .progress { - height : 10px; - margin-top : 5px; - margin-bottom : 0px; - } -} - -.history-details-cell { - .clickable(); - width: 10px; - - i { - .clickable(); - } -} - -.release-title-cell { - max-width: 400px; - word-wrap: break-word; -} - -.track-actions-cell { - width: 55px; - - i { - .clickable(); - margin-left : 8px; - - &:first-of-type { - margin-left : 0px; - } - } -} - -.track-history-details-cell { - width : 18px; -} - -.track-detail-modal { - .track-actions-cell { - width : 18px; - } -} - -.artist-actions-cell { - width : 56px; - min-width : 56px; -} - -.timeleft-cell { - cursor : default; - width : 80px; - text-align : center; -} - -.queue-status-cell { - width : 20px; - text-align : center !important; -} - -.queue-actions-cell { - min-width : 65px; - width : 65px; - text-align : right !important; - - i { - .clickable(); - margin-left : 1px; - margin-right : 1px; - } -} - -.download-log-cell { - width : 80px; -} - -td.delete-track-file-cell { - .clickable(); - - text-align : center; - width : 20px; - - i { - .clickable(); - } -} - -.artist-status-cell { - width: 16px; -} - -.track-number-cell { - cursor : default; -} - -.backup-type-cell { - width : 20px; -} - -.table>tbody>tr>td, .table>thead>tr>th { - - &.track-warning-cell { - width : 1px; - padding-left : 0px; - padding-right : 0px; - } -} - -.log-message-cell { - word-break: break-all; - word-wrap: break-word; -} - -.execute-task-cell { - width : 28px; - - i { - .clickable(); - } -} - -.task-interval-cell, .next-execution-cell { - cursor : default; -} - -.task-interval-cell { - width : 150px; -} - -.next-execution-cell { - width : 200px; -} - -.tasks { - .relative-time-cell { - width : 200px; - } -} - -.age-cell { - cursor : default; -} - -.blacklist-actions-cell { - min-width : 55px; - width : 55px; - text-align : right !important; - - i { - .clickable(); - margin-left : 2px; - margin-right : 2px; - } -} diff --git a/src/UI/Commands/CommandCollection.js b/src/UI/Commands/CommandCollection.js deleted file mode 100644 index b8eaae543..000000000 --- a/src/UI/Commands/CommandCollection.js +++ /dev/null @@ -1,20 +0,0 @@ -var Backbone = require('backbone'); -var CommandModel = require('./CommandModel'); -require('../Mixins/backbone.signalr.mixin'); - -var CommandCollection = Backbone.Collection.extend({ - url : window.NzbDrone.ApiRoot + '/command', - model : CommandModel, - - findCommand : function(command) { - return this.find(function(model) { - return model.isSameCommand(command); - }); - } -}); - -var collection = new CommandCollection().bindSignalR(); - -collection.fetch(); - -module.exports = collection; \ No newline at end of file diff --git a/src/UI/Commands/CommandController.js b/src/UI/Commands/CommandController.js deleted file mode 100644 index 2232d45ae..000000000 --- a/src/UI/Commands/CommandController.js +++ /dev/null @@ -1,94 +0,0 @@ -var vent = require('vent'); -var CommandModel = require('./CommandModel'); -var CommandCollection = require('./CommandCollection'); -var CommandMessengerCollectionView = require('./CommandMessengerCollectionView'); -var _ = require('underscore'); -var moment = require('moment'); -var Messenger = require('../Shared/Messenger'); -require('../jQuery/jquery.spin'); - -CommandMessengerCollectionView.render(); - -var singleton = function() { - - return { - - _lastCommand : {}, - - Execute : function(name, properties) { - - var attr = _.extend({ name : name.toLocaleLowerCase() }, properties); - var commandModel = new CommandModel(attr); - - if (this._lastCommand.command && this._lastCommand.command.isSameCommand(attr) && moment().add('seconds', -5).isBefore(this._lastCommand.time)) { - - Messenger.show({ - message : 'Please wait at least 5 seconds before running this command again', - hideAfter : 5, - type : 'error' - }); - - return this._lastCommand.promise; - } - - var promise = commandModel.save().success(function() { - CommandCollection.add(commandModel); - }); - - this._lastCommand = { - command : commandModel, - promise : promise, - time : moment() - }; - - return promise; - }, - - bindToCommand : function(options) { - - var self = this; - var existingCommand = CommandCollection.findCommand(options.command); - - if (existingCommand) { - this._bindToCommandModel.call(this, existingCommand, options); - } - - CommandCollection.bind('add', function(model) { - if (model.isSameCommand(options.command)) { - self._bindToCommandModel.call(self, model, options); - } - }); - - CommandCollection.bind('sync', function() { - var command = CommandCollection.findCommand(options.command); - if (command) { - self._bindToCommandModel.call(self, command, options); - } - }); - }, - - _bindToCommandModel : function bindToCommand (model, options) { - - if (!model.isActive()) { - options.element.stopSpin(); - return; - } - - model.bind('change:status', function(model) { - if (!model.isActive()) { - options.element.stopSpin(); - - if (model.isComplete()) { - vent.trigger(vent.Events.CommandComplete, { - command : model, - model : options.model - }); - } - } - }); - - options.element.startSpin(); - } - }; -}; -module.exports = singleton(); diff --git a/src/UI/Commands/CommandMessengerCollectionView.js b/src/UI/Commands/CommandMessengerCollectionView.js deleted file mode 100644 index 007760087..000000000 --- a/src/UI/Commands/CommandMessengerCollectionView.js +++ /dev/null @@ -1,11 +0,0 @@ -var Marionette = require('marionette'); -var commandCollection = require('./CommandCollection'); -var CommandMessengerItemView = require('./CommandMessengerItemView'); - -var CollectionView = Marionette.CollectionView.extend({ - itemView : CommandMessengerItemView -}); - -module.exports = new CollectionView({ - collection : commandCollection -}); diff --git a/src/UI/Commands/CommandMessengerItemView.js b/src/UI/Commands/CommandMessengerItemView.js deleted file mode 100644 index c7f419f31..000000000 --- a/src/UI/Commands/CommandMessengerItemView.js +++ /dev/null @@ -1,45 +0,0 @@ -var Marionette = require('marionette'); -var Messenger = require('../Shared/Messenger'); - -module.exports = Marionette.ItemView.extend({ - initialize : function() { - this.listenTo(this.model, 'change', this.render); - }, - - render : function() { - if (!this.model.get('message') || !this.model.get('sendUpdatesToClient')) { - return; - } - - var message = { - type : 'info', - message : '[{0}] {1}'.format(this.model.get('name'), this.model.get('message')), - id : this.model.id, - hideAfter : 0 - }; - - var isManual = this.model.get('manual'); - - switch (this.model.get('state')) { - case 'completed': - message.hideAfter = 4; - break; - case 'failed': - message.hideAfter = isManual ? 10 : 4; - message.type = 'error'; - break; - default : - message.hideAfter = 0; - } - - if (this.messenger) { - this.messenger.update(message); - } - - else { - this.messenger = Messenger.show(message); - } - - console.log(message.message); - } -}); \ No newline at end of file diff --git a/src/UI/Commands/CommandModel.js b/src/UI/Commands/CommandModel.js deleted file mode 100644 index 674067b24..000000000 --- a/src/UI/Commands/CommandModel.js +++ /dev/null @@ -1,50 +0,0 @@ -var _ = require('underscore'); -var Backbone = require('backbone'); - -module.exports = Backbone.Model.extend({ - url : window.NzbDrone.ApiRoot + '/command', - - parse : function(response) { - response.name = response.name.toLocaleLowerCase(); - response.body.name = response.body.name.toLocaleLowerCase(); - - for (var key in response.body) { - response[key] = response.body[key]; - } - - delete response.body; - - return response; - }, - - isSameCommand : function(command) { - - if (command.name.toLocaleLowerCase() !== this.get('name').toLocaleLowerCase()) { - return false; - } - - for (var key in command) { - if (key !== 'name') { - if (Array.isArray(command[key])) { - if (_.difference(command[key], this.get(key)).length > 0) { - return false; - } - } - - else if (command[key] !== this.get(key)) { - return false; - } - } - } - - return true; - }, - - isActive : function() { - return this.get('status') !== 'completed' && this.get('status') !== 'failed'; - }, - - isComplete : function() { - return this.get('status') === 'completed'; - } -}); \ No newline at end of file diff --git a/src/UI/Config.js b/src/UI/Config.js deleted file mode 100644 index a080f1343..000000000 --- a/src/UI/Config.js +++ /dev/null @@ -1,69 +0,0 @@ -var $ = require('jquery'); -var vent = require('./vent'); - -module.exports = { - Events : { - ConfigUpdatedEvent : 'ConfigUpdatedEvent' - }, - - Keys : { - DefaultProfileId : 'DefaultProfileId', - DefaultRootFolderId : 'DefaultRootFolderId', - UseAlbumFolder : 'UseAlbumFolder', - DefaultArtistType : 'DefaultArtistType', - MonitorEpisodes : 'MonitorEpisodes', - AdvancedSettings : 'advancedSettings' - }, - - getValueJson : function (key, defaultValue) { - defaultValue = defaultValue || {}; - - var storeValue = window.localStorage.getItem(key); - - if (!storeValue) { - return defaultValue; - } - - return $.parseJSON(storeValue); - }, - - getValueBoolean : function(key, defaultValue) { - defaultValue = defaultValue || false; - - return this.getValue(key, defaultValue.toString()) === 'true'; - }, - - getValue : function(key, defaultValue) { - var storeValue = window.localStorage.getItem(key); - - if (!storeValue) { - return defaultValue; - } - - return storeValue.toString(); - }, - - setValueJson : function(key, value) { - return this.setValue(key, JSON.stringify(value)); - }, - - setValue : function(key, value) { - - console.log('Config: [{0}] => [{1}]'.format(key, value)); - - if (this.getValue(key) === value.toString()) { - return; - } - - try { - window.localStorage.setItem(key, value); - vent.trigger(this.Events.ConfigUpdatedEvent, { - key : key, - value : value - }); - } - catch (error) { - console.error('Unable to save config: [{0}] => [{1}]'.format(key, value)); - } - } -}; diff --git a/src/UI/Content/Backgrid/backgrid.less b/src/UI/Content/Backgrid/backgrid.less deleted file mode 100644 index ae1d46943..000000000 --- a/src/UI/Content/Backgrid/backgrid.less +++ /dev/null @@ -1,3 +0,0 @@ -@import "filter"; -@import "paginator"; -@import "selectall"; \ No newline at end of file diff --git a/src/UI/Content/Backgrid/filter.less b/src/UI/Content/Backgrid/filter.less deleted file mode 100644 index 84313310a..000000000 --- a/src/UI/Content/Backgrid/filter.less +++ /dev/null @@ -1,11 +0,0 @@ -.backgrid-filter .close { - display : inline-block; - float : none; - width : 20px; - height : 20px; - margin-top : -4px; - font-size : 20px; - line-height : 20px; - text-align : center; - vertical-align : text-top; -} diff --git a/src/UI/Content/Backgrid/paginator.less b/src/UI/Content/Backgrid/paginator.less deleted file mode 100644 index 61fced052..000000000 --- a/src/UI/Content/Backgrid/paginator.less +++ /dev/null @@ -1,66 +0,0 @@ -@import "../prefixer"; -@import "../../Shared/Styles/clickable.less"; - -.backgrid-paginator { - text-align : center; - box-sizing : border-box; - border-top : none; - .box-sizing(border-box); - .border-radius(0 0 4px 4px); - position: relative; - - .total-records { - display : inline-block; - height : 30px; - padding : 0; - line-height: 30px; - font-size : 13px; - position : absolute; - right : 0; - - .label { - margin-top: 5px; - } - } - - ul { - display : inline-block; - - li { - display : inline; - - i, span { - float : left; - width : 30px; - height : 30px; - padding : 0; - line-height : 30px; - text-decoration : none; - } - - select { - width: auto; - } - - .pager-btn { - .clickable; - } - } - .active { - span { - background-color : #f5f5f5; - color : #999999; - cursor : default; - width : inherit; - padding : 0px 2px; - } - } - - .disabled { - i, span { - color : #999999; - cursor : default; - } - } - } -} diff --git a/src/UI/Content/Backgrid/selectall.less b/src/UI/Content/Backgrid/selectall.less deleted file mode 100644 index 322853304..000000000 --- a/src/UI/Content/Backgrid/selectall.less +++ /dev/null @@ -1,12 +0,0 @@ -/* - backgrid-select-all - http://github.com/wyuenho/backgrid - - Copyright (c) 2013 Jimmy Yuen Ho Wong and contributors - Licensed under the MIT @license. -*/ - -.select-row-cell, .select-all-header-cell { - text-align: center; - width: 16px; -} \ No newline at end of file diff --git a/src/UI/Content/Bootstrap/.csscomb.json b/src/UI/Content/Bootstrap/.csscomb.json deleted file mode 100644 index 40695a478..000000000 --- a/src/UI/Content/Bootstrap/.csscomb.json +++ /dev/null @@ -1,304 +0,0 @@ -{ - "always-semicolon": true, - "block-indent": 2, - "color-case": "lower", - "color-shorthand": true, - "element-case": "lower", - "eof-newline": true, - "leading-zero": false, - "remove-empty-rulesets": true, - "space-after-colon": 1, - "space-after-combinator": 1, - "space-before-selector-delimiter": 0, - "space-between-declarations": "\n", - "space-after-opening-brace": "\n", - "space-before-closing-brace": "\n", - "space-before-colon": 0, - "space-before-combinator": 1, - "space-before-opening-brace": 1, - "strip-spaces": true, - "unitless-zero": true, - "vendor-prefix-align": true, - "sort-order": [ - [ - "position", - "top", - "right", - "bottom", - "left", - "z-index", - "display", - "float", - "width", - "min-width", - "max-width", - "height", - "min-height", - "max-height", - "-webkit-box-sizing", - "-moz-box-sizing", - "box-sizing", - "-webkit-appearance", - "padding", - "padding-top", - "padding-right", - "padding-bottom", - "padding-left", - "margin", - "margin-top", - "margin-right", - "margin-bottom", - "margin-left", - "overflow", - "overflow-x", - "overflow-y", - "-webkit-overflow-scrolling", - "-ms-overflow-x", - "-ms-overflow-y", - "-ms-overflow-style", - "clip", - "clear", - "font", - "font-family", - "font-size", - "font-style", - "font-weight", - "font-variant", - "font-size-adjust", - "font-stretch", - "font-effect", - "font-emphasize", - "font-emphasize-position", - "font-emphasize-style", - "font-smooth", - "-webkit-hyphens", - "-moz-hyphens", - "hyphens", - "line-height", - "color", - "text-align", - "-webkit-text-align-last", - "-moz-text-align-last", - "-ms-text-align-last", - "text-align-last", - "text-emphasis", - "text-emphasis-color", - "text-emphasis-style", - "text-emphasis-position", - "text-decoration", - "text-indent", - "text-justify", - "text-outline", - "-ms-text-overflow", - "text-overflow", - "text-overflow-ellipsis", - "text-overflow-mode", - "text-shadow", - "text-transform", - "text-wrap", - "-webkit-text-size-adjust", - "-ms-text-size-adjust", - "letter-spacing", - "-ms-word-break", - "word-break", - "word-spacing", - "-ms-word-wrap", - "word-wrap", - "-moz-tab-size", - "-o-tab-size", - "tab-size", - "white-space", - "vertical-align", - "list-style", - "list-style-position", - "list-style-type", - "list-style-image", - "pointer-events", - "-ms-touch-action", - "touch-action", - "cursor", - "visibility", - "zoom", - "flex-direction", - "flex-order", - "flex-pack", - "flex-align", - "table-layout", - "empty-cells", - "caption-side", - "border-spacing", - "border-collapse", - "content", - "quotes", - "counter-reset", - "counter-increment", - "resize", - "-webkit-user-select", - "-moz-user-select", - "-ms-user-select", - "-o-user-select", - "user-select", - "nav-index", - "nav-up", - "nav-right", - "nav-down", - "nav-left", - "background", - "background-color", - "background-image", - "-ms-filter:\\'progid:DXImageTransform.Microsoft.gradient", - "filter:progid:DXImageTransform.Microsoft.gradient", - "filter:progid:DXImageTransform.Microsoft.AlphaImageLoader", - "filter", - "background-repeat", - "background-attachment", - "background-position", - "background-position-x", - "background-position-y", - "-webkit-background-clip", - "-moz-background-clip", - "background-clip", - "background-origin", - "-webkit-background-size", - "-moz-background-size", - "-o-background-size", - "background-size", - "border", - "border-color", - "border-style", - "border-width", - "border-top", - "border-top-color", - "border-top-style", - "border-top-width", - "border-right", - "border-right-color", - "border-right-style", - "border-right-width", - "border-bottom", - "border-bottom-color", - "border-bottom-style", - "border-bottom-width", - "border-left", - "border-left-color", - "border-left-style", - "border-left-width", - "border-radius", - "border-top-left-radius", - "border-top-right-radius", - "border-bottom-right-radius", - "border-bottom-left-radius", - "-webkit-border-image", - "-moz-border-image", - "-o-border-image", - "border-image", - "-webkit-border-image-source", - "-moz-border-image-source", - "-o-border-image-source", - "border-image-source", - "-webkit-border-image-slice", - "-moz-border-image-slice", - "-o-border-image-slice", - "border-image-slice", - "-webkit-border-image-width", - "-moz-border-image-width", - "-o-border-image-width", - "border-image-width", - "-webkit-border-image-outset", - "-moz-border-image-outset", - "-o-border-image-outset", - "border-image-outset", - "-webkit-border-image-repeat", - "-moz-border-image-repeat", - "-o-border-image-repeat", - "border-image-repeat", - "outline", - "outline-width", - "outline-style", - "outline-color", - "outline-offset", - "-webkit-box-shadow", - "-moz-box-shadow", - "box-shadow", - "filter:progid:DXImageTransform.Microsoft.Alpha(Opacity", - "-ms-filter:\\'progid:DXImageTransform.Microsoft.Alpha", - "opacity", - "-ms-interpolation-mode", - "-webkit-transition", - "-moz-transition", - "-ms-transition", - "-o-transition", - "transition", - "-webkit-transition-delay", - "-moz-transition-delay", - "-ms-transition-delay", - "-o-transition-delay", - "transition-delay", - "-webkit-transition-timing-function", - "-moz-transition-timing-function", - "-ms-transition-timing-function", - "-o-transition-timing-function", - "transition-timing-function", - "-webkit-transition-duration", - "-moz-transition-duration", - "-ms-transition-duration", - "-o-transition-duration", - "transition-duration", - "-webkit-transition-property", - "-moz-transition-property", - "-ms-transition-property", - "-o-transition-property", - "transition-property", - "-webkit-transform", - "-moz-transform", - "-ms-transform", - "-o-transform", - "transform", - "-webkit-transform-origin", - "-moz-transform-origin", - "-ms-transform-origin", - "-o-transform-origin", - "transform-origin", - "-webkit-animation", - "-moz-animation", - "-ms-animation", - "-o-animation", - "animation", - "-webkit-animation-name", - "-moz-animation-name", - "-ms-animation-name", - "-o-animation-name", - "animation-name", - "-webkit-animation-duration", - "-moz-animation-duration", - "-ms-animation-duration", - "-o-animation-duration", - "animation-duration", - "-webkit-animation-play-state", - "-moz-animation-play-state", - "-ms-animation-play-state", - "-o-animation-play-state", - "animation-play-state", - "-webkit-animation-timing-function", - "-moz-animation-timing-function", - "-ms-animation-timing-function", - "-o-animation-timing-function", - "animation-timing-function", - "-webkit-animation-delay", - "-moz-animation-delay", - "-ms-animation-delay", - "-o-animation-delay", - "animation-delay", - "-webkit-animation-iteration-count", - "-moz-animation-iteration-count", - "-ms-animation-iteration-count", - "-o-animation-iteration-count", - "animation-iteration-count", - "-webkit-animation-direction", - "-moz-animation-direction", - "-ms-animation-direction", - "-o-animation-direction", - "animation-direction" - ] - ] -} diff --git a/src/UI/Content/Bootstrap/.csslintrc b/src/UI/Content/Bootstrap/.csslintrc deleted file mode 100644 index 005b86236..000000000 --- a/src/UI/Content/Bootstrap/.csslintrc +++ /dev/null @@ -1,19 +0,0 @@ -{ - "adjoining-classes": false, - "box-sizing": false, - "box-model": false, - "compatible-vendor-prefixes": false, - "floats": false, - "font-sizes": false, - "gradients": false, - "important": false, - "known-properties": false, - "outline-none": false, - "qualified-headings": false, - "regex-selectors": false, - "shorthand": false, - "text-indent": false, - "unique-headings": false, - "universal-selector": false, - "unqualified-attributes": false -} diff --git a/src/UI/Content/Bootstrap/alerts.less b/src/UI/Content/Bootstrap/alerts.less deleted file mode 100644 index c4199db92..000000000 --- a/src/UI/Content/Bootstrap/alerts.less +++ /dev/null @@ -1,73 +0,0 @@ -// -// Alerts -// -------------------------------------------------- - - -// Base styles -// ------------------------- - -.alert { - padding: @alert-padding; - margin-bottom: @line-height-computed; - border: 1px solid transparent; - border-radius: @alert-border-radius; - - // Headings for larger alerts - h4 { - margin-top: 0; - // Specified for the h4 to prevent conflicts of changing @headings-color - color: inherit; - } - - // Provide class for links that match alerts - .alert-link { - font-weight: @alert-link-font-weight; - } - - // Improve alignment and spacing of inner content - > p, - > ul { - margin-bottom: 0; - } - - > p + p { - margin-top: 5px; - } -} - -// Dismissible alerts -// -// Expand the right padding and account for the close button's positioning. - -.alert-dismissable, // The misspelled .alert-dismissable was deprecated in 3.2.0. -.alert-dismissible { - padding-right: (@alert-padding + 20); - - // Adjust close link position - .close { - position: relative; - top: -2px; - right: -21px; - color: inherit; - } -} - -// Alternate styles -// -// Generate contextual modifier classes for colorizing the alert. - -.alert-success { - .alert-variant(@alert-success-bg; @alert-success-border; @alert-success-text); -} - -.alert-info { - .alert-variant(@alert-info-bg; @alert-info-border; @alert-info-text); -} - -.alert-warning { - .alert-variant(@alert-warning-bg; @alert-warning-border; @alert-warning-text); -} - -.alert-danger { - .alert-variant(@alert-danger-bg; @alert-danger-border; @alert-danger-text); -} diff --git a/src/UI/Content/Bootstrap/badges.less b/src/UI/Content/Bootstrap/badges.less deleted file mode 100644 index 6ee16dca4..000000000 --- a/src/UI/Content/Bootstrap/badges.less +++ /dev/null @@ -1,66 +0,0 @@ -// -// Badges -// -------------------------------------------------- - - -// Base class -.badge { - display: inline-block; - min-width: 10px; - padding: 3px 7px; - font-size: @font-size-small; - font-weight: @badge-font-weight; - color: @badge-color; - line-height: @badge-line-height; - vertical-align: middle; - white-space: nowrap; - text-align: center; - background-color: @badge-bg; - border-radius: @badge-border-radius; - - // Empty badges collapse automatically (not available in IE8) - &:empty { - display: none; - } - - // Quick fix for badges in buttons - .btn & { - position: relative; - top: -1px; - } - - .btn-xs &, - .btn-group-xs > .btn & { - top: 0; - padding: 1px 5px; - } - - // Hover state, but only for links - a& { - &:hover, - &:focus { - color: @badge-link-hover-color; - text-decoration: none; - cursor: pointer; - } - } - - // Account for badges in navs - .list-group-item.active > &, - .nav-pills > .active > a > & { - color: @badge-active-color; - background-color: @badge-active-bg; - } - - .list-group-item > & { - float: right; - } - - .list-group-item > & + & { - margin-right: 5px; - } - - .nav-pills > li > a > & { - margin-left: 3px; - } -} diff --git a/src/UI/Content/Bootstrap/bootstrap.less b/src/UI/Content/Bootstrap/bootstrap.less deleted file mode 100644 index 4b9916e6c..000000000 --- a/src/UI/Content/Bootstrap/bootstrap.less +++ /dev/null @@ -1,56 +0,0 @@ -/*! - * Bootstrap v3.3.5 (http://getbootstrap.com) - * Copyright 2011-2015 Twitter, Inc. - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) - */ - -// Core variables and mixins -@import "variables.less"; -@import "mixins.less"; - -// Reset and dependencies -@import "normalize.less"; -@import "print.less"; -@import "glyphicons.less"; - -// Core CSS -@import "scaffolding.less"; -@import "type.less"; -@import "code.less"; -@import "grid.less"; -@import "tables.less"; -@import "forms.less"; -@import "buttons.less"; - -// Components -@import "component-animations.less"; -@import "dropdowns.less"; -@import "button-groups.less"; -@import "input-groups.less"; -@import "navs.less"; -@import "navbar.less"; -@import "breadcrumbs.less"; -@import "pagination.less"; -@import "pager.less"; -@import "labels.less"; -@import "badges.less"; -@import "jumbotron.less"; -@import "thumbnails.less"; -@import "alerts.less"; -@import "progress-bars.less"; -@import "media.less"; -@import "list-group.less"; -@import "panels.less"; -@import "responsive-embed.less"; -@import "wells.less"; -@import "close.less"; - -// Components w/ JavaScript -@import "modals.less"; -@import "tooltip.less"; -@import "popovers.less"; -@import "carousel.less"; - -// Utility classes -@import "utilities.less"; -@import "responsive-utilities.less"; diff --git a/src/UI/Content/Bootstrap/breadcrumbs.less b/src/UI/Content/Bootstrap/breadcrumbs.less deleted file mode 100644 index cb01d503f..000000000 --- a/src/UI/Content/Bootstrap/breadcrumbs.less +++ /dev/null @@ -1,26 +0,0 @@ -// -// Breadcrumbs -// -------------------------------------------------- - - -.breadcrumb { - padding: @breadcrumb-padding-vertical @breadcrumb-padding-horizontal; - margin-bottom: @line-height-computed; - list-style: none; - background-color: @breadcrumb-bg; - border-radius: @border-radius-base; - - > li { - display: inline-block; - - + li:before { - content: "@{breadcrumb-separator}\00a0"; // Unicode space added since inline-block means non-collapsing white-space - padding: 0 5px; - color: @breadcrumb-color; - } - } - - > .active { - color: @breadcrumb-active-color; - } -} diff --git a/src/UI/Content/Bootstrap/button-groups.less b/src/UI/Content/Bootstrap/button-groups.less deleted file mode 100644 index 6a0c5a865..000000000 --- a/src/UI/Content/Bootstrap/button-groups.less +++ /dev/null @@ -1,244 +0,0 @@ -// -// Button groups -// -------------------------------------------------- - -// Make the div behave like a button -.btn-group, -.btn-group-vertical { - position: relative; - display: inline-block; - vertical-align: middle; // match .btn alignment given font-size hack above - > .btn { - position: relative; - float: left; - // Bring the "active" button to the front - &:hover, - &:focus, - &:active, - &.active { - z-index: 2; - } - } -} - -// Prevent double borders when buttons are next to each other -.btn-group { - .btn + .btn, - .btn + .btn-group, - .btn-group + .btn, - .btn-group + .btn-group { - margin-left: -1px; - } -} - -// Optional: Group multiple button groups together for a toolbar -.btn-toolbar { - margin-left: -5px; // Offset the first child's margin - &:extend(.clearfix all); - - .btn, - .btn-group, - .input-group { - float: left; - } - > .btn, - > .btn-group, - > .input-group { - margin-left: 5px; - } -} - -.btn-group > .btn:not(:first-child):not(:last-child):not(.dropdown-toggle) { - border-radius: 0; -} - -// Set corners individual because sometimes a single button can be in a .btn-group and we need :first-child and :last-child to both match -.btn-group > .btn:first-child { - margin-left: 0; - &:not(:last-child):not(.dropdown-toggle) { - .border-right-radius(0); - } -} -// Need .dropdown-toggle since :last-child doesn't apply given a .dropdown-menu immediately after it -.btn-group > .btn:last-child:not(:first-child), -.btn-group > .dropdown-toggle:not(:first-child) { - .border-left-radius(0); -} - -// Custom edits for including btn-groups within btn-groups (useful for including dropdown buttons within a btn-group) -.btn-group > .btn-group { - float: left; -} -.btn-group > .btn-group:not(:first-child):not(:last-child) > .btn { - border-radius: 0; -} -.btn-group > .btn-group:first-child:not(:last-child) { - > .btn:last-child, - > .dropdown-toggle { - .border-right-radius(0); - } -} -.btn-group > .btn-group:last-child:not(:first-child) > .btn:first-child { - .border-left-radius(0); -} - -// On active and open, don't show outline -.btn-group .dropdown-toggle:active, -.btn-group.open .dropdown-toggle { - outline: 0; -} - - -// Sizing -// -// Remix the default button sizing classes into new ones for easier manipulation. - -.btn-group-xs > .btn { &:extend(.btn-xs); } -.btn-group-sm > .btn { &:extend(.btn-sm); } -.btn-group-lg > .btn { &:extend(.btn-lg); } - - -// Split button dropdowns -// ---------------------- - -// Give the line between buttons some depth -.btn-group > .btn + .dropdown-toggle { - padding-left: 8px; - padding-right: 8px; -} -.btn-group > .btn-lg + .dropdown-toggle { - padding-left: 12px; - padding-right: 12px; -} - -// The clickable button for toggling the menu -// Remove the gradient and set the same inset shadow as the :active state -.btn-group.open .dropdown-toggle { - .box-shadow(inset 0 3px 5px rgba(0,0,0,.125)); - - // Show no shadow for `.btn-link` since it has no other button styles. - &.btn-link { - .box-shadow(none); - } -} - - -// Reposition the caret -.btn .caret { - margin-left: 0; -} -// Carets in other button sizes -.btn-lg .caret { - border-width: @caret-width-large @caret-width-large 0; - border-bottom-width: 0; -} -// Upside down carets for .dropup -.dropup .btn-lg .caret { - border-width: 0 @caret-width-large @caret-width-large; -} - - -// Vertical button groups -// ---------------------- - -.btn-group-vertical { - > .btn, - > .btn-group, - > .btn-group > .btn { - display: block; - float: none; - width: 100%; - max-width: 100%; - } - - // Clear floats so dropdown menus can be properly placed - > .btn-group { - &:extend(.clearfix all); - > .btn { - float: none; - } - } - - > .btn + .btn, - > .btn + .btn-group, - > .btn-group + .btn, - > .btn-group + .btn-group { - margin-top: -1px; - margin-left: 0; - } -} - -.btn-group-vertical > .btn { - &:not(:first-child):not(:last-child) { - border-radius: 0; - } - &:first-child:not(:last-child) { - border-top-right-radius: @btn-border-radius-base; - .border-bottom-radius(0); - } - &:last-child:not(:first-child) { - border-bottom-left-radius: @btn-border-radius-base; - .border-top-radius(0); - } -} -.btn-group-vertical > .btn-group:not(:first-child):not(:last-child) > .btn { - border-radius: 0; -} -.btn-group-vertical > .btn-group:first-child:not(:last-child) { - > .btn:last-child, - > .dropdown-toggle { - .border-bottom-radius(0); - } -} -.btn-group-vertical > .btn-group:last-child:not(:first-child) > .btn:first-child { - .border-top-radius(0); -} - - -// Justified button groups -// ---------------------- - -.btn-group-justified { - display: table; - width: 100%; - table-layout: fixed; - border-collapse: separate; - > .btn, - > .btn-group { - float: none; - display: table-cell; - width: 1%; - } - > .btn-group .btn { - width: 100%; - } - - > .btn-group .dropdown-menu { - left: auto; - } -} - - -// Checkbox and radio options -// -// In order to support the browser's form validation feedback, powered by the -// `required` attribute, we have to "hide" the inputs via `clip`. We cannot use -// `display: none;` or `visibility: hidden;` as that also hides the popover. -// Simply visually hiding the inputs via `opacity` would leave them clickable in -// certain cases which is prevented by using `clip` and `pointer-events`. -// This way, we ensure a DOM element is visible to position the popover from. -// -// See https://github.com/twbs/bootstrap/pull/12794 and -// https://github.com/twbs/bootstrap/pull/14559 for more information. - -[data-toggle="buttons"] { - > .btn, - > .btn-group > .btn { - input[type="radio"], - input[type="checkbox"] { - position: absolute; - clip: rect(0,0,0,0); - pointer-events: none; - } - } -} diff --git a/src/UI/Content/Bootstrap/buttons.less b/src/UI/Content/Bootstrap/buttons.less deleted file mode 100644 index 9cbb8f416..000000000 --- a/src/UI/Content/Bootstrap/buttons.less +++ /dev/null @@ -1,166 +0,0 @@ -// -// Buttons -// -------------------------------------------------- - - -// Base styles -// -------------------------------------------------- - -.btn { - display: inline-block; - margin-bottom: 0; // For input.btn - font-weight: @btn-font-weight; - text-align: center; - vertical-align: middle; - touch-action: manipulation; - cursor: pointer; - background-image: none; // Reset unusual Firefox-on-Android default style; see https://github.com/necolas/normalize.css/issues/214 - border: 1px solid transparent; - white-space: nowrap; - .button-size(@padding-base-vertical; @padding-base-horizontal; @font-size-base; @line-height-base; @btn-border-radius-base); - .user-select(none); - - &, - &:active, - &.active { - &:focus, - &.focus { - .tab-focus(); - } - } - - &:hover, - &:focus, - &.focus { - color: @btn-default-color; - text-decoration: none; - } - - &:active, - &.active { - outline: 0; - background-image: none; - .box-shadow(inset 0 3px 5px rgba(0,0,0,.125)); - } - - &.disabled, - &[disabled], - fieldset[disabled] & { - cursor: @cursor-disabled; - .opacity(.65); - .box-shadow(none); - } - - a& { - &.disabled, - fieldset[disabled] & { - pointer-events: none; // Future-proof disabling of clicks on `<a>` elements - } - } -} - - -// Alternate buttons -// -------------------------------------------------- - -.btn-default { - .button-variant(@btn-default-color; @btn-default-bg; @btn-default-border); -} -.btn-primary { - .button-variant(@btn-primary-color; @btn-primary-bg; @btn-primary-border); -} -// Success appears as green -.btn-success { - .button-variant(@btn-success-color; @btn-success-bg; @btn-success-border); -} -// Info appears as blue-green -.btn-info { - .button-variant(@btn-info-color; @btn-info-bg; @btn-info-border); -} -// Warning appears as orange -.btn-warning { - .button-variant(@btn-warning-color; @btn-warning-bg; @btn-warning-border); -} -// Danger and error appear as red -.btn-danger { - .button-variant(@btn-danger-color; @btn-danger-bg; @btn-danger-border); -} - - -// Link buttons -// ------------------------- - -// Make a button look and behave like a link -.btn-link { - color: @link-color; - font-weight: normal; - border-radius: 0; - - &, - &:active, - &.active, - &[disabled], - fieldset[disabled] & { - background-color: transparent; - .box-shadow(none); - } - &, - &:hover, - &:focus, - &:active { - border-color: transparent; - } - &:hover, - &:focus { - color: @link-hover-color; - text-decoration: @link-hover-decoration; - background-color: transparent; - } - &[disabled], - fieldset[disabled] & { - &:hover, - &:focus { - color: @btn-link-disabled-color; - text-decoration: none; - } - } -} - - -// Button Sizes -// -------------------------------------------------- - -.btn-lg { - // line-height: ensure even-numbered height of button next to large input - .button-size(@padding-large-vertical; @padding-large-horizontal; @font-size-large; @line-height-large; @btn-border-radius-large); -} -.btn-sm { - // line-height: ensure proper height of button next to small input - .button-size(@padding-small-vertical; @padding-small-horizontal; @font-size-small; @line-height-small; @btn-border-radius-small); -} -.btn-xs { - .button-size(@padding-xs-vertical; @padding-xs-horizontal; @font-size-small; @line-height-small; @btn-border-radius-small); -} - - -// Block button -// -------------------------------------------------- - -.btn-block { - display: block; - width: 100%; -} - -// Vertically space out multiple block buttons -.btn-block + .btn-block { - margin-top: 5px; -} - -// Specificity overrides -input[type="submit"], -input[type="reset"], -input[type="button"] { - &.btn-block { - width: 100%; - } -} diff --git a/src/UI/Content/Bootstrap/carousel.less b/src/UI/Content/Bootstrap/carousel.less deleted file mode 100644 index 87ed6961d..000000000 --- a/src/UI/Content/Bootstrap/carousel.less +++ /dev/null @@ -1,269 +0,0 @@ -// -// Carousel -// -------------------------------------------------- - - -// Wrapper for the slide container and indicators -.carousel { - position: relative; -} - -.carousel-inner { - position: relative; - overflow: hidden; - width: 100%; - - > .item { - display: none; - position: relative; - .transition(.6s ease-in-out left); - - // Account for jankitude on images - > img, - > a > img { - &:extend(.img-responsive); - line-height: 1; - } - - // WebKit CSS3 transforms for supported devices - @media all and (transform-3d), (-webkit-transform-3d) { - .transition-transform(~'0.6s ease-in-out'); - .backface-visibility(~'hidden'); - .perspective(1000px); - - &.next, - &.active.right { - .translate3d(100%, 0, 0); - left: 0; - } - &.prev, - &.active.left { - .translate3d(-100%, 0, 0); - left: 0; - } - &.next.left, - &.prev.right, - &.active { - .translate3d(0, 0, 0); - left: 0; - } - } - } - - > .active, - > .next, - > .prev { - display: block; - } - - > .active { - left: 0; - } - - > .next, - > .prev { - position: absolute; - top: 0; - width: 100%; - } - - > .next { - left: 100%; - } - > .prev { - left: -100%; - } - > .next.left, - > .prev.right { - left: 0; - } - - > .active.left { - left: -100%; - } - > .active.right { - left: 100%; - } - -} - -// Left/right controls for nav -// --------------------------- - -.carousel-control { - position: absolute; - top: 0; - left: 0; - bottom: 0; - width: @carousel-control-width; - .opacity(@carousel-control-opacity); - font-size: @carousel-control-font-size; - color: @carousel-control-color; - text-align: center; - text-shadow: @carousel-text-shadow; - // We can't have this transition here because WebKit cancels the carousel - // animation if you trip this while in the middle of another animation. - - // Set gradients for backgrounds - &.left { - #gradient > .horizontal(@start-color: rgba(0,0,0,.5); @end-color: rgba(0,0,0,.0001)); - } - &.right { - left: auto; - right: 0; - #gradient > .horizontal(@start-color: rgba(0,0,0,.0001); @end-color: rgba(0,0,0,.5)); - } - - // Hover/focus state - &:hover, - &:focus { - outline: 0; - color: @carousel-control-color; - text-decoration: none; - .opacity(.9); - } - - // Toggles - .icon-prev, - .icon-next, - .glyphicon-chevron-left, - .glyphicon-chevron-right { - position: absolute; - top: 50%; - margin-top: -10px; - z-index: 5; - display: inline-block; - } - .icon-prev, - .glyphicon-chevron-left { - left: 50%; - margin-left: -10px; - } - .icon-next, - .glyphicon-chevron-right { - right: 50%; - margin-right: -10px; - } - .icon-prev, - .icon-next { - width: 20px; - height: 20px; - line-height: 1; - font-family: serif; - } - - - .icon-prev { - &:before { - content: '\2039';// SINGLE LEFT-POINTING ANGLE QUOTATION MARK (U+2039) - } - } - .icon-next { - &:before { - content: '\203a';// SINGLE RIGHT-POINTING ANGLE QUOTATION MARK (U+203A) - } - } -} - -// Optional indicator pips -// -// Add an unordered list with the following class and add a list item for each -// slide your carousel holds. - -.carousel-indicators { - position: absolute; - bottom: 10px; - left: 50%; - z-index: 15; - width: 60%; - margin-left: -30%; - padding-left: 0; - list-style: none; - text-align: center; - - li { - display: inline-block; - width: 10px; - height: 10px; - margin: 1px; - text-indent: -999px; - border: 1px solid @carousel-indicator-border-color; - border-radius: 10px; - cursor: pointer; - - // IE8-9 hack for event handling - // - // Internet Explorer 8-9 does not support clicks on elements without a set - // `background-color`. We cannot use `filter` since that's not viewed as a - // background color by the browser. Thus, a hack is needed. - // See https://developer.mozilla.org/en-US/docs/Web/Events/click#Internet_Explorer - // - // For IE8, we set solid black as it doesn't support `rgba()`. For IE9, we - // set alpha transparency for the best results possible. - background-color: #000 \9; // IE8 - background-color: rgba(0,0,0,0); // IE9 - } - .active { - margin: 0; - width: 12px; - height: 12px; - background-color: @carousel-indicator-active-bg; - } -} - -// Optional captions -// ----------------------------- -// Hidden by default for smaller viewports -.carousel-caption { - position: absolute; - left: 15%; - right: 15%; - bottom: 20px; - z-index: 10; - padding-top: 20px; - padding-bottom: 20px; - color: @carousel-caption-color; - text-align: center; - text-shadow: @carousel-text-shadow; - & .btn { - text-shadow: none; // No shadow for button elements in carousel-caption - } -} - - -// Scale up controls for tablets and up -@media screen and (min-width: @screen-sm-min) { - - // Scale up the controls a smidge - .carousel-control { - .glyphicon-chevron-left, - .glyphicon-chevron-right, - .icon-prev, - .icon-next { - width: 30px; - height: 30px; - margin-top: -15px; - font-size: 30px; - } - .glyphicon-chevron-left, - .icon-prev { - margin-left: -15px; - } - .glyphicon-chevron-right, - .icon-next { - margin-right: -15px; - } - } - - // Show and left align the captions - .carousel-caption { - left: 20%; - right: 20%; - padding-bottom: 30px; - } - - // Move up the indicators - .carousel-indicators { - bottom: 20px; - } -} diff --git a/src/UI/Content/Bootstrap/close.less b/src/UI/Content/Bootstrap/close.less deleted file mode 100644 index 6d5bfe087..000000000 --- a/src/UI/Content/Bootstrap/close.less +++ /dev/null @@ -1,34 +0,0 @@ -// -// Close icons -// -------------------------------------------------- - - -.close { - float: right; - font-size: (@font-size-base * 1.5); - font-weight: @close-font-weight; - line-height: 1; - color: @close-color; - text-shadow: @close-text-shadow; - .opacity(.2); - - &:hover, - &:focus { - color: @close-color; - text-decoration: none; - cursor: pointer; - .opacity(.5); - } - - // Additional properties for button version - // iOS requires the button element instead of an anchor tag. - // If you want the anchor version, it requires `href="#"`. - // See https://developer.mozilla.org/en-US/docs/Web/Events/click#Safari_Mobile - button& { - padding: 0; - cursor: pointer; - background: transparent; - border: 0; - -webkit-appearance: none; - } -} diff --git a/src/UI/Content/Bootstrap/code.less b/src/UI/Content/Bootstrap/code.less deleted file mode 100644 index a08b4d48c..000000000 --- a/src/UI/Content/Bootstrap/code.less +++ /dev/null @@ -1,69 +0,0 @@ -// -// Code (inline and block) -// -------------------------------------------------- - - -// Inline and block code styles -code, -kbd, -pre, -samp { - font-family: @font-family-monospace; -} - -// Inline code -code { - padding: 2px 4px; - font-size: 90%; - color: @code-color; - background-color: @code-bg; - border-radius: @border-radius-base; -} - -// User input typically entered via keyboard -kbd { - padding: 2px 4px; - font-size: 90%; - color: @kbd-color; - background-color: @kbd-bg; - border-radius: @border-radius-small; - box-shadow: inset 0 -1px 0 rgba(0,0,0,.25); - - kbd { - padding: 0; - font-size: 100%; - font-weight: bold; - box-shadow: none; - } -} - -// Blocks of code -pre { - display: block; - padding: ((@line-height-computed - 1) / 2); - margin: 0 0 (@line-height-computed / 2); - font-size: (@font-size-base - 1); // 14px to 13px - line-height: @line-height-base; - word-break: break-all; - word-wrap: break-word; - color: @pre-color; - background-color: @pre-bg; - border: 1px solid @pre-border-color; - border-radius: @border-radius-base; - - // Account for some code outputs that place code tags in pre tags - code { - padding: 0; - font-size: inherit; - color: inherit; - white-space: pre-wrap; - background-color: transparent; - border-radius: 0; - } -} - -// Enable scrollable blocks of code -.pre-scrollable { - max-height: @pre-scrollable-max-height; - overflow-y: scroll; -} diff --git a/src/UI/Content/Bootstrap/component-animations.less b/src/UI/Content/Bootstrap/component-animations.less deleted file mode 100644 index 0bcee910a..000000000 --- a/src/UI/Content/Bootstrap/component-animations.less +++ /dev/null @@ -1,33 +0,0 @@ -// -// Component animations -// -------------------------------------------------- - -// Heads up! -// -// We don't use the `.opacity()` mixin here since it causes a bug with text -// fields in IE7-8. Source: https://github.com/twbs/bootstrap/pull/3552. - -.fade { - opacity: 0; - .transition(opacity .15s linear); - &.in { - opacity: 1; - } -} - -.collapse { - display: none; - - &.in { display: block; } - tr&.in { display: table-row; } - tbody&.in { display: table-row-group; } -} - -.collapsing { - position: relative; - height: 0; - overflow: hidden; - .transition-property(~"height, visibility"); - .transition-duration(.35s); - .transition-timing-function(ease); -} diff --git a/src/UI/Content/Bootstrap/dropdowns.less b/src/UI/Content/Bootstrap/dropdowns.less deleted file mode 100644 index f6876c1a9..000000000 --- a/src/UI/Content/Bootstrap/dropdowns.less +++ /dev/null @@ -1,216 +0,0 @@ -// -// Dropdown menus -// -------------------------------------------------- - - -// Dropdown arrow/caret -.caret { - display: inline-block; - width: 0; - height: 0; - margin-left: 2px; - vertical-align: middle; - border-top: @caret-width-base dashed; - border-top: @caret-width-base solid ~"\9"; // IE8 - border-right: @caret-width-base solid transparent; - border-left: @caret-width-base solid transparent; -} - -// The dropdown wrapper (div) -.dropup, -.dropdown { - position: relative; -} - -// Prevent the focus on the dropdown toggle when closing dropdowns -.dropdown-toggle:focus { - outline: 0; -} - -// The dropdown menu (ul) -.dropdown-menu { - position: absolute; - top: 100%; - left: 0; - z-index: @zindex-dropdown; - display: none; // none by default, but block on "open" of the menu - float: left; - min-width: 160px; - padding: 5px 0; - margin: 2px 0 0; // override default ul - list-style: none; - font-size: @font-size-base; - text-align: left; // Ensures proper alignment if parent has it changed (e.g., modal footer) - background-color: @dropdown-bg; - border: 1px solid @dropdown-fallback-border; // IE8 fallback - border: 1px solid @dropdown-border; - border-radius: @border-radius-base; - .box-shadow(0 6px 12px rgba(0,0,0,.175)); - background-clip: padding-box; - - // Aligns the dropdown menu to right - // - // Deprecated as of 3.1.0 in favor of `.dropdown-menu-[dir]` - &.pull-right { - right: 0; - left: auto; - } - - // Dividers (basically an hr) within the dropdown - .divider { - .nav-divider(@dropdown-divider-bg); - } - - // Links within the dropdown menu - > li > a { - display: block; - padding: 3px 20px; - clear: both; - font-weight: normal; - line-height: @line-height-base; - color: @dropdown-link-color; - white-space: nowrap; // prevent links from randomly breaking onto new lines - } -} - -// Hover/Focus state -.dropdown-menu > li > a { - &:hover, - &:focus { - text-decoration: none; - color: @dropdown-link-hover-color; - background-color: @dropdown-link-hover-bg; - } -} - -// Active state -.dropdown-menu > .active > a { - &, - &:hover, - &:focus { - color: @dropdown-link-active-color; - text-decoration: none; - outline: 0; - background-color: @dropdown-link-active-bg; - } -} - -// Disabled state -// -// Gray out text and ensure the hover/focus state remains gray - -.dropdown-menu > .disabled > a { - &, - &:hover, - &:focus { - color: @dropdown-link-disabled-color; - } - - // Nuke hover/focus effects - &:hover, - &:focus { - text-decoration: none; - background-color: transparent; - background-image: none; // Remove CSS gradient - .reset-filter(); - cursor: @cursor-disabled; - } -} - -// Open state for the dropdown -.open { - // Show the menu - > .dropdown-menu { - display: block; - } - - // Remove the outline when :focus is triggered - > a { - outline: 0; - } -} - -// Menu positioning -// -// Add extra class to `.dropdown-menu` to flip the alignment of the dropdown -// menu with the parent. -.dropdown-menu-right { - left: auto; // Reset the default from `.dropdown-menu` - right: 0; -} -// With v3, we enabled auto-flipping if you have a dropdown within a right -// aligned nav component. To enable the undoing of that, we provide an override -// to restore the default dropdown menu alignment. -// -// This is only for left-aligning a dropdown menu within a `.navbar-right` or -// `.pull-right` nav component. -.dropdown-menu-left { - left: 0; - right: auto; -} - -// Dropdown section headers -.dropdown-header { - display: block; - padding: 3px 20px; - font-size: @font-size-small; - line-height: @line-height-base; - color: @dropdown-header-color; - white-space: nowrap; // as with > li > a -} - -// Backdrop to catch body clicks on mobile, etc. -.dropdown-backdrop { - position: fixed; - left: 0; - right: 0; - bottom: 0; - top: 0; - z-index: (@zindex-dropdown - 10); -} - -// Right aligned dropdowns -.pull-right > .dropdown-menu { - right: 0; - left: auto; -} - -// Allow for dropdowns to go bottom up (aka, dropup-menu) -// -// Just add .dropup after the standard .dropdown class and you're set, bro. -// TODO: abstract this so that the navbar fixed styles are not placed here? - -.dropup, -.navbar-fixed-bottom .dropdown { - // Reverse the caret - .caret { - border-top: 0; - border-bottom: @caret-width-base dashed; - border-bottom: @caret-width-base solid ~"\9"; // IE8 - content: ""; - } - // Different positioning for bottom up menu - .dropdown-menu { - top: auto; - bottom: 100%; - margin-bottom: 2px; - } -} - - -// Component alignment -// -// Reiterate per navbar.less and the modified component alignment there. - -@media (min-width: @grid-float-breakpoint) { - .navbar-right { - .dropdown-menu { - .dropdown-menu-right(); - } - // Necessary for overrides of the default right aligned menu. - // Will remove come v4 in all likelihood. - .dropdown-menu-left { - .dropdown-menu-left(); - } - } -} diff --git a/src/UI/Content/Bootstrap/forms.less b/src/UI/Content/Bootstrap/forms.less deleted file mode 100644 index b064ede46..000000000 --- a/src/UI/Content/Bootstrap/forms.less +++ /dev/null @@ -1,607 +0,0 @@ -// -// Forms -// -------------------------------------------------- - - -// Normalize non-controls -// -// Restyle and baseline non-control form elements. - -fieldset { - padding: 0; - margin: 0; - border: 0; - // Chrome and Firefox set a `min-width: min-content;` on fieldsets, - // so we reset that to ensure it behaves more like a standard block element. - // See https://github.com/twbs/bootstrap/issues/12359. - min-width: 0; -} - -legend { - display: block; - width: 100%; - padding: 0; - margin-bottom: @line-height-computed; - font-size: (@font-size-base * 1.5); - line-height: inherit; - color: @legend-color; - border: 0; - border-bottom: 1px solid @legend-border-color; -} - -label { - display: inline-block; - max-width: 100%; // Force IE8 to wrap long content (see https://github.com/twbs/bootstrap/issues/13141) - margin-bottom: 5px; - font-weight: bold; -} - - -// Normalize form controls -// -// While most of our form styles require extra classes, some basic normalization -// is required to ensure optimum display with or without those classes to better -// address browser inconsistencies. - -// Override content-box in Normalize (* isn't specific enough) -input[type="search"] { - .box-sizing(border-box); -} - -// Position radios and checkboxes better -input[type="radio"], -input[type="checkbox"] { - margin: 4px 0 0; - margin-top: 1px \9; // IE8-9 - line-height: normal; -} - -input[type="file"] { - display: block; -} - -// Make range inputs behave like textual form controls -input[type="range"] { - display: block; - width: 100%; -} - -// Make multiple select elements height not fixed -select[multiple], -select[size] { - height: auto; -} - -// Focus for file, radio, and checkbox -input[type="file"]:focus, -input[type="radio"]:focus, -input[type="checkbox"]:focus { - .tab-focus(); -} - -// Adjust output element -output { - display: block; - padding-top: (@padding-base-vertical + 1); - font-size: @font-size-base; - line-height: @line-height-base; - color: @input-color; -} - - -// Common form controls -// -// Shared size and type resets for form controls. Apply `.form-control` to any -// of the following form controls: -// -// select -// textarea -// input[type="text"] -// input[type="password"] -// input[type="datetime"] -// input[type="datetime-local"] -// input[type="date"] -// input[type="month"] -// input[type="time"] -// input[type="week"] -// input[type="number"] -// input[type="email"] -// input[type="url"] -// input[type="search"] -// input[type="tel"] -// input[type="color"] - -.form-control { - display: block; - width: 100%; - height: @input-height-base; // Make inputs at least the height of their button counterpart (base line-height + padding + border) - padding: @padding-base-vertical @padding-base-horizontal; - font-size: @font-size-base; - line-height: @line-height-base; - color: @input-color; - background-color: @input-bg; - background-image: none; // Reset unusual Firefox-on-Android default style; see https://github.com/necolas/normalize.css/issues/214 - border: 1px solid @input-border; - border-radius: @input-border-radius; // Note: This has no effect on <select>s in some browsers, due to the limited stylability of <select>s in CSS. - .box-shadow(inset 0 1px 1px rgba(0,0,0,.075)); - .transition(~"border-color ease-in-out .15s, box-shadow ease-in-out .15s"); - - // Customize the `:focus` state to imitate native WebKit styles. - .form-control-focus(); - - // Placeholder - .placeholder(); - - // Disabled and read-only inputs - // - // HTML5 says that controls under a fieldset > legend:first-child won't be - // disabled if the fieldset is disabled. Due to implementation difficulty, we - // don't honor that edge case; we style them as disabled anyway. - &[disabled], - &[readonly], - fieldset[disabled] & { - background-color: @input-bg-disabled; - opacity: 1; // iOS fix for unreadable disabled content; see https://github.com/twbs/bootstrap/issues/11655 - } - - &[disabled], - fieldset[disabled] & { - cursor: @cursor-disabled; - } - - // Reset height for `textarea`s - textarea& { - height: auto; - } -} - - -// Search inputs in iOS -// -// This overrides the extra rounded corners on search inputs in iOS so that our -// `.form-control` class can properly style them. Note that this cannot simply -// be added to `.form-control` as it's not specific enough. For details, see -// https://github.com/twbs/bootstrap/issues/11586. - -input[type="search"] { - -webkit-appearance: none; -} - - -// Special styles for iOS temporal inputs -// -// In Mobile Safari, setting `display: block` on temporal inputs causes the -// text within the input to become vertically misaligned. As a workaround, we -// set a pixel line-height that matches the given height of the input, but only -// for Safari. See https://bugs.webkit.org/show_bug.cgi?id=139848 -// -// Note that as of 8.3, iOS doesn't support `datetime` or `week`. - -@media screen and (-webkit-min-device-pixel-ratio: 0) { - input[type="date"], - input[type="time"], - input[type="datetime-local"], - input[type="month"] { - &.form-control { - line-height: @input-height-base; - } - - &.input-sm, - .input-group-sm & { - line-height: @input-height-small; - } - - &.input-lg, - .input-group-lg & { - line-height: @input-height-large; - } - } -} - - -// Form groups -// -// Designed to help with the organization and spacing of vertical forms. For -// horizontal forms, use the predefined grid classes. - -.form-group { - margin-bottom: @form-group-margin-bottom; -} - - -// Checkboxes and radios -// -// Indent the labels to position radios/checkboxes as hanging controls. - -.radio, -.checkbox { - position: relative; - display: block; - margin-top: 10px; - margin-bottom: 10px; - - label { - min-height: @line-height-computed; // Ensure the input doesn't jump when there is no text - padding-left: 20px; - margin-bottom: 0; - font-weight: normal; - cursor: pointer; - } -} -.radio input[type="radio"], -.radio-inline input[type="radio"], -.checkbox input[type="checkbox"], -.checkbox-inline input[type="checkbox"] { - position: absolute; - margin-left: -20px; - margin-top: 4px \9; -} - -.radio + .radio, -.checkbox + .checkbox { - margin-top: -5px; // Move up sibling radios or checkboxes for tighter spacing -} - -// Radios and checkboxes on same line -.radio-inline, -.checkbox-inline { - position: relative; - display: inline-block; - padding-left: 20px; - margin-bottom: 0; - vertical-align: middle; - font-weight: normal; - cursor: pointer; -} -.radio-inline + .radio-inline, -.checkbox-inline + .checkbox-inline { - margin-top: 0; - margin-left: 10px; // space out consecutive inline controls -} - -// Apply same disabled cursor tweak as for inputs -// Some special care is needed because <label>s don't inherit their parent's `cursor`. -// -// Note: Neither radios nor checkboxes can be readonly. -input[type="radio"], -input[type="checkbox"] { - &[disabled], - &.disabled, - fieldset[disabled] & { - cursor: @cursor-disabled; - } -} -// These classes are used directly on <label>s -.radio-inline, -.checkbox-inline { - &.disabled, - fieldset[disabled] & { - cursor: @cursor-disabled; - } -} -// These classes are used on elements with <label> descendants -.radio, -.checkbox { - &.disabled, - fieldset[disabled] & { - label { - cursor: @cursor-disabled; - } - } -} - - -// Static form control text -// -// Apply class to a `p` element to make any string of text align with labels in -// a horizontal form layout. - -.form-control-static { - // Size it appropriately next to real form controls - padding-top: (@padding-base-vertical + 1); - padding-bottom: (@padding-base-vertical + 1); - // Remove default margin from `p` - margin-bottom: 0; - min-height: (@line-height-computed + @font-size-base); - - &.input-lg, - &.input-sm { - padding-left: 0; - padding-right: 0; - } -} - - -// Form control sizing -// -// Build on `.form-control` with modifier classes to decrease or increase the -// height and font-size of form controls. -// -// The `.form-group-* form-control` variations are sadly duplicated to avoid the -// issue documented in https://github.com/twbs/bootstrap/issues/15074. - -.input-sm { - .input-size(@input-height-small; @padding-small-vertical; @padding-small-horizontal; @font-size-small; @line-height-small; @input-border-radius-small); -} -.form-group-sm { - .form-control { - height: @input-height-small; - padding: @padding-small-vertical @padding-small-horizontal; - font-size: @font-size-small; - line-height: @line-height-small; - border-radius: @input-border-radius-small; - } - select.form-control { - height: @input-height-small; - line-height: @input-height-small; - } - textarea.form-control, - select[multiple].form-control { - height: auto; - } - .form-control-static { - height: @input-height-small; - min-height: (@line-height-computed + @font-size-small); - padding: (@padding-small-vertical + 1) @padding-small-horizontal; - font-size: @font-size-small; - line-height: @line-height-small; - } -} - -.input-lg { - .input-size(@input-height-large; @padding-large-vertical; @padding-large-horizontal; @font-size-large; @line-height-large; @input-border-radius-large); -} -.form-group-lg { - .form-control { - height: @input-height-large; - padding: @padding-large-vertical @padding-large-horizontal; - font-size: @font-size-large; - line-height: @line-height-large; - border-radius: @input-border-radius-large; - } - select.form-control { - height: @input-height-large; - line-height: @input-height-large; - } - textarea.form-control, - select[multiple].form-control { - height: auto; - } - .form-control-static { - height: @input-height-large; - min-height: (@line-height-computed + @font-size-large); - padding: (@padding-large-vertical + 1) @padding-large-horizontal; - font-size: @font-size-large; - line-height: @line-height-large; - } -} - - -// Form control feedback states -// -// Apply contextual and semantic states to individual form controls. - -.has-feedback { - // Enable absolute positioning - position: relative; - - // Ensure icons don't overlap text - .form-control { - padding-right: (@input-height-base * 1.25); - } -} -// Feedback icon (requires .glyphicon classes) -.form-control-feedback { - position: absolute; - top: 0; - right: 0; - z-index: 2; // Ensure icon is above input groups - display: block; - width: @input-height-base; - height: @input-height-base; - line-height: @input-height-base; - text-align: center; - pointer-events: none; -} -.input-lg + .form-control-feedback, -.input-group-lg + .form-control-feedback, -.form-group-lg .form-control + .form-control-feedback { - width: @input-height-large; - height: @input-height-large; - line-height: @input-height-large; -} -.input-sm + .form-control-feedback, -.input-group-sm + .form-control-feedback, -.form-group-sm .form-control + .form-control-feedback { - width: @input-height-small; - height: @input-height-small; - line-height: @input-height-small; -} - -// Feedback states -.has-success { - .form-control-validation(@state-success-text; @state-success-text; @state-success-bg); -} -.has-warning { - .form-control-validation(@state-warning-text; @state-warning-text; @state-warning-bg); -} -.has-error { - .form-control-validation(@state-danger-text; @state-danger-text; @state-danger-bg); -} - -// Reposition feedback icon if input has visible label above -.has-feedback label { - - & ~ .form-control-feedback { - top: (@line-height-computed + 5); // Height of the `label` and its margin - } - &.sr-only ~ .form-control-feedback { - top: 0; - } -} - - -// Help text -// -// Apply to any element you wish to create light text for placement immediately -// below a form control. Use for general help, formatting, or instructional text. - -.help-block { - display: block; // account for any element using help-block - margin-top: 5px; - margin-bottom: 10px; - color: lighten(@text-color, 25%); // lighten the text some for contrast -} - - -// Inline forms -// -// Make forms appear inline(-block) by adding the `.form-inline` class. Inline -// forms begin stacked on extra small (mobile) devices and then go inline when -// viewports reach <768px. -// -// Requires wrapping inputs and labels with `.form-group` for proper display of -// default HTML form controls and our custom form controls (e.g., input groups). -// -// Heads up! This is mixin-ed into `.navbar-form` in navbars.less. - -.form-inline { - - // Kick in the inline - @media (min-width: @screen-sm-min) { - // Inline-block all the things for "inline" - .form-group { - display: inline-block; - margin-bottom: 0; - vertical-align: middle; - } - - // In navbar-form, allow folks to *not* use `.form-group` - .form-control { - display: inline-block; - width: auto; // Prevent labels from stacking above inputs in `.form-group` - vertical-align: middle; - } - - // Make static controls behave like regular ones - .form-control-static { - display: inline-block; - } - - .input-group { - display: inline-table; - vertical-align: middle; - - .input-group-addon, - .input-group-btn, - .form-control { - width: auto; - } - } - - // Input groups need that 100% width though - .input-group > .form-control { - width: 100%; - } - - .control-label { - margin-bottom: 0; - vertical-align: middle; - } - - // Remove default margin on radios/checkboxes that were used for stacking, and - // then undo the floating of radios and checkboxes to match. - .radio, - .checkbox { - display: inline-block; - margin-top: 0; - margin-bottom: 0; - vertical-align: middle; - - label { - padding-left: 0; - } - } - .radio input[type="radio"], - .checkbox input[type="checkbox"] { - position: relative; - margin-left: 0; - } - - // Re-override the feedback icon. - .has-feedback .form-control-feedback { - top: 0; - } - } -} - - -// Horizontal forms -// -// Horizontal forms are built on grid classes and allow you to create forms with -// labels on the left and inputs on the right. - -.form-horizontal { - - // Consistent vertical alignment of radios and checkboxes - // - // Labels also get some reset styles, but that is scoped to a media query below. - .radio, - .checkbox, - .radio-inline, - .checkbox-inline { - margin-top: 0; - margin-bottom: 0; - padding-top: (@padding-base-vertical + 1); // Default padding plus a border - } - // Account for padding we're adding to ensure the alignment and of help text - // and other content below items - .radio, - .checkbox { - min-height: (@line-height-computed + (@padding-base-vertical + 1)); - } - - // Make form groups behave like rows - .form-group { - .make-row(); - } - - // Reset spacing and right align labels, but scope to media queries so that - // labels on narrow viewports stack the same as a default form example. - @media (min-width: @screen-sm-min) { - .control-label { - text-align: right; - margin-bottom: 0; - padding-top: (@padding-base-vertical + 1); // Default padding plus a border - } - } - - // Validation states - // - // Reposition the icon because it's now within a grid column and columns have - // `position: relative;` on them. Also accounts for the grid gutter padding. - .has-feedback .form-control-feedback { - right: floor((@grid-gutter-width / 2)); - } - - // Form group sizes - // - // Quick utility class for applying `.input-lg` and `.input-sm` styles to the - // inputs and labels within a `.form-group`. - .form-group-lg { - @media (min-width: @screen-sm-min) { - .control-label { - padding-top: ((@padding-large-vertical * @line-height-large) + 1); - font-size: @font-size-large; - } - } - } - .form-group-sm { - @media (min-width: @screen-sm-min) { - .control-label { - padding-top: (@padding-small-vertical + 1); - font-size: @font-size-small; - } - } - } -} diff --git a/src/UI/Content/Bootstrap/glyphicons.less b/src/UI/Content/Bootstrap/glyphicons.less deleted file mode 100644 index 335d80aa6..000000000 --- a/src/UI/Content/Bootstrap/glyphicons.less +++ /dev/null @@ -1,305 +0,0 @@ -// -// Glyphicons for Bootstrap -// -// Since icons are fonts, they can be placed anywhere text is placed and are -// thus automatically sized to match the surrounding child. To use, create an -// inline element with the appropriate classes, like so: -// -// <a href="#"><span class="glyphicon glyphicon-star"></span> Star</a> - -// Import the fonts -@font-face { - font-family: 'Glyphicons Halflings'; - src: url('@{icon-font-path}@{icon-font-name}.eot'); - src: url('@{icon-font-path}@{icon-font-name}.eot?#iefix') format('embedded-opentype'), - url('@{icon-font-path}@{icon-font-name}.woff2') format('woff2'), - url('@{icon-font-path}@{icon-font-name}.woff') format('woff'), - url('@{icon-font-path}@{icon-font-name}.ttf') format('truetype'), - url('@{icon-font-path}@{icon-font-name}.svg#@{icon-font-svg-id}') format('svg'); -} - -// Catchall baseclass -.glyphicon { - position: relative; - top: 1px; - display: inline-block; - font-family: 'Glyphicons Halflings'; - font-style: normal; - font-weight: normal; - line-height: 1; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; -} - -// Individual icons -.glyphicon-asterisk { &:before { content: "\2a"; } } -.glyphicon-plus { &:before { content: "\2b"; } } -.glyphicon-euro, -.glyphicon-eur { &:before { content: "\20ac"; } } -.glyphicon-minus { &:before { content: "\2212"; } } -.glyphicon-cloud { &:before { content: "\2601"; } } -.glyphicon-envelope { &:before { content: "\2709"; } } -.glyphicon-pencil { &:before { content: "\270f"; } } -.glyphicon-glass { &:before { content: "\e001"; } } -.glyphicon-music { &:before { content: "\e002"; } } -.glyphicon-search { &:before { content: "\e003"; } } -.glyphicon-heart { &:before { content: "\e005"; } } -.glyphicon-star { &:before { content: "\e006"; } } -.glyphicon-star-empty { &:before { content: "\e007"; } } -.glyphicon-user { &:before { content: "\e008"; } } -.glyphicon-film { &:before { content: "\e009"; } } -.glyphicon-th-large { &:before { content: "\e010"; } } -.glyphicon-th { &:before { content: "\e011"; } } -.glyphicon-th-list { &:before { content: "\e012"; } } -.glyphicon-ok { &:before { content: "\e013"; } } -.glyphicon-remove { &:before { content: "\e014"; } } -.glyphicon-zoom-in { &:before { content: "\e015"; } } -.glyphicon-zoom-out { &:before { content: "\e016"; } } -.glyphicon-off { &:before { content: "\e017"; } } -.glyphicon-signal { &:before { content: "\e018"; } } -.glyphicon-cog { &:before { content: "\e019"; } } -.glyphicon-trash { &:before { content: "\e020"; } } -.glyphicon-home { &:before { content: "\e021"; } } -.glyphicon-file { &:before { content: "\e022"; } } -.glyphicon-time { &:before { content: "\e023"; } } -.glyphicon-road { &:before { content: "\e024"; } } -.glyphicon-download-alt { &:before { content: "\e025"; } } -.glyphicon-download { &:before { content: "\e026"; } } -.glyphicon-upload { &:before { content: "\e027"; } } -.glyphicon-inbox { &:before { content: "\e028"; } } -.glyphicon-play-circle { &:before { content: "\e029"; } } -.glyphicon-repeat { &:before { content: "\e030"; } } -.glyphicon-refresh { &:before { content: "\e031"; } } -.glyphicon-list-alt { &:before { content: "\e032"; } } -.glyphicon-lock { &:before { content: "\e033"; } } -.glyphicon-flag { &:before { content: "\e034"; } } -.glyphicon-headphones { &:before { content: "\e035"; } } -.glyphicon-volume-off { &:before { content: "\e036"; } } -.glyphicon-volume-down { &:before { content: "\e037"; } } -.glyphicon-volume-up { &:before { content: "\e038"; } } -.glyphicon-qrcode { &:before { content: "\e039"; } } -.glyphicon-barcode { &:before { content: "\e040"; } } -.glyphicon-tag { &:before { content: "\e041"; } } -.glyphicon-tags { &:before { content: "\e042"; } } -.glyphicon-book { &:before { content: "\e043"; } } -.glyphicon-bookmark { &:before { content: "\e044"; } } -.glyphicon-print { &:before { content: "\e045"; } } -.glyphicon-camera { &:before { content: "\e046"; } } -.glyphicon-font { &:before { content: "\e047"; } } -.glyphicon-bold { &:before { content: "\e048"; } } -.glyphicon-italic { &:before { content: "\e049"; } } -.glyphicon-text-height { &:before { content: "\e050"; } } -.glyphicon-text-width { &:before { content: "\e051"; } } -.glyphicon-align-left { &:before { content: "\e052"; } } -.glyphicon-align-center { &:before { content: "\e053"; } } -.glyphicon-align-right { &:before { content: "\e054"; } } -.glyphicon-align-justify { &:before { content: "\e055"; } } -.glyphicon-list { &:before { content: "\e056"; } } -.glyphicon-indent-left { &:before { content: "\e057"; } } -.glyphicon-indent-right { &:before { content: "\e058"; } } -.glyphicon-facetime-video { &:before { content: "\e059"; } } -.glyphicon-picture { &:before { content: "\e060"; } } -.glyphicon-map-marker { &:before { content: "\e062"; } } -.glyphicon-adjust { &:before { content: "\e063"; } } -.glyphicon-tint { &:before { content: "\e064"; } } -.glyphicon-edit { &:before { content: "\e065"; } } -.glyphicon-share { &:before { content: "\e066"; } } -.glyphicon-check { &:before { content: "\e067"; } } -.glyphicon-move { &:before { content: "\e068"; } } -.glyphicon-step-backward { &:before { content: "\e069"; } } -.glyphicon-fast-backward { &:before { content: "\e070"; } } -.glyphicon-backward { &:before { content: "\e071"; } } -.glyphicon-play { &:before { content: "\e072"; } } -.glyphicon-pause { &:before { content: "\e073"; } } -.glyphicon-stop { &:before { content: "\e074"; } } -.glyphicon-forward { &:before { content: "\e075"; } } -.glyphicon-fast-forward { &:before { content: "\e076"; } } -.glyphicon-step-forward { &:before { content: "\e077"; } } -.glyphicon-eject { &:before { content: "\e078"; } } -.glyphicon-chevron-left { &:before { content: "\e079"; } } -.glyphicon-chevron-right { &:before { content: "\e080"; } } -.glyphicon-plus-sign { &:before { content: "\e081"; } } -.glyphicon-minus-sign { &:before { content: "\e082"; } } -.glyphicon-remove-sign { &:before { content: "\e083"; } } -.glyphicon-ok-sign { &:before { content: "\e084"; } } -.glyphicon-question-sign { &:before { content: "\e085"; } } -.glyphicon-info-sign { &:before { content: "\e086"; } } -.glyphicon-screenshot { &:before { content: "\e087"; } } -.glyphicon-remove-circle { &:before { content: "\e088"; } } -.glyphicon-ok-circle { &:before { content: "\e089"; } } -.glyphicon-ban-circle { &:before { content: "\e090"; } } -.glyphicon-arrow-left { &:before { content: "\e091"; } } -.glyphicon-arrow-right { &:before { content: "\e092"; } } -.glyphicon-arrow-up { &:before { content: "\e093"; } } -.glyphicon-arrow-down { &:before { content: "\e094"; } } -.glyphicon-share-alt { &:before { content: "\e095"; } } -.glyphicon-resize-full { &:before { content: "\e096"; } } -.glyphicon-resize-small { &:before { content: "\e097"; } } -.glyphicon-exclamation-sign { &:before { content: "\e101"; } } -.glyphicon-gift { &:before { content: "\e102"; } } -.glyphicon-leaf { &:before { content: "\e103"; } } -.glyphicon-fire { &:before { content: "\e104"; } } -.glyphicon-eye-open { &:before { content: "\e105"; } } -.glyphicon-eye-close { &:before { content: "\e106"; } } -.glyphicon-warning-sign { &:before { content: "\e107"; } } -.glyphicon-plane { &:before { content: "\e108"; } } -.glyphicon-calendar { &:before { content: "\e109"; } } -.glyphicon-random { &:before { content: "\e110"; } } -.glyphicon-comment { &:before { content: "\e111"; } } -.glyphicon-magnet { &:before { content: "\e112"; } } -.glyphicon-chevron-up { &:before { content: "\e113"; } } -.glyphicon-chevron-down { &:before { content: "\e114"; } } -.glyphicon-retweet { &:before { content: "\e115"; } } -.glyphicon-shopping-cart { &:before { content: "\e116"; } } -.glyphicon-folder-close { &:before { content: "\e117"; } } -.glyphicon-folder-open { &:before { content: "\e118"; } } -.glyphicon-resize-vertical { &:before { content: "\e119"; } } -.glyphicon-resize-horizontal { &:before { content: "\e120"; } } -.glyphicon-hdd { &:before { content: "\e121"; } } -.glyphicon-bullhorn { &:before { content: "\e122"; } } -.glyphicon-bell { &:before { content: "\e123"; } } -.glyphicon-certificate { &:before { content: "\e124"; } } -.glyphicon-thumbs-up { &:before { content: "\e125"; } } -.glyphicon-thumbs-down { &:before { content: "\e126"; } } -.glyphicon-hand-right { &:before { content: "\e127"; } } -.glyphicon-hand-left { &:before { content: "\e128"; } } -.glyphicon-hand-up { &:before { content: "\e129"; } } -.glyphicon-hand-down { &:before { content: "\e130"; } } -.glyphicon-circle-arrow-right { &:before { content: "\e131"; } } -.glyphicon-circle-arrow-left { &:before { content: "\e132"; } } -.glyphicon-circle-arrow-up { &:before { content: "\e133"; } } -.glyphicon-circle-arrow-down { &:before { content: "\e134"; } } -.glyphicon-globe { &:before { content: "\e135"; } } -.glyphicon-wrench { &:before { content: "\e136"; } } -.glyphicon-tasks { &:before { content: "\e137"; } } -.glyphicon-filter { &:before { content: "\e138"; } } -.glyphicon-briefcase { &:before { content: "\e139"; } } -.glyphicon-fullscreen { &:before { content: "\e140"; } } -.glyphicon-dashboard { &:before { content: "\e141"; } } -.glyphicon-paperclip { &:before { content: "\e142"; } } -.glyphicon-heart-empty { &:before { content: "\e143"; } } -.glyphicon-link { &:before { content: "\e144"; } } -.glyphicon-phone { &:before { content: "\e145"; } } -.glyphicon-pushpin { &:before { content: "\e146"; } } -.glyphicon-usd { &:before { content: "\e148"; } } -.glyphicon-gbp { &:before { content: "\e149"; } } -.glyphicon-sort { &:before { content: "\e150"; } } -.glyphicon-sort-by-alphabet { &:before { content: "\e151"; } } -.glyphicon-sort-by-alphabet-alt { &:before { content: "\e152"; } } -.glyphicon-sort-by-order { &:before { content: "\e153"; } } -.glyphicon-sort-by-order-alt { &:before { content: "\e154"; } } -.glyphicon-sort-by-attributes { &:before { content: "\e155"; } } -.glyphicon-sort-by-attributes-alt { &:before { content: "\e156"; } } -.glyphicon-unchecked { &:before { content: "\e157"; } } -.glyphicon-expand { &:before { content: "\e158"; } } -.glyphicon-collapse-down { &:before { content: "\e159"; } } -.glyphicon-collapse-up { &:before { content: "\e160"; } } -.glyphicon-log-in { &:before { content: "\e161"; } } -.glyphicon-flash { &:before { content: "\e162"; } } -.glyphicon-log-out { &:before { content: "\e163"; } } -.glyphicon-new-window { &:before { content: "\e164"; } } -.glyphicon-record { &:before { content: "\e165"; } } -.glyphicon-save { &:before { content: "\e166"; } } -.glyphicon-open { &:before { content: "\e167"; } } -.glyphicon-saved { &:before { content: "\e168"; } } -.glyphicon-import { &:before { content: "\e169"; } } -.glyphicon-export { &:before { content: "\e170"; } } -.glyphicon-send { &:before { content: "\e171"; } } -.glyphicon-floppy-disk { &:before { content: "\e172"; } } -.glyphicon-floppy-saved { &:before { content: "\e173"; } } -.glyphicon-floppy-remove { &:before { content: "\e174"; } } -.glyphicon-floppy-save { &:before { content: "\e175"; } } -.glyphicon-floppy-open { &:before { content: "\e176"; } } -.glyphicon-credit-card { &:before { content: "\e177"; } } -.glyphicon-transfer { &:before { content: "\e178"; } } -.glyphicon-cutlery { &:before { content: "\e179"; } } -.glyphicon-header { &:before { content: "\e180"; } } -.glyphicon-compressed { &:before { content: "\e181"; } } -.glyphicon-earphone { &:before { content: "\e182"; } } -.glyphicon-phone-alt { &:before { content: "\e183"; } } -.glyphicon-tower { &:before { content: "\e184"; } } -.glyphicon-stats { &:before { content: "\e185"; } } -.glyphicon-sd-video { &:before { content: "\e186"; } } -.glyphicon-hd-video { &:before { content: "\e187"; } } -.glyphicon-subtitles { &:before { content: "\e188"; } } -.glyphicon-sound-stereo { &:before { content: "\e189"; } } -.glyphicon-sound-dolby { &:before { content: "\e190"; } } -.glyphicon-sound-5-1 { &:before { content: "\e191"; } } -.glyphicon-sound-6-1 { &:before { content: "\e192"; } } -.glyphicon-sound-7-1 { &:before { content: "\e193"; } } -.glyphicon-copyright-mark { &:before { content: "\e194"; } } -.glyphicon-registration-mark { &:before { content: "\e195"; } } -.glyphicon-cloud-download { &:before { content: "\e197"; } } -.glyphicon-cloud-upload { &:before { content: "\e198"; } } -.glyphicon-tree-conifer { &:before { content: "\e199"; } } -.glyphicon-tree-deciduous { &:before { content: "\e200"; } } -.glyphicon-cd { &:before { content: "\e201"; } } -.glyphicon-save-file { &:before { content: "\e202"; } } -.glyphicon-open-file { &:before { content: "\e203"; } } -.glyphicon-level-up { &:before { content: "\e204"; } } -.glyphicon-copy { &:before { content: "\e205"; } } -.glyphicon-paste { &:before { content: "\e206"; } } -// The following 2 Glyphicons are omitted for the time being because -// they currently use Unicode codepoints that are outside the -// Basic Multilingual Plane (BMP). Older buggy versions of WebKit can't handle -// non-BMP codepoints in CSS string escapes, and thus can't display these two icons. -// Notably, the bug affects some older versions of the Android Browser. -// More info: https://github.com/twbs/bootstrap/issues/10106 -// .glyphicon-door { &:before { content: "\1f6aa"; } } -// .glyphicon-key { &:before { content: "\1f511"; } } -.glyphicon-alert { &:before { content: "\e209"; } } -.glyphicon-equalizer { &:before { content: "\e210"; } } -.glyphicon-king { &:before { content: "\e211"; } } -.glyphicon-queen { &:before { content: "\e212"; } } -.glyphicon-pawn { &:before { content: "\e213"; } } -.glyphicon-bishop { &:before { content: "\e214"; } } -.glyphicon-knight { &:before { content: "\e215"; } } -.glyphicon-baby-formula { &:before { content: "\e216"; } } -.glyphicon-tent { &:before { content: "\26fa"; } } -.glyphicon-blackboard { &:before { content: "\e218"; } } -.glyphicon-bed { &:before { content: "\e219"; } } -.glyphicon-apple { &:before { content: "\f8ff"; } } -.glyphicon-erase { &:before { content: "\e221"; } } -.glyphicon-hourglass { &:before { content: "\231b"; } } -.glyphicon-lamp { &:before { content: "\e223"; } } -.glyphicon-duplicate { &:before { content: "\e224"; } } -.glyphicon-piggy-bank { &:before { content: "\e225"; } } -.glyphicon-scissors { &:before { content: "\e226"; } } -.glyphicon-bitcoin { &:before { content: "\e227"; } } -.glyphicon-btc { &:before { content: "\e227"; } } -.glyphicon-xbt { &:before { content: "\e227"; } } -.glyphicon-yen { &:before { content: "\00a5"; } } -.glyphicon-jpy { &:before { content: "\00a5"; } } -.glyphicon-ruble { &:before { content: "\20bd"; } } -.glyphicon-rub { &:before { content: "\20bd"; } } -.glyphicon-scale { &:before { content: "\e230"; } } -.glyphicon-ice-lolly { &:before { content: "\e231"; } } -.glyphicon-ice-lolly-tasted { &:before { content: "\e232"; } } -.glyphicon-education { &:before { content: "\e233"; } } -.glyphicon-option-horizontal { &:before { content: "\e234"; } } -.glyphicon-option-vertical { &:before { content: "\e235"; } } -.glyphicon-menu-hamburger { &:before { content: "\e236"; } } -.glyphicon-modal-window { &:before { content: "\e237"; } } -.glyphicon-oil { &:before { content: "\e238"; } } -.glyphicon-grain { &:before { content: "\e239"; } } -.glyphicon-sunglasses { &:before { content: "\e240"; } } -.glyphicon-text-size { &:before { content: "\e241"; } } -.glyphicon-text-color { &:before { content: "\e242"; } } -.glyphicon-text-background { &:before { content: "\e243"; } } -.glyphicon-object-align-top { &:before { content: "\e244"; } } -.glyphicon-object-align-bottom { &:before { content: "\e245"; } } -.glyphicon-object-align-horizontal{ &:before { content: "\e246"; } } -.glyphicon-object-align-left { &:before { content: "\e247"; } } -.glyphicon-object-align-vertical { &:before { content: "\e248"; } } -.glyphicon-object-align-right { &:before { content: "\e249"; } } -.glyphicon-triangle-right { &:before { content: "\e250"; } } -.glyphicon-triangle-left { &:before { content: "\e251"; } } -.glyphicon-triangle-bottom { &:before { content: "\e252"; } } -.glyphicon-triangle-top { &:before { content: "\e253"; } } -.glyphicon-console { &:before { content: "\e254"; } } -.glyphicon-superscript { &:before { content: "\e255"; } } -.glyphicon-subscript { &:before { content: "\e256"; } } -.glyphicon-menu-left { &:before { content: "\e257"; } } -.glyphicon-menu-right { &:before { content: "\e258"; } } -.glyphicon-menu-down { &:before { content: "\e259"; } } -.glyphicon-menu-up { &:before { content: "\e260"; } } diff --git a/src/UI/Content/Bootstrap/grid.less b/src/UI/Content/Bootstrap/grid.less deleted file mode 100644 index e100655b7..000000000 --- a/src/UI/Content/Bootstrap/grid.less +++ /dev/null @@ -1,84 +0,0 @@ -// -// Grid system -// -------------------------------------------------- - - -// Container widths -// -// Set the container width, and override it for fixed navbars in media queries. - -.container { - .container-fixed(); - - @media (min-width: @screen-sm-min) { - width: @container-sm; - } - @media (min-width: @screen-md-min) { - width: @container-md; - } - @media (min-width: @screen-lg-min) { - width: @container-lg; - } -} - - -// Fluid container -// -// Utilizes the mixin meant for fixed width containers, but without any defined -// width for fluid, full width layouts. - -.container-fluid { - .container-fixed(); -} - - -// Row -// -// Rows contain and clear the floats of your columns. - -.row { - .make-row(); -} - - -// Columns -// -// Common styles for small and large grid columns - -.make-grid-columns(); - - -// Extra small grid -// -// Columns, offsets, pushes, and pulls for extra small devices like -// smartphones. - -.make-grid(xs); - - -// Small grid -// -// Columns, offsets, pushes, and pulls for the small device range, from phones -// to tablets. - -@media (min-width: @screen-sm-min) { - .make-grid(sm); -} - - -// Medium grid -// -// Columns, offsets, pushes, and pulls for the desktop device range. - -@media (min-width: @screen-md-min) { - .make-grid(md); -} - - -// Large grid -// -// Columns, offsets, pushes, and pulls for the large desktop device range. - -@media (min-width: @screen-lg-min) { - .make-grid(lg); -} diff --git a/src/UI/Content/Bootstrap/input-groups.less b/src/UI/Content/Bootstrap/input-groups.less deleted file mode 100644 index 457ea60ba..000000000 --- a/src/UI/Content/Bootstrap/input-groups.less +++ /dev/null @@ -1,167 +0,0 @@ -// -// Input groups -// -------------------------------------------------- - -// Base styles -// ------------------------- -.input-group { - position: relative; // For dropdowns - display: table; - border-collapse: separate; // prevent input groups from inheriting border styles from table cells when placed within a table - - // Undo padding and float of grid classes - &[class*="col-"] { - float: none; - padding-left: 0; - padding-right: 0; - } - - .form-control { - // Ensure that the input is always above the *appended* addon button for - // proper border colors. - position: relative; - z-index: 2; - - // IE9 fubars the placeholder attribute in text inputs and the arrows on - // select elements in input groups. To fix it, we float the input. Details: - // https://github.com/twbs/bootstrap/issues/11561#issuecomment-28936855 - float: left; - - width: 100%; - margin-bottom: 0; - } -} - -// Sizing options -// -// Remix the default form control sizing classes into new ones for easier -// manipulation. - -.input-group-lg > .form-control, -.input-group-lg > .input-group-addon, -.input-group-lg > .input-group-btn > .btn { - .input-lg(); -} -.input-group-sm > .form-control, -.input-group-sm > .input-group-addon, -.input-group-sm > .input-group-btn > .btn { - .input-sm(); -} - - -// Display as table-cell -// ------------------------- -.input-group-addon, -.input-group-btn, -.input-group .form-control { - display: table-cell; - - &:not(:first-child):not(:last-child) { - border-radius: 0; - } -} -// Addon and addon wrapper for buttons -.input-group-addon, -.input-group-btn { - width: 1%; - white-space: nowrap; - vertical-align: middle; // Match the inputs -} - -// Text input groups -// ------------------------- -.input-group-addon { - padding: @padding-base-vertical @padding-base-horizontal; - font-size: @font-size-base; - font-weight: normal; - line-height: 1; - color: @input-color; - text-align: center; - background-color: @input-group-addon-bg; - border: 1px solid @input-group-addon-border-color; - border-radius: @border-radius-base; - - // Sizing - &.input-sm { - padding: @padding-small-vertical @padding-small-horizontal; - font-size: @font-size-small; - border-radius: @border-radius-small; - } - &.input-lg { - padding: @padding-large-vertical @padding-large-horizontal; - font-size: @font-size-large; - border-radius: @border-radius-large; - } - - // Nuke default margins from checkboxes and radios to vertically center within. - input[type="radio"], - input[type="checkbox"] { - margin-top: 0; - } -} - -// Reset rounded corners -.input-group .form-control:first-child, -.input-group-addon:first-child, -.input-group-btn:first-child > .btn, -.input-group-btn:first-child > .btn-group > .btn, -.input-group-btn:first-child > .dropdown-toggle, -.input-group-btn:last-child > .btn:not(:last-child):not(.dropdown-toggle), -.input-group-btn:last-child > .btn-group:not(:last-child) > .btn { - .border-right-radius(0); -} -.input-group-addon:first-child { - border-right: 0; -} -.input-group .form-control:last-child, -.input-group-addon:last-child, -.input-group-btn:last-child > .btn, -.input-group-btn:last-child > .btn-group > .btn, -.input-group-btn:last-child > .dropdown-toggle, -.input-group-btn:first-child > .btn:not(:first-child), -.input-group-btn:first-child > .btn-group:not(:first-child) > .btn { - .border-left-radius(0); -} -.input-group-addon:last-child { - border-left: 0; -} - -// Button input groups -// ------------------------- -.input-group-btn { - position: relative; - // Jankily prevent input button groups from wrapping with `white-space` and - // `font-size` in combination with `inline-block` on buttons. - font-size: 0; - white-space: nowrap; - - // Negative margin for spacing, position for bringing hovered/focused/actived - // element above the siblings. - > .btn { - position: relative; - + .btn { - margin-left: -1px; - } - // Bring the "active" button to the front - &:hover, - &:focus, - &:active { - z-index: 2; - } - } - - // Negative margin to only have a 1px border between the two - &:first-child { - > .btn, - > .btn-group { - margin-right: -1px; - } - } - &:last-child { - > .btn, - > .btn-group { - z-index: 2; - margin-left: -1px; - } - } -} diff --git a/src/UI/Content/Bootstrap/jumbotron.less b/src/UI/Content/Bootstrap/jumbotron.less deleted file mode 100644 index fa80a38c6..000000000 --- a/src/UI/Content/Bootstrap/jumbotron.less +++ /dev/null @@ -1,52 +0,0 @@ -// -// Jumbotron -// -------------------------------------------------- - - -.jumbotron { - padding-top: @jumbotron-padding; - padding-bottom: @jumbotron-padding; - margin-bottom: @jumbotron-padding; - color: @jumbotron-color; - background-color: @jumbotron-bg; - - h1, - .h1 { - color: @jumbotron-heading-color; - } - - p { - margin-bottom: (@jumbotron-padding / 2); - font-size: @jumbotron-font-size; - font-weight: 200; - } - - > hr { - border-top-color: darken(@jumbotron-bg, 10%); - } - - .container &, - .container-fluid & { - border-radius: @border-radius-large; // Only round corners at higher resolutions if contained in a container - } - - .container { - max-width: 100%; - } - - @media screen and (min-width: @screen-sm-min) { - padding-top: (@jumbotron-padding * 1.6); - padding-bottom: (@jumbotron-padding * 1.6); - - .container &, - .container-fluid & { - padding-left: (@jumbotron-padding * 2); - padding-right: (@jumbotron-padding * 2); - } - - h1, - .h1 { - font-size: @jumbotron-heading-font-size; - } - } -} diff --git a/src/UI/Content/Bootstrap/labels.less b/src/UI/Content/Bootstrap/labels.less deleted file mode 100644 index 9a5a27006..000000000 --- a/src/UI/Content/Bootstrap/labels.less +++ /dev/null @@ -1,64 +0,0 @@ -// -// Labels -// -------------------------------------------------- - -.label { - display: inline; - padding: .2em .6em .3em; - font-size: 75%; - font-weight: bold; - line-height: 1; - color: @label-color; - text-align: center; - white-space: nowrap; - vertical-align: baseline; - border-radius: .25em; - - // Add hover effects, but only for links - a& { - &:hover, - &:focus { - color: @label-link-hover-color; - text-decoration: none; - cursor: pointer; - } - } - - // Empty labels collapse automatically (not available in IE8) - &:empty { - display: none; - } - - // Quick fix for labels in buttons - .btn & { - position: relative; - top: -1px; - } -} - -// Colors -// Contextual variations (linked labels get darker on :hover) - -.label-default { - .label-variant(@label-default-bg); -} - -.label-primary { - .label-variant(@label-primary-bg); -} - -.label-success { - .label-variant(@label-success-bg); -} - -.label-info { - .label-variant(@label-info-bg); -} - -.label-warning { - .label-variant(@label-warning-bg); -} - -.label-danger { - .label-variant(@label-danger-bg); -} diff --git a/src/UI/Content/Bootstrap/list-group.less b/src/UI/Content/Bootstrap/list-group.less deleted file mode 100644 index 216b91230..000000000 --- a/src/UI/Content/Bootstrap/list-group.less +++ /dev/null @@ -1,130 +0,0 @@ -// -// List groups -// -------------------------------------------------- - - -// Base class -// -// Easily usable on <ul>, <ol>, or <div>. - -.list-group { - // No need to set list-style: none; since .list-group-item is block level - margin-bottom: 20px; - padding-left: 0; // reset padding because ul and ol -} - - -// Individual list items -// -// Use on `li`s or `div`s within the `.list-group` parent. - -.list-group-item { - position: relative; - display: block; - padding: 10px 15px; - // Place the border on the list items and negative margin up for better styling - margin-bottom: -1px; - background-color: @list-group-bg; - border: 1px solid @list-group-border; - - // Round the first and last items - &:first-child { - .border-top-radius(@list-group-border-radius); - } - &:last-child { - margin-bottom: 0; - .border-bottom-radius(@list-group-border-radius); - } -} - - -// Interactive list items -// -// Use anchor or button elements instead of `li`s or `div`s to create interactive items. -// Includes an extra `.active` modifier class for showing selected items. - -a.list-group-item, -button.list-group-item { - color: @list-group-link-color; - - .list-group-item-heading { - color: @list-group-link-heading-color; - } - - // Hover state - &:hover, - &:focus { - text-decoration: none; - color: @list-group-link-hover-color; - background-color: @list-group-hover-bg; - } -} - -button.list-group-item { - width: 100%; - text-align: left; -} - -.list-group-item { - // Disabled state - &.disabled, - &.disabled:hover, - &.disabled:focus { - background-color: @list-group-disabled-bg; - color: @list-group-disabled-color; - cursor: @cursor-disabled; - - // Force color to inherit for custom content - .list-group-item-heading { - color: inherit; - } - .list-group-item-text { - color: @list-group-disabled-text-color; - } - } - - // Active class on item itself, not parent - &.active, - &.active:hover, - &.active:focus { - z-index: 2; // Place active items above their siblings for proper border styling - color: @list-group-active-color; - background-color: @list-group-active-bg; - border-color: @list-group-active-border; - - // Force color to inherit for custom content - .list-group-item-heading, - .list-group-item-heading > small, - .list-group-item-heading > .small { - color: inherit; - } - .list-group-item-text { - color: @list-group-active-text-color; - } - } -} - - -// Contextual variants -// -// Add modifier classes to change text and background color on individual items. -// Organizationally, this must come after the `:hover` states. - -.list-group-item-variant(success; @state-success-bg; @state-success-text); -.list-group-item-variant(info; @state-info-bg; @state-info-text); -.list-group-item-variant(warning; @state-warning-bg; @state-warning-text); -.list-group-item-variant(danger; @state-danger-bg; @state-danger-text); - - -// Custom content options -// -// Extra classes for creating well-formatted content within `.list-group-item`s. - -.list-group-item-heading { - margin-top: 0; - margin-bottom: 5px; -} -.list-group-item-text { - margin-bottom: 0; - line-height: 1.3; -} diff --git a/src/UI/Content/Bootstrap/media.less b/src/UI/Content/Bootstrap/media.less deleted file mode 100644 index 8c835e861..000000000 --- a/src/UI/Content/Bootstrap/media.less +++ /dev/null @@ -1,66 +0,0 @@ -.media { - // Proper spacing between instances of .media - margin-top: 15px; - - &:first-child { - margin-top: 0; - } -} - -.media, -.media-body { - zoom: 1; - overflow: hidden; -} - -.media-body { - width: 10000px; -} - -.media-object { - display: block; - - // Fix collapse in webkit from max-width: 100% and display: table-cell. - &.img-thumbnail { - max-width: none; - } -} - -.media-right, -.media > .pull-right { - padding-left: 10px; -} - -.media-left, -.media > .pull-left { - padding-right: 10px; -} - -.media-left, -.media-right, -.media-body { - display: table-cell; - vertical-align: top; -} - -.media-middle { - vertical-align: middle; -} - -.media-bottom { - vertical-align: bottom; -} - -// Reset margins on headings for tighter default spacing -.media-heading { - margin-top: 0; - margin-bottom: 5px; -} - -// Media list variation -// -// Undo default ul/ol styles -.media-list { - padding-left: 0; - list-style: none; -} diff --git a/src/UI/Content/Bootstrap/mixins.less b/src/UI/Content/Bootstrap/mixins.less deleted file mode 100644 index e6f9fe684..000000000 --- a/src/UI/Content/Bootstrap/mixins.less +++ /dev/null @@ -1,40 +0,0 @@ -// Mixins -// -------------------------------------------------- - -// Utilities -@import "mixins/hide-text.less"; -@import "mixins/opacity.less"; -@import "mixins/image.less"; -@import "mixins/labels.less"; -@import "mixins/reset-filter.less"; -@import "mixins/resize.less"; -@import "mixins/responsive-visibility.less"; -@import "mixins/size.less"; -@import "mixins/tab-focus.less"; -@import "mixins/reset-text.less"; -@import "mixins/text-emphasis.less"; -@import "mixins/text-overflow.less"; -@import "mixins/vendor-prefixes.less"; - -// Components -@import "mixins/alerts.less"; -@import "mixins/buttons.less"; -@import "mixins/panels.less"; -@import "mixins/pagination.less"; -@import "mixins/list-group.less"; -@import "mixins/nav-divider.less"; -@import "mixins/forms.less"; -@import "mixins/progress-bar.less"; -@import "mixins/table-row.less"; - -// Skins -@import "mixins/background-variant.less"; -@import "mixins/border-radius.less"; -@import "mixins/gradients.less"; - -// Layout -@import "mixins/clearfix.less"; -@import "mixins/center-block.less"; -@import "mixins/nav-vertical-align.less"; -@import "mixins/grid-framework.less"; -@import "mixins/grid.less"; diff --git a/src/UI/Content/Bootstrap/mixins/alerts.less b/src/UI/Content/Bootstrap/mixins/alerts.less deleted file mode 100644 index 396196f43..000000000 --- a/src/UI/Content/Bootstrap/mixins/alerts.less +++ /dev/null @@ -1,14 +0,0 @@ -// Alerts - -.alert-variant(@background; @border; @text-color) { - background-color: @background; - border-color: @border; - color: @text-color; - - hr { - border-top-color: darken(@border, 5%); - } - .alert-link { - color: darken(@text-color, 10%); - } -} diff --git a/src/UI/Content/Bootstrap/mixins/background-variant.less b/src/UI/Content/Bootstrap/mixins/background-variant.less deleted file mode 100644 index a85c22b74..000000000 --- a/src/UI/Content/Bootstrap/mixins/background-variant.less +++ /dev/null @@ -1,9 +0,0 @@ -// Contextual backgrounds - -.bg-variant(@color) { - background-color: @color; - a&:hover, - a&:focus { - background-color: darken(@color, 10%); - } -} diff --git a/src/UI/Content/Bootstrap/mixins/border-radius.less b/src/UI/Content/Bootstrap/mixins/border-radius.less deleted file mode 100644 index ca05dbf45..000000000 --- a/src/UI/Content/Bootstrap/mixins/border-radius.less +++ /dev/null @@ -1,18 +0,0 @@ -// Single side border-radius - -.border-top-radius(@radius) { - border-top-right-radius: @radius; - border-top-left-radius: @radius; -} -.border-right-radius(@radius) { - border-bottom-right-radius: @radius; - border-top-right-radius: @radius; -} -.border-bottom-radius(@radius) { - border-bottom-right-radius: @radius; - border-bottom-left-radius: @radius; -} -.border-left-radius(@radius) { - border-bottom-left-radius: @radius; - border-top-left-radius: @radius; -} diff --git a/src/UI/Content/Bootstrap/mixins/buttons.less b/src/UI/Content/Bootstrap/mixins/buttons.less deleted file mode 100644 index 6875a97c8..000000000 --- a/src/UI/Content/Bootstrap/mixins/buttons.less +++ /dev/null @@ -1,68 +0,0 @@ -// Button variants -// -// Easily pump out default styles, as well as :hover, :focus, :active, -// and disabled options for all buttons - -.button-variant(@color; @background; @border) { - color: @color; - background-color: @background; - border-color: @border; - - &:focus, - &.focus { - color: @color; - background-color: darken(@background, 10%); - border-color: darken(@border, 25%); - } - &:hover { - color: @color; - background-color: darken(@background, 10%); - border-color: darken(@border, 12%); - } - &:active, - &.active, - .open > .dropdown-toggle& { - color: @color; - background-color: darken(@background, 10%); - border-color: darken(@border, 12%); - - &:hover, - &:focus, - &.focus { - color: @color; - background-color: darken(@background, 17%); - border-color: darken(@border, 25%); - } - } - &:active, - &.active, - .open > .dropdown-toggle& { - background-image: none; - } - &.disabled, - &[disabled], - fieldset[disabled] & { - &, - &:hover, - &:focus, - &.focus, - &:active, - &.active { - background-color: @background; - border-color: @border; - } - } - - .badge { - color: @background; - background-color: @color; - } -} - -// Button sizes -.button-size(@padding-vertical; @padding-horizontal; @font-size; @line-height; @border-radius) { - padding: @padding-vertical @padding-horizontal; - font-size: @font-size; - line-height: @line-height; - border-radius: @border-radius; -} diff --git a/src/UI/Content/Bootstrap/mixins/center-block.less b/src/UI/Content/Bootstrap/mixins/center-block.less deleted file mode 100644 index d18d6de9e..000000000 --- a/src/UI/Content/Bootstrap/mixins/center-block.less +++ /dev/null @@ -1,7 +0,0 @@ -// Center-align a block level element - -.center-block() { - display: block; - margin-left: auto; - margin-right: auto; -} diff --git a/src/UI/Content/Bootstrap/mixins/clearfix.less b/src/UI/Content/Bootstrap/mixins/clearfix.less deleted file mode 100644 index 3f7a3820c..000000000 --- a/src/UI/Content/Bootstrap/mixins/clearfix.less +++ /dev/null @@ -1,22 +0,0 @@ -// Clearfix -// -// For modern browsers -// 1. The space content is one way to avoid an Opera bug when the -// contenteditable attribute is included anywhere else in the document. -// Otherwise it causes space to appear at the top and bottom of elements -// that are clearfixed. -// 2. The use of `table` rather than `block` is only necessary if using -// `:before` to contain the top-margins of child elements. -// -// Source: http://nicolasgallagher.com/micro-clearfix-hack/ - -.clearfix() { - &:before, - &:after { - content: " "; // 1 - display: table; // 2 - } - &:after { - clear: both; - } -} diff --git a/src/UI/Content/Bootstrap/mixins/forms.less b/src/UI/Content/Bootstrap/mixins/forms.less deleted file mode 100644 index 6f55ed967..000000000 --- a/src/UI/Content/Bootstrap/mixins/forms.less +++ /dev/null @@ -1,85 +0,0 @@ -// Form validation states -// -// Used in forms.less to generate the form validation CSS for warnings, errors, -// and successes. - -.form-control-validation(@text-color: #555; @border-color: #ccc; @background-color: #f5f5f5) { - // Color the label and help text - .help-block, - .control-label, - .radio, - .checkbox, - .radio-inline, - .checkbox-inline, - &.radio label, - &.checkbox label, - &.radio-inline label, - &.checkbox-inline label { - color: @text-color; - } - // Set the border and box shadow on specific inputs to match - .form-control { - border-color: @border-color; - .box-shadow(inset 0 1px 1px rgba(0,0,0,.075)); // Redeclare so transitions work - &:focus { - border-color: darken(@border-color, 10%); - @shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 6px lighten(@border-color, 20%); - .box-shadow(@shadow); - } - } - // Set validation states also for addons - .input-group-addon { - color: @text-color; - border-color: @border-color; - background-color: @background-color; - } - // Optional feedback icon - .form-control-feedback { - color: @text-color; - } -} - - -// Form control focus state -// -// Generate a customized focus state and for any input with the specified color, -// which defaults to the `@input-border-focus` variable. -// -// We highly encourage you to not customize the default value, but instead use -// this to tweak colors on an as-needed basis. This aesthetic change is based on -// WebKit's default styles, but applicable to a wider range of browsers. Its -// usability and accessibility should be taken into account with any change. -// -// Example usage: change the default blue border and shadow to white for better -// contrast against a dark gray background. -.form-control-focus(@color: @input-border-focus) { - @color-rgba: rgba(red(@color), green(@color), blue(@color), .6); - &:focus { - border-color: @color; - outline: 0; - .box-shadow(~"inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px @{color-rgba}"); - } -} - -// Form control sizing -// -// Relative text size, padding, and border-radii changes for form controls. For -// horizontal sizing, wrap controls in the predefined grid classes. `<select>` -// element gets special love because it's special, and that's a fact! -.input-size(@input-height; @padding-vertical; @padding-horizontal; @font-size; @line-height; @border-radius) { - height: @input-height; - padding: @padding-vertical @padding-horizontal; - font-size: @font-size; - line-height: @line-height; - border-radius: @border-radius; - - select& { - height: @input-height; - line-height: @input-height; - } - - textarea&, - select[multiple]& { - height: auto; - } -} diff --git a/src/UI/Content/Bootstrap/mixins/gradients.less b/src/UI/Content/Bootstrap/mixins/gradients.less deleted file mode 100644 index 0b88a89cc..000000000 --- a/src/UI/Content/Bootstrap/mixins/gradients.less +++ /dev/null @@ -1,59 +0,0 @@ -// Gradients - -#gradient { - - // Horizontal gradient, from left to right - // - // Creates two color stops, start and end, by specifying a color and position for each color stop. - // Color stops are not available in IE9 and below. - .horizontal(@start-color: #555; @end-color: #333; @start-percent: 0%; @end-percent: 100%) { - background-image: -webkit-linear-gradient(left, @start-color @start-percent, @end-color @end-percent); // Safari 5.1-6, Chrome 10+ - background-image: -o-linear-gradient(left, @start-color @start-percent, @end-color @end-percent); // Opera 12 - background-image: linear-gradient(to right, @start-color @start-percent, @end-color @end-percent); // Standard, IE10, Firefox 16+, Opera 12.10+, Safari 7+, Chrome 26+ - background-repeat: repeat-x; - filter: e(%("progid:DXImageTransform.Microsoft.gradient(startColorstr='%d', endColorstr='%d', GradientType=1)",argb(@start-color),argb(@end-color))); // IE9 and down - } - - // Vertical gradient, from top to bottom - // - // Creates two color stops, start and end, by specifying a color and position for each color stop. - // Color stops are not available in IE9 and below. - .vertical(@start-color: #555; @end-color: #333; @start-percent: 0%; @end-percent: 100%) { - background-image: -webkit-linear-gradient(top, @start-color @start-percent, @end-color @end-percent); // Safari 5.1-6, Chrome 10+ - background-image: -o-linear-gradient(top, @start-color @start-percent, @end-color @end-percent); // Opera 12 - background-image: linear-gradient(to bottom, @start-color @start-percent, @end-color @end-percent); // Standard, IE10, Firefox 16+, Opera 12.10+, Safari 7+, Chrome 26+ - background-repeat: repeat-x; - filter: e(%("progid:DXImageTransform.Microsoft.gradient(startColorstr='%d', endColorstr='%d', GradientType=0)",argb(@start-color),argb(@end-color))); // IE9 and down - } - - .directional(@start-color: #555; @end-color: #333; @deg: 45deg) { - background-repeat: repeat-x; - background-image: -webkit-linear-gradient(@deg, @start-color, @end-color); // Safari 5.1-6, Chrome 10+ - background-image: -o-linear-gradient(@deg, @start-color, @end-color); // Opera 12 - background-image: linear-gradient(@deg, @start-color, @end-color); // Standard, IE10, Firefox 16+, Opera 12.10+, Safari 7+, Chrome 26+ - } - .horizontal-three-colors(@start-color: #00b3ee; @mid-color: #7a43b6; @color-stop: 50%; @end-color: #c3325f) { - background-image: -webkit-linear-gradient(left, @start-color, @mid-color @color-stop, @end-color); - background-image: -o-linear-gradient(left, @start-color, @mid-color @color-stop, @end-color); - background-image: linear-gradient(to right, @start-color, @mid-color @color-stop, @end-color); - background-repeat: no-repeat; - filter: e(%("progid:DXImageTransform.Microsoft.gradient(startColorstr='%d', endColorstr='%d', GradientType=1)",argb(@start-color),argb(@end-color))); // IE9 and down, gets no color-stop at all for proper fallback - } - .vertical-three-colors(@start-color: #00b3ee; @mid-color: #7a43b6; @color-stop: 50%; @end-color: #c3325f) { - background-image: -webkit-linear-gradient(@start-color, @mid-color @color-stop, @end-color); - background-image: -o-linear-gradient(@start-color, @mid-color @color-stop, @end-color); - background-image: linear-gradient(@start-color, @mid-color @color-stop, @end-color); - background-repeat: no-repeat; - filter: e(%("progid:DXImageTransform.Microsoft.gradient(startColorstr='%d', endColorstr='%d', GradientType=0)",argb(@start-color),argb(@end-color))); // IE9 and down, gets no color-stop at all for proper fallback - } - .radial(@inner-color: #555; @outer-color: #333) { - background-image: -webkit-radial-gradient(circle, @inner-color, @outer-color); - background-image: radial-gradient(circle, @inner-color, @outer-color); - background-repeat: no-repeat; - } - .striped(@color: rgba(255,255,255,.15); @angle: 45deg) { - background-image: -webkit-linear-gradient(@angle, @color 25%, transparent 25%, transparent 50%, @color 50%, @color 75%, transparent 75%, transparent); - background-image: -o-linear-gradient(@angle, @color 25%, transparent 25%, transparent 50%, @color 50%, @color 75%, transparent 75%, transparent); - background-image: linear-gradient(@angle, @color 25%, transparent 25%, transparent 50%, @color 50%, @color 75%, transparent 75%, transparent); - } -} diff --git a/src/UI/Content/Bootstrap/mixins/grid-framework.less b/src/UI/Content/Bootstrap/mixins/grid-framework.less deleted file mode 100644 index 8c23eed24..000000000 --- a/src/UI/Content/Bootstrap/mixins/grid-framework.less +++ /dev/null @@ -1,91 +0,0 @@ -// Framework grid generation -// -// Used only by Bootstrap to generate the correct number of grid classes given -// any value of `@grid-columns`. - -.make-grid-columns() { - // Common styles for all sizes of grid columns, widths 1-12 - .col(@index) { // initial - @item: ~".col-xs-@{index}, .col-sm-@{index}, .col-md-@{index}, .col-lg-@{index}"; - .col((@index + 1), @item); - } - .col(@index, @list) when (@index =< @grid-columns) { // general; "=<" isn't a typo - @item: ~".col-xs-@{index}, .col-sm-@{index}, .col-md-@{index}, .col-lg-@{index}"; - .col((@index + 1), ~"@{list}, @{item}"); - } - .col(@index, @list) when (@index > @grid-columns) { // terminal - @{list} { - position: relative; - // Prevent columns from collapsing when empty - min-height: 1px; - // Inner gutter via padding - padding-left: ceil((@grid-gutter-width / 2)); - padding-right: floor((@grid-gutter-width / 2)); - } - } - .col(1); // kickstart it -} - -.float-grid-columns(@class) { - .col(@index) { // initial - @item: ~".col-@{class}-@{index}"; - .col((@index + 1), @item); - } - .col(@index, @list) when (@index =< @grid-columns) { // general - @item: ~".col-@{class}-@{index}"; - .col((@index + 1), ~"@{list}, @{item}"); - } - .col(@index, @list) when (@index > @grid-columns) { // terminal - @{list} { - float: left; - } - } - .col(1); // kickstart it -} - -.calc-grid-column(@index, @class, @type) when (@type = width) and (@index > 0) { - .col-@{class}-@{index} { - width: percentage((@index / @grid-columns)); - } -} -.calc-grid-column(@index, @class, @type) when (@type = push) and (@index > 0) { - .col-@{class}-push-@{index} { - left: percentage((@index / @grid-columns)); - } -} -.calc-grid-column(@index, @class, @type) when (@type = push) and (@index = 0) { - .col-@{class}-push-0 { - left: auto; - } -} -.calc-grid-column(@index, @class, @type) when (@type = pull) and (@index > 0) { - .col-@{class}-pull-@{index} { - right: percentage((@index / @grid-columns)); - } -} -.calc-grid-column(@index, @class, @type) when (@type = pull) and (@index = 0) { - .col-@{class}-pull-0 { - right: auto; - } -} -.calc-grid-column(@index, @class, @type) when (@type = offset) { - .col-@{class}-offset-@{index} { - margin-left: percentage((@index / @grid-columns)); - } -} - -// Basic looping in LESS -.loop-grid-columns(@index, @class, @type) when (@index >= 0) { - .calc-grid-column(@index, @class, @type); - // next iteration - .loop-grid-columns((@index - 1), @class, @type); -} - -// Create grid for specific class -.make-grid(@class) { - .float-grid-columns(@class); - .loop-grid-columns(@grid-columns, @class, width); - .loop-grid-columns(@grid-columns, @class, pull); - .loop-grid-columns(@grid-columns, @class, push); - .loop-grid-columns(@grid-columns, @class, offset); -} diff --git a/src/UI/Content/Bootstrap/mixins/grid.less b/src/UI/Content/Bootstrap/mixins/grid.less deleted file mode 100644 index f144c15f4..000000000 --- a/src/UI/Content/Bootstrap/mixins/grid.less +++ /dev/null @@ -1,122 +0,0 @@ -// Grid system -// -// Generate semantic grid columns with these mixins. - -// Centered container element -.container-fixed(@gutter: @grid-gutter-width) { - margin-right: auto; - margin-left: auto; - padding-left: (@gutter / 2); - padding-right: (@gutter / 2); - &:extend(.clearfix all); -} - -// Creates a wrapper for a series of columns -.make-row(@gutter: @grid-gutter-width) { - margin-left: ceil((@gutter / -2)); - margin-right: floor((@gutter / -2)); - &:extend(.clearfix all); -} - -// Generate the extra small columns -.make-xs-column(@columns; @gutter: @grid-gutter-width) { - position: relative; - float: left; - width: percentage((@columns / @grid-columns)); - min-height: 1px; - padding-left: (@gutter / 2); - padding-right: (@gutter / 2); -} -.make-xs-column-offset(@columns) { - margin-left: percentage((@columns / @grid-columns)); -} -.make-xs-column-push(@columns) { - left: percentage((@columns / @grid-columns)); -} -.make-xs-column-pull(@columns) { - right: percentage((@columns / @grid-columns)); -} - -// Generate the small columns -.make-sm-column(@columns; @gutter: @grid-gutter-width) { - position: relative; - min-height: 1px; - padding-left: (@gutter / 2); - padding-right: (@gutter / 2); - - @media (min-width: @screen-sm-min) { - float: left; - width: percentage((@columns / @grid-columns)); - } -} -.make-sm-column-offset(@columns) { - @media (min-width: @screen-sm-min) { - margin-left: percentage((@columns / @grid-columns)); - } -} -.make-sm-column-push(@columns) { - @media (min-width: @screen-sm-min) { - left: percentage((@columns / @grid-columns)); - } -} -.make-sm-column-pull(@columns) { - @media (min-width: @screen-sm-min) { - right: percentage((@columns / @grid-columns)); - } -} - -// Generate the medium columns -.make-md-column(@columns; @gutter: @grid-gutter-width) { - position: relative; - min-height: 1px; - padding-left: (@gutter / 2); - padding-right: (@gutter / 2); - - @media (min-width: @screen-md-min) { - float: left; - width: percentage((@columns / @grid-columns)); - } -} -.make-md-column-offset(@columns) { - @media (min-width: @screen-md-min) { - margin-left: percentage((@columns / @grid-columns)); - } -} -.make-md-column-push(@columns) { - @media (min-width: @screen-md-min) { - left: percentage((@columns / @grid-columns)); - } -} -.make-md-column-pull(@columns) { - @media (min-width: @screen-md-min) { - right: percentage((@columns / @grid-columns)); - } -} - -// Generate the large columns -.make-lg-column(@columns; @gutter: @grid-gutter-width) { - position: relative; - min-height: 1px; - padding-left: (@gutter / 2); - padding-right: (@gutter / 2); - - @media (min-width: @screen-lg-min) { - float: left; - width: percentage((@columns / @grid-columns)); - } -} -.make-lg-column-offset(@columns) { - @media (min-width: @screen-lg-min) { - margin-left: percentage((@columns / @grid-columns)); - } -} -.make-lg-column-push(@columns) { - @media (min-width: @screen-lg-min) { - left: percentage((@columns / @grid-columns)); - } -} -.make-lg-column-pull(@columns) { - @media (min-width: @screen-lg-min) { - right: percentage((@columns / @grid-columns)); - } -} diff --git a/src/UI/Content/Bootstrap/mixins/hide-text.less b/src/UI/Content/Bootstrap/mixins/hide-text.less deleted file mode 100644 index bc7011850..000000000 --- a/src/UI/Content/Bootstrap/mixins/hide-text.less +++ /dev/null @@ -1,21 +0,0 @@ -// CSS image replacement -// -// Heads up! v3 launched with only `.hide-text()`, but per our pattern for -// mixins being reused as classes with the same name, this doesn't hold up. As -// of v3.0.1 we have added `.text-hide()` and deprecated `.hide-text()`. -// -// Source: https://github.com/h5bp/html5-boilerplate/commit/aa0396eae757 - -// Deprecated as of v3.0.1 (will be removed in v4) -.hide-text() { - font: ~"0/0" a; - color: transparent; - text-shadow: none; - background-color: transparent; - border: 0; -} - -// New mixin to use as of v3.0.1 -.text-hide() { - .hide-text(); -} diff --git a/src/UI/Content/Bootstrap/mixins/image.less b/src/UI/Content/Bootstrap/mixins/image.less deleted file mode 100644 index f233cb3e1..000000000 --- a/src/UI/Content/Bootstrap/mixins/image.less +++ /dev/null @@ -1,33 +0,0 @@ -// Image Mixins -// - Responsive image -// - Retina image - - -// Responsive image -// -// Keep images from scaling beyond the width of their parents. -.img-responsive(@display: block) { - display: @display; - max-width: 100%; // Part 1: Set a maximum relative to the parent - height: auto; // Part 2: Scale the height according to the width, otherwise you get stretching -} - - -// Retina image -// -// Short retina mixin for setting background-image and -size. Note that the -// spelling of `min--moz-device-pixel-ratio` is intentional. -.img-retina(@file-1x; @file-2x; @width-1x; @height-1x) { - background-image: url("@{file-1x}"); - - @media - only screen and (-webkit-min-device-pixel-ratio: 2), - only screen and ( min--moz-device-pixel-ratio: 2), - only screen and ( -o-min-device-pixel-ratio: 2/1), - only screen and ( min-device-pixel-ratio: 2), - only screen and ( min-resolution: 192dpi), - only screen and ( min-resolution: 2dppx) { - background-image: url("@{file-2x}"); - background-size: @width-1x @height-1x; - } -} diff --git a/src/UI/Content/Bootstrap/mixins/labels.less b/src/UI/Content/Bootstrap/mixins/labels.less deleted file mode 100644 index 9f7a67ee3..000000000 --- a/src/UI/Content/Bootstrap/mixins/labels.less +++ /dev/null @@ -1,12 +0,0 @@ -// Labels - -.label-variant(@color) { - background-color: @color; - - &[href] { - &:hover, - &:focus { - background-color: darken(@color, 10%); - } - } -} diff --git a/src/UI/Content/Bootstrap/mixins/list-group.less b/src/UI/Content/Bootstrap/mixins/list-group.less deleted file mode 100644 index 03aa19069..000000000 --- a/src/UI/Content/Bootstrap/mixins/list-group.less +++ /dev/null @@ -1,30 +0,0 @@ -// List Groups - -.list-group-item-variant(@state; @background; @color) { - .list-group-item-@{state} { - color: @color; - background-color: @background; - - a&, - button& { - color: @color; - - .list-group-item-heading { - color: inherit; - } - - &:hover, - &:focus { - color: @color; - background-color: darken(@background, 5%); - } - &.active, - &.active:hover, - &.active:focus { - color: #fff; - background-color: @color; - border-color: @color; - } - } - } -} diff --git a/src/UI/Content/Bootstrap/mixins/nav-divider.less b/src/UI/Content/Bootstrap/mixins/nav-divider.less deleted file mode 100644 index feb1e9ed0..000000000 --- a/src/UI/Content/Bootstrap/mixins/nav-divider.less +++ /dev/null @@ -1,10 +0,0 @@ -// Horizontal dividers -// -// Dividers (basically an hr) within dropdowns and nav lists - -.nav-divider(@color: #e5e5e5) { - height: 1px; - margin: ((@line-height-computed / 2) - 1) 0; - overflow: hidden; - background-color: @color; -} diff --git a/src/UI/Content/Bootstrap/mixins/nav-vertical-align.less b/src/UI/Content/Bootstrap/mixins/nav-vertical-align.less deleted file mode 100644 index d458c7861..000000000 --- a/src/UI/Content/Bootstrap/mixins/nav-vertical-align.less +++ /dev/null @@ -1,9 +0,0 @@ -// Navbar vertical align -// -// Vertically center elements in the navbar. -// Example: an element has a height of 30px, so write out `.navbar-vertical-align(30px);` to calculate the appropriate top margin. - -.navbar-vertical-align(@element-height) { - margin-top: ((@navbar-height - @element-height) / 2); - margin-bottom: ((@navbar-height - @element-height) / 2); -} diff --git a/src/UI/Content/Bootstrap/mixins/opacity.less b/src/UI/Content/Bootstrap/mixins/opacity.less deleted file mode 100644 index 33ed25ce6..000000000 --- a/src/UI/Content/Bootstrap/mixins/opacity.less +++ /dev/null @@ -1,8 +0,0 @@ -// Opacity - -.opacity(@opacity) { - opacity: @opacity; - // IE8 filter - @opacity-ie: (@opacity * 100); - filter: ~"alpha(opacity=@{opacity-ie})"; -} diff --git a/src/UI/Content/Bootstrap/mixins/pagination.less b/src/UI/Content/Bootstrap/mixins/pagination.less deleted file mode 100644 index 618804f2d..000000000 --- a/src/UI/Content/Bootstrap/mixins/pagination.less +++ /dev/null @@ -1,24 +0,0 @@ -// Pagination - -.pagination-size(@padding-vertical; @padding-horizontal; @font-size; @line-height; @border-radius) { - > li { - > a, - > span { - padding: @padding-vertical @padding-horizontal; - font-size: @font-size; - line-height: @line-height; - } - &:first-child { - > a, - > span { - .border-left-radius(@border-radius); - } - } - &:last-child { - > a, - > span { - .border-right-radius(@border-radius); - } - } - } -} diff --git a/src/UI/Content/Bootstrap/mixins/panels.less b/src/UI/Content/Bootstrap/mixins/panels.less deleted file mode 100644 index 49ee10d4a..000000000 --- a/src/UI/Content/Bootstrap/mixins/panels.less +++ /dev/null @@ -1,24 +0,0 @@ -// Panels - -.panel-variant(@border; @heading-text-color; @heading-bg-color; @heading-border) { - border-color: @border; - - & > .panel-heading { - color: @heading-text-color; - background-color: @heading-bg-color; - border-color: @heading-border; - - + .panel-collapse > .panel-body { - border-top-color: @border; - } - .badge { - color: @heading-bg-color; - background-color: @heading-text-color; - } - } - & > .panel-footer { - + .panel-collapse > .panel-body { - border-bottom-color: @border; - } - } -} diff --git a/src/UI/Content/Bootstrap/mixins/progress-bar.less b/src/UI/Content/Bootstrap/mixins/progress-bar.less deleted file mode 100644 index f07996a34..000000000 --- a/src/UI/Content/Bootstrap/mixins/progress-bar.less +++ /dev/null @@ -1,10 +0,0 @@ -// Progress bars - -.progress-bar-variant(@color) { - background-color: @color; - - // Deprecated parent class requirement as of v3.2.0 - .progress-striped & { - #gradient > .striped(); - } -} diff --git a/src/UI/Content/Bootstrap/mixins/reset-filter.less b/src/UI/Content/Bootstrap/mixins/reset-filter.less deleted file mode 100644 index 68cdb5e18..000000000 --- a/src/UI/Content/Bootstrap/mixins/reset-filter.less +++ /dev/null @@ -1,8 +0,0 @@ -// Reset filters for IE -// -// When you need to remove a gradient background, do not forget to use this to reset -// the IE filter for IE9 and below. - -.reset-filter() { - filter: e(%("progid:DXImageTransform.Microsoft.gradient(enabled = false)")); -} diff --git a/src/UI/Content/Bootstrap/mixins/reset-text.less b/src/UI/Content/Bootstrap/mixins/reset-text.less deleted file mode 100644 index 58dd4d19b..000000000 --- a/src/UI/Content/Bootstrap/mixins/reset-text.less +++ /dev/null @@ -1,18 +0,0 @@ -.reset-text() { - font-family: @font-family-base; - // We deliberately do NOT reset font-size. - font-style: normal; - font-weight: normal; - letter-spacing: normal; - line-break: auto; - line-height: @line-height-base; - text-align: left; // Fallback for where `start` is not supported - text-align: start; - text-decoration: none; - text-shadow: none; - text-transform: none; - white-space: normal; - word-break: normal; - word-spacing: normal; - word-wrap: normal; -} diff --git a/src/UI/Content/Bootstrap/mixins/resize.less b/src/UI/Content/Bootstrap/mixins/resize.less deleted file mode 100644 index 3acd3afdb..000000000 --- a/src/UI/Content/Bootstrap/mixins/resize.less +++ /dev/null @@ -1,6 +0,0 @@ -// Resize anything - -.resizable(@direction) { - resize: @direction; // Options: horizontal, vertical, both - overflow: auto; // Per CSS3 UI, `resize` only applies when `overflow` isn't `visible` -} diff --git a/src/UI/Content/Bootstrap/mixins/responsive-visibility.less b/src/UI/Content/Bootstrap/mixins/responsive-visibility.less deleted file mode 100644 index ecf1e979f..000000000 --- a/src/UI/Content/Bootstrap/mixins/responsive-visibility.less +++ /dev/null @@ -1,15 +0,0 @@ -// Responsive utilities - -// -// More easily include all the states for responsive-utilities.less. -.responsive-visibility() { - display: block !important; - table& { display: table !important; } - tr& { display: table-row !important; } - th&, - td& { display: table-cell !important; } -} - -.responsive-invisibility() { - display: none !important; -} diff --git a/src/UI/Content/Bootstrap/mixins/size.less b/src/UI/Content/Bootstrap/mixins/size.less deleted file mode 100644 index a8be65089..000000000 --- a/src/UI/Content/Bootstrap/mixins/size.less +++ /dev/null @@ -1,10 +0,0 @@ -// Sizing shortcuts - -.size(@width; @height) { - width: @width; - height: @height; -} - -.square(@size) { - .size(@size; @size); -} diff --git a/src/UI/Content/Bootstrap/mixins/tab-focus.less b/src/UI/Content/Bootstrap/mixins/tab-focus.less deleted file mode 100644 index 1f1f05ab0..000000000 --- a/src/UI/Content/Bootstrap/mixins/tab-focus.less +++ /dev/null @@ -1,9 +0,0 @@ -// WebKit-style focus - -.tab-focus() { - // Default - outline: thin dotted; - // WebKit - outline: 5px auto -webkit-focus-ring-color; - outline-offset: -2px; -} diff --git a/src/UI/Content/Bootstrap/mixins/table-row.less b/src/UI/Content/Bootstrap/mixins/table-row.less deleted file mode 100644 index 0f287f1a8..000000000 --- a/src/UI/Content/Bootstrap/mixins/table-row.less +++ /dev/null @@ -1,28 +0,0 @@ -// Tables - -.table-row-variant(@state; @background) { - // Exact selectors below required to override `.table-striped` and prevent - // inheritance to nested tables. - .table > thead > tr, - .table > tbody > tr, - .table > tfoot > tr { - > td.@{state}, - > th.@{state}, - &.@{state} > td, - &.@{state} > th { - background-color: @background; - } - } - - // Hover states for `.table-hover` - // Note: this is not available for cells or rows within `thead` or `tfoot`. - .table-hover > tbody > tr { - > td.@{state}:hover, - > th.@{state}:hover, - &.@{state}:hover > td, - &:hover > .@{state}, - &.@{state}:hover > th { - background-color: darken(@background, 5%); - } - } -} diff --git a/src/UI/Content/Bootstrap/mixins/text-emphasis.less b/src/UI/Content/Bootstrap/mixins/text-emphasis.less deleted file mode 100644 index 9e8a77a69..000000000 --- a/src/UI/Content/Bootstrap/mixins/text-emphasis.less +++ /dev/null @@ -1,9 +0,0 @@ -// Typography - -.text-emphasis-variant(@color) { - color: @color; - a&:hover, - a&:focus { - color: darken(@color, 10%); - } -} diff --git a/src/UI/Content/Bootstrap/mixins/text-overflow.less b/src/UI/Content/Bootstrap/mixins/text-overflow.less deleted file mode 100644 index c11ad2fb7..000000000 --- a/src/UI/Content/Bootstrap/mixins/text-overflow.less +++ /dev/null @@ -1,8 +0,0 @@ -// Text overflow -// Requires inline-block or block for proper styling - -.text-overflow() { - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} diff --git a/src/UI/Content/Bootstrap/mixins/vendor-prefixes.less b/src/UI/Content/Bootstrap/mixins/vendor-prefixes.less deleted file mode 100644 index afd3331c3..000000000 --- a/src/UI/Content/Bootstrap/mixins/vendor-prefixes.less +++ /dev/null @@ -1,227 +0,0 @@ -// Vendor Prefixes -// -// All vendor mixins are deprecated as of v3.2.0 due to the introduction of -// Autoprefixer in our Gruntfile. They will be removed in v4. - -// - Animations -// - Backface visibility -// - Box shadow -// - Box sizing -// - Content columns -// - Hyphens -// - Placeholder text -// - Transformations -// - Transitions -// - User Select - - -// Animations -.animation(@animation) { - -webkit-animation: @animation; - -o-animation: @animation; - animation: @animation; -} -.animation-name(@name) { - -webkit-animation-name: @name; - animation-name: @name; -} -.animation-duration(@duration) { - -webkit-animation-duration: @duration; - animation-duration: @duration; -} -.animation-timing-function(@timing-function) { - -webkit-animation-timing-function: @timing-function; - animation-timing-function: @timing-function; -} -.animation-delay(@delay) { - -webkit-animation-delay: @delay; - animation-delay: @delay; -} -.animation-iteration-count(@iteration-count) { - -webkit-animation-iteration-count: @iteration-count; - animation-iteration-count: @iteration-count; -} -.animation-direction(@direction) { - -webkit-animation-direction: @direction; - animation-direction: @direction; -} -.animation-fill-mode(@fill-mode) { - -webkit-animation-fill-mode: @fill-mode; - animation-fill-mode: @fill-mode; -} - -// Backface visibility -// Prevent browsers from flickering when using CSS 3D transforms. -// Default value is `visible`, but can be changed to `hidden` - -.backface-visibility(@visibility){ - -webkit-backface-visibility: @visibility; - -moz-backface-visibility: @visibility; - backface-visibility: @visibility; -} - -// Drop shadows -// -// Note: Deprecated `.box-shadow()` as of v3.1.0 since all of Bootstrap's -// supported browsers that have box shadow capabilities now support it. - -.box-shadow(@shadow) { - -webkit-box-shadow: @shadow; // iOS <4.3 & Android <4.1 - box-shadow: @shadow; -} - -// Box sizing -.box-sizing(@boxmodel) { - -webkit-box-sizing: @boxmodel; - -moz-box-sizing: @boxmodel; - box-sizing: @boxmodel; -} - -// CSS3 Content Columns -.content-columns(@column-count; @column-gap: @grid-gutter-width) { - -webkit-column-count: @column-count; - -moz-column-count: @column-count; - column-count: @column-count; - -webkit-column-gap: @column-gap; - -moz-column-gap: @column-gap; - column-gap: @column-gap; -} - -// Optional hyphenation -.hyphens(@mode: auto) { - word-wrap: break-word; - -webkit-hyphens: @mode; - -moz-hyphens: @mode; - -ms-hyphens: @mode; // IE10+ - -o-hyphens: @mode; - hyphens: @mode; -} - -// Placeholder text -.placeholder(@color: @input-color-placeholder) { - // Firefox - &::-moz-placeholder { - color: @color; - opacity: 1; // Override Firefox's unusual default opacity; see https://github.com/twbs/bootstrap/pull/11526 - } - &:-ms-input-placeholder { color: @color; } // Internet Explorer 10+ - &::-webkit-input-placeholder { color: @color; } // Safari and Chrome -} - -// Transformations -.scale(@ratio) { - -webkit-transform: scale(@ratio); - -ms-transform: scale(@ratio); // IE9 only - -o-transform: scale(@ratio); - transform: scale(@ratio); -} -.scale(@ratioX; @ratioY) { - -webkit-transform: scale(@ratioX, @ratioY); - -ms-transform: scale(@ratioX, @ratioY); // IE9 only - -o-transform: scale(@ratioX, @ratioY); - transform: scale(@ratioX, @ratioY); -} -.scaleX(@ratio) { - -webkit-transform: scaleX(@ratio); - -ms-transform: scaleX(@ratio); // IE9 only - -o-transform: scaleX(@ratio); - transform: scaleX(@ratio); -} -.scaleY(@ratio) { - -webkit-transform: scaleY(@ratio); - -ms-transform: scaleY(@ratio); // IE9 only - -o-transform: scaleY(@ratio); - transform: scaleY(@ratio); -} -.skew(@x; @y) { - -webkit-transform: skewX(@x) skewY(@y); - -ms-transform: skewX(@x) skewY(@y); // See https://github.com/twbs/bootstrap/issues/4885; IE9+ - -o-transform: skewX(@x) skewY(@y); - transform: skewX(@x) skewY(@y); -} -.translate(@x; @y) { - -webkit-transform: translate(@x, @y); - -ms-transform: translate(@x, @y); // IE9 only - -o-transform: translate(@x, @y); - transform: translate(@x, @y); -} -.translate3d(@x; @y; @z) { - -webkit-transform: translate3d(@x, @y, @z); - transform: translate3d(@x, @y, @z); -} -.rotate(@degrees) { - -webkit-transform: rotate(@degrees); - -ms-transform: rotate(@degrees); // IE9 only - -o-transform: rotate(@degrees); - transform: rotate(@degrees); -} -.rotateX(@degrees) { - -webkit-transform: rotateX(@degrees); - -ms-transform: rotateX(@degrees); // IE9 only - -o-transform: rotateX(@degrees); - transform: rotateX(@degrees); -} -.rotateY(@degrees) { - -webkit-transform: rotateY(@degrees); - -ms-transform: rotateY(@degrees); // IE9 only - -o-transform: rotateY(@degrees); - transform: rotateY(@degrees); -} -.perspective(@perspective) { - -webkit-perspective: @perspective; - -moz-perspective: @perspective; - perspective: @perspective; -} -.perspective-origin(@perspective) { - -webkit-perspective-origin: @perspective; - -moz-perspective-origin: @perspective; - perspective-origin: @perspective; -} -.transform-origin(@origin) { - -webkit-transform-origin: @origin; - -moz-transform-origin: @origin; - -ms-transform-origin: @origin; // IE9 only - transform-origin: @origin; -} - - -// Transitions - -.transition(@transition) { - -webkit-transition: @transition; - -o-transition: @transition; - transition: @transition; -} -.transition-property(@transition-property) { - -webkit-transition-property: @transition-property; - transition-property: @transition-property; -} -.transition-delay(@transition-delay) { - -webkit-transition-delay: @transition-delay; - transition-delay: @transition-delay; -} -.transition-duration(@transition-duration) { - -webkit-transition-duration: @transition-duration; - transition-duration: @transition-duration; -} -.transition-timing-function(@timing-function) { - -webkit-transition-timing-function: @timing-function; - transition-timing-function: @timing-function; -} -.transition-transform(@transition) { - -webkit-transition: -webkit-transform @transition; - -moz-transition: -moz-transform @transition; - -o-transition: -o-transform @transition; - transition: transform @transition; -} - - -// User select -// For selecting text on the page - -.user-select(@select) { - -webkit-user-select: @select; - -moz-user-select: @select; - -ms-user-select: @select; // IE10+ - user-select: @select; -} diff --git a/src/UI/Content/Bootstrap/modals.less b/src/UI/Content/Bootstrap/modals.less deleted file mode 100644 index 1de622050..000000000 --- a/src/UI/Content/Bootstrap/modals.less +++ /dev/null @@ -1,150 +0,0 @@ -// -// Modals -// -------------------------------------------------- - -// .modal-open - body class for killing the scroll -// .modal - container to scroll within -// .modal-dialog - positioning shell for the actual modal -// .modal-content - actual modal w/ bg and corners and shit - -// Kill the scroll on the body -.modal-open { - overflow: hidden; -} - -// Container that the modal scrolls within -.modal { - display: none; - overflow: hidden; - position: fixed; - top: 0; - right: 0; - bottom: 0; - left: 0; - z-index: @zindex-modal; - -webkit-overflow-scrolling: touch; - - // Prevent Chrome on Windows from adding a focus outline. For details, see - // https://github.com/twbs/bootstrap/pull/10951. - outline: 0; - - // When fading in the modal, animate it to slide down - &.fade .modal-dialog { - .translate(0, -25%); - .transition-transform(~"0.3s ease-out"); - } - &.in .modal-dialog { .translate(0, 0) } -} -.modal-open .modal { - overflow-x: hidden; - overflow-y: auto; -} - -// Shell div to position the modal with bottom padding -.modal-dialog { - position: relative; - width: auto; - margin: 10px; -} - -// Actual modal -.modal-content { - position: relative; - background-color: @modal-content-bg; - border: 1px solid @modal-content-fallback-border-color; //old browsers fallback (ie8 etc) - border: 1px solid @modal-content-border-color; - border-radius: @border-radius-large; - .box-shadow(0 3px 9px rgba(0,0,0,.5)); - background-clip: padding-box; - // Remove focus outline from opened modal - outline: 0; -} - -// Modal background -.modal-backdrop { - position: fixed; - top: 0; - right: 0; - bottom: 0; - left: 0; - z-index: @zindex-modal-background; - background-color: @modal-backdrop-bg; - // Fade for backdrop - &.fade { .opacity(0); } - &.in { .opacity(@modal-backdrop-opacity); } -} - -// Modal header -// Top section of the modal w/ title and dismiss -.modal-header { - padding: @modal-title-padding; - border-bottom: 1px solid @modal-header-border-color; - min-height: (@modal-title-padding + @modal-title-line-height); -} -// Close icon -.modal-header .close { - margin-top: -2px; -} - -// Title text within header -.modal-title { - margin: 0; - line-height: @modal-title-line-height; -} - -// Modal body -// Where all modal content resides (sibling of .modal-header and .modal-footer) -.modal-body { - position: relative; - padding: @modal-inner-padding; -} - -// Footer (for actions) -.modal-footer { - padding: @modal-inner-padding; - text-align: right; // right align buttons - border-top: 1px solid @modal-footer-border-color; - &:extend(.clearfix all); // clear it in case folks use .pull-* classes on buttons - - // Properly space out buttons - .btn + .btn { - margin-left: 5px; - margin-bottom: 0; // account for input[type="submit"] which gets the bottom margin like all other inputs - } - // but override that for button groups - .btn-group .btn + .btn { - margin-left: -1px; - } - // and override it for block buttons as well - .btn-block + .btn-block { - margin-left: 0; - } -} - -// Measure scrollbar width for padding body during modal show/hide -.modal-scrollbar-measure { - position: absolute; - top: -9999px; - width: 50px; - height: 50px; - overflow: scroll; -} - -// Scale up the modal -@media (min-width: @screen-sm-min) { - // Automatically set modal's width for larger viewports - .modal-dialog { - width: @modal-md; - margin: 30px auto; - } - .modal-content { - .box-shadow(0 5px 15px rgba(0,0,0,.5)); - } - - // Modal sizes - .modal-sm { width: @modal-sm; } -} - -@media (min-width: @screen-md-min) { - .modal-lg { width: @modal-lg; } -} diff --git a/src/UI/Content/Bootstrap/navbar.less b/src/UI/Content/Bootstrap/navbar.less deleted file mode 100644 index 6d751bb9c..000000000 --- a/src/UI/Content/Bootstrap/navbar.less +++ /dev/null @@ -1,660 +0,0 @@ -// -// Navbars -// -------------------------------------------------- - - -// Wrapper and base class -// -// Provide a static navbar from which we expand to create full-width, fixed, and -// other navbar variations. - -.navbar { - position: relative; - min-height: @navbar-height; // Ensure a navbar always shows (e.g., without a .navbar-brand in collapsed mode) - margin-bottom: @navbar-margin-bottom; - border: 1px solid transparent; - - // Prevent floats from breaking the navbar - &:extend(.clearfix all); - - @media (min-width: @grid-float-breakpoint) { - border-radius: @navbar-border-radius; - } -} - - -// Navbar heading -// -// Groups `.navbar-brand` and `.navbar-toggle` into a single component for easy -// styling of responsive aspects. - -.navbar-header { - &:extend(.clearfix all); - - @media (min-width: @grid-float-breakpoint) { - float: left; - } -} - - -// Navbar collapse (body) -// -// Group your navbar content into this for easy collapsing and expanding across -// various device sizes. By default, this content is collapsed when <768px, but -// will expand past that for a horizontal display. -// -// To start (on mobile devices) the navbar links, forms, and buttons are stacked -// vertically and include a `max-height` to overflow in case you have too much -// content for the user's viewport. - -.navbar-collapse { - overflow-x: visible; - padding-right: @navbar-padding-horizontal; - padding-left: @navbar-padding-horizontal; - border-top: 1px solid transparent; - box-shadow: inset 0 1px 0 rgba(255,255,255,.1); - &:extend(.clearfix all); - -webkit-overflow-scrolling: touch; - - &.in { - overflow-y: auto; - } - - @media (min-width: @grid-float-breakpoint) { - width: auto; - border-top: 0; - box-shadow: none; - - &.collapse { - display: block !important; - height: auto !important; - padding-bottom: 0; // Override default setting - overflow: visible !important; - } - - &.in { - overflow-y: visible; - } - - // Undo the collapse side padding for navbars with containers to ensure - // alignment of right-aligned contents. - .navbar-fixed-top &, - .navbar-static-top &, - .navbar-fixed-bottom & { - padding-left: 0; - padding-right: 0; - } - } -} - -.navbar-fixed-top, -.navbar-fixed-bottom { - .navbar-collapse { - max-height: @navbar-collapse-max-height; - - @media (max-device-width: @screen-xs-min) and (orientation: landscape) { - max-height: 200px; - } - } -} - - -// Both navbar header and collapse -// -// When a container is present, change the behavior of the header and collapse. - -.container, -.container-fluid { - > .navbar-header, - > .navbar-collapse { - margin-right: -@navbar-padding-horizontal; - margin-left: -@navbar-padding-horizontal; - - @media (min-width: @grid-float-breakpoint) { - margin-right: 0; - margin-left: 0; - } - } -} - - -// -// Navbar alignment options -// -// Display the navbar across the entirety of the page or fixed it to the top or -// bottom of the page. - -// Static top (unfixed, but 100% wide) navbar -.navbar-static-top { - z-index: @zindex-navbar; - border-width: 0 0 1px; - - @media (min-width: @grid-float-breakpoint) { - border-radius: 0; - } -} - -// Fix the top/bottom navbars when screen real estate supports it -.navbar-fixed-top, -.navbar-fixed-bottom { - position: fixed; - right: 0; - left: 0; - z-index: @zindex-navbar-fixed; - - // Undo the rounded corners - @media (min-width: @grid-float-breakpoint) { - border-radius: 0; - } -} -.navbar-fixed-top { - top: 0; - border-width: 0 0 1px; -} -.navbar-fixed-bottom { - bottom: 0; - margin-bottom: 0; // override .navbar defaults - border-width: 1px 0 0; -} - - -// Brand/project name - -.navbar-brand { - float: left; - padding: @navbar-padding-vertical @navbar-padding-horizontal; - font-size: @font-size-large; - line-height: @line-height-computed; - height: @navbar-height; - - &:hover, - &:focus { - text-decoration: none; - } - - > img { - display: block; - } - - @media (min-width: @grid-float-breakpoint) { - .navbar > .container &, - .navbar > .container-fluid & { - margin-left: -@navbar-padding-horizontal; - } - } -} - - -// Navbar toggle -// -// Custom button for toggling the `.navbar-collapse`, powered by the collapse -// JavaScript plugin. - -.navbar-toggle { - position: relative; - float: right; - margin-right: @navbar-padding-horizontal; - padding: 9px 10px; - .navbar-vertical-align(34px); - background-color: transparent; - background-image: none; // Reset unusual Firefox-on-Android default style; see https://github.com/necolas/normalize.css/issues/214 - border: 1px solid transparent; - border-radius: @border-radius-base; - - // We remove the `outline` here, but later compensate by attaching `:hover` - // styles to `:focus`. - &:focus { - outline: 0; - } - - // Bars - .icon-bar { - display: block; - width: 22px; - height: 2px; - border-radius: 1px; - } - .icon-bar + .icon-bar { - margin-top: 4px; - } - - @media (min-width: @grid-float-breakpoint) { - display: none; - } -} - - -// Navbar nav links -// -// Builds on top of the `.nav` components with its own modifier class to make -// the nav the full height of the horizontal nav (above 768px). - -.navbar-nav { - margin: (@navbar-padding-vertical / 2) -@navbar-padding-horizontal; - - > li > a { - padding-top: 10px; - padding-bottom: 10px; - line-height: @line-height-computed; - } - - @media (max-width: @grid-float-breakpoint-max) { - // Dropdowns get custom display when collapsed - .open .dropdown-menu { - position: static; - float: none; - width: auto; - margin-top: 0; - background-color: transparent; - border: 0; - box-shadow: none; - > li > a, - .dropdown-header { - padding: 5px 15px 5px 25px; - } - > li > a { - line-height: @line-height-computed; - &:hover, - &:focus { - background-image: none; - } - } - } - } - - // Uncollapse the nav - @media (min-width: @grid-float-breakpoint) { - float: left; - margin: 0; - - > li { - float: left; - > a { - padding-top: @navbar-padding-vertical; - padding-bottom: @navbar-padding-vertical; - } - } - } -} - - -// Navbar form -// -// Extension of the `.form-inline` with some extra flavor for optimum display in -// our navbars. - -.navbar-form { - margin-left: -@navbar-padding-horizontal; - margin-right: -@navbar-padding-horizontal; - padding: 10px @navbar-padding-horizontal; - border-top: 1px solid transparent; - border-bottom: 1px solid transparent; - @shadow: inset 0 1px 0 rgba(255,255,255,.1), 0 1px 0 rgba(255,255,255,.1); - .box-shadow(@shadow); - - // Mixin behavior for optimum display - .form-inline(); - - .form-group { - @media (max-width: @grid-float-breakpoint-max) { - margin-bottom: 5px; - - &:last-child { - margin-bottom: 0; - } - } - } - - // Vertically center in expanded, horizontal navbar - .navbar-vertical-align(@input-height-base); - - // Undo 100% width for pull classes - @media (min-width: @grid-float-breakpoint) { - width: auto; - border: 0; - margin-left: 0; - margin-right: 0; - padding-top: 0; - padding-bottom: 0; - .box-shadow(none); - } -} - - -// Dropdown menus - -// Menu position and menu carets -.navbar-nav > li > .dropdown-menu { - margin-top: 0; - .border-top-radius(0); -} -// Menu position and menu caret support for dropups via extra dropup class -.navbar-fixed-bottom .navbar-nav > li > .dropdown-menu { - margin-bottom: 0; - .border-top-radius(@navbar-border-radius); - .border-bottom-radius(0); -} - - -// Buttons in navbars -// -// Vertically center a button within a navbar (when *not* in a form). - -.navbar-btn { - .navbar-vertical-align(@input-height-base); - - &.btn-sm { - .navbar-vertical-align(@input-height-small); - } - &.btn-xs { - .navbar-vertical-align(22); - } -} - - -// Text in navbars -// -// Add a class to make any element properly align itself vertically within the navbars. - -.navbar-text { - .navbar-vertical-align(@line-height-computed); - - @media (min-width: @grid-float-breakpoint) { - float: left; - margin-left: @navbar-padding-horizontal; - margin-right: @navbar-padding-horizontal; - } -} - - -// Component alignment -// -// Repurpose the pull utilities as their own navbar utilities to avoid specificity -// issues with parents and chaining. Only do this when the navbar is uncollapsed -// though so that navbar contents properly stack and align in mobile. -// -// Declared after the navbar components to ensure more specificity on the margins. - -@media (min-width: @grid-float-breakpoint) { - .navbar-left { .pull-left(); } - .navbar-right { - .pull-right(); - margin-right: -@navbar-padding-horizontal; - - ~ .navbar-right { - margin-right: 0; - } - } -} - - -// Alternate navbars -// -------------------------------------------------- - -// Default navbar -.navbar-default { - background-color: @navbar-default-bg; - border-color: @navbar-default-border; - - .navbar-brand { - color: @navbar-default-brand-color; - &:hover, - &:focus { - color: @navbar-default-brand-hover-color; - background-color: @navbar-default-brand-hover-bg; - } - } - - .navbar-text { - color: @navbar-default-color; - } - - .navbar-nav { - > li > a { - color: @navbar-default-link-color; - - &:hover, - &:focus { - color: @navbar-default-link-hover-color; - background-color: @navbar-default-link-hover-bg; - } - } - > .active > a { - &, - &:hover, - &:focus { - color: @navbar-default-link-active-color; - background-color: @navbar-default-link-active-bg; - } - } - > .disabled > a { - &, - &:hover, - &:focus { - color: @navbar-default-link-disabled-color; - background-color: @navbar-default-link-disabled-bg; - } - } - } - - .navbar-toggle { - border-color: @navbar-default-toggle-border-color; - &:hover, - &:focus { - background-color: @navbar-default-toggle-hover-bg; - } - .icon-bar { - background-color: @navbar-default-toggle-icon-bar-bg; - } - } - - .navbar-collapse, - .navbar-form { - border-color: @navbar-default-border; - } - - // Dropdown menu items - .navbar-nav { - // Remove background color from open dropdown - > .open > a { - &, - &:hover, - &:focus { - background-color: @navbar-default-link-active-bg; - color: @navbar-default-link-active-color; - } - } - - @media (max-width: @grid-float-breakpoint-max) { - // Dropdowns get custom display when collapsed - .open .dropdown-menu { - > li > a { - color: @navbar-default-link-color; - &:hover, - &:focus { - color: @navbar-default-link-hover-color; - background-color: @navbar-default-link-hover-bg; - } - } - > .active > a { - &, - &:hover, - &:focus { - color: @navbar-default-link-active-color; - background-color: @navbar-default-link-active-bg; - } - } - > .disabled > a { - &, - &:hover, - &:focus { - color: @navbar-default-link-disabled-color; - background-color: @navbar-default-link-disabled-bg; - } - } - } - } - } - - - // Links in navbars - // - // Add a class to ensure links outside the navbar nav are colored correctly. - - .navbar-link { - color: @navbar-default-link-color; - &:hover { - color: @navbar-default-link-hover-color; - } - } - - .btn-link { - color: @navbar-default-link-color; - &:hover, - &:focus { - color: @navbar-default-link-hover-color; - } - &[disabled], - fieldset[disabled] & { - &:hover, - &:focus { - color: @navbar-default-link-disabled-color; - } - } - } -} - -// Inverse navbar - -.navbar-inverse { - background-color: @navbar-inverse-bg; - border-color: @navbar-inverse-border; - - .navbar-brand { - color: @navbar-inverse-brand-color; - &:hover, - &:focus { - color: @navbar-inverse-brand-hover-color; - background-color: @navbar-inverse-brand-hover-bg; - } - } - - .navbar-text { - color: @navbar-inverse-color; - } - - .navbar-nav { - > li > a { - color: @navbar-inverse-link-color; - - &:hover, - &:focus { - color: @navbar-inverse-link-hover-color; - background-color: @navbar-inverse-link-hover-bg; - } - } - > .active > a { - &, - &:hover, - &:focus { - color: @navbar-inverse-link-active-color; - background-color: @navbar-inverse-link-active-bg; - } - } - > .disabled > a { - &, - &:hover, - &:focus { - color: @navbar-inverse-link-disabled-color; - background-color: @navbar-inverse-link-disabled-bg; - } - } - } - - // Darken the responsive nav toggle - .navbar-toggle { - border-color: @navbar-inverse-toggle-border-color; - &:hover, - &:focus { - background-color: @navbar-inverse-toggle-hover-bg; - } - .icon-bar { - background-color: @navbar-inverse-toggle-icon-bar-bg; - } - } - - .navbar-collapse, - .navbar-form { - border-color: darken(@navbar-inverse-bg, 7%); - } - - // Dropdowns - .navbar-nav { - > .open > a { - &, - &:hover, - &:focus { - background-color: @navbar-inverse-link-active-bg; - color: @navbar-inverse-link-active-color; - } - } - - @media (max-width: @grid-float-breakpoint-max) { - // Dropdowns get custom display - .open .dropdown-menu { - > .dropdown-header { - border-color: @navbar-inverse-border; - } - .divider { - background-color: @navbar-inverse-border; - } - > li > a { - color: @navbar-inverse-link-color; - &:hover, - &:focus { - color: @navbar-inverse-link-hover-color; - background-color: @navbar-inverse-link-hover-bg; - } - } - > .active > a { - &, - &:hover, - &:focus { - color: @navbar-inverse-link-active-color; - background-color: @navbar-inverse-link-active-bg; - } - } - > .disabled > a { - &, - &:hover, - &:focus { - color: @navbar-inverse-link-disabled-color; - background-color: @navbar-inverse-link-disabled-bg; - } - } - } - } - } - - .navbar-link { - color: @navbar-inverse-link-color; - &:hover { - color: @navbar-inverse-link-hover-color; - } - } - - .btn-link { - color: @navbar-inverse-link-color; - &:hover, - &:focus { - color: @navbar-inverse-link-hover-color; - } - &[disabled], - fieldset[disabled] & { - &:hover, - &:focus { - color: @navbar-inverse-link-disabled-color; - } - } - } -} diff --git a/src/UI/Content/Bootstrap/navs.less b/src/UI/Content/Bootstrap/navs.less deleted file mode 100644 index a3d11b136..000000000 --- a/src/UI/Content/Bootstrap/navs.less +++ /dev/null @@ -1,242 +0,0 @@ -// -// Navs -// -------------------------------------------------- - - -// Base class -// -------------------------------------------------- - -.nav { - margin-bottom: 0; - padding-left: 0; // Override default ul/ol - list-style: none; - &:extend(.clearfix all); - - > li { - position: relative; - display: block; - - > a { - position: relative; - display: block; - padding: @nav-link-padding; - &:hover, - &:focus { - text-decoration: none; - background-color: @nav-link-hover-bg; - } - } - - // Disabled state sets text to gray and nukes hover/tab effects - &.disabled > a { - color: @nav-disabled-link-color; - - &:hover, - &:focus { - color: @nav-disabled-link-hover-color; - text-decoration: none; - background-color: transparent; - cursor: @cursor-disabled; - } - } - } - - // Open dropdowns - .open > a { - &, - &:hover, - &:focus { - background-color: @nav-link-hover-bg; - border-color: @link-color; - } - } - - // Nav dividers (deprecated with v3.0.1) - // - // This should have been removed in v3 with the dropping of `.nav-list`, but - // we missed it. We don't currently support this anywhere, but in the interest - // of maintaining backward compatibility in case you use it, it's deprecated. - .nav-divider { - .nav-divider(); - } - - // Prevent IE8 from misplacing imgs - // - // See https://github.com/h5bp/html5-boilerplate/issues/984#issuecomment-3985989 - > li > a > img { - max-width: none; - } -} - - -// Tabs -// ------------------------- - -// Give the tabs something to sit on -.nav-tabs { - border-bottom: 1px solid @nav-tabs-border-color; - > li { - float: left; - // Make the list-items overlay the bottom border - margin-bottom: -1px; - - // Actual tabs (as links) - > a { - margin-right: 2px; - line-height: @line-height-base; - border: 1px solid transparent; - border-radius: @border-radius-base @border-radius-base 0 0; - &:hover { - border-color: @nav-tabs-link-hover-border-color @nav-tabs-link-hover-border-color @nav-tabs-border-color; - } - } - - // Active state, and its :hover to override normal :hover - &.active > a { - &, - &:hover, - &:focus { - color: @nav-tabs-active-link-hover-color; - background-color: @nav-tabs-active-link-hover-bg; - border: 1px solid @nav-tabs-active-link-hover-border-color; - border-bottom-color: transparent; - cursor: default; - } - } - } - // pulling this in mainly for less shorthand - &.nav-justified { - .nav-justified(); - .nav-tabs-justified(); - } -} - - -// Pills -// ------------------------- -.nav-pills { - > li { - float: left; - - // Links rendered as pills - > a { - border-radius: @nav-pills-border-radius; - } - + li { - margin-left: 2px; - } - - // Active state - &.active > a { - &, - &:hover, - &:focus { - color: @nav-pills-active-link-hover-color; - background-color: @nav-pills-active-link-hover-bg; - } - } - } -} - - -// Stacked pills -.nav-stacked { - > li { - float: none; - + li { - margin-top: 2px; - margin-left: 0; // no need for this gap between nav items - } - } -} - - -// Nav variations -// -------------------------------------------------- - -// Justified nav links -// ------------------------- - -.nav-justified { - width: 100%; - - > li { - float: none; - > a { - text-align: center; - margin-bottom: 5px; - } - } - - > .dropdown .dropdown-menu { - top: auto; - left: auto; - } - - @media (min-width: @screen-sm-min) { - > li { - display: table-cell; - width: 1%; - > a { - margin-bottom: 0; - } - } - } -} - -// Move borders to anchors instead of bottom of list -// -// Mixin for adding on top the shared `.nav-justified` styles for our tabs -.nav-tabs-justified { - border-bottom: 0; - - > li > a { - // Override margin from .nav-tabs - margin-right: 0; - border-radius: @border-radius-base; - } - - > .active > a, - > .active > a:hover, - > .active > a:focus { - border: 1px solid @nav-tabs-justified-link-border-color; - } - - @media (min-width: @screen-sm-min) { - > li > a { - border-bottom: 1px solid @nav-tabs-justified-link-border-color; - border-radius: @border-radius-base @border-radius-base 0 0; - } - > .active > a, - > .active > a:hover, - > .active > a:focus { - border-bottom-color: @nav-tabs-justified-active-link-border-color; - } - } -} - - -// Tabbable tabs -// ------------------------- - -// Hide tabbable panes to start, show them when `.active` -.tab-content { - > .tab-pane { - display: none; - } - > .active { - display: block; - } -} - - -// Dropdowns -// ------------------------- - -// Specific dropdowns -.nav-tabs .dropdown-menu { - // make dropdown border overlap tab border - margin-top: -1px; - // Remove the top rounded corners here since there is a hard edge above the menu - .border-top-radius(0); -} diff --git a/src/UI/Content/Bootstrap/normalize.less b/src/UI/Content/Bootstrap/normalize.less deleted file mode 100644 index 9dddf73ad..000000000 --- a/src/UI/Content/Bootstrap/normalize.less +++ /dev/null @@ -1,424 +0,0 @@ -/*! normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */ - -// -// 1. Set default font family to sans-serif. -// 2. Prevent iOS and IE text size adjust after device orientation change, -// without disabling user zoom. -// - -html { - font-family: sans-serif; // 1 - -ms-text-size-adjust: 100%; // 2 - -webkit-text-size-adjust: 100%; // 2 -} - -// -// Remove default margin. -// - -body { - margin: 0; -} - -// HTML5 display definitions -// ========================================================================== - -// -// Correct `block` display not defined for any HTML5 element in IE 8/9. -// Correct `block` display not defined for `details` or `summary` in IE 10/11 -// and Firefox. -// Correct `block` display not defined for `main` in IE 11. -// - -article, -aside, -details, -figcaption, -figure, -footer, -header, -hgroup, -main, -menu, -nav, -section, -summary { - display: block; -} - -// -// 1. Correct `inline-block` display not defined in IE 8/9. -// 2. Normalize vertical alignment of `progress` in Chrome, Firefox, and Opera. -// - -audio, -canvas, -progress, -video { - display: inline-block; // 1 - vertical-align: baseline; // 2 -} - -// -// Prevent modern browsers from displaying `audio` without controls. -// Remove excess height in iOS 5 devices. -// - -audio:not([controls]) { - display: none; - height: 0; -} - -// -// Address `[hidden]` styling not present in IE 8/9/10. -// Hide the `template` element in IE 8/9/10/11, Safari, and Firefox < 22. -// - -[hidden], -template { - display: none; -} - -// Links -// ========================================================================== - -// -// Remove the gray background color from active links in IE 10. -// - -a { - background-color: transparent; -} - -// -// Improve readability of focused elements when they are also in an -// active/hover state. -// - -a:active, -a:hover { - outline: 0; -} - -// Text-level semantics -// ========================================================================== - -// -// Address styling not present in IE 8/9/10/11, Safari, and Chrome. -// - -abbr[title] { - border-bottom: 1px dotted; -} - -// -// Address style set to `bolder` in Firefox 4+, Safari, and Chrome. -// - -b, -strong { - font-weight: bold; -} - -// -// Address styling not present in Safari and Chrome. -// - -dfn { - font-style: italic; -} - -// -// Address variable `h1` font-size and margin within `section` and `article` -// contexts in Firefox 4+, Safari, and Chrome. -// - -h1 { - font-size: 2em; - margin: 0.67em 0; -} - -// -// Address styling not present in IE 8/9. -// - -mark { - background: #ff0; - color: #000; -} - -// -// Address inconsistent and variable font size in all browsers. -// - -small { - font-size: 80%; -} - -// -// Prevent `sub` and `sup` affecting `line-height` in all browsers. -// - -sub, -sup { - font-size: 75%; - line-height: 0; - position: relative; - vertical-align: baseline; -} - -sup { - top: -0.5em; -} - -sub { - bottom: -0.25em; -} - -// Embedded content -// ========================================================================== - -// -// Remove border when inside `a` element in IE 8/9/10. -// - -img { - border: 0; -} - -// -// Correct overflow not hidden in IE 9/10/11. -// - -svg:not(:root) { - overflow: hidden; -} - -// Grouping content -// ========================================================================== - -// -// Address margin not present in IE 8/9 and Safari. -// - -figure { - margin: 1em 40px; -} - -// -// Address differences between Firefox and other browsers. -// - -hr { - box-sizing: content-box; - height: 0; -} - -// -// Contain overflow in all browsers. -// - -pre { - overflow: auto; -} - -// -// Address odd `em`-unit font size rendering in all browsers. -// - -code, -kbd, -pre, -samp { - font-family: monospace, monospace; - font-size: 1em; -} - -// Forms -// ========================================================================== - -// -// Known limitation: by default, Chrome and Safari on OS X allow very limited -// styling of `select`, unless a `border` property is set. -// - -// -// 1. Correct color not being inherited. -// Known issue: affects color of disabled elements. -// 2. Correct font properties not being inherited. -// 3. Address margins set differently in Firefox 4+, Safari, and Chrome. -// - -button, -input, -optgroup, -select, -textarea { - color: inherit; // 1 - font: inherit; // 2 - margin: 0; // 3 -} - -// -// Address `overflow` set to `hidden` in IE 8/9/10/11. -// - -button { - overflow: visible; -} - -// -// Address inconsistent `text-transform` inheritance for `button` and `select`. -// All other form control elements do not inherit `text-transform` values. -// Correct `button` style inheritance in Firefox, IE 8/9/10/11, and Opera. -// Correct `select` style inheritance in Firefox. -// - -button, -select { - text-transform: none; -} - -// -// 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio` -// and `video` controls. -// 2. Correct inability to style clickable `input` types in iOS. -// 3. Improve usability and consistency of cursor style between image-type -// `input` and others. -// - -button, -html input[type="button"], // 1 -input[type="reset"], -input[type="submit"] { - -webkit-appearance: button; // 2 - cursor: pointer; // 3 -} - -// -// Re-set default cursor for disabled elements. -// - -button[disabled], -html input[disabled] { - cursor: default; -} - -// -// Remove inner padding and border in Firefox 4+. -// - -button::-moz-focus-inner, -input::-moz-focus-inner { - border: 0; - padding: 0; -} - -// -// Address Firefox 4+ setting `line-height` on `input` using `!important` in -// the UA stylesheet. -// - -input { - line-height: normal; -} - -// -// It's recommended that you don't attempt to style these elements. -// Firefox's implementation doesn't respect box-sizing, padding, or width. -// -// 1. Address box sizing set to `content-box` in IE 8/9/10. -// 2. Remove excess padding in IE 8/9/10. -// - -input[type="checkbox"], -input[type="radio"] { - box-sizing: border-box; // 1 - padding: 0; // 2 -} - -// -// Fix the cursor style for Chrome's increment/decrement buttons. For certain -// `font-size` values of the `input`, it causes the cursor style of the -// decrement button to change from `default` to `text`. -// - -input[type="number"]::-webkit-inner-spin-button, -input[type="number"]::-webkit-outer-spin-button { - height: auto; -} - -// -// 1. Address `appearance` set to `searchfield` in Safari and Chrome. -// 2. Address `box-sizing` set to `border-box` in Safari and Chrome. -// - -input[type="search"] { - -webkit-appearance: textfield; // 1 - box-sizing: content-box; //2 -} - -// -// Remove inner padding and search cancel button in Safari and Chrome on OS X. -// Safari (but not Chrome) clips the cancel button when the search input has -// padding (and `textfield` appearance). -// - -input[type="search"]::-webkit-search-cancel-button, -input[type="search"]::-webkit-search-decoration { - -webkit-appearance: none; -} - -// -// Define consistent border, margin, and padding. -// - -fieldset { - border: 1px solid #c0c0c0; - margin: 0 2px; - padding: 0.35em 0.625em 0.75em; -} - -// -// 1. Correct `color` not being inherited in IE 8/9/10/11. -// 2. Remove padding so people aren't caught out if they zero out fieldsets. -// - -legend { - border: 0; // 1 - padding: 0; // 2 -} - -// -// Remove default vertical scrollbar in IE 8/9/10/11. -// - -textarea { - overflow: auto; -} - -// -// Don't inherit the `font-weight` (applied by a rule above). -// NOTE: the default cannot safely be changed in Chrome and Safari on OS X. -// - -optgroup { - font-weight: bold; -} - -// Tables -// ========================================================================== - -// -// Remove most spacing between table cells. -// - -table { - border-collapse: collapse; - border-spacing: 0; -} - -td, -th { - padding: 0; -} diff --git a/src/UI/Content/Bootstrap/pager.less b/src/UI/Content/Bootstrap/pager.less deleted file mode 100644 index 41abaaadc..000000000 --- a/src/UI/Content/Bootstrap/pager.less +++ /dev/null @@ -1,54 +0,0 @@ -// -// Pager pagination -// -------------------------------------------------- - - -.pager { - padding-left: 0; - margin: @line-height-computed 0; - list-style: none; - text-align: center; - &:extend(.clearfix all); - li { - display: inline; - > a, - > span { - display: inline-block; - padding: 5px 14px; - background-color: @pager-bg; - border: 1px solid @pager-border; - border-radius: @pager-border-radius; - } - - > a:hover, - > a:focus { - text-decoration: none; - background-color: @pager-hover-bg; - } - } - - .next { - > a, - > span { - float: right; - } - } - - .previous { - > a, - > span { - float: left; - } - } - - .disabled { - > a, - > a:hover, - > a:focus, - > span { - color: @pager-disabled-color; - background-color: @pager-bg; - cursor: @cursor-disabled; - } - } -} diff --git a/src/UI/Content/Bootstrap/pagination.less b/src/UI/Content/Bootstrap/pagination.less deleted file mode 100644 index 31a23bf79..000000000 --- a/src/UI/Content/Bootstrap/pagination.less +++ /dev/null @@ -1,89 +0,0 @@ -// -// Pagination (multiple pages) -// -------------------------------------------------- -.pagination { - display: inline-block; - padding-left: 0; - margin: @line-height-computed 0; - border-radius: @border-radius-base; - - > li { - display: inline; // Remove list-style and block-level defaults - > a, - > span { - position: relative; - float: left; // Collapse white-space - padding: @padding-base-vertical @padding-base-horizontal; - line-height: @line-height-base; - text-decoration: none; - color: @pagination-color; - background-color: @pagination-bg; - border: 1px solid @pagination-border; - margin-left: -1px; - } - &:first-child { - > a, - > span { - margin-left: 0; - .border-left-radius(@border-radius-base); - } - } - &:last-child { - > a, - > span { - .border-right-radius(@border-radius-base); - } - } - } - - > li > a, - > li > span { - &:hover, - &:focus { - z-index: 3; - color: @pagination-hover-color; - background-color: @pagination-hover-bg; - border-color: @pagination-hover-border; - } - } - - > .active > a, - > .active > span { - &, - &:hover, - &:focus { - z-index: 2; - color: @pagination-active-color; - background-color: @pagination-active-bg; - border-color: @pagination-active-border; - cursor: default; - } - } - - > .disabled { - > span, - > span:hover, - > span:focus, - > a, - > a:hover, - > a:focus { - color: @pagination-disabled-color; - background-color: @pagination-disabled-bg; - border-color: @pagination-disabled-border; - cursor: @cursor-disabled; - } - } -} - -// Sizing -// -------------------------------------------------- - -// Large -.pagination-lg { - .pagination-size(@padding-large-vertical; @padding-large-horizontal; @font-size-large; @line-height-large; @border-radius-large); -} - -// Small -.pagination-sm { - .pagination-size(@padding-small-vertical; @padding-small-horizontal; @font-size-small; @line-height-small; @border-radius-small); -} diff --git a/src/UI/Content/Bootstrap/panels.less b/src/UI/Content/Bootstrap/panels.less deleted file mode 100644 index 425eb5e64..000000000 --- a/src/UI/Content/Bootstrap/panels.less +++ /dev/null @@ -1,271 +0,0 @@ -// -// Panels -// -------------------------------------------------- - - -// Base class -.panel { - margin-bottom: @line-height-computed; - background-color: @panel-bg; - border: 1px solid transparent; - border-radius: @panel-border-radius; - .box-shadow(0 1px 1px rgba(0,0,0,.05)); -} - -// Panel contents -.panel-body { - padding: @panel-body-padding; - &:extend(.clearfix all); -} - -// Optional heading -.panel-heading { - padding: @panel-heading-padding; - border-bottom: 1px solid transparent; - .border-top-radius((@panel-border-radius - 1)); - - > .dropdown .dropdown-toggle { - color: inherit; - } -} - -// Within heading, strip any `h*` tag of its default margins for spacing. -.panel-title { - margin-top: 0; - margin-bottom: 0; - font-size: ceil((@font-size-base * 1.125)); - color: inherit; - - > a, - > small, - > .small, - > small > a, - > .small > a { - color: inherit; - } -} - -// Optional footer (stays gray in every modifier class) -.panel-footer { - padding: @panel-footer-padding; - background-color: @panel-footer-bg; - border-top: 1px solid @panel-inner-border; - .border-bottom-radius((@panel-border-radius - 1)); -} - - -// List groups in panels -// -// By default, space out list group content from panel headings to account for -// any kind of custom content between the two. - -.panel { - > .list-group, - > .panel-collapse > .list-group { - margin-bottom: 0; - - .list-group-item { - border-width: 1px 0; - border-radius: 0; - } - - // Add border top radius for first one - &:first-child { - .list-group-item:first-child { - border-top: 0; - .border-top-radius((@panel-border-radius - 1)); - } - } - - // Add border bottom radius for last one - &:last-child { - .list-group-item:last-child { - border-bottom: 0; - .border-bottom-radius((@panel-border-radius - 1)); - } - } - } - > .panel-heading + .panel-collapse > .list-group { - .list-group-item:first-child { - .border-top-radius(0); - } - } -} -// Collapse space between when there's no additional content. -.panel-heading + .list-group { - .list-group-item:first-child { - border-top-width: 0; - } -} -.list-group + .panel-footer { - border-top-width: 0; -} - -// Tables in panels -// -// Place a non-bordered `.table` within a panel (not within a `.panel-body`) and -// watch it go full width. - -.panel { - > .table, - > .table-responsive > .table, - > .panel-collapse > .table { - margin-bottom: 0; - - caption { - padding-left: @panel-body-padding; - padding-right: @panel-body-padding; - } - } - // Add border top radius for first one - > .table:first-child, - > .table-responsive:first-child > .table:first-child { - .border-top-radius((@panel-border-radius - 1)); - - > thead:first-child, - > tbody:first-child { - > tr:first-child { - border-top-left-radius: (@panel-border-radius - 1); - border-top-right-radius: (@panel-border-radius - 1); - - td:first-child, - th:first-child { - border-top-left-radius: (@panel-border-radius - 1); - } - td:last-child, - th:last-child { - border-top-right-radius: (@panel-border-radius - 1); - } - } - } - } - // Add border bottom radius for last one - > .table:last-child, - > .table-responsive:last-child > .table:last-child { - .border-bottom-radius((@panel-border-radius - 1)); - - > tbody:last-child, - > tfoot:last-child { - > tr:last-child { - border-bottom-left-radius: (@panel-border-radius - 1); - border-bottom-right-radius: (@panel-border-radius - 1); - - td:first-child, - th:first-child { - border-bottom-left-radius: (@panel-border-radius - 1); - } - td:last-child, - th:last-child { - border-bottom-right-radius: (@panel-border-radius - 1); - } - } - } - } - > .panel-body + .table, - > .panel-body + .table-responsive, - > .table + .panel-body, - > .table-responsive + .panel-body { - border-top: 1px solid @table-border-color; - } - > .table > tbody:first-child > tr:first-child th, - > .table > tbody:first-child > tr:first-child td { - border-top: 0; - } - > .table-bordered, - > .table-responsive > .table-bordered { - border: 0; - > thead, - > tbody, - > tfoot { - > tr { - > th:first-child, - > td:first-child { - border-left: 0; - } - > th:last-child, - > td:last-child { - border-right: 0; - } - } - } - > thead, - > tbody { - > tr:first-child { - > td, - > th { - border-bottom: 0; - } - } - } - > tbody, - > tfoot { - > tr:last-child { - > td, - > th { - border-bottom: 0; - } - } - } - } - > .table-responsive { - border: 0; - margin-bottom: 0; - } -} - - -// Collapsable panels (aka, accordion) -// -// Wrap a series of panels in `.panel-group` to turn them into an accordion with -// the help of our collapse JavaScript plugin. - -.panel-group { - margin-bottom: @line-height-computed; - - // Tighten up margin so it's only between panels - .panel { - margin-bottom: 0; - border-radius: @panel-border-radius; - - + .panel { - margin-top: 5px; - } - } - - .panel-heading { - border-bottom: 0; - - + .panel-collapse > .panel-body, - + .panel-collapse > .list-group { - border-top: 1px solid @panel-inner-border; - } - } - - .panel-footer { - border-top: 0; - + .panel-collapse .panel-body { - border-bottom: 1px solid @panel-inner-border; - } - } -} - - -// Contextual variations -.panel-default { - .panel-variant(@panel-default-border; @panel-default-text; @panel-default-heading-bg; @panel-default-border); -} -.panel-primary { - .panel-variant(@panel-primary-border; @panel-primary-text; @panel-primary-heading-bg; @panel-primary-border); -} -.panel-success { - .panel-variant(@panel-success-border; @panel-success-text; @panel-success-heading-bg; @panel-success-border); -} -.panel-info { - .panel-variant(@panel-info-border; @panel-info-text; @panel-info-heading-bg; @panel-info-border); -} -.panel-warning { - .panel-variant(@panel-warning-border; @panel-warning-text; @panel-warning-heading-bg; @panel-warning-border); -} -.panel-danger { - .panel-variant(@panel-danger-border; @panel-danger-text; @panel-danger-heading-bg; @panel-danger-border); -} diff --git a/src/UI/Content/Bootstrap/popovers.less b/src/UI/Content/Bootstrap/popovers.less deleted file mode 100644 index 3a62a6455..000000000 --- a/src/UI/Content/Bootstrap/popovers.less +++ /dev/null @@ -1,131 +0,0 @@ -// -// Popovers -// -------------------------------------------------- - - -.popover { - position: absolute; - top: 0; - left: 0; - z-index: @zindex-popover; - display: none; - max-width: @popover-max-width; - padding: 1px; - // Our parent element can be arbitrary since popovers are by default inserted as a sibling of their target element. - // So reset our font and text properties to avoid inheriting weird values. - .reset-text(); - font-size: @font-size-base; - - background-color: @popover-bg; - background-clip: padding-box; - border: 1px solid @popover-fallback-border-color; - border: 1px solid @popover-border-color; - border-radius: @border-radius-large; - .box-shadow(0 5px 10px rgba(0,0,0,.2)); - - // Offset the popover to account for the popover arrow - &.top { margin-top: -@popover-arrow-width; } - &.right { margin-left: @popover-arrow-width; } - &.bottom { margin-top: @popover-arrow-width; } - &.left { margin-left: -@popover-arrow-width; } -} - -.popover-title { - margin: 0; // reset heading margin - padding: 8px 14px; - font-size: @font-size-base; - background-color: @popover-title-bg; - border-bottom: 1px solid darken(@popover-title-bg, 5%); - border-radius: (@border-radius-large - 1) (@border-radius-large - 1) 0 0; -} - -.popover-content { - padding: 9px 14px; -} - -// Arrows -// -// .arrow is outer, .arrow:after is inner - -.popover > .arrow { - &, - &:after { - position: absolute; - display: block; - width: 0; - height: 0; - border-color: transparent; - border-style: solid; - } -} -.popover > .arrow { - border-width: @popover-arrow-outer-width; -} -.popover > .arrow:after { - border-width: @popover-arrow-width; - content: ""; -} - -.popover { - &.top > .arrow { - left: 50%; - margin-left: -@popover-arrow-outer-width; - border-bottom-width: 0; - border-top-color: @popover-arrow-outer-fallback-color; // IE8 fallback - border-top-color: @popover-arrow-outer-color; - bottom: -@popover-arrow-outer-width; - &:after { - content: " "; - bottom: 1px; - margin-left: -@popover-arrow-width; - border-bottom-width: 0; - border-top-color: @popover-arrow-color; - } - } - &.right > .arrow { - top: 50%; - left: -@popover-arrow-outer-width; - margin-top: -@popover-arrow-outer-width; - border-left-width: 0; - border-right-color: @popover-arrow-outer-fallback-color; // IE8 fallback - border-right-color: @popover-arrow-outer-color; - &:after { - content: " "; - left: 1px; - bottom: -@popover-arrow-width; - border-left-width: 0; - border-right-color: @popover-arrow-color; - } - } - &.bottom > .arrow { - left: 50%; - margin-left: -@popover-arrow-outer-width; - border-top-width: 0; - border-bottom-color: @popover-arrow-outer-fallback-color; // IE8 fallback - border-bottom-color: @popover-arrow-outer-color; - top: -@popover-arrow-outer-width; - &:after { - content: " "; - top: 1px; - margin-left: -@popover-arrow-width; - border-top-width: 0; - border-bottom-color: @popover-arrow-color; - } - } - - &.left > .arrow { - top: 50%; - right: -@popover-arrow-outer-width; - margin-top: -@popover-arrow-outer-width; - border-right-width: 0; - border-left-color: @popover-arrow-outer-fallback-color; // IE8 fallback - border-left-color: @popover-arrow-outer-color; - &:after { - content: " "; - right: 1px; - border-right-width: 0; - border-left-color: @popover-arrow-color; - bottom: -@popover-arrow-width; - } - } -} diff --git a/src/UI/Content/Bootstrap/print.less b/src/UI/Content/Bootstrap/print.less deleted file mode 100644 index 66e54ab48..000000000 --- a/src/UI/Content/Bootstrap/print.less +++ /dev/null @@ -1,101 +0,0 @@ -/*! Source: https://github.com/h5bp/html5-boilerplate/blob/master/src/css/main.css */ - -// ========================================================================== -// Print styles. -// Inlined to avoid the additional HTTP request: h5bp.com/r -// ========================================================================== - -@media print { - *, - *:before, - *:after { - background: transparent !important; - color: #000 !important; // Black prints faster: h5bp.com/s - box-shadow: none !important; - text-shadow: none !important; - } - - a, - a:visited { - text-decoration: underline; - } - - a[href]:after { - content: " (" attr(href) ")"; - } - - abbr[title]:after { - content: " (" attr(title) ")"; - } - - // Don't show links that are fragment identifiers, - // or use the `javascript:` pseudo protocol - a[href^="#"]:after, - a[href^="javascript:"]:after { - content: ""; - } - - pre, - blockquote { - border: 1px solid #999; - page-break-inside: avoid; - } - - thead { - display: table-header-group; // h5bp.com/t - } - - tr, - img { - page-break-inside: avoid; - } - - img { - max-width: 100% !important; - } - - p, - h2, - h3 { - orphans: 3; - widows: 3; - } - - h2, - h3 { - page-break-after: avoid; - } - - // Bootstrap specific changes start - - // Bootstrap components - .navbar { - display: none; - } - .btn, - .dropup > .btn { - > .caret { - border-top-color: #000 !important; - } - } - .label { - border: 1px solid #000; - } - - .table { - border-collapse: collapse !important; - - td, - th { - background-color: #fff !important; - } - } - .table-bordered { - th, - td { - border: 1px solid #ddd !important; - } - } - - // Bootstrap specific changes end -} diff --git a/src/UI/Content/Bootstrap/progress-bars.less b/src/UI/Content/Bootstrap/progress-bars.less deleted file mode 100644 index 8868a1fee..000000000 --- a/src/UI/Content/Bootstrap/progress-bars.less +++ /dev/null @@ -1,87 +0,0 @@ -// -// Progress bars -// -------------------------------------------------- - - -// Bar animations -// ------------------------- - -// WebKit -@-webkit-keyframes progress-bar-stripes { - from { background-position: 40px 0; } - to { background-position: 0 0; } -} - -// Spec and IE10+ -@keyframes progress-bar-stripes { - from { background-position: 40px 0; } - to { background-position: 0 0; } -} - - -// Bar itself -// ------------------------- - -// Outer container -.progress { - overflow: hidden; - height: @line-height-computed; - margin-bottom: @line-height-computed; - background-color: @progress-bg; - border-radius: @progress-border-radius; - .box-shadow(inset 0 1px 2px rgba(0,0,0,.1)); -} - -// Bar of progress -.progress-bar { - float: left; - width: 0%; - height: 100%; - font-size: @font-size-small; - line-height: @line-height-computed; - color: @progress-bar-color; - text-align: center; - background-color: @progress-bar-bg; - .box-shadow(inset 0 -1px 0 rgba(0,0,0,.15)); - .transition(width .6s ease); -} - -// Striped bars -// -// `.progress-striped .progress-bar` is deprecated as of v3.2.0 in favor of the -// `.progress-bar-striped` class, which you just add to an existing -// `.progress-bar`. -.progress-striped .progress-bar, -.progress-bar-striped { - #gradient > .striped(); - background-size: 40px 40px; -} - -// Call animation for the active one -// -// `.progress.active .progress-bar` is deprecated as of v3.2.0 in favor of the -// `.progress-bar.active` approach. -.progress.active .progress-bar, -.progress-bar.active { - .animation(progress-bar-stripes 2s linear infinite); -} - - -// Variations -// ------------------------- - -.progress-bar-success { - .progress-bar-variant(@progress-bar-success-bg); -} - -.progress-bar-info { - .progress-bar-variant(@progress-bar-info-bg); -} - -.progress-bar-warning { - .progress-bar-variant(@progress-bar-warning-bg); -} - -.progress-bar-danger { - .progress-bar-variant(@progress-bar-danger-bg); -} diff --git a/src/UI/Content/Bootstrap/responsive-embed.less b/src/UI/Content/Bootstrap/responsive-embed.less deleted file mode 100644 index 080a5118f..000000000 --- a/src/UI/Content/Bootstrap/responsive-embed.less +++ /dev/null @@ -1,35 +0,0 @@ -// Embeds responsive -// -// Credit: Nicolas Gallagher and SUIT CSS. - -.embed-responsive { - position: relative; - display: block; - height: 0; - padding: 0; - overflow: hidden; - - .embed-responsive-item, - iframe, - embed, - object, - video { - position: absolute; - top: 0; - left: 0; - bottom: 0; - height: 100%; - width: 100%; - border: 0; - } -} - -// Modifier class for 16:9 aspect ratio -.embed-responsive-16by9 { - padding-bottom: 56.25%; -} - -// Modifier class for 4:3 aspect ratio -.embed-responsive-4by3 { - padding-bottom: 75%; -} diff --git a/src/UI/Content/Bootstrap/responsive-utilities.less b/src/UI/Content/Bootstrap/responsive-utilities.less deleted file mode 100644 index b1db31d7b..000000000 --- a/src/UI/Content/Bootstrap/responsive-utilities.less +++ /dev/null @@ -1,194 +0,0 @@ -// -// Responsive: Utility classes -// -------------------------------------------------- - - -// IE10 in Windows (Phone) 8 -// -// Support for responsive views via media queries is kind of borked in IE10, for -// Surface/desktop in split view and for Windows Phone 8. This particular fix -// must be accompanied by a snippet of JavaScript to sniff the user agent and -// apply some conditional CSS to *only* the Surface/desktop Windows 8. Look at -// our Getting Started page for more information on this bug. -// -// For more information, see the following: -// -// Issue: https://github.com/twbs/bootstrap/issues/10497 -// Docs: http://getbootstrap.com/getting-started/#support-ie10-width -// Source: http://timkadlec.com/2013/01/windows-phone-8-and-device-width/ -// Source: http://timkadlec.com/2012/10/ie10-snap-mode-and-responsive-design/ - -@-ms-viewport { - width: device-width; -} - - -// Visibility utilities -// Note: Deprecated .visible-xs, .visible-sm, .visible-md, and .visible-lg as of v3.2.0 -.visible-xs, -.visible-sm, -.visible-md, -.visible-lg { - .responsive-invisibility(); -} - -.visible-xs-block, -.visible-xs-inline, -.visible-xs-inline-block, -.visible-sm-block, -.visible-sm-inline, -.visible-sm-inline-block, -.visible-md-block, -.visible-md-inline, -.visible-md-inline-block, -.visible-lg-block, -.visible-lg-inline, -.visible-lg-inline-block { - display: none !important; -} - -.visible-xs { - @media (max-width: @screen-xs-max) { - .responsive-visibility(); - } -} -.visible-xs-block { - @media (max-width: @screen-xs-max) { - display: block !important; - } -} -.visible-xs-inline { - @media (max-width: @screen-xs-max) { - display: inline !important; - } -} -.visible-xs-inline-block { - @media (max-width: @screen-xs-max) { - display: inline-block !important; - } -} - -.visible-sm { - @media (min-width: @screen-sm-min) and (max-width: @screen-sm-max) { - .responsive-visibility(); - } -} -.visible-sm-block { - @media (min-width: @screen-sm-min) and (max-width: @screen-sm-max) { - display: block !important; - } -} -.visible-sm-inline { - @media (min-width: @screen-sm-min) and (max-width: @screen-sm-max) { - display: inline !important; - } -} -.visible-sm-inline-block { - @media (min-width: @screen-sm-min) and (max-width: @screen-sm-max) { - display: inline-block !important; - } -} - -.visible-md { - @media (min-width: @screen-md-min) and (max-width: @screen-md-max) { - .responsive-visibility(); - } -} -.visible-md-block { - @media (min-width: @screen-md-min) and (max-width: @screen-md-max) { - display: block !important; - } -} -.visible-md-inline { - @media (min-width: @screen-md-min) and (max-width: @screen-md-max) { - display: inline !important; - } -} -.visible-md-inline-block { - @media (min-width: @screen-md-min) and (max-width: @screen-md-max) { - display: inline-block !important; - } -} - -.visible-lg { - @media (min-width: @screen-lg-min) { - .responsive-visibility(); - } -} -.visible-lg-block { - @media (min-width: @screen-lg-min) { - display: block !important; - } -} -.visible-lg-inline { - @media (min-width: @screen-lg-min) { - display: inline !important; - } -} -.visible-lg-inline-block { - @media (min-width: @screen-lg-min) { - display: inline-block !important; - } -} - -.hidden-xs { - @media (max-width: @screen-xs-max) { - .responsive-invisibility(); - } -} -.hidden-sm { - @media (min-width: @screen-sm-min) and (max-width: @screen-sm-max) { - .responsive-invisibility(); - } -} -.hidden-md { - @media (min-width: @screen-md-min) and (max-width: @screen-md-max) { - .responsive-invisibility(); - } -} -.hidden-lg { - @media (min-width: @screen-lg-min) { - .responsive-invisibility(); - } -} - - -// Print utilities -// -// Media queries are placed on the inside to be mixin-friendly. - -// Note: Deprecated .visible-print as of v3.2.0 -.visible-print { - .responsive-invisibility(); - - @media print { - .responsive-visibility(); - } -} -.visible-print-block { - display: none !important; - - @media print { - display: block !important; - } -} -.visible-print-inline { - display: none !important; - - @media print { - display: inline !important; - } -} -.visible-print-inline-block { - display: none !important; - - @media print { - display: inline-block !important; - } -} - -.hidden-print { - @media print { - .responsive-invisibility(); - } -} diff --git a/src/UI/Content/Bootstrap/scaffolding.less b/src/UI/Content/Bootstrap/scaffolding.less deleted file mode 100644 index 1929bfc5c..000000000 --- a/src/UI/Content/Bootstrap/scaffolding.less +++ /dev/null @@ -1,161 +0,0 @@ -// -// Scaffolding -// -------------------------------------------------- - - -// Reset the box-sizing -// -// Heads up! This reset may cause conflicts with some third-party widgets. -// For recommendations on resolving such conflicts, see -// http://getbootstrap.com/getting-started/#third-box-sizing -* { - .box-sizing(border-box); -} -*:before, -*:after { - .box-sizing(border-box); -} - - -// Body reset - -html { - font-size: 10px; - -webkit-tap-highlight-color: rgba(0,0,0,0); -} - -body { - font-family: @font-family-base; - font-size: @font-size-base; - line-height: @line-height-base; - color: @text-color; - background-color: @body-bg; -} - -// Reset fonts for relevant elements -input, -button, -select, -textarea { - font-family: inherit; - font-size: inherit; - line-height: inherit; -} - - -// Links - -a { - color: @link-color; - text-decoration: none; - - &:hover, - &:focus { - color: @link-hover-color; - text-decoration: @link-hover-decoration; - } - - &:focus { - .tab-focus(); - } -} - - -// Figures -// -// We reset this here because previously Normalize had no `figure` margins. This -// ensures we don't break anyone's use of the element. - -figure { - margin: 0; -} - - -// Images - -img { - vertical-align: middle; -} - -// Responsive images (ensure images don't scale beyond their parents) -.img-responsive { - .img-responsive(); -} - -// Rounded corners -.img-rounded { - border-radius: @border-radius-large; -} - -// Image thumbnails -// -// Heads up! This is mixin-ed into thumbnails.less for `.thumbnail`. -.img-thumbnail { - padding: @thumbnail-padding; - line-height: @line-height-base; - background-color: @thumbnail-bg; - border: 1px solid @thumbnail-border; - border-radius: @thumbnail-border-radius; - .transition(all .2s ease-in-out); - - // Keep them at most 100% wide - .img-responsive(inline-block); -} - -// Perfect circle -.img-circle { - border-radius: 50%; // set radius in percents -} - - -// Horizontal rules - -hr { - margin-top: @line-height-computed; - margin-bottom: @line-height-computed; - border: 0; - border-top: 1px solid @hr-border; -} - - -// Only display content to screen readers -// -// See: http://a11yproject.com/posts/how-to-hide-content/ - -.sr-only { - position: absolute; - width: 1px; - height: 1px; - margin: -1px; - padding: 0; - overflow: hidden; - clip: rect(0,0,0,0); - border: 0; -} - -// Use in conjunction with .sr-only to only display content when it's focused. -// Useful for "Skip to main content" links; see http://www.w3.org/TR/2013/NOTE-WCAG20-TECHS-20130905/G1 -// Credit: HTML5 Boilerplate - -.sr-only-focusable { - &:active, - &:focus { - position: static; - width: auto; - height: auto; - margin: 0; - overflow: visible; - clip: auto; - } -} - - -// iOS "clickable elements" fix for role="button" -// -// Fixes "clickability" issue (and more generally, the firing of events such as focus as well) -// for traditionally non-focusable elements with role="button" -// see https://developer.mozilla.org/en-US/docs/Web/Events/click#Safari_Mobile - -[role="button"] { - cursor: pointer; -} diff --git a/src/UI/Content/Bootstrap/tables.less b/src/UI/Content/Bootstrap/tables.less deleted file mode 100644 index 2242c0368..000000000 --- a/src/UI/Content/Bootstrap/tables.less +++ /dev/null @@ -1,234 +0,0 @@ -// -// Tables -// -------------------------------------------------- - - -table { - background-color: @table-bg; -} -caption { - padding-top: @table-cell-padding; - padding-bottom: @table-cell-padding; - color: @text-muted; - text-align: left; -} -th { - text-align: left; -} - - -// Baseline styles - -.table { - width: 100%; - max-width: 100%; - margin-bottom: @line-height-computed; - // Cells - > thead, - > tbody, - > tfoot { - > tr { - > th, - > td { - padding: @table-cell-padding; - line-height: @line-height-base; - vertical-align: top; - border-top: 1px solid @table-border-color; - } - } - } - // Bottom align for column headings - > thead > tr > th { - vertical-align: bottom; - border-bottom: 2px solid @table-border-color; - } - // Remove top border from thead by default - > caption + thead, - > colgroup + thead, - > thead:first-child { - > tr:first-child { - > th, - > td { - border-top: 0; - } - } - } - // Account for multiple tbody instances - > tbody + tbody { - border-top: 2px solid @table-border-color; - } - - // Nesting - .table { - background-color: @body-bg; - } -} - - -// Condensed table w/ half padding - -.table-condensed { - > thead, - > tbody, - > tfoot { - > tr { - > th, - > td { - padding: @table-condensed-cell-padding; - } - } - } -} - - -// Bordered version -// -// Add borders all around the table and between all the columns. - -.table-bordered { - border: 1px solid @table-border-color; - > thead, - > tbody, - > tfoot { - > tr { - > th, - > td { - border: 1px solid @table-border-color; - } - } - } - > thead > tr { - > th, - > td { - border-bottom-width: 2px; - } - } -} - - -// Zebra-striping -// -// Default zebra-stripe styles (alternating gray and transparent backgrounds) - -.table-striped { - > tbody > tr:nth-of-type(odd) { - background-color: @table-bg-accent; - } -} - - -// Hover effect -// -// Placed here since it has to come after the potential zebra striping - -.table-hover { - > tbody > tr:hover { - background-color: @table-bg-hover; - } -} - - -// Table cell sizing -// -// Reset default table behavior - -table col[class*="col-"] { - position: static; // Prevent border hiding in Firefox and IE9-11 (see https://github.com/twbs/bootstrap/issues/11623) - float: none; - display: table-column; -} -table { - td, - th { - &[class*="col-"] { - position: static; // Prevent border hiding in Firefox and IE9-11 (see https://github.com/twbs/bootstrap/issues/11623) - float: none; - display: table-cell; - } - } -} - - -// Table backgrounds -// -// Exact selectors below required to override `.table-striped` and prevent -// inheritance to nested tables. - -// Generate the contextual variants -.table-row-variant(active; @table-bg-active); -.table-row-variant(success; @state-success-bg); -.table-row-variant(info; @state-info-bg); -.table-row-variant(warning; @state-warning-bg); -.table-row-variant(danger; @state-danger-bg); - - -// Responsive tables -// -// Wrap your tables in `.table-responsive` and we'll make them mobile friendly -// by enabling horizontal scrolling. Only applies <768px. Everything above that -// will display normally. - -.table-responsive { - overflow-x: auto; - min-height: 0.01%; // Workaround for IE9 bug (see https://github.com/twbs/bootstrap/issues/14837) - - @media screen and (max-width: @screen-xs-max) { - width: 100%; - margin-bottom: (@line-height-computed * 0.75); - overflow-y: hidden; - -ms-overflow-style: -ms-autohiding-scrollbar; - border: 1px solid @table-border-color; - - // Tighten up spacing - > .table { - margin-bottom: 0; - - // Ensure the content doesn't wrap - > thead, - > tbody, - > tfoot { - > tr { - > th, - > td { - white-space: nowrap; - } - } - } - } - - // Special overrides for the bordered tables - > .table-bordered { - border: 0; - - // Nuke the appropriate borders so that the parent can handle them - > thead, - > tbody, - > tfoot { - > tr { - > th:first-child, - > td:first-child { - border-left: 0; - } - > th:last-child, - > td:last-child { - border-right: 0; - } - } - } - - // Only nuke the last row's bottom-border in `tbody` and `tfoot` since - // chances are there will be only one `tr` in a `thead` and that would - // remove the border altogether. - > tbody, - > tfoot { - > tr:last-child { - > th, - > td { - border-bottom: 0; - } - } - } - - } - } -} diff --git a/src/UI/Content/Bootstrap/theme.less b/src/UI/Content/Bootstrap/theme.less deleted file mode 100644 index 8371872b0..000000000 --- a/src/UI/Content/Bootstrap/theme.less +++ /dev/null @@ -1,291 +0,0 @@ -/*! - * Bootstrap v3.3.5 (http://getbootstrap.com) - * Copyright 2011-2015 Twitter, Inc. - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) - */ - -// -// Load core variables and mixins -// -------------------------------------------------- - -@import "variables.less"; -@import "mixins.less"; - - -// -// Buttons -// -------------------------------------------------- - -// Common styles -.btn-default, -.btn-primary, -.btn-success, -.btn-info, -.btn-warning, -.btn-danger { - text-shadow: 0 -1px 0 rgba(0,0,0,.2); - @shadow: inset 0 1px 0 rgba(255,255,255,.15), 0 1px 1px rgba(0,0,0,.075); - .box-shadow(@shadow); - - // Reset the shadow - &:active, - &.active { - .box-shadow(inset 0 3px 5px rgba(0,0,0,.125)); - } - - &.disabled, - &[disabled], - fieldset[disabled] & { - .box-shadow(none); - } - - .badge { - text-shadow: none; - } -} - -// Mixin for generating new styles -.btn-styles(@btn-color: #555) { - #gradient > .vertical(@start-color: @btn-color; @end-color: darken(@btn-color, 12%)); - .reset-filter(); // Disable gradients for IE9 because filter bleeds through rounded corners; see https://github.com/twbs/bootstrap/issues/10620 - background-repeat: repeat-x; - border-color: darken(@btn-color, 14%); - - &:hover, - &:focus { - background-color: darken(@btn-color, 12%); - background-position: 0 -15px; - } - - &:active, - &.active { - background-color: darken(@btn-color, 12%); - border-color: darken(@btn-color, 14%); - } - - &.disabled, - &[disabled], - fieldset[disabled] & { - &, - &:hover, - &:focus, - &.focus, - &:active, - &.active { - background-color: darken(@btn-color, 12%); - background-image: none; - } - } -} - -// Common styles -.btn { - // Remove the gradient for the pressed/active state - &:active, - &.active { - background-image: none; - } -} - -// Apply the mixin to the buttons -.btn-default { .btn-styles(@btn-default-bg); text-shadow: 0 1px 0 #fff; border-color: #ccc; } -.btn-primary { .btn-styles(@btn-primary-bg); } -.btn-success { .btn-styles(@btn-success-bg); } -.btn-info { .btn-styles(@btn-info-bg); } -.btn-warning { .btn-styles(@btn-warning-bg); } -.btn-danger { .btn-styles(@btn-danger-bg); } - - -// -// Images -// -------------------------------------------------- - -.thumbnail, -.img-thumbnail { - .box-shadow(0 1px 2px rgba(0,0,0,.075)); -} - - -// -// Dropdowns -// -------------------------------------------------- - -.dropdown-menu > li > a:hover, -.dropdown-menu > li > a:focus { - #gradient > .vertical(@start-color: @dropdown-link-hover-bg; @end-color: darken(@dropdown-link-hover-bg, 5%)); - background-color: darken(@dropdown-link-hover-bg, 5%); -} -.dropdown-menu > .active > a, -.dropdown-menu > .active > a:hover, -.dropdown-menu > .active > a:focus { - #gradient > .vertical(@start-color: @dropdown-link-active-bg; @end-color: darken(@dropdown-link-active-bg, 5%)); - background-color: darken(@dropdown-link-active-bg, 5%); -} - - -// -// Navbar -// -------------------------------------------------- - -// Default navbar -.navbar-default { - #gradient > .vertical(@start-color: lighten(@navbar-default-bg, 10%); @end-color: @navbar-default-bg); - .reset-filter(); // Remove gradient in IE<10 to fix bug where dropdowns don't get triggered - border-radius: @navbar-border-radius; - @shadow: inset 0 1px 0 rgba(255,255,255,.15), 0 1px 5px rgba(0,0,0,.075); - .box-shadow(@shadow); - - .navbar-nav > .open > a, - .navbar-nav > .active > a { - #gradient > .vertical(@start-color: darken(@navbar-default-link-active-bg, 5%); @end-color: darken(@navbar-default-link-active-bg, 2%)); - .box-shadow(inset 0 3px 9px rgba(0,0,0,.075)); - } -} -.navbar-brand, -.navbar-nav > li > a { - text-shadow: 0 1px 0 rgba(255,255,255,.25); -} - -// Inverted navbar -.navbar-inverse { - #gradient > .vertical(@start-color: lighten(@navbar-inverse-bg, 10%); @end-color: @navbar-inverse-bg); - .reset-filter(); // Remove gradient in IE<10 to fix bug where dropdowns don't get triggered; see https://github.com/twbs/bootstrap/issues/10257 - border-radius: @navbar-border-radius; - .navbar-nav > .open > a, - .navbar-nav > .active > a { - #gradient > .vertical(@start-color: @navbar-inverse-link-active-bg; @end-color: lighten(@navbar-inverse-link-active-bg, 2.5%)); - .box-shadow(inset 0 3px 9px rgba(0,0,0,.25)); - } - - .navbar-brand, - .navbar-nav > li > a { - text-shadow: 0 -1px 0 rgba(0,0,0,.25); - } -} - -// Undo rounded corners in static and fixed navbars -.navbar-static-top, -.navbar-fixed-top, -.navbar-fixed-bottom { - border-radius: 0; -} - -// Fix active state of dropdown items in collapsed mode -@media (max-width: @grid-float-breakpoint-max) { - .navbar .navbar-nav .open .dropdown-menu > .active > a { - &, - &:hover, - &:focus { - color: #fff; - #gradient > .vertical(@start-color: @dropdown-link-active-bg; @end-color: darken(@dropdown-link-active-bg, 5%)); - } - } -} - - -// -// Alerts -// -------------------------------------------------- - -// Common styles -.alert { - text-shadow: 0 1px 0 rgba(255,255,255,.2); - @shadow: inset 0 1px 0 rgba(255,255,255,.25), 0 1px 2px rgba(0,0,0,.05); - .box-shadow(@shadow); -} - -// Mixin for generating new styles -.alert-styles(@color) { - #gradient > .vertical(@start-color: @color; @end-color: darken(@color, 7.5%)); - border-color: darken(@color, 15%); -} - -// Apply the mixin to the alerts -.alert-success { .alert-styles(@alert-success-bg); } -.alert-info { .alert-styles(@alert-info-bg); } -.alert-warning { .alert-styles(@alert-warning-bg); } -.alert-danger { .alert-styles(@alert-danger-bg); } - - -// -// Progress bars -// -------------------------------------------------- - -// Give the progress background some depth -.progress { - #gradient > .vertical(@start-color: darken(@progress-bg, 4%); @end-color: @progress-bg) -} - -// Mixin for generating new styles -.progress-bar-styles(@color) { - #gradient > .vertical(@start-color: @color; @end-color: darken(@color, 10%)); -} - -// Apply the mixin to the progress bars -.progress-bar { .progress-bar-styles(@progress-bar-bg); } -.progress-bar-success { .progress-bar-styles(@progress-bar-success-bg); } -.progress-bar-info { .progress-bar-styles(@progress-bar-info-bg); } -.progress-bar-warning { .progress-bar-styles(@progress-bar-warning-bg); } -.progress-bar-danger { .progress-bar-styles(@progress-bar-danger-bg); } - -// Reset the striped class because our mixins don't do multiple gradients and -// the above custom styles override the new `.progress-bar-striped` in v3.2.0. -.progress-bar-striped { - #gradient > .striped(); -} - - -// -// List groups -// -------------------------------------------------- - -.list-group { - border-radius: @border-radius-base; - .box-shadow(0 1px 2px rgba(0,0,0,.075)); -} -.list-group-item.active, -.list-group-item.active:hover, -.list-group-item.active:focus { - text-shadow: 0 -1px 0 darken(@list-group-active-bg, 10%); - #gradient > .vertical(@start-color: @list-group-active-bg; @end-color: darken(@list-group-active-bg, 7.5%)); - border-color: darken(@list-group-active-border, 7.5%); - - .badge { - text-shadow: none; - } -} - - -// -// Panels -// -------------------------------------------------- - -// Common styles -.panel { - .box-shadow(0 1px 2px rgba(0,0,0,.05)); -} - -// Mixin for generating new styles -.panel-heading-styles(@color) { - #gradient > .vertical(@start-color: @color; @end-color: darken(@color, 5%)); -} - -// Apply the mixin to the panel headings only -.panel-default > .panel-heading { .panel-heading-styles(@panel-default-heading-bg); } -.panel-primary > .panel-heading { .panel-heading-styles(@panel-primary-heading-bg); } -.panel-success > .panel-heading { .panel-heading-styles(@panel-success-heading-bg); } -.panel-info > .panel-heading { .panel-heading-styles(@panel-info-heading-bg); } -.panel-warning > .panel-heading { .panel-heading-styles(@panel-warning-heading-bg); } -.panel-danger > .panel-heading { .panel-heading-styles(@panel-danger-heading-bg); } - - -// -// Wells -// -------------------------------------------------- - -.well { - #gradient > .vertical(@start-color: darken(@well-bg, 5%); @end-color: @well-bg); - border-color: darken(@well-bg, 10%); - @shadow: inset 0 1px 3px rgba(0,0,0,.05), 0 1px 0 rgba(255,255,255,.1); - .box-shadow(@shadow); -} diff --git a/src/UI/Content/Bootstrap/thumbnails.less b/src/UI/Content/Bootstrap/thumbnails.less deleted file mode 100644 index 0713e67d0..000000000 --- a/src/UI/Content/Bootstrap/thumbnails.less +++ /dev/null @@ -1,36 +0,0 @@ -// -// Thumbnails -// -------------------------------------------------- - - -// Mixin and adjust the regular image class -.thumbnail { - display: block; - padding: @thumbnail-padding; - margin-bottom: @line-height-computed; - line-height: @line-height-base; - background-color: @thumbnail-bg; - border: 1px solid @thumbnail-border; - border-radius: @thumbnail-border-radius; - .transition(border .2s ease-in-out); - - > img, - a > img { - &:extend(.img-responsive); - margin-left: auto; - margin-right: auto; - } - - // Add a hover state for linked versions only - a&:hover, - a&:focus, - a&.active { - border-color: @link-color; - } - - // Image captions - .caption { - padding: @thumbnail-caption-padding; - color: @thumbnail-caption-color; - } -} diff --git a/src/UI/Content/Bootstrap/tooltip.less b/src/UI/Content/Bootstrap/tooltip.less deleted file mode 100644 index b48d63e07..000000000 --- a/src/UI/Content/Bootstrap/tooltip.less +++ /dev/null @@ -1,101 +0,0 @@ -// -// Tooltips -// -------------------------------------------------- - - -// Base class -.tooltip { - position: absolute; - z-index: @zindex-tooltip; - display: block; - // Our parent element can be arbitrary since tooltips are by default inserted as a sibling of their target element. - // So reset our font and text properties to avoid inheriting weird values. - .reset-text(); - font-size: @font-size-small; - - .opacity(0); - - &.in { .opacity(@tooltip-opacity); } - &.top { margin-top: -3px; padding: @tooltip-arrow-width 0; } - &.right { margin-left: 3px; padding: 0 @tooltip-arrow-width; } - &.bottom { margin-top: 3px; padding: @tooltip-arrow-width 0; } - &.left { margin-left: -3px; padding: 0 @tooltip-arrow-width; } -} - -// Wrapper for the tooltip content -.tooltip-inner { - max-width: @tooltip-max-width; - padding: 3px 8px; - color: @tooltip-color; - text-align: center; - background-color: @tooltip-bg; - border-radius: @border-radius-base; -} - -// Arrows -.tooltip-arrow { - position: absolute; - width: 0; - height: 0; - border-color: transparent; - border-style: solid; -} -// Note: Deprecated .top-left, .top-right, .bottom-left, and .bottom-right as of v3.3.1 -.tooltip { - &.top .tooltip-arrow { - bottom: 0; - left: 50%; - margin-left: -@tooltip-arrow-width; - border-width: @tooltip-arrow-width @tooltip-arrow-width 0; - border-top-color: @tooltip-arrow-color; - } - &.top-left .tooltip-arrow { - bottom: 0; - right: @tooltip-arrow-width; - margin-bottom: -@tooltip-arrow-width; - border-width: @tooltip-arrow-width @tooltip-arrow-width 0; - border-top-color: @tooltip-arrow-color; - } - &.top-right .tooltip-arrow { - bottom: 0; - left: @tooltip-arrow-width; - margin-bottom: -@tooltip-arrow-width; - border-width: @tooltip-arrow-width @tooltip-arrow-width 0; - border-top-color: @tooltip-arrow-color; - } - &.right .tooltip-arrow { - top: 50%; - left: 0; - margin-top: -@tooltip-arrow-width; - border-width: @tooltip-arrow-width @tooltip-arrow-width @tooltip-arrow-width 0; - border-right-color: @tooltip-arrow-color; - } - &.left .tooltip-arrow { - top: 50%; - right: 0; - margin-top: -@tooltip-arrow-width; - border-width: @tooltip-arrow-width 0 @tooltip-arrow-width @tooltip-arrow-width; - border-left-color: @tooltip-arrow-color; - } - &.bottom .tooltip-arrow { - top: 0; - left: 50%; - margin-left: -@tooltip-arrow-width; - border-width: 0 @tooltip-arrow-width @tooltip-arrow-width; - border-bottom-color: @tooltip-arrow-color; - } - &.bottom-left .tooltip-arrow { - top: 0; - right: @tooltip-arrow-width; - margin-top: -@tooltip-arrow-width; - border-width: 0 @tooltip-arrow-width @tooltip-arrow-width; - border-bottom-color: @tooltip-arrow-color; - } - &.bottom-right .tooltip-arrow { - top: 0; - left: @tooltip-arrow-width; - margin-top: -@tooltip-arrow-width; - border-width: 0 @tooltip-arrow-width @tooltip-arrow-width; - border-bottom-color: @tooltip-arrow-color; - } -} diff --git a/src/UI/Content/Bootstrap/type.less b/src/UI/Content/Bootstrap/type.less deleted file mode 100644 index 68ba6017b..000000000 --- a/src/UI/Content/Bootstrap/type.less +++ /dev/null @@ -1,302 +0,0 @@ -// -// Typography -// -------------------------------------------------- - - -// Headings -// ------------------------- - -h1, h2, h3, h4, h5, h6, -.h1, .h2, .h3, .h4, .h5, .h6 { - font-family: @headings-font-family; - font-weight: @headings-font-weight; - line-height: @headings-line-height; - color: @headings-color; - - small, - .small { - font-weight: normal; - line-height: 1; - color: @headings-small-color; - } -} - -h1, .h1, -h2, .h2, -h3, .h3 { - margin-top: @line-height-computed; - margin-bottom: (@line-height-computed / 2); - - small, - .small { - font-size: 65%; - } -} -h4, .h4, -h5, .h5, -h6, .h6 { - margin-top: (@line-height-computed / 2); - margin-bottom: (@line-height-computed / 2); - - small, - .small { - font-size: 75%; - } -} - -h1, .h1 { font-size: @font-size-h1; } -h2, .h2 { font-size: @font-size-h2; } -h3, .h3 { font-size: @font-size-h3; } -h4, .h4 { font-size: @font-size-h4; } -h5, .h5 { font-size: @font-size-h5; } -h6, .h6 { font-size: @font-size-h6; } - - -// Body text -// ------------------------- - -p { - margin: 0 0 (@line-height-computed / 2); -} - -.lead { - margin-bottom: @line-height-computed; - font-size: floor((@font-size-base * 1.15)); - font-weight: 300; - line-height: 1.4; - - @media (min-width: @screen-sm-min) { - font-size: (@font-size-base * 1.5); - } -} - - -// Emphasis & misc -// ------------------------- - -// Ex: (12px small font / 14px base font) * 100% = about 85% -small, -.small { - font-size: floor((100% * @font-size-small / @font-size-base)); -} - -mark, -.mark { - background-color: @state-warning-bg; - padding: .2em; -} - -// Alignment -.text-left { text-align: left; } -.text-right { text-align: right; } -.text-center { text-align: center; } -.text-justify { text-align: justify; } -.text-nowrap { white-space: nowrap; } - -// Transformation -.text-lowercase { text-transform: lowercase; } -.text-uppercase { text-transform: uppercase; } -.text-capitalize { text-transform: capitalize; } - -// Contextual colors -.text-muted { - color: @text-muted; -} -.text-primary { - .text-emphasis-variant(@brand-primary); -} -.text-success { - .text-emphasis-variant(@state-success-text); -} -.text-info { - .text-emphasis-variant(@state-info-text); -} -.text-warning { - .text-emphasis-variant(@state-warning-text); -} -.text-danger { - .text-emphasis-variant(@state-danger-text); -} - -// Contextual backgrounds -// For now we'll leave these alongside the text classes until v4 when we can -// safely shift things around (per SemVer rules). -.bg-primary { - // Given the contrast here, this is the only class to have its color inverted - // automatically. - color: #fff; - .bg-variant(@brand-primary); -} -.bg-success { - .bg-variant(@state-success-bg); -} -.bg-info { - .bg-variant(@state-info-bg); -} -.bg-warning { - .bg-variant(@state-warning-bg); -} -.bg-danger { - .bg-variant(@state-danger-bg); -} - - -// Page header -// ------------------------- - -.page-header { - padding-bottom: ((@line-height-computed / 2) - 1); - margin: (@line-height-computed * 2) 0 @line-height-computed; - border-bottom: 1px solid @page-header-border-color; -} - - -// Lists -// ------------------------- - -// Unordered and Ordered lists -ul, -ol { - margin-top: 0; - margin-bottom: (@line-height-computed / 2); - ul, - ol { - margin-bottom: 0; - } -} - -// List options - -// Unstyled keeps list items block level, just removes default browser padding and list-style -.list-unstyled { - padding-left: 0; - list-style: none; -} - -// Inline turns list items into inline-block -.list-inline { - .list-unstyled(); - margin-left: -5px; - - > li { - display: inline-block; - padding-left: 5px; - padding-right: 5px; - } -} - -// Description Lists -dl { - margin-top: 0; // Remove browser default - margin-bottom: @line-height-computed; -} -dt, -dd { - line-height: @line-height-base; -} -dt { - font-weight: bold; -} -dd { - margin-left: 0; // Undo browser default -} - -// Horizontal description lists -// -// Defaults to being stacked without any of the below styles applied, until the -// grid breakpoint is reached (default of ~768px). - -.dl-horizontal { - dd { - &:extend(.clearfix all); // Clear the floated `dt` if an empty `dd` is present - } - - @media (min-width: @grid-float-breakpoint) { - dt { - float: left; - width: (@dl-horizontal-offset - 20); - clear: left; - text-align: right; - .text-overflow(); - } - dd { - margin-left: @dl-horizontal-offset; - } - } -} - - -// Misc -// ------------------------- - -// Abbreviations and acronyms -abbr[title], -// Add data-* attribute to help out our tooltip plugin, per https://github.com/twbs/bootstrap/issues/5257 -abbr[data-original-title] { - cursor: help; - border-bottom: 1px dotted @abbr-border-color; -} -.initialism { - font-size: 90%; - .text-uppercase(); -} - -// Blockquotes -blockquote { - padding: (@line-height-computed / 2) @line-height-computed; - margin: 0 0 @line-height-computed; - font-size: @blockquote-font-size; - border-left: 5px solid @blockquote-border-color; - - p, - ul, - ol { - &:last-child { - margin-bottom: 0; - } - } - - // Note: Deprecated small and .small as of v3.1.0 - // Context: https://github.com/twbs/bootstrap/issues/11660 - footer, - small, - .small { - display: block; - font-size: 80%; // back to default font-size - line-height: @line-height-base; - color: @blockquote-small-color; - - &:before { - content: '\2014 \00A0'; // em dash, nbsp - } - } -} - -// Opposite alignment of blockquote -// -// Heads up: `blockquote.pull-right` has been deprecated as of v3.1.0. -.blockquote-reverse, -blockquote.pull-right { - padding-right: 15px; - padding-left: 0; - border-right: 5px solid @blockquote-border-color; - border-left: 0; - text-align: right; - - // Account for citation - footer, - small, - .small { - &:before { content: ''; } - &:after { - content: '\00A0 \2014'; // nbsp, em dash - } - } -} - -// Addresses -address { - margin-bottom: @line-height-computed; - font-style: normal; - line-height: @line-height-base; -} diff --git a/src/UI/Content/Bootstrap/utilities.less b/src/UI/Content/Bootstrap/utilities.less deleted file mode 100644 index 7a8ca27a8..000000000 --- a/src/UI/Content/Bootstrap/utilities.less +++ /dev/null @@ -1,55 +0,0 @@ -// -// Utility classes -// -------------------------------------------------- - - -// Floats -// ------------------------- - -.clearfix { - .clearfix(); -} -.center-block { - .center-block(); -} -.pull-right { - float: right !important; -} -.pull-left { - float: left !important; -} - - -// Toggling content -// ------------------------- - -// Note: Deprecated .hide in favor of .hidden or .sr-only (as appropriate) in v3.0.1 -.hide { - display: none !important; -} -.show { - display: block !important; -} -.invisible { - visibility: hidden; -} -.text-hide { - .text-hide(); -} - - -// Hide from screenreaders and browsers -// -// Credit: HTML5 Boilerplate - -.hidden { - display: none !important; -} - - -// For Affix plugin -// ------------------------- - -.affix { - position: fixed; -} diff --git a/src/UI/Content/Bootstrap/variables.less b/src/UI/Content/Bootstrap/variables.less deleted file mode 100644 index c1861a8e0..000000000 --- a/src/UI/Content/Bootstrap/variables.less +++ /dev/null @@ -1,867 +0,0 @@ -// -// Variables -// -------------------------------------------------- - - -//== Colors -// -//## Gray and brand colors for use across Bootstrap. - -@gray-base: #000; -@gray-darker: lighten(@gray-base, 13.5%); // #222 -@gray-dark: lighten(@gray-base, 20%); // #333 -@gray: lighten(@gray-base, 33.5%); // #555 -@gray-light: lighten(@gray-base, 46.7%); // #777 -@gray-lighter: lighten(@gray-base, 93.5%); // #eee - -@brand-primary: darken(#428bca, 6.5%); // #337ab7 -@brand-success: #5cb85c; -@brand-info: #5bc0de; -@brand-warning: #f0ad4e; -@brand-danger: #d9534f; - - -//== Scaffolding -// -//## Settings for some of the most global styles. - -//** Background color for `<body>`. -@body-bg: #fff; -//** Global text color on `<body>`. -@text-color: @gray-dark; - -//** Global textual link color. -@link-color: @brand-primary; -//** Link hover color set via `darken()` function. -@link-hover-color: darken(@link-color, 15%); -//** Link hover decoration. -@link-hover-decoration: underline; - - -//== Typography -// -//## Font, line-height, and color for body text, headings, and more. - -@font-family-sans-serif: "Helvetica Neue", Helvetica, Arial, sans-serif; -@font-family-serif: Georgia, "Times New Roman", Times, serif; -//** Default monospace fonts for `<code>`, `<kbd>`, and `<pre>`. -@font-family-monospace: Menlo, Monaco, Consolas, "Courier New", monospace; -@font-family-base: @font-family-sans-serif; - -@font-size-base: 14px; -@font-size-large: ceil((@font-size-base * 1.25)); // ~18px -@font-size-small: ceil((@font-size-base * 0.85)); // ~12px - -@font-size-h1: floor((@font-size-base * 2.6)); // ~36px -@font-size-h2: floor((@font-size-base * 2.15)); // ~30px -@font-size-h3: ceil((@font-size-base * 1.7)); // ~24px -@font-size-h4: ceil((@font-size-base * 1.25)); // ~18px -@font-size-h5: @font-size-base; -@font-size-h6: ceil((@font-size-base * 0.85)); // ~12px - -//** Unit-less `line-height` for use in components like buttons. -@line-height-base: 1.428571429; // 20/14 -//** Computed "line-height" (`font-size` * `line-height`) for use with `margin`, `padding`, etc. -@line-height-computed: floor((@font-size-base * @line-height-base)); // ~20px - -//** By default, this inherits from the `<body>`. -@headings-font-family: inherit; -@headings-font-weight: 500; -@headings-line-height: 1.1; -@headings-color: inherit; - - -//== Iconography -// -//## Specify custom location and filename of the included Glyphicons icon font. Useful for those including Bootstrap via Bower. - -//** Load fonts from this directory. -@icon-font-path: "../fonts/"; -//** File name for all font files. -@icon-font-name: "glyphicons-halflings-regular"; -//** Element ID within SVG icon file. -@icon-font-svg-id: "glyphicons_halflingsregular"; - - -//== Components -// -//## Define common padding and border radius sizes and more. Values based on 14px text and 1.428 line-height (~20px to start). - -@padding-base-vertical: 6px; -@padding-base-horizontal: 12px; - -@padding-large-vertical: 10px; -@padding-large-horizontal: 16px; - -@padding-small-vertical: 5px; -@padding-small-horizontal: 10px; - -@padding-xs-vertical: 1px; -@padding-xs-horizontal: 5px; - -@line-height-large: 1.3333333; // extra decimals for Win 8.1 Chrome -@line-height-small: 1.5; - -@border-radius-base: 4px; -@border-radius-large: 6px; -@border-radius-small: 3px; - -//** Global color for active items (e.g., navs or dropdowns). -@component-active-color: #fff; -//** Global background color for active items (e.g., navs or dropdowns). -@component-active-bg: @brand-primary; - -//** Width of the `border` for generating carets that indicator dropdowns. -@caret-width-base: 4px; -//** Carets increase slightly in size for larger components. -@caret-width-large: 5px; - - -//== Tables -// -//## Customizes the `.table` component with basic values, each used across all table variations. - -//** Padding for `<th>`s and `<td>`s. -@table-cell-padding: 8px; -//** Padding for cells in `.table-condensed`. -@table-condensed-cell-padding: 5px; - -//** Default background color used for all tables. -@table-bg: transparent; -//** Background color used for `.table-striped`. -@table-bg-accent: #f9f9f9; -//** Background color used for `.table-hover`. -@table-bg-hover: #f5f5f5; -@table-bg-active: @table-bg-hover; - -//** Border color for table and cell borders. -@table-border-color: #ddd; - - -//== Buttons -// -//## For each of Bootstrap's buttons, define text, background and border color. - -@btn-font-weight: normal; - -@btn-default-color: #333; -@btn-default-bg: #fff; -@btn-default-border: #ccc; - -@btn-primary-color: #fff; -@btn-primary-bg: @brand-primary; -@btn-primary-border: darken(@btn-primary-bg, 5%); - -@btn-success-color: #fff; -@btn-success-bg: @brand-success; -@btn-success-border: darken(@btn-success-bg, 5%); - -@btn-info-color: #fff; -@btn-info-bg: @brand-info; -@btn-info-border: darken(@btn-info-bg, 5%); - -@btn-warning-color: #fff; -@btn-warning-bg: @brand-warning; -@btn-warning-border: darken(@btn-warning-bg, 5%); - -@btn-danger-color: #fff; -@btn-danger-bg: @brand-danger; -@btn-danger-border: darken(@btn-danger-bg, 5%); - -@btn-link-disabled-color: @gray-light; - -// Allows for customizing button radius independently from global border radius -@btn-border-radius-base: @border-radius-base; -@btn-border-radius-large: @border-radius-large; -@btn-border-radius-small: @border-radius-small; - - -//== Forms -// -//## - -//** `<input>` background color -@input-bg: #fff; -//** `<input disabled>` background color -@input-bg-disabled: @gray-lighter; - -//** Text color for `<input>`s -@input-color: @gray; -//** `<input>` border color -@input-border: #ccc; - -// TODO: Rename `@input-border-radius` to `@input-border-radius-base` in v4 -//** Default `.form-control` border radius -// This has no effect on `<select>`s in some browsers, due to the limited stylability of `<select>`s in CSS. -@input-border-radius: @border-radius-base; -//** Large `.form-control` border radius -@input-border-radius-large: @border-radius-large; -//** Small `.form-control` border radius -@input-border-radius-small: @border-radius-small; - -//** Border color for inputs on focus -@input-border-focus: #66afe9; - -//** Placeholder text color -@input-color-placeholder: #999; - -//** Default `.form-control` height -@input-height-base: (@line-height-computed + (@padding-base-vertical * 2) + 2); -//** Large `.form-control` height -@input-height-large: (ceil(@font-size-large * @line-height-large) + (@padding-large-vertical * 2) + 2); -//** Small `.form-control` height -@input-height-small: (floor(@font-size-small * @line-height-small) + (@padding-small-vertical * 2) + 2); - -//** `.form-group` margin -@form-group-margin-bottom: 15px; - -@legend-color: @gray-dark; -@legend-border-color: #e5e5e5; - -//** Background color for textual input addons -@input-group-addon-bg: @gray-lighter; -//** Border color for textual input addons -@input-group-addon-border-color: @input-border; - -//** Disabled cursor for form controls and buttons. -@cursor-disabled: not-allowed; - - -//== Dropdowns -// -//## Dropdown menu container and contents. - -//** Background for the dropdown menu. -@dropdown-bg: #fff; -//** Dropdown menu `border-color`. -@dropdown-border: rgba(0,0,0,.15); -//** Dropdown menu `border-color` **for IE8**. -@dropdown-fallback-border: #ccc; -//** Divider color for between dropdown items. -@dropdown-divider-bg: #e5e5e5; - -//** Dropdown link text color. -@dropdown-link-color: @gray-dark; -//** Hover color for dropdown links. -@dropdown-link-hover-color: darken(@gray-dark, 5%); -//** Hover background for dropdown links. -@dropdown-link-hover-bg: #f5f5f5; - -//** Active dropdown menu item text color. -@dropdown-link-active-color: @component-active-color; -//** Active dropdown menu item background color. -@dropdown-link-active-bg: @component-active-bg; - -//** Disabled dropdown menu item background color. -@dropdown-link-disabled-color: @gray-light; - -//** Text color for headers within dropdown menus. -@dropdown-header-color: @gray-light; - -//** Deprecated `@dropdown-caret-color` as of v3.1.0 -@dropdown-caret-color: #000; - - -//-- Z-index master list -// -// Warning: Avoid customizing these values. They're used for a bird's eye view -// of components dependent on the z-axis and are designed to all work together. -// -// Note: These variables are not generated into the Customizer. - -@zindex-navbar: 1000; -@zindex-dropdown: 1000; -@zindex-popover: 1060; -@zindex-tooltip: 1070; -@zindex-navbar-fixed: 1030; -@zindex-modal-background: 1040; -@zindex-modal: 1050; - - -//== Media queries breakpoints -// -//## Define the breakpoints at which your layout will change, adapting to different screen sizes. - -// Extra small screen / phone -//** Deprecated `@screen-xs` as of v3.0.1 -@screen-xs: 480px; -//** Deprecated `@screen-xs-min` as of v3.2.0 -@screen-xs-min: @screen-xs; -//** Deprecated `@screen-phone` as of v3.0.1 -@screen-phone: @screen-xs-min; - -// Small screen / tablet -//** Deprecated `@screen-sm` as of v3.0.1 -@screen-sm: 768px; -@screen-sm-min: @screen-sm; -//** Deprecated `@screen-tablet` as of v3.0.1 -@screen-tablet: @screen-sm-min; - -// Medium screen / desktop -//** Deprecated `@screen-md` as of v3.0.1 -@screen-md: 992px; -@screen-md-min: @screen-md; -//** Deprecated `@screen-desktop` as of v3.0.1 -@screen-desktop: @screen-md-min; - -// Large screen / wide desktop -//** Deprecated `@screen-lg` as of v3.0.1 -@screen-lg: 1200px; -@screen-lg-min: @screen-lg; -//** Deprecated `@screen-lg-desktop` as of v3.0.1 -@screen-lg-desktop: @screen-lg-min; - -// So media queries don't overlap when required, provide a maximum -@screen-xs-max: (@screen-sm-min - 1); -@screen-sm-max: (@screen-md-min - 1); -@screen-md-max: (@screen-lg-min - 1); - - -//== Grid system -// -//## Define your custom responsive grid. - -//** Number of columns in the grid. -@grid-columns: 12; -//** Padding between columns. Gets divided in half for the left and right. -@grid-gutter-width: 30px; -// Navbar collapse -//** Point at which the navbar becomes uncollapsed. -@grid-float-breakpoint: @screen-sm-min; -//** Point at which the navbar begins collapsing. -@grid-float-breakpoint-max: (@grid-float-breakpoint - 1); - - -//== Container sizes -// -//## Define the maximum width of `.container` for different screen sizes. - -// Small screen / tablet -@container-tablet: (720px + @grid-gutter-width); -//** For `@screen-sm-min` and up. -@container-sm: @container-tablet; - -// Medium screen / desktop -@container-desktop: (940px + @grid-gutter-width); -//** For `@screen-md-min` and up. -@container-md: @container-desktop; - -// Large screen / wide desktop -@container-large-desktop: (1140px + @grid-gutter-width); -//** For `@screen-lg-min` and up. -@container-lg: @container-large-desktop; - - -//== Navbar -// -//## - -// Basics of a navbar -@navbar-height: 50px; -@navbar-margin-bottom: @line-height-computed; -@navbar-border-radius: @border-radius-base; -@navbar-padding-horizontal: floor((@grid-gutter-width / 2)); -@navbar-padding-vertical: ((@navbar-height - @line-height-computed) / 2); -@navbar-collapse-max-height: 340px; - -@navbar-default-color: #777; -@navbar-default-bg: #f8f8f8; -@navbar-default-border: darken(@navbar-default-bg, 6.5%); - -// Navbar links -@navbar-default-link-color: #777; -@navbar-default-link-hover-color: #333; -@navbar-default-link-hover-bg: transparent; -@navbar-default-link-active-color: #555; -@navbar-default-link-active-bg: darken(@navbar-default-bg, 6.5%); -@navbar-default-link-disabled-color: #ccc; -@navbar-default-link-disabled-bg: transparent; - -// Navbar brand label -@navbar-default-brand-color: @navbar-default-link-color; -@navbar-default-brand-hover-color: darken(@navbar-default-brand-color, 10%); -@navbar-default-brand-hover-bg: transparent; - -// Navbar toggle -@navbar-default-toggle-hover-bg: #ddd; -@navbar-default-toggle-icon-bar-bg: #888; -@navbar-default-toggle-border-color: #ddd; - - -//=== Inverted navbar -// Reset inverted navbar basics -@navbar-inverse-color: lighten(@gray-light, 15%); -@navbar-inverse-bg: #222; -@navbar-inverse-border: darken(@navbar-inverse-bg, 10%); - -// Inverted navbar links -@navbar-inverse-link-color: lighten(@gray-light, 15%); -@navbar-inverse-link-hover-color: #fff; -@navbar-inverse-link-hover-bg: transparent; -@navbar-inverse-link-active-color: @navbar-inverse-link-hover-color; -@navbar-inverse-link-active-bg: darken(@navbar-inverse-bg, 10%); -@navbar-inverse-link-disabled-color: #444; -@navbar-inverse-link-disabled-bg: transparent; - -// Inverted navbar brand label -@navbar-inverse-brand-color: @navbar-inverse-link-color; -@navbar-inverse-brand-hover-color: #fff; -@navbar-inverse-brand-hover-bg: transparent; - -// Inverted navbar toggle -@navbar-inverse-toggle-hover-bg: #333; -@navbar-inverse-toggle-icon-bar-bg: #fff; -@navbar-inverse-toggle-border-color: #333; - - -//== Navs -// -//## - -//=== Shared nav styles -@nav-link-padding: 10px 15px; -@nav-link-hover-bg: @gray-lighter; - -@nav-disabled-link-color: @gray-light; -@nav-disabled-link-hover-color: @gray-light; - -//== Tabs -@nav-tabs-border-color: #ddd; - -@nav-tabs-link-hover-border-color: @gray-lighter; - -@nav-tabs-active-link-hover-bg: @body-bg; -@nav-tabs-active-link-hover-color: @gray; -@nav-tabs-active-link-hover-border-color: #ddd; - -@nav-tabs-justified-link-border-color: #ddd; -@nav-tabs-justified-active-link-border-color: @body-bg; - -//== Pills -@nav-pills-border-radius: @border-radius-base; -@nav-pills-active-link-hover-bg: @component-active-bg; -@nav-pills-active-link-hover-color: @component-active-color; - - -//== Pagination -// -//## - -@pagination-color: @link-color; -@pagination-bg: #fff; -@pagination-border: #ddd; - -@pagination-hover-color: @link-hover-color; -@pagination-hover-bg: @gray-lighter; -@pagination-hover-border: #ddd; - -@pagination-active-color: #fff; -@pagination-active-bg: @brand-primary; -@pagination-active-border: @brand-primary; - -@pagination-disabled-color: @gray-light; -@pagination-disabled-bg: #fff; -@pagination-disabled-border: #ddd; - - -//== Pager -// -//## - -@pager-bg: @pagination-bg; -@pager-border: @pagination-border; -@pager-border-radius: 15px; - -@pager-hover-bg: @pagination-hover-bg; - -@pager-active-bg: @pagination-active-bg; -@pager-active-color: @pagination-active-color; - -@pager-disabled-color: @pagination-disabled-color; - - -//== Jumbotron -// -//## - -@jumbotron-padding: 30px; -@jumbotron-color: inherit; -@jumbotron-bg: @gray-lighter; -@jumbotron-heading-color: inherit; -@jumbotron-font-size: ceil((@font-size-base * 1.5)); -@jumbotron-heading-font-size: ceil((@font-size-base * 4.5)); - - -//== Form states and alerts -// -//## Define colors for form feedback states and, by default, alerts. - -@state-success-text: #3c763d; -@state-success-bg: #dff0d8; -@state-success-border: darken(spin(@state-success-bg, -10), 5%); - -@state-info-text: #31708f; -@state-info-bg: #d9edf7; -@state-info-border: darken(spin(@state-info-bg, -10), 7%); - -@state-warning-text: #8a6d3b; -@state-warning-bg: #fcf8e3; -@state-warning-border: darken(spin(@state-warning-bg, -10), 5%); - -@state-danger-text: #a94442; -@state-danger-bg: #f2dede; -@state-danger-border: darken(spin(@state-danger-bg, -10), 5%); - - -//== Tooltips -// -//## - -//** Tooltip max width -@tooltip-max-width: 200px; -//** Tooltip text color -@tooltip-color: #fff; -//** Tooltip background color -@tooltip-bg: #000; -@tooltip-opacity: .9; - -//** Tooltip arrow width -@tooltip-arrow-width: 5px; -//** Tooltip arrow color -@tooltip-arrow-color: @tooltip-bg; - - -//== Popovers -// -//## - -//** Popover body background color -@popover-bg: #fff; -//** Popover maximum width -@popover-max-width: 276px; -//** Popover border color -@popover-border-color: rgba(0,0,0,.2); -//** Popover fallback border color -@popover-fallback-border-color: #ccc; - -//** Popover title background color -@popover-title-bg: darken(@popover-bg, 3%); - -//** Popover arrow width -@popover-arrow-width: 10px; -//** Popover arrow color -@popover-arrow-color: @popover-bg; - -//** Popover outer arrow width -@popover-arrow-outer-width: (@popover-arrow-width + 1); -//** Popover outer arrow color -@popover-arrow-outer-color: fadein(@popover-border-color, 5%); -//** Popover outer arrow fallback color -@popover-arrow-outer-fallback-color: darken(@popover-fallback-border-color, 20%); - - -//== Labels -// -//## - -//** Default label background color -@label-default-bg: @gray-light; -//** Primary label background color -@label-primary-bg: @brand-primary; -//** Success label background color -@label-success-bg: @brand-success; -//** Info label background color -@label-info-bg: @brand-info; -//** Warning label background color -@label-warning-bg: @brand-warning; -//** Danger label background color -@label-danger-bg: @brand-danger; - -//** Default label text color -@label-color: #fff; -//** Default text color of a linked label -@label-link-hover-color: #fff; - - -//== Modals -// -//## - -//** Padding applied to the modal body -@modal-inner-padding: 15px; - -//** Padding applied to the modal title -@modal-title-padding: 15px; -//** Modal title line-height -@modal-title-line-height: @line-height-base; - -//** Background color of modal content area -@modal-content-bg: #fff; -//** Modal content border color -@modal-content-border-color: rgba(0,0,0,.2); -//** Modal content border color **for IE8** -@modal-content-fallback-border-color: #999; - -//** Modal backdrop background color -@modal-backdrop-bg: #000; -//** Modal backdrop opacity -@modal-backdrop-opacity: .5; -//** Modal header border color -@modal-header-border-color: #e5e5e5; -//** Modal footer border color -@modal-footer-border-color: @modal-header-border-color; - -@modal-lg: 900px; -@modal-md: 600px; -@modal-sm: 300px; - - -//== Alerts -// -//## Define alert colors, border radius, and padding. - -@alert-padding: 15px; -@alert-border-radius: @border-radius-base; -@alert-link-font-weight: bold; - -@alert-success-bg: @state-success-bg; -@alert-success-text: @state-success-text; -@alert-success-border: @state-success-border; - -@alert-info-bg: @state-info-bg; -@alert-info-text: @state-info-text; -@alert-info-border: @state-info-border; - -@alert-warning-bg: @state-warning-bg; -@alert-warning-text: @state-warning-text; -@alert-warning-border: @state-warning-border; - -@alert-danger-bg: @state-danger-bg; -@alert-danger-text: @state-danger-text; -@alert-danger-border: @state-danger-border; - - -//== Progress bars -// -//## - -//** Background color of the whole progress component -@progress-bg: #f5f5f5; -//** Progress bar text color -@progress-bar-color: #fff; -//** Variable for setting rounded corners on progress bar. -@progress-border-radius: @border-radius-base; - -//** Default progress bar color -@progress-bar-bg: @brand-primary; -//** Success progress bar color -@progress-bar-success-bg: @brand-success; -//** Warning progress bar color -@progress-bar-warning-bg: @brand-warning; -//** Danger progress bar color -@progress-bar-danger-bg: @brand-danger; -//** Info progress bar color -@progress-bar-info-bg: @brand-info; - - -//== List group -// -//## - -//** Background color on `.list-group-item` -@list-group-bg: #fff; -//** `.list-group-item` border color -@list-group-border: #ddd; -//** List group border radius -@list-group-border-radius: @border-radius-base; - -//** Background color of single list items on hover -@list-group-hover-bg: #f5f5f5; -//** Text color of active list items -@list-group-active-color: @component-active-color; -//** Background color of active list items -@list-group-active-bg: @component-active-bg; -//** Border color of active list elements -@list-group-active-border: @list-group-active-bg; -//** Text color for content within active list items -@list-group-active-text-color: lighten(@list-group-active-bg, 40%); - -//** Text color of disabled list items -@list-group-disabled-color: @gray-light; -//** Background color of disabled list items -@list-group-disabled-bg: @gray-lighter; -//** Text color for content within disabled list items -@list-group-disabled-text-color: @list-group-disabled-color; - -@list-group-link-color: #555; -@list-group-link-hover-color: @list-group-link-color; -@list-group-link-heading-color: #333; - - -//== Panels -// -//## - -@panel-bg: #fff; -@panel-body-padding: 15px; -@panel-heading-padding: 10px 15px; -@panel-footer-padding: @panel-heading-padding; -@panel-border-radius: @border-radius-base; - -//** Border color for elements within panels -@panel-inner-border: #ddd; -@panel-footer-bg: #f5f5f5; - -@panel-default-text: @gray-dark; -@panel-default-border: #ddd; -@panel-default-heading-bg: #f5f5f5; - -@panel-primary-text: #fff; -@panel-primary-border: @brand-primary; -@panel-primary-heading-bg: @brand-primary; - -@panel-success-text: @state-success-text; -@panel-success-border: @state-success-border; -@panel-success-heading-bg: @state-success-bg; - -@panel-info-text: @state-info-text; -@panel-info-border: @state-info-border; -@panel-info-heading-bg: @state-info-bg; - -@panel-warning-text: @state-warning-text; -@panel-warning-border: @state-warning-border; -@panel-warning-heading-bg: @state-warning-bg; - -@panel-danger-text: @state-danger-text; -@panel-danger-border: @state-danger-border; -@panel-danger-heading-bg: @state-danger-bg; - - -//== Thumbnails -// -//## - -//** Padding around the thumbnail image -@thumbnail-padding: 4px; -//** Thumbnail background color -@thumbnail-bg: @body-bg; -//** Thumbnail border color -@thumbnail-border: #ddd; -//** Thumbnail border radius -@thumbnail-border-radius: @border-radius-base; - -//** Custom text color for thumbnail captions -@thumbnail-caption-color: @text-color; -//** Padding around the thumbnail caption -@thumbnail-caption-padding: 9px; - - -//== Wells -// -//## - -@well-bg: #f5f5f5; -@well-border: darken(@well-bg, 7%); - - -//== Badges -// -//## - -@badge-color: #fff; -//** Linked badge text color on hover -@badge-link-hover-color: #fff; -@badge-bg: @gray-light; - -//** Badge text color in active nav link -@badge-active-color: @link-color; -//** Badge background color in active nav link -@badge-active-bg: #fff; - -@badge-font-weight: bold; -@badge-line-height: 1; -@badge-border-radius: 10px; - - -//== Breadcrumbs -// -//## - -@breadcrumb-padding-vertical: 8px; -@breadcrumb-padding-horizontal: 15px; -//** Breadcrumb background color -@breadcrumb-bg: #f5f5f5; -//** Breadcrumb text color -@breadcrumb-color: #ccc; -//** Text color of current page in the breadcrumb -@breadcrumb-active-color: @gray-light; -//** Textual separator for between breadcrumb elements -@breadcrumb-separator: "/"; - - -//== Carousel -// -//## - -@carousel-text-shadow: 0 1px 2px rgba(0,0,0,.6); - -@carousel-control-color: #fff; -@carousel-control-width: 15%; -@carousel-control-opacity: .5; -@carousel-control-font-size: 20px; - -@carousel-indicator-active-bg: #fff; -@carousel-indicator-border-color: #fff; - -@carousel-caption-color: #fff; - - -//== Close -// -//## - -@close-font-weight: bold; -@close-color: #000; -@close-text-shadow: 0 1px 0 #fff; - - -//== Code -// -//## - -@code-color: #c7254e; -@code-bg: #f9f2f4; - -@kbd-color: #fff; -@kbd-bg: #333; - -@pre-bg: #f5f5f5; -@pre-color: @gray-dark; -@pre-border-color: #ccc; -@pre-scrollable-max-height: 340px; - - -//== Type -// -//## - -//** Horizontal offset for forms and lists. -@component-offset-horizontal: 180px; -//** Text muted color -@text-muted: @gray-light; -//** Abbreviations and acronyms border color -@abbr-border-color: @gray-light; -//** Headings small color -@headings-small-color: @gray-light; -//** Blockquote small color -@blockquote-small-color: @gray-light; -//** Blockquote font size -@blockquote-font-size: (@font-size-base * 1.25); -//** Blockquote border color -@blockquote-border-color: @gray-lighter; -//** Page header border color -@page-header-border-color: @gray-lighter; -//** Width of horizontal description list titles -@dl-horizontal-offset: @component-offset-horizontal; -//** Horizontal line color. -@hr-border: @gray-lighter; diff --git a/src/UI/Content/Bootstrap/wells.less b/src/UI/Content/Bootstrap/wells.less deleted file mode 100644 index 15d072b0c..000000000 --- a/src/UI/Content/Bootstrap/wells.less +++ /dev/null @@ -1,29 +0,0 @@ -// -// Wells -// -------------------------------------------------- - - -// Base class -.well { - min-height: 20px; - padding: 19px; - margin-bottom: 20px; - background-color: @well-bg; - border: 1px solid @well-border; - border-radius: @border-radius-base; - .box-shadow(inset 0 1px 1px rgba(0,0,0,.05)); - blockquote { - border-color: #ddd; - border-color: rgba(0,0,0,.15); - } -} - -// Sizes -.well-lg { - padding: 24px; - border-radius: @border-radius-large; -} -.well-sm { - padding: 9px; - border-radius: @border-radius-small; -} diff --git a/src/UI/Content/FontAwesome/FontAwesome.otf b/src/UI/Content/FontAwesome/FontAwesome.otf deleted file mode 100644 index f7936cc1e..000000000 Binary files a/src/UI/Content/FontAwesome/FontAwesome.otf and /dev/null differ diff --git a/src/UI/Content/FontAwesome/animated.less b/src/UI/Content/FontAwesome/animated.less deleted file mode 100644 index 66ad52a5b..000000000 --- a/src/UI/Content/FontAwesome/animated.less +++ /dev/null @@ -1,34 +0,0 @@ -// Animated Icons -// -------------------------- - -.@{fa-css-prefix}-spin { - -webkit-animation: fa-spin 2s infinite linear; - animation: fa-spin 2s infinite linear; -} - -.@{fa-css-prefix}-pulse { - -webkit-animation: fa-spin 1s infinite steps(8); - animation: fa-spin 1s infinite steps(8); -} - -@-webkit-keyframes fa-spin { - 0% { - -webkit-transform: rotate(0deg); - transform: rotate(0deg); - } - 100% { - -webkit-transform: rotate(359deg); - transform: rotate(359deg); - } -} - -@keyframes fa-spin { - 0% { - -webkit-transform: rotate(0deg); - transform: rotate(0deg); - } - 100% { - -webkit-transform: rotate(359deg); - transform: rotate(359deg); - } -} diff --git a/src/UI/Content/FontAwesome/bordered-pulled.less b/src/UI/Content/FontAwesome/bordered-pulled.less deleted file mode 100644 index 0c90eb567..000000000 --- a/src/UI/Content/FontAwesome/bordered-pulled.less +++ /dev/null @@ -1,16 +0,0 @@ -// Bordered & Pulled -// ------------------------- - -.@{fa-css-prefix}-border { - padding: .2em .25em .15em; - border: solid .08em @fa-border-color; - border-radius: .1em; -} - -.pull-right { float: right; } -.pull-left { float: left; } - -.@{fa-css-prefix} { - &.pull-left { margin-right: .3em; } - &.pull-right { margin-left: .3em; } -} diff --git a/src/UI/Content/FontAwesome/core.less b/src/UI/Content/FontAwesome/core.less deleted file mode 100644 index f814f1e17..000000000 --- a/src/UI/Content/FontAwesome/core.less +++ /dev/null @@ -1,13 +0,0 @@ -// Base Class Definition -// ------------------------- - -.@{fa-css-prefix} { - display: inline-block; - font: normal normal normal @fa-font-size-base/1 FontAwesome; // shortening font declaration - font-size: inherit; // can't have font-size inherit on line above, so need to override - text-rendering: auto; // optimizelegibility throws things off #1094 - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; - transform: translate(0, 0); // ensures no half-pixel rendering in firefox - -} diff --git a/src/UI/Content/FontAwesome/fixed-width.less b/src/UI/Content/FontAwesome/fixed-width.less deleted file mode 100644 index 110289f2f..000000000 --- a/src/UI/Content/FontAwesome/fixed-width.less +++ /dev/null @@ -1,6 +0,0 @@ -// Fixed Width Icons -// ------------------------- -.@{fa-css-prefix}-fw { - width: (18em / 14); - text-align: center; -} diff --git a/src/UI/Content/FontAwesome/font-awesome.less b/src/UI/Content/FontAwesome/font-awesome.less deleted file mode 100644 index 1f45c63d1..000000000 --- a/src/UI/Content/FontAwesome/font-awesome.less +++ /dev/null @@ -1,17 +0,0 @@ -/*! - * Font Awesome 4.3.0 by @davegandy - http://fontawesome.io - @fontawesome - * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) - */ - -@import "variables.less"; -@import "mixins.less"; -@import "path.less"; -@import "core.less"; -@import "larger.less"; -@import "fixed-width.less"; -@import "list.less"; -@import "bordered-pulled.less"; -@import "animated.less"; -@import "rotated-flipped.less"; -@import "stacked.less"; -@import "icons.less"; diff --git a/src/UI/Content/FontAwesome/fontawesome-webfont.eot b/src/UI/Content/FontAwesome/fontawesome-webfont.eot deleted file mode 100644 index 33b2bb800..000000000 Binary files a/src/UI/Content/FontAwesome/fontawesome-webfont.eot and /dev/null differ diff --git a/src/UI/Content/FontAwesome/fontawesome-webfont.svg b/src/UI/Content/FontAwesome/fontawesome-webfont.svg deleted file mode 100644 index 1ee89d436..000000000 --- a/src/UI/Content/FontAwesome/fontawesome-webfont.svg +++ /dev/null @@ -1,565 +0,0 @@ -<?xml version="1.0" standalone="no"?> -<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" > -<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1"> -<metadata></metadata> -<defs> -<font id="fontawesomeregular" horiz-adv-x="1536" > -<font-face units-per-em="1792" ascent="1536" descent="-256" /> -<missing-glyph horiz-adv-x="448" /> -<glyph unicode=" " horiz-adv-x="448" /> -<glyph unicode=" " horiz-adv-x="448" /> -<glyph unicode=" " horiz-adv-x="448" /> -<glyph unicode="¨" horiz-adv-x="1792" /> -<glyph unicode="©" horiz-adv-x="1792" /> -<glyph unicode="®" horiz-adv-x="1792" /> -<glyph unicode="´" horiz-adv-x="1792" /> -<glyph unicode="Æ" horiz-adv-x="1792" /> -<glyph unicode="Ø" horiz-adv-x="1792" /> -<glyph unicode=" " horiz-adv-x="768" /> -<glyph unicode=" " horiz-adv-x="1537" /> -<glyph unicode=" " horiz-adv-x="768" /> -<glyph unicode=" " horiz-adv-x="1537" /> -<glyph unicode=" " horiz-adv-x="512" /> -<glyph unicode=" " horiz-adv-x="384" /> -<glyph unicode=" " horiz-adv-x="256" /> -<glyph unicode=" " horiz-adv-x="256" /> -<glyph unicode=" " horiz-adv-x="192" /> -<glyph unicode=" " horiz-adv-x="307" /> -<glyph unicode=" " horiz-adv-x="85" /> -<glyph unicode=" " horiz-adv-x="307" /> -<glyph unicode=" " horiz-adv-x="384" /> -<glyph unicode="™" horiz-adv-x="1792" /> -<glyph unicode="∞" horiz-adv-x="1792" /> -<glyph unicode="≠" horiz-adv-x="1792" /> -<glyph unicode="◼" horiz-adv-x="500" d="M0 0z" /> -<glyph unicode="" horiz-adv-x="1792" d="M1699 1350q0 -35 -43 -78l-632 -632v-768h320q26 0 45 -19t19 -45t-19 -45t-45 -19h-896q-26 0 -45 19t-19 45t19 45t45 19h320v768l-632 632q-43 43 -43 78q0 23 18 36.5t38 17.5t43 4h1408q23 0 43 -4t38 -17.5t18 -36.5z" /> -<glyph unicode="" d="M1536 1312v-1120q0 -50 -34 -89t-86 -60.5t-103.5 -32t-96.5 -10.5t-96.5 10.5t-103.5 32t-86 60.5t-34 89t34 89t86 60.5t103.5 32t96.5 10.5q105 0 192 -39v537l-768 -237v-709q0 -50 -34 -89t-86 -60.5t-103.5 -32t-96.5 -10.5t-96.5 10.5t-103.5 32t-86 60.5t-34 89 t34 89t86 60.5t103.5 32t96.5 10.5q105 0 192 -39v967q0 31 19 56.5t49 35.5l832 256q12 4 28 4q40 0 68 -28t28 -68z" /> -<glyph unicode="" horiz-adv-x="1664" d="M1152 704q0 185 -131.5 316.5t-316.5 131.5t-316.5 -131.5t-131.5 -316.5t131.5 -316.5t316.5 -131.5t316.5 131.5t131.5 316.5zM1664 -128q0 -52 -38 -90t-90 -38q-54 0 -90 38l-343 342q-179 -124 -399 -124q-143 0 -273.5 55.5t-225 150t-150 225t-55.5 273.5 t55.5 273.5t150 225t225 150t273.5 55.5t273.5 -55.5t225 -150t150 -225t55.5 -273.5q0 -220 -124 -399l343 -343q37 -37 37 -90z" /> -<glyph unicode="" horiz-adv-x="1792" d="M1664 32v768q-32 -36 -69 -66q-268 -206 -426 -338q-51 -43 -83 -67t-86.5 -48.5t-102.5 -24.5h-1h-1q-48 0 -102.5 24.5t-86.5 48.5t-83 67q-158 132 -426 338q-37 30 -69 66v-768q0 -13 9.5 -22.5t22.5 -9.5h1472q13 0 22.5 9.5t9.5 22.5zM1664 1083v11v13.5t-0.5 13 t-3 12.5t-5.5 9t-9 7.5t-14 2.5h-1472q-13 0 -22.5 -9.5t-9.5 -22.5q0 -168 147 -284q193 -152 401 -317q6 -5 35 -29.5t46 -37.5t44.5 -31.5t50.5 -27.5t43 -9h1h1q20 0 43 9t50.5 27.5t44.5 31.5t46 37.5t35 29.5q208 165 401 317q54 43 100.5 115.5t46.5 131.5z M1792 1120v-1088q0 -66 -47 -113t-113 -47h-1472q-66 0 -113 47t-47 113v1088q0 66 47 113t113 47h1472q66 0 113 -47t47 -113z" /> -<glyph unicode="" horiz-adv-x="1792" d="M896 -128q-26 0 -44 18l-624 602q-10 8 -27.5 26t-55.5 65.5t-68 97.5t-53.5 121t-23.5 138q0 220 127 344t351 124q62 0 126.5 -21.5t120 -58t95.5 -68.5t76 -68q36 36 76 68t95.5 68.5t120 58t126.5 21.5q224 0 351 -124t127 -344q0 -221 -229 -450l-623 -600 q-18 -18 -44 -18z" /> -<glyph unicode="" horiz-adv-x="1664" d="M1664 889q0 -22 -26 -48l-363 -354l86 -500q1 -7 1 -20q0 -21 -10.5 -35.5t-30.5 -14.5q-19 0 -40 12l-449 236l-449 -236q-22 -12 -40 -12q-21 0 -31.5 14.5t-10.5 35.5q0 6 2 20l86 500l-364 354q-25 27 -25 48q0 37 56 46l502 73l225 455q19 41 49 41t49 -41l225 -455 l502 -73q56 -9 56 -46z" /> -<glyph unicode="" horiz-adv-x="1664" d="M1137 532l306 297l-422 62l-189 382l-189 -382l-422 -62l306 -297l-73 -421l378 199l377 -199zM1664 889q0 -22 -26 -48l-363 -354l86 -500q1 -7 1 -20q0 -50 -41 -50q-19 0 -40 12l-449 236l-449 -236q-22 -12 -40 -12q-21 0 -31.5 14.5t-10.5 35.5q0 6 2 20l86 500 l-364 354q-25 27 -25 48q0 37 56 46l502 73l225 455q19 41 49 41t49 -41l225 -455l502 -73q56 -9 56 -46z" /> -<glyph unicode="" horiz-adv-x="1408" d="M1408 131q0 -120 -73 -189.5t-194 -69.5h-874q-121 0 -194 69.5t-73 189.5q0 53 3.5 103.5t14 109t26.5 108.5t43 97.5t62 81t85.5 53.5t111.5 20q9 0 42 -21.5t74.5 -48t108 -48t133.5 -21.5t133.5 21.5t108 48t74.5 48t42 21.5q61 0 111.5 -20t85.5 -53.5t62 -81 t43 -97.5t26.5 -108.5t14 -109t3.5 -103.5zM1088 1024q0 -159 -112.5 -271.5t-271.5 -112.5t-271.5 112.5t-112.5 271.5t112.5 271.5t271.5 112.5t271.5 -112.5t112.5 -271.5z" /> -<glyph unicode="" horiz-adv-x="1920" d="M384 -64v128q0 26 -19 45t-45 19h-128q-26 0 -45 -19t-19 -45v-128q0 -26 19 -45t45 -19h128q26 0 45 19t19 45zM384 320v128q0 26 -19 45t-45 19h-128q-26 0 -45 -19t-19 -45v-128q0 -26 19 -45t45 -19h128q26 0 45 19t19 45zM384 704v128q0 26 -19 45t-45 19h-128 q-26 0 -45 -19t-19 -45v-128q0 -26 19 -45t45 -19h128q26 0 45 19t19 45zM1408 -64v512q0 26 -19 45t-45 19h-768q-26 0 -45 -19t-19 -45v-512q0 -26 19 -45t45 -19h768q26 0 45 19t19 45zM384 1088v128q0 26 -19 45t-45 19h-128q-26 0 -45 -19t-19 -45v-128q0 -26 19 -45 t45 -19h128q26 0 45 19t19 45zM1792 -64v128q0 26 -19 45t-45 19h-128q-26 0 -45 -19t-19 -45v-128q0 -26 19 -45t45 -19h128q26 0 45 19t19 45zM1408 704v512q0 26 -19 45t-45 19h-768q-26 0 -45 -19t-19 -45v-512q0 -26 19 -45t45 -19h768q26 0 45 19t19 45zM1792 320v128 q0 26 -19 45t-45 19h-128q-26 0 -45 -19t-19 -45v-128q0 -26 19 -45t45 -19h128q26 0 45 19t19 45zM1792 704v128q0 26 -19 45t-45 19h-128q-26 0 -45 -19t-19 -45v-128q0 -26 19 -45t45 -19h128q26 0 45 19t19 45zM1792 1088v128q0 26 -19 45t-45 19h-128q-26 0 -45 -19 t-19 -45v-128q0 -26 19 -45t45 -19h128q26 0 45 19t19 45zM1920 1248v-1344q0 -66 -47 -113t-113 -47h-1600q-66 0 -113 47t-47 113v1344q0 66 47 113t113 47h1600q66 0 113 -47t47 -113z" /> -<glyph unicode="" horiz-adv-x="1664" d="M768 512v-384q0 -52 -38 -90t-90 -38h-512q-52 0 -90 38t-38 90v384q0 52 38 90t90 38h512q52 0 90 -38t38 -90zM768 1280v-384q0 -52 -38 -90t-90 -38h-512q-52 0 -90 38t-38 90v384q0 52 38 90t90 38h512q52 0 90 -38t38 -90zM1664 512v-384q0 -52 -38 -90t-90 -38 h-512q-52 0 -90 38t-38 90v384q0 52 38 90t90 38h512q52 0 90 -38t38 -90zM1664 1280v-384q0 -52 -38 -90t-90 -38h-512q-52 0 -90 38t-38 90v384q0 52 38 90t90 38h512q52 0 90 -38t38 -90z" /> -<glyph unicode="" horiz-adv-x="1792" d="M512 288v-192q0 -40 -28 -68t-68 -28h-320q-40 0 -68 28t-28 68v192q0 40 28 68t68 28h320q40 0 68 -28t28 -68zM512 800v-192q0 -40 -28 -68t-68 -28h-320q-40 0 -68 28t-28 68v192q0 40 28 68t68 28h320q40 0 68 -28t28 -68zM1152 288v-192q0 -40 -28 -68t-68 -28h-320 q-40 0 -68 28t-28 68v192q0 40 28 68t68 28h320q40 0 68 -28t28 -68zM512 1312v-192q0 -40 -28 -68t-68 -28h-320q-40 0 -68 28t-28 68v192q0 40 28 68t68 28h320q40 0 68 -28t28 -68zM1152 800v-192q0 -40 -28 -68t-68 -28h-320q-40 0 -68 28t-28 68v192q0 40 28 68t68 28 h320q40 0 68 -28t28 -68zM1792 288v-192q0 -40 -28 -68t-68 -28h-320q-40 0 -68 28t-28 68v192q0 40 28 68t68 28h320q40 0 68 -28t28 -68zM1152 1312v-192q0 -40 -28 -68t-68 -28h-320q-40 0 -68 28t-28 68v192q0 40 28 68t68 28h320q40 0 68 -28t28 -68zM1792 800v-192 q0 -40 -28 -68t-68 -28h-320q-40 0 -68 28t-28 68v192q0 40 28 68t68 28h320q40 0 68 -28t28 -68zM1792 1312v-192q0 -40 -28 -68t-68 -28h-320q-40 0 -68 28t-28 68v192q0 40 28 68t68 28h320q40 0 68 -28t28 -68z" /> -<glyph unicode="" horiz-adv-x="1792" d="M512 288v-192q0 -40 -28 -68t-68 -28h-320q-40 0 -68 28t-28 68v192q0 40 28 68t68 28h320q40 0 68 -28t28 -68zM512 800v-192q0 -40 -28 -68t-68 -28h-320q-40 0 -68 28t-28 68v192q0 40 28 68t68 28h320q40 0 68 -28t28 -68zM1792 288v-192q0 -40 -28 -68t-68 -28h-960 q-40 0 -68 28t-28 68v192q0 40 28 68t68 28h960q40 0 68 -28t28 -68zM512 1312v-192q0 -40 -28 -68t-68 -28h-320q-40 0 -68 28t-28 68v192q0 40 28 68t68 28h320q40 0 68 -28t28 -68zM1792 800v-192q0 -40 -28 -68t-68 -28h-960q-40 0 -68 28t-28 68v192q0 40 28 68t68 28 h960q40 0 68 -28t28 -68zM1792 1312v-192q0 -40 -28 -68t-68 -28h-960q-40 0 -68 28t-28 68v192q0 40 28 68t68 28h960q40 0 68 -28t28 -68z" /> -<glyph unicode="" horiz-adv-x="1792" d="M1671 970q0 -40 -28 -68l-724 -724l-136 -136q-28 -28 -68 -28t-68 28l-136 136l-362 362q-28 28 -28 68t28 68l136 136q28 28 68 28t68 -28l294 -295l656 657q28 28 68 28t68 -28l136 -136q28 -28 28 -68z" /> -<glyph unicode="" horiz-adv-x="1408" d="M1298 214q0 -40 -28 -68l-136 -136q-28 -28 -68 -28t-68 28l-294 294l-294 -294q-28 -28 -68 -28t-68 28l-136 136q-28 28 -28 68t28 68l294 294l-294 294q-28 28 -28 68t28 68l136 136q28 28 68 28t68 -28l294 -294l294 294q28 28 68 28t68 -28l136 -136q28 -28 28 -68 t-28 -68l-294 -294l294 -294q28 -28 28 -68z" /> -<glyph unicode="" horiz-adv-x="1664" d="M1024 736v-64q0 -13 -9.5 -22.5t-22.5 -9.5h-224v-224q0 -13 -9.5 -22.5t-22.5 -9.5h-64q-13 0 -22.5 9.5t-9.5 22.5v224h-224q-13 0 -22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h224v224q0 13 9.5 22.5t22.5 9.5h64q13 0 22.5 -9.5t9.5 -22.5v-224h224 q13 0 22.5 -9.5t9.5 -22.5zM1152 704q0 185 -131.5 316.5t-316.5 131.5t-316.5 -131.5t-131.5 -316.5t131.5 -316.5t316.5 -131.5t316.5 131.5t131.5 316.5zM1664 -128q0 -53 -37.5 -90.5t-90.5 -37.5q-54 0 -90 38l-343 342q-179 -124 -399 -124q-143 0 -273.5 55.5 t-225 150t-150 225t-55.5 273.5t55.5 273.5t150 225t225 150t273.5 55.5t273.5 -55.5t225 -150t150 -225t55.5 -273.5q0 -220 -124 -399l343 -343q37 -37 37 -90z" /> -<glyph unicode="" horiz-adv-x="1664" d="M1024 736v-64q0 -13 -9.5 -22.5t-22.5 -9.5h-576q-13 0 -22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h576q13 0 22.5 -9.5t9.5 -22.5zM1152 704q0 185 -131.5 316.5t-316.5 131.5t-316.5 -131.5t-131.5 -316.5t131.5 -316.5t316.5 -131.5t316.5 131.5t131.5 316.5z M1664 -128q0 -53 -37.5 -90.5t-90.5 -37.5q-54 0 -90 38l-343 342q-179 -124 -399 -124q-143 0 -273.5 55.5t-225 150t-150 225t-55.5 273.5t55.5 273.5t150 225t225 150t273.5 55.5t273.5 -55.5t225 -150t150 -225t55.5 -273.5q0 -220 -124 -399l343 -343q37 -37 37 -90z " /> -<glyph unicode="" d="M1536 640q0 -156 -61 -298t-164 -245t-245 -164t-298 -61t-298 61t-245 164t-164 245t-61 298q0 182 80.5 343t226.5 270q43 32 95.5 25t83.5 -50q32 -42 24.5 -94.5t-49.5 -84.5q-98 -74 -151.5 -181t-53.5 -228q0 -104 40.5 -198.5t109.5 -163.5t163.5 -109.5 t198.5 -40.5t198.5 40.5t163.5 109.5t109.5 163.5t40.5 198.5q0 121 -53.5 228t-151.5 181q-42 32 -49.5 84.5t24.5 94.5q31 43 84 50t95 -25q146 -109 226.5 -270t80.5 -343zM896 1408v-640q0 -52 -38 -90t-90 -38t-90 38t-38 90v640q0 52 38 90t90 38t90 -38t38 -90z" /> -<glyph unicode="" horiz-adv-x="1792" d="M256 96v-192q0 -14 -9 -23t-23 -9h-192q-14 0 -23 9t-9 23v192q0 14 9 23t23 9h192q14 0 23 -9t9 -23zM640 224v-320q0 -14 -9 -23t-23 -9h-192q-14 0 -23 9t-9 23v320q0 14 9 23t23 9h192q14 0 23 -9t9 -23zM1024 480v-576q0 -14 -9 -23t-23 -9h-192q-14 0 -23 9t-9 23 v576q0 14 9 23t23 9h192q14 0 23 -9t9 -23zM1408 864v-960q0 -14 -9 -23t-23 -9h-192q-14 0 -23 9t-9 23v960q0 14 9 23t23 9h192q14 0 23 -9t9 -23zM1792 1376v-1472q0 -14 -9 -23t-23 -9h-192q-14 0 -23 9t-9 23v1472q0 14 9 23t23 9h192q14 0 23 -9t9 -23z" /> -<glyph unicode="" d="M1024 640q0 106 -75 181t-181 75t-181 -75t-75 -181t75 -181t181 -75t181 75t75 181zM1536 749v-222q0 -12 -8 -23t-20 -13l-185 -28q-19 -54 -39 -91q35 -50 107 -138q10 -12 10 -25t-9 -23q-27 -37 -99 -108t-94 -71q-12 0 -26 9l-138 108q-44 -23 -91 -38 q-16 -136 -29 -186q-7 -28 -36 -28h-222q-14 0 -24.5 8.5t-11.5 21.5l-28 184q-49 16 -90 37l-141 -107q-10 -9 -25 -9q-14 0 -25 11q-126 114 -165 168q-7 10 -7 23q0 12 8 23q15 21 51 66.5t54 70.5q-27 50 -41 99l-183 27q-13 2 -21 12.5t-8 23.5v222q0 12 8 23t19 13 l186 28q14 46 39 92q-40 57 -107 138q-10 12 -10 24q0 10 9 23q26 36 98.5 107.5t94.5 71.5q13 0 26 -10l138 -107q44 23 91 38q16 136 29 186q7 28 36 28h222q14 0 24.5 -8.5t11.5 -21.5l28 -184q49 -16 90 -37l142 107q9 9 24 9q13 0 25 -10q129 -119 165 -170q7 -8 7 -22 q0 -12 -8 -23q-15 -21 -51 -66.5t-54 -70.5q26 -50 41 -98l183 -28q13 -2 21 -12.5t8 -23.5z" /> -<glyph unicode="" horiz-adv-x="1408" d="M512 800v-576q0 -14 -9 -23t-23 -9h-64q-14 0 -23 9t-9 23v576q0 14 9 23t23 9h64q14 0 23 -9t9 -23zM768 800v-576q0 -14 -9 -23t-23 -9h-64q-14 0 -23 9t-9 23v576q0 14 9 23t23 9h64q14 0 23 -9t9 -23zM1024 800v-576q0 -14 -9 -23t-23 -9h-64q-14 0 -23 9t-9 23v576 q0 14 9 23t23 9h64q14 0 23 -9t9 -23zM1152 76v948h-896v-948q0 -22 7 -40.5t14.5 -27t10.5 -8.5h832q3 0 10.5 8.5t14.5 27t7 40.5zM480 1152h448l-48 117q-7 9 -17 11h-317q-10 -2 -17 -11zM1408 1120v-64q0 -14 -9 -23t-23 -9h-96v-948q0 -83 -47 -143.5t-113 -60.5h-832 q-66 0 -113 58.5t-47 141.5v952h-96q-14 0 -23 9t-9 23v64q0 14 9 23t23 9h309l70 167q15 37 54 63t79 26h320q40 0 79 -26t54 -63l70 -167h309q14 0 23 -9t9 -23z" /> -<glyph unicode="" horiz-adv-x="1664" d="M1408 544v-480q0 -26 -19 -45t-45 -19h-384v384h-256v-384h-384q-26 0 -45 19t-19 45v480q0 1 0.5 3t0.5 3l575 474l575 -474q1 -2 1 -6zM1631 613l-62 -74q-8 -9 -21 -11h-3q-13 0 -21 7l-692 577l-692 -577q-12 -8 -24 -7q-13 2 -21 11l-62 74q-8 10 -7 23.5t11 21.5 l719 599q32 26 76 26t76 -26l244 -204v195q0 14 9 23t23 9h192q14 0 23 -9t9 -23v-408l219 -182q10 -8 11 -21.5t-7 -23.5z" /> -<glyph unicode="" d="M1468 1156q28 -28 48 -76t20 -88v-1152q0 -40 -28 -68t-68 -28h-1344q-40 0 -68 28t-28 68v1600q0 40 28 68t68 28h896q40 0 88 -20t76 -48zM1024 1400v-376h376q-10 29 -22 41l-313 313q-12 12 -41 22zM1408 -128v1024h-416q-40 0 -68 28t-28 68v416h-768v-1536h1280z " /> -<glyph unicode="" d="M896 992v-448q0 -14 -9 -23t-23 -9h-320q-14 0 -23 9t-9 23v64q0 14 9 23t23 9h224v352q0 14 9 23t23 9h64q14 0 23 -9t9 -23zM1312 640q0 148 -73 273t-198 198t-273 73t-273 -73t-198 -198t-73 -273t73 -273t198 -198t273 -73t273 73t198 198t73 273zM1536 640 q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5t103 -385.5z" /> -<glyph unicode="" horiz-adv-x="1920" d="M1111 540v4l-24 320q-1 13 -11 22.5t-23 9.5h-186q-13 0 -23 -9.5t-11 -22.5l-24 -320v-4q-1 -12 8 -20t21 -8h244q12 0 21 8t8 20zM1870 73q0 -73 -46 -73h-704q13 0 22 9.5t8 22.5l-20 256q-1 13 -11 22.5t-23 9.5h-272q-13 0 -23 -9.5t-11 -22.5l-20 -256 q-1 -13 8 -22.5t22 -9.5h-704q-46 0 -46 73q0 54 26 116l417 1044q8 19 26 33t38 14h339q-13 0 -23 -9.5t-11 -22.5l-15 -192q-1 -14 8 -23t22 -9h166q13 0 22 9t8 23l-15 192q-1 13 -11 22.5t-23 9.5h339q20 0 38 -14t26 -33l417 -1044q26 -62 26 -116z" /> -<glyph unicode="" horiz-adv-x="1664" d="M1280 192q0 26 -19 45t-45 19t-45 -19t-19 -45t19 -45t45 -19t45 19t19 45zM1536 192q0 26 -19 45t-45 19t-45 -19t-19 -45t19 -45t45 -19t45 19t19 45zM1664 416v-320q0 -40 -28 -68t-68 -28h-1472q-40 0 -68 28t-28 68v320q0 40 28 68t68 28h465l135 -136 q58 -56 136 -56t136 56l136 136h464q40 0 68 -28t28 -68zM1339 985q17 -41 -14 -70l-448 -448q-18 -19 -45 -19t-45 19l-448 448q-31 29 -14 70q17 39 59 39h256v448q0 26 19 45t45 19h256q26 0 45 -19t19 -45v-448h256q42 0 59 -39z" /> -<glyph unicode="" d="M1120 608q0 -12 -10 -24l-319 -319q-11 -9 -23 -9t-23 9l-320 320q-15 16 -7 35q8 20 30 20h192v352q0 14 9 23t23 9h192q14 0 23 -9t9 -23v-352h192q14 0 23 -9t9 -23zM768 1184q-148 0 -273 -73t-198 -198t-73 -273t73 -273t198 -198t273 -73t273 73t198 198t73 273 t-73 273t-198 198t-273 73zM1536 640q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5t103 -385.5z" /> -<glyph unicode="" d="M1118 660q-8 -20 -30 -20h-192v-352q0 -14 -9 -23t-23 -9h-192q-14 0 -23 9t-9 23v352h-192q-14 0 -23 9t-9 23q0 12 10 24l319 319q11 9 23 9t23 -9l320 -320q15 -16 7 -35zM768 1184q-148 0 -273 -73t-198 -198t-73 -273t73 -273t198 -198t273 -73t273 73t198 198 t73 273t-73 273t-198 198t-273 73zM1536 640q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5t103 -385.5z" /> -<glyph unicode="" d="M1023 576h316q-1 3 -2.5 8t-2.5 8l-212 496h-708l-212 -496q-1 -2 -2.5 -8t-2.5 -8h316l95 -192h320zM1536 546v-482q0 -26 -19 -45t-45 -19h-1408q-26 0 -45 19t-19 45v482q0 62 25 123l238 552q10 25 36.5 42t52.5 17h832q26 0 52.5 -17t36.5 -42l238 -552 q25 -61 25 -123z" /> -<glyph unicode="" d="M1184 640q0 -37 -32 -55l-544 -320q-15 -9 -32 -9q-16 0 -32 8q-32 19 -32 56v640q0 37 32 56q33 18 64 -1l544 -320q32 -18 32 -55zM1312 640q0 148 -73 273t-198 198t-273 73t-273 -73t-198 -198t-73 -273t73 -273t198 -198t273 -73t273 73t198 198t73 273zM1536 640 q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5t103 -385.5z" /> -<glyph unicode="" d="M1536 1280v-448q0 -26 -19 -45t-45 -19h-448q-42 0 -59 40q-17 39 14 69l138 138q-148 137 -349 137q-104 0 -198.5 -40.5t-163.5 -109.5t-109.5 -163.5t-40.5 -198.5t40.5 -198.5t109.5 -163.5t163.5 -109.5t198.5 -40.5q119 0 225 52t179 147q7 10 23 12q14 0 25 -9 l137 -138q9 -8 9.5 -20.5t-7.5 -22.5q-109 -132 -264 -204.5t-327 -72.5q-156 0 -298 61t-245 164t-164 245t-61 298t61 298t164 245t245 164t298 61q147 0 284.5 -55.5t244.5 -156.5l130 129q29 31 70 14q39 -17 39 -59z" /> -<glyph unicode="" d="M1511 480q0 -5 -1 -7q-64 -268 -268 -434.5t-478 -166.5q-146 0 -282.5 55t-243.5 157l-129 -129q-19 -19 -45 -19t-45 19t-19 45v448q0 26 19 45t45 19h448q26 0 45 -19t19 -45t-19 -45l-137 -137q71 -66 161 -102t187 -36q134 0 250 65t186 179q11 17 53 117 q8 23 30 23h192q13 0 22.5 -9.5t9.5 -22.5zM1536 1280v-448q0 -26 -19 -45t-45 -19h-448q-26 0 -45 19t-19 45t19 45l138 138q-148 137 -349 137q-134 0 -250 -65t-186 -179q-11 -17 -53 -117q-8 -23 -30 -23h-199q-13 0 -22.5 9.5t-9.5 22.5v7q65 268 270 434.5t480 166.5 q146 0 284 -55.5t245 -156.5l130 129q19 19 45 19t45 -19t19 -45z" /> -<glyph unicode="" horiz-adv-x="1792" d="M384 352v-64q0 -13 -9.5 -22.5t-22.5 -9.5h-64q-13 0 -22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h64q13 0 22.5 -9.5t9.5 -22.5zM384 608v-64q0 -13 -9.5 -22.5t-22.5 -9.5h-64q-13 0 -22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h64q13 0 22.5 -9.5t9.5 -22.5z M384 864v-64q0 -13 -9.5 -22.5t-22.5 -9.5h-64q-13 0 -22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h64q13 0 22.5 -9.5t9.5 -22.5zM1536 352v-64q0 -13 -9.5 -22.5t-22.5 -9.5h-960q-13 0 -22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h960q13 0 22.5 -9.5t9.5 -22.5z M1536 608v-64q0 -13 -9.5 -22.5t-22.5 -9.5h-960q-13 0 -22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h960q13 0 22.5 -9.5t9.5 -22.5zM1536 864v-64q0 -13 -9.5 -22.5t-22.5 -9.5h-960q-13 0 -22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h960q13 0 22.5 -9.5 t9.5 -22.5zM1664 160v832q0 13 -9.5 22.5t-22.5 9.5h-1472q-13 0 -22.5 -9.5t-9.5 -22.5v-832q0 -13 9.5 -22.5t22.5 -9.5h1472q13 0 22.5 9.5t9.5 22.5zM1792 1248v-1088q0 -66 -47 -113t-113 -47h-1472q-66 0 -113 47t-47 113v1088q0 66 47 113t113 47h1472q66 0 113 -47 t47 -113z" /> -<glyph unicode="" horiz-adv-x="1152" d="M320 768h512v192q0 106 -75 181t-181 75t-181 -75t-75 -181v-192zM1152 672v-576q0 -40 -28 -68t-68 -28h-960q-40 0 -68 28t-28 68v576q0 40 28 68t68 28h32v192q0 184 132 316t316 132t316 -132t132 -316v-192h32q40 0 68 -28t28 -68z" /> -<glyph unicode="" horiz-adv-x="1792" d="M320 1280q0 -72 -64 -110v-1266q0 -13 -9.5 -22.5t-22.5 -9.5h-64q-13 0 -22.5 9.5t-9.5 22.5v1266q-64 38 -64 110q0 53 37.5 90.5t90.5 37.5t90.5 -37.5t37.5 -90.5zM1792 1216v-763q0 -25 -12.5 -38.5t-39.5 -27.5q-215 -116 -369 -116q-61 0 -123.5 22t-108.5 48 t-115.5 48t-142.5 22q-192 0 -464 -146q-17 -9 -33 -9q-26 0 -45 19t-19 45v742q0 32 31 55q21 14 79 43q236 120 421 120q107 0 200 -29t219 -88q38 -19 88 -19q54 0 117.5 21t110 47t88 47t54.5 21q26 0 45 -19t19 -45z" /> -<glyph unicode="" horiz-adv-x="1664" d="M1664 650q0 -166 -60 -314l-20 -49l-185 -33q-22 -83 -90.5 -136.5t-156.5 -53.5v-32q0 -14 -9 -23t-23 -9h-64q-14 0 -23 9t-9 23v576q0 14 9 23t23 9h64q14 0 23 -9t9 -23v-32q71 0 130 -35.5t93 -95.5l68 12q29 95 29 193q0 148 -88 279t-236.5 209t-315.5 78 t-315.5 -78t-236.5 -209t-88 -279q0 -98 29 -193l68 -12q34 60 93 95.5t130 35.5v32q0 14 9 23t23 9h64q14 0 23 -9t9 -23v-576q0 -14 -9 -23t-23 -9h-64q-14 0 -23 9t-9 23v32q-88 0 -156.5 53.5t-90.5 136.5l-185 33l-20 49q-60 148 -60 314q0 151 67 291t179 242.5 t266 163.5t320 61t320 -61t266 -163.5t179 -242.5t67 -291z" /> -<glyph unicode="" horiz-adv-x="768" d="M768 1184v-1088q0 -26 -19 -45t-45 -19t-45 19l-333 333h-262q-26 0 -45 19t-19 45v384q0 26 19 45t45 19h262l333 333q19 19 45 19t45 -19t19 -45z" /> -<glyph unicode="" horiz-adv-x="1152" d="M768 1184v-1088q0 -26 -19 -45t-45 -19t-45 19l-333 333h-262q-26 0 -45 19t-19 45v384q0 26 19 45t45 19h262l333 333q19 19 45 19t45 -19t19 -45zM1152 640q0 -76 -42.5 -141.5t-112.5 -93.5q-10 -5 -25 -5q-26 0 -45 18.5t-19 45.5q0 21 12 35.5t29 25t34 23t29 35.5 t12 57t-12 57t-29 35.5t-34 23t-29 25t-12 35.5q0 27 19 45.5t45 18.5q15 0 25 -5q70 -27 112.5 -93t42.5 -142z" /> -<glyph unicode="" horiz-adv-x="1664" d="M768 1184v-1088q0 -26 -19 -45t-45 -19t-45 19l-333 333h-262q-26 0 -45 19t-19 45v384q0 26 19 45t45 19h262l333 333q19 19 45 19t45 -19t19 -45zM1152 640q0 -76 -42.5 -141.5t-112.5 -93.5q-10 -5 -25 -5q-26 0 -45 18.5t-19 45.5q0 21 12 35.5t29 25t34 23t29 35.5 t12 57t-12 57t-29 35.5t-34 23t-29 25t-12 35.5q0 27 19 45.5t45 18.5q15 0 25 -5q70 -27 112.5 -93t42.5 -142zM1408 640q0 -153 -85 -282.5t-225 -188.5q-13 -5 -25 -5q-27 0 -46 19t-19 45q0 39 39 59q56 29 76 44q74 54 115.5 135.5t41.5 173.5t-41.5 173.5 t-115.5 135.5q-20 15 -76 44q-39 20 -39 59q0 26 19 45t45 19q13 0 26 -5q140 -59 225 -188.5t85 -282.5zM1664 640q0 -230 -127 -422.5t-338 -283.5q-13 -5 -26 -5q-26 0 -45 19t-19 45q0 36 39 59q7 4 22.5 10.5t22.5 10.5q46 25 82 51q123 91 192 227t69 289t-69 289 t-192 227q-36 26 -82 51q-7 4 -22.5 10.5t-22.5 10.5q-39 23 -39 59q0 26 19 45t45 19q13 0 26 -5q211 -91 338 -283.5t127 -422.5z" /> -<glyph unicode="" horiz-adv-x="1408" d="M384 384v-128h-128v128h128zM384 1152v-128h-128v128h128zM1152 1152v-128h-128v128h128zM128 129h384v383h-384v-383zM128 896h384v384h-384v-384zM896 896h384v384h-384v-384zM640 640v-640h-640v640h640zM1152 128v-128h-128v128h128zM1408 128v-128h-128v128h128z M1408 640v-384h-384v128h-128v-384h-128v640h384v-128h128v128h128zM640 1408v-640h-640v640h640zM1408 1408v-640h-640v640h640z" /> -<glyph unicode="" horiz-adv-x="1792" d="M63 0h-63v1408h63v-1408zM126 1h-32v1407h32v-1407zM220 1h-31v1407h31v-1407zM377 1h-31v1407h31v-1407zM534 1h-62v1407h62v-1407zM660 1h-31v1407h31v-1407zM723 1h-31v1407h31v-1407zM786 1h-31v1407h31v-1407zM943 1h-63v1407h63v-1407zM1100 1h-63v1407h63v-1407z M1226 1h-63v1407h63v-1407zM1352 1h-63v1407h63v-1407zM1446 1h-63v1407h63v-1407zM1635 1h-94v1407h94v-1407zM1698 1h-32v1407h32v-1407zM1792 0h-63v1408h63v-1408z" /> -<glyph unicode="" d="M448 1088q0 53 -37.5 90.5t-90.5 37.5t-90.5 -37.5t-37.5 -90.5t37.5 -90.5t90.5 -37.5t90.5 37.5t37.5 90.5zM1515 512q0 -53 -37 -90l-491 -492q-39 -37 -91 -37q-53 0 -90 37l-715 716q-38 37 -64.5 101t-26.5 117v416q0 52 38 90t90 38h416q53 0 117 -26.5t102 -64.5 l715 -714q37 -39 37 -91z" /> -<glyph unicode="" horiz-adv-x="1920" d="M448 1088q0 53 -37.5 90.5t-90.5 37.5t-90.5 -37.5t-37.5 -90.5t37.5 -90.5t90.5 -37.5t90.5 37.5t37.5 90.5zM1515 512q0 -53 -37 -90l-491 -492q-39 -37 -91 -37q-53 0 -90 37l-715 716q-38 37 -64.5 101t-26.5 117v416q0 52 38 90t90 38h416q53 0 117 -26.5t102 -64.5 l715 -714q37 -39 37 -91zM1899 512q0 -53 -37 -90l-491 -492q-39 -37 -91 -37q-36 0 -59 14t-53 45l470 470q37 37 37 90q0 52 -37 91l-715 714q-38 38 -102 64.5t-117 26.5h224q53 0 117 -26.5t102 -64.5l715 -714q37 -39 37 -91z" /> -<glyph unicode="" horiz-adv-x="1664" d="M1639 1058q40 -57 18 -129l-275 -906q-19 -64 -76.5 -107.5t-122.5 -43.5h-923q-77 0 -148.5 53.5t-99.5 131.5q-24 67 -2 127q0 4 3 27t4 37q1 8 -3 21.5t-3 19.5q2 11 8 21t16.5 23.5t16.5 23.5q23 38 45 91.5t30 91.5q3 10 0.5 30t-0.5 28q3 11 17 28t17 23 q21 36 42 92t25 90q1 9 -2.5 32t0.5 28q4 13 22 30.5t22 22.5q19 26 42.5 84.5t27.5 96.5q1 8 -3 25.5t-2 26.5q2 8 9 18t18 23t17 21q8 12 16.5 30.5t15 35t16 36t19.5 32t26.5 23.5t36 11.5t47.5 -5.5l-1 -3q38 9 51 9h761q74 0 114 -56t18 -130l-274 -906 q-36 -119 -71.5 -153.5t-128.5 -34.5h-869q-27 0 -38 -15q-11 -16 -1 -43q24 -70 144 -70h923q29 0 56 15.5t35 41.5l300 987q7 22 5 57q38 -15 59 -43zM575 1056q-4 -13 2 -22.5t20 -9.5h608q13 0 25.5 9.5t16.5 22.5l21 64q4 13 -2 22.5t-20 9.5h-608q-13 0 -25.5 -9.5 t-16.5 -22.5zM492 800q-4 -13 2 -22.5t20 -9.5h608q13 0 25.5 9.5t16.5 22.5l21 64q4 13 -2 22.5t-20 9.5h-608q-13 0 -25.5 -9.5t-16.5 -22.5z" /> -<glyph unicode="" horiz-adv-x="1280" d="M1164 1408q23 0 44 -9q33 -13 52.5 -41t19.5 -62v-1289q0 -34 -19.5 -62t-52.5 -41q-19 -8 -44 -8q-48 0 -83 32l-441 424l-441 -424q-36 -33 -83 -33q-23 0 -44 9q-33 13 -52.5 41t-19.5 62v1289q0 34 19.5 62t52.5 41q21 9 44 9h1048z" /> -<glyph unicode="" horiz-adv-x="1664" d="M384 0h896v256h-896v-256zM384 640h896v384h-160q-40 0 -68 28t-28 68v160h-640v-640zM1536 576q0 26 -19 45t-45 19t-45 -19t-19 -45t19 -45t45 -19t45 19t19 45zM1664 576v-416q0 -13 -9.5 -22.5t-22.5 -9.5h-224v-160q0 -40 -28 -68t-68 -28h-960q-40 0 -68 28t-28 68 v160h-224q-13 0 -22.5 9.5t-9.5 22.5v416q0 79 56.5 135.5t135.5 56.5h64v544q0 40 28 68t68 28h672q40 0 88 -20t76 -48l152 -152q28 -28 48 -76t20 -88v-256h64q79 0 135.5 -56.5t56.5 -135.5z" /> -<glyph unicode="" horiz-adv-x="1920" d="M960 864q119 0 203.5 -84.5t84.5 -203.5t-84.5 -203.5t-203.5 -84.5t-203.5 84.5t-84.5 203.5t84.5 203.5t203.5 84.5zM1664 1280q106 0 181 -75t75 -181v-896q0 -106 -75 -181t-181 -75h-1408q-106 0 -181 75t-75 181v896q0 106 75 181t181 75h224l51 136 q19 49 69.5 84.5t103.5 35.5h512q53 0 103.5 -35.5t69.5 -84.5l51 -136h224zM960 128q185 0 316.5 131.5t131.5 316.5t-131.5 316.5t-316.5 131.5t-316.5 -131.5t-131.5 -316.5t131.5 -316.5t316.5 -131.5z" /> -<glyph unicode="" horiz-adv-x="1664" d="M725 977l-170 -450q33 0 136.5 -2t160.5 -2q19 0 57 2q-87 253 -184 452zM0 -128l2 79q23 7 56 12.5t57 10.5t49.5 14.5t44.5 29t31 50.5l237 616l280 724h75h53q8 -14 11 -21l205 -480q33 -78 106 -257.5t114 -274.5q15 -34 58 -144.5t72 -168.5q20 -45 35 -57 q19 -15 88 -29.5t84 -20.5q6 -38 6 -57q0 -4 -0.5 -13t-0.5 -13q-63 0 -190 8t-191 8q-76 0 -215 -7t-178 -8q0 43 4 78l131 28q1 0 12.5 2.5t15.5 3.5t14.5 4.5t15 6.5t11 8t9 11t2.5 14q0 16 -31 96.5t-72 177.5t-42 100l-450 2q-26 -58 -76.5 -195.5t-50.5 -162.5 q0 -22 14 -37.5t43.5 -24.5t48.5 -13.5t57 -8.5t41 -4q1 -19 1 -58q0 -9 -2 -27q-58 0 -174.5 10t-174.5 10q-8 0 -26.5 -4t-21.5 -4q-80 -14 -188 -14z" /> -<glyph unicode="" horiz-adv-x="1408" d="M555 15q74 -32 140 -32q376 0 376 335q0 114 -41 180q-27 44 -61.5 74t-67.5 46.5t-80.5 25t-84 10.5t-94.5 2q-73 0 -101 -10q0 -53 -0.5 -159t-0.5 -158q0 -8 -1 -67.5t-0.5 -96.5t4.5 -83.5t12 -66.5zM541 761q42 -7 109 -7q82 0 143 13t110 44.5t74.5 89.5t25.5 142 q0 70 -29 122.5t-79 82t-108 43.5t-124 14q-50 0 -130 -13q0 -50 4 -151t4 -152q0 -27 -0.5 -80t-0.5 -79q0 -46 1 -69zM0 -128l2 94q15 4 85 16t106 27q7 12 12.5 27t8.5 33.5t5.5 32.5t3 37.5t0.5 34v35.5v30q0 982 -22 1025q-4 8 -22 14.5t-44.5 11t-49.5 7t-48.5 4.5 t-30.5 3l-4 83q98 2 340 11.5t373 9.5q23 0 68.5 -0.5t67.5 -0.5q70 0 136.5 -13t128.5 -42t108 -71t74 -104.5t28 -137.5q0 -52 -16.5 -95.5t-39 -72t-64.5 -57.5t-73 -45t-84 -40q154 -35 256.5 -134t102.5 -248q0 -100 -35 -179.5t-93.5 -130.5t-138 -85.5t-163.5 -48.5 t-176 -14q-44 0 -132 3t-132 3q-106 0 -307 -11t-231 -12z" /> -<glyph unicode="" horiz-adv-x="1024" d="M0 -126l17 85q6 2 81.5 21.5t111.5 37.5q28 35 41 101q1 7 62 289t114 543.5t52 296.5v25q-24 13 -54.5 18.5t-69.5 8t-58 5.5l19 103q33 -2 120 -6.5t149.5 -7t120.5 -2.5q48 0 98.5 2.5t121 7t98.5 6.5q-5 -39 -19 -89q-30 -10 -101.5 -28.5t-108.5 -33.5 q-8 -19 -14 -42.5t-9 -40t-7.5 -45.5t-6.5 -42q-27 -148 -87.5 -419.5t-77.5 -355.5q-2 -9 -13 -58t-20 -90t-16 -83.5t-6 -57.5l1 -18q17 -4 185 -31q-3 -44 -16 -99q-11 0 -32.5 -1.5t-32.5 -1.5q-29 0 -87 10t-86 10q-138 2 -206 2q-51 0 -143 -9t-121 -11z" /> -<glyph unicode="" horiz-adv-x="1792" d="M1744 128q33 0 42 -18.5t-11 -44.5l-126 -162q-20 -26 -49 -26t-49 26l-126 162q-20 26 -11 44.5t42 18.5h80v1024h-80q-33 0 -42 18.5t11 44.5l126 162q20 26 49 26t49 -26l126 -162q20 -26 11 -44.5t-42 -18.5h-80v-1024h80zM81 1407l54 -27q12 -5 211 -5q44 0 132 2 t132 2q36 0 107.5 -0.5t107.5 -0.5h293q6 0 21 -0.5t20.5 0t16 3t17.5 9t15 17.5l42 1q4 0 14 -0.5t14 -0.5q2 -112 2 -336q0 -80 -5 -109q-39 -14 -68 -18q-25 44 -54 128q-3 9 -11 48t-14.5 73.5t-7.5 35.5q-6 8 -12 12.5t-15.5 6t-13 2.5t-18 0.5t-16.5 -0.5 q-17 0 -66.5 0.5t-74.5 0.5t-64 -2t-71 -6q-9 -81 -8 -136q0 -94 2 -388t2 -455q0 -16 -2.5 -71.5t0 -91.5t12.5 -69q40 -21 124 -42.5t120 -37.5q5 -40 5 -50q0 -14 -3 -29l-34 -1q-76 -2 -218 8t-207 10q-50 0 -151 -9t-152 -9q-3 51 -3 52v9q17 27 61.5 43t98.5 29t78 27 q19 42 19 383q0 101 -3 303t-3 303v117q0 2 0.5 15.5t0.5 25t-1 25.5t-3 24t-5 14q-11 12 -162 12q-33 0 -93 -12t-80 -26q-19 -13 -34 -72.5t-31.5 -111t-42.5 -53.5q-42 26 -56 44v383z" /> -<glyph unicode="" d="M81 1407l54 -27q12 -5 211 -5q44 0 132 2t132 2q70 0 246.5 1t304.5 0.5t247 -4.5q33 -1 56 31l42 1q4 0 14 -0.5t14 -0.5q2 -112 2 -336q0 -80 -5 -109q-39 -14 -68 -18q-25 44 -54 128q-3 9 -11 47.5t-15 73.5t-7 36q-10 13 -27 19q-5 2 -66 2q-30 0 -93 1t-103 1 t-94 -2t-96 -7q-9 -81 -8 -136l1 -152v52q0 -55 1 -154t1.5 -180t0.5 -153q0 -16 -2.5 -71.5t0 -91.5t12.5 -69q40 -21 124 -42.5t120 -37.5q5 -40 5 -50q0 -14 -3 -29l-34 -1q-76 -2 -218 8t-207 10q-50 0 -151 -9t-152 -9q-3 51 -3 52v9q17 27 61.5 43t98.5 29t78 27 q7 16 11.5 74t6 145.5t1.5 155t-0.5 153.5t-0.5 89q0 7 -2.5 21.5t-2.5 22.5q0 7 0.5 44t1 73t0 76.5t-3 67.5t-6.5 32q-11 12 -162 12q-41 0 -163 -13.5t-138 -24.5q-19 -12 -34 -71.5t-31.5 -111.5t-42.5 -54q-42 26 -56 44v383zM1310 125q12 0 42 -19.5t57.5 -41.5 t59.5 -49t36 -30q26 -21 26 -49t-26 -49q-4 -3 -36 -30t-59.5 -49t-57.5 -41.5t-42 -19.5q-13 0 -20.5 10.5t-10 28.5t-2.5 33.5t1.5 33t1.5 19.5h-1024q0 -2 1.5 -19.5t1.5 -33t-2.5 -33.5t-10 -28.5t-20.5 -10.5q-12 0 -42 19.5t-57.5 41.5t-59.5 49t-36 30q-26 21 -26 49 t26 49q4 3 36 30t59.5 49t57.5 41.5t42 19.5q13 0 20.5 -10.5t10 -28.5t2.5 -33.5t-1.5 -33t-1.5 -19.5h1024q0 2 -1.5 19.5t-1.5 33t2.5 33.5t10 28.5t20.5 10.5z" /> -<glyph unicode="" horiz-adv-x="1792" d="M1792 192v-128q0 -26 -19 -45t-45 -19h-1664q-26 0 -45 19t-19 45v128q0 26 19 45t45 19h1664q26 0 45 -19t19 -45zM1408 576v-128q0 -26 -19 -45t-45 -19h-1280q-26 0 -45 19t-19 45v128q0 26 19 45t45 19h1280q26 0 45 -19t19 -45zM1664 960v-128q0 -26 -19 -45 t-45 -19h-1536q-26 0 -45 19t-19 45v128q0 26 19 45t45 19h1536q26 0 45 -19t19 -45zM1280 1344v-128q0 -26 -19 -45t-45 -19h-1152q-26 0 -45 19t-19 45v128q0 26 19 45t45 19h1152q26 0 45 -19t19 -45z" /> -<glyph unicode="" horiz-adv-x="1792" d="M1792 192v-128q0 -26 -19 -45t-45 -19h-1664q-26 0 -45 19t-19 45v128q0 26 19 45t45 19h1664q26 0 45 -19t19 -45zM1408 576v-128q0 -26 -19 -45t-45 -19h-896q-26 0 -45 19t-19 45v128q0 26 19 45t45 19h896q26 0 45 -19t19 -45zM1664 960v-128q0 -26 -19 -45t-45 -19 h-1408q-26 0 -45 19t-19 45v128q0 26 19 45t45 19h1408q26 0 45 -19t19 -45zM1280 1344v-128q0 -26 -19 -45t-45 -19h-640q-26 0 -45 19t-19 45v128q0 26 19 45t45 19h640q26 0 45 -19t19 -45z" /> -<glyph unicode="" horiz-adv-x="1792" d="M1792 192v-128q0 -26 -19 -45t-45 -19h-1664q-26 0 -45 19t-19 45v128q0 26 19 45t45 19h1664q26 0 45 -19t19 -45zM1792 576v-128q0 -26 -19 -45t-45 -19h-1280q-26 0 -45 19t-19 45v128q0 26 19 45t45 19h1280q26 0 45 -19t19 -45zM1792 960v-128q0 -26 -19 -45 t-45 -19h-1536q-26 0 -45 19t-19 45v128q0 26 19 45t45 19h1536q26 0 45 -19t19 -45zM1792 1344v-128q0 -26 -19 -45t-45 -19h-1152q-26 0 -45 19t-19 45v128q0 26 19 45t45 19h1152q26 0 45 -19t19 -45z" /> -<glyph unicode="" horiz-adv-x="1792" d="M1792 192v-128q0 -26 -19 -45t-45 -19h-1664q-26 0 -45 19t-19 45v128q0 26 19 45t45 19h1664q26 0 45 -19t19 -45zM1792 576v-128q0 -26 -19 -45t-45 -19h-1664q-26 0 -45 19t-19 45v128q0 26 19 45t45 19h1664q26 0 45 -19t19 -45zM1792 960v-128q0 -26 -19 -45 t-45 -19h-1664q-26 0 -45 19t-19 45v128q0 26 19 45t45 19h1664q26 0 45 -19t19 -45zM1792 1344v-128q0 -26 -19 -45t-45 -19h-1664q-26 0 -45 19t-19 45v128q0 26 19 45t45 19h1664q26 0 45 -19t19 -45z" /> -<glyph unicode="" horiz-adv-x="1792" d="M256 224v-192q0 -13 -9.5 -22.5t-22.5 -9.5h-192q-13 0 -22.5 9.5t-9.5 22.5v192q0 13 9.5 22.5t22.5 9.5h192q13 0 22.5 -9.5t9.5 -22.5zM256 608v-192q0 -13 -9.5 -22.5t-22.5 -9.5h-192q-13 0 -22.5 9.5t-9.5 22.5v192q0 13 9.5 22.5t22.5 9.5h192q13 0 22.5 -9.5 t9.5 -22.5zM256 992v-192q0 -13 -9.5 -22.5t-22.5 -9.5h-192q-13 0 -22.5 9.5t-9.5 22.5v192q0 13 9.5 22.5t22.5 9.5h192q13 0 22.5 -9.5t9.5 -22.5zM1792 224v-192q0 -13 -9.5 -22.5t-22.5 -9.5h-1344q-13 0 -22.5 9.5t-9.5 22.5v192q0 13 9.5 22.5t22.5 9.5h1344 q13 0 22.5 -9.5t9.5 -22.5zM256 1376v-192q0 -13 -9.5 -22.5t-22.5 -9.5h-192q-13 0 -22.5 9.5t-9.5 22.5v192q0 13 9.5 22.5t22.5 9.5h192q13 0 22.5 -9.5t9.5 -22.5zM1792 608v-192q0 -13 -9.5 -22.5t-22.5 -9.5h-1344q-13 0 -22.5 9.5t-9.5 22.5v192q0 13 9.5 22.5 t22.5 9.5h1344q13 0 22.5 -9.5t9.5 -22.5zM1792 992v-192q0 -13 -9.5 -22.5t-22.5 -9.5h-1344q-13 0 -22.5 9.5t-9.5 22.5v192q0 13 9.5 22.5t22.5 9.5h1344q13 0 22.5 -9.5t9.5 -22.5zM1792 1376v-192q0 -13 -9.5 -22.5t-22.5 -9.5h-1344q-13 0 -22.5 9.5t-9.5 22.5v192 q0 13 9.5 22.5t22.5 9.5h1344q13 0 22.5 -9.5t9.5 -22.5z" /> -<glyph unicode="" horiz-adv-x="1792" d="M384 992v-576q0 -13 -9.5 -22.5t-22.5 -9.5q-14 0 -23 9l-288 288q-9 9 -9 23t9 23l288 288q9 9 23 9q13 0 22.5 -9.5t9.5 -22.5zM1792 224v-192q0 -13 -9.5 -22.5t-22.5 -9.5h-1728q-13 0 -22.5 9.5t-9.5 22.5v192q0 13 9.5 22.5t22.5 9.5h1728q13 0 22.5 -9.5 t9.5 -22.5zM1792 608v-192q0 -13 -9.5 -22.5t-22.5 -9.5h-1088q-13 0 -22.5 9.5t-9.5 22.5v192q0 13 9.5 22.5t22.5 9.5h1088q13 0 22.5 -9.5t9.5 -22.5zM1792 992v-192q0 -13 -9.5 -22.5t-22.5 -9.5h-1088q-13 0 -22.5 9.5t-9.5 22.5v192q0 13 9.5 22.5t22.5 9.5h1088 q13 0 22.5 -9.5t9.5 -22.5zM1792 1376v-192q0 -13 -9.5 -22.5t-22.5 -9.5h-1728q-13 0 -22.5 9.5t-9.5 22.5v192q0 13 9.5 22.5t22.5 9.5h1728q13 0 22.5 -9.5t9.5 -22.5z" /> -<glyph unicode="" horiz-adv-x="1792" d="M352 704q0 -14 -9 -23l-288 -288q-9 -9 -23 -9q-13 0 -22.5 9.5t-9.5 22.5v576q0 13 9.5 22.5t22.5 9.5q14 0 23 -9l288 -288q9 -9 9 -23zM1792 224v-192q0 -13 -9.5 -22.5t-22.5 -9.5h-1728q-13 0 -22.5 9.5t-9.5 22.5v192q0 13 9.5 22.5t22.5 9.5h1728q13 0 22.5 -9.5 t9.5 -22.5zM1792 608v-192q0 -13 -9.5 -22.5t-22.5 -9.5h-1088q-13 0 -22.5 9.5t-9.5 22.5v192q0 13 9.5 22.5t22.5 9.5h1088q13 0 22.5 -9.5t9.5 -22.5zM1792 992v-192q0 -13 -9.5 -22.5t-22.5 -9.5h-1088q-13 0 -22.5 9.5t-9.5 22.5v192q0 13 9.5 22.5t22.5 9.5h1088 q13 0 22.5 -9.5t9.5 -22.5zM1792 1376v-192q0 -13 -9.5 -22.5t-22.5 -9.5h-1728q-13 0 -22.5 9.5t-9.5 22.5v192q0 13 9.5 22.5t22.5 9.5h1728q13 0 22.5 -9.5t9.5 -22.5z" /> -<glyph unicode="" horiz-adv-x="1792" d="M1792 1184v-1088q0 -42 -39 -59q-13 -5 -25 -5q-27 0 -45 19l-403 403v-166q0 -119 -84.5 -203.5t-203.5 -84.5h-704q-119 0 -203.5 84.5t-84.5 203.5v704q0 119 84.5 203.5t203.5 84.5h704q119 0 203.5 -84.5t84.5 -203.5v-165l403 402q18 19 45 19q12 0 25 -5 q39 -17 39 -59z" /> -<glyph unicode="" horiz-adv-x="1920" d="M640 960q0 -80 -56 -136t-136 -56t-136 56t-56 136t56 136t136 56t136 -56t56 -136zM1664 576v-448h-1408v192l320 320l160 -160l512 512zM1760 1280h-1600q-13 0 -22.5 -9.5t-9.5 -22.5v-1216q0 -13 9.5 -22.5t22.5 -9.5h1600q13 0 22.5 9.5t9.5 22.5v1216 q0 13 -9.5 22.5t-22.5 9.5zM1920 1248v-1216q0 -66 -47 -113t-113 -47h-1600q-66 0 -113 47t-47 113v1216q0 66 47 113t113 47h1600q66 0 113 -47t47 -113z" /> -<glyph unicode="" d="M363 0l91 91l-235 235l-91 -91v-107h128v-128h107zM886 928q0 22 -22 22q-10 0 -17 -7l-542 -542q-7 -7 -7 -17q0 -22 22 -22q10 0 17 7l542 542q7 7 7 17zM832 1120l416 -416l-832 -832h-416v416zM1515 1024q0 -53 -37 -90l-166 -166l-416 416l166 165q36 38 90 38 q53 0 91 -38l235 -234q37 -39 37 -91z" /> -<glyph unicode="" horiz-adv-x="1024" d="M768 896q0 106 -75 181t-181 75t-181 -75t-75 -181t75 -181t181 -75t181 75t75 181zM1024 896q0 -109 -33 -179l-364 -774q-16 -33 -47.5 -52t-67.5 -19t-67.5 19t-46.5 52l-365 774q-33 70 -33 179q0 212 150 362t362 150t362 -150t150 -362z" /> -<glyph unicode="" d="M768 96v1088q-148 0 -273 -73t-198 -198t-73 -273t73 -273t198 -198t273 -73zM1536 640q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5t103 -385.5z" /> -<glyph unicode="" horiz-adv-x="1024" d="M512 384q0 36 -20 69q-1 1 -15.5 22.5t-25.5 38t-25 44t-21 50.5q-4 16 -21 16t-21 -16q-7 -23 -21 -50.5t-25 -44t-25.5 -38t-15.5 -22.5q-20 -33 -20 -69q0 -53 37.5 -90.5t90.5 -37.5t90.5 37.5t37.5 90.5zM1024 512q0 -212 -150 -362t-362 -150t-362 150t-150 362 q0 145 81 275q6 9 62.5 90.5t101 151t99.5 178t83 201.5q9 30 34 47t51 17t51.5 -17t33.5 -47q28 -93 83 -201.5t99.5 -178t101 -151t62.5 -90.5q81 -127 81 -275z" /> -<glyph unicode="" horiz-adv-x="1792" d="M888 352l116 116l-152 152l-116 -116v-56h96v-96h56zM1328 1072q-16 16 -33 -1l-350 -350q-17 -17 -1 -33t33 1l350 350q17 17 1 33zM1408 478v-190q0 -119 -84.5 -203.5t-203.5 -84.5h-832q-119 0 -203.5 84.5t-84.5 203.5v832q0 119 84.5 203.5t203.5 84.5h832 q63 0 117 -25q15 -7 18 -23q3 -17 -9 -29l-49 -49q-14 -14 -32 -8q-23 6 -45 6h-832q-66 0 -113 -47t-47 -113v-832q0 -66 47 -113t113 -47h832q66 0 113 47t47 113v126q0 13 9 22l64 64q15 15 35 7t20 -29zM1312 1216l288 -288l-672 -672h-288v288zM1756 1084l-92 -92 l-288 288l92 92q28 28 68 28t68 -28l152 -152q28 -28 28 -68t-28 -68z" /> -<glyph unicode="" horiz-adv-x="1664" d="M1408 547v-259q0 -119 -84.5 -203.5t-203.5 -84.5h-832q-119 0 -203.5 84.5t-84.5 203.5v832q0 119 84.5 203.5t203.5 84.5h255v0q13 0 22.5 -9.5t9.5 -22.5q0 -27 -26 -32q-77 -26 -133 -60q-10 -4 -16 -4h-112q-66 0 -113 -47t-47 -113v-832q0 -66 47 -113t113 -47h832 q66 0 113 47t47 113v214q0 19 18 29q28 13 54 37q16 16 35 8q21 -9 21 -29zM1645 1043l-384 -384q-18 -19 -45 -19q-12 0 -25 5q-39 17 -39 59v192h-160q-323 0 -438 -131q-119 -137 -74 -473q3 -23 -20 -34q-8 -2 -12 -2q-16 0 -26 13q-10 14 -21 31t-39.5 68.5t-49.5 99.5 t-38.5 114t-17.5 122q0 49 3.5 91t14 90t28 88t47 81.5t68.5 74t94.5 61.5t124.5 48.5t159.5 30.5t196.5 11h160v192q0 42 39 59q13 5 25 5q26 0 45 -19l384 -384q19 -19 19 -45t-19 -45z" /> -<glyph unicode="" horiz-adv-x="1664" d="M1408 606v-318q0 -119 -84.5 -203.5t-203.5 -84.5h-832q-119 0 -203.5 84.5t-84.5 203.5v832q0 119 84.5 203.5t203.5 84.5h832q63 0 117 -25q15 -7 18 -23q3 -17 -9 -29l-49 -49q-10 -10 -23 -10q-3 0 -9 2q-23 6 -45 6h-832q-66 0 -113 -47t-47 -113v-832 q0 -66 47 -113t113 -47h832q66 0 113 47t47 113v254q0 13 9 22l64 64q10 10 23 10q6 0 12 -3q20 -8 20 -29zM1639 1095l-814 -814q-24 -24 -57 -24t-57 24l-430 430q-24 24 -24 57t24 57l110 110q24 24 57 24t57 -24l263 -263l647 647q24 24 57 24t57 -24l110 -110 q24 -24 24 -57t-24 -57z" /> -<glyph unicode="" horiz-adv-x="1792" d="M1792 640q0 -26 -19 -45l-256 -256q-19 -19 -45 -19t-45 19t-19 45v128h-384v-384h128q26 0 45 -19t19 -45t-19 -45l-256 -256q-19 -19 -45 -19t-45 19l-256 256q-19 19 -19 45t19 45t45 19h128v384h-384v-128q0 -26 -19 -45t-45 -19t-45 19l-256 256q-19 19 -19 45 t19 45l256 256q19 19 45 19t45 -19t19 -45v-128h384v384h-128q-26 0 -45 19t-19 45t19 45l256 256q19 19 45 19t45 -19l256 -256q19 -19 19 -45t-19 -45t-45 -19h-128v-384h384v128q0 26 19 45t45 19t45 -19l256 -256q19 -19 19 -45z" /> -<glyph unicode="" horiz-adv-x="1024" d="M979 1395q19 19 32 13t13 -32v-1472q0 -26 -13 -32t-32 13l-710 710q-9 9 -13 19v-678q0 -26 -19 -45t-45 -19h-128q-26 0 -45 19t-19 45v1408q0 26 19 45t45 19h128q26 0 45 -19t19 -45v-678q4 11 13 19z" /> -<glyph unicode="" horiz-adv-x="1792" d="M1747 1395q19 19 32 13t13 -32v-1472q0 -26 -13 -32t-32 13l-710 710q-9 9 -13 19v-710q0 -26 -13 -32t-32 13l-710 710q-9 9 -13 19v-678q0 -26 -19 -45t-45 -19h-128q-26 0 -45 19t-19 45v1408q0 26 19 45t45 19h128q26 0 45 -19t19 -45v-678q4 11 13 19l710 710 q19 19 32 13t13 -32v-710q4 11 13 19z" /> -<glyph unicode="" horiz-adv-x="1664" d="M1619 1395q19 19 32 13t13 -32v-1472q0 -26 -13 -32t-32 13l-710 710q-8 9 -13 19v-710q0 -26 -13 -32t-32 13l-710 710q-19 19 -19 45t19 45l710 710q19 19 32 13t13 -32v-710q5 11 13 19z" /> -<glyph unicode="" horiz-adv-x="1408" d="M1384 609l-1328 -738q-23 -13 -39.5 -3t-16.5 36v1472q0 26 16.5 36t39.5 -3l1328 -738q23 -13 23 -31t-23 -31z" /> -<glyph unicode="" d="M1536 1344v-1408q0 -26 -19 -45t-45 -19h-512q-26 0 -45 19t-19 45v1408q0 26 19 45t45 19h512q26 0 45 -19t19 -45zM640 1344v-1408q0 -26 -19 -45t-45 -19h-512q-26 0 -45 19t-19 45v1408q0 26 19 45t45 19h512q26 0 45 -19t19 -45z" /> -<glyph unicode="" d="M1536 1344v-1408q0 -26 -19 -45t-45 -19h-1408q-26 0 -45 19t-19 45v1408q0 26 19 45t45 19h1408q26 0 45 -19t19 -45z" /> -<glyph unicode="" horiz-adv-x="1664" d="M45 -115q-19 -19 -32 -13t-13 32v1472q0 26 13 32t32 -13l710 -710q8 -8 13 -19v710q0 26 13 32t32 -13l710 -710q19 -19 19 -45t-19 -45l-710 -710q-19 -19 -32 -13t-13 32v710q-5 -10 -13 -19z" /> -<glyph unicode="" horiz-adv-x="1792" d="M45 -115q-19 -19 -32 -13t-13 32v1472q0 26 13 32t32 -13l710 -710q8 -8 13 -19v710q0 26 13 32t32 -13l710 -710q8 -8 13 -19v678q0 26 19 45t45 19h128q26 0 45 -19t19 -45v-1408q0 -26 -19 -45t-45 -19h-128q-26 0 -45 19t-19 45v678q-5 -10 -13 -19l-710 -710 q-19 -19 -32 -13t-13 32v710q-5 -10 -13 -19z" /> -<glyph unicode="" horiz-adv-x="1024" d="M45 -115q-19 -19 -32 -13t-13 32v1472q0 26 13 32t32 -13l710 -710q8 -8 13 -19v678q0 26 19 45t45 19h128q26 0 45 -19t19 -45v-1408q0 -26 -19 -45t-45 -19h-128q-26 0 -45 19t-19 45v678q-5 -10 -13 -19z" /> -<glyph unicode="" horiz-adv-x="1538" d="M14 557l710 710q19 19 45 19t45 -19l710 -710q19 -19 13 -32t-32 -13h-1472q-26 0 -32 13t13 32zM1473 0h-1408q-26 0 -45 19t-19 45v256q0 26 19 45t45 19h1408q26 0 45 -19t19 -45v-256q0 -26 -19 -45t-45 -19z" /> -<glyph unicode="" horiz-adv-x="1280" d="M1171 1235l-531 -531l531 -531q19 -19 19 -45t-19 -45l-166 -166q-19 -19 -45 -19t-45 19l-742 742q-19 19 -19 45t19 45l742 742q19 19 45 19t45 -19l166 -166q19 -19 19 -45t-19 -45z" /> -<glyph unicode="" horiz-adv-x="1280" d="M1107 659l-742 -742q-19 -19 -45 -19t-45 19l-166 166q-19 19 -19 45t19 45l531 531l-531 531q-19 19 -19 45t19 45l166 166q19 19 45 19t45 -19l742 -742q19 -19 19 -45t-19 -45z" /> -<glyph unicode="" d="M1216 576v128q0 26 -19 45t-45 19h-256v256q0 26 -19 45t-45 19h-128q-26 0 -45 -19t-19 -45v-256h-256q-26 0 -45 -19t-19 -45v-128q0 -26 19 -45t45 -19h256v-256q0 -26 19 -45t45 -19h128q26 0 45 19t19 45v256h256q26 0 45 19t19 45zM1536 640q0 -209 -103 -385.5 t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5t103 -385.5z" /> -<glyph unicode="" d="M1216 576v128q0 26 -19 45t-45 19h-768q-26 0 -45 -19t-19 -45v-128q0 -26 19 -45t45 -19h768q26 0 45 19t19 45zM1536 640q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5 t103 -385.5z" /> -<glyph unicode="" d="M1149 414q0 26 -19 45l-181 181l181 181q19 19 19 45q0 27 -19 46l-90 90q-19 19 -46 19q-26 0 -45 -19l-181 -181l-181 181q-19 19 -45 19q-27 0 -46 -19l-90 -90q-19 -19 -19 -46q0 -26 19 -45l181 -181l-181 -181q-19 -19 -19 -45q0 -27 19 -46l90 -90q19 -19 46 -19 q26 0 45 19l181 181l181 -181q19 -19 45 -19q27 0 46 19l90 90q19 19 19 46zM1536 640q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5t103 -385.5z" /> -<glyph unicode="" d="M1284 802q0 28 -18 46l-91 90q-19 19 -45 19t-45 -19l-408 -407l-226 226q-19 19 -45 19t-45 -19l-91 -90q-18 -18 -18 -46q0 -27 18 -45l362 -362q19 -19 45 -19q27 0 46 19l543 543q18 18 18 45zM1536 640q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103 t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5t103 -385.5z" /> -<glyph unicode="" d="M896 160v192q0 14 -9 23t-23 9h-192q-14 0 -23 -9t-9 -23v-192q0 -14 9 -23t23 -9h192q14 0 23 9t9 23zM1152 832q0 88 -55.5 163t-138.5 116t-170 41q-243 0 -371 -213q-15 -24 8 -42l132 -100q7 -6 19 -6q16 0 25 12q53 68 86 92q34 24 86 24q48 0 85.5 -26t37.5 -59 q0 -38 -20 -61t-68 -45q-63 -28 -115.5 -86.5t-52.5 -125.5v-36q0 -14 9 -23t23 -9h192q14 0 23 9t9 23q0 19 21.5 49.5t54.5 49.5q32 18 49 28.5t46 35t44.5 48t28 60.5t12.5 81zM1536 640q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5 t-103 385.5t103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5t103 -385.5z" /> -<glyph unicode="" d="M1024 160v160q0 14 -9 23t-23 9h-96v512q0 14 -9 23t-23 9h-320q-14 0 -23 -9t-9 -23v-160q0 -14 9 -23t23 -9h96v-320h-96q-14 0 -23 -9t-9 -23v-160q0 -14 9 -23t23 -9h448q14 0 23 9t9 23zM896 1056v160q0 14 -9 23t-23 9h-192q-14 0 -23 -9t-9 -23v-160q0 -14 9 -23 t23 -9h192q14 0 23 9t9 23zM1536 640q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5t103 -385.5z" /> -<glyph unicode="" d="M1197 512h-109q-26 0 -45 19t-19 45v128q0 26 19 45t45 19h109q-32 108 -112.5 188.5t-188.5 112.5v-109q0 -26 -19 -45t-45 -19h-128q-26 0 -45 19t-19 45v109q-108 -32 -188.5 -112.5t-112.5 -188.5h109q26 0 45 -19t19 -45v-128q0 -26 -19 -45t-45 -19h-109 q32 -108 112.5 -188.5t188.5 -112.5v109q0 26 19 45t45 19h128q26 0 45 -19t19 -45v-109q108 32 188.5 112.5t112.5 188.5zM1536 704v-128q0 -26 -19 -45t-45 -19h-143q-37 -161 -154.5 -278.5t-278.5 -154.5v-143q0 -26 -19 -45t-45 -19h-128q-26 0 -45 19t-19 45v143 q-161 37 -278.5 154.5t-154.5 278.5h-143q-26 0 -45 19t-19 45v128q0 26 19 45t45 19h143q37 161 154.5 278.5t278.5 154.5v143q0 26 19 45t45 19h128q26 0 45 -19t19 -45v-143q161 -37 278.5 -154.5t154.5 -278.5h143q26 0 45 -19t19 -45z" /> -<glyph unicode="" d="M1097 457l-146 -146q-10 -10 -23 -10t-23 10l-137 137l-137 -137q-10 -10 -23 -10t-23 10l-146 146q-10 10 -10 23t10 23l137 137l-137 137q-10 10 -10 23t10 23l146 146q10 10 23 10t23 -10l137 -137l137 137q10 10 23 10t23 -10l146 -146q10 -10 10 -23t-10 -23 l-137 -137l137 -137q10 -10 10 -23t-10 -23zM1312 640q0 148 -73 273t-198 198t-273 73t-273 -73t-198 -198t-73 -273t73 -273t198 -198t273 -73t273 73t198 198t73 273zM1536 640q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5t-103 385.5 t103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5t103 -385.5z" /> -<glyph unicode="" d="M1171 723l-422 -422q-19 -19 -45 -19t-45 19l-294 294q-19 19 -19 45t19 45l102 102q19 19 45 19t45 -19l147 -147l275 275q19 19 45 19t45 -19l102 -102q19 -19 19 -45t-19 -45zM1312 640q0 148 -73 273t-198 198t-273 73t-273 -73t-198 -198t-73 -273t73 -273t198 -198 t273 -73t273 73t198 198t73 273zM1536 640q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5t103 -385.5z" /> -<glyph unicode="" d="M1312 643q0 161 -87 295l-754 -753q137 -89 297 -89q111 0 211.5 43.5t173.5 116.5t116 174.5t43 212.5zM313 344l755 754q-135 91 -300 91q-148 0 -273 -73t-198 -199t-73 -274q0 -162 89 -299zM1536 643q0 -157 -61 -300t-163.5 -246t-245 -164t-298.5 -61t-298.5 61 t-245 164t-163.5 246t-61 300t61 299.5t163.5 245.5t245 164t298.5 61t298.5 -61t245 -164t163.5 -245.5t61 -299.5z" /> -<glyph unicode="" d="M1536 640v-128q0 -53 -32.5 -90.5t-84.5 -37.5h-704l293 -294q38 -36 38 -90t-38 -90l-75 -76q-37 -37 -90 -37q-52 0 -91 37l-651 652q-37 37 -37 90q0 52 37 91l651 650q38 38 91 38q52 0 90 -38l75 -74q38 -38 38 -91t-38 -91l-293 -293h704q52 0 84.5 -37.5 t32.5 -90.5z" /> -<glyph unicode="" d="M1472 576q0 -54 -37 -91l-651 -651q-39 -37 -91 -37q-51 0 -90 37l-75 75q-38 38 -38 91t38 91l293 293h-704q-52 0 -84.5 37.5t-32.5 90.5v128q0 53 32.5 90.5t84.5 37.5h704l-293 294q-38 36 -38 90t38 90l75 75q38 38 90 38q53 0 91 -38l651 -651q37 -35 37 -90z" /> -<glyph unicode="" horiz-adv-x="1664" d="M1611 565q0 -51 -37 -90l-75 -75q-38 -38 -91 -38q-54 0 -90 38l-294 293v-704q0 -52 -37.5 -84.5t-90.5 -32.5h-128q-53 0 -90.5 32.5t-37.5 84.5v704l-294 -293q-36 -38 -90 -38t-90 38l-75 75q-38 38 -38 90q0 53 38 91l651 651q35 37 90 37q54 0 91 -37l651 -651 q37 -39 37 -91z" /> -<glyph unicode="" horiz-adv-x="1664" d="M1611 704q0 -53 -37 -90l-651 -652q-39 -37 -91 -37q-53 0 -90 37l-651 652q-38 36 -38 90q0 53 38 91l74 75q39 37 91 37q53 0 90 -37l294 -294v704q0 52 38 90t90 38h128q52 0 90 -38t38 -90v-704l294 294q37 37 90 37q52 0 91 -37l75 -75q37 -39 37 -91z" /> -<glyph unicode="" horiz-adv-x="1792" d="M1792 896q0 -26 -19 -45l-512 -512q-19 -19 -45 -19t-45 19t-19 45v256h-224q-98 0 -175.5 -6t-154 -21.5t-133 -42.5t-105.5 -69.5t-80 -101t-48.5 -138.5t-17.5 -181q0 -55 5 -123q0 -6 2.5 -23.5t2.5 -26.5q0 -15 -8.5 -25t-23.5 -10q-16 0 -28 17q-7 9 -13 22 t-13.5 30t-10.5 24q-127 285 -127 451q0 199 53 333q162 403 875 403h224v256q0 26 19 45t45 19t45 -19l512 -512q19 -19 19 -45z" /> -<glyph unicode="" d="M755 480q0 -13 -10 -23l-332 -332l144 -144q19 -19 19 -45t-19 -45t-45 -19h-448q-26 0 -45 19t-19 45v448q0 26 19 45t45 19t45 -19l144 -144l332 332q10 10 23 10t23 -10l114 -114q10 -10 10 -23zM1536 1344v-448q0 -26 -19 -45t-45 -19t-45 19l-144 144l-332 -332 q-10 -10 -23 -10t-23 10l-114 114q-10 10 -10 23t10 23l332 332l-144 144q-19 19 -19 45t19 45t45 19h448q26 0 45 -19t19 -45z" /> -<glyph unicode="" d="M768 576v-448q0 -26 -19 -45t-45 -19t-45 19l-144 144l-332 -332q-10 -10 -23 -10t-23 10l-114 114q-10 10 -10 23t10 23l332 332l-144 144q-19 19 -19 45t19 45t45 19h448q26 0 45 -19t19 -45zM1523 1248q0 -13 -10 -23l-332 -332l144 -144q19 -19 19 -45t-19 -45 t-45 -19h-448q-26 0 -45 19t-19 45v448q0 26 19 45t45 19t45 -19l144 -144l332 332q10 10 23 10t23 -10l114 -114q10 -10 10 -23z" /> -<glyph unicode="" horiz-adv-x="1408" d="M1408 800v-192q0 -40 -28 -68t-68 -28h-416v-416q0 -40 -28 -68t-68 -28h-192q-40 0 -68 28t-28 68v416h-416q-40 0 -68 28t-28 68v192q0 40 28 68t68 28h416v416q0 40 28 68t68 28h192q40 0 68 -28t28 -68v-416h416q40 0 68 -28t28 -68z" /> -<glyph unicode="" horiz-adv-x="1408" d="M1408 800v-192q0 -40 -28 -68t-68 -28h-1216q-40 0 -68 28t-28 68v192q0 40 28 68t68 28h1216q40 0 68 -28t28 -68z" /> -<glyph unicode="" horiz-adv-x="1664" d="M1482 486q46 -26 59.5 -77.5t-12.5 -97.5l-64 -110q-26 -46 -77.5 -59.5t-97.5 12.5l-266 153v-307q0 -52 -38 -90t-90 -38h-128q-52 0 -90 38t-38 90v307l-266 -153q-46 -26 -97.5 -12.5t-77.5 59.5l-64 110q-26 46 -12.5 97.5t59.5 77.5l266 154l-266 154 q-46 26 -59.5 77.5t12.5 97.5l64 110q26 46 77.5 59.5t97.5 -12.5l266 -153v307q0 52 38 90t90 38h128q52 0 90 -38t38 -90v-307l266 153q46 26 97.5 12.5t77.5 -59.5l64 -110q26 -46 12.5 -97.5t-59.5 -77.5l-266 -154z" /> -<glyph unicode="" d="M768 1408q209 0 385.5 -103t279.5 -279.5t103 -385.5t-103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103zM896 161v190q0 14 -9 23.5t-22 9.5h-192q-13 0 -23 -10t-10 -23v-190q0 -13 10 -23t23 -10h192 q13 0 22 9.5t9 23.5zM894 505l18 621q0 12 -10 18q-10 8 -24 8h-220q-14 0 -24 -8q-10 -6 -10 -18l17 -621q0 -10 10 -17.5t24 -7.5h185q14 0 23.5 7.5t10.5 17.5z" /> -<glyph unicode="" d="M928 180v56v468v192h-320v-192v-468v-56q0 -25 18 -38.5t46 -13.5h192q28 0 46 13.5t18 38.5zM472 1024h195l-126 161q-26 31 -69 31q-40 0 -68 -28t-28 -68t28 -68t68 -28zM1160 1120q0 40 -28 68t-68 28q-43 0 -69 -31l-125 -161h194q40 0 68 28t28 68zM1536 864v-320 q0 -14 -9 -23t-23 -9h-96v-416q0 -40 -28 -68t-68 -28h-1088q-40 0 -68 28t-28 68v416h-96q-14 0 -23 9t-9 23v320q0 14 9 23t23 9h440q-93 0 -158.5 65.5t-65.5 158.5t65.5 158.5t158.5 65.5q107 0 168 -77l128 -165l128 165q61 77 168 77q93 0 158.5 -65.5t65.5 -158.5 t-65.5 -158.5t-158.5 -65.5h440q14 0 23 -9t9 -23z" /> -<glyph unicode="" horiz-adv-x="1792" d="M1280 832q0 26 -19 45t-45 19q-172 0 -318 -49.5t-259.5 -134t-235.5 -219.5q-19 -21 -19 -45q0 -26 19 -45t45 -19q24 0 45 19q27 24 74 71t67 66q137 124 268.5 176t313.5 52q26 0 45 19t19 45zM1792 1030q0 -95 -20 -193q-46 -224 -184.5 -383t-357.5 -268 q-214 -108 -438 -108q-148 0 -286 47q-15 5 -88 42t-96 37q-16 0 -39.5 -32t-45 -70t-52.5 -70t-60 -32q-30 0 -51 11t-31 24t-27 42q-2 4 -6 11t-5.5 10t-3 9.5t-1.5 13.5q0 35 31 73.5t68 65.5t68 56t31 48q0 4 -14 38t-16 44q-9 51 -9 104q0 115 43.5 220t119 184.5 t170.5 139t204 95.5q55 18 145 25.5t179.5 9t178.5 6t163.5 24t113.5 56.5l29.5 29.5t29.5 28t27 20t36.5 16t43.5 4.5q39 0 70.5 -46t47.5 -112t24 -124t8 -96z" /> -<glyph unicode="" horiz-adv-x="1408" d="M1408 -160v-64q0 -13 -9.5 -22.5t-22.5 -9.5h-1344q-13 0 -22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h1344q13 0 22.5 -9.5t9.5 -22.5zM1152 896q0 -78 -24.5 -144t-64 -112.5t-87.5 -88t-96 -77.5t-87.5 -72t-64 -81.5t-24.5 -96.5q0 -96 67 -224l-4 1l1 -1 q-90 41 -160 83t-138.5 100t-113.5 122.5t-72.5 150.5t-27.5 184q0 78 24.5 144t64 112.5t87.5 88t96 77.5t87.5 72t64 81.5t24.5 96.5q0 94 -66 224l3 -1l-1 1q90 -41 160 -83t138.5 -100t113.5 -122.5t72.5 -150.5t27.5 -184z" /> -<glyph unicode="" horiz-adv-x="1792" d="M1664 576q-152 236 -381 353q61 -104 61 -225q0 -185 -131.5 -316.5t-316.5 -131.5t-316.5 131.5t-131.5 316.5q0 121 61 225q-229 -117 -381 -353q133 -205 333.5 -326.5t434.5 -121.5t434.5 121.5t333.5 326.5zM944 960q0 20 -14 34t-34 14q-125 0 -214.5 -89.5 t-89.5 -214.5q0 -20 14 -34t34 -14t34 14t14 34q0 86 61 147t147 61q20 0 34 14t14 34zM1792 576q0 -34 -20 -69q-140 -230 -376.5 -368.5t-499.5 -138.5t-499.5 139t-376.5 368q-20 35 -20 69t20 69q140 229 376.5 368t499.5 139t499.5 -139t376.5 -368q20 -35 20 -69z" /> -<glyph unicode="" horiz-adv-x="1792" d="M555 201l78 141q-87 63 -136 159t-49 203q0 121 61 225q-229 -117 -381 -353q167 -258 427 -375zM944 960q0 20 -14 34t-34 14q-125 0 -214.5 -89.5t-89.5 -214.5q0 -20 14 -34t34 -14t34 14t14 34q0 86 61 147t147 61q20 0 34 14t14 34zM1307 1151q0 -7 -1 -9 q-105 -188 -315 -566t-316 -567l-49 -89q-10 -16 -28 -16q-12 0 -134 70q-16 10 -16 28q0 12 44 87q-143 65 -263.5 173t-208.5 245q-20 31 -20 69t20 69q153 235 380 371t496 136q89 0 180 -17l54 97q10 16 28 16q5 0 18 -6t31 -15.5t33 -18.5t31.5 -18.5t19.5 -11.5 q16 -10 16 -27zM1344 704q0 -139 -79 -253.5t-209 -164.5l280 502q8 -45 8 -84zM1792 576q0 -35 -20 -69q-39 -64 -109 -145q-150 -172 -347.5 -267t-419.5 -95l74 132q212 18 392.5 137t301.5 307q-115 179 -282 294l63 112q95 -64 182.5 -153t144.5 -184q20 -34 20 -69z " /> -<glyph unicode="" horiz-adv-x="1792" d="M1024 161v190q0 14 -9.5 23.5t-22.5 9.5h-192q-13 0 -22.5 -9.5t-9.5 -23.5v-190q0 -14 9.5 -23.5t22.5 -9.5h192q13 0 22.5 9.5t9.5 23.5zM1022 535l18 459q0 12 -10 19q-13 11 -24 11h-220q-11 0 -24 -11q-10 -7 -10 -21l17 -457q0 -10 10 -16.5t24 -6.5h185 q14 0 23.5 6.5t10.5 16.5zM1008 1469l768 -1408q35 -63 -2 -126q-17 -29 -46.5 -46t-63.5 -17h-1536q-34 0 -63.5 17t-46.5 46q-37 63 -2 126l768 1408q17 31 47 49t65 18t65 -18t47 -49z" /> -<glyph unicode="" horiz-adv-x="1408" d="M1376 1376q44 -52 12 -148t-108 -172l-161 -161l160 -696q5 -19 -12 -33l-128 -96q-7 -6 -19 -6q-4 0 -7 1q-15 3 -21 16l-279 508l-259 -259l53 -194q5 -17 -8 -31l-96 -96q-9 -9 -23 -9h-2q-15 2 -24 13l-189 252l-252 189q-11 7 -13 23q-1 13 9 25l96 97q9 9 23 9 q6 0 8 -1l194 -53l259 259l-508 279q-14 8 -17 24q-2 16 9 27l128 128q14 13 30 8l665 -159l160 160q76 76 172 108t148 -12z" /> -<glyph unicode="" horiz-adv-x="1664" d="M128 -128h288v288h-288v-288zM480 -128h320v288h-320v-288zM128 224h288v320h-288v-320zM480 224h320v320h-320v-320zM128 608h288v288h-288v-288zM864 -128h320v288h-320v-288zM480 608h320v288h-320v-288zM1248 -128h288v288h-288v-288zM864 224h320v320h-320v-320z M512 1088v288q0 13 -9.5 22.5t-22.5 9.5h-64q-13 0 -22.5 -9.5t-9.5 -22.5v-288q0 -13 9.5 -22.5t22.5 -9.5h64q13 0 22.5 9.5t9.5 22.5zM1248 224h288v320h-288v-320zM864 608h320v288h-320v-288zM1248 608h288v288h-288v-288zM1280 1088v288q0 13 -9.5 22.5t-22.5 9.5h-64 q-13 0 -22.5 -9.5t-9.5 -22.5v-288q0 -13 9.5 -22.5t22.5 -9.5h64q13 0 22.5 9.5t9.5 22.5zM1664 1152v-1280q0 -52 -38 -90t-90 -38h-1408q-52 0 -90 38t-38 90v1280q0 52 38 90t90 38h128v96q0 66 47 113t113 47h64q66 0 113 -47t47 -113v-96h384v96q0 66 47 113t113 47 h64q66 0 113 -47t47 -113v-96h128q52 0 90 -38t38 -90z" /> -<glyph unicode="" horiz-adv-x="1792" d="M666 1055q-60 -92 -137 -273q-22 45 -37 72.5t-40.5 63.5t-51 56.5t-63 35t-81.5 14.5h-224q-14 0 -23 9t-9 23v192q0 14 9 23t23 9h224q250 0 410 -225zM1792 256q0 -14 -9 -23l-320 -320q-9 -9 -23 -9q-13 0 -22.5 9.5t-9.5 22.5v192q-32 0 -85 -0.5t-81 -1t-73 1 t-71 5t-64 10.5t-63 18.5t-58 28.5t-59 40t-55 53.5t-56 69.5q59 93 136 273q22 -45 37 -72.5t40.5 -63.5t51 -56.5t63 -35t81.5 -14.5h256v192q0 14 9 23t23 9q12 0 24 -10l319 -319q9 -9 9 -23zM1792 1152q0 -14 -9 -23l-320 -320q-9 -9 -23 -9q-13 0 -22.5 9.5t-9.5 22.5 v192h-256q-48 0 -87 -15t-69 -45t-51 -61.5t-45 -77.5q-32 -62 -78 -171q-29 -66 -49.5 -111t-54 -105t-64 -100t-74 -83t-90 -68.5t-106.5 -42t-128 -16.5h-224q-14 0 -23 9t-9 23v192q0 14 9 23t23 9h224q48 0 87 15t69 45t51 61.5t45 77.5q32 62 78 171q29 66 49.5 111 t54 105t64 100t74 83t90 68.5t106.5 42t128 16.5h256v192q0 14 9 23t23 9q12 0 24 -10l319 -319q9 -9 9 -23z" /> -<glyph unicode="" horiz-adv-x="1792" d="M1792 640q0 -174 -120 -321.5t-326 -233t-450 -85.5q-70 0 -145 8q-198 -175 -460 -242q-49 -14 -114 -22q-17 -2 -30.5 9t-17.5 29v1q-3 4 -0.5 12t2 10t4.5 9.5l6 9t7 8.5t8 9q7 8 31 34.5t34.5 38t31 39.5t32.5 51t27 59t26 76q-157 89 -247.5 220t-90.5 281 q0 130 71 248.5t191 204.5t286 136.5t348 50.5q244 0 450 -85.5t326 -233t120 -321.5z" /> -<glyph unicode="" d="M1536 704v-128q0 -201 -98.5 -362t-274 -251.5t-395.5 -90.5t-395.5 90.5t-274 251.5t-98.5 362v128q0 26 19 45t45 19h384q26 0 45 -19t19 -45v-128q0 -52 23.5 -90t53.5 -57t71 -30t64 -13t44 -2t44 2t64 13t71 30t53.5 57t23.5 90v128q0 26 19 45t45 19h384 q26 0 45 -19t19 -45zM512 1344v-384q0 -26 -19 -45t-45 -19h-384q-26 0 -45 19t-19 45v384q0 26 19 45t45 19h384q26 0 45 -19t19 -45zM1536 1344v-384q0 -26 -19 -45t-45 -19h-384q-26 0 -45 19t-19 45v384q0 26 19 45t45 19h384q26 0 45 -19t19 -45z" /> -<glyph unicode="" horiz-adv-x="1792" d="M1683 205l-166 -165q-19 -19 -45 -19t-45 19l-531 531l-531 -531q-19 -19 -45 -19t-45 19l-166 165q-19 19 -19 45.5t19 45.5l742 741q19 19 45 19t45 -19l742 -741q19 -19 19 -45.5t-19 -45.5z" /> -<glyph unicode="" horiz-adv-x="1792" d="M1683 728l-742 -741q-19 -19 -45 -19t-45 19l-742 741q-19 19 -19 45.5t19 45.5l166 165q19 19 45 19t45 -19l531 -531l531 531q19 19 45 19t45 -19l166 -165q19 -19 19 -45.5t-19 -45.5z" /> -<glyph unicode="" horiz-adv-x="1920" d="M1280 32q0 -13 -9.5 -22.5t-22.5 -9.5h-960q-8 0 -13.5 2t-9 7t-5.5 8t-3 11.5t-1 11.5v13v11v160v416h-192q-26 0 -45 19t-19 45q0 24 15 41l320 384q19 22 49 22t49 -22l320 -384q15 -17 15 -41q0 -26 -19 -45t-45 -19h-192v-384h576q16 0 25 -11l160 -192q7 -11 7 -21 zM1920 448q0 -24 -15 -41l-320 -384q-20 -23 -49 -23t-49 23l-320 384q-15 17 -15 41q0 26 19 45t45 19h192v384h-576q-16 0 -25 12l-160 192q-7 9 -7 20q0 13 9.5 22.5t22.5 9.5h960q8 0 13.5 -2t9 -7t5.5 -8t3 -11.5t1 -11.5v-13v-11v-160v-416h192q26 0 45 -19t19 -45z " /> -<glyph unicode="" horiz-adv-x="1664" d="M640 0q0 -52 -38 -90t-90 -38t-90 38t-38 90t38 90t90 38t90 -38t38 -90zM1536 0q0 -52 -38 -90t-90 -38t-90 38t-38 90t38 90t90 38t90 -38t38 -90zM1664 1088v-512q0 -24 -16.5 -42.5t-40.5 -21.5l-1044 -122q13 -60 13 -70q0 -16 -24 -64h920q26 0 45 -19t19 -45 t-19 -45t-45 -19h-1024q-26 0 -45 19t-19 45q0 11 8 31.5t16 36t21.5 40t15.5 29.5l-177 823h-204q-26 0 -45 19t-19 45t19 45t45 19h256q16 0 28.5 -6.5t19.5 -15.5t13 -24.5t8 -26t5.5 -29.5t4.5 -26h1201q26 0 45 -19t19 -45z" /> -<glyph unicode="" horiz-adv-x="1664" d="M1664 928v-704q0 -92 -66 -158t-158 -66h-1216q-92 0 -158 66t-66 158v960q0 92 66 158t158 66h320q92 0 158 -66t66 -158v-32h672q92 0 158 -66t66 -158z" /> -<glyph unicode="" horiz-adv-x="1920" d="M1879 584q0 -31 -31 -66l-336 -396q-43 -51 -120.5 -86.5t-143.5 -35.5h-1088q-34 0 -60.5 13t-26.5 43q0 31 31 66l336 396q43 51 120.5 86.5t143.5 35.5h1088q34 0 60.5 -13t26.5 -43zM1536 928v-160h-832q-94 0 -197 -47.5t-164 -119.5l-337 -396l-5 -6q0 4 -0.5 12.5 t-0.5 12.5v960q0 92 66 158t158 66h320q92 0 158 -66t66 -158v-32h544q92 0 158 -66t66 -158z" /> -<glyph unicode="" horiz-adv-x="768" d="M704 1216q0 -26 -19 -45t-45 -19h-128v-1024h128q26 0 45 -19t19 -45t-19 -45l-256 -256q-19 -19 -45 -19t-45 19l-256 256q-19 19 -19 45t19 45t45 19h128v1024h-128q-26 0 -45 19t-19 45t19 45l256 256q19 19 45 19t45 -19l256 -256q19 -19 19 -45z" /> -<glyph unicode="" horiz-adv-x="1792" d="M1792 640q0 -26 -19 -45l-256 -256q-19 -19 -45 -19t-45 19t-19 45v128h-1024v-128q0 -26 -19 -45t-45 -19t-45 19l-256 256q-19 19 -19 45t19 45l256 256q19 19 45 19t45 -19t19 -45v-128h1024v128q0 26 19 45t45 19t45 -19l256 -256q19 -19 19 -45z" /> -<glyph unicode="" horiz-adv-x="2048" d="M640 640v-512h-256v512h256zM1024 1152v-1024h-256v1024h256zM2048 0v-128h-2048v1536h128v-1408h1920zM1408 896v-768h-256v768h256zM1792 1280v-1152h-256v1152h256z" /> -<glyph unicode="" d="M1280 926q-56 -25 -121 -34q68 40 93 117q-65 -38 -134 -51q-61 66 -153 66q-87 0 -148.5 -61.5t-61.5 -148.5q0 -29 5 -48q-129 7 -242 65t-192 155q-29 -50 -29 -106q0 -114 91 -175q-47 1 -100 26v-2q0 -75 50 -133.5t123 -72.5q-29 -8 -51 -8q-13 0 -39 4 q21 -63 74.5 -104t121.5 -42q-116 -90 -261 -90q-26 0 -50 3q148 -94 322 -94q112 0 210 35.5t168 95t120.5 137t75 162t24.5 168.5q0 18 -1 27q63 45 105 109zM1536 1120v-960q0 -119 -84.5 -203.5t-203.5 -84.5h-960q-119 0 -203.5 84.5t-84.5 203.5v960q0 119 84.5 203.5 t203.5 84.5h960q119 0 203.5 -84.5t84.5 -203.5z" /> -<glyph unicode="" d="M1248 1408q119 0 203.5 -84.5t84.5 -203.5v-960q0 -119 -84.5 -203.5t-203.5 -84.5h-188v595h199l30 232h-229v148q0 56 23.5 84t91.5 28l122 1v207q-63 9 -178 9q-136 0 -217.5 -80t-81.5 -226v-171h-200v-232h200v-595h-532q-119 0 -203.5 84.5t-84.5 203.5v960 q0 119 84.5 203.5t203.5 84.5h960z" /> -<glyph unicode="" horiz-adv-x="1792" d="M928 704q0 14 -9 23t-23 9q-66 0 -113 -47t-47 -113q0 -14 9 -23t23 -9t23 9t9 23q0 40 28 68t68 28q14 0 23 9t9 23zM1152 574q0 -106 -75 -181t-181 -75t-181 75t-75 181t75 181t181 75t181 -75t75 -181zM128 0h1536v128h-1536v-128zM1280 574q0 159 -112.5 271.5 t-271.5 112.5t-271.5 -112.5t-112.5 -271.5t112.5 -271.5t271.5 -112.5t271.5 112.5t112.5 271.5zM256 1216h384v128h-384v-128zM128 1024h1536v118v138h-828l-64 -128h-644v-128zM1792 1280v-1280q0 -53 -37.5 -90.5t-90.5 -37.5h-1536q-53 0 -90.5 37.5t-37.5 90.5v1280 q0 53 37.5 90.5t90.5 37.5h1536q53 0 90.5 -37.5t37.5 -90.5z" /> -<glyph unicode="" horiz-adv-x="1792" d="M832 1024q0 80 -56 136t-136 56t-136 -56t-56 -136q0 -42 19 -83q-41 19 -83 19q-80 0 -136 -56t-56 -136t56 -136t136 -56t136 56t56 136q0 42 -19 83q41 -19 83 -19q80 0 136 56t56 136zM1683 320q0 -17 -49 -66t-66 -49q-9 0 -28.5 16t-36.5 33t-38.5 40t-24.5 26 l-96 -96l220 -220q28 -28 28 -68q0 -42 -39 -81t-81 -39q-40 0 -68 28l-671 671q-176 -131 -365 -131q-163 0 -265.5 102.5t-102.5 265.5q0 160 95 313t248 248t313 95q163 0 265.5 -102.5t102.5 -265.5q0 -189 -131 -365l355 -355l96 96q-3 3 -26 24.5t-40 38.5t-33 36.5 t-16 28.5q0 17 49 66t66 49q13 0 23 -10q6 -6 46 -44.5t82 -79.5t86.5 -86t73 -78t28.5 -41z" /> -<glyph unicode="" horiz-adv-x="1920" d="M896 640q0 106 -75 181t-181 75t-181 -75t-75 -181t75 -181t181 -75t181 75t75 181zM1664 128q0 52 -38 90t-90 38t-90 -38t-38 -90q0 -53 37.5 -90.5t90.5 -37.5t90.5 37.5t37.5 90.5zM1664 1152q0 52 -38 90t-90 38t-90 -38t-38 -90q0 -53 37.5 -90.5t90.5 -37.5 t90.5 37.5t37.5 90.5zM1280 731v-185q0 -10 -7 -19.5t-16 -10.5l-155 -24q-11 -35 -32 -76q34 -48 90 -115q7 -10 7 -20q0 -12 -7 -19q-23 -30 -82.5 -89.5t-78.5 -59.5q-11 0 -21 7l-115 90q-37 -19 -77 -31q-11 -108 -23 -155q-7 -24 -30 -24h-186q-11 0 -20 7.5t-10 17.5 l-23 153q-34 10 -75 31l-118 -89q-7 -7 -20 -7q-11 0 -21 8q-144 133 -144 160q0 9 7 19q10 14 41 53t47 61q-23 44 -35 82l-152 24q-10 1 -17 9.5t-7 19.5v185q0 10 7 19.5t16 10.5l155 24q11 35 32 76q-34 48 -90 115q-7 11 -7 20q0 12 7 20q22 30 82 89t79 59q11 0 21 -7 l115 -90q34 18 77 32q11 108 23 154q7 24 30 24h186q11 0 20 -7.5t10 -17.5l23 -153q34 -10 75 -31l118 89q8 7 20 7q11 0 21 -8q144 -133 144 -160q0 -9 -7 -19q-12 -16 -42 -54t-45 -60q23 -48 34 -82l152 -23q10 -2 17 -10.5t7 -19.5zM1920 198v-140q0 -16 -149 -31 q-12 -27 -30 -52q51 -113 51 -138q0 -4 -4 -7q-122 -71 -124 -71q-8 0 -46 47t-52 68q-20 -2 -30 -2t-30 2q-14 -21 -52 -68t-46 -47q-2 0 -124 71q-4 3 -4 7q0 25 51 138q-18 25 -30 52q-149 15 -149 31v140q0 16 149 31q13 29 30 52q-51 113 -51 138q0 4 4 7q4 2 35 20 t59 34t30 16q8 0 46 -46.5t52 -67.5q20 2 30 2t30 -2q51 71 92 112l6 2q4 0 124 -70q4 -3 4 -7q0 -25 -51 -138q17 -23 30 -52q149 -15 149 -31zM1920 1222v-140q0 -16 -149 -31q-12 -27 -30 -52q51 -113 51 -138q0 -4 -4 -7q-122 -71 -124 -71q-8 0 -46 47t-52 68 q-20 -2 -30 -2t-30 2q-14 -21 -52 -68t-46 -47q-2 0 -124 71q-4 3 -4 7q0 25 51 138q-18 25 -30 52q-149 15 -149 31v140q0 16 149 31q13 29 30 52q-51 113 -51 138q0 4 4 7q4 2 35 20t59 34t30 16q8 0 46 -46.5t52 -67.5q20 2 30 2t30 -2q51 71 92 112l6 2q4 0 124 -70 q4 -3 4 -7q0 -25 -51 -138q17 -23 30 -52q149 -15 149 -31z" /> -<glyph unicode="" horiz-adv-x="1792" d="M1408 768q0 -139 -94 -257t-256.5 -186.5t-353.5 -68.5q-86 0 -176 16q-124 -88 -278 -128q-36 -9 -86 -16h-3q-11 0 -20.5 8t-11.5 21q-1 3 -1 6.5t0.5 6.5t2 6l2.5 5t3.5 5.5t4 5t4.5 5t4 4.5q5 6 23 25t26 29.5t22.5 29t25 38.5t20.5 44q-124 72 -195 177t-71 224 q0 139 94 257t256.5 186.5t353.5 68.5t353.5 -68.5t256.5 -186.5t94 -257zM1792 512q0 -120 -71 -224.5t-195 -176.5q10 -24 20.5 -44t25 -38.5t22.5 -29t26 -29.5t23 -25q1 -1 4 -4.5t4.5 -5t4 -5t3.5 -5.5l2.5 -5t2 -6t0.5 -6.5t-1 -6.5q-3 -14 -13 -22t-22 -7 q-50 7 -86 16q-154 40 -278 128q-90 -16 -176 -16q-271 0 -472 132q58 -4 88 -4q161 0 309 45t264 129q125 92 192 212t67 254q0 77 -23 152q129 -71 204 -178t75 -230z" /> -<glyph unicode="" d="M256 192q0 26 -19 45t-45 19t-45 -19t-19 -45t19 -45t45 -19t45 19t19 45zM1408 768q0 51 -39 89.5t-89 38.5h-352q0 58 48 159.5t48 160.5q0 98 -32 145t-128 47q-26 -26 -38 -85t-30.5 -125.5t-59.5 -109.5q-22 -23 -77 -91q-4 -5 -23 -30t-31.5 -41t-34.5 -42.5 t-40 -44t-38.5 -35.5t-40 -27t-35.5 -9h-32v-640h32q13 0 31.5 -3t33 -6.5t38 -11t35 -11.5t35.5 -12.5t29 -10.5q211 -73 342 -73h121q192 0 192 167q0 26 -5 56q30 16 47.5 52.5t17.5 73.5t-18 69q53 50 53 119q0 25 -10 55.5t-25 47.5q32 1 53.5 47t21.5 81zM1536 769 q0 -89 -49 -163q9 -33 9 -69q0 -77 -38 -144q3 -21 3 -43q0 -101 -60 -178q1 -139 -85 -219.5t-227 -80.5h-36h-93q-96 0 -189.5 22.5t-216.5 65.5q-116 40 -138 40h-288q-53 0 -90.5 37.5t-37.5 90.5v640q0 53 37.5 90.5t90.5 37.5h274q36 24 137 155q58 75 107 128 q24 25 35.5 85.5t30.5 126.5t62 108q39 37 90 37q84 0 151 -32.5t102 -101.5t35 -186q0 -93 -48 -192h176q104 0 180 -76t76 -179z" /> -<glyph unicode="" d="M256 1088q0 26 -19 45t-45 19t-45 -19t-19 -45t19 -45t45 -19t45 19t19 45zM1408 512q0 35 -21.5 81t-53.5 47q15 17 25 47.5t10 55.5q0 69 -53 119q18 32 18 69t-17.5 73.5t-47.5 52.5q5 30 5 56q0 85 -49 126t-136 41h-128q-131 0 -342 -73q-5 -2 -29 -10.5 t-35.5 -12.5t-35 -11.5t-38 -11t-33 -6.5t-31.5 -3h-32v-640h32q16 0 35.5 -9t40 -27t38.5 -35.5t40 -44t34.5 -42.5t31.5 -41t23 -30q55 -68 77 -91q41 -43 59.5 -109.5t30.5 -125.5t38 -85q96 0 128 47t32 145q0 59 -48 160.5t-48 159.5h352q50 0 89 38.5t39 89.5z M1536 511q0 -103 -76 -179t-180 -76h-176q48 -99 48 -192q0 -118 -35 -186q-35 -69 -102 -101.5t-151 -32.5q-51 0 -90 37q-34 33 -54 82t-25.5 90.5t-17.5 84.5t-31 64q-48 50 -107 127q-101 131 -137 155h-274q-53 0 -90.5 37.5t-37.5 90.5v640q0 53 37.5 90.5t90.5 37.5 h288q22 0 138 40q128 44 223 66t200 22h112q140 0 226.5 -79t85.5 -216v-5q60 -77 60 -178q0 -22 -3 -43q38 -67 38 -144q0 -36 -9 -69q49 -74 49 -163z" /> -<glyph unicode="" horiz-adv-x="896" d="M832 1504v-1339l-449 -236q-22 -12 -40 -12q-21 0 -31.5 14.5t-10.5 35.5q0 6 2 20l86 500l-364 354q-25 27 -25 48q0 37 56 46l502 73l225 455q19 41 49 41z" /> -<glyph unicode="" horiz-adv-x="1792" d="M1664 940q0 81 -21.5 143t-55 98.5t-81.5 59.5t-94 31t-98 8t-112 -25.5t-110.5 -64t-86.5 -72t-60 -61.5q-18 -22 -49 -22t-49 22q-24 28 -60 61.5t-86.5 72t-110.5 64t-112 25.5t-98 -8t-94 -31t-81.5 -59.5t-55 -98.5t-21.5 -143q0 -168 187 -355l581 -560l580 559 q188 188 188 356zM1792 940q0 -221 -229 -450l-623 -600q-18 -18 -44 -18t-44 18l-624 602q-10 8 -27.5 26t-55.5 65.5t-68 97.5t-53.5 121t-23.5 138q0 220 127 344t351 124q62 0 126.5 -21.5t120 -58t95.5 -68.5t76 -68q36 36 76 68t95.5 68.5t120 58t126.5 21.5 q224 0 351 -124t127 -344z" /> -<glyph unicode="" horiz-adv-x="1664" d="M640 96q0 -4 1 -20t0.5 -26.5t-3 -23.5t-10 -19.5t-20.5 -6.5h-320q-119 0 -203.5 84.5t-84.5 203.5v704q0 119 84.5 203.5t203.5 84.5h320q13 0 22.5 -9.5t9.5 -22.5q0 -4 1 -20t0.5 -26.5t-3 -23.5t-10 -19.5t-20.5 -6.5h-320q-66 0 -113 -47t-47 -113v-704 q0 -66 47 -113t113 -47h288h11h13t11.5 -1t11.5 -3t8 -5.5t7 -9t2 -13.5zM1568 640q0 -26 -19 -45l-544 -544q-19 -19 -45 -19t-45 19t-19 45v288h-448q-26 0 -45 19t-19 45v384q0 26 19 45t45 19h448v288q0 26 19 45t45 19t45 -19l544 -544q19 -19 19 -45z" /> -<glyph unicode="" d="M237 122h231v694h-231v-694zM483 1030q-1 52 -36 86t-93 34t-94.5 -34t-36.5 -86q0 -51 35.5 -85.5t92.5 -34.5h1q59 0 95 34.5t36 85.5zM1068 122h231v398q0 154 -73 233t-193 79q-136 0 -209 -117h2v101h-231q3 -66 0 -694h231v388q0 38 7 56q15 35 45 59.5t74 24.5 q116 0 116 -157v-371zM1536 1120v-960q0 -119 -84.5 -203.5t-203.5 -84.5h-960q-119 0 -203.5 84.5t-84.5 203.5v960q0 119 84.5 203.5t203.5 84.5h960q119 0 203.5 -84.5t84.5 -203.5z" /> -<glyph unicode="" horiz-adv-x="1152" d="M480 672v448q0 14 -9 23t-23 9t-23 -9t-9 -23v-448q0 -14 9 -23t23 -9t23 9t9 23zM1152 320q0 -26 -19 -45t-45 -19h-429l-51 -483q-2 -12 -10.5 -20.5t-20.5 -8.5h-1q-27 0 -32 27l-76 485h-404q-26 0 -45 19t-19 45q0 123 78.5 221.5t177.5 98.5v512q-52 0 -90 38 t-38 90t38 90t90 38h640q52 0 90 -38t38 -90t-38 -90t-90 -38v-512q99 0 177.5 -98.5t78.5 -221.5z" /> -<glyph unicode="" horiz-adv-x="1792" d="M1408 608v-320q0 -119 -84.5 -203.5t-203.5 -84.5h-832q-119 0 -203.5 84.5t-84.5 203.5v832q0 119 84.5 203.5t203.5 84.5h704q14 0 23 -9t9 -23v-64q0 -14 -9 -23t-23 -9h-704q-66 0 -113 -47t-47 -113v-832q0 -66 47 -113t113 -47h832q66 0 113 47t47 113v320 q0 14 9 23t23 9h64q14 0 23 -9t9 -23zM1792 1472v-512q0 -26 -19 -45t-45 -19t-45 19l-176 176l-652 -652q-10 -10 -23 -10t-23 10l-114 114q-10 10 -10 23t10 23l652 652l-176 176q-19 19 -19 45t19 45t45 19h512q26 0 45 -19t19 -45z" /> -<glyph unicode="" d="M1184 640q0 -26 -19 -45l-544 -544q-19 -19 -45 -19t-45 19t-19 45v288h-448q-26 0 -45 19t-19 45v384q0 26 19 45t45 19h448v288q0 26 19 45t45 19t45 -19l544 -544q19 -19 19 -45zM1536 992v-704q0 -119 -84.5 -203.5t-203.5 -84.5h-320q-13 0 -22.5 9.5t-9.5 22.5 q0 4 -1 20t-0.5 26.5t3 23.5t10 19.5t20.5 6.5h320q66 0 113 47t47 113v704q0 66 -47 113t-113 47h-288h-11h-13t-11.5 1t-11.5 3t-8 5.5t-7 9t-2 13.5q0 4 -1 20t-0.5 26.5t3 23.5t10 19.5t20.5 6.5h320q119 0 203.5 -84.5t84.5 -203.5z" /> -<glyph unicode="" horiz-adv-x="1664" d="M458 653q-74 162 -74 371h-256v-96q0 -78 94.5 -162t235.5 -113zM1536 928v96h-256q0 -209 -74 -371q141 29 235.5 113t94.5 162zM1664 1056v-128q0 -71 -41.5 -143t-112 -130t-173 -97.5t-215.5 -44.5q-42 -54 -95 -95q-38 -34 -52.5 -72.5t-14.5 -89.5q0 -54 30.5 -91 t97.5 -37q75 0 133.5 -45.5t58.5 -114.5v-64q0 -14 -9 -23t-23 -9h-832q-14 0 -23 9t-9 23v64q0 69 58.5 114.5t133.5 45.5q67 0 97.5 37t30.5 91q0 51 -14.5 89.5t-52.5 72.5q-53 41 -95 95q-113 5 -215.5 44.5t-173 97.5t-112 130t-41.5 143v128q0 40 28 68t68 28h288v96 q0 66 47 113t113 47h576q66 0 113 -47t47 -113v-96h288q40 0 68 -28t28 -68z" /> -<glyph unicode="" d="M394 184q-8 -9 -20 3q-13 11 -4 19q8 9 20 -3q12 -11 4 -19zM352 245q9 -12 0 -19q-8 -6 -17 7t0 18q9 7 17 -6zM291 305q-5 -7 -13 -2q-10 5 -7 12q3 5 13 2q10 -5 7 -12zM322 271q-6 -7 -16 3q-9 11 -2 16q6 6 16 -3q9 -11 2 -16zM451 159q-4 -12 -19 -6q-17 4 -13 15 t19 7q16 -5 13 -16zM514 154q0 -11 -16 -11q-17 -2 -17 11q0 11 16 11q17 2 17 -11zM572 164q2 -10 -14 -14t-18 8t14 15q16 2 18 -9zM1536 1120v-960q0 -119 -84.5 -203.5t-203.5 -84.5h-224q-16 0 -24.5 1t-19.5 5t-16 14.5t-5 27.5v239q0 97 -52 142q57 6 102.5 18t94 39 t81 66.5t53 105t20.5 150.5q0 121 -79 206q37 91 -8 204q-28 9 -81 -11t-92 -44l-38 -24q-93 26 -192 26t-192 -26q-16 11 -42.5 27t-83.5 38.5t-86 13.5q-44 -113 -7 -204q-79 -85 -79 -206q0 -85 20.5 -150t52.5 -105t80.5 -67t94 -39t102.5 -18q-40 -36 -49 -103 q-21 -10 -45 -15t-57 -5t-65.5 21.5t-55.5 62.5q-19 32 -48.5 52t-49.5 24l-20 3q-21 0 -29 -4.5t-5 -11.5t9 -14t13 -12l7 -5q22 -10 43.5 -38t31.5 -51l10 -23q13 -38 44 -61.5t67 -30t69.5 -7t55.5 3.5l23 4q0 -38 0.5 -103t0.5 -68q0 -22 -11 -33.5t-22 -13t-33 -1.5 h-224q-119 0 -203.5 84.5t-84.5 203.5v960q0 119 84.5 203.5t203.5 84.5h960q119 0 203.5 -84.5t84.5 -203.5z" /> -<glyph unicode="" horiz-adv-x="1664" d="M1280 64q0 26 -19 45t-45 19t-45 -19t-19 -45t19 -45t45 -19t45 19t19 45zM1536 64q0 26 -19 45t-45 19t-45 -19t-19 -45t19 -45t45 -19t45 19t19 45zM1664 288v-320q0 -40 -28 -68t-68 -28h-1472q-40 0 -68 28t-28 68v320q0 40 28 68t68 28h427q21 -56 70.5 -92 t110.5 -36h256q61 0 110.5 36t70.5 92h427q40 0 68 -28t28 -68zM1339 936q-17 -40 -59 -40h-256v-448q0 -26 -19 -45t-45 -19h-256q-26 0 -45 19t-19 45v448h-256q-42 0 -59 40q-17 39 14 69l448 448q18 19 45 19t45 -19l448 -448q31 -30 14 -69z" /> -<glyph unicode="" d="M1407 710q0 44 -7 113.5t-18 96.5q-12 30 -17 44t-9 36.5t-4 48.5q0 23 5 68.5t5 67.5q0 37 -10 55q-4 1 -13 1q-19 0 -58 -4.5t-59 -4.5q-60 0 -176 24t-175 24q-43 0 -94.5 -11.5t-85 -23.5t-89.5 -34q-137 -54 -202 -103q-96 -73 -159.5 -189.5t-88 -236t-24.5 -248.5 q0 -40 12.5 -120t12.5 -121q0 -23 -11 -66.5t-11 -65.5t12 -36.5t34 -14.5q24 0 72.5 11t73.5 11q57 0 169.5 -15.5t169.5 -15.5q181 0 284 36q129 45 235.5 152.5t166 245.5t59.5 275zM1535 712q0 -165 -70 -327.5t-196 -288t-281 -180.5q-124 -44 -326 -44 q-57 0 -170 14.5t-169 14.5q-24 0 -72.5 -14.5t-73.5 -14.5q-73 0 -123.5 55.5t-50.5 128.5q0 24 11 68t11 67q0 40 -12.5 120.5t-12.5 121.5q0 111 18 217.5t54.5 209.5t100.5 194t150 156q78 59 232 120q194 78 316 78q60 0 175.5 -24t173.5 -24q19 0 57 5t58 5 q81 0 118 -50.5t37 -134.5q0 -23 -5 -68t-5 -68q0 -10 1 -18.5t3 -17t4 -13.5t6.5 -16t6.5 -17q16 -40 25 -118.5t9 -136.5z" /> -<glyph unicode="" horiz-adv-x="1408" d="M1408 296q0 -27 -10 -70.5t-21 -68.5q-21 -50 -122 -106q-94 -51 -186 -51q-27 0 -52.5 3.5t-57.5 12.5t-47.5 14.5t-55.5 20.5t-49 18q-98 35 -175 83q-128 79 -264.5 215.5t-215.5 264.5q-48 77 -83 175q-3 9 -18 49t-20.5 55.5t-14.5 47.5t-12.5 57.5t-3.5 52.5 q0 92 51 186q56 101 106 122q25 11 68.5 21t70.5 10q14 0 21 -3q18 -6 53 -76q11 -19 30 -54t35 -63.5t31 -53.5q3 -4 17.5 -25t21.5 -35.5t7 -28.5q0 -20 -28.5 -50t-62 -55t-62 -53t-28.5 -46q0 -9 5 -22.5t8.5 -20.5t14 -24t11.5 -19q76 -137 174 -235t235 -174 q2 -1 19 -11.5t24 -14t20.5 -8.5t22.5 -5q18 0 46 28.5t53 62t55 62t50 28.5q14 0 28.5 -7t35.5 -21.5t25 -17.5q25 -15 53.5 -31t63.5 -35t54 -30q70 -35 76 -53q3 -7 3 -21z" /> -<glyph unicode="" horiz-adv-x="1408" d="M1120 1280h-832q-66 0 -113 -47t-47 -113v-832q0 -66 47 -113t113 -47h832q66 0 113 47t47 113v832q0 66 -47 113t-113 47zM1408 1120v-832q0 -119 -84.5 -203.5t-203.5 -84.5h-832q-119 0 -203.5 84.5t-84.5 203.5v832q0 119 84.5 203.5t203.5 84.5h832 q119 0 203.5 -84.5t84.5 -203.5z" /> -<glyph unicode="" horiz-adv-x="1280" d="M1152 1280h-1024v-1242l423 406l89 85l89 -85l423 -406v1242zM1164 1408q23 0 44 -9q33 -13 52.5 -41t19.5 -62v-1289q0 -34 -19.5 -62t-52.5 -41q-19 -8 -44 -8q-48 0 -83 32l-441 424l-441 -424q-36 -33 -83 -33q-23 0 -44 9q-33 13 -52.5 41t-19.5 62v1289 q0 34 19.5 62t52.5 41q21 9 44 9h1048z" /> -<glyph unicode="" d="M1280 343q0 11 -2 16q-3 8 -38.5 29.5t-88.5 49.5l-53 29q-5 3 -19 13t-25 15t-21 5q-18 0 -47 -32.5t-57 -65.5t-44 -33q-7 0 -16.5 3.5t-15.5 6.5t-17 9.5t-14 8.5q-99 55 -170.5 126.5t-126.5 170.5q-2 3 -8.5 14t-9.5 17t-6.5 15.5t-3.5 16.5q0 13 20.5 33.5t45 38.5 t45 39.5t20.5 36.5q0 10 -5 21t-15 25t-13 19q-3 6 -15 28.5t-25 45.5t-26.5 47.5t-25 40.5t-16.5 18t-16 2q-48 0 -101 -22q-46 -21 -80 -94.5t-34 -130.5q0 -16 2.5 -34t5 -30.5t9 -33t10 -29.5t12.5 -33t11 -30q60 -164 216.5 -320.5t320.5 -216.5q6 -2 30 -11t33 -12.5 t29.5 -10t33 -9t30.5 -5t34 -2.5q57 0 130.5 34t94.5 80q22 53 22 101zM1536 1120v-960q0 -119 -84.5 -203.5t-203.5 -84.5h-960q-119 0 -203.5 84.5t-84.5 203.5v960q0 119 84.5 203.5t203.5 84.5h960q119 0 203.5 -84.5t84.5 -203.5z" /> -<glyph unicode="" horiz-adv-x="1664" d="M1620 1128q-67 -98 -162 -167q1 -14 1 -42q0 -130 -38 -259.5t-115.5 -248.5t-184.5 -210.5t-258 -146t-323 -54.5q-271 0 -496 145q35 -4 78 -4q225 0 401 138q-105 2 -188 64.5t-114 159.5q33 -5 61 -5q43 0 85 11q-112 23 -185.5 111.5t-73.5 205.5v4q68 -38 146 -41 q-66 44 -105 115t-39 154q0 88 44 163q121 -149 294.5 -238.5t371.5 -99.5q-8 38 -8 74q0 134 94.5 228.5t228.5 94.5q140 0 236 -102q109 21 205 78q-37 -115 -142 -178q93 10 186 50z" /> -<glyph unicode="" horiz-adv-x="1024" d="M959 1524v-264h-157q-86 0 -116 -36t-30 -108v-189h293l-39 -296h-254v-759h-306v759h-255v296h255v218q0 186 104 288.5t277 102.5q147 0 228 -12z" /> -<glyph unicode="" d="M1536 640q0 -251 -146.5 -451.5t-378.5 -277.5q-27 -5 -39.5 7t-12.5 30v211q0 97 -52 142q57 6 102.5 18t94 39t81 66.5t53 105t20.5 150.5q0 121 -79 206q37 91 -8 204q-28 9 -81 -11t-92 -44l-38 -24q-93 26 -192 26t-192 -26q-16 11 -42.5 27t-83.5 38.5t-86 13.5 q-44 -113 -7 -204q-79 -85 -79 -206q0 -85 20.5 -150t52.5 -105t80.5 -67t94 -39t102.5 -18q-40 -36 -49 -103q-21 -10 -45 -15t-57 -5t-65.5 21.5t-55.5 62.5q-19 32 -48.5 52t-49.5 24l-20 3q-21 0 -29 -4.5t-5 -11.5t9 -14t13 -12l7 -5q22 -10 43.5 -38t31.5 -51l10 -23 q13 -38 44 -61.5t67 -30t69.5 -7t55.5 3.5l23 4q0 -38 0.5 -89t0.5 -54q0 -18 -13 -30t-40 -7q-232 77 -378.5 277.5t-146.5 451.5q0 209 103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5t103 -385.5z" /> -<glyph unicode="" horiz-adv-x="1664" d="M1664 960v-256q0 -26 -19 -45t-45 -19h-64q-26 0 -45 19t-19 45v256q0 106 -75 181t-181 75t-181 -75t-75 -181v-192h96q40 0 68 -28t28 -68v-576q0 -40 -28 -68t-68 -28h-960q-40 0 -68 28t-28 68v576q0 40 28 68t68 28h672v192q0 185 131.5 316.5t316.5 131.5 t316.5 -131.5t131.5 -316.5z" /> -<glyph unicode="" horiz-adv-x="1920" d="M1760 1408q66 0 113 -47t47 -113v-1216q0 -66 -47 -113t-113 -47h-1600q-66 0 -113 47t-47 113v1216q0 66 47 113t113 47h1600zM160 1280q-13 0 -22.5 -9.5t-9.5 -22.5v-224h1664v224q0 13 -9.5 22.5t-22.5 9.5h-1600zM1760 0q13 0 22.5 9.5t9.5 22.5v608h-1664v-608 q0 -13 9.5 -22.5t22.5 -9.5h1600zM256 128v128h256v-128h-256zM640 128v128h384v-128h-384z" /> -<glyph unicode="" horiz-adv-x="1408" d="M384 192q0 -80 -56 -136t-136 -56t-136 56t-56 136t56 136t136 56t136 -56t56 -136zM896 69q2 -28 -17 -48q-18 -21 -47 -21h-135q-25 0 -43 16.5t-20 41.5q-22 229 -184.5 391.5t-391.5 184.5q-25 2 -41.5 20t-16.5 43v135q0 29 21 47q17 17 43 17h5q160 -13 306 -80.5 t259 -181.5q114 -113 181.5 -259t80.5 -306zM1408 67q2 -27 -18 -47q-18 -20 -46 -20h-143q-26 0 -44.5 17.5t-19.5 42.5q-12 215 -101 408.5t-231.5 336t-336 231.5t-408.5 102q-25 1 -42.5 19.5t-17.5 43.5v143q0 28 20 46q18 18 44 18h3q262 -13 501.5 -120t425.5 -294 q187 -186 294 -425.5t120 -501.5z" /> -<glyph unicode="" d="M1040 320q0 -33 -23.5 -56.5t-56.5 -23.5t-56.5 23.5t-23.5 56.5t23.5 56.5t56.5 23.5t56.5 -23.5t23.5 -56.5zM1296 320q0 -33 -23.5 -56.5t-56.5 -23.5t-56.5 23.5t-23.5 56.5t23.5 56.5t56.5 23.5t56.5 -23.5t23.5 -56.5zM1408 160v320q0 13 -9.5 22.5t-22.5 9.5 h-1216q-13 0 -22.5 -9.5t-9.5 -22.5v-320q0 -13 9.5 -22.5t22.5 -9.5h1216q13 0 22.5 9.5t9.5 22.5zM178 640h1180l-157 482q-4 13 -16 21.5t-26 8.5h-782q-14 0 -26 -8.5t-16 -21.5zM1536 480v-320q0 -66 -47 -113t-113 -47h-1216q-66 0 -113 47t-47 113v320q0 25 16 75 l197 606q17 53 63 86t101 33h782q55 0 101 -33t63 -86l197 -606q16 -50 16 -75z" /> -<glyph unicode="" horiz-adv-x="1792" d="M1664 896q53 0 90.5 -37.5t37.5 -90.5t-37.5 -90.5t-90.5 -37.5v-384q0 -52 -38 -90t-90 -38q-417 347 -812 380q-58 -19 -91 -66t-31 -100.5t40 -92.5q-20 -33 -23 -65.5t6 -58t33.5 -55t48 -50t61.5 -50.5q-29 -58 -111.5 -83t-168.5 -11.5t-132 55.5q-7 23 -29.5 87.5 t-32 94.5t-23 89t-15 101t3.5 98.5t22 110.5h-122q-66 0 -113 47t-47 113v192q0 66 47 113t113 47h480q435 0 896 384q52 0 90 -38t38 -90v-384zM1536 292v954q-394 -302 -768 -343v-270q377 -42 768 -341z" /> -<glyph unicode="" horiz-adv-x="1792" d="M912 -160q0 16 -16 16q-59 0 -101.5 42.5t-42.5 101.5q0 16 -16 16t-16 -16q0 -73 51.5 -124.5t124.5 -51.5q16 0 16 16zM246 128h1300q-266 300 -266 832q0 51 -24 105t-69 103t-121.5 80.5t-169.5 31.5t-169.5 -31.5t-121.5 -80.5t-69 -103t-24 -105q0 -532 -266 -832z M1728 128q0 -52 -38 -90t-90 -38h-448q0 -106 -75 -181t-181 -75t-181 75t-75 181h-448q-52 0 -90 38t-38 90q50 42 91 88t85 119.5t74.5 158.5t50 206t19.5 260q0 152 117 282.5t307 158.5q-8 19 -8 39q0 40 28 68t68 28t68 -28t28 -68q0 -20 -8 -39q190 -28 307 -158.5 t117 -282.5q0 -139 19.5 -260t50 -206t74.5 -158.5t85 -119.5t91 -88z" /> -<glyph unicode="" d="M1376 640l138 -135q30 -28 20 -70q-12 -41 -52 -51l-188 -48l53 -186q12 -41 -19 -70q-29 -31 -70 -19l-186 53l-48 -188q-10 -40 -51 -52q-12 -2 -19 -2q-31 0 -51 22l-135 138l-135 -138q-28 -30 -70 -20q-41 11 -51 52l-48 188l-186 -53q-41 -12 -70 19q-31 29 -19 70 l53 186l-188 48q-40 10 -52 51q-10 42 20 70l138 135l-138 135q-30 28 -20 70q12 41 52 51l188 48l-53 186q-12 41 19 70q29 31 70 19l186 -53l48 188q10 41 51 51q41 12 70 -19l135 -139l135 139q29 30 70 19q41 -10 51 -51l48 -188l186 53q41 12 70 -19q31 -29 19 -70 l-53 -186l188 -48q40 -10 52 -51q10 -42 -20 -70z" /> -<glyph unicode="" horiz-adv-x="1792" d="M256 192q0 26 -19 45t-45 19t-45 -19t-19 -45t19 -45t45 -19t45 19t19 45zM1664 768q0 51 -39 89.5t-89 38.5h-576q0 20 15 48.5t33 55t33 68t15 84.5q0 67 -44.5 97.5t-115.5 30.5q-24 0 -90 -139q-24 -44 -37 -65q-40 -64 -112 -145q-71 -81 -101 -106 q-69 -57 -140 -57h-32v-640h32q72 0 167 -32t193.5 -64t179.5 -32q189 0 189 167q0 26 -5 56q30 16 47.5 52.5t17.5 73.5t-18 69q53 50 53 119q0 25 -10 55.5t-25 47.5h331q52 0 90 38t38 90zM1792 769q0 -105 -75.5 -181t-180.5 -76h-169q-4 -62 -37 -119q3 -21 3 -43 q0 -101 -60 -178q1 -139 -85 -219.5t-227 -80.5q-133 0 -322 69q-164 59 -223 59h-288q-53 0 -90.5 37.5t-37.5 90.5v640q0 53 37.5 90.5t90.5 37.5h288q10 0 21.5 4.5t23.5 14t22.5 18t24 22.5t20.5 21.5t19 21.5t14 17q65 74 100 129q13 21 33 62t37 72t40.5 63t55 49.5 t69.5 17.5q125 0 206.5 -67t81.5 -189q0 -68 -22 -128h374q104 0 180 -76t76 -179z" /> -<glyph unicode="" horiz-adv-x="1792" d="M1376 128h32v640h-32q-35 0 -67.5 12t-62.5 37t-50 46t-49 54q-2 3 -3.5 4.5t-4 4.5t-4.5 5q-72 81 -112 145q-14 22 -38 68q-1 3 -10.5 22.5t-18.5 36t-20 35.5t-21.5 30.5t-18.5 11.5q-71 0 -115.5 -30.5t-44.5 -97.5q0 -43 15 -84.5t33 -68t33 -55t15 -48.5h-576 q-50 0 -89 -38.5t-39 -89.5q0 -52 38 -90t90 -38h331q-15 -17 -25 -47.5t-10 -55.5q0 -69 53 -119q-18 -32 -18 -69t17.5 -73.5t47.5 -52.5q-4 -24 -4 -56q0 -85 48.5 -126t135.5 -41q84 0 183 32t194 64t167 32zM1664 192q0 26 -19 45t-45 19t-45 -19t-19 -45t19 -45 t45 -19t45 19t19 45zM1792 768v-640q0 -53 -37.5 -90.5t-90.5 -37.5h-288q-59 0 -223 -59q-190 -69 -317 -69q-142 0 -230 77.5t-87 217.5l1 5q-61 76 -61 178q0 22 3 43q-33 57 -37 119h-169q-105 0 -180.5 76t-75.5 181q0 103 76 179t180 76h374q-22 60 -22 128 q0 122 81.5 189t206.5 67q38 0 69.5 -17.5t55 -49.5t40.5 -63t37 -72t33 -62q35 -55 100 -129q2 -3 14 -17t19 -21.5t20.5 -21.5t24 -22.5t22.5 -18t23.5 -14t21.5 -4.5h288q53 0 90.5 -37.5t37.5 -90.5z" /> -<glyph unicode="" d="M1280 -64q0 26 -19 45t-45 19t-45 -19t-19 -45t19 -45t45 -19t45 19t19 45zM1408 700q0 189 -167 189q-26 0 -56 -5q-16 30 -52.5 47.5t-73.5 17.5t-69 -18q-50 53 -119 53q-25 0 -55.5 -10t-47.5 -25v331q0 52 -38 90t-90 38q-51 0 -89.5 -39t-38.5 -89v-576 q-20 0 -48.5 15t-55 33t-68 33t-84.5 15q-67 0 -97.5 -44.5t-30.5 -115.5q0 -24 139 -90q44 -24 65 -37q64 -40 145 -112q81 -71 106 -101q57 -69 57 -140v-32h640v32q0 72 32 167t64 193.5t32 179.5zM1536 705q0 -133 -69 -322q-59 -164 -59 -223v-288q0 -53 -37.5 -90.5 t-90.5 -37.5h-640q-53 0 -90.5 37.5t-37.5 90.5v288q0 10 -4.5 21.5t-14 23.5t-18 22.5t-22.5 24t-21.5 20.5t-21.5 19t-17 14q-74 65 -129 100q-21 13 -62 33t-72 37t-63 40.5t-49.5 55t-17.5 69.5q0 125 67 206.5t189 81.5q68 0 128 -22v374q0 104 76 180t179 76 q105 0 181 -75.5t76 -180.5v-169q62 -4 119 -37q21 3 43 3q101 0 178 -60q139 1 219.5 -85t80.5 -227z" /> -<glyph unicode="" d="M1408 576q0 84 -32 183t-64 194t-32 167v32h-640v-32q0 -35 -12 -67.5t-37 -62.5t-46 -50t-54 -49q-9 -8 -14 -12q-81 -72 -145 -112q-22 -14 -68 -38q-3 -1 -22.5 -10.5t-36 -18.5t-35.5 -20t-30.5 -21.5t-11.5 -18.5q0 -71 30.5 -115.5t97.5 -44.5q43 0 84.5 15t68 33 t55 33t48.5 15v-576q0 -50 38.5 -89t89.5 -39q52 0 90 38t38 90v331q46 -35 103 -35q69 0 119 53q32 -18 69 -18t73.5 17.5t52.5 47.5q24 -4 56 -4q85 0 126 48.5t41 135.5zM1280 1344q0 26 -19 45t-45 19t-45 -19t-19 -45t19 -45t45 -19t45 19t19 45zM1536 580 q0 -142 -77.5 -230t-217.5 -87l-5 1q-76 -61 -178 -61q-22 0 -43 3q-54 -30 -119 -37v-169q0 -105 -76 -180.5t-181 -75.5q-103 0 -179 76t-76 180v374q-54 -22 -128 -22q-121 0 -188.5 81.5t-67.5 206.5q0 38 17.5 69.5t49.5 55t63 40.5t72 37t62 33q55 35 129 100 q3 2 17 14t21.5 19t21.5 20.5t22.5 24t18 22.5t14 23.5t4.5 21.5v288q0 53 37.5 90.5t90.5 37.5h640q53 0 90.5 -37.5t37.5 -90.5v-288q0 -59 59 -223q69 -190 69 -317z" /> -<glyph unicode="" d="M1280 576v128q0 26 -19 45t-45 19h-502l189 189q19 19 19 45t-19 45l-91 91q-18 18 -45 18t-45 -18l-362 -362l-91 -91q-18 -18 -18 -45t18 -45l91 -91l362 -362q18 -18 45 -18t45 18l91 91q18 18 18 45t-18 45l-189 189h502q26 0 45 19t19 45zM1536 640 q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5t103 -385.5z" /> -<glyph unicode="" d="M1285 640q0 27 -18 45l-91 91l-362 362q-18 18 -45 18t-45 -18l-91 -91q-18 -18 -18 -45t18 -45l189 -189h-502q-26 0 -45 -19t-19 -45v-128q0 -26 19 -45t45 -19h502l-189 -189q-19 -19 -19 -45t19 -45l91 -91q18 -18 45 -18t45 18l362 362l91 91q18 18 18 45zM1536 640 q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5t103 -385.5z" /> -<glyph unicode="" d="M1284 641q0 27 -18 45l-362 362l-91 91q-18 18 -45 18t-45 -18l-91 -91l-362 -362q-18 -18 -18 -45t18 -45l91 -91q18 -18 45 -18t45 18l189 189v-502q0 -26 19 -45t45 -19h128q26 0 45 19t19 45v502l189 -189q19 -19 45 -19t45 19l91 91q18 18 18 45zM1536 640 q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5t103 -385.5z" /> -<glyph unicode="" d="M1284 639q0 27 -18 45l-91 91q-18 18 -45 18t-45 -18l-189 -189v502q0 26 -19 45t-45 19h-128q-26 0 -45 -19t-19 -45v-502l-189 189q-19 19 -45 19t-45 -19l-91 -91q-18 -18 -18 -45t18 -45l362 -362l91 -91q18 -18 45 -18t45 18l91 91l362 362q18 18 18 45zM1536 640 q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5t103 -385.5z" /> -<glyph unicode="" d="M768 1408q209 0 385.5 -103t279.5 -279.5t103 -385.5t-103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103zM1042 887q-2 -1 -9.5 -9.5t-13.5 -9.5q2 0 4.5 5t5 11t3.5 7q6 7 22 15q14 6 52 12q34 8 51 -11 q-2 2 9.5 13t14.5 12q3 2 15 4.5t15 7.5l2 22q-12 -1 -17.5 7t-6.5 21q0 -2 -6 -8q0 7 -4.5 8t-11.5 -1t-9 -1q-10 3 -15 7.5t-8 16.5t-4 15q-2 5 -9.5 10.5t-9.5 10.5q-1 2 -2.5 5.5t-3 6.5t-4 5.5t-5.5 2.5t-7 -5t-7.5 -10t-4.5 -5q-3 2 -6 1.5t-4.5 -1t-4.5 -3t-5 -3.5 q-3 -2 -8.5 -3t-8.5 -2q15 5 -1 11q-10 4 -16 3q9 4 7.5 12t-8.5 14h5q-1 4 -8.5 8.5t-17.5 8.5t-13 6q-8 5 -34 9.5t-33 0.5q-5 -6 -4.5 -10.5t4 -14t3.5 -12.5q1 -6 -5.5 -13t-6.5 -12q0 -7 14 -15.5t10 -21.5q-3 -8 -16 -16t-16 -12q-5 -8 -1.5 -18.5t10.5 -16.5 q2 -2 1.5 -4t-3.5 -4.5t-5.5 -4t-6.5 -3.5l-3 -2q-11 -5 -20.5 6t-13.5 26q-7 25 -16 30q-23 8 -29 -1q-5 13 -41 26q-25 9 -58 4q6 1 0 15q-7 15 -19 12q3 6 4 17.5t1 13.5q3 13 12 23q1 1 7 8.5t9.5 13.5t0.5 6q35 -4 50 11q5 5 11.5 17t10.5 17q9 6 14 5.5t14.5 -5.5 t14.5 -5q14 -1 15.5 11t-7.5 20q12 -1 3 17q-5 7 -8 9q-12 4 -27 -5q-8 -4 2 -8q-1 1 -9.5 -10.5t-16.5 -17.5t-16 5q-1 1 -5.5 13.5t-9.5 13.5q-8 0 -16 -15q3 8 -11 15t-24 8q19 12 -8 27q-7 4 -20.5 5t-19.5 -4q-5 -7 -5.5 -11.5t5 -8t10.5 -5.5t11.5 -4t8.5 -3 q14 -10 8 -14q-2 -1 -8.5 -3.5t-11.5 -4.5t-6 -4q-3 -4 0 -14t-2 -14q-5 5 -9 17.5t-7 16.5q7 -9 -25 -6l-10 1q-4 0 -16 -2t-20.5 -1t-13.5 8q-4 8 0 20q1 4 4 2q-4 3 -11 9.5t-10 8.5q-46 -15 -94 -41q6 -1 12 1q5 2 13 6.5t10 5.5q34 14 42 7l5 5q14 -16 20 -25 q-7 4 -30 1q-20 -6 -22 -12q7 -12 5 -18q-4 3 -11.5 10t-14.5 11t-15 5q-16 0 -22 -1q-146 -80 -235 -222q7 -7 12 -8q4 -1 5 -9t2.5 -11t11.5 3q9 -8 3 -19q1 1 44 -27q19 -17 21 -21q3 -11 -10 -18q-1 2 -9 9t-9 4q-3 -5 0.5 -18.5t10.5 -12.5q-7 0 -9.5 -16t-2.5 -35.5 t-1 -23.5l2 -1q-3 -12 5.5 -34.5t21.5 -19.5q-13 -3 20 -43q6 -8 8 -9q3 -2 12 -7.5t15 -10t10 -10.5q4 -5 10 -22.5t14 -23.5q-2 -6 9.5 -20t10.5 -23q-1 0 -2.5 -1t-2.5 -1q3 -7 15.5 -14t15.5 -13q1 -3 2 -10t3 -11t8 -2q2 20 -24 62q-15 25 -17 29q-3 5 -5.5 15.5 t-4.5 14.5q2 0 6 -1.5t8.5 -3.5t7.5 -4t2 -3q-3 -7 2 -17.5t12 -18.5t17 -19t12 -13q6 -6 14 -19.5t0 -13.5q9 0 20 -10t17 -20q5 -8 8 -26t5 -24q2 -7 8.5 -13.5t12.5 -9.5l16 -8t13 -7q5 -2 18.5 -10.5t21.5 -11.5q10 -4 16 -4t14.5 2.5t13.5 3.5q15 2 29 -15t21 -21 q36 -19 55 -11q-2 -1 0.5 -7.5t8 -15.5t9 -14.5t5.5 -8.5q5 -6 18 -15t18 -15q6 4 7 9q-3 -8 7 -20t18 -10q14 3 14 32q-31 -15 -49 18q0 1 -2.5 5.5t-4 8.5t-2.5 8.5t0 7.5t5 3q9 0 10 3.5t-2 12.5t-4 13q-1 8 -11 20t-12 15q-5 -9 -16 -8t-16 9q0 -1 -1.5 -5.5t-1.5 -6.5 q-13 0 -15 1q1 3 2.5 17.5t3.5 22.5q1 4 5.5 12t7.5 14.5t4 12.5t-4.5 9.5t-17.5 2.5q-19 -1 -26 -20q-1 -3 -3 -10.5t-5 -11.5t-9 -7q-7 -3 -24 -2t-24 5q-13 8 -22.5 29t-9.5 37q0 10 2.5 26.5t3 25t-5.5 24.5q3 2 9 9.5t10 10.5q2 1 4.5 1.5t4.5 0t4 1.5t3 6q-1 1 -4 3 q-3 3 -4 3q7 -3 28.5 1.5t27.5 -1.5q15 -11 22 2q0 1 -2.5 9.5t-0.5 13.5q5 -27 29 -9q3 -3 15.5 -5t17.5 -5q3 -2 7 -5.5t5.5 -4.5t5 0.5t8.5 6.5q10 -14 12 -24q11 -40 19 -44q7 -3 11 -2t4.5 9.5t0 14t-1.5 12.5l-1 8v18l-1 8q-15 3 -18.5 12t1.5 18.5t15 18.5q1 1 8 3.5 t15.5 6.5t12.5 8q21 19 15 35q7 0 11 9q-1 0 -5 3t-7.5 5t-4.5 2q9 5 2 16q5 3 7.5 11t7.5 10q9 -12 21 -2q7 8 1 16q5 7 20.5 10.5t18.5 9.5q7 -2 8 2t1 12t3 12q4 5 15 9t13 5l17 11q3 4 0 4q18 -2 31 11q10 11 -6 20q3 6 -3 9.5t-15 5.5q3 1 11.5 0.5t10.5 1.5 q15 10 -7 16q-17 5 -43 -12zM879 10q206 36 351 189q-3 3 -12.5 4.5t-12.5 3.5q-18 7 -24 8q1 7 -2.5 13t-8 9t-12.5 8t-11 7q-2 2 -7 6t-7 5.5t-7.5 4.5t-8.5 2t-10 -1l-3 -1q-3 -1 -5.5 -2.5t-5.5 -3t-4 -3t0 -2.5q-21 17 -36 22q-5 1 -11 5.5t-10.5 7t-10 1.5t-11.5 -7 q-5 -5 -6 -15t-2 -13q-7 5 0 17.5t2 18.5q-3 6 -10.5 4.5t-12 -4.5t-11.5 -8.5t-9 -6.5t-8.5 -5.5t-8.5 -7.5q-3 -4 -6 -12t-5 -11q-2 4 -11.5 6.5t-9.5 5.5q2 -10 4 -35t5 -38q7 -31 -12 -48q-27 -25 -29 -40q-4 -22 12 -26q0 -7 -8 -20.5t-7 -21.5q0 -6 2 -16z" /> -<glyph unicode="" horiz-adv-x="1664" d="M384 64q0 26 -19 45t-45 19t-45 -19t-19 -45t19 -45t45 -19t45 19t19 45zM1028 484l-682 -682q-37 -37 -90 -37q-52 0 -91 37l-106 108q-38 36 -38 90q0 53 38 91l681 681q39 -98 114.5 -173.5t173.5 -114.5zM1662 919q0 -39 -23 -106q-47 -134 -164.5 -217.5 t-258.5 -83.5q-185 0 -316.5 131.5t-131.5 316.5t131.5 316.5t316.5 131.5q58 0 121.5 -16.5t107.5 -46.5q16 -11 16 -28t-16 -28l-293 -169v-224l193 -107q5 3 79 48.5t135.5 81t70.5 35.5q15 0 23.5 -10t8.5 -25z" /> -<glyph unicode="" horiz-adv-x="1792" d="M1024 128h640v128h-640v-128zM640 640h1024v128h-1024v-128zM1280 1152h384v128h-384v-128zM1792 320v-256q0 -26 -19 -45t-45 -19h-1664q-26 0 -45 19t-19 45v256q0 26 19 45t45 19h1664q26 0 45 -19t19 -45zM1792 832v-256q0 -26 -19 -45t-45 -19h-1664q-26 0 -45 19 t-19 45v256q0 26 19 45t45 19h1664q26 0 45 -19t19 -45zM1792 1344v-256q0 -26 -19 -45t-45 -19h-1664q-26 0 -45 19t-19 45v256q0 26 19 45t45 19h1664q26 0 45 -19t19 -45z" /> -<glyph unicode="" horiz-adv-x="1408" d="M1403 1241q17 -41 -14 -70l-493 -493v-742q0 -42 -39 -59q-13 -5 -25 -5q-27 0 -45 19l-256 256q-19 19 -19 45v486l-493 493q-31 29 -14 70q17 39 59 39h1280q42 0 59 -39z" /> -<glyph unicode="" horiz-adv-x="1792" d="M640 1280h512v128h-512v-128zM1792 640v-480q0 -66 -47 -113t-113 -47h-1472q-66 0 -113 47t-47 113v480h672v-160q0 -26 19 -45t45 -19h320q26 0 45 19t19 45v160h672zM1024 640v-128h-256v128h256zM1792 1120v-384h-1792v384q0 66 47 113t113 47h352v160q0 40 28 68 t68 28h576q40 0 68 -28t28 -68v-160h352q66 0 113 -47t47 -113z" /> -<glyph unicode="" d="M1283 995l-355 -355l355 -355l144 144q29 31 70 14q39 -17 39 -59v-448q0 -26 -19 -45t-45 -19h-448q-42 0 -59 40q-17 39 14 69l144 144l-355 355l-355 -355l144 -144q31 -30 14 -69q-17 -40 -59 -40h-448q-26 0 -45 19t-19 45v448q0 42 40 59q39 17 69 -14l144 -144 l355 355l-355 355l-144 -144q-19 -19 -45 -19q-12 0 -24 5q-40 17 -40 59v448q0 26 19 45t45 19h448q42 0 59 -40q17 -39 -14 -69l-144 -144l355 -355l355 355l-144 144q-31 30 -14 69q17 40 59 40h448q26 0 45 -19t19 -45v-448q0 -42 -39 -59q-13 -5 -25 -5q-26 0 -45 19z " /> -<glyph unicode="" horiz-adv-x="1920" d="M593 640q-162 -5 -265 -128h-134q-82 0 -138 40.5t-56 118.5q0 353 124 353q6 0 43.5 -21t97.5 -42.5t119 -21.5q67 0 133 23q-5 -37 -5 -66q0 -139 81 -256zM1664 3q0 -120 -73 -189.5t-194 -69.5h-874q-121 0 -194 69.5t-73 189.5q0 53 3.5 103.5t14 109t26.5 108.5 t43 97.5t62 81t85.5 53.5t111.5 20q10 0 43 -21.5t73 -48t107 -48t135 -21.5t135 21.5t107 48t73 48t43 21.5q61 0 111.5 -20t85.5 -53.5t62 -81t43 -97.5t26.5 -108.5t14 -109t3.5 -103.5zM640 1280q0 -106 -75 -181t-181 -75t-181 75t-75 181t75 181t181 75t181 -75 t75 -181zM1344 896q0 -159 -112.5 -271.5t-271.5 -112.5t-271.5 112.5t-112.5 271.5t112.5 271.5t271.5 112.5t271.5 -112.5t112.5 -271.5zM1920 671q0 -78 -56 -118.5t-138 -40.5h-134q-103 123 -265 128q81 117 81 256q0 29 -5 66q66 -23 133 -23q59 0 119 21.5t97.5 42.5 t43.5 21q124 0 124 -353zM1792 1280q0 -106 -75 -181t-181 -75t-181 75t-75 181t75 181t181 75t181 -75t75 -181z" /> -<glyph unicode="" horiz-adv-x="1664" d="M1456 320q0 40 -28 68l-208 208q-28 28 -68 28q-42 0 -72 -32q3 -3 19 -18.5t21.5 -21.5t15 -19t13 -25.5t3.5 -27.5q0 -40 -28 -68t-68 -28q-15 0 -27.5 3.5t-25.5 13t-19 15t-21.5 21.5t-18.5 19q-33 -31 -33 -73q0 -40 28 -68l206 -207q27 -27 68 -27q40 0 68 26 l147 146q28 28 28 67zM753 1025q0 40 -28 68l-206 207q-28 28 -68 28q-39 0 -68 -27l-147 -146q-28 -28 -28 -67q0 -40 28 -68l208 -208q27 -27 68 -27q42 0 72 31q-3 3 -19 18.5t-21.5 21.5t-15 19t-13 25.5t-3.5 27.5q0 40 28 68t68 28q15 0 27.5 -3.5t25.5 -13t19 -15 t21.5 -21.5t18.5 -19q33 31 33 73zM1648 320q0 -120 -85 -203l-147 -146q-83 -83 -203 -83q-121 0 -204 85l-206 207q-83 83 -83 203q0 123 88 209l-88 88q-86 -88 -208 -88q-120 0 -204 84l-208 208q-84 84 -84 204t85 203l147 146q83 83 203 83q121 0 204 -85l206 -207 q83 -83 83 -203q0 -123 -88 -209l88 -88q86 88 208 88q120 0 204 -84l208 -208q84 -84 84 -204z" /> -<glyph unicode="" horiz-adv-x="1920" d="M1920 384q0 -159 -112.5 -271.5t-271.5 -112.5h-1088q-185 0 -316.5 131.5t-131.5 316.5q0 132 71 241.5t187 163.5q-2 28 -2 43q0 212 150 362t362 150q158 0 286.5 -88t187.5 -230q70 62 166 62q106 0 181 -75t75 -181q0 -75 -41 -138q129 -30 213 -134.5t84 -239.5z " /> -<glyph unicode="" horiz-adv-x="1664" d="M1527 88q56 -89 21.5 -152.5t-140.5 -63.5h-1152q-106 0 -140.5 63.5t21.5 152.5l503 793v399h-64q-26 0 -45 19t-19 45t19 45t45 19h512q26 0 45 -19t19 -45t-19 -45t-45 -19h-64v-399zM748 813l-272 -429h712l-272 429l-20 31v37v399h-128v-399v-37z" /> -<glyph unicode="" horiz-adv-x="1792" d="M960 640q26 0 45 -19t19 -45t-19 -45t-45 -19t-45 19t-19 45t19 45t45 19zM1260 576l507 -398q28 -20 25 -56q-5 -35 -35 -51l-128 -64q-13 -7 -29 -7q-17 0 -31 8l-690 387l-110 -66q-8 -4 -12 -5q14 -49 10 -97q-7 -77 -56 -147.5t-132 -123.5q-132 -84 -277 -84 q-136 0 -222 78q-90 84 -79 207q7 76 56 147t131 124q132 84 278 84q83 0 151 -31q9 13 22 22l122 73l-122 73q-13 9 -22 22q-68 -31 -151 -31q-146 0 -278 84q-82 53 -131 124t-56 147q-5 59 15.5 113t63.5 93q85 79 222 79q145 0 277 -84q83 -52 132 -123t56 -148 q4 -48 -10 -97q4 -1 12 -5l110 -66l690 387q14 8 31 8q16 0 29 -7l128 -64q30 -16 35 -51q3 -36 -25 -56zM579 836q46 42 21 108t-106 117q-92 59 -192 59q-74 0 -113 -36q-46 -42 -21 -108t106 -117q92 -59 192 -59q74 0 113 36zM494 91q81 51 106 117t-21 108 q-39 36 -113 36q-100 0 -192 -59q-81 -51 -106 -117t21 -108q39 -36 113 -36q100 0 192 59zM672 704l96 -58v11q0 36 33 56l14 8l-79 47l-26 -26q-3 -3 -10 -11t-12 -12q-2 -2 -4 -3.5t-3 -2.5zM896 480l96 -32l736 576l-128 64l-768 -431v-113l-160 -96l9 -8q2 -2 7 -6 q4 -4 11 -12t11 -12l26 -26zM1600 64l128 64l-520 408l-177 -138q-2 -3 -13 -7z" /> -<glyph unicode="" horiz-adv-x="1792" d="M1696 1152q40 0 68 -28t28 -68v-1216q0 -40 -28 -68t-68 -28h-960q-40 0 -68 28t-28 68v288h-544q-40 0 -68 28t-28 68v672q0 40 20 88t48 76l408 408q28 28 76 48t88 20h416q40 0 68 -28t28 -68v-328q68 40 128 40h416zM1152 939l-299 -299h299v299zM512 1323l-299 -299 h299v299zM708 676l316 316v416h-384v-416q0 -40 -28 -68t-68 -28h-416v-640h512v256q0 40 20 88t48 76zM1664 -128v1152h-384v-416q0 -40 -28 -68t-68 -28h-416v-640h896z" /> -<glyph unicode="" horiz-adv-x="1408" d="M1404 151q0 -117 -79 -196t-196 -79q-135 0 -235 100l-777 776q-113 115 -113 271q0 159 110 270t269 111q158 0 273 -113l605 -606q10 -10 10 -22q0 -16 -30.5 -46.5t-46.5 -30.5q-13 0 -23 10l-606 607q-79 77 -181 77q-106 0 -179 -75t-73 -181q0 -105 76 -181 l776 -777q63 -63 145 -63q64 0 106 42t42 106q0 82 -63 145l-581 581q-26 24 -60 24q-29 0 -48 -19t-19 -48q0 -32 25 -59l410 -410q10 -10 10 -22q0 -16 -31 -47t-47 -31q-12 0 -22 10l-410 410q-63 61 -63 149q0 82 57 139t139 57q88 0 149 -63l581 -581q100 -98 100 -235 z" /> -<glyph unicode="" d="M384 0h768v384h-768v-384zM1280 0h128v896q0 14 -10 38.5t-20 34.5l-281 281q-10 10 -34 20t-39 10v-416q0 -40 -28 -68t-68 -28h-576q-40 0 -68 28t-28 68v416h-128v-1280h128v416q0 40 28 68t68 28h832q40 0 68 -28t28 -68v-416zM896 928v320q0 13 -9.5 22.5t-22.5 9.5 h-192q-13 0 -22.5 -9.5t-9.5 -22.5v-320q0 -13 9.5 -22.5t22.5 -9.5h192q13 0 22.5 9.5t9.5 22.5zM1536 896v-928q0 -40 -28 -68t-68 -28h-1344q-40 0 -68 28t-28 68v1344q0 40 28 68t68 28h928q40 0 88 -20t76 -48l280 -280q28 -28 48 -76t20 -88z" /> -<glyph unicode="" d="M1536 1120v-960q0 -119 -84.5 -203.5t-203.5 -84.5h-960q-119 0 -203.5 84.5t-84.5 203.5v960q0 119 84.5 203.5t203.5 84.5h960q119 0 203.5 -84.5t84.5 -203.5z" /> -<glyph unicode="" d="M1536 192v-128q0 -26 -19 -45t-45 -19h-1408q-26 0 -45 19t-19 45v128q0 26 19 45t45 19h1408q26 0 45 -19t19 -45zM1536 704v-128q0 -26 -19 -45t-45 -19h-1408q-26 0 -45 19t-19 45v128q0 26 19 45t45 19h1408q26 0 45 -19t19 -45zM1536 1216v-128q0 -26 -19 -45 t-45 -19h-1408q-26 0 -45 19t-19 45v128q0 26 19 45t45 19h1408q26 0 45 -19t19 -45z" /> -<glyph unicode="" horiz-adv-x="1792" d="M384 128q0 -80 -56 -136t-136 -56t-136 56t-56 136t56 136t136 56t136 -56t56 -136zM384 640q0 -80 -56 -136t-136 -56t-136 56t-56 136t56 136t136 56t136 -56t56 -136zM1792 224v-192q0 -13 -9.5 -22.5t-22.5 -9.5h-1216q-13 0 -22.5 9.5t-9.5 22.5v192q0 13 9.5 22.5 t22.5 9.5h1216q13 0 22.5 -9.5t9.5 -22.5zM384 1152q0 -80 -56 -136t-136 -56t-136 56t-56 136t56 136t136 56t136 -56t56 -136zM1792 736v-192q0 -13 -9.5 -22.5t-22.5 -9.5h-1216q-13 0 -22.5 9.5t-9.5 22.5v192q0 13 9.5 22.5t22.5 9.5h1216q13 0 22.5 -9.5t9.5 -22.5z M1792 1248v-192q0 -13 -9.5 -22.5t-22.5 -9.5h-1216q-13 0 -22.5 9.5t-9.5 22.5v192q0 13 9.5 22.5t22.5 9.5h1216q13 0 22.5 -9.5t9.5 -22.5z" /> -<glyph unicode="" horiz-adv-x="1792" d="M381 -84q0 -80 -54.5 -126t-135.5 -46q-106 0 -172 66l57 88q49 -45 106 -45q29 0 50.5 14.5t21.5 42.5q0 64 -105 56l-26 56q8 10 32.5 43.5t42.5 54t37 38.5v1q-16 0 -48.5 -1t-48.5 -1v-53h-106v152h333v-88l-95 -115q51 -12 81 -49t30 -88zM383 543v-159h-362 q-6 36 -6 54q0 51 23.5 93t56.5 68t66 47.5t56.5 43.5t23.5 45q0 25 -14.5 38.5t-39.5 13.5q-46 0 -81 -58l-85 59q24 51 71.5 79.5t105.5 28.5q73 0 123 -41.5t50 -112.5q0 -50 -34 -91.5t-75 -64.5t-75.5 -50.5t-35.5 -52.5h127v60h105zM1792 224v-192q0 -13 -9.5 -22.5 t-22.5 -9.5h-1216q-13 0 -22.5 9.5t-9.5 22.5v192q0 14 9 23t23 9h1216q13 0 22.5 -9.5t9.5 -22.5zM384 1123v-99h-335v99h107q0 41 0.5 122t0.5 121v12h-2q-8 -17 -50 -54l-71 76l136 127h106v-404h108zM1792 736v-192q0 -13 -9.5 -22.5t-22.5 -9.5h-1216q-13 0 -22.5 9.5 t-9.5 22.5v192q0 14 9 23t23 9h1216q13 0 22.5 -9.5t9.5 -22.5zM1792 1248v-192q0 -13 -9.5 -22.5t-22.5 -9.5h-1216q-13 0 -22.5 9.5t-9.5 22.5v192q0 13 9.5 22.5t22.5 9.5h1216q13 0 22.5 -9.5t9.5 -22.5z" /> -<glyph unicode="" horiz-adv-x="1792" d="M1760 640q14 0 23 -9t9 -23v-64q0 -14 -9 -23t-23 -9h-1728q-14 0 -23 9t-9 23v64q0 14 9 23t23 9h1728zM483 704q-28 35 -51 80q-48 97 -48 188q0 181 134 309q133 127 393 127q50 0 167 -19q66 -12 177 -48q10 -38 21 -118q14 -123 14 -183q0 -18 -5 -45l-12 -3l-84 6 l-14 2q-50 149 -103 205q-88 91 -210 91q-114 0 -182 -59q-67 -58 -67 -146q0 -73 66 -140t279 -129q69 -20 173 -66q58 -28 95 -52h-743zM990 448h411q7 -39 7 -92q0 -111 -41 -212q-23 -55 -71 -104q-37 -35 -109 -81q-80 -48 -153 -66q-80 -21 -203 -21q-114 0 -195 23 l-140 40q-57 16 -72 28q-8 8 -8 22v13q0 108 -2 156q-1 30 0 68l2 37v44l102 2q15 -34 30 -71t22.5 -56t12.5 -27q35 -57 80 -94q43 -36 105 -57q59 -22 132 -22q64 0 139 27q77 26 122 86q47 61 47 129q0 84 -81 157q-34 29 -137 71z" /> -<glyph unicode="" d="M48 1313q-37 2 -45 4l-3 88q13 1 40 1q60 0 112 -4q132 -7 166 -7q86 0 168 3q116 4 146 5q56 0 86 2l-1 -14l2 -64v-9q-60 -9 -124 -9q-60 0 -79 -25q-13 -14 -13 -132q0 -13 0.5 -32.5t0.5 -25.5l1 -229l14 -280q6 -124 51 -202q35 -59 96 -92q88 -47 177 -47 q104 0 191 28q56 18 99 51q48 36 65 64q36 56 53 114q21 73 21 229q0 79 -3.5 128t-11 122.5t-13.5 159.5l-4 59q-5 67 -24 88q-34 35 -77 34l-100 -2l-14 3l2 86h84l205 -10q76 -3 196 10l18 -2q6 -38 6 -51q0 -7 -4 -31q-45 -12 -84 -13q-73 -11 -79 -17q-15 -15 -15 -41 q0 -7 1.5 -27t1.5 -31q8 -19 22 -396q6 -195 -15 -304q-15 -76 -41 -122q-38 -65 -112 -123q-75 -57 -182 -89q-109 -33 -255 -33q-167 0 -284 46q-119 47 -179 122q-61 76 -83 195q-16 80 -16 237v333q0 188 -17 213q-25 36 -147 39zM1536 -96v64q0 14 -9 23t-23 9h-1472 q-14 0 -23 -9t-9 -23v-64q0 -14 9 -23t23 -9h1472q14 0 23 9t9 23z" /> -<glyph unicode="" horiz-adv-x="1664" d="M512 160v192q0 14 -9 23t-23 9h-320q-14 0 -23 -9t-9 -23v-192q0 -14 9 -23t23 -9h320q14 0 23 9t9 23zM512 544v192q0 14 -9 23t-23 9h-320q-14 0 -23 -9t-9 -23v-192q0 -14 9 -23t23 -9h320q14 0 23 9t9 23zM1024 160v192q0 14 -9 23t-23 9h-320q-14 0 -23 -9t-9 -23 v-192q0 -14 9 -23t23 -9h320q14 0 23 9t9 23zM512 928v192q0 14 -9 23t-23 9h-320q-14 0 -23 -9t-9 -23v-192q0 -14 9 -23t23 -9h320q14 0 23 9t9 23zM1024 544v192q0 14 -9 23t-23 9h-320q-14 0 -23 -9t-9 -23v-192q0 -14 9 -23t23 -9h320q14 0 23 9t9 23zM1536 160v192 q0 14 -9 23t-23 9h-320q-14 0 -23 -9t-9 -23v-192q0 -14 9 -23t23 -9h320q14 0 23 9t9 23zM1024 928v192q0 14 -9 23t-23 9h-320q-14 0 -23 -9t-9 -23v-192q0 -14 9 -23t23 -9h320q14 0 23 9t9 23zM1536 544v192q0 14 -9 23t-23 9h-320q-14 0 -23 -9t-9 -23v-192 q0 -14 9 -23t23 -9h320q14 0 23 9t9 23zM1536 928v192q0 14 -9 23t-23 9h-320q-14 0 -23 -9t-9 -23v-192q0 -14 9 -23t23 -9h320q14 0 23 9t9 23zM1664 1248v-1088q0 -66 -47 -113t-113 -47h-1344q-66 0 -113 47t-47 113v1088q0 66 47 113t113 47h1344q66 0 113 -47t47 -113 z" /> -<glyph unicode="" horiz-adv-x="1664" d="M1190 955l293 293l-107 107l-293 -293zM1637 1248q0 -27 -18 -45l-1286 -1286q-18 -18 -45 -18t-45 18l-198 198q-18 18 -18 45t18 45l1286 1286q18 18 45 18t45 -18l198 -198q18 -18 18 -45zM286 1438l98 -30l-98 -30l-30 -98l-30 98l-98 30l98 30l30 98zM636 1276 l196 -60l-196 -60l-60 -196l-60 196l-196 60l196 60l60 196zM1566 798l98 -30l-98 -30l-30 -98l-30 98l-98 30l98 30l30 98zM926 1438l98 -30l-98 -30l-30 -98l-30 98l-98 30l98 30l30 98z" /> -<glyph unicode="" horiz-adv-x="1792" d="M640 128q0 52 -38 90t-90 38t-90 -38t-38 -90t38 -90t90 -38t90 38t38 90zM256 640h384v256h-158q-13 0 -22 -9l-195 -195q-9 -9 -9 -22v-30zM1536 128q0 52 -38 90t-90 38t-90 -38t-38 -90t38 -90t90 -38t90 38t38 90zM1792 1216v-1024q0 -15 -4 -26.5t-13.5 -18.5 t-16.5 -11.5t-23.5 -6t-22.5 -2t-25.5 0t-22.5 0.5q0 -106 -75 -181t-181 -75t-181 75t-75 181h-384q0 -106 -75 -181t-181 -75t-181 75t-75 181h-64q-3 0 -22.5 -0.5t-25.5 0t-22.5 2t-23.5 6t-16.5 11.5t-13.5 18.5t-4 26.5q0 26 19 45t45 19v320q0 8 -0.5 35t0 38 t2.5 34.5t6.5 37t14 30.5t22.5 30l198 198q19 19 50.5 32t58.5 13h160v192q0 26 19 45t45 19h1024q26 0 45 -19t19 -45z" /> -<glyph unicode="" d="M1536 640q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103q-111 0 -218 32q59 93 78 164q9 34 54 211q20 -39 73 -67.5t114 -28.5q121 0 216 68.5t147 188.5t52 270q0 114 -59.5 214t-172.5 163t-255 63q-105 0 -196 -29t-154.5 -77t-109 -110.5t-67 -129.5t-21.5 -134 q0 -104 40 -183t117 -111q30 -12 38 20q2 7 8 31t8 30q6 23 -11 43q-51 61 -51 151q0 151 104.5 259.5t273.5 108.5q151 0 235.5 -82t84.5 -213q0 -170 -68.5 -289t-175.5 -119q-61 0 -98 43.5t-23 104.5q8 35 26.5 93.5t30 103t11.5 75.5q0 50 -27 83t-77 33 q-62 0 -105 -57t-43 -142q0 -73 25 -122l-99 -418q-17 -70 -13 -177q-206 91 -333 281t-127 423q0 209 103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5t103 -385.5z" /> -<glyph unicode="" d="M1248 1408q119 0 203.5 -84.5t84.5 -203.5v-960q0 -119 -84.5 -203.5t-203.5 -84.5h-725q85 122 108 210q9 34 53 209q21 -39 73.5 -67t112.5 -28q181 0 295.5 147.5t114.5 373.5q0 84 -35 162.5t-96.5 139t-152.5 97t-197 36.5q-104 0 -194.5 -28.5t-153 -76.5 t-107.5 -109.5t-66.5 -128t-21.5 -132.5q0 -102 39.5 -180t116.5 -110q13 -5 23.5 0t14.5 19q10 44 15 61q6 23 -11 42q-50 62 -50 150q0 150 103.5 256.5t270.5 106.5q149 0 232.5 -81t83.5 -210q0 -168 -67.5 -286t-173.5 -118q-60 0 -97 43.5t-23 103.5q8 34 26.5 92.5 t29.5 102t11 74.5q0 49 -26.5 81.5t-75.5 32.5q-61 0 -103.5 -56.5t-42.5 -139.5q0 -72 24 -121l-98 -414q-24 -100 -7 -254h-183q-119 0 -203.5 84.5t-84.5 203.5v960q0 119 84.5 203.5t203.5 84.5h960z" /> -<glyph unicode="" d="M829 318q0 -76 -58.5 -112.5t-139.5 -36.5q-41 0 -80.5 9.5t-75.5 28.5t-58 53t-22 78q0 46 25 80t65.5 51.5t82 25t84.5 7.5q20 0 31 -2q2 -1 23 -16.5t26 -19t23 -18t24.5 -22t19 -22.5t17 -26t9 -26.5t4.5 -31.5zM755 863q0 -60 -33 -99.5t-92 -39.5q-53 0 -93 42.5 t-57.5 96.5t-17.5 106q0 61 32 104t92 43q53 0 93.5 -45t58 -101t17.5 -107zM861 1120l88 64h-265q-85 0 -161 -32t-127.5 -98t-51.5 -153q0 -93 64.5 -154.5t158.5 -61.5q22 0 43 3q-13 -29 -13 -54q0 -44 40 -94q-175 -12 -257 -63q-47 -29 -75.5 -73t-28.5 -95 q0 -43 18.5 -77.5t48.5 -56.5t69 -37t77.5 -21t76.5 -6q60 0 120.5 15.5t113.5 46t86 82.5t33 117q0 49 -20 89.5t-49 66.5t-58 47.5t-49 44t-20 44.5t15.5 42.5t37.5 39.5t44 42t37.5 59.5t15.5 82.5q0 60 -22.5 99.5t-72.5 90.5h83zM1152 672h128v64h-128v128h-64v-128 h-128v-64h128v-160h64v160zM1536 1120v-960q0 -119 -84.5 -203.5t-203.5 -84.5h-960q-119 0 -203.5 84.5t-84.5 203.5v960q0 119 84.5 203.5t203.5 84.5h960q119 0 203.5 -84.5t84.5 -203.5z" /> -<glyph unicode="" horiz-adv-x="1664" d="M735 740q0 -36 32 -70.5t77.5 -68t90.5 -73.5t77 -104t32 -142q0 -90 -48 -173q-72 -122 -211 -179.5t-298 -57.5q-132 0 -246.5 41.5t-171.5 137.5q-37 60 -37 131q0 81 44.5 150t118.5 115q131 82 404 100q-32 42 -47.5 74t-15.5 73q0 36 21 85q-46 -4 -68 -4 q-148 0 -249.5 96.5t-101.5 244.5q0 82 36 159t99 131q77 66 182.5 98t217.5 32h418l-138 -88h-131q74 -63 112 -133t38 -160q0 -72 -24.5 -129.5t-59 -93t-69.5 -65t-59.5 -61.5t-24.5 -66zM589 836q38 0 78 16.5t66 43.5q53 57 53 159q0 58 -17 125t-48.5 129.5 t-84.5 103.5t-117 41q-42 0 -82.5 -19.5t-65.5 -52.5q-47 -59 -47 -160q0 -46 10 -97.5t31.5 -103t52 -92.5t75 -67t96.5 -26zM591 -37q58 0 111.5 13t99 39t73 73t27.5 109q0 25 -7 49t-14.5 42t-27 41.5t-29.5 35t-38.5 34.5t-36.5 29t-41.5 30t-36.5 26q-16 2 -48 2 q-53 0 -105 -7t-107.5 -25t-97 -46t-68.5 -74.5t-27 -105.5q0 -70 35 -123.5t91.5 -83t119 -44t127.5 -14.5zM1401 839h213v-108h-213v-219h-105v219h-212v108h212v217h105v-217z" /> -<glyph unicode="" horiz-adv-x="1920" d="M768 384h384v96h-128v448h-114l-148 -137l77 -80q42 37 55 57h2v-288h-128v-96zM1280 640q0 -70 -21 -142t-59.5 -134t-101.5 -101t-138 -39t-138 39t-101.5 101t-59.5 134t-21 142t21 142t59.5 134t101.5 101t138 39t138 -39t101.5 -101t59.5 -134t21 -142zM1792 384 v512q-106 0 -181 75t-75 181h-1152q0 -106 -75 -181t-181 -75v-512q106 0 181 -75t75 -181h1152q0 106 75 181t181 75zM1920 1216v-1152q0 -26 -19 -45t-45 -19h-1792q-26 0 -45 19t-19 45v1152q0 26 19 45t45 19h1792q26 0 45 -19t19 -45z" /> -<glyph unicode="" horiz-adv-x="1024" d="M1024 832q0 -26 -19 -45l-448 -448q-19 -19 -45 -19t-45 19l-448 448q-19 19 -19 45t19 45t45 19h896q26 0 45 -19t19 -45z" /> -<glyph unicode="" horiz-adv-x="1024" d="M1024 320q0 -26 -19 -45t-45 -19h-896q-26 0 -45 19t-19 45t19 45l448 448q19 19 45 19t45 -19l448 -448q19 -19 19 -45z" /> -<glyph unicode="" horiz-adv-x="640" d="M640 1088v-896q0 -26 -19 -45t-45 -19t-45 19l-448 448q-19 19 -19 45t19 45l448 448q19 19 45 19t45 -19t19 -45z" /> -<glyph unicode="" horiz-adv-x="640" d="M576 640q0 -26 -19 -45l-448 -448q-19 -19 -45 -19t-45 19t-19 45v896q0 26 19 45t45 19t45 -19l448 -448q19 -19 19 -45z" /> -<glyph unicode="" horiz-adv-x="1664" d="M160 0h608v1152h-640v-1120q0 -13 9.5 -22.5t22.5 -9.5zM1536 32v1120h-640v-1152h608q13 0 22.5 9.5t9.5 22.5zM1664 1248v-1216q0 -66 -47 -113t-113 -47h-1344q-66 0 -113 47t-47 113v1216q0 66 47 113t113 47h1344q66 0 113 -47t47 -113z" /> -<glyph unicode="" horiz-adv-x="1024" d="M1024 448q0 -26 -19 -45l-448 -448q-19 -19 -45 -19t-45 19l-448 448q-19 19 -19 45t19 45t45 19h896q26 0 45 -19t19 -45zM1024 832q0 -26 -19 -45t-45 -19h-896q-26 0 -45 19t-19 45t19 45l448 448q19 19 45 19t45 -19l448 -448q19 -19 19 -45z" /> -<glyph unicode="" horiz-adv-x="1024" d="M1024 448q0 -26 -19 -45l-448 -448q-19 -19 -45 -19t-45 19l-448 448q-19 19 -19 45t19 45t45 19h896q26 0 45 -19t19 -45z" /> -<glyph unicode="" horiz-adv-x="1024" d="M1024 832q0 -26 -19 -45t-45 -19h-896q-26 0 -45 19t-19 45t19 45l448 448q19 19 45 19t45 -19l448 -448q19 -19 19 -45z" /> -<glyph unicode="" horiz-adv-x="1792" d="M1792 826v-794q0 -66 -47 -113t-113 -47h-1472q-66 0 -113 47t-47 113v794q44 -49 101 -87q362 -246 497 -345q57 -42 92.5 -65.5t94.5 -48t110 -24.5h1h1q51 0 110 24.5t94.5 48t92.5 65.5q170 123 498 345q57 39 100 87zM1792 1120q0 -79 -49 -151t-122 -123 q-376 -261 -468 -325q-10 -7 -42.5 -30.5t-54 -38t-52 -32.5t-57.5 -27t-50 -9h-1h-1q-23 0 -50 9t-57.5 27t-52 32.5t-54 38t-42.5 30.5q-91 64 -262 182.5t-205 142.5q-62 42 -117 115.5t-55 136.5q0 78 41.5 130t118.5 52h1472q65 0 112.5 -47t47.5 -113z" /> -<glyph unicode="" d="M349 911v-991h-330v991h330zM370 1217q1 -73 -50.5 -122t-135.5 -49h-2q-82 0 -132 49t-50 122q0 74 51.5 122.5t134.5 48.5t133 -48.5t51 -122.5zM1536 488v-568h-329v530q0 105 -40.5 164.5t-126.5 59.5q-63 0 -105.5 -34.5t-63.5 -85.5q-11 -30 -11 -81v-553h-329 q2 399 2 647t-1 296l-1 48h329v-144h-2q20 32 41 56t56.5 52t87 43.5t114.5 15.5q171 0 275 -113.5t104 -332.5z" /> -<glyph unicode="" d="M1536 640q0 -156 -61 -298t-164 -245t-245 -164t-298 -61q-172 0 -327 72.5t-264 204.5q-7 10 -6.5 22.5t8.5 20.5l137 138q10 9 25 9q16 -2 23 -12q73 -95 179 -147t225 -52q104 0 198.5 40.5t163.5 109.5t109.5 163.5t40.5 198.5t-40.5 198.5t-109.5 163.5 t-163.5 109.5t-198.5 40.5q-98 0 -188 -35.5t-160 -101.5l137 -138q31 -30 14 -69q-17 -40 -59 -40h-448q-26 0 -45 19t-19 45v448q0 42 40 59q39 17 69 -14l130 -129q107 101 244.5 156.5t284.5 55.5q156 0 298 -61t245 -164t164 -245t61 -298z" /> -<glyph unicode="" horiz-adv-x="1792" d="M1771 0q0 -53 -37 -90l-107 -108q-39 -37 -91 -37q-53 0 -90 37l-363 364q-38 36 -38 90q0 53 43 96l-256 256l-126 -126q-14 -14 -34 -14t-34 14q2 -2 12.5 -12t12.5 -13t10 -11.5t10 -13.5t6 -13.5t5.5 -16.5t1.5 -18q0 -38 -28 -68q-3 -3 -16.5 -18t-19 -20.5 t-18.5 -16.5t-22 -15.5t-22 -9t-26 -4.5q-40 0 -68 28l-408 408q-28 28 -28 68q0 13 4.5 26t9 22t15.5 22t16.5 18.5t20.5 19t18 16.5q30 28 68 28q10 0 18 -1.5t16.5 -5.5t13.5 -6t13.5 -10t11.5 -10t13 -12.5t12 -12.5q-14 14 -14 34t14 34l348 348q14 14 34 14t34 -14 q-2 2 -12.5 12t-12.5 13t-10 11.5t-10 13.5t-6 13.5t-5.5 16.5t-1.5 18q0 38 28 68q3 3 16.5 18t19 20.5t18.5 16.5t22 15.5t22 9t26 4.5q40 0 68 -28l408 -408q28 -28 28 -68q0 -13 -4.5 -26t-9 -22t-15.5 -22t-16.5 -18.5t-20.5 -19t-18 -16.5q-30 -28 -68 -28 q-10 0 -18 1.5t-16.5 5.5t-13.5 6t-13.5 10t-11.5 10t-13 12.5t-12 12.5q14 -14 14 -34t-14 -34l-126 -126l256 -256q43 43 96 43q52 0 91 -37l363 -363q37 -39 37 -91z" /> -<glyph unicode="" horiz-adv-x="1792" d="M384 384q0 53 -37.5 90.5t-90.5 37.5t-90.5 -37.5t-37.5 -90.5t37.5 -90.5t90.5 -37.5t90.5 37.5t37.5 90.5zM576 832q0 53 -37.5 90.5t-90.5 37.5t-90.5 -37.5t-37.5 -90.5t37.5 -90.5t90.5 -37.5t90.5 37.5t37.5 90.5zM1004 351l101 382q6 26 -7.5 48.5t-38.5 29.5 t-48 -6.5t-30 -39.5l-101 -382q-60 -5 -107 -43.5t-63 -98.5q-20 -77 20 -146t117 -89t146 20t89 117q16 60 -6 117t-72 91zM1664 384q0 53 -37.5 90.5t-90.5 37.5t-90.5 -37.5t-37.5 -90.5t37.5 -90.5t90.5 -37.5t90.5 37.5t37.5 90.5zM1024 1024q0 53 -37.5 90.5 t-90.5 37.5t-90.5 -37.5t-37.5 -90.5t37.5 -90.5t90.5 -37.5t90.5 37.5t37.5 90.5zM1472 832q0 53 -37.5 90.5t-90.5 37.5t-90.5 -37.5t-37.5 -90.5t37.5 -90.5t90.5 -37.5t90.5 37.5t37.5 90.5zM1792 384q0 -261 -141 -483q-19 -29 -54 -29h-1402q-35 0 -54 29 q-141 221 -141 483q0 182 71 348t191 286t286 191t348 71t348 -71t286 -191t191 -286t71 -348z" /> -<glyph unicode="" horiz-adv-x="1792" d="M896 1152q-204 0 -381.5 -69.5t-282 -187.5t-104.5 -255q0 -112 71.5 -213.5t201.5 -175.5l87 -50l-27 -96q-24 -91 -70 -172q152 63 275 171l43 38l57 -6q69 -8 130 -8q204 0 381.5 69.5t282 187.5t104.5 255t-104.5 255t-282 187.5t-381.5 69.5zM1792 640 q0 -174 -120 -321.5t-326 -233t-450 -85.5q-70 0 -145 8q-198 -175 -460 -242q-49 -14 -114 -22h-5q-15 0 -27 10.5t-16 27.5v1q-3 4 -0.5 12t2 10t4.5 9.5l6 9t7 8.5t8 9q7 8 31 34.5t34.5 38t31 39.5t32.5 51t27 59t26 76q-157 89 -247.5 220t-90.5 281q0 174 120 321.5 t326 233t450 85.5t450 -85.5t326 -233t120 -321.5z" /> -<glyph unicode="" horiz-adv-x="1792" d="M704 1152q-153 0 -286 -52t-211.5 -141t-78.5 -191q0 -82 53 -158t149 -132l97 -56l-35 -84q34 20 62 39l44 31l53 -10q78 -14 153 -14q153 0 286 52t211.5 141t78.5 191t-78.5 191t-211.5 141t-286 52zM704 1280q191 0 353.5 -68.5t256.5 -186.5t94 -257t-94 -257 t-256.5 -186.5t-353.5 -68.5q-86 0 -176 16q-124 -88 -278 -128q-36 -9 -86 -16h-3q-11 0 -20.5 8t-11.5 21q-1 3 -1 6.5t0.5 6.5t2 6l2.5 5t3.5 5.5t4 5t4.5 5t4 4.5q5 6 23 25t26 29.5t22.5 29t25 38.5t20.5 44q-124 72 -195 177t-71 224q0 139 94 257t256.5 186.5 t353.5 68.5zM1526 111q10 -24 20.5 -44t25 -38.5t22.5 -29t26 -29.5t23 -25q1 -1 4 -4.5t4.5 -5t4 -5t3.5 -5.5l2.5 -5t2 -6t0.5 -6.5t-1 -6.5q-3 -14 -13 -22t-22 -7q-50 7 -86 16q-154 40 -278 128q-90 -16 -176 -16q-271 0 -472 132q58 -4 88 -4q161 0 309 45t264 129 q125 92 192 212t67 254q0 77 -23 152q129 -71 204 -178t75 -230q0 -120 -71 -224.5t-195 -176.5z" /> -<glyph unicode="" horiz-adv-x="896" d="M885 970q18 -20 7 -44l-540 -1157q-13 -25 -42 -25q-4 0 -14 2q-17 5 -25.5 19t-4.5 30l197 808l-406 -101q-4 -1 -12 -1q-18 0 -31 11q-18 15 -13 39l201 825q4 14 16 23t28 9h328q19 0 32 -12.5t13 -29.5q0 -8 -5 -18l-171 -463l396 98q8 2 12 2q19 0 34 -15z" /> -<glyph unicode="" horiz-adv-x="1792" d="M1792 288v-320q0 -40 -28 -68t-68 -28h-320q-40 0 -68 28t-28 68v320q0 40 28 68t68 28h96v192h-512v-192h96q40 0 68 -28t28 -68v-320q0 -40 -28 -68t-68 -28h-320q-40 0 -68 28t-28 68v320q0 40 28 68t68 28h96v192h-512v-192h96q40 0 68 -28t28 -68v-320 q0 -40 -28 -68t-68 -28h-320q-40 0 -68 28t-28 68v320q0 40 28 68t68 28h96v192q0 52 38 90t90 38h512v192h-96q-40 0 -68 28t-28 68v320q0 40 28 68t68 28h320q40 0 68 -28t28 -68v-320q0 -40 -28 -68t-68 -28h-96v-192h512q52 0 90 -38t38 -90v-192h96q40 0 68 -28t28 -68 z" /> -<glyph unicode="" horiz-adv-x="1664" d="M896 708v-580q0 -104 -76 -180t-180 -76t-180 76t-76 180q0 26 19 45t45 19t45 -19t19 -45q0 -50 39 -89t89 -39t89 39t39 89v580q33 11 64 11t64 -11zM1664 681q0 -13 -9.5 -22.5t-22.5 -9.5q-11 0 -23 10q-49 46 -93 69t-102 23q-68 0 -128 -37t-103 -97 q-7 -10 -17.5 -28t-14.5 -24q-11 -17 -28 -17q-18 0 -29 17q-4 6 -14.5 24t-17.5 28q-43 60 -102.5 97t-127.5 37t-127.5 -37t-102.5 -97q-7 -10 -17.5 -28t-14.5 -24q-11 -17 -29 -17q-17 0 -28 17q-4 6 -14.5 24t-17.5 28q-43 60 -103 97t-128 37q-58 0 -102 -23t-93 -69 q-12 -10 -23 -10q-13 0 -22.5 9.5t-9.5 22.5q0 5 1 7q45 183 172.5 319.5t298 204.5t360.5 68q140 0 274.5 -40t246.5 -113.5t194.5 -187t115.5 -251.5q1 -2 1 -7zM896 1408v-98q-42 2 -64 2t-64 -2v98q0 26 19 45t45 19t45 -19t19 -45z" /> -<glyph unicode="" horiz-adv-x="1792" d="M768 -128h896v640h-416q-40 0 -68 28t-28 68v416h-384v-1152zM1024 1312v64q0 13 -9.5 22.5t-22.5 9.5h-704q-13 0 -22.5 -9.5t-9.5 -22.5v-64q0 -13 9.5 -22.5t22.5 -9.5h704q13 0 22.5 9.5t9.5 22.5zM1280 640h299l-299 299v-299zM1792 512v-672q0 -40 -28 -68t-68 -28 h-960q-40 0 -68 28t-28 68v160h-544q-40 0 -68 28t-28 68v1344q0 40 28 68t68 28h1088q40 0 68 -28t28 -68v-328q21 -13 36 -28l408 -408q28 -28 48 -76t20 -88z" /> -<glyph unicode="" horiz-adv-x="1024" d="M736 960q0 -13 -9.5 -22.5t-22.5 -9.5t-22.5 9.5t-9.5 22.5q0 46 -54 71t-106 25q-13 0 -22.5 9.5t-9.5 22.5t9.5 22.5t22.5 9.5q50 0 99.5 -16t87 -54t37.5 -90zM896 960q0 72 -34.5 134t-90 101.5t-123 62t-136.5 22.5t-136.5 -22.5t-123 -62t-90 -101.5t-34.5 -134 q0 -101 68 -180q10 -11 30.5 -33t30.5 -33q128 -153 141 -298h228q13 145 141 298q10 11 30.5 33t30.5 33q68 79 68 180zM1024 960q0 -155 -103 -268q-45 -49 -74.5 -87t-59.5 -95.5t-34 -107.5q47 -28 47 -82q0 -37 -25 -64q25 -27 25 -64q0 -52 -45 -81q13 -23 13 -47 q0 -46 -31.5 -71t-77.5 -25q-20 -44 -60 -70t-87 -26t-87 26t-60 70q-46 0 -77.5 25t-31.5 71q0 24 13 47q-45 29 -45 81q0 37 25 64q-25 27 -25 64q0 54 47 82q-4 50 -34 107.5t-59.5 95.5t-74.5 87q-103 113 -103 268q0 99 44.5 184.5t117 142t164 89t186.5 32.5 t186.5 -32.5t164 -89t117 -142t44.5 -184.5z" /> -<glyph unicode="" horiz-adv-x="1792" d="M1792 352v-192q0 -13 -9.5 -22.5t-22.5 -9.5h-1376v-192q0 -13 -9.5 -22.5t-22.5 -9.5q-12 0 -24 10l-319 320q-9 9 -9 22q0 14 9 23l320 320q9 9 23 9q13 0 22.5 -9.5t9.5 -22.5v-192h1376q13 0 22.5 -9.5t9.5 -22.5zM1792 896q0 -14 -9 -23l-320 -320q-9 -9 -23 -9 q-13 0 -22.5 9.5t-9.5 22.5v192h-1376q-13 0 -22.5 9.5t-9.5 22.5v192q0 13 9.5 22.5t22.5 9.5h1376v192q0 14 9 23t23 9q12 0 24 -10l319 -319q9 -9 9 -23z" /> -<glyph unicode="" horiz-adv-x="1920" d="M1280 608q0 14 -9 23t-23 9h-224v352q0 13 -9.5 22.5t-22.5 9.5h-192q-13 0 -22.5 -9.5t-9.5 -22.5v-352h-224q-13 0 -22.5 -9.5t-9.5 -22.5q0 -14 9 -23l352 -352q9 -9 23 -9t23 9l351 351q10 12 10 24zM1920 384q0 -159 -112.5 -271.5t-271.5 -112.5h-1088 q-185 0 -316.5 131.5t-131.5 316.5q0 130 70 240t188 165q-2 30 -2 43q0 212 150 362t362 150q156 0 285.5 -87t188.5 -231q71 62 166 62q106 0 181 -75t75 -181q0 -76 -41 -138q130 -31 213.5 -135.5t83.5 -238.5z" /> -<glyph unicode="" horiz-adv-x="1920" d="M1280 672q0 14 -9 23l-352 352q-9 9 -23 9t-23 -9l-351 -351q-10 -12 -10 -24q0 -14 9 -23t23 -9h224v-352q0 -13 9.5 -22.5t22.5 -9.5h192q13 0 22.5 9.5t9.5 22.5v352h224q13 0 22.5 9.5t9.5 22.5zM1920 384q0 -159 -112.5 -271.5t-271.5 -112.5h-1088 q-185 0 -316.5 131.5t-131.5 316.5q0 130 70 240t188 165q-2 30 -2 43q0 212 150 362t362 150q156 0 285.5 -87t188.5 -231q71 62 166 62q106 0 181 -75t75 -181q0 -76 -41 -138q130 -31 213.5 -135.5t83.5 -238.5z" /> -<glyph unicode="" horiz-adv-x="1408" d="M384 192q0 -26 -19 -45t-45 -19t-45 19t-19 45t19 45t45 19t45 -19t19 -45zM1408 131q0 -121 -73 -190t-194 -69h-874q-121 0 -194 69t-73 190q0 68 5.5 131t24 138t47.5 132.5t81 103t120 60.5q-22 -52 -22 -120v-203q-58 -20 -93 -70t-35 -111q0 -80 56 -136t136 -56 t136 56t56 136q0 61 -35.5 111t-92.5 70v203q0 62 25 93q132 -104 295 -104t295 104q25 -31 25 -93v-64q-106 0 -181 -75t-75 -181v-89q-32 -29 -32 -71q0 -40 28 -68t68 -28t68 28t28 68q0 42 -32 71v89q0 52 38 90t90 38t90 -38t38 -90v-89q-32 -29 -32 -71q0 -40 28 -68 t68 -28t68 28t28 68q0 42 -32 71v89q0 68 -34.5 127.5t-93.5 93.5q0 10 0.5 42.5t0 48t-2.5 41.5t-7 47t-13 40q68 -15 120 -60.5t81 -103t47.5 -132.5t24 -138t5.5 -131zM1088 1024q0 -159 -112.5 -271.5t-271.5 -112.5t-271.5 112.5t-112.5 271.5t112.5 271.5t271.5 112.5 t271.5 -112.5t112.5 -271.5z" /> -<glyph unicode="" horiz-adv-x="1408" d="M1280 832q0 26 -19 45t-45 19t-45 -19t-19 -45t19 -45t45 -19t45 19t19 45zM1408 832q0 -62 -35.5 -111t-92.5 -70v-395q0 -159 -131.5 -271.5t-316.5 -112.5t-316.5 112.5t-131.5 271.5v132q-164 20 -274 128t-110 252v512q0 26 19 45t45 19q6 0 16 -2q17 30 47 48 t65 18q53 0 90.5 -37.5t37.5 -90.5t-37.5 -90.5t-90.5 -37.5q-33 0 -64 18v-402q0 -106 94 -181t226 -75t226 75t94 181v402q-31 -18 -64 -18q-53 0 -90.5 37.5t-37.5 90.5t37.5 90.5t90.5 37.5q35 0 65 -18t47 -48q10 2 16 2q26 0 45 -19t19 -45v-512q0 -144 -110 -252 t-274 -128v-132q0 -106 94 -181t226 -75t226 75t94 181v395q-57 21 -92.5 70t-35.5 111q0 80 56 136t136 56t136 -56t56 -136z" /> -<glyph unicode="" horiz-adv-x="1792" d="M640 1152h512v128h-512v-128zM288 1152v-1280h-64q-92 0 -158 66t-66 158v832q0 92 66 158t158 66h64zM1408 1152v-1280h-1024v1280h128v160q0 40 28 68t68 28h576q40 0 68 -28t28 -68v-160h128zM1792 928v-832q0 -92 -66 -158t-158 -66h-64v1280h64q92 0 158 -66 t66 -158z" /> -<glyph unicode="" horiz-adv-x="1792" d="M912 -160q0 16 -16 16q-59 0 -101.5 42.5t-42.5 101.5q0 16 -16 16t-16 -16q0 -73 51.5 -124.5t124.5 -51.5q16 0 16 16zM1728 128q0 -52 -38 -90t-90 -38h-448q0 -106 -75 -181t-181 -75t-181 75t-75 181h-448q-52 0 -90 38t-38 90q50 42 91 88t85 119.5t74.5 158.5 t50 206t19.5 260q0 152 117 282.5t307 158.5q-8 19 -8 39q0 40 28 68t68 28t68 -28t28 -68q0 -20 -8 -39q190 -28 307 -158.5t117 -282.5q0 -139 19.5 -260t50 -206t74.5 -158.5t85 -119.5t91 -88z" /> -<glyph unicode="" horiz-adv-x="1920" d="M1664 896q0 80 -56 136t-136 56h-64v-384h64q80 0 136 56t56 136zM0 128h1792q0 -106 -75 -181t-181 -75h-1280q-106 0 -181 75t-75 181zM1856 896q0 -159 -112.5 -271.5t-271.5 -112.5h-64v-32q0 -92 -66 -158t-158 -66h-704q-92 0 -158 66t-66 158v736q0 26 19 45 t45 19h1152q159 0 271.5 -112.5t112.5 -271.5z" /> -<glyph unicode="" horiz-adv-x="1408" d="M640 1472v-640q0 -61 -35.5 -111t-92.5 -70v-779q0 -52 -38 -90t-90 -38h-128q-52 0 -90 38t-38 90v779q-57 20 -92.5 70t-35.5 111v640q0 26 19 45t45 19t45 -19t19 -45v-416q0 -26 19 -45t45 -19t45 19t19 45v416q0 26 19 45t45 19t45 -19t19 -45v-416q0 -26 19 -45 t45 -19t45 19t19 45v416q0 26 19 45t45 19t45 -19t19 -45zM1408 1472v-1600q0 -52 -38 -90t-90 -38h-128q-52 0 -90 38t-38 90v512h-224q-13 0 -22.5 9.5t-9.5 22.5v800q0 132 94 226t226 94h256q26 0 45 -19t19 -45z" /> -<glyph unicode="" d="M1468 1156q28 -28 48 -76t20 -88v-1152q0 -40 -28 -68t-68 -28h-1344q-40 0 -68 28t-28 68v1600q0 40 28 68t68 28h896q40 0 88 -20t76 -48zM1024 1400v-376h376q-10 29 -22 41l-313 313q-12 12 -41 22zM1408 -128v1024h-416q-40 0 -68 28t-28 68v416h-768v-1536h1280z M384 736q0 14 9 23t23 9h704q14 0 23 -9t9 -23v-64q0 -14 -9 -23t-23 -9h-704q-14 0 -23 9t-9 23v64zM1120 512q14 0 23 -9t9 -23v-64q0 -14 -9 -23t-23 -9h-704q-14 0 -23 9t-9 23v64q0 14 9 23t23 9h704zM1120 256q14 0 23 -9t9 -23v-64q0 -14 -9 -23t-23 -9h-704 q-14 0 -23 9t-9 23v64q0 14 9 23t23 9h704z" /> -<glyph unicode="" horiz-adv-x="1408" d="M384 224v-64q0 -13 -9.5 -22.5t-22.5 -9.5h-64q-13 0 -22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h64q13 0 22.5 -9.5t9.5 -22.5zM384 480v-64q0 -13 -9.5 -22.5t-22.5 -9.5h-64q-13 0 -22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h64q13 0 22.5 -9.5t9.5 -22.5z M640 480v-64q0 -13 -9.5 -22.5t-22.5 -9.5h-64q-13 0 -22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h64q13 0 22.5 -9.5t9.5 -22.5zM384 736v-64q0 -13 -9.5 -22.5t-22.5 -9.5h-64q-13 0 -22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h64q13 0 22.5 -9.5t9.5 -22.5z M1152 224v-64q0 -13 -9.5 -22.5t-22.5 -9.5h-64q-13 0 -22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h64q13 0 22.5 -9.5t9.5 -22.5zM896 480v-64q0 -13 -9.5 -22.5t-22.5 -9.5h-64q-13 0 -22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h64q13 0 22.5 -9.5t9.5 -22.5z M640 736v-64q0 -13 -9.5 -22.5t-22.5 -9.5h-64q-13 0 -22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h64q13 0 22.5 -9.5t9.5 -22.5zM384 992v-64q0 -13 -9.5 -22.5t-22.5 -9.5h-64q-13 0 -22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h64q13 0 22.5 -9.5t9.5 -22.5z M1152 480v-64q0 -13 -9.5 -22.5t-22.5 -9.5h-64q-13 0 -22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h64q13 0 22.5 -9.5t9.5 -22.5zM896 736v-64q0 -13 -9.5 -22.5t-22.5 -9.5h-64q-13 0 -22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h64q13 0 22.5 -9.5t9.5 -22.5z M640 992v-64q0 -13 -9.5 -22.5t-22.5 -9.5h-64q-13 0 -22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h64q13 0 22.5 -9.5t9.5 -22.5zM384 1248v-64q0 -13 -9.5 -22.5t-22.5 -9.5h-64q-13 0 -22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h64q13 0 22.5 -9.5t9.5 -22.5z M1152 736v-64q0 -13 -9.5 -22.5t-22.5 -9.5h-64q-13 0 -22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h64q13 0 22.5 -9.5t9.5 -22.5zM896 992v-64q0 -13 -9.5 -22.5t-22.5 -9.5h-64q-13 0 -22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h64q13 0 22.5 -9.5t9.5 -22.5z M640 1248v-64q0 -13 -9.5 -22.5t-22.5 -9.5h-64q-13 0 -22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h64q13 0 22.5 -9.5t9.5 -22.5zM1152 992v-64q0 -13 -9.5 -22.5t-22.5 -9.5h-64q-13 0 -22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h64q13 0 22.5 -9.5t9.5 -22.5z M896 1248v-64q0 -13 -9.5 -22.5t-22.5 -9.5h-64q-13 0 -22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h64q13 0 22.5 -9.5t9.5 -22.5zM1152 1248v-64q0 -13 -9.5 -22.5t-22.5 -9.5h-64q-13 0 -22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h64q13 0 22.5 -9.5t9.5 -22.5z M896 -128h384v1536h-1152v-1536h384v224q0 13 9.5 22.5t22.5 9.5h320q13 0 22.5 -9.5t9.5 -22.5v-224zM1408 1472v-1664q0 -26 -19 -45t-45 -19h-1280q-26 0 -45 19t-19 45v1664q0 26 19 45t45 19h1280q26 0 45 -19t19 -45z" /> -<glyph unicode="" horiz-adv-x="1408" d="M384 224v-64q0 -13 -9.5 -22.5t-22.5 -9.5h-64q-13 0 -22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h64q13 0 22.5 -9.5t9.5 -22.5zM384 480v-64q0 -13 -9.5 -22.5t-22.5 -9.5h-64q-13 0 -22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h64q13 0 22.5 -9.5t9.5 -22.5z M640 480v-64q0 -13 -9.5 -22.5t-22.5 -9.5h-64q-13 0 -22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h64q13 0 22.5 -9.5t9.5 -22.5zM384 736v-64q0 -13 -9.5 -22.5t-22.5 -9.5h-64q-13 0 -22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h64q13 0 22.5 -9.5t9.5 -22.5z M1152 224v-64q0 -13 -9.5 -22.5t-22.5 -9.5h-64q-13 0 -22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h64q13 0 22.5 -9.5t9.5 -22.5zM896 480v-64q0 -13 -9.5 -22.5t-22.5 -9.5h-64q-13 0 -22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h64q13 0 22.5 -9.5t9.5 -22.5z M640 736v-64q0 -13 -9.5 -22.5t-22.5 -9.5h-64q-13 0 -22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h64q13 0 22.5 -9.5t9.5 -22.5zM1152 480v-64q0 -13 -9.5 -22.5t-22.5 -9.5h-64q-13 0 -22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h64q13 0 22.5 -9.5t9.5 -22.5z M896 736v-64q0 -13 -9.5 -22.5t-22.5 -9.5h-64q-13 0 -22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h64q13 0 22.5 -9.5t9.5 -22.5zM1152 736v-64q0 -13 -9.5 -22.5t-22.5 -9.5h-64q-13 0 -22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h64q13 0 22.5 -9.5t9.5 -22.5z M896 -128h384v1152h-256v-32q0 -40 -28 -68t-68 -28h-448q-40 0 -68 28t-28 68v32h-256v-1152h384v224q0 13 9.5 22.5t22.5 9.5h320q13 0 22.5 -9.5t9.5 -22.5v-224zM896 1056v320q0 13 -9.5 22.5t-22.5 9.5h-64q-13 0 -22.5 -9.5t-9.5 -22.5v-96h-128v96q0 13 -9.5 22.5 t-22.5 9.5h-64q-13 0 -22.5 -9.5t-9.5 -22.5v-320q0 -13 9.5 -22.5t22.5 -9.5h64q13 0 22.5 9.5t9.5 22.5v96h128v-96q0 -13 9.5 -22.5t22.5 -9.5h64q13 0 22.5 9.5t9.5 22.5zM1408 1088v-1280q0 -26 -19 -45t-45 -19h-1280q-26 0 -45 19t-19 45v1280q0 26 19 45t45 19h320 v288q0 40 28 68t68 28h448q40 0 68 -28t28 -68v-288h320q26 0 45 -19t19 -45z" /> -<glyph unicode="" horiz-adv-x="1920" d="M640 128q0 53 -37.5 90.5t-90.5 37.5t-90.5 -37.5t-37.5 -90.5t37.5 -90.5t90.5 -37.5t90.5 37.5t37.5 90.5zM256 640h384v256h-158q-14 -2 -22 -9l-195 -195q-7 -12 -9 -22v-30zM1536 128q0 53 -37.5 90.5t-90.5 37.5t-90.5 -37.5t-37.5 -90.5t37.5 -90.5t90.5 -37.5 t90.5 37.5t37.5 90.5zM1664 800v192q0 14 -9 23t-23 9h-224v224q0 14 -9 23t-23 9h-192q-14 0 -23 -9t-9 -23v-224h-224q-14 0 -23 -9t-9 -23v-192q0 -14 9 -23t23 -9h224v-224q0 -14 9 -23t23 -9h192q14 0 23 9t9 23v224h224q14 0 23 9t9 23zM1920 1344v-1152 q0 -26 -19 -45t-45 -19h-192q0 -106 -75 -181t-181 -75t-181 75t-75 181h-384q0 -106 -75 -181t-181 -75t-181 75t-75 181h-128q-26 0 -45 19t-19 45t19 45t45 19v416q0 26 13 58t32 51l198 198q19 19 51 32t58 13h160v320q0 26 19 45t45 19h1152q26 0 45 -19t19 -45z" /> -<glyph unicode="" horiz-adv-x="1792" d="M1280 416v192q0 14 -9 23t-23 9h-224v224q0 14 -9 23t-23 9h-192q-14 0 -23 -9t-9 -23v-224h-224q-14 0 -23 -9t-9 -23v-192q0 -14 9 -23t23 -9h224v-224q0 -14 9 -23t23 -9h192q14 0 23 9t9 23v224h224q14 0 23 9t9 23zM640 1152h512v128h-512v-128zM256 1152v-1280h-32 q-92 0 -158 66t-66 158v832q0 92 66 158t158 66h32zM1440 1152v-1280h-1088v1280h160v160q0 40 28 68t68 28h576q40 0 68 -28t28 -68v-160h160zM1792 928v-832q0 -92 -66 -158t-158 -66h-32v1280h32q92 0 158 -66t66 -158z" /> -<glyph unicode="" horiz-adv-x="1920" d="M1920 576q-1 -32 -288 -96l-352 -32l-224 -64h-64l-293 -352h69q26 0 45 -4.5t19 -11.5t-19 -11.5t-45 -4.5h-96h-160h-64v32h64v416h-160l-192 -224h-96l-32 32v192h32v32h128v8l-192 24v128l192 24v8h-128v32h-32v192l32 32h96l192 -224h160v416h-64v32h64h160h96 q26 0 45 -4.5t19 -11.5t-19 -11.5t-45 -4.5h-69l293 -352h64l224 -64l352 -32q261 -58 287 -93z" /> -<glyph unicode="" horiz-adv-x="1664" d="M640 640v384h-256v-256q0 -53 37.5 -90.5t90.5 -37.5h128zM1664 192v-192h-1152v192l128 192h-128q-159 0 -271.5 112.5t-112.5 271.5v320l-64 64l32 128h480l32 128h960l32 -192l-64 -32v-800z" /> -<glyph unicode="" d="M1280 192v896q0 26 -19 45t-45 19h-128q-26 0 -45 -19t-19 -45v-320h-512v320q0 26 -19 45t-45 19h-128q-26 0 -45 -19t-19 -45v-896q0 -26 19 -45t45 -19h128q26 0 45 19t19 45v320h512v-320q0 -26 19 -45t45 -19h128q26 0 45 19t19 45zM1536 1120v-960 q0 -119 -84.5 -203.5t-203.5 -84.5h-960q-119 0 -203.5 84.5t-84.5 203.5v960q0 119 84.5 203.5t203.5 84.5h960q119 0 203.5 -84.5t84.5 -203.5z" /> -<glyph unicode="" d="M1280 576v128q0 26 -19 45t-45 19h-320v320q0 26 -19 45t-45 19h-128q-26 0 -45 -19t-19 -45v-320h-320q-26 0 -45 -19t-19 -45v-128q0 -26 19 -45t45 -19h320v-320q0 -26 19 -45t45 -19h128q26 0 45 19t19 45v320h320q26 0 45 19t19 45zM1536 1120v-960 q0 -119 -84.5 -203.5t-203.5 -84.5h-960q-119 0 -203.5 84.5t-84.5 203.5v960q0 119 84.5 203.5t203.5 84.5h960q119 0 203.5 -84.5t84.5 -203.5z" /> -<glyph unicode="" horiz-adv-x="1024" d="M627 160q0 -13 -10 -23l-50 -50q-10 -10 -23 -10t-23 10l-466 466q-10 10 -10 23t10 23l466 466q10 10 23 10t23 -10l50 -50q10 -10 10 -23t-10 -23l-393 -393l393 -393q10 -10 10 -23zM1011 160q0 -13 -10 -23l-50 -50q-10 -10 -23 -10t-23 10l-466 466q-10 10 -10 23 t10 23l466 466q10 10 23 10t23 -10l50 -50q10 -10 10 -23t-10 -23l-393 -393l393 -393q10 -10 10 -23z" /> -<glyph unicode="" horiz-adv-x="1024" d="M595 576q0 -13 -10 -23l-466 -466q-10 -10 -23 -10t-23 10l-50 50q-10 10 -10 23t10 23l393 393l-393 393q-10 10 -10 23t10 23l50 50q10 10 23 10t23 -10l466 -466q10 -10 10 -23zM979 576q0 -13 -10 -23l-466 -466q-10 -10 -23 -10t-23 10l-50 50q-10 10 -10 23t10 23 l393 393l-393 393q-10 10 -10 23t10 23l50 50q10 10 23 10t23 -10l466 -466q10 -10 10 -23z" /> -<glyph unicode="" horiz-adv-x="1152" d="M1075 224q0 -13 -10 -23l-50 -50q-10 -10 -23 -10t-23 10l-393 393l-393 -393q-10 -10 -23 -10t-23 10l-50 50q-10 10 -10 23t10 23l466 466q10 10 23 10t23 -10l466 -466q10 -10 10 -23zM1075 608q0 -13 -10 -23l-50 -50q-10 -10 -23 -10t-23 10l-393 393l-393 -393 q-10 -10 -23 -10t-23 10l-50 50q-10 10 -10 23t10 23l466 466q10 10 23 10t23 -10l466 -466q10 -10 10 -23z" /> -<glyph unicode="" horiz-adv-x="1152" d="M1075 672q0 -13 -10 -23l-466 -466q-10 -10 -23 -10t-23 10l-466 466q-10 10 -10 23t10 23l50 50q10 10 23 10t23 -10l393 -393l393 393q10 10 23 10t23 -10l50 -50q10 -10 10 -23zM1075 1056q0 -13 -10 -23l-466 -466q-10 -10 -23 -10t-23 10l-466 466q-10 10 -10 23 t10 23l50 50q10 10 23 10t23 -10l393 -393l393 393q10 10 23 10t23 -10l50 -50q10 -10 10 -23z" /> -<glyph unicode="" horiz-adv-x="640" d="M627 992q0 -13 -10 -23l-393 -393l393 -393q10 -10 10 -23t-10 -23l-50 -50q-10 -10 -23 -10t-23 10l-466 466q-10 10 -10 23t10 23l466 466q10 10 23 10t23 -10l50 -50q10 -10 10 -23z" /> -<glyph unicode="" horiz-adv-x="640" d="M595 576q0 -13 -10 -23l-466 -466q-10 -10 -23 -10t-23 10l-50 50q-10 10 -10 23t10 23l393 393l-393 393q-10 10 -10 23t10 23l50 50q10 10 23 10t23 -10l466 -466q10 -10 10 -23z" /> -<glyph unicode="" horiz-adv-x="1152" d="M1075 352q0 -13 -10 -23l-50 -50q-10 -10 -23 -10t-23 10l-393 393l-393 -393q-10 -10 -23 -10t-23 10l-50 50q-10 10 -10 23t10 23l466 466q10 10 23 10t23 -10l466 -466q10 -10 10 -23z" /> -<glyph unicode="" horiz-adv-x="1152" d="M1075 800q0 -13 -10 -23l-466 -466q-10 -10 -23 -10t-23 10l-466 466q-10 10 -10 23t10 23l50 50q10 10 23 10t23 -10l393 -393l393 393q10 10 23 10t23 -10l50 -50q10 -10 10 -23z" /> -<glyph unicode="" horiz-adv-x="1920" d="M1792 544v832q0 13 -9.5 22.5t-22.5 9.5h-1600q-13 0 -22.5 -9.5t-9.5 -22.5v-832q0 -13 9.5 -22.5t22.5 -9.5h1600q13 0 22.5 9.5t9.5 22.5zM1920 1376v-1088q0 -66 -47 -113t-113 -47h-544q0 -37 16 -77.5t32 -71t16 -43.5q0 -26 -19 -45t-45 -19h-512q-26 0 -45 19 t-19 45q0 14 16 44t32 70t16 78h-544q-66 0 -113 47t-47 113v1088q0 66 47 113t113 47h1600q66 0 113 -47t47 -113z" /> -<glyph unicode="" horiz-adv-x="1920" d="M416 256q-66 0 -113 47t-47 113v704q0 66 47 113t113 47h1088q66 0 113 -47t47 -113v-704q0 -66 -47 -113t-113 -47h-1088zM384 1120v-704q0 -13 9.5 -22.5t22.5 -9.5h1088q13 0 22.5 9.5t9.5 22.5v704q0 13 -9.5 22.5t-22.5 9.5h-1088q-13 0 -22.5 -9.5t-9.5 -22.5z M1760 192h160v-96q0 -40 -47 -68t-113 -28h-1600q-66 0 -113 28t-47 68v96h160h1600zM1040 96q16 0 16 16t-16 16h-160q-16 0 -16 -16t16 -16h160z" /> -<glyph unicode="" horiz-adv-x="1152" d="M640 128q0 26 -19 45t-45 19t-45 -19t-19 -45t19 -45t45 -19t45 19t19 45zM1024 288v960q0 13 -9.5 22.5t-22.5 9.5h-832q-13 0 -22.5 -9.5t-9.5 -22.5v-960q0 -13 9.5 -22.5t22.5 -9.5h832q13 0 22.5 9.5t9.5 22.5zM1152 1248v-1088q0 -66 -47 -113t-113 -47h-832 q-66 0 -113 47t-47 113v1088q0 66 47 113t113 47h832q66 0 113 -47t47 -113z" /> -<glyph unicode="" horiz-adv-x="768" d="M464 128q0 33 -23.5 56.5t-56.5 23.5t-56.5 -23.5t-23.5 -56.5t23.5 -56.5t56.5 -23.5t56.5 23.5t23.5 56.5zM672 288v704q0 13 -9.5 22.5t-22.5 9.5h-512q-13 0 -22.5 -9.5t-9.5 -22.5v-704q0 -13 9.5 -22.5t22.5 -9.5h512q13 0 22.5 9.5t9.5 22.5zM480 1136 q0 16 -16 16h-160q-16 0 -16 -16t16 -16h160q16 0 16 16zM768 1152v-1024q0 -52 -38 -90t-90 -38h-512q-52 0 -90 38t-38 90v1024q0 52 38 90t90 38h512q52 0 90 -38t38 -90z" /> -<glyph unicode="" d="M768 1184q-148 0 -273 -73t-198 -198t-73 -273t73 -273t198 -198t273 -73t273 73t198 198t73 273t-73 273t-198 198t-273 73zM1536 640q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103t385.5 -103 t279.5 -279.5t103 -385.5z" /> -<glyph unicode="" horiz-adv-x="1664" d="M768 576v-384q0 -80 -56 -136t-136 -56h-384q-80 0 -136 56t-56 136v704q0 104 40.5 198.5t109.5 163.5t163.5 109.5t198.5 40.5h64q26 0 45 -19t19 -45v-128q0 -26 -19 -45t-45 -19h-64q-106 0 -181 -75t-75 -181v-32q0 -40 28 -68t68 -28h224q80 0 136 -56t56 -136z M1664 576v-384q0 -80 -56 -136t-136 -56h-384q-80 0 -136 56t-56 136v704q0 104 40.5 198.5t109.5 163.5t163.5 109.5t198.5 40.5h64q26 0 45 -19t19 -45v-128q0 -26 -19 -45t-45 -19h-64q-106 0 -181 -75t-75 -181v-32q0 -40 28 -68t68 -28h224q80 0 136 -56t56 -136z" /> -<glyph unicode="" horiz-adv-x="1664" d="M768 1216v-704q0 -104 -40.5 -198.5t-109.5 -163.5t-163.5 -109.5t-198.5 -40.5h-64q-26 0 -45 19t-19 45v128q0 26 19 45t45 19h64q106 0 181 75t75 181v32q0 40 -28 68t-68 28h-224q-80 0 -136 56t-56 136v384q0 80 56 136t136 56h384q80 0 136 -56t56 -136zM1664 1216 v-704q0 -104 -40.5 -198.5t-109.5 -163.5t-163.5 -109.5t-198.5 -40.5h-64q-26 0 -45 19t-19 45v128q0 26 19 45t45 19h64q106 0 181 75t75 181v32q0 40 -28 68t-68 28h-224q-80 0 -136 56t-56 136v384q0 80 56 136t136 56h384q80 0 136 -56t56 -136z" /> -<glyph unicode="" horiz-adv-x="1792" d="M526 142q0 -53 -37.5 -90.5t-90.5 -37.5q-52 0 -90 38t-38 90q0 53 37.5 90.5t90.5 37.5t90.5 -37.5t37.5 -90.5zM1024 -64q0 -53 -37.5 -90.5t-90.5 -37.5t-90.5 37.5t-37.5 90.5t37.5 90.5t90.5 37.5t90.5 -37.5t37.5 -90.5zM320 640q0 -53 -37.5 -90.5t-90.5 -37.5 t-90.5 37.5t-37.5 90.5t37.5 90.5t90.5 37.5t90.5 -37.5t37.5 -90.5zM1522 142q0 -52 -38 -90t-90 -38q-53 0 -90.5 37.5t-37.5 90.5t37.5 90.5t90.5 37.5t90.5 -37.5t37.5 -90.5zM558 1138q0 -66 -47 -113t-113 -47t-113 47t-47 113t47 113t113 47t113 -47t47 -113z M1728 640q0 -53 -37.5 -90.5t-90.5 -37.5t-90.5 37.5t-37.5 90.5t37.5 90.5t90.5 37.5t90.5 -37.5t37.5 -90.5zM1088 1344q0 -80 -56 -136t-136 -56t-136 56t-56 136t56 136t136 56t136 -56t56 -136zM1618 1138q0 -93 -66 -158.5t-158 -65.5q-93 0 -158.5 65.5t-65.5 158.5 q0 92 65.5 158t158.5 66q92 0 158 -66t66 -158z" /> -<glyph unicode="" d="M1536 640q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5t103 -385.5z" /> -<glyph unicode="" horiz-adv-x="1792" d="M1792 416q0 -166 -127 -451q-3 -7 -10.5 -24t-13.5 -30t-13 -22q-12 -17 -28 -17q-15 0 -23.5 10t-8.5 25q0 9 2.5 26.5t2.5 23.5q5 68 5 123q0 101 -17.5 181t-48.5 138.5t-80 101t-105.5 69.5t-133 42.5t-154 21.5t-175.5 6h-224v-256q0 -26 -19 -45t-45 -19t-45 19 l-512 512q-19 19 -19 45t19 45l512 512q19 19 45 19t45 -19t19 -45v-256h224q713 0 875 -403q53 -134 53 -333z" /> -<glyph unicode="" horiz-adv-x="1664" d="M640 320q0 -40 -12.5 -82t-43 -76t-72.5 -34t-72.5 34t-43 76t-12.5 82t12.5 82t43 76t72.5 34t72.5 -34t43 -76t12.5 -82zM1280 320q0 -40 -12.5 -82t-43 -76t-72.5 -34t-72.5 34t-43 76t-12.5 82t12.5 82t43 76t72.5 34t72.5 -34t43 -76t12.5 -82zM1440 320 q0 120 -69 204t-187 84q-41 0 -195 -21q-71 -11 -157 -11t-157 11q-152 21 -195 21q-118 0 -187 -84t-69 -204q0 -88 32 -153.5t81 -103t122 -60t140 -29.5t149 -7h168q82 0 149 7t140 29.5t122 60t81 103t32 153.5zM1664 496q0 -207 -61 -331q-38 -77 -105.5 -133t-141 -86 t-170 -47.5t-171.5 -22t-167 -4.5q-78 0 -142 3t-147.5 12.5t-152.5 30t-137 51.5t-121 81t-86 115q-62 123 -62 331q0 237 136 396q-27 82 -27 170q0 116 51 218q108 0 190 -39.5t189 -123.5q147 35 309 35q148 0 280 -32q105 82 187 121t189 39q51 -102 51 -218 q0 -87 -27 -168q136 -160 136 -398z" /> -<glyph unicode="" horiz-adv-x="1664" d="M1536 224v704q0 40 -28 68t-68 28h-704q-40 0 -68 28t-28 68v64q0 40 -28 68t-68 28h-320q-40 0 -68 -28t-28 -68v-960q0 -40 28 -68t68 -28h1216q40 0 68 28t28 68zM1664 928v-704q0 -92 -66 -158t-158 -66h-1216q-92 0 -158 66t-66 158v960q0 92 66 158t158 66h320 q92 0 158 -66t66 -158v-32h672q92 0 158 -66t66 -158z" /> -<glyph unicode="" horiz-adv-x="1920" d="M1781 605q0 35 -53 35h-1088q-40 0 -85.5 -21.5t-71.5 -52.5l-294 -363q-18 -24 -18 -40q0 -35 53 -35h1088q40 0 86 22t71 53l294 363q18 22 18 39zM640 768h768v160q0 40 -28 68t-68 28h-576q-40 0 -68 28t-28 68v64q0 40 -28 68t-68 28h-320q-40 0 -68 -28t-28 -68 v-853l256 315q44 53 116 87.5t140 34.5zM1909 605q0 -62 -46 -120l-295 -363q-43 -53 -116 -87.5t-140 -34.5h-1088q-92 0 -158 66t-66 158v960q0 92 66 158t158 66h320q92 0 158 -66t66 -158v-32h544q92 0 158 -66t66 -158v-160h192q54 0 99 -24.5t67 -70.5q15 -32 15 -68z " /> -<glyph unicode="" horiz-adv-x="1792" /> -<glyph unicode="" horiz-adv-x="1792" /> -<glyph unicode="" d="M1134 461q-37 -121 -138 -195t-228 -74t-228 74t-138 195q-8 25 4 48.5t38 31.5q25 8 48.5 -4t31.5 -38q25 -80 92.5 -129.5t151.5 -49.5t151.5 49.5t92.5 129.5q8 26 32 38t49 4t37 -31.5t4 -48.5zM640 896q0 -53 -37.5 -90.5t-90.5 -37.5t-90.5 37.5t-37.5 90.5 t37.5 90.5t90.5 37.5t90.5 -37.5t37.5 -90.5zM1152 896q0 -53 -37.5 -90.5t-90.5 -37.5t-90.5 37.5t-37.5 90.5t37.5 90.5t90.5 37.5t90.5 -37.5t37.5 -90.5zM1408 640q0 130 -51 248.5t-136.5 204t-204 136.5t-248.5 51t-248.5 -51t-204 -136.5t-136.5 -204t-51 -248.5 t51 -248.5t136.5 -204t204 -136.5t248.5 -51t248.5 51t204 136.5t136.5 204t51 248.5zM1536 640q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5t103 -385.5z" /> -<glyph unicode="" d="M1134 307q8 -25 -4 -48.5t-37 -31.5t-49 4t-32 38q-25 80 -92.5 129.5t-151.5 49.5t-151.5 -49.5t-92.5 -129.5q-8 -26 -31.5 -38t-48.5 -4q-26 8 -38 31.5t-4 48.5q37 121 138 195t228 74t228 -74t138 -195zM640 896q0 -53 -37.5 -90.5t-90.5 -37.5t-90.5 37.5 t-37.5 90.5t37.5 90.5t90.5 37.5t90.5 -37.5t37.5 -90.5zM1152 896q0 -53 -37.5 -90.5t-90.5 -37.5t-90.5 37.5t-37.5 90.5t37.5 90.5t90.5 37.5t90.5 -37.5t37.5 -90.5zM1408 640q0 130 -51 248.5t-136.5 204t-204 136.5t-248.5 51t-248.5 -51t-204 -136.5t-136.5 -204 t-51 -248.5t51 -248.5t136.5 -204t204 -136.5t248.5 -51t248.5 51t204 136.5t136.5 204t51 248.5zM1536 640q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5t103 -385.5z" /> -<glyph unicode="" d="M1152 448q0 -26 -19 -45t-45 -19h-640q-26 0 -45 19t-19 45t19 45t45 19h640q26 0 45 -19t19 -45zM640 896q0 -53 -37.5 -90.5t-90.5 -37.5t-90.5 37.5t-37.5 90.5t37.5 90.5t90.5 37.5t90.5 -37.5t37.5 -90.5zM1152 896q0 -53 -37.5 -90.5t-90.5 -37.5t-90.5 37.5 t-37.5 90.5t37.5 90.5t90.5 37.5t90.5 -37.5t37.5 -90.5zM1408 640q0 130 -51 248.5t-136.5 204t-204 136.5t-248.5 51t-248.5 -51t-204 -136.5t-136.5 -204t-51 -248.5t51 -248.5t136.5 -204t204 -136.5t248.5 -51t248.5 51t204 136.5t136.5 204t51 248.5zM1536 640 q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5t103 -385.5z" /> -<glyph unicode="" horiz-adv-x="1920" d="M832 448v128q0 14 -9 23t-23 9h-192v192q0 14 -9 23t-23 9h-128q-14 0 -23 -9t-9 -23v-192h-192q-14 0 -23 -9t-9 -23v-128q0 -14 9 -23t23 -9h192v-192q0 -14 9 -23t23 -9h128q14 0 23 9t9 23v192h192q14 0 23 9t9 23zM1408 384q0 53 -37.5 90.5t-90.5 37.5t-90.5 -37.5 t-37.5 -90.5t37.5 -90.5t90.5 -37.5t90.5 37.5t37.5 90.5zM1664 640q0 53 -37.5 90.5t-90.5 37.5t-90.5 -37.5t-37.5 -90.5t37.5 -90.5t90.5 -37.5t90.5 37.5t37.5 90.5zM1920 512q0 -212 -150 -362t-362 -150q-192 0 -338 128h-220q-146 -128 -338 -128q-212 0 -362 150 t-150 362t150 362t362 150h896q212 0 362 -150t150 -362z" /> -<glyph unicode="" horiz-adv-x="1920" d="M384 368v-96q0 -16 -16 -16h-96q-16 0 -16 16v96q0 16 16 16h96q16 0 16 -16zM512 624v-96q0 -16 -16 -16h-224q-16 0 -16 16v96q0 16 16 16h224q16 0 16 -16zM384 880v-96q0 -16 -16 -16h-96q-16 0 -16 16v96q0 16 16 16h96q16 0 16 -16zM1408 368v-96q0 -16 -16 -16 h-864q-16 0 -16 16v96q0 16 16 16h864q16 0 16 -16zM768 624v-96q0 -16 -16 -16h-96q-16 0 -16 16v96q0 16 16 16h96q16 0 16 -16zM640 880v-96q0 -16 -16 -16h-96q-16 0 -16 16v96q0 16 16 16h96q16 0 16 -16zM1024 624v-96q0 -16 -16 -16h-96q-16 0 -16 16v96q0 16 16 16 h96q16 0 16 -16zM896 880v-96q0 -16 -16 -16h-96q-16 0 -16 16v96q0 16 16 16h96q16 0 16 -16zM1280 624v-96q0 -16 -16 -16h-96q-16 0 -16 16v96q0 16 16 16h96q16 0 16 -16zM1664 368v-96q0 -16 -16 -16h-96q-16 0 -16 16v96q0 16 16 16h96q16 0 16 -16zM1152 880v-96 q0 -16 -16 -16h-96q-16 0 -16 16v96q0 16 16 16h96q16 0 16 -16zM1408 880v-96q0 -16 -16 -16h-96q-16 0 -16 16v96q0 16 16 16h96q16 0 16 -16zM1664 880v-352q0 -16 -16 -16h-224q-16 0 -16 16v96q0 16 16 16h112v240q0 16 16 16h96q16 0 16 -16zM1792 128v896h-1664v-896 h1664zM1920 1024v-896q0 -53 -37.5 -90.5t-90.5 -37.5h-1664q-53 0 -90.5 37.5t-37.5 90.5v896q0 53 37.5 90.5t90.5 37.5h1664q53 0 90.5 -37.5t37.5 -90.5z" /> -<glyph unicode="" horiz-adv-x="1792" d="M1664 491v616q-169 -91 -306 -91q-82 0 -145 32q-100 49 -184 76.5t-178 27.5q-173 0 -403 -127v-599q245 113 433 113q55 0 103.5 -7.5t98 -26t77 -31t82.5 -39.5l28 -14q44 -22 101 -22q120 0 293 92zM320 1280q0 -35 -17.5 -64t-46.5 -46v-1266q0 -14 -9 -23t-23 -9 h-64q-14 0 -23 9t-9 23v1266q-29 17 -46.5 46t-17.5 64q0 53 37.5 90.5t90.5 37.5t90.5 -37.5t37.5 -90.5zM1792 1216v-763q0 -39 -35 -57q-10 -5 -17 -9q-218 -116 -369 -116q-88 0 -158 35l-28 14q-64 33 -99 48t-91 29t-114 14q-102 0 -235.5 -44t-228.5 -102 q-15 -9 -33 -9q-16 0 -32 8q-32 19 -32 56v742q0 35 31 55q35 21 78.5 42.5t114 52t152.5 49.5t155 19q112 0 209 -31t209 -86q38 -19 89 -19q122 0 310 112q22 12 31 17q31 16 62 -2q31 -20 31 -55z" /> -<glyph unicode="" horiz-adv-x="1792" d="M832 536v192q-181 -16 -384 -117v-185q205 96 384 110zM832 954v197q-172 -8 -384 -126v-189q215 111 384 118zM1664 491v184q-235 -116 -384 -71v224q-20 6 -39 15q-5 3 -33 17t-34.5 17t-31.5 15t-34.5 15.5t-32.5 13t-36 12.5t-35 8.5t-39.5 7.5t-39.5 4t-44 2 q-23 0 -49 -3v-222h19q102 0 192.5 -29t197.5 -82q19 -9 39 -15v-188q42 -17 91 -17q120 0 293 92zM1664 918v189q-169 -91 -306 -91q-45 0 -78 8v-196q148 -42 384 90zM320 1280q0 -35 -17.5 -64t-46.5 -46v-1266q0 -14 -9 -23t-23 -9h-64q-14 0 -23 9t-9 23v1266 q-29 17 -46.5 46t-17.5 64q0 53 37.5 90.5t90.5 37.5t90.5 -37.5t37.5 -90.5zM1792 1216v-763q0 -39 -35 -57q-10 -5 -17 -9q-218 -116 -369 -116q-88 0 -158 35l-28 14q-64 33 -99 48t-91 29t-114 14q-102 0 -235.5 -44t-228.5 -102q-15 -9 -33 -9q-16 0 -32 8 q-32 19 -32 56v742q0 35 31 55q35 21 78.5 42.5t114 52t152.5 49.5t155 19q112 0 209 -31t209 -86q38 -19 89 -19q122 0 310 112q22 12 31 17q31 16 62 -2q31 -20 31 -55z" /> -<glyph unicode="" horiz-adv-x="1664" d="M585 553l-466 -466q-10 -10 -23 -10t-23 10l-50 50q-10 10 -10 23t10 23l393 393l-393 393q-10 10 -10 23t10 23l50 50q10 10 23 10t23 -10l466 -466q10 -10 10 -23t-10 -23zM1664 96v-64q0 -14 -9 -23t-23 -9h-960q-14 0 -23 9t-9 23v64q0 14 9 23t23 9h960q14 0 23 -9 t9 -23z" /> -<glyph unicode="" horiz-adv-x="1920" d="M617 137l-50 -50q-10 -10 -23 -10t-23 10l-466 466q-10 10 -10 23t10 23l466 466q10 10 23 10t23 -10l50 -50q10 -10 10 -23t-10 -23l-393 -393l393 -393q10 -10 10 -23t-10 -23zM1208 1204l-373 -1291q-4 -13 -15.5 -19.5t-23.5 -2.5l-62 17q-13 4 -19.5 15.5t-2.5 24.5 l373 1291q4 13 15.5 19.5t23.5 2.5l62 -17q13 -4 19.5 -15.5t2.5 -24.5zM1865 553l-466 -466q-10 -10 -23 -10t-23 10l-50 50q-10 10 -10 23t10 23l393 393l-393 393q-10 10 -10 23t10 23l50 50q10 10 23 10t23 -10l466 -466q10 -10 10 -23t-10 -23z" /> -<glyph unicode="" horiz-adv-x="1792" d="M640 454v-70q0 -42 -39 -59q-13 -5 -25 -5q-27 0 -45 19l-512 512q-19 19 -19 45t19 45l512 512q29 31 70 14q39 -17 39 -59v-69l-397 -398q-19 -19 -19 -45t19 -45zM1792 416q0 -58 -17 -133.5t-38.5 -138t-48 -125t-40.5 -90.5l-20 -40q-8 -17 -28 -17q-6 0 -9 1 q-25 8 -23 34q43 400 -106 565q-64 71 -170.5 110.5t-267.5 52.5v-251q0 -42 -39 -59q-13 -5 -25 -5q-27 0 -45 19l-512 512q-19 19 -19 45t19 45l512 512q29 31 70 14q39 -17 39 -59v-262q411 -28 599 -221q169 -173 169 -509z" /> -<glyph unicode="" horiz-adv-x="1664" d="M1186 579l257 250l-356 52l-66 10l-30 60l-159 322v-963l59 -31l318 -168l-60 355l-12 66zM1638 841l-363 -354l86 -500q5 -33 -6 -51.5t-34 -18.5q-17 0 -40 12l-449 236l-449 -236q-23 -12 -40 -12q-23 0 -34 18.5t-6 51.5l86 500l-364 354q-32 32 -23 59.5t54 34.5 l502 73l225 455q20 41 49 41q28 0 49 -41l225 -455l502 -73q45 -7 54 -34.5t-24 -59.5z" /> -<glyph unicode="" horiz-adv-x="1408" d="M1401 1187l-640 -1280q-17 -35 -57 -35q-5 0 -15 2q-22 5 -35.5 22.5t-13.5 39.5v576h-576q-22 0 -39.5 13.5t-22.5 35.5t4 42t29 30l1280 640q13 7 29 7q27 0 45 -19q15 -14 18.5 -34.5t-6.5 -39.5z" /> -<glyph unicode="" horiz-adv-x="1664" d="M557 256h595v595zM512 301l595 595h-595v-595zM1664 224v-192q0 -14 -9 -23t-23 -9h-224v-224q0 -14 -9 -23t-23 -9h-192q-14 0 -23 9t-9 23v224h-864q-14 0 -23 9t-9 23v864h-224q-14 0 -23 9t-9 23v192q0 14 9 23t23 9h224v224q0 14 9 23t23 9h192q14 0 23 -9t9 -23 v-224h851l246 247q10 9 23 9t23 -9q9 -10 9 -23t-9 -23l-247 -246v-851h224q14 0 23 -9t9 -23z" /> -<glyph unicode="" horiz-adv-x="1024" d="M288 64q0 40 -28 68t-68 28t-68 -28t-28 -68t28 -68t68 -28t68 28t28 68zM288 1216q0 40 -28 68t-68 28t-68 -28t-28 -68t28 -68t68 -28t68 28t28 68zM928 1088q0 40 -28 68t-68 28t-68 -28t-28 -68t28 -68t68 -28t68 28t28 68zM1024 1088q0 -52 -26 -96.5t-70 -69.5 q-2 -287 -226 -414q-68 -38 -203 -81q-128 -40 -169.5 -71t-41.5 -100v-26q44 -25 70 -69.5t26 -96.5q0 -80 -56 -136t-136 -56t-136 56t-56 136q0 52 26 96.5t70 69.5v820q-44 25 -70 69.5t-26 96.5q0 80 56 136t136 56t136 -56t56 -136q0 -52 -26 -96.5t-70 -69.5v-497 q54 26 154 57q55 17 87.5 29.5t70.5 31t59 39.5t40.5 51t28 69.5t8.5 91.5q-44 25 -70 69.5t-26 96.5q0 80 56 136t136 56t136 -56t56 -136z" /> -<glyph unicode="" horiz-adv-x="1664" d="M439 265l-256 -256q-10 -9 -23 -9q-12 0 -23 9q-9 10 -9 23t9 23l256 256q10 9 23 9t23 -9q9 -10 9 -23t-9 -23zM608 224v-320q0 -14 -9 -23t-23 -9t-23 9t-9 23v320q0 14 9 23t23 9t23 -9t9 -23zM384 448q0 -14 -9 -23t-23 -9h-320q-14 0 -23 9t-9 23t9 23t23 9h320 q14 0 23 -9t9 -23zM1648 320q0 -120 -85 -203l-147 -146q-83 -83 -203 -83q-121 0 -204 85l-334 335q-21 21 -42 56l239 18l273 -274q27 -27 68 -27.5t68 26.5l147 146q28 28 28 67q0 40 -28 68l-274 275l18 239q35 -21 56 -42l336 -336q84 -86 84 -204zM1031 1044l-239 -18 l-273 274q-28 28 -68 28q-39 0 -68 -27l-147 -146q-28 -28 -28 -67q0 -40 28 -68l274 -274l-18 -240q-35 21 -56 42l-336 336q-84 86 -84 204q0 120 85 203l147 146q83 83 203 83q121 0 204 -85l334 -335q21 -21 42 -56zM1664 960q0 -14 -9 -23t-23 -9h-320q-14 0 -23 9 t-9 23t9 23t23 9h320q14 0 23 -9t9 -23zM1120 1504v-320q0 -14 -9 -23t-23 -9t-23 9t-9 23v320q0 14 9 23t23 9t23 -9t9 -23zM1527 1353l-256 -256q-11 -9 -23 -9t-23 9q-9 10 -9 23t9 23l256 256q10 9 23 9t23 -9q9 -10 9 -23t-9 -23z" /> -<glyph unicode="" horiz-adv-x="1024" d="M704 280v-240q0 -16 -12 -28t-28 -12h-240q-16 0 -28 12t-12 28v240q0 16 12 28t28 12h240q16 0 28 -12t12 -28zM1020 880q0 -54 -15.5 -101t-35 -76.5t-55 -59.5t-57.5 -43.5t-61 -35.5q-41 -23 -68.5 -65t-27.5 -67q0 -17 -12 -32.5t-28 -15.5h-240q-15 0 -25.5 18.5 t-10.5 37.5v45q0 83 65 156.5t143 108.5q59 27 84 56t25 76q0 42 -46.5 74t-107.5 32q-65 0 -108 -29q-35 -25 -107 -115q-13 -16 -31 -16q-12 0 -25 8l-164 125q-13 10 -15.5 25t5.5 28q160 266 464 266q80 0 161 -31t146 -83t106 -127.5t41 -158.5z" /> -<glyph unicode="" horiz-adv-x="640" d="M640 192v-128q0 -26 -19 -45t-45 -19h-512q-26 0 -45 19t-19 45v128q0 26 19 45t45 19h64v384h-64q-26 0 -45 19t-19 45v128q0 26 19 45t45 19h384q26 0 45 -19t19 -45v-576h64q26 0 45 -19t19 -45zM512 1344v-192q0 -26 -19 -45t-45 -19h-256q-26 0 -45 19t-19 45v192 q0 26 19 45t45 19h256q26 0 45 -19t19 -45z" /> -<glyph unicode="" horiz-adv-x="640" d="M512 288v-224q0 -26 -19 -45t-45 -19h-256q-26 0 -45 19t-19 45v224q0 26 19 45t45 19h256q26 0 45 -19t19 -45zM542 1344l-28 -768q-1 -26 -20.5 -45t-45.5 -19h-256q-26 0 -45.5 19t-20.5 45l-28 768q-1 26 17.5 45t44.5 19h320q26 0 44.5 -19t17.5 -45z" /> -<glyph unicode="" d="M897 167v-167h-248l-159 252l-24 42q-8 9 -11 21h-3l-9 -21q-10 -20 -25 -44l-155 -250h-258v167h128l197 291l-185 272h-137v168h276l139 -228q2 -4 23 -42q8 -9 11 -21h3q3 9 11 21l25 42l140 228h257v-168h-125l-184 -267l204 -296h109zM1534 846v-206h-514l-3 27 q-4 28 -4 46q0 64 26 117t65 86.5t84 65t84 54.5t65 54t26 64q0 38 -29.5 62.5t-70.5 24.5q-51 0 -97 -39q-14 -11 -36 -38l-105 92q26 37 63 66q83 65 188 65q110 0 178 -59.5t68 -158.5q0 -56 -24.5 -103t-62 -76.5t-81.5 -58.5t-82 -50.5t-65.5 -51.5t-30.5 -63h232v80 h126z" /> -<glyph unicode="" d="M897 167v-167h-248l-159 252l-24 42q-8 9 -11 21h-3l-9 -21q-10 -20 -25 -44l-155 -250h-258v167h128l197 291l-185 272h-137v168h276l139 -228q2 -4 23 -42q8 -9 11 -21h3q3 9 11 21l25 42l140 228h257v-168h-125l-184 -267l204 -296h109zM1536 -50v-206h-514l-4 27 q-3 45 -3 46q0 64 26 117t65 86.5t84 65t84 54.5t65 54t26 64q0 38 -29.5 62.5t-70.5 24.5q-51 0 -97 -39q-14 -11 -36 -38l-105 92q26 37 63 66q80 65 188 65q110 0 178 -59.5t68 -158.5q0 -66 -34.5 -118.5t-84 -86t-99.5 -62.5t-87 -63t-41 -73h232v80h126z" /> -<glyph unicode="" horiz-adv-x="1920" d="M896 128l336 384h-768l-336 -384h768zM1909 1205q15 -34 9.5 -71.5t-30.5 -65.5l-896 -1024q-38 -44 -96 -44h-768q-38 0 -69.5 20.5t-47.5 54.5q-15 34 -9.5 71.5t30.5 65.5l896 1024q38 44 96 44h768q38 0 69.5 -20.5t47.5 -54.5z" /> -<glyph unicode="" horiz-adv-x="1664" d="M1664 438q0 -81 -44.5 -135t-123.5 -54q-41 0 -77.5 17.5t-59 38t-56.5 38t-71 17.5q-110 0 -110 -124q0 -39 16 -115t15 -115v-5q-22 0 -33 -1q-34 -3 -97.5 -11.5t-115.5 -13.5t-98 -5q-61 0 -103 26.5t-42 83.5q0 37 17.5 71t38 56.5t38 59t17.5 77.5q0 79 -54 123.5 t-135 44.5q-84 0 -143 -45.5t-59 -127.5q0 -43 15 -83t33.5 -64.5t33.5 -53t15 -50.5q0 -45 -46 -89q-37 -35 -117 -35q-95 0 -245 24q-9 2 -27.5 4t-27.5 4l-13 2q-1 0 -3 1q-2 0 -2 1v1024q2 -1 17.5 -3.5t34 -5t21.5 -3.5q150 -24 245 -24q80 0 117 35q46 44 46 89 q0 22 -15 50.5t-33.5 53t-33.5 64.5t-15 83q0 82 59 127.5t144 45.5q80 0 134 -44.5t54 -123.5q0 -41 -17.5 -77.5t-38 -59t-38 -56.5t-17.5 -71q0 -57 42 -83.5t103 -26.5q64 0 180 15t163 17v-2q-1 -2 -3.5 -17.5t-5 -34t-3.5 -21.5q-24 -150 -24 -245q0 -80 35 -117 q44 -46 89 -46q22 0 50.5 15t53 33.5t64.5 33.5t83 15q82 0 127.5 -59t45.5 -143z" /> -<glyph unicode="" horiz-adv-x="1152" d="M1152 832v-128q0 -221 -147.5 -384.5t-364.5 -187.5v-132h256q26 0 45 -19t19 -45t-19 -45t-45 -19h-640q-26 0 -45 19t-19 45t19 45t45 19h256v132q-217 24 -364.5 187.5t-147.5 384.5v128q0 26 19 45t45 19t45 -19t19 -45v-128q0 -185 131.5 -316.5t316.5 -131.5 t316.5 131.5t131.5 316.5v128q0 26 19 45t45 19t45 -19t19 -45zM896 1216v-512q0 -132 -94 -226t-226 -94t-226 94t-94 226v512q0 132 94 226t226 94t226 -94t94 -226z" /> -<glyph unicode="" horiz-adv-x="1408" d="M271 591l-101 -101q-42 103 -42 214v128q0 26 19 45t45 19t45 -19t19 -45v-128q0 -53 15 -113zM1385 1193l-361 -361v-128q0 -132 -94 -226t-226 -94q-55 0 -109 19l-96 -96q97 -51 205 -51q185 0 316.5 131.5t131.5 316.5v128q0 26 19 45t45 19t45 -19t19 -45v-128 q0 -221 -147.5 -384.5t-364.5 -187.5v-132h256q26 0 45 -19t19 -45t-19 -45t-45 -19h-640q-26 0 -45 19t-19 45t19 45t45 19h256v132q-125 13 -235 81l-254 -254q-10 -10 -23 -10t-23 10l-82 82q-10 10 -10 23t10 23l1234 1234q10 10 23 10t23 -10l82 -82q10 -10 10 -23 t-10 -23zM1005 1325l-621 -621v512q0 132 94 226t226 94q102 0 184.5 -59t116.5 -152z" /> -<glyph unicode="" horiz-adv-x="1280" d="M1088 576v640h-448v-1137q119 63 213 137q235 184 235 360zM1280 1344v-768q0 -86 -33.5 -170.5t-83 -150t-118 -127.5t-126.5 -103t-121 -77.5t-89.5 -49.5t-42.5 -20q-12 -6 -26 -6t-26 6q-16 7 -42.5 20t-89.5 49.5t-121 77.5t-126.5 103t-118 127.5t-83 150 t-33.5 170.5v768q0 26 19 45t45 19h1152q26 0 45 -19t19 -45z" /> -<glyph unicode="" horiz-adv-x="1664" d="M128 -128h1408v1024h-1408v-1024zM512 1088v288q0 14 -9 23t-23 9h-64q-14 0 -23 -9t-9 -23v-288q0 -14 9 -23t23 -9h64q14 0 23 9t9 23zM1280 1088v288q0 14 -9 23t-23 9h-64q-14 0 -23 -9t-9 -23v-288q0 -14 9 -23t23 -9h64q14 0 23 9t9 23zM1664 1152v-1280 q0 -52 -38 -90t-90 -38h-1408q-52 0 -90 38t-38 90v1280q0 52 38 90t90 38h128v96q0 66 47 113t113 47h64q66 0 113 -47t47 -113v-96h384v96q0 66 47 113t113 47h64q66 0 113 -47t47 -113v-96h128q52 0 90 -38t38 -90z" /> -<glyph unicode="" horiz-adv-x="1408" d="M512 1344q0 26 -19 45t-45 19t-45 -19t-19 -45t19 -45t45 -19t45 19t19 45zM1408 1376v-320q0 -16 -12 -25q-8 -7 -20 -7q-4 0 -7 1l-448 96q-11 2 -18 11t-7 20h-256v-102q111 -23 183.5 -111t72.5 -203v-800q0 -26 -19 -45t-45 -19h-512q-26 0 -45 19t-19 45v800 q0 106 62.5 190.5t161.5 114.5v111h-32q-59 0 -115 -23.5t-91.5 -53t-66 -66.5t-40.5 -53.5t-14 -24.5q-17 -35 -57 -35q-16 0 -29 7q-23 12 -31.5 37t3.5 49q5 10 14.5 26t37.5 53.5t60.5 70t85 67t108.5 52.5q-25 42 -25 86q0 66 47 113t113 47t113 -47t47 -113 q0 -33 -14 -64h302q0 11 7 20t18 11l448 96q3 1 7 1q12 0 20 -7q12 -9 12 -25z" /> -<glyph unicode="" horiz-adv-x="1664" d="M1440 1088q0 40 -28 68t-68 28t-68 -28t-28 -68t28 -68t68 -28t68 28t28 68zM1664 1376q0 -249 -75.5 -430.5t-253.5 -360.5q-81 -80 -195 -176l-20 -379q-2 -16 -16 -26l-384 -224q-7 -4 -16 -4q-12 0 -23 9l-64 64q-13 14 -8 32l85 276l-281 281l-276 -85q-3 -1 -9 -1 q-14 0 -23 9l-64 64q-17 19 -5 39l224 384q10 14 26 16l379 20q96 114 176 195q188 187 358 258t431 71q14 0 24 -9.5t10 -22.5z" /> -<glyph unicode="" horiz-adv-x="1792" d="M1745 763l-164 -763h-334l178 832q13 56 -15 88q-27 33 -83 33h-169l-204 -953h-334l204 953h-286l-204 -953h-334l204 953l-153 327h1276q101 0 189.5 -40.5t147.5 -113.5q60 -73 81 -168.5t0 -194.5z" /> -<glyph unicode="" d="M909 141l102 102q19 19 19 45t-19 45l-307 307l307 307q19 19 19 45t-19 45l-102 102q-19 19 -45 19t-45 -19l-454 -454q-19 -19 -19 -45t19 -45l454 -454q19 -19 45 -19t45 19zM1536 640q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5 t-103 385.5t103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5t103 -385.5z" /> -<glyph unicode="" d="M717 141l454 454q19 19 19 45t-19 45l-454 454q-19 19 -45 19t-45 -19l-102 -102q-19 -19 -19 -45t19 -45l307 -307l-307 -307q-19 -19 -19 -45t19 -45l102 -102q19 -19 45 -19t45 19zM1536 640q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5 t-103 385.5t103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5t103 -385.5z" /> -<glyph unicode="" d="M1165 397l102 102q19 19 19 45t-19 45l-454 454q-19 19 -45 19t-45 -19l-454 -454q-19 -19 -19 -45t19 -45l102 -102q19 -19 45 -19t45 19l307 307l307 -307q19 -19 45 -19t45 19zM1536 640q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5 t-103 385.5t103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5t103 -385.5z" /> -<glyph unicode="" d="M813 237l454 454q19 19 19 45t-19 45l-102 102q-19 19 -45 19t-45 -19l-307 -307l-307 307q-19 19 -45 19t-45 -19l-102 -102q-19 -19 -19 -45t19 -45l454 -454q19 -19 45 -19t45 19zM1536 640q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5 t-103 385.5t103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5t103 -385.5z" /> -<glyph unicode="" horiz-adv-x="1408" d="M1130 939l16 175h-884l47 -534h612l-22 -228l-197 -53l-196 53l-13 140h-175l22 -278l362 -100h4v1l359 99l50 544h-644l-15 181h674zM0 1408h1408l-128 -1438l-578 -162l-574 162z" /> -<glyph unicode="" horiz-adv-x="1792" d="M275 1408h1505l-266 -1333l-804 -267l-698 267l71 356h297l-29 -147l422 -161l486 161l68 339h-1208l58 297h1209l38 191h-1208z" /> -<glyph unicode="" horiz-adv-x="1792" d="M960 1280q0 26 -19 45t-45 19t-45 -19t-19 -45t19 -45t45 -19t45 19t19 45zM1792 352v-352q0 -22 -20 -30q-8 -2 -12 -2q-13 0 -23 9l-93 93q-119 -143 -318.5 -226.5t-429.5 -83.5t-429.5 83.5t-318.5 226.5l-93 -93q-9 -9 -23 -9q-4 0 -12 2q-20 8 -20 30v352 q0 14 9 23t23 9h352q22 0 30 -20q8 -19 -7 -35l-100 -100q67 -91 189.5 -153.5t271.5 -82.5v647h-192q-26 0 -45 19t-19 45v128q0 26 19 45t45 19h192v163q-58 34 -93 92.5t-35 128.5q0 106 75 181t181 75t181 -75t75 -181q0 -70 -35 -128.5t-93 -92.5v-163h192q26 0 45 -19 t19 -45v-128q0 -26 -19 -45t-45 -19h-192v-647q149 20 271.5 82.5t189.5 153.5l-100 100q-15 16 -7 35q8 20 30 20h352q14 0 23 -9t9 -23z" /> -<glyph unicode="" horiz-adv-x="1152" d="M1056 768q40 0 68 -28t28 -68v-576q0 -40 -28 -68t-68 -28h-960q-40 0 -68 28t-28 68v576q0 40 28 68t68 28h32v320q0 185 131.5 316.5t316.5 131.5t316.5 -131.5t131.5 -316.5q0 -26 -19 -45t-45 -19h-64q-26 0 -45 19t-19 45q0 106 -75 181t-181 75t-181 -75t-75 -181 v-320h736z" /> -<glyph unicode="" d="M1024 640q0 -106 -75 -181t-181 -75t-181 75t-75 181t75 181t181 75t181 -75t75 -181zM1152 640q0 159 -112.5 271.5t-271.5 112.5t-271.5 -112.5t-112.5 -271.5t112.5 -271.5t271.5 -112.5t271.5 112.5t112.5 271.5zM1280 640q0 -212 -150 -362t-362 -150t-362 150 t-150 362t150 362t362 150t362 -150t150 -362zM1408 640q0 130 -51 248.5t-136.5 204t-204 136.5t-248.5 51t-248.5 -51t-204 -136.5t-136.5 -204t-51 -248.5t51 -248.5t136.5 -204t204 -136.5t248.5 -51t248.5 51t204 136.5t136.5 204t51 248.5zM1536 640 q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5t103 -385.5z" /> -<glyph unicode="" horiz-adv-x="1408" d="M384 800v-192q0 -40 -28 -68t-68 -28h-192q-40 0 -68 28t-28 68v192q0 40 28 68t68 28h192q40 0 68 -28t28 -68zM896 800v-192q0 -40 -28 -68t-68 -28h-192q-40 0 -68 28t-28 68v192q0 40 28 68t68 28h192q40 0 68 -28t28 -68zM1408 800v-192q0 -40 -28 -68t-68 -28h-192 q-40 0 -68 28t-28 68v192q0 40 28 68t68 28h192q40 0 68 -28t28 -68z" /> -<glyph unicode="" horiz-adv-x="384" d="M384 288v-192q0 -40 -28 -68t-68 -28h-192q-40 0 -68 28t-28 68v192q0 40 28 68t68 28h192q40 0 68 -28t28 -68zM384 800v-192q0 -40 -28 -68t-68 -28h-192q-40 0 -68 28t-28 68v192q0 40 28 68t68 28h192q40 0 68 -28t28 -68zM384 1312v-192q0 -40 -28 -68t-68 -28h-192 q-40 0 -68 28t-28 68v192q0 40 28 68t68 28h192q40 0 68 -28t28 -68z" /> -<glyph unicode="" d="M512 256q0 53 -37.5 90.5t-90.5 37.5t-90.5 -37.5t-37.5 -90.5t37.5 -90.5t90.5 -37.5t90.5 37.5t37.5 90.5zM863 162q-13 232 -177 396t-396 177q-14 1 -24 -9t-10 -23v-128q0 -13 8.5 -22t21.5 -10q154 -11 264 -121t121 -264q1 -13 10 -21.5t22 -8.5h128q13 0 23 10 t9 24zM1247 161q-5 154 -56 297.5t-139.5 260t-205 205t-260 139.5t-297.5 56q-14 1 -23 -9q-10 -10 -10 -23v-128q0 -13 9 -22t22 -10q204 -7 378 -111.5t278.5 -278.5t111.5 -378q1 -13 10 -22t22 -9h128q13 0 23 10q11 9 9 23zM1536 1120v-960q0 -119 -84.5 -203.5 t-203.5 -84.5h-960q-119 0 -203.5 84.5t-84.5 203.5v960q0 119 84.5 203.5t203.5 84.5h960q119 0 203.5 -84.5t84.5 -203.5z" /> -<glyph unicode="" d="M768 1408q209 0 385.5 -103t279.5 -279.5t103 -385.5t-103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103zM1152 585q32 18 32 55t-32 55l-544 320q-31 19 -64 1q-32 -19 -32 -56v-640q0 -37 32 -56 q16 -8 32 -8q17 0 32 9z" /> -<glyph unicode="" horiz-adv-x="1792" d="M1024 1084l316 -316l-572 -572l-316 316zM813 105l618 618q19 19 19 45t-19 45l-362 362q-18 18 -45 18t-45 -18l-618 -618q-19 -19 -19 -45t19 -45l362 -362q18 -18 45 -18t45 18zM1702 742l-907 -908q-37 -37 -90.5 -37t-90.5 37l-126 126q56 56 56 136t-56 136 t-136 56t-136 -56l-125 126q-37 37 -37 90.5t37 90.5l907 906q37 37 90.5 37t90.5 -37l125 -125q-56 -56 -56 -136t56 -136t136 -56t136 56l126 -125q37 -37 37 -90.5t-37 -90.5z" /> -<glyph unicode="" d="M1280 576v128q0 26 -19 45t-45 19h-896q-26 0 -45 -19t-19 -45v-128q0 -26 19 -45t45 -19h896q26 0 45 19t19 45zM1536 1120v-960q0 -119 -84.5 -203.5t-203.5 -84.5h-960q-119 0 -203.5 84.5t-84.5 203.5v960q0 119 84.5 203.5t203.5 84.5h960q119 0 203.5 -84.5 t84.5 -203.5z" /> -<glyph unicode="" horiz-adv-x="1408" d="M1152 736v-64q0 -14 -9 -23t-23 -9h-832q-14 0 -23 9t-9 23v64q0 14 9 23t23 9h832q14 0 23 -9t9 -23zM1280 288v832q0 66 -47 113t-113 47h-832q-66 0 -113 -47t-47 -113v-832q0 -66 47 -113t113 -47h832q66 0 113 47t47 113zM1408 1120v-832q0 -119 -84.5 -203.5 t-203.5 -84.5h-832q-119 0 -203.5 84.5t-84.5 203.5v832q0 119 84.5 203.5t203.5 84.5h832q119 0 203.5 -84.5t84.5 -203.5z" /> -<glyph unicode="" horiz-adv-x="1024" d="M1018 933q-18 -37 -58 -37h-192v-864q0 -14 -9 -23t-23 -9h-704q-21 0 -29 18q-8 20 4 35l160 192q9 11 25 11h320v640h-192q-40 0 -58 37q-17 37 9 68l320 384q18 22 49 22t49 -22l320 -384q27 -32 9 -68z" /> -<glyph unicode="" horiz-adv-x="1024" d="M32 1280h704q13 0 22.5 -9.5t9.5 -23.5v-863h192q40 0 58 -37t-9 -69l-320 -384q-18 -22 -49 -22t-49 22l-320 384q-26 31 -9 69q18 37 58 37h192v640h-320q-14 0 -25 11l-160 192q-13 14 -4 34q9 19 29 19z" /> -<glyph unicode="" d="M685 237l614 614q19 19 19 45t-19 45l-102 102q-19 19 -45 19t-45 -19l-467 -467l-211 211q-19 19 -45 19t-45 -19l-102 -102q-19 -19 -19 -45t19 -45l358 -358q19 -19 45 -19t45 19zM1536 1120v-960q0 -119 -84.5 -203.5t-203.5 -84.5h-960q-119 0 -203.5 84.5 t-84.5 203.5v960q0 119 84.5 203.5t203.5 84.5h960q119 0 203.5 -84.5t84.5 -203.5z" /> -<glyph unicode="" d="M404 428l152 -152l-52 -52h-56v96h-96v56zM818 818q14 -13 -3 -30l-291 -291q-17 -17 -30 -3q-14 13 3 30l291 291q17 17 30 3zM544 128l544 544l-288 288l-544 -544v-288h288zM1152 736l92 92q28 28 28 68t-28 68l-152 152q-28 28 -68 28t-68 -28l-92 -92zM1536 1120 v-960q0 -119 -84.5 -203.5t-203.5 -84.5h-960q-119 0 -203.5 84.5t-84.5 203.5v960q0 119 84.5 203.5t203.5 84.5h960q119 0 203.5 -84.5t84.5 -203.5z" /> -<glyph unicode="" d="M1280 608v480q0 26 -19 45t-45 19h-480q-42 0 -59 -39q-17 -41 14 -70l144 -144l-534 -534q-19 -19 -19 -45t19 -45l102 -102q19 -19 45 -19t45 19l534 534l144 -144q18 -19 45 -19q12 0 25 5q39 17 39 59zM1536 1120v-960q0 -119 -84.5 -203.5t-203.5 -84.5h-960 q-119 0 -203.5 84.5t-84.5 203.5v960q0 119 84.5 203.5t203.5 84.5h960q119 0 203.5 -84.5t84.5 -203.5z" /> -<glyph unicode="" d="M1005 435l352 352q19 19 19 45t-19 45l-352 352q-30 31 -69 14q-40 -17 -40 -59v-160q-119 0 -216 -19.5t-162.5 -51t-114 -79t-76.5 -95.5t-44.5 -109t-21.5 -111.5t-5 -110.5q0 -181 167 -404q10 -12 25 -12q7 0 13 3q22 9 19 33q-44 354 62 473q46 52 130 75.5 t224 23.5v-160q0 -42 40 -59q12 -5 24 -5q26 0 45 19zM1536 1120v-960q0 -119 -84.5 -203.5t-203.5 -84.5h-960q-119 0 -203.5 84.5t-84.5 203.5v960q0 119 84.5 203.5t203.5 84.5h960q119 0 203.5 -84.5t84.5 -203.5z" /> -<glyph unicode="" d="M640 448l256 128l-256 128v-256zM1024 1039v-542l-512 -256v542zM1312 640q0 148 -73 273t-198 198t-273 73t-273 -73t-198 -198t-73 -273t73 -273t198 -198t273 -73t273 73t198 198t73 273zM1536 640q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103 t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5t103 -385.5z" /> -<glyph unicode="" d="M1145 861q18 -35 -5 -66l-320 -448q-19 -27 -52 -27t-52 27l-320 448q-23 31 -5 66q17 35 57 35h640q40 0 57 -35zM1280 160v960q0 13 -9.5 22.5t-22.5 9.5h-960q-13 0 -22.5 -9.5t-9.5 -22.5v-960q0 -13 9.5 -22.5t22.5 -9.5h960q13 0 22.5 9.5t9.5 22.5zM1536 1120 v-960q0 -119 -84.5 -203.5t-203.5 -84.5h-960q-119 0 -203.5 84.5t-84.5 203.5v960q0 119 84.5 203.5t203.5 84.5h960q119 0 203.5 -84.5t84.5 -203.5z" /> -<glyph unicode="" d="M1145 419q-17 -35 -57 -35h-640q-40 0 -57 35q-18 35 5 66l320 448q19 27 52 27t52 -27l320 -448q23 -31 5 -66zM1280 160v960q0 13 -9.5 22.5t-22.5 9.5h-960q-13 0 -22.5 -9.5t-9.5 -22.5v-960q0 -13 9.5 -22.5t22.5 -9.5h960q13 0 22.5 9.5t9.5 22.5zM1536 1120v-960 q0 -119 -84.5 -203.5t-203.5 -84.5h-960q-119 0 -203.5 84.5t-84.5 203.5v960q0 119 84.5 203.5t203.5 84.5h960q119 0 203.5 -84.5t84.5 -203.5z" /> -<glyph unicode="" d="M1088 640q0 -33 -27 -52l-448 -320q-31 -23 -66 -5q-35 17 -35 57v640q0 40 35 57q35 18 66 -5l448 -320q27 -19 27 -52zM1280 160v960q0 14 -9 23t-23 9h-960q-14 0 -23 -9t-9 -23v-960q0 -14 9 -23t23 -9h960q14 0 23 9t9 23zM1536 1120v-960q0 -119 -84.5 -203.5 t-203.5 -84.5h-960q-119 0 -203.5 84.5t-84.5 203.5v960q0 119 84.5 203.5t203.5 84.5h960q119 0 203.5 -84.5t84.5 -203.5z" /> -<glyph unicode="" horiz-adv-x="1024" d="M976 229l35 -159q3 -12 -3 -22.5t-17 -14.5l-5 -1q-4 -2 -10.5 -3.5t-16 -4.5t-21.5 -5.5t-25.5 -5t-30 -5t-33.5 -4.5t-36.5 -3t-38.5 -1q-234 0 -409 130.5t-238 351.5h-95q-13 0 -22.5 9.5t-9.5 22.5v113q0 13 9.5 22.5t22.5 9.5h66q-2 57 1 105h-67q-14 0 -23 9 t-9 23v114q0 14 9 23t23 9h98q67 210 243.5 338t400.5 128q102 0 194 -23q11 -3 20 -15q6 -11 3 -24l-43 -159q-3 -13 -14 -19.5t-24 -2.5l-4 1q-4 1 -11.5 2.5l-17.5 3.5t-22.5 3.5t-26 3t-29 2.5t-29.5 1q-126 0 -226 -64t-150 -176h468q16 0 25 -12q10 -12 7 -26 l-24 -114q-5 -26 -32 -26h-488q-3 -37 0 -105h459q15 0 25 -12q9 -12 6 -27l-24 -112q-2 -11 -11 -18.5t-20 -7.5h-387q48 -117 149.5 -185.5t228.5 -68.5q18 0 36 1.5t33.5 3.5t29.5 4.5t24.5 5t18.5 4.5l12 3l5 2q13 5 26 -2q12 -7 15 -21z" /> -<glyph unicode="" horiz-adv-x="1024" d="M1020 399v-367q0 -14 -9 -23t-23 -9h-956q-14 0 -23 9t-9 23v150q0 13 9.5 22.5t22.5 9.5h97v383h-95q-14 0 -23 9.5t-9 22.5v131q0 14 9 23t23 9h95v223q0 171 123.5 282t314.5 111q185 0 335 -125q9 -8 10 -20.5t-7 -22.5l-103 -127q-9 -11 -22 -12q-13 -2 -23 7 q-5 5 -26 19t-69 32t-93 18q-85 0 -137 -47t-52 -123v-215h305q13 0 22.5 -9t9.5 -23v-131q0 -13 -9.5 -22.5t-22.5 -9.5h-305v-379h414v181q0 13 9 22.5t23 9.5h162q14 0 23 -9.5t9 -22.5z" /> -<glyph unicode="" horiz-adv-x="1024" d="M978 351q0 -153 -99.5 -263.5t-258.5 -136.5v-175q0 -14 -9 -23t-23 -9h-135q-13 0 -22.5 9.5t-9.5 22.5v175q-66 9 -127.5 31t-101.5 44.5t-74 48t-46.5 37.5t-17.5 18q-17 21 -2 41l103 135q7 10 23 12q15 2 24 -9l2 -2q113 -99 243 -125q37 -8 74 -8q81 0 142.5 43 t61.5 122q0 28 -15 53t-33.5 42t-58.5 37.5t-66 32t-80 32.5q-39 16 -61.5 25t-61.5 26.5t-62.5 31t-56.5 35.5t-53.5 42.5t-43.5 49t-35.5 58t-21 66.5t-8.5 78q0 138 98 242t255 134v180q0 13 9.5 22.5t22.5 9.5h135q14 0 23 -9t9 -23v-176q57 -6 110.5 -23t87 -33.5 t63.5 -37.5t39 -29t15 -14q17 -18 5 -38l-81 -146q-8 -15 -23 -16q-14 -3 -27 7q-3 3 -14.5 12t-39 26.5t-58.5 32t-74.5 26t-85.5 11.5q-95 0 -155 -43t-60 -111q0 -26 8.5 -48t29.5 -41.5t39.5 -33t56 -31t60.5 -27t70 -27.5q53 -20 81 -31.5t76 -35t75.5 -42.5t62 -50 t53 -63.5t31.5 -76.5t13 -94z" /> -<glyph unicode="" horiz-adv-x="898" d="M898 1066v-102q0 -14 -9 -23t-23 -9h-168q-23 -144 -129 -234t-276 -110q167 -178 459 -536q14 -16 4 -34q-8 -18 -29 -18h-195q-16 0 -25 12q-306 367 -498 571q-9 9 -9 22v127q0 13 9.5 22.5t22.5 9.5h112q132 0 212.5 43t102.5 125h-427q-14 0 -23 9t-9 23v102 q0 14 9 23t23 9h413q-57 113 -268 113h-145q-13 0 -22.5 9.5t-9.5 22.5v133q0 14 9 23t23 9h832q14 0 23 -9t9 -23v-102q0 -14 -9 -23t-23 -9h-233q47 -61 64 -144h171q14 0 23 -9t9 -23z" /> -<glyph unicode="" horiz-adv-x="1027" d="M603 0h-172q-13 0 -22.5 9t-9.5 23v330h-288q-13 0 -22.5 9t-9.5 23v103q0 13 9.5 22.5t22.5 9.5h288v85h-288q-13 0 -22.5 9t-9.5 23v104q0 13 9.5 22.5t22.5 9.5h214l-321 578q-8 16 0 32q10 16 28 16h194q19 0 29 -18l215 -425q19 -38 56 -125q10 24 30.5 68t27.5 61 l191 420q8 19 29 19h191q17 0 27 -16q9 -14 1 -31l-313 -579h215q13 0 22.5 -9.5t9.5 -22.5v-104q0 -14 -9.5 -23t-22.5 -9h-290v-85h290q13 0 22.5 -9.5t9.5 -22.5v-103q0 -14 -9.5 -23t-22.5 -9h-290v-330q0 -13 -9.5 -22.5t-22.5 -9.5z" /> -<glyph unicode="" horiz-adv-x="1280" d="M1043 971q0 100 -65 162t-171 62h-320v-448h320q106 0 171 62t65 162zM1280 971q0 -193 -126.5 -315t-326.5 -122h-340v-118h505q14 0 23 -9t9 -23v-128q0 -14 -9 -23t-23 -9h-505v-192q0 -14 -9.5 -23t-22.5 -9h-167q-14 0 -23 9t-9 23v192h-224q-14 0 -23 9t-9 23v128 q0 14 9 23t23 9h224v118h-224q-14 0 -23 9t-9 23v149q0 13 9 22.5t23 9.5h224v629q0 14 9 23t23 9h539q200 0 326.5 -122t126.5 -315z" /> -<glyph unicode="" horiz-adv-x="1792" d="M514 341l81 299h-159l75 -300q1 -1 1 -3t1 -3q0 1 0.5 3.5t0.5 3.5zM630 768l35 128h-292l32 -128h225zM822 768h139l-35 128h-70zM1271 340l78 300h-162l81 -299q0 -1 0.5 -3.5t1.5 -3.5q0 1 0.5 3t0.5 3zM1382 768l33 128h-297l34 -128h230zM1792 736v-64q0 -14 -9 -23 t-23 -9h-213l-164 -616q-7 -24 -31 -24h-159q-24 0 -31 24l-166 616h-209l-167 -616q-7 -24 -31 -24h-159q-11 0 -19.5 7t-10.5 17l-160 616h-208q-14 0 -23 9t-9 23v64q0 14 9 23t23 9h175l-33 128h-142q-14 0 -23 9t-9 23v64q0 14 9 23t23 9h109l-89 344q-5 15 5 28 q10 12 26 12h137q26 0 31 -24l90 -360h359l97 360q7 24 31 24h126q24 0 31 -24l98 -360h365l93 360q5 24 31 24h137q16 0 26 -12q10 -13 5 -28l-91 -344h111q14 0 23 -9t9 -23v-64q0 -14 -9 -23t-23 -9h-145l-34 -128h179q14 0 23 -9t9 -23z" /> -<glyph unicode="" horiz-adv-x="1280" d="M1167 896q18 -182 -131 -258q117 -28 175 -103t45 -214q-7 -71 -32.5 -125t-64.5 -89t-97 -58.5t-121.5 -34.5t-145.5 -15v-255h-154v251q-80 0 -122 1v-252h-154v255q-18 0 -54 0.5t-55 0.5h-200l31 183h111q50 0 58 51v402h16q-6 1 -16 1v287q-13 68 -89 68h-111v164 l212 -1q64 0 97 1v252h154v-247q82 2 122 2v245h154v-252q79 -7 140 -22.5t113 -45t82.5 -78t36.5 -114.5zM952 351q0 36 -15 64t-37 46t-57.5 30.5t-65.5 18.5t-74 9t-69 3t-64.5 -1t-47.5 -1v-338q8 0 37 -0.5t48 -0.5t53 1.5t58.5 4t57 8.5t55.5 14t47.5 21t39.5 30 t24.5 40t9.5 51zM881 827q0 33 -12.5 58.5t-30.5 42t-48 28t-55 16.5t-61.5 8t-58 2.5t-54 -1t-39.5 -0.5v-307q5 0 34.5 -0.5t46.5 0t50 2t55 5.5t51.5 11t48.5 18.5t37 27t27 38.5t9 51z" /> -<glyph unicode="" d="M1024 1024v472q22 -14 36 -28l408 -408q14 -14 28 -36h-472zM896 992q0 -40 28 -68t68 -28h544v-1056q0 -40 -28 -68t-68 -28h-1344q-40 0 -68 28t-28 68v1600q0 40 28 68t68 28h800v-544z" /> -<glyph unicode="" d="M1468 1060q14 -14 28 -36h-472v472q22 -14 36 -28zM992 896h544v-1056q0 -40 -28 -68t-68 -28h-1344q-40 0 -68 28t-28 68v1600q0 40 28 68t68 28h800v-544q0 -40 28 -68t68 -28zM1152 160v64q0 14 -9 23t-23 9h-704q-14 0 -23 -9t-9 -23v-64q0 -14 9 -23t23 -9h704 q14 0 23 9t9 23zM1152 416v64q0 14 -9 23t-23 9h-704q-14 0 -23 -9t-9 -23v-64q0 -14 9 -23t23 -9h704q14 0 23 9t9 23zM1152 672v64q0 14 -9 23t-23 9h-704q-14 0 -23 -9t-9 -23v-64q0 -14 9 -23t23 -9h704q14 0 23 9t9 23z" /> -<glyph unicode="" horiz-adv-x="1664" d="M1191 1128h177l-72 218l-12 47q-2 16 -2 20h-4l-3 -20q0 -1 -3.5 -18t-7.5 -29zM736 96q0 -12 -10 -24l-319 -319q-10 -9 -23 -9q-12 0 -23 9l-320 320q-15 16 -7 35q8 20 30 20h192v1376q0 14 9 23t23 9h192q14 0 23 -9t9 -23v-1376h192q14 0 23 -9t9 -23zM1572 -23 v-233h-584v90l369 529q12 18 21 27l11 9v3q-2 0 -6.5 -0.5t-7.5 -0.5q-12 -3 -30 -3h-232v-115h-120v229h567v-89l-369 -530q-6 -8 -21 -26l-11 -11v-2l14 2q9 2 30 2h248v119h121zM1661 874v-106h-288v106h75l-47 144h-243l-47 -144h75v-106h-287v106h70l230 662h162 l230 -662h70z" /> -<glyph unicode="" horiz-adv-x="1664" d="M1191 104h177l-72 218l-12 47q-2 16 -2 20h-4l-3 -20q0 -1 -3.5 -18t-7.5 -29zM736 96q0 -12 -10 -24l-319 -319q-10 -9 -23 -9q-12 0 -23 9l-320 320q-15 16 -7 35q8 20 30 20h192v1376q0 14 9 23t23 9h192q14 0 23 -9t9 -23v-1376h192q14 0 23 -9t9 -23zM1661 -150 v-106h-288v106h75l-47 144h-243l-47 -144h75v-106h-287v106h70l230 662h162l230 -662h70zM1572 1001v-233h-584v90l369 529q12 18 21 27l11 9v3q-2 0 -6.5 -0.5t-7.5 -0.5q-12 -3 -30 -3h-232v-115h-120v229h567v-89l-369 -530q-6 -8 -21 -26l-11 -10v-3l14 3q9 1 30 1h248 v119h121z" /> -<glyph unicode="" horiz-adv-x="1792" d="M736 96q0 -12 -10 -24l-319 -319q-10 -9 -23 -9q-12 0 -23 9l-320 320q-15 16 -7 35q8 20 30 20h192v1376q0 14 9 23t23 9h192q14 0 23 -9t9 -23v-1376h192q14 0 23 -9t9 -23zM1792 -32v-192q0 -14 -9 -23t-23 -9h-832q-14 0 -23 9t-9 23v192q0 14 9 23t23 9h832 q14 0 23 -9t9 -23zM1600 480v-192q0 -14 -9 -23t-23 -9h-640q-14 0 -23 9t-9 23v192q0 14 9 23t23 9h640q14 0 23 -9t9 -23zM1408 992v-192q0 -14 -9 -23t-23 -9h-448q-14 0 -23 9t-9 23v192q0 14 9 23t23 9h448q14 0 23 -9t9 -23zM1216 1504v-192q0 -14 -9 -23t-23 -9h-256 q-14 0 -23 9t-9 23v192q0 14 9 23t23 9h256q14 0 23 -9t9 -23z" /> -<glyph unicode="" horiz-adv-x="1792" d="M1216 -32v-192q0 -14 -9 -23t-23 -9h-256q-14 0 -23 9t-9 23v192q0 14 9 23t23 9h256q14 0 23 -9t9 -23zM736 96q0 -12 -10 -24l-319 -319q-10 -9 -23 -9q-12 0 -23 9l-320 320q-15 16 -7 35q8 20 30 20h192v1376q0 14 9 23t23 9h192q14 0 23 -9t9 -23v-1376h192 q14 0 23 -9t9 -23zM1408 480v-192q0 -14 -9 -23t-23 -9h-448q-14 0 -23 9t-9 23v192q0 14 9 23t23 9h448q14 0 23 -9t9 -23zM1600 992v-192q0 -14 -9 -23t-23 -9h-640q-14 0 -23 9t-9 23v192q0 14 9 23t23 9h640q14 0 23 -9t9 -23zM1792 1504v-192q0 -14 -9 -23t-23 -9h-832 q-14 0 -23 9t-9 23v192q0 14 9 23t23 9h832q14 0 23 -9t9 -23z" /> -<glyph unicode="" d="M1346 223q0 63 -44 116t-103 53q-52 0 -83 -37t-31 -94t36.5 -95t104.5 -38q50 0 85 27t35 68zM736 96q0 -12 -10 -24l-319 -319q-10 -9 -23 -9q-12 0 -23 9l-320 320q-15 16 -7 35q8 20 30 20h192v1376q0 14 9 23t23 9h192q14 0 23 -9t9 -23v-1376h192q14 0 23 -9t9 -23 zM1486 165q0 -62 -13 -121.5t-41 -114t-68 -95.5t-98.5 -65.5t-127.5 -24.5q-62 0 -108 16q-24 8 -42 15l39 113q15 -7 31 -11q37 -13 75 -13q84 0 134.5 58.5t66.5 145.5h-2q-21 -23 -61.5 -37t-84.5 -14q-106 0 -173 71.5t-67 172.5q0 105 72 178t181 73q123 0 205 -94.5 t82 -252.5zM1456 882v-114h-469v114h167v432q0 7 0.5 19t0.5 17v16h-2l-7 -12q-8 -13 -26 -31l-62 -58l-82 86l192 185h123v-654h165z" /> -<glyph unicode="" d="M1346 1247q0 63 -44 116t-103 53q-52 0 -83 -37t-31 -94t36.5 -95t104.5 -38q50 0 85 27t35 68zM736 96q0 -12 -10 -24l-319 -319q-10 -9 -23 -9q-12 0 -23 9l-320 320q-15 16 -7 35q8 20 30 20h192v1376q0 14 9 23t23 9h192q14 0 23 -9t9 -23v-1376h192q14 0 23 -9 t9 -23zM1456 -142v-114h-469v114h167v432q0 7 0.5 19t0.5 17v16h-2l-7 -12q-8 -13 -26 -31l-62 -58l-82 86l192 185h123v-654h165zM1486 1189q0 -62 -13 -121.5t-41 -114t-68 -95.5t-98.5 -65.5t-127.5 -24.5q-62 0 -108 16q-24 8 -42 15l39 113q15 -7 31 -11q37 -13 75 -13 q84 0 134.5 58.5t66.5 145.5h-2q-21 -23 -61.5 -37t-84.5 -14q-106 0 -173 71.5t-67 172.5q0 105 72 178t181 73q123 0 205 -94.5t82 -252.5z" /> -<glyph unicode="" horiz-adv-x="1664" d="M256 192q0 26 -19 45t-45 19q-27 0 -45.5 -19t-18.5 -45q0 -27 18.5 -45.5t45.5 -18.5q26 0 45 18.5t19 45.5zM416 704v-640q0 -26 -19 -45t-45 -19h-288q-26 0 -45 19t-19 45v640q0 26 19 45t45 19h288q26 0 45 -19t19 -45zM1600 704q0 -86 -55 -149q15 -44 15 -76 q3 -76 -43 -137q17 -56 0 -117q-15 -57 -54 -94q9 -112 -49 -181q-64 -76 -197 -78h-36h-76h-17q-66 0 -144 15.5t-121.5 29t-120.5 39.5q-123 43 -158 44q-26 1 -45 19.5t-19 44.5v641q0 25 18 43.5t43 20.5q24 2 76 59t101 121q68 87 101 120q18 18 31 48t17.5 48.5 t13.5 60.5q7 39 12.5 61t19.5 52t34 50q19 19 45 19q46 0 82.5 -10.5t60 -26t40 -40.5t24 -45t12 -50t5 -45t0.5 -39q0 -38 -9.5 -76t-19 -60t-27.5 -56q-3 -6 -10 -18t-11 -22t-8 -24h277q78 0 135 -57t57 -135z" /> -<glyph unicode="" horiz-adv-x="1664" d="M256 960q0 -26 -19 -45t-45 -19q-27 0 -45.5 19t-18.5 45q0 27 18.5 45.5t45.5 18.5q26 0 45 -18.5t19 -45.5zM416 448v640q0 26 -19 45t-45 19h-288q-26 0 -45 -19t-19 -45v-640q0 -26 19 -45t45 -19h288q26 0 45 19t19 45zM1545 597q55 -61 55 -149q-1 -78 -57.5 -135 t-134.5 -57h-277q4 -14 8 -24t11 -22t10 -18q18 -37 27 -57t19 -58.5t10 -76.5q0 -24 -0.5 -39t-5 -45t-12 -50t-24 -45t-40 -40.5t-60 -26t-82.5 -10.5q-26 0 -45 19q-20 20 -34 50t-19.5 52t-12.5 61q-9 42 -13.5 60.5t-17.5 48.5t-31 48q-33 33 -101 120q-49 64 -101 121 t-76 59q-25 2 -43 20.5t-18 43.5v641q0 26 19 44.5t45 19.5q35 1 158 44q77 26 120.5 39.5t121.5 29t144 15.5h17h76h36q133 -2 197 -78q58 -69 49 -181q39 -37 54 -94q17 -61 0 -117q46 -61 43 -137q0 -32 -15 -76z" /> -<glyph unicode="" d="M919 233v157q0 50 -29 50q-17 0 -33 -16v-224q16 -16 33 -16q29 0 29 49zM1103 355h66v34q0 51 -33 51t-33 -51v-34zM532 621v-70h-80v-423h-74v423h-78v70h232zM733 495v-367h-67v40q-39 -45 -76 -45q-33 0 -42 28q-6 16 -6 54v290h66v-270q0 -24 1 -26q1 -15 15 -15 q20 0 42 31v280h67zM985 384v-146q0 -52 -7 -73q-12 -42 -53 -42q-35 0 -68 41v-36h-67v493h67v-161q32 40 68 40q41 0 53 -42q7 -21 7 -74zM1236 255v-9q0 -29 -2 -43q-3 -22 -15 -40q-27 -40 -80 -40q-52 0 -81 38q-21 27 -21 86v129q0 59 20 86q29 38 80 38t78 -38 q21 -28 21 -86v-76h-133v-65q0 -51 34 -51q24 0 30 26q0 1 0.5 7t0.5 16.5v21.5h68zM785 1079v-156q0 -51 -32 -51t-32 51v156q0 52 32 52t32 -52zM1318 366q0 177 -19 260q-10 44 -43 73.5t-76 34.5q-136 15 -412 15q-275 0 -411 -15q-44 -5 -76.5 -34.5t-42.5 -73.5 q-20 -87 -20 -260q0 -176 20 -260q10 -43 42.5 -73t75.5 -35q137 -15 412 -15t412 15q43 5 75.5 35t42.5 73q20 84 20 260zM563 1017l90 296h-75l-51 -195l-53 195h-78l24 -69t23 -69q35 -103 46 -158v-201h74v201zM852 936v130q0 58 -21 87q-29 38 -78 38q-51 0 -78 -38 q-21 -29 -21 -87v-130q0 -58 21 -87q27 -38 78 -38q49 0 78 38q21 27 21 87zM1033 816h67v370h-67v-283q-22 -31 -42 -31q-15 0 -16 16q-1 2 -1 26v272h-67v-293q0 -37 6 -55q11 -27 43 -27q36 0 77 45v-40zM1536 1120v-960q0 -119 -84.5 -203.5t-203.5 -84.5h-960 q-119 0 -203.5 84.5t-84.5 203.5v960q0 119 84.5 203.5t203.5 84.5h960q119 0 203.5 -84.5t84.5 -203.5z" /> -<glyph unicode="" d="M971 292v-211q0 -67 -39 -67q-23 0 -45 22v301q22 22 45 22q39 0 39 -67zM1309 291v-46h-90v46q0 68 45 68t45 -68zM343 509h107v94h-312v-94h105v-569h100v569zM631 -60h89v494h-89v-378q-30 -42 -57 -42q-18 0 -21 21q-1 3 -1 35v364h-89v-391q0 -49 8 -73 q12 -37 58 -37q48 0 102 61v-54zM1060 88v197q0 73 -9 99q-17 56 -71 56q-50 0 -93 -54v217h-89v-663h89v48q45 -55 93 -55q54 0 71 55q9 27 9 100zM1398 98v13h-91q0 -51 -2 -61q-7 -36 -40 -36q-46 0 -46 69v87h179v103q0 79 -27 116q-39 51 -106 51q-68 0 -107 -51 q-28 -37 -28 -116v-173q0 -79 29 -116q39 -51 108 -51q72 0 108 53q18 27 21 54q2 9 2 58zM790 1011v210q0 69 -43 69t-43 -69v-210q0 -70 43 -70t43 70zM1509 260q0 -234 -26 -350q-14 -59 -58 -99t-102 -46q-184 -21 -555 -21t-555 21q-58 6 -102.5 46t-57.5 99 q-26 112 -26 350q0 234 26 350q14 59 58 99t103 47q183 20 554 20t555 -20q58 -7 102.5 -47t57.5 -99q26 -112 26 -350zM511 1536h102l-121 -399v-271h-100v271q-14 74 -61 212q-37 103 -65 187h106l71 -263zM881 1203v-175q0 -81 -28 -118q-37 -51 -106 -51q-67 0 -105 51 q-28 38 -28 118v175q0 80 28 117q38 51 105 51q69 0 106 -51q28 -37 28 -117zM1216 1365v-499h-91v55q-53 -62 -103 -62q-46 0 -59 37q-8 24 -8 75v394h91v-367q0 -33 1 -35q3 -22 21 -22q27 0 57 43v381h91z" /> -<glyph unicode="" horiz-adv-x="1408" d="M597 869q-10 -18 -257 -456q-27 -46 -65 -46h-239q-21 0 -31 17t0 36l253 448q1 0 0 1l-161 279q-12 22 -1 37q9 15 32 15h239q40 0 66 -45zM1403 1511q11 -16 0 -37l-528 -934v-1l336 -615q11 -20 1 -37q-10 -15 -32 -15h-239q-42 0 -66 45l-339 622q18 32 531 942 q25 45 64 45h241q22 0 31 -15z" /> -<glyph unicode="" d="M685 771q0 1 -126 222q-21 34 -52 34h-184q-18 0 -26 -11q-7 -12 1 -29l125 -216v-1l-196 -346q-9 -14 0 -28q8 -13 24 -13h185q31 0 50 36zM1309 1268q-7 12 -24 12h-187q-30 0 -49 -35l-411 -729q1 -2 262 -481q20 -35 52 -35h184q18 0 25 12q8 13 -1 28l-260 476v1 l409 723q8 16 0 28zM1536 1120v-960q0 -119 -84.5 -203.5t-203.5 -84.5h-960q-119 0 -203.5 84.5t-84.5 203.5v960q0 119 84.5 203.5t203.5 84.5h960q119 0 203.5 -84.5t84.5 -203.5z" /> -<glyph unicode="" horiz-adv-x="1792" d="M1280 640q0 37 -30 54l-512 320q-31 20 -65 2q-33 -18 -33 -56v-640q0 -38 33 -56q16 -8 31 -8q20 0 34 10l512 320q30 17 30 54zM1792 640q0 -96 -1 -150t-8.5 -136.5t-22.5 -147.5q-16 -73 -69 -123t-124 -58q-222 -25 -671 -25t-671 25q-71 8 -124.5 58t-69.5 123 q-14 65 -21.5 147.5t-8.5 136.5t-1 150t1 150t8.5 136.5t22.5 147.5q16 73 69 123t124 58q222 25 671 25t671 -25q71 -8 124.5 -58t69.5 -123q14 -65 21.5 -147.5t8.5 -136.5t1 -150z" /> -<glyph unicode="" horiz-adv-x="1792" d="M402 829l494 -305l-342 -285l-490 319zM1388 274v-108l-490 -293v-1l-1 1l-1 -1v1l-489 293v108l147 -96l342 284v2l1 -1l1 1v-2l343 -284zM554 1418l342 -285l-494 -304l-338 270zM1390 829l338 -271l-489 -319l-343 285zM1239 1418l489 -319l-338 -270l-494 304z" /> -<glyph unicode="" horiz-adv-x="1408" d="M928 135v-151l-707 -1v151zM1169 481v-701l-1 -35v-1h-1132l-35 1h-1v736h121v-618h928v618h120zM241 393l704 -65l-13 -150l-705 65zM309 709l683 -183l-39 -146l-683 183zM472 1058l609 -360l-77 -130l-609 360zM832 1389l398 -585l-124 -85l-399 584zM1285 1536 l121 -697l-149 -26l-121 697z" /> -<glyph unicode="" d="M1362 110v648h-135q20 -63 20 -131q0 -126 -64 -232.5t-174 -168.5t-240 -62q-197 0 -337 135.5t-140 327.5q0 68 20 131h-141v-648q0 -26 17.5 -43.5t43.5 -17.5h1069q25 0 43 17.5t18 43.5zM1078 643q0 124 -90.5 211.5t-218.5 87.5q-127 0 -217.5 -87.5t-90.5 -211.5 t90.5 -211.5t217.5 -87.5q128 0 218.5 87.5t90.5 211.5zM1362 1003v165q0 28 -20 48.5t-49 20.5h-174q-29 0 -49 -20.5t-20 -48.5v-165q0 -29 20 -49t49 -20h174q29 0 49 20t20 49zM1536 1211v-1142q0 -81 -58 -139t-139 -58h-1142q-81 0 -139 58t-58 139v1142q0 81 58 139 t139 58h1142q81 0 139 -58t58 -139z" /> -<glyph unicode="" d="M1248 1408q119 0 203.5 -84.5t84.5 -203.5v-960q0 -119 -84.5 -203.5t-203.5 -84.5h-960q-119 0 -203.5 84.5t-84.5 203.5v960q0 119 84.5 203.5t203.5 84.5h960zM698 640q0 88 -62 150t-150 62t-150 -62t-62 -150t62 -150t150 -62t150 62t62 150zM1262 640q0 88 -62 150 t-150 62t-150 -62t-62 -150t62 -150t150 -62t150 62t62 150z" /> -<glyph unicode="" d="M768 914l201 -306h-402zM1133 384h94l-459 691l-459 -691h94l104 160h522zM1536 640q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5t103 -385.5z" /> -<glyph unicode="" horiz-adv-x="1408" d="M815 677q8 -63 -50.5 -101t-111.5 -6q-39 17 -53.5 58t-0.5 82t52 58q36 18 72.5 12t64 -35.5t27.5 -67.5zM926 698q-14 107 -113 164t-197 13q-63 -28 -100.5 -88.5t-34.5 -129.5q4 -91 77.5 -155t165.5 -56q91 8 152 84t50 168zM1165 1240q-20 27 -56 44.5t-58 22 t-71 12.5q-291 47 -566 -2q-43 -7 -66 -12t-55 -22t-50 -43q30 -28 76 -45.5t73.5 -22t87.5 -11.5q228 -29 448 -1q63 8 89.5 12t72.5 21.5t75 46.5zM1222 205q-8 -26 -15.5 -76.5t-14 -84t-28.5 -70t-58 -56.5q-86 -48 -189.5 -71.5t-202 -22t-201.5 18.5q-46 8 -81.5 18 t-76.5 27t-73 43.5t-52 61.5q-25 96 -57 292l6 16l18 9q223 -148 506.5 -148t507.5 148q21 -6 24 -23t-5 -45t-8 -37zM1403 1166q-26 -167 -111 -655q-5 -30 -27 -56t-43.5 -40t-54.5 -31q-252 -126 -610 -88q-248 27 -394 139q-15 12 -25.5 26.5t-17 35t-9 34t-6 39.5 t-5.5 35q-9 50 -26.5 150t-28 161.5t-23.5 147.5t-22 158q3 26 17.5 48.5t31.5 37.5t45 30t46 22.5t48 18.5q125 46 313 64q379 37 676 -50q155 -46 215 -122q16 -20 16.5 -51t-5.5 -54z" /> -<glyph unicode="" d="M848 666q0 43 -41 66t-77 1q-43 -20 -42.5 -72.5t43.5 -70.5q39 -23 81 4t36 72zM928 682q8 -66 -36 -121t-110 -61t-119 40t-56 113q-2 49 25.5 93t72.5 64q70 31 141.5 -10t81.5 -118zM1100 1073q-20 -21 -53.5 -34t-53 -16t-63.5 -8q-155 -20 -324 0q-44 6 -63 9.5 t-52.5 16t-54.5 32.5q13 19 36 31t40 15.5t47 8.5q198 35 408 1q33 -5 51 -8.5t43 -16t39 -31.5zM1142 327q0 7 5.5 26.5t3 32t-17.5 16.5q-161 -106 -365 -106t-366 106l-12 -6l-5 -12q26 -154 41 -210q47 -81 204 -108q249 -46 428 53q34 19 49 51.5t22.5 85.5t12.5 71z M1272 1020q9 53 -8 75q-43 55 -155 88q-216 63 -487 36q-132 -12 -226 -46q-38 -15 -59.5 -25t-47 -34t-29.5 -54q8 -68 19 -138t29 -171t24 -137q1 -5 5 -31t7 -36t12 -27t22 -28q105 -80 284 -100q259 -28 440 63q24 13 39.5 23t31 29t19.5 40q48 267 80 473zM1536 1120 v-960q0 -119 -84.5 -203.5t-203.5 -84.5h-960q-119 0 -203.5 84.5t-84.5 203.5v960q0 119 84.5 203.5t203.5 84.5h960q119 0 203.5 -84.5t84.5 -203.5z" /> -<glyph unicode="" horiz-adv-x="1024" d="M944 207l80 -237q-23 -35 -111 -66t-177 -32q-104 -2 -190.5 26t-142.5 74t-95 106t-55.5 120t-16.5 118v544h-168v215q72 26 129 69.5t91 90t58 102t34 99t15 88.5q1 5 4.5 8.5t7.5 3.5h244v-424h333v-252h-334v-518q0 -30 6.5 -56t22.5 -52.5t49.5 -41.5t81.5 -14 q78 2 134 29z" /> -<glyph unicode="" d="M1136 75l-62 183q-44 -22 -103 -22q-36 -1 -62 10.5t-38.5 31.5t-17.5 40.5t-5 43.5v398h257v194h-256v326h-188q-8 0 -9 -10q-5 -44 -17.5 -87t-39 -95t-77 -95t-118.5 -68v-165h130v-418q0 -57 21.5 -115t65 -111t121 -85.5t176.5 -30.5q69 1 136.5 25t85.5 50z M1536 1120v-960q0 -119 -84.5 -203.5t-203.5 -84.5h-960q-119 0 -203.5 84.5t-84.5 203.5v960q0 119 84.5 203.5t203.5 84.5h960q119 0 203.5 -84.5t84.5 -203.5z" /> -<glyph unicode="" horiz-adv-x="768" d="M765 237q8 -19 -5 -35l-350 -384q-10 -10 -23 -10q-14 0 -24 10l-355 384q-13 16 -5 35q9 19 29 19h224v1248q0 14 9 23t23 9h192q14 0 23 -9t9 -23v-1248h224q21 0 29 -19z" /> -<glyph unicode="" horiz-adv-x="768" d="M765 1043q-9 -19 -29 -19h-224v-1248q0 -14 -9 -23t-23 -9h-192q-14 0 -23 9t-9 23v1248h-224q-21 0 -29 19t5 35l350 384q10 10 23 10q14 0 24 -10l355 -384q13 -16 5 -35z" /> -<glyph unicode="" horiz-adv-x="1792" d="M1792 736v-192q0 -14 -9 -23t-23 -9h-1248v-224q0 -21 -19 -29t-35 5l-384 350q-10 10 -10 23q0 14 10 24l384 354q16 14 35 6q19 -9 19 -29v-224h1248q14 0 23 -9t9 -23z" /> -<glyph unicode="" horiz-adv-x="1792" d="M1728 643q0 -14 -10 -24l-384 -354q-16 -14 -35 -6q-19 9 -19 29v224h-1248q-14 0 -23 9t-9 23v192q0 14 9 23t23 9h1248v224q0 21 19 29t35 -5l384 -350q10 -10 10 -23z" /> -<glyph unicode="" horiz-adv-x="1408" d="M1393 321q-39 -125 -123 -250q-129 -196 -257 -196q-49 0 -140 32q-86 32 -151 32q-61 0 -142 -33q-81 -34 -132 -34q-152 0 -301 259q-147 261 -147 503q0 228 113 374q112 144 284 144q72 0 177 -30q104 -30 138 -30q45 0 143 34q102 34 173 34q119 0 213 -65 q52 -36 104 -100q-79 -67 -114 -118q-65 -94 -65 -207q0 -124 69 -223t158 -126zM1017 1494q0 -61 -29 -136q-30 -75 -93 -138q-54 -54 -108 -72q-37 -11 -104 -17q3 149 78 257q74 107 250 148q1 -3 2.5 -11t2.5 -11q0 -4 0.5 -10t0.5 -10z" /> -<glyph unicode="" horiz-adv-x="1664" d="M682 530v-651l-682 94v557h682zM682 1273v-659h-682v565zM1664 530v-786l-907 125v661h907zM1664 1408v-794h-907v669z" /> -<glyph unicode="" horiz-adv-x="1408" d="M493 1053q16 0 27.5 11.5t11.5 27.5t-11.5 27.5t-27.5 11.5t-27 -11.5t-11 -27.5t11 -27.5t27 -11.5zM915 1053q16 0 27 11.5t11 27.5t-11 27.5t-27 11.5t-27.5 -11.5t-11.5 -27.5t11.5 -27.5t27.5 -11.5zM103 869q42 0 72 -30t30 -72v-430q0 -43 -29.5 -73t-72.5 -30 t-73 30t-30 73v430q0 42 30 72t73 30zM1163 850v-666q0 -46 -32 -78t-77 -32h-75v-227q0 -43 -30 -73t-73 -30t-73 30t-30 73v227h-138v-227q0 -43 -30 -73t-73 -30q-42 0 -72 30t-30 73l-1 227h-74q-46 0 -78 32t-32 78v666h918zM931 1255q107 -55 171 -153.5t64 -215.5 h-925q0 117 64 215.5t172 153.5l-71 131q-7 13 5 20q13 6 20 -6l72 -132q95 42 201 42t201 -42l72 132q7 12 20 6q12 -7 5 -20zM1408 767v-430q0 -43 -30 -73t-73 -30q-42 0 -72 30t-30 73v430q0 43 30 72.5t72 29.5q43 0 73 -29.5t30 -72.5z" /> -<glyph unicode="" d="M663 1125q-11 -1 -15.5 -10.5t-8.5 -9.5q-5 -1 -5 5q0 12 19 15h10zM750 1111q-4 -1 -11.5 6.5t-17.5 4.5q24 11 32 -2q3 -6 -3 -9zM399 684q-4 1 -6 -3t-4.5 -12.5t-5.5 -13.5t-10 -13q-7 -10 -1 -12q4 -1 12.5 7t12.5 18q1 3 2 7t2 6t1.5 4.5t0.5 4v3t-1 2.5t-3 2z M1254 325q0 18 -55 42q4 15 7.5 27.5t5 26t3 21.5t0.5 22.5t-1 19.5t-3.5 22t-4 20.5t-5 25t-5.5 26.5q-10 48 -47 103t-72 75q24 -20 57 -83q87 -162 54 -278q-11 -40 -50 -42q-31 -4 -38.5 18.5t-8 83.5t-11.5 107q-9 39 -19.5 69t-19.5 45.5t-15.5 24.5t-13 15t-7.5 7 q-14 62 -31 103t-29.5 56t-23.5 33t-15 40q-4 21 6 53.5t4.5 49.5t-44.5 25q-15 3 -44.5 18t-35.5 16q-8 1 -11 26t8 51t36 27q37 3 51 -30t4 -58q-11 -19 -2 -26.5t30 -0.5q13 4 13 36v37q-5 30 -13.5 50t-21 30.5t-23.5 15t-27 7.5q-107 -8 -89 -134q0 -15 -1 -15 q-9 9 -29.5 10.5t-33 -0.5t-15.5 5q1 57 -16 90t-45 34q-27 1 -41.5 -27.5t-16.5 -59.5q-1 -15 3.5 -37t13 -37.5t15.5 -13.5q10 3 16 14q4 9 -7 8q-7 0 -15.5 14.5t-9.5 33.5q-1 22 9 37t34 14q17 0 27 -21t9.5 -39t-1.5 -22q-22 -15 -31 -29q-8 -12 -27.5 -23.5 t-20.5 -12.5q-13 -14 -15.5 -27t7.5 -18q14 -8 25 -19.5t16 -19t18.5 -13t35.5 -6.5q47 -2 102 15q2 1 23 7t34.5 10.5t29.5 13t21 17.5q9 14 20 8q5 -3 6.5 -8.5t-3 -12t-16.5 -9.5q-20 -6 -56.5 -21.5t-45.5 -19.5q-44 -19 -70 -23q-25 -5 -79 2q-10 2 -9 -2t17 -19 q25 -23 67 -22q17 1 36 7t36 14t33.5 17.5t30 17t24.5 12t17.5 2.5t8.5 -11q0 -2 -1 -4.5t-4 -5t-6 -4.5t-8.5 -5t-9 -4.5t-10 -5t-9.5 -4.5q-28 -14 -67.5 -44t-66.5 -43t-49 -1q-21 11 -63 73q-22 31 -25 22q-1 -3 -1 -10q0 -25 -15 -56.5t-29.5 -55.5t-21 -58t11.5 -63 q-23 -6 -62.5 -90t-47.5 -141q-2 -18 -1.5 -69t-5.5 -59q-8 -24 -29 -3q-32 31 -36 94q-2 28 4 56q4 19 -1 18l-4 -5q-36 -65 10 -166q5 -12 25 -28t24 -20q20 -23 104 -90.5t93 -76.5q16 -15 17.5 -38t-14 -43t-45.5 -23q8 -15 29 -44.5t28 -54t7 -70.5q46 24 7 92 q-4 8 -10.5 16t-9.5 12t-2 6q3 5 13 9.5t20 -2.5q46 -52 166 -36q133 15 177 87q23 38 34 30q12 -6 10 -52q-1 -25 -23 -92q-9 -23 -6 -37.5t24 -15.5q3 19 14.5 77t13.5 90q2 21 -6.5 73.5t-7.5 97t23 70.5q15 18 51 18q1 37 34.5 53t72.5 10.5t60 -22.5zM626 1152 q3 17 -2.5 30t-11.5 15q-9 2 -9 -7q2 -5 5 -6q10 0 7 -15q-3 -20 8 -20q3 0 3 3zM1045 955q-2 8 -6.5 11.5t-13 5t-14.5 5.5q-5 3 -9.5 8t-7 8t-5.5 6.5t-4 4t-4 -1.5q-14 -16 7 -43.5t39 -31.5q9 -1 14.5 8t3.5 20zM867 1168q0 11 -5 19.5t-11 12.5t-9 3q-14 -1 -7 -7l4 -2 q14 -4 18 -31q0 -3 8 2zM921 1401q0 2 -2.5 5t-9 7t-9.5 6q-15 15 -24 15q-9 -1 -11.5 -7.5t-1 -13t-0.5 -12.5q-1 -4 -6 -10.5t-6 -9t3 -8.5q4 -3 8 0t11 9t15 9q1 1 9 1t15 2t9 7zM1486 60q20 -12 31 -24.5t12 -24t-2.5 -22.5t-15.5 -22t-23.5 -19.5t-30 -18.5 t-31.5 -16.5t-32 -15.5t-27 -13q-38 -19 -85.5 -56t-75.5 -64q-17 -16 -68 -19.5t-89 14.5q-18 9 -29.5 23.5t-16.5 25.5t-22 19.5t-47 9.5q-44 1 -130 1q-19 0 -57 -1.5t-58 -2.5q-44 -1 -79.5 -15t-53.5 -30t-43.5 -28.5t-53.5 -11.5q-29 1 -111 31t-146 43q-19 4 -51 9.5 t-50 9t-39.5 9.5t-33.5 14.5t-17 19.5q-10 23 7 66.5t18 54.5q1 16 -4 40t-10 42.5t-4.5 36.5t10.5 27q14 12 57 14t60 12q30 18 42 35t12 51q21 -73 -32 -106q-32 -20 -83 -15q-34 3 -43 -10q-13 -15 5 -57q2 -6 8 -18t8.5 -18t4.5 -17t1 -22q0 -15 -17 -49t-14 -48 q3 -17 37 -26q20 -6 84.5 -18.5t99.5 -20.5q24 -6 74 -22t82.5 -23t55.5 -4q43 6 64.5 28t23 48t-7.5 58.5t-19 52t-20 36.5q-121 190 -169 242q-68 74 -113 40q-11 -9 -15 15q-3 16 -2 38q1 29 10 52t24 47t22 42q8 21 26.5 72t29.5 78t30 61t39 54q110 143 124 195 q-12 112 -16 310q-2 90 24 151.5t106 104.5q39 21 104 21q53 1 106 -13.5t89 -41.5q57 -42 91.5 -121.5t29.5 -147.5q-5 -95 30 -214q34 -113 133 -218q55 -59 99.5 -163t59.5 -191q8 -49 5 -84.5t-12 -55.5t-20 -22q-10 -2 -23.5 -19t-27 -35.5t-40.5 -33.5t-61 -14 q-18 1 -31.5 5t-22.5 13.5t-13.5 15.5t-11.5 20.5t-9 19.5q-22 37 -41 30t-28 -49t7 -97q20 -70 1 -195q-10 -65 18 -100.5t73 -33t85 35.5q59 49 89.5 66.5t103.5 42.5q53 18 77 36.5t18.5 34.5t-25 28.5t-51.5 23.5q-33 11 -49.5 48t-15 72.5t15.5 47.5q1 -31 8 -56.5 t14.5 -40.5t20.5 -28.5t21 -19t21.5 -13t16.5 -9.5z" /> -<glyph unicode="" d="M1024 36q-42 241 -140 498h-2l-2 -1q-16 -6 -43 -16.5t-101 -49t-137 -82t-131 -114.5t-103 -148l-15 11q184 -150 418 -150q132 0 256 52zM839 643q-21 49 -53 111q-311 -93 -673 -93q-1 -7 -1 -21q0 -124 44 -236.5t124 -201.5q50 89 123.5 166.5t142.5 124.5t130.5 81 t99.5 48l37 13q4 1 13 3.5t13 4.5zM732 855q-120 213 -244 378q-138 -65 -234 -186t-128 -272q302 0 606 80zM1416 536q-210 60 -409 29q87 -239 128 -469q111 75 185 189.5t96 250.5zM611 1277q-1 0 -2 -1q1 1 2 1zM1201 1132q-185 164 -433 164q-76 0 -155 -19 q131 -170 246 -382q69 26 130 60.5t96.5 61.5t65.5 57t37.5 40.5zM1424 647q-3 232 -149 410l-1 -1q-9 -12 -19 -24.5t-43.5 -44.5t-71 -60.5t-100 -65t-131.5 -64.5q25 -53 44 -95q2 -6 6.5 -17.5t7.5 -16.5q36 5 74.5 7t73.5 2t69 -1.5t64 -4t56.5 -5.5t48 -6.5t36.5 -6 t25 -4.5zM1536 640q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5t103 -385.5z" /> -<glyph unicode="" d="M1173 473q0 50 -19.5 91.5t-48.5 68.5t-73 49t-82.5 34t-87.5 23l-104 24q-30 7 -44 10.5t-35 11.5t-30 16t-16.5 21t-7.5 30q0 77 144 77q43 0 77 -12t54 -28.5t38 -33.5t40 -29t48 -12q47 0 75.5 32t28.5 77q0 55 -56 99.5t-142 67.5t-182 23q-68 0 -132 -15.5 t-119.5 -47t-89 -87t-33.5 -128.5q0 -61 19 -106.5t56 -75.5t80 -48.5t103 -32.5l146 -36q90 -22 112 -36q32 -20 32 -60q0 -39 -40 -64.5t-105 -25.5q-51 0 -91.5 16t-65 38.5t-45.5 45t-46 38.5t-54 16q-50 0 -75.5 -30t-25.5 -75q0 -92 122 -157.5t291 -65.5 q73 0 140 18.5t122.5 53.5t88.5 93.5t33 131.5zM1536 256q0 -159 -112.5 -271.5t-271.5 -112.5q-130 0 -234 80q-77 -16 -150 -16q-143 0 -273.5 55.5t-225 150t-150 225t-55.5 273.5q0 73 16 150q-80 104 -80 234q0 159 112.5 271.5t271.5 112.5q130 0 234 -80 q77 16 150 16q143 0 273.5 -55.5t225 -150t150 -225t55.5 -273.5q0 -73 -16 -150q80 -104 80 -234z" /> -<glyph unicode="" horiz-adv-x="1280" d="M1000 1102l37 194q5 23 -9 40t-35 17h-712q-23 0 -38.5 -17t-15.5 -37v-1101q0 -7 6 -1l291 352q23 26 38 33.5t48 7.5h239q22 0 37 14.5t18 29.5q24 130 37 191q4 21 -11.5 40t-36.5 19h-294q-29 0 -48 19t-19 48v42q0 29 19 47.5t48 18.5h346q18 0 35 13.5t20 29.5z M1227 1324q-15 -73 -53.5 -266.5t-69.5 -350t-35 -173.5q-6 -22 -9 -32.5t-14 -32.5t-24.5 -33t-38.5 -21t-58 -10h-271q-13 0 -22 -10q-8 -9 -426 -494q-22 -25 -58.5 -28.5t-48.5 5.5q-55 22 -55 98v1410q0 55 38 102.5t120 47.5h888q95 0 127 -53t10 -159zM1227 1324 l-158 -790q4 17 35 173.5t69.5 350t53.5 266.5z" /> -<glyph unicode="" d="M704 192v1024q0 14 -9 23t-23 9h-480q-14 0 -23 -9t-9 -23v-1024q0 -14 9 -23t23 -9h480q14 0 23 9t9 23zM1376 576v640q0 14 -9 23t-23 9h-480q-14 0 -23 -9t-9 -23v-640q0 -14 9 -23t23 -9h480q14 0 23 9t9 23zM1536 1344v-1408q0 -26 -19 -45t-45 -19h-1408 q-26 0 -45 19t-19 45v1408q0 26 19 45t45 19h1408q26 0 45 -19t19 -45z" /> -<glyph unicode="" horiz-adv-x="1280" d="M1280 480q0 -40 -28 -68t-68 -28q-51 0 -80 43l-227 341h-45v-132l247 -411q9 -15 9 -33q0 -26 -19 -45t-45 -19h-192v-272q0 -46 -33 -79t-79 -33h-160q-46 0 -79 33t-33 79v272h-192q-26 0 -45 19t-19 45q0 18 9 33l247 411v132h-45l-227 -341q-29 -43 -80 -43 q-40 0 -68 28t-28 68q0 29 16 53l256 384q73 107 176 107h384q103 0 176 -107l256 -384q16 -24 16 -53zM864 1280q0 -93 -65.5 -158.5t-158.5 -65.5t-158.5 65.5t-65.5 158.5t65.5 158.5t158.5 65.5t158.5 -65.5t65.5 -158.5z" /> -<glyph unicode="" horiz-adv-x="1024" d="M1024 832v-416q0 -40 -28 -68t-68 -28t-68 28t-28 68v352h-64v-912q0 -46 -33 -79t-79 -33t-79 33t-33 79v464h-64v-464q0 -46 -33 -79t-79 -33t-79 33t-33 79v912h-64v-352q0 -40 -28 -68t-68 -28t-68 28t-28 68v416q0 80 56 136t136 56h640q80 0 136 -56t56 -136z M736 1280q0 -93 -65.5 -158.5t-158.5 -65.5t-158.5 65.5t-65.5 158.5t65.5 158.5t158.5 65.5t158.5 -65.5t65.5 -158.5z" /> -<glyph unicode="" d="M773 234l350 473q16 22 24.5 59t-6 85t-61.5 79q-40 26 -83 25.5t-73.5 -17.5t-54.5 -45q-36 -40 -96 -40q-59 0 -95 40q-24 28 -54.5 45t-73.5 17.5t-84 -25.5q-46 -31 -60.5 -79t-6 -85t24.5 -59zM1536 640q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103 t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5t103 -385.5z" /> -<glyph unicode="" horiz-adv-x="1792" d="M1472 640q0 117 -45.5 223.5t-123 184t-184 123t-223.5 45.5t-223.5 -45.5t-184 -123t-123 -184t-45.5 -223.5t45.5 -223.5t123 -184t184 -123t223.5 -45.5t223.5 45.5t184 123t123 184t45.5 223.5zM1748 363q-4 -15 -20 -20l-292 -96v-306q0 -16 -13 -26q-15 -10 -29 -4 l-292 94l-180 -248q-10 -13 -26 -13t-26 13l-180 248l-292 -94q-14 -6 -29 4q-13 10 -13 26v306l-292 96q-16 5 -20 20q-5 17 4 29l180 248l-180 248q-9 13 -4 29q4 15 20 20l292 96v306q0 16 13 26q15 10 29 4l292 -94l180 248q9 12 26 12t26 -12l180 -248l292 94 q14 6 29 -4q13 -10 13 -26v-306l292 -96q16 -5 20 -20q5 -16 -4 -29l-180 -248l180 -248q9 -12 4 -29z" /> -<glyph unicode="" d="M1262 233q-54 -9 -110 -9q-182 0 -337 90t-245 245t-90 337q0 192 104 357q-201 -60 -328.5 -229t-127.5 -384q0 -130 51 -248.5t136.5 -204t204 -136.5t248.5 -51q144 0 273.5 61.5t220.5 171.5zM1465 318q-94 -203 -283.5 -324.5t-413.5 -121.5q-156 0 -298 61 t-245 164t-164 245t-61 298q0 153 57.5 292.5t156 241.5t235.5 164.5t290 68.5q44 2 61 -39q18 -41 -15 -72q-86 -78 -131.5 -181.5t-45.5 -218.5q0 -148 73 -273t198 -198t273 -73q118 0 228 51q41 18 72 -13q14 -14 17.5 -34t-4.5 -38z" /> -<glyph unicode="" horiz-adv-x="1792" d="M1088 704q0 26 -19 45t-45 19h-256q-26 0 -45 -19t-19 -45t19 -45t45 -19h256q26 0 45 19t19 45zM1664 896v-960q0 -26 -19 -45t-45 -19h-1408q-26 0 -45 19t-19 45v960q0 26 19 45t45 19h1408q26 0 45 -19t19 -45zM1728 1344v-256q0 -26 -19 -45t-45 -19h-1536 q-26 0 -45 19t-19 45v256q0 26 19 45t45 19h1536q26 0 45 -19t19 -45z" /> -<glyph unicode="" horiz-adv-x="1664" d="M1632 576q0 -26 -19 -45t-45 -19h-224q0 -171 -67 -290l208 -209q19 -19 19 -45t-19 -45q-18 -19 -45 -19t-45 19l-198 197q-5 -5 -15 -13t-42 -28.5t-65 -36.5t-82 -29t-97 -13v896h-128v-896q-51 0 -101.5 13.5t-87 33t-66 39t-43.5 32.5l-15 14l-183 -207 q-20 -21 -48 -21q-24 0 -43 16q-19 18 -20.5 44.5t15.5 46.5l202 227q-58 114 -58 274h-224q-26 0 -45 19t-19 45t19 45t45 19h224v294l-173 173q-19 19 -19 45t19 45t45 19t45 -19l173 -173h844l173 173q19 19 45 19t45 -19t19 -45t-19 -45l-173 -173v-294h224q26 0 45 -19 t19 -45zM1152 1152h-640q0 133 93.5 226.5t226.5 93.5t226.5 -93.5t93.5 -226.5z" /> -<glyph unicode="" horiz-adv-x="1920" d="M1917 1016q23 -64 -150 -294q-24 -32 -65 -85q-78 -100 -90 -131q-17 -41 14 -81q17 -21 81 -82h1l1 -1l1 -1l2 -2q141 -131 191 -221q3 -5 6.5 -12.5t7 -26.5t-0.5 -34t-25 -27.5t-59 -12.5l-256 -4q-24 -5 -56 5t-52 22l-20 12q-30 21 -70 64t-68.5 77.5t-61 58 t-56.5 15.5q-3 -1 -8 -3.5t-17 -14.5t-21.5 -29.5t-17 -52t-6.5 -77.5q0 -15 -3.5 -27.5t-7.5 -18.5l-4 -5q-18 -19 -53 -22h-115q-71 -4 -146 16.5t-131.5 53t-103 66t-70.5 57.5l-25 24q-10 10 -27.5 30t-71.5 91t-106 151t-122.5 211t-130.5 272q-6 16 -6 27t3 16l4 6 q15 19 57 19l274 2q12 -2 23 -6.5t16 -8.5l5 -3q16 -11 24 -32q20 -50 46 -103.5t41 -81.5l16 -29q29 -60 56 -104t48.5 -68.5t41.5 -38.5t34 -14t27 5q2 1 5 5t12 22t13.5 47t9.5 81t0 125q-2 40 -9 73t-14 46l-6 12q-25 34 -85 43q-13 2 5 24q17 19 38 30q53 26 239 24 q82 -1 135 -13q20 -5 33.5 -13.5t20.5 -24t10.5 -32t3.5 -45.5t-1 -55t-2.5 -70.5t-1.5 -82.5q0 -11 -1 -42t-0.5 -48t3.5 -40.5t11.5 -39t22.5 -24.5q8 -2 17 -4t26 11t38 34.5t52 67t68 107.5q60 104 107 225q4 10 10 17.5t11 10.5l4 3l5 2.5t13 3t20 0.5l288 2 q39 5 64 -2.5t31 -16.5z" /> -<glyph unicode="" horiz-adv-x="1792" d="M675 252q21 34 11 69t-45 50q-34 14 -73 1t-60 -46q-22 -34 -13 -68.5t43 -50.5t74.5 -2.5t62.5 47.5zM769 373q8 13 3.5 26.5t-17.5 18.5q-14 5 -28.5 -0.5t-21.5 -18.5q-17 -31 13 -45q14 -5 29 0.5t22 18.5zM943 266q-45 -102 -158 -150t-224 -12 q-107 34 -147.5 126.5t6.5 187.5q47 93 151.5 139t210.5 19q111 -29 158.5 -119.5t2.5 -190.5zM1255 426q-9 96 -89 170t-208.5 109t-274.5 21q-223 -23 -369.5 -141.5t-132.5 -264.5q9 -96 89 -170t208.5 -109t274.5 -21q223 23 369.5 141.5t132.5 264.5zM1563 422 q0 -68 -37 -139.5t-109 -137t-168.5 -117.5t-226 -83t-270.5 -31t-275 33.5t-240.5 93t-171.5 151t-65 199.5q0 115 69.5 245t197.5 258q169 169 341.5 236t246.5 -7q65 -64 20 -209q-4 -14 -1 -20t10 -7t14.5 0.5t13.5 3.5l6 2q139 59 246 59t153 -61q45 -63 0 -178 q-2 -13 -4.5 -20t4.5 -12.5t12 -7.5t17 -6q57 -18 103 -47t80 -81.5t34 -116.5zM1489 1046q42 -47 54.5 -108.5t-6.5 -117.5q-8 -23 -29.5 -34t-44.5 -4q-23 8 -34 29.5t-4 44.5q20 63 -24 111t-107 35q-24 -5 -45 8t-25 37q-5 24 8 44.5t37 25.5q60 13 119 -5.5t101 -65.5z M1670 1209q87 -96 112.5 -222.5t-13.5 -241.5q-9 -27 -34 -40t-52 -4t-40 34t-5 52q28 82 10 172t-80 158q-62 69 -148 95.5t-173 8.5q-28 -6 -52 9.5t-30 43.5t9.5 51.5t43.5 29.5q123 26 244 -11.5t208 -134.5z" /> -<glyph unicode="" d="M1133 -34q-171 -94 -368 -94q-196 0 -367 94q138 87 235.5 211t131.5 268q35 -144 132.5 -268t235.5 -211zM638 1394v-485q0 -252 -126.5 -459.5t-330.5 -306.5q-181 215 -181 495q0 187 83.5 349.5t229.5 269.5t325 137zM1536 638q0 -280 -181 -495 q-204 99 -330.5 306.5t-126.5 459.5v485q179 -30 325 -137t229.5 -269.5t83.5 -349.5z" /> -<glyph unicode="" horiz-adv-x="1408" d="M1402 433q-32 -80 -76 -138t-91 -88.5t-99 -46.5t-101.5 -14.5t-96.5 8.5t-86.5 22t-69.5 27.5t-46 22.5l-17 10q-113 -228 -289.5 -359.5t-384.5 -132.5q-19 0 -32 13t-13 32t13 31.5t32 12.5q173 1 322.5 107.5t251.5 294.5q-36 -14 -72 -23t-83 -13t-91 2.5t-93 28.5 t-92 59t-84.5 100t-74.5 146q114 47 214 57t167.5 -7.5t124.5 -56.5t88.5 -77t56.5 -82q53 131 79 291q-7 -1 -18 -2.5t-46.5 -2.5t-69.5 0.5t-81.5 10t-88.5 23t-84 42.5t-75 65t-54.5 94.5t-28.5 127.5q70 28 133.5 36.5t112.5 -1t92 -30t73.5 -50t56 -61t42 -63t27.5 -56 t16 -39.5l4 -16q12 122 12 195q-8 6 -21.5 16t-49 44.5t-63.5 71.5t-54 93t-33 112.5t12 127t70 138.5q73 -25 127.5 -61.5t84.5 -76.5t48 -85t20.5 -89t-0.5 -85.5t-13 -76.5t-19 -62t-17 -42l-7 -15q1 -5 1 -50.5t-1 -71.5q3 7 10 18.5t30.5 43t50.5 58t71 55.5t91.5 44.5 t112 14.5t132.5 -24q-2 -78 -21.5 -141.5t-50 -104.5t-69.5 -71.5t-81.5 -45.5t-84.5 -24t-80 -9.5t-67.5 1t-46.5 4.5l-17 3q-23 -147 -73 -283q6 7 18 18.5t49.5 41t77.5 52.5t99.5 42t117.5 20t129 -23.5t137 -77.5z" /> -<glyph unicode="" horiz-adv-x="1280" d="M1259 283v-66q0 -85 -57.5 -144.5t-138.5 -59.5h-57l-260 -269v269h-529q-81 0 -138.5 59.5t-57.5 144.5v66h1238zM1259 609v-255h-1238v255h1238zM1259 937v-255h-1238v255h1238zM1259 1077v-67h-1238v67q0 84 57.5 143.5t138.5 59.5h846q81 0 138.5 -59.5t57.5 -143.5z " /> -<glyph unicode="" d="M1152 640q0 -14 -9 -23l-320 -320q-9 -9 -23 -9q-13 0 -22.5 9.5t-9.5 22.5v192h-352q-13 0 -22.5 9.5t-9.5 22.5v192q0 13 9.5 22.5t22.5 9.5h352v192q0 14 9 23t23 9q12 0 24 -10l319 -319q9 -9 9 -23zM1312 640q0 148 -73 273t-198 198t-273 73t-273 -73t-198 -198 t-73 -273t73 -273t198 -198t273 -73t273 73t198 198t73 273zM1536 640q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5t103 -385.5z" /> -<glyph unicode="" d="M1152 736v-192q0 -13 -9.5 -22.5t-22.5 -9.5h-352v-192q0 -14 -9 -23t-23 -9q-12 0 -24 10l-319 319q-9 9 -9 23t9 23l320 320q9 9 23 9q13 0 22.5 -9.5t9.5 -22.5v-192h352q13 0 22.5 -9.5t9.5 -22.5zM1312 640q0 148 -73 273t-198 198t-273 73t-273 -73t-198 -198 t-73 -273t73 -273t198 -198t273 -73t273 73t198 198t73 273zM1536 640q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5t103 -385.5z" /> -<glyph unicode="" d="M1024 960v-640q0 -26 -19 -45t-45 -19q-20 0 -37 12l-448 320q-27 19 -27 52t27 52l448 320q17 12 37 12q26 0 45 -19t19 -45zM1280 160v960q0 13 -9.5 22.5t-22.5 9.5h-960q-13 0 -22.5 -9.5t-9.5 -22.5v-960q0 -13 9.5 -22.5t22.5 -9.5h960q13 0 22.5 9.5t9.5 22.5z M1536 1120v-960q0 -119 -84.5 -203.5t-203.5 -84.5h-960q-119 0 -203.5 84.5t-84.5 203.5v960q0 119 84.5 203.5t203.5 84.5h960q119 0 203.5 -84.5t84.5 -203.5z" /> -<glyph unicode="" d="M1024 640q0 -106 -75 -181t-181 -75t-181 75t-75 181t75 181t181 75t181 -75t75 -181zM768 1184q-148 0 -273 -73t-198 -198t-73 -273t73 -273t198 -198t273 -73t273 73t198 198t73 273t-73 273t-198 198t-273 73zM1536 640q0 -209 -103 -385.5t-279.5 -279.5 t-385.5 -103t-385.5 103t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5t103 -385.5z" /> -<glyph unicode="" horiz-adv-x="1664" d="M1023 349l102 -204q-58 -179 -210 -290t-339 -111q-156 0 -288.5 77.5t-210 210t-77.5 288.5q0 181 104.5 330t274.5 211l17 -131q-122 -54 -195 -165.5t-73 -244.5q0 -185 131.5 -316.5t316.5 -131.5q126 0 232.5 65t165 175.5t49.5 236.5zM1571 249l58 -114l-256 -128 q-13 -7 -29 -7q-40 0 -57 35l-239 477h-472q-24 0 -42.5 16.5t-21.5 40.5l-96 779q-2 16 6 42q14 51 57 82.5t97 31.5q66 0 113 -47t47 -113q0 -69 -52 -117.5t-120 -41.5l37 -289h423v-128h-407l16 -128h455q40 0 57 -35l228 -455z" /> -<glyph unicode="" d="M1254 899q16 85 -21 132q-52 65 -187 45q-17 -3 -41 -12.5t-57.5 -30.5t-64.5 -48.5t-59.5 -70t-44.5 -91.5q80 7 113.5 -16t26.5 -99q-5 -52 -52 -143q-43 -78 -71 -99q-44 -32 -87 14q-23 24 -37.5 64.5t-19 73t-10 84t-8.5 71.5q-23 129 -34 164q-12 37 -35.5 69 t-50.5 40q-57 16 -127 -25q-54 -32 -136.5 -106t-122.5 -102v-7q16 -8 25.5 -26t21.5 -20q21 -3 54.5 8.5t58 10.5t41.5 -30q11 -18 18.5 -38.5t15 -48t12.5 -40.5q17 -46 53 -187q36 -146 57 -197q42 -99 103 -125q43 -12 85 -1.5t76 31.5q131 77 250 237 q104 139 172.5 292.5t82.5 226.5zM1536 1120v-960q0 -119 -84.5 -203.5t-203.5 -84.5h-960q-119 0 -203.5 84.5t-84.5 203.5v960q0 119 84.5 203.5t203.5 84.5h960q119 0 203.5 -84.5t84.5 -203.5z" /> -<glyph unicode="" horiz-adv-x="1152" d="M1152 704q0 -191 -94.5 -353t-256.5 -256.5t-353 -94.5h-160q-14 0 -23 9t-9 23v611l-215 -66q-3 -1 -9 -1q-10 0 -19 6q-13 10 -13 26v128q0 23 23 31l233 71v93l-215 -66q-3 -1 -9 -1q-10 0 -19 6q-13 10 -13 26v128q0 23 23 31l233 71v250q0 14 9 23t23 9h160 q14 0 23 -9t9 -23v-181l375 116q15 5 28 -5t13 -26v-128q0 -23 -23 -31l-393 -121v-93l375 116q15 5 28 -5t13 -26v-128q0 -23 -23 -31l-393 -121v-487q188 13 318 151t130 328q0 14 9 23t23 9h160q14 0 23 -9t9 -23z" /> -<glyph unicode="" horiz-adv-x="1408" d="M1152 736v-64q0 -14 -9 -23t-23 -9h-352v-352q0 -14 -9 -23t-23 -9h-64q-14 0 -23 9t-9 23v352h-352q-14 0 -23 9t-9 23v64q0 14 9 23t23 9h352v352q0 14 9 23t23 9h64q14 0 23 -9t9 -23v-352h352q14 0 23 -9t9 -23zM1280 288v832q0 66 -47 113t-113 47h-832 q-66 0 -113 -47t-47 -113v-832q0 -66 47 -113t113 -47h832q66 0 113 47t47 113zM1408 1120v-832q0 -119 -84.5 -203.5t-203.5 -84.5h-832q-119 0 -203.5 84.5t-84.5 203.5v832q0 119 84.5 203.5t203.5 84.5h832q119 0 203.5 -84.5t84.5 -203.5z" /> -<glyph unicode="" horiz-adv-x="2176" d="M620 416q-110 -64 -268 -64h-128v64h-64q-13 0 -22.5 23.5t-9.5 56.5q0 24 7 49q-58 2 -96.5 10.5t-38.5 20.5t38.5 20.5t96.5 10.5q-7 25 -7 49q0 33 9.5 56.5t22.5 23.5h64v64h128q158 0 268 -64h1113q42 -7 106.5 -18t80.5 -14q89 -15 150 -40.5t83.5 -47.5t22.5 -40 t-22.5 -40t-83.5 -47.5t-150 -40.5q-16 -3 -80.5 -14t-106.5 -18h-1113zM1739 668q53 -36 53 -92t-53 -92l81 -30q68 48 68 122t-68 122zM625 400h1015q-217 -38 -456 -80q-57 0 -113 -24t-83 -48l-28 -24l-288 -288q-26 -26 -70.5 -45t-89.5 -19h-96l-93 464h29 q157 0 273 64zM352 816h-29l93 464h96q46 0 90 -19t70 -45l288 -288q4 -4 11 -10.5t30.5 -23t48.5 -29t61.5 -23t72.5 -10.5l456 -80h-1015q-116 64 -273 64z" /> -<glyph unicode="" horiz-adv-x="1664" d="M1519 760q62 0 103.5 -40.5t41.5 -101.5q0 -97 -93 -130l-172 -59l56 -167q7 -21 7 -47q0 -59 -42 -102t-101 -43q-47 0 -85.5 27t-53.5 72l-55 165l-310 -106l55 -164q8 -24 8 -47q0 -59 -42 -102t-102 -43q-47 0 -85 27t-53 72l-55 163l-153 -53q-29 -9 -50 -9 q-61 0 -101.5 40t-40.5 101q0 47 27.5 85t71.5 53l156 53l-105 313l-156 -54q-26 -8 -48 -8q-60 0 -101 40.5t-41 100.5q0 47 27.5 85t71.5 53l157 53l-53 159q-8 24 -8 47q0 60 42 102.5t102 42.5q47 0 85 -27t53 -72l54 -160l310 105l-54 160q-8 24 -8 47q0 59 42.5 102 t101.5 43q47 0 85.5 -27.5t53.5 -71.5l53 -161l162 55q21 6 43 6q60 0 102.5 -39.5t42.5 -98.5q0 -45 -30 -81.5t-74 -51.5l-157 -54l105 -316l164 56q24 8 46 8zM725 498l310 105l-105 315l-310 -107z" /> -<glyph unicode="" d="M1248 1408q119 0 203.5 -84.5t84.5 -203.5v-960q0 -119 -84.5 -203.5t-203.5 -84.5h-960q-119 0 -203.5 84.5t-84.5 203.5v960q0 119 84.5 203.5t203.5 84.5h960zM1280 352v436q-31 -35 -64 -55q-34 -22 -132.5 -85t-151.5 -99q-98 -69 -164 -69v0v0q-66 0 -164 69 q-46 32 -141.5 92.5t-142.5 92.5q-12 8 -33 27t-31 27v-436q0 -40 28 -68t68 -28h832q40 0 68 28t28 68zM1280 925q0 41 -27.5 70t-68.5 29h-832q-40 0 -68 -28t-28 -68q0 -37 30.5 -76.5t67.5 -64.5q47 -32 137.5 -89t129.5 -83q3 -2 17 -11.5t21 -14t21 -13t23.5 -13 t21.5 -9.5t22.5 -7.5t20.5 -2.5t20.5 2.5t22.5 7.5t21.5 9.5t23.5 13t21 13t21 14t17 11.5l267 174q35 23 66.5 62.5t31.5 73.5z" /> -<glyph unicode="" horiz-adv-x="1792" d="M127 640q0 163 67 313l367 -1005q-196 95 -315 281t-119 411zM1415 679q0 -19 -2.5 -38.5t-10 -49.5t-11.5 -44t-17.5 -59t-17.5 -58l-76 -256l-278 826q46 3 88 8q19 2 26 18.5t-2.5 31t-28.5 13.5l-205 -10q-75 1 -202 10q-12 1 -20.5 -5t-11.5 -15t-1.5 -18.5t9 -16.5 t19.5 -8l80 -8l120 -328l-168 -504l-280 832q46 3 88 8q19 2 26 18.5t-2.5 31t-28.5 13.5l-205 -10q-7 0 -23 0.5t-26 0.5q105 160 274.5 253.5t367.5 93.5q147 0 280.5 -53t238.5 -149h-10q-55 0 -92 -40.5t-37 -95.5q0 -12 2 -24t4 -21.5t8 -23t9 -21t12 -22.5t12.5 -21 t14.5 -24t14 -23q63 -107 63 -212zM909 573l237 -647q1 -6 5 -11q-126 -44 -255 -44q-112 0 -217 32zM1570 1009q95 -174 95 -369q0 -209 -104 -385.5t-279 -278.5l235 678q59 169 59 276q0 42 -6 79zM896 1536q182 0 348 -71t286 -191t191 -286t71 -348t-71 -348t-191 -286 t-286 -191t-348 -71t-348 71t-286 191t-191 286t-71 348t71 348t191 286t286 191t348 71zM896 -215q173 0 331.5 68t273 182.5t182.5 273t68 331.5t-68 331.5t-182.5 273t-273 182.5t-331.5 68t-331.5 -68t-273 -182.5t-182.5 -273t-68 -331.5t68 -331.5t182.5 -273 t273 -182.5t331.5 -68z" /> -<glyph unicode="" horiz-adv-x="1792" d="M1086 1536v-1536l-272 -128q-228 20 -414 102t-293 208.5t-107 272.5q0 140 100.5 263.5t275 205.5t391.5 108v-172q-217 -38 -356.5 -150t-139.5 -255q0 -152 154.5 -267t388.5 -145v1360zM1755 954l37 -390l-525 114l147 83q-119 70 -280 99v172q277 -33 481 -157z" /> -<glyph unicode="" horiz-adv-x="2048" d="M960 1536l960 -384v-128h-128q0 -26 -20.5 -45t-48.5 -19h-1526q-28 0 -48.5 19t-20.5 45h-128v128zM256 896h256v-768h128v768h256v-768h128v768h256v-768h128v768h256v-768h59q28 0 48.5 -19t20.5 -45v-64h-1664v64q0 26 20.5 45t48.5 19h59v768zM1851 -64 q28 0 48.5 -19t20.5 -45v-128h-1920v128q0 26 20.5 45t48.5 19h1782z" /> -<glyph unicode="" horiz-adv-x="2304" d="M1774 700l18 -316q4 -69 -82 -128t-235 -93.5t-323 -34.5t-323 34.5t-235 93.5t-82 128l18 316l574 -181q22 -7 48 -7t48 7zM2304 1024q0 -23 -22 -31l-1120 -352q-4 -1 -10 -1t-10 1l-652 206q-43 -34 -71 -111.5t-34 -178.5q63 -36 63 -109q0 -69 -58 -107l58 -433 q2 -14 -8 -25q-9 -11 -24 -11h-192q-15 0 -24 11q-10 11 -8 25l58 433q-58 38 -58 107q0 73 65 111q11 207 98 330l-333 104q-22 8 -22 31t22 31l1120 352q4 1 10 1t10 -1l1120 -352q22 -8 22 -31z" /> -<glyph unicode="" d="M859 579l13 -707q-62 11 -105 11q-41 0 -105 -11l13 707q-40 69 -168.5 295.5t-216.5 374.5t-181 287q58 -15 108 -15q43 0 111 15q63 -111 133.5 -229.5t167 -276.5t138.5 -227q37 61 109.5 177.5t117.5 190t105 176t107 189.5q54 -14 107 -14q56 0 114 14v0 q-28 -39 -60 -88.5t-49.5 -78.5t-56.5 -96t-49 -84q-146 -248 -353 -610z" /> -<glyph unicode="" horiz-adv-x="1280" d="M981 197q0 25 -7 49t-14.5 42t-27 41.5t-29.5 35t-38.5 34.5t-36.5 29t-41.5 30t-36.5 26q-16 2 -49 2q-53 0 -104.5 -7t-107 -25t-97 -46t-68.5 -74.5t-27 -105.5q0 -56 23.5 -102t61 -75.5t87 -50t100 -29t101.5 -8.5q58 0 111.5 13t99 39t73 73t27.5 109zM864 1055 q0 59 -17 125.5t-48 129t-84 103.5t-117 41q-42 0 -82.5 -19.5t-66.5 -52.5q-46 -59 -46 -160q0 -46 10 -97.5t31.5 -103t52 -92.5t75 -67t96.5 -26q37 0 77.5 16.5t65.5 43.5q53 56 53 159zM752 1536h417l-137 -88h-132q75 -63 113 -133t38 -160q0 -72 -24.5 -129.5 t-59.5 -93t-69.5 -65t-59 -61.5t-24.5 -66q0 -36 32 -70.5t77 -68t90.5 -73.5t77.5 -104t32 -142q0 -91 -49 -173q-71 -122 -209.5 -179.5t-298.5 -57.5q-132 0 -246.5 41.5t-172.5 137.5q-36 59 -36 131q0 81 44.5 150t118.5 115q131 82 404 100q-32 41 -47.5 73.5 t-15.5 73.5q0 40 21 85q-46 -4 -68 -4q-148 0 -249.5 96.5t-101.5 244.5q0 82 36 159t99 131q76 66 182 98t218 32z" /> -<glyph unicode="" horiz-adv-x="2304" d="M1509 107q0 -14 -12 -29q-52 -59 -147.5 -83t-196.5 -24q-252 0 -346 107q-12 15 -12 29q0 17 12 29.5t29 12.5q15 0 30 -12q58 -49 125.5 -66t159.5 -17t160 17t127 66q15 12 30 12q17 0 29 -12.5t12 -29.5zM978 498q0 -61 -43 -104t-104 -43q-60 0 -104.5 43.5 t-44.5 103.5q0 61 44 105t105 44t104 -44t43 -105zM1622 498q0 -61 -43 -104t-104 -43q-60 0 -104.5 43.5t-44.5 103.5q0 61 44 105t105 44t104 -44t43 -105zM415 793q-39 27 -88 27q-66 0 -113 -47t-47 -113q0 -72 54 -121q53 141 194 254zM2020 382q0 222 -249 387 q-128 85 -291.5 126.5t-331.5 41.5t-331.5 -41.5t-292.5 -126.5q-249 -165 -249 -387t249 -387q129 -85 292.5 -126.5t331.5 -41.5t331.5 41.5t291.5 126.5q249 165 249 387zM2137 660q0 66 -47 113t-113 47q-50 0 -93 -30q140 -114 192 -256q61 48 61 126zM1993 1335 q0 49 -34.5 83.5t-82.5 34.5q-49 0 -83.5 -34.5t-34.5 -83.5q0 -48 34.5 -82.5t83.5 -34.5q48 0 82.5 34.5t34.5 82.5zM2220 660q0 -65 -33 -122t-89 -90q5 -35 5 -66q0 -139 -79 -255.5t-208 -201.5q-140 -92 -313.5 -136.5t-354.5 -44.5t-355 44.5t-314 136.5 q-129 85 -208 201.5t-79 255.5q0 36 6 71q-53 33 -83.5 88.5t-30.5 118.5q0 100 71 171.5t172 71.5q91 0 159 -60q265 170 638 177l144 456q10 29 40 29q24 0 384 -90q24 55 74 88t110 33q82 0 141 -59t59 -142t-59 -141.5t-141 -58.5q-83 0 -141.5 58.5t-59.5 140.5 l-339 80l-125 -395q349 -15 603 -179q71 63 163 63q101 0 172 -71.5t71 -171.5z" /> -<glyph unicode="" d="M950 393q7 7 17.5 7t17.5 -7t7 -18t-7 -18q-65 -64 -208 -64h-1h-1q-143 0 -207 64q-8 7 -8 18t8 18q7 7 17.5 7t17.5 -7q49 -51 172 -51h1h1q122 0 173 51zM671 613q0 -37 -26 -64t-63 -27t-63 27t-26 64t26 63t63 26t63 -26t26 -63zM1214 1049q-29 0 -50 21t-21 50 q0 30 21 51t50 21q30 0 51 -21t21 -51q0 -29 -21 -50t-51 -21zM1216 1408q132 0 226 -94t94 -227v-894q0 -133 -94 -227t-226 -94h-896q-132 0 -226 94t-94 227v894q0 133 94 227t226 94h896zM1321 596q35 14 57 45.5t22 70.5q0 51 -36 87.5t-87 36.5q-60 0 -98 -48 q-151 107 -375 115l83 265l206 -49q1 -50 36.5 -85t84.5 -35q50 0 86 35.5t36 85.5t-36 86t-86 36q-36 0 -66 -20.5t-45 -53.5l-227 54q-9 2 -17.5 -2.5t-11.5 -14.5l-95 -302q-224 -4 -381 -113q-36 43 -93 43q-51 0 -87 -36.5t-36 -87.5q0 -37 19.5 -67.5t52.5 -45.5 q-7 -25 -7 -54q0 -98 74 -181.5t201.5 -132t278.5 -48.5q150 0 277.5 48.5t201.5 132t74 181.5q0 27 -6 54zM971 702q37 0 63 -26t26 -63t-26 -64t-63 -27t-63 27t-26 64t26 63t63 26z" /> -<glyph unicode="" d="M866 697l90 27v62q0 79 -58 135t-138 56t-138 -55.5t-58 -134.5v-283q0 -20 -14 -33.5t-33 -13.5t-32.5 13.5t-13.5 33.5v120h-151v-122q0 -82 57.5 -139t139.5 -57q81 0 138.5 56.5t57.5 136.5v280q0 19 13.5 33t33.5 14q19 0 32.5 -14t13.5 -33v-54zM1199 502v122h-150 v-126q0 -20 -13.5 -33.5t-33.5 -13.5q-19 0 -32.5 14t-13.5 33v123l-90 -26l-60 28v-123q0 -80 58 -137t139 -57t138.5 57t57.5 139zM1536 640q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103 t385.5 -103t279.5 -279.5t103 -385.5z" /> -<glyph unicode="" horiz-adv-x="1920" d="M1062 824v118q0 42 -30 72t-72 30t-72 -30t-30 -72v-612q0 -175 -126 -299t-303 -124q-178 0 -303.5 125.5t-125.5 303.5v266h328v-262q0 -43 30 -72.5t72 -29.5t72 29.5t30 72.5v620q0 171 126.5 292t301.5 121q176 0 302 -122t126 -294v-136l-195 -58zM1592 602h328 v-266q0 -178 -125.5 -303.5t-303.5 -125.5q-177 0 -303 124.5t-126 300.5v268l131 -61l195 58v-270q0 -42 30 -71.5t72 -29.5t72 29.5t30 71.5v275z" /> -<glyph unicode="" d="M1472 160v480h-704v704h-480q-93 0 -158.5 -65.5t-65.5 -158.5v-480h704v-704h480q93 0 158.5 65.5t65.5 158.5zM1536 1120v-960q0 -119 -84.5 -203.5t-203.5 -84.5h-960q-119 0 -203.5 84.5t-84.5 203.5v960q0 119 84.5 203.5t203.5 84.5h960q119 0 203.5 -84.5 t84.5 -203.5z" /> -<glyph unicode="" horiz-adv-x="2048" d="M328 1254h204v-983h-532v697h328v286zM328 435v369h-123v-369h123zM614 968v-697h205v697h-205zM614 1254v-204h205v204h-205zM901 968h533v-942h-533v163h328v82h-328v697zM1229 435v369h-123v-369h123zM1516 968h532v-942h-532v163h327v82h-327v697zM1843 435v369h-123 v-369h123z" /> -<glyph unicode="" d="M1046 516q0 -64 -38 -109t-91 -45q-43 0 -70 15v277q28 17 70 17q53 0 91 -45.5t38 -109.5zM703 944q0 -64 -38 -109.5t-91 -45.5q-43 0 -70 15v277q28 17 70 17q53 0 91 -45t38 -109zM1265 513q0 134 -88 229t-213 95q-20 0 -39 -3q-23 -78 -78 -136q-87 -95 -211 -101 v-636l211 41v206q51 -19 117 -19q125 0 213 95t88 229zM922 940q0 134 -88.5 229t-213.5 95q-74 0 -141 -36h-186v-840l211 41v206q55 -19 116 -19q125 0 213.5 95t88.5 229zM1536 1120v-960q0 -119 -84.5 -203.5t-203.5 -84.5h-960q-119 0 -203.5 84.5t-84.5 203.5v960 q0 119 84.5 203.5t203.5 84.5h960q119 0 203.5 -84.5t84.5 -203.5z" /> -<glyph unicode="" horiz-adv-x="2038" d="M1222 607q75 3 143.5 -20.5t118 -58.5t101 -94.5t84 -108t75.5 -120.5q33 -56 78.5 -109t75.5 -80.5t99 -88.5q-48 -30 -108.5 -57.5t-138.5 -59t-114 -47.5q-44 37 -74 115t-43.5 164.5t-33 180.5t-42.5 168.5t-72.5 123t-122.5 48.5l-10 -2l-6 -4q4 -5 13 -14 q6 -5 28 -23.5t25.5 -22t19 -18t18 -20.5t11.5 -21t10.5 -27.5t4.5 -31t4 -40.5l1 -33q1 -26 -2.5 -57.5t-7.5 -52t-12.5 -58.5t-11.5 -53q-35 1 -101 -9.5t-98 -10.5q-39 0 -72 10q-2 16 -2 47q0 74 3 96q2 13 31.5 41.5t57 59t26.5 51.5q-24 2 -43 -24 q-36 -53 -111.5 -99.5t-136.5 -46.5q-25 0 -75.5 63t-106.5 139.5t-84 96.5q-6 4 -27 30q-482 -112 -513 -112q-16 0 -28 11t-12 27q0 15 8.5 26.5t22.5 14.5l486 106q-8 14 -8 25t5.5 17.5t16 11.5t20 7t23 4.5t18.5 4.5q4 1 15.5 7.5t17.5 6.5q15 0 28 -16t20 -33 q163 37 172 37q17 0 29.5 -11t12.5 -28q0 -15 -8.5 -26t-23.5 -14l-182 -40l-1 -16q-1 -26 81.5 -117.5t104.5 -91.5q47 0 119 80t72 129q0 36 -23.5 53t-51 18.5t-51 11.5t-23.5 34q0 16 10 34l-68 19q43 44 43 117q0 26 -5 58q82 16 144 16q44 0 71.5 -1.5t48.5 -8.5 t31 -13.5t20.5 -24.5t15.5 -33.5t17 -47.5t24 -60l50 25q-3 -40 -23 -60t-42.5 -21t-40 -6.5t-16.5 -20.5zM1282 842q-5 5 -13.5 15.5t-12 14.5t-10.5 11.5t-10 10.5l-8 8t-8.5 7.5t-8 5t-8.5 4.5q-7 3 -14.5 5t-20.5 2.5t-22 0.5h-32.5h-37.5q-126 0 -217 -43 q16 30 36 46.5t54 29.5t65.5 36t46 36.5t50 55t43.5 50.5q12 -9 28 -31.5t32 -36.5t38 -13l12 1v-76l22 -1q247 95 371 190q28 21 50 39t42.5 37.5t33 31t29.5 34t24 31t24.5 37t23 38t27 47.5t29.5 53l7 9q-2 -53 -43 -139q-79 -165 -205 -264t-306 -142q-14 -3 -42 -7.5 t-50 -9.5t-39 -14q3 -19 24.5 -46t21.5 -34q0 -11 -26 -30zM1061 -79q39 26 131.5 47.5t146.5 21.5q9 0 22.5 -15.5t28 -42.5t26 -50t24 -51t14.5 -33q-121 -45 -244 -45q-61 0 -125 11zM822 568l48 12l109 -177l-73 -48zM1323 51q3 -15 3 -16q0 -7 -17.5 -14.5t-46 -13 t-54 -9.5t-53.5 -7.5t-32 -4.5l-7 43q21 2 60.5 8.5t72 10t60.5 3.5h14zM866 679l-96 -20l-6 17q10 1 32.5 7t34.5 6q19 0 35 -10zM1061 45h31l10 -83l-41 -12v95zM1950 1535v1v-1zM1950 1535l-1 -5l-2 -2l1 3zM1950 1535l1 1z" /> -<glyph unicode="" d="M1167 -50q-5 19 -24 5q-30 -22 -87 -39t-131 -17q-129 0 -193 49q-5 4 -13 4q-11 0 -26 -12q-7 -6 -7.5 -16t7.5 -20q34 -32 87.5 -46t102.5 -12.5t99 4.5q41 4 84.5 20.5t65 30t28.5 20.5q12 12 7 29zM1128 65q-19 47 -39 61q-23 15 -76 15q-47 0 -71 -10 q-29 -12 -78 -56q-26 -24 -12 -44q9 -8 17.5 -4.5t31.5 23.5q3 2 10.5 8.5t10.5 8.5t10 7t11.5 7t12.5 5t15 4.5t16.5 2.5t20.5 1q27 0 44.5 -7.5t23 -14.5t13.5 -22q10 -17 12.5 -20t12.5 1q23 12 14 34zM1483 346q0 22 -5 44.5t-16.5 45t-34 36.5t-52.5 14 q-33 0 -97 -41.5t-129 -83.5t-101 -42q-27 -1 -63.5 19t-76 49t-83.5 58t-100 49t-111 19q-115 -1 -197 -78.5t-84 -178.5q-2 -112 74 -164q29 -20 62.5 -28.5t103.5 -8.5q57 0 132 32.5t134 71t120 70.5t93 31q26 -1 65 -31.5t71.5 -67t68 -67.5t55.5 -32q35 -3 58.5 14 t55.5 63q28 41 42.5 101t14.5 106zM1536 506q0 -164 -62 -304.5t-166 -236t-242.5 -149.5t-290.5 -54t-293 57.5t-247.5 157t-170.5 241.5t-64 302q0 89 19.5 172.5t49 145.5t70.5 118.5t78.5 94t78.5 69.5t64.5 46.5t42.5 24.5q14 8 51 26.5t54.5 28.5t48 30t60.5 44 q36 28 58 72.5t30 125.5q129 -155 186 -193q44 -29 130 -68t129 -66q21 -13 39 -25t60.5 -46.5t76 -70.5t75 -95t69 -122t47 -148.5t19.5 -177.5z" /> -<glyph unicode="" d="M1070 463l-160 -160l-151 -152l-30 -30q-65 -64 -151.5 -87t-171.5 -2q-16 -70 -72 -115t-129 -45q-85 0 -145 60.5t-60 145.5q0 72 44.5 128t113.5 72q-22 86 1 173t88 152l12 12l151 -152l-11 -11q-37 -37 -37 -89t37 -90q37 -37 89 -37t89 37l30 30l151 152l161 160z M729 1145l12 -12l-152 -152l-12 12q-37 37 -89 37t-89 -37t-37 -89.5t37 -89.5l29 -29l152 -152l160 -160l-151 -152l-161 160l-151 152l-30 30q-68 67 -90 159.5t5 179.5q-70 15 -115 71t-45 129q0 85 60 145.5t145 60.5q76 0 133.5 -49t69.5 -123q84 20 169.5 -3.5 t149.5 -87.5zM1536 78q0 -85 -60 -145.5t-145 -60.5q-74 0 -131 47t-71 118q-86 -28 -179.5 -6t-161.5 90l-11 12l151 152l12 -12q37 -37 89 -37t89 37t37 89t-37 89l-30 30l-152 152l-160 160l152 152l160 -160l152 -152l29 -30q64 -64 87.5 -150.5t2.5 -171.5 q76 -11 126.5 -68.5t50.5 -134.5zM1534 1202q0 -77 -51 -135t-127 -69q26 -85 3 -176.5t-90 -158.5l-12 -12l-151 152l12 12q37 37 37 89t-37 89t-89 37t-89 -37l-30 -30l-152 -152l-160 -160l-152 152l161 160l152 152l29 30q67 67 159 89.5t178 -3.5q11 75 68.5 126 t135.5 51q85 0 145 -60.5t60 -145.5z" /> -<glyph unicode="" d="M654 458q-1 -3 -12.5 0.5t-31.5 11.5l-20 9q-44 20 -87 49q-7 5 -41 31.5t-38 28.5q-67 -103 -134 -181q-81 -95 -105 -110q-4 -2 -19.5 -4t-18.5 0q6 4 82 92q21 24 85.5 115t78.5 118q17 30 51 98.5t36 77.5q-8 1 -110 -33q-8 -2 -27.5 -7.5t-34.5 -9.5t-17 -5 q-2 -2 -2 -10.5t-1 -9.5q-5 -10 -31 -15q-23 -7 -47 0q-18 4 -28 21q-4 6 -5 23q6 2 24.5 5t29.5 6q58 16 105 32q100 35 102 35q10 2 43 19.5t44 21.5q9 3 21.5 8t14.5 5.5t6 -0.5q2 -12 -1 -33q0 -2 -12.5 -27t-26.5 -53.5t-17 -33.5q-25 -50 -77 -131l64 -28 q12 -6 74.5 -32t67.5 -28q4 -1 10.5 -25.5t4.5 -30.5zM449 944q3 -15 -4 -28q-12 -23 -50 -38q-30 -12 -60 -12q-26 3 -49 26q-14 15 -18 41l1 3q3 -3 19.5 -5t26.5 0t58 16q36 12 55 14q17 0 21 -17zM1147 815l63 -227l-139 42zM39 15l694 232v1032l-694 -233v-1031z M1280 332l102 -31l-181 657l-100 31l-216 -536l102 -31l45 110l211 -65zM777 1294l573 -184v380zM1088 -29l158 -13l-54 -160l-40 66q-130 -83 -276 -108q-58 -12 -91 -12h-84q-79 0 -199.5 39t-183.5 85q-8 7 -8 16q0 8 5 13.5t13 5.5q4 0 18 -7.5t30.5 -16.5t20.5 -11 q73 -37 159.5 -61.5t157.5 -24.5q95 0 167 14.5t157 50.5q15 7 30.5 15.5t34 19t28.5 16.5zM1536 1050v-1079l-774 246q-14 -6 -375 -127.5t-368 -121.5q-13 0 -18 13q0 1 -1 3v1078q3 9 4 10q5 6 20 11q106 35 149 50v384l558 -198q2 0 160.5 55t316 108.5t161.5 53.5 q20 0 20 -21v-418z" /> -<glyph unicode="" horiz-adv-x="1792" d="M288 1152q66 0 113 -47t47 -113v-1088q0 -66 -47 -113t-113 -47h-128q-66 0 -113 47t-47 113v1088q0 66 47 113t113 47h128zM1664 989q58 -34 93 -93t35 -128v-768q0 -106 -75 -181t-181 -75h-864q-66 0 -113 47t-47 113v1536q0 40 28 68t68 28h672q40 0 88 -20t76 -48 l152 -152q28 -28 48 -76t20 -88v-163zM928 0v128q0 14 -9 23t-23 9h-128q-14 0 -23 -9t-9 -23v-128q0 -14 9 -23t23 -9h128q14 0 23 9t9 23zM928 256v128q0 14 -9 23t-23 9h-128q-14 0 -23 -9t-9 -23v-128q0 -14 9 -23t23 -9h128q14 0 23 9t9 23zM928 512v128q0 14 -9 23 t-23 9h-128q-14 0 -23 -9t-9 -23v-128q0 -14 9 -23t23 -9h128q14 0 23 9t9 23zM1184 0v128q0 14 -9 23t-23 9h-128q-14 0 -23 -9t-9 -23v-128q0 -14 9 -23t23 -9h128q14 0 23 9t9 23zM1184 256v128q0 14 -9 23t-23 9h-128q-14 0 -23 -9t-9 -23v-128q0 -14 9 -23t23 -9h128 q14 0 23 9t9 23zM1184 512v128q0 14 -9 23t-23 9h-128q-14 0 -23 -9t-9 -23v-128q0 -14 9 -23t23 -9h128q14 0 23 9t9 23zM1440 0v128q0 14 -9 23t-23 9h-128q-14 0 -23 -9t-9 -23v-128q0 -14 9 -23t23 -9h128q14 0 23 9t9 23zM1440 256v128q0 14 -9 23t-23 9h-128 q-14 0 -23 -9t-9 -23v-128q0 -14 9 -23t23 -9h128q14 0 23 9t9 23zM1440 512v128q0 14 -9 23t-23 9h-128q-14 0 -23 -9t-9 -23v-128q0 -14 9 -23t23 -9h128q14 0 23 9t9 23zM1536 896v256h-160q-40 0 -68 28t-28 68v160h-640v-512h896z" /> -<glyph unicode="" d="M1344 1536q26 0 45 -19t19 -45v-1664q0 -26 -19 -45t-45 -19h-1280q-26 0 -45 19t-19 45v1664q0 26 19 45t45 19h1280zM512 1248v-64q0 -14 9 -23t23 -9h64q14 0 23 9t9 23v64q0 14 -9 23t-23 9h-64q-14 0 -23 -9t-9 -23zM512 992v-64q0 -14 9 -23t23 -9h64q14 0 23 9 t9 23v64q0 14 -9 23t-23 9h-64q-14 0 -23 -9t-9 -23zM512 736v-64q0 -14 9 -23t23 -9h64q14 0 23 9t9 23v64q0 14 -9 23t-23 9h-64q-14 0 -23 -9t-9 -23zM512 480v-64q0 -14 9 -23t23 -9h64q14 0 23 9t9 23v64q0 14 -9 23t-23 9h-64q-14 0 -23 -9t-9 -23zM384 160v64 q0 14 -9 23t-23 9h-64q-14 0 -23 -9t-9 -23v-64q0 -14 9 -23t23 -9h64q14 0 23 9t9 23zM384 416v64q0 14 -9 23t-23 9h-64q-14 0 -23 -9t-9 -23v-64q0 -14 9 -23t23 -9h64q14 0 23 9t9 23zM384 672v64q0 14 -9 23t-23 9h-64q-14 0 -23 -9t-9 -23v-64q0 -14 9 -23t23 -9h64 q14 0 23 9t9 23zM384 928v64q0 14 -9 23t-23 9h-64q-14 0 -23 -9t-9 -23v-64q0 -14 9 -23t23 -9h64q14 0 23 9t9 23zM384 1184v64q0 14 -9 23t-23 9h-64q-14 0 -23 -9t-9 -23v-64q0 -14 9 -23t23 -9h64q14 0 23 9t9 23zM896 -96v192q0 14 -9 23t-23 9h-320q-14 0 -23 -9 t-9 -23v-192q0 -14 9 -23t23 -9h320q14 0 23 9t9 23zM896 416v64q0 14 -9 23t-23 9h-64q-14 0 -23 -9t-9 -23v-64q0 -14 9 -23t23 -9h64q14 0 23 9t9 23zM896 672v64q0 14 -9 23t-23 9h-64q-14 0 -23 -9t-9 -23v-64q0 -14 9 -23t23 -9h64q14 0 23 9t9 23zM896 928v64 q0 14 -9 23t-23 9h-64q-14 0 -23 -9t-9 -23v-64q0 -14 9 -23t23 -9h64q14 0 23 9t9 23zM896 1184v64q0 14 -9 23t-23 9h-64q-14 0 -23 -9t-9 -23v-64q0 -14 9 -23t23 -9h64q14 0 23 9t9 23zM1152 160v64q0 14 -9 23t-23 9h-64q-14 0 -23 -9t-9 -23v-64q0 -14 9 -23t23 -9h64 q14 0 23 9t9 23zM1152 416v64q0 14 -9 23t-23 9h-64q-14 0 -23 -9t-9 -23v-64q0 -14 9 -23t23 -9h64q14 0 23 9t9 23zM1152 672v64q0 14 -9 23t-23 9h-64q-14 0 -23 -9t-9 -23v-64q0 -14 9 -23t23 -9h64q14 0 23 9t9 23zM1152 928v64q0 14 -9 23t-23 9h-64q-14 0 -23 -9 t-9 -23v-64q0 -14 9 -23t23 -9h64q14 0 23 9t9 23zM1152 1184v64q0 14 -9 23t-23 9h-64q-14 0 -23 -9t-9 -23v-64q0 -14 9 -23t23 -9h64q14 0 23 9t9 23z" /> -<glyph unicode="" horiz-adv-x="1280" d="M1188 988l-292 -292v-824q0 -46 -33 -79t-79 -33t-79 33t-33 79v384h-64v-384q0 -46 -33 -79t-79 -33t-79 33t-33 79v824l-292 292q-28 28 -28 68t28 68t68 28t68 -28l228 -228h368l228 228q28 28 68 28t68 -28t28 -68t-28 -68zM864 1152q0 -93 -65.5 -158.5 t-158.5 -65.5t-158.5 65.5t-65.5 158.5t65.5 158.5t158.5 65.5t158.5 -65.5t65.5 -158.5z" /> -<glyph unicode="" horiz-adv-x="1664" d="M780 1064q0 -60 -19 -113.5t-63 -92.5t-105 -39q-76 0 -138 57.5t-92 135.5t-30 151q0 60 19 113.5t63 92.5t105 39q77 0 138.5 -57.5t91.5 -135t30 -151.5zM438 581q0 -80 -42 -139t-119 -59q-76 0 -141.5 55.5t-100.5 133.5t-35 152q0 80 42 139.5t119 59.5 q76 0 141.5 -55.5t100.5 -134t35 -152.5zM832 608q118 0 255 -97.5t229 -237t92 -254.5q0 -46 -17 -76.5t-48.5 -45t-64.5 -20t-76 -5.5q-68 0 -187.5 45t-182.5 45q-66 0 -192.5 -44.5t-200.5 -44.5q-183 0 -183 146q0 86 56 191.5t139.5 192.5t187.5 146t193 59zM1071 819 q-61 0 -105 39t-63 92.5t-19 113.5q0 74 30 151.5t91.5 135t138.5 57.5q61 0 105 -39t63 -92.5t19 -113.5q0 -73 -30 -151t-92 -135.5t-138 -57.5zM1503 923q77 0 119 -59.5t42 -139.5q0 -74 -35 -152t-100.5 -133.5t-141.5 -55.5q-77 0 -119 59t-42 139q0 74 35 152.5 t100.5 134t141.5 55.5z" /> -<glyph unicode="" horiz-adv-x="768" d="M704 1008q0 -145 -57 -243.5t-152 -135.5l45 -821q2 -26 -16 -45t-44 -19h-192q-26 0 -44 19t-16 45l45 821q-95 37 -152 135.5t-57 243.5q0 128 42.5 249.5t117.5 200t160 78.5t160 -78.5t117.5 -200t42.5 -249.5z" /> -<glyph unicode="" horiz-adv-x="1792" d="M896 -93l640 349v636l-640 -233v-752zM832 772l698 254l-698 254l-698 -254zM1664 1024v-768q0 -35 -18 -65t-49 -47l-704 -384q-28 -16 -61 -16t-61 16l-704 384q-31 17 -49 47t-18 65v768q0 40 23 73t61 47l704 256q22 8 44 8t44 -8l704 -256q38 -14 61 -47t23 -73z " /> -<glyph unicode="" horiz-adv-x="2304" d="M640 -96l384 192v314l-384 -164v-342zM576 358l404 173l-404 173l-404 -173zM1664 -96l384 192v314l-384 -164v-342zM1600 358l404 173l-404 173l-404 -173zM1152 651l384 165v266l-384 -164v-267zM1088 1030l441 189l-441 189l-441 -189zM2176 512v-416q0 -36 -19 -67 t-52 -47l-448 -224q-25 -14 -57 -14t-57 14l-448 224q-5 2 -7 4q-2 -2 -7 -4l-448 -224q-25 -14 -57 -14t-57 14l-448 224q-33 16 -52 47t-19 67v416q0 38 21.5 70t56.5 48l434 186v400q0 38 21.5 70t56.5 48l448 192q23 10 50 10t50 -10l448 -192q35 -16 56.5 -48t21.5 -70 v-400l434 -186q36 -16 57 -48t21 -70z" /> -<glyph unicode="" horiz-adv-x="2048" d="M1848 1197h-511v-124h511v124zM1596 771q-90 0 -146 -52.5t-62 -142.5h408q-18 195 -200 195zM1612 186q63 0 122 32t76 87h221q-100 -307 -427 -307q-214 0 -340.5 132t-126.5 347q0 208 130.5 345.5t336.5 137.5q138 0 240.5 -68t153 -179t50.5 -248q0 -17 -2 -47h-658 q0 -111 57.5 -171.5t166.5 -60.5zM277 236h296q205 0 205 167q0 180 -199 180h-302v-347zM277 773h281q78 0 123.5 36.5t45.5 113.5q0 144 -190 144h-260v-294zM0 1282h594q87 0 155 -14t126.5 -47.5t90 -96.5t31.5 -154q0 -181 -172 -263q114 -32 172 -115t58 -204 q0 -75 -24.5 -136.5t-66 -103.5t-98.5 -71t-121 -42t-134 -13h-611v1260z" /> -<glyph unicode="" d="M1248 1408q119 0 203.5 -84.5t84.5 -203.5v-960q0 -119 -84.5 -203.5t-203.5 -84.5h-960q-119 0 -203.5 84.5t-84.5 203.5v960q0 119 84.5 203.5t203.5 84.5h960zM499 1041h-371v-787h382q117 0 197 57.5t80 170.5q0 158 -143 200q107 52 107 164q0 57 -19.5 96.5 t-56.5 60.5t-79 29.5t-97 8.5zM477 723h-176v184h163q119 0 119 -90q0 -94 -106 -94zM486 388h-185v217h189q124 0 124 -113q0 -104 -128 -104zM1136 356q-68 0 -104 38t-36 107h411q1 10 1 30q0 132 -74.5 220.5t-203.5 88.5q-128 0 -210 -86t-82 -216q0 -135 79 -217 t213 -82q205 0 267 191h-138q-11 -34 -47.5 -54t-75.5 -20zM1126 722q113 0 124 -122h-254q4 56 39 89t91 33zM964 988h319v-77h-319v77z" /> -<glyph unicode="" horiz-adv-x="1792" d="M1582 954q0 -101 -71.5 -172.5t-172.5 -71.5t-172.5 71.5t-71.5 172.5t71.5 172.5t172.5 71.5t172.5 -71.5t71.5 -172.5zM812 212q0 104 -73 177t-177 73q-27 0 -54 -6l104 -42q77 -31 109.5 -106.5t1.5 -151.5q-31 -77 -107 -109t-152 -1q-21 8 -62 24.5t-61 24.5 q32 -60 91 -96.5t130 -36.5q104 0 177 73t73 177zM1642 953q0 126 -89.5 215.5t-215.5 89.5q-127 0 -216.5 -89.5t-89.5 -215.5q0 -127 89.5 -216t216.5 -89q126 0 215.5 89t89.5 216zM1792 953q0 -189 -133.5 -322t-321.5 -133l-437 -319q-12 -129 -109 -218t-229 -89 q-121 0 -214 76t-118 192l-230 92v429l389 -157q79 48 173 48q13 0 35 -2l284 407q2 187 135.5 319t320.5 132q188 0 321.5 -133.5t133.5 -321.5z" /> -<glyph unicode="" d="M1242 889q0 80 -57 136.5t-137 56.5t-136.5 -57t-56.5 -136q0 -80 56.5 -136.5t136.5 -56.5t137 56.5t57 136.5zM632 301q0 -83 -58 -140.5t-140 -57.5q-56 0 -103 29t-72 77q52 -20 98 -40q60 -24 120 1.5t85 86.5q24 60 -1.5 120t-86.5 84l-82 33q22 5 42 5 q82 0 140 -57.5t58 -140.5zM1536 1120v-960q0 -119 -84.5 -203.5t-203.5 -84.5h-960q-119 0 -203.5 84.5t-84.5 203.5v153l172 -69q20 -92 93.5 -152t168.5 -60q104 0 181 70t87 173l345 252q150 0 255.5 105.5t105.5 254.5q0 150 -105.5 255.5t-255.5 105.5 q-148 0 -253 -104.5t-107 -252.5l-225 -322q-9 1 -28 1q-75 0 -137 -37l-297 119v468q0 119 84.5 203.5t203.5 84.5h960q119 0 203.5 -84.5t84.5 -203.5zM1289 887q0 -100 -71 -170.5t-171 -70.5t-170.5 70.5t-70.5 170.5t70.5 171t170.5 71q101 0 171.5 -70.5t70.5 -171.5z " /> -<glyph unicode="" horiz-adv-x="1792" d="M836 367l-15 -368l-2 -22l-420 29q-36 3 -67 31.5t-47 65.5q-11 27 -14.5 55t4 65t12 55t21.5 64t19 53q78 -12 509 -28zM449 953l180 -379l-147 92q-63 -72 -111.5 -144.5t-72.5 -125t-39.5 -94.5t-18.5 -63l-4 -21l-190 357q-17 26 -18 56t6 47l8 18q35 63 114 188 l-140 86zM1680 436l-188 -359q-12 -29 -36.5 -46.5t-43.5 -20.5l-18 -4q-71 -7 -219 -12l8 -164l-230 367l211 362l7 -173q170 -16 283 -5t170 33zM895 1360q-47 -63 -265 -435l-317 187l-19 12l225 356q20 31 60 45t80 10q24 -2 48.5 -12t42 -21t41.5 -33t36 -34.5 t36 -39.5t32 -35zM1550 1053l212 -363q18 -37 12.5 -76t-27.5 -74q-13 -20 -33 -37t-38 -28t-48.5 -22t-47 -16t-51.5 -14t-46 -12q-34 72 -265 436l313 195zM1407 1279l142 83l-220 -373l-419 20l151 86q-34 89 -75 166t-75.5 123.5t-64.5 80t-47 46.5l-17 13l405 -1 q31 3 58 -10.5t39 -28.5l11 -15q39 -61 112 -190z" /> -<glyph unicode="" horiz-adv-x="2048" d="M480 448q0 66 -47 113t-113 47t-113 -47t-47 -113t47 -113t113 -47t113 47t47 113zM516 768h1016l-89 357q-2 8 -14 17.5t-21 9.5h-768q-9 0 -21 -9.5t-14 -17.5zM1888 448q0 66 -47 113t-113 47t-113 -47t-47 -113t47 -113t113 -47t113 47t47 113zM2048 544v-384 q0 -14 -9 -23t-23 -9h-96v-128q0 -80 -56 -136t-136 -56t-136 56t-56 136v128h-1024v-128q0 -80 -56 -136t-136 -56t-136 56t-56 136v128h-96q-14 0 -23 9t-9 23v384q0 93 65.5 158.5t158.5 65.5h28l105 419q23 94 104 157.5t179 63.5h768q98 0 179 -63.5t104 -157.5 l105 -419h28q93 0 158.5 -65.5t65.5 -158.5z" /> -<glyph unicode="" horiz-adv-x="2048" d="M1824 640q93 0 158.5 -65.5t65.5 -158.5v-384q0 -14 -9 -23t-23 -9h-96v-64q0 -80 -56 -136t-136 -56t-136 56t-56 136v64h-1024v-64q0 -80 -56 -136t-136 -56t-136 56t-56 136v64h-96q-14 0 -23 9t-9 23v384q0 93 65.5 158.5t158.5 65.5h28l105 419q23 94 104 157.5 t179 63.5h128v224q0 14 9 23t23 9h448q14 0 23 -9t9 -23v-224h128q98 0 179 -63.5t104 -157.5l105 -419h28zM320 160q66 0 113 47t47 113t-47 113t-113 47t-113 -47t-47 -113t47 -113t113 -47zM516 640h1016l-89 357q-2 8 -14 17.5t-21 9.5h-768q-9 0 -21 -9.5t-14 -17.5z M1728 160q66 0 113 47t47 113t-47 113t-113 47t-113 -47t-47 -113t47 -113t113 -47z" /> -<glyph unicode="" d="M1504 64q0 -26 -19 -45t-45 -19h-462q1 -17 6 -87.5t5 -108.5q0 -25 -18 -42.5t-43 -17.5h-320q-25 0 -43 17.5t-18 42.5q0 38 5 108.5t6 87.5h-462q-26 0 -45 19t-19 45t19 45l402 403h-229q-26 0 -45 19t-19 45t19 45l402 403h-197q-26 0 -45 19t-19 45t19 45l384 384 q19 19 45 19t45 -19l384 -384q19 -19 19 -45t-19 -45t-45 -19h-197l402 -403q19 -19 19 -45t-19 -45t-45 -19h-229l402 -403q19 -19 19 -45z" /> -<glyph unicode="" d="M1127 326q0 32 -30 51q-193 115 -447 115q-133 0 -287 -34q-42 -9 -42 -52q0 -20 13.5 -34.5t35.5 -14.5q5 0 37 8q132 27 243 27q226 0 397 -103q19 -11 33 -11q19 0 33 13.5t14 34.5zM1223 541q0 40 -35 61q-237 141 -548 141q-153 0 -303 -42q-48 -13 -48 -64 q0 -25 17.5 -42.5t42.5 -17.5q7 0 37 8q122 33 251 33q279 0 488 -124q24 -13 38 -13q25 0 42.5 17.5t17.5 42.5zM1331 789q0 47 -40 70q-126 73 -293 110.5t-343 37.5q-204 0 -364 -47q-23 -7 -38.5 -25.5t-15.5 -48.5q0 -31 20.5 -52t51.5 -21q11 0 40 8q133 37 307 37 q159 0 309.5 -34t253.5 -95q21 -12 40 -12q29 0 50.5 20.5t21.5 51.5zM1536 640q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5t103 -385.5z" /> -<glyph unicode="" horiz-adv-x="1024" d="M1024 1233l-303 -582l24 -31h279v-415h-507l-44 -30l-142 -273l-30 -30h-301v303l303 583l-24 30h-279v415h507l44 30l142 273l30 30h301v-303z" /> -<glyph unicode="" horiz-adv-x="2304" d="M784 164l16 241l-16 523q-1 10 -7.5 17t-16.5 7q-9 0 -16 -7t-7 -17l-14 -523l14 -241q1 -10 7.5 -16.5t15.5 -6.5q22 0 24 23zM1080 193l11 211l-12 586q0 16 -13 24q-8 5 -16 5t-16 -5q-13 -8 -13 -24l-1 -6l-10 -579q0 -1 11 -236v-1q0 -10 6 -17q9 -11 23 -11 q11 0 20 9q9 7 9 20zM35 533l20 -128l-20 -126q-2 -9 -9 -9t-9 9l-17 126l17 128q2 9 9 9t9 -9zM121 612l26 -207l-26 -203q-2 -9 -10 -9q-9 0 -9 10l-23 202l23 207q0 9 9 9q8 0 10 -9zM401 159zM213 650l25 -245l-25 -237q0 -11 -11 -11q-10 0 -12 11l-21 237l21 245 q2 12 12 12q11 0 11 -12zM307 657l23 -252l-23 -244q-2 -13 -14 -13q-13 0 -13 13l-21 244l21 252q0 13 13 13q12 0 14 -13zM401 639l21 -234l-21 -246q-2 -16 -16 -16q-6 0 -10.5 4.5t-4.5 11.5l-20 246l20 234q0 6 4.5 10.5t10.5 4.5q14 0 16 -15zM784 164zM495 785 l21 -380l-21 -246q0 -7 -5 -12.5t-12 -5.5q-16 0 -18 18l-18 246l18 380q2 18 18 18q7 0 12 -5.5t5 -12.5zM589 871l19 -468l-19 -244q0 -8 -5.5 -13.5t-13.5 -5.5q-18 0 -20 19l-16 244l16 468q2 19 20 19q8 0 13.5 -5.5t5.5 -13.5zM687 911l18 -506l-18 -242 q-2 -21 -22 -21q-19 0 -21 21l-16 242l16 506q0 9 6.5 15.5t14.5 6.5q9 0 15 -6.5t7 -15.5zM1079 169v0v0zM881 915l15 -510l-15 -239q0 -10 -7.5 -17.5t-17.5 -7.5t-17 7t-8 18l-14 239l14 510q0 11 7.5 18t17.5 7t17.5 -7t7.5 -18zM980 896l14 -492l-14 -236q0 -11 -8 -19 t-19 -8t-19 8t-9 19l-12 236l12 492q1 12 9 20t19 8t18.5 -8t8.5 -20zM1192 404l-14 -231v0q0 -13 -9 -22t-22 -9t-22 9t-10 22l-6 114l-6 117l12 636v3q2 15 12 24q9 7 20 7q8 0 15 -5q14 -8 16 -26zM2304 423q0 -117 -83 -199.5t-200 -82.5h-786q-13 2 -22 11t-9 22v899 q0 23 28 33q85 34 181 34q195 0 338 -131.5t160 -323.5q53 22 110 22q117 0 200 -83t83 -201z" /> -<glyph unicode="" d="M768 768q237 0 443 43t325 127v-170q0 -69 -103 -128t-280 -93.5t-385 -34.5t-385 34.5t-280 93.5t-103 128v170q119 -84 325 -127t443 -43zM768 0q237 0 443 43t325 127v-170q0 -69 -103 -128t-280 -93.5t-385 -34.5t-385 34.5t-280 93.5t-103 128v170q119 -84 325 -127 t443 -43zM768 384q237 0 443 43t325 127v-170q0 -69 -103 -128t-280 -93.5t-385 -34.5t-385 34.5t-280 93.5t-103 128v170q119 -84 325 -127t443 -43zM768 1536q208 0 385 -34.5t280 -93.5t103 -128v-128q0 -69 -103 -128t-280 -93.5t-385 -34.5t-385 34.5t-280 93.5 t-103 128v128q0 69 103 128t280 93.5t385 34.5z" /> -<glyph unicode="" d="M1468 1156q28 -28 48 -76t20 -88v-1152q0 -40 -28 -68t-68 -28h-1344q-40 0 -68 28t-28 68v1600q0 40 28 68t68 28h896q40 0 88 -20t76 -48zM1024 1400v-376h376q-10 29 -22 41l-313 313q-12 12 -41 22zM1408 -128v1024h-416q-40 0 -68 28t-28 68v416h-768v-1536h1280z M894 465q33 -26 84 -56q59 7 117 7q147 0 177 -49q16 -22 2 -52q0 -1 -1 -2l-2 -2v-1q-6 -38 -71 -38q-48 0 -115 20t-130 53q-221 -24 -392 -83q-153 -262 -242 -262q-15 0 -28 7l-24 12q-1 1 -6 5q-10 10 -6 36q9 40 56 91.5t132 96.5q14 9 23 -6q2 -2 2 -4q52 85 107 197 q68 136 104 262q-24 82 -30.5 159.5t6.5 127.5q11 40 42 40h21h1q23 0 35 -15q18 -21 9 -68q-2 -6 -4 -8q1 -3 1 -8v-30q-2 -123 -14 -192q55 -164 146 -238zM318 54q52 24 137 158q-51 -40 -87.5 -84t-49.5 -74zM716 974q-15 -42 -2 -132q1 7 7 44q0 3 7 43q1 4 4 8 q-1 1 -1 2t-0.5 1.5t-0.5 1.5q-1 22 -13 36q0 -1 -1 -2v-2zM592 313q135 54 284 81q-2 1 -13 9.5t-16 13.5q-76 67 -127 176q-27 -86 -83 -197q-30 -56 -45 -83zM1238 329q-24 24 -140 24q76 -28 124 -28q14 0 18 1q0 1 -2 3z" /> -<glyph unicode="" d="M1468 1156q28 -28 48 -76t20 -88v-1152q0 -40 -28 -68t-68 -28h-1344q-40 0 -68 28t-28 68v1600q0 40 28 68t68 28h896q40 0 88 -20t76 -48zM1024 1400v-376h376q-10 29 -22 41l-313 313q-12 12 -41 22zM1408 -128v1024h-416q-40 0 -68 28t-28 68v416h-768v-1536h1280z M233 768v-107h70l164 -661h159l128 485q7 20 10 46q2 16 2 24h4l3 -24q1 -3 3.5 -20t5.5 -26l128 -485h159l164 661h70v107h-300v-107h90l-99 -438q-5 -20 -7 -46l-2 -21h-4l-3 21q-1 5 -4 21t-5 25l-144 545h-114l-144 -545q-2 -9 -4.5 -24.5t-3.5 -21.5l-4 -21h-4l-2 21 q-2 26 -7 46l-99 438h90v107h-300z" /> -<glyph unicode="" d="M1468 1156q28 -28 48 -76t20 -88v-1152q0 -40 -28 -68t-68 -28h-1344q-40 0 -68 28t-28 68v1600q0 40 28 68t68 28h896q40 0 88 -20t76 -48zM1024 1400v-376h376q-10 29 -22 41l-313 313q-12 12 -41 22zM1408 -128v1024h-416q-40 0 -68 28t-28 68v416h-768v-1536h1280z M429 106v-106h281v106h-75l103 161q5 7 10 16.5t7.5 13.5t3.5 4h2q1 -4 5 -10q2 -4 4.5 -7.5t6 -8t6.5 -8.5l107 -161h-76v-106h291v106h-68l-192 273l195 282h67v107h-279v-107h74l-103 -159q-4 -7 -10 -16.5t-9 -13.5l-2 -3h-2q-1 4 -5 10q-6 11 -17 23l-106 159h76v107 h-290v-107h68l189 -272l-194 -283h-68z" /> -<glyph unicode="" d="M1468 1156q28 -28 48 -76t20 -88v-1152q0 -40 -28 -68t-68 -28h-1344q-40 0 -68 28t-28 68v1600q0 40 28 68t68 28h896q40 0 88 -20t76 -48zM1024 1400v-376h376q-10 29 -22 41l-313 313q-12 12 -41 22zM1408 -128v1024h-416q-40 0 -68 28t-28 68v416h-768v-1536h1280z M416 106v-106h327v106h-93v167h137q76 0 118 15q67 23 106.5 87t39.5 146q0 81 -37 141t-100 87q-48 19 -130 19h-368v-107h92v-555h-92zM769 386h-119v268h120q52 0 83 -18q56 -33 56 -115q0 -89 -62 -120q-31 -15 -78 -15z" /> -<glyph unicode="" d="M1468 1156q28 -28 48 -76t20 -88v-1152q0 -40 -28 -68t-68 -28h-1344q-40 0 -68 28t-28 68v1600q0 40 28 68t68 28h896q40 0 88 -20t76 -48zM1024 1400v-376h376q-10 29 -22 41l-313 313q-12 12 -41 22zM1408 -128v1024h-416q-40 0 -68 28t-28 68v416h-768v-1536h1280z M1280 320v-320h-1024v192l192 192l128 -128l384 384zM448 512q-80 0 -136 56t-56 136t56 136t136 56t136 -56t56 -136t-56 -136t-136 -56z" /> -<glyph unicode="" d="M640 1152v128h-128v-128h128zM768 1024v128h-128v-128h128zM640 896v128h-128v-128h128zM768 768v128h-128v-128h128zM1468 1156q28 -28 48 -76t20 -88v-1152q0 -40 -28 -68t-68 -28h-1344q-40 0 -68 28t-28 68v1600q0 40 28 68t68 28h896q40 0 88 -20t76 -48zM1024 1400 v-376h376q-10 29 -22 41l-313 313q-12 12 -41 22zM1408 -128v1024h-416q-40 0 -68 28t-28 68v416h-128v-128h-128v128h-512v-1536h1280zM781 593l107 -349q8 -27 8 -52q0 -83 -72.5 -137.5t-183.5 -54.5t-183.5 54.5t-72.5 137.5q0 25 8 52q21 63 120 396v128h128v-128h79 q22 0 39 -13t23 -34zM640 128q53 0 90.5 19t37.5 45t-37.5 45t-90.5 19t-90.5 -19t-37.5 -45t37.5 -45t90.5 -19z" /> -<glyph unicode="" d="M1468 1156q28 -28 48 -76t20 -88v-1152q0 -40 -28 -68t-68 -28h-1344q-40 0 -68 28t-28 68v1600q0 40 28 68t68 28h896q40 0 88 -20t76 -48zM1024 1400v-376h376q-10 29 -22 41l-313 313q-12 12 -41 22zM1408 -128v1024h-416q-40 0 -68 28t-28 68v416h-768v-1536h1280z M620 686q20 -8 20 -30v-544q0 -22 -20 -30q-8 -2 -12 -2q-12 0 -23 9l-166 167h-131q-14 0 -23 9t-9 23v192q0 14 9 23t23 9h131l166 167q16 15 35 7zM1037 -3q31 0 50 24q129 159 129 363t-129 363q-16 21 -43 24t-47 -14q-21 -17 -23.5 -43.5t14.5 -47.5 q100 -123 100 -282t-100 -282q-17 -21 -14.5 -47.5t23.5 -42.5q18 -15 40 -15zM826 145q27 0 47 20q87 93 87 219t-87 219q-18 19 -45 20t-46 -17t-20 -44.5t18 -46.5q52 -57 52 -131t-52 -131q-19 -20 -18 -46.5t20 -44.5q20 -17 44 -17z" /> -<glyph unicode="" d="M1468 1156q28 -28 48 -76t20 -88v-1152q0 -40 -28 -68t-68 -28h-1344q-40 0 -68 28t-28 68v1600q0 40 28 68t68 28h896q40 0 88 -20t76 -48zM1024 1400v-376h376q-10 29 -22 41l-313 313q-12 12 -41 22zM1408 -128v1024h-416q-40 0 -68 28t-28 68v416h-768v-1536h1280z M768 768q52 0 90 -38t38 -90v-384q0 -52 -38 -90t-90 -38h-384q-52 0 -90 38t-38 90v384q0 52 38 90t90 38h384zM1260 766q20 -8 20 -30v-576q0 -22 -20 -30q-8 -2 -12 -2q-14 0 -23 9l-265 266v90l265 266q9 9 23 9q4 0 12 -2z" /> -<glyph unicode="" d="M1468 1156q28 -28 48 -76t20 -88v-1152q0 -40 -28 -68t-68 -28h-1344q-40 0 -68 28t-28 68v1600q0 40 28 68t68 28h896q40 0 88 -20t76 -48zM1024 1400v-376h376q-10 29 -22 41l-313 313q-12 12 -41 22zM1408 -128v1024h-416q-40 0 -68 28t-28 68v416h-768v-1536h1280z M480 768q8 11 21 12.5t24 -6.5l51 -38q11 -8 12.5 -21t-6.5 -24l-182 -243l182 -243q8 -11 6.5 -24t-12.5 -21l-51 -38q-11 -8 -24 -6.5t-21 12.5l-226 301q-14 19 0 38zM1282 467q14 -19 0 -38l-226 -301q-8 -11 -21 -12.5t-24 6.5l-51 38q-11 8 -12.5 21t6.5 24l182 243 l-182 243q-8 11 -6.5 24t12.5 21l51 38q11 8 24 6.5t21 -12.5zM662 6q-13 2 -20.5 13t-5.5 24l138 831q2 13 13 20.5t24 5.5l63 -10q13 -2 20.5 -13t5.5 -24l-138 -831q-2 -13 -13 -20.5t-24 -5.5z" /> -<glyph unicode="" d="M1497 709v-198q-101 -23 -198 -23q-65 -136 -165.5 -271t-181.5 -215.5t-128 -106.5q-80 -45 -162 3q-28 17 -60.5 43.5t-85 83.5t-102.5 128.5t-107.5 184t-105.5 244t-91.5 314.5t-70.5 390h283q26 -218 70 -398.5t104.5 -317t121.5 -235.5t140 -195q169 169 287 406 q-142 72 -223 220t-81 333q0 192 104 314.5t284 122.5q178 0 273 -105.5t95 -297.5q0 -159 -58 -286q-7 -1 -19.5 -3t-46 -2t-63 6t-62 25.5t-50.5 51.5q31 103 31 184q0 87 -29 132t-79 45q-53 0 -85 -49.5t-32 -140.5q0 -186 105 -293.5t267 -107.5q62 0 121 14z" /> -<glyph unicode="" horiz-adv-x="1792" d="M216 367l603 -402v359l-334 223zM154 511l193 129l-193 129v-258zM973 -35l603 402l-269 180l-334 -223v-359zM896 458l272 182l-272 182l-272 -182zM485 733l334 223v359l-603 -402zM1445 640l193 -129v258zM1307 733l269 180l-603 402v-359zM1792 913v-546 q0 -41 -34 -64l-819 -546q-21 -13 -43 -13t-43 13l-819 546q-34 23 -34 64v546q0 41 34 64l819 546q21 13 43 13t43 -13l819 -546q34 -23 34 -64z" /> -<glyph unicode="" horiz-adv-x="2048" d="M1800 764q111 -46 179.5 -145.5t68.5 -221.5q0 -164 -118 -280.5t-285 -116.5q-4 0 -11.5 0.5t-10.5 0.5h-1209h-1h-2h-5q-170 10 -288 125.5t-118 280.5q0 110 55 203t147 147q-12 39 -12 82q0 115 82 196t199 81q95 0 172 -58q75 154 222.5 248t326.5 94 q166 0 306 -80.5t221.5 -218.5t81.5 -301q0 -6 -0.5 -18t-0.5 -18zM468 498q0 -122 84 -193t208 -71q137 0 240 99q-16 20 -47.5 56.5t-43.5 50.5q-67 -65 -144 -65q-55 0 -93.5 33.5t-38.5 87.5q0 53 38.5 87t91.5 34q44 0 84.5 -21t73 -55t65 -75t69 -82t77 -75t97 -55 t121.5 -21q121 0 204.5 71.5t83.5 190.5q0 121 -84 192t-207 71q-143 0 -241 -97q14 -16 29.5 -34t34.5 -40t29 -34q66 64 142 64q52 0 92 -33t40 -84q0 -57 -37 -91.5t-94 -34.5q-43 0 -82.5 21t-72 55t-65.5 75t-69.5 82t-77.5 75t-96.5 55t-118.5 21q-122 0 -207 -70.5 t-85 -189.5z" /> -<glyph unicode="" horiz-adv-x="1792" d="M896 1536q182 0 348 -71t286 -191t191 -286t71 -348t-71 -348t-191 -286t-286 -191t-348 -71t-348 71t-286 191t-191 286t-71 348t71 348t191 286t286 191t348 71zM896 1408q-190 0 -361 -90l194 -194q82 28 167 28t167 -28l194 194q-171 90 -361 90zM218 279l194 194 q-28 82 -28 167t28 167l-194 194q-90 -171 -90 -361t90 -361zM896 -128q190 0 361 90l-194 194q-82 -28 -167 -28t-167 28l-194 -194q171 -90 361 -90zM896 256q159 0 271.5 112.5t112.5 271.5t-112.5 271.5t-271.5 112.5t-271.5 -112.5t-112.5 -271.5t112.5 -271.5 t271.5 -112.5zM1380 473l194 -194q90 171 90 361t-90 361l-194 -194q28 -82 28 -167t-28 -167z" /> -<glyph unicode="" horiz-adv-x="1792" d="M1792 640q0 -182 -71 -348t-191 -286t-286 -191t-348 -71t-348 71t-286 191t-191 286t-71 348q0 222 101 414.5t276.5 317t390.5 155.5v-260q-221 -45 -366.5 -221t-145.5 -406q0 -130 51 -248.5t136.5 -204t204 -136.5t248.5 -51t248.5 51t204 136.5t136.5 204t51 248.5 q0 230 -145.5 406t-366.5 221v260q215 -31 390.5 -155.5t276.5 -317t101 -414.5z" /> -<glyph unicode="" horiz-adv-x="1792" d="M19 662q8 217 116 406t305 318h5q0 -1 -1 -3q-8 -8 -28 -33.5t-52 -76.5t-60 -110.5t-44.5 -135.5t-14 -150.5t39 -157.5t108.5 -154q50 -50 102 -69.5t90.5 -11.5t69.5 23.5t47 32.5l16 16q39 51 53 116.5t6.5 122.5t-21 107t-26.5 80l-14 29q-10 25 -30.5 49.5t-43 41 t-43.5 29.5t-35 19l-13 6l104 115q39 -17 78 -52t59 -61l19 -27q1 48 -18.5 103.5t-40.5 87.5l-20 31l161 183l160 -181q-33 -46 -52.5 -102.5t-22.5 -90.5l-4 -33q22 37 61.5 72.5t67.5 52.5l28 17l103 -115q-44 -14 -85 -50t-60 -65l-19 -29q-31 -56 -48 -133.5t-7 -170 t57 -156.5q33 -45 77.5 -60.5t85 -5.5t76 26.5t57.5 33.5l21 16q60 53 96.5 115t48.5 121.5t10 121.5t-18 118t-37 107.5t-45.5 93t-45 72t-34.5 47.5l-13 17q-14 13 -7 13l10 -3q40 -29 62.5 -46t62 -50t64 -58t58.5 -65t55.5 -77t45.5 -88t38 -103t23.5 -117t10.5 -136 q3 -259 -108 -465t-312 -321t-456 -115q-185 0 -351 74t-283.5 198t-184 293t-60.5 353z" /> -<glyph unicode="" horiz-adv-x="1792" d="M874 -102v-66q-208 6 -385 109.5t-283 275.5l58 34q29 -49 73 -99l65 57q148 -168 368 -212l-17 -86q65 -12 121 -13zM276 428l-83 -28q22 -60 49 -112l-57 -33q-98 180 -98 385t98 385l57 -33q-30 -56 -49 -112l82 -28q-35 -100 -35 -212q0 -109 36 -212zM1528 251 l58 -34q-106 -172 -283 -275.5t-385 -109.5v66q56 1 121 13l-17 86q220 44 368 212l65 -57q44 50 73 99zM1377 805l-233 -80q14 -42 14 -85t-14 -85l232 -80q-31 -92 -98 -169l-185 162q-57 -67 -147 -85l48 -241q-52 -10 -98 -10t-98 10l48 241q-90 18 -147 85l-185 -162 q-67 77 -98 169l232 80q-14 42 -14 85t14 85l-233 80q33 93 99 169l185 -162q59 68 147 86l-48 240q44 10 98 10t98 -10l-48 -240q88 -18 147 -86l185 162q66 -76 99 -169zM874 1448v-66q-65 -2 -121 -13l17 -86q-220 -42 -368 -211l-65 56q-38 -42 -73 -98l-57 33 q106 172 282 275.5t385 109.5zM1705 640q0 -205 -98 -385l-57 33q27 52 49 112l-83 28q36 103 36 212q0 112 -35 212l82 28q-19 56 -49 112l57 33q98 -180 98 -385zM1585 1063l-57 -33q-35 56 -73 98l-65 -56q-148 169 -368 211l17 86q-56 11 -121 13v66q209 -6 385 -109.5 t282 -275.5zM1748 640q0 173 -67.5 331t-181.5 272t-272 181.5t-331 67.5t-331 -67.5t-272 -181.5t-181.5 -272t-67.5 -331t67.5 -331t181.5 -272t272 -181.5t331 -67.5t331 67.5t272 181.5t181.5 272t67.5 331zM1792 640q0 -182 -71 -348t-191 -286t-286 -191t-348 -71 t-348 71t-286 191t-191 286t-71 348t71 348t191 286t286 191t348 71t348 -71t286 -191t191 -286t71 -348z" /> -<glyph unicode="" d="M582 228q0 -66 -93 -66q-107 0 -107 63q0 64 98 64q102 0 102 -61zM546 694q0 -85 -74 -85q-77 0 -77 84q0 90 77 90q36 0 55 -25.5t19 -63.5zM712 769v125q-78 -29 -135 -29q-50 29 -110 29q-86 0 -145 -57t-59 -143q0 -50 29.5 -102t73.5 -67v-3q-38 -17 -38 -85 q0 -53 41 -77v-3q-113 -37 -113 -139q0 -45 20 -78.5t54 -51t72 -25.5t81 -8q224 0 224 188q0 67 -48 99t-126 46q-27 5 -51.5 20.5t-24.5 39.5q0 44 49 52q77 15 122 70t45 134q0 24 -10 52q37 9 49 13zM771 350h137q-2 27 -2 82v387q0 46 2 69h-137q3 -23 3 -71v-392 q0 -50 -3 -75zM1280 366v121q-30 -21 -68 -21q-53 0 -53 82v225h52q9 0 26.5 -1t26.5 -1v117h-105q0 82 3 102h-140q4 -24 4 -55v-47h-60v-117q36 3 37 3q3 0 11 -0.5t12 -0.5v-2h-2v-217q0 -37 2.5 -64t11.5 -56.5t24.5 -48.5t43.5 -31t66 -12q64 0 108 24zM924 1072 q0 36 -24 63.5t-60 27.5t-60.5 -27t-24.5 -64q0 -36 25 -62.5t60 -26.5t59.5 27t24.5 62zM1536 1120v-960q0 -119 -84.5 -203.5t-203.5 -84.5h-960q-119 0 -203.5 84.5t-84.5 203.5v960q0 119 84.5 203.5t203.5 84.5h960q119 0 203.5 -84.5t84.5 -203.5z" /> -<glyph unicode="" horiz-adv-x="1792" d="M595 22q0 100 -165 100q-158 0 -158 -104q0 -101 172 -101q151 0 151 105zM536 777q0 61 -30 102t-89 41q-124 0 -124 -145q0 -135 124 -135q119 0 119 137zM805 1101v-202q-36 -12 -79 -22q16 -43 16 -84q0 -127 -73 -216.5t-197 -112.5q-40 -8 -59.5 -27t-19.5 -58 q0 -31 22.5 -51.5t58 -32t78.5 -22t86 -25.5t78.5 -37.5t58 -64t22.5 -98.5q0 -304 -363 -304q-69 0 -130 12.5t-116 41t-87.5 82t-32.5 127.5q0 165 182 225v4q-67 41 -67 126q0 109 63 137v4q-72 24 -119.5 108.5t-47.5 165.5q0 139 95 231.5t235 92.5q96 0 178 -47 q98 0 218 47zM1123 220h-222q4 45 4 134v609q0 94 -4 128h222q-4 -33 -4 -124v-613q0 -89 4 -134zM1724 442v-196q-71 -39 -174 -39q-62 0 -107 20t-70 50t-39.5 78t-18.5 92t-4 103v351h2v4q-7 0 -19 1t-18 1q-21 0 -59 -6v190h96v76q0 54 -6 89h227q-6 -41 -6 -165h171 v-190q-15 0 -43.5 2t-42.5 2h-85v-365q0 -131 87 -131q61 0 109 33zM1148 1389q0 -58 -39 -101.5t-96 -43.5q-58 0 -98 43.5t-40 101.5q0 59 39.5 103t98.5 44q58 0 96.5 -44.5t38.5 -102.5z" /> -<glyph unicode="" d="M825 547l343 588h-150q-21 -39 -63.5 -118.5t-68 -128.5t-59.5 -118.5t-60 -128.5h-3q-21 48 -44.5 97t-52 105.5t-46.5 92t-54 104.5t-49 95h-150l323 -589v-435h134v436zM1536 1120v-960q0 -119 -84.5 -203.5t-203.5 -84.5h-960q-119 0 -203.5 84.5t-84.5 203.5v960 q0 119 84.5 203.5t203.5 84.5h960q119 0 203.5 -84.5t84.5 -203.5z" /> -<glyph unicode="" horiz-adv-x="1280" d="M842 964q0 -80 -57 -136.5t-136 -56.5q-60 0 -111 35q-62 -67 -115 -146q-247 -371 -202 -859q1 -22 -12.5 -38.5t-34.5 -18.5h-5q-20 0 -35 13.5t-17 33.5q-14 126 -3.5 247.5t29.5 217t54 186t69 155.5t74 125q61 90 132 165q-16 35 -16 77q0 80 56.5 136.5t136.5 56.5 t136.5 -56.5t56.5 -136.5zM1223 953q0 -158 -78 -292t-212.5 -212t-292.5 -78q-64 0 -131 14q-21 5 -32.5 23.5t-6.5 39.5q5 20 23 31.5t39 7.5q51 -13 108 -13q97 0 186 38t153 102t102 153t38 186t-38 186t-102 153t-153 102t-186 38t-186 -38t-153 -102t-102 -153 t-38 -186q0 -114 52 -218q10 -20 3.5 -40t-25.5 -30t-39.5 -3t-30.5 26q-64 123 -64 265q0 119 46.5 227t124.5 186t186 124t226 46q158 0 292.5 -78t212.5 -212.5t78 -292.5z" /> -<glyph unicode="" horiz-adv-x="1792" d="M270 730q-8 19 -8 52q0 20 11 49t24 45q-1 22 7.5 53t22.5 43q0 139 92.5 288.5t217.5 209.5q139 66 324 66q133 0 266 -55q49 -21 90 -48t71 -56t55 -68t42 -74t32.5 -84.5t25.5 -89.5t22 -98l1 -5q55 -83 55 -150q0 -14 -9 -40t-9 -38q0 -1 1.5 -3.5t3.5 -5t2 -3.5 q77 -114 120.5 -214.5t43.5 -208.5q0 -43 -19.5 -100t-55.5 -57q-9 0 -19.5 7.5t-19 17.5t-19 26t-16 26.5t-13.5 26t-9 17.5q-1 1 -3 1l-5 -4q-59 -154 -132 -223q20 -20 61.5 -38.5t69 -41.5t35.5 -65q-2 -4 -4 -16t-7 -18q-64 -97 -302 -97q-53 0 -110.5 9t-98 20 t-104.5 30q-15 5 -23 7q-14 4 -46 4.5t-40 1.5q-41 -45 -127.5 -65t-168.5 -20q-35 0 -69 1.5t-93 9t-101 20.5t-74.5 40t-32.5 64q0 40 10 59.5t41 48.5q11 2 40.5 13t49.5 12q4 0 14 2q2 2 2 4l-2 3q-48 11 -108 105.5t-73 156.5l-5 3q-4 0 -12 -20q-18 -41 -54.5 -74.5 t-77.5 -37.5h-1q-4 0 -6 4.5t-5 5.5q-23 54 -23 100q0 275 252 466z" /> -<glyph unicode="" horiz-adv-x="2048" d="M580 1075q0 41 -25 66t-66 25q-43 0 -76 -25.5t-33 -65.5q0 -39 33 -64.5t76 -25.5q41 0 66 24.5t25 65.5zM1323 568q0 28 -25.5 50t-65.5 22q-27 0 -49.5 -22.5t-22.5 -49.5q0 -28 22.5 -50.5t49.5 -22.5q40 0 65.5 22t25.5 51zM1087 1075q0 41 -24.5 66t-65.5 25 q-43 0 -76 -25.5t-33 -65.5q0 -39 33 -64.5t76 -25.5q41 0 65.5 24.5t24.5 65.5zM1722 568q0 28 -26 50t-65 22q-27 0 -49.5 -22.5t-22.5 -49.5q0 -28 22.5 -50.5t49.5 -22.5q39 0 65 22t26 51zM1456 965q-31 4 -70 4q-169 0 -311 -77t-223.5 -208.5t-81.5 -287.5 q0 -78 23 -152q-35 -3 -68 -3q-26 0 -50 1.5t-55 6.5t-44.5 7t-54.5 10.5t-50 10.5l-253 -127l72 218q-290 203 -290 490q0 169 97.5 311t264 223.5t363.5 81.5q176 0 332.5 -66t262 -182.5t136.5 -260.5zM2048 404q0 -117 -68.5 -223.5t-185.5 -193.5l55 -181l-199 109 q-150 -37 -218 -37q-169 0 -311 70.5t-223.5 191.5t-81.5 264t81.5 264t223.5 191.5t311 70.5q161 0 303 -70.5t227.5 -192t85.5 -263.5z" /> -<glyph unicode="" horiz-adv-x="1792" d="M1764 1525q33 -24 27 -64l-256 -1536q-5 -29 -32 -45q-14 -8 -31 -8q-11 0 -24 5l-453 185l-242 -295q-18 -23 -49 -23q-13 0 -22 4q-19 7 -30.5 23.5t-11.5 36.5v349l864 1059l-1069 -925l-395 162q-37 14 -40 55q-2 40 32 59l1664 960q15 9 32 9q20 0 36 -11z" /> -<glyph unicode="" horiz-adv-x="1792" d="M1764 1525q33 -24 27 -64l-256 -1536q-5 -29 -32 -45q-14 -8 -31 -8q-11 0 -24 5l-527 215l-298 -327q-18 -21 -47 -21q-14 0 -23 4q-19 7 -30 23.5t-11 36.5v452l-472 193q-37 14 -40 55q-3 39 32 59l1664 960q35 21 68 -2zM1422 26l221 1323l-1434 -827l336 -137 l863 639l-478 -797z" /> -<glyph unicode="" d="M1536 640q0 -156 -61 -298t-164 -245t-245 -164t-298 -61q-172 0 -327 72.5t-264 204.5q-7 10 -6.5 22.5t8.5 20.5l137 138q10 9 25 9q16 -2 23 -12q73 -95 179 -147t225 -52q104 0 198.5 40.5t163.5 109.5t109.5 163.5t40.5 198.5t-40.5 198.5t-109.5 163.5 t-163.5 109.5t-198.5 40.5q-98 0 -188 -35.5t-160 -101.5l137 -138q31 -30 14 -69q-17 -40 -59 -40h-448q-26 0 -45 19t-19 45v448q0 42 40 59q39 17 69 -14l130 -129q107 101 244.5 156.5t284.5 55.5q156 0 298 -61t245 -164t164 -245t61 -298zM896 928v-448q0 -14 -9 -23 t-23 -9h-320q-14 0 -23 9t-9 23v64q0 14 9 23t23 9h224v352q0 14 9 23t23 9h64q14 0 23 -9t9 -23z" /> -<glyph unicode="" d="M768 1280q-130 0 -248.5 -51t-204 -136.5t-136.5 -204t-51 -248.5t51 -248.5t136.5 -204t204 -136.5t248.5 -51t248.5 51t204 136.5t136.5 204t51 248.5t-51 248.5t-136.5 204t-204 136.5t-248.5 51zM1536 640q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103 t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5t103 -385.5z" /> -<glyph unicode="" horiz-adv-x="1792" d="M1682 -128q-44 0 -132.5 3.5t-133.5 3.5q-44 0 -132 -3.5t-132 -3.5q-24 0 -37 20.5t-13 45.5q0 31 17 46t39 17t51 7t45 15q33 21 33 140l-1 391q0 21 -1 31q-13 4 -50 4h-675q-38 0 -51 -4q-1 -10 -1 -31l-1 -371q0 -142 37 -164q16 -10 48 -13t57 -3.5t45 -15 t20 -45.5q0 -26 -12.5 -48t-36.5 -22q-47 0 -139.5 3.5t-138.5 3.5q-43 0 -128 -3.5t-127 -3.5q-23 0 -35.5 21t-12.5 45q0 30 15.5 45t36 17.5t47.5 7.5t42 15q33 23 33 143l-1 57v813q0 3 0.5 26t0 36.5t-1.5 38.5t-3.5 42t-6.5 36.5t-11 31.5t-16 18q-15 10 -45 12t-53 2 t-41 14t-18 45q0 26 12 48t36 22q46 0 138.5 -3.5t138.5 -3.5q42 0 126.5 3.5t126.5 3.5q25 0 37.5 -22t12.5 -48q0 -30 -17 -43.5t-38.5 -14.5t-49.5 -4t-43 -13q-35 -21 -35 -160l1 -320q0 -21 1 -32q13 -3 39 -3h699q25 0 38 3q1 11 1 32l1 320q0 139 -35 160 q-18 11 -58.5 12.5t-66 13t-25.5 49.5q0 26 12.5 48t37.5 22q44 0 132 -3.5t132 -3.5q43 0 129 3.5t129 3.5q25 0 37.5 -22t12.5 -48q0 -30 -17.5 -44t-40 -14.5t-51.5 -3t-44 -12.5q-35 -23 -35 -161l1 -943q0 -119 34 -140q16 -10 46 -13.5t53.5 -4.5t41.5 -15.5t18 -44.5 q0 -26 -12 -48t-36 -22z" /> -<glyph unicode="" horiz-adv-x="1280" d="M1278 1347v-73q0 -29 -18.5 -61t-42.5 -32q-50 0 -54 -1q-26 -6 -32 -31q-3 -11 -3 -64v-1152q0 -25 -18 -43t-43 -18h-108q-25 0 -43 18t-18 43v1218h-143v-1218q0 -25 -17.5 -43t-43.5 -18h-108q-26 0 -43.5 18t-17.5 43v496q-147 12 -245 59q-126 58 -192 179 q-64 117 -64 259q0 166 88 286q88 118 209 159q111 37 417 37h479q25 0 43 -18t18 -43z" /> -<glyph unicode="" d="M352 128v-128h-352v128h352zM704 256q26 0 45 -19t19 -45v-256q0 -26 -19 -45t-45 -19h-256q-26 0 -45 19t-19 45v256q0 26 19 45t45 19h256zM864 640v-128h-864v128h864zM224 1152v-128h-224v128h224zM1536 128v-128h-736v128h736zM576 1280q26 0 45 -19t19 -45v-256 q0 -26 -19 -45t-45 -19h-256q-26 0 -45 19t-19 45v256q0 26 19 45t45 19h256zM1216 768q26 0 45 -19t19 -45v-256q0 -26 -19 -45t-45 -19h-256q-26 0 -45 19t-19 45v256q0 26 19 45t45 19h256zM1536 640v-128h-224v128h224zM1536 1152v-128h-864v128h864z" /> -<glyph unicode="" d="M1216 512q133 0 226.5 -93.5t93.5 -226.5t-93.5 -226.5t-226.5 -93.5t-226.5 93.5t-93.5 226.5q0 12 2 34l-360 180q-92 -86 -218 -86q-133 0 -226.5 93.5t-93.5 226.5t93.5 226.5t226.5 93.5q126 0 218 -86l360 180q-2 22 -2 34q0 133 93.5 226.5t226.5 93.5 t226.5 -93.5t93.5 -226.5t-93.5 -226.5t-226.5 -93.5q-126 0 -218 86l-360 -180q2 -22 2 -34t-2 -34l360 -180q92 86 218 86z" /> -<glyph unicode="" d="M1280 341q0 88 -62.5 151t-150.5 63q-84 0 -145 -58l-241 120q2 16 2 23t-2 23l241 120q61 -58 145 -58q88 0 150.5 63t62.5 151t-62.5 150.5t-150.5 62.5t-151 -62.5t-63 -150.5q0 -7 2 -23l-241 -120q-62 57 -145 57q-88 0 -150.5 -62.5t-62.5 -150.5t62.5 -150.5 t150.5 -62.5q83 0 145 57l241 -120q-2 -16 -2 -23q0 -88 63 -150.5t151 -62.5t150.5 62.5t62.5 150.5zM1536 1120v-960q0 -119 -84.5 -203.5t-203.5 -84.5h-960q-119 0 -203.5 84.5t-84.5 203.5v960q0 119 84.5 203.5t203.5 84.5h960q119 0 203.5 -84.5t84.5 -203.5z" /> -<glyph unicode="" horiz-adv-x="1792" d="M571 947q-10 25 -34 35t-49 0q-108 -44 -191 -127t-127 -191q-10 -25 0 -49t35 -34q13 -5 24 -5q42 0 60 40q34 84 98.5 148.5t148.5 98.5q25 11 35 35t0 49zM1513 1303l46 -46l-244 -243l68 -68q19 -19 19 -45.5t-19 -45.5l-64 -64q89 -161 89 -343q0 -143 -55.5 -273.5 t-150 -225t-225 -150t-273.5 -55.5t-273.5 55.5t-225 150t-150 225t-55.5 273.5t55.5 273.5t150 225t225 150t273.5 55.5q182 0 343 -89l64 64q19 19 45.5 19t45.5 -19l68 -68zM1521 1359q-10 -10 -22 -10q-13 0 -23 10l-91 90q-9 10 -9 23t9 23q10 9 23 9t23 -9l90 -91 q10 -9 10 -22.5t-10 -22.5zM1751 1129q-11 -9 -23 -9t-23 9l-90 91q-10 9 -10 22.5t10 22.5q9 10 22.5 10t22.5 -10l91 -90q9 -10 9 -23t-9 -23zM1792 1312q0 -14 -9 -23t-23 -9h-96q-14 0 -23 9t-9 23t9 23t23 9h96q14 0 23 -9t9 -23zM1600 1504v-96q0 -14 -9 -23t-23 -9 t-23 9t-9 23v96q0 14 9 23t23 9t23 -9t9 -23zM1751 1449l-91 -90q-10 -10 -22 -10q-13 0 -23 10q-10 9 -10 22.5t10 22.5l90 91q10 9 23 9t23 -9q9 -10 9 -23t-9 -23z" /> -<glyph unicode="" horiz-adv-x="1792" d="M609 720l287 208l287 -208l-109 -336h-355zM896 1536q182 0 348 -71t286 -191t191 -286t71 -348t-71 -348t-191 -286t-286 -191t-348 -71t-348 71t-286 191t-191 286t-71 348t71 348t191 286t286 191t348 71zM1515 186q149 203 149 454v3l-102 -89l-240 224l63 323 l134 -12q-150 206 -389 282l53 -124l-287 -159l-287 159l53 124q-239 -76 -389 -282l135 12l62 -323l-240 -224l-102 89v-3q0 -251 149 -454l30 132l326 -40l139 -298l-116 -69q117 -39 240 -39t240 39l-116 69l139 298l326 40z" /> -<glyph unicode="" horiz-adv-x="1792" d="M448 224v-192q0 -14 -9 -23t-23 -9h-192q-14 0 -23 9t-9 23v192q0 14 9 23t23 9h192q14 0 23 -9t9 -23zM256 608v-192q0 -14 -9 -23t-23 -9h-192q-14 0 -23 9t-9 23v192q0 14 9 23t23 9h192q14 0 23 -9t9 -23zM832 224v-192q0 -14 -9 -23t-23 -9h-192q-14 0 -23 9t-9 23 v192q0 14 9 23t23 9h192q14 0 23 -9t9 -23zM640 608v-192q0 -14 -9 -23t-23 -9h-192q-14 0 -23 9t-9 23v192q0 14 9 23t23 9h192q14 0 23 -9t9 -23zM66 768q-28 0 -47 19t-19 46v129h514v-129q0 -27 -19 -46t-46 -19h-383zM1216 224v-192q0 -14 -9 -23t-23 -9h-192 q-14 0 -23 9t-9 23v192q0 14 9 23t23 9h192q14 0 23 -9t9 -23zM1024 608v-192q0 -14 -9 -23t-23 -9h-192q-14 0 -23 9t-9 23v192q0 14 9 23t23 9h192q14 0 23 -9t9 -23zM1600 224v-192q0 -14 -9 -23t-23 -9h-192q-14 0 -23 9t-9 23v192q0 14 9 23t23 9h192q14 0 23 -9t9 -23 zM1408 608v-192q0 -14 -9 -23t-23 -9h-192q-14 0 -23 9t-9 23v192q0 14 9 23t23 9h192q14 0 23 -9t9 -23zM1792 1016v-13h-514v10q0 104 -382 102q-382 -1 -382 -102v-10h-514v13q0 17 8.5 43t34 64t65.5 75.5t110.5 76t160 67.5t224 47.5t293.5 18.5t293 -18.5t224 -47.5 t160.5 -67.5t110.5 -76t65.5 -75.5t34 -64t8.5 -43zM1792 608v-192q0 -14 -9 -23t-23 -9h-192q-14 0 -23 9t-9 23v192q0 14 9 23t23 9h192q14 0 23 -9t9 -23zM1792 962v-129q0 -27 -19 -46t-46 -19h-384q-27 0 -46 19t-19 46v129h514z" /> -<glyph unicode="" horiz-adv-x="1792" d="M704 1216v-768q0 -26 -19 -45t-45 -19v-576q0 -26 -19 -45t-45 -19h-512q-26 0 -45 19t-19 45v512l249 873q7 23 31 23h424zM1024 1216v-704h-256v704h256zM1792 320v-512q0 -26 -19 -45t-45 -19h-512q-26 0 -45 19t-19 45v576q-26 0 -45 19t-19 45v768h424q24 0 31 -23z M736 1504v-224h-352v224q0 14 9 23t23 9h288q14 0 23 -9t9 -23zM1408 1504v-224h-352v224q0 14 9 23t23 9h288q14 0 23 -9t9 -23z" /> -<glyph unicode="" horiz-adv-x="1792" d="M1755 1083q37 -37 37 -90t-37 -91l-401 -400l150 -150l-160 -160q-163 -163 -389.5 -186.5t-411.5 100.5l-362 -362h-181v181l362 362q-124 185 -100.5 411.5t186.5 389.5l160 160l150 -150l400 401q38 37 91 37t90 -37t37 -90.5t-37 -90.5l-400 -401l234 -234l401 400 q38 37 91 37t90 -37z" /> -<glyph unicode="" horiz-adv-x="1792" d="M873 796q0 -83 -63.5 -142.5t-152.5 -59.5t-152.5 59.5t-63.5 142.5q0 84 63.5 143t152.5 59t152.5 -59t63.5 -143zM1375 796q0 -83 -63 -142.5t-153 -59.5q-89 0 -152.5 59.5t-63.5 142.5q0 84 63.5 143t152.5 59q90 0 153 -59t63 -143zM1600 616v667q0 87 -32 123.5 t-111 36.5h-1112q-83 0 -112.5 -34t-29.5 -126v-673q43 -23 88.5 -40t81 -28t81 -18.5t71 -11t70 -4t58.5 -0.5t56.5 2t44.5 2q68 1 95 -27q6 -6 10 -9q26 -25 61 -51q7 91 118 87q5 0 36.5 -1.5t43 -2t45.5 -1t53 1t54.5 4.5t61 8.5t62 13.5t67 19.5t67.5 27t72 34.5z M1763 621q-121 -149 -372 -252q84 -285 -23 -465q-66 -113 -183 -148q-104 -32 -182 15q-86 51 -82 164l-1 326v1q-8 2 -24.5 6t-23.5 5l-1 -338q4 -114 -83 -164q-79 -47 -183 -15q-117 36 -182 150q-105 180 -22 463q-251 103 -372 252q-25 37 -4 63t60 -1q3 -2 11 -7 t11 -8v694q0 72 47 123t114 51h1257q67 0 114 -51t47 -123v-694l21 15q39 27 60 1t-4 -63z" /> -<glyph unicode="" horiz-adv-x="1792" d="M896 1102v-434h-145v434h145zM1294 1102v-434h-145v434h145zM1294 342l253 254v795h-1194v-1049h326v-217l217 217h398zM1692 1536v-1013l-434 -434h-326l-217 -217h-217v217h-398v1158l109 289h1483z" /> -<glyph unicode="" d="M773 217v-127q-1 -292 -6 -305q-12 -32 -51 -40q-54 -9 -181.5 38t-162.5 89q-13 15 -17 36q-1 12 4 26q4 10 34 47t181 216q1 0 60 70q15 19 39.5 24.5t49.5 -3.5q24 -10 37.5 -29t12.5 -42zM624 468q-3 -55 -52 -70l-120 -39q-275 -88 -292 -88q-35 2 -54 36 q-12 25 -17 75q-8 76 1 166.5t30 124.5t56 32q13 0 202 -77q70 -29 115 -47l84 -34q23 -9 35.5 -30.5t11.5 -48.5zM1450 171q-7 -54 -91.5 -161t-135.5 -127q-37 -14 -63 7q-14 10 -184 287l-47 77q-14 21 -11.5 46t19.5 46q35 43 83 26q1 -1 119 -40q203 -66 242 -79.5 t47 -20.5q28 -22 22 -61zM778 803q5 -102 -54 -122q-58 -17 -114 71l-378 598q-8 35 19 62q41 43 207.5 89.5t224.5 31.5q40 -10 49 -45q3 -18 22 -305.5t24 -379.5zM1440 695q3 -39 -26 -59q-15 -10 -329 -86q-67 -15 -91 -23l1 2q-23 -6 -46 4t-37 32q-30 47 0 87 q1 1 75 102q125 171 150 204t34 39q28 19 65 2q48 -23 123 -133.5t81 -167.5v-3z" /> -<glyph unicode="" horiz-adv-x="2048" d="M1024 1024h-384v-384h384v384zM1152 384v-128h-640v128h640zM1152 1152v-640h-640v640h640zM1792 384v-128h-512v128h512zM1792 640v-128h-512v128h512zM1792 896v-128h-512v128h512zM1792 1152v-128h-512v128h512zM256 192v960h-128v-960q0 -26 19 -45t45 -19t45 19 t19 45zM1920 192v1088h-1536v-1088q0 -33 -11 -64h1483q26 0 45 19t19 45zM2048 1408v-1216q0 -80 -56 -136t-136 -56h-1664q-80 0 -136 56t-56 136v1088h256v128h1792z" /> -<glyph unicode="" horiz-adv-x="2048" d="M1024 13q-20 0 -93 73.5t-73 93.5q0 32 62.5 54t103.5 22t103.5 -22t62.5 -54q0 -20 -73 -93.5t-93 -73.5zM1294 284q-2 0 -40 25t-101.5 50t-128.5 25t-128.5 -25t-101 -50t-40.5 -25q-18 0 -93.5 75t-75.5 93q0 13 10 23q78 77 196 121t233 44t233 -44t196 -121 q10 -10 10 -23q0 -18 -75.5 -93t-93.5 -75zM1567 556q-11 0 -23 8q-136 105 -252 154.5t-268 49.5q-85 0 -170.5 -22t-149 -53t-113.5 -62t-79 -53t-31 -22q-17 0 -92 75t-75 93q0 12 10 22q132 132 320 205t380 73t380 -73t320 -205q10 -10 10 -22q0 -18 -75 -93t-92 -75z M1838 827q-11 0 -22 9q-179 157 -371.5 236.5t-420.5 79.5t-420.5 -79.5t-371.5 -236.5q-11 -9 -22 -9q-17 0 -92.5 75t-75.5 93q0 13 10 23q187 186 445 288t527 102t527 -102t445 -288q10 -10 10 -23q0 -18 -75.5 -93t-92.5 -75z" /> -<glyph unicode="" horiz-adv-x="1792" d="M384 0q0 53 -37.5 90.5t-90.5 37.5t-90.5 -37.5t-37.5 -90.5t37.5 -90.5t90.5 -37.5t90.5 37.5t37.5 90.5zM768 0q0 53 -37.5 90.5t-90.5 37.5t-90.5 -37.5t-37.5 -90.5t37.5 -90.5t90.5 -37.5t90.5 37.5t37.5 90.5zM384 384q0 53 -37.5 90.5t-90.5 37.5t-90.5 -37.5 t-37.5 -90.5t37.5 -90.5t90.5 -37.5t90.5 37.5t37.5 90.5zM1152 0q0 53 -37.5 90.5t-90.5 37.5t-90.5 -37.5t-37.5 -90.5t37.5 -90.5t90.5 -37.5t90.5 37.5t37.5 90.5zM768 384q0 53 -37.5 90.5t-90.5 37.5t-90.5 -37.5t-37.5 -90.5t37.5 -90.5t90.5 -37.5t90.5 37.5 t37.5 90.5zM384 768q0 53 -37.5 90.5t-90.5 37.5t-90.5 -37.5t-37.5 -90.5t37.5 -90.5t90.5 -37.5t90.5 37.5t37.5 90.5zM1152 384q0 53 -37.5 90.5t-90.5 37.5t-90.5 -37.5t-37.5 -90.5t37.5 -90.5t90.5 -37.5t90.5 37.5t37.5 90.5zM768 768q0 53 -37.5 90.5t-90.5 37.5 t-90.5 -37.5t-37.5 -90.5t37.5 -90.5t90.5 -37.5t90.5 37.5t37.5 90.5zM1536 0v384q0 52 -38 90t-90 38t-90 -38t-38 -90v-384q0 -52 38 -90t90 -38t90 38t38 90zM1152 768q0 53 -37.5 90.5t-90.5 37.5t-90.5 -37.5t-37.5 -90.5t37.5 -90.5t90.5 -37.5t90.5 37.5t37.5 90.5z M1536 1088v256q0 26 -19 45t-45 19h-1280q-26 0 -45 -19t-19 -45v-256q0 -26 19 -45t45 -19h1280q26 0 45 19t19 45zM1536 768q0 53 -37.5 90.5t-90.5 37.5t-90.5 -37.5t-37.5 -90.5t37.5 -90.5t90.5 -37.5t90.5 37.5t37.5 90.5zM1664 1408v-1536q0 -52 -38 -90t-90 -38 h-1408q-52 0 -90 38t-38 90v1536q0 52 38 90t90 38h1408q52 0 90 -38t38 -90z" /> -<glyph unicode="" horiz-adv-x="1792" d="M1112 1090q0 159 -237 159h-70q-32 0 -59.5 -21.5t-34.5 -52.5l-63 -276q-2 -5 -2 -16q0 -24 17 -39.5t41 -15.5h53q69 0 128.5 13t112.5 41t83.5 81.5t30.5 126.5zM1716 938q0 -265 -220 -428q-219 -161 -612 -161h-61q-32 0 -59 -21.5t-34 -52.5l-73 -316 q-8 -36 -40.5 -61.5t-69.5 -25.5h-213q-31 0 -53 20t-22 51q0 10 13 65h151q34 0 64 23.5t38 56.5l73 316q8 33 37.5 57t63.5 24h61q390 0 607 160t217 421q0 129 -51 207q183 -92 183 -335zM1533 1123q0 -264 -221 -428q-218 -161 -612 -161h-60q-32 0 -59.5 -22t-34.5 -53 l-73 -315q-8 -36 -40 -61.5t-69 -25.5h-214q-31 0 -52.5 19.5t-21.5 51.5q0 8 2 20l300 1301q8 36 40.5 61.5t69.5 25.5h444q68 0 125 -4t120.5 -15t113.5 -30t96.5 -50.5t77.5 -74t49.5 -103.5t18.5 -136z" /> -<glyph unicode="" horiz-adv-x="1792" d="M602 949q19 -61 31 -123.5t17 -141.5t-14 -159t-62 -145q-21 81 -67 157t-95.5 127t-99 90.5t-78.5 57.5t-33 19q-62 34 -81.5 100t14.5 128t101 81.5t129 -14.5q138 -83 238 -177zM927 1236q11 -25 20.5 -46t36.5 -100.5t42.5 -150.5t25.5 -179.5t0 -205.5t-47.5 -209.5 t-105.5 -208.5q-51 -72 -138 -72q-54 0 -98 31q-57 40 -69 109t28 127q60 85 81 195t13 199.5t-32 180.5t-39 128t-22 52q-31 63 -8.5 129.5t85.5 97.5q34 17 75 17q47 0 88.5 -25t63.5 -69zM1248 567q-17 -160 -72 -311q-17 131 -63 246q25 174 -5 361q-27 178 -94 342 q114 -90 212 -211q9 -37 15 -80q26 -179 7 -347zM1520 1440q9 -17 23.5 -49.5t43.5 -117.5t50.5 -178t34 -227.5t5 -269t-47 -300t-112.5 -323.5q-22 -48 -66 -75.5t-95 -27.5q-39 0 -74 16q-67 31 -92.5 100t4.5 136q58 126 90 257.5t37.5 239.5t-3.5 213.5t-26.5 180.5 t-38.5 138.5t-32.5 90t-15.5 32.5q-34 65 -11.5 135.5t87.5 104.5q37 20 81 20q49 0 91.5 -25.5t66.5 -70.5z" /> -<glyph unicode="" horiz-adv-x="2304" d="M1975 546h-138q14 37 66 179l3 9q4 10 10 26t9 26l12 -55zM531 611l-58 295q-11 54 -75 54h-268l-2 -13q311 -79 403 -336zM710 960l-162 -438l-17 89q-26 70 -85 129.5t-131 88.5l135 -510h175l261 641h-176zM849 318h166l104 642h-166zM1617 944q-69 27 -149 27 q-123 0 -201 -59t-79 -153q-1 -102 145 -174q48 -23 67 -41t19 -39q0 -30 -30 -46t-69 -16q-86 0 -156 33l-22 11l-23 -144q74 -34 185 -34q130 -1 208.5 59t80.5 160q0 106 -140 174q-49 25 -71 42t-22 38q0 22 24.5 38.5t70.5 16.5q70 1 124 -24l15 -8zM2042 960h-128 q-65 0 -87 -54l-246 -588h174l35 96h212q5 -22 20 -96h154zM2304 1280v-1280q0 -52 -38 -90t-90 -38h-2048q-52 0 -90 38t-38 90v1280q0 52 38 90t90 38h2048q52 0 90 -38t38 -90z" /> -<glyph unicode="" horiz-adv-x="2304" d="M671 603h-13q-47 0 -47 -32q0 -22 20 -22q17 0 28 15t12 39zM1066 639h62v3q1 4 0.5 6.5t-1 7t-2 8t-4.5 6.5t-7.5 5t-11.5 2q-28 0 -36 -38zM1606 603h-12q-48 0 -48 -32q0 -22 20 -22q17 0 28 15t12 39zM1925 629q0 41 -30 41q-19 0 -31 -20t-12 -51q0 -42 28 -42 q20 0 32.5 20t12.5 52zM480 770h87l-44 -262h-56l32 201l-71 -201h-39l-4 200l-34 -200h-53l44 262h81l2 -163zM733 663q0 -6 -4 -42q-16 -101 -17 -113h-47l1 22q-20 -26 -58 -26q-23 0 -37.5 16t-14.5 42q0 39 26 60.5t73 21.5q14 0 23 -1q0 3 0.5 5.5t1 4.5t0.5 3 q0 20 -36 20q-29 0 -59 -10q0 4 7 48q38 11 67 11q74 0 74 -62zM889 721l-8 -49q-22 3 -41 3q-27 0 -27 -17q0 -8 4.5 -12t21.5 -11q40 -19 40 -60q0 -72 -87 -71q-34 0 -58 6q0 2 7 49q29 -8 51 -8q32 0 32 19q0 7 -4.5 11.5t-21.5 12.5q-43 20 -43 59q0 72 84 72 q30 0 50 -4zM977 721h28l-7 -52h-29q-2 -17 -6.5 -40.5t-7 -38.5t-2.5 -18q0 -16 19 -16q8 0 16 2l-8 -47q-21 -7 -40 -7q-43 0 -45 47q0 12 8 56q3 20 25 146h55zM1180 648q0 -23 -7 -52h-111q-3 -22 10 -33t38 -11q30 0 58 14l-9 -54q-30 -8 -57 -8q-95 0 -95 95 q0 55 27.5 90.5t69.5 35.5q35 0 55.5 -21t20.5 -56zM1319 722q-13 -23 -22 -62q-22 2 -31 -24t-25 -128h-56l3 14q22 130 29 199h51l-3 -33q14 21 25.5 29.5t28.5 4.5zM1506 763l-9 -57q-28 14 -50 14q-31 0 -51 -27.5t-20 -70.5q0 -30 13.5 -47t38.5 -17q21 0 48 13 l-10 -59q-28 -8 -50 -8q-45 0 -71.5 30.5t-26.5 82.5q0 70 35.5 114.5t91.5 44.5q26 0 61 -13zM1668 663q0 -18 -4 -42q-13 -79 -17 -113h-46l1 22q-20 -26 -59 -26q-23 0 -37 16t-14 42q0 39 25.5 60.5t72.5 21.5q15 0 23 -1q2 7 2 13q0 20 -36 20q-29 0 -59 -10q0 4 8 48 q38 11 67 11q73 0 73 -62zM1809 722q-14 -24 -21 -62q-23 2 -31.5 -23t-25.5 -129h-56l3 14q19 104 29 199h52q0 -11 -4 -33q15 21 26.5 29.5t27.5 4.5zM1950 770h56l-43 -262h-53l3 19q-23 -23 -52 -23q-31 0 -49.5 24t-18.5 64q0 53 27.5 92t64.5 39q31 0 53 -29z M2061 640q0 148 -72.5 273t-198 198t-273.5 73q-181 0 -328 -110q127 -116 171 -284h-50q-44 150 -158 253q-114 -103 -158 -253h-50q44 168 171 284q-147 110 -328 110q-148 0 -273.5 -73t-198 -198t-72.5 -273t72.5 -273t198 -198t273.5 -73q181 0 328 110 q-120 111 -165 264h50q46 -138 152 -233q106 95 152 233h50q-45 -153 -165 -264q147 -110 328 -110q148 0 273.5 73t198 198t72.5 273zM2304 1280v-1280q0 -52 -38 -90t-90 -38h-2048q-52 0 -90 38t-38 90v1280q0 52 38 90t90 38h2048q52 0 90 -38t38 -90z" /> -<glyph unicode="" horiz-adv-x="2304" d="M313 759q0 -51 -36 -84q-29 -26 -89 -26h-17v220h17q61 0 89 -27q36 -31 36 -83zM2089 824q0 -52 -64 -52h-19v101h20q63 0 63 -49zM380 759q0 74 -50 120.5t-129 46.5h-95v-333h95q74 0 119 38q60 51 60 128zM410 593h65v333h-65v-333zM730 694q0 40 -20.5 62t-75.5 42 q-29 10 -39.5 19t-10.5 23q0 16 13.5 26.5t34.5 10.5q29 0 53 -27l34 44q-41 37 -98 37q-44 0 -74 -27.5t-30 -67.5q0 -35 18 -55.5t64 -36.5q37 -13 45 -19q19 -12 19 -34q0 -20 -14 -33.5t-36 -13.5q-48 0 -71 44l-42 -40q44 -64 115 -64q51 0 83 30.5t32 79.5zM1008 604 v77q-37 -37 -78 -37q-49 0 -80.5 32.5t-31.5 82.5q0 48 31.5 81.5t77.5 33.5q43 0 81 -38v77q-40 20 -80 20q-74 0 -125.5 -50.5t-51.5 -123.5t51 -123.5t125 -50.5q42 0 81 19zM2240 0v527q-65 -40 -144.5 -84t-237.5 -117t-329.5 -137.5t-417.5 -134.5t-504 -118h1569 q26 0 45 19t19 45zM1389 757q0 75 -53 128t-128 53t-128 -53t-53 -128t53 -128t128 -53t128 53t53 128zM1541 584l144 342h-71l-90 -224l-89 224h-71l142 -342h35zM1714 593h184v56h-119v90h115v56h-115v74h119v57h-184v-333zM2105 593h80l-105 140q76 16 76 94q0 47 -31 73 t-87 26h-97v-333h65v133h9zM2304 1274v-1268q0 -56 -38.5 -95t-93.5 -39h-2040q-55 0 -93.5 39t-38.5 95v1268q0 56 38.5 95t93.5 39h2040q55 0 93.5 -39t38.5 -95z" /> -<glyph unicode="" horiz-adv-x="2304" d="M119 854h89l-45 108zM740 328l74 79l-70 79h-163v-49h142v-55h-142v-54h159zM898 406l99 -110v217zM1186 453q0 33 -40 33h-84v-69h83q41 0 41 36zM1475 457q0 29 -42 29h-82v-61h81q43 0 43 32zM1197 923q0 29 -42 29h-82v-60h81q43 0 43 31zM1656 854h89l-44 108z M699 1009v-271h-66v212l-94 -212h-57l-94 212v-212h-132l-25 60h-135l-25 -60h-70l116 271h96l110 -257v257h106l85 -184l77 184h108zM1255 453q0 -20 -5.5 -35t-14 -25t-22.5 -16.5t-26 -10t-31.5 -4.5t-31.5 -1t-32.5 0.5t-29.5 0.5v-91h-126l-80 90l-83 -90h-256v271h260 l80 -89l82 89h207q109 0 109 -89zM964 794v-56h-217v271h217v-57h-152v-49h148v-55h-148v-54h152zM2304 235v-229q0 -55 -38.5 -94.5t-93.5 -39.5h-2040q-55 0 -93.5 39.5t-38.5 94.5v678h111l25 61h55l25 -61h218v46l19 -46h113l20 47v-47h541v99l10 1q10 0 10 -14v-86h279 v23q23 -12 55 -18t52.5 -6.5t63 0.5t51.5 1l25 61h56l25 -61h227v58l34 -58h182v378h-180v-44l-25 44h-185v-44l-23 44h-249q-69 0 -109 -22v22h-172v-22q-24 22 -73 22h-628l-43 -97l-43 97h-198v-44l-22 44h-169l-78 -179v391q0 55 38.5 94.5t93.5 39.5h2040 q55 0 93.5 -39.5t38.5 -94.5v-678h-120q-51 0 -81 -22v22h-177q-55 0 -78 -22v22h-316v-22q-31 22 -87 22h-209v-22q-23 22 -91 22h-234l-54 -58l-50 58h-349v-378h343l55 59l52 -59h211v89h21q59 0 90 13v-102h174v99h8q8 0 10 -2t2 -10v-87h529q57 0 88 24v-24h168 q60 0 95 17zM1546 469q0 -23 -12 -43t-34 -29q25 -9 34 -26t9 -46v-54h-65v45q0 33 -12 43.5t-46 10.5h-69v-99h-65v271h154q48 0 77 -15t29 -58zM1269 936q0 -24 -12.5 -44t-33.5 -29q26 -9 34.5 -25.5t8.5 -46.5v-53h-65q0 9 0.5 26.5t0 25t-3 18.5t-8.5 16t-17.5 8.5 t-29.5 3.5h-70v-98h-64v271l153 -1q49 0 78 -14.5t29 -57.5zM1798 327v-56h-216v271h216v-56h-151v-49h148v-55h-148v-54zM1372 1009v-271h-66v271h66zM2065 357q0 -86 -102 -86h-126v58h126q34 0 34 25q0 16 -17 21t-41.5 5t-49.5 3.5t-42 22.5t-17 55q0 39 26 60t66 21 h130v-57h-119q-36 0 -36 -25q0 -16 17.5 -20.5t42 -4t49 -2.5t42 -21.5t17.5 -54.5zM2304 407v-101q-24 -35 -88 -35h-125v58h125q33 0 33 25q0 13 -12.5 19t-31 5.5t-40 2t-40 8t-31 24t-12.5 48.5q0 39 26.5 60t66.5 21h129v-57h-118q-36 0 -36 -25q0 -20 29 -22t68.5 -5 t56.5 -26zM2139 1008v-270h-92l-122 203v-203h-132l-26 60h-134l-25 -60h-75q-129 0 -129 133q0 138 133 138h63v-59q-7 0 -28 1t-28.5 0.5t-23 -2t-21.5 -6.5t-14.5 -13.5t-11.5 -23t-3 -33.5q0 -38 13.5 -58t49.5 -20h29l92 213h97l109 -256v256h99l114 -188v188h66z" /> -<glyph unicode="" horiz-adv-x="2304" d="M322 689h-15q-19 0 -19 18q0 28 19 85q5 15 15 19.5t28 4.5q77 0 77 -49q0 -41 -30.5 -59.5t-74.5 -18.5zM664 528q-47 0 -47 29q0 62 123 62l3 -3q-5 -88 -79 -88zM1438 687h-15q-19 0 -19 19q0 28 19 85q5 15 14.5 19t28.5 4q77 0 77 -49q0 -41 -30.5 -59.5 t-74.5 -18.5zM1780 527q-47 0 -47 30q0 62 123 62l3 -3q-5 -89 -79 -89zM373 894h-128q-8 0 -14.5 -4t-8.5 -7.5t-7 -12.5q-3 -7 -45 -190t-42 -192q0 -7 5.5 -12.5t13.5 -5.5h62q25 0 32.5 34.5l15 69t32.5 34.5q47 0 87.5 7.5t80.5 24.5t63.5 52.5t23.5 84.5 q0 36 -14.5 61t-41 36.5t-53.5 15.5t-62 4zM719 798q-38 0 -74 -6q-2 0 -8.5 -1t-9 -1.5l-7.5 -1.5t-7.5 -2t-6.5 -3t-6.5 -4t-5 -5t-4.5 -7t-4 -9q-9 -29 -9 -39t9 -10q5 0 21.5 5t19.5 6q30 8 58 8q74 0 74 -36q0 -11 -10 -14q-8 -2 -18 -3t-21.5 -1.5t-17.5 -1.5 q-38 -4 -64.5 -10t-56.5 -19.5t-45.5 -39t-15.5 -62.5q0 -38 26 -59.5t64 -21.5q24 0 45.5 6.5t33 13t38.5 23.5q-3 -7 -3 -15t5.5 -13.5t12.5 -5.5h56q1 1 7 3.5t7.5 3.5t5 3.5t5 5.5t2.5 8l45 194q4 13 4 30q0 81 -145 81zM1247 793h-74q-22 0 -39 -23q-5 -7 -29.5 -51 t-46.5 -81.5t-26 -38.5l-5 4q0 77 -27 166q-1 5 -3.5 8.5t-6 6.5t-6.5 5t-8.5 3t-8.5 1.5t-9.5 1t-9 0.5h-10h-8.5q-38 0 -38 -21l1 -5q5 -53 25 -151t25 -143q2 -16 2 -24q0 -19 -30.5 -61.5t-30.5 -58.5q0 -13 40 -13q61 0 76 25l245 415q10 20 10 26q0 9 -8 9zM1489 892 h-129q-18 0 -29 -23q-6 -13 -46.5 -191.5t-40.5 -190.5q0 -20 43 -20h7.5h9h9t9.5 1t8.5 2t8.5 3t6.5 4.5t5.5 6t3 8.5l21 91q2 10 10.5 17t19.5 7q47 0 87.5 7t80.5 24.5t63.5 52.5t23.5 84q0 36 -14.5 61t-41 36.5t-53.5 15.5t-62 4zM1835 798q-26 0 -74 -6 q-38 -6 -48 -16q-7 -8 -11 -19q-8 -24 -8 -39q0 -10 8 -10q1 0 41 12q30 8 58 8q74 0 74 -36q0 -12 -10 -14q-4 -1 -57 -7q-38 -4 -64.5 -10t-56.5 -19.5t-45.5 -39t-15.5 -62.5t26 -58.5t64 -21.5q24 0 45 6t34 13t38 24q-3 -15 -3 -16q0 -5 2 -8.5t6.5 -5.5t8 -3.5 t10.5 -2t9.5 -0.5h9.5h8q42 0 48 25l45 194q3 15 3 31q0 81 -145 81zM2157 889h-55q-25 0 -33 -40q-10 -44 -36.5 -167t-42.5 -190v-5q0 -16 16 -18h1h57q10 0 18.5 6.5t10.5 16.5l83 374h-1l1 5q0 7 -5.5 12.5t-13.5 5.5zM2304 1280v-1280q0 -52 -38 -90t-90 -38h-2048 q-52 0 -90 38t-38 90v1280q0 52 38 90t90 38h2048q52 0 90 -38t38 -90z" /> -<glyph unicode="" horiz-adv-x="2304" d="M1597 633q0 -69 -21 -106q-19 -35 -52 -35q-23 0 -41 9v224q29 30 57 30q57 0 57 -122zM2035 669h-110q6 98 56 98q51 0 54 -98zM476 534q0 59 -33 91.5t-101 57.5q-36 13 -52 24t-16 25q0 26 38 26q58 0 124 -33l18 112q-67 32 -149 32q-77 0 -123 -38q-48 -39 -48 -109 q0 -58 32.5 -90.5t99.5 -56.5q39 -14 54.5 -25.5t15.5 -27.5q0 -31 -48 -31q-29 0 -70 12.5t-72 30.5l-18 -113q72 -41 168 -41q81 0 129 37q51 41 51 117zM771 749l19 111h-96v135l-129 -21l-18 -114l-46 -8l-17 -103h62v-219q0 -84 44 -120q38 -30 111 -30q32 0 79 11v118 q-32 -7 -44 -7q-42 0 -42 50v197h77zM1087 724v139q-15 3 -28 3q-32 0 -55.5 -16t-33.5 -46l-10 56h-131v-471h150v306q26 31 82 31q16 0 26 -2zM1124 389h150v471h-150v-471zM1746 638q0 122 -45 179q-40 52 -111 52q-64 0 -117 -56l-8 47h-132v-645l150 25v151 q36 -11 68 -11q83 0 134 56q61 65 61 202zM1278 986q0 33 -23 56t-56 23t-56 -23t-23 -56t23 -56.5t56 -23.5t56 23.5t23 56.5zM2176 629q0 113 -48 176q-50 64 -144 64q-96 0 -151.5 -66t-55.5 -180q0 -128 63 -188q55 -55 161 -55q101 0 160 40l-16 103q-57 -31 -128 -31 q-43 0 -63 19q-23 19 -28 66h248q2 14 2 52zM2304 1280v-1280q0 -52 -38 -90t-90 -38h-2048q-52 0 -90 38t-38 90v1280q0 52 38 90t90 38h2048q52 0 90 -38t38 -90z" /> -<glyph unicode="" horiz-adv-x="2048" d="M1558 684q61 -356 298 -556q0 -52 -38 -90t-90 -38h-448q0 -106 -75 -181t-181 -75t-180.5 74.5t-75.5 180.5zM1024 -176q16 0 16 16t-16 16q-59 0 -101.5 42.5t-42.5 101.5q0 16 -16 16t-16 -16q0 -73 51.5 -124.5t124.5 -51.5zM2026 1424q8 -10 7.5 -23.5t-10.5 -22.5 l-1872 -1622q-10 -8 -23.5 -7t-21.5 11l-84 96q-8 10 -7.5 23.5t10.5 21.5l186 161q-19 32 -19 66q50 42 91 88t85 119.5t74.5 158.5t50 206t19.5 260q0 152 117 282.5t307 158.5q-8 19 -8 39q0 40 28 68t68 28t68 -28t28 -68q0 -20 -8 -39q124 -18 219 -82.5t148 -157.5 l418 363q10 8 23.5 7t21.5 -11z" /> -<glyph unicode="" horiz-adv-x="2048" d="M1040 -160q0 16 -16 16q-59 0 -101.5 42.5t-42.5 101.5q0 16 -16 16t-16 -16q0 -73 51.5 -124.5t124.5 -51.5q16 0 16 16zM503 315l877 760q-42 88 -132.5 146.5t-223.5 58.5q-93 0 -169.5 -31.5t-121.5 -80.5t-69 -103t-24 -105q0 -384 -137 -645zM1856 128 q0 -52 -38 -90t-90 -38h-448q0 -106 -75 -181t-181 -75t-180.5 74.5t-75.5 180.5l149 129h757q-166 187 -227 459l111 97q61 -356 298 -556zM1942 1520l84 -96q8 -10 7.5 -23.5t-10.5 -22.5l-1872 -1622q-10 -8 -23.5 -7t-21.5 11l-84 96q-8 10 -7.5 23.5t10.5 21.5l186 161 q-19 32 -19 66q50 42 91 88t85 119.5t74.5 158.5t50 206t19.5 260q0 152 117 282.5t307 158.5q-8 19 -8 39q0 40 28 68t68 28t68 -28t28 -68q0 -20 -8 -39q124 -18 219 -82.5t148 -157.5l418 363q10 8 23.5 7t21.5 -11z" /> -<glyph unicode="" horiz-adv-x="1408" d="M512 160v704q0 14 -9 23t-23 9h-64q-14 0 -23 -9t-9 -23v-704q0 -14 9 -23t23 -9h64q14 0 23 9t9 23zM768 160v704q0 14 -9 23t-23 9h-64q-14 0 -23 -9t-9 -23v-704q0 -14 9 -23t23 -9h64q14 0 23 9t9 23zM1024 160v704q0 14 -9 23t-23 9h-64q-14 0 -23 -9t-9 -23v-704 q0 -14 9 -23t23 -9h64q14 0 23 9t9 23zM480 1152h448l-48 117q-7 9 -17 11h-317q-10 -2 -17 -11zM1408 1120v-64q0 -14 -9 -23t-23 -9h-96v-948q0 -83 -47 -143.5t-113 -60.5h-832q-66 0 -113 58.5t-47 141.5v952h-96q-14 0 -23 9t-9 23v64q0 14 9 23t23 9h309l70 167 q15 37 54 63t79 26h320q40 0 79 -26t54 -63l70 -167h309q14 0 23 -9t9 -23z" /> -<glyph unicode="" d="M1150 462v-109q0 -50 -36.5 -89t-94 -60.5t-118 -32.5t-117.5 -11q-205 0 -342.5 139t-137.5 346q0 203 136 339t339 136q34 0 75.5 -4.5t93 -18t92.5 -34t69 -56.5t28 -81v-109q0 -16 -16 -16h-118q-16 0 -16 16v70q0 43 -65.5 67.5t-137.5 24.5q-140 0 -228.5 -91.5 t-88.5 -237.5q0 -151 91.5 -249.5t233.5 -98.5q68 0 138 24t70 66v70q0 7 4.5 11.5t10.5 4.5h119q6 0 11 -4.5t5 -11.5zM768 1280q-130 0 -248.5 -51t-204 -136.5t-136.5 -204t-51 -248.5t51 -248.5t136.5 -204t204 -136.5t248.5 -51t248.5 51t204 136.5t136.5 204t51 248.5 t-51 248.5t-136.5 204t-204 136.5t-248.5 51zM1536 640q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5t103 -385.5z" /> -<glyph unicode="" d="M972 761q0 108 -53.5 169t-147.5 61q-63 0 -124 -30.5t-110 -84.5t-79.5 -137t-30.5 -180q0 -112 53.5 -173t150.5 -61q96 0 176 66.5t122.5 166t42.5 203.5zM1536 640q0 -111 -37 -197t-98.5 -135t-131.5 -74.5t-145 -27.5q-6 0 -15.5 -0.5t-16.5 -0.5q-95 0 -142 53 q-28 33 -33 83q-52 -66 -131.5 -110t-173.5 -44q-161 0 -249.5 95.5t-88.5 269.5q0 157 66 290t179 210.5t246 77.5q87 0 155 -35.5t106 -99.5l2 19l11 56q1 6 5.5 12t9.5 6h118q5 0 13 -11q5 -5 3 -16l-120 -614q-5 -24 -5 -48q0 -39 12.5 -52t44.5 -13q28 1 57 5.5t73 24 t77 50t57 89.5t24 137q0 292 -174 466t-466 174q-130 0 -248.5 -51t-204 -136.5t-136.5 -204t-51 -248.5t51 -248.5t136.5 -204t204 -136.5t248.5 -51q228 0 405 144q11 9 24 8t21 -12l41 -49q8 -12 7 -24q-2 -13 -12 -22q-102 -83 -227.5 -128t-258.5 -45q-156 0 -298 61 t-245 164t-164 245t-61 298t61 298t164 245t245 164t298 61q344 0 556 -212t212 -556z" /> -<glyph unicode="" horiz-adv-x="1792" d="M1698 1442q94 -94 94 -226.5t-94 -225.5l-225 -223l104 -104q10 -10 10 -23t-10 -23l-210 -210q-10 -10 -23 -10t-23 10l-105 105l-603 -603q-37 -37 -90 -37h-203l-256 -128l-64 64l128 256v203q0 53 37 90l603 603l-105 105q-10 10 -10 23t10 23l210 210q10 10 23 10 t23 -10l104 -104l223 225q93 94 225.5 94t226.5 -94zM512 64l576 576l-192 192l-576 -576v-192h192z" /> -<glyph unicode="" horiz-adv-x="1792" d="M1615 1536q70 0 122.5 -46.5t52.5 -116.5q0 -63 -45 -151q-332 -629 -465 -752q-97 -91 -218 -91q-126 0 -216.5 92.5t-90.5 219.5q0 128 92 212l638 579q59 54 130 54zM706 502q39 -76 106.5 -130t150.5 -76l1 -71q4 -213 -129.5 -347t-348.5 -134q-123 0 -218 46.5 t-152.5 127.5t-86.5 183t-29 220q7 -5 41 -30t62 -44.5t59 -36.5t46 -17q41 0 55 37q25 66 57.5 112.5t69.5 76t88 47.5t103 25.5t125 10.5z" /> -<glyph unicode="" horiz-adv-x="1792" d="M1792 128v-384h-1792v384q45 0 85 14t59 27.5t47 37.5q30 27 51.5 38t56.5 11t55.5 -11t52.5 -38q29 -25 47 -38t58 -27t86 -14q45 0 85 14.5t58 27t48 37.5q21 19 32.5 27t31 15t43.5 7q35 0 56.5 -11t51.5 -38q28 -24 47 -37.5t59 -27.5t85 -14t85 14t59 27.5t47 37.5 q30 27 51.5 38t56.5 11q34 0 55.5 -11t51.5 -38q28 -24 47 -37.5t59 -27.5t85 -14zM1792 448v-192q-35 0 -55.5 11t-52.5 38q-29 25 -47 38t-58 27t-85 14q-46 0 -86 -14t-58 -27t-47 -38q-22 -19 -33 -27t-31 -15t-44 -7q-35 0 -56.5 11t-51.5 38q-29 25 -47 38t-58 27 t-86 14q-45 0 -85 -14.5t-58 -27t-48 -37.5q-21 -19 -32.5 -27t-31 -15t-43.5 -7q-35 0 -56.5 11t-51.5 38q-28 24 -47 37.5t-59 27.5t-85 14q-46 0 -86 -14t-58 -27t-47 -38q-30 -27 -51.5 -38t-56.5 -11v192q0 80 56 136t136 56h64v448h256v-448h256v448h256v-448h256v448 h256v-448h64q80 0 136 -56t56 -136zM512 1312q0 -77 -36 -118.5t-92 -41.5q-53 0 -90.5 37.5t-37.5 90.5q0 29 9.5 51t23.5 34t31 28t31 31.5t23.5 44.5t9.5 67q38 0 83 -74t45 -150zM1024 1312q0 -77 -36 -118.5t-92 -41.5q-53 0 -90.5 37.5t-37.5 90.5q0 29 9.5 51 t23.5 34t31 28t31 31.5t23.5 44.5t9.5 67q38 0 83 -74t45 -150zM1536 1312q0 -77 -36 -118.5t-92 -41.5q-53 0 -90.5 37.5t-37.5 90.5q0 29 9.5 51t23.5 34t31 28t31 31.5t23.5 44.5t9.5 67q38 0 83 -74t45 -150z" /> -<glyph unicode="" horiz-adv-x="2048" d="M2048 0v-128h-2048v1536h128v-1408h1920zM1664 1024l256 -896h-1664v576l448 576l576 -576z" /> -<glyph unicode="" horiz-adv-x="1792" d="M768 646l546 -546q-106 -108 -247.5 -168t-298.5 -60q-209 0 -385.5 103t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103v-762zM955 640h773q0 -157 -60 -298.5t-168 -247.5zM1664 768h-768v768q209 0 385.5 -103t279.5 -279.5t103 -385.5z" /> -<glyph unicode="" horiz-adv-x="2048" d="M2048 0v-128h-2048v1536h128v-1408h1920zM1920 1248v-435q0 -21 -19.5 -29.5t-35.5 7.5l-121 121l-633 -633q-10 -10 -23 -10t-23 10l-233 233l-416 -416l-192 192l585 585q10 10 23 10t23 -10l233 -233l464 464l-121 121q-16 16 -7.5 35.5t29.5 19.5h435q14 0 23 -9 t9 -23z" /> -<glyph unicode="" horiz-adv-x="1792" d="M1292 832q0 -6 10 -41q10 -29 25 -49.5t41 -34t44 -20t55 -16.5q325 -91 325 -332q0 -146 -105.5 -242.5t-254.5 -96.5q-59 0 -111.5 18.5t-91.5 45.5t-77 74.5t-63 87.5t-53.5 103.5t-43.5 103t-39.5 106.5t-35.5 95q-32 81 -61.5 133.5t-73.5 96.5t-104 64t-142 20 q-96 0 -183 -55.5t-138 -144.5t-51 -185q0 -160 106.5 -279.5t263.5 -119.5q177 0 258 95q56 63 83 116l84 -152q-15 -34 -44 -70l1 -1q-131 -152 -388 -152q-147 0 -269.5 79t-190.5 207.5t-68 274.5q0 105 43.5 206t116 176.5t172 121.5t204.5 46q87 0 159 -19t123.5 -50 t95 -80t72.5 -99t58.5 -117t50.5 -124.5t50 -130.5t55 -127q96 -200 233 -200q81 0 138.5 48.5t57.5 128.5q0 42 -19 72t-50.5 46t-72.5 31.5t-84.5 27t-87.5 34t-81 52t-65 82t-39 122.5q-3 16 -3 33q0 110 87.5 192t198.5 78q78 -3 120.5 -14.5t90.5 -53.5h-1 q12 -11 23 -24.5t26 -36t19 -27.5l-129 -99q-26 49 -54 70v1q-23 21 -97 21q-49 0 -84 -33t-35 -83z" /> -<glyph unicode="" d="M1432 484q0 173 -234 239q-35 10 -53 16.5t-38 25t-29 46.5q0 2 -2 8.5t-3 12t-1 7.5q0 36 24.5 59.5t60.5 23.5q54 0 71 -15h-1q20 -15 39 -51l93 71q-39 54 -49 64q-33 29 -67.5 39t-85.5 10q-80 0 -142 -57.5t-62 -137.5q0 -7 2 -23q16 -96 64.5 -140t148.5 -73 q29 -8 49 -15.5t45 -21.5t38.5 -34.5t13.5 -46.5v-5q1 -58 -40.5 -93t-100.5 -35q-97 0 -167 144q-23 47 -51.5 121.5t-48 125.5t-54 110.5t-74 95.5t-103.5 60.5t-147 24.5q-101 0 -192 -56t-144 -148t-50 -192v-1q4 -108 50.5 -199t133.5 -147.5t196 -56.5q186 0 279 110 q20 27 31 51l-60 109q-42 -80 -99 -116t-146 -36q-115 0 -191 87t-76 204q0 105 82 189t186 84q112 0 170 -53.5t104 -172.5q8 -21 25.5 -68.5t28.5 -76.5t31.5 -74.5t38.5 -74t45.5 -62.5t55.5 -53.5t66 -33t80 -13.5q107 0 183 69.5t76 174.5zM1536 1120v-960 q0 -119 -84.5 -203.5t-203.5 -84.5h-960q-119 0 -203.5 84.5t-84.5 203.5v960q0 119 84.5 203.5t203.5 84.5h960q119 0 203.5 -84.5t84.5 -203.5z" /> -<glyph unicode="" horiz-adv-x="2048" d="M1152 640q0 104 -40.5 198.5t-109.5 163.5t-163.5 109.5t-198.5 40.5t-198.5 -40.5t-163.5 -109.5t-109.5 -163.5t-40.5 -198.5t40.5 -198.5t109.5 -163.5t163.5 -109.5t198.5 -40.5t198.5 40.5t163.5 109.5t109.5 163.5t40.5 198.5zM1920 640q0 104 -40.5 198.5 t-109.5 163.5t-163.5 109.5t-198.5 40.5h-386q119 -90 188.5 -224t69.5 -288t-69.5 -288t-188.5 -224h386q104 0 198.5 40.5t163.5 109.5t109.5 163.5t40.5 198.5zM2048 640q0 -130 -51 -248.5t-136.5 -204t-204 -136.5t-248.5 -51h-768q-130 0 -248.5 51t-204 136.5 t-136.5 204t-51 248.5t51 248.5t136.5 204t204 136.5t248.5 51h768q130 0 248.5 -51t204 -136.5t136.5 -204t51 -248.5z" /> -<glyph unicode="" horiz-adv-x="2048" d="M0 640q0 130 51 248.5t136.5 204t204 136.5t248.5 51h768q130 0 248.5 -51t204 -136.5t136.5 -204t51 -248.5t-51 -248.5t-136.5 -204t-204 -136.5t-248.5 -51h-768q-130 0 -248.5 51t-204 136.5t-136.5 204t-51 248.5zM1408 128q104 0 198.5 40.5t163.5 109.5 t109.5 163.5t40.5 198.5t-40.5 198.5t-109.5 163.5t-163.5 109.5t-198.5 40.5t-198.5 -40.5t-163.5 -109.5t-109.5 -163.5t-40.5 -198.5t40.5 -198.5t109.5 -163.5t163.5 -109.5t198.5 -40.5z" /> -<glyph unicode="" horiz-adv-x="2304" d="M762 384h-314q-40 0 -57.5 35t6.5 67l188 251q-65 31 -137 31q-132 0 -226 -94t-94 -226t94 -226t226 -94q115 0 203 72.5t111 183.5zM576 512h186q-18 85 -75 148zM1056 512l288 384h-480l-99 -132q105 -103 126 -252h165zM2176 448q0 132 -94 226t-226 94 q-60 0 -121 -24l174 -260q15 -23 10 -49t-27 -40q-15 -11 -36 -11q-35 0 -53 29l-174 260q-93 -95 -93 -225q0 -132 94 -226t226 -94t226 94t94 226zM2304 448q0 -185 -131.5 -316.5t-316.5 -131.5t-316.5 131.5t-131.5 316.5q0 97 39.5 183.5t109.5 149.5l-65 98l-353 -469 q-18 -26 -51 -26h-197q-23 -164 -149 -274t-294 -110q-185 0 -316.5 131.5t-131.5 316.5t131.5 316.5t316.5 131.5q114 0 215 -55l137 183h-224q-26 0 -45 19t-19 45t19 45t45 19h384v-128h435l-85 128h-222q-26 0 -45 19t-19 45t19 45t45 19h256q33 0 53 -28l267 -400 q91 44 192 44q185 0 316.5 -131.5t131.5 -316.5z" /> -<glyph unicode="" d="M384 320q0 53 -37.5 90.5t-90.5 37.5t-90.5 -37.5t-37.5 -90.5t37.5 -90.5t90.5 -37.5t90.5 37.5t37.5 90.5zM1408 320q0 53 -37.5 90.5t-90.5 37.5t-90.5 -37.5t-37.5 -90.5t37.5 -90.5t90.5 -37.5t90.5 37.5t37.5 90.5zM1362 716l-72 384q-5 23 -22.5 37.5t-40.5 14.5 h-918q-23 0 -40.5 -14.5t-22.5 -37.5l-72 -384q-5 -30 14 -53t49 -23h1062q30 0 49 23t14 53zM1136 1328q0 20 -14 34t-34 14h-640q-20 0 -34 -14t-14 -34t14 -34t34 -14h640q20 0 34 14t14 34zM1536 603v-603h-128v-128q0 -53 -37.5 -90.5t-90.5 -37.5t-90.5 37.5 t-37.5 90.5v128h-768v-128q0 -53 -37.5 -90.5t-90.5 -37.5t-90.5 37.5t-37.5 90.5v128h-128v603q0 112 25 223l103 454q9 78 97.5 137t230 89t312.5 30t312.5 -30t230 -89t97.5 -137l105 -454q23 -102 23 -223z" /> -<glyph unicode="" horiz-adv-x="2048" d="M1463 704q0 -35 -25 -60.5t-61 -25.5h-702q-36 0 -61 25.5t-25 60.5t25 60.5t61 25.5h702q36 0 61 -25.5t25 -60.5zM1677 704q0 86 -23 170h-982q-36 0 -61 25t-25 60q0 36 25 61t61 25h908q-88 143 -235 227t-320 84q-177 0 -327.5 -87.5t-238 -237.5t-87.5 -327 q0 -86 23 -170h982q36 0 61 -25t25 -60q0 -36 -25 -61t-61 -25h-908q88 -143 235.5 -227t320.5 -84q132 0 253 51.5t208 139t139 208t52 253.5zM2048 959q0 -35 -25 -60t-61 -25h-131q17 -85 17 -170q0 -167 -65.5 -319.5t-175.5 -263t-262.5 -176t-319.5 -65.5 q-246 0 -448.5 133t-301.5 350h-189q-36 0 -61 25t-25 61q0 35 25 60t61 25h132q-17 85 -17 170q0 167 65.5 319.5t175.5 263t262.5 176t320.5 65.5q245 0 447.5 -133t301.5 -350h188q36 0 61 -25t25 -61z" /> -<glyph unicode="" horiz-adv-x="1280" d="M953 1158l-114 -328l117 -21q165 451 165 518q0 56 -38 56q-57 0 -130 -225zM654 471l33 -88q37 42 71 67l-33 5.5t-38.5 7t-32.5 8.5zM362 1367q0 -98 159 -521q18 10 49 10q15 0 75 -5l-121 351q-75 220 -123 220q-19 0 -29 -17.5t-10 -37.5zM283 608q0 -36 51.5 -119 t117.5 -153t100 -70q14 0 25.5 13t11.5 27q0 24 -32 102q-13 32 -32 72t-47.5 89t-61.5 81t-62 32q-20 0 -45.5 -27t-25.5 -47zM125 273q0 -41 25 -104q59 -145 183.5 -227t281.5 -82q227 0 382 170q152 169 152 427q0 43 -1 67t-11.5 62t-30.5 56q-56 49 -211.5 75.5 t-270.5 26.5q-37 0 -49 -11q-12 -5 -12 -35q0 -34 21.5 -60t55.5 -40t77.5 -23.5t87.5 -11.5t85 -4t70 0h23q24 0 40 -19q15 -19 19 -55q-28 -28 -96 -54q-61 -22 -93 -46q-64 -46 -108.5 -114t-44.5 -137q0 -31 18.5 -88.5t18.5 -87.5l-3 -12q-4 -12 -4 -14 q-137 10 -146 216q-8 -2 -41 -2q2 -7 2 -21q0 -53 -40.5 -89.5t-94.5 -36.5q-82 0 -166.5 78t-84.5 159q0 34 33 67q52 -64 60 -76q77 -104 133 -104q12 0 26.5 8.5t14.5 20.5q0 34 -87.5 145t-116.5 111q-43 0 -70 -44.5t-27 -90.5zM11 264q0 101 42.5 163t136.5 88 q-28 74 -28 104q0 62 61 123t122 61q29 0 70 -15q-163 462 -163 567q0 80 41 130.5t119 50.5q131 0 325 -581q6 -17 8 -23q6 16 29 79.5t43.5 118.5t54 127.5t64.5 123t70.5 86.5t76.5 36q71 0 112 -49t41 -122q0 -108 -159 -550q61 -15 100.5 -46t58.5 -78t26 -93.5 t7 -110.5q0 -150 -47 -280t-132 -225t-211 -150t-278 -55q-111 0 -223 42q-149 57 -258 191.5t-109 286.5z" /> -<glyph unicode="" horiz-adv-x="2048" d="M785 528h207q-14 -158 -98.5 -248.5t-214.5 -90.5q-162 0 -254.5 116t-92.5 316q0 194 93 311.5t233 117.5q148 0 232 -87t97 -247h-203q-5 64 -35.5 99t-81.5 35q-57 0 -88.5 -60.5t-31.5 -177.5q0 -48 5 -84t18 -69.5t40 -51.5t66 -18q95 0 109 139zM1497 528h206 q-14 -158 -98 -248.5t-214 -90.5q-162 0 -254.5 116t-92.5 316q0 194 93 311.5t233 117.5q148 0 232 -87t97 -247h-204q-4 64 -35 99t-81 35q-57 0 -88.5 -60.5t-31.5 -177.5q0 -48 5 -84t18 -69.5t39.5 -51.5t65.5 -18q49 0 76.5 38t33.5 101zM1856 647q0 207 -15.5 307 t-60.5 161q-6 8 -13.5 14t-21.5 15t-16 11q-86 63 -697 63q-625 0 -710 -63q-5 -4 -17.5 -11.5t-21 -14t-14.5 -14.5q-45 -60 -60 -159.5t-15 -308.5q0 -208 15 -307.5t60 -160.5q6 -8 15 -15t20.5 -14t17.5 -12q44 -33 239.5 -49t470.5 -16q610 0 697 65q5 4 17 11t20.5 14 t13.5 16q46 60 61 159t15 309zM2048 1408v-1536h-2048v1536h2048z" /> -<glyph unicode="" d="M992 912v-496q0 -14 -9 -23t-23 -9h-160q-14 0 -23 9t-9 23v496q0 112 -80 192t-192 80h-272v-1152q0 -14 -9 -23t-23 -9h-160q-14 0 -23 9t-9 23v1344q0 14 9 23t23 9h464q135 0 249 -66.5t180.5 -180.5t66.5 -249zM1376 1376v-880q0 -135 -66.5 -249t-180.5 -180.5 t-249 -66.5h-464q-14 0 -23 9t-9 23v960q0 14 9 23t23 9h160q14 0 23 -9t9 -23v-768h272q112 0 192 80t80 192v880q0 14 9 23t23 9h160q14 0 23 -9t9 -23z" /> -<glyph unicode="" d="M1311 694v-114q0 -24 -13.5 -38t-37.5 -14h-202q-24 0 -38 14t-14 38v114q0 24 14 38t38 14h202q24 0 37.5 -14t13.5 -38zM821 464v250q0 53 -32.5 85.5t-85.5 32.5h-133q-68 0 -96 -52q-28 52 -96 52h-130q-53 0 -85.5 -32.5t-32.5 -85.5v-250q0 -22 21 -22h55 q22 0 22 22v230q0 24 13.5 38t38.5 14h94q24 0 38 -14t14 -38v-230q0 -22 21 -22h54q22 0 22 22v230q0 24 14 38t38 14h97q24 0 37.5 -14t13.5 -38v-230q0 -22 22 -22h55q21 0 21 22zM1410 560v154q0 53 -33 85.5t-86 32.5h-264q-53 0 -86 -32.5t-33 -85.5v-410 q0 -21 22 -21h55q21 0 21 21v180q31 -42 94 -42h191q53 0 86 32.5t33 85.5zM1536 1176v-1072q0 -96 -68 -164t-164 -68h-1072q-96 0 -164 68t-68 164v1072q0 96 68 164t164 68h1072q96 0 164 -68t68 -164z" /> -<glyph unicode="" d="M915 450h-294l147 551zM1001 128h311l-324 1024h-440l-324 -1024h311l383 314zM1536 1120v-960q0 -118 -85 -203t-203 -85h-960q-118 0 -203 85t-85 203v960q0 118 85 203t203 85h960q118 0 203 -85t85 -203z" /> -<glyph unicode="" horiz-adv-x="2048" d="M2048 641q0 -21 -13 -36.5t-33 -19.5l-205 -356q3 -9 3 -18q0 -20 -12.5 -35.5t-32.5 -19.5l-193 -337q3 -8 3 -16q0 -23 -16.5 -40t-40.5 -17q-25 0 -41 18h-400q-17 -20 -43 -20t-43 20h-399q-17 -20 -43 -20q-23 0 -40 16.5t-17 40.5q0 8 4 20l-193 335 q-20 4 -32.5 19.5t-12.5 35.5q0 9 3 18l-206 356q-20 5 -32.5 20.5t-12.5 35.5q0 21 13.5 36.5t33.5 19.5l199 344q0 1 -0.5 3t-0.5 3q0 36 34 51l209 363q-4 10 -4 18q0 24 17 40.5t40 16.5q26 0 44 -21h396q16 21 43 21t43 -21h398q18 21 44 21q23 0 40 -16.5t17 -40.5 q0 -6 -4 -18l207 -358q23 -1 39 -17.5t16 -38.5q0 -13 -7 -27l187 -324q19 -4 31.5 -19.5t12.5 -35.5zM1063 -158h389l-342 354h-143l-342 -354h360q18 16 39 16t39 -16zM112 654q1 -4 1 -13q0 -10 -2 -15l208 -360q2 0 4.5 -1t5.5 -2.5l5 -2.5l188 199v347l-187 194 q-13 -8 -29 -10zM986 1438h-388l190 -200l554 200h-280q-16 -16 -38 -16t-38 16zM1689 226q1 6 5 11l-64 68l-17 -79h76zM1583 226l22 105l-252 266l-296 -307l63 -64h463zM1495 -142l16 28l65 310h-427l333 -343q8 4 13 5zM578 -158h5l342 354h-373v-335l4 -6q14 -5 22 -13 zM552 226h402l64 66l-309 321l-157 -166v-221zM359 226h163v189l-168 -177q4 -8 5 -12zM358 1051q0 -1 0.5 -2t0.5 -2q0 -16 -8 -29l171 -177v269zM552 1121v-311l153 -157l297 314l-223 236zM556 1425l-4 -8v-264l205 74l-191 201q-6 -2 -10 -3zM1447 1438h-16l-621 -224 l213 -225zM1023 946l-297 -315l311 -319l296 307zM688 634l-136 141v-284zM1038 270l-42 -44h85zM1374 618l238 -251l132 624l-3 5l-1 1zM1718 1018q-8 13 -8 29v2l-216 376q-5 1 -13 5l-437 -463l310 -327zM522 1142v223l-163 -282zM522 196h-163l163 -283v283zM1607 196 l-48 -227l130 227h-82zM1729 266l207 361q-2 10 -2 14q0 1 3 16l-171 296l-129 -612l77 -82q5 3 15 7z" /> -<glyph unicode="" d="M0 856q0 131 91.5 226.5t222.5 95.5h742l352 358v-1470q0 -132 -91.5 -227t-222.5 -95h-780q-131 0 -222.5 95t-91.5 227v790zM1232 102l-176 180v425q0 46 -32 79t-78 33h-484q-46 0 -78 -33t-32 -79v-492q0 -46 32.5 -79.5t77.5 -33.5h770z" /> -<glyph unicode="" d="M934 1386q-317 -121 -556 -362.5t-358 -560.5q-20 89 -20 176q0 208 102.5 384.5t278.5 279t384 102.5q82 0 169 -19zM1203 1267q93 -65 164 -155q-389 -113 -674.5 -400.5t-396.5 -676.5q-93 72 -155 162q112 386 395 671t667 399zM470 -67q115 356 379.5 622t619.5 384 q40 -92 54 -195q-292 -120 -516 -345t-343 -518q-103 14 -194 52zM1536 -125q-193 50 -367 115q-135 -84 -290 -107q109 205 274 370.5t369 275.5q-21 -152 -101 -284q65 -175 115 -370z" /> -<glyph unicode="" horiz-adv-x="2048" d="M1893 1144l155 -1272q-131 0 -257 57q-200 91 -393 91q-226 0 -374 -148q-148 148 -374 148q-193 0 -393 -91q-128 -57 -252 -57h-5l155 1272q224 127 482 127q233 0 387 -106q154 106 387 106q258 0 482 -127zM1398 157q129 0 232 -28.5t260 -93.5l-124 1021 q-171 78 -368 78q-224 0 -374 -141q-150 141 -374 141q-197 0 -368 -78l-124 -1021q105 43 165.5 65t148.5 39.5t178 17.5q202 0 374 -108q172 108 374 108zM1438 191l-55 907q-211 -4 -359 -155q-152 155 -374 155q-176 0 -336 -66l-114 -941q124 51 228.5 76t221.5 25 q209 0 374 -102q172 107 374 102z" /> -<glyph unicode="" horiz-adv-x="2048" d="M1500 165v733q0 21 -15 36t-35 15h-93q-20 0 -35 -15t-15 -36v-733q0 -20 15 -35t35 -15h93q20 0 35 15t15 35zM1216 165v531q0 20 -15 35t-35 15h-101q-20 0 -35 -15t-15 -35v-531q0 -20 15 -35t35 -15h101q20 0 35 15t15 35zM924 165v429q0 20 -15 35t-35 15h-101 q-20 0 -35 -15t-15 -35v-429q0 -20 15 -35t35 -15h101q20 0 35 15t15 35zM632 165v362q0 20 -15 35t-35 15h-101q-20 0 -35 -15t-15 -35v-362q0 -20 15 -35t35 -15h101q20 0 35 15t15 35zM2048 311q0 -166 -118 -284t-284 -118h-1244q-166 0 -284 118t-118 284 q0 116 63 214.5t168 148.5q-10 34 -10 73q0 113 80.5 193.5t193.5 80.5q102 0 180 -67q45 183 194 300t338 117q149 0 275 -73.5t199.5 -199.5t73.5 -275q0 -66 -14 -122q135 -33 221 -142.5t86 -247.5z" /> -<glyph unicode="" d="M0 1536h1536v-1392l-776 -338l-760 338v1392zM1436 209v926h-1336v-926l661 -294zM1436 1235v201h-1336v-201h1336zM181 937v-115h-37v115h37zM181 789v-115h-37v115h37zM181 641v-115h-37v115h37zM181 493v-115h-37v115h37zM181 345v-115h-37v115h37zM207 202l15 34 l105 -47l-15 -33zM343 142l15 34l105 -46l-15 -34zM478 82l15 34l105 -46l-15 -34zM614 23l15 33l104 -46l-15 -34zM797 10l105 46l15 -33l-105 -47zM932 70l105 46l15 -34l-105 -46zM1068 130l105 46l15 -34l-105 -46zM1203 189l105 47l15 -34l-105 -46zM259 1389v-36h-114 v36h114zM421 1389v-36h-115v36h115zM583 1389v-36h-115v36h115zM744 1389v-36h-114v36h114zM906 1389v-36h-114v36h114zM1068 1389v-36h-115v36h115zM1230 1389v-36h-115v36h115zM1391 1389v-36h-114v36h114zM181 1049v-79h-37v115h115v-36h-78zM421 1085v-36h-115v36h115z M583 1085v-36h-115v36h115zM744 1085v-36h-114v36h114zM906 1085v-36h-114v36h114zM1068 1085v-36h-115v36h115zM1230 1085v-36h-115v36h115zM1355 970v79h-78v36h115v-115h-37zM1355 822v115h37v-115h-37zM1355 674v115h37v-115h-37zM1355 526v115h37v-115h-37zM1355 378 v115h37v-115h-37zM1355 230v115h37v-115h-37zM760 265q-129 0 -221 91.5t-92 221.5q0 129 92 221t221 92q130 0 221.5 -92t91.5 -221q0 -130 -91.5 -221.5t-221.5 -91.5zM595 646q0 -36 19.5 -56.5t49.5 -25t64 -7t64 -2t49.5 -9t19.5 -30.5q0 -49 -112 -49q-97 0 -123 51 h-3l-31 -63q67 -42 162 -42q29 0 56.5 5t55.5 16t45.5 33t17.5 53q0 46 -27.5 69.5t-67.5 27t-79.5 3t-67 5t-27.5 25.5q0 21 20.5 33t40.5 15t41 3q34 0 70.5 -11t51.5 -34h3l30 58q-3 1 -21 8.5t-22.5 9t-19.5 7t-22 7t-20 4.5t-24 4t-23 1q-29 0 -56.5 -5t-54 -16.5 t-43 -34t-16.5 -53.5z" /> -<glyph unicode="" horiz-adv-x="2048" d="M863 504q0 112 -79.5 191.5t-191.5 79.5t-191 -79.5t-79 -191.5t79 -191t191 -79t191.5 79t79.5 191zM1726 505q0 112 -79 191t-191 79t-191.5 -79t-79.5 -191q0 -113 79.5 -192t191.5 -79t191 79.5t79 191.5zM2048 1314v-1348q0 -44 -31.5 -75.5t-76.5 -31.5h-1832 q-45 0 -76.5 31.5t-31.5 75.5v1348q0 44 31.5 75.5t76.5 31.5h431q44 0 76 -31.5t32 -75.5v-161h754v161q0 44 32 75.5t76 31.5h431q45 0 76.5 -31.5t31.5 -75.5z" /> -<glyph unicode="" horiz-adv-x="2048" d="M1430 953zM1690 749q148 0 253 -98.5t105 -244.5q0 -157 -109 -261.5t-267 -104.5q-85 0 -162 27.5t-138 73.5t-118 106t-109 126.5t-103.5 132.5t-108.5 126t-117 106t-136 73.5t-159 27.5q-154 0 -251.5 -91.5t-97.5 -244.5q0 -157 104 -250t263 -93q100 0 208 37.5 t193 98.5q5 4 21 18.5t30 24t22 9.5q14 0 24.5 -10.5t10.5 -24.5q0 -24 -60 -77q-101 -88 -234.5 -142t-260.5 -54q-133 0 -245.5 58t-180 165t-67.5 241q0 205 141.5 341t347.5 136q120 0 226.5 -43.5t185.5 -113t151.5 -153t139 -167.5t133.5 -153.5t149.5 -113 t172.5 -43.5q102 0 168.5 61.5t66.5 162.5q0 95 -64.5 159t-159.5 64q-30 0 -81.5 -18.5t-68.5 -18.5q-20 0 -35.5 15t-15.5 35q0 18 8.5 57t8.5 59q0 159 -107.5 263t-266.5 104q-58 0 -111.5 -18.5t-84 -40.5t-55.5 -40.5t-33 -18.5q-15 0 -25.5 10.5t-10.5 25.5 q0 19 25 46q59 67 147 103.5t182 36.5q191 0 318 -125.5t127 -315.5q0 -37 -4 -66q57 15 115 15z" /> -<glyph unicode="" horiz-adv-x="1664" d="M1216 832q0 26 -19 45t-45 19h-128v128q0 26 -19 45t-45 19t-45 -19t-19 -45v-128h-128q-26 0 -45 -19t-19 -45t19 -45t45 -19h128v-128q0 -26 19 -45t45 -19t45 19t19 45v128h128q26 0 45 19t19 45zM640 0q0 -53 -37.5 -90.5t-90.5 -37.5t-90.5 37.5t-37.5 90.5 t37.5 90.5t90.5 37.5t90.5 -37.5t37.5 -90.5zM1536 0q0 -53 -37.5 -90.5t-90.5 -37.5t-90.5 37.5t-37.5 90.5t37.5 90.5t90.5 37.5t90.5 -37.5t37.5 -90.5zM1664 1088v-512q0 -24 -16 -42.5t-41 -21.5l-1044 -122q1 -7 4.5 -21.5t6 -26.5t2.5 -22q0 -16 -24 -64h920 q26 0 45 -19t19 -45t-19 -45t-45 -19h-1024q-26 0 -45 19t-19 45q0 14 11 39.5t29.5 59.5t20.5 38l-177 823h-204q-26 0 -45 19t-19 45t19 45t45 19h256q16 0 28.5 -6.5t20 -15.5t13 -24.5t7.5 -26.5t5.5 -29.5t4.5 -25.5h1201q26 0 45 -19t19 -45z" /> -<glyph unicode="" horiz-adv-x="1792" d="M1280 832q0 26 -19 45t-45 19t-45 -19l-147 -146v293q0 26 -19 45t-45 19t-45 -19t-19 -45v-293l-147 146q-19 19 -45 19t-45 -19t-19 -45t19 -45l256 -256q19 -19 45 -19t45 19l256 256q19 19 19 45zM640 0q0 -53 -37.5 -90.5t-90.5 -37.5t-90.5 37.5t-37.5 90.5 t37.5 90.5t90.5 37.5t90.5 -37.5t37.5 -90.5zM1536 0q0 -53 -37.5 -90.5t-90.5 -37.5t-90.5 37.5t-37.5 90.5t37.5 90.5t90.5 37.5t90.5 -37.5t37.5 -90.5zM1664 1088v-512q0 -24 -16 -42.5t-41 -21.5l-1044 -122q1 -7 4.5 -21.5t6 -26.5t2.5 -22q0 -16 -24 -64h920 q26 0 45 -19t19 -45t-19 -45t-45 -19h-1024q-26 0 -45 19t-19 45q0 14 11 39.5t29.5 59.5t20.5 38l-177 823h-204q-26 0 -45 19t-19 45t19 45t45 19h256q16 0 28.5 -6.5t20 -15.5t13 -24.5t7.5 -26.5t5.5 -29.5t4.5 -25.5h1201q26 0 45 -19t19 -45z" /> -<glyph unicode="" horiz-adv-x="2048" d="M212 768l623 -665l-300 665h-323zM1024 -4l349 772h-698zM538 896l204 384h-262l-288 -384h346zM1213 103l623 665h-323zM683 896h682l-204 384h-274zM1510 896h346l-288 384h-262zM1651 1382l384 -512q14 -18 13 -41.5t-17 -40.5l-960 -1024q-18 -20 -47 -20t-47 20 l-960 1024q-16 17 -17 40.5t13 41.5l384 512q18 26 51 26h1152q33 0 51 -26z" /> -<glyph unicode="" horiz-adv-x="2048" d="M1811 -19q19 19 45 19t45 -19l128 -128l-90 -90l-83 83l-83 -83q-18 -19 -45 -19t-45 19l-83 83l-83 -83q-19 -19 -45 -19t-45 19l-83 83l-83 -83q-19 -19 -45 -19t-45 19l-83 83l-83 -83q-19 -19 -45 -19t-45 19l-83 83l-83 -83q-19 -19 -45 -19t-45 19l-83 83l-83 -83 q-19 -19 -45 -19t-45 19l-83 83l-83 -83q-19 -19 -45 -19t-45 19l-128 128l90 90l83 -83l83 83q19 19 45 19t45 -19l83 -83l83 83q19 19 45 19t45 -19l83 -83l83 83q19 19 45 19t45 -19l83 -83l83 83q19 19 45 19t45 -19l83 -83l83 83q19 19 45 19t45 -19l83 -83l83 83 q19 19 45 19t45 -19l83 -83zM237 19q-19 -19 -45 -19t-45 19l-128 128l90 90l83 -82l83 82q19 19 45 19t45 -19l83 -82l64 64v293l-210 314q-17 26 -7 56.5t40 40.5l177 58v299h128v128h256v128h256v-128h256v-128h128v-299l177 -58q30 -10 40 -40.5t-7 -56.5l-210 -314 v-293l19 18q19 19 45 19t45 -19l83 -82l83 82q19 19 45 19t45 -19l128 -128l-90 -90l-83 83l-83 -83q-18 -19 -45 -19t-45 19l-83 83l-83 -83q-19 -19 -45 -19t-45 19l-83 83l-83 -83q-19 -19 -45 -19t-45 19l-83 83l-83 -83q-19 -19 -45 -19t-45 19l-83 83l-83 -83 q-19 -19 -45 -19t-45 19l-83 83l-83 -83q-19 -19 -45 -19t-45 19l-83 83zM640 1152v-128l384 128l384 -128v128h-128v128h-512v-128h-128z" /> -<glyph unicode="" d="M576 0l96 448l-96 128l-128 64zM832 0l128 640l-128 -64l-96 -128zM992 1010q-2 4 -4 6q-10 8 -96 8q-70 0 -167 -19q-7 -2 -21 -2t-21 2q-97 19 -167 19q-86 0 -96 -8q-2 -2 -4 -6q2 -18 4 -27q2 -3 7.5 -6.5t7.5 -10.5q2 -4 7.5 -20.5t7 -20.5t7.5 -17t8.5 -17t9 -14 t12 -13.5t14 -9.5t17.5 -8t20.5 -4t24.5 -2q36 0 59 12.5t32.5 30t14.5 34.5t11.5 29.5t17.5 12.5h12q11 0 17.5 -12.5t11.5 -29.5t14.5 -34.5t32.5 -30t59 -12.5q13 0 24.5 2t20.5 4t17.5 8t14 9.5t12 13.5t9 14t8.5 17t7.5 17t7 20.5t7.5 20.5q2 7 7.5 10.5t7.5 6.5 q2 9 4 27zM1408 131q0 -121 -73 -190t-194 -69h-874q-121 0 -194 69t-73 190q0 61 4.5 118t19 125.5t37.5 123.5t63.5 103.5t93.5 74.5l-90 220h214q-22 64 -22 128q0 12 2 32q-194 40 -194 96q0 57 210 99q17 62 51.5 134t70.5 114q32 37 76 37q30 0 84 -31t84 -31t84 31 t84 31q44 0 76 -37q36 -42 70.5 -114t51.5 -134q210 -42 210 -99q0 -56 -194 -96q7 -81 -20 -160h214l-82 -225q63 -33 107.5 -96.5t65.5 -143.5t29 -151.5t8 -148.5z" /> -<glyph unicode="" horiz-adv-x="2304" d="M2301 500q12 -103 -22 -198.5t-99 -163.5t-158.5 -106t-196.5 -31q-161 11 -279.5 125t-134.5 274q-12 111 27.5 210.5t118.5 170.5l-71 107q-96 -80 -151 -194t-55 -244q0 -27 -18.5 -46.5t-45.5 -19.5h-256h-69q-23 -164 -149 -274t-294 -110q-185 0 -316.5 131.5 t-131.5 316.5t131.5 316.5t316.5 131.5q76 0 152 -27l24 45q-123 110 -304 110h-64q-26 0 -45 19t-19 45t19 45t45 19h128q78 0 145 -13.5t116.5 -38.5t71.5 -39.5t51 -36.5h512h115l-85 128h-222q-30 0 -49 22.5t-14 52.5q4 23 23 38t43 15h253q33 0 53 -28l70 -105 l114 114q19 19 46 19h101q26 0 45 -19t19 -45v-128q0 -26 -19 -45t-45 -19h-179l115 -172q131 63 275 36q143 -26 244 -134.5t118 -253.5zM448 128q115 0 203 72.5t111 183.5h-314q-35 0 -55 31q-18 32 -1 63l147 277q-47 13 -91 13q-132 0 -226 -94t-94 -226t94 -226 t226 -94zM1856 128q132 0 226 94t94 226t-94 226t-226 94q-60 0 -121 -24l174 -260q15 -23 10 -49t-27 -40q-15 -11 -36 -11q-35 0 -53 29l-174 260q-93 -95 -93 -225q0 -132 94 -226t226 -94z" /> -<glyph unicode="" d="M1408 0q0 -63 -61.5 -113.5t-164 -81t-225 -46t-253.5 -15.5t-253.5 15.5t-225 46t-164 81t-61.5 113.5q0 49 33 88.5t91 66.5t118 44.5t131 29.5q26 5 48 -10.5t26 -41.5q5 -26 -10.5 -48t-41.5 -26q-58 -10 -106 -23.5t-76.5 -25.5t-48.5 -23.5t-27.5 -19.5t-8.5 -12 q3 -11 27 -26.5t73 -33t114 -32.5t160.5 -25t201.5 -10t201.5 10t160.5 25t114 33t73 33.5t27 27.5q-1 4 -8.5 11t-27.5 19t-48.5 23.5t-76.5 25t-106 23.5q-26 4 -41.5 26t-10.5 48q4 26 26 41.5t48 10.5q71 -12 131 -29.5t118 -44.5t91 -66.5t33 -88.5zM1024 896v-384 q0 -26 -19 -45t-45 -19h-64v-384q0 -26 -19 -45t-45 -19h-256q-26 0 -45 19t-19 45v384h-64q-26 0 -45 19t-19 45v384q0 53 37.5 90.5t90.5 37.5h384q53 0 90.5 -37.5t37.5 -90.5zM928 1280q0 -93 -65.5 -158.5t-158.5 -65.5t-158.5 65.5t-65.5 158.5t65.5 158.5t158.5 65.5 t158.5 -65.5t65.5 -158.5z" /> -<glyph unicode="" horiz-adv-x="1792" d="M1280 512h305q-5 -6 -10 -10.5t-9 -7.5l-3 -4l-623 -600q-18 -18 -44 -18t-44 18l-624 602q-5 2 -21 20h369q22 0 39.5 13.5t22.5 34.5l70 281l190 -667q6 -20 23 -33t39 -13q21 0 38 13t23 33l146 485l56 -112q18 -35 57 -35zM1792 940q0 -145 -103 -300h-369l-111 221 q-8 17 -25.5 27t-36.5 8q-45 -5 -56 -46l-129 -430l-196 686q-6 20 -23.5 33t-39.5 13t-39 -13.5t-22 -34.5l-116 -464h-423q-103 155 -103 300q0 220 127 344t351 124q62 0 126.5 -21.5t120 -58t95.5 -68.5t76 -68q36 36 76 68t95.5 68.5t120 58t126.5 21.5q224 0 351 -124 t127 -344z" /> -<glyph unicode="" horiz-adv-x="1280" d="M1152 960q0 -221 -147.5 -384.5t-364.5 -187.5v-260h224q14 0 23 -9t9 -23v-64q0 -14 -9 -23t-23 -9h-224v-224q0 -14 -9 -23t-23 -9h-64q-14 0 -23 9t-9 23v224h-224q-14 0 -23 9t-9 23v64q0 14 9 23t23 9h224v260q-150 16 -271.5 103t-186 224t-52.5 292 q11 134 80.5 249t182 188t245.5 88q170 19 319 -54t236 -212t87 -306zM128 960q0 -185 131.5 -316.5t316.5 -131.5t316.5 131.5t131.5 316.5t-131.5 316.5t-316.5 131.5t-316.5 -131.5t-131.5 -316.5z" /> -<glyph unicode="" horiz-adv-x="1792" d="M1280 1504q0 14 9 23t23 9h416q26 0 45 -19t19 -45v-416q0 -14 -9 -23t-23 -9h-64q-14 0 -23 9t-9 23v262l-419 -420q87 -104 129.5 -236.5t30.5 -276.5q-22 -250 -200.5 -431t-428.5 -206q-163 -17 -314 39.5t-256.5 162t-162 256.5t-39.5 314q25 250 206 428.5 t431 200.5q144 12 276.5 -30.5t236.5 -129.5l419 419h-261q-14 0 -23 9t-9 23v64zM704 -128q117 0 223.5 45.5t184 123t123 184t45.5 223.5t-45.5 223.5t-123 184t-184 123t-223.5 45.5t-223.5 -45.5t-184 -123t-123 -184t-45.5 -223.5t45.5 -223.5t123 -184t184 -123 t223.5 -45.5z" /> -<glyph unicode="" horiz-adv-x="1280" d="M830 1220q145 -72 233.5 -210.5t88.5 -305.5q0 -221 -147.5 -384.5t-364.5 -187.5v-132h96q14 0 23 -9t9 -23v-64q0 -14 -9 -23t-23 -9h-96v-96q0 -14 -9 -23t-23 -9h-64q-14 0 -23 9t-9 23v96h-96q-14 0 -23 9t-9 23v64q0 14 9 23t23 9h96v132q-217 24 -364.5 187.5 t-147.5 384.5q0 167 88.5 305.5t233.5 210.5q-165 96 -228 273q-6 16 3.5 29.5t26.5 13.5h69q21 0 29 -20q44 -106 140 -171t214 -65t214 65t140 171q8 20 37 20h61q17 0 26.5 -13.5t3.5 -29.5q-63 -177 -228 -273zM576 256q185 0 316.5 131.5t131.5 316.5t-131.5 316.5 t-316.5 131.5t-316.5 -131.5t-131.5 -316.5t131.5 -316.5t316.5 -131.5z" /> -<glyph unicode="" d="M1024 1504q0 14 9 23t23 9h288q26 0 45 -19t19 -45v-288q0 -14 -9 -23t-23 -9h-64q-14 0 -23 9t-9 23v134l-254 -255q126 -158 126 -359q0 -221 -147.5 -384.5t-364.5 -187.5v-132h96q14 0 23 -9t9 -23v-64q0 -14 -9 -23t-23 -9h-96v-96q0 -14 -9 -23t-23 -9h-64 q-14 0 -23 9t-9 23v96h-96q-14 0 -23 9t-9 23v64q0 14 9 23t23 9h96v132q-149 16 -270.5 103t-186.5 223.5t-53 291.5q16 204 160 353.5t347 172.5q118 14 228 -19t198 -103l255 254h-134q-14 0 -23 9t-9 23v64zM576 256q185 0 316.5 131.5t131.5 316.5t-131.5 316.5 t-316.5 131.5t-316.5 -131.5t-131.5 -316.5t131.5 -316.5t316.5 -131.5z" /> -<glyph unicode="" horiz-adv-x="1792" d="M1280 1504q0 14 9 23t23 9h288q26 0 45 -19t19 -45v-288q0 -14 -9 -23t-23 -9h-64q-14 0 -23 9t-9 23v134l-254 -255q126 -158 126 -359q0 -221 -147.5 -384.5t-364.5 -187.5v-132h96q14 0 23 -9t9 -23v-64q0 -14 -9 -23t-23 -9h-96v-96q0 -14 -9 -23t-23 -9h-64 q-14 0 -23 9t-9 23v96h-96q-14 0 -23 9t-9 23v64q0 14 9 23t23 9h96v132q-217 24 -364.5 187.5t-147.5 384.5q0 201 126 359l-52 53l-101 -111q-9 -10 -22 -10.5t-23 7.5l-48 44q-10 8 -10.5 21.5t8.5 23.5l105 115l-111 112v-134q0 -14 -9 -23t-23 -9h-64q-14 0 -23 9 t-9 23v288q0 26 19 45t45 19h288q14 0 23 -9t9 -23v-64q0 -14 -9 -23t-23 -9h-133l106 -107l86 94q9 10 22 10.5t23 -7.5l48 -44q10 -8 10.5 -21.5t-8.5 -23.5l-90 -99l57 -56q158 126 359 126t359 -126l255 254h-134q-14 0 -23 9t-9 23v64zM832 256q185 0 316.5 131.5 t131.5 316.5t-131.5 316.5t-316.5 131.5t-316.5 -131.5t-131.5 -316.5t131.5 -316.5t316.5 -131.5z" /> -<glyph unicode="" horiz-adv-x="1792" d="M1790 1007q12 -155 -52.5 -292t-186 -224t-271.5 -103v-260h224q14 0 23 -9t9 -23v-64q0 -14 -9 -23t-23 -9h-224v-224q0 -14 -9 -23t-23 -9h-64q-14 0 -23 9t-9 23v224h-512v-224q0 -14 -9 -23t-23 -9h-64q-14 0 -23 9t-9 23v224h-224q-14 0 -23 9t-9 23v64q0 14 9 23 t23 9h224v260q-150 16 -271.5 103t-186 224t-52.5 292q17 206 164.5 356.5t352.5 169.5q206 21 377 -94q171 115 377 94q205 -19 352.5 -169.5t164.5 -356.5zM896 647q128 131 128 313t-128 313q-128 -131 -128 -313t128 -313zM576 512q115 0 218 57q-154 165 -154 391 q0 224 154 391q-103 57 -218 57q-185 0 -316.5 -131.5t-131.5 -316.5t131.5 -316.5t316.5 -131.5zM1152 128v260q-137 15 -256 94q-119 -79 -256 -94v-260h512zM1216 512q185 0 316.5 131.5t131.5 316.5t-131.5 316.5t-316.5 131.5q-115 0 -218 -57q154 -167 154 -391 q0 -226 -154 -391q103 -57 218 -57z" /> -<glyph unicode="" horiz-adv-x="1920" d="M1536 1120q0 14 9 23t23 9h288q26 0 45 -19t19 -45v-288q0 -14 -9 -23t-23 -9h-64q-14 0 -23 9t-9 23v134l-254 -255q76 -95 107.5 -214t9.5 -247q-31 -182 -166 -312t-318 -156q-210 -29 -384.5 80t-241.5 300q-117 6 -221 57.5t-177.5 133t-113.5 192.5t-32 230 q9 135 78 252t182 191.5t248 89.5q118 14 227.5 -19t198.5 -103l255 254h-134q-14 0 -23 9t-9 23v64q0 14 9 23t23 9h288q26 0 45 -19t19 -45v-288q0 -14 -9 -23t-23 -9h-64q-14 0 -23 9t-9 23v134l-254 -255q59 -74 93 -169q182 -9 328 -124l255 254h-134q-14 0 -23 9 t-9 23v64zM1024 704q0 20 -4 58q-162 -25 -271 -150t-109 -292q0 -20 4 -58q162 25 271 150t109 292zM128 704q0 -168 111 -294t276 -149q-3 29 -3 59q0 210 135 369.5t338 196.5q-53 120 -163.5 193t-245.5 73q-185 0 -316.5 -131.5t-131.5 -316.5zM1088 -128 q185 0 316.5 131.5t131.5 316.5q0 168 -111 294t-276 149q3 -29 3 -59q0 -210 -135 -369.5t-338 -196.5q53 -120 163.5 -193t245.5 -73z" /> -<glyph unicode="" horiz-adv-x="2048" d="M1664 1504q0 14 9 23t23 9h288q26 0 45 -19t19 -45v-288q0 -14 -9 -23t-23 -9h-64q-14 0 -23 9t-9 23v134l-254 -255q76 -95 107.5 -214t9.5 -247q-32 -180 -164.5 -310t-313.5 -157q-223 -34 -409 90q-117 -78 -256 -93v-132h96q14 0 23 -9t9 -23v-64q0 -14 -9 -23 t-23 -9h-96v-96q0 -14 -9 -23t-23 -9h-64q-14 0 -23 9t-9 23v96h-96q-14 0 -23 9t-9 23v64q0 14 9 23t23 9h96v132q-155 17 -279.5 109.5t-187 237.5t-39.5 307q25 187 159.5 322.5t320.5 164.5q224 34 410 -90q146 97 320 97q201 0 359 -126l255 254h-134q-14 0 -23 9 t-9 23v64zM896 391q128 131 128 313t-128 313q-128 -131 -128 -313t128 -313zM128 704q0 -185 131.5 -316.5t316.5 -131.5q117 0 218 57q-154 167 -154 391t154 391q-101 57 -218 57q-185 0 -316.5 -131.5t-131.5 -316.5zM1216 256q185 0 316.5 131.5t131.5 316.5 t-131.5 316.5t-316.5 131.5q-117 0 -218 -57q154 -167 154 -391t-154 -391q101 -57 218 -57z" /> -<glyph unicode="" horiz-adv-x="1792" d="M1728 1536q26 0 45 -19t19 -45v-416q0 -14 -9 -23t-23 -9h-64q-14 0 -23 9t-9 23v262l-229 -230l156 -156q9 -10 9 -23t-9 -22l-46 -46q-9 -9 -22 -9t-23 9l-156 157l-99 -100q87 -104 129.5 -236.5t30.5 -276.5q-22 -250 -200.5 -431t-428.5 -206q-163 -17 -314 39.5 t-256.5 162t-162 256.5t-39.5 314q25 250 206 428.5t431 200.5q144 12 276.5 -30.5t236.5 -129.5l99 99l-156 156q-9 10 -9 23t9 22l46 46q9 9 22 9t23 -9l156 -156l229 229h-261q-14 0 -23 9t-9 23v64q0 14 9 23t23 9h416zM1280 448q0 117 -45.5 223.5t-123 184t-184 123 t-223.5 45.5t-223.5 -45.5t-184 -123t-123 -184t-45.5 -223.5t45.5 -223.5t123 -184t184 -123t223.5 -45.5t223.5 45.5t184 123t123 184t45.5 223.5z" /> -<glyph unicode="" horiz-adv-x="1280" d="M640 892q217 -24 364.5 -187.5t147.5 -384.5q0 -167 -87 -306t-236 -212t-319 -54q-133 15 -245.5 88t-182 188t-80.5 249q-12 155 52.5 292t186 224t271.5 103v132h-160q-14 0 -23 9t-9 23v64q0 14 9 23t23 9h160v165l-92 -92q-10 -9 -23 -9t-22 9l-46 46q-9 9 -9 22 t9 23l202 201q19 19 45 19t45 -19l202 -201q9 -10 9 -23t-9 -22l-46 -46q-9 -9 -22 -9t-23 9l-92 92v-165h160q14 0 23 -9t9 -23v-64q0 -14 -9 -23t-23 -9h-160v-132zM576 -128q185 0 316.5 131.5t131.5 316.5t-131.5 316.5t-316.5 131.5t-316.5 -131.5t-131.5 -316.5 t131.5 -316.5t316.5 -131.5z" /> -<glyph unicode="" horiz-adv-x="2048" d="M2029 685q19 -19 19 -45t-19 -45l-294 -294q-9 -10 -22.5 -10t-22.5 10l-45 45q-10 9 -10 22.5t10 22.5l185 185h-294v-224q0 -14 -9 -23t-23 -9h-64q-14 0 -23 9t-9 23v224h-131q-12 -119 -67 -226t-139 -183.5t-196.5 -121.5t-234.5 -45q-180 0 -330.5 91t-234.5 247 t-74 337q8 162 94 300t226.5 219.5t302.5 85.5q166 4 310.5 -71.5t235.5 -208.5t107 -296h131v224q0 14 9 23t23 9h64q14 0 23 -9t9 -23v-224h294l-185 185q-10 9 -10 22.5t10 22.5l45 45q9 10 22.5 10t22.5 -10zM640 128q104 0 198.5 40.5t163.5 109.5t109.5 163.5 t40.5 198.5t-40.5 198.5t-109.5 163.5t-163.5 109.5t-198.5 40.5t-198.5 -40.5t-163.5 -109.5t-109.5 -163.5t-40.5 -198.5t40.5 -198.5t109.5 -163.5t163.5 -109.5t198.5 -40.5z" /> -<glyph unicode="" horiz-adv-x="1280" d="M1152 960q0 -221 -147.5 -384.5t-364.5 -187.5v-612q0 -14 -9 -23t-23 -9h-64q-14 0 -23 9t-9 23v612q-217 24 -364.5 187.5t-147.5 384.5q0 117 45.5 223.5t123 184t184 123t223.5 45.5t223.5 -45.5t184 -123t123 -184t45.5 -223.5zM576 512q185 0 316.5 131.5 t131.5 316.5t-131.5 316.5t-316.5 131.5t-316.5 -131.5t-131.5 -316.5t131.5 -316.5t316.5 -131.5z" /> -<glyph unicode="" horiz-adv-x="1792" /> -<glyph unicode="" horiz-adv-x="1792" /> -<glyph unicode="" horiz-adv-x="1792" /> -<glyph unicode="" d="M1451 1408q35 0 60 -25t25 -60v-1366q0 -35 -25 -60t-60 -25h-391v595h199l30 232h-229v148q0 56 23.5 84t91.5 28l122 1v207q-63 9 -178 9q-136 0 -217.5 -80t-81.5 -226v-171h-200v-232h200v-595h-735q-35 0 -60 25t-25 60v1366q0 35 25 60t60 25h1366z" /> -<glyph unicode="" horiz-adv-x="1280" d="M0 939q0 108 37.5 203.5t103.5 166.5t152 123t185 78t202 26q158 0 294 -66.5t221 -193.5t85 -287q0 -96 -19 -188t-60 -177t-100 -149.5t-145 -103t-189 -38.5q-68 0 -135 32t-96 88q-10 -39 -28 -112.5t-23.5 -95t-20.5 -71t-26 -71t-32 -62.5t-46 -77.5t-62 -86.5 l-14 -5l-9 10q-15 157 -15 188q0 92 21.5 206.5t66.5 287.5t52 203q-32 65 -32 169q0 83 52 156t132 73q61 0 95 -40.5t34 -102.5q0 -66 -44 -191t-44 -187q0 -63 45 -104.5t109 -41.5q55 0 102 25t78.5 68t56 95t38 110.5t20 111t6.5 99.5q0 173 -109.5 269.5t-285.5 96.5 q-200 0 -334 -129.5t-134 -328.5q0 -44 12.5 -85t27 -65t27 -45.5t12.5 -30.5q0 -28 -15 -73t-37 -45q-2 0 -17 3q-51 15 -90.5 56t-61 94.5t-32.5 108t-11 106.5z" /> -<glyph unicode="" d="M985 562q13 0 97.5 -44t89.5 -53q2 -5 2 -15q0 -33 -17 -76q-16 -39 -71 -65.5t-102 -26.5q-57 0 -190 62q-98 45 -170 118t-148 185q-72 107 -71 194v8q3 91 74 158q24 22 52 22q6 0 18 -1.5t19 -1.5q19 0 26.5 -6.5t15.5 -27.5q8 -20 33 -88t25 -75q0 -21 -34.5 -57.5 t-34.5 -46.5q0 -7 5 -15q34 -73 102 -137q56 -53 151 -101q12 -7 22 -7q15 0 54 48.5t52 48.5zM782 32q127 0 243.5 50t200.5 134t134 200.5t50 243.5t-50 243.5t-134 200.5t-200.5 134t-243.5 50t-243.5 -50t-200.5 -134t-134 -200.5t-50 -243.5q0 -203 120 -368l-79 -233 l242 77q158 -104 345 -104zM782 1414q153 0 292.5 -60t240.5 -161t161 -240.5t60 -292.5t-60 -292.5t-161 -240.5t-240.5 -161t-292.5 -60q-195 0 -365 94l-417 -134l136 405q-108 178 -108 389q0 153 60 292.5t161 240.5t240.5 161t292.5 60z" /> -<glyph unicode="" horiz-adv-x="1792" d="M128 128h1024v128h-1024v-128zM128 640h1024v128h-1024v-128zM1696 192q0 40 -28 68t-68 28t-68 -28t-28 -68t28 -68t68 -28t68 28t28 68zM128 1152h1024v128h-1024v-128zM1696 704q0 40 -28 68t-68 28t-68 -28t-28 -68t28 -68t68 -28t68 28t28 68zM1696 1216 q0 40 -28 68t-68 28t-68 -28t-28 -68t28 -68t68 -28t68 28t28 68zM1792 384v-384h-1792v384h1792zM1792 896v-384h-1792v384h1792zM1792 1408v-384h-1792v384h1792z" /> -<glyph unicode="" horiz-adv-x="2048" d="M704 640q-159 0 -271.5 112.5t-112.5 271.5t112.5 271.5t271.5 112.5t271.5 -112.5t112.5 -271.5t-112.5 -271.5t-271.5 -112.5zM1664 512h352q13 0 22.5 -9.5t9.5 -22.5v-192q0 -13 -9.5 -22.5t-22.5 -9.5h-352v-352q0 -13 -9.5 -22.5t-22.5 -9.5h-192q-13 0 -22.5 9.5 t-9.5 22.5v352h-352q-13 0 -22.5 9.5t-9.5 22.5v192q0 13 9.5 22.5t22.5 9.5h352v352q0 13 9.5 22.5t22.5 9.5h192q13 0 22.5 -9.5t9.5 -22.5v-352zM928 288q0 -52 38 -90t90 -38h256v-238q-68 -50 -171 -50h-874q-121 0 -194 69t-73 190q0 53 3.5 103.5t14 109t26.5 108.5 t43 97.5t62 81t85.5 53.5t111.5 20q19 0 39 -17q79 -61 154.5 -91.5t164.5 -30.5t164.5 30.5t154.5 91.5q20 17 39 17q132 0 217 -96h-223q-52 0 -90 -38t-38 -90v-192z" /> -<glyph unicode="" horiz-adv-x="2048" d="M704 640q-159 0 -271.5 112.5t-112.5 271.5t112.5 271.5t271.5 112.5t271.5 -112.5t112.5 -271.5t-112.5 -271.5t-271.5 -112.5zM1781 320l249 -249q9 -9 9 -23q0 -13 -9 -22l-136 -136q-9 -9 -22 -9q-14 0 -23 9l-249 249l-249 -249q-9 -9 -23 -9q-13 0 -22 9l-136 136 q-9 9 -9 22q0 14 9 23l249 249l-249 249q-9 9 -9 23q0 13 9 22l136 136q9 9 22 9q14 0 23 -9l249 -249l249 249q9 9 23 9q13 0 22 -9l136 -136q9 -9 9 -22q0 -14 -9 -23zM1283 320l-181 -181q-37 -37 -37 -91q0 -53 37 -90l83 -83q-21 -3 -44 -3h-874q-121 0 -194 69 t-73 190q0 53 3.5 103.5t14 109t26.5 108.5t43 97.5t62 81t85.5 53.5t111.5 20q19 0 39 -17q154 -122 319 -122t319 122q20 17 39 17q28 0 57 -6q-28 -27 -41 -50t-13 -56q0 -54 37 -91z" /> -<glyph unicode="" horiz-adv-x="2048" d="M256 512h1728q26 0 45 -19t19 -45v-448h-256v256h-1536v-256h-256v1216q0 26 19 45t45 19h128q26 0 45 -19t19 -45v-704zM832 832q0 106 -75 181t-181 75t-181 -75t-75 -181t75 -181t181 -75t181 75t75 181zM2048 576v64q0 159 -112.5 271.5t-271.5 112.5h-704 q-26 0 -45 -19t-19 -45v-384h1152z" /> -<glyph unicode="" d="M1536 1536l-192 -448h192v-192h-274l-55 -128h329v-192h-411l-357 -832l-357 832h-411v192h329l-55 128h-274v192h192l-192 448h256l323 -768h378l323 768h256zM768 320l108 256h-216z" /> -<glyph unicode="" d="M1088 1536q185 0 316.5 -93.5t131.5 -226.5v-896q0 -130 -125.5 -222t-305.5 -97l213 -202q16 -15 8 -35t-30 -20h-1056q-22 0 -30 20t8 35l213 202q-180 5 -305.5 97t-125.5 222v896q0 133 131.5 226.5t316.5 93.5h640zM768 192q80 0 136 56t56 136t-56 136t-136 56 t-136 -56t-56 -136t56 -136t136 -56zM1344 768v512h-1152v-512h1152z" /> -<glyph unicode="" d="M1088 1536q185 0 316.5 -93.5t131.5 -226.5v-896q0 -130 -125.5 -222t-305.5 -97l213 -202q16 -15 8 -35t-30 -20h-1056q-22 0 -30 20t8 35l213 202q-180 5 -305.5 97t-125.5 222v896q0 133 131.5 226.5t316.5 93.5h640zM288 224q66 0 113 47t47 113t-47 113t-113 47 t-113 -47t-47 -113t47 -113t113 -47zM704 768v512h-544v-512h544zM1248 224q66 0 113 47t47 113t-47 113t-113 47t-113 -47t-47 -113t47 -113t113 -47zM1408 768v512h-576v-512h576z" /> -<glyph unicode="" horiz-adv-x="1792" d="M1792 204v-209h-642v209h134v926h-6l-314 -1135h-243l-310 1135h-8v-926h135v-209h-538v209h69q21 0 43 19.5t22 37.5v881q0 18 -22 40t-43 22h-69v209h672l221 -821h6l223 821h670v-209h-71q-19 0 -41 -22t-22 -40v-881q0 -18 21.5 -37.5t41.5 -19.5h71z" /> -<glyph unicode="" horiz-adv-x="1792" /> -<glyph unicode="" horiz-adv-x="1792" /> -<glyph unicode="" horiz-adv-x="1792" /> -<glyph unicode="" horiz-adv-x="1792" /> -<glyph unicode="" horiz-adv-x="1792" /> -</font> -</defs></svg> \ No newline at end of file diff --git a/src/UI/Content/FontAwesome/fontawesome-webfont.ttf b/src/UI/Content/FontAwesome/fontawesome-webfont.ttf deleted file mode 100644 index ed9372f8e..000000000 Binary files a/src/UI/Content/FontAwesome/fontawesome-webfont.ttf and /dev/null differ diff --git a/src/UI/Content/FontAwesome/fontawesome-webfont.woff b/src/UI/Content/FontAwesome/fontawesome-webfont.woff deleted file mode 100644 index 8b280b98f..000000000 Binary files a/src/UI/Content/FontAwesome/fontawesome-webfont.woff and /dev/null differ diff --git a/src/UI/Content/FontAwesome/fontawesome-webfont.woff2 b/src/UI/Content/FontAwesome/fontawesome-webfont.woff2 deleted file mode 100644 index 3311d5851..000000000 Binary files a/src/UI/Content/FontAwesome/fontawesome-webfont.woff2 and /dev/null differ diff --git a/src/UI/Content/FontAwesome/icons.less b/src/UI/Content/FontAwesome/icons.less deleted file mode 100644 index c265de5a6..000000000 --- a/src/UI/Content/FontAwesome/icons.less +++ /dev/null @@ -1,596 +0,0 @@ -/* Font Awesome uses the Unicode Private Use Area (PUA) to ensure screen - readers do not read off random characters that represent icons */ - -.@{fa-css-prefix}-glass:before { content: @fa-var-glass; } -.@{fa-css-prefix}-music:before { content: @fa-var-music; } -.@{fa-css-prefix}-search:before { content: @fa-var-search; } -.@{fa-css-prefix}-envelope-o:before { content: @fa-var-envelope-o; } -.@{fa-css-prefix}-heart:before { content: @fa-var-heart; } -.@{fa-css-prefix}-star:before { content: @fa-var-star; } -.@{fa-css-prefix}-star-o:before { content: @fa-var-star-o; } -.@{fa-css-prefix}-user:before { content: @fa-var-user; } -.@{fa-css-prefix}-film:before { content: @fa-var-film; } -.@{fa-css-prefix}-th-large:before { content: @fa-var-th-large; } -.@{fa-css-prefix}-th:before { content: @fa-var-th; } -.@{fa-css-prefix}-th-list:before { content: @fa-var-th-list; } -.@{fa-css-prefix}-check:before { content: @fa-var-check; } -.@{fa-css-prefix}-remove:before, -.@{fa-css-prefix}-close:before, -.@{fa-css-prefix}-times:before { content: @fa-var-times; } -.@{fa-css-prefix}-search-plus:before { content: @fa-var-search-plus; } -.@{fa-css-prefix}-search-minus:before { content: @fa-var-search-minus; } -.@{fa-css-prefix}-power-off:before { content: @fa-var-power-off; } -.@{fa-css-prefix}-signal:before { content: @fa-var-signal; } -.@{fa-css-prefix}-gear:before, -.@{fa-css-prefix}-cog:before { content: @fa-var-cog; } -.@{fa-css-prefix}-trash-o:before { content: @fa-var-trash-o; } -.@{fa-css-prefix}-home:before { content: @fa-var-home; } -.@{fa-css-prefix}-file-o:before { content: @fa-var-file-o; } -.@{fa-css-prefix}-clock-o:before { content: @fa-var-clock-o; } -.@{fa-css-prefix}-road:before { content: @fa-var-road; } -.@{fa-css-prefix}-download:before { content: @fa-var-download; } -.@{fa-css-prefix}-arrow-circle-o-down:before { content: @fa-var-arrow-circle-o-down; } -.@{fa-css-prefix}-arrow-circle-o-up:before { content: @fa-var-arrow-circle-o-up; } -.@{fa-css-prefix}-inbox:before { content: @fa-var-inbox; } -.@{fa-css-prefix}-play-circle-o:before { content: @fa-var-play-circle-o; } -.@{fa-css-prefix}-rotate-right:before, -.@{fa-css-prefix}-repeat:before { content: @fa-var-repeat; } -.@{fa-css-prefix}-refresh:before { content: @fa-var-refresh; } -.@{fa-css-prefix}-list-alt:before { content: @fa-var-list-alt; } -.@{fa-css-prefix}-lock:before { content: @fa-var-lock; } -.@{fa-css-prefix}-flag:before { content: @fa-var-flag; } -.@{fa-css-prefix}-headphones:before { content: @fa-var-headphones; } -.@{fa-css-prefix}-volume-off:before { content: @fa-var-volume-off; } -.@{fa-css-prefix}-volume-down:before { content: @fa-var-volume-down; } -.@{fa-css-prefix}-volume-up:before { content: @fa-var-volume-up; } -.@{fa-css-prefix}-qrcode:before { content: @fa-var-qrcode; } -.@{fa-css-prefix}-barcode:before { content: @fa-var-barcode; } -.@{fa-css-prefix}-tag:before { content: @fa-var-tag; } -.@{fa-css-prefix}-tags:before { content: @fa-var-tags; } -.@{fa-css-prefix}-book:before { content: @fa-var-book; } -.@{fa-css-prefix}-bookmark:before { content: @fa-var-bookmark; } -.@{fa-css-prefix}-print:before { content: @fa-var-print; } -.@{fa-css-prefix}-camera:before { content: @fa-var-camera; } -.@{fa-css-prefix}-font:before { content: @fa-var-font; } -.@{fa-css-prefix}-bold:before { content: @fa-var-bold; } -.@{fa-css-prefix}-italic:before { content: @fa-var-italic; } -.@{fa-css-prefix}-text-height:before { content: @fa-var-text-height; } -.@{fa-css-prefix}-text-width:before { content: @fa-var-text-width; } -.@{fa-css-prefix}-align-left:before { content: @fa-var-align-left; } -.@{fa-css-prefix}-align-center:before { content: @fa-var-align-center; } -.@{fa-css-prefix}-align-right:before { content: @fa-var-align-right; } -.@{fa-css-prefix}-align-justify:before { content: @fa-var-align-justify; } -.@{fa-css-prefix}-list:before { content: @fa-var-list; } -.@{fa-css-prefix}-dedent:before, -.@{fa-css-prefix}-outdent:before { content: @fa-var-outdent; } -.@{fa-css-prefix}-indent:before { content: @fa-var-indent; } -.@{fa-css-prefix}-video-camera:before { content: @fa-var-video-camera; } -.@{fa-css-prefix}-photo:before, -.@{fa-css-prefix}-image:before, -.@{fa-css-prefix}-picture-o:before { content: @fa-var-picture-o; } -.@{fa-css-prefix}-pencil:before { content: @fa-var-pencil; } -.@{fa-css-prefix}-map-marker:before { content: @fa-var-map-marker; } -.@{fa-css-prefix}-adjust:before { content: @fa-var-adjust; } -.@{fa-css-prefix}-tint:before { content: @fa-var-tint; } -.@{fa-css-prefix}-edit:before, -.@{fa-css-prefix}-pencil-square-o:before { content: @fa-var-pencil-square-o; } -.@{fa-css-prefix}-share-square-o:before { content: @fa-var-share-square-o; } -.@{fa-css-prefix}-check-square-o:before { content: @fa-var-check-square-o; } -.@{fa-css-prefix}-arrows:before { content: @fa-var-arrows; } -.@{fa-css-prefix}-step-backward:before { content: @fa-var-step-backward; } -.@{fa-css-prefix}-fast-backward:before { content: @fa-var-fast-backward; } -.@{fa-css-prefix}-backward:before { content: @fa-var-backward; } -.@{fa-css-prefix}-play:before { content: @fa-var-play; } -.@{fa-css-prefix}-pause:before { content: @fa-var-pause; } -.@{fa-css-prefix}-stop:before { content: @fa-var-stop; } -.@{fa-css-prefix}-forward:before { content: @fa-var-forward; } -.@{fa-css-prefix}-fast-forward:before { content: @fa-var-fast-forward; } -.@{fa-css-prefix}-step-forward:before { content: @fa-var-step-forward; } -.@{fa-css-prefix}-eject:before { content: @fa-var-eject; } -.@{fa-css-prefix}-chevron-left:before { content: @fa-var-chevron-left; } -.@{fa-css-prefix}-chevron-right:before { content: @fa-var-chevron-right; } -.@{fa-css-prefix}-plus-circle:before { content: @fa-var-plus-circle; } -.@{fa-css-prefix}-minus-circle:before { content: @fa-var-minus-circle; } -.@{fa-css-prefix}-times-circle:before { content: @fa-var-times-circle; } -.@{fa-css-prefix}-check-circle:before { content: @fa-var-check-circle; } -.@{fa-css-prefix}-question-circle:before { content: @fa-var-question-circle; } -.@{fa-css-prefix}-info-circle:before { content: @fa-var-info-circle; } -.@{fa-css-prefix}-crosshairs:before { content: @fa-var-crosshairs; } -.@{fa-css-prefix}-times-circle-o:before { content: @fa-var-times-circle-o; } -.@{fa-css-prefix}-check-circle-o:before { content: @fa-var-check-circle-o; } -.@{fa-css-prefix}-ban:before { content: @fa-var-ban; } -.@{fa-css-prefix}-arrow-left:before { content: @fa-var-arrow-left; } -.@{fa-css-prefix}-arrow-right:before { content: @fa-var-arrow-right; } -.@{fa-css-prefix}-arrow-up:before { content: @fa-var-arrow-up; } -.@{fa-css-prefix}-arrow-down:before { content: @fa-var-arrow-down; } -.@{fa-css-prefix}-mail-forward:before, -.@{fa-css-prefix}-share:before { content: @fa-var-share; } -.@{fa-css-prefix}-expand:before { content: @fa-var-expand; } -.@{fa-css-prefix}-compress:before { content: @fa-var-compress; } -.@{fa-css-prefix}-plus:before { content: @fa-var-plus; } -.@{fa-css-prefix}-minus:before { content: @fa-var-minus; } -.@{fa-css-prefix}-asterisk:before { content: @fa-var-asterisk; } -.@{fa-css-prefix}-exclamation-circle:before { content: @fa-var-exclamation-circle; } -.@{fa-css-prefix}-gift:before { content: @fa-var-gift; } -.@{fa-css-prefix}-leaf:before { content: @fa-var-leaf; } -.@{fa-css-prefix}-fire:before { content: @fa-var-fire; } -.@{fa-css-prefix}-eye:before { content: @fa-var-eye; } -.@{fa-css-prefix}-eye-slash:before { content: @fa-var-eye-slash; } -.@{fa-css-prefix}-warning:before, -.@{fa-css-prefix}-exclamation-triangle:before { content: @fa-var-exclamation-triangle; } -.@{fa-css-prefix}-plane:before { content: @fa-var-plane; } -.@{fa-css-prefix}-calendar:before { content: @fa-var-calendar; } -.@{fa-css-prefix}-random:before { content: @fa-var-random; } -.@{fa-css-prefix}-comment:before { content: @fa-var-comment; } -.@{fa-css-prefix}-magnet:before { content: @fa-var-magnet; } -.@{fa-css-prefix}-chevron-up:before { content: @fa-var-chevron-up; } -.@{fa-css-prefix}-chevron-down:before { content: @fa-var-chevron-down; } -.@{fa-css-prefix}-retweet:before { content: @fa-var-retweet; } -.@{fa-css-prefix}-shopping-cart:before { content: @fa-var-shopping-cart; } -.@{fa-css-prefix}-folder:before { content: @fa-var-folder; } -.@{fa-css-prefix}-folder-open:before { content: @fa-var-folder-open; } -.@{fa-css-prefix}-arrows-v:before { content: @fa-var-arrows-v; } -.@{fa-css-prefix}-arrows-h:before { content: @fa-var-arrows-h; } -.@{fa-css-prefix}-bar-chart-o:before, -.@{fa-css-prefix}-bar-chart:before { content: @fa-var-bar-chart; } -.@{fa-css-prefix}-twitter-square:before { content: @fa-var-twitter-square; } -.@{fa-css-prefix}-facebook-square:before { content: @fa-var-facebook-square; } -.@{fa-css-prefix}-camera-retro:before { content: @fa-var-camera-retro; } -.@{fa-css-prefix}-key:before { content: @fa-var-key; } -.@{fa-css-prefix}-gears:before, -.@{fa-css-prefix}-cogs:before { content: @fa-var-cogs; } -.@{fa-css-prefix}-comments:before { content: @fa-var-comments; } -.@{fa-css-prefix}-thumbs-o-up:before { content: @fa-var-thumbs-o-up; } -.@{fa-css-prefix}-thumbs-o-down:before { content: @fa-var-thumbs-o-down; } -.@{fa-css-prefix}-star-half:before { content: @fa-var-star-half; } -.@{fa-css-prefix}-heart-o:before { content: @fa-var-heart-o; } -.@{fa-css-prefix}-sign-out:before { content: @fa-var-sign-out; } -.@{fa-css-prefix}-linkedin-square:before { content: @fa-var-linkedin-square; } -.@{fa-css-prefix}-thumb-tack:before { content: @fa-var-thumb-tack; } -.@{fa-css-prefix}-external-link:before { content: @fa-var-external-link; } -.@{fa-css-prefix}-sign-in:before { content: @fa-var-sign-in; } -.@{fa-css-prefix}-trophy:before { content: @fa-var-trophy; } -.@{fa-css-prefix}-github-square:before { content: @fa-var-github-square; } -.@{fa-css-prefix}-upload:before { content: @fa-var-upload; } -.@{fa-css-prefix}-lemon-o:before { content: @fa-var-lemon-o; } -.@{fa-css-prefix}-phone:before { content: @fa-var-phone; } -.@{fa-css-prefix}-square-o:before { content: @fa-var-square-o; } -.@{fa-css-prefix}-bookmark-o:before { content: @fa-var-bookmark-o; } -.@{fa-css-prefix}-phone-square:before { content: @fa-var-phone-square; } -.@{fa-css-prefix}-twitter:before { content: @fa-var-twitter; } -.@{fa-css-prefix}-facebook-f:before, -.@{fa-css-prefix}-facebook:before { content: @fa-var-facebook; } -.@{fa-css-prefix}-github:before { content: @fa-var-github; } -.@{fa-css-prefix}-unlock:before { content: @fa-var-unlock; } -.@{fa-css-prefix}-credit-card:before { content: @fa-var-credit-card; } -.@{fa-css-prefix}-rss:before { content: @fa-var-rss; } -.@{fa-css-prefix}-hdd-o:before { content: @fa-var-hdd-o; } -.@{fa-css-prefix}-bullhorn:before { content: @fa-var-bullhorn; } -.@{fa-css-prefix}-bell:before { content: @fa-var-bell; } -.@{fa-css-prefix}-certificate:before { content: @fa-var-certificate; } -.@{fa-css-prefix}-hand-o-right:before { content: @fa-var-hand-o-right; } -.@{fa-css-prefix}-hand-o-left:before { content: @fa-var-hand-o-left; } -.@{fa-css-prefix}-hand-o-up:before { content: @fa-var-hand-o-up; } -.@{fa-css-prefix}-hand-o-down:before { content: @fa-var-hand-o-down; } -.@{fa-css-prefix}-arrow-circle-left:before { content: @fa-var-arrow-circle-left; } -.@{fa-css-prefix}-arrow-circle-right:before { content: @fa-var-arrow-circle-right; } -.@{fa-css-prefix}-arrow-circle-up:before { content: @fa-var-arrow-circle-up; } -.@{fa-css-prefix}-arrow-circle-down:before { content: @fa-var-arrow-circle-down; } -.@{fa-css-prefix}-globe:before { content: @fa-var-globe; } -.@{fa-css-prefix}-wrench:before { content: @fa-var-wrench; } -.@{fa-css-prefix}-tasks:before { content: @fa-var-tasks; } -.@{fa-css-prefix}-filter:before { content: @fa-var-filter; } -.@{fa-css-prefix}-briefcase:before { content: @fa-var-briefcase; } -.@{fa-css-prefix}-arrows-alt:before { content: @fa-var-arrows-alt; } -.@{fa-css-prefix}-group:before, -.@{fa-css-prefix}-users:before { content: @fa-var-users; } -.@{fa-css-prefix}-chain:before, -.@{fa-css-prefix}-link:before { content: @fa-var-link; } -.@{fa-css-prefix}-cloud:before { content: @fa-var-cloud; } -.@{fa-css-prefix}-flask:before { content: @fa-var-flask; } -.@{fa-css-prefix}-cut:before, -.@{fa-css-prefix}-scissors:before { content: @fa-var-scissors; } -.@{fa-css-prefix}-copy:before, -.@{fa-css-prefix}-files-o:before { content: @fa-var-files-o; } -.@{fa-css-prefix}-paperclip:before { content: @fa-var-paperclip; } -.@{fa-css-prefix}-save:before, -.@{fa-css-prefix}-floppy-o:before { content: @fa-var-floppy-o; } -.@{fa-css-prefix}-square:before { content: @fa-var-square; } -.@{fa-css-prefix}-navicon:before, -.@{fa-css-prefix}-reorder:before, -.@{fa-css-prefix}-bars:before { content: @fa-var-bars; } -.@{fa-css-prefix}-list-ul:before { content: @fa-var-list-ul; } -.@{fa-css-prefix}-list-ol:before { content: @fa-var-list-ol; } -.@{fa-css-prefix}-strikethrough:before { content: @fa-var-strikethrough; } -.@{fa-css-prefix}-underline:before { content: @fa-var-underline; } -.@{fa-css-prefix}-table:before { content: @fa-var-table; } -.@{fa-css-prefix}-magic:before { content: @fa-var-magic; } -.@{fa-css-prefix}-truck:before { content: @fa-var-truck; } -.@{fa-css-prefix}-pinterest:before { content: @fa-var-pinterest; } -.@{fa-css-prefix}-pinterest-square:before { content: @fa-var-pinterest-square; } -.@{fa-css-prefix}-google-plus-square:before { content: @fa-var-google-plus-square; } -.@{fa-css-prefix}-google-plus:before { content: @fa-var-google-plus; } -.@{fa-css-prefix}-money:before { content: @fa-var-money; } -.@{fa-css-prefix}-caret-down:before { content: @fa-var-caret-down; } -.@{fa-css-prefix}-caret-up:before { content: @fa-var-caret-up; } -.@{fa-css-prefix}-caret-left:before { content: @fa-var-caret-left; } -.@{fa-css-prefix}-caret-right:before { content: @fa-var-caret-right; } -.@{fa-css-prefix}-columns:before { content: @fa-var-columns; } -.@{fa-css-prefix}-unsorted:before, -.@{fa-css-prefix}-sort:before { content: @fa-var-sort; } -.@{fa-css-prefix}-sort-down:before, -.@{fa-css-prefix}-sort-desc:before { content: @fa-var-sort-desc; } -.@{fa-css-prefix}-sort-up:before, -.@{fa-css-prefix}-sort-asc:before { content: @fa-var-sort-asc; } -.@{fa-css-prefix}-envelope:before { content: @fa-var-envelope; } -.@{fa-css-prefix}-linkedin:before { content: @fa-var-linkedin; } -.@{fa-css-prefix}-rotate-left:before, -.@{fa-css-prefix}-undo:before { content: @fa-var-undo; } -.@{fa-css-prefix}-legal:before, -.@{fa-css-prefix}-gavel:before { content: @fa-var-gavel; } -.@{fa-css-prefix}-dashboard:before, -.@{fa-css-prefix}-tachometer:before { content: @fa-var-tachometer; } -.@{fa-css-prefix}-comment-o:before { content: @fa-var-comment-o; } -.@{fa-css-prefix}-comments-o:before { content: @fa-var-comments-o; } -.@{fa-css-prefix}-flash:before, -.@{fa-css-prefix}-bolt:before { content: @fa-var-bolt; } -.@{fa-css-prefix}-sitemap:before { content: @fa-var-sitemap; } -.@{fa-css-prefix}-umbrella:before { content: @fa-var-umbrella; } -.@{fa-css-prefix}-paste:before, -.@{fa-css-prefix}-clipboard:before { content: @fa-var-clipboard; } -.@{fa-css-prefix}-lightbulb-o:before { content: @fa-var-lightbulb-o; } -.@{fa-css-prefix}-exchange:before { content: @fa-var-exchange; } -.@{fa-css-prefix}-cloud-download:before { content: @fa-var-cloud-download; } -.@{fa-css-prefix}-cloud-upload:before { content: @fa-var-cloud-upload; } -.@{fa-css-prefix}-user-md:before { content: @fa-var-user-md; } -.@{fa-css-prefix}-stethoscope:before { content: @fa-var-stethoscope; } -.@{fa-css-prefix}-suitcase:before { content: @fa-var-suitcase; } -.@{fa-css-prefix}-bell-o:before { content: @fa-var-bell-o; } -.@{fa-css-prefix}-coffee:before { content: @fa-var-coffee; } -.@{fa-css-prefix}-cutlery:before { content: @fa-var-cutlery; } -.@{fa-css-prefix}-file-text-o:before { content: @fa-var-file-text-o; } -.@{fa-css-prefix}-building-o:before { content: @fa-var-building-o; } -.@{fa-css-prefix}-hospital-o:before { content: @fa-var-hospital-o; } -.@{fa-css-prefix}-ambulance:before { content: @fa-var-ambulance; } -.@{fa-css-prefix}-medkit:before { content: @fa-var-medkit; } -.@{fa-css-prefix}-fighter-jet:before { content: @fa-var-fighter-jet; } -.@{fa-css-prefix}-beer:before { content: @fa-var-beer; } -.@{fa-css-prefix}-h-square:before { content: @fa-var-h-square; } -.@{fa-css-prefix}-plus-square:before { content: @fa-var-plus-square; } -.@{fa-css-prefix}-angle-double-left:before { content: @fa-var-angle-double-left; } -.@{fa-css-prefix}-angle-double-right:before { content: @fa-var-angle-double-right; } -.@{fa-css-prefix}-angle-double-up:before { content: @fa-var-angle-double-up; } -.@{fa-css-prefix}-angle-double-down:before { content: @fa-var-angle-double-down; } -.@{fa-css-prefix}-angle-left:before { content: @fa-var-angle-left; } -.@{fa-css-prefix}-angle-right:before { content: @fa-var-angle-right; } -.@{fa-css-prefix}-angle-up:before { content: @fa-var-angle-up; } -.@{fa-css-prefix}-angle-down:before { content: @fa-var-angle-down; } -.@{fa-css-prefix}-desktop:before { content: @fa-var-desktop; } -.@{fa-css-prefix}-laptop:before { content: @fa-var-laptop; } -.@{fa-css-prefix}-tablet:before { content: @fa-var-tablet; } -.@{fa-css-prefix}-mobile-phone:before, -.@{fa-css-prefix}-mobile:before { content: @fa-var-mobile; } -.@{fa-css-prefix}-circle-o:before { content: @fa-var-circle-o; } -.@{fa-css-prefix}-quote-left:before { content: @fa-var-quote-left; } -.@{fa-css-prefix}-quote-right:before { content: @fa-var-quote-right; } -.@{fa-css-prefix}-spinner:before { content: @fa-var-spinner; } -.@{fa-css-prefix}-circle:before { content: @fa-var-circle; } -.@{fa-css-prefix}-mail-reply:before, -.@{fa-css-prefix}-reply:before { content: @fa-var-reply; } -.@{fa-css-prefix}-github-alt:before { content: @fa-var-github-alt; } -.@{fa-css-prefix}-folder-o:before { content: @fa-var-folder-o; } -.@{fa-css-prefix}-folder-open-o:before { content: @fa-var-folder-open-o; } -.@{fa-css-prefix}-smile-o:before { content: @fa-var-smile-o; } -.@{fa-css-prefix}-frown-o:before { content: @fa-var-frown-o; } -.@{fa-css-prefix}-meh-o:before { content: @fa-var-meh-o; } -.@{fa-css-prefix}-gamepad:before { content: @fa-var-gamepad; } -.@{fa-css-prefix}-keyboard-o:before { content: @fa-var-keyboard-o; } -.@{fa-css-prefix}-flag-o:before { content: @fa-var-flag-o; } -.@{fa-css-prefix}-flag-checkered:before { content: @fa-var-flag-checkered; } -.@{fa-css-prefix}-terminal:before { content: @fa-var-terminal; } -.@{fa-css-prefix}-code:before { content: @fa-var-code; } -.@{fa-css-prefix}-mail-reply-all:before, -.@{fa-css-prefix}-reply-all:before { content: @fa-var-reply-all; } -.@{fa-css-prefix}-star-half-empty:before, -.@{fa-css-prefix}-star-half-full:before, -.@{fa-css-prefix}-star-half-o:before { content: @fa-var-star-half-o; } -.@{fa-css-prefix}-location-arrow:before { content: @fa-var-location-arrow; } -.@{fa-css-prefix}-crop:before { content: @fa-var-crop; } -.@{fa-css-prefix}-code-fork:before { content: @fa-var-code-fork; } -.@{fa-css-prefix}-unlink:before, -.@{fa-css-prefix}-chain-broken:before { content: @fa-var-chain-broken; } -.@{fa-css-prefix}-question:before { content: @fa-var-question; } -.@{fa-css-prefix}-info:before { content: @fa-var-info; } -.@{fa-css-prefix}-exclamation:before { content: @fa-var-exclamation; } -.@{fa-css-prefix}-superscript:before { content: @fa-var-superscript; } -.@{fa-css-prefix}-subscript:before { content: @fa-var-subscript; } -.@{fa-css-prefix}-eraser:before { content: @fa-var-eraser; } -.@{fa-css-prefix}-puzzle-piece:before { content: @fa-var-puzzle-piece; } -.@{fa-css-prefix}-microphone:before { content: @fa-var-microphone; } -.@{fa-css-prefix}-microphone-slash:before { content: @fa-var-microphone-slash; } -.@{fa-css-prefix}-shield:before { content: @fa-var-shield; } -.@{fa-css-prefix}-calendar-o:before { content: @fa-var-calendar-o; } -.@{fa-css-prefix}-fire-extinguisher:before { content: @fa-var-fire-extinguisher; } -.@{fa-css-prefix}-rocket:before { content: @fa-var-rocket; } -.@{fa-css-prefix}-maxcdn:before { content: @fa-var-maxcdn; } -.@{fa-css-prefix}-chevron-circle-left:before { content: @fa-var-chevron-circle-left; } -.@{fa-css-prefix}-chevron-circle-right:before { content: @fa-var-chevron-circle-right; } -.@{fa-css-prefix}-chevron-circle-up:before { content: @fa-var-chevron-circle-up; } -.@{fa-css-prefix}-chevron-circle-down:before { content: @fa-var-chevron-circle-down; } -.@{fa-css-prefix}-html5:before { content: @fa-var-html5; } -.@{fa-css-prefix}-css3:before { content: @fa-var-css3; } -.@{fa-css-prefix}-anchor:before { content: @fa-var-anchor; } -.@{fa-css-prefix}-unlock-alt:before { content: @fa-var-unlock-alt; } -.@{fa-css-prefix}-bullseye:before { content: @fa-var-bullseye; } -.@{fa-css-prefix}-ellipsis-h:before { content: @fa-var-ellipsis-h; } -.@{fa-css-prefix}-ellipsis-v:before { content: @fa-var-ellipsis-v; } -.@{fa-css-prefix}-rss-square:before { content: @fa-var-rss-square; } -.@{fa-css-prefix}-play-circle:before { content: @fa-var-play-circle; } -.@{fa-css-prefix}-ticket:before { content: @fa-var-ticket; } -.@{fa-css-prefix}-minus-square:before { content: @fa-var-minus-square; } -.@{fa-css-prefix}-minus-square-o:before { content: @fa-var-minus-square-o; } -.@{fa-css-prefix}-level-up:before { content: @fa-var-level-up; } -.@{fa-css-prefix}-level-down:before { content: @fa-var-level-down; } -.@{fa-css-prefix}-check-square:before { content: @fa-var-check-square; } -.@{fa-css-prefix}-pencil-square:before { content: @fa-var-pencil-square; } -.@{fa-css-prefix}-external-link-square:before { content: @fa-var-external-link-square; } -.@{fa-css-prefix}-share-square:before { content: @fa-var-share-square; } -.@{fa-css-prefix}-compass:before { content: @fa-var-compass; } -.@{fa-css-prefix}-toggle-down:before, -.@{fa-css-prefix}-caret-square-o-down:before { content: @fa-var-caret-square-o-down; } -.@{fa-css-prefix}-toggle-up:before, -.@{fa-css-prefix}-caret-square-o-up:before { content: @fa-var-caret-square-o-up; } -.@{fa-css-prefix}-toggle-right:before, -.@{fa-css-prefix}-caret-square-o-right:before { content: @fa-var-caret-square-o-right; } -.@{fa-css-prefix}-euro:before, -.@{fa-css-prefix}-eur:before { content: @fa-var-eur; } -.@{fa-css-prefix}-gbp:before { content: @fa-var-gbp; } -.@{fa-css-prefix}-dollar:before, -.@{fa-css-prefix}-usd:before { content: @fa-var-usd; } -.@{fa-css-prefix}-rupee:before, -.@{fa-css-prefix}-inr:before { content: @fa-var-inr; } -.@{fa-css-prefix}-cny:before, -.@{fa-css-prefix}-rmb:before, -.@{fa-css-prefix}-yen:before, -.@{fa-css-prefix}-jpy:before { content: @fa-var-jpy; } -.@{fa-css-prefix}-ruble:before, -.@{fa-css-prefix}-rouble:before, -.@{fa-css-prefix}-rub:before { content: @fa-var-rub; } -.@{fa-css-prefix}-won:before, -.@{fa-css-prefix}-krw:before { content: @fa-var-krw; } -.@{fa-css-prefix}-bitcoin:before, -.@{fa-css-prefix}-btc:before { content: @fa-var-btc; } -.@{fa-css-prefix}-file:before { content: @fa-var-file; } -.@{fa-css-prefix}-file-text:before { content: @fa-var-file-text; } -.@{fa-css-prefix}-sort-alpha-asc:before { content: @fa-var-sort-alpha-asc; } -.@{fa-css-prefix}-sort-alpha-desc:before { content: @fa-var-sort-alpha-desc; } -.@{fa-css-prefix}-sort-amount-asc:before { content: @fa-var-sort-amount-asc; } -.@{fa-css-prefix}-sort-amount-desc:before { content: @fa-var-sort-amount-desc; } -.@{fa-css-prefix}-sort-numeric-asc:before { content: @fa-var-sort-numeric-asc; } -.@{fa-css-prefix}-sort-numeric-desc:before { content: @fa-var-sort-numeric-desc; } -.@{fa-css-prefix}-thumbs-up:before { content: @fa-var-thumbs-up; } -.@{fa-css-prefix}-thumbs-down:before { content: @fa-var-thumbs-down; } -.@{fa-css-prefix}-youtube-square:before { content: @fa-var-youtube-square; } -.@{fa-css-prefix}-youtube:before { content: @fa-var-youtube; } -.@{fa-css-prefix}-xing:before { content: @fa-var-xing; } -.@{fa-css-prefix}-xing-square:before { content: @fa-var-xing-square; } -.@{fa-css-prefix}-youtube-play:before { content: @fa-var-youtube-play; } -.@{fa-css-prefix}-dropbox:before { content: @fa-var-dropbox; } -.@{fa-css-prefix}-stack-overflow:before { content: @fa-var-stack-overflow; } -.@{fa-css-prefix}-instagram:before { content: @fa-var-instagram; } -.@{fa-css-prefix}-flickr:before { content: @fa-var-flickr; } -.@{fa-css-prefix}-adn:before { content: @fa-var-adn; } -.@{fa-css-prefix}-bitbucket:before { content: @fa-var-bitbucket; } -.@{fa-css-prefix}-bitbucket-square:before { content: @fa-var-bitbucket-square; } -.@{fa-css-prefix}-tumblr:before { content: @fa-var-tumblr; } -.@{fa-css-prefix}-tumblr-square:before { content: @fa-var-tumblr-square; } -.@{fa-css-prefix}-long-arrow-down:before { content: @fa-var-long-arrow-down; } -.@{fa-css-prefix}-long-arrow-up:before { content: @fa-var-long-arrow-up; } -.@{fa-css-prefix}-long-arrow-left:before { content: @fa-var-long-arrow-left; } -.@{fa-css-prefix}-long-arrow-right:before { content: @fa-var-long-arrow-right; } -.@{fa-css-prefix}-apple:before { content: @fa-var-apple; } -.@{fa-css-prefix}-windows:before { content: @fa-var-windows; } -.@{fa-css-prefix}-android:before { content: @fa-var-android; } -.@{fa-css-prefix}-linux:before { content: @fa-var-linux; } -.@{fa-css-prefix}-dribbble:before { content: @fa-var-dribbble; } -.@{fa-css-prefix}-skype:before { content: @fa-var-skype; } -.@{fa-css-prefix}-foursquare:before { content: @fa-var-foursquare; } -.@{fa-css-prefix}-trello:before { content: @fa-var-trello; } -.@{fa-css-prefix}-female:before { content: @fa-var-female; } -.@{fa-css-prefix}-male:before { content: @fa-var-male; } -.@{fa-css-prefix}-gittip:before, -.@{fa-css-prefix}-gratipay:before { content: @fa-var-gratipay; } -.@{fa-css-prefix}-sun-o:before { content: @fa-var-sun-o; } -.@{fa-css-prefix}-moon-o:before { content: @fa-var-moon-o; } -.@{fa-css-prefix}-archive:before { content: @fa-var-archive; } -.@{fa-css-prefix}-bug:before { content: @fa-var-bug; } -.@{fa-css-prefix}-vk:before { content: @fa-var-vk; } -.@{fa-css-prefix}-weibo:before { content: @fa-var-weibo; } -.@{fa-css-prefix}-renren:before { content: @fa-var-renren; } -.@{fa-css-prefix}-pagelines:before { content: @fa-var-pagelines; } -.@{fa-css-prefix}-stack-exchange:before { content: @fa-var-stack-exchange; } -.@{fa-css-prefix}-arrow-circle-o-right:before { content: @fa-var-arrow-circle-o-right; } -.@{fa-css-prefix}-arrow-circle-o-left:before { content: @fa-var-arrow-circle-o-left; } -.@{fa-css-prefix}-toggle-left:before, -.@{fa-css-prefix}-caret-square-o-left:before { content: @fa-var-caret-square-o-left; } -.@{fa-css-prefix}-dot-circle-o:before { content: @fa-var-dot-circle-o; } -.@{fa-css-prefix}-wheelchair:before { content: @fa-var-wheelchair; } -.@{fa-css-prefix}-vimeo-square:before { content: @fa-var-vimeo-square; } -.@{fa-css-prefix}-turkish-lira:before, -.@{fa-css-prefix}-try:before { content: @fa-var-try; } -.@{fa-css-prefix}-plus-square-o:before { content: @fa-var-plus-square-o; } -.@{fa-css-prefix}-space-shuttle:before { content: @fa-var-space-shuttle; } -.@{fa-css-prefix}-slack:before { content: @fa-var-slack; } -.@{fa-css-prefix}-envelope-square:before { content: @fa-var-envelope-square; } -.@{fa-css-prefix}-wordpress:before { content: @fa-var-wordpress; } -.@{fa-css-prefix}-openid:before { content: @fa-var-openid; } -.@{fa-css-prefix}-institution:before, -.@{fa-css-prefix}-bank:before, -.@{fa-css-prefix}-university:before { content: @fa-var-university; } -.@{fa-css-prefix}-mortar-board:before, -.@{fa-css-prefix}-graduation-cap:before { content: @fa-var-graduation-cap; } -.@{fa-css-prefix}-yahoo:before { content: @fa-var-yahoo; } -.@{fa-css-prefix}-google:before { content: @fa-var-google; } -.@{fa-css-prefix}-reddit:before { content: @fa-var-reddit; } -.@{fa-css-prefix}-reddit-square:before { content: @fa-var-reddit-square; } -.@{fa-css-prefix}-stumbleupon-circle:before { content: @fa-var-stumbleupon-circle; } -.@{fa-css-prefix}-stumbleupon:before { content: @fa-var-stumbleupon; } -.@{fa-css-prefix}-delicious:before { content: @fa-var-delicious; } -.@{fa-css-prefix}-digg:before { content: @fa-var-digg; } -.@{fa-css-prefix}-pied-piper:before { content: @fa-var-pied-piper; } -.@{fa-css-prefix}-pied-piper-alt:before { content: @fa-var-pied-piper-alt; } -.@{fa-css-prefix}-drupal:before { content: @fa-var-drupal; } -.@{fa-css-prefix}-joomla:before { content: @fa-var-joomla; } -.@{fa-css-prefix}-language:before { content: @fa-var-language; } -.@{fa-css-prefix}-fax:before { content: @fa-var-fax; } -.@{fa-css-prefix}-building:before { content: @fa-var-building; } -.@{fa-css-prefix}-child:before { content: @fa-var-child; } -.@{fa-css-prefix}-paw:before { content: @fa-var-paw; } -.@{fa-css-prefix}-spoon:before { content: @fa-var-spoon; } -.@{fa-css-prefix}-cube:before { content: @fa-var-cube; } -.@{fa-css-prefix}-cubes:before { content: @fa-var-cubes; } -.@{fa-css-prefix}-behance:before { content: @fa-var-behance; } -.@{fa-css-prefix}-behance-square:before { content: @fa-var-behance-square; } -.@{fa-css-prefix}-steam:before { content: @fa-var-steam; } -.@{fa-css-prefix}-steam-square:before { content: @fa-var-steam-square; } -.@{fa-css-prefix}-recycle:before { content: @fa-var-recycle; } -.@{fa-css-prefix}-automobile:before, -.@{fa-css-prefix}-car:before { content: @fa-var-car; } -.@{fa-css-prefix}-cab:before, -.@{fa-css-prefix}-taxi:before { content: @fa-var-taxi; } -.@{fa-css-prefix}-tree:before { content: @fa-var-tree; } -.@{fa-css-prefix}-spotify:before { content: @fa-var-spotify; } -.@{fa-css-prefix}-deviantart:before { content: @fa-var-deviantart; } -.@{fa-css-prefix}-soundcloud:before { content: @fa-var-soundcloud; } -.@{fa-css-prefix}-database:before { content: @fa-var-database; } -.@{fa-css-prefix}-file-pdf-o:before { content: @fa-var-file-pdf-o; } -.@{fa-css-prefix}-file-word-o:before { content: @fa-var-file-word-o; } -.@{fa-css-prefix}-file-excel-o:before { content: @fa-var-file-excel-o; } -.@{fa-css-prefix}-file-powerpoint-o:before { content: @fa-var-file-powerpoint-o; } -.@{fa-css-prefix}-file-photo-o:before, -.@{fa-css-prefix}-file-picture-o:before, -.@{fa-css-prefix}-file-image-o:before { content: @fa-var-file-image-o; } -.@{fa-css-prefix}-file-zip-o:before, -.@{fa-css-prefix}-file-archive-o:before { content: @fa-var-file-archive-o; } -.@{fa-css-prefix}-file-sound-o:before, -.@{fa-css-prefix}-file-audio-o:before { content: @fa-var-file-audio-o; } -.@{fa-css-prefix}-file-movie-o:before, -.@{fa-css-prefix}-file-video-o:before { content: @fa-var-file-video-o; } -.@{fa-css-prefix}-file-code-o:before { content: @fa-var-file-code-o; } -.@{fa-css-prefix}-vine:before { content: @fa-var-vine; } -.@{fa-css-prefix}-codepen:before { content: @fa-var-codepen; } -.@{fa-css-prefix}-jsfiddle:before { content: @fa-var-jsfiddle; } -.@{fa-css-prefix}-life-bouy:before, -.@{fa-css-prefix}-life-buoy:before, -.@{fa-css-prefix}-life-saver:before, -.@{fa-css-prefix}-support:before, -.@{fa-css-prefix}-life-ring:before { content: @fa-var-life-ring; } -.@{fa-css-prefix}-circle-o-notch:before { content: @fa-var-circle-o-notch; } -.@{fa-css-prefix}-ra:before, -.@{fa-css-prefix}-rebel:before { content: @fa-var-rebel; } -.@{fa-css-prefix}-ge:before, -.@{fa-css-prefix}-empire:before { content: @fa-var-empire; } -.@{fa-css-prefix}-git-square:before { content: @fa-var-git-square; } -.@{fa-css-prefix}-git:before { content: @fa-var-git; } -.@{fa-css-prefix}-hacker-news:before { content: @fa-var-hacker-news; } -.@{fa-css-prefix}-tencent-weibo:before { content: @fa-var-tencent-weibo; } -.@{fa-css-prefix}-qq:before { content: @fa-var-qq; } -.@{fa-css-prefix}-wechat:before, -.@{fa-css-prefix}-weixin:before { content: @fa-var-weixin; } -.@{fa-css-prefix}-send:before, -.@{fa-css-prefix}-paper-plane:before { content: @fa-var-paper-plane; } -.@{fa-css-prefix}-send-o:before, -.@{fa-css-prefix}-paper-plane-o:before { content: @fa-var-paper-plane-o; } -.@{fa-css-prefix}-history:before { content: @fa-var-history; } -.@{fa-css-prefix}-genderless:before, -.@{fa-css-prefix}-circle-thin:before { content: @fa-var-circle-thin; } -.@{fa-css-prefix}-header:before { content: @fa-var-header; } -.@{fa-css-prefix}-paragraph:before { content: @fa-var-paragraph; } -.@{fa-css-prefix}-sliders:before { content: @fa-var-sliders; } -.@{fa-css-prefix}-share-alt:before { content: @fa-var-share-alt; } -.@{fa-css-prefix}-share-alt-square:before { content: @fa-var-share-alt-square; } -.@{fa-css-prefix}-bomb:before { content: @fa-var-bomb; } -.@{fa-css-prefix}-soccer-ball-o:before, -.@{fa-css-prefix}-futbol-o:before { content: @fa-var-futbol-o; } -.@{fa-css-prefix}-tty:before { content: @fa-var-tty; } -.@{fa-css-prefix}-binoculars:before { content: @fa-var-binoculars; } -.@{fa-css-prefix}-plug:before { content: @fa-var-plug; } -.@{fa-css-prefix}-slideshare:before { content: @fa-var-slideshare; } -.@{fa-css-prefix}-twitch:before { content: @fa-var-twitch; } -.@{fa-css-prefix}-yelp:before { content: @fa-var-yelp; } -.@{fa-css-prefix}-newspaper-o:before { content: @fa-var-newspaper-o; } -.@{fa-css-prefix}-wifi:before { content: @fa-var-wifi; } -.@{fa-css-prefix}-calculator:before { content: @fa-var-calculator; } -.@{fa-css-prefix}-paypal:before { content: @fa-var-paypal; } -.@{fa-css-prefix}-google-wallet:before { content: @fa-var-google-wallet; } -.@{fa-css-prefix}-cc-visa:before { content: @fa-var-cc-visa; } -.@{fa-css-prefix}-cc-mastercard:before { content: @fa-var-cc-mastercard; } -.@{fa-css-prefix}-cc-discover:before { content: @fa-var-cc-discover; } -.@{fa-css-prefix}-cc-amex:before { content: @fa-var-cc-amex; } -.@{fa-css-prefix}-cc-paypal:before { content: @fa-var-cc-paypal; } -.@{fa-css-prefix}-cc-stripe:before { content: @fa-var-cc-stripe; } -.@{fa-css-prefix}-bell-slash:before { content: @fa-var-bell-slash; } -.@{fa-css-prefix}-bell-slash-o:before { content: @fa-var-bell-slash-o; } -.@{fa-css-prefix}-trash:before { content: @fa-var-trash; } -.@{fa-css-prefix}-copyright:before { content: @fa-var-copyright; } -.@{fa-css-prefix}-at:before { content: @fa-var-at; } -.@{fa-css-prefix}-eyedropper:before { content: @fa-var-eyedropper; } -.@{fa-css-prefix}-paint-brush:before { content: @fa-var-paint-brush; } -.@{fa-css-prefix}-birthday-cake:before { content: @fa-var-birthday-cake; } -.@{fa-css-prefix}-area-chart:before { content: @fa-var-area-chart; } -.@{fa-css-prefix}-pie-chart:before { content: @fa-var-pie-chart; } -.@{fa-css-prefix}-line-chart:before { content: @fa-var-line-chart; } -.@{fa-css-prefix}-lastfm:before { content: @fa-var-lastfm; } -.@{fa-css-prefix}-lastfm-square:before { content: @fa-var-lastfm-square; } -.@{fa-css-prefix}-toggle-off:before { content: @fa-var-toggle-off; } -.@{fa-css-prefix}-toggle-on:before { content: @fa-var-toggle-on; } -.@{fa-css-prefix}-bicycle:before { content: @fa-var-bicycle; } -.@{fa-css-prefix}-bus:before { content: @fa-var-bus; } -.@{fa-css-prefix}-ioxhost:before { content: @fa-var-ioxhost; } -.@{fa-css-prefix}-angellist:before { content: @fa-var-angellist; } -.@{fa-css-prefix}-cc:before { content: @fa-var-cc; } -.@{fa-css-prefix}-shekel:before, -.@{fa-css-prefix}-sheqel:before, -.@{fa-css-prefix}-ils:before { content: @fa-var-ils; } -.@{fa-css-prefix}-meanpath:before { content: @fa-var-meanpath; } -.@{fa-css-prefix}-buysellads:before { content: @fa-var-buysellads; } -.@{fa-css-prefix}-connectdevelop:before { content: @fa-var-connectdevelop; } -.@{fa-css-prefix}-dashcube:before { content: @fa-var-dashcube; } -.@{fa-css-prefix}-forumbee:before { content: @fa-var-forumbee; } -.@{fa-css-prefix}-leanpub:before { content: @fa-var-leanpub; } -.@{fa-css-prefix}-sellsy:before { content: @fa-var-sellsy; } -.@{fa-css-prefix}-shirtsinbulk:before { content: @fa-var-shirtsinbulk; } -.@{fa-css-prefix}-simplybuilt:before { content: @fa-var-simplybuilt; } -.@{fa-css-prefix}-skyatlas:before { content: @fa-var-skyatlas; } -.@{fa-css-prefix}-cart-plus:before { content: @fa-var-cart-plus; } -.@{fa-css-prefix}-cart-arrow-down:before { content: @fa-var-cart-arrow-down; } -.@{fa-css-prefix}-diamond:before { content: @fa-var-diamond; } -.@{fa-css-prefix}-ship:before { content: @fa-var-ship; } -.@{fa-css-prefix}-user-secret:before { content: @fa-var-user-secret; } -.@{fa-css-prefix}-motorcycle:before { content: @fa-var-motorcycle; } -.@{fa-css-prefix}-street-view:before { content: @fa-var-street-view; } -.@{fa-css-prefix}-heartbeat:before { content: @fa-var-heartbeat; } -.@{fa-css-prefix}-venus:before { content: @fa-var-venus; } -.@{fa-css-prefix}-mars:before { content: @fa-var-mars; } -.@{fa-css-prefix}-mercury:before { content: @fa-var-mercury; } -.@{fa-css-prefix}-transgender:before { content: @fa-var-transgender; } -.@{fa-css-prefix}-transgender-alt:before { content: @fa-var-transgender-alt; } -.@{fa-css-prefix}-venus-double:before { content: @fa-var-venus-double; } -.@{fa-css-prefix}-mars-double:before { content: @fa-var-mars-double; } -.@{fa-css-prefix}-venus-mars:before { content: @fa-var-venus-mars; } -.@{fa-css-prefix}-mars-stroke:before { content: @fa-var-mars-stroke; } -.@{fa-css-prefix}-mars-stroke-v:before { content: @fa-var-mars-stroke-v; } -.@{fa-css-prefix}-mars-stroke-h:before { content: @fa-var-mars-stroke-h; } -.@{fa-css-prefix}-neuter:before { content: @fa-var-neuter; } -.@{fa-css-prefix}-facebook-official:before { content: @fa-var-facebook-official; } -.@{fa-css-prefix}-pinterest-p:before { content: @fa-var-pinterest-p; } -.@{fa-css-prefix}-whatsapp:before { content: @fa-var-whatsapp; } -.@{fa-css-prefix}-server:before { content: @fa-var-server; } -.@{fa-css-prefix}-user-plus:before { content: @fa-var-user-plus; } -.@{fa-css-prefix}-user-times:before { content: @fa-var-user-times; } -.@{fa-css-prefix}-hotel:before, -.@{fa-css-prefix}-bed:before { content: @fa-var-bed; } -.@{fa-css-prefix}-viacoin:before { content: @fa-var-viacoin; } -.@{fa-css-prefix}-train:before { content: @fa-var-train; } -.@{fa-css-prefix}-subway:before { content: @fa-var-subway; } -.@{fa-css-prefix}-medium:before { content: @fa-var-medium; } diff --git a/src/UI/Content/FontAwesome/larger.less b/src/UI/Content/FontAwesome/larger.less deleted file mode 100644 index c9d646770..000000000 --- a/src/UI/Content/FontAwesome/larger.less +++ /dev/null @@ -1,13 +0,0 @@ -// Icon Sizes -// ------------------------- - -/* makes the font 33% larger relative to the icon container */ -.@{fa-css-prefix}-lg { - font-size: (4em / 3); - line-height: (3em / 4); - vertical-align: -15%; -} -.@{fa-css-prefix}-2x { font-size: 2em; } -.@{fa-css-prefix}-3x { font-size: 3em; } -.@{fa-css-prefix}-4x { font-size: 4em; } -.@{fa-css-prefix}-5x { font-size: 5em; } diff --git a/src/UI/Content/FontAwesome/list.less b/src/UI/Content/FontAwesome/list.less deleted file mode 100644 index 0b440382f..000000000 --- a/src/UI/Content/FontAwesome/list.less +++ /dev/null @@ -1,19 +0,0 @@ -// List Icons -// ------------------------- - -.@{fa-css-prefix}-ul { - padding-left: 0; - margin-left: @fa-li-width; - list-style-type: none; - > li { position: relative; } -} -.@{fa-css-prefix}-li { - position: absolute; - left: -@fa-li-width; - width: @fa-li-width; - top: (2em / 14); - text-align: center; - &.@{fa-css-prefix}-lg { - left: (-@fa-li-width + (4em / 14)); - } -} diff --git a/src/UI/Content/FontAwesome/mixins.less b/src/UI/Content/FontAwesome/mixins.less deleted file mode 100644 index c97f4604c..000000000 --- a/src/UI/Content/FontAwesome/mixins.less +++ /dev/null @@ -1,27 +0,0 @@ -// Mixins -// -------------------------- - -.fa-icon() { - display: inline-block; - font: normal normal normal @fa-font-size-base/1 FontAwesome; // shortening font declaration - font-size: inherit; // can't have font-size inherit on line above, so need to override - text-rendering: auto; // optimizelegibility throws things off #1094 - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; - transform: translate(0, 0); // ensures no half-pixel rendering in firefox - -} - -.fa-icon-rotate(@degrees, @rotation) { - filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=@rotation); - -webkit-transform: rotate(@degrees); - -ms-transform: rotate(@degrees); - transform: rotate(@degrees); -} - -.fa-icon-flip(@horiz, @vert, @rotation) { - filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=@rotation, mirror=1); - -webkit-transform: scale(@horiz, @vert); - -ms-transform: scale(@horiz, @vert); - transform: scale(@horiz, @vert); -} diff --git a/src/UI/Content/FontAwesome/path.less b/src/UI/Content/FontAwesome/path.less deleted file mode 100644 index 9211e6659..000000000 --- a/src/UI/Content/FontAwesome/path.less +++ /dev/null @@ -1,15 +0,0 @@ -/* FONT PATH - * -------------------------- */ - -@font-face { - font-family: 'FontAwesome'; - src: url('@{fa-font-path}/fontawesome-webfont.eot?v=@{fa-version}'); - src: url('@{fa-font-path}/fontawesome-webfont.eot?#iefix&v=@{fa-version}') format('embedded-opentype'), - url('@{fa-font-path}/fontawesome-webfont.woff2?v=@{fa-version}') format('woff2'), - url('@{fa-font-path}/fontawesome-webfont.woff?v=@{fa-version}') format('woff'), - url('@{fa-font-path}/fontawesome-webfont.ttf?v=@{fa-version}') format('truetype'), - url('@{fa-font-path}/fontawesome-webfont.svg?v=@{fa-version}#fontawesomeregular') format('svg'); -// src: url('@{fa-font-path}/FontAwesome.otf') format('opentype'); // used when developing fonts - font-weight: normal; - font-style: normal; -} diff --git a/src/UI/Content/FontAwesome/rotated-flipped.less b/src/UI/Content/FontAwesome/rotated-flipped.less deleted file mode 100644 index f6ba81475..000000000 --- a/src/UI/Content/FontAwesome/rotated-flipped.less +++ /dev/null @@ -1,20 +0,0 @@ -// Rotated & Flipped Icons -// ------------------------- - -.@{fa-css-prefix}-rotate-90 { .fa-icon-rotate(90deg, 1); } -.@{fa-css-prefix}-rotate-180 { .fa-icon-rotate(180deg, 2); } -.@{fa-css-prefix}-rotate-270 { .fa-icon-rotate(270deg, 3); } - -.@{fa-css-prefix}-flip-horizontal { .fa-icon-flip(-1, 1, 0); } -.@{fa-css-prefix}-flip-vertical { .fa-icon-flip(1, -1, 2); } - -// Hook for IE8-9 -// ------------------------- - -:root .@{fa-css-prefix}-rotate-90, -:root .@{fa-css-prefix}-rotate-180, -:root .@{fa-css-prefix}-rotate-270, -:root .@{fa-css-prefix}-flip-horizontal, -:root .@{fa-css-prefix}-flip-vertical { - filter: none; -} diff --git a/src/UI/Content/FontAwesome/stacked.less b/src/UI/Content/FontAwesome/stacked.less deleted file mode 100644 index fc53fb0e7..000000000 --- a/src/UI/Content/FontAwesome/stacked.less +++ /dev/null @@ -1,20 +0,0 @@ -// Stacked Icons -// ------------------------- - -.@{fa-css-prefix}-stack { - position: relative; - display: inline-block; - width: 2em; - height: 2em; - line-height: 2em; - vertical-align: middle; -} -.@{fa-css-prefix}-stack-1x, .@{fa-css-prefix}-stack-2x { - position: absolute; - left: 0; - width: 100%; - text-align: center; -} -.@{fa-css-prefix}-stack-1x { line-height: inherit; } -.@{fa-css-prefix}-stack-2x { font-size: 2em; } -.@{fa-css-prefix}-inverse { color: @fa-inverse; } diff --git a/src/UI/Content/FontAwesome/variables.less b/src/UI/Content/FontAwesome/variables.less deleted file mode 100644 index 7d026a20d..000000000 --- a/src/UI/Content/FontAwesome/variables.less +++ /dev/null @@ -1,606 +0,0 @@ -// Variables -// -------------------------- - -@fa-font-path: "../Content/FontAwesome"; -@fa-font-size-base: 14px; -//@fa-font-path: "//netdna.bootstrapcdn.com/font-awesome/4.3.0/fonts"; // for referencing Bootstrap CDN font files directly -@fa-css-prefix: fa; -@fa-version: "4.3.0"; -@fa-border-color: #eee; -@fa-inverse: #fff; -@fa-li-width: (30em / 14); - -@fa-var-adjust: "\f042"; -@fa-var-adn: "\f170"; -@fa-var-align-center: "\f037"; -@fa-var-align-justify: "\f039"; -@fa-var-align-left: "\f036"; -@fa-var-align-right: "\f038"; -@fa-var-ambulance: "\f0f9"; -@fa-var-anchor: "\f13d"; -@fa-var-android: "\f17b"; -@fa-var-angellist: "\f209"; -@fa-var-angle-double-down: "\f103"; -@fa-var-angle-double-left: "\f100"; -@fa-var-angle-double-right: "\f101"; -@fa-var-angle-double-up: "\f102"; -@fa-var-angle-down: "\f107"; -@fa-var-angle-left: "\f104"; -@fa-var-angle-right: "\f105"; -@fa-var-angle-up: "\f106"; -@fa-var-apple: "\f179"; -@fa-var-archive: "\f187"; -@fa-var-area-chart: "\f1fe"; -@fa-var-arrow-circle-down: "\f0ab"; -@fa-var-arrow-circle-left: "\f0a8"; -@fa-var-arrow-circle-o-down: "\f01a"; -@fa-var-arrow-circle-o-left: "\f190"; -@fa-var-arrow-circle-o-right: "\f18e"; -@fa-var-arrow-circle-o-up: "\f01b"; -@fa-var-arrow-circle-right: "\f0a9"; -@fa-var-arrow-circle-up: "\f0aa"; -@fa-var-arrow-down: "\f063"; -@fa-var-arrow-left: "\f060"; -@fa-var-arrow-right: "\f061"; -@fa-var-arrow-up: "\f062"; -@fa-var-arrows: "\f047"; -@fa-var-arrows-alt: "\f0b2"; -@fa-var-arrows-h: "\f07e"; -@fa-var-arrows-v: "\f07d"; -@fa-var-asterisk: "\f069"; -@fa-var-at: "\f1fa"; -@fa-var-automobile: "\f1b9"; -@fa-var-backward: "\f04a"; -@fa-var-ban: "\f05e"; -@fa-var-bank: "\f19c"; -@fa-var-bar-chart: "\f080"; -@fa-var-bar-chart-o: "\f080"; -@fa-var-barcode: "\f02a"; -@fa-var-bars: "\f0c9"; -@fa-var-bed: "\f236"; -@fa-var-beer: "\f0fc"; -@fa-var-behance: "\f1b4"; -@fa-var-behance-square: "\f1b5"; -@fa-var-bell: "\f0f3"; -@fa-var-bell-o: "\f0a2"; -@fa-var-bell-slash: "\f1f6"; -@fa-var-bell-slash-o: "\f1f7"; -@fa-var-bicycle: "\f206"; -@fa-var-binoculars: "\f1e5"; -@fa-var-birthday-cake: "\f1fd"; -@fa-var-bitbucket: "\f171"; -@fa-var-bitbucket-square: "\f172"; -@fa-var-bitcoin: "\f15a"; -@fa-var-bold: "\f032"; -@fa-var-bolt: "\f0e7"; -@fa-var-bomb: "\f1e2"; -@fa-var-book: "\f02d"; -@fa-var-bookmark: "\f02e"; -@fa-var-bookmark-o: "\f097"; -@fa-var-briefcase: "\f0b1"; -@fa-var-btc: "\f15a"; -@fa-var-bug: "\f188"; -@fa-var-building: "\f1ad"; -@fa-var-building-o: "\f0f7"; -@fa-var-bullhorn: "\f0a1"; -@fa-var-bullseye: "\f140"; -@fa-var-bus: "\f207"; -@fa-var-buysellads: "\f20d"; -@fa-var-cab: "\f1ba"; -@fa-var-calculator: "\f1ec"; -@fa-var-calendar: "\f073"; -@fa-var-calendar-o: "\f133"; -@fa-var-camera: "\f030"; -@fa-var-camera-retro: "\f083"; -@fa-var-car: "\f1b9"; -@fa-var-caret-down: "\f0d7"; -@fa-var-caret-left: "\f0d9"; -@fa-var-caret-right: "\f0da"; -@fa-var-caret-square-o-down: "\f150"; -@fa-var-caret-square-o-left: "\f191"; -@fa-var-caret-square-o-right: "\f152"; -@fa-var-caret-square-o-up: "\f151"; -@fa-var-caret-up: "\f0d8"; -@fa-var-cart-arrow-down: "\f218"; -@fa-var-cart-plus: "\f217"; -@fa-var-cc: "\f20a"; -@fa-var-cc-amex: "\f1f3"; -@fa-var-cc-discover: "\f1f2"; -@fa-var-cc-mastercard: "\f1f1"; -@fa-var-cc-paypal: "\f1f4"; -@fa-var-cc-stripe: "\f1f5"; -@fa-var-cc-visa: "\f1f0"; -@fa-var-certificate: "\f0a3"; -@fa-var-chain: "\f0c1"; -@fa-var-chain-broken: "\f127"; -@fa-var-check: "\f00c"; -@fa-var-check-circle: "\f058"; -@fa-var-check-circle-o: "\f05d"; -@fa-var-check-square: "\f14a"; -@fa-var-check-square-o: "\f046"; -@fa-var-chevron-circle-down: "\f13a"; -@fa-var-chevron-circle-left: "\f137"; -@fa-var-chevron-circle-right: "\f138"; -@fa-var-chevron-circle-up: "\f139"; -@fa-var-chevron-down: "\f078"; -@fa-var-chevron-left: "\f053"; -@fa-var-chevron-right: "\f054"; -@fa-var-chevron-up: "\f077"; -@fa-var-child: "\f1ae"; -@fa-var-circle: "\f111"; -@fa-var-circle-o: "\f10c"; -@fa-var-circle-o-notch: "\f1ce"; -@fa-var-circle-thin: "\f1db"; -@fa-var-clipboard: "\f0ea"; -@fa-var-clock-o: "\f017"; -@fa-var-close: "\f00d"; -@fa-var-cloud: "\f0c2"; -@fa-var-cloud-download: "\f0ed"; -@fa-var-cloud-upload: "\f0ee"; -@fa-var-cny: "\f157"; -@fa-var-code: "\f121"; -@fa-var-code-fork: "\f126"; -@fa-var-codepen: "\f1cb"; -@fa-var-coffee: "\f0f4"; -@fa-var-cog: "\f013"; -@fa-var-cogs: "\f085"; -@fa-var-columns: "\f0db"; -@fa-var-comment: "\f075"; -@fa-var-comment-o: "\f0e5"; -@fa-var-comments: "\f086"; -@fa-var-comments-o: "\f0e6"; -@fa-var-compass: "\f14e"; -@fa-var-compress: "\f066"; -@fa-var-connectdevelop: "\f20e"; -@fa-var-copy: "\f0c5"; -@fa-var-copyright: "\f1f9"; -@fa-var-credit-card: "\f09d"; -@fa-var-crop: "\f125"; -@fa-var-crosshairs: "\f05b"; -@fa-var-css3: "\f13c"; -@fa-var-cube: "\f1b2"; -@fa-var-cubes: "\f1b3"; -@fa-var-cut: "\f0c4"; -@fa-var-cutlery: "\f0f5"; -@fa-var-dashboard: "\f0e4"; -@fa-var-dashcube: "\f210"; -@fa-var-database: "\f1c0"; -@fa-var-dedent: "\f03b"; -@fa-var-delicious: "\f1a5"; -@fa-var-desktop: "\f108"; -@fa-var-deviantart: "\f1bd"; -@fa-var-diamond: "\f219"; -@fa-var-digg: "\f1a6"; -@fa-var-dollar: "\f155"; -@fa-var-dot-circle-o: "\f192"; -@fa-var-download: "\f019"; -@fa-var-dribbble: "\f17d"; -@fa-var-dropbox: "\f16b"; -@fa-var-drupal: "\f1a9"; -@fa-var-edit: "\f044"; -@fa-var-eject: "\f052"; -@fa-var-ellipsis-h: "\f141"; -@fa-var-ellipsis-v: "\f142"; -@fa-var-empire: "\f1d1"; -@fa-var-envelope: "\f0e0"; -@fa-var-envelope-o: "\f003"; -@fa-var-envelope-square: "\f199"; -@fa-var-eraser: "\f12d"; -@fa-var-eur: "\f153"; -@fa-var-euro: "\f153"; -@fa-var-exchange: "\f0ec"; -@fa-var-exclamation: "\f12a"; -@fa-var-exclamation-circle: "\f06a"; -@fa-var-exclamation-triangle: "\f071"; -@fa-var-expand: "\f065"; -@fa-var-external-link: "\f08e"; -@fa-var-external-link-square: "\f14c"; -@fa-var-eye: "\f06e"; -@fa-var-eye-slash: "\f070"; -@fa-var-eyedropper: "\f1fb"; -@fa-var-facebook: "\f09a"; -@fa-var-facebook-f: "\f09a"; -@fa-var-facebook-official: "\f230"; -@fa-var-facebook-square: "\f082"; -@fa-var-fast-backward: "\f049"; -@fa-var-fast-forward: "\f050"; -@fa-var-fax: "\f1ac"; -@fa-var-female: "\f182"; -@fa-var-fighter-jet: "\f0fb"; -@fa-var-file: "\f15b"; -@fa-var-file-archive-o: "\f1c6"; -@fa-var-file-audio-o: "\f1c7"; -@fa-var-file-code-o: "\f1c9"; -@fa-var-file-excel-o: "\f1c3"; -@fa-var-file-image-o: "\f1c5"; -@fa-var-file-movie-o: "\f1c8"; -@fa-var-file-o: "\f016"; -@fa-var-file-pdf-o: "\f1c1"; -@fa-var-file-photo-o: "\f1c5"; -@fa-var-file-picture-o: "\f1c5"; -@fa-var-file-powerpoint-o: "\f1c4"; -@fa-var-file-sound-o: "\f1c7"; -@fa-var-file-text: "\f15c"; -@fa-var-file-text-o: "\f0f6"; -@fa-var-file-video-o: "\f1c8"; -@fa-var-file-word-o: "\f1c2"; -@fa-var-file-zip-o: "\f1c6"; -@fa-var-files-o: "\f0c5"; -@fa-var-film: "\f008"; -@fa-var-filter: "\f0b0"; -@fa-var-fire: "\f06d"; -@fa-var-fire-extinguisher: "\f134"; -@fa-var-flag: "\f024"; -@fa-var-flag-checkered: "\f11e"; -@fa-var-flag-o: "\f11d"; -@fa-var-flash: "\f0e7"; -@fa-var-flask: "\f0c3"; -@fa-var-flickr: "\f16e"; -@fa-var-floppy-o: "\f0c7"; -@fa-var-folder: "\f07b"; -@fa-var-folder-o: "\f114"; -@fa-var-folder-open: "\f07c"; -@fa-var-folder-open-o: "\f115"; -@fa-var-font: "\f031"; -@fa-var-forumbee: "\f211"; -@fa-var-forward: "\f04e"; -@fa-var-foursquare: "\f180"; -@fa-var-frown-o: "\f119"; -@fa-var-futbol-o: "\f1e3"; -@fa-var-gamepad: "\f11b"; -@fa-var-gavel: "\f0e3"; -@fa-var-gbp: "\f154"; -@fa-var-ge: "\f1d1"; -@fa-var-gear: "\f013"; -@fa-var-gears: "\f085"; -@fa-var-genderless: "\f1db"; -@fa-var-gift: "\f06b"; -@fa-var-git: "\f1d3"; -@fa-var-git-square: "\f1d2"; -@fa-var-github: "\f09b"; -@fa-var-github-alt: "\f113"; -@fa-var-github-square: "\f092"; -@fa-var-gittip: "\f184"; -@fa-var-glass: "\f000"; -@fa-var-globe: "\f0ac"; -@fa-var-google: "\f1a0"; -@fa-var-google-plus: "\f0d5"; -@fa-var-google-plus-square: "\f0d4"; -@fa-var-google-wallet: "\f1ee"; -@fa-var-graduation-cap: "\f19d"; -@fa-var-gratipay: "\f184"; -@fa-var-group: "\f0c0"; -@fa-var-h-square: "\f0fd"; -@fa-var-hacker-news: "\f1d4"; -@fa-var-hand-o-down: "\f0a7"; -@fa-var-hand-o-left: "\f0a5"; -@fa-var-hand-o-right: "\f0a4"; -@fa-var-hand-o-up: "\f0a6"; -@fa-var-hdd-o: "\f0a0"; -@fa-var-header: "\f1dc"; -@fa-var-headphones: "\f025"; -@fa-var-heart: "\f004"; -@fa-var-heart-o: "\f08a"; -@fa-var-heartbeat: "\f21e"; -@fa-var-history: "\f1da"; -@fa-var-home: "\f015"; -@fa-var-hospital-o: "\f0f8"; -@fa-var-hotel: "\f236"; -@fa-var-html5: "\f13b"; -@fa-var-ils: "\f20b"; -@fa-var-image: "\f03e"; -@fa-var-inbox: "\f01c"; -@fa-var-indent: "\f03c"; -@fa-var-info: "\f129"; -@fa-var-info-circle: "\f05a"; -@fa-var-inr: "\f156"; -@fa-var-instagram: "\f16d"; -@fa-var-institution: "\f19c"; -@fa-var-ioxhost: "\f208"; -@fa-var-italic: "\f033"; -@fa-var-joomla: "\f1aa"; -@fa-var-jpy: "\f157"; -@fa-var-jsfiddle: "\f1cc"; -@fa-var-key: "\f084"; -@fa-var-keyboard-o: "\f11c"; -@fa-var-krw: "\f159"; -@fa-var-language: "\f1ab"; -@fa-var-laptop: "\f109"; -@fa-var-lastfm: "\f202"; -@fa-var-lastfm-square: "\f203"; -@fa-var-leaf: "\f06c"; -@fa-var-leanpub: "\f212"; -@fa-var-legal: "\f0e3"; -@fa-var-lemon-o: "\f094"; -@fa-var-level-down: "\f149"; -@fa-var-level-up: "\f148"; -@fa-var-life-bouy: "\f1cd"; -@fa-var-life-buoy: "\f1cd"; -@fa-var-life-ring: "\f1cd"; -@fa-var-life-saver: "\f1cd"; -@fa-var-lightbulb-o: "\f0eb"; -@fa-var-line-chart: "\f201"; -@fa-var-link: "\f0c1"; -@fa-var-linkedin: "\f0e1"; -@fa-var-linkedin-square: "\f08c"; -@fa-var-linux: "\f17c"; -@fa-var-list: "\f03a"; -@fa-var-list-alt: "\f022"; -@fa-var-list-ol: "\f0cb"; -@fa-var-list-ul: "\f0ca"; -@fa-var-location-arrow: "\f124"; -@fa-var-lock: "\f023"; -@fa-var-long-arrow-down: "\f175"; -@fa-var-long-arrow-left: "\f177"; -@fa-var-long-arrow-right: "\f178"; -@fa-var-long-arrow-up: "\f176"; -@fa-var-magic: "\f0d0"; -@fa-var-magnet: "\f076"; -@fa-var-mail-forward: "\f064"; -@fa-var-mail-reply: "\f112"; -@fa-var-mail-reply-all: "\f122"; -@fa-var-male: "\f183"; -@fa-var-map-marker: "\f041"; -@fa-var-mars: "\f222"; -@fa-var-mars-double: "\f227"; -@fa-var-mars-stroke: "\f229"; -@fa-var-mars-stroke-h: "\f22b"; -@fa-var-mars-stroke-v: "\f22a"; -@fa-var-maxcdn: "\f136"; -@fa-var-meanpath: "\f20c"; -@fa-var-medium: "\f23a"; -@fa-var-medkit: "\f0fa"; -@fa-var-meh-o: "\f11a"; -@fa-var-mercury: "\f223"; -@fa-var-microphone: "\f130"; -@fa-var-microphone-slash: "\f131"; -@fa-var-minus: "\f068"; -@fa-var-minus-circle: "\f056"; -@fa-var-minus-square: "\f146"; -@fa-var-minus-square-o: "\f147"; -@fa-var-mobile: "\f10b"; -@fa-var-mobile-phone: "\f10b"; -@fa-var-money: "\f0d6"; -@fa-var-moon-o: "\f186"; -@fa-var-mortar-board: "\f19d"; -@fa-var-motorcycle: "\f21c"; -@fa-var-music: "\f001"; -@fa-var-navicon: "\f0c9"; -@fa-var-neuter: "\f22c"; -@fa-var-newspaper-o: "\f1ea"; -@fa-var-openid: "\f19b"; -@fa-var-outdent: "\f03b"; -@fa-var-pagelines: "\f18c"; -@fa-var-paint-brush: "\f1fc"; -@fa-var-paper-plane: "\f1d8"; -@fa-var-paper-plane-o: "\f1d9"; -@fa-var-paperclip: "\f0c6"; -@fa-var-paragraph: "\f1dd"; -@fa-var-paste: "\f0ea"; -@fa-var-pause: "\f04c"; -@fa-var-paw: "\f1b0"; -@fa-var-paypal: "\f1ed"; -@fa-var-pencil: "\f040"; -@fa-var-pencil-square: "\f14b"; -@fa-var-pencil-square-o: "\f044"; -@fa-var-phone: "\f095"; -@fa-var-phone-square: "\f098"; -@fa-var-photo: "\f03e"; -@fa-var-picture-o: "\f03e"; -@fa-var-pie-chart: "\f200"; -@fa-var-pied-piper: "\f1a7"; -@fa-var-pied-piper-alt: "\f1a8"; -@fa-var-pinterest: "\f0d2"; -@fa-var-pinterest-p: "\f231"; -@fa-var-pinterest-square: "\f0d3"; -@fa-var-plane: "\f072"; -@fa-var-play: "\f04b"; -@fa-var-play-circle: "\f144"; -@fa-var-play-circle-o: "\f01d"; -@fa-var-plug: "\f1e6"; -@fa-var-plus: "\f067"; -@fa-var-plus-circle: "\f055"; -@fa-var-plus-square: "\f0fe"; -@fa-var-plus-square-o: "\f196"; -@fa-var-power-off: "\f011"; -@fa-var-print: "\f02f"; -@fa-var-puzzle-piece: "\f12e"; -@fa-var-qq: "\f1d6"; -@fa-var-qrcode: "\f029"; -@fa-var-question: "\f128"; -@fa-var-question-circle: "\f059"; -@fa-var-quote-left: "\f10d"; -@fa-var-quote-right: "\f10e"; -@fa-var-ra: "\f1d0"; -@fa-var-random: "\f074"; -@fa-var-rebel: "\f1d0"; -@fa-var-recycle: "\f1b8"; -@fa-var-reddit: "\f1a1"; -@fa-var-reddit-square: "\f1a2"; -@fa-var-refresh: "\f021"; -@fa-var-remove: "\f00d"; -@fa-var-renren: "\f18b"; -@fa-var-reorder: "\f0c9"; -@fa-var-repeat: "\f01e"; -@fa-var-reply: "\f112"; -@fa-var-reply-all: "\f122"; -@fa-var-retweet: "\f079"; -@fa-var-rmb: "\f157"; -@fa-var-road: "\f018"; -@fa-var-rocket: "\f135"; -@fa-var-rotate-left: "\f0e2"; -@fa-var-rotate-right: "\f01e"; -@fa-var-rouble: "\f158"; -@fa-var-rss: "\f09e"; -@fa-var-rss-square: "\f143"; -@fa-var-rub: "\f158"; -@fa-var-ruble: "\f158"; -@fa-var-rupee: "\f156"; -@fa-var-save: "\f0c7"; -@fa-var-scissors: "\f0c4"; -@fa-var-search: "\f002"; -@fa-var-search-minus: "\f010"; -@fa-var-search-plus: "\f00e"; -@fa-var-sellsy: "\f213"; -@fa-var-send: "\f1d8"; -@fa-var-send-o: "\f1d9"; -@fa-var-server: "\f233"; -@fa-var-share: "\f064"; -@fa-var-share-alt: "\f1e0"; -@fa-var-share-alt-square: "\f1e1"; -@fa-var-share-square: "\f14d"; -@fa-var-share-square-o: "\f045"; -@fa-var-shekel: "\f20b"; -@fa-var-sheqel: "\f20b"; -@fa-var-shield: "\f132"; -@fa-var-ship: "\f21a"; -@fa-var-shirtsinbulk: "\f214"; -@fa-var-shopping-cart: "\f07a"; -@fa-var-sign-in: "\f090"; -@fa-var-sign-out: "\f08b"; -@fa-var-signal: "\f012"; -@fa-var-simplybuilt: "\f215"; -@fa-var-sitemap: "\f0e8"; -@fa-var-skyatlas: "\f216"; -@fa-var-skype: "\f17e"; -@fa-var-slack: "\f198"; -@fa-var-sliders: "\f1de"; -@fa-var-slideshare: "\f1e7"; -@fa-var-smile-o: "\f118"; -@fa-var-soccer-ball-o: "\f1e3"; -@fa-var-sort: "\f0dc"; -@fa-var-sort-alpha-asc: "\f15d"; -@fa-var-sort-alpha-desc: "\f15e"; -@fa-var-sort-amount-asc: "\f160"; -@fa-var-sort-amount-desc: "\f161"; -@fa-var-sort-asc: "\f0de"; -@fa-var-sort-desc: "\f0dd"; -@fa-var-sort-down: "\f0dd"; -@fa-var-sort-numeric-asc: "\f162"; -@fa-var-sort-numeric-desc: "\f163"; -@fa-var-sort-up: "\f0de"; -@fa-var-soundcloud: "\f1be"; -@fa-var-space-shuttle: "\f197"; -@fa-var-spinner: "\f110"; -@fa-var-spoon: "\f1b1"; -@fa-var-spotify: "\f1bc"; -@fa-var-square: "\f0c8"; -@fa-var-square-o: "\f096"; -@fa-var-stack-exchange: "\f18d"; -@fa-var-stack-overflow: "\f16c"; -@fa-var-star: "\f005"; -@fa-var-star-half: "\f089"; -@fa-var-star-half-empty: "\f123"; -@fa-var-star-half-full: "\f123"; -@fa-var-star-half-o: "\f123"; -@fa-var-star-o: "\f006"; -@fa-var-steam: "\f1b6"; -@fa-var-steam-square: "\f1b7"; -@fa-var-step-backward: "\f048"; -@fa-var-step-forward: "\f051"; -@fa-var-stethoscope: "\f0f1"; -@fa-var-stop: "\f04d"; -@fa-var-street-view: "\f21d"; -@fa-var-strikethrough: "\f0cc"; -@fa-var-stumbleupon: "\f1a4"; -@fa-var-stumbleupon-circle: "\f1a3"; -@fa-var-subscript: "\f12c"; -@fa-var-subway: "\f239"; -@fa-var-suitcase: "\f0f2"; -@fa-var-sun-o: "\f185"; -@fa-var-superscript: "\f12b"; -@fa-var-support: "\f1cd"; -@fa-var-table: "\f0ce"; -@fa-var-tablet: "\f10a"; -@fa-var-tachometer: "\f0e4"; -@fa-var-tag: "\f02b"; -@fa-var-tags: "\f02c"; -@fa-var-tasks: "\f0ae"; -@fa-var-taxi: "\f1ba"; -@fa-var-tencent-weibo: "\f1d5"; -@fa-var-terminal: "\f120"; -@fa-var-text-height: "\f034"; -@fa-var-text-width: "\f035"; -@fa-var-th: "\f00a"; -@fa-var-th-large: "\f009"; -@fa-var-th-list: "\f00b"; -@fa-var-thumb-tack: "\f08d"; -@fa-var-thumbs-down: "\f165"; -@fa-var-thumbs-o-down: "\f088"; -@fa-var-thumbs-o-up: "\f087"; -@fa-var-thumbs-up: "\f164"; -@fa-var-ticket: "\f145"; -@fa-var-times: "\f00d"; -@fa-var-times-circle: "\f057"; -@fa-var-times-circle-o: "\f05c"; -@fa-var-tint: "\f043"; -@fa-var-toggle-down: "\f150"; -@fa-var-toggle-left: "\f191"; -@fa-var-toggle-off: "\f204"; -@fa-var-toggle-on: "\f205"; -@fa-var-toggle-right: "\f152"; -@fa-var-toggle-up: "\f151"; -@fa-var-train: "\f238"; -@fa-var-transgender: "\f224"; -@fa-var-transgender-alt: "\f225"; -@fa-var-trash: "\f1f8"; -@fa-var-trash-o: "\f014"; -@fa-var-tree: "\f1bb"; -@fa-var-trello: "\f181"; -@fa-var-trophy: "\f091"; -@fa-var-truck: "\f0d1"; -@fa-var-try: "\f195"; -@fa-var-tty: "\f1e4"; -@fa-var-tumblr: "\f173"; -@fa-var-tumblr-square: "\f174"; -@fa-var-turkish-lira: "\f195"; -@fa-var-twitch: "\f1e8"; -@fa-var-twitter: "\f099"; -@fa-var-twitter-square: "\f081"; -@fa-var-umbrella: "\f0e9"; -@fa-var-underline: "\f0cd"; -@fa-var-undo: "\f0e2"; -@fa-var-university: "\f19c"; -@fa-var-unlink: "\f127"; -@fa-var-unlock: "\f09c"; -@fa-var-unlock-alt: "\f13e"; -@fa-var-unsorted: "\f0dc"; -@fa-var-upload: "\f093"; -@fa-var-usd: "\f155"; -@fa-var-user: "\f007"; -@fa-var-user-md: "\f0f0"; -@fa-var-user-plus: "\f234"; -@fa-var-user-secret: "\f21b"; -@fa-var-user-times: "\f235"; -@fa-var-users: "\f0c0"; -@fa-var-venus: "\f221"; -@fa-var-venus-double: "\f226"; -@fa-var-venus-mars: "\f228"; -@fa-var-viacoin: "\f237"; -@fa-var-video-camera: "\f03d"; -@fa-var-vimeo-square: "\f194"; -@fa-var-vine: "\f1ca"; -@fa-var-vk: "\f189"; -@fa-var-volume-down: "\f027"; -@fa-var-volume-off: "\f026"; -@fa-var-volume-up: "\f028"; -@fa-var-warning: "\f071"; -@fa-var-wechat: "\f1d7"; -@fa-var-weibo: "\f18a"; -@fa-var-weixin: "\f1d7"; -@fa-var-whatsapp: "\f232"; -@fa-var-wheelchair: "\f193"; -@fa-var-wifi: "\f1eb"; -@fa-var-windows: "\f17a"; -@fa-var-won: "\f159"; -@fa-var-wordpress: "\f19a"; -@fa-var-wrench: "\f0ad"; -@fa-var-xing: "\f168"; -@fa-var-xing-square: "\f169"; -@fa-var-yahoo: "\f19e"; -@fa-var-yelp: "\f1e9"; -@fa-var-yen: "\f157"; -@fa-var-youtube: "\f167"; -@fa-var-youtube-play: "\f16a"; -@fa-var-youtube-square: "\f166"; - diff --git a/src/UI/Content/Images/background/logo.png b/src/UI/Content/Images/background/logo.png deleted file mode 100644 index dbbe0448f..000000000 Binary files a/src/UI/Content/Images/background/logo.png and /dev/null differ diff --git a/src/UI/Content/Images/cover-dark.png b/src/UI/Content/Images/cover-dark.png deleted file mode 100644 index b8924fb6e..000000000 Binary files a/src/UI/Content/Images/cover-dark.png and /dev/null differ diff --git a/src/UI/Content/Images/favicon-debug.ico b/src/UI/Content/Images/favicon-debug.ico deleted file mode 100644 index 0dfc41a11..000000000 Binary files a/src/UI/Content/Images/favicon-debug.ico and /dev/null differ diff --git a/src/UI/Content/Images/favicon.ico b/src/UI/Content/Images/favicon.ico deleted file mode 100644 index 0dfc41a11..000000000 Binary files a/src/UI/Content/Images/favicon.ico and /dev/null differ diff --git a/src/UI/Content/Images/logos/128.png b/src/UI/Content/Images/logos/128.png deleted file mode 100644 index 7f0f927d1..000000000 Binary files a/src/UI/Content/Images/logos/128.png and /dev/null differ diff --git a/src/UI/Content/Images/logos/32.png b/src/UI/Content/Images/logos/32.png deleted file mode 100644 index 55a6033ef..000000000 Binary files a/src/UI/Content/Images/logos/32.png and /dev/null differ diff --git a/src/UI/Content/Images/logos/48.png b/src/UI/Content/Images/logos/48.png deleted file mode 100644 index 12d6fee53..000000000 Binary files a/src/UI/Content/Images/logos/48.png and /dev/null differ diff --git a/src/UI/Content/Images/logos/64.png b/src/UI/Content/Images/logos/64.png deleted file mode 100644 index 49234e167..000000000 Binary files a/src/UI/Content/Images/logos/64.png and /dev/null differ diff --git a/src/UI/Content/Images/safari/logo.svg b/src/UI/Content/Images/safari/logo.svg deleted file mode 100644 index d3eece392..000000000 --- a/src/UI/Content/Images/safari/logo.svg +++ /dev/null @@ -1 +0,0 @@ -<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" x="0px" y="0px" width="218px" height="218px" viewBox="0 0 218 218" enable-background="new 0 0 218 218" xml:space="preserve"><g display="none"/><g display="none"><path display="inline" fill-rule="evenodd" clip-rule="evenodd" fill="#EFEEEE" d="M217.5 108.95c0 29.833-10.533 55.399-31.6 76.7 -0.7 0.833-1.484 1.6-2.351 2.3 -3.466 3.399-7.134 6.483-11 9.25 -18.267 13.467-39.366 20.2-63.3 20.2 -23.967 0-45.033-6.733-63.2-20.2 -4.8-3.4-9.3-7.25-13.5-11.55 -16.367-16.267-26.417-35.167-30.15-56.7 -0.733-4.2-1.217-8.467-1.45-12.8 -0.1-2.4-0.15-4.801-0.15-7.2 0-2.534 0.05-4.95 0.15-7.25 0-0.233 0.066-0.467 0.2-0.7 1.567-26.6 12.033-49.583 31.4-68.95C53.85 11.017 79.417 0.5 109.25 0.5c29.934 0 55.483 10.517 76.65 31.55C206.967 53.483 217.5 79.117 217.5 108.95z"/></g><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="134.724 60.365 129.7 63.282 129.7 69.117 134.724 72.034 139.802 69.116 139.802 63.282 "/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="139.157 74.994 142.869 71.227 140.087 69.611 135.008 72.529 135.008 78.362 140.087 81.28 143.517 79.289 "/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="145.396 60.366 140.373 63.282 140.373 69.117 143.283 70.807 150.418 63.566 150.418 63.282 "/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="129.415 69.611 124.392 72.528 124.392 78.363 129.415 81.28 134.438 78.363 134.438 72.528 "/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="124.106 60.366 119.084 63.282 119.084 69.117 124.106 72.034 129.129 69.117 129.129 63.282 "/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="113.49 60.366 108.468 63.282 108.468 69.117 113.49 72.034 118.513 69.117 118.513 63.282 "/><path fill-rule="evenodd" clip-rule="evenodd" fill="#010101" d="M113.49 78.857l-0.479 0.278c0.423 0.05 0.843 0.109 1.259 0.176L113.49 78.857z"/><path fill-rule="evenodd" clip-rule="evenodd" fill="#010101" d="M123.821 72.528l-5.022-2.917 -5.023 2.917v5.835l2.188 1.27c-0.001 0-0.003 0-0.004 0 1.372 0.303 2.705 0.701 3.998 1.195 -0.076-0.029-0.152-0.059-0.229-0.087l4.093-2.377V72.528z"/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="150.703 51.175 145.681 54.037 145.681 59.871 150.703 62.787 151.831 62.132 155.726 58.18 155.726 54.037 "/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="182.551 32.684 177.528 35.6 177.528 40.433 184.392 33.753 "/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="163.429 163.159 166.342 166.172 166.342 164.819 "/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="182.551 51.175 177.528 54.037 177.528 59.871 178.193 60.257 185.561 52.89 "/><path fill-rule="evenodd" clip-rule="evenodd" fill="#010101" d="M175 157.7c-5.063-6.93-8.862-14.367-11.396-22.313 0.053 0.166 0.105 0.33 0.159 0.495l-2.159 1.254v5.836l5.022 2.916 0.941-0.546c0.083 0.172 0.166 0.343 0.25 0.515l-0.906 0.525v5.836l5.023 2.916 0.91-0.528c0.105 0.159 0.211 0.316 0.317 0.476l-0.942 0.547v5.834l5.022 2.862 2.8-1.596L175 157.7z"/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="145.681 151.549 145.681 152.219 147.293 153.155 "/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="157.737 153.715 159.54 155.54 157.05 158 157.769 157.305 161.034 160.683 161.034 155.629 "/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="164.581 53.033 161.319 51.175 156.297 54.037 156.297 57.981 157.888 59.548 157.901 59.535 158.597 60.247 157.902 59.533 "/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="158.808 42.894 161.319 44.352 166.342 41.436 166.342 35.6 166.202 35.519 166.62 35.102 166.627 35.106 171.65 32.19 171.65 30.085 172.173 29.564 "/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="186.188 34.796 178.68 42.104 182.551 44.352 187.573 41.436 187.573 35.6 "/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="171.936 32.684 166.912 35.6 166.912 41.436 171.936 44.352 175.817 42.098 176.958 40.988 176.958 35.6 "/><path fill-rule="evenodd" clip-rule="evenodd" fill="#010101" d="M113.205 72.528l-5.022-2.917 -5.022 2.917v5.835l1.447 0.84c-0.001 0-0.002 0-0.002 0 1.457-0.202 2.955-0.304 4.495-0.304 1.005 0 1.991 0.045 2.961 0.13 -0.002 0-0.003 0-0.004 0l1.148-0.667V72.528z"/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="71.026 97.292 66.004 100.209 66.004 106.044 71.026 108.961 76.049 106.044 76.049 100.209 "/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="65.718 106.539 60.639 109.457 60.639 115.29 65.718 118.208 70.741 115.291 70.741 109.456 "/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="71.026 115.785 66.004 118.702 66.004 124.535 71.026 127.397 76.049 124.535 76.049 118.702 "/><path fill-rule="evenodd" clip-rule="evenodd" fill="#010101" d="M79.5 114.5c-0.2-1.166-0.333-2.35-0.4-3.55 -0.033-0.667-0.05-1.333-0.05-2 0-0.456 0.019-0.875 0.033-1.302 -0.004 0.159-0.009 0.318-0.012 0.479l-2.735-1.588 -5.023 2.917v5.835l5.023 2.916 3.52-2.043c0.001 0.006 0.002 0.011 0.004 0.017C79.728 115.625 79.599 115.069 79.5 114.5z"/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="65.718 125.029 60.639 127.893 60.639 133.725 65.718 136.643 70.741 133.726 70.741 127.892 "/><path fill-rule="evenodd" clip-rule="evenodd" fill="#010101" d="M81.174 97.564l-4.554 2.645v5.835l2.467 1.433c0.006-0.171 0.004-0.36 0.013-0.527 0-0.067 0.017-0.134 0.05-0.2 0.192-3.263 0.87-6.329 2.03-9.199C81.178 97.556 81.176 97.56 81.174 97.564z"/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="71.026 134.221 66.004 137.137 66.004 142.973 69.161 144.806 74.729 139.022 76.049 140.294 76.049 137.137 "/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="65.718 88.102 60.639 90.965 60.639 96.797 65.718 99.715 70.741 96.798 70.741 90.964 "/><path fill-rule="evenodd" clip-rule="evenodd" fill="#010101" d="M60.354 78.857l-5.023 2.917v3.562c-0.007-0.021-0.014-0.043-0.021-0.064 0.298 0.951 0.577 1.909 0.838 2.874 -0.008-0.028-0.015-0.056-0.022-0.084l4.229 2.41 5.079-2.864v-5.832L60.354 78.857z"/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="139.802 137.138 134.724 134.22 129.7 137.137 129.7 142.973 134.724 145.89 138.07 143.967 136.979 142.88 139.802 140.048 "/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="76.335 125.03 71.312 127.892 71.312 133.726 76.335 136.643 81.357 133.726 81.357 127.892 "/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="102.875 60.366 97.852 63.282 97.852 69.117 102.875 72.034 107.897 69.117 107.897 63.282 "/><path fill-rule="evenodd" clip-rule="evenodd" fill="#010101" d="M60.068 78.363v-5.835l-5.023-2.917 -5.022 2.917v0.058c-0.099-0.188-0.199-0.374-0.299-0.561l0.014 0.008 5.022-2.917v-5.835l-5.022-2.916 -5.022 2.916v0.68c-0.121-0.171-0.243-0.342-0.365-0.512 4.843 6.734 8.48 13.96 10.913 21.677 -0.163-0.517-0.329-1.032-0.503-1.545v-1.809l-0.815-0.473c-0.106-0.284-0.214-0.566-0.324-0.849l1.425 0.828L60.068 78.363z"/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="61.124 153.154 60.354 152.712 55.331 155.629 55.331 161.463 56.063 161.88 59.78 158.156 57.979 156.422 "/><path fill-rule="evenodd" clip-rule="evenodd" fill="#010101" d="M99.735 80.313c0.003-0.001 0.007-0.002 0.011-0.004C99.743 80.311 99.739 80.312 99.735 80.313z"/><path fill-rule="evenodd" clip-rule="evenodd" fill="#010101" d="M103.72 79.348l-0.845-0.491 -1.861 1.081c0.906-0.244 1.83-0.444 2.771-0.601C103.763 79.341 103.742 79.345 103.72 79.348z"/><path fill-rule="evenodd" clip-rule="evenodd" fill="#010101" d="M97.566 69.611l-5.022 2.917v5.835l4.844 2.813c-0.076 0.032-0.15 0.067-0.226 0.1 0.439-0.189 0.884-0.368 1.332-0.534l4.095-2.378v-5.835L97.566 69.611z"/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="74.331 80.776 74.129 80.979 73.361 80.213 71.026 78.857 66.004 81.774 66.004 87.608 71.026 90.47 76.049 87.608 76.049 81.774 "/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="44.43 161.957 39.407 164.819 39.407 170.653 44.407 173.558 49.452 168.503 49.452 164.819 "/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="44.715 47.496 44.715 50.68 49.738 53.542 50.399 53.166 "/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="39.155 41.949 39.122 41.93 34.1 44.846 34.1 50.68 39.122 53.542 44.145 50.68 44.145 46.926 "/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="187.859 41.929 182.837 44.846 182.837 50.68 185.979 52.471 185.742 52.708 192.939 45.511 192.939 44.847 "/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="161.319 143.467 156.297 146.383 156.297 152.219 161.319 155.135 166.342 152.219 166.342 146.383 "/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="166.627 152.713 161.604 155.629 161.604 161.272 162.015 161.697 166.627 164.325 171.65 161.463 171.65 155.629 "/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="145.396 78.857 144.886 79.153 144.144 79.907 143.938 79.704 140.373 81.774 140.373 87.608 145.396 90.47 150.418 87.608 150.418 81.774 "/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="150.703 143.467 148.671 144.646 155.521 151.471 153.756 153.241 153.762 153.247 155.54 151.49 155.726 151.678 155.726 146.383 "/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="171.936 161.957 166.912 164.819 166.912 166.762 172.934 172.99 176.958 170.653 176.958 164.819 "/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="159.313 60.952 160.967 62.583 161.319 62.787 166.342 59.871 166.342 54.111 "/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="156.012 78.857 150.989 81.774 150.989 87.608 156.012 90.47 161.034 87.608 161.034 81.774 "/><path fill-rule="evenodd" clip-rule="evenodd" fill="#010101" d="M132.188 89.683c-0.002-0.003-0.005-0.005-0.007-0.008 0.826 0.988 1.577 2.005 2.256 3.052v-1.762L132.188 89.683z"/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="150.703 88.102 145.681 90.964 145.681 96.798 150.703 99.715 155.726 96.798 155.726 90.964 "/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="139.004 106.502 139.005 106.513 139.004 106.502 "/><path fill-rule="evenodd" clip-rule="evenodd" fill="#010101" d="M139.802 100.21l-2.347-1.349c-0.001-0.003-0.002-0.006-0.004-0.01 0.844 2.42 1.357 4.972 1.553 7.651l0.798-0.458V100.21zM140.087 106.539l-1.04 0.598c-0.012-0.209-0.027-0.416-0.042-0.623 0.058 0.802 0.095 1.612 0.095 2.437 0 3.003-0.39 5.849-1.16 8.539 0.041-0.148 0.08-0.297 0.12-0.446l2.027 1.165 5.023-2.917v-5.835L140.087 106.539z"/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="44.43 180.395 39.407 183.311 39.407 189.146 44.43 192.063 48.943 189.441 47.571 190.817 49.452 188.932 49.452 183.311 "/><path fill-rule="evenodd" clip-rule="evenodd" fill="#010101" d="M187.999 183.478c0.021-0.022 0.042-0.044 0.063-0.066C188.041 183.434 188.021 183.455 187.999 183.478z"/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="150.703 125.03 145.681 127.893 145.681 133.726 150.703 136.643 155.726 133.726 155.726 127.893 "/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="166.912 169.64 166.912 170.653 169.146 171.95 "/><path fill-rule="evenodd" clip-rule="evenodd" fill="#010101" d="M180.216 162.902l0.246 0.246 -2.934 1.671v5.834l5.022 2.917 5.022-2.917v-0.412l1.755 1.75 -1.469-0.844 -5.022 2.917v5.835l5.023 2.917 1.595-0.916c1.2-1.314 2.361-2.642 3.484-3.981v-2.326L180.216 162.902z"/><path fill-rule="evenodd" clip-rule="evenodd" fill="#010101" d="M188.145 183.311v0.011c0.008-0.008 0.015-0.016 0.021-0.023L188.145 183.311z"/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="63.821 70.701 60.639 72.529 60.639 78.362 65.718 81.28 70.741 78.363 70.741 77.601 "/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="65.718 51.174 60.639 54.038 60.639 59.871 60.789 59.957 61.871 58.872 65.769 62.758 70.741 59.871 70.741 54.037 "/><path fill-rule="evenodd" clip-rule="evenodd" fill="#010101" d="M31.319 34.132l-1.746 1.014c-0.264 0.283-0.522 0.569-0.782 0.854v5.436l5.023 2.917 4.898-2.845L31.319 34.132z"/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="44.715 192.557 44.715 193.681 47.379 191.01 "/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="156.012 134.221 150.989 137.137 150.989 142.973 156.012 145.889 161.034 142.973 161.034 137.137 "/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="167.349 53.131 171.65 50.68 171.65 48.944 "/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="172.221 44.846 172.221 45.599 174.137 43.733 "/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="160.711 63.094 153.397 70.516 156.012 72.034 161.034 69.117 161.034 63.282 "/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="178.255 42.517 172.221 48.39 172.221 50.68 177.243 53.542 182.266 50.68 182.266 44.846 "/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="171.936 51.175 166.912 54.037 166.912 59.871 171.936 62.787 176.958 59.871 176.958 54.037 "/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="166.627 41.93 161.604 44.847 161.604 50.68 165.007 52.619 171.65 46.153 171.65 44.846 "/><path fill-rule="evenodd" clip-rule="evenodd" fill="#010101" d="M134.724 78.857l-5.023 2.917v5.258c-0.003-0.003-0.006-0.005-0.009-0.008 0.22 0.207 0.441 0.41 0.658 0.625 0.23 0.235 0.449 0.474 0.671 0.713 -0.002-0.002-0.003-0.003-0.005-0.005l3.708 2.113 5.078-2.864v-5.832L134.724 78.857z"/><path fill-rule="evenodd" clip-rule="evenodd" fill="#010101" d="M124.106 78.857l-3.693 2.145c3.143 1.263 6.049 3.093 8.716 5.496v-4.724L124.106 78.857z"/><path fill-rule="evenodd" clip-rule="evenodd" fill="#010101" d="M133.052 127.103c0.055-0.071 0.109-0.143 0.164-0.214 -0.847 1.12-1.78 2.206-2.811 3.252l0.002 0.002 -0.058 0.058c-0.199 0.233-0.416 0.45-0.649 0.649l-0.707 0.707 -0.037-0.037c-0.744 0.669-1.506 1.306-2.306 1.881 -0.504 0.371-1.021 0.71-1.54 1.044 0.079-0.056 0.159-0.109 0.239-0.162l4.065 2.36 5.022-2.917v-5.833L133.052 127.103z"/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="152.983 70.936 145.681 78.347 145.681 78.363 150.703 81.28 155.726 78.363 155.726 72.528 "/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="135.008 93.631 135.008 93.631 135.007 93.629 "/><path fill-rule="evenodd" clip-rule="evenodd" fill="#010101" d="M140.087 88.102l-5.079 2.864v2.666c0.859 1.437 1.576 2.931 2.164 4.477 -0.011-0.028-0.021-0.057-0.031-0.085l2.945 1.693 5.024-2.917v-5.834L140.087 88.102z"/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="145.396 97.292 140.373 100.209 140.373 106.044 145.396 108.961 150.418 106.044 150.418 100.209 "/><path fill-rule="evenodd" clip-rule="evenodd" fill="#010101" d="M137.906 117.613c-0.944 3.243-2.448 6.259-4.515 9.046 0.004-0.005 0.007-0.009 0.011-0.014l1.321 0.753 5.078-2.863v-5.833L137.906 117.613z"/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="150.703 106.539 145.681 109.456 145.681 115.291 150.703 118.207 155.726 115.291 155.726 109.456 "/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="145.396 115.785 140.373 118.702 140.373 124.535 145.396 127.397 150.418 124.535 150.418 118.702 "/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="140.087 125.029 135.008 127.893 135.008 133.725 140.087 136.643 145.11 133.726 145.11 127.892 "/><path fill-rule="evenodd" clip-rule="evenodd" fill="#010101" d="M159.8 110c0-1.027 0.018-2.046 0.051-3.06 -0.005 0.153-0.011 0.306-0.015 0.46l-3.539 2.055v5.835l3.809 2.211c0.019 0.232 0.039 0.464 0.06 0.695l-4.153-2.412 -5.022 2.917v5.833l5.022 2.862 4.976-2.836c0.034 0.199 0.068 0.4 0.104 0.599l-4.794 2.731v5.834l5.022 2.917 2.266-1.315c0.004 0.013 0.008 0.025 0.012 0.038C161.065 127.418 159.8 118.963 159.8 110z"/><path fill-rule="evenodd" clip-rule="evenodd" fill="#010101" d="M169.602 70.966l-2.689 1.562v3.698c-0.068 0.148-0.133 0.297-0.2 0.445 0.96-2.113 2.02-4.185 3.18-6.213C169.795 70.628 169.697 70.796 169.602 70.966zM166.658 76.79c0.018-0.04 0.035-0.08 0.054-0.119C166.693 76.71 166.676 76.75 166.658 76.79zM166.658 76.79c-0.105 0.233-0.213 0.467-0.316 0.702v-4.964l-5.022-2.917 -5.022 2.917v5.835l5.022 2.917 4.523-2.627c-0.121 0.288-0.24 0.579-0.358 0.868l-3.88 2.253v5.834l0.979 0.558c-0.051 0.19-0.102 0.38-0.151 0.571l-1.113-0.635 -5.022 2.862v5.834l4.146 2.408c-0.024 0.206-0.049 0.411-0.073 0.617l-4.358-2.531 -5.022 2.917v5.835l5.022 2.917 3.846-2.233c-0.002 0.037-0.002 0.073-0.004 0.11C160.222 96.019 162.491 86.003 166.658 76.79z"/><path fill-rule="evenodd" clip-rule="evenodd" fill="#010101" d="M172.221 63.282v3.363c-0.192 0.296-0.383 0.591-0.57 0.889v-4.252l-5.023-2.916 -5.022 2.916v5.835l5.022 2.917 3.539-2.055c-0.059 0.102-0.116 0.205-0.175 0.307 0.953-1.657 1.972-3.285 3.059-4.885l4.726-4.726 -0.532-0.309L172.221 63.282z"/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="145.396 134.221 140.373 137.137 140.373 139.475 141.921 137.921 148.253 144.229 150.418 142.973 150.418 137.137 "/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="86.951 69.611 81.928 72.528 81.928 78.363 86.951 81.28 91.973 78.363 91.973 72.528 "/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="172.221 175.131 172.221 179.899 177.243 182.815 178.785 181.92 "/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="177.243 171.147 173.342 173.413 180.566 180.886 182.266 179.899 182.266 174.064 "/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="170.6 173.455 166.627 171.147 161.604 174.064 161.604 179.899 166.627 182.815 171.65 179.899 171.65 174.541 "/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="160.136 162.632 156.297 164.819 156.297 170.653 161.319 173.57 166.342 170.653 166.342 169.05 "/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="156.338 158.703 154.56 160.46 150.989 156.844 150.989 161.463 156.012 164.325 159.727 162.208 "/><path fill-rule="evenodd" clip-rule="evenodd" fill="#010101" d="M195.45 43c-2.051-2.664-4.251-5.266-6.585-7.813l-0.721 0.414v5.833l5.079 2.918 2.084-1.21 -0.755 0.755L195.45 43z"/><path fill-rule="evenodd" clip-rule="evenodd" fill="#010101" d="M158.39 43.311l0.146-0.146L158.1 43.6c-13.934 10.733-30.133 16.1-48.6 16.1 -2.231 0-4.431-0.08-6.598-0.238 0.086 0.006 0.172 0.013 0.258 0.019v0.39l5.022 2.916 5.022-2.916v-0.244c0.19-0.008 0.381-0.016 0.57-0.024v0.269l5.023 2.916 5.022-2.916V58.55c0.19-0.032 0.38-0.064 0.57-0.097v1.418l5.023 2.916 5.022-2.916v-3.853c0.189-0.06 0.381-0.121 0.57-0.182v4.034l5.079 2.917 5.023-2.917v-5.834l-2.209-1.258c0.213-0.097 0.426-0.195 0.638-0.293l1.856 1.058 5.022-2.862v-1.843c0.19-0.114 0.381-0.229 0.571-0.345v2.188l5.022 2.862 5.022-2.862v-5.834L158.39 43.311z"/><path fill-rule="evenodd" clip-rule="evenodd" fill="#010101" d="M177.4 24.35l-5.18 5.166v2.674l5.022 2.916 5.022-2.916v-3.614C180.664 27.105 179.044 25.692 177.4 24.35z"/><path fill-rule="evenodd" clip-rule="evenodd" fill="#010101" d="M61.9 44.35l-2.099-2.099 -4.47 2.595v5.834l5.023 2.863 5.079-2.864v-3.851c0.19 0.125 0.38 0.25 0.571 0.375v3.477l5.022 2.862 3.063-1.745c0.207 0.102 0.415 0.202 0.623 0.302l-3.4 1.938v5.834l5.023 2.916 5.022-2.916v-4.97c0.189 0.069 0.38 0.139 0.571 0.207v4.763l5.022 2.916 5.022-2.916v-1.935c0.191 0.04 0.379 0.079 0.571 0.117v1.818l5.022 2.916 5.023-2.916v-0.432c0.084 0.006 0.168 0.012 0.252 0.019C87.614 58.334 73.967 53.298 61.9 44.35z"/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="193.51 44.846 193.51 44.94 193.733 44.717 "/><path fill-rule="evenodd" clip-rule="evenodd" fill="#010101" d="M185.9 32.05c-1.012-1.011-2.034-1.986-3.063-2.943v3.083l1.979 1.15 1.086-1.057 1.395 1.434 -0.684 0.666 1.246 0.724 0.615-0.354C187.635 33.844 186.777 32.943 185.9 32.05z"/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="108.183 143.467 103.16 146.383 103.16 152.219 108.183 155.135 113.205 152.219 113.205 146.383 "/><path fill-rule="evenodd" clip-rule="evenodd" fill="#010101" d="M109.1 139c-0.341 0-0.671-0.026-1.008-0.036 0.125 0.005 0.25 0.011 0.376 0.014v3.995l5.022 2.916 5.022-2.916v-5.391c0.016-0.005 0.03-0.012 0.046-0.017C115.605 138.52 112.453 139 109.1 139z"/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="118.799 143.467 113.775 146.383 113.775 152.219 118.799 155.135 123.821 152.219 123.821 146.383 "/><path fill-rule="evenodd" clip-rule="evenodd" fill="#010101" d="M98.096 136.994l-0.244 0.143v5.836l5.023 2.916 5.022-2.916v-4.016C104.402 138.835 101.133 138.185 98.096 136.994z"/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="134.724 152.712 129.7 155.629 129.7 161.463 134.724 164.326 139.802 161.462 139.802 155.63 "/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="138.489 144.384 135.008 146.384 135.008 152.218 140.087 155.135 145.11 152.219 145.11 150.98 "/><path fill-rule="evenodd" clip-rule="evenodd" fill="#010101" d="M124.824 134.638c-1.817 1.14-3.734 2.043-5.74 2.735v5.6l5.022 2.916 5.022-2.916v-5.836L124.824 134.638z"/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="148.88 154.736 145.396 152.713 140.373 155.629 140.373 161.463 145.396 164.325 150.418 161.463 150.418 156.269 "/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="129.415 143.467 124.392 146.383 124.392 152.219 129.415 155.135 134.438 152.219 134.438 146.383 "/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="52.001 34.452 50.023 35.6 50.023 41.436 55.045 44.352 59.383 41.833 "/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="161.319 180.395 161.208 180.458 166.342 185.592 166.342 183.311 "/><path fill-rule="evenodd" clip-rule="evenodd" fill="#010101" d="M156.012 171.147l-3.688 2.143c-0.181-0.115-0.363-0.23-0.545-0.345l3.947-2.292v-5.834l-5.022-2.862 -5.022 2.862v4.685c-0.189-0.096-0.38-0.191-0.57-0.285v-4.399l-5.023-2.862 -5.079 2.863v0.293c-0.189-0.061-0.381-0.122-0.57-0.182v-0.112l-5.022-2.862 -1.981 1.129c-0.28-0.06-0.563-0.118-0.844-0.176l2.539-1.447v-5.834l-5.022-2.916 -5.022 2.916v5.834l0.633 0.361c-0.011-0.001-0.022-0.002-0.033-0.004 14.299 1.64 27.104 6.815 38.416 15.529l2.691 2.691 0.243-0.142v-5.835L156.012 171.147z"/><path fill-rule="evenodd" clip-rule="evenodd" fill="#010101" d="M118.168 161.66l0.345-0.197v-5.834l-5.022-2.916 -5.022 2.916v5.627c-0.19 0.002-0.38 0.005-0.57 0.008v-5.635l-5.022-2.916 -5.023 2.916v5.834l0.776 0.442c-0.314 0.039-0.627 0.079-0.939 0.121l-0.122-0.069 -0.198 0.113c-0.245 0.034-0.488 0.073-0.732 0.109 4.166-0.617 8.453-0.93 12.864-0.93 3.433 0 6.786 0.187 10.062 0.558C119.099 161.755 118.634 161.705 118.168 161.66z"/><path fill-rule="evenodd" clip-rule="evenodd" fill="#010101" d="M67.759 172.57c0.415-0.252 0.832-0.499 1.25-0.742C68.591 172.071 68.174 172.318 67.759 172.57z"/><path fill-rule="evenodd" clip-rule="evenodd" fill="#010101" d="M65.4 174.046l-5.045-2.898 -5.023 2.917v5.835l1.981 1.15 -0.417 0.418 -1.85-1.073 -5.022 2.916v5.048l-0.326 0.327L61.8 176.55c1.433-1.054 2.893-2.04 4.369-2.984C65.914 173.729 65.654 173.879 65.4 174.046z"/><path fill-rule="evenodd" clip-rule="evenodd" fill="#010101" d="M182.551 180.395l-1.575 0.914 4.443 4.596 -1.438 1.391 -4.787-4.951 -1.666 0.967v5.836l2.934 1.703c1.044-0.94 2.076-1.901 3.088-2.899 0.866-0.7 1.65-1.467 2.351-2.3 0.567-0.574 1.121-1.152 1.673-1.731v-0.608L182.551 180.395z"/><path fill-rule="evenodd" clip-rule="evenodd" fill="#010101" d="M193.51 176.161v1.075c0.162-0.195 0.33-0.39 0.49-0.586L193.51 176.161z"/><path fill-rule="evenodd" clip-rule="evenodd" fill="#010101" d="M39.122 189.641l-1.48 0.859c1.818 1.626 3.683 3.185 5.608 4.65l0.895-0.896v-1.697L39.122 189.641z"/><path fill-rule="evenodd" clip-rule="evenodd" fill="#010101" d="M177.243 189.641l-4.336 2.517 2.743 2.743c1.486-1.16 2.94-2.38 4.368-3.649L177.243 189.641z"/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="97.566 143.467 92.544 146.383 92.544 152.219 97.566 155.135 102.589 152.219 102.589 146.383 "/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="171.936 180.395 166.912 183.311 166.912 186.162 172.49 191.74 176.958 189.146 176.958 183.311 "/><path fill-rule="evenodd" clip-rule="evenodd" fill="#010101" d="M41.55 24c-0.044 0.036-0.088 0.074-0.132 0.11l0.58 0.337L41.55 24z"/><path fill-rule="evenodd" clip-rule="evenodd" fill="#010101" d="M43.57 26.021L40.951 24.5c-2.322 1.917-4.606 3.939-6.852 6.068v1.622l5.022 2.916 5.022-2.916v-5.595L43.57 26.021z"/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="44.715 27.166 44.715 32.19 49.738 35.106 51.584 34.034 "/><path fill-rule="evenodd" clip-rule="evenodd" fill="#010101" d="M32.55 32.05c-0.536 0.536-1.055 1.081-1.578 1.624l0.37-0.215 1.064-1.067 0.285 0.284 0.838-0.486v-1.076C33.203 31.426 32.875 31.733 32.55 32.05z"/><path fill-rule="evenodd" clip-rule="evenodd" fill="#010101" d="M29.823 171.913L24.7 177.05c1.479 1.784 3.036 3.54 4.652 5.274l4.176-2.425v-5.835L29.823 171.913z"/><path fill-rule="evenodd" clip-rule="evenodd" fill="#010101" d="M37.988 182.818l-4.126 4.134c1.09 1.081 2.206 2.123 3.335 3.146l1.64-0.951v-5.836L37.988 182.818z"/><path fill-rule="evenodd" clip-rule="evenodd" fill="#010101" d="M33.814 180.395l-4.062 2.357c0.88 0.937 1.772 1.868 2.692 2.79l3.755-3.763L33.814 180.395z"/><path fill-rule="evenodd" clip-rule="evenodd" fill="#010101" d="M30.321 34.344c-0.134 0.141-0.268 0.282-0.4 0.423C30.054 34.625 30.188 34.485 30.321 34.344z"/><path fill-rule="evenodd" clip-rule="evenodd" fill="#010101" d="M28.22 36.623c-1.713 1.903-3.359 3.843-4.92 5.827l1.282 1.279 -0.115-0.115 3.753-2.179V36.623z"/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="71.026 60.366 66.188 63.175 73.583 70.549 76.049 69.117 76.049 63.282 "/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="55.045 161.957 50.023 164.819 50.023 167.931 55.645 162.299 "/><path fill-rule="evenodd" clip-rule="evenodd" fill="#010101" d="M46.365 154.153c-0.026 0.041-0.052 0.081-0.078 0.121C46.313 154.234 46.339 154.194 46.365 154.153zM55.045 136.643l5.023-2.917v-5.834l-2.624-1.495c0.009-0.049 0.018-0.098 0.027-0.146 -1.89 10.016-5.593 19.316-11.107 27.903 0.049-0.076 0.1-0.152 0.148-0.229l2.939-1.706v-3.293c0.193-0.357 0.384-0.717 0.571-1.077v4.37l5.022 2.916 5.023-2.916v-5.836l-5.023-2.916 -3.985 2.314c0.146-0.303 0.291-0.607 0.433-0.912l3.268-1.896v-5.836l-0.126-0.073c0.063-0.185 0.124-0.367 0.186-0.552L55.045 136.643zM38.019 163.696l1.104 0.629 5.022-2.862v-4.024c0.192-0.27 0.382-0.54 0.571-0.812v4.836l5.022 2.862 5.022-2.862v-5.834l-5.022-2.916 -3.911 2.271c0.155-0.236 0.307-0.473 0.459-0.709 -0.777 1.206-1.588 2.397-2.437 3.575L38.019 163.696z"/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="39.407 58.519 39.407 58.519 38.896 58.008 "/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="44.43 51.175 39.407 54.037 39.407 58.519 42.65 61.754 44.43 62.787 49.452 59.871 49.452 54.037 "/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="50.818 53.583 50.023 54.037 50.023 59.871 55.045 62.787 58.206 60.953 "/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="56.929 63.829 59.003 61.748 58.624 61.37 55.331 63.282 55.331 69.117 60.354 72.034 63.402 70.283 "/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="71.312 78.17 71.312 78.363 71.776 78.633 "/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="92.258 134.221 87.236 137.137 87.236 142.973 92.258 145.889 97.281 142.973 97.281 137.137 "/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="81.643 152.713 76.62 155.629 76.62 161.463 81.643 164.325 86.665 161.463 86.665 155.629 "/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="86.951 143.467 81.928 146.383 81.928 152.219 86.951 155.135 91.973 152.219 91.973 146.383 "/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="78.793 144.895 71.588 152.379 76.335 155.135 81.357 152.219 81.357 146.383 "/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="71.18 152.803 66.004 158.18 66.004 161.463 71.026 164.325 76.049 161.463 76.049 155.629 "/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="65.718 143.466 60.639 146.384 60.639 152.218 61.532 152.73 68.754 145.229 "/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="60.354 134.22 55.331 137.137 55.331 142.973 60.354 145.89 65.433 142.972 65.433 137.138 "/><path fill-rule="evenodd" clip-rule="evenodd" fill="#010101" d="M60.354 115.784l-1.648 0.958c0.002-0.028 0.004-0.057 0.006-0.085 -0.236 3.253-0.646 6.437-1.232 9.552 0.025-0.135 0.051-0.271 0.076-0.406l2.799 1.596 5.079-2.863v-5.833L60.354 115.784z"/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="81.643 134.221 76.62 137.137 76.62 140.844 79.771 143.878 79.201 144.471 81.643 145.889 86.665 142.973 86.665 137.137 "/><path fill-rule="evenodd" clip-rule="evenodd" fill="#010101" d="M92.258 78.857l-5.022 2.917v5.834l0.423 0.241c0.064-0.066 0.125-0.134 0.191-0.199 2.713-2.682 5.679-4.74 8.892-6.188L92.258 78.857z"/><path fill-rule="evenodd" clip-rule="evenodd" fill="#010101" d="M79.983 116.749l-3.363 1.953v5.833l5.022 2.862 2.593-1.478c-1.984-2.82-3.409-5.875-4.253-9.173C79.982 116.747 79.983 116.748 79.983 116.749zM84.235 125.92c0.068 0.099 0.142 0.194 0.212 0.292 -0.068-0.099-0.136-0.198-0.205-0.296L84.235 125.92zM91.6 133.4c-1.333-0.934-2.583-2-3.75-3.2 -1.278-1.269-2.402-2.603-3.402-3.988 0.041 0.059 0.08 0.117 0.122 0.175l-2.641 1.505v5.834l5.022 2.917 5.022-2.917v-0.071C91.85 133.566 91.722 133.491 91.6 133.4z"/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="81.643 78.857 76.62 81.774 76.62 87.608 81.643 90.47 86.665 87.608 86.665 81.774 "/><path fill-rule="evenodd" clip-rule="evenodd" fill="#010101" d="M97.281 155.629l-5.022-2.916 -5.022 2.916v5.834l3.271 1.864c-0.085 0.02-0.17 0.04-0.255 0.06 1.974-0.458 3.978-0.841 6.011-1.151 -0.151 0.022-0.303 0.044-0.454 0.067l1.473-0.84V155.629z"/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="76.335 88.102 71.312 90.964 71.312 96.798 76.335 99.715 81.357 96.798 81.357 90.964 "/><path fill-rule="evenodd" clip-rule="evenodd" fill="#010101" d="M86.951 88.102l-5.022 2.862v4.912c1.315-2.72 3.09-5.254 5.323-7.603L86.951 88.102z"/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="49.738 171.147 49.498 171.287 44.715 176.078 44.715 179.899 49.738 182.815 54.76 179.899 54.76 174.064 "/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="39.122 171.147 34.1 174.064 34.1 179.899 36.617 181.361 43.99 173.975 "/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="38.406 182.399 39.122 182.815 44.145 179.899 44.145 176.65 "/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="32.688 51.816 32.688 51.816 32.383 51.512 "/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="33.529 50.68 33.529 44.846 28.506 41.93 24.886 44.032 32.383 51.512 32.269 51.398 "/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="38.836 54.037 33.814 51.175 32.688 51.816 38.836 57.95 "/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="37.601 164.115 30.24 171.495 33.814 173.57 38.836 170.653 38.836 164.819 "/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="57.445 163.324 50.091 170.692 55.045 173.57 60.068 170.653 60.068 164.819 "/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="65.718 161.957 60.639 164.82 60.639 170.652 65.718 173.57 70.741 170.653 70.741 164.819 "/><path fill-rule="evenodd" clip-rule="evenodd" fill="#010101" d="M89.689 163.519l-2.739-1.562 -5.022 2.862v0.997c-0.191 0.067-0.38 0.136-0.571 0.205v-1.202l-5.022-2.862 -5.023 2.862v5.704c-0.228 0.123-0.451 0.259-0.678 0.384 6.138-3.381 12.626-5.884 19.474-7.486C89.968 163.453 89.829 163.485 89.689 163.519z"/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="63.021 161.277 60.5 158.85 61.208 159.556 57.863 162.906 60.354 164.326 65.433 161.462 65.433 158.772 "/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="81.643 60.366 76.62 63.282 76.62 69.117 81.643 72.034 86.665 69.117 86.665 63.282 "/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="76.335 69.611 74.001 70.966 79.071 76.021 74.748 80.358 76.335 81.28 81.357 78.363 81.357 72.528 "/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="92.258 60.366 87.236 63.282 87.236 69.117 92.258 72.034 97.281 69.117 97.281 63.282 "/><path fill-rule="evenodd" clip-rule="evenodd" fill="#010101" d="M96.715 136.402c-0.133-0.061-0.266-0.122-0.398-0.185C96.449 136.28 96.582 136.342 96.715 136.402z"/><path fill-rule="evenodd" clip-rule="evenodd" fill="#010101" d="M95.012 135.559c-0.099-0.054-0.195-0.111-0.292-0.166C94.817 135.447 94.914 135.505 95.012 135.559z"/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="44.43 32.684 39.407 35.6 39.407 39.376 44.348 44.305 44.43 44.352 49.452 41.436 49.452 35.6 "/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="33.814 32.684 33.109 33.093 38.836 38.807 38.836 35.6 "/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="49.738 41.93 44.826 44.782 52.201 52.139 54.76 50.68 54.76 44.846 "/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="55.045 51.175 52.621 52.557 59.996 59.914 60.068 59.871 60.068 54.037 "/><path fill-rule="evenodd" clip-rule="evenodd" fill="#010101" d="M60.354 97.292l-2.138 1.241c-0.027-0.204-0.054-0.409-0.083-0.612l1.934-1.123v-5.834l-3.736-2.129c-0.055-0.21-0.111-0.419-0.167-0.629 1.856 6.886 2.786 14.15 2.786 21.793 0 2.23-0.079 4.43-0.234 6.599 0.013-0.182 0.026-0.363 0.038-0.545l1.314-0.763v-5.835l-1.126-0.654c-0.003-0.222-0.006-0.445-0.011-0.667l1.423 0.826 5.079-2.917v-5.834L60.354 97.292z"/></svg> \ No newline at end of file diff --git a/src/UI/Content/Images/touch/114.png b/src/UI/Content/Images/touch/114.png deleted file mode 100644 index e19a08155..000000000 Binary files a/src/UI/Content/Images/touch/114.png and /dev/null differ diff --git a/src/UI/Content/Images/touch/144.png b/src/UI/Content/Images/touch/144.png deleted file mode 100644 index 99fc3a98e..000000000 Binary files a/src/UI/Content/Images/touch/144.png and /dev/null differ diff --git a/src/UI/Content/Images/touch/57.png b/src/UI/Content/Images/touch/57.png deleted file mode 100644 index 9f84befd6..000000000 Binary files a/src/UI/Content/Images/touch/57.png and /dev/null differ diff --git a/src/UI/Content/Images/touch/72.png b/src/UI/Content/Images/touch/72.png deleted file mode 100644 index 6b6808caa..000000000 Binary files a/src/UI/Content/Images/touch/72.png and /dev/null differ diff --git a/src/UI/Content/Messenger/messenger.css b/src/UI/Content/Messenger/messenger.css deleted file mode 100644 index 9fc58c936..000000000 --- a/src/UI/Content/Messenger/messenger.css +++ /dev/null @@ -1,101 +0,0 @@ -/* line 4, ../../src/sass/messenger.sass */ -ul.messenger { - margin: 0; - padding: 0; -} -/* line 8, ../../src/sass/messenger.sass */ -ul.messenger > li { - list-style: none; - margin: 0; - padding: 0; -} -/* line 14, ../../src/sass/messenger.sass */ -ul.messenger.messenger-empty { - display: none; -} -/* line 17, ../../src/sass/messenger.sass */ -ul.messenger .messenger-message { - overflow: hidden; - *zoom: 1; -} -/* line 20, ../../src/sass/messenger.sass */ -ul.messenger .messenger-message.messenger-hidden { - display: none; -} -/* line 23, ../../src/sass/messenger.sass */ -ul.messenger .messenger-message .messenger-phrase, ul.messenger .messenger-message .messenger-actions a { - padding-right: 5px; -} -/* line 26, ../../src/sass/messenger.sass */ -ul.messenger .messenger-message .messenger-actions { - float: right; -} -/* line 29, ../../src/sass/messenger.sass */ -ul.messenger .messenger-message .messenger-actions a { - cursor: pointer; - text-decoration: underline; -} -/* line 33, ../../src/sass/messenger.sass */ -ul.messenger .messenger-message ul, ul.messenger .messenger-message ol { - margin: 10px 18px 0; -} -/* line 36, ../../src/sass/messenger.sass */ -ul.messenger.messenger-fixed { - position: fixed; - z-index: 10000; -} -/* line 40, ../../src/sass/messenger.sass */ -ul.messenger.messenger-fixed .messenger-message { - min-width: 0; - -webkit-box-sizing: border-box; - -moz-box-sizing: border-box; - box-sizing: border-box; -} -/* line 45, ../../src/sass/messenger.sass */ -ul.messenger.messenger-fixed .message .messenger-actions { - float: left; -} -/* line 48, ../../src/sass/messenger.sass */ -ul.messenger.messenger-fixed.messenger-on-top { - top: 20px; -} -/* line 51, ../../src/sass/messenger.sass */ -ul.messenger.messenger-fixed.messenger-on-bottom { - bottom: 20px; -} -/* line 54, ../../src/sass/messenger.sass */ -ul.messenger.messenger-fixed.messenger-on-top, ul.messenger.messenger-fixed.messenger-on-bottom { - left: 50%; - width: 800px; - margin-left: -400px; -} -@media (max-width: 960px) { - /* line 54, ../../src/sass/messenger.sass */ - ul.messenger.messenger-fixed.messenger-on-top, ul.messenger.messenger-fixed.messenger-on-bottom { - left: 10%; - width: 80%; - margin-left: 0px; - } -} -/* line 64, ../../src/sass/messenger.sass */ -ul.messenger.messenger-fixed.messenger-on-top.messenger-on-right, ul.messenger.messenger-fixed.messenger-on-bottom.messenger-on-right { - right: 20px; - left: auto; -} -/* line 68, ../../src/sass/messenger.sass */ -ul.messenger.messenger-fixed.messenger-on-top.messenger-on-left, ul.messenger.messenger-fixed.messenger-on-bottom.messenger-on-left { - left: 20px; - margin-left: 0px; -} -/* line 72, ../../src/sass/messenger.sass */ -ul.messenger.messenger-fixed.messenger-on-right, ul.messenger.messenger-fixed.messenger-on-left { - width: 350px; -} -/* line 75, ../../src/sass/messenger.sass */ -ul.messenger.messenger-fixed.messenger-on-right .messenger-actions, ul.messenger.messenger-fixed.messenger-on-left .messenger-actions { - float: left; -} -/* line 78, ../../src/sass/messenger.sass */ -ul.messenger .messenger-spinner { - display: none; -} diff --git a/src/UI/Content/Messenger/messenger.flat.css b/src/UI/Content/Messenger/messenger.flat.css deleted file mode 100644 index df8d35aeb..000000000 --- a/src/UI/Content/Messenger/messenger.flat.css +++ /dev/null @@ -1,462 +0,0 @@ -@-webkit-keyframes ui-spinner-rotate-right { - /* line 64, ../../src/sass/messenger-spinner.scss */ - 0% { - -webkit-transform: rotate(0deg); - } - - /* line 65, ../../src/sass/messenger-spinner.scss */ - 25% { - -webkit-transform: rotate(180deg); - } - - /* line 66, ../../src/sass/messenger-spinner.scss */ - 50% { - -webkit-transform: rotate(180deg); - } - - /* line 67, ../../src/sass/messenger-spinner.scss */ - 75% { - -webkit-transform: rotate(360deg); - } - - /* line 68, ../../src/sass/messenger-spinner.scss */ - 100% { - -webkit-transform: rotate(360deg); - } -} - -@-webkit-keyframes ui-spinner-rotate-left { - /* line 72, ../../src/sass/messenger-spinner.scss */ - 0% { - -webkit-transform: rotate(0deg); - } - - /* line 73, ../../src/sass/messenger-spinner.scss */ - 25% { - -webkit-transform: rotate(0deg); - } - - /* line 74, ../../src/sass/messenger-spinner.scss */ - 50% { - -webkit-transform: rotate(180deg); - } - - /* line 75, ../../src/sass/messenger-spinner.scss */ - 75% { - -webkit-transform: rotate(180deg); - } - - /* line 76, ../../src/sass/messenger-spinner.scss */ - 100% { - -webkit-transform: rotate(360deg); - } -} - -@-moz-keyframes ui-spinner-rotate-right { - /* line 80, ../../src/sass/messenger-spinner.scss */ - 0% { - -moz-transform: rotate(0deg); - } - - /* line 81, ../../src/sass/messenger-spinner.scss */ - 25% { - -moz-transform: rotate(180deg); - } - - /* line 82, ../../src/sass/messenger-spinner.scss */ - 50% { - -moz-transform: rotate(180deg); - } - - /* line 83, ../../src/sass/messenger-spinner.scss */ - 75% { - -moz-transform: rotate(360deg); - } - - /* line 84, ../../src/sass/messenger-spinner.scss */ - 100% { - -moz-transform: rotate(360deg); - } -} - -@-moz-keyframes ui-spinner-rotate-left { - /* line 88, ../../src/sass/messenger-spinner.scss */ - 0% { - -moz-transform: rotate(0deg); - } - - /* line 89, ../../src/sass/messenger-spinner.scss */ - 25% { - -moz-transform: rotate(0deg); - } - - /* line 90, ../../src/sass/messenger-spinner.scss */ - 50% { - -moz-transform: rotate(180deg); - } - - /* line 91, ../../src/sass/messenger-spinner.scss */ - 75% { - -moz-transform: rotate(180deg); - } - - /* line 92, ../../src/sass/messenger-spinner.scss */ - 100% { - -moz-transform: rotate(360deg); - } -} - -@keyframes ui-spinner-rotate-right { - /* line 96, ../../src/sass/messenger-spinner.scss */ - 0% { - transform: rotate(0deg); - } - - /* line 97, ../../src/sass/messenger-spinner.scss */ - 25% { - transform: rotate(180deg); - } - - /* line 98, ../../src/sass/messenger-spinner.scss */ - 50% { - transform: rotate(180deg); - } - - /* line 99, ../../src/sass/messenger-spinner.scss */ - 75% { - transform: rotate(360deg); - } - - /* line 100, ../../src/sass/messenger-spinner.scss */ - 100% { - transform: rotate(360deg); - } -} - -@keyframes ui-spinner-rotate-left { - /* line 104, ../../src/sass/messenger-spinner.scss */ - 0% { - transform: rotate(0deg); - } - - /* line 105, ../../src/sass/messenger-spinner.scss */ - 25% { - transform: rotate(0deg); - } - - /* line 106, ../../src/sass/messenger-spinner.scss */ - 50% { - transform: rotate(180deg); - } - - /* line 107, ../../src/sass/messenger-spinner.scss */ - 75% { - transform: rotate(180deg); - } - - /* line 108, ../../src/sass/messenger-spinner.scss */ - 100% { - transform: rotate(360deg); - } -} - -/* line 116, ../../src/sass/messenger-spinner.scss */ -.messenger-spinner { - position: relative; - border-radius: 100%; -} -/* line 120, ../../src/sass/messenger-spinner.scss */ -ul.messenger.messenger-spinner-active .messenger-spinner .messenger-spinner { - display: block; -} -/* line 124, ../../src/sass/messenger-spinner.scss */ -.messenger-spinner .messenger-spinner-side { - width: 50%; - height: 100%; - overflow: hidden; - position: absolute; -} -/* line 130, ../../src/sass/messenger-spinner.scss */ -.messenger-spinner .messenger-spinner-side .messenger-spinner-fill { - border-radius: 999px; - position: absolute; - width: 100%; - height: 100%; - -webkit-animation-iteration-count: infinite; - -moz-animation-iteration-count: infinite; - -ms-animation-iteration-count: infinite; - -o-animation-iteration-count: infinite; - animation-iteration-count: infinite; - -webkit-animation-timing-function: linear; - -moz-animation-timing-function: linear; - -ms-animation-timing-function: linear; - -o-animation-timing-function: linear; - animation-timing-function: linear; -} -/* line 140, ../../src/sass/messenger-spinner.scss */ -.messenger-spinner .messenger-spinner-side-left { - left: 0; -} -/* line 143, ../../src/sass/messenger-spinner.scss */ -.messenger-spinner .messenger-spinner-side-left .messenger-spinner-fill { - left: 100%; - border-top-left-radius: 0; - border-bottom-left-radius: 0; - -webkit-animation-name: ui-spinner-rotate-left; - -moz-animation-name: ui-spinner-rotate-left; - -ms-animation-name: ui-spinner-rotate-left; - -o-animation-name: ui-spinner-rotate-left; - animation-name: ui-spinner-rotate-left; - -webkit-transform-origin: 0 50%; - -moz-transform-origin: 0 50%; - -ms-transform-origin: 0 50%; - -o-transform-origin: 0 50%; - transform-origin: 0 50%; -} -/* line 152, ../../src/sass/messenger-spinner.scss */ -.messenger-spinner .messenger-spinner-side-right { - left: 50%; -} -/* line 155, ../../src/sass/messenger-spinner.scss */ -.messenger-spinner .messenger-spinner-side-right .messenger-spinner-fill { - left: -100%; - border-top-right-radius: 0; - border-bottom-right-radius: 0; - -webkit-animation-name: ui-spinner-rotate-right; - -moz-animation-name: ui-spinner-rotate-right; - -ms-animation-name: ui-spinner-rotate-right; - -o-animation-name: ui-spinner-rotate-right; - animation-name: ui-spinner-rotate-right; - -webkit-transform-origin: 100% 50%; - -moz-transform-origin: 100% 50%; - -ms-transform-origin: 100% 50%; - -o-transform-origin: 100% 50%; - transform-origin: 100% 50%; -} - -/* line 15, ../../src/sass/messenger-theme-flat.sass */ -ul.messenger-theme-flat { - -webkit-border-radius: 4px; - -moz-border-radius: 4px; - -ms-border-radius: 4px; - -o-border-radius: 4px; - border-radius: 4px; - -moz-user-select: none; - -webkit-user-select: none; - -o-user-select: none; - user-select: none; - background: #404040; -} -/* line 20, ../../src/sass/messenger-theme-flat.sass */ -ul.messenger-theme-flat.messenger-empty { - display: none; -} -/* line 23, ../../src/sass/messenger-theme-flat.sass */ -ul.messenger-theme-flat .messenger-message { - -webkit-box-shadow: inset 0px 1px rgba(255, 255, 255, 0.13), inset 48px 0px 0px #292929; - -moz-box-shadow: inset 0px 1px rgba(255, 255, 255, 0.13), inset 48px 0px 0px #292929; - box-shadow: inset 0px 1px rgba(255, 255, 255, 0.13), inset 48px 0px 0px #292929; - -webkit-border-radius: 0px; - -moz-border-radius: 0px; - -ms-border-radius: 0px; - -o-border-radius: 0px; - border-radius: 0px; - position: relative; - border: 0px; - margin-bottom: 0px; - font-size: 13px; - background: transparent; - color: #f0f0f0; - font-weight: 500; - padding: 10px 30px 13px 65px; -} -/* line 35, ../../src/sass/messenger-theme-flat.sass */ -ul.messenger-theme-flat .messenger-message .messenger-close { - position: absolute; - top: 0px; - right: 0px; - color: #888888; - opacity: 1; - font-weight: bold; - display: block; - font-size: 20px; - line-height: 20px; - padding: 8px 10px 7px 7px; - cursor: pointer; - background: transparent; - border: 0; - -webkit-appearance: none; -} -/* line 51, ../../src/sass/messenger-theme-flat.sass */ -ul.messenger-theme-flat .messenger-message .messenger-close:hover { - color: #bbbbbb; -} -/* line 54, ../../src/sass/messenger-theme-flat.sass */ -ul.messenger-theme-flat .messenger-message .messenger-close:active { - color: #777777; -} -/* line 57, ../../src/sass/messenger-theme-flat.sass */ -ul.messenger-theme-flat .messenger-message .messenger-actions { - float: none; - margin-top: 10px; -} -/* line 61, ../../src/sass/messenger-theme-flat.sass */ -ul.messenger-theme-flat .messenger-message .messenger-actions a { - -webkit-border-radius: 4px; - -moz-border-radius: 4px; - -ms-border-radius: 4px; - -o-border-radius: 4px; - border-radius: 4px; - text-decoration: none; - color: #aaaaaa; - background: #2e2e2e; - display: inline-block; - padding: 10px; - margin-right: 10px; - padding: 4px 11px 6px; - text-transform: capitalize; -} -/* line 72, ../../src/sass/messenger-theme-flat.sass */ -ul.messenger-theme-flat .messenger-message .messenger-actions a:hover { - color: #f0f0f0; - background: #2e2e2e; -} -/* line 76, ../../src/sass/messenger-theme-flat.sass */ -ul.messenger-theme-flat .messenger-message .messenger-actions a:active { - background: #292929; - color: #aaaaaa; -} -/* line 80, ../../src/sass/messenger-theme-flat.sass */ -ul.messenger-theme-flat .messenger-message .messenger-actions .messenger-phrase { - display: none; -} -/* line 83, ../../src/sass/messenger-theme-flat.sass */ -ul.messenger-theme-flat .messenger-message .messenger-message-inner:before { - -webkit-border-radius: 50%; - -moz-border-radius: 50%; - -ms-border-radius: 50%; - -o-border-radius: 50%; - border-radius: 50%; - position: absolute; - left: 17px; - display: block; - content: " "; - top: 50%; - margin-top: -8px; - height: 13px; - width: 13px; - z-index: 20; -} -/* line 95, ../../src/sass/messenger-theme-flat.sass */ -ul.messenger-theme-flat .messenger-message.alert-success .messenger-message-inner:before { - background: #5fca4a; -} -/* line 98, ../../src/sass/messenger-theme-flat.sass */ -ul.messenger-theme-flat .messenger-message.alert-info .messenger-message-inner:before { - background: #61c4b8; -} -/* line 103, ../../src/sass/messenger-theme-flat.sass */ -ul.messenger-theme-flat .messenger-message.alert-error .messenger-message-inner:before { - background: #dd6a45; -} -/* line 32, ../../src/sass/messenger-spinner.scss */ -ul.messenger-theme-flat .messenger-message.alert-error.messenger-retry-soon .messenger-spinner { - width: 32px; - height: 32px; - background: transparent; -} -/* line 37, ../../src/sass/messenger-spinner.scss */ -ul.messenger-theme-flat .messenger-message.alert-error.messenger-retry-soon .messenger-spinner .messenger-spinner-side .messenger-spinner-fill { - background: #dd6a45; - -webkit-animation-duration: 20s; - -moz-animation-duration: 20s; - -ms-animation-duration: 20s; - -o-animation-duration: 20s; - animation-duration: 20s; - opacity: 1; -} -/* line 45, ../../src/sass/messenger-spinner.scss */ -ul.messenger-theme-flat .messenger-message.alert-error.messenger-retry-soon .messenger-spinner:after { - content: ""; - background: #292929; - position: absolute; - width: 26px; - height: 26px; - border-radius: 50%; - top: 3px; - left: 3px; - display: block; -} -/* line 32, ../../src/sass/messenger-spinner.scss */ -ul.messenger-theme-flat .messenger-message.alert-error.messenger-retry-later .messenger-spinner { - width: 32px; - height: 32px; - background: transparent; -} -/* line 37, ../../src/sass/messenger-spinner.scss */ -ul.messenger-theme-flat .messenger-message.alert-error.messenger-retry-later .messenger-spinner .messenger-spinner-side .messenger-spinner-fill { - background: #dd6a45; - -webkit-animation-duration: 600s; - -moz-animation-duration: 600s; - -ms-animation-duration: 600s; - -o-animation-duration: 600s; - animation-duration: 600s; - opacity: 1; -} -/* line 45, ../../src/sass/messenger-spinner.scss */ -ul.messenger-theme-flat .messenger-message.alert-error.messenger-retry-later .messenger-spinner:after { - content: ""; - background: #292929; - position: absolute; - width: 26px; - height: 26px; - border-radius: 50%; - top: 3px; - left: 3px; - display: block; -} -/* line 114, ../../src/sass/messenger-theme-flat.sass */ -ul.messenger-theme-flat .messenger-message-slot.messenger-last .messenger-message { - -webkit-border-radius: 4px 4px 0px 0px; - -moz-border-radius: 4px 4px 0px 0px; - -ms-border-radius: 4px 4px 0px 0px; - -o-border-radius: 4px 4px 0px 0px; - border-radius: 4px 4px 0px 0px; - -webkit-box-shadow: inset 48px 0px 0px #292929; - -moz-box-shadow: inset 48px 0px 0px #292929; - box-shadow: inset 48px 0px 0px #292929; -} -/* line 118, ../../src/sass/messenger-theme-flat.sass */ -ul.messenger-theme-flat .messenger-message-slot.messenger-first .messenger-message { - -webkit-border-radius: 0px 0px 4px 4px; - -moz-border-radius: 0px 0px 4px 4px; - -ms-border-radius: 0px 0px 4px 4px; - -o-border-radius: 0px 0px 4px 4px; - border-radius: 0px 0px 4px 4px; - -webkit-box-shadow: inset 0px 1px rgba(255, 255, 255, 0.13), inset 48px 0px 0px #292929; - -moz-box-shadow: inset 0px 1px rgba(255, 255, 255, 0.13), inset 48px 0px 0px #292929; - box-shadow: inset 0px 1px rgba(255, 255, 255, 0.13), inset 48px 0px 0px #292929; -} -/* line 122, ../../src/sass/messenger-theme-flat.sass */ -ul.messenger-theme-flat .messenger-message-slot.messenger-first.messenger-last .messenger-message { - -webkit-border-radius: 4px; - -moz-border-radius: 4px; - -ms-border-radius: 4px; - -o-border-radius: 4px; - border-radius: 4px; - -webkit-box-shadow: inset 48px 0px 0px #292929; - -moz-box-shadow: inset 48px 0px 0px #292929; - box-shadow: inset 48px 0px 0px #292929; -} -/* line 126, ../../src/sass/messenger-theme-flat.sass */ -ul.messenger-theme-flat .messenger-spinner { - display: block; - position: absolute; - left: 7px; - top: 50%; - margin-top: -18px; - z-index: 999; - height: 32px; - width: 32px; - z-index: 10; -} diff --git a/src/UI/Content/Overrides/bootstrap.less b/src/UI/Content/Overrides/bootstrap.less deleted file mode 100644 index a72e21e65..000000000 --- a/src/UI/Content/Overrides/bootstrap.less +++ /dev/null @@ -1,82 +0,0 @@ -@import "../prefixer"; -@import "../Bootstrap/variables"; -@import "../variables"; - -@input-border-focus: @droneTeal; -@font-family-sans-serif: "open sans", "Helvetica Neue", Helvetica, Arial, sans-serif; -@modal-md: 800px; -@modal-lg: 1000px; - -.label, .badge, i { - cursor : default; -} - -.slide-button { - min-width : 0px; -} - -.popover-title { - text-transform : none; -} - -.line &>[class^="icon-lidarr-"], .line &>[class*="icon-lidarr-"] { - margin-top : 1em; - height : 1em; - line-height : 1em; -} - -.tooltip-inner { - word-wrap: break-word; -} - -.dropdown-submenu { - position:relative; - & > .dropdown-menu { - top:0; - left:100%; - margin-top:-6px; - margin-left:-1px; - -webkit-border-radius:0 6px 6px 6px; - -moz-border-radius:0 6px 6px 6px; - border-radius:0 6px 6px 6px; - } - & > a:after { - display:block; - content:" "; - float:right; - width:0; - height:0; - border-color:transparent; - border-style:solid; - border-width:5px 0 5px 5px; - border-left-color:#cccccc; - margin-top:5px; - margin-right:-10px; - } -} -.dropdown-submenu:hover { - & > .dropdown-menu { - display:block; - } - & > a:after { - border-left-color:#ffffff; - } -} -.dropdown-submenu.pull-left { - float:none; - & > .dropdown-menu { - left:-100%; - margin-left:10px; - -webkit-border-radius:6px 0 6px 6px; - -moz-border-radius:6px 0 6px 6px; - border-radius:6px 0 6px 6px; - } -} - -.btn { - text-transform: capitalize; -} - -.table-responsive { - overflow-x: visible; -} diff --git a/src/UI/Content/Overrides/bootstrap.tagsinput.less b/src/UI/Content/Overrides/bootstrap.tagsinput.less deleted file mode 100644 index 85f726ae6..000000000 --- a/src/UI/Content/Overrides/bootstrap.tagsinput.less +++ /dev/null @@ -1,35 +0,0 @@ -@import "../Bootstrap/variables"; - -.bootstrap-tagsinput { - width : 100%; - - .twitter-typeahead { - width : auto; - } - - .tag { - margin-right: 0px; - - [data-role="remove"] { - &:hover { - color: @brand-danger; - } - } - } - - .tt-dropdown-menu { - - .opacity(0.95); - - .tt-suggestion { - color: #222222; - cursor: pointer; - - //selected item - &.tt-cursor { - background-color: @droneTeal; - color: #ffffff; - } - } - } -} \ No newline at end of file diff --git a/src/UI/Content/Overrides/bootstrap.toggle-switch.less b/src/UI/Content/Overrides/bootstrap.toggle-switch.less deleted file mode 100644 index 4e980373d..000000000 --- a/src/UI/Content/Overrides/bootstrap.toggle-switch.less +++ /dev/null @@ -1,33 +0,0 @@ -@import "../Bootstrap/variables"; -@import "../Bootstrap/mixins"; - -.toggle { - height: 34px; - box-sizing: border-box; - font-weight: normal; - - .slide-button { - .button-variant(@btn-danger-color, @btn-danger-bg, @btn-danger-border); - - &.btn-danger, &.btn-warning { - //.buttonBackground(@btnInverseBackground, @btnInverseBackgroundHighlight); - .button-variant(@btn-warning-color, @btn-warning-bg, @btn-warning-border); - } - } - - input:first-of-type:checked ~ .slide-button { - .button-variant(@btn-primary-color, @btn-primary-bg, @btn-primary-border); - - &.btn-danger { - .button-variant(@btn-danger-color, @btn-danger-bg, @btn-danger-border); - } - - &.btn-warning { - .button-variant(@btn-warning-color, @btn-warning-bg, @btn-warning-border); - } - } - - input:first-of-type:disabled ~ .slide-button { - opacity: 0.5; - } -} \ No newline at end of file diff --git a/src/UI/Content/Overrides/browser.less b/src/UI/Content/Overrides/browser.less deleted file mode 100644 index 236f2f8d8..000000000 --- a/src/UI/Content/Overrides/browser.less +++ /dev/null @@ -1,17 +0,0 @@ -html { - overflow : -moz-scrollbars-vertical; - overflow-y : scroll; -} - -button::-moz-focus-inner, a::-moz-focus-inner { - border : 0; -} - -a:focus { - outline : none; -} - -body h1, body h2, body h3, body h4, body h5, body h6 { - text-transform : capitalize; - font-weight : 300; -} diff --git a/src/UI/Content/Overrides/fullcalendar.less b/src/UI/Content/Overrides/fullcalendar.less deleted file mode 100644 index 76d1b32d5..000000000 --- a/src/UI/Content/Overrides/fullcalendar.less +++ /dev/null @@ -1,49 +0,0 @@ -.fc-view { - overflow: visible; -} - -.fc-time { - padding: 0 1px; -} - -.fc-title { - padding: 0 1px; - display: block; - text-overflow: ellipsis; - white-space: nowrap; - overflow: hidden; -} - -.fc-scroller { - overflow-y: visible; -} - -@media (max-width: @screen-xs-max) { - .fc-button { - padding: 0px 5px; - } - - .fc-header-space { - padding-left: 5px; - } -} - -.fc-header { - .fc-state-active { - z-index: 1; - } -} - -.fc-event-container { - .fc-event { - line-height : inherit; - } -} - -.fc-icon { - width: auto; -} - -.fc-icon::after { - margin: 0px; -} diff --git a/src/UI/Content/Overrides/messenger.less b/src/UI/Content/Overrides/messenger.less deleted file mode 100644 index 160ed9512..000000000 --- a/src/UI/Content/Overrides/messenger.less +++ /dev/null @@ -1,23 +0,0 @@ -@import "../variables"; - -body.control-panel-visible { - ul.messenger.messenger-fixed.messenger-on-bottom { - bottom: 95px; - } -} - -ul.messenger-theme-flat .messenger-message.alert-info .messenger-message-inner:before { - background: @droneTeal; -} - -@media (max-width: @screen-xs-max) { - ul.messenger.messenger-fixed.messenger-on-bottom { - width: 100%; - bottom: 0px; - .border-bottom-radius(0); - - &.messenger-on-right { - right : 0px; - } - } -} \ No newline at end of file diff --git a/src/UI/Content/badges.less b/src/UI/Content/badges.less deleted file mode 100644 index 68caf5c45..000000000 --- a/src/UI/Content/badges.less +++ /dev/null @@ -1,37 +0,0 @@ -@import "../Content/Bootstrap/variables"; - -.badge-inverse { - background-color: #eee; - border: 1px solid @badge-bg; - color: @badge-bg; -} - -.badge-primary { - .badge-variant(@label-primary-bg); -} - -.badge-success { - .badge-variant(@label-success-bg); -} - -.badge-info { - .badge-variant(@label-info-bg); -} - -.badge-warning { - .badge-variant(@label-warning-bg); -} - -.badge-danger { - .badge-variant(@label-danger-bg); -} - -.badge-variant(@color) { - background-color: @color; - &[href] { - &:hover, - &:focus { - background-color: darken(@color, 10%); - } - } -} \ No newline at end of file diff --git a/src/UI/Content/bootstrap.less b/src/UI/Content/bootstrap.less deleted file mode 100644 index 10e23ce63..000000000 --- a/src/UI/Content/bootstrap.less +++ /dev/null @@ -1,3 +0,0 @@ -@import "./Bootstrap/bootstrap"; -@import "./Overrides/bootstrap"; -@import "./bootstrap.tagsinput.less"; \ No newline at end of file diff --git a/src/UI/Content/bootstrap.tagsinput.less b/src/UI/Content/bootstrap.tagsinput.less deleted file mode 100644 index face63f18..000000000 --- a/src/UI/Content/bootstrap.tagsinput.less +++ /dev/null @@ -1,50 +0,0 @@ -.bootstrap-tagsinput { - background-color: #fff; - border: 1px solid #ccc; - box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); - display: inline-block; - padding: 4px 6px; - margin-bottom: 10px; - color: #555; - vertical-align: middle; - border-radius: 4px; - max-width: 100%; - line-height: 22px; - cursor: text; - - input { - border: none; - box-shadow: none; - outline: none; - background-color: transparent; - padding: 0; - margin: 0; - width: auto !important; - max-width: inherit; - - &:focus { - border: none; - box-shadow: none; - } - } - - .tag { - margin-right: 2px; - color: white; - - [data-role="remove"] { - margin-left:8px; - cursor:pointer; - &:after{ - content: "x"; - padding:0px 2px; - } - &:hover { - box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); - &:active { - box-shadow: inset 0 3px 5px rgba(0,0,0,0.125); - } - } - } - } -} diff --git a/src/UI/Content/bootstrap.toggle-switch.css b/src/UI/Content/bootstrap.toggle-switch.css deleted file mode 100644 index ce924d3ce..000000000 --- a/src/UI/Content/bootstrap.toggle-switch.css +++ /dev/null @@ -1,228 +0,0 @@ -/* ------------------------------------------ -CSS TOGGLE SWITCHES (IonuÈ› Colceriu) -Licensed under Unlicense -https://github.com/ghinda/css-toggle-switch ------------------------------------------- */ - -/* Hide by default */ - -.switch .slide-button, -.toggle p span { - display: none; -} - -/* Toggle Switches */ - -/* We can't test for a specific feature, - * so we only target browsers with support for media queries. - */ -@media only screen { - - /* Checkbox - */ - .toggle { - position: relative; - padding: 0; - margin-left: 100px; - } - - /* Position the label over all the elements, except the slide-button - * Clicking anywhere on the label will change the switch-state - */ - .toggle label { - position: relative; - z-index: 3; - display: block; - width: 100%; - } - - /* Don't hide the input from screen-readers and keyboard access - */ - .toggle input { - position: absolute; - opacity: 0; - z-index: 5; - } - - .toggle p { - position: absolute; - left: -100px; - width: 100%; - margin: 0; - text-align: left; - } - - .toggle p span { - position: absolute; - top: 0; - left: 0; - z-index: 5; - display: block; - width: 50%; - margin-left: 100px; - text-align: center; - color: #F5F5F5; - } - - .toggle p span:last-child { - left: 50%; - } - - .toggle .slide-button { - position: absolute; - right: 0; - top: 0; - z-index: 4; - display: inline; - width: 50%; - height: 100%; - padding: 0; - } - - /* Radio Switch - */ - .switch { - position: relative; - padding: 0; - } - - .switch input { - position: absolute; - opacity: 0; - } - - .switch label { - position: relative; - z-index: 2; - - float: left; - width: 50%; - height: 100%; - - margin: 0; - text-align: center; - } - - .switch .slide-button { - position: absolute; - top: 0; - left: 0; - padding: 0; - z-index: 1; - - width: 50%; - height: 100%; - } - - .switch input:last-of-type:checked ~ .slide-button { - left: 50%; - } - - /* Switch with 3 items */ - .switch.switch-three label, - .switch.switch-three .slide-button { - width: 33.3%; - } - - .switch.switch-three input:checked:nth-of-type(2) ~ .slide-button { - left: 33.3%; - } - - .switch.switch-three input:checked:last-of-type ~ .slide-button { - left: 66.6%; - } - - /* Switch with 4 items */ - .switch.switch-four label, - .switch.switch-four .slide-button { - width: 25%; - } - - .switch.switch-four input:checked:nth-of-type(2) ~ .slide-button { - left: 25%; - } - - .switch.switch-four input:checked:nth-of-type(3) ~ .slide-button { - left: 50%; - } - - .switch.switch-four input:checked:last-of-type ~ .slide-button { - left: 75%; - } - - /* Switch with 5 items */ - .switch.switch-five label, - .switch.switch-five .slide-button { - width: 20%; - } - - .switch.switch-five input:checked:nth-of-type(2) ~ .slide-button { - left: 20%; - } - - .switch.switch-five input:checked:nth-of-type(3) ~ .slide-button { - left: 40%; - } - - .switch.switch-five input:checked:nth-of-type(4) ~ .slide-button { - left: 60%; - } - - .switch.switch-five input:checked:last-of-type ~ .slide-button { - left: 80%; - } - - /* Shared */ - .toggle, - .switch { - display: block; - height: 30px; - } - - .switch *, - .toggle * { - -webkit-box-sizing: border-box; - -moz-box-sizing: border-box; - -ms-box-sizing: border-box; - -o-box-sizing: border-box; - box-sizing: border-box; - } - - .switch .slide-button, - .toggle .slide-button { - display: block; - - -webkit-transition: all 0.3s ease-in 0s; - -moz-transition: all 0.3s ease-in 0s; - -ms-transition: all 0.3s ease-in 0s; - -o-transition: all 0.3s ease-in 0s; - transition: all 0.3s ease-in 0s; - } - - .toggle label, - .toggle p, - .switch label { - line-height: 30px; - vertical-align: middle; - } - - .toggle input:checked ~ .slide-button { - right: 50%; - } - - /* Outline the toggles when the inputs are focused */ - /*.toggle input:focus ~ .slide-button,*/ - /* WHY?! It looks awful and it seems to bug out in FF */ - /*.switch input:focus + label {*/ - /*outline: 1px dotted #888;*/ - /*}*/ - - /* Bugfix for older Webkit, including mobile Webkit. Adapted from: - * http://css-tricks.com/webkit-sibling-bug/ - */ - .switch, .toggle { - -webkit-animation: bugfix infinite 1s; - } - - @-webkit-keyframes bugfix { from { position: relative; } to { position: relative; } } -} diff --git a/src/UI/Content/checkbox-button.less b/src/UI/Content/checkbox-button.less deleted file mode 100644 index 51f54c7d8..000000000 --- a/src/UI/Content/checkbox-button.less +++ /dev/null @@ -1,33 +0,0 @@ -@import "Bootstrap/variables"; -@import "Bootstrap/mixins"; - -.checkbox-button div { - display: none; -} - -@media only screen { - .checkbox-button { - input { - position: absolute; - opacity: 0; - z-index: 5; - } - - div { - display: block; - } - - .btn { - .button-variant(@btn-default-color, @btn-default-bg, @btn-default-border); - color: #333333; - } - - .btn:hover { - color: #333333; - } - - input:first-of-type:checked ~ .btn-primary { - .button-variant(@btn-primary-color, @btn-primary-bg, @btn-primary-border); - } - } -} diff --git a/src/UI/Content/font.less b/src/UI/Content/font.less deleted file mode 100644 index f20b10dc1..000000000 --- a/src/UI/Content/font.less +++ /dev/null @@ -1,47 +0,0 @@ -@font-face { - font-family: 'Open Sans'; - font-style: normal; - font-weight: 300; - src: url('./fonts/opensans-light.eot'); - src: local('Open Sans Light'), - local('OpenSans-Light'), - url('./fonts/opensans-light.eot?#iefix') format('embedded-opentype'), - url('./fonts/opensans-light.woff') format('woff'), - url('./fonts/opensans-light.ttf') format('truetype'); -} - -@font-face { - font-family: 'Open Sans'; - font-style: normal; - font-weight: 400; - src: url('./fonts/opensans-regular.eot'); - src: local('Open Sans'), - local('OpenSans'), - url('./fonts/opensans-regular.eot?#iefix') format('embedded-opentype'), - url('./fonts/opensans-regular.woff') format('woff'), - url('./fonts/opensans-regular.ttf') format('truetype') -} - -@font-face { - font-family: 'Open Sans'; - font-style: normal; - font-weight: 600; - src: url('./fonts/opensans-semibold.eot'); - src: local('Open Sans SemiBold'), - local('OpenSans-SemiBold'), - url('./fonts/opensans-semibold.eot?#iefix') format('embedded-opentype'), - url('./fonts/opensans-semibold.woff') format('woff'), - url('./fonts/opensans-semibold.ttf') format('truetype') -} - -@font-face { - font-family: 'Ubuntu Mono'; - font-style: normal; - font-weight: 400; - src: url('./fonts/ubuntumono-regular.eot'); - src: local('Open Sans'), - local('OpenSans'), - url('./fonts/ubuntumono-regular.eot?#iefix') format('embedded-opentype'), - url('./fonts/ubuntumono-regular.woff') format('woff'), - url('./fonts/ubuntumono-regular.ttf') format('truetype') -} \ No newline at end of file diff --git a/src/UI/Content/fonts/opensans-light.eot b/src/UI/Content/fonts/opensans-light.eot deleted file mode 100644 index 3c203d8e7..000000000 Binary files a/src/UI/Content/fonts/opensans-light.eot and /dev/null differ diff --git a/src/UI/Content/fonts/opensans-light.ttf b/src/UI/Content/fonts/opensans-light.ttf deleted file mode 100644 index 0d381897d..000000000 Binary files a/src/UI/Content/fonts/opensans-light.ttf and /dev/null differ diff --git a/src/UI/Content/fonts/opensans-light.woff b/src/UI/Content/fonts/opensans-light.woff deleted file mode 100644 index 99f335326..000000000 Binary files a/src/UI/Content/fonts/opensans-light.woff and /dev/null differ diff --git a/src/UI/Content/fonts/opensans-regular.eot b/src/UI/Content/fonts/opensans-regular.eot deleted file mode 100644 index 091cd51b9..000000000 Binary files a/src/UI/Content/fonts/opensans-regular.eot and /dev/null differ diff --git a/src/UI/Content/fonts/opensans-regular.ttf b/src/UI/Content/fonts/opensans-regular.ttf deleted file mode 100644 index db433349b..000000000 Binary files a/src/UI/Content/fonts/opensans-regular.ttf and /dev/null differ diff --git a/src/UI/Content/fonts/opensans-regular.woff b/src/UI/Content/fonts/opensans-regular.woff deleted file mode 100644 index 55b25f867..000000000 Binary files a/src/UI/Content/fonts/opensans-regular.woff and /dev/null differ diff --git a/src/UI/Content/fonts/opensans-semibold.eot b/src/UI/Content/fonts/opensans-semibold.eot deleted file mode 100644 index 55d28c378..000000000 Binary files a/src/UI/Content/fonts/opensans-semibold.eot and /dev/null differ diff --git a/src/UI/Content/fonts/opensans-semibold.ttf b/src/UI/Content/fonts/opensans-semibold.ttf deleted file mode 100644 index 1a7679e39..000000000 Binary files a/src/UI/Content/fonts/opensans-semibold.ttf and /dev/null differ diff --git a/src/UI/Content/fonts/opensans-semibold.woff b/src/UI/Content/fonts/opensans-semibold.woff deleted file mode 100644 index e83bb333d..000000000 Binary files a/src/UI/Content/fonts/opensans-semibold.woff and /dev/null differ diff --git a/src/UI/Content/form.less b/src/UI/Content/form.less deleted file mode 100644 index 28474c962..000000000 --- a/src/UI/Content/form.less +++ /dev/null @@ -1,133 +0,0 @@ -@import "../Shared/Styles/clickable.less"; - -.form-group { - .input-group { - .checkbox { - width : 100px; - margin-left : 0px; - display : inline-block; - padding-top : 0px; - margin-bottom : 0px; - } - - .help-inline-checkbox { - display : inline-block; - margin-top : -20px; - margin-bottom : 0; - margin-left : 10px; - vertical-align : middle; - } - - .btn { - i { - margin-right : 0px; - color : inherit; - } - } - } - - .btn { - i { - margin-right : 0px; - color : inherit; - } - } - - i { - font-size : 16px; - color : #595959; - margin-right : 5px; - } - - .help-inline { - display : inline-block; - margin-top : 8px; - padding-left : 0px; - - @media (max-width: @screen-xs-max) { - margin-left: 0px; - } - } -} - -.text-area-help { - display : block; - color : #777777; - font-size : 12px; -} - -textarea.release-restrictions { - width : 100%; - max-width : 100%; -} - -.help-inline-text-area { - margin-top: 25px !important; -} - -.help-link { - text-decoration : none !important; - - i { - .clickable; - } -} - -h3 { - .help-inline { - font-size: 16px; - padding-left: 0px; - margin-top: -5px; - text-transform: none; - } -} - -.form-inline { - div { - display : inline-block; - } -} - -.has-error { - .help-inline { - color: #b94a48; - margin-left: 0px; - } -} - -.validation-error { - i { - text-decoration: none; - color: #b94a48; - } -} - -.has-warning { - .help-inline { - color: orange; - margin-left: 0px; - } -} - -.validation-warning { - i { - text-decoration: none; - color: orange; - } -} - -// Tooltips - -.help-inline-checkbox, .help-inline { - .tooltip-inner { - white-space : pre-wrap; - min-width : 200px; - } - - .help-link ~ .tooltip { - .tooltip-inner { - white-space : normal; - min-width : 0px; - } - } -} diff --git a/src/UI/Content/fullcalendar.css b/src/UI/Content/fullcalendar.css deleted file mode 100644 index 1022ff39c..000000000 --- a/src/UI/Content/fullcalendar.css +++ /dev/null @@ -1,1413 +0,0 @@ -/*! - * FullCalendar v3.4.0 Stylesheet - * Docs & License: https://fullcalendar.io/ - * (c) 2017 Adam Shaw - */ - - -.fc { - direction: ltr; - text-align: left; -} - -.fc-rtl { - text-align: right; -} - -body .fc { /* extra precedence to overcome jqui */ - font-size: 1em; -} - - -/* Colors ---------------------------------------------------------------------------------------------------*/ - -.fc-unthemed th, -.fc-unthemed td, -.fc-unthemed thead, -.fc-unthemed tbody, -.fc-unthemed .fc-divider, -.fc-unthemed .fc-row, -.fc-unthemed .fc-content, /* for gutter border */ -.fc-unthemed .fc-popover, -.fc-unthemed .fc-list-view, -.fc-unthemed .fc-list-heading td { - border-color: #ddd; -} - -.fc-unthemed .fc-popover { - background-color: #fff; -} - -.fc-unthemed .fc-divider, -.fc-unthemed .fc-popover .fc-header, -.fc-unthemed .fc-list-heading td { - background: #eee; -} - -.fc-unthemed .fc-popover .fc-header .fc-close { - color: #666; -} - -.fc-unthemed td.fc-today { - background: #fcf8e3; -} - -.fc-highlight { /* when user is selecting cells */ - background: #bce8f1; - opacity: .3; -} - -.fc-bgevent { /* default look for background events */ - background: rgb(143, 223, 130); - opacity: .3; -} - -.fc-nonbusiness { /* default look for non-business-hours areas */ - /* will inherit .fc-bgevent's styles */ - background: #d7d7d7; -} - -.fc-unthemed .fc-disabled-day { - background: #d7d7d7; - opacity: .3; -} - -.ui-widget .fc-disabled-day { /* themed */ - background-image: none; -} - - -/* Icons (inline elements with styled text that mock arrow icons) ---------------------------------------------------------------------------------------------------*/ - -.fc-icon { - display: inline-block; - height: 1em; - line-height: 1em; - font-size: 1em; - text-align: center; - overflow: hidden; - font-family: "Courier New", Courier, monospace; - - /* don't allow browser text-selection */ - -webkit-touch-callout: none; - -webkit-user-select: none; - -khtml-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - user-select: none; - } - -/* -Acceptable font-family overrides for individual icons: - "Arial", sans-serif - "Times New Roman", serif - -NOTE: use percentage font sizes or else old IE chokes -*/ - -.fc-icon:after { - position: relative; -} - -.fc-icon-left-single-arrow:after { - content: "\02039"; - font-weight: bold; - font-size: 200%; - top: -7%; -} - -.fc-icon-right-single-arrow:after { - content: "\0203A"; - font-weight: bold; - font-size: 200%; - top: -7%; -} - -.fc-icon-left-double-arrow:after { - content: "\000AB"; - font-size: 160%; - top: -7%; -} - -.fc-icon-right-double-arrow:after { - content: "\000BB"; - font-size: 160%; - top: -7%; -} - -.fc-icon-left-triangle:after { - content: "\25C4"; - font-size: 125%; - top: 3%; -} - -.fc-icon-right-triangle:after { - content: "\25BA"; - font-size: 125%; - top: 3%; -} - -.fc-icon-down-triangle:after { - content: "\25BC"; - font-size: 125%; - top: 2%; -} - -.fc-icon-x:after { - content: "\000D7"; - font-size: 200%; - top: 6%; -} - - -/* Buttons (styled <button> tags, normalized to work cross-browser) ---------------------------------------------------------------------------------------------------*/ - -.fc button { - /* force height to include the border and padding */ - -moz-box-sizing: border-box; - -webkit-box-sizing: border-box; - box-sizing: border-box; - - /* dimensions */ - margin: 0; - height: 2.1em; - padding: 0 .6em; - - /* text & cursor */ - font-size: 1em; /* normalize */ - white-space: nowrap; - cursor: pointer; -} - -/* Firefox has an annoying inner border */ -.fc button::-moz-focus-inner { margin: 0; padding: 0; } - -.fc-state-default { /* non-theme */ - border: 1px solid; -} - -.fc-state-default.fc-corner-left { /* non-theme */ - border-top-left-radius: 4px; - border-bottom-left-radius: 4px; -} - -.fc-state-default.fc-corner-right { /* non-theme */ - border-top-right-radius: 4px; - border-bottom-right-radius: 4px; -} - -/* icons in buttons */ - -.fc button .fc-icon { /* non-theme */ - position: relative; - top: -0.05em; /* seems to be a good adjustment across browsers */ - margin: 0 .2em; - vertical-align: middle; -} - -/* - button states - borrowed from twitter bootstrap (http://twitter.github.com/bootstrap/) -*/ - -.fc-state-default { - background-color: #f5f5f5; - background-image: -moz-linear-gradient(top, #ffffff, #e6e6e6); - background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#ffffff), to(#e6e6e6)); - background-image: -webkit-linear-gradient(top, #ffffff, #e6e6e6); - background-image: -o-linear-gradient(top, #ffffff, #e6e6e6); - background-image: linear-gradient(to bottom, #ffffff, #e6e6e6); - background-repeat: repeat-x; - border-color: #e6e6e6 #e6e6e6 #bfbfbf; - border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); - color: #333; - text-shadow: 0 1px 1px rgba(255, 255, 255, 0.75); - box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); -} - -.fc-state-hover, -.fc-state-down, -.fc-state-active, -.fc-state-disabled { - color: #333333; - background-color: #e6e6e6; -} - -.fc-state-hover { - color: #333333; - text-decoration: none; - background-position: 0 -15px; - -webkit-transition: background-position 0.1s linear; - -moz-transition: background-position 0.1s linear; - -o-transition: background-position 0.1s linear; - transition: background-position 0.1s linear; -} - -.fc-state-down, -.fc-state-active { - background-color: #cccccc; - background-image: none; - box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); -} - -.fc-state-disabled { - cursor: default; - background-image: none; - opacity: 0.65; - box-shadow: none; -} - - -/* Buttons Groups ---------------------------------------------------------------------------------------------------*/ - -.fc-button-group { - display: inline-block; -} - -/* -every button that is not first in a button group should scootch over one pixel and cover the -previous button's border... -*/ - -.fc .fc-button-group > * { /* extra precedence b/c buttons have margin set to zero */ - float: left; - margin: 0 0 0 -1px; -} - -.fc .fc-button-group > :first-child { /* same */ - margin-left: 0; -} - - -/* Popover ---------------------------------------------------------------------------------------------------*/ - -.fc-popover { - position: absolute; - box-shadow: 0 2px 6px rgba(0,0,0,.15); -} - -.fc-popover .fc-header { /* TODO: be more consistent with fc-head/fc-body */ - padding: 2px 4px; -} - -.fc-popover .fc-header .fc-title { - margin: 0 2px; -} - -.fc-popover .fc-header .fc-close { - cursor: pointer; -} - -.fc-ltr .fc-popover .fc-header .fc-title, -.fc-rtl .fc-popover .fc-header .fc-close { - float: left; -} - -.fc-rtl .fc-popover .fc-header .fc-title, -.fc-ltr .fc-popover .fc-header .fc-close { - float: right; -} - -/* unthemed */ - -.fc-unthemed .fc-popover { - border-width: 1px; - border-style: solid; -} - -.fc-unthemed .fc-popover .fc-header .fc-close { - font-size: .9em; - margin-top: 2px; -} - -/* jqui themed */ - -.fc-popover > .ui-widget-header + .ui-widget-content { - border-top: 0; /* where they meet, let the header have the border */ -} - - -/* Misc Reusable Components ---------------------------------------------------------------------------------------------------*/ - -.fc-divider { - border-style: solid; - border-width: 1px; -} - -hr.fc-divider { - height: 0; - margin: 0; - padding: 0 0 2px; /* height is unreliable across browsers, so use padding */ - border-width: 1px 0; -} - -.fc-clear { - clear: both; -} - -.fc-bg, -.fc-bgevent-skeleton, -.fc-highlight-skeleton, -.fc-helper-skeleton { - /* these element should always cling to top-left/right corners */ - position: absolute; - top: 0; - left: 0; - right: 0; -} - -.fc-bg { - bottom: 0; /* strech bg to bottom edge */ -} - -.fc-bg table { - height: 100%; /* strech bg to bottom edge */ -} - - -/* Tables ---------------------------------------------------------------------------------------------------*/ - -.fc table { - width: 100%; - box-sizing: border-box; /* fix scrollbar issue in firefox */ - table-layout: fixed; - border-collapse: collapse; - border-spacing: 0; - font-size: 1em; /* normalize cross-browser */ -} - -.fc th { - text-align: center; -} - -.fc th, -.fc td { - border-style: solid; - border-width: 1px; - padding: 0; - vertical-align: top; -} - -.fc td.fc-today { - border-style: double; /* overcome neighboring borders */ -} - - -/* Internal Nav Links ---------------------------------------------------------------------------------------------------*/ - -a[data-goto] { - cursor: pointer; -} - -a[data-goto]:hover { - text-decoration: underline; -} - - -/* Fake Table Rows ---------------------------------------------------------------------------------------------------*/ - -.fc .fc-row { /* extra precedence to overcome themes w/ .ui-widget-content forcing a 1px border */ - /* no visible border by default. but make available if need be (scrollbar width compensation) */ - border-style: solid; - border-width: 0; -} - -.fc-row table { - /* don't put left/right border on anything within a fake row. - the outer tbody will worry about this */ - border-left: 0 hidden transparent; - border-right: 0 hidden transparent; - - /* no bottom borders on rows */ - border-bottom: 0 hidden transparent; -} - -.fc-row:first-child table { - border-top: 0 hidden transparent; /* no top border on first row */ -} - - -/* Day Row (used within the header and the DayGrid) ---------------------------------------------------------------------------------------------------*/ - -.fc-row { - position: relative; -} - -.fc-row .fc-bg { - z-index: 1; -} - -/* highlighting cells & background event skeleton */ - -.fc-row .fc-bgevent-skeleton, -.fc-row .fc-highlight-skeleton { - bottom: 0; /* stretch skeleton to bottom of row */ -} - -.fc-row .fc-bgevent-skeleton table, -.fc-row .fc-highlight-skeleton table { - height: 100%; /* stretch skeleton to bottom of row */ -} - -.fc-row .fc-highlight-skeleton td, -.fc-row .fc-bgevent-skeleton td { - border-color: transparent; -} - -.fc-row .fc-bgevent-skeleton { - z-index: 2; - -} - -.fc-row .fc-highlight-skeleton { - z-index: 3; -} - -/* -row content (which contains day/week numbers and events) as well as "helper" (which contains -temporary rendered events). -*/ - -.fc-row .fc-content-skeleton { - position: relative; - z-index: 4; - padding-bottom: 2px; /* matches the space above the events */ -} - -.fc-row .fc-helper-skeleton { - z-index: 5; -} - -.fc-row .fc-content-skeleton td, -.fc-row .fc-helper-skeleton td { - /* see-through to the background below */ - background: none; /* in case <td>s are globally styled */ - border-color: transparent; - - /* don't put a border between events and/or the day number */ - border-bottom: 0; -} - -.fc-row .fc-content-skeleton tbody td, /* cells with events inside (so NOT the day number cell) */ -.fc-row .fc-helper-skeleton tbody td { - /* don't put a border between event cells */ - border-top: 0; -} - - -/* Scrolling Container ---------------------------------------------------------------------------------------------------*/ - -.fc-scroller { - -webkit-overflow-scrolling: touch; -} - -/* TODO: move to agenda/basic */ -.fc-scroller > .fc-day-grid, -.fc-scroller > .fc-time-grid { - position: relative; /* re-scope all positions */ - width: 100%; /* hack to force re-sizing this inner element when scrollbars appear/disappear */ -} - - -/* Global Event Styles ---------------------------------------------------------------------------------------------------*/ - -.fc-event { - position: relative; /* for resize handle and other inner positioning */ - display: block; /* make the <a> tag block */ - font-size: .85em; - line-height: 1.3; - border-radius: 3px; - border: 1px solid #3a87ad; /* default BORDER color */ - font-weight: normal; /* undo jqui's ui-widget-header bold */ -} - -.fc-event, -.fc-event-dot { - background-color: #3a87ad; /* default BACKGROUND color */ -} - -/* overpower some of bootstrap's and jqui's styles on <a> tags */ -.fc-event, -.fc-event:hover, -.ui-widget .fc-event { - color: #fff; /* default TEXT color */ - text-decoration: none; /* if <a> has an href */ -} - -.fc-event[href], -.fc-event.fc-draggable { - cursor: pointer; /* give events with links and draggable events a hand mouse pointer */ -} - -.fc-not-allowed, /* causes a "warning" cursor. applied on body */ -.fc-not-allowed .fc-event { /* to override an event's custom cursor */ - cursor: not-allowed; -} - -.fc-event .fc-bg { /* the generic .fc-bg already does position */ - z-index: 1; - background: #fff; - opacity: .25; -} - -.fc-event .fc-content { - position: relative; - z-index: 2; -} - -/* resizer (cursor AND touch devices) */ - -.fc-event .fc-resizer { - position: absolute; - z-index: 4; -} - -/* resizer (touch devices) */ - -.fc-event .fc-resizer { - display: none; -} - -.fc-event.fc-allow-mouse-resize .fc-resizer, -.fc-event.fc-selected .fc-resizer { - /* only show when hovering or selected (with touch) */ - display: block; -} - -/* hit area */ - -.fc-event.fc-selected .fc-resizer:before { - /* 40x40 touch area */ - content: ""; - position: absolute; - z-index: 9999; /* user of this util can scope within a lower z-index */ - top: 50%; - left: 50%; - width: 40px; - height: 40px; - margin-left: -20px; - margin-top: -20px; -} - - -/* Event Selection (only for touch devices) ---------------------------------------------------------------------------------------------------*/ - -.fc-event.fc-selected { - z-index: 9999 !important; /* overcomes inline z-index */ - box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2); -} - -.fc-event.fc-selected.fc-dragging { - box-shadow: 0 2px 7px rgba(0, 0, 0, 0.3); -} - - -/* Horizontal Events ---------------------------------------------------------------------------------------------------*/ - -/* bigger touch area when selected */ -.fc-h-event.fc-selected:before { - content: ""; - position: absolute; - z-index: 3; /* below resizers */ - top: -10px; - bottom: -10px; - left: 0; - right: 0; -} - -/* events that are continuing to/from another week. kill rounded corners and butt up against edge */ - -.fc-ltr .fc-h-event.fc-not-start, -.fc-rtl .fc-h-event.fc-not-end { - margin-left: 0; - border-left-width: 0; - padding-left: 1px; /* replace the border with padding */ - border-top-left-radius: 0; - border-bottom-left-radius: 0; -} - -.fc-ltr .fc-h-event.fc-not-end, -.fc-rtl .fc-h-event.fc-not-start { - margin-right: 0; - border-right-width: 0; - padding-right: 1px; /* replace the border with padding */ - border-top-right-radius: 0; - border-bottom-right-radius: 0; -} - -/* resizer (cursor AND touch devices) */ - -/* left resizer */ -.fc-ltr .fc-h-event .fc-start-resizer, -.fc-rtl .fc-h-event .fc-end-resizer { - cursor: w-resize; - left: -1px; /* overcome border */ -} - -/* right resizer */ -.fc-ltr .fc-h-event .fc-end-resizer, -.fc-rtl .fc-h-event .fc-start-resizer { - cursor: e-resize; - right: -1px; /* overcome border */ -} - -/* resizer (mouse devices) */ - -.fc-h-event.fc-allow-mouse-resize .fc-resizer { - width: 7px; - top: -1px; /* overcome top border */ - bottom: -1px; /* overcome bottom border */ -} - -/* resizer (touch devices) */ - -.fc-h-event.fc-selected .fc-resizer { - /* 8x8 little dot */ - border-radius: 4px; - border-width: 1px; - width: 6px; - height: 6px; - border-style: solid; - border-color: inherit; - background: #fff; - /* vertically center */ - top: 50%; - margin-top: -4px; -} - -/* left resizer */ -.fc-ltr .fc-h-event.fc-selected .fc-start-resizer, -.fc-rtl .fc-h-event.fc-selected .fc-end-resizer { - margin-left: -4px; /* centers the 8x8 dot on the left edge */ -} - -/* right resizer */ -.fc-ltr .fc-h-event.fc-selected .fc-end-resizer, -.fc-rtl .fc-h-event.fc-selected .fc-start-resizer { - margin-right: -4px; /* centers the 8x8 dot on the right edge */ -} - - -/* DayGrid events ----------------------------------------------------------------------------------------------------- -We use the full "fc-day-grid-event" class instead of using descendants because the event won't -be a descendant of the grid when it is being dragged. -*/ - -.fc-day-grid-event { - margin: 1px 2px 0; /* spacing between events and edges */ - padding: 0 1px; -} - -tr:first-child > td > .fc-day-grid-event { - margin-top: 2px; /* a little bit more space before the first event */ -} - -.fc-day-grid-event.fc-selected:after { - content: ""; - position: absolute; - z-index: 1; /* same z-index as fc-bg, behind text */ - /* overcome the borders */ - top: -1px; - right: -1px; - bottom: -1px; - left: -1px; - /* darkening effect */ - background: #000; - opacity: .25; -} - -.fc-day-grid-event .fc-content { /* force events to be one-line tall */ - white-space: nowrap; - overflow: hidden; -} - -.fc-day-grid-event .fc-time { - font-weight: bold; -} - -/* resizer (cursor devices) */ - -/* left resizer */ -.fc-ltr .fc-day-grid-event.fc-allow-mouse-resize .fc-start-resizer, -.fc-rtl .fc-day-grid-event.fc-allow-mouse-resize .fc-end-resizer { - margin-left: -2px; /* to the day cell's edge */ -} - -/* right resizer */ -.fc-ltr .fc-day-grid-event.fc-allow-mouse-resize .fc-end-resizer, -.fc-rtl .fc-day-grid-event.fc-allow-mouse-resize .fc-start-resizer { - margin-right: -2px; /* to the day cell's edge */ -} - - -/* Event Limiting ---------------------------------------------------------------------------------------------------*/ - -/* "more" link that represents hidden events */ - -a.fc-more { - margin: 1px 3px; - font-size: .85em; - cursor: pointer; - text-decoration: none; -} - -a.fc-more:hover { - text-decoration: underline; -} - -.fc-limited { /* rows and cells that are hidden because of a "more" link */ - display: none; -} - -/* popover that appears when "more" link is clicked */ - -.fc-day-grid .fc-row { - z-index: 1; /* make the "more" popover one higher than this */ -} - -.fc-more-popover { - z-index: 2; - width: 220px; -} - -.fc-more-popover .fc-event-container { - padding: 10px; -} - - -/* Now Indicator ---------------------------------------------------------------------------------------------------*/ - -.fc-now-indicator { - position: absolute; - border: 0 solid red; -} - - -/* Utilities ---------------------------------------------------------------------------------------------------*/ - -.fc-unselectable { - -webkit-user-select: none; - -khtml-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - user-select: none; - -webkit-touch-callout: none; - -webkit-tap-highlight-color: rgba(0, 0, 0, 0); -} - - - -/* Toolbar ---------------------------------------------------------------------------------------------------*/ - -.fc-toolbar { - text-align: center; -} - -.fc-toolbar.fc-header-toolbar { - margin-bottom: 1em; -} - -.fc-toolbar.fc-footer-toolbar { - margin-top: 1em; -} - -.fc-toolbar .fc-left { - float: left; -} - -.fc-toolbar .fc-right { - float: right; -} - -.fc-toolbar .fc-center { - display: inline-block; -} - -/* the things within each left/right/center section */ -.fc .fc-toolbar > * > * { /* extra precedence to override button border margins */ - float: left; - margin-left: .75em; -} - -/* the first thing within each left/center/right section */ -.fc .fc-toolbar > * > :first-child { /* extra precedence to override button border margins */ - margin-left: 0; -} - -/* title text */ - -.fc-toolbar h2 { - margin: 0; -} - -/* button layering (for border precedence) */ - -.fc-toolbar button { - position: relative; -} - -.fc-toolbar .fc-state-hover, -.fc-toolbar .ui-state-hover { - z-index: 2; -} - -.fc-toolbar .fc-state-down { - z-index: 3; -} - -.fc-toolbar .fc-state-active, -.fc-toolbar .ui-state-active { - z-index: 4; -} - -.fc-toolbar button:focus { - z-index: 5; -} - - -/* View Structure ---------------------------------------------------------------------------------------------------*/ - -/* undo twitter bootstrap's box-sizing rules. normalizes positioning techniques */ -/* don't do this for the toolbar because we'll want bootstrap to style those buttons as some pt */ -.fc-view-container *, -.fc-view-container *:before, -.fc-view-container *:after { - -webkit-box-sizing: content-box; - -moz-box-sizing: content-box; - box-sizing: content-box; -} - -.fc-view, /* scope positioning and z-index's for everything within the view */ -.fc-view > table { /* so dragged elements can be above the view's main element */ - position: relative; - z-index: 1; -} - - - -/* BasicView ---------------------------------------------------------------------------------------------------*/ - -/* day row structure */ - -.fc-basicWeek-view .fc-content-skeleton, -.fc-basicDay-view .fc-content-skeleton { - /* there may be week numbers in these views, so no padding-top */ - padding-bottom: 1em; /* ensure a space at bottom of cell for user selecting/clicking */ -} - -.fc-basic-view .fc-body .fc-row { - min-height: 4em; /* ensure that all rows are at least this tall */ -} - -/* a "rigid" row will take up a constant amount of height because content-skeleton is absolute */ - -.fc-row.fc-rigid { - overflow: hidden; -} - -.fc-row.fc-rigid .fc-content-skeleton { - position: absolute; - top: 0; - left: 0; - right: 0; -} - -/* week and day number styling */ - -.fc-day-top.fc-other-month { - opacity: 0.3; -} - -.fc-basic-view .fc-week-number, -.fc-basic-view .fc-day-number { - padding: 2px; -} - -.fc-basic-view th.fc-week-number, -.fc-basic-view th.fc-day-number { - padding: 0 2px; /* column headers can't have as much v space */ -} - -.fc-ltr .fc-basic-view .fc-day-top .fc-day-number { float: right; } -.fc-rtl .fc-basic-view .fc-day-top .fc-day-number { float: left; } - -.fc-ltr .fc-basic-view .fc-day-top .fc-week-number { float: left; border-radius: 0 0 3px 0; } -.fc-rtl .fc-basic-view .fc-day-top .fc-week-number { float: right; border-radius: 0 0 0 3px; } - -.fc-basic-view .fc-day-top .fc-week-number { - min-width: 1.5em; - text-align: center; - background-color: #f2f2f2; - color: #808080; -} - -/* when week/day number have own column */ - -.fc-basic-view td.fc-week-number { - text-align: center; -} - -.fc-basic-view td.fc-week-number > * { - /* work around the way we do column resizing and ensure a minimum width */ - display: inline-block; - min-width: 1.25em; -} - - -/* AgendaView all-day area ---------------------------------------------------------------------------------------------------*/ - -.fc-agenda-view .fc-day-grid { - position: relative; - z-index: 2; /* so the "more.." popover will be over the time grid */ -} - -.fc-agenda-view .fc-day-grid .fc-row { - min-height: 3em; /* all-day section will never get shorter than this */ -} - -.fc-agenda-view .fc-day-grid .fc-row .fc-content-skeleton { - padding-bottom: 1em; /* give space underneath events for clicking/selecting days */ -} - - -/* TimeGrid axis running down the side (for both the all-day area and the slot area) ---------------------------------------------------------------------------------------------------*/ - -.fc .fc-axis { /* .fc to overcome default cell styles */ - vertical-align: middle; - padding: 0 4px; - white-space: nowrap; -} - -.fc-ltr .fc-axis { - text-align: right; -} - -.fc-rtl .fc-axis { - text-align: left; -} - -.ui-widget td.fc-axis { - font-weight: normal; /* overcome jqui theme making it bold */ -} - - -/* TimeGrid Structure ---------------------------------------------------------------------------------------------------*/ - -.fc-time-grid-container, /* so scroll container's z-index is below all-day */ -.fc-time-grid { /* so slats/bg/content/etc positions get scoped within here */ - position: relative; - z-index: 1; -} - -.fc-time-grid { - min-height: 100%; /* so if height setting is 'auto', .fc-bg stretches to fill height */ -} - -.fc-time-grid table { /* don't put outer borders on slats/bg/content/etc */ - border: 0 hidden transparent; -} - -.fc-time-grid > .fc-bg { - z-index: 1; -} - -.fc-time-grid .fc-slats, -.fc-time-grid > hr { /* the <hr> AgendaView injects when grid is shorter than scroller */ - position: relative; - z-index: 2; -} - -.fc-time-grid .fc-content-col { - position: relative; /* because now-indicator lives directly inside */ -} - -.fc-time-grid .fc-content-skeleton { - position: absolute; - z-index: 3; - top: 0; - left: 0; - right: 0; -} - -/* divs within a cell within the fc-content-skeleton */ - -.fc-time-grid .fc-business-container { - position: relative; - z-index: 1; -} - -.fc-time-grid .fc-bgevent-container { - position: relative; - z-index: 2; -} - -.fc-time-grid .fc-highlight-container { - position: relative; - z-index: 3; -} - -.fc-time-grid .fc-event-container { - position: relative; - z-index: 4; -} - -.fc-time-grid .fc-now-indicator-line { - z-index: 5; -} - -.fc-time-grid .fc-helper-container { /* also is fc-event-container */ - position: relative; - z-index: 6; -} - - -/* TimeGrid Slats (lines that run horizontally) ---------------------------------------------------------------------------------------------------*/ - -.fc-time-grid .fc-slats td { - height: 1.5em; - border-bottom: 0; /* each cell is responsible for its top border */ -} - -.fc-time-grid .fc-slats .fc-minor td { - border-top-style: dotted; -} - -.fc-time-grid .fc-slats .ui-widget-content { /* for jqui theme */ - background: none; /* see through to fc-bg */ -} - - -/* TimeGrid Highlighting Slots ---------------------------------------------------------------------------------------------------*/ - -.fc-time-grid .fc-highlight-container { /* a div within a cell within the fc-highlight-skeleton */ - position: relative; /* scopes the left/right of the fc-highlight to be in the column */ -} - -.fc-time-grid .fc-highlight { - position: absolute; - left: 0; - right: 0; - /* top and bottom will be in by JS */ -} - - -/* TimeGrid Event Containment ---------------------------------------------------------------------------------------------------*/ - -.fc-ltr .fc-time-grid .fc-event-container { /* space on the sides of events for LTR (default) */ - margin: 0 2.5% 0 2px; -} - -.fc-rtl .fc-time-grid .fc-event-container { /* space on the sides of events for RTL */ - margin: 0 2px 0 2.5%; -} - -.fc-time-grid .fc-event, -.fc-time-grid .fc-bgevent { - position: absolute; - z-index: 1; /* scope inner z-index's */ -} - -.fc-time-grid .fc-bgevent { - /* background events always span full width */ - left: 0; - right: 0; -} - - -/* Generic Vertical Event ---------------------------------------------------------------------------------------------------*/ - -.fc-v-event.fc-not-start { /* events that are continuing from another day */ - /* replace space made by the top border with padding */ - border-top-width: 0; - padding-top: 1px; - - /* remove top rounded corners */ - border-top-left-radius: 0; - border-top-right-radius: 0; -} - -.fc-v-event.fc-not-end { - /* replace space made by the top border with padding */ - border-bottom-width: 0; - padding-bottom: 1px; - - /* remove bottom rounded corners */ - border-bottom-left-radius: 0; - border-bottom-right-radius: 0; -} - - -/* TimeGrid Event Styling ----------------------------------------------------------------------------------------------------- -We use the full "fc-time-grid-event" class instead of using descendants because the event won't -be a descendant of the grid when it is being dragged. -*/ - -.fc-time-grid-event { - overflow: hidden; /* don't let the bg flow over rounded corners */ -} - -.fc-time-grid-event.fc-selected { - /* need to allow touch resizers to extend outside event's bounding box */ - /* common fc-selected styles hide the fc-bg, so don't need this anyway */ - overflow: visible; -} - -.fc-time-grid-event.fc-selected .fc-bg { - display: none; /* hide semi-white background, to appear darker */ -} - -.fc-time-grid-event .fc-content { - overflow: hidden; /* for when .fc-selected */ -} - -.fc-time-grid-event .fc-time, -.fc-time-grid-event .fc-title { - padding: 0 1px; -} - -.fc-time-grid-event .fc-time { - font-size: .85em; - white-space: nowrap; -} - -/* short mode, where time and title are on the same line */ - -.fc-time-grid-event.fc-short .fc-content { - /* don't wrap to second line (now that contents will be inline) */ - white-space: nowrap; -} - -.fc-time-grid-event.fc-short .fc-time, -.fc-time-grid-event.fc-short .fc-title { - /* put the time and title on the same line */ - display: inline-block; - vertical-align: top; -} - -.fc-time-grid-event.fc-short .fc-time span { - display: none; /* don't display the full time text... */ -} - -.fc-time-grid-event.fc-short .fc-time:before { - content: attr(data-start); /* ...instead, display only the start time */ -} - -.fc-time-grid-event.fc-short .fc-time:after { - content: "\000A0-\000A0"; /* seperate with a dash, wrapped in nbsp's */ -} - -.fc-time-grid-event.fc-short .fc-title { - font-size: .85em; /* make the title text the same size as the time */ - padding: 0; /* undo padding from above */ -} - -/* resizer (cursor device) */ - -.fc-time-grid-event.fc-allow-mouse-resize .fc-resizer { - left: 0; - right: 0; - bottom: 0; - height: 8px; - overflow: hidden; - line-height: 8px; - font-size: 11px; - font-family: monospace; - text-align: center; - cursor: s-resize; -} - -.fc-time-grid-event.fc-allow-mouse-resize .fc-resizer:after { - content: "="; -} - -/* resizer (touch device) */ - -.fc-time-grid-event.fc-selected .fc-resizer { - /* 10x10 dot */ - border-radius: 5px; - border-width: 1px; - width: 8px; - height: 8px; - border-style: solid; - border-color: inherit; - background: #fff; - /* horizontally center */ - left: 50%; - margin-left: -5px; - /* center on the bottom edge */ - bottom: -5px; -} - - -/* Now Indicator ---------------------------------------------------------------------------------------------------*/ - -.fc-time-grid .fc-now-indicator-line { - border-top-width: 1px; - left: 0; - right: 0; -} - -/* arrow on axis */ - -.fc-time-grid .fc-now-indicator-arrow { - margin-top: -5px; /* vertically center on top coordinate */ -} - -.fc-ltr .fc-time-grid .fc-now-indicator-arrow { - left: 0; - /* triangle pointing right... */ - border-width: 5px 0 5px 6px; - border-top-color: transparent; - border-bottom-color: transparent; -} - -.fc-rtl .fc-time-grid .fc-now-indicator-arrow { - right: 0; - /* triangle pointing left... */ - border-width: 5px 6px 5px 0; - border-top-color: transparent; - border-bottom-color: transparent; -} - - - -/* List View ---------------------------------------------------------------------------------------------------*/ - -/* possibly reusable */ - -.fc-event-dot { - display: inline-block; - width: 10px; - height: 10px; - border-radius: 5px; -} - -/* view wrapper */ - -.fc-rtl .fc-list-view { - direction: rtl; /* unlike core views, leverage browser RTL */ -} - -.fc-list-view { - border-width: 1px; - border-style: solid; -} - -/* table resets */ - -.fc .fc-list-table { - table-layout: auto; /* for shrinkwrapping cell content */ -} - -.fc-list-table td { - border-width: 1px 0 0; - padding: 8px 14px; -} - -.fc-list-table tr:first-child td { - border-top-width: 0; -} - -/* day headings with the list */ - -.fc-list-heading { - border-bottom-width: 1px; -} - -.fc-list-heading td { - font-weight: bold; -} - -.fc-ltr .fc-list-heading-main { float: left; } -.fc-ltr .fc-list-heading-alt { float: right; } - -.fc-rtl .fc-list-heading-main { float: right; } -.fc-rtl .fc-list-heading-alt { float: left; } - -/* event list items */ - -.fc-list-item.fc-has-url { - cursor: pointer; /* whole row will be clickable */ -} - -.fc-list-item:hover td { - background-color: #f5f5f5; -} - -.fc-list-item-marker, -.fc-list-item-time { - white-space: nowrap; - width: 1px; -} - -/* make the dot closer to the event title */ -.fc-ltr .fc-list-item-marker { padding-right: 0; } -.fc-rtl .fc-list-item-marker { padding-left: 0; } - -.fc-list-item-title a { - /* every event title cell has an <a> tag */ - text-decoration: none; - color: inherit; -} - -.fc-list-item-title a[href]:hover { - /* hover effect only on titles with hrefs */ - text-decoration: underline; -} - -/* message when no events */ - -.fc-list-empty-wrap2 { - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; -} - -.fc-list-empty-wrap1 { - width: 100%; - height: 100%; - display: table; -} - -.fc-list-empty { - display: table-cell; - vertical-align: middle; - text-align: center; -} - -.fc-unthemed .fc-list-empty { /* theme will provide own background */ - background-color: #eee; -} diff --git a/src/UI/Content/icons.less b/src/UI/Content/icons.less deleted file mode 100644 index a3eb16c29..000000000 --- a/src/UI/Content/icons.less +++ /dev/null @@ -1,505 +0,0 @@ -@import "FontAwesome/font-awesome"; -@import "Bootstrap/variables"; -@import "variables"; - -/* Icon rotations and mirroring */ -.fa-rotate-90() { - -webkit-transform : rotate(90deg); - -moz-transform : rotate(90deg); - -ms-transform : rotate(90deg); - -o-transform : rotate(90deg); - transform : rotate(90deg); - filter : progid:DXImageTransform.Microsoft.BasicImage(rotation=1); -} - -.fa-rotate-180() { - -webkit-transform : rotate(180deg); - -moz-transform : rotate(180deg); - -ms-transform : rotate(180deg); - -o-transform : rotate(180deg); - transform : rotate(180deg); - filter : progid:DXImageTransform.Microsoft.BasicImage(rotation=2); -} - -.fa-rotate-270() { - -webkit-transform : rotate(270deg); - -moz-transform : rotate(270deg); - -ms-transform : rotate(270deg); - -o-transform : rotate(270deg); - transform : rotate(270deg); - filter : progid:DXImageTransform.Microsoft.BasicImage(rotation=3); -} - -.fa-flip-horizontal() { - -webkit-transform : scale(-1, 1); - -moz-transform : scale(-1, 1); - -ms-transform : scale(-1, 1); - -o-transform : scale(-1, 1); - transform : scale(-1, 1); -} - -.fa-flip-vertical() { - -webkit-transform : scale(1, -1); - -moz-transform : scale(1, -1); - -ms-transform : scale(1, -1); - -o-transform : scale(1, -1); - transform : scale(1, -1); -} - -.fa-icon-content(@fa-icon) { - .fa-icon(); - &:before { content: @fa-icon; } -} - -.fa-icon-color(@color) { - &:before { color: @color; } -} - -.icon-lidarr-warning { - .fa-icon-content(@fa-var-exclamation-triangle); - .fa-icon-color(@brand-warning); -} - -.icon-lidarr-edit { - .fa-icon-content(@fa-var-wrench); -} - -.icon-lidarr-blacklist { - .fa-icon-content(@fa-var-ban); - .fa-icon-color(@brand-danger); - -} - -.icon-lidarr-spinner { - .fa-icon-content(@fa-var-spinner); -} - -.fa-spin-overlay { - .fa-icon(); - position : relative; - text-align : center; - vertical-align : baseline; - - i { - opacity : 0.0; - margin : 0 !important; - - &.icon-lidarr-spinner { - opacity : 1.0; - margin : 0 -0.5em !important; - } - } - - span { - position : absolute; - top : 0; - left : 0; - right : 0; - bottom : 0; - } -} - -.icon-lidarr-rename { - .fa-icon-content(@fa-var-sitemap) -} - -.icon-lidarr-add { - .fa-icon-content(@fa-var-plus); -} - -.icon-lidarr-form-info { - .fa-icon-content(@fa-var-question-circle); -} - -.icon-lidarr-form-warning { - .fa-icon-content(@fa-var-exclamation-triangle); - .fa-icon-color(@brand-warning); -} - -.icon-lidarr-form-danger { - .fa-icon-content(@fa-var-exclamation-circle); - .fa-icon-color(@brand-danger); -} - -.icon-lidarr-form-info-link { - .clickable(); - .fa-icon-content(@fa-var-info-circle); - .fa-icon-color(@brand-primary) -} - -.icon-lidarr-form-external-link { - .fa-icon-content(@fa-var-external-link); -} - -.icon-lidarr-update { - .fa-icon-content(@fa-var-download); -} - -.icon-lidarr-download { - .fa-icon-content(@fa-var-download); -} - -.icon-lidarr-downloading { - .fa-icon-content(@fa-var-cloud-download); -} - -.icon-lidarr-downloaded { - .fa-icon-content(@fa-var-inbox); -} - -.icon-lidarr-pending { - .fa-icon-content(@fa-var-clock-o); -} - -.icon-lidarr-queued { - .fa-icon-content(@fa-var-cloud); -} - -.icon-lidarr-paused { - .fa-icon-content(@fa-var-pause); -} - -.icon-lidarr-active { - .fa-icon-content(@fa-var-play); -} - -.icon-lidarr-tba { - .fa-icon-content(@fa-var-question-circle); -} - -.icon-lidarr-missing { - .fa-icon-content(@fa-var-exclamation-triangle); -} - -.icon-lidarr-not-aired { - .fa-icon-content(@fa-var-clock-o); -} - -.icon-lidarr-import { - .fa-icon-content(@fa-var-inbox); -} - -.icon-lidarr-import-manual { - .fa-icon-content(@fa-var-user); -} - -.icon-lidarr-imported { - .fa-icon-content(@fa-var-download); -} - -.icon-lidarr-status { - .fa-icon-content(@fa-var-circle); -} - -.icon-lidarr-monitored { - .fa-icon-content(@fa-var-bookmark); -} - -.icon-lidarr-unmonitored { - .fa-icon-content(@fa-var-bookmark-o); -} - -.icon-lidarr-log-info { - .fa-icon-content(@fa-var-info-circle); - .fa-icon-color(dodgerblue); -} - -.icon-lidarr-log-debug { - .fa-icon-content(@fa-var-info-circle); - .fa-icon-color(gray); -} - -.icon-lidarr-log-trace { - .fa-icon-content(@fa-var-info-circle); - .fa-icon-color(lightgrey); -} - -.icon-lidarr-log-warn { - .fa-icon-content(@fa-var-exclamation-circle); - .fa-icon-color(@brand-warning); -} - -.icon-lidarr-log-error { - .fa-icon-content(@fa-var-bug); - .fa-icon-color(@brand-danger); -} - -.icon-lidarr-log-fatal { - .fa-icon-content(@fa-var-times-circle); - .fa-icon-color(purple); -} - -.icon-lidarr-import-failed { - .fa-icon-content(@fa-var-download); - .fa-icon-color(@brand-danger); -} - -.icon-lidarr-download-failed { - .fa-icon-content(@fa-var-cloud-download); - .fa-icon-color(@brand-danger); -} - -.icon-lidarr-download-warning { - .fa-icon-content(@fa-var-cloud-download); - .fa-icon-color(@brand-warning); -} - -.icon-lidarr-shutdown { - .fa-icon-content(@fa-var-power-off); - .fa-icon-color(@brand-danger); -} - -.icon-lidarr-restart { - .fa-icon-content(@fa-var-repeat); -} - -.icon-lidarr-health-warning { - .fa-icon-content(@fa-var-exclamation-circle); - .fa-icon-color(@brand-warning); -} - -.icon-lidarr-health-error { - .fa-icon-content(@fa-var-exclamation-circle); - .fa-icon-color(@brand-danger); -} - -.icon-lidarr-search { - .fa-icon-content(@fa-var-search); -} - -.icon-lidarr-search-manual { - .fa-icon-content(@fa-var-user); -} - -.icon-lidarr-search-automatic { - .fa-icon-content(@fa-var-rocket); -} - -.icon-lidarr-delete { - .fa-icon-content(@fa-var-remove); - .fa-icon-color(@brand-danger); -} - -.icon-lidarr-deleted { - .fa-icon-content(@fa-var-trash); -} - -.icon-lidarr-clear { - .fa-icon-content(@fa-var-trash); -} - -.icon-lidarr-existing { - .fa-icon-content(@fa-var-minus); - .fa-icon-color(@brand-danger); -} - -.icon-lidarr-suggested { - .fa-icon-content(@fa-var-plus); - .fa-icon-color(@brand-success); -} - -.icon-lidarr-info { - .fa-icon-content(@fa-var-info-circle); -} - -.icon-lidarr-all { - .fa-icon-content(@fa-var-circle-o); -} - -//Navbar -.icon-lidarr-navbar-collapsed { - .fa-icon-content(@fa-var-bars); -} - -.icon-lidarr-navbar-artist { - .fa-icon-content(@fa-var-music); -} - -.icon-lidarr-navbar-calendar { - .fa-icon-content(@fa-var-calendar); -} - -.icon-lidarr-navbar-activity { - .fa-icon-content(@fa-var-clock-o); -} - -.icon-lidarr-navbar-wanted { - .fa-icon-content(@fa-var-exclamation-triangle); -} - -.icon-lidarr-navbar-settings { - .fa-icon-content(@fa-var-cogs); -} - -.icon-lidarr-navbar-system { - .fa-icon-content(@fa-var-laptop); -} - -.icon-lidarr-navbar-donate { - .fa-icon-content(@fa-var-heart); - .fa-icon-color(@nzbdroneRed); -} - -.icon-lidarr-back-to-top { - .fa-icon-content(@fa-var-arrow-circle-up); -} - -.icon-lidarr-hdd { - .fa-icon-content(@fa-var-hdd-o); -} - -.icon-lidarr-copy { - .fa-icon-content(@fa-var-clipboard); -} - -.icon-lidarr-unknown { - .fa-icon-content(@fa-var-question); -} - -.icon-lidarr-load-more { - .fa-icon-content(@fa-var-angle-down); -} - -.icon-lidarr-ok { - .fa-icon-content(@fa-var-check); -} - -.icon-lidarr-calendar-o { - .fa-icon-content(@fa-var-calendar-o); -} - -.icon-lidarr-folder-open { - .fa-icon-content(@fa-var-folder-open); -} - -.icon-lidarr-refresh { - .fa-icon-content(@fa-var-refresh); -} - -.icon-lidarr-artist-ended { - .fa-icon-content(@fa-var-stop); -} - -.icon-lidarr-artist-continuing { - .fa-icon-content(@fa-var-play); -} - -.icon-lidarr-artist-unmonitored { - .fa-icon-content(@fa-var-pause); -} - -.icon-lidarr-checked { - .fa-icon-content(@fa-var-check-square); -} - -.icon-lidarr-unchecked { - .fa-icon-content(@fa-var-square-o); -} - -.icon-lidarr-expand { - .fa-icon-content(@fa-var-chevron-right); -} - -.icon-lidarr-expanded { - .fa-icon-content(@fa-var-chevron-down); -} - -.icon-lidarr-panel-show { - .fa-icon-content(@fa-var-chevron-circle-down); -} - -.icon-lidarr-panel-hide { - .fa-icon-content(@fa-var-chevron-circle-up); -} - -.icon-lidarr-comment { - .fa-icon-content(@fa-var-comment) -} - -.icon-lidarr-rss { - .fa-icon-content(@fa-var-rss) -} - -.icon-lidarr-view-poster { - .fa-icon-content(@fa-var-th-large) -} - -.icon-lidarr-view-list { - .fa-icon-content(@fa-var-th-list) -} - -.icon-lidarr-view-table { - .fa-icon-content(@fa-var-table) -} - -.icon-lidarr-reorder { - .fa-icon-content(@fa-var-bars); -} - -.icon-lidarr-browser-computer { - .fa-icon-content(@fa-var-desktop); -} - -.icon-lidarr-browser-up { - .fa-icon-content(@fa-var-level-up); -} - -.icon-lidarr-browser-folder { - .fa-icon-content(@fa-var-folder-o); -} - -.icon-lidarr-browser-file { - .fa-icon-content(@fa-var-file-o); -} - -.icon-lidarr-sort-asc { - .fa-icon-content(@fa-var-sort-asc); -} - -.icon-lidarr-sort-desc { - .fa-icon-content(@fa-var-sort-desc); -} - -.icon-lidarr-pager-first { - .fa-icon-content(@fa-var-fast-backward); -} - -.icon-lidarr-pager-previous { - .fa-icon-content(@fa-var-backward); -} - -.icon-lidarr-pager-next { - .fa-icon-content(@fa-var-forward); -} - -.icon-lidarr-pager-last { - .fa-icon-content(@fa-var-fast-forward); -} - -.icon-lidarr-logout { - .fa-icon-content(@fa-var-sign-out); -} - -.icon-lidarr-file-text { - .fa-icon-content(@fa-var-file-text); -} - -.icon-lidarr-backup-scheduled { - .fa-icon-content(@fa-var-clock-o); -} - -.icon-lidarr-backup-manual { - .fa-icon-content(@fa-var-book); -} - -.icon-lidarr-backup-update { - .fa-icon-content(@fa-var-retweet); -} - -.icon-lidarr-track-file { - .fa-icon-content(@fa-var-file-video-o); -} - -.icon-lidarr-header-rejections { - .fa-icon-content(@fa-var-exclamation-circle); -} \ No newline at end of file diff --git a/src/UI/Content/legend.less b/src/UI/Content/legend.less deleted file mode 100644 index 2335acd30..000000000 --- a/src/UI/Content/legend.less +++ /dev/null @@ -1,32 +0,0 @@ -@import "./Bootstrap/mixins"; - -.legend { - margin: 5px; - - ul { - margin: 0; - margin-bottom: 5px; - padding: 0; - float: left; - list-style: none; - - li { - font-size: 80%; - list-style: none; - margin-left: 0; - line-height: 18px; - margin-bottom: 2px; - - span { - display: block; - float: left; - height: 16px; - width: 30px; - margin-right: 5px; - margin-left: 0; - border: none; - border-radius: 3px; - } - } - } -} diff --git a/src/UI/Content/mixins.less b/src/UI/Content/mixins.less deleted file mode 100644 index d6da04c3a..000000000 --- a/src/UI/Content/mixins.less +++ /dev/null @@ -1,21 +0,0 @@ -.selectable() { - -moz-user-select : all; - -webkit-user-select : all; - -ms-user-select : all; -} - -.not-selectable() { - -moz-user-select : none; - -webkit-user-select : none; - -ms-user-select : none; -} - -.color-impaired-background-gradient(@angle, @color) { - .color-impaired-mode & { - background : repeating-linear-gradient(@angle, - darken(@color, 3%), - darken(@color, 3%) 6px, - @color 6px, - @color 12px); - } -} diff --git a/src/UI/Content/navbar.less b/src/UI/Content/navbar.less deleted file mode 100644 index bc220bdd4..000000000 --- a/src/UI/Content/navbar.less +++ /dev/null @@ -1,235 +0,0 @@ -@import "prefixer"; -@import "variables"; - -@grid-float-breakpoint: @screen-xs-min; - -.backdrop { - .navbar-nzbdrone { - .opacity(0.85); - background-color : #000000; - padding-bottom: 10px; - z-index: 10; - } -} - -.navbar-nzbdrone { - text-align : center; - - i:before { - font-size : 35px; - display : block; - margin-bottom : 1px; - } - - .icon-lidarr-navbar-icon { - display: inline; - } - - .navbar-nav, .navbar-nav>li { - float : none; - } - - .navbar-toggle { - border-color: #333; - - &:hover, - &:focus { - color : #222; - background-color : #333; - } - } - - .navbar-brand { - position: absolute; - - @media (max-width: @screen-xs-max) { - padding: 9px 15px; - font-size: 14px; - } - - @media (min-width: @screen-sm-min) and (max-width: @screen-sm-max) { - padding: 20px 15px; - } - - @media (min-width: @screen-md-min) and (max-width: @screen-md-max) { - padding: 30px 15px; - } - - @media (min-width: @screen-lg-min) { - padding: 22px 15px; - } - } - - .logo-text { - color: white; - font-weight: 300; - - .highlight { - font-weight: 400; - color: @droneTeal; - } - } - - li { - list-style-type : none; - display : inline-block; - position : relative; - - a { - display : block; - color : #b9b9b9; - font-weight : 100; - - &:focus { - background-color : transparent; - text-decoration : none; - } - - &:hover { - background-color : #555555; - text-decoration : none; - } - - .label { - cursor: pointer; - } - - @media (min-width: @screen-sm-min) and (max-width: @screen-sm-max) { - border-radius : 6px; - padding : 5px 0px 5px; - min-height : 76px; - min-width : 64px; - margin : 20px 5px 5px; - } - - @media (min-width: @screen-md-min) and (max-width: @screen-md-max) { - border-radius : 6px; - padding : 15px 10px 5px; - min-height : 76px; - min-width : 64px; - margin : 20px 10px 5px; - } - - @media (min-width: @screen-lg-min) { - border-radius : 6px; - padding : 15px 10px 5px; - min-height : 76px; - min-width : 84px; - margin : 20px 10px 5px; - } - } - - .navbar-info { - .label { - position : absolute; - top : 10px; - right : 10px; - padding-left : 4px; - padding-right : 4px; - } - } - } - - @media (max-width: @screen-xs-max) { - text-align : left; - - i:before { - font-size : 14px; - display: inline-block; - } - - li { - display: block; - - a:hover { - background-color: transparent; - } - - .navbar-info { - margin-left: 5px; - - .label { - position : static; - } - } - } - } - - @media (max-width: @screen-xs-max) { - .navbar-collapse { - .navbar-nav { - li { - &:focus, &:hover { - background-color : #555555; - } - } - } - } - } -} - -.search { - - i:before { - font-size: 14px; - } - - .input-group { - input, .input-group-addon { - background-color: #333333; - } - } - - input, .input-group-addon { - border-color: #333333; - color: #cccccc; - } - - ul { - text-align: left; - } - - .tt-dropdown-menu { - - background-color: #333333; - color: #cccccc; - .opacity(0.95); - - .tt-suggestion { - color: #cccccc; - cursor: pointer; - - &.tt-cursor { - //item selected - - background-color: @droneTeal; - color: #222222; - - a { - //link in item selected - color: #222222; - } - } - } - } - - ::-webkit-input-placeholder { - color: #cccccc; - opacity: 0.25; - } - - :-moz-placeholder { /* Firefox 18- */ - color: #cccccc; - opacity: 0.25; - } - - ::-moz-placeholder { /* Firefox 19+ */ - color: #cccccc; - opacity: 0.25; - } - - :-ms-input-placeholder { - color: #cccccc; - opacity: 0.25; - } -} diff --git a/src/UI/Content/overrides.less b/src/UI/Content/overrides.less deleted file mode 100644 index abee53862..000000000 --- a/src/UI/Content/overrides.less +++ /dev/null @@ -1,6 +0,0 @@ -@import "Overrides/bootstrap"; -@import "Overrides/browser"; -@import "Overrides/bootstrap.toggle-switch"; -@import "Overrides/bootstrap.tagsinput.less"; -@import "Overrides/fullcalendar"; -@import "Overrides/messenger"; diff --git a/src/UI/Content/prefixer.less b/src/UI/Content/prefixer.less deleted file mode 100644 index c9040baa1..000000000 --- a/src/UI/Content/prefixer.less +++ /dev/null @@ -1,344 +0,0 @@ -//--------------------------------------------------- -// LESS Prefixer -//--------------------------------------------------- -// -// All of the CSS3 fun, none of the prefixes! -// -// As a rule, you can use the CSS properties you -// would expect just by adding a '.': -// -// box-shadow => .box-shadow(@args) -// -// Also, when shorthand is available, arguments are -// not parameterized. Learn CSS, not LESS Prefixer. -// -// ------------------------------------------------- -// TABLE OF CONTENTS -// (*) denotes a syntax-sugar helper -// ------------------------------------------------- -// -// .animation(@args) -// .animation-delay(@delay) -// .animation-direction(@direction) -// .animation-duration(@duration) -// .animation-iteration-count(@count) -// .animation-name(@name) -// .animation-play-state(@state) -// .animation-timing-function(@function) -// .background-size(@args) -// .border-radius(@args) -// .box-shadow(@args) -// .inner-shadow(@args) * -// .box-sizing(@args) -// .border-box() * -// .content-box() * -// .columns(@args) -// .column-count(@count) -// .column-gap(@gap) -// .column-rule(@args) -// .column-width(@width) -// .gradient(@default,@start,@stop) * -// .linear-gradient-top(@default,@color1,@stop1,@color2,@stop2,[@color3,@stop3,@color4,@stop4])* -// .linear-gradient-left(@default,@color1,@stop1,@color2,@stop2,[@color3,@stop3,@color4,@stop4])* -// .opacity(@factor) -// .transform(@args) -// .rotate(@deg) -// .scale(@factor) -// .translate(@x,@y) -// .translate3d(@x,@y,@z) -// .translateHardware(@x,@y) * -// .text-shadow(@args) -// .transition(@args) -// .transition-delay(@delay) -// .transition-duration(@duration) -// .transition-property(@property) -// .transition-timing-function(@function) -// -// -// -// Credit to LESS Elements for the motivation and -// to CSS3Please.com for implementation. -// -// Copyright (c) 2012 Joel Sutherland -// MIT Licensed: -// http://www.opensource.org/licenses/mit-license.php -// -//--------------------------------------------------- - - -// Animation - -.animation(@args) { - -webkit-animation: @args; - -moz-animation: @args; - -ms-animation: @args; - -o-animation: @args; - animation: @args; -} -.animation-delay(@delay) { - -webkit-animation-delay: @delay; - -moz-animation-delay: @delay; - -ms-animation-delay: @delay; - -o-animation-delay: @delay; - animation-delay: @delay; -} -.animation-direction(@direction) { - -webkit-animation-direction: @direction; - -moz-animation-direction: @direction; - -ms-animation-direction: @direction; - -o-animation-direction: @direction; - animation-direction: @direction; -} -.animation-duration(@duration) { - -webkit-animation-duration: @duration; - -moz-animation-duration: @duration; - -ms-animation-duration: @duration; - -o-animation-duration: @duration; - animation-duration: @duration; -} -.animation-iteration-count(@count) { - -webkit-animation-iteration-count: @count; - -moz-animation-iteration-count: @count; - -ms-animation-iteration-count: @count; - -o-animation-iteration-count: @count; - animation-iteration-count: @count; -} -.animation-name(@name) { - -webkit-animation-name: @name; - -moz-animation-name: @name; - -ms-animation-name: @name; - -o-animation-name: @name; - animation-name: @name; -} -.animation-play-state(@state) { - -webkit-animation-play-state: @state; - -moz-animation-play-state: @state; - -ms-animation-play-state: @state; - -o-animation-play-state: @state; - animation-play-state: @state; -} -.animation-timing-function(@function) { - -webkit-animation-timing-function: @function; - -moz-animation-timing-function: @function; - -ms-animation-timing-function: @function; - -o-animation-timing-function: @function; - animation-timing-function: @function; -} - - -// Background Size - -.background-size(@args) { - -webkit-background-size: @args; - -moz-background-size: @args; - background-size: @args; -} - - -// Border Radius - -.border-radius(@args) { - -webkit-border-radius: @args; - -moz-border-radius: @args; - border-radius: @args; - - -webkit-background-clip: padding-box; - -moz-background-clip: padding; - background-clip: padding-box; -} - - -// Box Shadows - -.box-shadow(@args) { - -webkit-box-shadow: @args; - -moz-box-shadow: @args; - box-shadow: @args; -} -.inner-shadow(@args) { - .box-shadow(inset @args); -} - - -// Box Sizing - -.box-sizing(@args){ - -webkit-box-sizing: @args; - -moz-box-sizing: @args; - box-sizing: @args; -} -.border-box(){ - .box-sizing(border-box); -} -.content-box(){ - .box-sizing(content-box); -} - - -// Columns - -.columns(@args){ - -webkit-columns: @args; - -moz-columns: @args; - columns: @args; -} -.column-count(@count) { - -webkit-column-count: @count; - -moz-column-count: @count; - column-count: @count; -} -.column-gap(@gap) { - -webkit-column-gap: @gap; - -moz-column-gap: @gap; - column-gap: @gap; -} -.column-width(@width){ - -webkit-column-width: @width; - -moz-column-width: @width; - column-width: @width; -} -.column-rule(@args){ - -webkit-column-rule: @args; - -moz-column-rule: @args; - column-rule: @args; -} - - -// Gradients - -.gradient(@default: #F5F5F5, @start: #EEE, @stop: #FFF) { - .linear-gradient-top(@default,@start,0%,@stop,100%); -} -.linear-gradient-top(@default,@color1,@stop1,@color2,@stop2) { - background-color: @default; - background-image: -webkit-gradient(linear, left top, left bottom, color-stop(@stop1, @color1), color-stop(@stop2 @color2)); - background-image: -webkit-linear-gradient(top, @color1 @stop1, @color2 @stop2); - background-image: -moz-linear-gradient(top, @color1 @stop1, @color2 @stop2); - background-image: -ms-linear-gradient(top, @color1 @stop1, @color2 @stop2); - background-image: -o-linear-gradient(top, @color1 @stop1, @color2 @stop2); - background-image: linear-gradient(top, @color1 @stop1, @color2 @stop2); -} -.linear-gradient-top(@default,@color1,@stop1,@color2,@stop2,@color3,@stop3) { - background-color: @default; - background-image: -webkit-gradient(linear, left top, left bottom, color-stop(@stop1, @color1), color-stop(@stop2 @color2), color-stop(@stop3 @color3)); - background-image: -webkit-linear-gradient(top, @color1 @stop1, @color2 @stop2, @color3 @stop3); - background-image: -moz-linear-gradient(top, @color1 @stop1, @color2 @stop2, @color3 @stop3); - background-image: -ms-linear-gradient(top, @color1 @stop1, @color2 @stop2, @color3 @stop3); - background-image: -o-linear-gradient(top, @color1 @stop1, @color2 @stop2, @color3 @stop3); - background-image: linear-gradient(top, @color1 @stop1, @color2 @stop2, @color3 @stop3); -} -.linear-gradient-top(@default,@color1,@stop1,@color2,@stop2,@color3,@stop3,@color4,@stop4) { - background-color: @default; - background-image: -webkit-gradient(linear, left top, left bottom, color-stop(@stop1, @color1), color-stop(@stop2 @color2), color-stop(@stop3 @color3), color-stop(@stop4 @color4)); - background-image: -webkit-linear-gradient(top, @color1 @stop1, @color2 @stop2, @color3 @stop3, @color4 @stop4); - background-image: -moz-linear-gradient(top, @color1 @stop1, @color2 @stop2, @color3 @stop3, @color4 @stop4); - background-image: -ms-linear-gradient(top, @color1 @stop1, @color2 @stop2, @color3 @stop3, @color4 @stop4); - background-image: -o-linear-gradient(top, @color1 @stop1, @color2 @stop2, @color3 @stop3, @color4 @stop4); - background-image: linear-gradient(top, @color1 @stop1, @color2 @stop2, @color3 @stop3, @color4 @stop4); -} -.linear-gradient-left(@default,@color1,@stop1,@color2,@stop2) { - background-color: @default; - background-image: -webkit-gradient(linear, left top, left top, color-stop(@stop1, @color1), color-stop(@stop2 @color2)); - background-image: -webkit-linear-gradient(left, @color1 @stop1, @color2 @stop2); - background-image: -moz-linear-gradient(left, @color1 @stop1, @color2 @stop2); - background-image: -ms-linear-gradient(left, @color1 @stop1, @color2 @stop2); - background-image: -o-linear-gradient(left, @color1 @stop1, @color2 @stop2); - background-image: linear-gradient(left, @color1 @stop1, @color2 @stop2); -} -.linear-gradient-left(@default,@color1,@stop1,@color2,@stop2,@color3,@stop3) { - background-color: @default; - background-image: -webkit-gradient(linear, left top, left top, color-stop(@stop1, @color1), color-stop(@stop2 @color2), color-stop(@stop3 @color3)); - background-image: -webkit-linear-gradient(left, @color1 @stop1, @color2 @stop2, @color3 @stop3); - background-image: -moz-linear-gradient(left, @color1 @stop1, @color2 @stop2, @color3 @stop3); - background-image: -ms-linear-gradient(left, @color1 @stop1, @color2 @stop2, @color3 @stop3); - background-image: -o-linear-gradient(left, @color1 @stop1, @color2 @stop2, @color3 @stop3); - background-image: linear-gradient(left, @color1 @stop1, @color2 @stop2, @color3 @stop3); -} -.linear-gradient-left(@default,@color1,@stop1,@color2,@stop2,@color3,@stop3,@color4,@stop4) { - background-color: @default; - background-image: -webkit-gradient(linear, left top, left top, color-stop(@stop1, @color1), color-stop(@stop2 @color2), color-stop(@stop3 @color3), color-stop(@stop4 @color4)); - background-image: -webkit-linear-gradient(left, @color1 @stop1, @color2 @stop2, @color3 @stop3, @color4 @stop4); - background-image: -moz-linear-gradient(left, @color1 @stop1, @color2 @stop2, @color3 @stop3, @color4 @stop4); - background-image: -ms-linear-gradient(left, @color1 @stop1, @color2 @stop2, @color3 @stop3, @color4 @stop4); - background-image: -o-linear-gradient(left, @color1 @stop1, @color2 @stop2, @color3 @stop3, @color4 @stop4); - background-image: linear-gradient(left, @color1 @stop1, @color2 @stop2, @color3 @stop3, @color4 @stop4); -} - - -// Opacity - -.opacity(@factor){ - opacity: @factor; - @iefactor: @factor*100; - filter: alpha(opacity=@iefactor); -} - - -// Text Shadow - -.text-shadow(@args){ - text-shadow: @args; -} - - -// Transforms - -.transform(@args) { - -webkit-transform: @args; - -moz-transform: @args; - -ms-transform: @args; - -o-transform: @args; - transform: @args; -} -.rotate(@deg:45deg){ - .transform(rotate(@deg)); -} -.scale(@factor:.5){ - .transform(scale(@factor)); -} -.translate(@x,@y){ - .transform(translate(@x,@y)); -} -.translate3d(@x,@y,@z) { - .transform(translate3d(@x,@y,@z)); -} -.translateHardware(@x,@y){ - .translate(@x,@y); - -webkit-transform: translate3d(@x,@y,0); - -moz-transform: translate3d(@x,@y,0); -} - - -// Transitions - -.transition(@args:200ms) { - -webkit-transition: @args; - -moz-transition: @args; - -o-transition: @args; - transition: @args; -} -.transition-delay(@delay:0) { - -webkit-transition-delay: @delay; - -moz-transition-delay: @delay; - -o-transition-delay: @delay; - transition-delay: @delay; -} -.transition-duration(@duration:200ms) { - -webkit-transition-duration: @duration; - -moz-transition-duration: @duration; - -o-transition-duration: @duration; - transition-duration: @duration; -} -.transition-property(@property:all) { - -webkit-transition-property: @property; - -moz-transition-property: @property; - -o-transition-property: @property; - transition-property: @property; -} -.transition-timing-function(@function:ease) { - -webkit-transition-timing-function: @function; - -moz-transition-timing-function: @function; - -o-transition-timing-function: @function; - transition-timing-function: @function; -} - diff --git a/src/UI/Content/progress-bars.less b/src/UI/Content/progress-bars.less deleted file mode 100644 index bc9d058dc..000000000 --- a/src/UI/Content/progress-bars.less +++ /dev/null @@ -1,39 +0,0 @@ -@import "Bootstrap/mixins"; -@import "Bootstrap/variables"; -@import "variables"; - -.progress.track-progress { - position : relative; - margin-bottom : 2px; - - &, .progressbar-back-text, .progressbar-front-text { - width : 125px; - } - - .progressbar-back-text, .progressbar-front-text { - font-size : 12px; - font-weight : bold; - text-align : center; - cursor : default; - line-height : 20px; - } - - .progressbar-back-text { - position : absolute; - height : 100%; - } - - .progressbar-front-text { - display : block; - height : 100%; - } - - .progress-bar { - position : absolute; - overflow : hidden; - } -} - -.progress-bar-purple { - #gradient > .vertical(@purple, @nzbdronePurple); -} diff --git a/src/UI/Content/robots.txt b/src/UI/Content/robots.txt deleted file mode 100644 index 77470cb39..000000000 --- a/src/UI/Content/robots.txt +++ /dev/null @@ -1,2 +0,0 @@ -User-agent: * -Disallow: / \ No newline at end of file diff --git a/src/UI/Content/spinner.less b/src/UI/Content/spinner.less deleted file mode 100644 index 2a02f136b..000000000 --- a/src/UI/Content/spinner.less +++ /dev/null @@ -1,130 +0,0 @@ -@import "prefixer"; -@import "Bootstrap/variables"; - -@colorDark : @gray-dark; -@colorLight : @gray-lighter; - -#followingBalls { - position : relative; - height : 20px; - width : 256px; - margin : 50px auto; - display : block; - - .ball { - background-color : @colorDark; - position : absolute; - top : 0; - left : 0; - width : 20px; - height : 20px; - .border-radius(10px); - .animation-name(bounce); - .animation-duration(1.9s); - .animation-iteration-count(infinite); - .animation-direction(linear); - } - - #ball-1 { - .animation-delay(0s); - } - - #ball-2 { - .animation-delay(0.19s); - } - - #ball-3 { - .animation-delay(0.38s); - } - - #ball-4 { - .animation-delay(0.57s); - } - - @keyframes bounce { - 0% { - left : 0px; - background-color : @colorDark; - } - - 50% { - left : 236px; - background-color : @colorLight; - } - - 100% { - left : 0px; - background-color : @colorDark; - } - } - - @-moz-keyframes bounce { - 0% { - left : 0px; - background-color : @colorDark; - } - - 50% { - left : 236px; - background-color : @colorLight; - } - - 100% { - left : 0px; - background-color : @colorDark; - } - - } - - @-webkit-keyframes bounce { - 0% { - left : 0px; - background-color : @colorDark; - } - - 50% { - left : 236px; - background-color : @colorLight; - } - - 100% { - left : 0px; - background-color : @colorDark; - } - - } - - @-ms-keyframes bounce { - 0% { - left : 0px; - background-color : @colorDark; - } - - 50% { - left : 236px; - background-color : @colorLight; - } - - 100% { - left : 0px; - background-color : @colorDark; - } - } - - @-o-keyframes bounce { - 0% { - left : 0px; - background-color : @colorDark; - } - - 50% { - left : 236px; - background-color : @colorLight; - } - - 100% { - left : 0px; - background-color : @colorDark; - } - } -} diff --git a/src/UI/Content/theme.less b/src/UI/Content/theme.less deleted file mode 100644 index cf943be73..000000000 --- a/src/UI/Content/theme.less +++ /dev/null @@ -1,306 +0,0 @@ -@import "Bootstrap/variables"; -@import "Bootstrap/mixins"; -@import "Bootstrap/type"; -@import "font"; -@import "form"; -@import "navbar"; -@import "Backgrid/backgrid"; -@import "prefixer"; -@import "icons"; -@import "checkbox-button"; -@import "spinner"; -@import "legend"; -@import "progress-bars"; -@import "../Shared/Styles/clickable"; -@import "../Shared/Styles/card"; -@import "../Rename/rename"; -@import "typeahead"; -@import "utilities"; -@import "../Hotkeys/hotkeys"; -@import "../Shared/FileBrowser/filebrowser"; -@import "badges"; -@import "../ManualImport/manualimport"; -@import "../AlbumStudio/albumstudio"; - -.main-region { - @media (min-width : @screen-lg-min) { - padding-left : 30px; - padding-right : 30px; - } -} - -.toolbar { - - &:after { - visibility : hidden; - display : block; - font-size : 0; - content : " "; - clear : both; - height : 0; - } - - .page-toolbar { - margin-top : 10px; - margin-bottom : 30px; - - .toolbar-group { - display : inline-block; - } - - .sorting-buttons { - .sorting-title { - display : inline-block; - width : 110px; - } - } - } -} - -.toolbars { - margin-top : 5px; - margin-bottom : 30px; - - .page-toolbar { - margin-top : 5px; - margin-bottom : 0px; - } -} - -.page-container { - min-height : 500px; -} - -#scroll-up { - - i { - .clickable; - .opacity(0.3); - margin: 0px 20px; - - &:hover { - .opacity(0.4); - } - } - - position : fixed; - z-index : 9999; - bottom : 30px; - right : 0px; - display : none; - font-size : 56px; - color : gray; -} - -.control-panel-visible { - #scroll-up { - bottom : 100px; - } -} - -.label-large { - padding : 4px 6px; - font-size : 16px; -} - -.label-white { - color : black; - background-color : white; -} - -.label-disabled { - opacity : 0.5; -} - -th { - cursor : default; - - &.sortable { - &:hover { - background : @table-bg-hover; - } - .clickable(); - - } -} - -a, .btn { - i { - cursor : pointer; - } -} - -body { - background : url('../Content/Images/background/logo.png') 50px 75px no-repeat; - background-color : #272727; - margin-bottom : 100px; - p { - font-size : 0.9em; - } -} - -.footer { - font-size : 13px; - font-weight : lighter; - padding-top : 0px; - padding-bottom : 20px; - color : #999999; - margin : 0; - text-decoration : none; - - a { - color : #999999; - text-decoration : underline; - } - - p { - margin-bottom : 0px; - } - - #footer-region { - .text-center(); - position : relative; - width : 256px; - margin : 50px auto 0px auto; - display : block; - } -} - -.started #page { - .card(#aaaaaa); - /* width : 1210px; - min-width : 1210px; */ - max-width : 1210px; - margin : auto; - // margin-top : -70px; - padding : 20px 0px; - - .header { - padding-bottom : 10px; - margin-bottom : 20px; - border-bottom : 1px solid #eeeeee; - } -} - -.backdrop #page { - background-color : transparent; - box-shadow : none; -} - -.validation-errors { - i { - padding-right : 5px; - } -} - -.status-primary { - color : @link-color; -} - -.status-success { - color : @state-success-text; -} - -.status-warning { - color : @state-warning-text; -} - -.status-danger { - color : @state-danger-text; -} - -.error { - background : #FF0000; -} - -#errors { - display : none; -} - -.mono-space { - font-family : "ubuntu mono" -} - -.file-path { - .mono-space(); -} - -.control-panel { - .card(#333333); - - color : #f5f5f5; - background-color : #333333; - margin : 0px; - margin-bottom : -100px; - position : fixed; - left : 0; - bottom : 0; - width : 100%; - height : 80px; - opacity : 0; - - @media (max-width : @screen-sm-max) { - height : initial; - position : static; - } -} - -.tab-content { - .tab-pane { - padding-top : 10px; - } -} - -.modal-header { - h3 { - margin-top : 0px; - margin-bottom : 0px; - } -} - -.modal-body { - table { - font-size : 12px; - font-weight : bold; - - i { - font-size : 14px; - } - } -} - -.tooltip { - .tooltip-inner { - max-width : 250px; - } -} - -dl.info { - dt, dd { - padding-bottom : 5px; - } -} - -.label { - &.protocol-torrent { - background-color : #00853D; - } - - &.protocol-usenet { - background-color : #17B1D9; - } -} - -.login { - color : #ececec; - - h2 { - vertical-align : bottom; - } -} - -.sort-direction-icon { - .pull-right(); - position : relative; - width : 0px; -} diff --git a/src/UI/Content/typeahead.less b/src/UI/Content/typeahead.less deleted file mode 100644 index 5a901c3ee..000000000 --- a/src/UI/Content/typeahead.less +++ /dev/null @@ -1,152 +0,0 @@ -/* - * typehead.js-bootstrap3.less - * @version 0.2.3 - * https://github.com/hyspace/typeahead.js-bootstrap3.less - * - * Licensed under the MIT license: - * http://www.opensource.org/licenses/MIT - */ - -//custom mixin for .form-control-validation -.typeahead-form-control(@border-color: #ccc;) { - border-color: @border-color; - .box-shadow(inset 0 1px 1px rgba(0,0,0,.075)); // Redeclare so transitions work - &:focus { - border-color: darken(@border-color, 10%); - @shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 6px lighten(@border-color, 20%); - .box-shadow(@shadow); - } -} - -//main styles for control -.tt-input, -.tt-hint { - .twitter-typeahead &{ - //validation states - .has-warning &{ - .typeahead-form-control(@state-warning-text); - } - .has-error &{ - .typeahead-form-control(@state-danger-text); - } - .has-success &{ - .typeahead-form-control(@state-success-text); - } - } - - //border - .input-group .twitter-typeahead:first-child &{ - .border-left-radius(@border-radius-base); - } - .input-group .twitter-typeahead:last-child &{ - .border-right-radius(@border-radius-base); - } - - //sizing - small:size and border - .input-group.input-group-sm .twitter-typeahead &{ - .input-size(@input-height-small; @padding-small-vertical; @padding-small-horizontal; @font-size-small; @line-height-small; @border-radius-small); - } - .input-group.input-group-sm .twitter-typeahead:not(:first-child):not(:last-child) &{ - border-radius: 0; - } - .input-group.input-group-sm .twitter-typeahead:first-child &{ - .border-left-radius(@border-radius-small); - .border-right-radius(0); - } - .input-group.input-group-sm .twitter-typeahead:last-child &{ - .border-left-radius(0); - .border-right-radius(@border-radius-small); - } - - //sizing - large:size and border - .input-group.input-group-lg .twitter-typeahead &{ - .input-size(@input-height-large; @padding-large-vertical; @padding-large-horizontal; @font-size-large; @line-height-large; @border-radius-large); - } - .input-group.input-group-lg .twitter-typeahead:not(:first-child):not(:last-child) &{ - border-radius: 0; - } - .input-group.input-group-lg .twitter-typeahead:first-child &{ - .border-left-radius(@border-radius-large); - .border-right-radius(0); - } - .input-group.input-group-lg .twitter-typeahead:last-child &{ - .border-left-radius(0); - .border-right-radius(@border-radius-large); - } -} - -//for wrapper -.twitter-typeahead { - width: 100%; - .input-group &{ - //overwrite `display:inline-block` style - display: table-cell!important; - float: left; - } -} - -//particular style for each other -.twitter-typeahead .tt-hint { - color: @text-muted;//color - hint -} -.twitter-typeahead .tt-input { - z-index: 2; - //disabled status - //overwrite inline styles of .tt-query - &[disabled], - &[readonly], - fieldset[disabled] & { - cursor: not-allowed; - //overwirte inline style - background-color: @input-bg-disabled!important; - } -} - -//dropdown styles -.tt-dropdown-menu { - //dropdown menu - position: absolute; - top: 100%; - left: 0; - z-index: @zindex-dropdown; - min-width: 160px; - width: 100%; - padding: 5px 0; - margin: 2px 0 0; - list-style: none; - font-size: @font-size-base; - background-color: @dropdown-bg; - border: 1px solid @dropdown-fallback-border; - border: 1px solid @dropdown-border; - border-radius: @border-radius-base; - .box-shadow(0 6px 12px rgba(0,0,0,.175)); - background-clip: padding-box; - *border-right-width: 2px; - *border-bottom-width: 2px; - - .tt-suggestion { - //item - display: block; - padding: 3px 20px; - clear: both; - font-weight: normal; - line-height: @line-height-base; - color: @dropdown-link-color; - white-space: nowrap; - - &.tt-cursor { - //item selected - text-decoration: none; - outline: 0; - background-color: @dropdown-link-hover-bg; - color: @dropdown-link-hover-color; - a { - //link in item selected - color: @dropdown-link-hover-color; - } - } - p { - margin: 0; - } - } -} diff --git a/src/UI/Content/utilities.less b/src/UI/Content/utilities.less deleted file mode 100644 index cc2f2cc75..000000000 --- a/src/UI/Content/utilities.less +++ /dev/null @@ -1,19 +0,0 @@ -@import "Bootstrap/variables"; -@import "Bootstrap/mixins"; - -@media (max-width: @screen-sm-max) { - .pull-none-xs { - float : none !important; - } - - .btn-group { - &.btn-group-collapse { - > .btn { - margin : 2px; - display : block; - float : none; - border-radius : @border-radius-base !important; - } - } - } -} \ No newline at end of file diff --git a/src/UI/Content/variables.less b/src/UI/Content/variables.less deleted file mode 100644 index 4b898e1d0..000000000 --- a/src/UI/Content/variables.less +++ /dev/null @@ -1,13 +0,0 @@ -@nzbdroneRed : #c4273c; -@purple : #7a43b6; -@nzbdronePurple : #7932ea; -@nzbdronePink : #F43565; -@droneTeal : #35c5f4; -@brand-info : @droneTeal; - -@screen-tn-max: @screen-xs-min - 1; -@tn: ~'(max-width: @{screen-tn-max})'; -@xs: ~'(min-width: @{screen-xs-max}) and (max-width: @{screen-xs-max})'; -@sm: ~'(min-width: @{screen-sm-min}) and (max-width: @{screen-sm-max})'; -@md: ~'(min-width: @{screen-md-min}) and (max-width: @{screen-md-max})'; -@lg: ~'(min-width: @{screen-lg-min})'; \ No newline at end of file diff --git a/src/UI/Content/zero.clipboard.swf b/src/UI/Content/zero.clipboard.swf deleted file mode 100644 index 8bad6a3e3..000000000 Binary files a/src/UI/Content/zero.clipboard.swf and /dev/null differ diff --git a/src/UI/Controller.js b/src/UI/Controller.js deleted file mode 100644 index 0b69eb02f..000000000 --- a/src/UI/Controller.js +++ /dev/null @@ -1,61 +0,0 @@ -var NzbDroneController = require('./Shared/NzbDroneController'); -var AppLayout = require('./AppLayout'); -var Marionette = require('marionette'); -var ActivityLayout = require('./Activity/ActivityLayout'); -var SettingsLayout = require('./Settings/SettingsLayout'); -//var AddSeriesLayout = require('./AddSeries/AddSeriesLayout'); -var AddArtistLayout = require('./AddArtist/AddArtistLayout'); -var WantedLayout = require('./Wanted/WantedLayout'); -var CalendarLayout = require('./Calendar/CalendarLayout'); -var ReleaseLayout = require('./Release/ReleaseLayout'); -var SystemLayout = require('./System/SystemLayout'); -var AlbumStudioLayout = require('./AlbumStudio/AlbumStudioLayout'); -//var SeriesEditorLayout = require('./Series/Editor/SeriesEditorLayout'); -var ArtistEditorLayout = require('./Artist/Editor/ArtistEditorLayout'); - -module.exports = NzbDroneController.extend({ - addArtist : function(action) { - this.setTitle('Add Artist'); - this.showMainRegion(new AddArtistLayout({ action : action })); - }, - - calendar : function() { - this.setTitle('Calendar'); - this.showMainRegion(new CalendarLayout()); - }, - - settings : function(action) { - this.setTitle('Settings'); - this.showMainRegion(new SettingsLayout({ action : action })); - }, - - wanted : function(action) { - this.setTitle('Wanted'); - this.showMainRegion(new WantedLayout({ action : action })); - }, - - activity : function(action) { - this.setTitle('Activity'); - this.showMainRegion(new ActivityLayout({ action : action })); - }, - - rss : function() { - this.setTitle('RSS'); - this.showMainRegion(new ReleaseLayout()); - }, - - system : function(action) { - this.setTitle('System'); - this.showMainRegion(new SystemLayout({ action : action })); - }, - - albumStudio : function() { - this.setTitle('Album Studio'); - this.showMainRegion(new AlbumStudioLayout()); - }, - - artistEditor : function() { - this.setTitle('Artist Editor'); - this.showMainRegion(new ArtistEditorLayout()); - } -}); \ No newline at end of file diff --git a/src/UI/Episode/EpisodeDetailsLayout.js b/src/UI/Episode/EpisodeDetailsLayout.js deleted file mode 100644 index 8aa58a91d..000000000 --- a/src/UI/Episode/EpisodeDetailsLayout.js +++ /dev/null @@ -1,130 +0,0 @@ -var Marionette = require('marionette'); -var SummaryLayout = require('./Summary/EpisodeSummaryLayout'); -var SearchLayout = require('./Search/EpisodeSearchLayout'); -var EpisodeHistoryLayout = require('./History/EpisodeHistoryLayout'); -var SeriesCollection = require('../Series/SeriesCollection'); -var Messenger = require('../Shared/Messenger'); - -module.exports = Marionette.Layout.extend({ - className : 'modal-lg', - template : 'Episode/EpisodeDetailsLayoutTemplate', - - regions : { - summary : '#episode-summary', - history : '#episode-history', - search : '#episode-search' - }, - - ui : { - summary : '.x-episode-summary', - history : '.x-episode-history', - search : '.x-episode-search', - monitored : '.x-episode-monitored' - }, - - events : { - - 'click .x-episode-summary' : '_showSummary', - 'click .x-episode-history' : '_showHistory', - 'click .x-episode-search' : '_showSearch', - 'click .x-episode-monitored' : '_toggleMonitored' - }, - - templateHelpers : {}, - - initialize : function(options) { - this.templateHelpers.hideSeriesLink = options.hideSeriesLink; - - this.series = SeriesCollection.get(this.model.get('seriesId')); - this.templateHelpers.series = this.series.toJSON(); - this.openingTab = options.openingTab || 'summary'; - - this.listenTo(this.model, 'sync', this._setMonitoredState); - }, - - onShow : function() { - this.searchLayout = new SearchLayout({ model : this.model }); - - if (this.openingTab === 'search') { - this.searchLayout.startManualSearch = true; - this._showSearch(); - } - - else { - this._showSummary(); - } - - this._setMonitoredState(); - - if (this.series.get('monitored')) { - this.$el.removeClass('series-not-monitored'); - } - - else { - this.$el.addClass('series-not-monitored'); - } - }, - - _showSummary : function(e) { - if (e) { - e.preventDefault(); - } - - this.ui.summary.tab('show'); - this.summary.show(new SummaryLayout({ - model : this.model, - series : this.series - })); - }, - - _showHistory : function(e) { - if (e) { - e.preventDefault(); - } - - this.ui.history.tab('show'); - this.history.show(new EpisodeHistoryLayout({ - model : this.model, - series : this.series - })); - }, - - _showSearch : function(e) { - if (e) { - e.preventDefault(); - } - - this.ui.search.tab('show'); - this.search.show(this.searchLayout); - }, - - _toggleMonitored : function() { - if (!this.series.get('monitored')) { - - Messenger.show({ - message : 'Unable to change monitored state when series is not monitored', - type : 'error' - }); - - return; - } - - var name = 'monitored'; - this.model.set(name, !this.model.get(name), { silent : true }); - - this.ui.monitored.addClass('icon-lidarr-spinner fa-spin'); - this.model.save(); - }, - - _setMonitoredState : function() { - this.ui.monitored.removeClass('fa-spin icon-lidarr-spinner'); - - if (this.model.get('monitored')) { - this.ui.monitored.addClass('icon-lidarr-monitored'); - this.ui.monitored.removeClass('icon-lidarr-unmonitored'); - } else { - this.ui.monitored.addClass('icon-lidarr-unmonitored'); - this.ui.monitored.removeClass('icon-lidarr-monitored'); - } - } -}); \ No newline at end of file diff --git a/src/UI/Episode/EpisodeDetailsLayoutTemplate.hbs b/src/UI/Episode/EpisodeDetailsLayoutTemplate.hbs deleted file mode 100644 index a65c9b27a..000000000 --- a/src/UI/Episode/EpisodeDetailsLayoutTemplate.hbs +++ /dev/null @@ -1,35 +0,0 @@ -<div class="modal-content"> - <div class="episode-detail-modal"> - <div class="modal-header"> - <span class="hidden-series-title x-series-title">{{series.title}}</span> - <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> - - <h3> - <i class="icon-lidarr-monitored x-episode-monitored episode-monitored" title="Toggle monitored status" /> - {{series.title}} - {{EpisodeNumber}} - {{title}} - </h3> - - </div> - <div class="modal-body"> - <ul class="nav nav-tabs" id="myTab"> - <li><a href="#episode-summary" class="x-episode-summary">Summary</a></li> - <li><a href="#episode-history" class="x-episode-history">History</a></li> - <li><a href="#episode-search" class="x-episode-search">Search</a></li> - </ul> - <div class="tab-content"> - <div class="tab-pane" id="episode-summary"/> - <div class="tab-pane" id="episode-history"/> - <div class="tab-pane" id="episode-search"/> - </div> - </div> - <div class="modal-footer"> - {{#unless hideSeriesLink}} - {{#with series}} - <a href="{{route}}" class="btn btn-default pull-left" data-dismiss="modal">Go to Series</a> - {{/with}} - {{/unless}} - - <button class="btn btn-default" data-dismiss="modal">Close</button> - </div> - </div> -</div> diff --git a/src/UI/Episode/History/EpisodeHistoryActionsCell.js b/src/UI/Episode/History/EpisodeHistoryActionsCell.js deleted file mode 100644 index ae4bbfafb..000000000 --- a/src/UI/Episode/History/EpisodeHistoryActionsCell.js +++ /dev/null @@ -1,35 +0,0 @@ -var $ = require('jquery'); -var vent = require('vent'); -var Marionette = require('marionette'); -var NzbDroneCell = require('../../Cells/NzbDroneCell'); - -module.exports = NzbDroneCell.extend({ - className : 'episode-actions-cell', - - events : { - 'click .x-failed' : '_markAsFailed' - }, - - render : function() { - this.$el.empty(); - - if (this.model.get('eventType') === 'grabbed') { - this.$el.html('<i class="icon-lidarr-delete x-failed" title="Mark download as failed"></i>'); - } - - return this; - }, - - _markAsFailed : function() { - var url = window.NzbDrone.ApiRoot + '/history/failed'; - var data = { - id : this.model.get('id') - }; - - $.ajax({ - url : url, - type : 'POST', - data : data - }); - } -}); \ No newline at end of file diff --git a/src/UI/Episode/History/EpisodeHistoryDetailsCell.js b/src/UI/Episode/History/EpisodeHistoryDetailsCell.js deleted file mode 100644 index 231c40473..000000000 --- a/src/UI/Episode/History/EpisodeHistoryDetailsCell.js +++ /dev/null @@ -1,28 +0,0 @@ -var $ = require('jquery'); -var vent = require('vent'); -var Marionette = require('marionette'); -var NzbDroneCell = require('../../Cells/NzbDroneCell'); -var HistoryDetailsView = require('../../Activity/History/Details/HistoryDetailsView'); -require('bootstrap'); - -module.exports = NzbDroneCell.extend({ - className : 'episode-history-details-cell', - - render : function() { - this.$el.empty(); - this.$el.html('<i class="icon-lidarr-form-info"></i>'); - - var html = new HistoryDetailsView({ model : this.model }).render().$el; - - this.$el.popover({ - content : html, - html : true, - trigger : 'hover', - title : 'Details', - placement : 'left', - container : this.$el - }); - - return this; - } -}); \ No newline at end of file diff --git a/src/UI/Episode/History/EpisodeHistoryLayout.js b/src/UI/Episode/History/EpisodeHistoryLayout.js deleted file mode 100644 index f474f4566..000000000 --- a/src/UI/Episode/History/EpisodeHistoryLayout.js +++ /dev/null @@ -1,84 +0,0 @@ -var Marionette = require('marionette'); -var Backgrid = require('backgrid'); -var HistoryCollection = require('../../Activity/History/HistoryCollection'); -var EventTypeCell = require('../../Cells/EventTypeCell'); -var QualityCell = require('../../Cells/QualityCell'); -var RelativeDateCell = require('../../Cells/RelativeDateCell'); -var EpisodeHistoryActionsCell = require('./EpisodeHistoryActionsCell'); -var EpisodeHistoryDetailsCell = require('./EpisodeHistoryDetailsCell'); -var NoHistoryView = require('./NoHistoryView'); -var LoadingView = require('../../Shared/LoadingView'); - -module.exports = Marionette.Layout.extend({ - template : 'Episode/History/EpisodeHistoryLayoutTemplate', - - regions : { - historyTable : '.history-table' - }, - - columns : [ - { - name : 'eventType', - label : '', - cell : EventTypeCell, - cellValue : 'this' - }, - { - name : 'sourceTitle', - label : 'Source Title', - cell : 'string' - }, - { - name : 'quality', - label : 'Quality', - cell : QualityCell - }, - { - name : 'date', - label : 'Date', - cell : RelativeDateCell - }, - { - name : 'this', - label : '', - cell : EpisodeHistoryDetailsCell, - sortable : false - }, - { - name : 'this', - label : '', - cell : EpisodeHistoryActionsCell, - sortable : false - } - ], - - initialize : function(options) { - this.model = options.model; - this.series = options.series; - - this.collection = new HistoryCollection({ - episodeId : this.model.id, - tableName : 'episodeHistory' - }); - this.collection.fetch(); - this.listenTo(this.collection, 'sync', this._showTable); - }, - - onRender : function() { - this.historyTable.show(new LoadingView()); - }, - - _showTable : function() { - if (this.collection.any()) { - this.historyTable.show(new Backgrid.Grid({ - collection : this.collection, - columns : this.columns, - className : 'table table-hover table-condensed' - })); - } - - else { - this.historyTable.show(new NoHistoryView()); - } - } -}); \ No newline at end of file diff --git a/src/UI/Episode/History/EpisodeHistoryLayoutTemplate.hbs b/src/UI/Episode/History/EpisodeHistoryLayoutTemplate.hbs deleted file mode 100644 index 54fb50522..000000000 --- a/src/UI/Episode/History/EpisodeHistoryLayoutTemplate.hbs +++ /dev/null @@ -1 +0,0 @@ -<div class="history-table table-responsive"></div> \ No newline at end of file diff --git a/src/UI/Episode/History/NoHistoryView.js b/src/UI/Episode/History/NoHistoryView.js deleted file mode 100644 index 883b5dfdc..000000000 --- a/src/UI/Episode/History/NoHistoryView.js +++ /dev/null @@ -1,5 +0,0 @@ -var Marionette = require('marionette'); - -module.exports = Marionette.ItemView.extend({ - template : 'Episode/History/NoHistoryViewTemplate' -}); \ No newline at end of file diff --git a/src/UI/Episode/History/NoHistoryViewTemplate.hbs b/src/UI/Episode/History/NoHistoryViewTemplate.hbs deleted file mode 100644 index 561e84d59..000000000 --- a/src/UI/Episode/History/NoHistoryViewTemplate.hbs +++ /dev/null @@ -1,3 +0,0 @@ -<p class="text-warning"> - No history for this episode. -</p> \ No newline at end of file diff --git a/src/UI/Episode/Search/ButtonsView.js b/src/UI/Episode/Search/ButtonsView.js deleted file mode 100644 index 6972f1201..000000000 --- a/src/UI/Episode/Search/ButtonsView.js +++ /dev/null @@ -1,5 +0,0 @@ -var Marionette = require('marionette'); - -module.exports = Marionette.ItemView.extend({ - template : 'Episode/Search/ButtonsViewTemplate' -}); \ No newline at end of file diff --git a/src/UI/Episode/Search/ButtonsViewTemplate.hbs b/src/UI/Episode/Search/ButtonsViewTemplate.hbs deleted file mode 100644 index 9e578f9db..000000000 --- a/src/UI/Episode/Search/ButtonsViewTemplate.hbs +++ /dev/null @@ -1,4 +0,0 @@ -<div class="search-buttons"> - <button class="btn btn-lg btn-block x-search-auto"><i class="icon-lidarr-search-automatic"/> Automatic Search</button> - <button class="btn btn-lg btn-block btn-primary x-search-manual"><i class="icon-lidarr-search-manual"/> Manual Search</button> -</div> \ No newline at end of file diff --git a/src/UI/Episode/Search/EpisodeSearchLayout.js b/src/UI/Episode/Search/EpisodeSearchLayout.js deleted file mode 100644 index 14ee5ca42..000000000 --- a/src/UI/Episode/Search/EpisodeSearchLayout.js +++ /dev/null @@ -1,82 +0,0 @@ -var vent = require('vent'); -var Marionette = require('marionette'); -var ButtonsView = require('./ButtonsView'); -var ManualSearchLayout = require('./ManualLayout'); -var ReleaseCollection = require('../../Release/ReleaseCollection'); -var CommandController = require('../../Commands/CommandController'); -var LoadingView = require('../../Shared/LoadingView'); -var NoResultsView = require('./NoResultsView'); - -module.exports = Marionette.Layout.extend({ - template : 'Episode/Search/EpisodeSearchLayoutTemplate', - - regions : { - main : '#episode-search-region' - }, - - events : { - 'click .x-search-auto' : '_searchAuto', - 'click .x-search-manual' : '_searchManual', - 'click .x-search-back' : '_showButtons' - }, - - initialize : function() { - this.mainView = new ButtonsView(); - this.releaseCollection = new ReleaseCollection(); - - this.listenTo(this.releaseCollection, 'sync', this._showSearchResults); - }, - - onShow : function() { - if (this.startManualSearch) { - this._searchManual(); - } - - else { - this._showMainView(); - } - }, - - _searchAuto : function(e) { - if (e) { - e.preventDefault(); - } - - CommandController.Execute('episodeSearch', { - episodeIds : [this.model.get('id')] - }); - - vent.trigger(vent.Commands.CloseModalCommand); - }, - - _searchManual : function(e) { - if (e) { - e.preventDefault(); - } - - this.mainView = new LoadingView(); - this._showMainView(); - this.releaseCollection.fetchEpisodeReleases(this.model.id); - }, - - _showMainView : function() { - this.main.show(this.mainView); - }, - - _showButtons : function() { - this.mainView = new ButtonsView(); - this._showMainView(); - }, - - _showSearchResults : function() { - if (this.releaseCollection.length === 0) { - this.mainView = new NoResultsView(); - } - - else { - this.mainView = new ManualSearchLayout({ collection : this.releaseCollection }); - } - - this._showMainView(); - } -}); \ No newline at end of file diff --git a/src/UI/Episode/Search/EpisodeSearchLayoutTemplate.hbs b/src/UI/Episode/Search/EpisodeSearchLayoutTemplate.hbs deleted file mode 100644 index 879e0b356..000000000 --- a/src/UI/Episode/Search/EpisodeSearchLayoutTemplate.hbs +++ /dev/null @@ -1 +0,0 @@ -<div id="episode-search-region"></div> \ No newline at end of file diff --git a/src/UI/Episode/Search/ManualLayout.js b/src/UI/Episode/Search/ManualLayout.js deleted file mode 100644 index 7bb39bab5..000000000 --- a/src/UI/Episode/Search/ManualLayout.js +++ /dev/null @@ -1,86 +0,0 @@ -var Marionette = require('marionette'); -var Backgrid = require('backgrid'); -var ReleaseTitleCell = require('../../Cells/ReleaseTitleCell'); -var FileSizeCell = require('../../Cells/FileSizeCell'); -var QualityCell = require('../../Cells/QualityCell'); -var ApprovalStatusCell = require('../../Cells/ApprovalStatusCell'); -var DownloadReportCell = require('../../Release/DownloadReportCell'); -var AgeCell = require('../../Release/AgeCell'); -var ProtocolCell = require('../../Release/ProtocolCell'); -var PeersCell = require('../../Release/PeersCell'); - -module.exports = Marionette.Layout.extend({ - template : 'Episode/Search/ManualLayoutTemplate', - - regions : { - grid : '#episode-release-grid' - }, - - columns : [ - { - name : 'protocol', - label : 'Source', - cell : ProtocolCell - }, - { - name : 'age', - label : 'Age', - cell : AgeCell - }, - { - name : 'title', - label : 'Title', - cell : ReleaseTitleCell - }, - { - name : 'indexer', - label : 'Indexer', - cell : Backgrid.StringCell - }, - { - name : 'size', - label : 'Size', - cell : FileSizeCell - }, - { - name : 'seeders', - label : 'Peers', - cell : PeersCell - }, - { - name : 'quality', - label : 'Quality', - cell : QualityCell - }, - { - name : 'rejections', - label : '<i class="icon-lidarr-header-rejections" />', - tooltip : 'Rejections', - cell : ApprovalStatusCell, - sortable : true, - sortType : 'fixed', - direction : 'ascending', - title : 'Release Rejected' - }, - { - name : 'download', - label : '<i class="icon-lidarr-download" />', - tooltip : 'Auto-Search Prioritization', - cell : DownloadReportCell, - sortable : true, - sortType : 'fixed', - direction : 'ascending' - } - ], - - onShow : function() { - if (!this.isClosed) { - this.grid.show(new Backgrid.Grid({ - row : Backgrid.Row, - columns : this.columns, - collection : this.collection, - className : 'table table-hover' - })); - } - } -}); \ No newline at end of file diff --git a/src/UI/Episode/Search/ManualLayoutTemplate.hbs b/src/UI/Episode/Search/ManualLayoutTemplate.hbs deleted file mode 100644 index 1797eb289..000000000 --- a/src/UI/Episode/Search/ManualLayoutTemplate.hbs +++ /dev/null @@ -1,2 +0,0 @@ -<div id="episode-release-grid" class="table-responsive"></div> -<button class="btn x-search-back">Back</button> \ No newline at end of file diff --git a/src/UI/Episode/Search/NoResultsView.js b/src/UI/Episode/Search/NoResultsView.js deleted file mode 100644 index a1a68c4fa..000000000 --- a/src/UI/Episode/Search/NoResultsView.js +++ /dev/null @@ -1,5 +0,0 @@ -var Marionette = require('marionette'); - -module.exports = Marionette.ItemView.extend({ - template : 'Episode/Search/NoResultsViewTemplate' -}); \ No newline at end of file diff --git a/src/UI/Episode/Search/NoResultsViewTemplate.hbs b/src/UI/Episode/Search/NoResultsViewTemplate.hbs deleted file mode 100644 index 7904e5520..000000000 --- a/src/UI/Episode/Search/NoResultsViewTemplate.hbs +++ /dev/null @@ -1 +0,0 @@ -<div>No results found</div> \ No newline at end of file diff --git a/src/UI/Episode/Summary/EpisodeSummaryLayout.js b/src/UI/Episode/Summary/EpisodeSummaryLayout.js deleted file mode 100644 index 29eaad626..000000000 --- a/src/UI/Episode/Summary/EpisodeSummaryLayout.js +++ /dev/null @@ -1,119 +0,0 @@ -var reqres = require('../../reqres'); -var Marionette = require('marionette'); -var Backgrid = require('backgrid'); -var EpisodeFileModel = require('../../Series/EpisodeFileModel'); -var EpisodeFileCollection = require('../../Series/EpisodeFileCollection'); -var FileSizeCell = require('../../Cells/FileSizeCell'); -var QualityCell = require('../../Cells/QualityCell'); -var DeleteEpisodeFileCell = require('../../Cells/DeleteEpisodeFileCell'); -var NoFileView = require('./NoFileView'); -var LoadingView = require('../../Shared/LoadingView'); - -module.exports = Marionette.Layout.extend({ - template : 'Episode/Summary/EpisodeSummaryLayoutTemplate', - - regions : { - overview : '.episode-overview', - activity : '.episode-file-info' - }, - - columns : [ - { - name : 'path', - label : 'Path', - cell : 'string', - sortable : false - }, - { - name : 'size', - label : 'Size', - cell : FileSizeCell, - sortable : false - }, - { - name : 'quality', - label : 'Quality', - cell : QualityCell, - sortable : false, - editable : true - }, - { - name : 'this', - label : '', - cell : DeleteEpisodeFileCell, - sortable : false - } - ], - - templateHelpers : {}, - - initialize : function(options) { - if (!this.model.series) { - this.templateHelpers.series = options.series.toJSON(); - } - }, - - onShow : function() { - if (this.model.get('hasFile')) { - var episodeFileId = this.model.get('episodeFileId'); - - if (reqres.hasHandler(reqres.Requests.GetEpisodeFileById)) { - var episodeFile = reqres.request(reqres.Requests.GetEpisodeFileById, episodeFileId); - this.episodeFileCollection = new EpisodeFileCollection(episodeFile, { seriesId : this.model.get('seriesId') }); - this.listenTo(episodeFile, 'destroy', this._episodeFileDeleted); - - this._showTable(); - } - - else { - this.activity.show(new LoadingView()); - - var self = this; - var newEpisodeFile = new EpisodeFileModel({ id : episodeFileId }); - this.episodeFileCollection = new EpisodeFileCollection(newEpisodeFile, { seriesId : this.model.get('seriesId') }); - var promise = newEpisodeFile.fetch(); - this.listenTo(newEpisodeFile, 'destroy', this._episodeFileDeleted); - - promise.done(function() { - self._showTable(); - }); - } - - this.listenTo(this.episodeFileCollection, 'add remove', this._collectionChanged); - } - - else { - this._showNoFileView(); - } - }, - - _showTable : function() { - this.activity.show(new Backgrid.Grid({ - collection : this.episodeFileCollection, - columns : this.columns, - className : 'table table-bordered', - emptyText : 'Nothing to see here!' - })); - }, - - _showNoFileView : function() { - this.activity.show(new NoFileView()); - }, - - _collectionChanged : function() { - if (!this.episodeFileCollection.any()) { - this._showNoFileView(); - } - - else { - this._showTable(); - } - }, - - _episodeFileDeleted : function() { - this.model.set({ - episodeFileId : 0, - hasFile : false - }); - } -}); \ No newline at end of file diff --git a/src/UI/Episode/Summary/EpisodeSummaryLayoutTemplate.hbs b/src/UI/Episode/Summary/EpisodeSummaryLayoutTemplate.hbs deleted file mode 100644 index 9cfeca2da..000000000 --- a/src/UI/Episode/Summary/EpisodeSummaryLayoutTemplate.hbs +++ /dev/null @@ -1,14 +0,0 @@ -<div class="episode-info"> - {{#with series}} - {{profile profileId}} - <span class="label label-info">{{network}}</span> - {{/with}} - <span class="label label-info">{{StartTime airDateUtc}}</span> - <span class="label label-info">{{RelativeDate airDateUtc}}</span> -</div> - -<div class="episode-overview"> - {{overview}} -</div> - -<div class="episode-file-info"></div> diff --git a/src/UI/Episode/Summary/NoFileView.js b/src/UI/Episode/Summary/NoFileView.js deleted file mode 100644 index 07aabc810..000000000 --- a/src/UI/Episode/Summary/NoFileView.js +++ /dev/null @@ -1,5 +0,0 @@ -var Marionette = require('marionette'); - -module.exports = Marionette.ItemView.extend({ - template : 'Episode/Summary/NoFileViewTemplate' -}); \ No newline at end of file diff --git a/src/UI/Episode/Summary/NoFileViewTemplate.hbs b/src/UI/Episode/Summary/NoFileViewTemplate.hbs deleted file mode 100644 index 0f923737d..000000000 --- a/src/UI/Episode/Summary/NoFileViewTemplate.hbs +++ /dev/null @@ -1,3 +0,0 @@ -<p class="text-warning"> - No file available for this episode. -</p> \ No newline at end of file diff --git a/src/UI/EpisodeFile/Editor/EmptyView.js b/src/UI/EpisodeFile/Editor/EmptyView.js deleted file mode 100644 index e84453524..000000000 --- a/src/UI/EpisodeFile/Editor/EmptyView.js +++ /dev/null @@ -1,5 +0,0 @@ -var Marionette = require('marionette'); - -module.exports = Marionette.CompositeView.extend({ - template : 'EpisodeFile/Editor/EmptyViewTemplate' -}); \ No newline at end of file diff --git a/src/UI/EpisodeFile/Editor/EmptyViewTemplate.hbs b/src/UI/EpisodeFile/Editor/EmptyViewTemplate.hbs deleted file mode 100644 index 0a51692de..000000000 --- a/src/UI/EpisodeFile/Editor/EmptyViewTemplate.hbs +++ /dev/null @@ -1,5 +0,0 @@ -<div class="row"> - <div class="col-md-12"> - No episode files - </div> -</div> diff --git a/src/UI/EpisodeFile/Editor/EpisodeFileEditorLayout.js b/src/UI/EpisodeFile/Editor/EpisodeFileEditorLayout.js deleted file mode 100644 index a974c8f7c..000000000 --- a/src/UI/EpisodeFile/Editor/EpisodeFileEditorLayout.js +++ /dev/null @@ -1,200 +0,0 @@ -var _ = require('underscore'); -var reqres = require('../../reqres'); -var Marionette = require('marionette'); -var Backgrid = require('backgrid'); -var FormatHelpers = require('../../Shared/FormatHelpers'); -var SelectAllCell = require('../../Cells/SelectAllCell'); -var EpisodeNumberCell = require('../../Series/Details/EpisodeNumberCell'); -var SeasonEpisodeNumberCell = require('../../Cells/EpisodeNumberCell'); -var EpisodeFilePathCell = require('../../Cells/EpisodeFilePathCell'); -var EpisodeStatusCell = require('../../Cells/EpisodeStatusCell'); -var RelativeDateCell = require('../../Cells/RelativeDateCell'); -var EpisodeCollection = require('../../Series/EpisodeCollection'); -var ProfileSchemaCollection = require('../../Settings/Profile/ProfileSchemaCollection'); -var QualitySelectView = require('./QualitySelectView'); -var EmptyView = require('./EmptyView'); - -module.exports = Marionette.Layout.extend({ - className : 'modal-lg', - template : 'EpisodeFile/Editor/EpisodeFileEditorLayoutTemplate', - - regions : { - episodeGrid : '.x-episode-list', - quality : '.x-quality' - }, - - ui : { - seasonMonitored : '.x-season-monitored' - }, - - events : { - 'click .x-season-monitored' : '_seasonMonitored', - 'click .x-delete-files' : '_deleteFiles' - }, - - initialize : function(options) { - if (!options.series) { - throw 'series is required'; - } - - if (!options.episodeCollection) { - throw 'episodeCollection is required'; - } - - var filtered = options.episodeCollection.filter(function(episode) { - return episode.get('episodeFileId') > 0; - }); - - this.series = options.series; - this.episodeCollection = options.episodeCollection; - this.filteredEpisodes = new EpisodeCollection(filtered); - - this.templateHelpers = {}; - this.templateHelpers.series = this.series.toJSON(); - - this._getColumns(); - }, - - onRender : function() { - this._getQualities(); - this._showEpisodes(); - }, - - _getColumns : function () { - var episodeCell = {}; - - if (this.model) { - episodeCell.name = 'episodeNumber'; - episodeCell.label = '#'; - episodeCell.cell = EpisodeNumberCell; - } - - else { - episodeCell.name = 'seasonEpisode'; - episodeCell.cellValue = 'this'; - episodeCell.label = 'Episode'; - episodeCell.cell = SeasonEpisodeNumberCell; - episodeCell.sortValue = this._seasonEpisodeSorter; - } - - this.columns = [ - { - name : '', - cell : SelectAllCell, - headerCell : 'select-all', - sortable : false - }, - episodeCell, - { - name : 'episodeNumber', - label : 'Relative Path', - cell : EpisodeFilePathCell, - sortable : false - }, - { - name : 'airDateUtc', - label : 'Air Date', - cell : RelativeDateCell - }, - { - name : 'status', - label : 'Quality', - cell : EpisodeStatusCell, - sortable : false - } - ]; - }, - - _showEpisodes : function() { - if (this.filteredEpisodes.length === 0) { - this.episodeGrid.show(new EmptyView()); - return; - } - - this._setInitialSort(); - - this.episodeGridView = new Backgrid.Grid({ - columns : this.columns, - collection : this.filteredEpisodes, - className : 'table table-hover season-grid' - }); - - this.episodeGrid.show(this.episodeGridView); - }, - - _setInitialSort : function () { - if (!this.model) { - this.filteredEpisodes.setSorting('seasonEpisode', 1, { sortValue: this._seasonEpisodeSorter }); - this.filteredEpisodes.fullCollection.sort(); - } - }, - - _getQualities : function() { - var self = this; - - var profileSchemaCollection = new ProfileSchemaCollection(); - var promise = profileSchemaCollection.fetch(); - - promise.done(function() { - var profile = profileSchemaCollection.first(); - - self.qualitySelectView = new QualitySelectView({ qualities: _.map(profile.get('items'), 'quality') }); - self.listenTo(self.qualitySelectView, 'seasonedit:quality', self._changeQuality); - - self.quality.show(self.qualitySelectView); - }); - }, - - _changeQuality : function(options) { - var newQuality = { - quality : options.selected, - revision : { - version : 1, - real : 0 - } - }; - - var selected = this._getSelectedEpisodeFileIds(); - - _.each(selected, function(episodeFileId) { - if (reqres.hasHandler(reqres.Requests.GetEpisodeFileById)) { - var episodeFile = reqres.request(reqres.Requests.GetEpisodeFileById, episodeFileId); - episodeFile.set('quality', newQuality); - episodeFile.save(); - } - }); - }, - - _deleteFiles : function() { - if (!window.confirm('Are you sure you want to delete the episode files for the selected episodes?')) { - return; - } - - var selected = this._getSelectedEpisodeFileIds(); - - _.each(selected, function(episodeFileId) { - if (reqres.hasHandler(reqres.Requests.GetEpisodeFileById)) { - var episodeFile = reqres.request(reqres.Requests.GetEpisodeFileById, episodeFileId); - - episodeFile.destroy(); - } - }); - - _.each(this.episodeGridView.getSelectedModels(), function(episode) { - this.episodeGridView.removeRow(episode); - }, this); - }, - - _getSelectedEpisodeFileIds: function () { - return _.uniq(_.map(this.episodeGridView.getSelectedModels(), function (episode) { - return episode.get('episodeFileId'); - })); - }, - - _seasonEpisodeSorter : function (model, attr) { - var seasonNumber = FormatHelpers.pad(model.get('seasonNumber'), 4, 0); - var episodeNumber = FormatHelpers.pad(model.get('episodeNumber'), 4, 0); - - return seasonNumber + episodeNumber; - } -}); diff --git a/src/UI/EpisodeFile/Editor/EpisodeFileEditorLayoutTemplate.hbs b/src/UI/EpisodeFile/Editor/EpisodeFileEditorLayoutTemplate.hbs deleted file mode 100644 index 6f7e84109..000000000 --- a/src/UI/EpisodeFile/Editor/EpisodeFileEditorLayoutTemplate.hbs +++ /dev/null @@ -1,28 +0,0 @@ -<div class="modal-content"> - <div class="edit-season-modal"> - <div class="modal-header"> - <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> - - <h3> - {{#if seasonNumber}} - {{#if_eq seasonNumber compare="0"}} - {{series.title}} - Specials - {{else}} - {{series.title}} - Season {{seasonNumber}} - {{/if_eq}} - {{else}} - {{series.title}} - {{/if}} - </h3> - - </div> - <div class="modal-body"> - <div class="x-episode-list"></div> - <div class="x-quality"></div> - </div> - <div class="modal-footer"> - <button class="btn btn-danger x-delete-files">Delete Files</button> - <button class="btn btn-default" data-dismiss="modal">Close</button> - </div> - </div> -</div> diff --git a/src/UI/EpisodeFile/Editor/QualitySelectView.js b/src/UI/EpisodeFile/Editor/QualitySelectView.js deleted file mode 100644 index beac4f304..000000000 --- a/src/UI/EpisodeFile/Editor/QualitySelectView.js +++ /dev/null @@ -1,35 +0,0 @@ -var _ = require('underscore'); -var Marionette = require('marionette'); - -module.exports = Marionette.ItemView.extend({ - template : 'EpisodeFile/Editor/QualitySelectViewTemplate', - - ui : { - select : '.x-select' - }, - - events : { - 'change .x-select' : '_changeSelect' - }, - - initialize : function (options) { - this.qualities = options.qualities; - - this.templateHelpers = { - qualities : this.qualities - }; - }, - - _changeSelect : function () { - var value = this.ui.select.val(); - - if (value === 'choose') { - return; - } - - var quality = _.find(this.qualities, { 'id': parseInt(value) }); - - this.trigger('seasonedit:quality', { selected : quality }); - this.ui.select.val('choose'); - } -}); \ No newline at end of file diff --git a/src/UI/EpisodeFile/Editor/QualitySelectViewTemplate.hbs b/src/UI/EpisodeFile/Editor/QualitySelectViewTemplate.hbs deleted file mode 100644 index 4ab83c931..000000000 --- a/src/UI/EpisodeFile/Editor/QualitySelectViewTemplate.hbs +++ /dev/null @@ -1,10 +0,0 @@ -<div class="row"> - <div class="form-group col-md-3 col-md-offset-9"> - <select class="form-control x-select"> - <option value="choose">Select quality</option> - {{#eachReverse qualities}} - <option value="{{id}}">{{name}}</option> - {{/eachReverse}} - </select> - </div> -</div> diff --git a/src/UI/Form/ActionTemplate.hbs b/src/UI/Form/ActionTemplate.hbs deleted file mode 100644 index ecb861f99..000000000 --- a/src/UI/Form/ActionTemplate.hbs +++ /dev/null @@ -1,7 +0,0 @@ -<div class="form-group {{#if advanced}}advanced-setting{{/if}}"> - <label class="col-sm-3 control-label"></label> - - <div class="col-sm-5"> - <button class="form-control {{name}}" validation-name="{{name}}" data-value="{{value}}">{{label}}</button> - </div> -</div> diff --git a/src/UI/Form/CaptchaTemplate.hbs b/src/UI/Form/CaptchaTemplate.hbs deleted file mode 100644 index 806c3a32e..000000000 --- a/src/UI/Form/CaptchaTemplate.hbs +++ /dev/null @@ -1,15 +0,0 @@ -<div class="form-group {{#if advanced}}advanced-setting{{/if}}"> - <label class="col-sm-3 control-label">{{label}}</label> - - <div class="col-sm-5"> - <div class="input-group"> - <input type="text" name="fields.{{order}}.value" validation-name="{{name}}" spellcheck="false" class="form-control x-captcha" readonly placeholder="(optional)" /> - <span class="input-group-btn"><button class="btn btn-primary x-captcha-refresh" title="Refresh CAPTCHA Token"><i class="icon-lidarr-refresh" /></button></span> - </div> - </div> - - <span class="col-sm-1 help-inline"> - <i class="icon-lidarr-form-warning" title="Expires periodically and will need to be refreshed."/> - <i class="icon-lidarr-form-warning" title="Refreshing the CAPTCHA Token will embed a temporary Google reCaptcha widget on this page."/> - </span> -</div> diff --git a/src/UI/Form/CheckboxTemplate.hbs b/src/UI/Form/CheckboxTemplate.hbs deleted file mode 100644 index d382aaa00..000000000 --- a/src/UI/Form/CheckboxTemplate.hbs +++ /dev/null @@ -1,23 +0,0 @@ -<div class="form-group {{#if advanced}}advanced-setting{{/if}}"> - <label class="col-sm-3 control-label">{{label}}</label> - - <div class="col-sm-5"> - <div class="input-group"> - <label class="checkbox toggle well"> - <input type="checkbox" name="fields.{{order}}.value"/> - <p> - <span>Yes</span> - <span>No</span> - </p> - - <div class="btn btn-primary slide-button"/> - </label> - - {{#if helpText}} - <span class="help-inline-checkbox"> - <i class="icon-lidarr-form-info" title="{{helpText}}"/> - </span> - {{/if}} - </div> - </div> -</div> diff --git a/src/UI/Form/FormBuilder.js b/src/UI/Form/FormBuilder.js deleted file mode 100644 index eef48eb91..000000000 --- a/src/UI/Form/FormBuilder.js +++ /dev/null @@ -1,66 +0,0 @@ -var Marionette = require('marionette'); -var Handlebars = require('handlebars'); -var _ = require('underscore'); -require('./FormMessage'); - -var _templateRenderer = function(templateName) { - var templateFunction = Marionette.TemplateCache.get(templateName); - return new Handlebars.SafeString(templateFunction(this)); -}; - -var _fieldBuilder = function(field) { - if (!field.type) { - return _templateRenderer.call(field, 'Form/TextboxTemplate'); - } - - if (field.type === 'hidden') { - return _templateRenderer.call(field, 'Form/HiddenTemplate'); - } - - if (field.type === 'url') { - return _templateRenderer.call(field, 'Form/UrlTemplate'); - } - - if (field.type === 'password') { - return _templateRenderer.call(field, 'Form/PasswordTemplate'); - } - - if (field.type === 'checkbox') { - return _templateRenderer.call(field, 'Form/CheckboxTemplate'); - } - - if (field.type === 'select') { - return _templateRenderer.call(field, 'Form/SelectTemplate'); - } - - if (field.type === 'hidden') { - return _templateRenderer.call(field, 'Form/HiddenTemplate'); - } - - if (field.type === 'path' || field.type === 'filepath') { - return _templateRenderer.call(field, 'Form/PathTemplate'); - } - - if (field.type === 'tag') { - return _templateRenderer.call(field, 'Form/TagTemplate'); - } - - if (field.type === 'action') { - return _templateRenderer.call(field, 'Form/ActionTemplate'); - } - - if (field.type === 'captcha') { - return _templateRenderer.call(field, 'Form/CaptchaTemplate'); - } - - return _templateRenderer.call(field, 'Form/TextboxTemplate'); -}; - -Handlebars.registerHelper('formBuilder', function() { - var ret = ''; - _.each(this.fields, function(field) { - ret += _fieldBuilder(field); - }); - - return new Handlebars.SafeString(ret); -}); diff --git a/src/UI/Form/FormHelpPartial.hbs b/src/UI/Form/FormHelpPartial.hbs deleted file mode 100644 index e8072f190..000000000 --- a/src/UI/Form/FormHelpPartial.hbs +++ /dev/null @@ -1,8 +0,0 @@ -<span class="col-sm-1 help-inline"> - {{#if helpText}} - <i class="icon-lidarr-form-info" title="{{helpText}}"/> - {{/if}} - {{#if helpLink}} - <a href="{{helpLink}}" class="help-link"><i class="icon-lidarr-form-info-link"/></a> - {{/if}} -</span> diff --git a/src/UI/Form/FormMessage.js b/src/UI/Form/FormMessage.js deleted file mode 100644 index 209bccd42..000000000 --- a/src/UI/Form/FormMessage.js +++ /dev/null @@ -1,17 +0,0 @@ -var Handlebars = require('handlebars'); - -Handlebars.registerHelper('formMessage', function(message) { - if (!message) { - return ''; - } - - var level = message.type; - - if (message.type === 'error') { - level = 'danger'; - } - - var messageHtml = '<div class="alert alert-{0}" role="alert">{1}</div>'.format(level, message.message); - - return new Handlebars.SafeString(messageHtml); -}); \ No newline at end of file diff --git a/src/UI/Form/HiddenTemplate.hbs b/src/UI/Form/HiddenTemplate.hbs deleted file mode 100644 index 03933b122..000000000 --- a/src/UI/Form/HiddenTemplate.hbs +++ /dev/null @@ -1 +0,0 @@ -<input type="hidden" name="fields.{{order}}.value" validation-name="{{name}}" spellcheck="false"/> \ No newline at end of file diff --git a/src/UI/Form/PasswordTemplate.hbs b/src/UI/Form/PasswordTemplate.hbs deleted file mode 100644 index 3a96cab7f..000000000 --- a/src/UI/Form/PasswordTemplate.hbs +++ /dev/null @@ -1,8 +0,0 @@ -<div class="form-group {{#if advanced}}advanced-setting{{/if}}"> - <label class="col-sm-3 control-label">{{label}}</label> - - <div class="col-sm-5"> - <input type="password" name="fields.{{order}}.value" validation-name="{{name}}" autocomplete="new-password" class="form-control"/> - </div> - {{> FormHelpPartial}} -</div> diff --git a/src/UI/Form/PathTemplate.hbs b/src/UI/Form/PathTemplate.hbs deleted file mode 100644 index 5a95305e1..000000000 --- a/src/UI/Form/PathTemplate.hbs +++ /dev/null @@ -1,8 +0,0 @@ -<div class="form-group {{#if advanced}}advanced-setting{{/if}}"> - <label class="col-sm-3 control-label">{{label}}</label> - - <div class="col-sm-5"> - <input type="text" name="fields.{{order}}.value" validation-name="{{name}}" class="form-control x-path {{#if_eq type compare="filepath"}}x-filepath{{/if_eq}}"/> - </div> - {{> FormHelpPartial}} -</div> diff --git a/src/UI/Form/SelectTemplate.hbs b/src/UI/Form/SelectTemplate.hbs deleted file mode 100644 index 978d432df..000000000 --- a/src/UI/Form/SelectTemplate.hbs +++ /dev/null @@ -1,12 +0,0 @@ -<div class="form-group {{#if advanced}}advanced-setting{{/if}}"> - <label class="col-sm-3 control-label">{{label}}</label> - - <div class="col-sm-5"> - <select name="fields.{{order}}.value" class="form-control"> - {{#each selectOptions}} - <option value="{{value}}">{{name}}</option> - {{/each}} - </select> - </div> - {{> FormHelpPartial}} -</div> diff --git a/src/UI/Form/TagTemplate.hbs b/src/UI/Form/TagTemplate.hbs deleted file mode 100644 index 4df3ca6ba..000000000 --- a/src/UI/Form/TagTemplate.hbs +++ /dev/null @@ -1,9 +0,0 @@ -<div class="form-group {{#if advanced}}advanced-setting{{/if}}"> - <label class="col-sm-3 control-label">{{label}}</label> - - <div class="col-sm-5"> - <input type="text" name="fields.{{order}}.value" validation-name="{{name}}" class="form-control x-form-tag"/> - </div> - - {{> FormHelpPartial}} -</div> \ No newline at end of file diff --git a/src/UI/Form/TextboxTemplate.hbs b/src/UI/Form/TextboxTemplate.hbs deleted file mode 100644 index e7054cfac..000000000 --- a/src/UI/Form/TextboxTemplate.hbs +++ /dev/null @@ -1,8 +0,0 @@ -<div class="form-group {{#if advanced}}advanced-setting{{/if}}"> - <label class="col-sm-3 control-label">{{label}}</label> - - <div class="col-sm-5"> - <input type="text" name="fields.{{order}}.value" validation-name="{{name}}" spellcheck="false" class="form-control"/> - </div> - {{> FormHelpPartial}} -</div> diff --git a/src/UI/Form/UrlTemplate.hbs b/src/UI/Form/UrlTemplate.hbs deleted file mode 100644 index 7f41272f1..000000000 --- a/src/UI/Form/UrlTemplate.hbs +++ /dev/null @@ -1,8 +0,0 @@ -<div class="form-group {{#if advanced}}advanced-setting{{/if}}"> - <label class="col-sm-3 control-label">{{label}}</label> - - <div class="col-sm-5"> - <input type="url" name="fields.{{order}}.value" validation-name="{{name}}" spellcheck="false" class="form-control"/> - </div> - {{> FormHelpPartial}} -</div> diff --git a/src/UI/Handlebars/Handlebars.Debug.js b/src/UI/Handlebars/Handlebars.Debug.js deleted file mode 100644 index 84360f665..000000000 --- a/src/UI/Handlebars/Handlebars.Debug.js +++ /dev/null @@ -1,7 +0,0 @@ -var Handlebars = require('handlebars'); - -Handlebars.registerHelper('debug', function() { - console.group('Handlebar context'); - console.log(this); - console.groupEnd(); -}); \ No newline at end of file diff --git a/src/UI/Handlebars/Helpers/Album.js b/src/UI/Handlebars/Helpers/Album.js deleted file mode 100644 index beb346b78..000000000 --- a/src/UI/Handlebars/Helpers/Album.js +++ /dev/null @@ -1,75 +0,0 @@ -var Handlebars = require('handlebars'); -var StatusModel = require('../../System/StatusModel'); -var moment = require('moment'); -var _ = require('underscore'); - -Handlebars.registerHelper('cover', function() { - - var placeholder = StatusModel.get('urlBase') + '/Content/Images/cover-dark.png'; - var cover = _.where(this.images, { coverType : 'cover' }); - - if (cover[0]) { - if (!cover[0].url.match(/^https?:\/\//)) { - return new Handlebars.SafeString('<img class="album-cover x-album-cover" {0}>'.format(Handlebars.helpers.defaultImg.call(null, cover[0].url, 250))); - } else { - var url = cover[0].url.replace(/^https?\:/, ''); - return new Handlebars.SafeString('<img class="album-cover x-album-cover" {0}>'.format(Handlebars.helpers.defaultImg.call(null, url))); - } - } - - return new Handlebars.SafeString('<img class="album-cover placeholder-image" src="{0}">'.format(placeholder)); -}); - -Handlebars.registerHelper('StatusLevel', function() { - var hasFile = false; //this.hasFile; #TODO Refactor for Albums - var downloading = false; //require('../../Activity/Queue/QueueCollection').findEpisode(this.id) || this.downloading; #TODO Queue Refactor for Albums - var currentTime = moment(); - var start = moment(this.releaseDate); - var end = moment(this.end); - var monitored = this.artist.monitored && this.monitored; - - if (hasFile) { - return 'success'; - } - - if (downloading) { - return 'purple'; - } - - else if (!monitored) { - return 'unmonitored'; - } - - if (currentTime.isAfter(start) && currentTime.isBefore(end)) { - return 'warning'; - } - - if (start.isBefore(currentTime) && !hasFile) { - return 'danger'; - } - - return 'primary'; -}); - -Handlebars.registerHelper('MBAlbumUrl', function() { - return 'https://musicbrainz.org/release-group/' + this.mbId; -}); - -Handlebars.registerHelper('TADBAlbumUrl', function() { - return 'http://www.theaudiodb.com/album/' + this.tadbId; -}); - -Handlebars.registerHelper('discogsAlbumUrl', function() { - return 'https://www.discogs.com/master/' + this.discogsId; -}); - -Handlebars.registerHelper('allMusicAlbumUrl', function() { - return 'http://www.allmusic.com/album/' + this.allMusicId; -}); - -Handlebars.registerHelper('albumYear', function() { - return new Handlebars.SafeString('<span class="year">{0}</span>'.format(moment(this.releaseDate).format('YYYY'))); -}); -Handlebars.registerHelper('albumReleaseDate', function() { - return new Handlebars.SafeString('<span class="release">{0}</span>'.format(moment(this.releaseDate).format('L'))); -}); diff --git a/src/UI/Handlebars/Helpers/Artist.js b/src/UI/Handlebars/Helpers/Artist.js deleted file mode 100644 index 4840d9467..000000000 --- a/src/UI/Handlebars/Helpers/Artist.js +++ /dev/null @@ -1,117 +0,0 @@ -var Handlebars = require('handlebars'); -var StatusModel = require('../../System/StatusModel'); -var _ = require('underscore'); - -Handlebars.registerHelper('poster', function() { - - var placeholder = StatusModel.get('urlBase') + '/Content/Images/poster-dark.png'; - var poster = _.where(this.images, { coverType : 'poster' }); - - if (poster[0]) { - if (!poster[0].url.match(/^https?:\/\//)) { - return new Handlebars.SafeString('<img class="artist-poster x-artist-poster" {0}>'.format(Handlebars.helpers.defaultImg.call(null, poster[0].url, 250))); - } else { - var url = poster[0].url.replace(/^https?\:/, ''); - return new Handlebars.SafeString('<img class="artist-poster x-artist-poster" {0}>'.format(Handlebars.helpers.defaultImg.call(null, url))); - } - } - - return new Handlebars.SafeString('<img class="artist-poster placeholder-image" src="{0}">'.format(placeholder)); -}); - - - -Handlebars.registerHelper('MBUrl', function() { - return 'https://musicbrainz.org/artist/' + this.foreignArtistId; -}); - -Handlebars.registerHelper('TADBUrl', function() { - return 'http://www.theaudiodb.com/artist/' + this.tadbId; -}); - -Handlebars.registerHelper('discogsUrl', function() { - return 'https://www.discogs.com/artist/' + this.discogsId; -}); - -Handlebars.registerHelper('allMusicUrl', function() { - return 'http://www.allmusic.com/artist/' + this.allMusicId; -}); - -Handlebars.registerHelper('route', function() { - return StatusModel.get('urlBase') + '/artist/' + this.nameSlug; -}); - -// Handlebars.registerHelper('percentOfEpisodes', function() { -// var episodeCount = this.episodeCount; -// var episodeFileCount = this.episodeFileCount; - -// var percent = 100; - -// if (episodeCount > 0) { -// percent = episodeFileCount / episodeCount * 100; -// } - -// return percent; -// }); - -Handlebars.registerHelper('percentOfTracks', function() { - var trackCount = this.trackCount; - var trackFileCount = this.trackFileCount; - - var percent = 100; - - if (trackCount > 0) { - percent = trackFileCount / trackCount * 100; - } - - return percent; -}); - -Handlebars.registerHelper('seasonCountHelper', function() { - var seasonCount = this.seasonCount; - var continuing = this.status === 'continuing'; - - if (continuing) { - return new Handlebars.SafeString('<span class="label label-info">Season {0}</span>'.format(seasonCount)); - } - - if (seasonCount === 1) { - return new Handlebars.SafeString('<span class="label label-info">{0} Season</span>'.format(seasonCount)); - } - - return new Handlebars.SafeString('<span class="label label-info">{0} Seasons</span>'.format(seasonCount)); -}); - -Handlebars.registerHelper ('truncate', function (str, len) { - if (str && str.length > len && str.length > 0) { - var new_str = str + " "; - new_str = str.substr (0, len); - new_str = str.substr (0, new_str.lastIndexOf(" ")); - new_str = (new_str.length > 0) ? new_str : str.substr (0, len); - - return new Handlebars.SafeString ( new_str +'...' ); - } - return str; -}); - -Handlebars.registerHelper('albumCountHelper', function() { - var albumCount = this.albumCount; - - if (albumCount === 1) { - return new Handlebars.SafeString('<span class="label label-info">{0} Album</span>'.format(albumCount)); - } - - return new Handlebars.SafeString('<span class="label label-info">{0} Albums</span>'.format(albumCount)); -}); - -/*Handlebars.registerHelper('titleWithYear', function() { - if (this.title.endsWith(' ({0})'.format(this.year))) { - return this.title; - } - - if (!this.year) { - return this.title; - } - - return new Handlebars.SafeString('{0} <span class="year">({1})</span>'.format(this.title, this.year)); -});*/ diff --git a/src/UI/Handlebars/Helpers/DateTime.js b/src/UI/Handlebars/Helpers/DateTime.js deleted file mode 100644 index 18a5f7cbc..000000000 --- a/src/UI/Handlebars/Helpers/DateTime.js +++ /dev/null @@ -1,90 +0,0 @@ -var Handlebars = require('handlebars'); -var moment = require('moment'); -var FormatHelpers = require('../../Shared/FormatHelpers'); -var UiSettings = require('../../Shared/UiSettingsModel'); - -Handlebars.registerHelper('ShortDate', function(input) { - if (!input) { - return ''; - } - - var date = moment(input); - var result = '<span title="' + date.format(UiSettings.longDateTime()) + '">' + date.format(UiSettings.get('shortDateFormat')) + '</span>'; - - return new Handlebars.SafeString(result); -}); - -Handlebars.registerHelper('RelativeDate', function(input) { - if (!input) { - return ''; - } - - var date = moment(input); - var result = '<span title="{0}">{1}</span>'; - var tooltip = date.format(UiSettings.longDateTime()); - var text; - - if (UiSettings.get('showRelativeDates')) { - text = FormatHelpers.relativeDate(input); - } else { - text = date.format(UiSettings.get('shortDateFormat')); - } - - result = result.format(tooltip, text); - - return new Handlebars.SafeString(result); -}); - -Handlebars.registerHelper('Day', function(input) { - if (!input) { - return ''; - } - - return moment(input).format('DD'); -}); - -Handlebars.registerHelper('Month', function(input) { - if (!input) { - return ''; - } - - return moment(input).format('MMM'); -}); - -Handlebars.registerHelper('StartTime', function(input) { - if (!input) { - return ''; - } - - return moment(input).format(UiSettings.time(false, false)); -}); - -Handlebars.registerHelper('LTS', function(input) { - if (!input) { - return ''; - } - - return moment(input).format(UiSettings.time(true, true)); -}); - -Handlebars.registerHelper('if_today', function(context, options) { - var date = moment(context).startOf('day'); - var today = moment().startOf('day'); - - if (date.isSame(today)) { - return options.fn(this); - } - - return options.inverse(this); -}); - -Handlebars.registerHelper('unless_today', function(context, options) { - var date = moment(context).startOf('day'); - var today = moment().startOf('day'); - - if (date.isSame(today)) { - return options.inverse(this); - } - - return options.fn(this); -}); \ No newline at end of file diff --git a/src/UI/Handlebars/Helpers/EachReverse.js b/src/UI/Handlebars/Helpers/EachReverse.js deleted file mode 100644 index 7e5e0983a..000000000 --- a/src/UI/Handlebars/Helpers/EachReverse.js +++ /dev/null @@ -1,16 +0,0 @@ -var Handlebars = require('handlebars'); - -Handlebars.registerHelper('eachReverse', function(context) { - var options = arguments[arguments.length - 1]; - var ret = ''; - - if (context && context.length > 0) { - for (var i = context.length - 1; i >= 0; i--) { - ret += options.fn(context[i]); - } - } else { - ret = options.inverse(this); - } - - return ret; -}); \ No newline at end of file diff --git a/src/UI/Handlebars/Helpers/Enumerable.js b/src/UI/Handlebars/Helpers/Enumerable.js deleted file mode 100644 index ae7f8708d..000000000 --- a/src/UI/Handlebars/Helpers/Enumerable.js +++ /dev/null @@ -1,21 +0,0 @@ -var Handlebars = require('handlebars'); - -Handlebars.registerHelper('times', function(n, block) { - var accum = ''; - - for (var i = 0; i < n; ++i) { - accum += block.fn(i); - } - - return accum; -}); - -Handlebars.registerHelper('for', function(from, to, incr, block) { - var accum = ''; - - for (var i = from; i < to; i += incr) { - accum += block.fn(i); - } - - return accum; -}); \ No newline at end of file diff --git a/src/UI/Handlebars/Helpers/Episode.js b/src/UI/Handlebars/Helpers/Episode.js deleted file mode 100644 index 3bd821b66..000000000 --- a/src/UI/Handlebars/Helpers/Episode.js +++ /dev/null @@ -1,33 +0,0 @@ -var Handlebars = require('handlebars'); -var FormatHelpers = require('../../Shared/FormatHelpers'); -var moment = require('moment'); -require('../../Activity/Queue/QueueCollection'); - -Handlebars.registerHelper('EpisodeNumber', function() { - - if (this.series.seriesType === 'daily') { - return moment(this.airDate).format('L'); - } else if (this.series.seriesType === 'anime' && this.absoluteEpisodeNumber !== undefined) { - return '{0}x{1} ({2})'.format(this.seasonNumber, FormatHelpers.pad(this.episodeNumber, 2), FormatHelpers.pad(this.absoluteEpisodeNumber, 2)); - } else { - return '{0}x{1}'.format(this.seasonNumber, FormatHelpers.pad(this.episodeNumber, 2)); - } -}); - - - -Handlebars.registerHelper('EpisodeProgressClass', function() { - if (this.episodeFileCount === this.episodeCount) { - if (this.status === 'continuing') { - return ''; - } - - return 'progress-bar-success'; - } - - if (this.monitored) { - return 'progress-bar-danger'; - } - - return 'progress-bar-warning'; -}); \ No newline at end of file diff --git a/src/UI/Handlebars/Helpers/Html.js b/src/UI/Handlebars/Helpers/Html.js deleted file mode 100644 index 962ccf10d..000000000 --- a/src/UI/Handlebars/Helpers/Html.js +++ /dev/null @@ -1,40 +0,0 @@ -var $ = require('jquery'); -var Handlebars = require('handlebars'); -var StatusModel = require('../../System/StatusModel'); - -var placeholder = StatusModel.get('urlBase') + '/Content/Images/poster-dark.png'; - -window.NzbDrone.imageError = function(img) { - if (!img.src.contains(placeholder)) { - img.src = placeholder; - img.srcset = ""; - $(img).addClass('placeholder-image'); - } - - img.onerror = null; -}; - -Handlebars.registerHelper('defaultImg', function(src, size) { - var endOfPath = /\.jpg($|\?)/g; - var errorAttr = 'onerror="window.NzbDrone.imageError(this);"'; - var srcsetAttr = ''; - var oneX = src, twoX; - - if (!src) { - return new Handlebars.SafeString(errorAttr); - } - - if (size) { - oneX = src.replace(endOfPath, '-' + size + '.jpg$1'); - twoX = src.replace(endOfPath, '-' + size * 2 + '.jpg$1'); - srcsetAttr = 'srcset="{0} 1x, {1} 2x"'.format(oneX, twoX); - } - - return new Handlebars.SafeString( - 'src="{0}" {1} {2}'.format(oneX, srcsetAttr, errorAttr) - ); -}); - -Handlebars.registerHelper('UrlBase', function() { - return new Handlebars.SafeString(StatusModel.get('urlBase')); -}); \ No newline at end of file diff --git a/src/UI/Handlebars/Helpers/Numbers.js b/src/UI/Handlebars/Helpers/Numbers.js deleted file mode 100644 index 19d7c63ab..000000000 --- a/src/UI/Handlebars/Helpers/Numbers.js +++ /dev/null @@ -1,14 +0,0 @@ -var Handlebars = require('handlebars'); -var FormatHelpers = require('../../Shared/FormatHelpers'); - -Handlebars.registerHelper('Bytes', function(size) { - return new Handlebars.SafeString(FormatHelpers.bytes(size)); -}); - -Handlebars.registerHelper('Pad2', function(input) { - return FormatHelpers.pad(input, 2); -}); - -Handlebars.registerHelper('Number', function(input) { - return FormatHelpers.number(input); -}); diff --git a/src/UI/Handlebars/Helpers/Quality.js b/src/UI/Handlebars/Helpers/Quality.js deleted file mode 100644 index 96b9c840f..000000000 --- a/src/UI/Handlebars/Helpers/Quality.js +++ /dev/null @@ -1,12 +0,0 @@ -var Handlebars = require('handlebars'); -var ProfileCollection = require('../../Profile/ProfileCollection'); - -Handlebars.registerHelper('profile', function(profileId) { - var profile = ProfileCollection.get(profileId); - - if (profile) { - return new Handlebars.SafeString('<span class="label label-default profile-label">' + profile.get('name') + '</span>'); - } - - return undefined; -}); \ No newline at end of file diff --git a/src/UI/Handlebars/Helpers/Series.js b/src/UI/Handlebars/Helpers/Series.js deleted file mode 100644 index dc9af4c97..000000000 --- a/src/UI/Handlebars/Helpers/Series.js +++ /dev/null @@ -1,96 +0,0 @@ -var Handlebars = require('handlebars'); -var StatusModel = require('../../System/StatusModel'); -var _ = require('underscore'); - -Handlebars.registerHelper('poster', function() { - - var placeholder = StatusModel.get('urlBase') + '/Content/Images/poster-dark.png'; - var poster = _.where(this.images, { coverType : 'poster' }); - - if (poster[0]) { - if (!poster[0].url.match(/^https?:\/\//)) { - return new Handlebars.SafeString('<img class="series-poster x-series-poster" {0}>'.format(Handlebars.helpers.defaultImg.call(null, poster[0].url, 250))); - } else { - var url = poster[0].url.replace(/^https?\:/, ''); - return new Handlebars.SafeString('<img class="series-poster x-series-poster" {0}>'.format(Handlebars.helpers.defaultImg.call(null, url))); - } - } - - return new Handlebars.SafeString('<img class="series-poster placeholder-image" src="{0}">'.format(placeholder)); -}); - -Handlebars.registerHelper('traktUrl', function() { - return 'http://trakt.tv/search/tvdb/' + this.tvdbId + '?id_type=show'; -}); - -Handlebars.registerHelper('imdbUrl', function() { - return 'http://imdb.com/title/' + this.imdbId; -}); - -Handlebars.registerHelper('tvdbUrl', function() { - return 'http://www.thetvdb.com/?tab=series&id=' + this.tvdbId; -}); - -Handlebars.registerHelper('tvRageUrl', function() { - return 'http://www.tvrage.com/shows/id-' + this.tvRageId; -}); - -Handlebars.registerHelper('tvMazeUrl', function() { - return 'http://www.tvmaze.com/shows/' + this.tvMazeId + '/_'; -}); - -Handlebars.registerHelper('route', function() { - return StatusModel.get('urlBase') + '/series/' + this.titleSlug; -}); - -Handlebars.registerHelper('percentOfEpisodes', function() { - var episodeCount = this.episodeCount; - var episodeFileCount = this.episodeFileCount; - - var percent = 100; - - if (episodeCount > 0) { - percent = episodeFileCount / episodeCount * 100; - } - - return percent; -}); - -Handlebars.registerHelper('seasonCountHelper', function() { - var seasonCount = this.seasonCount; - var continuing = this.status === 'continuing'; - - if (continuing) { - return new Handlebars.SafeString('<span class="label label-info">Season {0}</span>'.format(seasonCount)); - } - - if (seasonCount === 1) { - return new Handlebars.SafeString('<span class="label label-info">{0} Season</span>'.format(seasonCount)); - } - - return new Handlebars.SafeString('<span class="label label-info">{0} Seasons</span>'.format(seasonCount)); -}); - -Handlebars.registerHelper ('truncate', function (str, len) { - if (str && str.length > len && str.length > 0) { - var new_str = str + " "; - new_str = str.substr (0, len); - new_str = str.substr (0, new_str.lastIndexOf(" ")); - new_str = (new_str.length > 0) ? new_str : str.substr (0, len); - - return new Handlebars.SafeString ( new_str +'...' ); - } - return str; -}); - -/*Handlebars.registerHelper('titleWithYear', function() { - if (this.title.endsWith(' ({0})'.format(this.year))) { - return this.title; - } - - if (!this.year) { - return this.title; - } - - return new Handlebars.SafeString('{0} <span class="year">({1})</span>'.format(this.title, this.year)); -});*/ diff --git a/src/UI/Handlebars/Helpers/String.js b/src/UI/Handlebars/Helpers/String.js deleted file mode 100644 index 761f565c0..000000000 --- a/src/UI/Handlebars/Helpers/String.js +++ /dev/null @@ -1,7 +0,0 @@ -var Handlebars = require('handlebars'); - -Handlebars.registerHelper('TitleCase', function(input) { - return new Handlebars.SafeString(input.replace(/\w\S*/g, function(txt) { - return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase(); - })); -}); \ No newline at end of file diff --git a/src/UI/Handlebars/Helpers/System.js b/src/UI/Handlebars/Helpers/System.js deleted file mode 100644 index 269414666..000000000 --- a/src/UI/Handlebars/Helpers/System.js +++ /dev/null @@ -1,18 +0,0 @@ -var Handlebars = require('handlebars'); -var StatusModel = require('../../System/StatusModel'); - -Handlebars.registerHelper('if_windows', function(options) { - if (StatusModel.get('isWindows')) { - return options.fn(this); - } - - return options.inverse(this); -}); - -Handlebars.registerHelper('if_mono', function(options) { - if (StatusModel.get('isMono')) { - return options.fn(this); - } - - return options.inverse(this); -}); \ No newline at end of file diff --git a/src/UI/Handlebars/backbone.marionette.templates.js b/src/UI/Handlebars/backbone.marionette.templates.js deleted file mode 100644 index 6b47d0253..000000000 --- a/src/UI/Handlebars/backbone.marionette.templates.js +++ /dev/null @@ -1,38 +0,0 @@ -var Handlebars = require('handlebars'); -require('handlebars.helpers'); -require('./Helpers/DateTime'); -require('./Helpers/Html'); -require('./Helpers/Numbers'); -require('./Helpers/Episode'); -//require('./Helpers/Series'); -require('./Helpers/Artist'); -require('./Helpers/Album'); -require('./Helpers/Quality'); -require('./Helpers/System'); -require('./Helpers/EachReverse'); -require('./Helpers/String'); -require('./Handlebars.Debug'); - -module.exports = function() { - this.get = function(templateId) { - var templateKey = templateId.toLowerCase().replace('template', ''); - - var templateFunction = window.T[templateKey]; - - if (!templateFunction) { - throw 'couldn\'t find pre-compiled template ' + templateKey; - } - - return function(data) { - try { - var wrappedTemplate = Handlebars.template.call(Handlebars, templateFunction); - return wrappedTemplate(data); - } - catch (error) { - console.error('template render failed for ' + templateKey + ' ' + error); - console.error(data); - throw error; - } - }; - }; -}; \ No newline at end of file diff --git a/src/UI/Health/HealthCollection.js b/src/UI/Health/HealthCollection.js deleted file mode 100644 index e0935a885..000000000 --- a/src/UI/Health/HealthCollection.js +++ /dev/null @@ -1,13 +0,0 @@ -var Backbone = require('backbone'); -var HealthModel = require('./HealthModel'); -require('../Mixins/backbone.signalr.mixin'); - -var Collection = Backbone.Collection.extend({ - url : window.NzbDrone.ApiRoot + '/health', - model : HealthModel -}); - -var collection = new Collection().bindSignalR(); -collection.fetch(); - -module.exports = collection; \ No newline at end of file diff --git a/src/UI/Health/HealthModel.js b/src/UI/Health/HealthModel.js deleted file mode 100644 index 3986a5948..000000000 --- a/src/UI/Health/HealthModel.js +++ /dev/null @@ -1,3 +0,0 @@ -var Backbone = require('backbone'); - -module.exports = Backbone.Model.extend({}); \ No newline at end of file diff --git a/src/UI/Health/HealthView.js b/src/UI/Health/HealthView.js deleted file mode 100644 index 61ebd525a..000000000 --- a/src/UI/Health/HealthView.js +++ /dev/null @@ -1,37 +0,0 @@ -var _ = require('underscore'); -var Marionette = require('marionette'); -var HealthCollection = require('./HealthCollection'); - -module.exports = Marionette.ItemView.extend({ - tagName : 'span', - - initialize : function() { - this.listenTo(HealthCollection, 'sync', this._healthSync); - HealthCollection.fetch(); - }, - - render : function() { - this.$el.empty(); - - if (HealthCollection.length === 0) { - return this; - } - - var count = HealthCollection.length; - var label = 'label-warning'; - var errors = HealthCollection.some(function(model) { - return model.get('type') === 'error'; - }); - - if (errors) { - label = 'label-danger'; - } - - this.$el.html('<span class="label {0}">{1}</span>'.format(label, count)); - return this; - }, - - _healthSync : function() { - this.render(); - } -}); \ No newline at end of file diff --git a/src/UI/Hotkeys/Hotkeys.js b/src/UI/Hotkeys/Hotkeys.js deleted file mode 100644 index b72a574da..000000000 --- a/src/UI/Hotkeys/Hotkeys.js +++ /dev/null @@ -1,34 +0,0 @@ -var $ = require('jquery'); -var vent = require('vent'); -var HotkeysView = require('./HotkeysView'); - -$(document).on('keypress', function(e) { - if ($(e.target).is('input') || $(e.target).is('textarea')) { - return; - } - - if (e.charCode === 63) { - vent.trigger(vent.Commands.OpenModalCommand, new HotkeysView()); - } -}); - -$(document).on('keydown', function(e) { - if (e.ctrlKey && e.keyCode === 83) { - vent.trigger(vent.Hotkeys.SaveSettings); - e.preventDefault(); - return; - } - - if ($(e.target).is('input') || $(e.target).is('textarea')) { - return; - } - - if (e.ctrlKey || e.metaKey || e.altKey) { - return; - } - - if (e.keyCode === 84) { - vent.trigger(vent.Hotkeys.NavbarSearch); - e.preventDefault(); - } -}); diff --git a/src/UI/Hotkeys/HotkeysView.js b/src/UI/Hotkeys/HotkeysView.js deleted file mode 100644 index ee643fbb2..000000000 --- a/src/UI/Hotkeys/HotkeysView.js +++ /dev/null @@ -1,6 +0,0 @@ -var vent = require('vent'); -var Marionette = require('marionette'); - -module.exports = Marionette.ItemView.extend({ - template : 'Hotkeys/HotkeysViewTemplate' -}); \ No newline at end of file diff --git a/src/UI/Hotkeys/HotkeysViewTemplate.hbs b/src/UI/Hotkeys/HotkeysViewTemplate.hbs deleted file mode 100644 index bce6e86c8..000000000 --- a/src/UI/Hotkeys/HotkeysViewTemplate.hbs +++ /dev/null @@ -1,45 +0,0 @@ -<div class="modal-content"> - <div class="modal-header"> - <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> - <h3>Keyboard Shortcuts</h3> - </div> - <div class="modal-body hotkeys-modal"> - <div class="row hotkey-group"> - <div class="col-md-12"> - <div class="row"> - <div class="col-md-5 col-md-offset-1"> - <h3>Focus Search Box</h3> - </div> - <div class="col-md-3"> - <kbd class="hotkey">t</kbd> - </div> - </div> - <div class="row"> - <div class="col-md-11 col-md-offset-1"> - Pressing 't' puts the cursor in the search box below the navigation links - </div> - </div> - </div> - </div> - <div class="row hotkey-group"> - <div class="col-md-12"> - <div class="row"> - <div class="col-md-5 col-md-offset-1"> - <h3>Save Settings</h3> - </div> - <div class="col-md-3"> - <kbd class="hotkey">ctrl + s</kbd> - </div> - </div> - <div class="row"> - <div class="col-md-11 col-md-offset-1"> - Pressing ctrl + 's' saves your settings (only in settings) - </div> - </div> - </div> - </div> - </div> - <div class="modal-footer"> - <button class="btn" data-dismiss="modal">Close</button> - </div> -</div> diff --git a/src/UI/Hotkeys/hotkeys.less b/src/UI/Hotkeys/hotkeys.less deleted file mode 100644 index b3213825d..000000000 --- a/src/UI/Hotkeys/hotkeys.less +++ /dev/null @@ -1,23 +0,0 @@ -.hotkeys-modal { - h3 { - margin-top : 0px; - margin-botton : 0px; - } - - .hotkey-group { - &:first-of-type { - margin-top : 0px; - } - - &:last-of-type { - margin-bottom : 0px; - } - - margin-top : 25px; - margin-bottom : 25px; - - .hotkey { - font-size : 22px; - } - } -} \ No newline at end of file diff --git a/src/UI/Instrumentation/ErrorHandler.js b/src/UI/Instrumentation/ErrorHandler.js deleted file mode 100644 index 189173662..000000000 --- a/src/UI/Instrumentation/ErrorHandler.js +++ /dev/null @@ -1,86 +0,0 @@ -var $ = require('jquery'); -var Messenger = require('messenger'); - -window.alert = function(message) { - new Messenger().post(message); -}; - -var addError = function(message) { - $('#errors').append('<div>' + message + '</div>'); -}; - -window.onerror = function(msg, url, line) { - - try { - - var a = document.createElement('a'); - a.href = url; - var filename = a.pathname.split('/').pop(); - - //Suppress Firefox debug errors when console window is closed - if (filename.toLowerCase() === 'markupview.jsm' || filename.toLowerCase() === 'markup-view.js') { - return false; - } - - var messageText = filename + ' : ' + line + '</br>' + msg; - - var message = { - message : messageText, - type : 'error', - hideAfter : 1000, - showCloseButton : true - }; - - new Messenger().post(message); - - addError(message.message); - - } - catch (error) { - console.log('An error occurred while reporting error. ' + error); - console.log(msg); - new Messenger().post('Couldn\'t report JS error. ' + msg); - } - - return false; //don't suppress default alerts and logs. -}; - -$(document).ajaxError(function(event, xmlHttpRequest, ajaxOptions) { - - //don't report 200 error codes - if (xmlHttpRequest.status >= 200 && xmlHttpRequest.status <= 300) { - return undefined; - } - - //don't report aborted requests - if (xmlHttpRequest.statusText === 'abort') { - return undefined; - } - - var message = { - type : 'error', - hideAfter : 1000, - showCloseButton : true - }; - - if (xmlHttpRequest.status === 0 && xmlHttpRequest.readyState === 0) { - return false; - } - - if (xmlHttpRequest.status === 400 && ajaxOptions.isValidatedCall) { - return false; - } - - if (xmlHttpRequest.status === 503) { - message.message = xmlHttpRequest.responseJSON.message; - } else if (xmlHttpRequest.status === 409) { - message.message = xmlHttpRequest.responseJSON.message; - } else { - message.message = '[{0}] {1} : {2}'.format(ajaxOptions.type, xmlHttpRequest.statusText, ajaxOptions.url); - } - - new Messenger().post(message); - addError(message.message); - - return false; -}); \ No newline at end of file diff --git a/src/UI/Instrumentation/StringFormat.js b/src/UI/Instrumentation/StringFormat.js deleted file mode 100644 index 059d25f5e..000000000 --- a/src/UI/Instrumentation/StringFormat.js +++ /dev/null @@ -1,13 +0,0 @@ -'use strict'; - -String.prototype.format = function() { - var args = arguments; - - return this.replace(/{(\d+)}/g, function(match, number) { - if (typeof args[number] !== 'undefined') { - return args[number]; - } else { - return match; - } - }); -}; \ No newline at end of file diff --git a/src/UI/JsLibraries/backbone.backgrid.js b/src/UI/JsLibraries/backbone.backgrid.js deleted file mode 100644 index 6a0af616c..000000000 --- a/src/UI/JsLibraries/backbone.backgrid.js +++ /dev/null @@ -1,2764 +0,0 @@ -/*! - backgrid - http://github.com/wyuenho/backgrid - - Copyright (c) 2013 Jimmy Yuen Ho Wong and contributors <wyuenho@gmail.com> - Licensed under the MIT license. -*/ - -(function (factory) { - - // CommonJS - if (typeof exports == "object") { - module.exports = factory(module.exports, - require("underscore"), - require("backbone")); - } - // Browser - else if (typeof _ !== "undefined" && - typeof Backbone !== "undefined") { - factory(window, _, Backbone); - } -}(function (root, _, Backbone) { - - "use strict"; -/* - backgrid - http://github.com/wyuenho/backgrid - - Copyright (c) 2013 Jimmy Yuen Ho Wong and contributors - Licensed under the MIT license. -*/ - -// Copyright 2009, 2010 Kristopher Michael Kowal -// https://github.com/kriskowal/es5-shim -// ES5 15.5.4.20 -// http://es5.github.com/#x15.5.4.20 -var ws = "\x09\x0A\x0B\x0C\x0D\x20\xA0\u1680\u180E\u2000\u2001\u2002\u2003" + - "\u2004\u2005\u2006\u2007\u2008\u2009\u200A\u202F\u205F\u3000\u2028" + - "\u2029\uFEFF"; -if (!String.prototype.trim || ws.trim()) { - // http://blog.stevenlevithan.com/archives/faster-trim-javascript - // http://perfectionkills.com/whitespace-deviations/ - ws = "[" + ws + "]"; - var trimBeginRegexp = new RegExp("^" + ws + ws + "*"), - trimEndRegexp = new RegExp(ws + ws + "*$"); - String.prototype.trim = function trim() { - if (this === undefined || this === null) { - throw new TypeError("can't convert " + this + " to object"); - } - return String(this) - .replace(trimBeginRegexp, "") - .replace(trimEndRegexp, ""); - }; -} - -function lpad(str, length, padstr) { - var paddingLen = length - (str + '').length; - paddingLen = paddingLen < 0 ? 0 : paddingLen; - var padding = ''; - for (var i = 0; i < paddingLen; i++) { - padding = padding + padstr; - } - return padding + str; -} - -var $ = Backbone.$; - -var Backgrid = root.Backgrid = { - - VERSION: "0.3.0", - - Extension: {}, - - resolveNameToClass: function (name, suffix) { - if (_.isString(name)) { - var key = _.map(name.split('-'), function (e) { - return e.slice(0, 1).toUpperCase() + e.slice(1); - }).join('') + suffix; - var klass = Backgrid[key] || Backgrid.Extension[key]; - if (_.isUndefined(klass)) { - throw new ReferenceError("Class '" + key + "' not found"); - } - return klass; - } - - return name; - }, - - callByNeed: function () { - var value = arguments[0]; - if (!_.isFunction(value)) return value; - - var context = arguments[1]; - var args = [].slice.call(arguments, 2); - return value.apply(context, !!(args + '') ? args : void 0); - } - -}; -_.extend(Backgrid, Backbone.Events); - -/** - Command translates a DOM Event into commands that Backgrid - recognizes. Interested parties can listen on selected Backgrid events that - come with an instance of this class and act on the commands. - - It is also possible to globally rebind the keyboard shortcuts by replacing - the methods in this class' prototype. - - @class Backgrid.Command - @constructor - */ -var Command = Backgrid.Command = function (evt) { - _.extend(this, { - altKey: !!evt.altKey, - "char": evt["char"], - charCode: evt.charCode, - ctrlKey: !!evt.ctrlKey, - key: evt.key, - keyCode: evt.keyCode, - locale: evt.locale, - location: evt.location, - metaKey: !!evt.metaKey, - repeat: !!evt.repeat, - shiftKey: !!evt.shiftKey, - which: evt.which - }); -}; -_.extend(Command.prototype, { - /** - Up Arrow - - @member Backgrid.Command - */ - moveUp: function () { return this.keyCode == 38; }, - /** - Down Arrow - - @member Backgrid.Command - */ - moveDown: function () { return this.keyCode === 40; }, - /** - Shift Tab - - @member Backgrid.Command - */ - moveLeft: function () { return this.shiftKey && this.keyCode === 9; }, - /** - Tab - - @member Backgrid.Command - */ - moveRight: function () { return !this.shiftKey && this.keyCode === 9; }, - /** - Enter - - @member Backgrid.Command - */ - save: function () { return this.keyCode === 13; }, - /** - Esc - - @member Backgrid.Command - */ - cancel: function () { return this.keyCode === 27; }, - /** - None of the above. - - @member Backgrid.Command - */ - passThru: function () { - return !(this.moveUp() || this.moveDown() || this.moveLeft() || - this.moveRight() || this.save() || this.cancel()); - } -}); - - -/* - backgrid - http://github.com/wyuenho/backgrid - - Copyright (c) 2013 Jimmy Yuen Ho Wong and contributors - Licensed under the MIT license. -*/ - -/** - Just a convenient class for interested parties to subclass. - - The default Cell classes don't require the formatter to be a subclass of - Formatter as long as the fromRaw(rawData) and toRaw(formattedData) methods - are defined. - - @abstract - @class Backgrid.CellFormatter - @constructor -*/ -var CellFormatter = Backgrid.CellFormatter = function () {}; -_.extend(CellFormatter.prototype, { - - /** - Takes a raw value from a model and returns an optionally formatted string - for display. The default implementation simply returns the supplied value - as is without any type conversion. - - @member Backgrid.CellFormatter - @param {*} rawData - @param {Backbone.Model} model Used for more complicated formatting - @return {*} - */ - fromRaw: function (rawData, model) { - return rawData; - }, - - /** - Takes a formatted string, usually from user input, and returns a - appropriately typed value for persistence in the model. - - If the user input is invalid or unable to be converted to a raw value - suitable for persistence in the model, toRaw must return `undefined`. - - @member Backgrid.CellFormatter - @param {string} formattedData - @param {Backbone.Model} model Used for more complicated formatting - @return {*|undefined} - */ - toRaw: function (formattedData, model) { - return formattedData; - } - -}); - -/** - A floating point number formatter. Doesn't understand scientific notation at - the moment. - - @class Backgrid.NumberFormatter - @extends Backgrid.CellFormatter - @constructor - @throws {RangeError} If decimals < 0 or > 20. -*/ -var NumberFormatter = Backgrid.NumberFormatter = function (options) { - _.extend(this, this.defaults, options || {}); - - if (this.decimals < 0 || this.decimals > 20) { - throw new RangeError("decimals must be between 0 and 20"); - } -}; -NumberFormatter.prototype = new CellFormatter(); -_.extend(NumberFormatter.prototype, { - - /** - @member Backgrid.NumberFormatter - @cfg {Object} options - - @cfg {number} [options.decimals=2] Number of decimals to display. Must be an integer. - - @cfg {string} [options.decimalSeparator='.'] The separator to use when - displaying decimals. - - @cfg {string} [options.orderSeparator=','] The separator to use to - separator thousands. May be an empty string. - */ - defaults: { - decimals: 2, - decimalSeparator: '.', - orderSeparator: ',' - }, - - HUMANIZED_NUM_RE: /(\d)(?=(?:\d{3})+$)/g, - - /** - Takes a floating point number and convert it to a formatted string where - every thousand is separated by `orderSeparator`, with a `decimal` number of - decimals separated by `decimalSeparator`. The number returned is rounded - the usual way. - - @member Backgrid.NumberFormatter - @param {number} number - @param {Backbone.Model} model Used for more complicated formatting - @return {string} - */ - fromRaw: function (number, model) { - if (_.isNull(number) || _.isUndefined(number)) return ''; - - number = number.toFixed(~~this.decimals); - - var parts = number.split('.'); - var integerPart = parts[0]; - var decimalPart = parts[1] ? (this.decimalSeparator || '.') + parts[1] : ''; - - return integerPart.replace(this.HUMANIZED_NUM_RE, '$1' + this.orderSeparator) + decimalPart; - }, - - /** - Takes a string, possibly formatted with `orderSeparator` and/or - `decimalSeparator`, and convert it back to a number. - - @member Backgrid.NumberFormatter - @param {string} formattedData - @param {Backbone.Model} model Used for more complicated formatting - @return {number|undefined} Undefined if the string cannot be converted to - a number. - */ - toRaw: function (formattedData, model) { - formattedData = formattedData.trim(); - - if (formattedData === '') return null; - - var rawData = ''; - - var thousands = formattedData.split(this.orderSeparator); - for (var i = 0; i < thousands.length; i++) { - rawData += thousands[i]; - } - - var decimalParts = rawData.split(this.decimalSeparator); - rawData = ''; - for (var i = 0; i < decimalParts.length; i++) { - rawData = rawData + decimalParts[i] + '.'; - } - - if (rawData[rawData.length - 1] === '.') { - rawData = rawData.slice(0, rawData.length - 1); - } - - var result = (rawData * 1).toFixed(~~this.decimals) * 1; - if (_.isNumber(result) && !_.isNaN(result)) return result; - } - -}); - -/** - Formatter to converts between various datetime formats. - - This class only understands ISO-8601 formatted datetime strings and UNIX - offset (number of milliseconds since UNIX Epoch). See - Backgrid.Extension.MomentFormatter if you need a much more flexible datetime - formatter. - - @class Backgrid.DatetimeFormatter - @extends Backgrid.CellFormatter - @constructor - @throws {Error} If both `includeDate` and `includeTime` are false. -*/ -var DatetimeFormatter = Backgrid.DatetimeFormatter = function (options) { - _.extend(this, this.defaults, options || {}); - - if (!this.includeDate && !this.includeTime) { - throw new Error("Either includeDate or includeTime must be true"); - } -}; -DatetimeFormatter.prototype = new CellFormatter(); -_.extend(DatetimeFormatter.prototype, { - - /** - @member Backgrid.DatetimeFormatter - - @cfg {Object} options - - @cfg {boolean} [options.includeDate=true] Whether the values include the - date part. - - @cfg {boolean} [options.includeTime=true] Whether the values include the - time part. - - @cfg {boolean} [options.includeMilli=false] If `includeTime` is true, - whether to include the millisecond part, if it exists. - */ - defaults: { - includeDate: true, - includeTime: true, - includeMilli: false - }, - - DATE_RE: /^([+\-]?\d{4})-(\d{2})-(\d{2})$/, - TIME_RE: /^(\d{2}):(\d{2}):(\d{2})(\.(\d{3}))?$/, - ISO_SPLITTER_RE: /T|Z| +/, - - _convert: function (data, validate) { - if ((data + '').trim() === '') return null; - - var date, time = null; - if (_.isNumber(data)) { - var jsDate = new Date(data); - date = lpad(jsDate.getUTCFullYear(), 4, 0) + '-' + lpad(jsDate.getUTCMonth() + 1, 2, 0) + '-' + lpad(jsDate.getUTCDate(), 2, 0); - time = lpad(jsDate.getUTCHours(), 2, 0) + ':' + lpad(jsDate.getUTCMinutes(), 2, 0) + ':' + lpad(jsDate.getUTCSeconds(), 2, 0); - } - else { - data = data.trim(); - var parts = data.split(this.ISO_SPLITTER_RE) || []; - date = this.DATE_RE.test(parts[0]) ? parts[0] : ''; - time = date && parts[1] ? parts[1] : this.TIME_RE.test(parts[0]) ? parts[0] : ''; - } - - var YYYYMMDD = this.DATE_RE.exec(date) || []; - var HHmmssSSS = this.TIME_RE.exec(time) || []; - - if (validate) { - if (this.includeDate && _.isUndefined(YYYYMMDD[0])) return; - if (this.includeTime && _.isUndefined(HHmmssSSS[0])) return; - if (!this.includeDate && date) return; - if (!this.includeTime && time) return; - } - - var jsDate = new Date(Date.UTC(YYYYMMDD[1] * 1 || 0, - YYYYMMDD[2] * 1 - 1 || 0, - YYYYMMDD[3] * 1 || 0, - HHmmssSSS[1] * 1 || null, - HHmmssSSS[2] * 1 || null, - HHmmssSSS[3] * 1 || null, - HHmmssSSS[5] * 1 || null)); - - var result = ''; - - if (this.includeDate) { - result = lpad(jsDate.getUTCFullYear(), 4, 0) + '-' + lpad(jsDate.getUTCMonth() + 1, 2, 0) + '-' + lpad(jsDate.getUTCDate(), 2, 0); - } - - if (this.includeTime) { - result = result + (this.includeDate ? 'T' : '') + lpad(jsDate.getUTCHours(), 2, 0) + ':' + lpad(jsDate.getUTCMinutes(), 2, 0) + ':' + lpad(jsDate.getUTCSeconds(), 2, 0); - - if (this.includeMilli) { - result = result + '.' + lpad(jsDate.getUTCMilliseconds(), 3, 0); - } - } - - if (this.includeDate && this.includeTime) { - result += "Z"; - } - - return result; - }, - - /** - Converts an ISO-8601 formatted datetime string to a datetime string, date - string or a time string. The timezone is ignored if supplied. - - @member Backgrid.DatetimeFormatter - @param {string} rawData - @param {Backbone.Model} model Used for more complicated formatting - @return {string|null|undefined} ISO-8601 string in UTC. Null and undefined - values are returned as is. - */ - fromRaw: function (rawData, model) { - if (_.isNull(rawData) || _.isUndefined(rawData)) return ''; - return this._convert(rawData); - }, - - /** - Converts an ISO-8601 formatted datetime string to a datetime string, date - string or a time string. The timezone is ignored if supplied. This method - parses the input values exactly the same way as - Backgrid.Extension.MomentFormatter#fromRaw(), in addition to doing some - sanity checks. - - @member Backgrid.DatetimeFormatter - @param {string} formattedData - @param {Backbone.Model} model Used for more complicated formatting - @return {string|undefined} ISO-8601 string in UTC. Undefined if a date is - found when `includeDate` is false, or a time is found when `includeTime` is - false, or if `includeDate` is true and a date is not found, or if - `includeTime` is true and a time is not found. - */ - toRaw: function (formattedData, model) { - return this._convert(formattedData, true); - } - -}); - -/** - Formatter to convert any value to string. - - @class Backgrid.StringFormatter - @extends Backgrid.CellFormatter - @constructor - */ -var StringFormatter = Backgrid.StringFormatter = function () {}; -StringFormatter.prototype = new CellFormatter(); -_.extend(StringFormatter.prototype, { - /** - Converts any value to a string using Ecmascript's implicit type - conversion. If the given value is `null` or `undefined`, an empty string is - returned instead. - - @member Backgrid.StringFormatter - @param {*} rawValue - @param {Backbone.Model} model Used for more complicated formatting - @return {string} - */ - fromRaw: function (rawValue, model) { - if (_.isUndefined(rawValue) || _.isNull(rawValue)) return ''; - return rawValue + ''; - } -}); - -/** - Simple email validation formatter. - - @class Backgrid.EmailFormatter - @extends Backgrid.CellFormatter - @constructor - */ -var EmailFormatter = Backgrid.EmailFormatter = function () {}; -EmailFormatter.prototype = new CellFormatter(); -_.extend(EmailFormatter.prototype, { - /** - Return the input if it is a string that contains an '@' character and if - the strings before and after '@' are non-empty. If the input does not - validate, `undefined` is returned. - - @member Backgrid.EmailFormatter - @param {*} formattedData - @param {Backbone.Model} model Used for more complicated formatting - @return {string|undefined} - */ - toRaw: function (formattedData, model) { - var parts = formattedData.trim().split("@"); - if (parts.length === 2 && _.all(parts)) { - return formattedData; - } - } -}); - -/** - Formatter for SelectCell. - - @class Backgrid.SelectFormatter - @extends Backgrid.CellFormatter - @constructor -*/ -var SelectFormatter = Backgrid.SelectFormatter = function () {}; -SelectFormatter.prototype = new CellFormatter(); -_.extend(SelectFormatter.prototype, { - - /** - Normalizes raw scalar or array values to an array. - - @member Backgrid.SelectFormatter - @param {*} rawValue - @param {Backbone.Model} model Used for more complicated formatting - @return {Array.<*>} - */ - fromRaw: function (rawValue, model) { - return _.isArray(rawValue) ? rawValue : rawValue != null ? [rawValue] : []; - } -}); - - -/* - backgrid - http://github.com/wyuenho/backgrid - - Copyright (c) 2013 Jimmy Yuen Ho Wong and contributors - Licensed under the MIT license. -*/ - -/** - Generic cell editor base class. Only defines an initializer for a number of - required parameters. - - @abstract - @class Backgrid.CellEditor - @extends Backbone.View -*/ -var CellEditor = Backgrid.CellEditor = Backbone.View.extend({ - - /** - Initializer. - - @param {Object} options - @param {Backgrid.CellFormatter} options.formatter - @param {Backgrid.Column} options.column - @param {Backbone.Model} options.model - - @throws {TypeError} If `formatter` is not a formatter instance, or when - `model` or `column` are undefined. - */ - initialize: function (options) { - this.formatter = options.formatter; - this.column = options.column; - if (!(this.column instanceof Column)) { - this.column = new Column(this.column); - } - - this.listenTo(this.model, "backgrid:editing", this.postRender); - }, - - /** - Post-rendering setup and initialization. Focuses the cell editor's `el` in - this default implementation. **Should** be called by Cell classes after - calling Backgrid.CellEditor#render. - */ - postRender: function (model, column) { - if (column == null || column.get("name") == this.column.get("name")) { - this.$el.focus(); - } - return this; - } - -}); - -/** - InputCellEditor the cell editor type used by most core cell types. This cell - editor renders a text input box as its editor. The input will render a - placeholder if the value is empty on supported browsers. - - @class Backgrid.InputCellEditor - @extends Backgrid.CellEditor -*/ -var InputCellEditor = Backgrid.InputCellEditor = CellEditor.extend({ - - /** @property */ - tagName: "input", - - /** @property */ - attributes: { - type: "text" - }, - - /** @property */ - events: { - "blur": "saveOrCancel", - "keydown": "saveOrCancel" - }, - - /** - Initializer. Removes this `el` from the DOM when a `done` event is - triggered. - - @param {Object} options - @param {Backgrid.CellFormatter} options.formatter - @param {Backgrid.Column} options.column - @param {Backbone.Model} options.model - @param {string} [options.placeholder] - */ - initialize: function (options) { - InputCellEditor.__super__.initialize.apply(this, arguments); - - if (options.placeholder) { - this.$el.attr("placeholder", options.placeholder); - } - }, - - /** - Renders a text input with the cell value formatted for display, if it - exists. - */ - render: function () { - var model = this.model - this.$el.val(this.formatter.fromRaw(model.get(this.column.get("name")), model)); - return this; - }, - - /** - If the key pressed is `enter`, `tab`, `up`, or `down`, converts the value - in the editor to a raw value for saving into the model using the formatter. - - If the key pressed is `esc` the changes are undone. - - If the editor goes out of focus (`blur`) but the value is invalid, the - event is intercepted and cancelled so the cell remains in focus pending for - further action. The changes are saved otherwise. - - Triggers a Backbone `backgrid:edited` event from the model when successful, - and `backgrid:error` if the value cannot be converted. Classes listening to - the `error` event, usually the Cell classes, should respond appropriately, - usually by rendering some kind of error feedback. - - @param {Event} e - */ - saveOrCancel: function (e) { - - var formatter = this.formatter; - var model = this.model; - var column = this.column; - - var command = new Command(e); - var blurred = e.type === "blur"; - - if (command.moveUp() || command.moveDown() || command.moveLeft() || command.moveRight() || - command.save() || blurred) { - - e.preventDefault(); - e.stopPropagation(); - - var val = this.$el.val(); - var newValue = formatter.toRaw(val, model); - if (_.isUndefined(newValue)) { - model.trigger("backgrid:error", model, column, val); - } - else { - model.set(column.get("name"), newValue); - model.trigger("backgrid:edited", model, column, command); - } - } - // esc - else if (command.cancel()) { - // undo - e.stopPropagation(); - model.trigger("backgrid:edited", model, column, command); - } - }, - - postRender: function (model, column) { - if (column == null || column.get("name") == this.column.get("name")) { - // move the cursor to the end on firefox if text is right aligned - if (this.$el.css("text-align") === "right") { - var val = this.$el.val(); - this.$el.focus().val(null).val(val); - } - else this.$el.focus(); - } - return this; - } - -}); - -/** - The super-class for all Cell types. By default, this class renders a plain - table cell with the model value converted to a string using the - formatter. The table cell is clickable, upon which the cell will go into - editor mode, which is rendered by a Backgrid.InputCellEditor instance by - default. Upon encountering any formatting errors, this class will add an - `error` CSS class to the table cell. - - @abstract - @class Backgrid.Cell - @extends Backbone.View -*/ -var Cell = Backgrid.Cell = Backbone.View.extend({ - - /** @property */ - tagName: "td", - - /** - @property {Backgrid.CellFormatter|Object|string} [formatter=CellFormatter] - */ - formatter: CellFormatter, - - /** - @property {Backgrid.CellEditor} [editor=Backgrid.InputCellEditor] The - default editor for all cell instances of this class. This value must be a - class, it will be automatically instantiated upon entering edit mode. - - See Backgrid.CellEditor - */ - editor: InputCellEditor, - - /** @property */ - events: { - "click": "enterEditMode" - }, - - /** - Initializer. - - @param {Object} options - @param {Backbone.Model} options.model - @param {Backgrid.Column} options.column - - @throws {ReferenceError} If formatter is a string but a formatter class of - said name cannot be found in the Backgrid module. - */ - initialize: function (options) { - this.column = options.column; - if (!(this.column instanceof Column)) { - this.column = new Column(this.column); - } - - var column = this.column, model = this.model, $el = this.$el; - - var formatter = Backgrid.resolveNameToClass(column.get("formatter") || - this.formatter, "Formatter"); - - if (!_.isFunction(formatter.fromRaw) && !_.isFunction(formatter.toRaw)) { - formatter = new formatter(); - } - - this.formatter = formatter; - - this.editor = Backgrid.resolveNameToClass(this.editor, "CellEditor"); - - this.listenTo(model, "change:" + column.get("name"), function () { - if (!$el.hasClass("editor")) this.render(); - }); - - this.listenTo(model, "backgrid:error", this.renderError); - - this.listenTo(column, "change:editable change:sortable change:renderable", - function (column) { - var changed = column.changedAttributes(); - for (var key in changed) { - if (changed.hasOwnProperty(key)) { - $el.toggleClass(key, changed[key]); - } - } - }); - - if (column.get("editable")) $el.addClass("editable"); - if (column.get("sortable")) $el.addClass("sortable"); - if (column.get("renderable")) $el.addClass("renderable"); - }, - - /** - Render a text string in a table cell. The text is converted from the - model's raw value for this cell's column. - */ - render: function () { - this.$el.empty(); - var model = this.model; - this.$el.text(this.formatter.fromRaw(model.get(this.column.get("name")), model)); - this.delegateEvents(); - return this; - }, - - /** - If this column is editable, a new CellEditor instance is instantiated with - its required parameters. An `editor` CSS class is added to the cell upon - entering edit mode. - - This method triggers a Backbone `backgrid:edit` event from the model when - the cell is entering edit mode and an editor instance has been constructed, - but before it is rendered and inserted into the DOM. The cell and the - constructed cell editor instance are sent as event parameters when this - event is triggered. - - When this cell has finished switching to edit mode, a Backbone - `backgrid:editing` event is triggered from the model. The cell and the - constructed cell instance are also sent as parameters in the event. - - When the model triggers a `backgrid:error` event, it means the editor is - unable to convert the current user input to an apprpriate value for the - model's column, and an `error` CSS class is added to the cell accordingly. - */ - enterEditMode: function () { - var model = this.model; - var column = this.column; - - var editable = Backgrid.callByNeed(column.editable(), column, model); - if (editable) { - - this.currentEditor = new this.editor({ - column: this.column, - model: this.model, - formatter: this.formatter - }); - - model.trigger("backgrid:edit", model, column, this, this.currentEditor); - - // Need to redundantly undelegate events for Firefox - this.undelegateEvents(); - this.$el.empty(); - this.$el.append(this.currentEditor.$el); - this.currentEditor.render(); - this.$el.addClass("editor"); - - model.trigger("backgrid:editing", model, column, this, this.currentEditor); - } - }, - - /** - Put an `error` CSS class on the table cell. - */ - renderError: function (model, column) { - if (column == null || column.get("name") == this.column.get("name")) { - this.$el.addClass("error"); - } - }, - - /** - Removes the editor and re-render in display mode. - */ - exitEditMode: function () { - this.$el.removeClass("error"); - this.currentEditor.remove(); - this.stopListening(this.currentEditor); - delete this.currentEditor; - this.$el.removeClass("editor"); - this.render(); - }, - - /** - Clean up this cell. - - @chainable - */ - remove: function () { - if (this.currentEditor) { - this.currentEditor.remove.apply(this.currentEditor, arguments); - delete this.currentEditor; - } - return Cell.__super__.remove.apply(this, arguments); - } - -}); - -/** - StringCell displays HTML escaped strings and accepts anything typed in. - - @class Backgrid.StringCell - @extends Backgrid.Cell -*/ -var StringCell = Backgrid.StringCell = Cell.extend({ - - /** @property */ - className: "string-cell", - - formatter: StringFormatter - -}); - -/** - UriCell renders an HTML `<a>` anchor for the value and accepts URIs as user - input values. No type conversion or URL validation is done by the formatter - of this cell. Users who need URL validation are encourage to subclass UriCell - to take advantage of the parsing capabilities of the HTMLAnchorElement - available on HTML5-capable browsers or using a third-party library like - [URI.js](https://github.com/medialize/URI.js). - - @class Backgrid.UriCell - @extends Backgrid.Cell -*/ -var UriCell = Backgrid.UriCell = Cell.extend({ - - /** @property */ - className: "uri-cell", - - /** - @property {string} [title] The title attribute of the generated anchor. It - uses the display value formatted by the `formatter.fromRaw` by default. - */ - title: null, - - /** - @property {string} [target="_blank"] The target attribute of the generated - anchor. - */ - target: "_blank", - - initialize: function (options) { - UriCell.__super__.initialize.apply(this, arguments); - this.title = options.title || this.title; - this.target = options.target || this.target; - }, - - render: function () { - this.$el.empty(); - var rawValue = this.model.get(this.column.get("name")); - var formattedValue = this.formatter.fromRaw(rawValue, this.model); - this.$el.append($("<a>", { - tabIndex: -1, - href: rawValue, - title: this.title || formattedValue, - target: this.target, - }).text(formattedValue)); - this.delegateEvents(); - return this; - } - -}); - -/** - Like Backgrid.UriCell, EmailCell renders an HTML `<a>` anchor for the - value. The `href` in the anchor is prefixed with `mailto:`. EmailCell will - complain if the user enters a string that doesn't contain the `@` sign. - - @class Backgrid.EmailCell - @extends Backgrid.StringCell -*/ -var EmailCell = Backgrid.EmailCell = StringCell.extend({ - - /** @property */ - className: "email-cell", - - formatter: EmailFormatter, - - render: function () { - this.$el.empty(); - var model = this.model; - var formattedValue = this.formatter.fromRaw(model.get(this.column.get("name")), model); - this.$el.append($("<a>", { - tabIndex: -1, - href: "mailto:" + formattedValue, - title: formattedValue - }).text(formattedValue)); - this.delegateEvents(); - return this; - } - -}); - -/** - NumberCell is a generic cell that renders all numbers. Numbers are formatted - using a Backgrid.NumberFormatter. - - @class Backgrid.NumberCell - @extends Backgrid.Cell -*/ -var NumberCell = Backgrid.NumberCell = Cell.extend({ - - /** @property */ - className: "number-cell", - - /** - @property {number} [decimals=2] Must be an integer. - */ - decimals: NumberFormatter.prototype.defaults.decimals, - - /** @property {string} [decimalSeparator='.'] */ - decimalSeparator: NumberFormatter.prototype.defaults.decimalSeparator, - - /** @property {string} [orderSeparator=','] */ - orderSeparator: NumberFormatter.prototype.defaults.orderSeparator, - - /** @property {Backgrid.CellFormatter} [formatter=Backgrid.NumberFormatter] */ - formatter: NumberFormatter, - - /** - Initializes this cell and the number formatter. - - @param {Object} options - @param {Backbone.Model} options.model - @param {Backgrid.Column} options.column - */ - initialize: function (options) { - NumberCell.__super__.initialize.apply(this, arguments); - var formatter = this.formatter; - formatter.decimals = this.decimals; - formatter.decimalSeparator = this.decimalSeparator; - formatter.orderSeparator = this.orderSeparator; - } - -}); - -/** - An IntegerCell is just a Backgrid.NumberCell with 0 decimals. If a floating - point number is supplied, the number is simply rounded the usual way when - displayed. - - @class Backgrid.IntegerCell - @extends Backgrid.NumberCell -*/ -var IntegerCell = Backgrid.IntegerCell = NumberCell.extend({ - - /** @property */ - className: "integer-cell", - - /** - @property {number} decimals Must be an integer. - */ - decimals: 0 -}); - -/** - DatetimeCell is a basic cell that accepts datetime string values in RFC-2822 - or W3C's subset of ISO-8601 and displays them in ISO-8601 format. For a much - more sophisticated date time cell with better datetime formatting, take a - look at the Backgrid.Extension.MomentCell extension. - - @class Backgrid.DatetimeCell - @extends Backgrid.Cell - - See: - - - Backgrid.Extension.MomentCell - - Backgrid.DatetimeFormatter -*/ -var DatetimeCell = Backgrid.DatetimeCell = Cell.extend({ - - /** @property */ - className: "datetime-cell", - - /** - @property {boolean} [includeDate=true] - */ - includeDate: DatetimeFormatter.prototype.defaults.includeDate, - - /** - @property {boolean} [includeTime=true] - */ - includeTime: DatetimeFormatter.prototype.defaults.includeTime, - - /** - @property {boolean} [includeMilli=false] - */ - includeMilli: DatetimeFormatter.prototype.defaults.includeMilli, - - /** @property {Backgrid.CellFormatter} [formatter=Backgrid.DatetimeFormatter] */ - formatter: DatetimeFormatter, - - /** - Initializes this cell and the datetime formatter. - - @param {Object} options - @param {Backbone.Model} options.model - @param {Backgrid.Column} options.column - */ - initialize: function (options) { - DatetimeCell.__super__.initialize.apply(this, arguments); - var formatter = this.formatter; - formatter.includeDate = this.includeDate; - formatter.includeTime = this.includeTime; - formatter.includeMilli = this.includeMilli; - - var placeholder = this.includeDate ? "YYYY-MM-DD" : ""; - placeholder += (this.includeDate && this.includeTime) ? "T" : ""; - placeholder += this.includeTime ? "HH:mm:ss" : ""; - placeholder += (this.includeTime && this.includeMilli) ? ".SSS" : ""; - - this.editor = this.editor.extend({ - attributes: _.extend({}, this.editor.prototype.attributes, this.editor.attributes, { - placeholder: placeholder - }) - }); - } - -}); - -/** - DateCell is a Backgrid.DatetimeCell without the time part. - - @class Backgrid.DateCell - @extends Backgrid.DatetimeCell -*/ -var DateCell = Backgrid.DateCell = DatetimeCell.extend({ - - /** @property */ - className: "date-cell", - - /** @property */ - includeTime: false - -}); - -/** - TimeCell is a Backgrid.DatetimeCell without the date part. - - @class Backgrid.TimeCell - @extends Backgrid.DatetimeCell -*/ -var TimeCell = Backgrid.TimeCell = DatetimeCell.extend({ - - /** @property */ - className: "time-cell", - - /** @property */ - includeDate: false - -}); - -/** - BooleanCellEditor renders a checkbox as its editor. - - @class Backgrid.BooleanCellEditor - @extends Backgrid.CellEditor -*/ -var BooleanCellEditor = Backgrid.BooleanCellEditor = CellEditor.extend({ - - /** @property */ - tagName: "input", - - /** @property */ - attributes: { - tabIndex: -1, - type: "checkbox" - }, - - /** @property */ - events: { - "mousedown": function () { - this.mouseDown = true; - }, - "blur": "enterOrExitEditMode", - "mouseup": function () { - this.mouseDown = false; - }, - "change": "saveOrCancel", - "keydown": "saveOrCancel" - }, - - /** - Renders a checkbox and check it if the model value of this column is true, - uncheck otherwise. - */ - render: function () { - var model = this.model; - var val = this.formatter.fromRaw(model.get(this.column.get("name")), model); - this.$el.prop("checked", val); - return this; - }, - - /** - Event handler. Hack to deal with the case where `blur` is fired before - `change` and `click` on a checkbox. - */ - enterOrExitEditMode: function (e) { - if (!this.mouseDown) { - var model = this.model; - model.trigger("backgrid:edited", model, this.column, new Command(e)); - } - }, - - /** - Event handler. Save the value into the model if the event is `change` or - one of the keyboard navigation key presses. Exit edit mode without saving - if `escape` was pressed. - */ - saveOrCancel: function (e) { - var model = this.model; - var column = this.column; - var formatter = this.formatter; - var command = new Command(e); - // skip ahead to `change` when space is pressed - if (command.passThru() && e.type != "change") return true; - if (command.cancel()) { - e.stopPropagation(); - model.trigger("backgrid:edited", model, column, command); - } - - var $el = this.$el; - if (command.save() || command.moveLeft() || command.moveRight() || command.moveUp() || - command.moveDown()) { - e.preventDefault(); - e.stopPropagation(); - var val = formatter.toRaw($el.prop("checked"), model); - model.set(column.get("name"), val); - model.trigger("backgrid:edited", model, column, command); - } - else if (e.type == "change") { - var val = formatter.toRaw($el.prop("checked"), model); - model.set(column.get("name"), val); - $el.focus(); - } - } - -}); - -/** - BooleanCell renders a checkbox both during display mode and edit mode. The - checkbox is checked if the model value is true, unchecked otherwise. - - @class Backgrid.BooleanCell - @extends Backgrid.Cell -*/ -var BooleanCell = Backgrid.BooleanCell = Cell.extend({ - - /** @property */ - className: "boolean-cell", - - /** @property */ - editor: BooleanCellEditor, - - /** @property */ - events: { - "click": "enterEditMode" - }, - - /** - Renders a checkbox and check it if the model value of this column is true, - uncheck otherwise. - */ - render: function () { - this.$el.empty(); - var model = this.model, column = this.column; - var editable = Backgrid.callByNeed(column.editable(), column, model); - this.$el.append($("<input>", { - tabIndex: -1, - type: "checkbox", - checked: this.formatter.fromRaw(model.get(column.get("name")), model), - disabled: !editable - })); - this.delegateEvents(); - return this; - } - -}); - -/** - SelectCellEditor renders an HTML `<select>` fragment as the editor. - - @class Backgrid.SelectCellEditor - @extends Backgrid.CellEditor -*/ -var SelectCellEditor = Backgrid.SelectCellEditor = CellEditor.extend({ - - /** @property */ - tagName: "select", - - /** @property */ - events: { - "change": "save", - "blur": "close", - "keydown": "close" - }, - - /** @property {function(Object, ?Object=): string} template */ - template: _.template('<option value="<%- value %>" <%= selected ? \'selected="selected"\' : "" %>><%- text %></option>', null, {variable: null}), - - setOptionValues: function (optionValues) { - this.optionValues = optionValues; - this.optionValues = _.result(this, "optionValues"); - }, - - setMultiple: function (multiple) { - this.multiple = multiple; - this.$el.prop("multiple", multiple); - }, - - _renderOptions: function (nvps, selectedValues) { - var options = ''; - for (var i = 0; i < nvps.length; i++) { - options = options + this.template({ - text: nvps[i][0], - value: nvps[i][1], - selected: selectedValues.indexOf(nvps[i][1]) > -1 - }); - } - return options; - }, - - /** - Renders the options if `optionValues` is a list of name-value pairs. The - options are contained inside option groups if `optionValues` is a list of - object hashes. The name is rendered at the option text and the value is the - option value. If `optionValues` is a function, it is called without a - parameter. - */ - render: function () { - this.$el.empty(); - - var optionValues = _.result(this, "optionValues"); - var model = this.model; - var selectedValues = this.formatter.fromRaw(model.get(this.column.get("name")), model); - - if (!_.isArray(optionValues)) throw new TypeError("optionValues must be an array"); - - var optionValue = null; - var optionText = null; - var optionValue = null; - var optgroupName = null; - var optgroup = null; - - for (var i = 0; i < optionValues.length; i++) { - var optionValue = optionValues[i]; - - if (_.isArray(optionValue)) { - optionText = optionValue[0]; - optionValue = optionValue[1]; - - this.$el.append(this.template({ - text: optionText, - value: optionValue, - selected: selectedValues.indexOf(optionValue) > -1 - })); - } - else if (_.isObject(optionValue)) { - optgroupName = optionValue.name; - optgroup = $("<optgroup></optgroup>", { label: optgroupName }); - optgroup.append(this._renderOptions(optionValue.values, selectedValues)); - this.$el.append(optgroup); - } - else { - throw new TypeError("optionValues elements must be a name-value pair or an object hash of { name: 'optgroup label', value: [option name-value pairs] }"); - } - } - - this.delegateEvents(); - - return this; - }, - - /** - Saves the value of the selected option to the model attribute. Triggers a - `backgrid:edited` Backbone event from the model. - */ - save: function (e) { - var model = this.model; - var column = this.column; - model.set(column.get("name"), this.formatter.toRaw(this.$el.val(), model)); - model.trigger("backgrid:edited", model, column, new Command(e)); - }, - - /** - Triggers a `backgrid:edited` event from the model so the body can close - this editor. - */ - close: function (e) { - var model = this.model; - var column = this.column; - var command = new Command(e); - if (command.cancel()) { - e.stopPropagation(); - model.trigger("backgrid:edited", model, column, new Command(e)); - } - else if (command.save() || command.moveLeft() || command.moveRight() || - command.moveUp() || command.moveDown() || e.type == "blur") { - e.preventDefault(); - e.stopPropagation(); - if (e.type == "blur" && this.$el.find("option").length === 1) { - model.set(column.get("name"), this.formatter.toRaw(this.$el.val(), model)); - } - model.trigger("backgrid:edited", model, column, new Command(e)); - } - } - -}); - -/** - SelectCell is also a different kind of cell in that upon going into edit mode - the cell renders a list of options to pick from, as opposed to an input box. - - SelectCell cannot be referenced by its string name when used in a column - definition because it requires an `optionValues` class attribute to be - defined. `optionValues` can either be a list of name-value pairs, to be - rendered as options, or a list of object hashes which consist of a key *name* - which is the option group name, and a key *values* which is a list of - name-value pairs to be rendered as options under that option group. - - In addition, `optionValues` can also be a parameter-less function that - returns one of the above. If the options are static, it is recommended the - returned values to be memoized. `_.memoize()` is a good function to help with - that. - - During display mode, the default formatter will normalize the raw model value - to an array of values whether the raw model value is a scalar or an - array. Each value is compared with the `optionValues` values using - Ecmascript's implicit type conversion rules. When exiting edit mode, no type - conversion is performed when saving into the model. This behavior is not - always desirable when the value type is anything other than string. To - control type conversion on the client-side, you should subclass SelectCell to - provide a custom formatter or provide the formatter to your column - definition. - - See: - [$.fn.val()](http://api.jquery.com/val/) - - @class Backgrid.SelectCell - @extends Backgrid.Cell -*/ -var SelectCell = Backgrid.SelectCell = Cell.extend({ - - /** @property */ - className: "select-cell", - - /** @property */ - editor: SelectCellEditor, - - /** @property */ - multiple: false, - - /** @property */ - formatter: SelectFormatter, - - /** - @property {Array.<Array>|Array.<{name: string, values: Array.<Array>}>} optionValues - */ - optionValues: undefined, - - /** @property */ - delimiter: ', ', - - /** - Initializer. - - @param {Object} options - @param {Backbone.Model} options.model - @param {Backgrid.Column} options.column - - @throws {TypeError} If `optionsValues` is undefined. - */ - initialize: function (options) { - SelectCell.__super__.initialize.apply(this, arguments); - this.listenTo(this.model, "backgrid:edit", function (model, column, cell, editor) { - if (column.get("name") == this.column.get("name")) { - editor.setOptionValues(this.optionValues); - editor.setMultiple(this.multiple); - } - }); - }, - - /** - Renders the label using the raw value as key to look up from `optionValues`. - - @throws {TypeError} If `optionValues` is malformed. - */ - render: function () { - this.$el.empty(); - - var optionValues = _.result(this, "optionValues"); - var model = this.model; - var rawData = this.formatter.fromRaw(model.get(this.column.get("name")), model); - - var selectedText = []; - - try { - if (!_.isArray(optionValues) || _.isEmpty(optionValues)) throw new TypeError; - - for (var k = 0; k < rawData.length; k++) { - var rawDatum = rawData[k]; - - for (var i = 0; i < optionValues.length; i++) { - var optionValue = optionValues[i]; - - if (_.isArray(optionValue)) { - var optionText = optionValue[0]; - var optionValue = optionValue[1]; - - if (optionValue == rawDatum) selectedText.push(optionText); - } - else if (_.isObject(optionValue)) { - var optionGroupValues = optionValue.values; - - for (var j = 0; j < optionGroupValues.length; j++) { - var optionGroupValue = optionGroupValues[j]; - if (optionGroupValue[1] == rawDatum) { - selectedText.push(optionGroupValue[0]); - } - } - } - else { - throw new TypeError; - } - } - } - - this.$el.append(selectedText.join(this.delimiter)); - } - catch (ex) { - if (ex instanceof TypeError) { - throw new TypeError("'optionValues' must be of type {Array.<Array>|Array.<{name: string, values: Array.<Array>}>}"); - } - throw ex; - } - - this.delegateEvents(); - - return this; - } - -}); - -/* - backgrid - http://github.com/wyuenho/backgrid - - Copyright (c) 2013 Jimmy Yuen Ho Wong and contributors - Licensed under the MIT license. -*/ - -/** - A Column is a placeholder for column metadata. - - You usually don't need to create an instance of this class yourself as a - collection of column instances will be created for you from a list of column - attributes in the Backgrid.js view class constructors. - - @class Backgrid.Column - @extends Backbone.Model -*/ -var Column = Backgrid.Column = Backbone.Model.extend({ - - /** - @cfg {Object} defaults Column defaults. To override any of these default - values, you can either change the prototype directly to override - Column.defaults globally or extend Column and supply the custom class to - Backgrid.Grid: - - // Override Column defaults globally - Column.prototype.defaults.sortable = false; - - // Override Column defaults locally - var MyColumn = Column.extend({ - defaults: _.defaults({ - editable: false - }, Column.prototype.defaults) - }); - - var grid = new Backgrid.Grid(columns: new Columns([{...}, {...}], { - model: MyColumn - })); - - @cfg {string} [defaults.name] The default name of the model attribute. - - @cfg {string} [defaults.label] The default label to show in the header. - - @cfg {string|Backgrid.Cell} [defaults.cell] The default cell type. If this - is a string, the capitalized form will be used to look up a cell class in - Backbone, i.e.: string => StringCell. If a Cell subclass is supplied, it is - initialized with a hash of parameters. If a Cell instance is supplied, it - is used directly. - - @cfg {string|Backgrid.HeaderCell} [defaults.headerCell] The default header - cell type. - - @cfg {boolean|string} [defaults.sortable=true] Whether this column is - sortable. If the value is a string, a method will the same name will be - looked up from the column instance to determine whether the column should - be sortable. The method's signature must be `function (Backgrid.Column, - Backbone.Model): boolean`. - - @cfg {boolean|string} [defaults.editable=true] Whether this column is - editable. If the value is a string, a method will the same name will be - looked up from the column instance to determine whether the column should - be editable. The method's signature must be `function (Backgrid.Column, - Backbone.Model): boolean`. - - @cfg {boolean|string} [defaults.renderable=true] Whether this column is - renderable. If the value is a string, a method will the same name will be - looked up from the column instance to determine whether the column should - be renderable. The method's signature must be `function (Backrid.Column, - Backbone.Model): boolean`. - - @cfg {Backgrid.CellFormatter | Object | string} [defaults.formatter] The - formatter to use to convert between raw model values and user input. - - @cfg {"toggle"|"cycle"} [defaults.sortType="cycle"] Whether sorting will - toggle between ascending and descending order, or cycle between insertion - order, ascending and descending order. - - @cfg {(function(Backbone.Model, string): *) | string} [defaults.sortValue] - The function to use to extract a value from the model for comparison during - sorting. If this value is a string, a method with the same name will be - looked up from the column instance. - - @cfg {"ascending"|"descending"|null} [defaults.direction=null] The initial - sorting direction for this column. The default is ordered by - Backbone.Model.cid, which usually means the collection is ordered by - insertion order. - */ - defaults: { - name: undefined, - label: undefined, - sortable: true, - editable: true, - renderable: true, - formatter: undefined, - sortType: "cycle", - sortValue: undefined, - direction: null, - cell: undefined, - headerCell: undefined - }, - - /** - Initializes this Column instance. - - @param {Object} attrs - - @param {string} attrs.name The model attribute this column is responsible - for. - - @param {string|Backgrid.Cell} attrs.cell The cell type to use to render - this column. - - @param {string} [attrs.label] - - @param {string|Backgrid.HeaderCell} [attrs.headerCell] - - @param {boolean|string} [attrs.sortable=true] - - @param {boolean|string} [attrs.editable=true] - - @param {boolean|string} [attrs.renderable=true] - - @param {Backgrid.CellFormatter | Object | string} [attrs.formatter] - - @param {"toggle"|"cycle"} [attrs.sortType="cycle"] - - @param {(function(Backbone.Model, string): *) | string} [attrs.sortValue] - - @throws {TypeError} If attrs.cell or attrs.options are not supplied. - - @throws {ReferenceError} If formatter is a string but a formatter class of - said name cannot be found in the Backgrid module. - - See: - - - Backgrid.Column.defaults - - Backgrid.Cell - - Backgrid.CellFormatter - */ - initialize: function (attrs) { - if (!this.has("label")) { - this.set({ label: this.get("name") }, { silent: true }); - } - - var headerCell = Backgrid.resolveNameToClass(this.get("headerCell"), "HeaderCell"); - - var cell = Backgrid.resolveNameToClass(this.get("cell"), "Cell"); - - this.set({cell: cell, headerCell: headerCell}, { silent: true }); - }, - - /** - Returns an appropriate value extraction function from a model for sorting. - - If the column model contains an attribute `sortValue`, if it is a string, a - method from the column instance identifified by the `sortValue` string is - returned. If it is a function, it it returned as is. If `sortValue` isn't - found from the column model's attributes, a default value extraction - function is returned which will compare according to the natural order of - the value's type. - - @return {function(Backbone.Model, string): *} - */ - sortValue: function () { - var sortValue = this.get("sortValue"); - if (_.isString(sortValue)) return this[sortValue]; - else if (_.isFunction(sortValue)) return sortValue; - - return function (model, colName) { - return model.get(colName); - }; - } - - /** - @member Backgrid.Column - @protected - @method sortable - @return {function(Backgrid.Column, Backbone.Model): boolean | boolean} - */ - - /** - @member Backgrid.Column - @protected - @method editable - @return {function(Backgrid.Column, Backbone.Model): boolean | boolean} - */ - - /** - @member Backgrid.Column - @protected - @method renderable - @return {function(Backgrid.Column, Backbone.Model): boolean | boolean} - */ -}); - -_.each(["sortable", "renderable", "editable"], function (key) { - Column.prototype[key] = function () { - var value = this.get(key); - if (_.isString(value)) return this[value]; - return !!value; - }; -}); - -/** - A Backbone collection of Column instances. - - @class Backgrid.Columns - @extends Backbone.Collection - */ -var Columns = Backgrid.Columns = Backbone.Collection.extend({ - - /** - @property {Backgrid.Column} model - */ - model: Column -}); - -/* - backgrid - http://github.com/wyuenho/backgrid - - Copyright (c) 2013 Jimmy Yuen Ho Wong and contributors - Licensed under the MIT license. -*/ - -/** - Row is a simple container view that takes a model instance and a list of - column metadata describing how each of the model's attribute is to be - rendered, and apply the appropriate cell to each attribute. - - @class Backgrid.Row - @extends Backbone.View -*/ -var Row = Backgrid.Row = Backbone.View.extend({ - - /** @property */ - tagName: "tr", - - /** - Initializes a row view instance. - - @param {Object} options - @param {Backbone.Collection.<Backgrid.Column>|Array.<Backgrid.Column>|Array.<Object>} options.columns Column metadata. - @param {Backbone.Model} options.model The model instance to render. - - @throws {TypeError} If options.columns or options.model is undefined. - */ - initialize: function (options) { - - var columns = this.columns = options.columns; - if (!(columns instanceof Backbone.Collection)) { - columns = this.columns = new Columns(columns); - } - - var cells = this.cells = []; - for (var i = 0; i < columns.length; i++) { - cells.push(this.makeCell(columns.at(i), options)); - } - - this.listenTo(columns, "add", function (column, columns) { - var i = columns.indexOf(column); - var cell = this.makeCell(column, options); - cells.splice(i, 0, cell); - - var $el = this.$el; - if (i === 0) { - $el.prepend(cell.render().$el); - } - else if (i === columns.length - 1) { - $el.append(cell.render().$el); - } - else { - $el.children().eq(i).before(cell.render().$el); - } - }); - - this.listenTo(columns, "remove", function (column, columns, opts) { - cells[opts.index].remove(); - cells.splice(opts.index, 1); - }); - }, - - /** - Factory method for making a cell. Used by #initialize internally. Override - this to provide an appropriate cell instance for a custom Row subclass. - - @protected - - @param {Backgrid.Column} column - @param {Object} options The options passed to #initialize. - - @return {Backgrid.Cell} - */ - makeCell: function (column) { - return new (column.get("cell"))({ - column: column, - model: this.model - }); - }, - - /** - Renders a row of cells for this row's model. - */ - render: function () { - this.$el.empty(); - - var fragment = document.createDocumentFragment(); - for (var i = 0; i < this.cells.length; i++) { - fragment.appendChild(this.cells[i].render().el); - } - - this.el.appendChild(fragment); - - this.delegateEvents(); - - return this; - }, - - /** - Clean up this row and its cells. - - @chainable - */ - remove: function () { - for (var i = 0; i < this.cells.length; i++) { - var cell = this.cells[i]; - cell.remove.apply(cell, arguments); - } - return Backbone.View.prototype.remove.apply(this, arguments); - } - -}); - -/** - EmptyRow is a simple container view that takes a list of column and render a - row with a single column. - - @class Backgrid.EmptyRow - @extends Backbone.View -*/ -var EmptyRow = Backgrid.EmptyRow = Backbone.View.extend({ - - /** @property */ - tagName: "tr", - - /** @property */ - emptyText: null, - - /** - Initializer. - - @param {Object} options - @param {string} options.emptyText - @param {Backbone.Collection.<Backgrid.Column>|Array.<Backgrid.Column>|Array.<Object>} options.columns Column metadata. - */ - initialize: function (options) { - this.emptyText = options.emptyText; - this.columns = options.columns; - }, - - /** - Renders an empty row. - */ - render: function () { - this.$el.empty(); - - var td = document.createElement("td"); - td.setAttribute("colspan", this.columns.length); - td.textContent = this.emptyText; - - this.el.setAttribute("class", "empty"); - this.el.appendChild(td); - - return this; - } -}); - -/* - backgrid - http://github.com/wyuenho/backgrid - - Copyright (c) 2013 Jimmy Yuen Ho Wong and contributors - Licensed under the MIT license. -*/ - -/** - HeaderCell is a special cell class that renders a column header cell. If the - column is sortable, a sorter is also rendered and will trigger a table - refresh after sorting. - - @class Backgrid.HeaderCell - @extends Backbone.View - */ -var HeaderCell = Backgrid.HeaderCell = Backbone.View.extend({ - - /** @property */ - tagName: "th", - - /** @property */ - events: { - "click a": "onClick" - }, - - /** - Initializer. - - @param {Object} options - @param {Backgrid.Column|Object} options.column - - @throws {TypeError} If options.column or options.collection is undefined. - */ - initialize: function (options) { - this.column = options.column; - if (!(this.column instanceof Column)) { - this.column = new Column(this.column); - } - - this.listenTo(this.collection, "backgrid:sort", this._resetCellDirection); - - var column = this.column, $el = this.$el; - - this.listenTo(column, "change:editable change:sortable change:renderable", - function (column) { - var changed = column.changedAttributes(); - for (var key in changed) { - if (changed.hasOwnProperty(key)) { - $el.toggleClass(key, changed[key]); - } - } - }); - - this.listenTo(column, "change:name change:label", this.render); - - if (column.get("editable")) $el.addClass("editable"); - if (column.get("sortable")) $el.addClass("sortable"); - if (column.get("renderable")) $el.addClass("renderable"); - }, - - /** - Gets or sets the direction of this cell. If called directly without - parameters, returns the current direction of this cell, otherwise sets - it. If a `null` is given, sets this cell back to the default order. - - @param {null|"ascending"|"descending"} dir - @return {null|string} The current direction or the changed direction. - */ - direction: function (dir) { - if (arguments.length) { - var direction = this.column.get('direction'); - if (direction) this.$el.removeClass(direction); - if (dir) this.$el.addClass(dir); - this.column.set('direction', dir) - } - - return this.column.get('direction'); - }, - - /** - Event handler for the Backbone `backgrid:sort` event. Resets this cell's - direction to default if sorting is being done on another column. - - @private - */ - _resetCellDirection: function (columnToSort, direction) { - if (columnToSort !== this.column) this.direction(null); - else this.direction(direction); - }, - - /** - Event handler for the `click` event on the cell's anchor. If the column is - sortable, clicking on the anchor will cycle through 3 sorting orderings - - `ascending`, `descending`, and default. - */ - onClick: function (e) { - e.preventDefault(); - - var collection = this.collection, event = "backgrid:sort"; - - function cycleSort(header, col) { - if (header.direction() === "ascending") collection.trigger(event, col, "descending"); - else if (header.direction() === "descending") collection.trigger(event, col, null); - else collection.trigger(event, col, "ascending"); - } - - function toggleSort(header, col) { - if (header.direction() === "ascending") collection.trigger(event, col, "descending"); - else collection.trigger(event, col, "ascending"); - } - - var column = this.column; - var sortable = Backgrid.callByNeed(column.sortable(), column, this.collection); - if (sortable) { - var sortType = column.get("sortType"); - if (sortType === "toggle") toggleSort(this, column); - else cycleSort(this, column); - } - }, - - /** - Renders a header cell with a sorter, a label, and a class name for this - column. - */ - render: function () { - this.$el.empty(); - var column = this.column; - var $label = $("<a>").text(column.get("label")); - var sortable = Backgrid.callByNeed(column.sortable(), column, this.collection); - if (sortable) $label.append("<b class='sort-caret'></b>"); - this.$el.append($label); - this.$el.addClass(column.get("name")); - this.delegateEvents(); - this.direction(column.get("direction")); - return this; -} - -}); - -/** - HeaderRow is a controller for a row of header cells. - - @class Backgrid.HeaderRow - @extends Backgrid.Row - */ -var HeaderRow = Backgrid.HeaderRow = Backgrid.Row.extend({ - - requiredOptions: ["columns", "collection"], - - /** - Initializer. - - @param {Object} options - @param {Backbone.Collection.<Backgrid.Column>|Array.<Backgrid.Column>|Array.<Object>} options.columns - @param {Backgrid.HeaderCell} [options.headerCell] Customized default - HeaderCell for all the columns. Supply a HeaderCell class or instance to a - the `headerCell` key in a column definition for column-specific header - rendering. - - @throws {TypeError} If options.columns or options.collection is undefined. - */ - initialize: function () { - Backgrid.Row.prototype.initialize.apply(this, arguments); - }, - - makeCell: function (column, options) { - var headerCell = column.get("headerCell") || options.headerCell || HeaderCell; - headerCell = new headerCell({ - column: column, - collection: this.collection - }); - return headerCell; - } - -}); - -/** - Header is a special structural view class that renders a table head with a - single row of header cells. - - @class Backgrid.Header - @extends Backbone.View - */ -var Header = Backgrid.Header = Backbone.View.extend({ - - /** @property */ - tagName: "thead", - - /** - Initializer. Initializes this table head view to contain a single header - row view. - - @param {Object} options - @param {Backbone.Collection.<Backgrid.Column>|Array.<Backgrid.Column>|Array.<Object>} options.columns Column metadata. - @param {Backbone.Model} options.model The model instance to render. - - @throws {TypeError} If options.columns or options.model is undefined. - */ - initialize: function (options) { - this.columns = options.columns; - if (!(this.columns instanceof Backbone.Collection)) { - this.columns = new Columns(this.columns); - } - - this.row = new Backgrid.HeaderRow({ - columns: this.columns, - collection: this.collection - }); - }, - - /** - Renders this table head with a single row of header cells. - */ - render: function () { - this.$el.append(this.row.render().$el); - this.delegateEvents(); - return this; - }, - - /** - Clean up this header and its row. - - @chainable - */ - remove: function () { - this.row.remove.apply(this.row, arguments); - return Backbone.View.prototype.remove.apply(this, arguments); - } - -}); - -/* - backgrid - http://github.com/wyuenho/backgrid - - Copyright (c) 2013 Jimmy Yuen Ho Wong and contributors - Licensed under the MIT license. -*/ - -/** - Body is the table body which contains the rows inside a table. Body is - responsible for refreshing the rows after sorting, insertion and removal. - - @class Backgrid.Body - @extends Backbone.View -*/ -var Body = Backgrid.Body = Backbone.View.extend({ - - /** @property */ - tagName: "tbody", - - /** - Initializer. - - @param {Object} options - @param {Backbone.Collection} options.collection - @param {Backbone.Collection.<Backgrid.Column>|Array.<Backgrid.Column>|Array.<Object>} options.columns - Column metadata. - @param {Backgrid.Row} [options.row=Backgrid.Row] The Row class to use. - @param {string} [options.emptyText] The text to display in the empty row. - - @throws {TypeError} If options.columns or options.collection is undefined. - - See Backgrid.Row. - */ - initialize: function (options) { - - this.columns = options.columns; - if (!(this.columns instanceof Backbone.Collection)) { - this.columns = new Columns(this.columns); - } - - this.row = options.row || Row; - this.rows = this.collection.map(function (model) { - var row = new this.row({ - columns: this.columns, - model: model - }); - - return row; - }, this); - - this.emptyText = options.emptyText; - this._unshiftEmptyRowMayBe(); - - var collection = this.collection; - this.listenTo(collection, "add", this.insertRow); - this.listenTo(collection, "remove", this.removeRow); - this.listenTo(collection, "sort", this.refresh); - this.listenTo(collection, "reset", this.refresh); - this.listenTo(collection, "backgrid:sort", this.sort); - this.listenTo(collection, "backgrid:edited", this.moveToNextCell); - }, - - _unshiftEmptyRowMayBe: function () { - if (this.rows.length === 0 && this.emptyText != null) { - this.rows.unshift(new EmptyRow({ - emptyText: this.emptyText, - columns: this.columns - })); - } - }, - - /** - This method can be called either directly or as a callback to a - [Backbone.Collecton#add](http://backbonejs.org/#Collection-add) event. - - When called directly, it accepts a model or an array of models and an - option hash just like - [Backbone.Collection#add](http://backbonejs.org/#Collection-add) and - delegates to it. Once the model is added, a new row is inserted into the - body and automatically rendered. - - When called as a callback of an `add` event, splices a new row into the - body and renders it. - - @param {Backbone.Model} model The model to render as a row. - @param {Backbone.Collection} collection When called directly, this - parameter is actually the options to - [Backbone.Collection#add](http://backbonejs.org/#Collection-add). - @param {Object} options When called directly, this must be null. - - See: - - - [Backbone.Collection#add](http://backbonejs.org/#Collection-add) - */ - insertRow: function (model, collection, options) { - - if (this.rows[0] instanceof EmptyRow) this.rows.pop().remove(); - - // insertRow() is called directly - if (!(collection instanceof Backbone.Collection) && !options) { - this.collection.add(model, (options = collection)); - return; - } - - options = _.extend({render: true}, options || {}); - - var row = new this.row({ - columns: this.columns, - model: model - }); - - var index = collection.indexOf(model); - this.rows.splice(index, 0, row); - - var $el = this.$el; - var $children = $el.children(); - var $rowEl = row.render().$el; - - if (options.render) { - if (index >= $children.length) { - $el.append($rowEl); - } - else { - $children.eq(index).before($rowEl); - } - } - - return this; - }, - - /** - The method can be called either directly or as a callback to a - [Backbone.Collection#remove](http://backbonejs.org/#Collection-remove) - event. - - When called directly, it accepts a model or an array of models and an - option hash just like - [Backbone.Collection#remove](http://backbonejs.org/#Collection-remove) and - delegates to it. Once the model is removed, a corresponding row is removed - from the body. - - When called as a callback of a `remove` event, splices into the rows and - removes the row responsible for rendering the model. - - @param {Backbone.Model} model The model to remove from the body. - @param {Backbone.Collection} collection When called directly, this - parameter is actually the options to - [Backbone.Collection#remove](http://backbonejs.org/#Collection-remove). - @param {Object} options When called directly, this must be null. - - See: - - - [Backbone.Collection#remove](http://backbonejs.org/#Collection-remove) - */ - removeRow: function (model, collection, options) { - - // removeRow() is called directly - if (!options) { - this.collection.remove(model, (options = collection)); - this._unshiftEmptyRowMayBe(); - return; - } - - if (_.isUndefined(options.render) || options.render) { - this.rows[options.index].remove(); - } - - this.rows.splice(options.index, 1); - this._unshiftEmptyRowMayBe(); - - return this; - }, - - /** - Reinitialize all the rows inside the body and re-render them. Triggers a - Backbone `backgrid:refresh` event from the collection along with the body - instance as its sole parameter when done. - */ - refresh: function () { - for (var i = 0; i < this.rows.length; i++) { - this.rows[i].remove(); - } - - this.rows = this.collection.map(function (model) { - var row = new this.row({ - columns: this.columns, - model: model - }); - - return row; - }, this); - this._unshiftEmptyRowMayBe(); - - this.render(); - - this.collection.trigger("backgrid:refresh", this); - - return this; - }, - - /** - Renders all the rows inside this body. If the collection is empty and - `options.emptyText` is defined and not null in the constructor, an empty - row is rendered, otherwise no row is rendered. - */ - render: function () { - this.$el.empty(); - - var fragment = document.createDocumentFragment(); - for (var i = 0; i < this.rows.length; i++) { - var row = this.rows[i]; - fragment.appendChild(row.render().el); - } - - this.el.appendChild(fragment); - - this.delegateEvents(); - - return this; - }, - - /** - Clean up this body and it's rows. - - @chainable - */ - remove: function () { - for (var i = 0; i < this.rows.length; i++) { - var row = this.rows[i]; - row.remove.apply(row, arguments); - } - return Backbone.View.prototype.remove.apply(this, arguments); - }, - - /** - If the underlying collection is a Backbone.PageableCollection in - server-mode or infinite-mode, a page of models is fetched after sorting is - done on the server. - - If the underlying collection is a Backbone.PageableCollection in - client-mode, or any - [Backbone.Collection](http://backbonejs.org/#Collection) instance, sorting - is done on the client side. If the collection is an instance of a - Backbone.PageableCollection, sorting will be done globally on all the pages - and the current page will then be returned. - - Triggers a Backbone `backgrid:sort` event from the collection when done - with the column, direction, comparator and a reference to the collection. - - @param {Backgrid.Column} column - @param {null|"ascending"|"descending"} direction - - See [Backbone.Collection#comparator](http://backbonejs.org/#Collection-comparator) - */ - sort: function (column, direction) { - - if (_.isString(column)) column = this.columns.findWhere({name: column}); - - var collection = this.collection; - - var order; - if (direction === "ascending") order = -1; - else if (direction === "descending") order = 1; - else order = null; - - var comparator = this.makeComparator(column.get("name"), order, - order ? - column.sortValue() : - function (model) { - return model.cid; - }); - - if (Backbone.PageableCollection && - collection instanceof Backbone.PageableCollection) { - - collection.setSorting(order && column.get("name"), order, - {sortValue: column.sortValue()}); - - if (collection.mode == "client") { - if (collection.fullCollection.comparator == null) { - collection.fullCollection.comparator = comparator; - } - collection.fullCollection.sort(); - } - else collection.fetch({reset: true}); - } - else { - collection.comparator = comparator; - collection.sort(); - } - - return this; - }, - - makeComparator: function (attr, order, func) { - - return function (left, right) { - // extract the values from the models - var l = func(left, attr), r = func(right, attr), t; - - // if descending order, swap left and right - if (order === 1) t = l, l = r, r = t; - - // compare as usual - if (l === r) return 0; - else if (l < r) return -1; - return 1; - }; - }, - - /** - Moves focus to the next renderable and editable cell and return the - currently editing cell to display mode. - - @param {Backbone.Model} model The originating model - @param {Backgrid.Column} column The originating model column - @param {Backgrid.Command} command The Command object constructed from a DOM - Event - */ - moveToNextCell: function (model, column, command) { - var i = this.collection.indexOf(model); - var j = this.columns.indexOf(column); - var cell, renderable, editable; - - this.rows[i].cells[j].exitEditMode(); - - if (command.moveUp() || command.moveDown() || command.moveLeft() || - command.moveRight() || command.save()) { - var l = this.columns.length; - var maxOffset = l * this.collection.length; - - if (command.moveUp() || command.moveDown()) { - var row = this.rows[i + (command.moveUp() ? -1 : 1)]; - if (row) { - cell = row.cells[j]; - if (Backgrid.callByNeed(cell.column.editable(), cell.column, model)) { - cell.enterEditMode(); - } - } - } - else if (command.moveLeft() || command.moveRight()) { - var right = command.moveRight(); - for (var offset = i * l + j + (right ? 1 : -1); - offset >= 0 && offset < maxOffset; - right ? offset++ : offset--) { - var m = ~~(offset / l); - var n = offset - m * l; - cell = this.rows[m].cells[n]; - renderable = Backgrid.callByNeed(cell.column.renderable(), cell.column, cell.model); - editable = Backgrid.callByNeed(cell.column.editable(), cell.column, model); - if (renderable && editable) { - cell.enterEditMode(); - break; - } - } - } - } - - return this; - } -}); - -/* - backgrid - http://github.com/wyuenho/backgrid - - Copyright (c) 2013 Jimmy Yuen Ho Wong and contributors - Licensed under the MIT license. -*/ - -/** - A Footer is a generic class that only defines a default tag `tfoot` and - number of required parameters in the initializer. - - @abstract - @class Backgrid.Footer - @extends Backbone.View - */ -var Footer = Backgrid.Footer = Backbone.View.extend({ - - /** @property */ - tagName: "tfoot", - - /** - Initializer. - - @param {Object} options - @param {Backbone.Collection.<Backgrid.Column>|Array.<Backgrid.Column>|Array.<Object>} options.columns - Column metadata. - @param {Backbone.Collection} options.collection - - @throws {TypeError} If options.columns or options.collection is undefined. - */ - initialize: function (options) { - this.columns = options.columns; - if (!(this.columns instanceof Backbone.Collection)) { - this.columns = new Backgrid.Columns(this.columns); - } - } - -}); - -/* - backgrid - http://github.com/wyuenho/backgrid - - Copyright (c) 2013 Jimmy Yuen Ho Wong and contributors - Licensed under the MIT license. -*/ - -/** - Grid represents a data grid that has a header, body and an optional footer. - - By default, a Grid treats each model in a collection as a row, and each - attribute in a model as a column. To render a grid you must provide a list of - column metadata and a collection to the Grid constructor. Just like any - Backbone.View class, the grid is rendered as a DOM node fragment when you - call render(). - - var grid = Backgrid.Grid({ - columns: [{ name: "id", label: "ID", type: "string" }, - // ... - ], - collections: books - }); - - $("#table-container").append(grid.render().el); - - Optionally, if you want to customize the rendering of the grid's header and - footer, you may choose to extend Backgrid.Header and Backgrid.Footer, and - then supply that class or an instance of that class to the Grid constructor. - See the documentation for Header and Footer for further details. - - var grid = Backgrid.Grid({ - columns: [{ name: "id", label: "ID", type: "string" }], - collections: books, - header: Backgrid.Header.extend({ - //... - }), - footer: Backgrid.Paginator - }); - - Finally, if you want to override how the rows are rendered in the table body, - you can supply a Body subclass as the `body` attribute that uses a different - Row class. - - @class Backgrid.Grid - @extends Backbone.View - - See: - - - Backgrid.Column - - Backgrid.Header - - Backgrid.Body - - Backgrid.Row - - Backgrid.Footer -*/ -var Grid = Backgrid.Grid = Backbone.View.extend({ - - /** @property */ - tagName: "table", - - /** @property */ - className: "backgrid", - - /** @property */ - header: Header, - - /** @property */ - body: Body, - - /** @property */ - footer: null, - - /** - Initializes a Grid instance. - - @param {Object} options - @param {Backbone.Collection.<Backgrid.Columns>|Array.<Backgrid.Column>|Array.<Object>} options.columns Column metadata. - @param {Backbone.Collection} options.collection The collection of tabular model data to display. - @param {Backgrid.Header} [options.header=Backgrid.Header] An optional Header class to override the default. - @param {Backgrid.Body} [options.body=Backgrid.Body] An optional Body class to override the default. - @param {Backgrid.Row} [options.row=Backgrid.Row] An optional Row class to override the default. - @param {Backgrid.Footer} [options.footer=Backgrid.Footer] An optional Footer class. - */ - initialize: function (options) { - // Convert the list of column objects here first so the subviews don't have - // to. - if (!(options.columns instanceof Backbone.Collection)) { - options.columns = new Columns(options.columns); - } - this.columns = options.columns; - - var filteredOptions = _.omit(options, ["el", "id", "attributes", - "className", "tagName", "events"]); - - // must construct body first so it listens to backgrid:sort first - this.body = options.body || this.body; - this.body = new this.body(filteredOptions); - - this.header = options.header || this.header; - if (this.header) { - this.header = new this.header(filteredOptions); - } - - this.footer = options.footer || this.footer; - if (this.footer) { - this.footer = new this.footer(filteredOptions); - } - - this.listenTo(this.columns, "reset", function () { - if (this.header) { - this.header = new (this.header.remove().constructor)(filteredOptions); - } - this.body = new (this.body.remove().constructor)(filteredOptions); - if (this.footer) { - this.footer = new (this.footer.remove().constructor)(filteredOptions); - } - this.render(); - }); - }, - - /** - Delegates to Backgrid.Body#insertRow. - */ - insertRow: function (model, collection, options) { - this.body.insertRow(model, collection, options); - return this; - }, - - /** - Delegates to Backgrid.Body#removeRow. - */ - removeRow: function (model, collection, options) { - this.body.removeRow(model, collection, options); - return this; - }, - - /** - Delegates to Backgrid.Columns#add for adding a column. Subviews can listen - to the `add` event from their internal `columns` if rerendering needs to - happen. - - @param {Object} [options] Options for `Backgrid.Columns#add`. - @param {boolean} [options.render=true] Whether to render the column - immediately after insertion. - */ - insertColumn: function (column, options) { - options = options || {render: true}; - this.columns.add(column, options); - return this; - }, - - /** - Delegates to Backgrid.Columns#remove for removing a column. Subviews can - listen to the `remove` event from the internal `columns` if rerendering - needs to happen. - - @param {Object} [options] Options for `Backgrid.Columns#remove`. - */ - removeColumn: function (column, options) { - this.columns.remove(column, options); - return this; - }, - - /** - Delegates to Backgrid.Body#sort. - */ - sort: function () { - this.body.sort(arguments); - return this; - }, - - /** - Renders the grid's header, then footer, then finally the body. Triggers a - Backbone `backgrid:rendered` event along with a reference to the grid when - the it has successfully been rendered. - */ - render: function () { - this.$el.empty(); - - if (this.header) { - this.$el.append(this.header.render().$el); - } - - if (this.footer) { - this.$el.append(this.footer.render().$el); - } - - this.$el.append(this.body.render().$el); - - this.delegateEvents(); - - this.trigger("backgrid:rendered", this); - - return this; - }, - - /** - Clean up this grid and its subviews. - - @chainable - */ - remove: function () { - this.header && this.header.remove.apply(this.header, arguments); - this.body.remove.apply(this.body, arguments); - this.footer && this.footer.remove.apply(this.footer, arguments); - return Backbone.View.prototype.remove.apply(this, arguments); - } - -}); -return Backgrid; -})); \ No newline at end of file diff --git a/src/UI/JsLibraries/backbone.backgrid.paginator.js b/src/UI/JsLibraries/backbone.backgrid.paginator.js deleted file mode 100644 index 03255f84d..000000000 --- a/src/UI/JsLibraries/backbone.backgrid.paginator.js +++ /dev/null @@ -1,352 +0,0 @@ -/* - backgrid-paginator - http://github.com/wyuenho/backgrid - - Copyright (c) 2013 Jimmy Yuen Ho Wong and contributors - Licensed under the MIT @license. -*/ -(function (factory) { - - // CommonJS - if (typeof exports == "object") { - module.exports = factory(require("underscore"), - require("backbone"), - require("backgrid"), - require("backbone-pageable")); - } - // Browser - else if (typeof _ !== "undefined" && - typeof Backbone !== "undefined" && - typeof Backgrid !== "undefined") { - factory(_, Backbone, Backgrid); - } - -}(function (_, Backbone, Backgrid) { - - "use strict"; - - /** - PageHandle is a class that renders the actual page handles and reacts to - click events for pagination. - - This class acts in two modes - control or discrete page handle modes. If - one of the `is*` flags is `true`, an instance of this class is under - control page handle mode. Setting a `pageIndex` to an instance of this - class under control mode has no effect and the correct page index will - always be inferred from the `is*` flag. Only one of the `is*` flags should - be set to `true` at a time. For example, an instance of this class cannot - simultaneously be a rewind control and a fast forward control. A `label` - and a `title` template or a string are required to be passed to the - constuctor under this mode. If a `title` template is provided, it __MUST__ - accept a parameter `label`. When the `label` is provided to the `title` - template function, its result will be used to render the generated anchor's - title attribute. - - If all of the `is*` flags is set to `false`, which is the default, an - instance of this class will be in discrete page handle mode. An instance - under this mode requires the `pageIndex` to be passed from the constructor - as an option and it __MUST__ be a 0-based index of the list of page numbers - to render. The constuctor will normalize the base to the same base the - underlying PageableCollection collection instance uses. A `label` is not - required under this mode, which will default to the equivalent 1-based page - index calculated from `pageIndex` and the underlying PageableCollection - instance. A provided `label` will still be honored however. The `title` - parameter is also not required under this mode, in which case the default - `title` template will be used. You are encouraged to provide your own - `title` template however if you wish to localize the title strings. - - If this page handle represents the current page, an `active` class will be - placed on the root list element. - - if this page handle is at the border of the list of pages, a `disabled` - class will be placed on the root list element. - - Only page handles that are neither `active` nor `disabled` will respond to - click events and triggers pagination. - - @class Backgrid.Extension.PageHandle - */ - var PageHandle = Backgrid.Extension.PageHandle = Backbone.View.extend({ - - /** @property */ - tagName: "li", - - /** @property */ - events: { - "click a": "changePage" - }, - - /** - @property {string|function(Object.<string, string>): string} title - The title to use for the `title` attribute of the generated page handle - anchor elements. It can be a string or an Underscore template function - that takes a mandatory `label` parameter. - */ - title: _.template('Page <%- label %>', null, {variable: null}), - - /** - @property {boolean} isRewind Whether this handle represents a rewind - control - */ - isRewind: false, - - /** - @property {boolean} isBack Whether this handle represents a back - control - */ - isBack: false, - - /** - @property {boolean} isForward Whether this handle represents a forward - control - */ - isForward: false, - - /** - @property {boolean} isFastForward Whether this handle represents a fast - forward control - */ - isFastForward: false, - - /** - Initializer. - - @param {Object} options - @param {Backbone.Collection} options.collection - @param {number} pageIndex 0-based index of the page number this handle - handles. This parameter will be normalized to the base the underlying - PageableCollection uses. - @param {string} [options.label] If provided it is used to render the - anchor text, otherwise the normalized pageIndex will be used - instead. Required if any of the `is*` flags is set to `true`. - @param {string} [options.title] - @param {boolean} [options.isRewind=false] - @param {boolean} [options.isBack=false] - @param {boolean} [options.isForward=false] - @param {boolean} [options.isFastForward=false] - */ - initialize: function (options) { - Backbone.View.prototype.initialize.apply(this, arguments); - - var collection = this.collection; - var state = collection.state; - var currentPage = state.currentPage; - var firstPage = state.firstPage; - var lastPage = state.lastPage; - - _.extend(this, _.pick(options, - ["isRewind", "isBack", "isForward", "isFastForward"])); - - var pageIndex; - if (this.isRewind) pageIndex = firstPage; - else if (this.isBack) pageIndex = Math.max(firstPage, currentPage - 1); - else if (this.isForward) pageIndex = Math.min(lastPage, currentPage + 1); - else if (this.isFastForward) pageIndex = lastPage; - else { - pageIndex = +options.pageIndex; - pageIndex = (firstPage ? pageIndex + 1 : pageIndex); - } - this.pageIndex = pageIndex; - - if (((this.isRewind || this.isBack) && currentPage == firstPage) || - ((this.isForward || this.isFastForward) && currentPage == lastPage)) { - this.$el.addClass("disabled"); - } - else if (!(this.isRewind || - this.isBack || - this.isForward || - this.isFastForward) && - currentPage == pageIndex) { - this.$el.addClass("active"); - } - - this.label = (options.label || (firstPage ? pageIndex : pageIndex + 1)) + ''; - var title = options.title || this.title; - this.title = _.isFunction(title) ? title({label: this.label}) : title; - }, - - /** - Renders a clickable anchor element under a list item. - */ - render: function () { - this.$el.empty(); - var anchor = document.createElement("a"); - anchor.href = '#'; - if (this.title) anchor.title = this.title; - anchor.innerHTML = this.label; - this.el.appendChild(anchor); - this.delegateEvents(); - return this; - }, - - /** - jQuery click event handler. Goes to the page this PageHandle instance - represents. No-op if this page handle is currently active or disabled. - */ - changePage: function (e) { - e.preventDefault(); - var $el = this.$el; - if (!$el.hasClass("active") && !$el.hasClass("disabled")) { - this.collection.getPage(this.pageIndex); - } - return this; - } - - }); - - /** - Paginator is a Backgrid extension that renders a series of configurable - pagination handles. This extension is best used for splitting a large data - set across multiple pages. If the number of pages is larger then a - threshold, which is set to 10 by default, the page handles are rendered - within a sliding window, plus the rewind, back, forward and fast forward - control handles. The individual control handles can be turned off. - - @class Backgrid.Extension.Paginator - */ - Backgrid.Extension.Paginator = Backbone.View.extend({ - - /** @property */ - className: "backgrid-paginator", - - /** @property */ - windowSize: 10, - - /** - @property {Object.<string, Object.<string, string>>} controls You can - disable specific control handles by omitting certain keys. - */ - controls: { - rewind: { - label: "《", - title: "First" - }, - back: { - label: "〈", - title: "Previous" - }, - forward: { - label: "〉", - title: "Next" - }, - fastForward: { - label: "》", - title: "Last" - } - }, - - /** - @property {Backgrid.Extension.PageHandle} pageHandle. The PageHandle - class to use for rendering individual handles - */ - pageHandle: PageHandle, - - /** @property */ - goBackFirstOnSort: true, - - /** - Initializer. - - @param {Object} options - @param {Backbone.Collection} options.collection - @param {boolean} [options.controls] - @param {boolean} [options.pageHandle=Backgrid.Extension.PageHandle] - @param {boolean} [options.goBackFirstOnSort=true] - */ - initialize: function (options) { - this.controls = options.controls || this.controls; - this.pageHandle = options.pageHandle || this.pageHandle; - - var collection = this.collection; - this.listenTo(collection, "add", this.render); - this.listenTo(collection, "remove", this.render); - this.listenTo(collection, "reset", this.render); - if ((options.goBackFirstOnSort || this.goBackFirstOnSort) && - collection.fullCollection) { - this.listenTo(collection.fullCollection, "sort", function () { - collection.getFirstPage(); - }); - } - }, - - _calculateWindow: function () { - var collection = this.collection; - var state = collection.state; - - // convert all indices to 0-based here - var firstPage = state.firstPage; - var lastPage = +state.lastPage; - lastPage = Math.max(0, firstPage ? lastPage - 1 : lastPage); - var currentPage = Math.max(state.currentPage, state.firstPage); - currentPage = firstPage ? currentPage - 1 : currentPage; - var windowStart = Math.floor(currentPage / this.windowSize) * this.windowSize; - var windowEnd = Math.min(lastPage + 1, windowStart + this.windowSize); - return [windowStart, windowEnd]; - }, - - /** - Creates a list of page handle objects for rendering. - - @return {Array.<Object>} an array of page handle objects hashes - */ - makeHandles: function () { - - var handles = []; - var collection = this.collection; - - var window = this._calculateWindow(); - var winStart = window[0], winEnd = window[1]; - - for (var i = winStart; i < winEnd; i++) { - handles.push(new this.pageHandle({ - collection: collection, - pageIndex: i - })); - } - - var controls = this.controls; - _.each(["back", "rewind", "forward", "fastForward"], function (key) { - var value = controls[key]; - if (value) { - var handleCtorOpts = { - collection: collection, - title: value.title, - label: value.label - }; - handleCtorOpts["is" + key.slice(0, 1).toUpperCase() + key.slice(1)] = true; - var handle = new this.pageHandle(handleCtorOpts); - if (key == "rewind" || key == "back") handles.unshift(handle); - else handles.push(handle); - } - }, this); - - return handles; - }, - - /** - Render the paginator handles inside an unordered list. - */ - render: function () { - this.$el.empty(); - - if (this.handles) { - for (var i = 0, l = this.handles.length; i < l; i++) { - this.handles[i].remove(); - } - } - - var handles = this.handles = this.makeHandles(); - - var ul = document.createElement("ul"); - for (var i = 0; i < handles.length; i++) { - ul.appendChild(handles[i].render().el); - } - - this.el.appendChild(ul); - - return this; - } - - }); - -})); diff --git a/src/UI/JsLibraries/backbone.backgrid.selectall.js b/src/UI/JsLibraries/backbone.backgrid.selectall.js deleted file mode 100644 index 7d36c73ae..000000000 --- a/src/UI/JsLibraries/backbone.backgrid.selectall.js +++ /dev/null @@ -1,243 +0,0 @@ -/* - backgrid-select-all - http://github.com/wyuenho/backgrid - - Copyright (c) 2013 Jimmy Yuen Ho Wong and contributors - Licensed under the MIT @license. -*/ -(function (factory) { - - // CommonJS - if (typeof exports == "object") { - module.exports = factory(require("backbone"), require("backgrid")); - } - // Browser - else if (typeof Backbone !== "undefined" && typeof Backgrid !== "undefined") { - factory(Backbone, Backgrid); - } - -}(function (Backbone, Backgrid) { - - "use strict"; - - var $ = Backbone.$; - - /** - Renders a checkbox for row selection. - - @class Backgrid.Extension.SelectRowCell - @extends Backbone.View - */ - var SelectRowCell = Backgrid.Extension.SelectRowCell = Backbone.View.extend({ - - /** @property */ - className: "select-row-cell", - - /** @property */ - tagName: "td", - - /** @property */ - events: { - "keydown :checkbox": "onKeydown", - "change :checkbox": "onChange", - "click :checkbox": "enterEditMode" - }, - - /** - Initializer. If the underlying model triggers a `select` event, this cell - will change its checked value according to the event's `selected` value. - - @param {Object} options - @param {Backgrid.Column} options.column - @param {Backbone.Model} options.model - */ - initialize: function (options) { - - this.column = options.column; - if (!(this.column instanceof Backgrid.Column)) { - this.column = new Backgrid.Column(this.column); - } - - this.listenTo(this.model, "backgrid:select", function (model, selected) { - this.$el.find(":checkbox").prop("checked", selected).change(); - }); - - var column = this.column, $el = this.$el; - this.listenTo(column, "change:renderable", function (column, renderable) { - $el.toggleClass("renderable", renderable); - }); - - if (column.get("renderable")) $el.addClass("renderable"); - }, - - /** - Focuses the checkbox. - */ - enterEditMode: function () { - this.$el.find(":checkbox").focus(); - }, - - /** - Unfocuses the checkbox. - */ - exitEditMode: function () { - this.$el.find(":checkbox").blur(); - }, - - /** - Process keyboard navigation. - */ - onKeydown: function (e) { - var command = new Backgrid.Command(e); - if (command.passThru()) return true; // skip ahead to `change` - if (command.cancel()) { - e.stopPropagation(); - this.$el.find(":checkbox").blur(); - } - else if (command.save() || command.moveLeft() || command.moveRight() || - command.moveUp() || command.moveDown()) { - e.preventDefault(); - e.stopPropagation(); - this.model.trigger("backgrid:edited", this.model, this.column, command); - } - }, - - /** - When the checkbox's value changes, this method will trigger a Backbone - `backgrid:selected` event with a reference of the model and the - checkbox's `checked` value. - */ - onChange: function (e) { - var checked = $(e.target).prop('checked'); - this.$el.parent().toggleClass('selected', checked); - this.model.trigger("backgrid:selected", this.model, checked); - }, - - /** - Renders a checkbox in a table cell. - */ - render: function () { - this.$el.empty().append('<input tabindex="-1" type="checkbox" />'); - this.delegateEvents(); - return this; - } - - }); - - /** - Renders a checkbox to select all rows on the current page. - - @class Backgrid.Extension.SelectAllHeaderCell - @extends Backgrid.Extension.SelectRowCell - */ - var SelectAllHeaderCell = Backgrid.Extension.SelectAllHeaderCell = SelectRowCell.extend({ - - /** @property */ - className: "select-all-header-cell", - - /** @property */ - tagName: "th", - - /** - Initializer. When this cell's checkbox is checked, a Backbone - `backgrid:select` event will be triggered for each model for the current - page in the underlying collection. If a `SelectRowCell` instance exists - for the rows representing the models, they will check themselves. If any - of the SelectRowCell instances trigger a Backbone `backgrid:selected` - event with a `false` value, this cell will uncheck its checkbox. In the - event of a Backbone `backgrid:refresh` event, which is triggered when the - body refreshes its rows, which can happen under a number of conditions - such as paging or the columns were reset, this cell will still remember - the previously selected models and trigger a Backbone `backgrid:select` - event on them such that the SelectRowCells can recheck themselves upon - refreshing. - - @param {Object} options - @param {Backgrid.Column} options.column - @param {Backbone.Collection} options.collection - */ - initialize: function (options) { - - this.column = options.column; - if (!(this.column instanceof Backgrid.Column)) { - this.column = new Backgrid.Column(this.column); - } - - var collection = this.collection; - var selectedModels = this.selectedModels = {}; - this.listenTo(collection, "backgrid:selected", function (model, selected) { - if (selected) selectedModels[model.id || model.cid] = model; - else { - delete selectedModels[model.id || model.cid]; - this.$el.find(":checkbox").prop("checked", false); - } - }); - - this.listenTo(collection, "remove", function (model) { - delete selectedModels[model.id || model.cid]; - }); - - this.listenTo(collection, "backgrid:refresh", function () { - this.$el.find(":checkbox").prop("checked", false); - for (var i = 0; i < collection.length; i++) { - var model = collection.at(i); - if (selectedModels[model.id || model.cid]) { - model.trigger('backgrid:select', model, true); - } - } - }); - - var column = this.column, $el = this.$el; - this.listenTo(column, "change:renderable", function (column, renderable) { - $el.toggleClass("renderable", renderable); - }); - - if (column.get("renderable")) $el.addClass("renderable"); - }, - - /** - Progagates the checked value of this checkbox to all the models of the - underlying collection by triggering a Backbone `backgrid:select` event on - the models themselves, passing each model and the current `checked` value - of the checkbox in each event. - */ - onChange: function (e) { - var checked = $(e.target).prop("checked"); - - var collection = this.collection; - collection.each(function (model) { - model.trigger("backgrid:select", model, checked); - }); - } - - }); - - /** - Convenient method to retrieve a list of selected models. This method only - exists when the `SelectAll` extension has been included. - - @member Backgrid.Grid - @return {Array.<Backbone.Model>} - */ - Backgrid.Grid.prototype.getSelectedModels = function () { - var selectAllHeaderCell; - var headerCells = this.header.row.cells; - for (var i = 0, l = headerCells.length; i < l; i++) { - var headerCell = headerCells[i]; - if (headerCell instanceof SelectAllHeaderCell) { - selectAllHeaderCell = headerCell; - break; - } - } - - var result = []; - if (selectAllHeaderCell) { - for (var modelId in selectAllHeaderCell.selectedModels) { - result.push(this.collection.get(modelId)); - } - } - - return result; - }; - -})); diff --git a/src/UI/JsLibraries/backbone.collectionview.js b/src/UI/JsLibraries/backbone.collectionview.js deleted file mode 100644 index 29ec982ff..000000000 --- a/src/UI/JsLibraries/backbone.collectionview.js +++ /dev/null @@ -1,1072 +0,0 @@ -/*! -* Backbone.CollectionView, v0.8.1 -* Copyright (c)2013 Rotunda Software, LLC. -* Distributed under MIT license -* http://github.com/rotundasoftware/backbone-collection-view -*/ - - -(function() { - var mDefaultModelViewConstructor = Backbone.View; - - var kDefaultReferenceBy = "model"; - - var kAllowedOptions = [ - "collection", "modelView", "modelViewOptions", "itemTemplate", "emptyListCaption", - "selectable", "clickToSelect", "selectableModelsFilter", "visibleModelsFilter", - "selectMultiple", "clickToToggle", "processKeyEvents", "sortable", "sortableOptions", "sortableModelsFilter", "itemTemplateFunction", "detachedRendering" - ]; - - var kOptionsRequiringRerendering = [ "collection", "modelView", "modelViewOptions", "itemTemplate", "selectableModelsFilter", "sortableModelsFilter", "visibleModelsFilter", "itemTemplateFunction", "detachedRendering", "sortableOptions" ]; - - var kStylesForEmptyListCaption = { - "background" : "transparent", - "border" : "none", - "box-shadow" : "none" - }; - - Backbone.CollectionView = Backbone.View.extend( { - - tagName : "ul", - - events : { - "mousedown li, td" : "_listItem_onMousedown", - "dblclick li, td" : "_listItem_onDoubleClick", - "click" : "_listBackground_onClick", - "click ul.collection-list, table.collection-list" : "_listBackground_onClick", - "keydown" : "_onKeydown" - }, - - // only used if Backbone.Courier is available - spawnMessages : { - "focus" : "focus" - }, - - //only used if Backbone.Courier is available - passMessages : { "*" : "." }, - - initialize : function( options ) { - var _this = this; - - this._hasBeenRendered = false; - - // default options - options = _.extend( {}, { - collection : null, - modelView : this.modelView || null, - modelViewOptions : {}, - itemTemplate : null, - itemTemplateFunction : null, - selectable : true, - clickToSelect : true, - selectableModelsFilter : null, - visibleModelsFilter : null, - sortableModelsFilter : null, - selectMultiple : false, - clickToToggle : false, - processKeyEvents : true, - sortable : false, - sortableOptions : null, - detachedRendering : false, - emptyListCaption : null - }, options ); - - // add each of the white-listed options to the CollectionView object itself - _.each( kAllowedOptions, function( option ) { - _this[ option ] = options[option]; - } ); - - if( ! this.collection ) this.collection = new Backbone.Collection(); - - if( this._isBackboneCourierAvailable() ) { - Backbone.Courier.add( this ); - } - - this.$el.data( "view", this ); // needed for connected sortable lists - this.$el.addClass( "collection-list" ); - if( this.processKeyEvents ) - this.$el.attr( "tabindex", 0 ); // so we get keyboard events - - this.selectedItems = []; - - this._updateItemTemplate(); - - if( this.collection ) - this._registerCollectionEvents(); - - this.viewManager = new ChildViewContainer(); - - //this.listenTo( this.collection, "change", function() { this.render(); this.spawn( "change" ); } ); // don't want changes to models bubbling up and triggering the list's render() function - - // note we do NOT call render here anymore, because if we inherit from this class we will likely call this - // function using __super__ before the rest of the initialization logic for the decedent class. however, we may - // override the render() function in that decedent class as well, and that will certainly expect all the initialization - // to be done already. so we have to make sure to not jump the gun and start rending at this point. - // this.render(); - }, - - setOption : function( name, value ) { - - var _this = this; - - if( name === "collection" ) { - this._setCollection( value ); - } - else { - if( _.contains( kAllowedOptions, name ) ) { - - switch( name ) { - case "selectMultiple" : - this[ name ] = value; - if( !value && this.selectedItems.length > 1 ) - this.setSelectedModel( _.first( this.selectedItems ), { by : "cid" } ); - break; - case "selectable" : - if( !value && this.selectedItems.length > 0 ) - this.setSelectedModels( [] ); - this[ name ] = value; - break; - case "selectableModelsFilter" : - this[ name ] = value; - if( value && _.isFunction( value ) ) - this._validateSelection(); - break; - case "itemTemplate" : - this[ name ] = value; - this._updateItemTemplate(); - break; - case "processKeyEvents" : - this[ name ] = value; - if( value ) this.$el.attr( "tabindex", 0 ); // so we get keyboard events - break; - case "modelView" : - this[ name ] = value; - //need to remove all old view instances - this.viewManager.each( function( view ) { - _this.viewManager.remove( view ); - // destroy the View itself - view.remove(); - } ); - break; - default : - this[ name ] = value; - } - - if( _.contains( kOptionsRequiringRerendering, name ) ) this.render(); - } - else throw name + " is not an allowed option"; - } - }, - - getSelectedModel : function( options ) { - return _.first( this.getSelectedModels( options ) ); - }, - - getSelectedModels : function ( options ) { - var _this = this; - - options = _.extend( {}, { - by : kDefaultReferenceBy - }, options ); - - var referenceBy = options.by; - var items = []; - - switch( referenceBy ) { - case "id" : - _.each( this.selectedItems, function ( item ) { - items.push( _this.collection.get( item ).id ); - } ); - break; - case "cid" : - items = items.concat( this.selectedItems ); - break; - case "offset" : - var curLineNumber = 0; - - var itemElements; - if( this._isRenderedAsTable() ) - itemElements = this.$el.find( "> tbody > [data-model-cid]:not(.not-visible)" ); - else if( this._isRenderedAsList() ) - itemElements = this.$el.find( "> [data-model-cid]:not(.not-visible)" ); - - itemElements.each( function() { - var thisItemEl = $( this ); - if( thisItemEl.is( ".selected" ) ) - items.push( curLineNumber ); - curLineNumber++; - } ); - break; - case "model" : - _.each( this.selectedItems, function ( item ) { - items.push( _this.collection.get( item ) ); - } ); - break; - case "view" : - _.each( this.selectedItems, function ( item ) { - items.push( _this.viewManager.findByModel( _this.collection.get( item ) ) ); - } ); - break; - } - - return items; - - }, - - setSelectedModels : function( newSelectedItems, options ) { - if( ! this.selectable ) return; // used to throw error, but there are some circumstances in which a list can be selectable at times and not at others, don't want to have to worry about catching errors - if( ! _.isArray( newSelectedItems ) ) throw "Invalid parameter value"; - - options = _.extend( {}, { - silent : false, - by : kDefaultReferenceBy - }, options ); - - var referenceBy = options.by; - var newSelectedCids = []; - - switch( referenceBy ) { - case "cid" : - newSelectedCids = newSelectedItems; - break; - case "id" : - this.collection.each( function( thisModel ) { - if( _.contains( newSelectedItems, thisModel.id ) ) newSelectedCids.push( thisModel.cid ); - } ); - break; - case "model" : - newSelectedCids = _.pluck( newSelectedItems, "cid" ); - break; - case "view" : - _.each( newSelectedItems, function( item ) { - newSelectedCids.push( item.model.cid ); - } ); - break; - case "offset" : - var curLineNumber = 0; - var selectedItems = []; - - var itemElements; - if( this._isRenderedAsTable() ) - itemElements = this.$el.find( "> tbody > [data-model-cid]:not(.not-visible)" ); - else if( this._isRenderedAsList() ) - itemElements = this.$el.find( "> [data-model-cid]:not(.not-visible)" ); - - itemElements.each( function() { - var thisItemEl = $( this ); - if( _.contains( newSelectedItems, curLineNumber ) ) - newSelectedCids.push( thisItemEl.attr( "data-model-cid" ) ); - curLineNumber++; - } ); - break; - } - - var oldSelectedModels = this.getSelectedModels(); - var oldSelectedCids = _.clone( this.selectedItems ); - - this.selectedItems = this._convertStringsToInts( newSelectedCids ); - this._validateSelection(); - - var newSelectedModels = this.getSelectedModels(); - - if( ! this._containSameElements( oldSelectedCids, this.selectedItems ) ) - { - this._addSelectedClassToSelectedItems( oldSelectedCids ); - - if( ! options.silent ) - { - this.trigger( "selectionChanged", newSelectedModels, oldSelectedModels ); - if( this._isBackboneCourierAvailable() ) { - this.spawn( "selectionChanged", { - selectedModels : newSelectedModels, - oldSelectedModels : oldSelectedModels - } ); - } - } - - this.updateDependentControls(); - } - }, - - setSelectedModel : function( newSelectedItem, options ) { - if( ! newSelectedItem && newSelectedItem !== 0 ) - this.setSelectedModels( [], options ); - else - this.setSelectedModels( [ newSelectedItem ], options ); - }, - - render : function(){ - var _this = this; - - this._hasBeenRendered = true; - - if( this.selectable ) this._saveSelection(); - - var modelViewContainerEl; - - // If collection view element is a table and it has a tbody - // within it, render the model views inside of the tbody - if( this._isRenderedAsTable() ) { - var tbodyChild = this.$el.find( "> tbody" ); - if( tbodyChild.length > 0 ) - modelViewContainerEl = tbodyChild; - } - - if( _.isUndefined( modelViewContainerEl ) ) - modelViewContainerEl = this.$el; - - var oldViewManager = this.viewManager; - this.viewManager = new ChildViewContainer(); - - // detach each of our subviews that we have already created to represent models - // in the collection. We are going to re-use the ones that represent models that - // are still here, instead of creating new ones, so that we don't loose state - // information in the views. - oldViewManager.each( function( thisModelView ) { - // to boost performance, only detach those views that will be sticking around. - // we won't need the other ones later, so no need to detach them individually. - if( _this.collection.get( thisModelView.model.cid ) ) - thisModelView.$el.detach(); - else - thisModelView.remove(); - } ); - - modelViewContainerEl.empty(); - var fragmentContainer; - - if( this.detachedRendering ) - fragmentContainer = document.createDocumentFragment(); - - this.collection.each( function( thisModel ) { - var thisModelView; - - thisModelView = oldViewManager.findByModelCid( thisModel.cid ); - if( _.isUndefined( thisModelView ) ) { - // if the model view was not already created on previous render, - // then create and initialize it now. - - var modelViewOptions = this._getModelViewOptions( thisModel ); - thisModelView = this._createNewModelView( thisModel, modelViewOptions ); - - thisModelView.collectionListView = _this; - } - - var thisModelViewWrapped = this._wrapModelView( thisModelView ); - if( this.detachedRendering ) - fragmentContainer.appendChild( thisModelViewWrapped[0] ); - else - modelViewContainerEl.append( thisModelViewWrapped ); - - // we have to render the modelView after it has been put in context, as opposed to in the - // initialize function of the modelView, because some rendering might be dependent on - // the modelView's context in the DOM tree. For example, if the modelView stretch()'s itself, - // it must be in full context in the DOM tree or else the stretch will not behave as intended. - var renderResult = thisModelView.render(); - - // return false from the view's render function to hide this item - if( renderResult === false ) { - thisModelViewWrapped.hide(); - thisModelViewWrapped.addClass( "not-visible" ); - } - - if( _.isFunction( this.visibleModelsFilter ) ) { - if( ! this.visibleModelsFilter( thisModel ) ) { - if( thisModelViewWrapped.children().length === 1 ) - thisModelViewWrapped.hide(); - else thisModelView.$el.hide(); - - thisModelViewWrapped.addClass( "not-visible" ); - } - } - - this.viewManager.add( thisModelView ); - }, this ); - - if( this.detachedRendering ) - modelViewContainerEl.append( fragmentContainer ); - - if( this.sortable ) - { - var sortableOptions = _.extend( { - axis: "y", - distance: 10, - forcePlaceholderSize : true, - start : _.bind( this._sortStart, this ), - change : _.bind( this._sortChange, this ), - stop : _.bind( this._sortStop, this ), - receive : _.bind( this._receive, this ), - over : _.bind( this._over, this ) - }, _.result( this, "sortableOptions" ) ); - - if( _this._isRenderedAsTable() ) { - sortableOptions.items = "> tbody > tr:not(.not-sortable)"; - } - else if( _this._isRenderedAsList() ) { - sortableOptions.items = "> li:not(.not-sortable)"; - } - - this.$el = this.$el.sortable( sortableOptions ); - } - - if( this.emptyListCaption ) { - var visibleView = this.viewManager.find( function( view ) { - return ! view.$el.hasClass( "not-visible" ); - } ); - - if( _.isUndefined( visibleView ) ) { - var emptyListString; - - if( _.isFunction( this.emptyListCaption ) ) - emptyListString = this.emptyListCaption(); - else - emptyListString = this.emptyListCaption; - - var $emptyCaptionEl; - var $varEl = $( "<var class='empty-list-caption'>" + emptyListString + "</var>" ); - - //need to wrap the empty caption to make it fit the rendered list structure (either with an li or a tr td) - if( this._isRenderedAsList() ) - $emptyListCaptionEl = $varEl.wrapAll( "<li class='not-sortable'></li>" ).parent().css( kStylesForEmptyListCaption ); - else - $emptyListCaptionEl = $varEl.wrapAll( "<tr class='not-sortable'><td></td></tr>" ).parent().parent().css( kStylesForEmptyListCaption ); - - this.$el.append( $emptyListCaptionEl ); - - } - } - - this.trigger( "render" ); - if( this._isBackboneCourierAvailable() ) - this.spawn( "render" ); - - if( this.selectable ) { - this._restoreSelection(); - this.updateDependentControls(); - } - - if( _.isFunction( this.onAfterRender ) ) - this.onAfterRender(); - }, - - updateDependentControls : function() { - this.trigger( "updateDependentControls", this.getSelectedModels() ); - if( this._isBackboneCourierAvailable() ) { - this.spawn( "updateDependentControls", { - selectedModels : this.getSelectedModels() - } ); - } - }, - - // Override `Backbone.View.remove` to also destroy all Views in `viewManager` - remove : function() { - this.viewManager.each( function( view ) { - view.remove(); - } ); - - Backbone.View.prototype.remove.apply( this, arguments ); - }, - - _validateSelectionAndRender : function() { - this._validateSelection(); - this.render(); - }, - - _registerCollectionEvents : function() { - this.listenTo( this.collection, "add", function() { - if( this._hasBeenRendered ) this.render(); - if( this._isBackboneCourierAvailable() ) - this.spawn( "add" ); - } ); - - this.listenTo( this.collection, "remove", function() { - if( this._hasBeenRendered ) this.render(); - if( this._isBackboneCourierAvailable() ) - this.spawn( "remove" ); - } ); - - this.listenTo( this.collection, "reset", function() { - if( this._hasBeenRendered ) this.render(); - if( this._isBackboneCourierAvailable() ) - this.spawn( "reset" ); - } ); - - // It should be up to the model to rerender itself when it changes. - // this.listenTo( this.collection, "change", function( model ) { - // if( this._hasBeenRendered ) this.viewManager.findByModel( model ).render(); - // if( this._isBackboneCourierAvailable() ) - // this.spawn( "change", { model : model } ); - // } ); - - this.listenTo( this.collection, "sort", function() { - if( this._hasBeenRendered ) this.render(); - if( this._isBackboneCourierAvailable() ) - this.spawn( "sort" ); - } ); - }, - - _getClickedItemId : function( theEvent ) { - var clickedItemId = null; - - // important to use currentTarget as opposed to target, since we could be bubbling - // an event that took place within another collectionList - var clickedItemEl = $( theEvent.currentTarget ); - if( clickedItemEl.closest( ".collection-list" ).get(0) !== this.$el.get(0) ) return; - - // determine which list item was clicked. If we clicked in the blank area - // underneath all the elements, we want to know that too, since in this - // case we will want to deselect all elements. so check to see if the clicked - // DOM element is the list itself to find that out. - var clickedItem = clickedItemEl.closest( "[data-model-cid]" ); - if( clickedItem.length > 0 ) - { - clickedItemId = clickedItem.attr( "data-model-cid" ); - if( $.isNumeric( clickedItemId ) ) clickedItemId = parseInt( clickedItemId, 10 ); - } - - return clickedItemId; - }, - - _setCollection : function( newCollection ) { - if( newCollection !== this.collection ) - { - this.stopListening( this.collection ); - this.collection = newCollection; - this._registerCollectionEvents(); - } - - if( this._hasBeenRendered ) this.render(); - }, - - _updateItemTemplate : function() { - var itemTemplateHtml; - if( this.itemTemplate ) - { - if( $( this.itemTemplate ).length === 0 ) - throw "Could not find item template from selector: " + this.itemTemplate; - - itemTemplateHtml = $( this.itemTemplate ).html(); - } - else - itemTemplateHtml = this.$( ".item-template" ).html(); - - if( itemTemplateHtml ) this.itemTemplateFunction = _.template( itemTemplateHtml ); - - }, - - _validateSelection : function() { - // note can't use the collection's proxy to underscore because "cid" ais not an attribute, - // but an element of the model object itself. - var modelReferenceIds = _.pluck( this.collection.models, "cid" ); - this.selectedItems = _.intersection( modelReferenceIds, this.selectedItems ); - - if( _.isFunction( this.selectableModelsFilter ) ) - { - this.selectedItems = _.filter( this.selectedItems, function( thisItemId ) { - return this.selectableModelsFilter.call( this, this.collection.get( thisItemId ) ); - }, this ); - } - }, - - _saveSelection : function() { - // save the current selection. use restoreSelection() to restore the selection to the state it was in the last time saveSelection() was called. - if( ! this.selectable ) throw "Attempt to save selection on non-selectable list"; - this.savedSelection = { - items : this.selectedItems, - offset : this.getSelectedModel( { by : "offset" } ) - }; - }, - - _restoreSelection : function() { - if( ! this.savedSelection ) throw "Attempt to restore selection but no selection has been saved!"; - - // reset selectedItems to empty so that we "redraw" all "selected" classes - // when we set our new selection. We do this because it is likely that our - // contents have been refreshed, and we have thus lost all old "selected" classes. - this.setSelectedModels( [], { silent : true } ); - - if( this.savedSelection.items.length > 0 ) - { - // first try to restore the old selected items using their reference ids. - this.setSelectedModels( this.savedSelection.items, { by : "cid", silent : true } ); - - // all the items with the saved reference ids have been removed from the list. - // ok. try to restore the selection based on the offset that used to be selected. - // this is the expected behavior after a item is deleted from a list (i.e. select - // the line that immediately follows the deleted line). - if( this.selectedItems.length === 0 ) - this.setSelectedModel( this.savedSelection.offset, { by : "offset" } ); - - // Trigger a selection changed if the previously selected items were not all found - if (this.selectedItems.length !== this.savedSelection.items.length) - { - this.trigger( "selectionChanged", this.getSelectedModels(), [] ); - if( this._isBackboneCourierAvailable() ) { - this.spawn( "selectionChanged", { - selectedModels : this.getSelectedModels(), - oldSelectedModels : [] - } ); - } - } - } - - delete this.savedSelection; - }, - - _addSelectedClassToSelectedItems : function( oldItemsIdsWithSelectedClass ) { - if( _.isUndefined( oldItemsIdsWithSelectedClass ) ) oldItemsIdsWithSelectedClass = []; - - // oldItemsIdsWithSelectedClass is used for optimization purposes only. If this info is supplied then we - // only have to add / remove the "selected" class from those items that "selected" state has changed. - - var itemsIdsFromWhichSelectedClassNeedsToBeRemoved = oldItemsIdsWithSelectedClass; - itemsIdsFromWhichSelectedClassNeedsToBeRemoved = _.without( itemsIdsFromWhichSelectedClassNeedsToBeRemoved, this.selectedItems ); - - _.each( itemsIdsFromWhichSelectedClassNeedsToBeRemoved, function( thisItemId ) { - this.$el.find( "[data-model-cid=" + thisItemId + "]" ).removeClass( "selected" ); - }, this ); - - var itemsIdsFromWhichSelectedClassNeedsToBeAdded = this.selectedItems; - itemsIdsFromWhichSelectedClassNeedsToBeAdded = _.without( itemsIdsFromWhichSelectedClassNeedsToBeAdded, oldItemsIdsWithSelectedClass ); - - _.each( itemsIdsFromWhichSelectedClassNeedsToBeAdded, function( thisItemId ) { - this.$el.find( "[data-model-cid=" + thisItemId + "]" ).addClass( "selected" ); - }, this ); - }, - - _reorderCollectionBasedOnHTML : function() { - var _this = this; - - this.$el.children().each( function() { - var thisModelCid = $( this ).attr( "data-model-cid" ); - - if( thisModelCid ) - { - // remove the current model and then add it back (at the end of the collection). - // When we are done looping through all models, they will be in the correct order. - var thisModel = _this.collection.get( thisModelCid ); - if( thisModel ) - { - _this.collection.remove( thisModel, { silent : true } ); - _this.collection.add( thisModel, { silent : true, sort : ! _this.collection.comparator } ); - } - } - } ); - - this.collection.trigger( "reorder" ); - - if( this._isBackboneCourierAvailable() ) this.spawn( "reorder" ); - - if( this.collection.comparator ) this.collection.sort(); - - }, - - _getModelViewConstructor : function( thisModel ) { - return this.modelView || mDefaultModelViewConstructor; - }, - - _getModelViewOptions : function( thisModel ) { - return _.extend( { model : thisModel }, this.modelViewOptions ); - }, - - _createNewModelView : function( model, modelViewOptions ) { - var modelViewConstructor = this._getModelViewConstructor( model ); - if( _.isUndefined( modelViewConstructor ) ) throw "Could not find modelView constructor for model"; - - return new ( modelViewConstructor )( modelViewOptions ); - }, - - _wrapModelView : function( modelView ) { - var _this = this; - - // we use items client ids as opposed to real ids, since we may not have a representation - // of these models on the server - var wrappedModelView; - - if( this._isRenderedAsTable() ) { - // if we are rendering the collection in a table, the template $el is a tr so we just need to set the data-model-cid - wrappedModelView = modelView.$el.attr( "data-model-cid", modelView.model.cid ); - } - else if( this._isRenderedAsList() ) { - // if we are rendering the collection in a list, we need wrap each item in an <li></li> (if its not already an <li>) - // and set the data-model-cid - if( modelView.$el.prop( "tagName" ).toLowerCase() === "li" ) { - wrappedModelView = modelView.$el.attr( "data-model-cid", modelView.model.cid ); - } else { - wrappedModelView = modelView.$el.wrapAll( "<li data-model-cid='" + modelView.model.cid + "'></li>" ).parent(); - } - } - - if( _.isFunction( this.sortableModelsFilter ) ) - if( ! this.sortableModelsFilter.call( _this, modelView.model ) ) - wrappedModelView.addClass( "not-sortable" ); - - if( _.isFunction( this.selectableModelsFilter ) ) - if( ! this.selectableModelsFilter.call( _this, modelView.model ) ) - wrappedModelView.addClass( "not-selectable" ); - - return wrappedModelView; - }, - - _convertStringsToInts : function( theArray ) { - return _.map( theArray, function( thisEl ) { - if( ! _.isString( thisEl ) ) return thisEl; - var thisElAsNumber = parseInt( thisEl, 10 ); - return( thisElAsNumber == thisEl ? thisElAsNumber : thisEl ); - } ); - }, - - _containSameElements : function( arrayA, arrayB ) { - if( arrayA.length != arrayB.length ) return false; - var intersectionSize = _.intersection( arrayA, arrayB ).length; - return intersectionSize == arrayA.length; // and must also equal arrayB.length, since arrayA.length == arrayB.length - }, - - _isRenderedAsTable : function() { - return this.$el.prop('tagName').toLowerCase() === 'table'; - }, - - - _isRenderedAsList : function() { - return ! this._isRenderedAsTable(); - }, - - _charCodes : { - upArrow : 38, - downArrow : 40 - }, - - _isBackboneCourierAvailable : function() { - return !_.isUndefined( Backbone.Courier ); - }, - - _sortStart : function( event, ui ) { - var modelBeingSorted = this.collection.get( ui.item.attr( "data-model-cid" ) ); - this.trigger( "sortStart", modelBeingSorted ); - if( this._isBackboneCourierAvailable() ) - this.spawn( "sortStart", { modelBeingSorted : modelBeingSorted } ); - }, - - _sortChange : function( event, ui ) { - var modelBeingSorted = this.collection.get( ui.item.attr( "data-model-cid" ) ); - this.trigger( "sortChange", modelBeingSorted ); - if( this._isBackboneCourierAvailable() ) - this.spawn( "sortChange", { modelBeingSorted : modelBeingSorted } ); - }, - - _sortStop : function( event, ui ) { - var modelBeingSorted = this.collection.get( ui.item.attr( "data-model-cid" ) ); - var modelViewContainerEl = (this._isRenderedAsTable()) ? this.$el.find( "> tbody" ) : this.$el; - var newIndex = modelViewContainerEl.children().index( ui.item ); - - if( newIndex == -1 ) { - // the element was removed from this list. can happen if this sortable is connected - // to another sortable, and the item was dropped into the other sortable. - this.collection.remove( modelBeingSorted ); - } - - this._reorderCollectionBasedOnHTML(); - this.updateDependentControls(); - this.trigger( "sortStop", modelBeingSorted, newIndex ); - if( this._isBackboneCourierAvailable() ) - this.spawn( "sortStop", { modelBeingSorted : modelBeingSorted, newIndex : newIndex } ); - }, - - _receive : function( event, ui ) { - var senderListEl = ui.sender; - var senderCollectionListView = senderListEl.data( "view" ); - if( ! senderCollectionListView || ! senderCollectionListView.collection ) return; - - var newIndex = this.$el.children().index( ui.item ); - var modelReceived = senderCollectionListView.collection.get( ui.item.attr( "data-model-cid" ) ); - this.collection.add( modelReceived, { at : newIndex } ); - modelReceived.collection = this.collection; // otherwise will not get properly set, since modelReceived.collection might already have a value. - this.setSelectedModel( modelReceived ); - }, - - _over : function( event, ui ) { - // when an item is being dragged into the sortable, - // hide the empty list caption if it exists - this.$el.find( ".empty-list-caption" ).hide(); - }, - - _onKeydown : function( event ) { - if( ! this.processKeyEvents ) return true; - - var trap = false; - - if( this.getSelectedModels( { by : "offset" } ).length == 1 ) - { - // need to trap down and up arrows or else the browser - // will end up scrolling a autoscroll div. - - var currentOffset = this.getSelectedModel( { by : "offset" } ); - if( event.which === this._charCodes.upArrow && currentOffset !== 0 ) - { - this.setSelectedModel( currentOffset - 1, { by : "offset" } ); - trap = true; - } - else if( event.which === this._charCodes.downArrow && currentOffset !== this.collection.length - 1 ) - { - this.setSelectedModel( currentOffset + 1, { by : "offset" } ); - trap = true; - } - } - - return ! trap; - }, - - _listItem_onMousedown : function( theEvent ) { - if( ! this.selectable || ! this.clickToSelect ) return; - - var clickedItemId = this._getClickedItemId( theEvent ); - - if( clickedItemId ) - { - // Exit if an unselectable item was clicked - if( _.isFunction( this.selectableModelsFilter ) && - ! this.selectableModelsFilter.call( this, this.collection.get( clickedItemId ) ) ) - { - return; - } - - // a selectable list item was clicked - if( this.selectMultiple && theEvent.shiftKey ) - { - var firstSelectedItemIndex = -1; - - if( this.selectedItems.length > 0 ) - { - this.collection.find( function( thisItemModel ) { - firstSelectedItemIndex++; - - // exit when we find our first selected element - return _.contains( this.selectedItems, thisItemModel.cid ); - }, this ); - } - - var clickedItemIndex = -1; - this.collection.find( function( thisItemModel ) { - clickedItemIndex++; - - // exit when we find the clicked element - return thisItemModel.cid == clickedItemId; - }, this ); - - var shiftKeyRootSelectedItemIndex = firstSelectedItemIndex == -1 ? clickedItemIndex : firstSelectedItemIndex; - var minSelectedItemIndex = Math.min( clickedItemIndex, shiftKeyRootSelectedItemIndex ); - var maxSelectedItemIndex = Math.max( clickedItemIndex, shiftKeyRootSelectedItemIndex ); - - var newSelectedItems = []; - for( var thisIndex = minSelectedItemIndex; thisIndex <= maxSelectedItemIndex; thisIndex ++ ) - newSelectedItems.push( this.collection.at( thisIndex ).cid ); - this.setSelectedModels( newSelectedItems, { by : "cid" } ); - - // shift clicking will usually highlight selectable text, which we do not want. - // this is a cross browser (hopefully) snippet that deselects all text selection. - if( document.selection && document.selection.empty ) - document.selection.empty(); - else if(window.getSelection) { - var sel = window.getSelection(); - if( sel && sel.removeAllRanges ) - sel.removeAllRanges(); - } - } - else if( this.selectMultiple && ( this.clickToToggle || theEvent.metaKey ) ) - { - if( _.contains( this.selectedItems, clickedItemId ) ) - this.setSelectedModels( _.without( this.selectedItems, clickedItemId ), { by : "cid" } ); - else this.setSelectedModels( _.union( this.selectedItems, [ clickedItemId ] ), { by : "cid" } ); - } - else - this.setSelectedModels( [ clickedItemId ], { by : "cid" } ); - } - else - // the blank area of the list was clicked - this.setSelectedModels( [] ); - - }, - - _listItem_onDoubleClick : function( theEvent ) { - var clickedItemId = this._getClickedItemId( theEvent ); - - if( clickedItemId ) - { - var clickedModel = this.collection.get( clickedItemId ); - this.trigger( "doubleClick", clickedModel ); - if( this._isBackboneCourierAvailable() ) - this.spawn( "doubleClick", { clickedModel : clickedModel } ); - } - }, - - _listBackground_onClick : function( theEvent ) { - if( ! this.selectable ) return; - if( ! $( theEvent.target ).is( ".collection-list" ) ) return; - - this.setSelectedModels( [] ); - } - - }, { - setDefaultModelViewConstructor : function( theConstructor ) { - mDefaultModelViewConstructor = theConstructor; - } - }); - - - // Backbone.BabySitter - // ------------------- - // v0.0.6 - // - // Copyright (c)2013 Derick Bailey, Muted Solutions, LLC. - // Distributed under MIT license - // - // http://github.com/babysitterjs/backbone.babysitter - - // Backbone.ChildViewContainer - // --------------------------- - // - // Provide a container to store, retrieve and - // shut down child views. - - ChildViewContainer = (function(Backbone, _){ - - // Container Constructor - // --------------------- - - var Container = function(views){ - this._views = {}; - this._indexByModel = {}; - this._indexByCustom = {}; - this._updateLength(); - - _.each(views, this.add, this); - }; - - // Container Methods - // ----------------- - - _.extend(Container.prototype, { - - // Add a view to this container. Stores the view - // by `cid` and makes it searchable by the model - // cid (and model itself). Optionally specify - // a custom key to store an retrieve the view. - add: function(view, customIndex){ - var viewCid = view.cid; - - // store the view - this._views[viewCid] = view; - - // index it by model - if (view.model){ - this._indexByModel[view.model.cid] = viewCid; - } - - // index by custom - if (customIndex){ - this._indexByCustom[customIndex] = viewCid; - } - - this._updateLength(); - }, - - // Find a view by the model that was attached to - // it. Uses the model's `cid` to find it. - findByModel: function(model){ - return this.findByModelCid(model.cid); - }, - - // Find a view by the `cid` of the model that was attached to - // it. Uses the model's `cid` to find the view `cid` and - // retrieve the view using it. - findByModelCid: function(modelCid){ - var viewCid = this._indexByModel[modelCid]; - return this.findByCid(viewCid); - }, - - // Find a view by a custom indexer. - findByCustom: function(index){ - var viewCid = this._indexByCustom[index]; - return this.findByCid(viewCid); - }, - - // Find by index. This is not guaranteed to be a - // stable index. - findByIndex: function(index){ - return _.values(this._views)[index]; - }, - - // retrieve a view by it's `cid` directly - findByCid: function(cid){ - return this._views[cid]; - }, - - // Remove a view - remove: function(view){ - var viewCid = view.cid; - - // delete model index - if (view.model){ - delete this._indexByModel[view.model.cid]; - } - - // delete custom index - _.any(this._indexByCustom, function(cid, key) { - if (cid === viewCid) { - delete this._indexByCustom[key]; - return true; - } - }, this); - - // remove the view from the container - delete this._views[viewCid]; - - // update the length - this._updateLength(); - }, - - // Call a method on every view in the container, - // passing parameters to the call method one at a - // time, like `function.call`. - call: function(method){ - this.apply(method, _.tail(arguments)); - }, - - // Apply a method on every view in the container, - // passing parameters to the call method one at a - // time, like `function.apply`. - apply: function(method, args){ - _.each(this._views, function(view){ - if (_.isFunction(view[method])){ - view[method].apply(view, args || []); - } - }); - }, - - // Update the `.length` attribute on this container - _updateLength: function(){ - this.length = _.size(this._views); - } - }); - - // Borrowing this code from Backbone.Collection: - // http://backbonejs.org/docs/backbone.html#section-106 - // - // Mix in methods from Underscore, for iteration, and other - // collection related features. - var methods = ['forEach', 'each', 'map', 'find', 'detect', 'filter', - 'select', 'reject', 'every', 'all', 'some', 'any', 'include', - 'contains', 'invoke', 'toArray', 'first', 'initial', 'rest', - 'last', 'without', 'isEmpty', 'pluck']; - - _.each(methods, function(method) { - Container.prototype[method] = function() { - var views = _.values(this._views); - var args = [views].concat(_.toArray(arguments)); - return _[method].apply(_, args); - }; - }); - - // return the public API - return Container; - })(Backbone, _); -})(); \ No newline at end of file diff --git a/src/UI/JsLibraries/backbone.deep.model.js b/src/UI/JsLibraries/backbone.deep.model.js deleted file mode 100644 index 7d65d8802..000000000 --- a/src/UI/JsLibraries/backbone.deep.model.js +++ /dev/null @@ -1,437 +0,0 @@ -/*jshint expr:true eqnull:true */ -/** - * - * Backbone.DeepModel v0.10.4 - * - * Copyright (c) 2013 Charles Davison, Pow Media Ltd - * - * https://github.com/powmedia/backbone-deep-model - * Licensed under the MIT License - */ - -/** - * Underscore mixins for deep objects - * - * Based on https://gist.github.com/echong/3861963 - */ -(function() { - var arrays, basicObjects, deepClone, deepExtend, deepExtendCouple, isBasicObject, - __slice = [].slice; - - deepClone = function(obj) { - var func, isArr; - if (!_.isObject(obj) || _.isFunction(obj)) { - return obj; - } - if (obj instanceof Backbone.Collection || obj instanceof Backbone.Model) { - return obj; - } - if (_.isDate(obj)) { - return new Date(obj.getTime()); - } - if (_.isRegExp(obj)) { - return new RegExp(obj.source, obj.toString().replace(/.*\//, "")); - } - isArr = _.isArray(obj || _.isArguments(obj)); - func = function(memo, value, key) { - if (isArr) { - memo.push(deepClone(value)); - } else { - memo[key] = deepClone(value); - } - return memo; - }; - return _.reduce(obj, func, isArr ? [] : {}); - }; - - isBasicObject = function(object) { - if (object == null) return false; - return (object.prototype === {}.prototype || object.prototype === Object.prototype) && _.isObject(object) && !_.isArray(object) && !_.isFunction(object) && !_.isDate(object) && !_.isRegExp(object) && !_.isArguments(object); - }; - - basicObjects = function(object) { - return _.filter(_.keys(object), function(key) { - return isBasicObject(object[key]); - }); - }; - - arrays = function(object) { - return _.filter(_.keys(object), function(key) { - return _.isArray(object[key]); - }); - }; - - deepExtendCouple = function(destination, source, maxDepth) { - var combine, recurse, sharedArrayKey, sharedArrayKeys, sharedObjectKey, sharedObjectKeys, _i, _j, _len, _len1; - if (maxDepth == null) { - maxDepth = 20; - } - if (maxDepth <= 0) { - console.warn('_.deepExtend(): Maximum depth of recursion hit.'); - return _.extend(destination, source); - } - sharedObjectKeys = _.intersection(basicObjects(destination), basicObjects(source)); - recurse = function(key) { - return source[key] = deepExtendCouple(destination[key], source[key], maxDepth - 1); - }; - for (_i = 0, _len = sharedObjectKeys.length; _i < _len; _i++) { - sharedObjectKey = sharedObjectKeys[_i]; - recurse(sharedObjectKey); - } - sharedArrayKeys = _.intersection(arrays(destination), arrays(source)); - combine = function(key) { - return source[key] = _.union(destination[key], source[key]); - }; - for (_j = 0, _len1 = sharedArrayKeys.length; _j < _len1; _j++) { - sharedArrayKey = sharedArrayKeys[_j]; - combine(sharedArrayKey); - } - return _.extend(destination, source); - }; - - deepExtend = function() { - var finalObj, maxDepth, objects, _i; - objects = 2 <= arguments.length ? __slice.call(arguments, 0, _i = arguments.length - 1) : (_i = 0, []), maxDepth = arguments[_i++]; - if (!_.isNumber(maxDepth)) { - objects.push(maxDepth); - maxDepth = 20; - } - if (objects.length <= 1) { - return objects[0]; - } - if (maxDepth <= 0) { - return _.extend.apply(this, objects); - } - finalObj = objects.shift(); - while (objects.length > 0) { - finalObj = deepExtendCouple(finalObj, deepClone(objects.shift()), maxDepth); - } - return finalObj; - }; - - _.mixin({ - deepClone: deepClone, - isBasicObject: isBasicObject, - basicObjects: basicObjects, - arrays: arrays, - deepExtend: deepExtend - }); - -}).call(this); - -/** - * Main source - */ - -;(function(factory) { - if (typeof define === 'function' && define.amd) { - // AMD - define(['underscore', 'backbone'], factory); - } else { - // globals - factory(_, Backbone); - } -}(function(_, Backbone) { - - /** - * Takes a nested object and returns a shallow object keyed with the path names - * e.g. { "level1.level2": "value" } - * - * @param {Object} Nested object e.g. { level1: { level2: 'value' } } - * @return {Object} Shallow object with path names e.g. { 'level1.level2': 'value' } - */ - function objToPaths(obj) { - var ret = {}, - separator = DeepModel.keyPathSeparator; - - for (var key in obj) { - var val = obj[key]; - - if (val && val.constructor === Object && !_.isEmpty(val)) { - //Recursion for embedded objects - var obj2 = objToPaths(val); - - for (var key2 in obj2) { - var val2 = obj2[key2]; - - ret[key + separator + key2] = val2; - } - } else { - ret[key] = val; - } - } - - return ret; - } - - /** - * @param {Object} Object to fetch attribute from - * @param {String} Object path e.g. 'user.name' - * @return {Mixed} - */ - function getNested(obj, path, return_exists) { - var separator = DeepModel.keyPathSeparator; - - var fields = path.split(separator); - var result = obj; - return_exists || (return_exists === false); - for (var i = 0, n = fields.length; i < n; i++) { - if (return_exists && !_.has(result, fields[i])) { - return false; - } - result = result[fields[i]]; - - if (result == null && i < n - 1) { - result = {}; - } - - if (typeof result === 'undefined') { - if (return_exists) - { - return true; - } - return result; - } - } - if (return_exists) - { - return true; - } - return result; - } - - /** - * @param {Object} obj Object to fetch attribute from - * @param {String} path Object path e.g. 'user.name' - * @param {Object} [options] Options - * @param {Boolean} [options.unset] Whether to delete the value - * @param {Mixed} Value to set - */ - function setNested(obj, path, val, options) { - options = options || {}; - - var separator = DeepModel.keyPathSeparator; - - var fields = path.split(separator); - var result = obj; - for (var i = 0, n = fields.length; i < n && result !== undefined ; i++) { - var field = fields[i]; - - //If the last in the path, set the value - if (i === n - 1) { - options.unset ? delete result[field] : result[field] = val; - } else { - //Create the child object if it doesn't exist, or isn't an object - if (typeof result[field] === 'undefined' || ! _.isObject(result[field])) { - result[field] = {}; - } - - //Move onto the next part of the path - result = result[field]; - } - } - } - - function deleteNested(obj, path) { - setNested(obj, path, null, { unset: true }); - } - - var DeepModel = Backbone.Model.extend({ - - // Override constructor - // Support having nested defaults by using _.deepExtend instead of _.extend - constructor: function(attributes, options) { - var defaults; - var attrs = attributes || {}; - this.cid = _.uniqueId('c'); - this.attributes = {}; - if (options && options.collection) this.collection = options.collection; - if (options && options.parse) attrs = this.parse(attrs, options) || {}; - if (defaults = _.result(this, 'defaults')) { - //<custom code> - // Replaced the call to _.defaults with _.deepExtend. - attrs = _.deepExtend({}, defaults, attrs); - //</custom code> - } - this.set(attrs, options); - this.changed = {}; - this.initialize.apply(this, arguments); - }, - - // Return a copy of the model's `attributes` object. - toJSON: function(options) { - return _.deepClone(this.attributes); - }, - - // Override get - // Supports nested attributes via the syntax 'obj.attr' e.g. 'author.user.name' - get: function(attr) { - return getNested(this.attributes, attr); - }, - - // Override set - // Supports nested attributes via the syntax 'obj.attr' e.g. 'author.user.name' - set: function(key, val, options) { - var attr, attrs, unset, changes, silent, changing, prev, current; - if (key == null) return this; - - // Handle both `"key", value` and `{key: value}` -style arguments. - if (typeof key === 'object') { - attrs = key; - options = val || {}; - } else { - (attrs = {})[key] = val; - } - - options || (options = {}); - - // Run validation. - if (!this._validate(attrs, options)) return false; - - // Extract attributes and options. - unset = options.unset; - silent = options.silent; - changes = []; - changing = this._changing; - this._changing = true; - - if (!changing) { - this._previousAttributes = _.deepClone(this.attributes); //<custom>: Replaced _.clone with _.deepClone - this.changed = {}; - } - current = this.attributes, prev = this._previousAttributes; - - // Check for changes of `id`. - if (this.idAttribute in attrs) this.id = attrs[this.idAttribute]; - - //<custom code> - attrs = objToPaths(attrs); - //</custom code> - - // For each `set` attribute, update or delete the current value. - for (attr in attrs) { - val = attrs[attr]; - - //<custom code>: Using getNested, setNested and deleteNested - if (!_.isEqual(getNested(current, attr), val)) changes.push(attr); - if (!_.isEqual(getNested(prev, attr), val)) { - setNested(this.changed, attr, val); - } else { - deleteNested(this.changed, attr); - } - unset ? deleteNested(current, attr) : setNested(current, attr, val); - //</custom code> - } - - // Trigger all relevant attribute changes. - if (!silent) { - if (changes.length) this._pending = true; - - //<custom code> - var separator = DeepModel.keyPathSeparator; - - for (var i = 0, l = changes.length; i < l; i++) { - var key = changes[i]; - - this.trigger('change:' + key, this, getNested(current, key), options); - - var fields = key.split(separator); - - //Trigger change events for parent keys with wildcard (*) notation - for(var n = fields.length - 1; n > 0; n--) { - var parentKey = _.first(fields, n).join(separator), - wildcardKey = parentKey + separator + '*'; - - this.trigger('change:' + wildcardKey, this, getNested(current, parentKey), options); - } - //</custom code> - } - } - - if (changing) return this; - if (!silent) { - while (this._pending) { - this._pending = false; - this.trigger('change', this, options); - } - } - this._pending = false; - this._changing = false; - return this; - }, - - // Clear all attributes on the model, firing `"change"` unless you choose - // to silence it. - clear: function(options) { - var attrs = {}; - var shallowAttributes = objToPaths(this.attributes); - for (var key in shallowAttributes) attrs[key] = void 0; - return this.set(attrs, _.extend({}, options, {unset: true})); - }, - - // Determine if the model has changed since the last `"change"` event. - // If you specify an attribute name, determine if that attribute has changed. - hasChanged: function(attr) { - if (attr == null) return !_.isEmpty(this.changed); - return getNested(this.changed, attr) !== undefined; - }, - - // Return an object containing all the attributes that have changed, or - // false if there are no changed attributes. Useful for determining what - // parts of a view need to be updated and/or what attributes need to be - // persisted to the server. Unset attributes will be set to undefined. - // You can also pass an attributes object to diff against the model, - // determining if there *would be* a change. - changedAttributes: function(diff) { - //<custom code>: objToPaths - if (!diff) return this.hasChanged() ? objToPaths(this.changed) : false; - //</custom code> - - var old = this._changing ? this._previousAttributes : this.attributes; - - //<custom code> - diff = objToPaths(diff); - old = objToPaths(old); - //</custom code> - - var val, changed = false; - for (var attr in diff) { - if (_.isEqual(old[attr], (val = diff[attr]))) continue; - (changed || (changed = {}))[attr] = val; - } - return changed; - }, - - // Get the previous value of an attribute, recorded at the time the last - // `"change"` event was fired. - previous: function(attr) { - if (attr == null || !this._previousAttributes) return null; - - //<custom code> - return getNested(this._previousAttributes, attr); - //</custom code> - }, - - // Get all of the attributes of the model at the time of the previous - // `"change"` event. - previousAttributes: function() { - //<custom code> - return _.deepClone(this._previousAttributes); - //</custom code> - } - }); - - - //Config; override in your app to customise - DeepModel.keyPathSeparator = '.'; - - - //Exports - Backbone.DeepModel = DeepModel; - - //For use in NodeJS - if (typeof module != 'undefined') module.exports = DeepModel; - - return Backbone; - -})); diff --git a/src/UI/JsLibraries/backbone.js b/src/UI/JsLibraries/backbone.js deleted file mode 100644 index 70a854d31..000000000 --- a/src/UI/JsLibraries/backbone.js +++ /dev/null @@ -1,1571 +0,0 @@ -// Backbone.js 1.0.0 - -// (c) 2010-2013 Jeremy Ashkenas, DocumentCloud Inc. -// Backbone may be freely distributed under the MIT license. -// For all details and documentation: -// http://backbonejs.org - -(function(){ - - // Initial Setup - // ------------- - - // Save a reference to the global object (`window` in the browser, `exports` - // on the server). - var root = this; - - // Save the previous value of the `Backbone` variable, so that it can be - // restored later on, if `noConflict` is used. - var previousBackbone = root.Backbone; - - // Create local references to array methods we'll want to use later. - var array = []; - var push = array.push; - var slice = array.slice; - var splice = array.splice; - - // The top-level namespace. All public Backbone classes and modules will - // be attached to this. Exported for both the browser and the server. - var Backbone; - if (typeof exports !== 'undefined') { - Backbone = exports; - } else { - Backbone = root.Backbone = {}; - } - - // Current version of the library. Keep in sync with `package.json`. - Backbone.VERSION = '1.0.0'; - - // Require Underscore, if we're on the server, and it's not already present. - var _ = root._; - if (!_ && (typeof require !== 'undefined')) _ = require('underscore'); - - // For Backbone's purposes, jQuery, Zepto, Ender, or My Library (kidding) owns - // the `$` variable. - Backbone.$ = root.jQuery || root.Zepto || root.ender || root.$; - - // Runs Backbone.js in *noConflict* mode, returning the `Backbone` variable - // to its previous owner. Returns a reference to this Backbone object. - Backbone.noConflict = function() { - root.Backbone = previousBackbone; - return this; - }; - - // Turn on `emulateHTTP` to support legacy HTTP servers. Setting this option - // will fake `"PUT"` and `"DELETE"` requests via the `_method` parameter and - // set a `X-Http-Method-Override` header. - Backbone.emulateHTTP = false; - - // Turn on `emulateJSON` to support legacy servers that can't deal with direct - // `application/json` requests ... will encode the body as - // `application/x-www-form-urlencoded` instead and will send the model in a - // form param named `model`. - Backbone.emulateJSON = false; - - // Backbone.Events - // --------------- - - // A module that can be mixed in to *any object* in order to provide it with - // custom events. You may bind with `on` or remove with `off` callback - // functions to an event; `trigger`-ing an event fires all callbacks in - // succession. - // - // var object = {}; - // _.extend(object, Backbone.Events); - // object.on('expand', function(){ alert('expanded'); }); - // object.trigger('expand'); - // - var Events = Backbone.Events = { - - // Bind an event to a `callback` function. Passing `"all"` will bind - // the callback to all events fired. - on: function(name, callback, context) { - if (!eventsApi(this, 'on', name, [callback, context]) || !callback) return this; - this._events || (this._events = {}); - var events = this._events[name] || (this._events[name] = []); - events.push({callback: callback, context: context, ctx: context || this}); - return this; - }, - - // Bind an event to only be triggered a single time. After the first time - // the callback is invoked, it will be removed. - once: function(name, callback, context) { - if (!eventsApi(this, 'once', name, [callback, context]) || !callback) return this; - var self = this; - var once = _.once(function() { - self.off(name, once); - callback.apply(this, arguments); - }); - once._callback = callback; - return this.on(name, once, context); - }, - - // Remove one or many callbacks. If `context` is null, removes all - // callbacks with that function. If `callback` is null, removes all - // callbacks for the event. If `name` is null, removes all bound - // callbacks for all events. - off: function(name, callback, context) { - var retain, ev, events, names, i, l, j, k; - if (!this._events || !eventsApi(this, 'off', name, [callback, context])) return this; - if (!name && !callback && !context) { - this._events = {}; - return this; - } - - names = name ? [name] : _.keys(this._events); - for (i = 0, l = names.length; i < l; i++) { - name = names[i]; - if (events = this._events[name]) { - this._events[name] = retain = []; - if (callback || context) { - for (j = 0, k = events.length; j < k; j++) { - ev = events[j]; - if ((callback && callback !== ev.callback && callback !== ev.callback._callback) || - (context && context !== ev.context)) { - retain.push(ev); - } - } - } - if (!retain.length) delete this._events[name]; - } - } - - return this; - }, - - // Trigger one or many events, firing all bound callbacks. Callbacks are - // passed the same arguments as `trigger` is, apart from the event name - // (unless you're listening on `"all"`, which will cause your callback to - // receive the true name of the event as the first argument). - trigger: function(name) { - if (!this._events) return this; - var args = slice.call(arguments, 1); - if (!eventsApi(this, 'trigger', name, args)) return this; - var events = this._events[name]; - var allEvents = this._events.all; - if (events) triggerEvents(events, args); - if (allEvents) triggerEvents(allEvents, arguments); - return this; - }, - - // Tell this object to stop listening to either specific events ... or - // to every object it's currently listening to. - stopListening: function(obj, name, callback) { - var listeners = this._listeners; - if (!listeners) return this; - var deleteListener = !name && !callback; - if (typeof name === 'object') callback = this; - if (obj) (listeners = {})[obj._listenerId] = obj; - for (var id in listeners) { - listeners[id].off(name, callback, this); - if (deleteListener) delete this._listeners[id]; - } - return this; - } - - }; - - // Regular expression used to split event strings. - var eventSplitter = /\s+/; - - // Implement fancy features of the Events API such as multiple event - // names `"change blur"` and jQuery-style event maps `{change: action}` - // in terms of the existing API. - var eventsApi = function(obj, action, name, rest) { - if (!name) return true; - - // Handle event maps. - if (typeof name === 'object') { - for (var key in name) { - obj[action].apply(obj, [key, name[key]].concat(rest)); - } - return false; - } - - // Handle space separated event names. - if (eventSplitter.test(name)) { - var names = name.split(eventSplitter); - for (var i = 0, l = names.length; i < l; i++) { - obj[action].apply(obj, [names[i]].concat(rest)); - } - return false; - } - - return true; - }; - - // A difficult-to-believe, but optimized internal dispatch function for - // triggering events. Tries to keep the usual cases speedy (most internal - // Backbone events have 3 arguments). - var triggerEvents = function(events, args) { - var ev, i = -1, l = events.length, a1 = args[0], a2 = args[1], a3 = args[2]; - switch (args.length) { - case 0: while (++i < l) (ev = events[i]).callback.call(ev.ctx); return; - case 1: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1); return; - case 2: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1, a2); return; - case 3: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1, a2, a3); return; - default: while (++i < l) (ev = events[i]).callback.apply(ev.ctx, args); - } - }; - - var listenMethods = {listenTo: 'on', listenToOnce: 'once'}; - - // Inversion-of-control versions of `on` and `once`. Tell *this* object to - // listen to an event in another object ... keeping track of what it's - // listening to. - _.each(listenMethods, function(implementation, method) { - Events[method] = function(obj, name, callback) { - var listeners = this._listeners || (this._listeners = {}); - var id = obj._listenerId || (obj._listenerId = _.uniqueId('l')); - listeners[id] = obj; - if (typeof name === 'object') callback = this; - obj[implementation](name, callback, this); - return this; - }; - }); - - // Aliases for backwards compatibility. - Events.bind = Events.on; - Events.unbind = Events.off; - - // Allow the `Backbone` object to serve as a global event bus, for folks who - // want global "pubsub" in a convenient place. - _.extend(Backbone, Events); - - // Backbone.Model - // -------------- - - // Backbone **Models** are the basic data object in the framework -- - // frequently representing a row in a table in a database on your server. - // A discrete chunk of data and a bunch of useful, related methods for - // performing computations and transformations on that data. - - // Create a new model with the specified attributes. A client id (`cid`) - // is automatically generated and assigned for you. - var Model = Backbone.Model = function(attributes, options) { - var defaults; - var attrs = attributes || {}; - options || (options = {}); - this.cid = _.uniqueId('c'); - this.attributes = {}; - _.extend(this, _.pick(options, modelOptions)); - if (options.parse) attrs = this.parse(attrs, options) || {}; - if (defaults = _.result(this, 'defaults')) { - attrs = _.defaults({}, attrs, defaults); - } - this.set(attrs, options); - this.changed = {}; - this.initialize.apply(this, arguments); - }; - - // A list of options to be attached directly to the model, if provided. - var modelOptions = ['urlRoot', 'collection']; - - // Attach all inheritable methods to the Model prototype. - _.extend(Model.prototype, Events, { - - // A hash of attributes whose current and previous value differ. - changed: null, - - // The value returned during the last failed validation. - validationError: null, - - // The default name for the JSON `id` attribute is `"id"`. MongoDB and - // CouchDB users may want to set this to `"_id"`. - idAttribute: 'id', - - // Initialize is an empty function by default. Override it with your own - // initialization logic. - initialize: function(){}, - - // Return a copy of the model's `attributes` object. - toJSON: function(options) { - return _.clone(this.attributes); - }, - - // Proxy `Backbone.sync` by default -- but override this if you need - // custom syncing semantics for *this* particular model. - sync: function() { - return Backbone.sync.apply(this, arguments); - }, - - // Get the value of an attribute. - get: function(attr) { - return this.attributes[attr]; - }, - - // Get the HTML-escaped value of an attribute. - escape: function(attr) { - return _.escape(this.get(attr)); - }, - - // Returns `true` if the attribute contains a value that is not null - // or undefined. - has: function(attr) { - return this.get(attr) != null; - }, - - // Set a hash of model attributes on the object, firing `"change"`. This is - // the core primitive operation of a model, updating the data and notifying - // anyone who needs to know about the change in state. The heart of the beast. - set: function(key, val, options) { - var attr, attrs, unset, changes, silent, changing, prev, current; - if (key == null) return this; - - // Handle both `"key", value` and `{key: value}` -style arguments. - if (typeof key === 'object') { - attrs = key; - options = val; - } else { - (attrs = {})[key] = val; - } - - options || (options = {}); - - // Run validation. - if (!this._validate(attrs, options)) return false; - - // Extract attributes and options. - unset = options.unset; - silent = options.silent; - changes = []; - changing = this._changing; - this._changing = true; - - if (!changing) { - this._previousAttributes = _.clone(this.attributes); - this.changed = {}; - } - current = this.attributes, prev = this._previousAttributes; - - // Check for changes of `id`. - if (this.idAttribute in attrs) this.id = attrs[this.idAttribute]; - - // For each `set` attribute, update or delete the current value. - for (attr in attrs) { - val = attrs[attr]; - if (!_.isEqual(current[attr], val)) changes.push(attr); - if (!_.isEqual(prev[attr], val)) { - this.changed[attr] = val; - } else { - delete this.changed[attr]; - } - unset ? delete current[attr] : current[attr] = val; - } - - // Trigger all relevant attribute changes. - if (!silent) { - if (changes.length) this._pending = true; - for (var i = 0, l = changes.length; i < l; i++) { - this.trigger('change:' + changes[i], this, current[changes[i]], options); - } - } - - // You might be wondering why there's a `while` loop here. Changes can - // be recursively nested within `"change"` events. - if (changing) return this; - if (!silent) { - while (this._pending) { - this._pending = false; - this.trigger('change', this, options); - } - } - this._pending = false; - this._changing = false; - return this; - }, - - // Remove an attribute from the model, firing `"change"`. `unset` is a noop - // if the attribute doesn't exist. - unset: function(attr, options) { - return this.set(attr, void 0, _.extend({}, options, {unset: true})); - }, - - // Clear all attributes on the model, firing `"change"`. - clear: function(options) { - var attrs = {}; - for (var key in this.attributes) attrs[key] = void 0; - return this.set(attrs, _.extend({}, options, {unset: true})); - }, - - // Determine if the model has changed since the last `"change"` event. - // If you specify an attribute name, determine if that attribute has changed. - hasChanged: function(attr) { - if (attr == null) return !_.isEmpty(this.changed); - return _.has(this.changed, attr); - }, - - // Return an object containing all the attributes that have changed, or - // false if there are no changed attributes. Useful for determining what - // parts of a view need to be updated and/or what attributes need to be - // persisted to the server. Unset attributes will be set to undefined. - // You can also pass an attributes object to diff against the model, - // determining if there *would be* a change. - changedAttributes: function(diff) { - if (!diff) return this.hasChanged() ? _.clone(this.changed) : false; - var val, changed = false; - var old = this._changing ? this._previousAttributes : this.attributes; - for (var attr in diff) { - if (_.isEqual(old[attr], (val = diff[attr]))) continue; - (changed || (changed = {}))[attr] = val; - } - return changed; - }, - - // Get the previous value of an attribute, recorded at the time the last - // `"change"` event was fired. - previous: function(attr) { - if (attr == null || !this._previousAttributes) return null; - return this._previousAttributes[attr]; - }, - - // Get all of the attributes of the model at the time of the previous - // `"change"` event. - previousAttributes: function() { - return _.clone(this._previousAttributes); - }, - - // Fetch the model from the server. If the server's representation of the - // model differs from its current attributes, they will be overridden, - // triggering a `"change"` event. - fetch: function(options) { - options = options ? _.clone(options) : {}; - if (options.parse === void 0) options.parse = true; - var model = this; - var success = options.success; - options.success = function(resp) { - if (!model.set(model.parse(resp, options), options)) return false; - if (success) success(model, resp, options); - model.trigger('sync', model, resp, options); - }; - wrapError(this, options); - return this.sync('read', this, options); - }, - - // Set a hash of model attributes, and sync the model to the server. - // If the server returns an attributes hash that differs, the model's - // state will be `set` again. - save: function(key, val, options) { - var attrs, method, xhr, attributes = this.attributes; - - // Handle both `"key", value` and `{key: value}` -style arguments. - if (key == null || typeof key === 'object') { - attrs = key; - options = val; - } else { - (attrs = {})[key] = val; - } - - // If we're not waiting and attributes exist, save acts as `set(attr).save(null, opts)`. - if (attrs && (!options || !options.wait) && !this.set(attrs, options)) return false; - - options = _.extend({validate: true}, options); - - // Do not persist invalid models. - if (!this._validate(attrs, options)) return false; - - // Set temporary attributes if `{wait: true}`. - if (attrs && options.wait) { - this.attributes = _.extend({}, attributes, attrs); - } - - // After a successful server-side save, the client is (optionally) - // updated with the server-side state. - if (options.parse === void 0) options.parse = true; - var model = this; - var success = options.success; - options.success = function(resp) { - // Ensure attributes are restored during synchronous saves. - model.attributes = attributes; - var serverAttrs = model.parse(resp, options); - if (options.wait) serverAttrs = _.extend(attrs || {}, serverAttrs); - if (_.isObject(serverAttrs) && !model.set(serverAttrs, options)) { - return false; - } - if (success) success(model, resp, options); - model.trigger('sync', model, resp, options); - }; - wrapError(this, options); - - method = this.isNew() ? 'create' : (options.patch ? 'patch' : 'update'); - if (method === 'patch') options.attrs = attrs; - xhr = this.sync(method, this, options); - - // Restore attributes. - if (attrs && options.wait) this.attributes = attributes; - - return xhr; - }, - - // Destroy this model on the server if it was already persisted. - // Optimistically removes the model from its collection, if it has one. - // If `wait: true` is passed, waits for the server to respond before removal. - destroy: function(options) { - options = options ? _.clone(options) : {}; - var model = this; - var success = options.success; - - var destroy = function() { - model.trigger('destroy', model, model.collection, options); - }; - - options.success = function(resp) { - if (options.wait || model.isNew()) destroy(); - if (success) success(model, resp, options); - if (!model.isNew()) model.trigger('sync', model, resp, options); - }; - - if (this.isNew()) { - options.success(); - return false; - } - wrapError(this, options); - - var xhr = this.sync('delete', this, options); - if (!options.wait) destroy(); - return xhr; - }, - - // Default URL for the model's representation on the server -- if you're - // using Backbone's restful methods, override this to change the endpoint - // that will be called. - url: function() { - var base = _.result(this, 'urlRoot') || _.result(this.collection, 'url') || urlError(); - if (this.isNew()) return base; - return base + (base.charAt(base.length - 1) === '/' ? '' : '/') + encodeURIComponent(this.id); - }, - - // **parse** converts a response into the hash of attributes to be `set` on - // the model. The default implementation is just to pass the response along. - parse: function(resp, options) { - return resp; - }, - - // Create a new model with identical attributes to this one. - clone: function() { - return new this.constructor(this.attributes); - }, - - // A model is new if it has never been saved to the server, and lacks an id. - isNew: function() { - return this.id == null; - }, - - // Check if the model is currently in a valid state. - isValid: function(options) { - return this._validate({}, _.extend(options || {}, { validate: true })); - }, - - // Run validation against the next complete set of model attributes, - // returning `true` if all is well. Otherwise, fire an `"invalid"` event. - _validate: function(attrs, options) { - if (!options.validate || !this.validate) return true; - attrs = _.extend({}, this.attributes, attrs); - var error = this.validationError = this.validate(attrs, options) || null; - if (!error) return true; - this.trigger('invalid', this, error, _.extend(options || {}, {validationError: error})); - return false; - } - - }); - - // Underscore methods that we want to implement on the Model. - var modelMethods = ['keys', 'values', 'pairs', 'invert', 'pick', 'omit']; - - // Mix in each Underscore method as a proxy to `Model#attributes`. - _.each(modelMethods, function(method) { - Model.prototype[method] = function() { - var args = slice.call(arguments); - args.unshift(this.attributes); - return _[method].apply(_, args); - }; - }); - - // Backbone.Collection - // ------------------- - - // If models tend to represent a single row of data, a Backbone Collection is - // more analagous to a table full of data ... or a small slice or page of that - // table, or a collection of rows that belong together for a particular reason - // -- all of the messages in this particular folder, all of the documents - // belonging to this particular author, and so on. Collections maintain - // indexes of their models, both in order, and for lookup by `id`. - - // Create a new **Collection**, perhaps to contain a specific type of `model`. - // If a `comparator` is specified, the Collection will maintain - // its models in sort order, as they're added and removed. - var Collection = Backbone.Collection = function(models, options) { - options || (options = {}); - if (options.url) this.url = options.url; - if (options.model) this.model = options.model; - if (options.comparator !== void 0) this.comparator = options.comparator; - this._reset(); - this.initialize.apply(this, arguments); - if (models) this.reset(models, _.extend({silent: true}, options)); - }; - - // Default options for `Collection#set`. - var setOptions = {add: true, remove: true, merge: true}; - var addOptions = {add: true, merge: false, remove: false}; - - // Define the Collection's inheritable methods. - _.extend(Collection.prototype, Events, { - - // The default model for a collection is just a **Backbone.Model**. - // This should be overridden in most cases. - model: Model, - - // Initialize is an empty function by default. Override it with your own - // initialization logic. - initialize: function(){}, - - // The JSON representation of a Collection is an array of the - // models' attributes. - toJSON: function(options) { - return this.map(function(model){ return model.toJSON(options); }); - }, - - // Proxy `Backbone.sync` by default. - sync: function() { - return Backbone.sync.apply(this, arguments); - }, - - // Add a model, or list of models to the set. - add: function(models, options) { - return this.set(models, _.defaults(options || {}, addOptions)); - }, - - // Remove a model, or a list of models from the set. - remove: function(models, options) { - models = _.isArray(models) ? models.slice() : [models]; - options || (options = {}); - var i, l, index, model; - for (i = 0, l = models.length; i < l; i++) { - model = this.get(models[i]); - if (!model) continue; - delete this._byId[model.id]; - delete this._byId[model.cid]; - index = this.indexOf(model); - this.models.splice(index, 1); - this.length--; - if (!options.silent) { - options.index = index; - model.trigger('remove', model, this, options); - } - this._removeReference(model); - } - return this; - }, - - // Update a collection by `set`-ing a new list of models, adding new ones, - // removing models that are no longer present, and merging models that - // already exist in the collection, as necessary. Similar to **Model#set**, - // the core operation for updating the data contained by the collection. - set: function(models, options) { - options = _.defaults(options || {}, setOptions); - if (options.parse) models = this.parse(models, options); - if (!_.isArray(models)) models = models ? [models] : []; - var i, l, model, attrs, existing, sort; - var at = options.at; - var sortable = this.comparator && (at == null) && options.sort !== false; - var sortAttr = _.isString(this.comparator) ? this.comparator : null; - var toAdd = [], toRemove = [], modelMap = {}; - - // Turn bare objects into model references, and prevent invalid models - // from being added. - for (i = 0, l = models.length; i < l; i++) { - if (!(model = this._prepareModel(models[i], options))) continue; - - // If a duplicate is found, prevent it from being added and - // optionally merge it into the existing model. - if (existing = this.get(model)) { - if (options.remove) modelMap[existing.cid] = true; - if (options.merge) { - existing.set(model.attributes, options); - if (sortable && !sort && existing.hasChanged(sortAttr)) sort = true; - } - - // This is a new model, push it to the `toAdd` list. - } else if (options.add) { - toAdd.push(model); - - // Listen to added models' events, and index models for lookup by - // `id` and by `cid`. - model.on('all', this._onModelEvent, this); - this._byId[model.cid] = model; - if (model.id != null) this._byId[model.id] = model; - } - } - - // Remove nonexistent models if appropriate. - if (options.remove) { - for (i = 0, l = this.length; i < l; ++i) { - if (!modelMap[(model = this.models[i]).cid]) toRemove.push(model); - } - if (toRemove.length) this.remove(toRemove, options); - } - - // See if sorting is needed, update `length` and splice in new models. - if (toAdd.length) { - if (sortable) sort = true; - this.length += toAdd.length; - if (at != null) { - splice.apply(this.models, [at, 0].concat(toAdd)); - } else { - push.apply(this.models, toAdd); - } - } - - // Silently sort the collection if appropriate. - if (sort) this.sort({silent: true}); - - if (options.silent) return this; - - // Trigger `add` events. - for (i = 0, l = toAdd.length; i < l; i++) { - (model = toAdd[i]).trigger('add', model, this, options); - } - - // Trigger `sort` if the collection was sorted. - if (sort) this.trigger('sort', this, options); - return this; - }, - - // When you have more items than you want to add or remove individually, - // you can reset the entire set with a new list of models, without firing - // any granular `add` or `remove` events. Fires `reset` when finished. - // Useful for bulk operations and optimizations. - reset: function(models, options) { - options || (options = {}); - for (var i = 0, l = this.models.length; i < l; i++) { - this._removeReference(this.models[i]); - } - options.previousModels = this.models; - this._reset(); - this.add(models, _.extend({silent: true}, options)); - if (!options.silent) this.trigger('reset', this, options); - return this; - }, - - // Add a model to the end of the collection. - push: function(model, options) { - model = this._prepareModel(model, options); - this.add(model, _.extend({at: this.length}, options)); - return model; - }, - - // Remove a model from the end of the collection. - pop: function(options) { - var model = this.at(this.length - 1); - this.remove(model, options); - return model; - }, - - // Add a model to the beginning of the collection. - unshift: function(model, options) { - model = this._prepareModel(model, options); - this.add(model, _.extend({at: 0}, options)); - return model; - }, - - // Remove a model from the beginning of the collection. - shift: function(options) { - var model = this.at(0); - this.remove(model, options); - return model; - }, - - // Slice out a sub-array of models from the collection. - slice: function(begin, end) { - return this.models.slice(begin, end); - }, - - // Get a model from the set by id. - get: function(obj) { - if (obj == null) return void 0; - return this._byId[obj.id != null ? obj.id : obj.cid || obj]; - }, - - // Get the model at the given index. - at: function(index) { - return this.models[index]; - }, - - // Return models with matching attributes. Useful for simple cases of - // `filter`. - where: function(attrs, first) { - if (_.isEmpty(attrs)) return first ? void 0 : []; - return this[first ? 'find' : 'filter'](function(model) { - for (var key in attrs) { - if (attrs[key] !== model.get(key)) return false; - } - return true; - }); - }, - - // Return the first model with matching attributes. Useful for simple cases - // of `find`. - findWhere: function(attrs) { - return this.where(attrs, true); - }, - - // Force the collection to re-sort itself. You don't need to call this under - // normal circumstances, as the set will maintain sort order as each item - // is added. - sort: function(options) { - if (!this.comparator) throw new Error('Cannot sort a set without a comparator'); - options || (options = {}); - - // Run sort based on type of `comparator`. - if (_.isString(this.comparator) || this.comparator.length === 1) { - this.models = this.sortBy(this.comparator, this); - } else { - this.models.sort(_.bind(this.comparator, this)); - } - - if (!options.silent) this.trigger('sort', this, options); - return this; - }, - - // Figure out the smallest index at which a model should be inserted so as - // to maintain order. - sortedIndex: function(model, value, context) { - value || (value = this.comparator); - var iterator = _.isFunction(value) ? value : function(model) { - return model.get(value); - }; - return _.sortedIndex(this.models, model, iterator, context); - }, - - // Pluck an attribute from each model in the collection. - pluck: function(attr) { - return _.invoke(this.models, 'get', attr); - }, - - // Fetch the default set of models for this collection, resetting the - // collection when they arrive. If `reset: true` is passed, the response - // data will be passed through the `reset` method instead of `set`. - fetch: function(options) { - options = options ? _.clone(options) : {}; - if (options.parse === void 0) options.parse = true; - var success = options.success; - var collection = this; - options.success = function(resp) { - var method = options.reset ? 'reset' : 'set'; - collection[method](resp, options); - if (success) success(collection, resp, options); - collection.trigger('sync', collection, resp, options); - }; - wrapError(this, options); - return this.sync('read', this, options); - }, - - // Create a new instance of a model in this collection. Add the model to the - // collection immediately, unless `wait: true` is passed, in which case we - // wait for the server to agree. - create: function(model, options) { - options = options ? _.clone(options) : {}; - if (!(model = this._prepareModel(model, options))) return false; - if (!options.wait) this.add(model, options); - var collection = this; - var success = options.success; - options.success = function(resp) { - if (options.wait) collection.add(model, options); - if (success) success(model, resp, options); - }; - model.save(null, options); - return model; - }, - - // **parse** converts a response into a list of models to be added to the - // collection. The default implementation is just to pass it through. - parse: function(resp, options) { - return resp; - }, - - // Create a new collection with an identical list of models as this one. - clone: function() { - return new this.constructor(this.models); - }, - - // Private method to reset all internal state. Called when the collection - // is first initialized or reset. - _reset: function() { - this.length = 0; - this.models = []; - this._byId = {}; - }, - - // Prepare a hash of attributes (or other model) to be added to this - // collection. - _prepareModel: function(attrs, options) { - if (attrs instanceof Model) { - if (!attrs.collection) attrs.collection = this; - return attrs; - } - options || (options = {}); - options.collection = this; - var model = new this.model(attrs, options); - if (!model._validate(attrs, options)) { - this.trigger('invalid', this, attrs, options); - return false; - } - return model; - }, - - // Internal method to sever a model's ties to a collection. - _removeReference: function(model) { - if (this === model.collection) delete model.collection; - model.off('all', this._onModelEvent, this); - }, - - // Internal method called every time a model in the set fires an event. - // Sets need to update their indexes when models change ids. All other - // events simply proxy through. "add" and "remove" events that originate - // in other collections are ignored. - _onModelEvent: function(event, model, collection, options) { - if ((event === 'add' || event === 'remove') && collection !== this) return; - if (event === 'destroy') this.remove(model, options); - if (model && event === 'change:' + model.idAttribute) { - delete this._byId[model.previous(model.idAttribute)]; - if (model.id != null) this._byId[model.id] = model; - } - this.trigger.apply(this, arguments); - } - - }); - - // Underscore methods that we want to implement on the Collection. - // 90% of the core usefulness of Backbone Collections is actually implemented - // right here: - var methods = ['forEach', 'each', 'map', 'collect', 'reduce', 'foldl', - 'inject', 'reduceRight', 'foldr', 'find', 'detect', 'filter', 'select', - 'reject', 'every', 'all', 'some', 'any', 'include', 'contains', 'invoke', - 'max', 'min', 'toArray', 'size', 'first', 'head', 'take', 'initial', 'rest', - 'tail', 'drop', 'last', 'without', 'indexOf', 'shuffle', 'lastIndexOf', - 'isEmpty', 'chain']; - - // Mix in each Underscore method as a proxy to `Collection#models`. - _.each(methods, function(method) { - Collection.prototype[method] = function() { - var args = slice.call(arguments); - args.unshift(this.models); - return _[method].apply(_, args); - }; - }); - - // Underscore methods that take a property name as an argument. - var attributeMethods = ['groupBy', 'countBy', 'sortBy']; - - // Use attributes instead of properties. - _.each(attributeMethods, function(method) { - Collection.prototype[method] = function(value, context) { - var iterator = _.isFunction(value) ? value : function(model) { - return model.get(value); - }; - return _[method](this.models, iterator, context); - }; - }); - - // Backbone.View - // ------------- - - // Backbone Views are almost more convention than they are actual code. A View - // is simply a JavaScript object that represents a logical chunk of UI in the - // DOM. This might be a single item, an entire list, a sidebar or panel, or - // even the surrounding frame which wraps your whole app. Defining a chunk of - // UI as a **View** allows you to define your DOM events declaratively, without - // having to worry about render order ... and makes it easy for the view to - // react to specific changes in the state of your models. - - // Creating a Backbone.View creates its initial element outside of the DOM, - // if an existing element is not provided... - var View = Backbone.View = function(options) { - this.cid = _.uniqueId('view'); - this._configure(options || {}); - this._ensureElement(); - this.initialize.apply(this, arguments); - this.delegateEvents(); - }; - - // Cached regex to split keys for `delegate`. - var delegateEventSplitter = /^(\S+)\s*(.*)$/; - - // List of view options to be merged as properties. - var viewOptions = ['model', 'collection', 'el', 'id', 'attributes', 'className', 'tagName', 'events']; - - // Set up all inheritable **Backbone.View** properties and methods. - _.extend(View.prototype, Events, { - - // The default `tagName` of a View's element is `"div"`. - tagName: 'div', - - // jQuery delegate for element lookup, scoped to DOM elements within the - // current view. This should be prefered to global lookups where possible. - $: function(selector) { - return this.$el.find(selector); - }, - - // Initialize is an empty function by default. Override it with your own - // initialization logic. - initialize: function(){}, - - // **render** is the core function that your view should override, in order - // to populate its element (`this.el`), with the appropriate HTML. The - // convention is for **render** to always return `this`. - render: function() { - return this; - }, - - // Remove this view by taking the element out of the DOM, and removing any - // applicable Backbone.Events listeners. - remove: function() { - this.$el.remove(); - this.stopListening(); - return this; - }, - - // Change the view's element (`this.el` property), including event - // re-delegation. - setElement: function(element, delegate) { - if (this.$el) this.undelegateEvents(); - this.$el = element instanceof Backbone.$ ? element : Backbone.$(element); - this.el = this.$el[0]; - if (delegate !== false) this.delegateEvents(); - return this; - }, - - // Set callbacks, where `this.events` is a hash of - // - // *{"event selector": "callback"}* - // - // { - // 'mousedown .title': 'edit', - // 'click .button': 'save' - // 'click .open': function(e) { ... } - // } - // - // pairs. Callbacks will be bound to the view, with `this` set properly. - // Uses event delegation for efficiency. - // Omitting the selector binds the event to `this.el`. - // This only works for delegate-able events: not `focus`, `blur`, and - // not `change`, `submit`, and `reset` in Internet Explorer. - delegateEvents: function(events) { - if (!(events || (events = _.result(this, 'events')))) return this; - this.undelegateEvents(); - for (var key in events) { - var method = events[key]; - if (!_.isFunction(method)) method = this[events[key]]; - if (!method) continue; - - var match = key.match(delegateEventSplitter); - var eventName = match[1], selector = match[2]; - method = _.bind(method, this); - eventName += '.delegateEvents' + this.cid; - if (selector === '') { - this.$el.on(eventName, method); - } else { - this.$el.on(eventName, selector, method); - } - } - return this; - }, - - // Clears all callbacks previously bound to the view with `delegateEvents`. - // You usually don't need to use this, but may wish to if you have multiple - // Backbone views attached to the same DOM element. - undelegateEvents: function() { - this.$el.off('.delegateEvents' + this.cid); - return this; - }, - - // Performs the initial configuration of a View with a set of options. - // Keys with special meaning *(e.g. model, collection, id, className)* are - // attached directly to the view. See `viewOptions` for an exhaustive - // list. - _configure: function(options) { - if (this.options) options = _.extend({}, _.result(this, 'options'), options); - _.extend(this, _.pick(options, viewOptions)); - this.options = options; - }, - - // Ensure that the View has a DOM element to render into. - // If `this.el` is a string, pass it through `$()`, take the first - // matching element, and re-assign it to `el`. Otherwise, create - // an element from the `id`, `className` and `tagName` properties. - _ensureElement: function() { - if (!this.el) { - var attrs = _.extend({}, _.result(this, 'attributes')); - if (this.id) attrs.id = _.result(this, 'id'); - if (this.className) attrs['class'] = _.result(this, 'className'); - var $el = Backbone.$('<' + _.result(this, 'tagName') + '>').attr(attrs); - this.setElement($el, false); - } else { - this.setElement(_.result(this, 'el'), false); - } - } - - }); - - // Backbone.sync - // ------------- - - // Override this function to change the manner in which Backbone persists - // models to the server. You will be passed the type of request, and the - // model in question. By default, makes a RESTful Ajax request - // to the model's `url()`. Some possible customizations could be: - // - // * Use `setTimeout` to batch rapid-fire updates into a single request. - // * Send up the models as XML instead of JSON. - // * Persist models via WebSockets instead of Ajax. - // - // Turn on `Backbone.emulateHTTP` in order to send `PUT` and `DELETE` requests - // as `POST`, with a `_method` parameter containing the true HTTP method, - // as well as all requests with the body as `application/x-www-form-urlencoded` - // instead of `application/json` with the model in a param named `model`. - // Useful when interfacing with server-side languages like **PHP** that make - // it difficult to read the body of `PUT` requests. - Backbone.sync = function(method, model, options) { - var type = methodMap[method]; - - // Default options, unless specified. - _.defaults(options || (options = {}), { - emulateHTTP: Backbone.emulateHTTP, - emulateJSON: Backbone.emulateJSON - }); - - // Default JSON-request options. - var params = {type: type, dataType: 'json'}; - - // Ensure that we have a URL. - if (!options.url) { - params.url = _.result(model, 'url') || urlError(); - } - - // Ensure that we have the appropriate request data. - if (options.data == null && model && (method === 'create' || method === 'update' || method === 'patch')) { - params.contentType = 'application/json'; - params.data = JSON.stringify(options.attrs || model.toJSON(options)); - } - - // For older servers, emulate JSON by encoding the request into an HTML-form. - if (options.emulateJSON) { - params.contentType = 'application/x-www-form-urlencoded'; - params.data = params.data ? {model: params.data} : {}; - } - - // For older servers, emulate HTTP by mimicking the HTTP method with `_method` - // And an `X-HTTP-Method-Override` header. - if (options.emulateHTTP && (type === 'PUT' || type === 'DELETE' || type === 'PATCH')) { - params.type = 'POST'; - if (options.emulateJSON) params.data._method = type; - var beforeSend = options.beforeSend; - options.beforeSend = function(xhr) { - xhr.setRequestHeader('X-HTTP-Method-Override', type); - if (beforeSend) return beforeSend.apply(this, arguments); - }; - } - - // Don't process data on a non-GET request. - if (params.type !== 'GET' && !options.emulateJSON) { - params.processData = false; - } - - // If we're sending a `PATCH` request, and we're in an old Internet Explorer - // that still has ActiveX enabled by default, override jQuery to use that - // for XHR instead. Remove this line when jQuery supports `PATCH` on IE8. - if (params.type === 'PATCH' && window.ActiveXObject && - !(window.external && window.external.msActiveXFilteringEnabled)) { - params.xhr = function() { - return new ActiveXObject("Microsoft.XMLHTTP"); - }; - } - - // Make the request, allowing the user to override any Ajax options. - var xhr = options.xhr = Backbone.ajax(_.extend(params, options)); - model.trigger('request', model, xhr, options); - return xhr; - }; - - // Map from CRUD to HTTP for our default `Backbone.sync` implementation. - var methodMap = { - 'create': 'POST', - 'update': 'PUT', - 'patch': 'PATCH', - 'delete': 'DELETE', - 'read': 'GET' - }; - - // Set the default implementation of `Backbone.ajax` to proxy through to `$`. - // Override this if you'd like to use a different library. - Backbone.ajax = function() { - return Backbone.$.ajax.apply(Backbone.$, arguments); - }; - - // Backbone.Router - // --------------- - - // Routers map faux-URLs to actions, and fire events when routes are - // matched. Creating a new one sets its `routes` hash, if not set statically. - var Router = Backbone.Router = function(options) { - options || (options = {}); - if (options.routes) this.routes = options.routes; - this._bindRoutes(); - this.initialize.apply(this, arguments); - }; - - // Cached regular expressions for matching named param parts and splatted - // parts of route strings. - var optionalParam = /\((.*?)\)/g; - var namedParam = /(\(\?)?:\w+/g; - var splatParam = /\*\w+/g; - var escapeRegExp = /[\-{}\[\]+?.,\\\^$|#\s]/g; - - // Set up all inheritable **Backbone.Router** properties and methods. - _.extend(Router.prototype, Events, { - - // Initialize is an empty function by default. Override it with your own - // initialization logic. - initialize: function(){}, - - // Manually bind a single named route to a callback. For example: - // - // this.route('search/:query/p:num', 'search', function(query, num) { - // ... - // }); - // - route: function(route, name, callback) { - if (!_.isRegExp(route)) route = this._routeToRegExp(route); - if (_.isFunction(name)) { - callback = name; - name = ''; - } - if (!callback) callback = this[name]; - var router = this; - Backbone.history.route(route, function(fragment) { - var args = router._extractParameters(route, fragment); - callback && callback.apply(router, args); - router.trigger.apply(router, ['route:' + name].concat(args)); - router.trigger('route', name, args); - Backbone.history.trigger('route', router, name, args); - }); - return this; - }, - - // Simple proxy to `Backbone.history` to save a fragment into the history. - navigate: function(fragment, options) { - Backbone.history.navigate(fragment, options); - return this; - }, - - // Bind all defined routes to `Backbone.history`. We have to reverse the - // order of the routes here to support behavior where the most general - // routes can be defined at the bottom of the route map. - _bindRoutes: function() { - if (!this.routes) return; - this.routes = _.result(this, 'routes'); - var route, routes = _.keys(this.routes); - while ((route = routes.pop()) != null) { - this.route(route, this.routes[route]); - } - }, - - // Convert a route string into a regular expression, suitable for matching - // against the current location hash. - _routeToRegExp: function(route) { - route = route.replace(escapeRegExp, '\\$&') - .replace(optionalParam, '(?:$1)?') - .replace(namedParam, function(match, optional){ - return optional ? match : '([^\/]+)'; - }) - .replace(splatParam, '(.*?)'); - return new RegExp('^' + route + '$'); - }, - - // Given a route, and a URL fragment that it matches, return the array of - // extracted decoded parameters. Empty or unmatched parameters will be - // treated as `null` to normalize cross-browser behavior. - _extractParameters: function(route, fragment) { - var params = route.exec(fragment).slice(1); - return _.map(params, function(param) { - return param ? decodeURIComponent(param) : null; - }); - } - - }); - - // Backbone.History - // ---------------- - - // Handles cross-browser history management, based on either - // [pushState](http://diveintohtml5.info/history.html) and real URLs, or - // [onhashchange](https://developer.mozilla.org/en-US/docs/DOM/window.onhashchange) - // and URL fragments. If the browser supports neither (old IE, natch), - // falls back to polling. - var History = Backbone.History = function() { - this.handlers = []; - _.bindAll(this, 'checkUrl'); - - // Ensure that `History` can be used outside of the browser. - if (typeof window !== 'undefined') { - this.location = window.location; - this.history = window.history; - } - }; - - // Cached regex for stripping a leading hash/slash and trailing space. - var routeStripper = /^[#\/]|\s+$/g; - - // Cached regex for stripping leading and trailing slashes. - var rootStripper = /^\/+|\/+$/g; - - // Cached regex for detecting MSIE. - var isExplorer = /msie [\w.]+/; - - // Cached regex for removing a trailing slash. - var trailingSlash = /\/$/; - - // Has the history handling already been started? - History.started = false; - - // Set up all inheritable **Backbone.History** properties and methods. - _.extend(History.prototype, Events, { - - // The default interval to poll for hash changes, if necessary, is - // twenty times a second. - interval: 50, - - // Gets the true hash value. Cannot use location.hash directly due to bug - // in Firefox where location.hash will always be decoded. - getHash: function(window) { - var match = (window || this).location.href.match(/#(.*)$/); - return match ? match[1] : ''; - }, - - // Get the cross-browser normalized URL fragment, either from the URL, - // the hash, or the override. - getFragment: function(fragment, forcePushState) { - if (fragment == null) { - if (this._hasPushState || !this._wantsHashChange || forcePushState) { - fragment = this.location.pathname; - var root = this.root.replace(trailingSlash, ''); - if (!fragment.indexOf(root)) fragment = fragment.substr(root.length); - } else { - fragment = this.getHash(); - } - } - return fragment.replace(routeStripper, ''); - }, - - // Start the hash change handling, returning `true` if the current URL matches - // an existing route, and `false` otherwise. - start: function(options) { - if (History.started) throw new Error("Backbone.history has already been started"); - History.started = true; - - // Figure out the initial configuration. Do we need an iframe? - // Is pushState desired ... is it available? - this.options = _.extend({}, {root: '/'}, this.options, options); - this.root = this.options.root; - this._wantsHashChange = this.options.hashChange !== false; - this._wantsPushState = !!this.options.pushState; - this._hasPushState = !!(this.options.pushState && this.history && this.history.pushState); - var fragment = this.getFragment(); - var docMode = document.documentMode; - var oldIE = (isExplorer.exec(navigator.userAgent.toLowerCase()) && (!docMode || docMode <= 7)); - - // Normalize root to always include a leading and trailing slash. - this.root = ('/' + this.root + '/').replace(rootStripper, '/'); - - if (oldIE && this._wantsHashChange) { - this.iframe = Backbone.$('<iframe src="javascript:0" tabindex="-1" />').hide().appendTo('body')[0].contentWindow; - this.navigate(fragment); - } - - // Depending on whether we're using pushState or hashes, and whether - // 'onhashchange' is supported, determine how we check the URL state. - if (this._hasPushState) { - Backbone.$(window).on('popstate', this.checkUrl); - } else if (this._wantsHashChange && ('onhashchange' in window) && !oldIE) { - Backbone.$(window).on('hashchange', this.checkUrl); - } else if (this._wantsHashChange) { - this._checkUrlInterval = setInterval(this.checkUrl, this.interval); - } - - // Determine if we need to change the base url, for a pushState link - // opened by a non-pushState browser. - this.fragment = fragment; - var loc = this.location; - var atRoot = loc.pathname.replace(/[^\/]$/, '$&/') === this.root; - - // If we've started off with a route from a `pushState`-enabled browser, - // but we're currently in a browser that doesn't support it... - if (this._wantsHashChange && this._wantsPushState && !this._hasPushState && !atRoot) { - this.fragment = this.getFragment(null, true); - this.location.replace(this.root + this.location.search + '#' + this.fragment); - // Return immediately as browser will do redirect to new url - return true; - - // Or if we've started out with a hash-based route, but we're currently - // in a browser where it could be `pushState`-based instead... - } else if (this._wantsPushState && this._hasPushState && atRoot && loc.hash) { - this.fragment = this.getHash().replace(routeStripper, ''); - this.history.replaceState({}, document.title, this.root + this.fragment + loc.search); - } - - if (!this.options.silent) return this.loadUrl(); - }, - - // Disable Backbone.history, perhaps temporarily. Not useful in a real app, - // but possibly useful for unit testing Routers. - stop: function() { - Backbone.$(window).off('popstate', this.checkUrl).off('hashchange', this.checkUrl); - clearInterval(this._checkUrlInterval); - History.started = false; - }, - - // Add a route to be tested when the fragment changes. Routes added later - // may override previous routes. - route: function(route, callback) { - this.handlers.unshift({route: route, callback: callback}); - }, - - // Checks the current URL to see if it has changed, and if it has, - // calls `loadUrl`, normalizing across the hidden iframe. - checkUrl: function(e) { - var current = this.getFragment(); - if (current === this.fragment && this.iframe) { - current = this.getFragment(this.getHash(this.iframe)); - } - if (current === this.fragment) return false; - if (this.iframe) this.navigate(current); - this.loadUrl() || this.loadUrl(this.getHash()); - }, - - // Attempt to load the current URL fragment. If a route succeeds with a - // match, returns `true`. If no defined routes matches the fragment, - // returns `false`. - loadUrl: function(fragmentOverride) { - var fragment = this.fragment = this.getFragment(fragmentOverride); - var matched = _.any(this.handlers, function(handler) { - if (handler.route.test(fragment)) { - handler.callback(fragment); - return true; - } - }); - return matched; - }, - - // Save a fragment into the hash history, or replace the URL state if the - // 'replace' option is passed. You are responsible for properly URL-encoding - // the fragment in advance. - // - // The options object can contain `trigger: true` if you wish to have the - // route callback be fired (not usually desirable), or `replace: true`, if - // you wish to modify the current URL without adding an entry to the history. - navigate: function(fragment, options) { - if (!History.started) return false; - if (!options || options === true) options = {trigger: options}; - fragment = this.getFragment(fragment || ''); - if (this.fragment === fragment) return; - this.fragment = fragment; - var url = this.root + fragment; - - // If pushState is available, we use it to set the fragment as a real URL. - if (this._hasPushState) { - this.history[options.replace ? 'replaceState' : 'pushState']({}, document.title, url); - - // If hash changes haven't been explicitly disabled, update the hash - // fragment to store history. - } else if (this._wantsHashChange) { - this._updateHash(this.location, fragment, options.replace); - if (this.iframe && (fragment !== this.getFragment(this.getHash(this.iframe)))) { - // Opening and closing the iframe tricks IE7 and earlier to push a - // history entry on hash-tag change. When replace is true, we don't - // want this. - if(!options.replace) this.iframe.document.open().close(); - this._updateHash(this.iframe.location, fragment, options.replace); - } - - // If you've told us that you explicitly don't want fallback hashchange- - // based history, then `navigate` becomes a page refresh. - } else { - return this.location.assign(url); - } - if (options.trigger) this.loadUrl(fragment); - }, - - // Update the hash location, either replacing the current entry, or adding - // a new one to the browser history. - _updateHash: function(location, fragment, replace) { - if (replace) { - var href = location.href.replace(/(javascript:|#).*$/, ''); - location.replace(href + '#' + fragment); - } else { - // Some browsers require that `hash` contains a leading #. - location.hash = '#' + fragment; - } - } - - }); - - // Create the default Backbone.history. - Backbone.history = new History; - - // Helpers - // ------- - - // Helper function to correctly set up the prototype chain, for subclasses. - // Similar to `goog.inherits`, but uses a hash of prototype properties and - // class properties to be extended. - var extend = function(protoProps, staticProps) { - var parent = this; - var child; - - // The constructor function for the new subclass is either defined by you - // (the "constructor" property in your `extend` definition), or defaulted - // by us to simply call the parent's constructor. - if (protoProps && _.has(protoProps, 'constructor')) { - child = protoProps.constructor; - } else { - child = function(){ return parent.apply(this, arguments); }; - } - - // Add static properties to the constructor function, if supplied. - _.extend(child, parent, staticProps); - - // Set the prototype chain to inherit from `parent`, without calling - // `parent`'s constructor function. - var Surrogate = function(){ this.constructor = child; }; - Surrogate.prototype = parent.prototype; - child.prototype = new Surrogate; - - // Add prototype properties (instance properties) to the subclass, - // if supplied. - if (protoProps) _.extend(child.prototype, protoProps); - - // Set a convenience property in case the parent's prototype is needed - // later. - child.__super__ = parent.prototype; - - return child; - }; - - // Set up inheritance for the model, collection, router, view and history. - Model.extend = Collection.extend = Router.extend = View.extend = History.extend = extend; - - // Throw an error when a URL is needed, and none is supplied. - var urlError = function() { - throw new Error('A "url" property or function must be specified'); - }; - - // Wrap an optional error callback with a fallback error event. - var wrapError = function (model, options) { - var error = options.error; - options.error = function(resp) { - if (error) error(model, resp, options); - model.trigger('error', model, resp, options); - }; - }; - -}).call(this); diff --git a/src/UI/JsLibraries/backbone.marionette.js b/src/UI/JsLibraries/backbone.marionette.js deleted file mode 100644 index 5ad3a5d9a..000000000 --- a/src/UI/JsLibraries/backbone.marionette.js +++ /dev/null @@ -1,2329 +0,0 @@ -// MarionetteJS (Backbone.Marionette) -// ---------------------------------- -// v1.0.4 -// -// Copyright (c)2013 Derick Bailey, Muted Solutions, LLC. -// Distributed under MIT license -// -// http://marionettejs.com - - - -/*! - * Includes BabySitter - * https://github.com/marionettejs/backbone.babysitter/ - * - * Includes Wreqr - * https://github.com/marionettejs/backbone.wreqr/ - */ - -// Backbone.BabySitter -// ------------------- -// v0.0.6 -// -// Copyright (c)2013 Derick Bailey, Muted Solutions, LLC. -// Distributed under MIT license -// -// http://github.com/babysitterjs/backbone.babysitter - -// Backbone.ChildViewContainer -// --------------------------- -// -// Provide a container to store, retrieve and -// shut down child views. - -Backbone.ChildViewContainer = (function(Backbone, _){ - - // Container Constructor - // --------------------- - - var Container = function(views){ - this._views = {}; - this._indexByModel = {}; - this._indexByCustom = {}; - this._updateLength(); - - _.each(views, this.add, this); - }; - - // Container Methods - // ----------------- - - _.extend(Container.prototype, { - - // Add a view to this container. Stores the view - // by `cid` and makes it searchable by the model - // cid (and model itself). Optionally specify - // a custom key to store an retrieve the view. - add: function(view, customIndex){ - var viewCid = view.cid; - - // store the view - this._views[viewCid] = view; - - // index it by model - if (view.model){ - this._indexByModel[view.model.cid] = viewCid; - } - - // index by custom - if (customIndex){ - this._indexByCustom[customIndex] = viewCid; - } - - this._updateLength(); - }, - - // Find a view by the model that was attached to - // it. Uses the model's `cid` to find it. - findByModel: function(model){ - return this.findByModelCid(model.cid); - }, - - // Find a view by the `cid` of the model that was attached to - // it. Uses the model's `cid` to find the view `cid` and - // retrieve the view using it. - findByModelCid: function(modelCid){ - var viewCid = this._indexByModel[modelCid]; - return this.findByCid(viewCid); - }, - - // Find a view by a custom indexer. - findByCustom: function(index){ - var viewCid = this._indexByCustom[index]; - return this.findByCid(viewCid); - }, - - // Find by index. This is not guaranteed to be a - // stable index. - findByIndex: function(index){ - return _.values(this._views)[index]; - }, - - // retrieve a view by it's `cid` directly - findByCid: function(cid){ - return this._views[cid]; - }, - - // Remove a view - remove: function(view){ - var viewCid = view.cid; - - // delete model index - if (view.model){ - delete this._indexByModel[view.model.cid]; - } - - // delete custom index - _.any(this._indexByCustom, function(cid, key) { - if (cid === viewCid) { - delete this._indexByCustom[key]; - return true; - } - }, this); - - // remove the view from the container - delete this._views[viewCid]; - - // update the length - this._updateLength(); - }, - - // Call a method on every view in the container, - // passing parameters to the call method one at a - // time, like `function.call`. - call: function(method){ - this.apply(method, _.tail(arguments)); - }, - - // Apply a method on every view in the container, - // passing parameters to the call method one at a - // time, like `function.apply`. - apply: function(method, args){ - _.each(this._views, function(view){ - if (_.isFunction(view[method])){ - view[method].apply(view, args || []); - } - }); - }, - - // Update the `.length` attribute on this container - _updateLength: function(){ - this.length = _.size(this._views); - } - }); - - // Borrowing this code from Backbone.Collection: - // http://backbonejs.org/docs/backbone.html#section-106 - // - // Mix in methods from Underscore, for iteration, and other - // collection related features. - var methods = ['forEach', 'each', 'map', 'find', 'detect', 'filter', - 'select', 'reject', 'every', 'all', 'some', 'any', 'include', - 'contains', 'invoke', 'toArray', 'first', 'initial', 'rest', - 'last', 'without', 'isEmpty', 'pluck']; - - _.each(methods, function(method) { - Container.prototype[method] = function() { - var views = _.values(this._views); - var args = [views].concat(_.toArray(arguments)); - return _[method].apply(_, args); - }; - }); - - // return the public API - return Container; -})(Backbone, _); - -// Backbone.Wreqr (Backbone.Marionette) -// ---------------------------------- -// v0.2.0 -// -// Copyright (c)2013 Derick Bailey, Muted Solutions, LLC. -// Distributed under MIT license -// -// http://github.com/marionettejs/backbone.wreqr - - -Backbone.Wreqr = (function(Backbone, Marionette, _){ - "use strict"; - var Wreqr = {}; - - // Handlers -// -------- -// A registry of functions to call, given a name - -Wreqr.Handlers = (function(Backbone, _){ - "use strict"; - - // Constructor - // ----------- - - var Handlers = function(options){ - this.options = options; - this._wreqrHandlers = {}; - - if (_.isFunction(this.initialize)){ - this.initialize(options); - } - }; - - Handlers.extend = Backbone.Model.extend; - - // Instance Members - // ---------------- - - _.extend(Handlers.prototype, Backbone.Events, { - - // Add multiple handlers using an object literal configuration - setHandlers: function(handlers){ - _.each(handlers, function(handler, name){ - var context = null; - - if (_.isObject(handler) && !_.isFunction(handler)){ - context = handler.context; - handler = handler.callback; - } - - this.setHandler(name, handler, context); - }, this); - }, - - // Add a handler for the given name, with an - // optional context to run the handler within - setHandler: function(name, handler, context){ - var config = { - callback: handler, - context: context - }; - - this._wreqrHandlers[name] = config; - - this.trigger("handler:add", name, handler, context); - }, - - // Determine whether or not a handler is registered - hasHandler: function(name){ - return !! this._wreqrHandlers[name]; - }, - - // Get the currently registered handler for - // the specified name. Throws an exception if - // no handler is found. - getHandler: function(name){ - var config = this._wreqrHandlers[name]; - - if (!config){ - throw new Error("Handler not found for '" + name + "'"); - } - - return function(){ - var args = Array.prototype.slice.apply(arguments); - return config.callback.apply(config.context, args); - }; - }, - - // Remove a handler for the specified name - removeHandler: function(name){ - delete this._wreqrHandlers[name]; - }, - - // Remove all handlers from this registry - removeAllHandlers: function(){ - this._wreqrHandlers = {}; - } - }); - - return Handlers; -})(Backbone, _); - - // Wreqr.CommandStorage -// -------------------- -// -// Store and retrieve commands for execution. -Wreqr.CommandStorage = (function(){ - "use strict"; - - // Constructor function - var CommandStorage = function(options){ - this.options = options; - this._commands = {}; - - if (_.isFunction(this.initialize)){ - this.initialize(options); - } - }; - - // Instance methods - _.extend(CommandStorage.prototype, Backbone.Events, { - - // Get an object literal by command name, that contains - // the `commandName` and the `instances` of all commands - // represented as an array of arguments to process - getCommands: function(commandName){ - var commands = this._commands[commandName]; - - // we don't have it, so add it - if (!commands){ - - // build the configuration - commands = { - command: commandName, - instances: [] - }; - - // store it - this._commands[commandName] = commands; - } - - return commands; - }, - - // Add a command by name, to the storage and store the - // args for the command - addCommand: function(commandName, args){ - var command = this.getCommands(commandName); - command.instances.push(args); - }, - - // Clear all commands for the given `commandName` - clearCommands: function(commandName){ - var command = this.getCommands(commandName); - command.instances = []; - } - }); - - return CommandStorage; -})(); - - // Wreqr.Commands -// -------------- -// -// A simple command pattern implementation. Register a command -// handler and execute it. -Wreqr.Commands = (function(Wreqr){ - "use strict"; - - return Wreqr.Handlers.extend({ - // default storage type - storageType: Wreqr.CommandStorage, - - constructor: function(options){ - this.options = options || {}; - - this._initializeStorage(this.options); - this.on("handler:add", this._executeCommands, this); - - var args = Array.prototype.slice.call(arguments); - Wreqr.Handlers.prototype.constructor.apply(this, args); - }, - - // Execute a named command with the supplied args - execute: function(name, args){ - name = arguments[0]; - args = Array.prototype.slice.call(arguments, 1); - - if (this.hasHandler(name)){ - this.getHandler(name).apply(this, args); - } else { - this.storage.addCommand(name, args); - } - - }, - - // Internal method to handle bulk execution of stored commands - _executeCommands: function(name, handler, context){ - var command = this.storage.getCommands(name); - - // loop through and execute all the stored command instances - _.each(command.instances, function(args){ - handler.apply(context, args); - }); - - this.storage.clearCommands(name); - }, - - // Internal method to initialize storage either from the type's - // `storageType` or the instance `options.storageType`. - _initializeStorage: function(options){ - var storage; - - var StorageType = options.storageType || this.storageType; - if (_.isFunction(StorageType)){ - storage = new StorageType(); - } else { - storage = StorageType; - } - - this.storage = storage; - } - }); - -})(Wreqr); - - // Wreqr.RequestResponse -// --------------------- -// -// A simple request/response implementation. Register a -// request handler, and return a response from it -Wreqr.RequestResponse = (function(Wreqr){ - "use strict"; - - return Wreqr.Handlers.extend({ - request: function(){ - var name = arguments[0]; - var args = Array.prototype.slice.call(arguments, 1); - - return this.getHandler(name).apply(this, args); - } - }); - -})(Wreqr); - - // Event Aggregator -// ---------------- -// A pub-sub object that can be used to decouple various parts -// of an application through event-driven architecture. - -Wreqr.EventAggregator = (function(Backbone, _){ - "use strict"; - var EA = function(){}; - - // Copy the `extend` function used by Backbone's classes - EA.extend = Backbone.Model.extend; - - // Copy the basic Backbone.Events on to the event aggregator - _.extend(EA.prototype, Backbone.Events); - - return EA; -})(Backbone, _); - - - return Wreqr; -})(Backbone, Backbone.Marionette, _); - -var Marionette = (function(global, Backbone, _){ - "use strict"; - - // Define and export the Marionette namespace - var Marionette = {}; - Backbone.Marionette = Marionette; - - // Get the DOM manipulator for later use - Marionette.$ = Backbone.$; - -// Helpers -// ------- - -// For slicing `arguments` in functions -var protoSlice = Array.prototype.slice; -function slice(args) { - return protoSlice.call(args); -} - -function throwError(message, name) { - var error = new Error(message); - error.name = name || 'Error'; - throw error; -} - -// Marionette.extend -// ----------------- - -// Borrow the Backbone `extend` method so we can use it as needed -Marionette.extend = Backbone.Model.extend; - -// Marionette.getOption -// -------------------- - -// Retrieve an object, function or other value from a target -// object or its `options`, with `options` taking precedence. -Marionette.getOption = function(target, optionName){ - if (!target || !optionName){ return; } - var value; - - if (target.options && (optionName in target.options) && (target.options[optionName] !== undefined)){ - value = target.options[optionName]; - } else { - value = target[optionName]; - } - - return value; -}; - -// Trigger an event and a corresponding method name. Examples: -// -// `this.triggerMethod("foo")` will trigger the "foo" event and -// call the "onFoo" method. -// -// `this.triggerMethod("foo:bar") will trigger the "foo:bar" event and -// call the "onFooBar" method. -Marionette.triggerMethod = (function(){ - - // split the event name on the : - var splitter = /(^|:)(\w)/gi; - - // take the event section ("section1:section2:section3") - // and turn it in to uppercase name - function getEventName(match, prefix, eventName) { - return eventName.toUpperCase(); - } - - // actual triggerMethod name - var triggerMethod = function(event) { - // get the method name from the event name - var methodName = 'on' + event.replace(splitter, getEventName); - var method = this[methodName]; - - // trigger the event - this.trigger.apply(this, arguments); - - // call the onMethodName if it exists - if (_.isFunction(method)) { - // pass all arguments, except the event name - return method.apply(this, _.tail(arguments)); - } - }; - - return triggerMethod; -})(); - -// DOMRefresh -// ---------- -// -// Monitor a view's state, and after it has been rendered and shown -// in the DOM, trigger a "dom:refresh" event every time it is -// re-rendered. - -Marionette.MonitorDOMRefresh = (function(){ - // track when the view has been rendered - function handleShow(view){ - view._isShown = true; - triggerDOMRefresh(view); - } - - // track when the view has been shown in the DOM, - // using a Marionette.Region (or by other means of triggering "show") - function handleRender(view){ - view._isRendered = true; - triggerDOMRefresh(view); - } - - // Trigger the "dom:refresh" event and corresponding "onDomRefresh" method - function triggerDOMRefresh(view){ - if (view._isShown && view._isRendered){ - if (_.isFunction(view.triggerMethod)){ - view.triggerMethod("dom:refresh"); - } - } - } - - // Export public API - return function(view){ - view.listenTo(view, "show", function(){ - handleShow(view); - }); - - view.listenTo(view, "render", function(){ - handleRender(view); - }); - }; -})(); - - -// Marionette.bindEntityEvents & unbindEntityEvents -// --------------------------- -// -// These methods are used to bind/unbind a backbone "entity" (collection/model) -// to methods on a target object. -// -// The first parameter, `target`, must have a `listenTo` method from the -// EventBinder object. -// -// The second parameter is the entity (Backbone.Model or Backbone.Collection) -// to bind the events from. -// -// The third parameter is a hash of { "event:name": "eventHandler" } -// configuration. Multiple handlers can be separated by a space. A -// function can be supplied instead of a string handler name. - -(function(Marionette){ - "use strict"; - - // Bind the event to handlers specified as a string of - // handler names on the target object - function bindFromStrings(target, entity, evt, methods){ - var methodNames = methods.split(/\s+/); - - _.each(methodNames,function(methodName) { - - var method = target[methodName]; - if(!method) { - throwError("Method '"+ methodName +"' was configured as an event handler, but does not exist."); - } - - target.listenTo(entity, evt, method, target); - }); - } - - // Bind the event to a supplied callback function - function bindToFunction(target, entity, evt, method){ - target.listenTo(entity, evt, method, target); - } - - // Bind the event to handlers specified as a string of - // handler names on the target object - function unbindFromStrings(target, entity, evt, methods){ - var methodNames = methods.split(/\s+/); - - _.each(methodNames,function(methodName) { - var method = target[methodName]; - target.stopListening(entity, evt, method, target); - }); - } - - // Bind the event to a supplied callback function - function unbindToFunction(target, entity, evt, method){ - target.stopListening(entity, evt, method, target); - } - - - // generic looping function - function iterateEvents(target, entity, bindings, functionCallback, stringCallback){ - if (!entity || !bindings) { return; } - - // allow the bindings to be a function - if (_.isFunction(bindings)){ - bindings = bindings.call(target); - } - - // iterate the bindings and bind them - _.each(bindings, function(methods, evt){ - - // allow for a function as the handler, - // or a list of event names as a string - if (_.isFunction(methods)){ - functionCallback(target, entity, evt, methods); - } else { - stringCallback(target, entity, evt, methods); - } - - }); - } - - // Export Public API - Marionette.bindEntityEvents = function(target, entity, bindings){ - iterateEvents(target, entity, bindings, bindToFunction, bindFromStrings); - }; - - Marionette.unbindEntityEvents = function(target, entity, bindings){ - iterateEvents(target, entity, bindings, unbindToFunction, unbindFromStrings); - }; - -})(Marionette); - - -// Callbacks -// --------- - -// A simple way of managing a collection of callbacks -// and executing them at a later point in time, using jQuery's -// `Deferred` object. -Marionette.Callbacks = function(){ - this._deferred = Marionette.$.Deferred(); - this._callbacks = []; -}; - -_.extend(Marionette.Callbacks.prototype, { - - // Add a callback to be executed. Callbacks added here are - // guaranteed to execute, even if they are added after the - // `run` method is called. - add: function(callback, contextOverride){ - this._callbacks.push({cb: callback, ctx: contextOverride}); - - this._deferred.done(function(context, options){ - if (contextOverride){ context = contextOverride; } - callback.call(context, options); - }); - }, - - // Run all registered callbacks with the context specified. - // Additional callbacks can be added after this has been run - // and they will still be executed. - run: function(options, context){ - this._deferred.resolve(context, options); - }, - - // Resets the list of callbacks to be run, allowing the same list - // to be run multiple times - whenever the `run` method is called. - reset: function(){ - var callbacks = this._callbacks; - this._deferred = Marionette.$.Deferred(); - this._callbacks = []; - - _.each(callbacks, function(cb){ - this.add(cb.cb, cb.ctx); - }, this); - } -}); - - -// Marionette Controller -// --------------------- -// -// A multi-purpose object to use as a controller for -// modules and routers, and as a mediator for workflow -// and coordination of other objects, views, and more. -Marionette.Controller = function(options){ - this.triggerMethod = Marionette.triggerMethod; - this.options = options || {}; - - if (_.isFunction(this.initialize)){ - this.initialize(this.options); - } -}; - -Marionette.Controller.extend = Marionette.extend; - -// Controller Methods -// -------------- - -// Ensure it can trigger events with Backbone.Events -_.extend(Marionette.Controller.prototype, Backbone.Events, { - close: function(){ - this.stopListening(); - this.triggerMethod("close"); - this.unbind(); - } -}); - -// Region -// ------ -// -// Manage the visual regions of your composite application. See -// http://lostechies.com/derickbailey/2011/12/12/composite-js-apps-regions-and-region-managers/ - -Marionette.Region = function(options){ - this.options = options || {}; - - this.el = Marionette.getOption(this, "el"); - - if (!this.el){ - var err = new Error("An 'el' must be specified for a region."); - err.name = "NoElError"; - throw err; - } - - if (this.initialize){ - var args = Array.prototype.slice.apply(arguments); - this.initialize.apply(this, args); - } -}; - - -// Region Type methods -// ------------------- - -_.extend(Marionette.Region, { - - // Build an instance of a region by passing in a configuration object - // and a default region type to use if none is specified in the config. - // - // The config object should either be a string as a jQuery DOM selector, - // a Region type directly, or an object literal that specifies both - // a selector and regionType: - // - // ```js - // { - // selector: "#foo", - // regionType: MyCustomRegion - // } - // ``` - // - buildRegion: function(regionConfig, defaultRegionType){ - var regionIsString = (typeof regionConfig === "string"); - var regionSelectorIsString = (typeof regionConfig.selector === "string"); - var regionTypeIsUndefined = (typeof regionConfig.regionType === "undefined"); - var regionIsType = (typeof regionConfig === "function"); - - if (!regionIsType && !regionIsString && !regionSelectorIsString) { - throw new Error("Region must be specified as a Region type, a selector string or an object with selector property"); - } - - var selector, RegionType; - - // get the selector for the region - - if (regionIsString) { - selector = regionConfig; - } - - if (regionConfig.selector) { - selector = regionConfig.selector; - } - - // get the type for the region - - if (regionIsType){ - RegionType = regionConfig; - } - - if (!regionIsType && regionTypeIsUndefined) { - RegionType = defaultRegionType; - } - - if (regionConfig.regionType) { - RegionType = regionConfig.regionType; - } - - // build the region instance - var region = new RegionType({ - el: selector - }); - - // override the `getEl` function if we have a parentEl - // this must be overridden to ensure the selector is found - // on the first use of the region. if we try to assign the - // region's `el` to `parentEl.find(selector)` in the object - // literal to build the region, the element will not be - // guaranteed to be in the DOM already, and will cause problems - if (regionConfig.parentEl){ - - region.getEl = function(selector) { - var parentEl = regionConfig.parentEl; - if (_.isFunction(parentEl)){ - parentEl = parentEl(); - } - return parentEl.find(selector); - }; - } - - return region; - } - -}); - -// Region Instance Methods -// ----------------------- - -_.extend(Marionette.Region.prototype, Backbone.Events, { - - // Displays a backbone view instance inside of the region. - // Handles calling the `render` method for you. Reads content - // directly from the `el` attribute. Also calls an optional - // `onShow` and `close` method on your view, just after showing - // or just before closing the view, respectively. - show: function(view){ - - this.ensureEl(); - - var isViewClosed = view.isClosed || _.isUndefined(view.$el); - - var isDifferentView = view !== this.currentView; - - if (isDifferentView) { - this.close(); - } - - view.render(); - - if (isDifferentView || isViewClosed) { - this.open(view); - } - - this.currentView = view; - - Marionette.triggerMethod.call(this, "show", view); - Marionette.triggerMethod.call(view, "show"); - }, - - ensureEl: function(){ - if (!this.$el || this.$el.length === 0){ - this.$el = this.getEl(this.el); - } - }, - - // Override this method to change how the region finds the - // DOM element that it manages. Return a jQuery selector object. - getEl: function(selector){ - return Marionette.$(selector); - }, - - // Override this method to change how the new view is - // appended to the `$el` that the region is managing - open: function(view){ - this.$el.empty().append(view.el); - }, - - // Close the current view, if there is one. If there is no - // current view, it does nothing and returns immediately. - close: function(){ - var view = this.currentView; - if (!view || view.isClosed){ return; } - - // call 'close' or 'remove', depending on which is found - if (view.close) { view.close(); } - else if (view.remove) { view.remove(); } - - Marionette.triggerMethod.call(this, "close"); - - delete this.currentView; - }, - - // Attach an existing view to the region. This - // will not call `render` or `onShow` for the new view, - // and will not replace the current HTML for the `el` - // of the region. - attachView: function(view){ - this.currentView = view; - }, - - // Reset the region by closing any existing view and - // clearing out the cached `$el`. The next time a view - // is shown via this region, the region will re-query the - // DOM for the region's `el`. - reset: function(){ - this.close(); - delete this.$el; - } -}); - -// Copy the `extend` function used by Backbone's classes -Marionette.Region.extend = Marionette.extend; - -// Marionette.RegionManager -// ------------------------ -// -// Manage one or more related `Marionette.Region` objects. -Marionette.RegionManager = (function(Marionette){ - - var RegionManager = Marionette.Controller.extend({ - constructor: function(options){ - this._regions = {}; - Marionette.Controller.prototype.constructor.call(this, options); - }, - - // Add multiple regions using an object literal, where - // each key becomes the region name, and each value is - // the region definition. - addRegions: function(regionDefinitions, defaults){ - var regions = {}; - - _.each(regionDefinitions, function(definition, name){ - if (typeof definition === "string"){ - definition = { selector: definition }; - } - - if (definition.selector){ - definition = _.defaults({}, definition, defaults); - } - - var region = this.addRegion(name, definition); - regions[name] = region; - }, this); - - return regions; - }, - - // Add an individual region to the region manager, - // and return the region instance - addRegion: function(name, definition){ - var region; - - var isObject = _.isObject(definition); - var isString = _.isString(definition); - var hasSelector = !!definition.selector; - - if (isString || (isObject && hasSelector)){ - region = Marionette.Region.buildRegion(definition, Marionette.Region); - } else if (_.isFunction(definition)){ - region = Marionette.Region.buildRegion(definition, Marionette.Region); - } else { - region = definition; - } - - this._store(name, region); - this.triggerMethod("region:add", name, region); - return region; - }, - - // Get a region by name - get: function(name){ - return this._regions[name]; - }, - - // Remove a region by name - removeRegion: function(name){ - var region = this._regions[name]; - this._remove(name, region); - }, - - // Close all regions in the region manager, and - // remove them - removeRegions: function(){ - _.each(this._regions, function(region, name){ - this._remove(name, region); - }, this); - }, - - // Close all regions in the region manager, but - // leave them attached - closeRegions: function(){ - _.each(this._regions, function(region, name){ - region.close(); - }, this); - }, - - // Close all regions and shut down the region - // manager entirely - close: function(){ - this.removeRegions(); - var args = Array.prototype.slice.call(arguments); - Marionette.Controller.prototype.close.apply(this, args); - }, - - // internal method to store regions - _store: function(name, region){ - this._regions[name] = region; - this._setLength(); - }, - - // internal method to remove a region - _remove: function(name, region){ - region.close(); - delete this._regions[name]; - this._setLength(); - this.triggerMethod("region:remove", name, region); - }, - - // set the number of regions current held - _setLength: function(){ - this.length = _.size(this._regions); - } - - }); - - // Borrowing this code from Backbone.Collection: - // http://backbonejs.org/docs/backbone.html#section-106 - // - // Mix in methods from Underscore, for iteration, and other - // collection related features. - var methods = ['forEach', 'each', 'map', 'find', 'detect', 'filter', - 'select', 'reject', 'every', 'all', 'some', 'any', 'include', - 'contains', 'invoke', 'toArray', 'first', 'initial', 'rest', - 'last', 'without', 'isEmpty', 'pluck']; - - _.each(methods, function(method) { - RegionManager.prototype[method] = function() { - var regions = _.values(this._regions); - var args = [regions].concat(_.toArray(arguments)); - return _[method].apply(_, args); - }; - }); - - return RegionManager; -})(Marionette); - - -// Template Cache -// -------------- - -// Manage templates stored in `<script>` blocks, -// caching them for faster access. -Marionette.TemplateCache = function(templateId){ - this.templateId = templateId; -}; - -// TemplateCache object-level methods. Manage the template -// caches from these method calls instead of creating -// your own TemplateCache instances -_.extend(Marionette.TemplateCache, { - templateCaches: {}, - - // Get the specified template by id. Either - // retrieves the cached version, or loads it - // from the DOM. - get: function(templateId){ - var cachedTemplate = this.templateCaches[templateId]; - - if (!cachedTemplate){ - cachedTemplate = new Marionette.TemplateCache(templateId); - this.templateCaches[templateId] = cachedTemplate; - } - - return cachedTemplate.load(); - }, - - // Clear templates from the cache. If no arguments - // are specified, clears all templates: - // `clear()` - // - // If arguments are specified, clears each of the - // specified templates from the cache: - // `clear("#t1", "#t2", "...")` - clear: function(){ - var i; - var args = slice(arguments); - var length = args.length; - - if (length > 0){ - for(i=0; i<length; i++){ - delete this.templateCaches[args[i]]; - } - } else { - this.templateCaches = {}; - } - } -}); - -// TemplateCache instance methods, allowing each -// template cache object to manage its own state -// and know whether or not it has been loaded -_.extend(Marionette.TemplateCache.prototype, { - - // Internal method to load the template - load: function(){ - // Guard clause to prevent loading this template more than once - if (this.compiledTemplate){ - return this.compiledTemplate; - } - - // Load the template and compile it - var template = this.loadTemplate(this.templateId); - this.compiledTemplate = this.compileTemplate(template); - - return this.compiledTemplate; - }, - - // Load a template from the DOM, by default. Override - // this method to provide your own template retrieval - // For asynchronous loading with AMD/RequireJS, consider - // using a template-loader plugin as described here: - // https://github.com/marionettejs/backbone.marionette/wiki/Using-marionette-with-requirejs - loadTemplate: function(templateId){ - var template = Marionette.$(templateId).html(); - - if (!template || template.length === 0){ - throwError("Could not find template: '" + templateId + "'", "NoTemplateError"); - } - - return template; - }, - - // Pre-compile the template before caching it. Override - // this method if you do not need to pre-compile a template - // (JST / RequireJS for example) or if you want to change - // the template engine used (Handebars, etc). - compileTemplate: function(rawTemplate){ - return _.template(rawTemplate); - } -}); - - -// Renderer -// -------- - -// Render a template with data by passing in the template -// selector and the data to render. -Marionette.Renderer = { - - // Render a template with data. The `template` parameter is - // passed to the `TemplateCache` object to retrieve the - // template function. Override this method to provide your own - // custom rendering and template handling for all of Marionette. - render: function(template, data){ - - if (!template) { - var error = new Error("Cannot render the template since it's false, null or undefined."); - error.name = "TemplateNotFoundError"; - throw error; - } - - var templateFunc; - if (typeof template === "function"){ - templateFunc = template; - } else { - templateFunc = Marionette.TemplateCache.get(template); - } - - return templateFunc(data); - } -}; - - - -// Marionette.View -// --------------- - -// The core view type that other Marionette views extend from. -Marionette.View = Backbone.View.extend({ - - constructor: function(){ - _.bindAll(this, "render"); - - var args = Array.prototype.slice.apply(arguments); - Backbone.View.prototype.constructor.apply(this, args); - - Marionette.MonitorDOMRefresh(this); - this.listenTo(this, "show", this.onShowCalled, this); - }, - - // import the "triggerMethod" to trigger events with corresponding - // methods if the method exists - triggerMethod: Marionette.triggerMethod, - - // Get the template for this view - // instance. You can set a `template` attribute in the view - // definition or pass a `template: "whatever"` parameter in - // to the constructor options. - getTemplate: function(){ - return Marionette.getOption(this, "template"); - }, - - // Mix in template helper methods. Looks for a - // `templateHelpers` attribute, which can either be an - // object literal, or a function that returns an object - // literal. All methods and attributes from this object - // are copies to the object passed in. - mixinTemplateHelpers: function(target){ - target = target || {}; - var templateHelpers = this.templateHelpers; - if (_.isFunction(templateHelpers)){ - templateHelpers = templateHelpers.call(this); - } - return _.extend(target, templateHelpers); - }, - - // Configure `triggers` to forward DOM events to view - // events. `triggers: {"click .foo": "do:foo"}` - configureTriggers: function(){ - if (!this.triggers) { return; } - - var triggerEvents = {}; - - // Allow `triggers` to be configured as a function - var triggers = _.result(this, "triggers"); - - // Configure the triggers, prevent default - // action and stop propagation of DOM events - _.each(triggers, function(value, key){ - - // build the event handler function for the DOM event - triggerEvents[key] = function(e){ - - // stop the event in its tracks - if (e && e.preventDefault){ e.preventDefault(); } - if (e && e.stopPropagation){ e.stopPropagation(); } - - // build the args for the event - var args = { - view: this, - model: this.model, - collection: this.collection - }; - - // trigger the event - this.triggerMethod(value, args); - }; - - }, this); - - return triggerEvents; - }, - - // Overriding Backbone.View's delegateEvents to handle - // the `triggers`, `modelEvents`, and `collectionEvents` configuration - delegateEvents: function(events){ - this._delegateDOMEvents(events); - Marionette.bindEntityEvents(this, this.model, Marionette.getOption(this, "modelEvents")); - Marionette.bindEntityEvents(this, this.collection, Marionette.getOption(this, "collectionEvents")); - }, - - // internal method to delegate DOM events and triggers - _delegateDOMEvents: function(events){ - events = events || this.events; - if (_.isFunction(events)){ events = events.call(this); } - - var combinedEvents = {}; - var triggers = this.configureTriggers(); - _.extend(combinedEvents, events, triggers); - - Backbone.View.prototype.delegateEvents.call(this, combinedEvents); - }, - - // Overriding Backbone.View's undelegateEvents to handle unbinding - // the `triggers`, `modelEvents`, and `collectionEvents` config - undelegateEvents: function(){ - var args = Array.prototype.slice.call(arguments); - Backbone.View.prototype.undelegateEvents.apply(this, args); - - Marionette.unbindEntityEvents(this, this.model, Marionette.getOption(this, "modelEvents")); - Marionette.unbindEntityEvents(this, this.collection, Marionette.getOption(this, "collectionEvents")); - }, - - // Internal method, handles the `show` event. - onShowCalled: function(){}, - - // Default `close` implementation, for removing a view from the - // DOM and unbinding it. Regions will call this method - // for you. You can specify an `onClose` method in your view to - // add custom code that is called after the view is closed. - close: function(){ - if (this.isClosed) { return; } - - // allow the close to be stopped by returning `false` - // from the `onBeforeClose` method - var shouldClose = this.triggerMethod("before:close"); - if (shouldClose === false){ - return; - } - - // mark as closed before doing the actual close, to - // prevent infinite loops within "close" event handlers - // that are trying to close other views - this.isClosed = true; - this.triggerMethod("close"); - - // unbind UI elements - this.unbindUIElements(); - - // remove the view from the DOM - this.remove(); - }, - - // This method binds the elements specified in the "ui" hash inside the view's code with - // the associated jQuery selectors. - bindUIElements: function(){ - if (!this.ui) { return; } - - // store the ui hash in _uiBindings so they can be reset later - // and so re-rendering the view will be able to find the bindings - if (!this._uiBindings){ - this._uiBindings = this.ui; - } - - // get the bindings result, as a function or otherwise - var bindings = _.result(this, "_uiBindings"); - - // empty the ui so we don't have anything to start with - this.ui = {}; - - // bind each of the selectors - _.each(_.keys(bindings), function(key) { - var selector = bindings[key]; - this.ui[key] = this.$(selector); - }, this); - }, - - // This method unbinds the elements specified in the "ui" hash - unbindUIElements: function(){ - if (!this.ui){ return; } - - // delete all of the existing ui bindings - _.each(this.ui, function($el, name){ - delete this.ui[name]; - }, this); - - // reset the ui element to the original bindings configuration - this.ui = this._uiBindings; - delete this._uiBindings; - } -}); - -// Item View -// --------- - -// A single item view implementation that contains code for rendering -// with underscore.js templates, serializing the view's model or collection, -// and calling several methods on extended views, such as `onRender`. -Marionette.ItemView = Marionette.View.extend({ - - // Setting up the inheritance chain which allows changes to - // Marionette.View.prototype.constructor which allows overriding - constructor: function(){ - Marionette.View.prototype.constructor.apply(this, slice(arguments)); - }, - - // Serialize the model or collection for the view. If a model is - // found, `.toJSON()` is called. If a collection is found, `.toJSON()` - // is also called, but is used to populate an `items` array in the - // resulting data. If both are found, defaults to the model. - // You can override the `serializeData` method in your own view - // definition, to provide custom serialization for your view's data. - serializeData: function(){ - var data = {}; - - if (this.model) { - data = this.model.toJSON(); - } - else if (this.collection) { - data = { items: this.collection.toJSON() }; - } - - return data; - }, - - // Render the view, defaulting to underscore.js templates. - // You can override this in your view definition to provide - // a very specific rendering for your view. In general, though, - // you should override the `Marionette.Renderer` object to - // change how Marionette renders views. - render: function(){ - this.isClosed = false; - - this.triggerMethod("before:render", this); - this.triggerMethod("item:before:render", this); - - var data = this.serializeData(); - data = this.mixinTemplateHelpers(data); - - var template = this.getTemplate(); - var html = Marionette.Renderer.render(template, data); - - this.$el.html(html); - this.bindUIElements(); - - this.triggerMethod("render", this); - this.triggerMethod("item:rendered", this); - - return this; - }, - - // Override the default close event to add a few - // more events that are triggered. - close: function(){ - if (this.isClosed){ return; } - - this.triggerMethod('item:before:close'); - - Marionette.View.prototype.close.apply(this, slice(arguments)); - - this.triggerMethod('item:closed'); - } -}); - -// Collection View -// --------------- - -// A view that iterates over a Backbone.Collection -// and renders an individual ItemView for each model. -Marionette.CollectionView = Marionette.View.extend({ - // used as the prefix for item view events - // that are forwarded through the collectionview - itemViewEventPrefix: "itemview", - - // constructor - constructor: function(options){ - this._initChildViewStorage(); - - Marionette.View.prototype.constructor.apply(this, slice(arguments)); - - this._initialEvents(); - }, - - // Configured the initial events that the collection view - // binds to. Override this method to prevent the initial - // events, or to add your own initial events. - _initialEvents: function(){ - if (this.collection){ - this.listenTo(this.collection, "add", this.addChildView, this); - this.listenTo(this.collection, "remove", this.removeItemView, this); - this.listenTo(this.collection, "reset", this.render, this); - } - }, - - // Handle a child item added to the collection - addChildView: function(item, collection, options){ - this.closeEmptyView(); - var ItemView = this.getItemView(item); - var index = this.collection.indexOf(item); - this.addItemView(item, ItemView, index); - }, - - // Override from `Marionette.View` to guarantee the `onShow` method - // of child views is called. - onShowCalled: function(){ - this.children.each(function(child){ - Marionette.triggerMethod.call(child, "show"); - }); - }, - - // Internal method to trigger the before render callbacks - // and events - triggerBeforeRender: function(){ - this.triggerMethod("before:render", this); - this.triggerMethod("collection:before:render", this); - }, - - // Internal method to trigger the rendered callbacks and - // events - triggerRendered: function(){ - this.triggerMethod("render", this); - this.triggerMethod("collection:rendered", this); - }, - - // Render the collection of items. Override this method to - // provide your own implementation of a render function for - // the collection view. - render: function(){ - this.isClosed = false; - this.triggerBeforeRender(); - this._renderChildren(); - this.triggerRendered(); - return this; - }, - - // Internal method. Separated so that CompositeView can have - // more control over events being triggered, around the rendering - // process - _renderChildren: function(){ - this.closeEmptyView(); - this.closeChildren(); - - if (this.collection && this.collection.length > 0) { - this.showCollection(); - } else { - this.showEmptyView(); - } - }, - - // Internal method to loop through each item in the - // collection view and show it - showCollection: function(){ - var ItemView; - this.collection.each(function(item, index){ - ItemView = this.getItemView(item); - this.addItemView(item, ItemView, index); - }, this); - }, - - // Internal method to show an empty view in place of - // a collection of item views, when the collection is - // empty - showEmptyView: function(){ - var EmptyView = Marionette.getOption(this, "emptyView"); - - if (EmptyView && !this._showingEmptyView){ - this._showingEmptyView = true; - var model = new Backbone.Model(); - this.addItemView(model, EmptyView, 0); - } - }, - - // Internal method to close an existing emptyView instance - // if one exists. Called when a collection view has been - // rendered empty, and then an item is added to the collection. - closeEmptyView: function(){ - if (this._showingEmptyView){ - this.closeChildren(); - delete this._showingEmptyView; - } - }, - - // Retrieve the itemView type, either from `this.options.itemView` - // or from the `itemView` in the object definition. The "options" - // takes precedence. - getItemView: function(item){ - var itemView = Marionette.getOption(this, "itemView"); - - if (!itemView){ - throwError("An `itemView` must be specified", "NoItemViewError"); - } - - return itemView; - }, - - // Render the child item's view and add it to the - // HTML for the collection view. - addItemView: function(item, ItemView, index){ - // get the itemViewOptions if any were specified - var itemViewOptions = Marionette.getOption(this, "itemViewOptions"); - if (_.isFunction(itemViewOptions)){ - itemViewOptions = itemViewOptions.call(this, item, index); - } - - // build the view - var view = this.buildItemView(item, ItemView, itemViewOptions); - - // set up the child view event forwarding - this.addChildViewEventForwarding(view); - - // this view is about to be added - this.triggerMethod("before:item:added", view); - - // Store the child view itself so we can properly - // remove and/or close it later - this.children.add(view); - - // Render it and show it - this.renderItemView(view, index); - - // call the "show" method if the collection view - // has already been shown - if (this._isShown){ - Marionette.triggerMethod.call(view, "show"); - } - - // this view was added - this.triggerMethod("after:item:added", view); - }, - - // Set up the child view event forwarding. Uses an "itemview:" - // prefix in front of all forwarded events. - addChildViewEventForwarding: function(view){ - var prefix = Marionette.getOption(this, "itemViewEventPrefix"); - - // Forward all child item view events through the parent, - // prepending "itemview:" to the event name - this.listenTo(view, "all", function(){ - var args = slice(arguments); - args[0] = prefix + ":" + args[0]; - args.splice(1, 0, view); - - Marionette.triggerMethod.apply(this, args); - }, this); - }, - - // render the item view - renderItemView: function(view, index) { - view.render(); - this.appendHtml(this, view, index); - }, - - // Build an `itemView` for every model in the collection. - buildItemView: function(item, ItemViewType, itemViewOptions){ - var options = _.extend({model: item}, itemViewOptions); - return new ItemViewType(options); - }, - - // get the child view by item it holds, and remove it - removeItemView: function(item){ - var view = this.children.findByModel(item); - this.removeChildView(view); - this.checkEmpty(); - }, - - // Remove the child view and close it - removeChildView: function(view){ - - // shut down the child view properly, - // including events that the collection has from it - if (view){ - this.stopListening(view); - - // call 'close' or 'remove', depending on which is found - if (view.close) { view.close(); } - else if (view.remove) { view.remove(); } - - this.children.remove(view); - } - - this.triggerMethod("item:removed", view); - }, - - // helper to show the empty view if the collection is empty - checkEmpty: function() { - // check if we're empty now, and if we are, show the - // empty view - if (!this.collection || this.collection.length === 0){ - this.showEmptyView(); - } - }, - - // Append the HTML to the collection's `el`. - // Override this method to do something other - // then `.append`. - appendHtml: function(collectionView, itemView, index){ - collectionView.$el.append(itemView.el); - }, - - // Internal method to set up the `children` object for - // storing all of the child views - _initChildViewStorage: function(){ - this.children = new Backbone.ChildViewContainer(); - }, - - // Handle cleanup and other closing needs for - // the collection of views. - close: function(){ - if (this.isClosed){ return; } - - this.triggerMethod("collection:before:close"); - this.closeChildren(); - this.triggerMethod("collection:closed"); - - Marionette.View.prototype.close.apply(this, slice(arguments)); - }, - - // Close the child views that this collection view - // is holding on to, if any - closeChildren: function(){ - this.children.each(function(child){ - this.removeChildView(child); - }, this); - this.checkEmpty(); - } -}); - - -// Composite View -// -------------- - -// Used for rendering a branch-leaf, hierarchical structure. -// Extends directly from CollectionView and also renders an -// an item view as `modelView`, for the top leaf -Marionette.CompositeView = Marionette.CollectionView.extend({ - - // Setting up the inheritance chain which allows changes to - // Marionette.CollectionView.prototype.constructor which allows overriding - constructor: function(){ - Marionette.CollectionView.prototype.constructor.apply(this, slice(arguments)); - }, - - // Configured the initial events that the composite view - // binds to. Override this method to prevent the initial - // events, or to add your own initial events. - _initialEvents: function(){ - if (this.collection){ - this.listenTo(this.collection, "add", this.addChildView, this); - this.listenTo(this.collection, "remove", this.removeItemView, this); - this.listenTo(this.collection, "reset", this._renderChildren, this); - } - }, - - // Retrieve the `itemView` to be used when rendering each of - // the items in the collection. The default is to return - // `this.itemView` or Marionette.CompositeView if no `itemView` - // has been defined - getItemView: function(item){ - var itemView = Marionette.getOption(this, "itemView") || this.constructor; - - if (!itemView){ - throwError("An `itemView` must be specified", "NoItemViewError"); - } - - return itemView; - }, - - // Serialize the collection for the view. - // You can override the `serializeData` method in your own view - // definition, to provide custom serialization for your view's data. - serializeData: function(){ - var data = {}; - - if (this.model){ - data = this.model.toJSON(); - } - - return data; - }, - - // Renders the model once, and the collection once. Calling - // this again will tell the model's view to re-render itself - // but the collection will not re-render. - render: function(){ - this.isRendered = true; - this.isClosed = false; - this.resetItemViewContainer(); - - this.triggerBeforeRender(); - var html = this.renderModel(); - this.$el.html(html); - // the ui bindings is done here and not at the end of render since they - // will not be available until after the model is rendered, but should be - // available before the collection is rendered. - this.bindUIElements(); - this.triggerMethod("composite:model:rendered"); - - this._renderChildren(); - - this.triggerMethod("composite:rendered"); - this.triggerRendered(); - return this; - }, - - _renderChildren: function(){ - if (this.isRendered){ - Marionette.CollectionView.prototype._renderChildren.call(this); - this.triggerMethod("composite:collection:rendered"); - } - }, - - // Render an individual model, if we have one, as - // part of a composite view (branch / leaf). For example: - // a treeview. - renderModel: function(){ - var data = {}; - data = this.serializeData(); - data = this.mixinTemplateHelpers(data); - - var template = this.getTemplate(); - return Marionette.Renderer.render(template, data); - }, - - // Appends the `el` of itemView instances to the specified - // `itemViewContainer` (a jQuery selector). Override this method to - // provide custom logic of how the child item view instances have their - // HTML appended to the composite view instance. - appendHtml: function(cv, iv, index){ - var $container = this.getItemViewContainer(cv); - $container.append(iv.el); - }, - - // Internal method to ensure an `$itemViewContainer` exists, for the - // `appendHtml` method to use. - getItemViewContainer: function(containerView){ - if ("$itemViewContainer" in containerView){ - return containerView.$itemViewContainer; - } - - var container; - if (containerView.itemViewContainer){ - - var selector = _.result(containerView, "itemViewContainer"); - container = containerView.$(selector); - if (container.length <= 0) { - throwError("The specified `itemViewContainer` was not found: " + containerView.itemViewContainer, "ItemViewContainerMissingError"); - } - - } else { - container = containerView.$el; - } - - containerView.$itemViewContainer = container; - return container; - }, - - // Internal method to reset the `$itemViewContainer` on render - resetItemViewContainer: function(){ - if (this.$itemViewContainer){ - delete this.$itemViewContainer; - } - } -}); - - -// Layout -// ------ - -// Used for managing application layouts, nested layouts and -// multiple regions within an application or sub-application. -// -// A specialized view type that renders an area of HTML and then -// attaches `Region` instances to the specified `regions`. -// Used for composite view management and sub-application areas. -Marionette.Layout = Marionette.ItemView.extend({ - regionType: Marionette.Region, - - // Ensure the regions are available when the `initialize` method - // is called. - constructor: function (options) { - options = options || {}; - - this._firstRender = true; - this._initializeRegions(options); - - Marionette.ItemView.prototype.constructor.call(this, options); - }, - - // Layout's render will use the existing region objects the - // first time it is called. Subsequent calls will close the - // views that the regions are showing and then reset the `el` - // for the regions to the newly rendered DOM elements. - render: function(){ - - if (this._firstRender){ - // if this is the first render, don't do anything to - // reset the regions - this._firstRender = false; - } else if (this.isClosed){ - // a previously closed layout means we need to - // completely re-initialize the regions - this._initializeRegions(); - } else { - // If this is not the first render call, then we need to - // re-initializing the `el` for each region - this._reInitializeRegions(); - } - - var args = Array.prototype.slice.apply(arguments); - var result = Marionette.ItemView.prototype.render.apply(this, args); - - return result; - }, - - // Handle closing regions, and then close the view itself. - close: function () { - if (this.isClosed){ return; } - this.regionManager.close(); - var args = Array.prototype.slice.apply(arguments); - Marionette.ItemView.prototype.close.apply(this, args); - }, - - // Add a single region, by name, to the layout - addRegion: function(name, definition){ - var regions = {}; - regions[name] = definition; - return this.addRegions(regions)[name]; - }, - - // Add multiple regions as a {name: definition, name2: def2} object literal - addRegions: function(regions){ - this.regions = _.extend(this.regions || {}, regions); - return this._buildRegions(regions); - }, - - // Remove a single region from the Layout, by name - removeRegion: function(name){ - return this.regionManager.removeRegion(name); - }, - - // internal method to build regions - _buildRegions: function(regions){ - var that = this; - - var defaults = { - parentEl: function(){ return that.$el; } - }; - - return this.regionManager.addRegions(regions, defaults); - }, - - // Internal method to initialize the regions that have been defined in a - // `regions` attribute on this layout. - _initializeRegions: function (options) { - var regions; - this._initRegionManager(); - - if (_.isFunction(this.regions)) { - regions = this.regions(options); - } else { - regions = this.regions || {}; - } - - this.addRegions(regions); - }, - - // Internal method to re-initialize all of the regions by updating the `el` that - // they point to - _reInitializeRegions: function(){ - this.regionManager.closeRegions(); - this.regionManager.each(function(region){ - region.reset(); - }); - }, - - // Internal method to initialize the region manager - // and all regions in it - _initRegionManager: function(){ - this.regionManager = new Marionette.RegionManager(); - - this.listenTo(this.regionManager, "region:add", function(name, region){ - this[name] = region; - this.trigger("region:add", name, region); - }); - - this.listenTo(this.regionManager, "region:remove", function(name, region){ - delete this[name]; - this.trigger("region:remove", name, region); - }); - } -}); - - -// AppRouter -// --------- - -// Reduce the boilerplate code of handling route events -// and then calling a single method on another object. -// Have your routers configured to call the method on -// your object, directly. -// -// Configure an AppRouter with `appRoutes`. -// -// App routers can only take one `controller` object. -// It is recommended that you divide your controller -// objects in to smaller pieces of related functionality -// and have multiple routers / controllers, instead of -// just one giant router and controller. -// -// You can also add standard routes to an AppRouter. - -Marionette.AppRouter = Backbone.Router.extend({ - - constructor: function(options){ - Backbone.Router.prototype.constructor.apply(this, slice(arguments)); - - this.options = options; - - if (this.appRoutes){ - var controller = Marionette.getOption(this, "controller"); - this.processAppRoutes(controller, this.appRoutes); - } - }, - - // Internal method to process the `appRoutes` for the - // router, and turn them in to routes that trigger the - // specified method on the specified `controller`. - processAppRoutes: function(controller, appRoutes) { - var routeNames = _.keys(appRoutes).reverse(); // Backbone requires reverted order of routes - - _.each(routeNames, function(route) { - var methodName = appRoutes[route]; - var method = controller[methodName]; - - if (!method) { - throw new Error("Method '" + methodName + "' was not found on the controller"); - } - - this.route(route, methodName, _.bind(method, controller)); - }, this); - } -}); - - -// Application -// ----------- - -// Contain and manage the composite application as a whole. -// Stores and starts up `Region` objects, includes an -// event aggregator as `app.vent` -Marionette.Application = function(options){ - this._initRegionManager(); - this._initCallbacks = new Marionette.Callbacks(); - this.vent = new Backbone.Wreqr.EventAggregator(); - this.commands = new Backbone.Wreqr.Commands(); - this.reqres = new Backbone.Wreqr.RequestResponse(); - this.submodules = {}; - - _.extend(this, options); - - this.triggerMethod = Marionette.triggerMethod; -}; - -_.extend(Marionette.Application.prototype, Backbone.Events, { - // Command execution, facilitated by Backbone.Wreqr.Commands - execute: function(){ - var args = Array.prototype.slice.apply(arguments); - this.commands.execute.apply(this.commands, args); - }, - - // Request/response, facilitated by Backbone.Wreqr.RequestResponse - request: function(){ - var args = Array.prototype.slice.apply(arguments); - return this.reqres.request.apply(this.reqres, args); - }, - - // Add an initializer that is either run at when the `start` - // method is called, or run immediately if added after `start` - // has already been called. - addInitializer: function(initializer){ - this._initCallbacks.add(initializer); - }, - - // kick off all of the application's processes. - // initializes all of the regions that have been added - // to the app, and runs all of the initializer functions - start: function(options){ - this.triggerMethod("initialize:before", options); - this._initCallbacks.run(options, this); - this.triggerMethod("initialize:after", options); - - this.triggerMethod("start", options); - }, - - // Add regions to your app. - // Accepts a hash of named strings or Region objects - // addRegions({something: "#someRegion"}) - // addRegions({something: Region.extend({el: "#someRegion"}) }); - addRegions: function(regions){ - return this._regionManager.addRegions(regions); - }, - - // Removes a region from your app. - // Accepts the regions name - // removeRegion('myRegion') - removeRegion: function(region) { - this._regionManager.removeRegion(region); - }, - - // Create a module, attached to the application - module: function(moduleNames, moduleDefinition){ - // slice the args, and add this application object as the - // first argument of the array - var args = slice(arguments); - args.unshift(this); - - // see the Marionette.Module object for more information - return Marionette.Module.create.apply(Marionette.Module, args); - }, - - // Internal method to set up the region manager - _initRegionManager: function(){ - this._regionManager = new Marionette.RegionManager(); - - this.listenTo(this._regionManager, "region:add", function(name, region){ - this[name] = region; - }); - - this.listenTo(this._regionManager, "region:remove", function(name, region){ - delete this[name]; - }); - } -}); - -// Copy the `extend` function used by Backbone's classes -Marionette.Application.extend = Marionette.extend; - -// Module -// ------ - -// A simple module system, used to create privacy and encapsulation in -// Marionette applications -Marionette.Module = function(moduleName, app){ - this.moduleName = moduleName; - - // store sub-modules - this.submodules = {}; - - this._setupInitializersAndFinalizers(); - - // store the configuration for this module - this.app = app; - this.startWithParent = true; - - this.triggerMethod = Marionette.triggerMethod; -}; - -// Extend the Module prototype with events / listenTo, so that the module -// can be used as an event aggregator or pub/sub. -_.extend(Marionette.Module.prototype, Backbone.Events, { - - // Initializer for a specific module. Initializers are run when the - // module's `start` method is called. - addInitializer: function(callback){ - this._initializerCallbacks.add(callback); - }, - - // Finalizers are run when a module is stopped. They are used to teardown - // and finalize any variables, references, events and other code that the - // module had set up. - addFinalizer: function(callback){ - this._finalizerCallbacks.add(callback); - }, - - // Start the module, and run all of its initializers - start: function(options){ - // Prevent re-starting a module that is already started - if (this._isInitialized){ return; } - - // start the sub-modules (depth-first hierarchy) - _.each(this.submodules, function(mod){ - // check to see if we should start the sub-module with this parent - if (mod.startWithParent){ - mod.start(options); - } - }); - - // run the callbacks to "start" the current module - this.triggerMethod("before:start", options); - - this._initializerCallbacks.run(options, this); - this._isInitialized = true; - - this.triggerMethod("start", options); - }, - - // Stop this module by running its finalizers and then stop all of - // the sub-modules for this module - stop: function(){ - // if we are not initialized, don't bother finalizing - if (!this._isInitialized){ return; } - this._isInitialized = false; - - Marionette.triggerMethod.call(this, "before:stop"); - - // stop the sub-modules; depth-first, to make sure the - // sub-modules are stopped / finalized before parents - _.each(this.submodules, function(mod){ mod.stop(); }); - - // run the finalizers - this._finalizerCallbacks.run(undefined,this); - - // reset the initializers and finalizers - this._initializerCallbacks.reset(); - this._finalizerCallbacks.reset(); - - Marionette.triggerMethod.call(this, "stop"); - }, - - // Configure the module with a definition function and any custom args - // that are to be passed in to the definition function - addDefinition: function(moduleDefinition, customArgs){ - this._runModuleDefinition(moduleDefinition, customArgs); - }, - - // Internal method: run the module definition function with the correct - // arguments - _runModuleDefinition: function(definition, customArgs){ - if (!definition){ return; } - - // build the correct list of arguments for the module definition - var args = _.flatten([ - this, - this.app, - Backbone, - Marionette, - Marionette.$, _, - customArgs - ]); - - definition.apply(this, args); - }, - - // Internal method: set up new copies of initializers and finalizers. - // Calling this method will wipe out all existing initializers and - // finalizers. - _setupInitializersAndFinalizers: function(){ - this._initializerCallbacks = new Marionette.Callbacks(); - this._finalizerCallbacks = new Marionette.Callbacks(); - } -}); - -// Type methods to create modules -_.extend(Marionette.Module, { - - // Create a module, hanging off the app parameter as the parent object. - create: function(app, moduleNames, moduleDefinition){ - var module = app; - - // get the custom args passed in after the module definition and - // get rid of the module name and definition function - var customArgs = slice(arguments); - customArgs.splice(0, 3); - - // split the module names and get the length - moduleNames = moduleNames.split("."); - var length = moduleNames.length; - - // store the module definition for the last module in the chain - var moduleDefinitions = []; - moduleDefinitions[length-1] = moduleDefinition; - - // Loop through all the parts of the module definition - _.each(moduleNames, function(moduleName, i){ - var parentModule = module; - module = this._getModule(parentModule, moduleName, app); - this._addModuleDefinition(parentModule, module, moduleDefinitions[i], customArgs); - }, this); - - // Return the last module in the definition chain - return module; - }, - - _getModule: function(parentModule, moduleName, app, def, args){ - // Get an existing module of this name if we have one - var module = parentModule[moduleName]; - - if (!module){ - // Create a new module if we don't have one - module = new Marionette.Module(moduleName, app); - parentModule[moduleName] = module; - // store the module on the parent - parentModule.submodules[moduleName] = module; - } - - return module; - }, - - _addModuleDefinition: function(parentModule, module, def, args){ - var fn; - var startWithParent; - - if (_.isFunction(def)){ - // if a function is supplied for the module definition - fn = def; - startWithParent = true; - - } else if (_.isObject(def)){ - // if an object is supplied - fn = def.define; - startWithParent = def.startWithParent; - - } else { - // if nothing is supplied - startWithParent = true; - } - - // add module definition if needed - if (fn){ - module.addDefinition(fn, args); - } - - // `and` the two together, ensuring a single `false` will prevent it - // from starting with the parent - module.startWithParent = module.startWithParent && startWithParent; - - // setup auto-start if needed - if (module.startWithParent && !module.startWithParentIsConfigured){ - - // only configure this once - module.startWithParentIsConfigured = true; - - // add the module initializer config - parentModule.addInitializer(function(options){ - if (module.startWithParent){ - module.start(options); - } - }); - - } - - } -}); - - - window.Marionette = Marionette; - return Marionette; -})(this, Backbone, _); diff --git a/src/UI/JsLibraries/backbone.modelbinder.js b/src/UI/JsLibraries/backbone.modelbinder.js deleted file mode 100644 index 8dde7b055..000000000 --- a/src/UI/JsLibraries/backbone.modelbinder.js +++ /dev/null @@ -1,576 +0,0 @@ -// Backbone.ModelBinder v1.0.2 -// (c) 2013 Bart Wood -// Distributed Under MIT License - -(function (factory) { - if (typeof define === 'function' && define.amd) { - // AMD. Register as an anonymous module. - define(['underscore', 'jquery', 'backbone'], factory); - } else { - // Browser globals - factory(_, $, Backbone); - } -}(function(_, $, Backbone){ - - if(!Backbone){ - throw 'Please include Backbone.js before Backbone.ModelBinder.js'; - } - - Backbone.ModelBinder = function(){ - _.bindAll.apply(_, [this].concat(_.functions(this))); - }; - - // Static setter for class level options - Backbone.ModelBinder.SetOptions = function(options){ - Backbone.ModelBinder.options = options; - }; - - // Current version of the library. - Backbone.ModelBinder.VERSION = '1.0.2'; - Backbone.ModelBinder.Constants = {}; - Backbone.ModelBinder.Constants.ModelToView = 'ModelToView'; - Backbone.ModelBinder.Constants.ViewToModel = 'ViewToModel'; - - _.extend(Backbone.ModelBinder.prototype, { - - bind:function (model, rootEl, attributeBindings, options) { - this.unbind(); - - this._model = model; - this._rootEl = rootEl; - this._setOptions(options); - - if (!this._model) this._throwException('model must be specified'); - if (!this._rootEl) this._throwException('rootEl must be specified'); - - if(attributeBindings){ - // Create a deep clone of the attribute bindings - this._attributeBindings = $.extend(true, {}, attributeBindings); - - this._initializeAttributeBindings(); - this._initializeElBindings(); - } - else { - this._initializeDefaultBindings(); - } - - this._bindModelToView(); - this._bindViewToModel(); - }, - - bindCustomTriggers: function (model, rootEl, triggers, attributeBindings, modelSetOptions) { - this._triggers = triggers; - this.bind(model, rootEl, attributeBindings, modelSetOptions) - }, - - unbind:function () { - this._unbindModelToView(); - this._unbindViewToModel(); - - if(this._attributeBindings){ - delete this._attributeBindings; - this._attributeBindings = undefined; - } - }, - - _setOptions: function(options){ - this._options = _.extend({ - boundAttribute: 'name' - }, Backbone.ModelBinder.options, options); - - // initialize default options - if(!this._options['modelSetOptions']){ - this._options['modelSetOptions'] = {}; - } - this._options['modelSetOptions'].changeSource = 'ModelBinder'; - - if(!this._options['changeTriggers']){ - this._options['changeTriggers'] = {'': 'change', '[contenteditable]': 'blur'}; - } - - if(!this._options['initialCopyDirection']){ - this._options['initialCopyDirection'] = Backbone.ModelBinder.Constants.ModelToView; - } - }, - - // Converts the input bindings, which might just be empty or strings, to binding objects - _initializeAttributeBindings:function () { - var attributeBindingKey, inputBinding, attributeBinding, elementBindingCount, elementBinding; - - for (attributeBindingKey in this._attributeBindings) { - inputBinding = this._attributeBindings[attributeBindingKey]; - - if (_.isString(inputBinding)) { - attributeBinding = {elementBindings: [{selector: inputBinding}]}; - } - else if (_.isArray(inputBinding)) { - attributeBinding = {elementBindings: inputBinding}; - } - else if(_.isObject(inputBinding)){ - attributeBinding = {elementBindings: [inputBinding]}; - } - else { - this._throwException('Unsupported type passed to Model Binder ' + attributeBinding); - } - - // Add a linkage from the element binding back to the attribute binding - for(elementBindingCount = 0; elementBindingCount < attributeBinding.elementBindings.length; elementBindingCount++){ - elementBinding = attributeBinding.elementBindings[elementBindingCount]; - elementBinding.attributeBinding = attributeBinding; - } - - attributeBinding.attributeName = attributeBindingKey; - this._attributeBindings[attributeBindingKey] = attributeBinding; - } - }, - - // If the bindings are not specified, the default binding is performed on the specified attribute, name by default - _initializeDefaultBindings: function(){ - var elCount, elsWithAttribute, matchedEl, name, attributeBinding; - - this._attributeBindings = {}; - elsWithAttribute = $('[' + this._options['boundAttribute'] + ']', this._rootEl); - - for(elCount = 0; elCount < elsWithAttribute.length; elCount++){ - matchedEl = elsWithAttribute[elCount]; - name = $(matchedEl).attr(this._options['boundAttribute']); - - // For elements like radio buttons we only want a single attribute binding with possibly multiple element bindings - if(!this._attributeBindings[name]){ - attributeBinding = {attributeName: name}; - attributeBinding.elementBindings = [{attributeBinding: attributeBinding, boundEls: [matchedEl]}]; - this._attributeBindings[name] = attributeBinding; - } - else{ - this._attributeBindings[name].elementBindings.push({attributeBinding: this._attributeBindings[name], boundEls: [matchedEl]}); - } - } - }, - - _initializeElBindings:function () { - var bindingKey, attributeBinding, bindingCount, elementBinding, foundEls, elCount, el; - for (bindingKey in this._attributeBindings) { - attributeBinding = this._attributeBindings[bindingKey]; - - for (bindingCount = 0; bindingCount < attributeBinding.elementBindings.length; bindingCount++) { - elementBinding = attributeBinding.elementBindings[bindingCount]; - if (elementBinding.selector === '') { - foundEls = $(this._rootEl); - } - else { - foundEls = $(elementBinding.selector, this._rootEl); - } - - if (foundEls.length === 0) { - this._throwException('Bad binding found. No elements returned for binding selector ' + elementBinding.selector); - } - else { - elementBinding.boundEls = []; - for (elCount = 0; elCount < foundEls.length; elCount++) { - el = foundEls[elCount]; - elementBinding.boundEls.push(el); - } - } - } - } - }, - - _bindModelToView: function () { - this._model.on('change', this._onModelChange, this); - - if(this._options['initialCopyDirection'] === Backbone.ModelBinder.Constants.ModelToView){ - this.copyModelAttributesToView(); - } - }, - - // attributesToCopy is an optional parameter - if empty, all attributes - // that are bound will be copied. Otherwise, only attributeBindings specified - // in the attributesToCopy are copied. - copyModelAttributesToView: function(attributesToCopy){ - var attributeName, attributeBinding; - - for (attributeName in this._attributeBindings) { - if(attributesToCopy === undefined || _.indexOf(attributesToCopy, attributeName) !== -1){ - attributeBinding = this._attributeBindings[attributeName]; - this._copyModelToView(attributeBinding); - } - } - }, - - copyViewValuesToModel: function(){ - var bindingKey, attributeBinding, bindingCount, elementBinding, elCount, el; - for (bindingKey in this._attributeBindings) { - attributeBinding = this._attributeBindings[bindingKey]; - - for (bindingCount = 0; bindingCount < attributeBinding.elementBindings.length; bindingCount++) { - elementBinding = attributeBinding.elementBindings[bindingCount]; - - if(this._isBindingUserEditable(elementBinding)){ - if(this._isBindingRadioGroup(elementBinding)){ - el = this._getRadioButtonGroupCheckedEl(elementBinding); - if(el){ - this._copyViewToModel(elementBinding, el); - } - } - else { - for(elCount = 0; elCount < elementBinding.boundEls.length; elCount++){ - el = $(elementBinding.boundEls[elCount]); - if(this._isElUserEditable(el)){ - this._copyViewToModel(elementBinding, el); - } - } - } - } - } - } - }, - - _unbindModelToView: function(){ - if(this._model){ - this._model.off('change', this._onModelChange); - this._model = undefined; - } - }, - - _bindViewToModel: function () { - _.each(this._options['changeTriggers'], function (event, selector) { - $(this._rootEl).delegate(selector, event, this._onElChanged); - }, this); - - if(this._options['initialCopyDirection'] === Backbone.ModelBinder.Constants.ViewToModel){ - this.copyViewValuesToModel(); - } - }, - - _unbindViewToModel: function () { - if(this._options && this._options['changeTriggers']){ - _.each(this._options['changeTriggers'], function (event, selector) { - $(this._rootEl).undelegate(selector, event, this._onElChanged); - }, this); - } - }, - - _onElChanged:function (event) { - var el, elBindings, elBindingCount, elBinding; - - el = $(event.target)[0]; - elBindings = this._getElBindings(el); - - for(elBindingCount = 0; elBindingCount < elBindings.length; elBindingCount++){ - elBinding = elBindings[elBindingCount]; - if (this._isBindingUserEditable(elBinding)) { - this._copyViewToModel(elBinding, el); - } - } - }, - - _isBindingUserEditable: function(elBinding){ - return elBinding.elAttribute === undefined || - elBinding.elAttribute === 'text' || - elBinding.elAttribute === 'html'; - }, - - _isElUserEditable: function(el){ - var isContentEditable = el.attr('contenteditable'); - return isContentEditable || el.is('input') || el.is('select') || el.is('textarea'); - }, - - _isBindingRadioGroup: function(elBinding){ - var elCount, el; - var isAllRadioButtons = elBinding.boundEls.length > 0; - for(elCount = 0; elCount < elBinding.boundEls.length; elCount++){ - el = $(elBinding.boundEls[elCount]); - if(el.attr('type') !== 'radio'){ - isAllRadioButtons = false; - break; - } - } - - return isAllRadioButtons; - }, - - _getRadioButtonGroupCheckedEl: function(elBinding){ - var elCount, el; - for(elCount = 0; elCount < elBinding.boundEls.length; elCount++){ - el = $(elBinding.boundEls[elCount]); - if(el.attr('type') === 'radio' && el.attr('checked')){ - return el; - } - } - - return undefined; - }, - - _getElBindings:function (findEl) { - var attributeName, attributeBinding, elementBindingCount, elementBinding, boundElCount, boundEl; - var elBindings = []; - - for (attributeName in this._attributeBindings) { - attributeBinding = this._attributeBindings[attributeName]; - - for (elementBindingCount = 0; elementBindingCount < attributeBinding.elementBindings.length; elementBindingCount++) { - elementBinding = attributeBinding.elementBindings[elementBindingCount]; - - for (boundElCount = 0; boundElCount < elementBinding.boundEls.length; boundElCount++) { - boundEl = elementBinding.boundEls[boundElCount]; - - if (boundEl === findEl) { - elBindings.push(elementBinding); - } - } - } - } - - return elBindings; - }, - - _onModelChange:function () { - var changedAttribute, attributeBinding; - - for (changedAttribute in this._model.changedAttributes()) { - attributeBinding = this._attributeBindings[changedAttribute]; - - if (attributeBinding) { - this._copyModelToView(attributeBinding); - } - } - }, - - _copyModelToView:function (attributeBinding) { - var elementBindingCount, elementBinding, boundElCount, boundEl, value, convertedValue; - - value = this._model.get(attributeBinding.attributeName); - - for (elementBindingCount = 0; elementBindingCount < attributeBinding.elementBindings.length; elementBindingCount++) { - elementBinding = attributeBinding.elementBindings[elementBindingCount]; - - for (boundElCount = 0; boundElCount < elementBinding.boundEls.length; boundElCount++) { - boundEl = elementBinding.boundEls[boundElCount]; - - if(!boundEl._isSetting){ - convertedValue = this._getConvertedValue(Backbone.ModelBinder.Constants.ModelToView, elementBinding, value); - this._setEl($(boundEl), elementBinding, convertedValue); - } - } - } - }, - - _setEl: function (el, elementBinding, convertedValue) { - if (elementBinding.elAttribute) { - this._setElAttribute(el, elementBinding, convertedValue); - } - else { - this._setElValue(el, convertedValue); - } - }, - - _setElAttribute:function (el, elementBinding, convertedValue) { - switch (elementBinding.elAttribute) { - case 'html': - el.html(convertedValue); - break; - case 'text': - el.text(convertedValue); - break; - case 'enabled': - el.prop('disabled', !convertedValue); - break; - case 'displayed': - el[convertedValue ? 'show' : 'hide'](); - break; - case 'hidden': - el[convertedValue ? 'hide' : 'show'](); - break; - case 'css': - el.css(elementBinding.cssAttribute, convertedValue); - break; - case 'class': - var previousValue = this._model.previous(elementBinding.attributeBinding.attributeName); - var currentValue = this._model.get(elementBinding.attributeBinding.attributeName); - // is current value is now defined then remove the class the may have been set for the undefined value - if(!_.isUndefined(previousValue) || !_.isUndefined(currentValue)){ - previousValue = this._getConvertedValue(Backbone.ModelBinder.Constants.ModelToView, elementBinding, previousValue); - el.removeClass(previousValue); - } - - if(convertedValue){ - el.addClass(convertedValue); - } - break; - default: - el.attr(elementBinding.elAttribute, convertedValue); - } - }, - - _setElValue:function (el, convertedValue) { - if(el.attr('type')){ - switch (el.attr('type')) { - case 'radio': - if (el.val() === convertedValue) { - // must defer the change trigger or the change will actually fire with the old value - el.prop('checked') || _.defer(function() { el.trigger('change'); }); - el.prop('checked', true); - } - else { - // must defer the change trigger or the change will actually fire with the old value - el.prop('checked', false); - } - break; - case 'checkbox': - // must defer the change trigger or the change will actually fire with the old value - el.prop('checked') === !!convertedValue || _.defer(function() { el.trigger('change') }); - el.prop('checked', !!convertedValue); - break; - case 'file': - break; - default: - el.val(convertedValue); - } - } - else if(el.is('input') || el.is('select') || el.is('textarea')){ - el.val(convertedValue || (convertedValue === 0 ? '0' : '')); - } - else { - el.text(convertedValue || (convertedValue === 0 ? '0' : '')); - } - }, - - _copyViewToModel: function (elementBinding, el) { - var result, value, convertedValue; - - if (!el._isSetting) { - - el._isSetting = true; - result = this._setModel(elementBinding, $(el)); - el._isSetting = false; - - if(result && elementBinding.converter){ - value = this._model.get(elementBinding.attributeBinding.attributeName); - convertedValue = this._getConvertedValue(Backbone.ModelBinder.Constants.ModelToView, elementBinding, value); - this._setEl($(el), elementBinding, convertedValue); - } - } - }, - - _getElValue: function(elementBinding, el){ - switch (el.attr('type')) { - case 'checkbox': - return el.prop('checked') ? true : false; - default: - if(el.attr('contenteditable') !== undefined){ - return el.html(); - } - else { - return el.val(); - } - } - }, - - _setModel: function (elementBinding, el) { - var data = {}; - var elVal = this._getElValue(elementBinding, el); - elVal = this._getConvertedValue(Backbone.ModelBinder.Constants.ViewToModel, elementBinding, elVal); - data[elementBinding.attributeBinding.attributeName] = elVal; - return this._model.set(data, this._options['modelSetOptions']); - }, - - _getConvertedValue: function (direction, elementBinding, value) { - if (elementBinding.converter) { - value = elementBinding.converter(direction, value, elementBinding.attributeBinding.attributeName, this._model, elementBinding.boundEls); - } - - return value; - }, - - _throwException: function(message){ - if(this._options.suppressThrows){ - if(console && console.error){ - console.error(message); - } - } - else { - throw message; - } - } - }); - - Backbone.ModelBinder.CollectionConverter = function(collection){ - this._collection = collection; - - if(!this._collection){ - throw 'Collection must be defined'; - } - _.bindAll(this, 'convert'); - }; - - _.extend(Backbone.ModelBinder.CollectionConverter.prototype, { - convert: function(direction, value){ - if (direction === Backbone.ModelBinder.Constants.ModelToView) { - return value ? value.id : undefined; - } - else { - return this._collection.get(value); - } - } - }); - - // A static helper function to create a default set of bindings that you can customize before calling the bind() function - // rootEl - where to find all of the bound elements - // attributeType - probably 'name' or 'id' in most cases - // converter(optional) - the default converter you want applied to all your bindings - // elAttribute(optional) - the default elAttribute you want applied to all your bindings - Backbone.ModelBinder.createDefaultBindings = function(rootEl, attributeType, converter, elAttribute){ - var foundEls, elCount, foundEl, attributeName; - var bindings = {}; - - foundEls = $('[' + attributeType + ']', rootEl); - - for(elCount = 0; elCount < foundEls.length; elCount++){ - foundEl = foundEls[elCount]; - attributeName = $(foundEl).attr(attributeType); - - if(!bindings[attributeName]){ - var attributeBinding = {selector: '[' + attributeType + '="' + attributeName + '"]'}; - bindings[attributeName] = attributeBinding; - - if(converter){ - bindings[attributeName].converter = converter; - } - - if(elAttribute){ - bindings[attributeName].elAttribute = elAttribute; - } - } - } - - return bindings; - }; - - // Helps you to combine 2 sets of bindings - Backbone.ModelBinder.combineBindings = function(destination, source){ - _.each(source, function(value, key){ - var elementBinding = {selector: value.selector}; - - if(value.converter){ - elementBinding.converter = value.converter; - } - - if(value.elAttribute){ - elementBinding.elAttribute = value.elAttribute; - } - - if(!destination[key]){ - destination[key] = elementBinding; - } - else { - destination[key] = [destination[key], elementBinding]; - } - }); - - return destination; - }; - - - return Backbone.ModelBinder; - -})); \ No newline at end of file diff --git a/src/UI/JsLibraries/backbone.pageable.js b/src/UI/JsLibraries/backbone.pageable.js deleted file mode 100644 index f6cdbcacd..000000000 --- a/src/UI/JsLibraries/backbone.pageable.js +++ /dev/null @@ -1,1345 +0,0 @@ -/* - backbone-pageable 1.4.1 - http://github.com/wyuenho/backbone-pageable - - Copyright (c) 2013 Jimmy Yuen Ho Wong - Licensed under the MIT @license. -*/ - -(function (factory) { - - // CommonJS - if (typeof exports == "object") { - module.exports = factory(require("underscore"), require("backbone")); - } - // AMD - else if (typeof define == "function" && define.amd) { - define(["underscore", "backbone"], factory); - } - // Browser - else if (typeof _ !== "undefined" && typeof Backbone !== "undefined") { - var oldPageableCollection = Backbone.PageableCollection; - var PageableCollection = factory(_, Backbone); - - /** - __BROWSER ONLY__ - - If you already have an object named `PageableCollection` attached to the - `Backbone` module, you can use this to return a local reference to this - Backbone.PageableCollection class and reset the name - Backbone.PageableCollection to its previous definition. - - // The left hand side gives you a reference to this - // Backbone.PageableCollection implementation, the right hand side - // resets Backbone.PageableCollection to your other - // Backbone.PageableCollection. - var PageableCollection = Backbone.PageableCollection.noConflict(); - - @static - @member Backbone.PageableCollection - @return {Backbone.PageableCollection} - */ - Backbone.PageableCollection.noConflict = function () { - Backbone.PageableCollection = oldPageableCollection; - return PageableCollection; - }; - } - -}(function (_, Backbone) { - - "use strict"; - - var _extend = _.extend; - var _omit = _.omit; - var _clone = _.clone; - var _each = _.each; - var _pick = _.pick; - var _contains = _.contains; - var _isEmpty = _.isEmpty; - var _pairs = _.pairs; - var _invert = _.invert; - var _isArray = _.isArray; - var _isFunction = _.isFunction; - var _isObject = _.isObject; - var _keys = _.keys; - var _isUndefined = _.isUndefined; - var _result = _.result; - var ceil = Math.ceil; - var floor = Math.floor; - var max = Math.max; - - var BBColProto = Backbone.Collection.prototype; - - function finiteInt (val, name) { - if (!_.isNumber(val) || _.isNaN(val) || !_.isFinite(val) || ~~val !== val) { - throw new TypeError("`" + name + "` must be a finite integer"); - } - return val; - } - - function queryStringToParams (qs) { - var kvp, k, v, ls, params = {}, decode = decodeURIComponent; - var kvps = qs.split('&'); - for (var i = 0, l = kvps.length; i < l; i++) { - var param = kvps[i]; - kvp = param.split('='), k = kvp[0], v = kvp[1] || true; - k = decode(k), v = decode(v), ls = params[k]; - if (_isArray(ls)) ls.push(v); - else if (ls) params[k] = [ls, v]; - else params[k] = v; - } - return params; - } - - // hack to make sure the whatever event handlers for this event is run - // before func is, and the event handlers that func will trigger. - function runOnceAtLastHandler (col, event, func) { - var eventHandlers = col._events[event]; - if (eventHandlers && eventHandlers.length) { - var lastHandler = eventHandlers[eventHandlers.length - 1]; - var oldCallback = lastHandler.callback; - lastHandler.callback = function () { - try { - oldCallback.apply(this, arguments); - func(); - } - catch (e) { - throw e; - } - finally { - lastHandler.callback = oldCallback; - } - }; - } - else func(); - } - - var PARAM_TRIM_RE = /[\s'"]/g; - var URL_TRIM_RE = /[<>\s'"]/g; - - /** - Drop-in replacement for Backbone.Collection. Supports server-side and - client-side pagination and sorting. Client-side mode also support fully - multi-directional synchronization of changes between pages. - - @class Backbone.PageableCollection - @extends Backbone.Collection - */ - var PageableCollection = Backbone.PageableCollection = Backbone.Collection.extend({ - - /** - The container object to store all pagination states. - - You can override the default state by extending this class or specifying - them in an `options` hash to the constructor. - - @property {Object} state - - @property {0|1} [state.firstPage=1] The first page index. Set to 0 if - your server API uses 0-based indices. You should only override this value - during extension, initialization or reset by the server after - fetching. This value should be read only at other times. - - @property {number} [state.lastPage=null] The last page index. This value - is __read only__ and it's calculated based on whether `firstPage` is 0 or - 1, during bootstrapping, fetching and resetting. Please don't change this - value under any circumstances. - - @property {number} [state.currentPage=null] The current page index. You - should only override this value during extension, initialization or reset - by the server after fetching. This value should be read only at other - times. Can be a 0-based or 1-based index, depending on whether - `firstPage` is 0 or 1. If left as default, it will be set to `firstPage` - on initialization. - - @property {number} [state.pageSize=25] How many records to show per - page. This value is __read only__ after initialization, if you want to - change the page size after initialization, you must call #setPageSize. - - @property {number} [state.totalPages=null] How many pages there are. This - value is __read only__ and it is calculated from `totalRecords`. - - @property {number} [state.totalRecords=null] How many records there - are. This value is __required__ under server mode. This value is optional - for client mode as the number will be the same as the number of models - during bootstrapping and during fetching, either supplied by the server - in the metadata, or calculated from the size of the response. - - @property {string} [state.sortKey=null] The model attribute to use for - sorting. - - @property {-1|0|1} [state.order=-1] The order to use for sorting. Specify - -1 for ascending order or 1 for descending order. If 0, no client side - sorting will be done and the order query parameter will not be sent to - the server during a fetch. - */ - state: { - firstPage: 1, - lastPage: null, - currentPage: null, - pageSize: 25, - totalPages: null, - totalRecords: null, - sortKey: null, - order: -1 - }, - - /** - @property {"server"|"client"|"infinite"} [mode="server"] The mode of - operations for this collection. `"server"` paginates on the server-side, - `"client"` paginates on the client-side and `"infinite"` paginates on the - server-side for APIs that do not support `totalRecords`. - */ - mode: "server", - - /** - A translation map to convert Backbone.PageableCollection state attributes - to the query parameters accepted by your server API. - - You can override the default state by extending this class or specifying - them in `options.queryParams` object hash to the constructor. - - @property {Object} queryParams - @property {string} [queryParams.currentPage="page"] - @property {string} [queryParams.pageSize="per_page"] - @property {string} [queryParams.totalPages="total_pages"] - @property {string} [queryParams.totalRecords="total_entries"] - @property {string} [queryParams.sortKey="sort_by"] - @property {string} [queryParams.order="order"] - @property {string} [queryParams.directions={"-1": "asc", "1": "desc"}] A - map for translating a Backbone.PageableCollection#state.order constant to - the ones your server API accepts. - */ - queryParams: { - currentPage: "page", - pageSize: "per_page", - totalPages: "total_pages", - totalRecords: "total_entries", - sortKey: "sort_by", - order: "order", - directions: { - "-1": "asc", - "1": "desc" - } - }, - - /** - __CLIENT MODE ONLY__ - - This collection is the internal storage for the bootstrapped or fetched - models. You can use this if you want to operate on all the pages. - - @property {Backbone.Collection} fullCollection - */ - - /** - Given a list of models or model attributues, bootstraps the full - collection in client mode or infinite mode, or just the page you want in - server mode. - - If you want to initialize a collection to a different state than the - default, you can specify them in `options.state`. Any state parameters - supplied will be merged with the default. If you want to change the - default mapping from #state keys to your server API's query parameter - names, you can specifiy an object hash in `option.queryParams`. Likewise, - any mapping provided will be merged with the default. Lastly, all - Backbone.Collection constructor options are also accepted. - - See: - - - Backbone.PageableCollection#state - - Backbone.PageableCollection#queryParams - - [Backbone.Collection#initialize](http://backbonejs.org/#Collection-constructor) - - @param {Array.<Object>} [models] - - @param {Object} [options] - - @param {function(*, *): number} [options.comparator] If specified, this - comparator is set to the current page under server mode, or the #fullCollection - otherwise. - - @param {boolean} [options.full] If `false` and either a - `options.comparator` or `sortKey` is defined, the comparator is attached - to the current page. Default is `true` under client or infinite mode and - the comparator will be attached to the #fullCollection. - - @param {Object} [options.state] The state attributes overriding the defaults. - - @param {string} [options.state.sortKey] The model attribute to use for - sorting. If specified instead of `options.comparator`, a comparator will - be automatically created using this value, and optionally a sorting order - specified in `options.state.order`. The comparator is then attached to - the new collection instance. - - @param {-1|1} [options.state.order] The order to use for sorting. Specify - -1 for ascending order and 1 for descending order. - - @param {Object} [options.queryParam] - */ - constructor: function (models, options) { - - BBColProto.constructor.apply(this, arguments); - - options = options || {}; - - var mode = this.mode = options.mode || this.mode || PageableProto.mode; - - var queryParams = _extend({}, PageableProto.queryParams, this.queryParams, - options.queryParams || {}); - - queryParams.directions = _extend({}, - PageableProto.queryParams.directions, - this.queryParams.directions, - queryParams.directions || {}); - - this.queryParams = queryParams; - - var state = this.state = _extend({}, PageableProto.state, this.state, - options.state || {}); - - state.currentPage = state.currentPage == null ? - state.firstPage : - state.currentPage; - - if (!_isArray(models)) models = models ? [models] : []; - - if (mode != "server" && state.totalRecords == null && !_isEmpty(models)) { - state.totalRecords = models.length; - } - - this.switchMode(mode, _extend({fetch: false, - resetState: false, - models: models}, options)); - - var comparator = options.comparator; - - if (state.sortKey && !comparator) { - this.setSorting(state.sortKey, state.order, options); - } - - if (mode != "server") { - var fullCollection = this.fullCollection; - - if (comparator && options.full) { - this.comparator = null; - fullCollection.comparator = comparator; - } - - if (options.full) fullCollection.sort(); - - // make sure the models in the current page and full collection have the - // same references - if (models && !_isEmpty(models)) { - this.reset([].slice.call(models), _extend({silent: true}, options)); - this.getPage(state.currentPage); - models.splice.apply(models, [0, models.length].concat(this.models)); - } - } - - this._initState = _clone(this.state); - }, - - /** - Makes a Backbone.Collection that contains all the pages. - - @private - @param {Array.<Object|Backbone.Model>} models - @param {Object} options Options for Backbone.Collection constructor. - @return {Backbone.Collection} - */ - _makeFullCollection: function (models, options) { - - var properties = ["url", "model", "sync", "comparator"]; - var thisProto = this.constructor.prototype; - var i, length, prop; - - var proto = {}; - for (i = 0, length = properties.length; i < length; i++) { - prop = properties[i]; - if (!_isUndefined(thisProto[prop])) { - proto[prop] = thisProto[prop]; - } - } - - var fullCollection = new (Backbone.Collection.extend(proto))(models, options); - - for (i = 0, length = properties.length; i < length; i++) { - prop = properties[i]; - if (this[prop] !== thisProto[prop]) { - fullCollection[prop] = this[prop]; - } - } - - return fullCollection; - }, - - /** - Factory method that returns a Backbone event handler that responses to - the `add`, `remove`, `reset`, and the `sort` events. The returned event - handler will synchronize the current page collection and the full - collection's models. - - @private - - @param {Backbone.PageableCollection} pageCol - @param {Backbone.Collection} fullCol - - @return {function(string, Backbone.Model, Backbone.Collection, Object)} - Collection event handler - */ - _makeCollectionEventHandler: function (pageCol, fullCol) { - - return function collectionEventHandler (event, model, collection, options) { - - var handlers = pageCol._handlers; - _each(_keys(handlers), function (event) { - var handler = handlers[event]; - pageCol.off(event, handler); - fullCol.off(event, handler); - }); - - var state = _clone(pageCol.state); - var firstPage = state.firstPage; - var currentPage = firstPage === 0 ? - state.currentPage : - state.currentPage - 1; - var pageSize = state.pageSize; - var pageStart = currentPage * pageSize, pageEnd = pageStart + pageSize; - - if (event == "add") { - var pageIndex, fullIndex, addAt, colToAdd, options = options || {}; - if (collection == fullCol) { - fullIndex = fullCol.indexOf(model); - if (fullIndex >= pageStart && fullIndex < pageEnd) { - colToAdd = pageCol; - pageIndex = addAt = fullIndex - pageStart; - } - } - else { - pageIndex = pageCol.indexOf(model); - fullIndex = pageStart + pageIndex; - colToAdd = fullCol; - var addAt = !_isUndefined(options.at) ? - options.at + pageStart : - fullIndex; - } - - ++state.totalRecords; - pageCol.state = pageCol._checkState(state); - - if (colToAdd) { - colToAdd.add(model, _extend({}, options || {}, {at: addAt})); - var modelToRemove = pageIndex >= pageSize ? - model : - !_isUndefined(options.at) && addAt < pageEnd && pageCol.length > pageSize ? - pageCol.at(pageSize) : - null; - if (modelToRemove) { - var popOptions = {onAdd: true}; - runOnceAtLastHandler(collection, event, function () { - pageCol.remove(modelToRemove, popOptions); - }); - } - } - } - - // remove the model from the other collection as well - if (event == "remove") { - if (!options.onAdd) { - // decrement totalRecords and update totalPages and lastPage - if (!--state.totalRecords) { - state.totalRecords = null; - state.totalPages = null; - } - else { - var totalPages = state.totalPages = ceil(state.totalRecords / pageSize); - state.lastPage = firstPage === 0 ? totalPages - 1 : totalPages || firstPage; - if (state.currentPage > totalPages) state.currentPage = state.lastPage; - } - pageCol.state = pageCol._checkState(state); - - var nextModel, removedIndex = options.index; - if (collection == pageCol) { - if (nextModel = fullCol.at(pageEnd)) { - runOnceAtLastHandler(pageCol, event, function () { - pageCol.push(nextModel); - }); - } - fullCol.remove(model); - } - else if (removedIndex >= pageStart && removedIndex < pageEnd) { - pageCol.remove(model); - var at = removedIndex + 1 - nextModel = fullCol.at(at) || fullCol.last(); - if (nextModel) pageCol.add(nextModel, {at: at}); - } - } - else delete options.onAdd; - } - - if (event == "reset") { - options = collection; - collection = model; - - // Reset that's not a result of getPage - if (collection == pageCol && options.from == null && - options.to == null) { - var head = fullCol.models.slice(0, pageStart); - var tail = fullCol.models.slice(pageStart + pageCol.models.length); - fullCol.reset(head.concat(pageCol.models).concat(tail), options); - } - else if (collection == fullCol) { - if (!(state.totalRecords = fullCol.models.length)) { - state.totalRecords = null; - state.totalPages = null; - } - if (pageCol.mode == "client") { - state.lastPage = state.currentPage = state.firstPage; - } - pageCol.state = pageCol._checkState(state); - pageCol.reset(fullCol.models.slice(pageStart, pageEnd), - _extend({}, options, {parse: false})); - } - } - - if (event == "sort") { - options = collection; - collection = model; - if (collection === fullCol) { - pageCol.reset(fullCol.models.slice(pageStart, pageEnd), - _extend({}, options, {parse: false})); - } - } - - _each(_keys(handlers), function (event) { - var handler = handlers[event]; - _each([pageCol, fullCol], function (col) { - col.on(event, handler); - var callbacks = col._events[event] || []; - callbacks.unshift(callbacks.pop()); - }); - }); - }; - }, - - /** - Sanity check this collection's pagination states. Only perform checks - when all the required pagination state values are defined and not null. - If `totalPages` is undefined or null, it is set to `totalRecords` / - `pageSize`. `lastPage` is set according to whether `firstPage` is 0 or 1 - when no error occurs. - - @private - - @throws {TypeError} If `totalRecords`, `pageSize`, `currentPage` or - `firstPage` is not a finite integer. - - @throws {RangeError} If `pageSize`, `currentPage` or `firstPage` is out - of bounds. - - @return {Object} Returns the `state` object if no error was found. - */ - _checkState: function (state) { - - var mode = this.mode; - var links = this.links; - var totalRecords = state.totalRecords; - var pageSize = state.pageSize; - var currentPage = state.currentPage; - var firstPage = state.firstPage; - var totalPages = state.totalPages; - - if (totalRecords != null && pageSize != null && currentPage != null && - firstPage != null && (mode == "infinite" ? links : true)) { - - totalRecords = finiteInt(totalRecords, "totalRecords"); - pageSize = finiteInt(pageSize, "pageSize"); - currentPage = finiteInt(currentPage, "currentPage"); - firstPage = finiteInt(firstPage, "firstPage"); - - if (pageSize < 1) { - throw new RangeError("`pageSize` must be >= 1"); - } - - totalPages = state.totalPages = ceil(totalRecords / pageSize); - - if (firstPage < 0 || firstPage > 1) { - throw new RangeError("`firstPage must be 0 or 1`"); - } - - state.lastPage = firstPage === 0 ? max(0, totalPages - 1) : totalPages || firstPage; - - if (mode == "infinite") { - if (!links[currentPage + '']) { - throw new RangeError("No link found for page " + currentPage); - } - } - else if (currentPage < firstPage || - (totalPages > 0 && - (firstPage ? currentPage > totalPages : currentPage >= totalPages))) { - var op = firstPage ? ">=" : ">"; - - throw new RangeError("`currentPage` must be firstPage <= currentPage " + - (firstPage ? ">" : ">=") + - " totalPages if " + firstPage + "-based. Got " + - currentPage + '.'); - } - } - - return state; - }, - - /** - Change the page size of this collection. - - Under most if not all circumstances, you should call this method to - change the page size of a pageable collection because it will keep the - pagination state sane. By default, the method will recalculate the - current page number to one that will retain the current page's models - when increasing the page size. When decreasing the page size, this method - will retain the last models to the current page that will fit into the - smaller page size. - - If `options.first` is true, changing the page size will also reset the - current page back to the first page instead of trying to be smart. - - For server mode operations, changing the page size will trigger a #fetch - and subsequently a `reset` event. - - For client mode operations, changing the page size will `reset` the - current page by recalculating the current page boundary on the client - side. - - If `options.fetch` is true, a fetch can be forced if the collection is in - client mode. - - @param {number} pageSize The new page size to set to #state. - @param {Object} [options] {@link #fetch} options. - @param {boolean} [options.first=false] Reset the current page number to - the first page if `true`. - @param {boolean} [options.fetch] If `true`, force a fetch in client mode. - - @throws {TypeError} If `pageSize` is not a finite integer. - @throws {RangeError} If `pageSize` is less than 1. - - @chainable - @return {XMLHttpRequest|Backbone.PageableCollection} The XMLHttpRequest - from fetch or this. - */ - setPageSize: function (pageSize, options) { - pageSize = finiteInt(pageSize, "pageSize"); - - options = options || {first: false}; - - var state = this.state; - var totalPages = ceil(state.totalRecords / pageSize); - var currentPage = totalPages ? - max(state.firstPage, - floor(totalPages * - (state.firstPage ? - state.currentPage : - state.currentPage + 1) / - state.totalPages)) : - state.firstPage; - - state = this.state = this._checkState(_extend({}, state, { - pageSize: pageSize, - currentPage: options.first ? state.firstPage : currentPage, - totalPages: totalPages - })); - - return this.getPage(state.currentPage, _omit(options, ["first"])); - }, - - /** - Switching between client, server and infinite mode. - - If switching from client to server mode, the #fullCollection is emptied - first and then deleted and a fetch is immediately issued for the current - page from the server. Pass `false` to `options.fetch` to skip fetching. - - If switching to infinite mode, and if `options.models` is given for an - array of models, #links will be populated with a URL per page, using the - default URL for this collection. - - If switching from server to client mode, all of the pages are immediately - refetched. If you have too many pages, you can pass `false` to - `options.fetch` to skip fetching. - - If switching to any mode from infinite mode, the #links will be deleted. - - @param {"server"|"client"|"infinite"} [mode] The mode to switch to. - - @param {Object} [options] - - @param {boolean} [options.fetch=true] If `false`, no fetching is done. - - @param {boolean} [options.resetState=true] If 'false', the state is not - reset, but checked for sanity instead. - - @chainable - @return {XMLHttpRequest|Backbone.PageableCollection} The XMLHttpRequest - from fetch or this if `options.fetch` is `false`. - */ - switchMode: function (mode, options) { - - if (!_contains(["server", "client", "infinite"], mode)) { - throw new TypeError('`mode` must be one of "server", "client" or "infinite"'); - } - - options = options || {fetch: true, resetState: true}; - - var state = this.state = options.resetState ? - _clone(this._initState) : - this._checkState(_extend({}, this.state)); - - this.mode = mode; - - var self = this; - var fullCollection = this.fullCollection; - var handlers = this._handlers = this._handlers || {}, handler; - if (mode != "server" && !fullCollection) { - fullCollection = this._makeFullCollection(options.models || [], options); - fullCollection.pageableCollection = this; - this.fullCollection = fullCollection; - var allHandler = this._makeCollectionEventHandler(this, fullCollection); - _each(["add", "remove", "reset", "sort"], function (event) { - handlers[event] = handler = _.bind(allHandler, {}, event); - self.on(event, handler); - fullCollection.on(event, handler); - }); - fullCollection.comparator = this._fullComparator; - } - else if (mode == "server" && fullCollection) { - _each(_keys(handlers), function (event) { - handler = handlers[event]; - self.off(event, handler); - fullCollection.off(event, handler); - }); - delete this._handlers; - this._fullComparator = fullCollection.comparator; - delete this.fullCollection; - } - - if (mode == "infinite") { - var links = this.links = {}; - var firstPage = state.firstPage; - var totalPages = ceil(state.totalRecords / state.pageSize); - var lastPage = firstPage === 0 ? max(0, totalPages - 1) : totalPages || firstPage; - for (var i = state.firstPage; i <= lastPage; i++) { - links[i] = this.url; - } - } - else if (this.links) delete this.links; - - return options.fetch ? - this.fetch(_omit(options, "fetch", "resetState")) : - this; - }, - - /** - @return {boolean} `true` if this collection can page backward, `false` - otherwise. - */ - hasPrevious: function () { - var state = this.state; - var currentPage = state.currentPage; - if (this.mode != "infinite") return currentPage > state.firstPage; - return !!this.links[currentPage - 1]; - }, - - /** - @return {boolean} `true` if this collection can page forward, `false` - otherwise. - */ - hasNext: function () { - var state = this.state; - var currentPage = this.state.currentPage; - if (this.mode != "infinite") return currentPage < state.lastPage; - return !!this.links[currentPage + 1]; - }, - - /** - Fetch the first page in server mode, or reset the current page of this - collection to the first page in client or infinite mode. - - @param {Object} options {@link #getPage} options. - - @chainable - @return {XMLHttpRequest|Backbone.PageableCollection} The XMLHttpRequest - from fetch or this. - */ - getFirstPage: function (options) { - return this.getPage("first", options); - }, - - /** - Fetch the previous page in server mode, or reset the current page of this - collection to the previous page in client or infinite mode. - - @param {Object} options {@link #getPage} options. - - @chainable - @return {XMLHttpRequest|Backbone.PageableCollection} The XMLHttpRequest - from fetch or this. - */ - getPreviousPage: function (options) { - return this.getPage("prev", options); - }, - - /** - Fetch the next page in server mode, or reset the current page of this - collection to the next page in client mode. - - @param {Object} options {@link #getPage} options. - - @chainable - @return {XMLHttpRequest|Backbone.PageableCollection} The XMLHttpRequest - from fetch or this. - */ - getNextPage: function (options) { - return this.getPage("next", options); - }, - - /** - Fetch the last page in server mode, or reset the current page of this - collection to the last page in client mode. - - @param {Object} options {@link #getPage} options. - - @chainable - @return {XMLHttpRequest|Backbone.PageableCollection} The XMLHttpRequest - from fetch or this. - */ - getLastPage: function (options) { - return this.getPage("last", options); - }, - - /** - Given a page index, set #state.currentPage to that index. If this - collection is in server mode, fetch the page using the updated state, - otherwise, reset the current page of this collection to the page - specified by `index` in client mode. If `options.fetch` is true, a fetch - can be forced in client mode before resetting the current page. Under - infinite mode, if the index is less than the current page, a reset is - done as in client mode. If the index is greater than the current page - number, a fetch is made with the results **appended** to #fullCollection. - The current page will then be reset after fetching. - - @param {number|string} index The page index to go to, or the page name to - look up from #links in infinite mode. - @param {Object} [options] {@link #fetch} options or - [reset](http://backbonejs.org/#Collection-reset) options for client mode - when `options.fetch` is `false`. - @param {boolean} [options.fetch=false] If true, force a {@link #fetch} in - client mode. - - @throws {TypeError} If `index` is not a finite integer under server or - client mode, or does not yield a URL from #links under infinite mode. - - @throws {RangeError} If `index` is out of bounds. - - @chainable - @return {XMLHttpRequest|Backbone.PageableCollection} The XMLHttpRequest - from fetch or this. - */ - getPage: function (index, options) { - - var mode = this.mode, fullCollection = this.fullCollection; - - options = options || {fetch: false}; - - var state = this.state, - firstPage = state.firstPage, - currentPage = state.currentPage, - lastPage = state.lastPage, - pageSize = state.pageSize; - - var pageNum = index; - switch (index) { - case "first": pageNum = firstPage; break; - case "prev": pageNum = currentPage - 1; break; - case "next": pageNum = currentPage + 1; break; - case "last": pageNum = lastPage; break; - default: pageNum = finiteInt(index, "index"); - } - - this.state = this._checkState(_extend({}, state, {currentPage: pageNum})); - - options.from = currentPage, options.to = pageNum; - - var pageStart = (firstPage === 0 ? pageNum : pageNum - 1) * pageSize; - var pageModels = fullCollection && fullCollection.length ? - fullCollection.models.slice(pageStart, pageStart + pageSize) : - []; - if ((mode == "client" || (mode == "infinite" && !_isEmpty(pageModels))) && - !options.fetch) { - this.reset(pageModels, _omit(options, "fetch")); - return this; - } - - if (mode == "infinite") options.url = this.links[pageNum]; - - return this.fetch(_omit(options, "fetch")); - }, - - /** - Fetch the page for the provided item offset in server mode, or reset the current page of this - collection to the page for the provided item offset in client mode. - - @param {Object} options {@link #getPage} options. - - @chainable - @return {XMLHttpRequest|Backbone.PageableCollection} The XMLHttpRequest - from fetch or this. - */ - getPageByOffset: function (offset, options) { - if (offset < 0) { - throw new RangeError("`offset must be > 0`"); - } - offset = finiteInt(offset); - - var page = floor(offset / this.state.pageSize); - if (this.state.firstPage !== 0) page++; - if (page > this.state.lastPage) page = this.state.lastPage; - return this.getPage(page, options); - }, - - /** - Overidden to make `getPage` compatible with Zepto. - - @param {string} method - @param {Backbone.Model|Backbone.Collection} model - @param {Object} [options] - - @return {XMLHttpRequest} - */ - sync: function (method, model, options) { - var self = this; - if (self.mode == "infinite") { - var success = options.success; - var currentPage = self.state.currentPage; - options.success = function (resp, status, xhr) { - var links = self.links; - var newLinks = self.parseLinks(resp, _extend({xhr: xhr}, options)); - if (newLinks.first) links[self.state.firstPage] = newLinks.first; - if (newLinks.prev) links[currentPage - 1] = newLinks.prev; - if (newLinks.next) links[currentPage + 1] = newLinks.next; - if (success) success(resp, status, xhr); - }; - } - - return (BBColProto.sync || Backbone.sync).call(self, method, model, options); - }, - - /** - Parse pagination links from the server response. Only valid under - infinite mode. - - Given a response body and a XMLHttpRequest object, extract pagination - links from them for infinite paging. - - This default implementation parses the RFC 5988 `Link` header and extract - 3 links from it - `first`, `prev`, `next`. If a `previous` link is found, - it will be found in the `prev` key in the returned object hash. Any - subclasses overriding this method __must__ return an object hash having - only the keys above. If `first` is missing, the collection's default URL - is assumed to be the `first` URL. If `prev` or `next` is missing, it is - assumed to be `null`. An empty object hash must be returned if there are - no links found. If either the response or the header contains information - pertaining to the total number of records on the server, #state.totalRecords - must be set to that number. The default implementation uses the `last` - link from the header to calculate it. - - @param {*} resp The deserialized response body. - @param {Object} [options] - @param {XMLHttpRequest} [options.xhr] The XMLHttpRequest object for this - response. - @return {Object} - */ - parseLinks: function (resp, options) { - var links = {}; - var linkHeader = options.xhr.getResponseHeader("Link"); - if (linkHeader) { - var relations = ["first", "prev", "previous", "next", "last"]; - _each(linkHeader.split(","), function (linkValue) { - var linkParts = linkValue.split(";"); - var url = linkParts[0].replace(URL_TRIM_RE, ''); - var params = linkParts.slice(1); - _each(params, function (param) { - var paramParts = param.split("="); - var key = paramParts[0].replace(PARAM_TRIM_RE, ''); - var value = paramParts[1].replace(PARAM_TRIM_RE, ''); - if (key == "rel" && _contains(relations, value)) { - if (value == "previous") links.prev = url; - else links[value] = url; - } - }); - }); - - var last = links.last || '', qsi, qs; - if (qs = (qsi = last.indexOf('?')) ? last.slice(qsi + 1) : '') { - var params = queryStringToParams(qs); - - var state = _clone(this.state); - var queryParams = this.queryParams; - var pageSize = state.pageSize; - - var totalRecords = params[queryParams.totalRecords] * 1; - var pageNum = params[queryParams.currentPage] * 1; - var totalPages = params[queryParams.totalPages]; - - if (!totalRecords) { - if (pageNum) totalRecords = (state.firstPage === 0 ? - pageNum + 1 : - pageNum) * pageSize; - else if (totalPages) totalRecords = totalPages * pageSize; - } - - if (totalRecords) state.totalRecords = totalRecords; - - this.state = this._checkState(state); - } - } - - delete links.last; - - return links; - }, - - /** - Parse server response data. - - This default implementation assumes the response data is in one of two - structures: - - [ - {}, // Your new pagination state - [{}, ...] // An array of JSON objects - ] - - Or, - - [{}] // An array of JSON objects - - The first structure is the preferred form because the pagination states - may have been updated on the server side, sending them down again allows - this collection to update its states. If the response has a pagination - state object, it is checked for errors. - - The second structure is the - [Backbone.Collection#parse](http://backbonejs.org/#Collection-parse) - default. - - **Note:** this method has been further simplified since 1.1.7. While - existing #parse implementations will continue to work, new code is - encouraged to override #parseState and #parseRecords instead. - - @param {Object} resp The deserialized response data from the server. - @param {Object} the options for the ajax request - - @return {Array.<Object>} An array of model objects - */ - parse: function (resp, options) { - var newState = this.parseState(resp, _clone(this.queryParams), _clone(this.state), options); - if (newState) this.state = this._checkState(_extend({}, this.state, newState)); - return this.parseRecords(resp, options); - }, - - /** - Parse server response for server pagination state updates. - - This default implementation first checks whether the response has any - state object as documented in #parse. If it exists, a state object is - returned by mapping the server state keys to this pageable collection - instance's query parameter keys using `queryParams`. - - It is __NOT__ neccessary to return a full state object complete with all - the mappings defined in #queryParams. Any state object resulted is merged - with a copy of the current pageable collection state and checked for - sanity before actually updating. Most of the time, simply providing a new - `totalRecords` value is enough to trigger a full pagination state - recalculation. - - parseState: function (resp, queryParams, state, options) { - return {totalRecords: resp.total_entries}; - } - - If you want to use header fields use: - - parseState: function (resp, queryParams, state, options) { - return {totalRecords: options.xhr.getResponseHeader("X-total")}; - } - - This method __MUST__ return a new state object instead of directly - modifying the #state object. The behavior of directly modifying #state is - undefined. - - @param {Object} resp The deserialized response data from the server. - @param {Object} queryParams A copy of #queryParams. - @param {Object} state A copy of #state. - @param {Object} [options] The options passed through from - `parse`. (backbone >= 0.9.10 only) - - @return {Object} A new (partial) state object. - */ - parseState: function (resp, queryParams, state, options) { - if (resp && resp.length === 2 && _isObject(resp[0]) && _isArray(resp[1])) { - - var newState = _clone(state); - var serverState = resp[0]; - - _each(_pairs(_omit(queryParams, "directions")), function (kvp) { - var k = kvp[0], v = kvp[1]; - var serverVal = serverState[v]; - if (!_isUndefined(serverVal) && !_.isNull(serverVal)) newState[k] = serverState[v]; - }); - - if (serverState.order) { - newState.order = _invert(queryParams.directions)[serverState.order] * 1; - } - - return newState; - } - }, - - /** - Parse server response for an array of model objects. - - This default implementation first checks whether the response has any - state object as documented in #parse. If it exists, the array of model - objects is assumed to be the second element, otherwise the entire - response is returned directly. - - @param {Object} resp The deserialized response data from the server. - @param {Object} [options] The options passed through from the - `parse`. (backbone >= 0.9.10 only) - - @return {Array.<Object>} An array of model objects - */ - parseRecords: function (resp, options) { - if (resp && resp.length === 2 && _isObject(resp[0]) && _isArray(resp[1])) { - return resp[1]; - } - - return resp; - }, - - /** - Fetch a page from the server in server mode, or all the pages in client - mode. Under infinite mode, the current page is refetched by default and - then reset. - - The query string is constructed by translating the current pagination - state to your server API query parameter using #queryParams. The current - page will reset after fetch. - - @param {Object} [options] Accepts all - [Backbone.Collection#fetch](http://backbonejs.org/#Collection-fetch) - options. - - @return {XMLHttpRequest} - */ - fetch: function (options) { - - options = options || {}; - - var state = this._checkState(this.state); - - var mode = this.mode; - - if (mode == "infinite" && !options.url) { - options.url = this.links[state.currentPage]; - } - - var data = options.data || {}; - - // dedup query params - var url = _result(options, "url") || _result(this, "url") || ''; - var qsi = url.indexOf('?'); - if (qsi != -1) { - _extend(data, queryStringToParams(url.slice(qsi + 1))); - url = url.slice(0, qsi); - } - - options.url = url; - options.data = data; - - // map params except directions - var queryParams = this.mode == "client" ? - _pick(this.queryParams, "sortKey", "order") : - _omit(_pick(this.queryParams, _keys(PageableProto.queryParams)), - "directions"); - - var i, kvp, k, v, kvps = _pairs(queryParams), thisCopy = _clone(this); - for (i = 0; i < kvps.length; i++) { - kvp = kvps[i], k = kvp[0], v = kvp[1]; - v = _isFunction(v) ? v.call(thisCopy) : v; - if (state[k] != null && v != null) { - data[v] = state[k]; - } - } - - // fix up sorting parameters - if (state.sortKey && state.order) { - data[queryParams.order] = this.queryParams.directions[state.order + ""]; - } - else if (!state.sortKey) delete data[queryParams.order]; - - // map extra query parameters - var extraKvps = _pairs(_omit(this.queryParams, - _keys(PageableProto.queryParams))); - for (i = 0; i < extraKvps.length; i++) { - kvp = extraKvps[i]; - v = kvp[1]; - v = _isFunction(v) ? v.call(thisCopy) : v; - if (v != null) data[kvp[0]] = v; - } - - if (mode != "server") { - var self = this, fullCol = this.fullCollection; - var success = options.success; - options.success = function (col, resp, opts) { - - // make sure the caller's intent is obeyed - opts = opts || {}; - if (_isUndefined(options.silent)) delete opts.silent; - else opts.silent = options.silent; - - var models = col.models; - if (mode == "client") fullCol.reset(models, opts); - else fullCol.add(models, _extend({at: fullCol.length}, opts)); - - if (success) success(col, resp, opts); - }; - - // silent the first reset from backbone - return BBColProto.fetch.call(self, _extend({}, options, {silent: true})); - } - - return BBColProto.fetch.call(this, options); - }, - - /** - Convenient method for making a `comparator` sorted by a model attribute - identified by `sortKey` and ordered by `order`. - - Like a Backbone.Collection, a Backbone.PageableCollection will maintain - the __current page__ in sorted order on the client side if a `comparator` - is attached to it. If the collection is in client mode, you can attach a - comparator to #fullCollection to have all the pages reflect the global - sorting order by specifying an option `full` to `true`. You __must__ call - `sort` manually or #fullCollection.sort after calling this method to - force a resort. - - While you can use this method to sort the current page in server mode, - the sorting order may not reflect the global sorting order due to the - additions or removals of the records on the server since the last - fetch. If you want the most updated page in a global sorting order, it is - recommended that you set #state.sortKey and optionally #state.order, and - then call #fetch. - - @protected - - @param {string} [sortKey=this.state.sortKey] See `state.sortKey`. - @param {number} [order=this.state.order] See `state.order`. - @param {(function(Backbone.Model, string): Object) | string} [sortValue] See #setSorting. - - See [Backbone.Collection.comparator](http://backbonejs.org/#Collection-comparator). - */ - _makeComparator: function (sortKey, order, sortValue) { - var state = this.state; - - sortKey = sortKey || state.sortKey; - order = order || state.order; - - if (!sortKey || !order) return; - - if (!sortValue) sortValue = function (model, attr) { - return model.get(attr); - }; - - return function (left, right) { - var l = sortValue(left, sortKey), r = sortValue(right, sortKey), t; - if (order === 1) t = l, l = r, r = t; - if (l === r) return 0; - else if (l < r) return -1; - return 1; - }; - }, - - /** - Adjusts the sorting for this pageable collection. - - Given a `sortKey` and an `order`, sets `state.sortKey` and - `state.order`. A comparator can be applied on the client side to sort in - the order defined if `options.side` is `"client"`. By default the - comparator is applied to the #fullCollection. Set `options.full` to - `false` to apply a comparator to the current page under any mode. Setting - `sortKey` to `null` removes the comparator from both the current page and - the full collection. - - If a `sortValue` function is given, it will be passed the `(model, - sortKey)` arguments and is used to extract a value from the model during - comparison sorts. If `sortValue` is not given, `model.get(sortKey)` is - used for sorting. - - @chainable - - @param {string} sortKey See `state.sortKey`. - @param {number} [order=this.state.order] See `state.order`. - @param {Object} [options] - @param {"server"|"client"} [options.side] By default, `"client"` if - `mode` is `"client"`, `"server"` otherwise. - @param {boolean} [options.full=true] - @param {(function(Backbone.Model, string): Object) | string} [options.sortValue] - */ - setSorting: function (sortKey, order, options) { - - var state = this.state; - - state.sortKey = sortKey; - state.order = order = order || state.order; - - var fullCollection = this.fullCollection; - - var delComp = false, delFullComp = false; - - if (!sortKey) delComp = delFullComp = true; - - var mode = this.mode; - options = _extend({side: mode == "client" ? mode : "server", full: true}, - options); - - var comparator = this._makeComparator(sortKey, order, options.sortValue); - - var full = options.full, side = options.side; - - if (side == "client") { - if (full) { - if (fullCollection) fullCollection.comparator = comparator; - delComp = true; - } - else { - this.comparator = comparator; - delFullComp = true; - } - } - else if (side == "server" && !full) { - this.comparator = comparator; - } - - if (delComp) this.comparator = null; - if (delFullComp && fullCollection) fullCollection.comparator = null; - - return this; - } - - }); - - var PageableProto = PageableCollection.prototype; - - return PageableCollection; - -})); diff --git a/src/UI/JsLibraries/backbone.validation.js b/src/UI/JsLibraries/backbone.validation.js deleted file mode 100644 index d81836168..000000000 --- a/src/UI/JsLibraries/backbone.validation.js +++ /dev/null @@ -1,606 +0,0 @@ -// Backbone.Validation v0.8.1 -// -// Copyright (c) 2011-2013 Thomas Pedersen -// Distributed under MIT License -// -// Documentation and full license available at: -// http://thedersen.com/projects/backbone-validation -Backbone.Validation = (function(_){ - 'use strict'; - - // Default options - // --------------- - - var defaultOptions = { - forceUpdate: false, - selector: 'name', - labelFormatter: 'sentenceCase', - valid: Function.prototype, - invalid: Function.prototype - }; - - - // Helper functions - // ---------------- - - // Formatting functions used for formatting error messages - var formatFunctions = { - // Uses the configured label formatter to format the attribute name - // to make it more readable for the user - formatLabel: function(attrName, model) { - return defaultLabelFormatters[defaultOptions.labelFormatter](attrName, model); - }, - - // Replaces nummeric placeholders like {0} in a string with arguments - // passed to the function - format: function() { - var args = Array.prototype.slice.call(arguments), - text = args.shift(); - return text.replace(/\{(\d+)\}/g, function(match, number) { - return typeof args[number] !== 'undefined' ? args[number] : match; - }); - } - }; - - // Flattens an object - // eg: - // - // var o = { - // address: { - // street: 'Street', - // zip: 1234 - // } - // }; - // - // becomes: - // - // var o = { - // 'address.street': 'Street', - // 'address.zip': 1234 - // }; - var flatten = function (obj, into, prefix) { - into = into || {}; - prefix = prefix || ''; - - _.each(obj, function(val, key) { - if(obj.hasOwnProperty(key)) { - if (val && typeof val === 'object' && !( - val instanceof Array || - val instanceof Date || - val instanceof RegExp || - val instanceof Backbone.Model || - val instanceof Backbone.Collection) - ) { - flatten(val, into, prefix + key + '.'); - } - else { - into[prefix + key] = val; - } - } - }); - - return into; - }; - - // Validation - // ---------- - - var Validation = (function(){ - - // Returns an object with undefined properties for all - // attributes on the model that has defined one or more - // validation rules. - var getValidatedAttrs = function(model) { - return _.reduce(_.keys(model.validation || {}), function(memo, key) { - memo[key] = void 0; - return memo; - }, {}); - }; - - // Looks on the model for validations for a specified - // attribute. Returns an array of any validators defined, - // or an empty array if none is defined. - var getValidators = function(model, attr) { - var attrValidationSet = model.validation ? model.validation[attr] || {} : {}; - - // If the validator is a function or a string, wrap it in a function validator - if (_.isFunction(attrValidationSet) || _.isString(attrValidationSet)) { - attrValidationSet = { - fn: attrValidationSet - }; - } - - // Stick the validator object into an array - if(!_.isArray(attrValidationSet)) { - attrValidationSet = [attrValidationSet]; - } - - // Reduces the array of validators into a new array with objects - // with a validation method to call, the value to validate against - // and the specified error message, if any - return _.reduce(attrValidationSet, function(memo, attrValidation) { - _.each(_.without(_.keys(attrValidation), 'msg'), function(validator) { - memo.push({ - fn: defaultValidators[validator], - val: attrValidation[validator], - msg: attrValidation.msg - }); - }); - return memo; - }, []); - }; - - // Validates an attribute against all validators defined - // for that attribute. If one or more errors are found, - // the first error message is returned. - // If the attribute is valid, an empty string is returned. - var validateAttr = function(model, attr, value, computed) { - // Reduces the array of validators to an error message by - // applying all the validators and returning the first error - // message, if any. - return _.reduce(getValidators(model, attr), function(memo, validator){ - // Pass the format functions plus the default - // validators as the context to the validator - var ctx = _.extend({}, formatFunctions, defaultValidators), - result = validator.fn.call(ctx, value, attr, validator.val, model, computed); - - if(result === false || memo === false) { - return false; - } - if (result && !memo) { - return validator.msg || result; - } - return memo; - }, ''); - }; - - // Loops through the model's attributes and validates them all. - // Returns and object containing names of invalid attributes - // as well as error messages. - var validateModel = function(model, attrs) { - var error, - invalidAttrs = {}, - isValid = true, - computed = _.clone(attrs), - flattened = flatten(attrs); - - _.each(flattened, function(val, attr) { - error = validateAttr(model, attr, val, computed); - if (error) { - invalidAttrs[attr] = error; - isValid = false; - } - }); - - return { - invalidAttrs: invalidAttrs, - isValid: isValid - }; - }; - - // Contains the methods that are mixed in on the model when binding - var mixin = function(view, options) { - return { - - // Check whether or not a value passes validation - // without updating the model - preValidate: function(attr, value) { - return validateAttr(this, attr, value, _.extend({}, this.attributes)); - }, - - // Check to see if an attribute, an array of attributes or the - // entire model is valid. Passing true will force a validation - // of the model. - isValid: function(option) { - var flattened = flatten(this.attributes); - - if(_.isString(option)){ - return !validateAttr(this, option, flattened[option], _.extend({}, this.attributes)); - } - if(_.isArray(option)){ - return _.reduce(option, function(memo, attr) { - return memo && !validateAttr(this, attr, flattened[attr], _.extend({}, this.attributes)); - }, true, this); - } - if(option === true) { - this.validate(); - } - return this.validation ? this._isValid : true; - }, - - // This is called by Backbone when it needs to perform validation. - // You can call it manually without any parameters to validate the - // entire model. - validate: function(attrs, setOptions){ - var model = this, - validateAll = !attrs, - opt = _.extend({}, options, setOptions), - validatedAttrs = getValidatedAttrs(model), - allAttrs = _.extend({}, validatedAttrs, model.attributes, attrs), - changedAttrs = flatten(attrs || allAttrs), - - result = validateModel(model, allAttrs); - - model._isValid = result.isValid; - - // After validation is performed, loop through all changed attributes - // and call the valid callbacks so the view is updated. - _.each(validatedAttrs, function(val, attr){ - var invalid = result.invalidAttrs.hasOwnProperty(attr); - if(!invalid){ - opt.valid(view, attr, opt.selector); - } - }); - - // After validation is performed, loop through all changed attributes - // and call the invalid callback so the view is updated. - _.each(validatedAttrs, function(val, attr){ - var invalid = result.invalidAttrs.hasOwnProperty(attr), - changed = changedAttrs.hasOwnProperty(attr); - - if(invalid && (changed || validateAll)){ - opt.invalid(view, attr, result.invalidAttrs[attr], opt.selector); - } - }); - - // Trigger validated events. - // Need to defer this so the model is actually updated before - // the event is triggered. - _.defer(function() { - model.trigger('validated', model._isValid, model, result.invalidAttrs); - model.trigger('validated:' + (model._isValid ? 'valid' : 'invalid'), model, result.invalidAttrs); - }); - - // Return any error messages to Backbone, unless the forceUpdate flag is set. - // Then we do not return anything and fools Backbone to believe the validation was - // a success. That way Backbone will update the model regardless. - if (!opt.forceUpdate && _.intersection(_.keys(result.invalidAttrs), _.keys(changedAttrs)).length > 0) { - return result.invalidAttrs; - } - } - }; - }; - - // Helper to mix in validation on a model - var bindModel = function(view, model, options) { - _.extend(model, mixin(view, options)); - }; - - // Removes the methods added to a model - var unbindModel = function(model) { - delete model.validate; - delete model.preValidate; - delete model.isValid; - }; - - // Mix in validation on a model whenever a model is - // added to a collection - var collectionAdd = function(model) { - bindModel(this.view, model, this.options); - }; - - // Remove validation from a model whenever a model is - // removed from a collection - var collectionRemove = function(model) { - unbindModel(model); - }; - - // Returns the public methods on Backbone.Validation - return { - - // Current version of the library - version: '0.8.1', - - // Called to configure the default options - configure: function(options) { - _.extend(defaultOptions, options); - }, - - // Hooks up validation on a view with a model - // or collection - bind: function(view, options) { - var model = view.model, - collection = view.collection; - - options = _.extend({}, defaultOptions, defaultCallbacks, options); - - if(typeof model === 'undefined' && typeof collection === 'undefined'){ - throw 'Before you execute the binding your view must have a model or a collection.\n' + - 'See http://thedersen.com/projects/backbone-validation/#using-form-model-validation for more information.'; - } - - if(model) { - bindModel(view, model, options); - } - else if(collection) { - collection.each(function(model){ - bindModel(view, model, options); - }); - collection.bind('add', collectionAdd, {view: view, options: options}); - collection.bind('remove', collectionRemove); - } - }, - - // Removes validation from a view with a model - // or collection - unbind: function(view) { - var model = view.model, - collection = view.collection; - - if(model) { - unbindModel(view.model); - } - if(collection) { - collection.each(function(model){ - unbindModel(model); - }); - collection.unbind('add', collectionAdd); - collection.unbind('remove', collectionRemove); - } - }, - - // Used to extend the Backbone.Model.prototype - // with validation - mixin: mixin(null, defaultOptions) - }; - }()); - - - // Callbacks - // --------- - - var defaultCallbacks = Validation.callbacks = { - - // Gets called when a previously invalid field in the - // view becomes valid. Removes any error message. - // Should be overridden with custom functionality. - valid: function(view, attr, selector) { - view.$('[' + selector + '~="' + attr + '"]') - .removeClass('invalid') - .removeAttr('data-error'); - }, - - // Gets called when a field in the view becomes invalid. - // Adds a error message. - // Should be overridden with custom functionality. - invalid: function(view, attr, error, selector) { - view.$('[' + selector + '~="' + attr + '"]') - .addClass('invalid') - .attr('data-error', error); - } - }; - - - // Patterns - // -------- - - var defaultPatterns = Validation.patterns = { - // Matches any digit(s) (i.e. 0-9) - digits: /^\d+$/, - - // Matched any number (e.g. 100.000) - number: /^-?(?:\d+|\d{1,3}(?:,\d{3})+)(?:\.\d+)?$/, - - // Matches a valid email address (e.g. mail@example.com) - email: /^((([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+(\.([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+)*)|((\x22)((((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(([\x01-\x08\x0b\x0c\x0e-\x1f\x7f]|\x21|[\x23-\x5b]|[\x5d-\x7e]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(\\([\x01-\x09\x0b\x0c\x0d-\x7f]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]))))*(((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(\x22)))@((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))$/i, - - // Mathes any valid url (e.g. http://www.xample.com) - url: /^(https?|ftp):\/\/(((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:)*@)?(((\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5]))|((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.?)(:\d*)?)(\/((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)+(\/(([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)*)*)?)?(\?((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|[\uE000-\uF8FF]|\/|\?)*)?(\#((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|\/|\?)*)?$/i - }; - - - // Error messages - // -------------- - - // Error message for the build in validators. - // {x} gets swapped out with arguments form the validator. - var defaultMessages = Validation.messages = { - required: '{0} is required', - acceptance: '{0} must be accepted', - min: '{0} must be greater than or equal to {1}', - max: '{0} must be less than or equal to {1}', - range: '{0} must be between {1} and {2}', - length: '{0} must be {1} characters', - minLength: '{0} must be at least {1} characters', - maxLength: '{0} must be at most {1} characters', - rangeLength: '{0} must be between {1} and {2} characters', - oneOf: '{0} must be one of: {1}', - equalTo: '{0} must be the same as {1}', - pattern: '{0} must be a valid {1}' - }; - - // Label formatters - // ---------------- - - // Label formatters are used to convert the attribute name - // to a more human friendly label when using the built in - // error messages. - // Configure which one to use with a call to - // - // Backbone.Validation.configure({ - // labelFormatter: 'label' - // }); - var defaultLabelFormatters = Validation.labelFormatters = { - - // Returns the attribute name with applying any formatting - none: function(attrName) { - return attrName; - }, - - // Converts attributeName or attribute_name to Attribute name - sentenceCase: function(attrName) { - return attrName.replace(/(?:^\w|[A-Z]|\b\w)/g, function(match, index) { - return index === 0 ? match.toUpperCase() : ' ' + match.toLowerCase(); - }).replace(/_/g, ' '); - }, - - // Looks for a label configured on the model and returns it - // - // var Model = Backbone.Model.extend({ - // validation: { - // someAttribute: { - // required: true - // } - // }, - // - // labels: { - // someAttribute: 'Custom label' - // } - // }); - label: function(attrName, model) { - return (model.labels && model.labels[attrName]) || defaultLabelFormatters.sentenceCase(attrName, model); - } - }; - - - // Built in validators - // ------------------- - - var defaultValidators = Validation.validators = (function(){ - // Use native trim when defined - var trim = String.prototype.trim ? - function(text) { - return text === null ? '' : String.prototype.trim.call(text); - } : - function(text) { - var trimLeft = /^\s+/, - trimRight = /\s+$/; - - return text === null ? '' : text.toString().replace(trimLeft, '').replace(trimRight, ''); - }; - - // Determines whether or not a value is a number - var isNumber = function(value){ - return _.isNumber(value) || (_.isString(value) && value.match(defaultPatterns.number)); - }; - - // Determines whether or not a value is empty - var hasValue = function(value) { - return !(_.isNull(value) || _.isUndefined(value) || (_.isString(value) && trim(value) === '') || (_.isArray(value) && _.isEmpty(value))); - }; - - return { - // Function validator - // Lets you implement a custom function used for validation - fn: function(value, attr, fn, model, computed) { - if(_.isString(fn)){ - fn = model[fn]; - } - return fn.call(model, value, attr, computed); - }, - - // Required validator - // Validates if the attribute is required or not - required: function(value, attr, required, model, computed) { - var isRequired = _.isFunction(required) ? required.call(model, value, attr, computed) : required; - if(!isRequired && !hasValue(value)) { - return false; // overrides all other validators - } - if (isRequired && !hasValue(value)) { - return this.format(defaultMessages.required, this.formatLabel(attr, model)); - } - }, - - // Acceptance validator - // Validates that something has to be accepted, e.g. terms of use - // `true` or 'true' are valid - acceptance: function(value, attr, accept, model) { - if(value !== 'true' && (!_.isBoolean(value) || value === false)) { - return this.format(defaultMessages.acceptance, this.formatLabel(attr, model)); - } - }, - - // Min validator - // Validates that the value has to be a number and equal to or greater than - // the min value specified - min: function(value, attr, minValue, model) { - if (!isNumber(value) || value < minValue) { - return this.format(defaultMessages.min, this.formatLabel(attr, model), minValue); - } - }, - - // Max validator - // Validates that the value has to be a number and equal to or less than - // the max value specified - max: function(value, attr, maxValue, model) { - if (!isNumber(value) || value > maxValue) { - return this.format(defaultMessages.max, this.formatLabel(attr, model), maxValue); - } - }, - - // Range validator - // Validates that the value has to be a number and equal to or between - // the two numbers specified - range: function(value, attr, range, model) { - if(!isNumber(value) || value < range[0] || value > range[1]) { - return this.format(defaultMessages.range, this.formatLabel(attr, model), range[0], range[1]); - } - }, - - // Length validator - // Validates that the value has to be a string with length equal to - // the length value specified - length: function(value, attr, length, model) { - if (!hasValue(value) || trim(value).length !== length) { - return this.format(defaultMessages.length, this.formatLabel(attr, model), length); - } - }, - - // Min length validator - // Validates that the value has to be a string with length equal to or greater than - // the min length value specified - minLength: function(value, attr, minLength, model) { - if (!hasValue(value) || trim(value).length < minLength) { - return this.format(defaultMessages.minLength, this.formatLabel(attr, model), minLength); - } - }, - - // Max length validator - // Validates that the value has to be a string with length equal to or less than - // the max length value specified - maxLength: function(value, attr, maxLength, model) { - if (!hasValue(value) || trim(value).length > maxLength) { - return this.format(defaultMessages.maxLength, this.formatLabel(attr, model), maxLength); - } - }, - - // Range length validator - // Validates that the value has to be a string and equal to or between - // the two numbers specified - rangeLength: function(value, attr, range, model) { - if(!hasValue(value) || trim(value).length < range[0] || trim(value).length > range[1]) { - return this.format(defaultMessages.rangeLength, this.formatLabel(attr, model), range[0], range[1]); - } - }, - - // One of validator - // Validates that the value has to be equal to one of the elements in - // the specified array. Case sensitive matching - oneOf: function(value, attr, values, model) { - if(!_.include(values, value)){ - return this.format(defaultMessages.oneOf, this.formatLabel(attr, model), values.join(', ')); - } - }, - - // Equal to validator - // Validates that the value has to be equal to the value of the attribute - // with the name specified - equalTo: function(value, attr, equalTo, model, computed) { - if(value !== computed[equalTo]) { - return this.format(defaultMessages.equalTo, this.formatLabel(attr, model), this.formatLabel(equalTo, model)); - } - }, - - // Pattern validator - // Validates that the value has to match the pattern specified. - // Can be a regular expression or the name of one of the built in patterns - pattern: function(value, attr, pattern, model) { - if (!hasValue(value) || !value.toString().match(defaultPatterns[pattern] || pattern)) { - return this.format(defaultMessages.pattern, this.formatLabel(attr, model), pattern); - } - } - }; - }()); - - return Validation; -}(_)); \ No newline at end of file diff --git a/src/UI/JsLibraries/backbone.wreqr.js b/src/UI/JsLibraries/backbone.wreqr.js deleted file mode 100644 index d8aa9bc9b..000000000 --- a/src/UI/JsLibraries/backbone.wreqr.js +++ /dev/null @@ -1,276 +0,0 @@ -(function (root, factory) { - if (typeof exports === 'object') { - - var underscore = require('underscore'); - var backbone = require('backbone'); - - module.exports = factory(underscore, backbone); - - } else if (typeof define === 'function' && define.amd) { - - define(['underscore', 'backbone'], factory); - - } -}(this, function (_, Backbone) { - 'use strict'; - - Backbone.Wreqr = (function(Backbone, Marionette, _){ - 'use strict'; - var Wreqr = {}; - - // Handlers -// -------- -// A registry of functions to call, given a name - -Wreqr.Handlers = (function(Backbone, _){ - 'use strict'; - - // Constructor - // ----------- - - var Handlers = function(options){ - this.options = options; - this._wreqrHandlers = {}; - - if (_.isFunction(this.initialize)){ - this.initialize(options); - } - }; - - Handlers.extend = Backbone.Model.extend; - - // Instance Members - // ---------------- - - _.extend(Handlers.prototype, Backbone.Events, { - - // Add multiple handlers using an object literal configuration - setHandlers: function(handlers){ - _.each(handlers, function(handler, name){ - var context = null; - - if (_.isObject(handler) && !_.isFunction(handler)){ - context = handler.context; - handler = handler.callback; - } - - this.setHandler(name, handler, context); - }, this); - }, - - // Add a handler for the given name, with an - // optional context to run the handler within - setHandler: function(name, handler, context){ - var config = { - callback: handler, - context: context - }; - - this._wreqrHandlers[name] = config; - - this.trigger("handler:add", name, handler, context); - }, - - // Determine whether or not a handler is registered - hasHandler: function(name){ - return !! this._wreqrHandlers[name]; - }, - - // Get the currently registered handler for - // the specified name. Throws an exception if - // no handler is found. - getHandler: function(name){ - var config = this._wreqrHandlers[name]; - - if (!config){ - throw new Error("Handler not found for '" + name + "'"); - } - - return function(){ - var args = Array.prototype.slice.apply(arguments); - return config.callback.apply(config.context, args); - }; - }, - - // Remove a handler for the specified name - removeHandler: function(name){ - delete this._wreqrHandlers[name]; - }, - - // Remove all handlers from this registry - removeAllHandlers: function(){ - this._wreqrHandlers = {}; - } - }); - - return Handlers; -})(Backbone, _); - - // Wreqr.CommandStorage -// -------------------- -// -// Store and retrieve commands for execution. -Wreqr.CommandStorage = (function(){ - 'use strict'; - - // Constructor function - var CommandStorage = function(options){ - this.options = options; - this._commands = {}; - - if (_.isFunction(this.initialize)){ - this.initialize(options); - } - }; - - // Instance methods - _.extend(CommandStorage.prototype, Backbone.Events, { - - // Get an object literal by command name, that contains - // the `commandName` and the `instances` of all commands - // represented as an array of arguments to process - getCommands: function(commandName){ - var commands = this._commands[commandName]; - - // we don't have it, so add it - if (!commands){ - - // build the configuration - commands = { - command: commandName, - instances: [] - }; - - // store it - this._commands[commandName] = commands; - } - - return commands; - }, - - // Add a command by name, to the storage and store the - // args for the command - addCommand: function(commandName, args){ - var command = this.getCommands(commandName); - command.instances.push(args); - }, - - // Clear all commands for the given `commandName` - clearCommands: function(commandName){ - var command = this.getCommands(commandName); - command.instances = []; - } - }); - - return CommandStorage; -})(); - - // Wreqr.Commands -// -------------- -// -// A simple command pattern implementation. Register a command -// handler and execute it. -Wreqr.Commands = (function(Wreqr){ - 'use strict'; - - return Wreqr.Handlers.extend({ - // default storage type - storageType: Wreqr.CommandStorage, - - constructor: function(options){ - this.options = options || {}; - - this._initializeStorage(this.options); - this.on("handler:add", this._executeCommands, this); - - var args = Array.prototype.slice.call(arguments); - Wreqr.Handlers.prototype.constructor.apply(this, args); - }, - - // Execute a named command with the supplied args - execute: function(name, args){ - name = arguments[0]; - args = Array.prototype.slice.call(arguments, 1); - - if (this.hasHandler(name)){ - this.getHandler(name).apply(this, args); - } else { - this.storage.addCommand(name, args); - } - - }, - - // Internal method to handle bulk execution of stored commands - _executeCommands: function(name, handler, context){ - var command = this.storage.getCommands(name); - - // loop through and execute all the stored command instances - _.each(command.instances, function(args){ - handler.apply(context, args); - }); - - this.storage.clearCommands(name); - }, - - // Internal method to initialize storage either from the type's - // `storageType` or the instance `options.storageType`. - _initializeStorage: function(options){ - var storage; - - var StorageType = options.storageType || this.storageType; - if (_.isFunction(StorageType)){ - storage = new StorageType(); - } else { - storage = StorageType; - } - - this.storage = storage; - } - }); - -})(Wreqr); - - // Wreqr.RequestResponse -// --------------------- -// -// A simple request/response implementation. Register a -// request handler, and return a response from it -Wreqr.RequestResponse = (function(Wreqr){ - 'use strict'; - - return Wreqr.Handlers.extend({ - request: function(){ - var name = arguments[0]; - var args = Array.prototype.slice.call(arguments, 1); - - return this.getHandler(name).apply(this, args); - } - }); - -})(Wreqr); - - // Event Aggregator -// ---------------- -// A pub-sub object that can be used to decouple various parts -// of an application through event-driven architecture. - -Wreqr.EventAggregator = (function(Backbone, _){ - 'use strict'; - var EA = function(){}; - - // Copy the `extend` function used by Backbone's classes - EA.extend = Backbone.Model.extend; - - // Copy the basic Backbone.Events on to the event aggregator - _.extend(EA.prototype, Backbone.Events); - - return EA; -})(Backbone, _); - - - return Wreqr; -})(Backbone, Backbone.Marionette, _); - - return Backbone.Wreqr; - -})); \ No newline at end of file diff --git a/src/UI/JsLibraries/bootstrap.js b/src/UI/JsLibraries/bootstrap.js deleted file mode 100644 index 5debfd7de..000000000 --- a/src/UI/JsLibraries/bootstrap.js +++ /dev/null @@ -1,2363 +0,0 @@ -/*! - * Bootstrap v3.3.5 (http://getbootstrap.com) - * Copyright 2011-2015 Twitter, Inc. - * Licensed under the MIT license - */ - -if (typeof jQuery === 'undefined') { - throw new Error('Bootstrap\'s JavaScript requires jQuery') -} - -+function ($) { - 'use strict'; - var version = $.fn.jquery.split(' ')[0].split('.') - if ((version[0] < 2 && version[1] < 9) || (version[0] == 1 && version[1] == 9 && version[2] < 1)) { - throw new Error('Bootstrap\'s JavaScript requires jQuery version 1.9.1 or higher') - } -}(jQuery); - -/* ======================================================================== - * Bootstrap: transition.js v3.3.5 - * http://getbootstrap.com/javascript/#transitions - * ======================================================================== - * Copyright 2011-2015 Twitter, Inc. - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) - * ======================================================================== */ - - -+function ($) { - 'use strict'; - - // CSS TRANSITION SUPPORT (Shoutout: http://www.modernizr.com/) - // ============================================================ - - function transitionEnd() { - var el = document.createElement('bootstrap') - - var transEndEventNames = { - WebkitTransition : 'webkitTransitionEnd', - MozTransition : 'transitionend', - OTransition : 'oTransitionEnd otransitionend', - transition : 'transitionend' - } - - for (var name in transEndEventNames) { - if (el.style[name] !== undefined) { - return { end: transEndEventNames[name] } - } - } - - return false // explicit for ie8 ( ._.) - } - - // http://blog.alexmaccaw.com/css-transitions - $.fn.emulateTransitionEnd = function (duration) { - var called = false - var $el = this - $(this).one('bsTransitionEnd', function () { called = true }) - var callback = function () { if (!called) $($el).trigger($.support.transition.end) } - setTimeout(callback, duration) - return this - } - - $(function () { - $.support.transition = transitionEnd() - - if (!$.support.transition) return - - $.event.special.bsTransitionEnd = { - bindType: $.support.transition.end, - delegateType: $.support.transition.end, - handle: function (e) { - if ($(e.target).is(this)) return e.handleObj.handler.apply(this, arguments) - } - } - }) - -}(jQuery); - -/* ======================================================================== - * Bootstrap: alert.js v3.3.5 - * http://getbootstrap.com/javascript/#alerts - * ======================================================================== - * Copyright 2011-2015 Twitter, Inc. - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) - * ======================================================================== */ - - -+function ($) { - 'use strict'; - - // ALERT CLASS DEFINITION - // ====================== - - var dismiss = '[data-dismiss="alert"]' - var Alert = function (el) { - $(el).on('click', dismiss, this.close) - } - - Alert.VERSION = '3.3.5' - - Alert.TRANSITION_DURATION = 150 - - Alert.prototype.close = function (e) { - var $this = $(this) - var selector = $this.attr('data-target') - - if (!selector) { - selector = $this.attr('href') - selector = selector && selector.replace(/.*(?=#[^\s]*$)/, '') // strip for ie7 - } - - var $parent = $(selector) - - if (e) e.preventDefault() - - if (!$parent.length) { - $parent = $this.closest('.alert') - } - - $parent.trigger(e = $.Event('close.bs.alert')) - - if (e.isDefaultPrevented()) return - - $parent.removeClass('in') - - function removeElement() { - // detach from parent, fire event then clean up data - $parent.detach().trigger('closed.bs.alert').remove() - } - - $.support.transition && $parent.hasClass('fade') ? - $parent - .one('bsTransitionEnd', removeElement) - .emulateTransitionEnd(Alert.TRANSITION_DURATION) : - removeElement() - } - - - // ALERT PLUGIN DEFINITION - // ======================= - - function Plugin(option) { - return this.each(function () { - var $this = $(this) - var data = $this.data('bs.alert') - - if (!data) $this.data('bs.alert', (data = new Alert(this))) - if (typeof option == 'string') data[option].call($this) - }) - } - - var old = $.fn.alert - - $.fn.alert = Plugin - $.fn.alert.Constructor = Alert - - - // ALERT NO CONFLICT - // ================= - - $.fn.alert.noConflict = function () { - $.fn.alert = old - return this - } - - - // ALERT DATA-API - // ============== - - $(document).on('click.bs.alert.data-api', dismiss, Alert.prototype.close) - -}(jQuery); - -/* ======================================================================== - * Bootstrap: button.js v3.3.5 - * http://getbootstrap.com/javascript/#buttons - * ======================================================================== - * Copyright 2011-2015 Twitter, Inc. - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) - * ======================================================================== */ - - -+function ($) { - 'use strict'; - - // BUTTON PUBLIC CLASS DEFINITION - // ============================== - - var Button = function (element, options) { - this.$element = $(element) - this.options = $.extend({}, Button.DEFAULTS, options) - this.isLoading = false - } - - Button.VERSION = '3.3.5' - - Button.DEFAULTS = { - loadingText: 'loading...' - } - - Button.prototype.setState = function (state) { - var d = 'disabled' - var $el = this.$element - var val = $el.is('input') ? 'val' : 'html' - var data = $el.data() - - state += 'Text' - - if (data.resetText == null) $el.data('resetText', $el[val]()) - - // push to event loop to allow forms to submit - setTimeout($.proxy(function () { - $el[val](data[state] == null ? this.options[state] : data[state]) - - if (state == 'loadingText') { - this.isLoading = true - $el.addClass(d).attr(d, d) - } else if (this.isLoading) { - this.isLoading = false - $el.removeClass(d).removeAttr(d) - } - }, this), 0) - } - - Button.prototype.toggle = function () { - var changed = true - var $parent = this.$element.closest('[data-toggle="buttons"]') - - if ($parent.length) { - var $input = this.$element.find('input') - if ($input.prop('type') == 'radio') { - if ($input.prop('checked')) changed = false - $parent.find('.active').removeClass('active') - this.$element.addClass('active') - } else if ($input.prop('type') == 'checkbox') { - if (($input.prop('checked')) !== this.$element.hasClass('active')) changed = false - this.$element.toggleClass('active') - } - $input.prop('checked', this.$element.hasClass('active')) - if (changed) $input.trigger('change') - } else { - this.$element.attr('aria-pressed', !this.$element.hasClass('active')) - this.$element.toggleClass('active') - } - } - - - // BUTTON PLUGIN DEFINITION - // ======================== - - function Plugin(option) { - return this.each(function () { - var $this = $(this) - var data = $this.data('bs.button') - var options = typeof option == 'object' && option - - if (!data) $this.data('bs.button', (data = new Button(this, options))) - - if (option == 'toggle') data.toggle() - else if (option) data.setState(option) - }) - } - - var old = $.fn.button - - $.fn.button = Plugin - $.fn.button.Constructor = Button - - - // BUTTON NO CONFLICT - // ================== - - $.fn.button.noConflict = function () { - $.fn.button = old - return this - } - - - // BUTTON DATA-API - // =============== - - $(document) - .on('click.bs.button.data-api', '[data-toggle^="button"]', function (e) { - var $btn = $(e.target) - if (!$btn.hasClass('btn')) $btn = $btn.closest('.btn') - Plugin.call($btn, 'toggle') - if (!($(e.target).is('input[type="radio"]') || $(e.target).is('input[type="checkbox"]'))) e.preventDefault() - }) - .on('focus.bs.button.data-api blur.bs.button.data-api', '[data-toggle^="button"]', function (e) { - $(e.target).closest('.btn').toggleClass('focus', /^focus(in)?$/.test(e.type)) - }) - -}(jQuery); - -/* ======================================================================== - * Bootstrap: carousel.js v3.3.5 - * http://getbootstrap.com/javascript/#carousel - * ======================================================================== - * Copyright 2011-2015 Twitter, Inc. - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) - * ======================================================================== */ - - -+function ($) { - 'use strict'; - - // CAROUSEL CLASS DEFINITION - // ========================= - - var Carousel = function (element, options) { - this.$element = $(element) - this.$indicators = this.$element.find('.carousel-indicators') - this.options = options - this.paused = null - this.sliding = null - this.interval = null - this.$active = null - this.$items = null - - this.options.keyboard && this.$element.on('keydown.bs.carousel', $.proxy(this.keydown, this)) - - this.options.pause == 'hover' && !('ontouchstart' in document.documentElement) && this.$element - .on('mouseenter.bs.carousel', $.proxy(this.pause, this)) - .on('mouseleave.bs.carousel', $.proxy(this.cycle, this)) - } - - Carousel.VERSION = '3.3.5' - - Carousel.TRANSITION_DURATION = 600 - - Carousel.DEFAULTS = { - interval: 5000, - pause: 'hover', - wrap: true, - keyboard: true - } - - Carousel.prototype.keydown = function (e) { - if (/input|textarea/i.test(e.target.tagName)) return - switch (e.which) { - case 37: this.prev(); break - case 39: this.next(); break - default: return - } - - e.preventDefault() - } - - Carousel.prototype.cycle = function (e) { - e || (this.paused = false) - - this.interval && clearInterval(this.interval) - - this.options.interval - && !this.paused - && (this.interval = setInterval($.proxy(this.next, this), this.options.interval)) - - return this - } - - Carousel.prototype.getItemIndex = function (item) { - this.$items = item.parent().children('.item') - return this.$items.index(item || this.$active) - } - - Carousel.prototype.getItemForDirection = function (direction, active) { - var activeIndex = this.getItemIndex(active) - var willWrap = (direction == 'prev' && activeIndex === 0) - || (direction == 'next' && activeIndex == (this.$items.length - 1)) - if (willWrap && !this.options.wrap) return active - var delta = direction == 'prev' ? -1 : 1 - var itemIndex = (activeIndex + delta) % this.$items.length - return this.$items.eq(itemIndex) - } - - Carousel.prototype.to = function (pos) { - var that = this - var activeIndex = this.getItemIndex(this.$active = this.$element.find('.item.active')) - - if (pos > (this.$items.length - 1) || pos < 0) return - - if (this.sliding) return this.$element.one('slid.bs.carousel', function () { that.to(pos) }) // yes, "slid" - if (activeIndex == pos) return this.pause().cycle() - - return this.slide(pos > activeIndex ? 'next' : 'prev', this.$items.eq(pos)) - } - - Carousel.prototype.pause = function (e) { - e || (this.paused = true) - - if (this.$element.find('.next, .prev').length && $.support.transition) { - this.$element.trigger($.support.transition.end) - this.cycle(true) - } - - this.interval = clearInterval(this.interval) - - return this - } - - Carousel.prototype.next = function () { - if (this.sliding) return - return this.slide('next') - } - - Carousel.prototype.prev = function () { - if (this.sliding) return - return this.slide('prev') - } - - Carousel.prototype.slide = function (type, next) { - var $active = this.$element.find('.item.active') - var $next = next || this.getItemForDirection(type, $active) - var isCycling = this.interval - var direction = type == 'next' ? 'left' : 'right' - var that = this - - if ($next.hasClass('active')) return (this.sliding = false) - - var relatedTarget = $next[0] - var slideEvent = $.Event('slide.bs.carousel', { - relatedTarget: relatedTarget, - direction: direction - }) - this.$element.trigger(slideEvent) - if (slideEvent.isDefaultPrevented()) return - - this.sliding = true - - isCycling && this.pause() - - if (this.$indicators.length) { - this.$indicators.find('.active').removeClass('active') - var $nextIndicator = $(this.$indicators.children()[this.getItemIndex($next)]) - $nextIndicator && $nextIndicator.addClass('active') - } - - var slidEvent = $.Event('slid.bs.carousel', { relatedTarget: relatedTarget, direction: direction }) // yes, "slid" - if ($.support.transition && this.$element.hasClass('slide')) { - $next.addClass(type) - $next[0].offsetWidth // force reflow - $active.addClass(direction) - $next.addClass(direction) - $active - .one('bsTransitionEnd', function () { - $next.removeClass([type, direction].join(' ')).addClass('active') - $active.removeClass(['active', direction].join(' ')) - that.sliding = false - setTimeout(function () { - that.$element.trigger(slidEvent) - }, 0) - }) - .emulateTransitionEnd(Carousel.TRANSITION_DURATION) - } else { - $active.removeClass('active') - $next.addClass('active') - this.sliding = false - this.$element.trigger(slidEvent) - } - - isCycling && this.cycle() - - return this - } - - - // CAROUSEL PLUGIN DEFINITION - // ========================== - - function Plugin(option) { - return this.each(function () { - var $this = $(this) - var data = $this.data('bs.carousel') - var options = $.extend({}, Carousel.DEFAULTS, $this.data(), typeof option == 'object' && option) - var action = typeof option == 'string' ? option : options.slide - - if (!data) $this.data('bs.carousel', (data = new Carousel(this, options))) - if (typeof option == 'number') data.to(option) - else if (action) data[action]() - else if (options.interval) data.pause().cycle() - }) - } - - var old = $.fn.carousel - - $.fn.carousel = Plugin - $.fn.carousel.Constructor = Carousel - - - // CAROUSEL NO CONFLICT - // ==================== - - $.fn.carousel.noConflict = function () { - $.fn.carousel = old - return this - } - - - // CAROUSEL DATA-API - // ================= - - var clickHandler = function (e) { - var href - var $this = $(this) - var $target = $($this.attr('data-target') || (href = $this.attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '')) // strip for ie7 - if (!$target.hasClass('carousel')) return - var options = $.extend({}, $target.data(), $this.data()) - var slideIndex = $this.attr('data-slide-to') - if (slideIndex) options.interval = false - - Plugin.call($target, options) - - if (slideIndex) { - $target.data('bs.carousel').to(slideIndex) - } - - e.preventDefault() - } - - $(document) - .on('click.bs.carousel.data-api', '[data-slide]', clickHandler) - .on('click.bs.carousel.data-api', '[data-slide-to]', clickHandler) - - $(window).on('load', function () { - $('[data-ride="carousel"]').each(function () { - var $carousel = $(this) - Plugin.call($carousel, $carousel.data()) - }) - }) - -}(jQuery); - -/* ======================================================================== - * Bootstrap: collapse.js v3.3.5 - * http://getbootstrap.com/javascript/#collapse - * ======================================================================== - * Copyright 2011-2015 Twitter, Inc. - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) - * ======================================================================== */ - - -+function ($) { - 'use strict'; - - // COLLAPSE PUBLIC CLASS DEFINITION - // ================================ - - var Collapse = function (element, options) { - this.$element = $(element) - this.options = $.extend({}, Collapse.DEFAULTS, options) - this.$trigger = $('[data-toggle="collapse"][href="#' + element.id + '"],' + - '[data-toggle="collapse"][data-target="#' + element.id + '"]') - this.transitioning = null - - if (this.options.parent) { - this.$parent = this.getParent() - } else { - this.addAriaAndCollapsedClass(this.$element, this.$trigger) - } - - if (this.options.toggle) this.toggle() - } - - Collapse.VERSION = '3.3.5' - - Collapse.TRANSITION_DURATION = 350 - - Collapse.DEFAULTS = { - toggle: true - } - - Collapse.prototype.dimension = function () { - var hasWidth = this.$element.hasClass('width') - return hasWidth ? 'width' : 'height' - } - - Collapse.prototype.show = function () { - if (this.transitioning || this.$element.hasClass('in')) return - - var activesData - var actives = this.$parent && this.$parent.children('.panel').children('.in, .collapsing') - - if (actives && actives.length) { - activesData = actives.data('bs.collapse') - if (activesData && activesData.transitioning) return - } - - var startEvent = $.Event('show.bs.collapse') - this.$element.trigger(startEvent) - if (startEvent.isDefaultPrevented()) return - - if (actives && actives.length) { - Plugin.call(actives, 'hide') - activesData || actives.data('bs.collapse', null) - } - - var dimension = this.dimension() - - this.$element - .removeClass('collapse') - .addClass('collapsing')[dimension](0) - .attr('aria-expanded', true) - - this.$trigger - .removeClass('collapsed') - .attr('aria-expanded', true) - - this.transitioning = 1 - - var complete = function () { - this.$element - .removeClass('collapsing') - .addClass('collapse in')[dimension]('') - this.transitioning = 0 - this.$element - .trigger('shown.bs.collapse') - } - - if (!$.support.transition) return complete.call(this) - - var scrollSize = $.camelCase(['scroll', dimension].join('-')) - - this.$element - .one('bsTransitionEnd', $.proxy(complete, this)) - .emulateTransitionEnd(Collapse.TRANSITION_DURATION)[dimension](this.$element[0][scrollSize]) - } - - Collapse.prototype.hide = function () { - if (this.transitioning || !this.$element.hasClass('in')) return - - var startEvent = $.Event('hide.bs.collapse') - this.$element.trigger(startEvent) - if (startEvent.isDefaultPrevented()) return - - var dimension = this.dimension() - - this.$element[dimension](this.$element[dimension]())[0].offsetHeight - - this.$element - .addClass('collapsing') - .removeClass('collapse in') - .attr('aria-expanded', false) - - this.$trigger - .addClass('collapsed') - .attr('aria-expanded', false) - - this.transitioning = 1 - - var complete = function () { - this.transitioning = 0 - this.$element - .removeClass('collapsing') - .addClass('collapse') - .trigger('hidden.bs.collapse') - } - - if (!$.support.transition) return complete.call(this) - - this.$element - [dimension](0) - .one('bsTransitionEnd', $.proxy(complete, this)) - .emulateTransitionEnd(Collapse.TRANSITION_DURATION) - } - - Collapse.prototype.toggle = function () { - this[this.$element.hasClass('in') ? 'hide' : 'show']() - } - - Collapse.prototype.getParent = function () { - return $(this.options.parent) - .find('[data-toggle="collapse"][data-parent="' + this.options.parent + '"]') - .each($.proxy(function (i, element) { - var $element = $(element) - this.addAriaAndCollapsedClass(getTargetFromTrigger($element), $element) - }, this)) - .end() - } - - Collapse.prototype.addAriaAndCollapsedClass = function ($element, $trigger) { - var isOpen = $element.hasClass('in') - - $element.attr('aria-expanded', isOpen) - $trigger - .toggleClass('collapsed', !isOpen) - .attr('aria-expanded', isOpen) - } - - function getTargetFromTrigger($trigger) { - var href - var target = $trigger.attr('data-target') - || (href = $trigger.attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '') // strip for ie7 - - return $(target) - } - - - // COLLAPSE PLUGIN DEFINITION - // ========================== - - function Plugin(option) { - return this.each(function () { - var $this = $(this) - var data = $this.data('bs.collapse') - var options = $.extend({}, Collapse.DEFAULTS, $this.data(), typeof option == 'object' && option) - - if (!data && options.toggle && /show|hide/.test(option)) options.toggle = false - if (!data) $this.data('bs.collapse', (data = new Collapse(this, options))) - if (typeof option == 'string') data[option]() - }) - } - - var old = $.fn.collapse - - $.fn.collapse = Plugin - $.fn.collapse.Constructor = Collapse - - - // COLLAPSE NO CONFLICT - // ==================== - - $.fn.collapse.noConflict = function () { - $.fn.collapse = old - return this - } - - - // COLLAPSE DATA-API - // ================= - - $(document).on('click.bs.collapse.data-api', '[data-toggle="collapse"]', function (e) { - var $this = $(this) - - if (!$this.attr('data-target')) e.preventDefault() - - var $target = getTargetFromTrigger($this) - var data = $target.data('bs.collapse') - var option = data ? 'toggle' : $this.data() - - Plugin.call($target, option) - }) - -}(jQuery); - -/* ======================================================================== - * Bootstrap: dropdown.js v3.3.5 - * http://getbootstrap.com/javascript/#dropdowns - * ======================================================================== - * Copyright 2011-2015 Twitter, Inc. - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) - * ======================================================================== */ - - -+function ($) { - 'use strict'; - - // DROPDOWN CLASS DEFINITION - // ========================= - - var backdrop = '.dropdown-backdrop' - var toggle = '[data-toggle="dropdown"]' - var Dropdown = function (element) { - $(element).on('click.bs.dropdown', this.toggle) - } - - Dropdown.VERSION = '3.3.5' - - function getParent($this) { - var selector = $this.attr('data-target') - - if (!selector) { - selector = $this.attr('href') - selector = selector && /#[A-Za-z]/.test(selector) && selector.replace(/.*(?=#[^\s]*$)/, '') // strip for ie7 - } - - var $parent = selector && $(selector) - - return $parent && $parent.length ? $parent : $this.parent() - } - - function clearMenus(e) { - if (e && e.which === 3) return - $(backdrop).remove() - $(toggle).each(function () { - var $this = $(this) - var $parent = getParent($this) - var relatedTarget = { relatedTarget: this } - - if (!$parent.hasClass('open')) return - - if (e && e.type == 'click' && /input|textarea/i.test(e.target.tagName) && $.contains($parent[0], e.target)) return - - $parent.trigger(e = $.Event('hide.bs.dropdown', relatedTarget)) - - if (e.isDefaultPrevented()) return - - $this.attr('aria-expanded', 'false') - $parent.removeClass('open').trigger('hidden.bs.dropdown', relatedTarget) - }) - } - - Dropdown.prototype.toggle = function (e) { - var $this = $(this) - - if ($this.is('.disabled, :disabled')) return - - var $parent = getParent($this) - var isActive = $parent.hasClass('open') - - clearMenus() - - if (!isActive) { - if ('ontouchstart' in document.documentElement && !$parent.closest('.navbar-nav').length) { - // if mobile we use a backdrop because click events don't delegate - $(document.createElement('div')) - .addClass('dropdown-backdrop') - .insertAfter($(this)) - .on('click', clearMenus) - } - - var relatedTarget = { relatedTarget: this } - $parent.trigger(e = $.Event('show.bs.dropdown', relatedTarget)) - - if (e.isDefaultPrevented()) return - - $this - .trigger('focus') - .attr('aria-expanded', 'true') - - $parent - .toggleClass('open') - .trigger('shown.bs.dropdown', relatedTarget) - } - - return false - } - - Dropdown.prototype.keydown = function (e) { - if (!/(38|40|27|32)/.test(e.which) || /input|textarea/i.test(e.target.tagName)) return - - var $this = $(this) - - e.preventDefault() - e.stopPropagation() - - if ($this.is('.disabled, :disabled')) return - - var $parent = getParent($this) - var isActive = $parent.hasClass('open') - - if (!isActive && e.which != 27 || isActive && e.which == 27) { - if (e.which == 27) $parent.find(toggle).trigger('focus') - return $this.trigger('click') - } - - var desc = ' li:not(.disabled):visible a' - var $items = $parent.find('.dropdown-menu' + desc) - - if (!$items.length) return - - var index = $items.index(e.target) - - if (e.which == 38 && index > 0) index-- // up - if (e.which == 40 && index < $items.length - 1) index++ // down - if (!~index) index = 0 - - $items.eq(index).trigger('focus') - } - - - // DROPDOWN PLUGIN DEFINITION - // ========================== - - function Plugin(option) { - return this.each(function () { - var $this = $(this) - var data = $this.data('bs.dropdown') - - if (!data) $this.data('bs.dropdown', (data = new Dropdown(this))) - if (typeof option == 'string') data[option].call($this) - }) - } - - var old = $.fn.dropdown - - $.fn.dropdown = Plugin - $.fn.dropdown.Constructor = Dropdown - - - // DROPDOWN NO CONFLICT - // ==================== - - $.fn.dropdown.noConflict = function () { - $.fn.dropdown = old - return this - } - - - // APPLY TO STANDARD DROPDOWN ELEMENTS - // =================================== - - $(document) - .on('click.bs.dropdown.data-api', clearMenus) - .on('click.bs.dropdown.data-api', '.dropdown form', function (e) { e.stopPropagation() }) - .on('click.bs.dropdown.data-api', toggle, Dropdown.prototype.toggle) - .on('keydown.bs.dropdown.data-api', toggle, Dropdown.prototype.keydown) - .on('keydown.bs.dropdown.data-api', '.dropdown-menu', Dropdown.prototype.keydown) - -}(jQuery); - -/* ======================================================================== - * Bootstrap: modal.js v3.3.5 - * http://getbootstrap.com/javascript/#modals - * ======================================================================== - * Copyright 2011-2015 Twitter, Inc. - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) - * ======================================================================== */ - - -+function ($) { - 'use strict'; - - // MODAL CLASS DEFINITION - // ====================== - - var Modal = function (element, options) { - this.options = options - this.$body = $(document.body) - this.$element = $(element) - this.$dialog = this.$element.find('.modal-dialog') - this.$backdrop = null - this.isShown = null - this.originalBodyPad = null - this.scrollbarWidth = 0 - this.ignoreBackdropClick = false - - if (this.options.remote) { - this.$element - .find('.modal-content') - .load(this.options.remote, $.proxy(function () { - this.$element.trigger('loaded.bs.modal') - }, this)) - } - } - - Modal.VERSION = '3.3.5' - - Modal.TRANSITION_DURATION = 300 - Modal.BACKDROP_TRANSITION_DURATION = 150 - - Modal.DEFAULTS = { - backdrop: true, - keyboard: true, - show: true - } - - Modal.prototype.toggle = function (_relatedTarget) { - return this.isShown ? this.hide() : this.show(_relatedTarget) - } - - Modal.prototype.show = function (_relatedTarget) { - var that = this - var e = $.Event('show.bs.modal', { relatedTarget: _relatedTarget }) - - this.$element.trigger(e) - - if (this.isShown || e.isDefaultPrevented()) return - - this.isShown = true - - this.checkScrollbar() - this.setScrollbar() - this.$body.addClass('modal-open') - - this.escape() - this.resize() - - this.$element.on('click.dismiss.bs.modal', '[data-dismiss="modal"]', $.proxy(this.hide, this)) - - this.$dialog.on('mousedown.dismiss.bs.modal', function () { - that.$element.one('mouseup.dismiss.bs.modal', function (e) { - if ($(e.target).is(that.$element)) that.ignoreBackdropClick = true - }) - }) - - this.backdrop(function () { - var transition = $.support.transition && that.$element.hasClass('fade') - - if (!that.$element.parent().length) { - that.$element.appendTo(that.$body) // don't move modals dom position - } - - that.$element - .show() - .scrollTop(0) - - that.adjustDialog() - - if (transition) { - that.$element[0].offsetWidth // force reflow - } - - that.$element.addClass('in') - - that.enforceFocus() - - var e = $.Event('shown.bs.modal', { relatedTarget: _relatedTarget }) - - transition ? - that.$dialog // wait for modal to slide in - .one('bsTransitionEnd', function () { - that.$element.trigger('focus').trigger(e) - }) - .emulateTransitionEnd(Modal.TRANSITION_DURATION) : - that.$element.trigger('focus').trigger(e) - }) - } - - Modal.prototype.hide = function (e) { - if (e) e.preventDefault() - - e = $.Event('hide.bs.modal') - - this.$element.trigger(e) - - if (!this.isShown || e.isDefaultPrevented()) return - - this.isShown = false - - this.escape() - this.resize() - - $(document).off('focusin.bs.modal') - - this.$element - .removeClass('in') - .off('click.dismiss.bs.modal') - .off('mouseup.dismiss.bs.modal') - - this.$dialog.off('mousedown.dismiss.bs.modal') - - $.support.transition && this.$element.hasClass('fade') ? - this.$element - .one('bsTransitionEnd', $.proxy(this.hideModal, this)) - .emulateTransitionEnd(Modal.TRANSITION_DURATION) : - this.hideModal() - } - - Modal.prototype.enforceFocus = function () { - $(document) - .off('focusin.bs.modal') // guard against infinite focus loop - .on('focusin.bs.modal', $.proxy(function (e) { - if (this.$element[0] !== e.target && !this.$element.has(e.target).length) { - this.$element.trigger('focus') - } - }, this)) - } - - Modal.prototype.escape = function () { - if (this.isShown && this.options.keyboard) { - this.$element.on('keydown.dismiss.bs.modal', $.proxy(function (e) { - e.which == 27 && this.hide() - }, this)) - } else if (!this.isShown) { - this.$element.off('keydown.dismiss.bs.modal') - } - } - - Modal.prototype.resize = function () { - if (this.isShown) { - $(window).on('resize.bs.modal', $.proxy(this.handleUpdate, this)) - } else { - $(window).off('resize.bs.modal') - } - } - - Modal.prototype.hideModal = function () { - var that = this - this.$element.hide() - this.backdrop(function () { - that.$body.removeClass('modal-open') - that.resetAdjustments() - that.resetScrollbar() - that.$element.trigger('hidden.bs.modal') - }) - } - - Modal.prototype.removeBackdrop = function () { - this.$backdrop && this.$backdrop.remove() - this.$backdrop = null - } - - Modal.prototype.backdrop = function (callback) { - var that = this - var animate = this.$element.hasClass('fade') ? 'fade' : '' - - if (this.isShown && this.options.backdrop) { - var doAnimate = $.support.transition && animate - - this.$backdrop = $(document.createElement('div')) - .addClass('modal-backdrop ' + animate) - .appendTo(this.$body) - - this.$element.on('click.dismiss.bs.modal', $.proxy(function (e) { - if (this.ignoreBackdropClick) { - this.ignoreBackdropClick = false - return - } - if (e.target !== e.currentTarget) return - this.options.backdrop == 'static' - ? this.$element[0].focus() - : this.hide() - }, this)) - - if (doAnimate) this.$backdrop[0].offsetWidth // force reflow - - this.$backdrop.addClass('in') - - if (!callback) return - - doAnimate ? - this.$backdrop - .one('bsTransitionEnd', callback) - .emulateTransitionEnd(Modal.BACKDROP_TRANSITION_DURATION) : - callback() - - } else if (!this.isShown && this.$backdrop) { - this.$backdrop.removeClass('in') - - var callbackRemove = function () { - that.removeBackdrop() - callback && callback() - } - $.support.transition && this.$element.hasClass('fade') ? - this.$backdrop - .one('bsTransitionEnd', callbackRemove) - .emulateTransitionEnd(Modal.BACKDROP_TRANSITION_DURATION) : - callbackRemove() - - } else if (callback) { - callback() - } - } - - // these following methods are used to handle overflowing modals - - Modal.prototype.handleUpdate = function () { - this.adjustDialog() - } - - Modal.prototype.adjustDialog = function () { - var modalIsOverflowing = this.$element[0].scrollHeight > document.documentElement.clientHeight - - this.$element.css({ - paddingLeft: !this.bodyIsOverflowing && modalIsOverflowing ? this.scrollbarWidth : '', - paddingRight: this.bodyIsOverflowing && !modalIsOverflowing ? this.scrollbarWidth : '' - }) - } - - Modal.prototype.resetAdjustments = function () { - this.$element.css({ - paddingLeft: '', - paddingRight: '' - }) - } - - Modal.prototype.checkScrollbar = function () { - var fullWindowWidth = window.innerWidth - if (!fullWindowWidth) { // workaround for missing window.innerWidth in IE8 - var documentElementRect = document.documentElement.getBoundingClientRect() - fullWindowWidth = documentElementRect.right - Math.abs(documentElementRect.left) - } - this.bodyIsOverflowing = document.body.clientWidth < fullWindowWidth - this.scrollbarWidth = this.measureScrollbar() - } - - Modal.prototype.setScrollbar = function () { - var bodyPad = parseInt((this.$body.css('padding-right') || 0), 10) - this.originalBodyPad = document.body.style.paddingRight || '' - if (this.bodyIsOverflowing) this.$body.css('padding-right', bodyPad + this.scrollbarWidth) - } - - Modal.prototype.resetScrollbar = function () { - this.$body.css('padding-right', this.originalBodyPad) - } - - Modal.prototype.measureScrollbar = function () { // thx walsh - var scrollDiv = document.createElement('div') - scrollDiv.className = 'modal-scrollbar-measure' - this.$body.append(scrollDiv) - var scrollbarWidth = scrollDiv.offsetWidth - scrollDiv.clientWidth - this.$body[0].removeChild(scrollDiv) - return scrollbarWidth - } - - - // MODAL PLUGIN DEFINITION - // ======================= - - function Plugin(option, _relatedTarget) { - return this.each(function () { - var $this = $(this) - var data = $this.data('bs.modal') - var options = $.extend({}, Modal.DEFAULTS, $this.data(), typeof option == 'object' && option) - - if (!data) $this.data('bs.modal', (data = new Modal(this, options))) - if (typeof option == 'string') data[option](_relatedTarget) - else if (options.show) data.show(_relatedTarget) - }) - } - - var old = $.fn.modal - - $.fn.modal = Plugin - $.fn.modal.Constructor = Modal - - - // MODAL NO CONFLICT - // ================= - - $.fn.modal.noConflict = function () { - $.fn.modal = old - return this - } - - - // MODAL DATA-API - // ============== - - $(document).on('click.bs.modal.data-api', '[data-toggle="modal"]', function (e) { - var $this = $(this) - var href = $this.attr('href') - var $target = $($this.attr('data-target') || (href && href.replace(/.*(?=#[^\s]+$)/, ''))) // strip for ie7 - var option = $target.data('bs.modal') ? 'toggle' : $.extend({ remote: !/#/.test(href) && href }, $target.data(), $this.data()) - - if ($this.is('a')) e.preventDefault() - - $target.one('show.bs.modal', function (showEvent) { - if (showEvent.isDefaultPrevented()) return // only register focus restorer if modal will actually get shown - $target.one('hidden.bs.modal', function () { - $this.is(':visible') && $this.trigger('focus') - }) - }) - Plugin.call($target, option, this) - }) - -}(jQuery); - -/* ======================================================================== - * Bootstrap: tooltip.js v3.3.5 - * http://getbootstrap.com/javascript/#tooltip - * Inspired by the original jQuery.tipsy by Jason Frame - * ======================================================================== - * Copyright 2011-2015 Twitter, Inc. - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) - * ======================================================================== */ - - -+function ($) { - 'use strict'; - - // TOOLTIP PUBLIC CLASS DEFINITION - // =============================== - - var Tooltip = function (element, options) { - this.type = null - this.options = null - this.enabled = null - this.timeout = null - this.hoverState = null - this.$element = null - this.inState = null - - this.init('tooltip', element, options) - } - - Tooltip.VERSION = '3.3.5' - - Tooltip.TRANSITION_DURATION = 150 - - Tooltip.DEFAULTS = { - animation: true, - placement: 'top', - selector: false, - template: '<div class="tooltip" role="tooltip"><div class="tooltip-arrow"></div><div class="tooltip-inner"></div></div>', - trigger: 'hover focus', - title: '', - delay: 0, - html: false, - container: false, - viewport: { - selector: 'body', - padding: 0 - } - } - - Tooltip.prototype.init = function (type, element, options) { - this.enabled = true - this.type = type - this.$element = $(element) - this.options = this.getOptions(options) - this.$viewport = this.options.viewport && $($.isFunction(this.options.viewport) ? this.options.viewport.call(this, this.$element) : (this.options.viewport.selector || this.options.viewport)) - this.inState = { click: false, hover: false, focus: false } - - if (this.$element[0] instanceof document.constructor && !this.options.selector) { - throw new Error('`selector` option must be specified when initializing ' + this.type + ' on the window.document object!') - } - - var triggers = this.options.trigger.split(' ') - - for (var i = triggers.length; i--;) { - var trigger = triggers[i] - - if (trigger == 'click') { - this.$element.on('click.' + this.type, this.options.selector, $.proxy(this.toggle, this)) - } else if (trigger != 'manual') { - var eventIn = trigger == 'hover' ? 'mouseenter' : 'focusin' - var eventOut = trigger == 'hover' ? 'mouseleave' : 'focusout' - - this.$element.on(eventIn + '.' + this.type, this.options.selector, $.proxy(this.enter, this)) - this.$element.on(eventOut + '.' + this.type, this.options.selector, $.proxy(this.leave, this)) - } - } - - this.options.selector ? - (this._options = $.extend({}, this.options, { trigger: 'manual', selector: '' })) : - this.fixTitle() - } - - Tooltip.prototype.getDefaults = function () { - return Tooltip.DEFAULTS - } - - Tooltip.prototype.getOptions = function (options) { - options = $.extend({}, this.getDefaults(), this.$element.data(), options) - - if (options.delay && typeof options.delay == 'number') { - options.delay = { - show: options.delay, - hide: options.delay - } - } - - return options - } - - Tooltip.prototype.getDelegateOptions = function () { - var options = {} - var defaults = this.getDefaults() - - this._options && $.each(this._options, function (key, value) { - if (defaults[key] != value) options[key] = value - }) - - return options - } - - Tooltip.prototype.enter = function (obj) { - var self = obj instanceof this.constructor ? - obj : $(obj.currentTarget).data('bs.' + this.type) - - if (!self) { - self = new this.constructor(obj.currentTarget, this.getDelegateOptions()) - $(obj.currentTarget).data('bs.' + this.type, self) - } - - if (obj instanceof $.Event) { - self.inState[obj.type == 'focusin' ? 'focus' : 'hover'] = true - } - - if (self.tip().hasClass('in') || self.hoverState == 'in') { - self.hoverState = 'in' - return - } - - clearTimeout(self.timeout) - - self.hoverState = 'in' - - if (!self.options.delay || !self.options.delay.show) return self.show() - - self.timeout = setTimeout(function () { - if (self.hoverState == 'in') self.show() - }, self.options.delay.show) - } - - Tooltip.prototype.isInStateTrue = function () { - for (var key in this.inState) { - if (this.inState[key]) return true - } - - return false - } - - Tooltip.prototype.leave = function (obj) { - var self = obj instanceof this.constructor ? - obj : $(obj.currentTarget).data('bs.' + this.type) - - if (!self) { - self = new this.constructor(obj.currentTarget, this.getDelegateOptions()) - $(obj.currentTarget).data('bs.' + this.type, self) - } - - if (obj instanceof $.Event) { - self.inState[obj.type == 'focusout' ? 'focus' : 'hover'] = false - } - - if (self.isInStateTrue()) return - - clearTimeout(self.timeout) - - self.hoverState = 'out' - - if (!self.options.delay || !self.options.delay.hide) return self.hide() - - self.timeout = setTimeout(function () { - if (self.hoverState == 'out') self.hide() - }, self.options.delay.hide) - } - - Tooltip.prototype.show = function () { - var e = $.Event('show.bs.' + this.type) - - if (this.hasContent() && this.enabled) { - this.$element.trigger(e) - - var inDom = $.contains(this.$element[0].ownerDocument.documentElement, this.$element[0]) - if (e.isDefaultPrevented() || !inDom) return - var that = this - - var $tip = this.tip() - - var tipId = this.getUID(this.type) - - this.setContent() - $tip.attr('id', tipId) - this.$element.attr('aria-describedby', tipId) - - if (this.options.animation) $tip.addClass('fade') - - var placement = typeof this.options.placement == 'function' ? - this.options.placement.call(this, $tip[0], this.$element[0]) : - this.options.placement - - var autoToken = /\s?auto?\s?/i - var autoPlace = autoToken.test(placement) - if (autoPlace) placement = placement.replace(autoToken, '') || 'top' - - $tip - .detach() - .css({ top: 0, left: 0, display: 'block' }) - .addClass(placement) - .data('bs.' + this.type, this) - - this.options.container ? $tip.appendTo(this.options.container) : $tip.insertAfter(this.$element) - this.$element.trigger('inserted.bs.' + this.type) - - var pos = this.getPosition() - var actualWidth = $tip[0].offsetWidth - var actualHeight = $tip[0].offsetHeight - - if (autoPlace) { - var orgPlacement = placement - var viewportDim = this.getPosition(this.$viewport) - - placement = placement == 'bottom' && pos.bottom + actualHeight > viewportDim.bottom ? 'top' : - placement == 'top' && pos.top - actualHeight < viewportDim.top ? 'bottom' : - placement == 'right' && pos.right + actualWidth > viewportDim.width ? 'left' : - placement == 'left' && pos.left - actualWidth < viewportDim.left ? 'right' : - placement - - $tip - .removeClass(orgPlacement) - .addClass(placement) - } - - var calculatedOffset = this.getCalculatedOffset(placement, pos, actualWidth, actualHeight) - - this.applyPlacement(calculatedOffset, placement) - - var complete = function () { - var prevHoverState = that.hoverState - that.$element.trigger('shown.bs.' + that.type) - that.hoverState = null - - if (prevHoverState == 'out') that.leave(that) - } - - $.support.transition && this.$tip.hasClass('fade') ? - $tip - .one('bsTransitionEnd', complete) - .emulateTransitionEnd(Tooltip.TRANSITION_DURATION) : - complete() - } - } - - Tooltip.prototype.applyPlacement = function (offset, placement) { - var $tip = this.tip() - var width = $tip[0].offsetWidth - var height = $tip[0].offsetHeight - - // manually read margins because getBoundingClientRect includes difference - var marginTop = parseInt($tip.css('margin-top'), 10) - var marginLeft = parseInt($tip.css('margin-left'), 10) - - // we must check for NaN for ie 8/9 - if (isNaN(marginTop)) marginTop = 0 - if (isNaN(marginLeft)) marginLeft = 0 - - offset.top += marginTop - offset.left += marginLeft - - // $.fn.offset doesn't round pixel values - // so we use setOffset directly with our own function B-0 - $.offset.setOffset($tip[0], $.extend({ - using: function (props) { - $tip.css({ - top: Math.round(props.top), - left: Math.round(props.left) - }) - } - }, offset), 0) - - $tip.addClass('in') - - // check to see if placing tip in new offset caused the tip to resize itself - var actualWidth = $tip[0].offsetWidth - var actualHeight = $tip[0].offsetHeight - - if (placement == 'top' && actualHeight != height) { - offset.top = offset.top + height - actualHeight - } - - var delta = this.getViewportAdjustedDelta(placement, offset, actualWidth, actualHeight) - - if (delta.left) offset.left += delta.left - else offset.top += delta.top - - var isVertical = /top|bottom/.test(placement) - var arrowDelta = isVertical ? delta.left * 2 - width + actualWidth : delta.top * 2 - height + actualHeight - var arrowOffsetPosition = isVertical ? 'offsetWidth' : 'offsetHeight' - - $tip.offset(offset) - this.replaceArrow(arrowDelta, $tip[0][arrowOffsetPosition], isVertical) - } - - Tooltip.prototype.replaceArrow = function (delta, dimension, isVertical) { - this.arrow() - .css(isVertical ? 'left' : 'top', 50 * (1 - delta / dimension) + '%') - .css(isVertical ? 'top' : 'left', '') - } - - Tooltip.prototype.setContent = function () { - var $tip = this.tip() - var title = this.getTitle() - - $tip.find('.tooltip-inner')[this.options.html ? 'html' : 'text'](title) - $tip.removeClass('fade in top bottom left right') - } - - Tooltip.prototype.hide = function (callback) { - var that = this - var $tip = $(this.$tip) - var e = $.Event('hide.bs.' + this.type) - - function complete() { - if (that.hoverState != 'in') $tip.detach() - that.$element - .removeAttr('aria-describedby') - .trigger('hidden.bs.' + that.type) - callback && callback() - } - - this.$element.trigger(e) - - if (e.isDefaultPrevented()) return - - $tip.removeClass('in') - - $.support.transition && $tip.hasClass('fade') ? - $tip - .one('bsTransitionEnd', complete) - .emulateTransitionEnd(Tooltip.TRANSITION_DURATION) : - complete() - - this.hoverState = null - - return this - } - - Tooltip.prototype.fixTitle = function () { - var $e = this.$element - if ($e.attr('title') || typeof $e.attr('data-original-title') != 'string') { - $e.attr('data-original-title', $e.attr('title') || '').attr('title', '') - } - } - - Tooltip.prototype.hasContent = function () { - return this.getTitle() - } - - Tooltip.prototype.getPosition = function ($element) { - $element = $element || this.$element - - var el = $element[0] - var isBody = el.tagName == 'BODY' - - var elRect = el.getBoundingClientRect() - if (elRect.width == null) { - // width and height are missing in IE8, so compute them manually; see https://github.com/twbs/bootstrap/issues/14093 - elRect = $.extend({}, elRect, { width: elRect.right - elRect.left, height: elRect.bottom - elRect.top }) - } - var elOffset = isBody ? { top: 0, left: 0 } : $element.offset() - var scroll = { scroll: isBody ? document.documentElement.scrollTop || document.body.scrollTop : $element.scrollTop() } - var outerDims = isBody ? { width: $(window).width(), height: $(window).height() } : null - - return $.extend({}, elRect, scroll, outerDims, elOffset) - } - - Tooltip.prototype.getCalculatedOffset = function (placement, pos, actualWidth, actualHeight) { - return placement == 'bottom' ? { top: pos.top + pos.height, left: pos.left + pos.width / 2 - actualWidth / 2 } : - placement == 'top' ? { top: pos.top - actualHeight, left: pos.left + pos.width / 2 - actualWidth / 2 } : - placement == 'left' ? { top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left - actualWidth } : - /* placement == 'right' */ { top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left + pos.width } - - } - - Tooltip.prototype.getViewportAdjustedDelta = function (placement, pos, actualWidth, actualHeight) { - var delta = { top: 0, left: 0 } - if (!this.$viewport) return delta - - var viewportPadding = this.options.viewport && this.options.viewport.padding || 0 - var viewportDimensions = this.getPosition(this.$viewport) - - if (/right|left/.test(placement)) { - var topEdgeOffset = pos.top - viewportPadding - viewportDimensions.scroll - var bottomEdgeOffset = pos.top + viewportPadding - viewportDimensions.scroll + actualHeight - if (topEdgeOffset < viewportDimensions.top) { // top overflow - delta.top = viewportDimensions.top - topEdgeOffset - } else if (bottomEdgeOffset > viewportDimensions.top + viewportDimensions.height) { // bottom overflow - delta.top = viewportDimensions.top + viewportDimensions.height - bottomEdgeOffset - } - } else { - var leftEdgeOffset = pos.left - viewportPadding - var rightEdgeOffset = pos.left + viewportPadding + actualWidth - if (leftEdgeOffset < viewportDimensions.left) { // left overflow - delta.left = viewportDimensions.left - leftEdgeOffset - } else if (rightEdgeOffset > viewportDimensions.right) { // right overflow - delta.left = viewportDimensions.left + viewportDimensions.width - rightEdgeOffset - } - } - - return delta - } - - Tooltip.prototype.getTitle = function () { - var title - var $e = this.$element - var o = this.options - - title = $e.attr('data-original-title') - || (typeof o.title == 'function' ? o.title.call($e[0]) : o.title) - - return title - } - - Tooltip.prototype.getUID = function (prefix) { - do prefix += ~~(Math.random() * 1000000) - while (document.getElementById(prefix)) - return prefix - } - - Tooltip.prototype.tip = function () { - if (!this.$tip) { - this.$tip = $(this.options.template) - if (this.$tip.length != 1) { - throw new Error(this.type + ' `template` option must consist of exactly 1 top-level element!') - } - } - return this.$tip - } - - Tooltip.prototype.arrow = function () { - return (this.$arrow = this.$arrow || this.tip().find('.tooltip-arrow')) - } - - Tooltip.prototype.enable = function () { - this.enabled = true - } - - Tooltip.prototype.disable = function () { - this.enabled = false - } - - Tooltip.prototype.toggleEnabled = function () { - this.enabled = !this.enabled - } - - Tooltip.prototype.toggle = function (e) { - var self = this - if (e) { - self = $(e.currentTarget).data('bs.' + this.type) - if (!self) { - self = new this.constructor(e.currentTarget, this.getDelegateOptions()) - $(e.currentTarget).data('bs.' + this.type, self) - } - } - - if (e) { - self.inState.click = !self.inState.click - if (self.isInStateTrue()) self.enter(self) - else self.leave(self) - } else { - self.tip().hasClass('in') ? self.leave(self) : self.enter(self) - } - } - - Tooltip.prototype.destroy = function () { - var that = this - clearTimeout(this.timeout) - this.hide(function () { - that.$element.off('.' + that.type).removeData('bs.' + that.type) - if (that.$tip) { - that.$tip.detach() - } - that.$tip = null - that.$arrow = null - that.$viewport = null - }) - } - - - // TOOLTIP PLUGIN DEFINITION - // ========================= - - function Plugin(option) { - return this.each(function () { - var $this = $(this) - var data = $this.data('bs.tooltip') - var options = typeof option == 'object' && option - - if (!data && /destroy|hide/.test(option)) return - if (!data) $this.data('bs.tooltip', (data = new Tooltip(this, options))) - if (typeof option == 'string') data[option]() - }) - } - - var old = $.fn.tooltip - - $.fn.tooltip = Plugin - $.fn.tooltip.Constructor = Tooltip - - - // TOOLTIP NO CONFLICT - // =================== - - $.fn.tooltip.noConflict = function () { - $.fn.tooltip = old - return this - } - -}(jQuery); - -/* ======================================================================== - * Bootstrap: popover.js v3.3.5 - * http://getbootstrap.com/javascript/#popovers - * ======================================================================== - * Copyright 2011-2015 Twitter, Inc. - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) - * ======================================================================== */ - - -+function ($) { - 'use strict'; - - // POPOVER PUBLIC CLASS DEFINITION - // =============================== - - var Popover = function (element, options) { - this.init('popover', element, options) - } - - if (!$.fn.tooltip) throw new Error('Popover requires tooltip.js') - - Popover.VERSION = '3.3.5' - - Popover.DEFAULTS = $.extend({}, $.fn.tooltip.Constructor.DEFAULTS, { - placement: 'right', - trigger: 'click', - content: '', - template: '<div class="popover" role="tooltip"><div class="arrow"></div><h3 class="popover-title"></h3><div class="popover-content"></div></div>' - }) - - - // NOTE: POPOVER EXTENDS tooltip.js - // ================================ - - Popover.prototype = $.extend({}, $.fn.tooltip.Constructor.prototype) - - Popover.prototype.constructor = Popover - - Popover.prototype.getDefaults = function () { - return Popover.DEFAULTS - } - - Popover.prototype.setContent = function () { - var $tip = this.tip() - var title = this.getTitle() - var content = this.getContent() - - $tip.find('.popover-title')[this.options.html ? 'html' : 'text'](title) - $tip.find('.popover-content').children().detach().end()[ // we use append for html objects to maintain js events - this.options.html ? (typeof content == 'string' ? 'html' : 'append') : 'text' - ](content) - - $tip.removeClass('fade top bottom left right in') - - // IE8 doesn't accept hiding via the `:empty` pseudo selector, we have to do - // this manually by checking the contents. - if (!$tip.find('.popover-title').html()) $tip.find('.popover-title').hide() - } - - Popover.prototype.hasContent = function () { - return this.getTitle() || this.getContent() - } - - Popover.prototype.getContent = function () { - var $e = this.$element - var o = this.options - - return $e.attr('data-content') - || (typeof o.content == 'function' ? - o.content.call($e[0]) : - o.content) - } - - Popover.prototype.arrow = function () { - return (this.$arrow = this.$arrow || this.tip().find('.arrow')) - } - - - // POPOVER PLUGIN DEFINITION - // ========================= - - function Plugin(option) { - return this.each(function () { - var $this = $(this) - var data = $this.data('bs.popover') - var options = typeof option == 'object' && option - - if (!data && /destroy|hide/.test(option)) return - if (!data) $this.data('bs.popover', (data = new Popover(this, options))) - if (typeof option == 'string') data[option]() - }) - } - - var old = $.fn.popover - - $.fn.popover = Plugin - $.fn.popover.Constructor = Popover - - - // POPOVER NO CONFLICT - // =================== - - $.fn.popover.noConflict = function () { - $.fn.popover = old - return this - } - -}(jQuery); - -/* ======================================================================== - * Bootstrap: scrollspy.js v3.3.5 - * http://getbootstrap.com/javascript/#scrollspy - * ======================================================================== - * Copyright 2011-2015 Twitter, Inc. - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) - * ======================================================================== */ - - -+function ($) { - 'use strict'; - - // SCROLLSPY CLASS DEFINITION - // ========================== - - function ScrollSpy(element, options) { - this.$body = $(document.body) - this.$scrollElement = $(element).is(document.body) ? $(window) : $(element) - this.options = $.extend({}, ScrollSpy.DEFAULTS, options) - this.selector = (this.options.target || '') + ' .nav li > a' - this.offsets = [] - this.targets = [] - this.activeTarget = null - this.scrollHeight = 0 - - this.$scrollElement.on('scroll.bs.scrollspy', $.proxy(this.process, this)) - this.refresh() - this.process() - } - - ScrollSpy.VERSION = '3.3.5' - - ScrollSpy.DEFAULTS = { - offset: 10 - } - - ScrollSpy.prototype.getScrollHeight = function () { - return this.$scrollElement[0].scrollHeight || Math.max(this.$body[0].scrollHeight, document.documentElement.scrollHeight) - } - - ScrollSpy.prototype.refresh = function () { - var that = this - var offsetMethod = 'offset' - var offsetBase = 0 - - this.offsets = [] - this.targets = [] - this.scrollHeight = this.getScrollHeight() - - if (!$.isWindow(this.$scrollElement[0])) { - offsetMethod = 'position' - offsetBase = this.$scrollElement.scrollTop() - } - - this.$body - .find(this.selector) - .map(function () { - var $el = $(this) - var href = $el.data('target') || $el.attr('href') - var $href = /^#./.test(href) && $(href) - - return ($href - && $href.length - && $href.is(':visible') - && [[$href[offsetMethod]().top + offsetBase, href]]) || null - }) - .sort(function (a, b) { return a[0] - b[0] }) - .each(function () { - that.offsets.push(this[0]) - that.targets.push(this[1]) - }) - } - - ScrollSpy.prototype.process = function () { - var scrollTop = this.$scrollElement.scrollTop() + this.options.offset - var scrollHeight = this.getScrollHeight() - var maxScroll = this.options.offset + scrollHeight - this.$scrollElement.height() - var offsets = this.offsets - var targets = this.targets - var activeTarget = this.activeTarget - var i - - if (this.scrollHeight != scrollHeight) { - this.refresh() - } - - if (scrollTop >= maxScroll) { - return activeTarget != (i = targets[targets.length - 1]) && this.activate(i) - } - - if (activeTarget && scrollTop < offsets[0]) { - this.activeTarget = null - return this.clear() - } - - for (i = offsets.length; i--;) { - activeTarget != targets[i] - && scrollTop >= offsets[i] - && (offsets[i + 1] === undefined || scrollTop < offsets[i + 1]) - && this.activate(targets[i]) - } - } - - ScrollSpy.prototype.activate = function (target) { - this.activeTarget = target - - this.clear() - - var selector = this.selector + - '[data-target="' + target + '"],' + - this.selector + '[href="' + target + '"]' - - var active = $(selector) - .parents('li') - .addClass('active') - - if (active.parent('.dropdown-menu').length) { - active = active - .closest('li.dropdown') - .addClass('active') - } - - active.trigger('activate.bs.scrollspy') - } - - ScrollSpy.prototype.clear = function () { - $(this.selector) - .parentsUntil(this.options.target, '.active') - .removeClass('active') - } - - - // SCROLLSPY PLUGIN DEFINITION - // =========================== - - function Plugin(option) { - return this.each(function () { - var $this = $(this) - var data = $this.data('bs.scrollspy') - var options = typeof option == 'object' && option - - if (!data) $this.data('bs.scrollspy', (data = new ScrollSpy(this, options))) - if (typeof option == 'string') data[option]() - }) - } - - var old = $.fn.scrollspy - - $.fn.scrollspy = Plugin - $.fn.scrollspy.Constructor = ScrollSpy - - - // SCROLLSPY NO CONFLICT - // ===================== - - $.fn.scrollspy.noConflict = function () { - $.fn.scrollspy = old - return this - } - - - // SCROLLSPY DATA-API - // ================== - - $(window).on('load.bs.scrollspy.data-api', function () { - $('[data-spy="scroll"]').each(function () { - var $spy = $(this) - Plugin.call($spy, $spy.data()) - }) - }) - -}(jQuery); - -/* ======================================================================== - * Bootstrap: tab.js v3.3.5 - * http://getbootstrap.com/javascript/#tabs - * ======================================================================== - * Copyright 2011-2015 Twitter, Inc. - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) - * ======================================================================== */ - - -+function ($) { - 'use strict'; - - // TAB CLASS DEFINITION - // ==================== - - var Tab = function (element) { - // jscs:disable requireDollarBeforejQueryAssignment - this.element = $(element) - // jscs:enable requireDollarBeforejQueryAssignment - } - - Tab.VERSION = '3.3.5' - - Tab.TRANSITION_DURATION = 150 - - Tab.prototype.show = function () { - var $this = this.element - var $ul = $this.closest('ul:not(.dropdown-menu)') - var selector = $this.data('target') - - if (!selector) { - selector = $this.attr('href') - selector = selector && selector.replace(/.*(?=#[^\s]*$)/, '') // strip for ie7 - } - - if ($this.parent('li').hasClass('active')) return - - var $previous = $ul.find('.active:last a') - var hideEvent = $.Event('hide.bs.tab', { - relatedTarget: $this[0] - }) - var showEvent = $.Event('show.bs.tab', { - relatedTarget: $previous[0] - }) - - $previous.trigger(hideEvent) - $this.trigger(showEvent) - - if (showEvent.isDefaultPrevented() || hideEvent.isDefaultPrevented()) return - - var $target = $(selector) - - this.activate($this.closest('li'), $ul) - this.activate($target, $target.parent(), function () { - $previous.trigger({ - type: 'hidden.bs.tab', - relatedTarget: $this[0] - }) - $this.trigger({ - type: 'shown.bs.tab', - relatedTarget: $previous[0] - }) - }) - } - - Tab.prototype.activate = function (element, container, callback) { - var $active = container.find('> .active') - var transition = callback - && $.support.transition - && ($active.length && $active.hasClass('fade') || !!container.find('> .fade').length) - - function next() { - $active - .removeClass('active') - .find('> .dropdown-menu > .active') - .removeClass('active') - .end() - .find('[data-toggle="tab"]') - .attr('aria-expanded', false) - - element - .addClass('active') - .find('[data-toggle="tab"]') - .attr('aria-expanded', true) - - if (transition) { - element[0].offsetWidth // reflow for transition - element.addClass('in') - } else { - element.removeClass('fade') - } - - if (element.parent('.dropdown-menu').length) { - element - .closest('li.dropdown') - .addClass('active') - .end() - .find('[data-toggle="tab"]') - .attr('aria-expanded', true) - } - - callback && callback() - } - - $active.length && transition ? - $active - .one('bsTransitionEnd', next) - .emulateTransitionEnd(Tab.TRANSITION_DURATION) : - next() - - $active.removeClass('in') - } - - - // TAB PLUGIN DEFINITION - // ===================== - - function Plugin(option) { - return this.each(function () { - var $this = $(this) - var data = $this.data('bs.tab') - - if (!data) $this.data('bs.tab', (data = new Tab(this))) - if (typeof option == 'string') data[option]() - }) - } - - var old = $.fn.tab - - $.fn.tab = Plugin - $.fn.tab.Constructor = Tab - - - // TAB NO CONFLICT - // =============== - - $.fn.tab.noConflict = function () { - $.fn.tab = old - return this - } - - - // TAB DATA-API - // ============ - - var clickHandler = function (e) { - e.preventDefault() - Plugin.call($(this), 'show') - } - - $(document) - .on('click.bs.tab.data-api', '[data-toggle="tab"]', clickHandler) - .on('click.bs.tab.data-api', '[data-toggle="pill"]', clickHandler) - -}(jQuery); - -/* ======================================================================== - * Bootstrap: affix.js v3.3.5 - * http://getbootstrap.com/javascript/#affix - * ======================================================================== - * Copyright 2011-2015 Twitter, Inc. - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) - * ======================================================================== */ - - -+function ($) { - 'use strict'; - - // AFFIX CLASS DEFINITION - // ====================== - - var Affix = function (element, options) { - this.options = $.extend({}, Affix.DEFAULTS, options) - - this.$target = $(this.options.target) - .on('scroll.bs.affix.data-api', $.proxy(this.checkPosition, this)) - .on('click.bs.affix.data-api', $.proxy(this.checkPositionWithEventLoop, this)) - - this.$element = $(element) - this.affixed = null - this.unpin = null - this.pinnedOffset = null - - this.checkPosition() - } - - Affix.VERSION = '3.3.5' - - Affix.RESET = 'affix affix-top affix-bottom' - - Affix.DEFAULTS = { - offset: 0, - target: window - } - - Affix.prototype.getState = function (scrollHeight, height, offsetTop, offsetBottom) { - var scrollTop = this.$target.scrollTop() - var position = this.$element.offset() - var targetHeight = this.$target.height() - - if (offsetTop != null && this.affixed == 'top') return scrollTop < offsetTop ? 'top' : false - - if (this.affixed == 'bottom') { - if (offsetTop != null) return (scrollTop + this.unpin <= position.top) ? false : 'bottom' - return (scrollTop + targetHeight <= scrollHeight - offsetBottom) ? false : 'bottom' - } - - var initializing = this.affixed == null - var colliderTop = initializing ? scrollTop : position.top - var colliderHeight = initializing ? targetHeight : height - - if (offsetTop != null && scrollTop <= offsetTop) return 'top' - if (offsetBottom != null && (colliderTop + colliderHeight >= scrollHeight - offsetBottom)) return 'bottom' - - return false - } - - Affix.prototype.getPinnedOffset = function () { - if (this.pinnedOffset) return this.pinnedOffset - this.$element.removeClass(Affix.RESET).addClass('affix') - var scrollTop = this.$target.scrollTop() - var position = this.$element.offset() - return (this.pinnedOffset = position.top - scrollTop) - } - - Affix.prototype.checkPositionWithEventLoop = function () { - setTimeout($.proxy(this.checkPosition, this), 1) - } - - Affix.prototype.checkPosition = function () { - if (!this.$element.is(':visible')) return - - var height = this.$element.height() - var offset = this.options.offset - var offsetTop = offset.top - var offsetBottom = offset.bottom - var scrollHeight = Math.max($(document).height(), $(document.body).height()) - - if (typeof offset != 'object') offsetBottom = offsetTop = offset - if (typeof offsetTop == 'function') offsetTop = offset.top(this.$element) - if (typeof offsetBottom == 'function') offsetBottom = offset.bottom(this.$element) - - var affix = this.getState(scrollHeight, height, offsetTop, offsetBottom) - - if (this.affixed != affix) { - if (this.unpin != null) this.$element.css('top', '') - - var affixType = 'affix' + (affix ? '-' + affix : '') - var e = $.Event(affixType + '.bs.affix') - - this.$element.trigger(e) - - if (e.isDefaultPrevented()) return - - this.affixed = affix - this.unpin = affix == 'bottom' ? this.getPinnedOffset() : null - - this.$element - .removeClass(Affix.RESET) - .addClass(affixType) - .trigger(affixType.replace('affix', 'affixed') + '.bs.affix') - } - - if (affix == 'bottom') { - this.$element.offset({ - top: scrollHeight - height - offsetBottom - }) - } - } - - - // AFFIX PLUGIN DEFINITION - // ======================= - - function Plugin(option) { - return this.each(function () { - var $this = $(this) - var data = $this.data('bs.affix') - var options = typeof option == 'object' && option - - if (!data) $this.data('bs.affix', (data = new Affix(this, options))) - if (typeof option == 'string') data[option]() - }) - } - - var old = $.fn.affix - - $.fn.affix = Plugin - $.fn.affix.Constructor = Affix - - - // AFFIX NO CONFLICT - // ================= - - $.fn.affix.noConflict = function () { - $.fn.affix = old - return this - } - - - // AFFIX DATA-API - // ============== - - $(window).on('load', function () { - $('[data-spy="affix"]').each(function () { - var $spy = $(this) - var data = $spy.data() - - data.offset = data.offset || {} - - if (data.offsetBottom != null) data.offset.bottom = data.offsetBottom - if (data.offsetTop != null) data.offset.top = data.offsetTop - - Plugin.call($spy, data) - }) - }) - -}(jQuery); diff --git a/src/UI/JsLibraries/bootstrap.tagsinput.js b/src/UI/JsLibraries/bootstrap.tagsinput.js deleted file mode 100644 index 93e7548a4..000000000 --- a/src/UI/JsLibraries/bootstrap.tagsinput.js +++ /dev/null @@ -1,617 +0,0 @@ -(function ($) { - "use strict"; - - var defaultOptions = { - tagClass: function(item) { - return 'label label-info'; - }, - itemValue: function(item) { - return item ? item.toString() : item; - }, - itemText: function(item) { - return this.itemValue(item); - }, - freeInput: true, - addOnBlur: true, - maxTags: undefined, - maxChars: undefined, - confirmKeys: [13, 44], - onTagExists: function(item, $tag) { - $tag.hide().fadeIn(); - }, - trimValue: false, - allowDuplicates: false - }; - - /** - * Constructor function - */ - function TagsInput(element, options) { - this.itemsArray = []; - - this.$element = $(element); - this.$element.hide(); - - this.isSelect = (element.tagName === 'SELECT'); - this.multiple = (this.isSelect && element.hasAttribute('multiple')); - this.objectItems = options && options.itemValue; - this.placeholderText = element.hasAttribute('placeholder') ? this.$element.attr('placeholder') : ''; - this.inputSize = Math.max(1, this.placeholderText.length); - - this.$container = $('<div class="bootstrap-tagsinput"></div>'); - this.$input = $('<input type="text" placeholder="' + this.placeholderText + '"/>').appendTo(this.$container); - - this.$element.after(this.$container); - -// var inputWidth = (this.inputSize < 3 ? 3 : this.inputSize) + "em"; -// this.$input.get(0).style.cssText = "width: " + inputWidth + " !important;"; - this.build(options); - } - - TagsInput.prototype = { - constructor: TagsInput, - - /** - * Adds the given item as a new tag. Pass true to dontPushVal to prevent - * updating the elements val() - */ - add: function(item, dontPushVal) { - var self = this; - - if (self.options.maxTags && self.itemsArray.length >= self.options.maxTags) - return; - - // Ignore falsey values, except false - if (item !== false && !item) - return; - - // Trim value - if (typeof item === "string" && self.options.trimValue) { - item = $.trim(item); - } - - // Throw an error when trying to add an object while the itemValue option was not set - if (typeof item === "object" && !self.objectItems) - throw("Can't add objects when itemValue option is not set"); - - // Ignore strings only containg whitespace - if (item.toString().match(/^\s*$/)) - return; - - // If SELECT but not multiple, remove current tag - if (self.isSelect && !self.multiple && self.itemsArray.length > 0) - self.remove(self.itemsArray[0]); - - if (typeof item === "string" && this.$element[0].tagName === 'INPUT') { - var items = item.split(','); - if (items.length > 1) { - for (var i = 0; i < items.length; i++) { - this.add(items[i], true); - } - - if (!dontPushVal) - self.pushVal(); - return; - } - } - - var itemValue = self.options.itemValue(item), - itemText = self.options.itemText(item), - tagClass = self.options.tagClass(item); - - // Ignore items allready added - var existing = $.grep(self.itemsArray, function(item) { return self.options.itemValue(item) === itemValue; } )[0]; - if (existing && !self.options.allowDuplicates) { - // Invoke onTagExists - if (self.options.onTagExists) { - var $existingTag = $(".tag", self.$container).filter(function() { return $(this).data("item") === existing; }); - self.options.onTagExists(item, $existingTag); - } - return; - } - - // if length greater than limit - if (self.items().toString().length + item.length + 1 > self.options.maxInputLength) - return; - - // raise beforeItemAdd arg - var beforeItemAddEvent = $.Event('beforeItemAdd', { item: item, cancel: false }); - self.$element.trigger(beforeItemAddEvent); - if (beforeItemAddEvent.cancel) - return; - - // register item in internal array and map - self.itemsArray.push(item); - - // add a tag element - var $tag = $('<span class="tag ' + htmlEncode(tagClass) + '">' + htmlEncode(itemText) + '<span data-role="remove"></span></span>'); - $tag.data('item', item); - self.findInputWrapper().before($tag); - $tag.after(' '); - - // add <option /> if item represents a value not present in one of the <select />'s options - if (self.isSelect && !$('option[value="' + encodeURIComponent(itemValue) + '"]',self.$element)[0]) { - var $option = $('<option selected>' + htmlEncode(itemText) + '</option>'); - $option.data('item', item); - $option.attr('value', itemValue); - self.$element.append($option); - } - - if (!dontPushVal) - self.pushVal(); - - // Add class when reached maxTags - if (self.options.maxTags === self.itemsArray.length || self.items().toString().length === self.options.maxInputLength) - self.$container.addClass('bootstrap-tagsinput-max'); - - self.$element.trigger($.Event('itemAdded', { item: item })); - }, - - /** - * Removes the given item. Pass true to dontPushVal to prevent updating the - * elements val() - */ - remove: function(item, dontPushVal) { - var self = this; - - if (self.objectItems) { - if (typeof item === "object") - item = $.grep(self.itemsArray, function(other) { return self.options.itemValue(other) == self.options.itemValue(item); } ); - else - item = $.grep(self.itemsArray, function(other) { return self.options.itemValue(other) == item; } ); - - item = item[item.length-1]; - } - - if (item) { - var beforeItemRemoveEvent = $.Event('beforeItemRemove', { item: item, cancel: false }); - self.$element.trigger(beforeItemRemoveEvent); - if (beforeItemRemoveEvent.cancel) - return; - - $('.tag', self.$container).filter(function() { return $(this).data('item') === item; }).remove(); - $('option', self.$element).filter(function() { return $(this).data('item') === item; }).remove(); - if($.inArray(item, self.itemsArray) !== -1) - self.itemsArray.splice($.inArray(item, self.itemsArray), 1); - } - - if (!dontPushVal) - self.pushVal(); - - // Remove class when reached maxTags - if (self.options.maxTags > self.itemsArray.length) - self.$container.removeClass('bootstrap-tagsinput-max'); - - self.$element.trigger($.Event('itemRemoved', { item: item })); - }, - - /** - * Removes all items - */ - removeAll: function() { - var self = this; - - $('.tag', self.$container).remove(); - $('option', self.$element).remove(); - - while(self.itemsArray.length > 0) - self.itemsArray.pop(); - - self.pushVal(); - }, - - /** - * Refreshes the tags so they match the text/value of their corresponding - * item. - */ - refresh: function() { - var self = this; - $('.tag', self.$container).each(function() { - var $tag = $(this), - item = $tag.data('item'), - itemValue = self.options.itemValue(item), - itemText = self.options.itemText(item), - tagClass = self.options.tagClass(item); - - // Update tag's class and inner text - $tag.attr('class', null); - $tag.addClass('tag ' + htmlEncode(tagClass)); - $tag.contents().filter(function() { - return this.nodeType == 3; - })[0].nodeValue = htmlEncode(itemText); - - if (self.isSelect) { - var option = $('option', self.$element).filter(function() { return $(this).data('item') === item; }); - option.attr('value', itemValue); - } - }); - }, - - /** - * Returns the items added as tags - */ - items: function() { - return this.itemsArray; - }, - - /** - * Assembly value by retrieving the value of each item, and set it on the - * element. - */ - pushVal: function() { - var self = this, - val = $.map(self.items(), function(item) { - return self.options.itemValue(item).toString(); - }); - - self.$element.val(val, true).trigger('change'); - }, - - /** - * Initializes the tags input behaviour on the element - */ - build: function(options) { - var self = this; - - self.options = $.extend({}, defaultOptions, options); - // When itemValue is set, freeInput should always be false - if (self.objectItems) - self.options.freeInput = false; - - makeOptionItemFunction(self.options, 'itemValue'); - makeOptionItemFunction(self.options, 'itemText'); - makeOptionFunction(self.options, 'tagClass'); - - // Typeahead Bootstrap version 2.3.2 - if (self.options.typeahead) { - var typeahead = self.options.typeahead || {}; - - makeOptionFunction(typeahead, 'source'); - - self.$input.typeahead($.extend({}, typeahead, { - source: function (query, process) { - function processItems(items) { - var texts = []; - - for (var i = 0; i < items.length; i++) { - var text = self.options.itemText(items[i]); - map[text] = items[i]; - texts.push(text); - } - process(texts); - } - - this.map = {}; - var map = this.map, - data = typeahead.source(query); - - if ($.isFunction(data.success)) { - // support for Angular callbacks - data.success(processItems); - } else if ($.isFunction(data.then)) { - // support for Angular promises - data.then(processItems); - } else { - // support for functions and jquery promises - $.when(data) - .then(processItems); - } - }, - updater: function (text) { - self.add(this.map[text]); - }, - matcher: function (text) { - return (text.toLowerCase().indexOf(this.query.trim().toLowerCase()) !== -1); - }, - sorter: function (texts) { - return texts.sort(); - }, - highlighter: function (text) { - var regex = new RegExp( '(' + this.query + ')', 'gi' ); - return text.replace( regex, "<strong>$1</strong>" ); - } - })); - } - - // typeahead.js - if (self.options.typeaheadjs) { - var typeaheadjs = self.options.typeaheadjs || {}; - - self.$input.typeahead(null, typeaheadjs).on('typeahead:selected', $.proxy(function (obj, datum) { - if (typeaheadjs.valueKey) - self.add(datum[typeaheadjs.valueKey]); - else - self.add(datum); - self.$input.typeahead('val', ''); - }, self)); - } - - self.$container.on('click', $.proxy(function(event) { - if (! self.$element.attr('disabled')) { - self.$input.removeAttr('disabled'); - } - self.$input.focus(); - }, self)); - - if (self.options.addOnBlur && self.options.freeInput) { - self.$input.on('focusout', $.proxy(function(event) { - // HACK: only process on focusout when no typeahead opened, to - // avoid adding the typeahead text as tag - if ($('.typeahead, .twitter-typeahead', self.$container).length === 0) { - self.add(self.$input.val()); - self.$input.val(''); - } - }, self)); - } - - - self.$container.on('keydown', 'input', $.proxy(function(event) { - var $input = $(event.target), - $inputWrapper = self.findInputWrapper(); - - if (self.$element.attr('disabled')) { - self.$input.attr('disabled', 'disabled'); - return; - } - - switch (event.which) { - // BACKSPACE - case 8: - if (doGetCaretPosition($input[0]) === 0) { - var prev = $inputWrapper.prev(); - if (prev) { - self.remove(prev.data('item')); - } - } - break; - - // DELETE - case 46: - if (doGetCaretPosition($input[0]) === 0) { - var next = $inputWrapper.next(); - if (next) { - self.remove(next.data('item')); - } - } - break; - - // LEFT ARROW - case 37: - // Try to move the input before the previous tag - var $prevTag = $inputWrapper.prev(); - if ($input.val().length === 0 && $prevTag[0]) { - $prevTag.before($inputWrapper); - $input.focus(); - } - break; - // RIGHT ARROW - case 39: - // Try to move the input after the next tag - var $nextTag = $inputWrapper.next(); - if ($input.val().length === 0 && $nextTag[0]) { - $nextTag.after($inputWrapper); - $input.focus(); - } - break; - default: - // ignore - } - - // Reset internal input's size - var textLength = $input.val().length, - wordSpace = Math.ceil(textLength / 5), - size = textLength + wordSpace + 1; - $input.attr('size', Math.max(this.inputSize, $input.val().length)); - }, self)); - - self.$container.on('keypress', 'input', $.proxy(function(event) { - var $input = $(event.target); - - if (self.$element.attr('disabled')) { - self.$input.attr('disabled', 'disabled'); - return; - } - - var text = $input.val(), - maxLengthReached = self.options.maxChars && text.length >= self.options.maxChars; - if (self.options.freeInput && (keyCombinationInList(event, self.options.confirmKeys) || maxLengthReached)) { - self.add(maxLengthReached ? text.substr(0, self.options.maxChars) : text); - $input.val(''); - event.preventDefault(); - } - - // Reset internal input's size - var textLength = $input.val().length, - wordSpace = Math.ceil(textLength / 5), - size = textLength + wordSpace + 1; - $input.attr('size', Math.max(this.inputSize, $input.val().length)); - }, self)); - - // Remove icon clicked - self.$container.on('click', '[data-role=remove]', $.proxy(function(event) { - if (self.$element.attr('disabled')) { - return; - } - self.remove($(event.target).closest('.tag').data('item')); - }, self)); - - // Only add existing value as tags when using strings as tags - if (self.options.itemValue === defaultOptions.itemValue) { - if (self.$element[0].tagName === 'INPUT') { - self.add(self.$element.val()); - } else { - $('option', self.$element).each(function() { - self.add($(this).attr('value'), true); - }); - } - } - }, - - /** - * Removes all tagsinput behaviour and unregsiter all event handlers - */ - destroy: function() { - var self = this; - - // Unbind events - self.$container.off('keypress', 'input'); - self.$container.off('click', '[role=remove]'); - - self.$container.remove(); - self.$element.removeData('tagsinput'); - self.$element.show(); - }, - - /** - * Sets focus on the tagsinput - */ - focus: function() { - this.$input.focus(); - }, - - /** - * Returns the internal input element - */ - input: function() { - return this.$input; - }, - - /** - * Returns the element which is wrapped around the internal input. This - * is normally the $container, but typeahead.js moves the $input element. - */ - findInputWrapper: function() { - var elt = this.$input[0], - container = this.$container[0]; - while(elt && elt.parentNode !== container) - elt = elt.parentNode; - - return $(elt); - } - }; - - /** - * Register JQuery plugin - */ - $.fn.tagsinput = function(arg1, arg2) { - var results = []; - - this.each(function() { - var tagsinput = $(this).data('tagsinput'); - // Initialize a new tags input - if (!tagsinput) { - tagsinput = new TagsInput(this, arg1); - $(this).data('tagsinput', tagsinput); - results.push(tagsinput); - - if (this.tagName === 'SELECT') { - $('option', $(this)).attr('selected', 'selected'); - } - - // Init tags from $(this).val() - $(this).val($(this).val()); - } else if (!arg1 && !arg2) { - // tagsinput already exists - // no function, trying to init - results.push(tagsinput); - } else if(tagsinput[arg1] !== undefined) { - // Invoke function on existing tags input - var retVal = tagsinput[arg1](arg2); - if (retVal !== undefined) - results.push(retVal); - } - }); - - if ( typeof arg1 == 'string') { - // Return the results from the invoked function calls - return results.length > 1 ? results : results[0]; - } else { - return results; - } - }; - - $.fn.tagsinput.Constructor = TagsInput; - - /** - * Most options support both a string or number as well as a function as - * option value. This function makes sure that the option with the given - * key in the given options is wrapped in a function - */ - function makeOptionItemFunction(options, key) { - if (typeof options[key] !== 'function') { - var propertyName = options[key]; - options[key] = function(item) { return item[propertyName]; }; - } - } - function makeOptionFunction(options, key) { - if (typeof options[key] !== 'function') { - var value = options[key]; - options[key] = function() { return value; }; - } - } - /** - * HtmlEncodes the given value - */ - var htmlEncodeContainer = $('<div />'); - function htmlEncode(value) { - if (value) { - return htmlEncodeContainer.text(value).html(); - } else { - return ''; - } - } - - /** - * Returns the position of the caret in the given input field - * http://flightschool.acylt.com/devnotes/caret-position-woes/ - */ - function doGetCaretPosition(oField) { - var iCaretPos = 0; - if (document.selection) { - oField.focus (); - var oSel = document.selection.createRange(); - oSel.moveStart ('character', -oField.value.length); - iCaretPos = oSel.text.length; - } else if (oField.selectionStart || oField.selectionStart == '0') { - iCaretPos = oField.selectionStart; - } - return (iCaretPos); - } - - /** - * Returns boolean indicates whether user has pressed an expected key combination. - * @param object keyPressEvent: JavaScript event object, refer - * http://www.w3.org/TR/2003/WD-DOM-Level-3-Events-20030331/ecma-script-binding.html - * @param object lookupList: expected key combinations, as in: - * [13, {which: 188, shiftKey: true}] - */ - function keyCombinationInList(keyPressEvent, lookupList) { - var found = false; - $.each(lookupList, function (index, keyCombination) { - if (typeof (keyCombination) === 'number' && keyPressEvent.which === keyCombination) { - found = true; - return false; - } - - if (keyPressEvent.which === keyCombination.which) { - var alt = !keyCombination.hasOwnProperty('altKey') || keyPressEvent.altKey === keyCombination.altKey, - shift = !keyCombination.hasOwnProperty('shiftKey') || keyPressEvent.shiftKey === keyCombination.shiftKey, - ctrl = !keyCombination.hasOwnProperty('ctrlKey') || keyPressEvent.ctrlKey === keyCombination.ctrlKey; - if (alt && shift && ctrl) { - found = true; - return false; - } - } - }); - - return found; - } - - /** - * Initialize tagsinput behaviour on inputs and selects which have - * data-role=tagsinput - */ - $(function() { - $("input[data-role=tagsinput], select[multiple][data-role=tagsinput]").tagsinput(); - }); -})(window.jQuery); diff --git a/src/UI/JsLibraries/filesize.js b/src/UI/JsLibraries/filesize.js deleted file mode 100644 index 74f600064..000000000 --- a/src/UI/JsLibraries/filesize.js +++ /dev/null @@ -1,141 +0,0 @@ -/** - * filesize - * - * @author Jason Mulligan <jason.mulligan@avoidwork.com> - * @copyright 2013 Jason Mulligan - * @license BSD-3 <https://raw.github.com/avoidwork/filesize.js/master/LICENSE> - * @link http://filesizejs.com - * @module filesize - * @version 2.0.0 - */ -( function ( global ) { -"use strict"; - -var bit = /b$/, - bite = /^B$/, - radix = 10, - right = /\.(.*)/, - zero = /^0$/; - -/** - * filesize - * - * @method filesize - * @param {Mixed} arg String, Int or Float to transform - * @param {Object} descriptor [Optional] Flags - * @return {String} Readable file size String - */ -function filesize ( arg, descriptor ) { - var result = "", - skip = false, - i = 6, - base, bits, neg, num, round, size, sizes, unix, spacer, suffix, z; - - if ( isNaN( arg ) ) { - throw new Error( "Invalid arguments" ); - } - - descriptor = descriptor || {}; - bits = ( descriptor.bits === true ); - unix = ( descriptor.unix === true ); - base = descriptor.base !== undefined ? descriptor.base : unix ? 2 : 10; - round = descriptor.round !== undefined ? descriptor.round : unix ? 1 : 2; - spacer = descriptor.spacer !== undefined ? descriptor.spacer : unix ? "" : " "; - num = Number( arg ); - neg = ( num < 0 ); - - // Flipping a negative number to determine the size - if ( neg ) { - num = -num; - } - - // Zero is now a special case because bytes divide by 1 - if ( num === 0 ) { - if ( unix ) { - result = "0"; - } - else { - result = "0" + spacer + "B"; - } - } - else { - sizes = options[base][bits ? "bits" : "bytes"]; - - while ( i-- ) { - size = sizes[i][1]; - suffix = sizes[i][0]; - - if ( num >= size ) { - // Treating bytes as cardinal - if ( bite.test( suffix ) ) { - skip = true; - round = 0; - } - - result = ( num / size ).toFixed( round ); - - if ( !skip && unix ) { - if ( bits && bit.test( suffix ) ) { - suffix = suffix.toLowerCase(); - } - - suffix = suffix.charAt( 0 ); - z = right.exec( result ); - - if ( !bits && suffix === "k" ) { - suffix = "K"; - } - - if ( z !== null && z[1] !== undefined && zero.test( z[1] ) ) { - result = parseInt( result, radix ); - } - - result += spacer + suffix; - } - else if ( !unix ) { - result += spacer + suffix; - } - - break; - } - } - } - - // Decorating a 'diff' - if ( neg ) { - result = "-" + result; - } - - return result; -} - -/** - * Size options - * - * @type {Object} - */ -var options = { - 2 : { - bits : [["B", 1], ["kb", 128], ["Mb", 131072], ["Gb", 134217728], ["Tb", 137438953472], ["Pb", 140737488355328]], - bytes : [["B", 1], ["kB", 1024], ["MB", 1048576], ["GB", 1073741824], ["TB", 1099511627776], ["PB", 1125899906842624]] - }, - 10 : { - bits : [["B", 1], ["kb", 125], ["Mb", 125000], ["Gb", 125000000], ["Tb", 125000000000], ["Pb", 125000000000000]], - bytes : [["B", 1], ["kB", 1000], ["MB", 1000000], ["GB", 1000000000], ["TB", 1000000000000], ["PB", 1000000000000000]] - } -}; - -// CommonJS, AMD, script tag -if ( typeof exports !== "undefined" ) { - module.exports = filesize; -} -else if ( typeof define === "function" ) { - define( function () { - return filesize; - } ); -} -else { - global.filesize = filesize; -} - -} )( this ); diff --git a/src/UI/JsLibraries/fullcalendar.js b/src/UI/JsLibraries/fullcalendar.js deleted file mode 100644 index d4c97f0dc..000000000 --- a/src/UI/JsLibraries/fullcalendar.js +++ /dev/null @@ -1,15591 +0,0 @@ -/*! - * FullCalendar v3.4.0 - * Docs & License: https://fullcalendar.io/ - * (c) 2017 Adam Shaw - */ - -(function(factory) { - if (typeof define === 'function' && define.amd) { - define([ 'jquery', 'moment' ], factory); - } - else if (typeof exports === 'object') { // Node/CommonJS - module.exports = factory(require('jquery'), require('moment')); - } - else { - factory(jQuery, moment); - } -})(function($, moment) { - -;; - -var FC = $.fullCalendar = { - version: "3.4.0", - // When introducing internal API incompatibilities (where fullcalendar plugins would break), - // the minor version of the calendar should be upped (ex: 2.7.2 -> 2.8.0) - // and the below integer should be incremented. - internalApiVersion: 9 -}; -var fcViews = FC.views = {}; - - -$.fn.fullCalendar = function(options) { - var args = Array.prototype.slice.call(arguments, 1); // for a possible method call - var res = this; // what this function will return (this jQuery object by default) - - this.each(function(i, _element) { // loop each DOM element involved - var element = $(_element); - var calendar = element.data('fullCalendar'); // get the existing calendar object (if any) - var singleRes; // the returned value of this single method call - - // a method call - if (typeof options === 'string') { - if (calendar && $.isFunction(calendar[options])) { - singleRes = calendar[options].apply(calendar, args); - if (!i) { - res = singleRes; // record the first method call result - } - if (options === 'destroy') { // for the destroy method, must remove Calendar object data - element.removeData('fullCalendar'); - } - } - } - // a new calendar initialization - else if (!calendar) { // don't initialize twice - calendar = new Calendar(element, options); - element.data('fullCalendar', calendar); - calendar.render(); - } - }); - - return res; -}; - - -var complexOptions = [ // names of options that are objects whose properties should be combined - 'header', - 'footer', - 'buttonText', - 'buttonIcons', - 'themeButtonIcons' -]; - - -// Merges an array of option objects into a single object -function mergeOptions(optionObjs) { - return mergeProps(optionObjs, complexOptions); -} - -;; - -// exports -FC.intersectRanges = intersectRanges; -FC.applyAll = applyAll; -FC.debounce = debounce; -FC.isInt = isInt; -FC.htmlEscape = htmlEscape; -FC.cssToStr = cssToStr; -FC.proxy = proxy; -FC.capitaliseFirstLetter = capitaliseFirstLetter; - - -/* FullCalendar-specific DOM Utilities -----------------------------------------------------------------------------------------------------------------------*/ - - -// Given the scrollbar widths of some other container, create borders/margins on rowEls in order to match the left -// and right space that was offset by the scrollbars. A 1-pixel border first, then margin beyond that. -function compensateScroll(rowEls, scrollbarWidths) { - if (scrollbarWidths.left) { - rowEls.css({ - 'border-left-width': 1, - 'margin-left': scrollbarWidths.left - 1 - }); - } - if (scrollbarWidths.right) { - rowEls.css({ - 'border-right-width': 1, - 'margin-right': scrollbarWidths.right - 1 - }); - } -} - - -// Undoes compensateScroll and restores all borders/margins -function uncompensateScroll(rowEls) { - rowEls.css({ - 'margin-left': '', - 'margin-right': '', - 'border-left-width': '', - 'border-right-width': '' - }); -} - - -// Make the mouse cursor express that an event is not allowed in the current area -function disableCursor() { - $('body').addClass('fc-not-allowed'); -} - - -// Returns the mouse cursor to its original look -function enableCursor() { - $('body').removeClass('fc-not-allowed'); -} - - -// Given a total available height to fill, have `els` (essentially child rows) expand to accomodate. -// By default, all elements that are shorter than the recommended height are expanded uniformly, not considering -// any other els that are already too tall. if `shouldRedistribute` is on, it considers these tall rows and -// reduces the available height. -function distributeHeight(els, availableHeight, shouldRedistribute) { - - // *FLOORING NOTE*: we floor in certain places because zoom can give inaccurate floating-point dimensions, - // and it is better to be shorter than taller, to avoid creating unnecessary scrollbars. - - var minOffset1 = Math.floor(availableHeight / els.length); // for non-last element - var minOffset2 = Math.floor(availableHeight - minOffset1 * (els.length - 1)); // for last element *FLOORING NOTE* - var flexEls = []; // elements that are allowed to expand. array of DOM nodes - var flexOffsets = []; // amount of vertical space it takes up - var flexHeights = []; // actual css height - var usedHeight = 0; - - undistributeHeight(els); // give all elements their natural height - - // find elements that are below the recommended height (expandable). - // important to query for heights in a single first pass (to avoid reflow oscillation). - els.each(function(i, el) { - var minOffset = i === els.length - 1 ? minOffset2 : minOffset1; - var naturalOffset = $(el).outerHeight(true); - - if (naturalOffset < minOffset) { - flexEls.push(el); - flexOffsets.push(naturalOffset); - flexHeights.push($(el).height()); - } - else { - // this element stretches past recommended height (non-expandable). mark the space as occupied. - usedHeight += naturalOffset; - } - }); - - // readjust the recommended height to only consider the height available to non-maxed-out rows. - if (shouldRedistribute) { - availableHeight -= usedHeight; - minOffset1 = Math.floor(availableHeight / flexEls.length); - minOffset2 = Math.floor(availableHeight - minOffset1 * (flexEls.length - 1)); // *FLOORING NOTE* - } - - // assign heights to all expandable elements - $(flexEls).each(function(i, el) { - var minOffset = i === flexEls.length - 1 ? minOffset2 : minOffset1; - var naturalOffset = flexOffsets[i]; - var naturalHeight = flexHeights[i]; - var newHeight = minOffset - (naturalOffset - naturalHeight); // subtract the margin/padding - - if (naturalOffset < minOffset) { // we check this again because redistribution might have changed things - $(el).height(newHeight); - } - }); -} - - -// Undoes distrubuteHeight, restoring all els to their natural height -function undistributeHeight(els) { - els.height(''); -} - - -// Given `els`, a jQuery set of <td> cells, find the cell with the largest natural width and set the widths of all the -// cells to be that width. -// PREREQUISITE: if you want a cell to take up width, it needs to have a single inner element w/ display:inline -function matchCellWidths(els) { - var maxInnerWidth = 0; - - els.find('> *').each(function(i, innerEl) { - var innerWidth = $(innerEl).outerWidth(); - if (innerWidth > maxInnerWidth) { - maxInnerWidth = innerWidth; - } - }); - - maxInnerWidth++; // sometimes not accurate of width the text needs to stay on one line. insurance - - els.width(maxInnerWidth); - - return maxInnerWidth; -} - - -// Given one element that resides inside another, -// Subtracts the height of the inner element from the outer element. -function subtractInnerElHeight(outerEl, innerEl) { - var both = outerEl.add(innerEl); - var diff; - - // effin' IE8/9/10/11 sometimes returns 0 for dimensions. this weird hack was the only thing that worked - both.css({ - position: 'relative', // cause a reflow, which will force fresh dimension recalculation - left: -1 // ensure reflow in case the el was already relative. negative is less likely to cause new scroll - }); - diff = outerEl.outerHeight() - innerEl.outerHeight(); // grab the dimensions - both.css({ position: '', left: '' }); // undo hack - - return diff; -} - - -/* Element Geom Utilities -----------------------------------------------------------------------------------------------------------------------*/ - -FC.getOuterRect = getOuterRect; -FC.getClientRect = getClientRect; -FC.getContentRect = getContentRect; -FC.getScrollbarWidths = getScrollbarWidths; - - -// borrowed from https://github.com/jquery/jquery-ui/blob/1.11.0/ui/core.js#L51 -function getScrollParent(el) { - var position = el.css('position'), - scrollParent = el.parents().filter(function() { - var parent = $(this); - return (/(auto|scroll)/).test( - parent.css('overflow') + parent.css('overflow-y') + parent.css('overflow-x') - ); - }).eq(0); - - return position === 'fixed' || !scrollParent.length ? $(el[0].ownerDocument || document) : scrollParent; -} - - -// Queries the outer bounding area of a jQuery element. -// Returns a rectangle with absolute coordinates: left, right (exclusive), top, bottom (exclusive). -// Origin is optional. -function getOuterRect(el, origin) { - var offset = el.offset(); - var left = offset.left - (origin ? origin.left : 0); - var top = offset.top - (origin ? origin.top : 0); - - return { - left: left, - right: left + el.outerWidth(), - top: top, - bottom: top + el.outerHeight() - }; -} - - -// Queries the area within the margin/border/scrollbars of a jQuery element. Does not go within the padding. -// Returns a rectangle with absolute coordinates: left, right (exclusive), top, bottom (exclusive). -// Origin is optional. -// WARNING: given element can't have borders -// NOTE: should use clientLeft/clientTop, but very unreliable cross-browser. -function getClientRect(el, origin) { - var offset = el.offset(); - var scrollbarWidths = getScrollbarWidths(el); - var left = offset.left + getCssFloat(el, 'border-left-width') + scrollbarWidths.left - (origin ? origin.left : 0); - var top = offset.top + getCssFloat(el, 'border-top-width') + scrollbarWidths.top - (origin ? origin.top : 0); - - return { - left: left, - right: left + el[0].clientWidth, // clientWidth includes padding but NOT scrollbars - top: top, - bottom: top + el[0].clientHeight // clientHeight includes padding but NOT scrollbars - }; -} - - -// Queries the area within the margin/border/padding of a jQuery element. Assumed not to have scrollbars. -// Returns a rectangle with absolute coordinates: left, right (exclusive), top, bottom (exclusive). -// Origin is optional. -function getContentRect(el, origin) { - var offset = el.offset(); // just outside of border, margin not included - var left = offset.left + getCssFloat(el, 'border-left-width') + getCssFloat(el, 'padding-left') - - (origin ? origin.left : 0); - var top = offset.top + getCssFloat(el, 'border-top-width') + getCssFloat(el, 'padding-top') - - (origin ? origin.top : 0); - - return { - left: left, - right: left + el.width(), - top: top, - bottom: top + el.height() - }; -} - - -// Returns the computed left/right/top/bottom scrollbar widths for the given jQuery element. -// WARNING: given element can't have borders (which will cause offsetWidth/offsetHeight to be larger). -// NOTE: should use clientLeft/clientTop, but very unreliable cross-browser. -function getScrollbarWidths(el) { - var leftRightWidth = el[0].offsetWidth - el[0].clientWidth; - var bottomWidth = el[0].offsetHeight - el[0].clientHeight; - var widths; - - leftRightWidth = sanitizeScrollbarWidth(leftRightWidth); - bottomWidth = sanitizeScrollbarWidth(bottomWidth); - - widths = { left: 0, right: 0, top: 0, bottom: bottomWidth }; - - if (getIsLeftRtlScrollbars() && el.css('direction') == 'rtl') { // is the scrollbar on the left side? - widths.left = leftRightWidth; - } - else { - widths.right = leftRightWidth; - } - - return widths; -} - - -// The scrollbar width computations in getScrollbarWidths are sometimes flawed when it comes to -// retina displays, rounding, and IE11. Massage them into a usable value. -function sanitizeScrollbarWidth(width) { - width = Math.max(0, width); // no negatives - width = Math.round(width); - return width; -} - - -// Logic for determining if, when the element is right-to-left, the scrollbar appears on the left side - -var _isLeftRtlScrollbars = null; - -function getIsLeftRtlScrollbars() { // responsible for caching the computation - if (_isLeftRtlScrollbars === null) { - _isLeftRtlScrollbars = computeIsLeftRtlScrollbars(); - } - return _isLeftRtlScrollbars; -} - -function computeIsLeftRtlScrollbars() { // creates an offscreen test element, then removes it - var el = $('<div><div/></div>') - .css({ - position: 'absolute', - top: -1000, - left: 0, - border: 0, - padding: 0, - overflow: 'scroll', - direction: 'rtl' - }) - .appendTo('body'); - var innerEl = el.children(); - var res = innerEl.offset().left > el.offset().left; // is the inner div shifted to accommodate a left scrollbar? - el.remove(); - return res; -} - - -// Retrieves a jQuery element's computed CSS value as a floating-point number. -// If the queried value is non-numeric (ex: IE can return "medium" for border width), will just return zero. -function getCssFloat(el, prop) { - return parseFloat(el.css(prop)) || 0; -} - - -/* Mouse / Touch Utilities -----------------------------------------------------------------------------------------------------------------------*/ - -FC.preventDefault = preventDefault; - - -// Returns a boolean whether this was a left mouse click and no ctrl key (which means right click on Mac) -function isPrimaryMouseButton(ev) { - return ev.which == 1 && !ev.ctrlKey; -} - - -function getEvX(ev) { - var touches = ev.originalEvent.touches; - - // on mobile FF, pageX for touch events is present, but incorrect, - // so, look at touch coordinates first. - if (touches && touches.length) { - return touches[0].pageX; - } - - return ev.pageX; -} - - -function getEvY(ev) { - var touches = ev.originalEvent.touches; - - // on mobile FF, pageX for touch events is present, but incorrect, - // so, look at touch coordinates first. - if (touches && touches.length) { - return touches[0].pageY; - } - - return ev.pageY; -} - - -function getEvIsTouch(ev) { - return /^touch/.test(ev.type); -} - - -function preventSelection(el) { - el.addClass('fc-unselectable') - .on('selectstart', preventDefault); -} - - -function allowSelection(el) { - el.removeClass('fc-unselectable') - .off('selectstart', preventDefault); -} - - -// Stops a mouse/touch event from doing it's native browser action -function preventDefault(ev) { - ev.preventDefault(); -} - - -/* General Geometry Utils -----------------------------------------------------------------------------------------------------------------------*/ - -FC.intersectRects = intersectRects; - -// Returns a new rectangle that is the intersection of the two rectangles. If they don't intersect, returns false -function intersectRects(rect1, rect2) { - var res = { - left: Math.max(rect1.left, rect2.left), - right: Math.min(rect1.right, rect2.right), - top: Math.max(rect1.top, rect2.top), - bottom: Math.min(rect1.bottom, rect2.bottom) - }; - - if (res.left < res.right && res.top < res.bottom) { - return res; - } - return false; -} - - -// Returns a new point that will have been moved to reside within the given rectangle -function constrainPoint(point, rect) { - return { - left: Math.min(Math.max(point.left, rect.left), rect.right), - top: Math.min(Math.max(point.top, rect.top), rect.bottom) - }; -} - - -// Returns a point that is the center of the given rectangle -function getRectCenter(rect) { - return { - left: (rect.left + rect.right) / 2, - top: (rect.top + rect.bottom) / 2 - }; -} - - -// Subtracts point2's coordinates from point1's coordinates, returning a delta -function diffPoints(point1, point2) { - return { - left: point1.left - point2.left, - top: point1.top - point2.top - }; -} - - -/* Object Ordering by Field -----------------------------------------------------------------------------------------------------------------------*/ - -FC.parseFieldSpecs = parseFieldSpecs; -FC.compareByFieldSpecs = compareByFieldSpecs; -FC.compareByFieldSpec = compareByFieldSpec; -FC.flexibleCompare = flexibleCompare; - - -function parseFieldSpecs(input) { - var specs = []; - var tokens = []; - var i, token; - - if (typeof input === 'string') { - tokens = input.split(/\s*,\s*/); - } - else if (typeof input === 'function') { - tokens = [ input ]; - } - else if ($.isArray(input)) { - tokens = input; - } - - for (i = 0; i < tokens.length; i++) { - token = tokens[i]; - - if (typeof token === 'string') { - specs.push( - token.charAt(0) == '-' ? - { field: token.substring(1), order: -1 } : - { field: token, order: 1 } - ); - } - else if (typeof token === 'function') { - specs.push({ func: token }); - } - } - - return specs; -} - - -function compareByFieldSpecs(obj1, obj2, fieldSpecs) { - var i; - var cmp; - - for (i = 0; i < fieldSpecs.length; i++) { - cmp = compareByFieldSpec(obj1, obj2, fieldSpecs[i]); - if (cmp) { - return cmp; - } - } - - return 0; -} - - -function compareByFieldSpec(obj1, obj2, fieldSpec) { - if (fieldSpec.func) { - return fieldSpec.func(obj1, obj2); - } - return flexibleCompare(obj1[fieldSpec.field], obj2[fieldSpec.field]) * - (fieldSpec.order || 1); -} - - -function flexibleCompare(a, b) { - if (!a && !b) { - return 0; - } - if (b == null) { - return -1; - } - if (a == null) { - return 1; - } - if ($.type(a) === 'string' || $.type(b) === 'string') { - return String(a).localeCompare(String(b)); - } - return a - b; -} - - -/* FullCalendar-specific Misc Utilities -----------------------------------------------------------------------------------------------------------------------*/ - - -// Computes the intersection of the two ranges. Will return fresh date clones in a range. -// Returns undefined if no intersection. -// Expects all dates to be normalized to the same timezone beforehand. -// TODO: move to date section? -function intersectRanges(subjectRange, constraintRange) { - var subjectStart = subjectRange.start; - var subjectEnd = subjectRange.end; - var constraintStart = constraintRange.start; - var constraintEnd = constraintRange.end; - var segStart, segEnd; - var isStart, isEnd; - - if (subjectEnd > constraintStart && subjectStart < constraintEnd) { // in bounds at all? - - if (subjectStart >= constraintStart) { - segStart = subjectStart.clone(); - isStart = true; - } - else { - segStart = constraintStart.clone(); - isStart = false; - } - - if (subjectEnd <= constraintEnd) { - segEnd = subjectEnd.clone(); - isEnd = true; - } - else { - segEnd = constraintEnd.clone(); - isEnd = false; - } - - return { - start: segStart, - end: segEnd, - isStart: isStart, - isEnd: isEnd - }; - } -} - - -/* Date Utilities -----------------------------------------------------------------------------------------------------------------------*/ - -FC.computeGreatestUnit = computeGreatestUnit; -FC.divideRangeByDuration = divideRangeByDuration; -FC.divideDurationByDuration = divideDurationByDuration; -FC.multiplyDuration = multiplyDuration; -FC.durationHasTime = durationHasTime; - -var dayIDs = [ 'sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat' ]; -var unitsDesc = [ 'year', 'month', 'week', 'day', 'hour', 'minute', 'second', 'millisecond' ]; // descending - - -// Diffs the two moments into a Duration where full-days are recorded first, then the remaining time. -// Moments will have their timezones normalized. -function diffDayTime(a, b) { - return moment.duration({ - days: a.clone().stripTime().diff(b.clone().stripTime(), 'days'), - ms: a.time() - b.time() // time-of-day from day start. disregards timezone - }); -} - - -// Diffs the two moments via their start-of-day (regardless of timezone). Produces whole-day durations. -function diffDay(a, b) { - return moment.duration({ - days: a.clone().stripTime().diff(b.clone().stripTime(), 'days') - }); -} - - -// Diffs two moments, producing a duration, made of a whole-unit-increment of the given unit. Uses rounding. -function diffByUnit(a, b, unit) { - return moment.duration( - Math.round(a.diff(b, unit, true)), // returnFloat=true - unit - ); -} - - -// Computes the unit name of the largest whole-unit period of time. -// For example, 48 hours will be "days" whereas 49 hours will be "hours". -// Accepts start/end, a range object, or an original duration object. -function computeGreatestUnit(start, end) { - var i, unit; - var val; - - for (i = 0; i < unitsDesc.length; i++) { - unit = unitsDesc[i]; - val = computeRangeAs(unit, start, end); - - if (val >= 1 && isInt(val)) { - break; - } - } - - return unit; // will be "milliseconds" if nothing else matches -} - - -// like computeGreatestUnit, but has special abilities to interpret the source input for clues -function computeDurationGreatestUnit(duration, durationInput) { - var unit = computeGreatestUnit(duration); - - // prevent days:7 from being interpreted as a week - if (unit === 'week' && typeof durationInput === 'object' && durationInput.days) { - unit = 'day'; - } - - return unit; -} - - -// Computes the number of units (like "hours") in the given range. -// Range can be a {start,end} object, separate start/end args, or a Duration. -// Results are based on Moment's .as() and .diff() methods, so results can depend on internal handling -// of month-diffing logic (which tends to vary from version to version). -function computeRangeAs(unit, start, end) { - - if (end != null) { // given start, end - return end.diff(start, unit, true); - } - else if (moment.isDuration(start)) { // given duration - return start.as(unit); - } - else { // given { start, end } range object - return start.end.diff(start.start, unit, true); - } -} - - -// Intelligently divides a range (specified by a start/end params) by a duration -function divideRangeByDuration(start, end, dur) { - var months; - - if (durationHasTime(dur)) { - return (end - start) / dur; - } - months = dur.asMonths(); - if (Math.abs(months) >= 1 && isInt(months)) { - return end.diff(start, 'months', true) / months; - } - return end.diff(start, 'days', true) / dur.asDays(); -} - - -// Intelligently divides one duration by another -function divideDurationByDuration(dur1, dur2) { - var months1, months2; - - if (durationHasTime(dur1) || durationHasTime(dur2)) { - return dur1 / dur2; - } - months1 = dur1.asMonths(); - months2 = dur2.asMonths(); - if ( - Math.abs(months1) >= 1 && isInt(months1) && - Math.abs(months2) >= 1 && isInt(months2) - ) { - return months1 / months2; - } - return dur1.asDays() / dur2.asDays(); -} - - -// Intelligently multiplies a duration by a number -function multiplyDuration(dur, n) { - var months; - - if (durationHasTime(dur)) { - return moment.duration(dur * n); - } - months = dur.asMonths(); - if (Math.abs(months) >= 1 && isInt(months)) { - return moment.duration({ months: months * n }); - } - return moment.duration({ days: dur.asDays() * n }); -} - - -function cloneRange(range) { - return { - start: range.start.clone(), - end: range.end.clone() - }; -} - - -// Trims the beginning and end of inner range to be completely within outerRange. -// Returns a new range object. -function constrainRange(innerRange, outerRange) { - innerRange = cloneRange(innerRange); - - if (outerRange.start) { - // needs to be inclusively before outerRange's end - innerRange.start = constrainDate(innerRange.start, outerRange); - } - - if (outerRange.end) { - innerRange.end = minMoment(innerRange.end, outerRange.end); - } - - return innerRange; -} - - -// If the given date is not within the given range, move it inside. -// (If it's past the end, make it one millisecond before the end). -// Always returns a new moment. -function constrainDate(date, range) { - date = date.clone(); - - if (range.start) { - date = maxMoment(date, range.start); - } - - if (range.end && date >= range.end) { - date = range.end.clone().subtract(1); - } - - return date; -} - - -function isDateWithinRange(date, range) { - return (!range.start || date >= range.start) && - (!range.end || date < range.end); -} - - -// TODO: deal with repeat code in intersectRanges -// constraintRange can have unspecified start/end, an open-ended range. -function doRangesIntersect(subjectRange, constraintRange) { - return (!constraintRange.start || subjectRange.end >= constraintRange.start) && - (!constraintRange.end || subjectRange.start < constraintRange.end); -} - - -function isRangeWithinRange(innerRange, outerRange) { - return (!outerRange.start || innerRange.start >= outerRange.start) && - (!outerRange.end || innerRange.end <= outerRange.end); -} - - -function isRangesEqual(range0, range1) { - return ((range0.start && range1.start && range0.start.isSame(range1.start)) || (!range0.start && !range1.start)) && - ((range0.end && range1.end && range0.end.isSame(range1.end)) || (!range0.end && !range1.end)); -} - - -// Returns the moment that's earlier in time. Always a copy. -function minMoment(mom1, mom2) { - return (mom1.isBefore(mom2) ? mom1 : mom2).clone(); -} - - -// Returns the moment that's later in time. Always a copy. -function maxMoment(mom1, mom2) { - return (mom1.isAfter(mom2) ? mom1 : mom2).clone(); -} - - -// Returns a boolean about whether the given duration has any time parts (hours/minutes/seconds/ms) -function durationHasTime(dur) { - return Boolean(dur.hours() || dur.minutes() || dur.seconds() || dur.milliseconds()); -} - - -function isNativeDate(input) { - return Object.prototype.toString.call(input) === '[object Date]' || input instanceof Date; -} - - -// Returns a boolean about whether the given input is a time string, like "06:40:00" or "06:00" -function isTimeString(str) { - return /^\d+\:\d+(?:\:\d+\.?(?:\d{3})?)?$/.test(str); -} - - -/* Logging and Debug -----------------------------------------------------------------------------------------------------------------------*/ - -FC.log = function() { - var console = window.console; - - if (console && console.log) { - return console.log.apply(console, arguments); - } -}; - -FC.warn = function() { - var console = window.console; - - if (console && console.warn) { - return console.warn.apply(console, arguments); - } - else { - return FC.log.apply(FC, arguments); - } -}; - - -/* General Utilities -----------------------------------------------------------------------------------------------------------------------*/ - -var hasOwnPropMethod = {}.hasOwnProperty; - - -// Merges an array of objects into a single object. -// The second argument allows for an array of property names who's object values will be merged together. -function mergeProps(propObjs, complexProps) { - var dest = {}; - var i, name; - var complexObjs; - var j, val; - var props; - - if (complexProps) { - for (i = 0; i < complexProps.length; i++) { - name = complexProps[i]; - complexObjs = []; - - // collect the trailing object values, stopping when a non-object is discovered - for (j = propObjs.length - 1; j >= 0; j--) { - val = propObjs[j][name]; - - if (typeof val === 'object') { - complexObjs.unshift(val); - } - else if (val !== undefined) { - dest[name] = val; // if there were no objects, this value will be used - break; - } - } - - // if the trailing values were objects, use the merged value - if (complexObjs.length) { - dest[name] = mergeProps(complexObjs); - } - } - } - - // copy values into the destination, going from last to first - for (i = propObjs.length - 1; i >= 0; i--) { - props = propObjs[i]; - - for (name in props) { - if (!(name in dest)) { // if already assigned by previous props or complex props, don't reassign - dest[name] = props[name]; - } - } - } - - return dest; -} - - -// Create an object that has the given prototype. Just like Object.create -function createObject(proto) { - var f = function() {}; - f.prototype = proto; - return new f(); -} -FC.createObject = createObject; - - -function copyOwnProps(src, dest) { - for (var name in src) { - if (hasOwnProp(src, name)) { - dest[name] = src[name]; - } - } -} - - -function hasOwnProp(obj, name) { - return hasOwnPropMethod.call(obj, name); -} - - -// Is the given value a non-object non-function value? -function isAtomic(val) { - return /undefined|null|boolean|number|string/.test($.type(val)); -} - - -function applyAll(functions, thisObj, args) { - if ($.isFunction(functions)) { - functions = [ functions ]; - } - if (functions) { - var i; - var ret; - for (i=0; i<functions.length; i++) { - ret = functions[i].apply(thisObj, args) || ret; - } - return ret; - } -} - - -function firstDefined() { - for (var i=0; i<arguments.length; i++) { - if (arguments[i] !== undefined) { - return arguments[i]; - } - } -} - - -function htmlEscape(s) { - return (s + '').replace(/&/g, '&') - .replace(/</g, '<') - .replace(/>/g, '>') - .replace(/'/g, ''') - .replace(/"/g, '"') - .replace(/\n/g, '<br />'); -} - - -function stripHtmlEntities(text) { - return text.replace(/&.*?;/g, ''); -} - - -// Given a hash of CSS properties, returns a string of CSS. -// Uses property names as-is (no camel-case conversion). Will not make statements for null/undefined values. -function cssToStr(cssProps) { - var statements = []; - - $.each(cssProps, function(name, val) { - if (val != null) { - statements.push(name + ':' + val); - } - }); - - return statements.join(';'); -} - - -// Given an object hash of HTML attribute names to values, -// generates a string that can be injected between < > in HTML -function attrsToStr(attrs) { - var parts = []; - - $.each(attrs, function(name, val) { - if (val != null) { - parts.push(name + '="' + htmlEscape(val) + '"'); - } - }); - - return parts.join(' '); -} - - -function capitaliseFirstLetter(str) { - return str.charAt(0).toUpperCase() + str.slice(1); -} - - -function compareNumbers(a, b) { // for .sort() - return a - b; -} - - -function isInt(n) { - return n % 1 === 0; -} - - -// Returns a method bound to the given object context. -// Just like one of the jQuery.proxy signatures, but without the undesired behavior of treating the same method with -// different contexts as identical when binding/unbinding events. -function proxy(obj, methodName) { - var method = obj[methodName]; - - return function() { - return method.apply(obj, arguments); - }; -} - - -// Returns a function, that, as long as it continues to be invoked, will not -// be triggered. The function will be called after it stops being called for -// N milliseconds. If `immediate` is passed, trigger the function on the -// leading edge, instead of the trailing. -// https://github.com/jashkenas/underscore/blob/1.6.0/underscore.js#L714 -function debounce(func, wait, immediate) { - var timeout, args, context, timestamp, result; - - var later = function() { - var last = +new Date() - timestamp; - if (last < wait) { - timeout = setTimeout(later, wait - last); - } - else { - timeout = null; - if (!immediate) { - result = func.apply(context, args); - context = args = null; - } - } - }; - - return function() { - context = this; - args = arguments; - timestamp = +new Date(); - var callNow = immediate && !timeout; - if (!timeout) { - timeout = setTimeout(later, wait); - } - if (callNow) { - result = func.apply(context, args); - context = args = null; - } - return result; - }; -} - -;; - -/* -GENERAL NOTE on moments throughout the *entire rest* of the codebase: -All moments are assumed to be ambiguously-zoned unless otherwise noted, -with the NOTABLE EXCEOPTION of start/end dates that live on *Event Objects*. -Ambiguously-TIMED moments are assumed to be ambiguously-zoned by nature. -*/ - -var ambigDateOfMonthRegex = /^\s*\d{4}-\d\d$/; -var ambigTimeOrZoneRegex = - /^\s*\d{4}-(?:(\d\d-\d\d)|(W\d\d$)|(W\d\d-\d)|(\d\d\d))((T| )(\d\d(:\d\d(:\d\d(\.\d+)?)?)?)?)?$/; -var newMomentProto = moment.fn; // where we will attach our new methods -var oldMomentProto = $.extend({}, newMomentProto); // copy of original moment methods - -// tell momentjs to transfer these properties upon clone -var momentProperties = moment.momentProperties; -momentProperties.push('_fullCalendar'); -momentProperties.push('_ambigTime'); -momentProperties.push('_ambigZone'); - - -// Creating -// ------------------------------------------------------------------------------------------------- - -// Creates a new moment, similar to the vanilla moment(...) constructor, but with -// extra features (ambiguous time, enhanced formatting). When given an existing moment, -// it will function as a clone (and retain the zone of the moment). Anything else will -// result in a moment in the local zone. -FC.moment = function() { - return makeMoment(arguments); -}; - -// Sames as FC.moment, but forces the resulting moment to be in the UTC timezone. -FC.moment.utc = function() { - var mom = makeMoment(arguments, true); - - // Force it into UTC because makeMoment doesn't guarantee it - // (if given a pre-existing moment for example) - if (mom.hasTime()) { // don't give ambiguously-timed moments a UTC zone - mom.utc(); - } - - return mom; -}; - -// Same as FC.moment, but when given an ISO8601 string, the timezone offset is preserved. -// ISO8601 strings with no timezone offset will become ambiguously zoned. -FC.moment.parseZone = function() { - return makeMoment(arguments, true, true); -}; - -// Builds an enhanced moment from args. When given an existing moment, it clones. When given a -// native Date, or called with no arguments (the current time), the resulting moment will be local. -// Anything else needs to be "parsed" (a string or an array), and will be affected by: -// parseAsUTC - if there is no zone information, should we parse the input in UTC? -// parseZone - if there is zone information, should we force the zone of the moment? -function makeMoment(args, parseAsUTC, parseZone) { - var input = args[0]; - var isSingleString = args.length == 1 && typeof input === 'string'; - var isAmbigTime; - var isAmbigZone; - var ambigMatch; - var mom; - - if (moment.isMoment(input) || isNativeDate(input) || input === undefined) { - mom = moment.apply(null, args); - } - else { // "parsing" is required - isAmbigTime = false; - isAmbigZone = false; - - if (isSingleString) { - if (ambigDateOfMonthRegex.test(input)) { - // accept strings like '2014-05', but convert to the first of the month - input += '-01'; - args = [ input ]; // for when we pass it on to moment's constructor - isAmbigTime = true; - isAmbigZone = true; - } - else if ((ambigMatch = ambigTimeOrZoneRegex.exec(input))) { - isAmbigTime = !ambigMatch[5]; // no time part? - isAmbigZone = true; - } - } - else if ($.isArray(input)) { - // arrays have no timezone information, so assume ambiguous zone - isAmbigZone = true; - } - // otherwise, probably a string with a format - - if (parseAsUTC || isAmbigTime) { - mom = moment.utc.apply(moment, args); - } - else { - mom = moment.apply(null, args); - } - - if (isAmbigTime) { - mom._ambigTime = true; - mom._ambigZone = true; // ambiguous time always means ambiguous zone - } - else if (parseZone) { // let's record the inputted zone somehow - if (isAmbigZone) { - mom._ambigZone = true; - } - else if (isSingleString) { - mom.utcOffset(input); // if not a valid zone, will assign UTC - } - } - } - - mom._fullCalendar = true; // flag for extended functionality - - return mom; -} - - -// Week Number -// ------------------------------------------------------------------------------------------------- - - -// Returns the week number, considering the locale's custom week number calcuation -// `weeks` is an alias for `week` -newMomentProto.week = newMomentProto.weeks = function(input) { - var weekCalc = this._locale._fullCalendar_weekCalc; - - if (input == null && typeof weekCalc === 'function') { // custom function only works for getter - return weekCalc(this); - } - else if (weekCalc === 'ISO') { - return oldMomentProto.isoWeek.apply(this, arguments); // ISO getter/setter - } - - return oldMomentProto.week.apply(this, arguments); // local getter/setter -}; - - -// Time-of-day -// ------------------------------------------------------------------------------------------------- - -// GETTER -// Returns a Duration with the hours/minutes/seconds/ms values of the moment. -// If the moment has an ambiguous time, a duration of 00:00 will be returned. -// -// SETTER -// You can supply a Duration, a Moment, or a Duration-like argument. -// When setting the time, and the moment has an ambiguous time, it then becomes unambiguous. -newMomentProto.time = function(time) { - - // Fallback to the original method (if there is one) if this moment wasn't created via FullCalendar. - // `time` is a generic enough method name where this precaution is necessary to avoid collisions w/ other plugins. - if (!this._fullCalendar) { - return oldMomentProto.time.apply(this, arguments); - } - - if (time == null) { // getter - return moment.duration({ - hours: this.hours(), - minutes: this.minutes(), - seconds: this.seconds(), - milliseconds: this.milliseconds() - }); - } - else { // setter - - this._ambigTime = false; // mark that the moment now has a time - - if (!moment.isDuration(time) && !moment.isMoment(time)) { - time = moment.duration(time); - } - - // The day value should cause overflow (so 24 hours becomes 00:00:00 of next day). - // Only for Duration times, not Moment times. - var dayHours = 0; - if (moment.isDuration(time)) { - dayHours = Math.floor(time.asDays()) * 24; - } - - // We need to set the individual fields. - // Can't use startOf('day') then add duration. In case of DST at start of day. - return this.hours(dayHours + time.hours()) - .minutes(time.minutes()) - .seconds(time.seconds()) - .milliseconds(time.milliseconds()); - } -}; - -// Converts the moment to UTC, stripping out its time-of-day and timezone offset, -// but preserving its YMD. A moment with a stripped time will display no time -// nor timezone offset when .format() is called. -newMomentProto.stripTime = function() { - - if (!this._ambigTime) { - - this.utc(true); // keepLocalTime=true (for keeping *date* value) - - // set time to zero - this.set({ - hours: 0, - minutes: 0, - seconds: 0, - ms: 0 - }); - - // Mark the time as ambiguous. This needs to happen after the .utc() call, which might call .utcOffset(), - // which clears all ambig flags. - this._ambigTime = true; - this._ambigZone = true; // if ambiguous time, also ambiguous timezone offset - } - - return this; // for chaining -}; - -// Returns if the moment has a non-ambiguous time (boolean) -newMomentProto.hasTime = function() { - return !this._ambigTime; -}; - - -// Timezone -// ------------------------------------------------------------------------------------------------- - -// Converts the moment to UTC, stripping out its timezone offset, but preserving its -// YMD and time-of-day. A moment with a stripped timezone offset will display no -// timezone offset when .format() is called. -newMomentProto.stripZone = function() { - var wasAmbigTime; - - if (!this._ambigZone) { - - wasAmbigTime = this._ambigTime; - - this.utc(true); // keepLocalTime=true (for keeping date and time values) - - // the above call to .utc()/.utcOffset() unfortunately might clear the ambig flags, so restore - this._ambigTime = wasAmbigTime || false; - - // Mark the zone as ambiguous. This needs to happen after the .utc() call, which might call .utcOffset(), - // which clears the ambig flags. - this._ambigZone = true; - } - - return this; // for chaining -}; - -// Returns of the moment has a non-ambiguous timezone offset (boolean) -newMomentProto.hasZone = function() { - return !this._ambigZone; -}; - - -// implicitly marks a zone -newMomentProto.local = function(keepLocalTime) { - - // for when converting from ambiguously-zoned to local, - // keep the time values when converting from UTC -> local - oldMomentProto.local.call(this, this._ambigZone || keepLocalTime); - - // ensure non-ambiguous - // this probably already happened via local() -> utcOffset(), but don't rely on Moment's internals - this._ambigTime = false; - this._ambigZone = false; - - return this; // for chaining -}; - - -// implicitly marks a zone -newMomentProto.utc = function(keepLocalTime) { - - oldMomentProto.utc.call(this, keepLocalTime); - - // ensure non-ambiguous - // this probably already happened via utc() -> utcOffset(), but don't rely on Moment's internals - this._ambigTime = false; - this._ambigZone = false; - - return this; -}; - - -// implicitly marks a zone (will probably get called upon .utc() and .local()) -newMomentProto.utcOffset = function(tzo) { - - if (tzo != null) { // setter - // these assignments needs to happen before the original zone method is called. - // I forget why, something to do with a browser crash. - this._ambigTime = false; - this._ambigZone = false; - } - - return oldMomentProto.utcOffset.apply(this, arguments); -}; - - -// Formatting -// ------------------------------------------------------------------------------------------------- - -newMomentProto.format = function() { - - if (this._fullCalendar && arguments[0]) { // an enhanced moment? and a format string provided? - return formatDate(this, arguments[0]); // our extended formatting - } - if (this._ambigTime) { - return oldMomentFormat(englishMoment(this), 'YYYY-MM-DD'); - } - if (this._ambigZone) { - return oldMomentFormat(englishMoment(this), 'YYYY-MM-DD[T]HH:mm:ss'); - } - if (this._fullCalendar) { // enhanced non-ambig moment? - // moment.format() doesn't ensure english, but we want to. - return oldMomentFormat(englishMoment(this)); - } - - return oldMomentProto.format.apply(this, arguments); -}; - -newMomentProto.toISOString = function() { - - if (this._ambigTime) { - return oldMomentFormat(englishMoment(this), 'YYYY-MM-DD'); - } - if (this._ambigZone) { - return oldMomentFormat(englishMoment(this), 'YYYY-MM-DD[T]HH:mm:ss'); - } - if (this._fullCalendar) { // enhanced non-ambig moment? - // depending on browser, moment might not output english. ensure english. - // https://github.com/moment/moment/blob/2.18.1/src/lib/moment/format.js#L22 - return oldMomentProto.toISOString.apply(englishMoment(this), arguments); - } - - return oldMomentProto.toISOString.apply(this, arguments); -}; - -function englishMoment(mom) { - if (mom.locale() !== 'en') { - return mom.clone().locale('en'); - } - return mom; -} - -;; -(function() { - -// exports -FC.formatDate = formatDate; -FC.formatRange = formatRange; -FC.oldMomentFormat = oldMomentFormat; -FC.queryMostGranularFormatUnit = queryMostGranularFormatUnit; - - -// Config -// --------------------------------------------------------------------------------------------------------------------- - -/* -Inserted between chunks in the fake ("intermediate") formatting string. -Important that it passes as whitespace (\s) because moment often identifies non-standalone months -via a regexp with an \s. -*/ -var PART_SEPARATOR = '\u000b'; // vertical tab - -/* -Inserted as the first character of a literal-text chunk to indicate that the literal text is not actually literal text, -but rather, a "special" token that has custom rendering (see specialTokens map). -*/ -var SPECIAL_TOKEN_MARKER = '\u001f'; // information separator 1 - -/* -Inserted at the beginning and end of a span of text that must have non-zero numeric characters. -Handling of these markers is done in a post-processing step at the very end of text rendering. -*/ -var MAYBE_MARKER = '\u001e'; // information separator 2 -var MAYBE_REGEXP = new RegExp(MAYBE_MARKER + '([^' + MAYBE_MARKER + ']*)' + MAYBE_MARKER, 'g'); // must be global - -/* -Addition formatting tokens we want recognized -*/ -var specialTokens = { - t: function(date) { // "a" or "p" - return oldMomentFormat(date, 'a').charAt(0); - }, - T: function(date) { // "A" or "P" - return oldMomentFormat(date, 'A').charAt(0); - } -}; - -/* -The first characters of formatting tokens for units that are 1 day or larger. -`value` is for ranking relative size (lower means bigger). -`unit` is a normalized unit, used for comparing moments. -*/ -var largeTokenMap = { - Y: { value: 1, unit: 'year' }, - M: { value: 2, unit: 'month' }, - W: { value: 3, unit: 'week' }, // ISO week - w: { value: 3, unit: 'week' }, // local week - D: { value: 4, unit: 'day' }, // day of month - d: { value: 4, unit: 'day' } // day of week -}; - - -// Single Date Formatting -// --------------------------------------------------------------------------------------------------------------------- - -/* -Formats `date` with a Moment formatting string, but allow our non-zero areas and special token -*/ -function formatDate(date, formatStr) { - return renderFakeFormatString( - getParsedFormatString(formatStr).fakeFormatString, - date - ); -} - -/* -Call this if you want Moment's original format method to be used -*/ -function oldMomentFormat(mom, formatStr) { - return oldMomentProto.format.call(mom, formatStr); // oldMomentProto defined in moment-ext.js -} - - -// Date Range Formatting -// ------------------------------------------------------------------------------------------------- -// TODO: make it work with timezone offset - -/* -Using a formatting string meant for a single date, generate a range string, like -"Sep 2 - 9 2013", that intelligently inserts a separator where the dates differ. -If the dates are the same as far as the format string is concerned, just return a single -rendering of one date, without any separator. -*/ -function formatRange(date1, date2, formatStr, separator, isRTL) { - var localeData; - - date1 = FC.moment.parseZone(date1); - date2 = FC.moment.parseZone(date2); - - localeData = date1.localeData(); - - // Expand localized format strings, like "LL" -> "MMMM D YYYY". - // BTW, this is not important for `formatDate` because it is impossible to put custom tokens - // or non-zero areas in Moment's localized format strings. - formatStr = localeData.longDateFormat(formatStr) || formatStr; - - return renderParsedFormat( - getParsedFormatString(formatStr), - date1, - date2, - separator || ' - ', - isRTL - ); -} - -/* -Renders a range with an already-parsed format string. -*/ -function renderParsedFormat(parsedFormat, date1, date2, separator, isRTL) { - var sameUnits = parsedFormat.sameUnits; - var unzonedDate1 = date1.clone().stripZone(); // for same-unit comparisons - var unzonedDate2 = date2.clone().stripZone(); // " - - var renderedParts1 = renderFakeFormatStringParts(parsedFormat.fakeFormatString, date1); - var renderedParts2 = renderFakeFormatStringParts(parsedFormat.fakeFormatString, date2); - - var leftI; - var leftStr = ''; - var rightI; - var rightStr = ''; - var middleI; - var middleStr1 = ''; - var middleStr2 = ''; - var middleStr = ''; - - // Start at the leftmost side of the formatting string and continue until you hit a token - // that is not the same between dates. - for ( - leftI = 0; - leftI < sameUnits.length && (!sameUnits[leftI] || unzonedDate1.isSame(unzonedDate2, sameUnits[leftI])); - leftI++ - ) { - leftStr += renderedParts1[leftI]; - } - - // Similarly, start at the rightmost side of the formatting string and move left - for ( - rightI = sameUnits.length - 1; - rightI > leftI && (!sameUnits[rightI] || unzonedDate1.isSame(unzonedDate2, sameUnits[rightI])); - rightI-- - ) { - // If current chunk is on the boundary of unique date-content, and is a special-case - // date-formatting postfix character, then don't consume it. Consider it unique date-content. - // TODO: make configurable - if (rightI - 1 === leftI && renderedParts1[rightI] === '.') { - break; - } - - rightStr = renderedParts1[rightI] + rightStr; - } - - // The area in the middle is different for both of the dates. - // Collect them distinctly so we can jam them together later. - for (middleI = leftI; middleI <= rightI; middleI++) { - middleStr1 += renderedParts1[middleI]; - middleStr2 += renderedParts2[middleI]; - } - - if (middleStr1 || middleStr2) { - if (isRTL) { - middleStr = middleStr2 + separator + middleStr1; - } - else { - middleStr = middleStr1 + separator + middleStr2; - } - } - - return processMaybeMarkers( - leftStr + middleStr + rightStr - ); -} - - -// Format String Parsing -// --------------------------------------------------------------------------------------------------------------------- - -var parsedFormatStrCache = {}; - -/* -Returns a parsed format string, leveraging a cache. -*/ -function getParsedFormatString(formatStr) { - return parsedFormatStrCache[formatStr] || - (parsedFormatStrCache[formatStr] = parseFormatString(formatStr)); -} - -/* -Parses a format string into the following: -- fakeFormatString: a momentJS formatting string, littered with special control characters that get post-processed. -- sameUnits: for every part in fakeFormatString, if the part is a token, the value will be a unit string (like "day"), - that indicates how similar a range's start & end must be in order to share the same formatted text. - If not a token, then the value is null. - Always a flat array (not nested liked "chunks"). -*/ -function parseFormatString(formatStr) { - var chunks = chunkFormatString(formatStr); - - return { - fakeFormatString: buildFakeFormatString(chunks), - sameUnits: buildSameUnits(chunks) - }; -} - -/* -Break the formatting string into an array of chunks. -A 'maybe' chunk will have nested chunks. -*/ -function chunkFormatString(formatStr) { - var chunks = []; - var match; - - // TODO: more descrimination - // \4 is a backreference to the first character of a multi-character set. - var chunker = /\[([^\]]*)\]|\(([^\)]*)\)|(LTS|LT|(\w)\4*o?)|([^\w\[\(]+)/g; - - while ((match = chunker.exec(formatStr))) { - if (match[1]) { // a literal string inside [ ... ] - chunks.push.apply(chunks, // append - splitStringLiteral(match[1]) - ); - } - else if (match[2]) { // non-zero formatting inside ( ... ) - chunks.push({ maybe: chunkFormatString(match[2]) }); - } - else if (match[3]) { // a formatting token - chunks.push({ token: match[3] }); - } - else if (match[5]) { // an unenclosed literal string - chunks.push.apply(chunks, // append - splitStringLiteral(match[5]) - ); - } - } - - return chunks; -} - -/* -Potentially splits a literal-text string into multiple parts. For special cases. -*/ -function splitStringLiteral(s) { - if (s === '. ') { - return [ '.', ' ' ]; // for locales with periods bound to the end of each year/month/date - } - else { - return [ s ]; - } -} - -/* -Given chunks parsed from a real format string, generate a fake (aka "intermediate") format string with special control -characters that will eventually be given to moment for formatting, and then post-processed. -*/ -function buildFakeFormatString(chunks) { - var parts = []; - var i, chunk; - - for (i = 0; i < chunks.length; i++) { - chunk = chunks[i]; - - if (typeof chunk === 'string') { - parts.push('[' + chunk + ']'); - } - else if (chunk.token) { - if (chunk.token in specialTokens) { - parts.push( - SPECIAL_TOKEN_MARKER + // useful during post-processing - '[' + chunk.token + ']' // preserve as literal text - ); - } - else { - parts.push(chunk.token); // unprotected text implies a format string - } - } - else if (chunk.maybe) { - parts.push( - MAYBE_MARKER + // useful during post-processing - buildFakeFormatString(chunk.maybe) + - MAYBE_MARKER - ); - } - } - - return parts.join(PART_SEPARATOR); -} - -/* -Given parsed chunks from a real formatting string, generates an array of unit strings (like "day") that indicate -in which regard two dates must be similar in order to share range formatting text. -The `chunks` can be nested (because of "maybe" chunks), however, the returned array will be flat. -*/ -function buildSameUnits(chunks) { - var units = []; - var i, chunk; - var tokenInfo; - - for (i = 0; i < chunks.length; i++) { - chunk = chunks[i]; - - if (chunk.token) { - tokenInfo = largeTokenMap[chunk.token.charAt(0)]; - units.push(tokenInfo ? tokenInfo.unit : 'second'); // default to a very strict same-second - } - else if (chunk.maybe) { - units.push.apply(units, // append - buildSameUnits(chunk.maybe) - ); - } - else { - units.push(null); - } - } - - return units; -} - - -// Rendering to text -// --------------------------------------------------------------------------------------------------------------------- - -/* -Formats a date with a fake format string, post-processes the control characters, then returns. -*/ -function renderFakeFormatString(fakeFormatString, date) { - return processMaybeMarkers( - renderFakeFormatStringParts(fakeFormatString, date).join('') - ); -} - -/* -Formats a date into parts that will have been post-processed, EXCEPT for the "maybe" markers. -*/ -function renderFakeFormatStringParts(fakeFormatString, date) { - var parts = []; - var fakeRender = oldMomentFormat(date, fakeFormatString); - var fakeParts = fakeRender.split(PART_SEPARATOR); - var i, fakePart; - - for (i = 0; i < fakeParts.length; i++) { - fakePart = fakeParts[i]; - - if (fakePart.charAt(0) === SPECIAL_TOKEN_MARKER) { - parts.push( - // the literal string IS the token's name. - // call special token's registered function. - specialTokens[fakePart.substring(1)](date) - ); - } - else { - parts.push(fakePart); - } - } - - return parts; -} - -/* -Accepts an almost-finally-formatted string and processes the "maybe" control characters, returning a new string. -*/ -function processMaybeMarkers(s) { - return s.replace(MAYBE_REGEXP, function(m0, m1) { // regex assumed to have 'g' flag - if (m1.match(/[1-9]/)) { // any non-zero numeric characters? - return m1; - } - else { - return ''; - } - }); -} - - -// Misc Utils -// ------------------------------------------------------------------------------------------------- - -/* -Returns a unit string, either 'year', 'month', 'day', or null for the most granular formatting token in the string. -*/ -function queryMostGranularFormatUnit(formatStr) { - var chunks = chunkFormatString(formatStr); - var i, chunk; - var candidate; - var best; - - for (i = 0; i < chunks.length; i++) { - chunk = chunks[i]; - - if (chunk.token) { - candidate = largeTokenMap[chunk.token.charAt(0)]; - if (candidate) { - if (!best || candidate.value > best.value) { - best = candidate; - } - } - } - } - - if (best) { - return best.unit; - } - - return null; -}; - -})(); - -// quick local references -var formatDate = FC.formatDate; -var formatRange = FC.formatRange; -var oldMomentFormat = FC.oldMomentFormat; - -;; - -FC.Class = Class; // export - -// Class that all other classes will inherit from -function Class() { } - - -// Called on a class to create a subclass. -// Last argument contains instance methods. Any argument before the last are considered mixins. -Class.extend = function() { - var len = arguments.length; - var i; - var members; - - for (i = 0; i < len; i++) { - members = arguments[i]; - if (i < len - 1) { // not the last argument? - mixIntoClass(this, members); - } - } - - return extendClass(this, members || {}); // members will be undefined if no arguments -}; - - -// Adds new member variables/methods to the class's prototype. -// Can be called with another class, or a plain object hash containing new members. -Class.mixin = function(members) { - mixIntoClass(this, members); -}; - - -function extendClass(superClass, members) { - var subClass; - - // ensure a constructor for the subclass, forwarding all arguments to the super-constructor if it doesn't exist - if (hasOwnProp(members, 'constructor')) { - subClass = members.constructor; - } - if (typeof subClass !== 'function') { - subClass = members.constructor = function() { - superClass.apply(this, arguments); - }; - } - - // build the base prototype for the subclass, which is an new object chained to the superclass's prototype - subClass.prototype = createObject(superClass.prototype); - - // copy each member variable/method onto the the subclass's prototype - copyOwnProps(members, subClass.prototype); - - // copy over all class variables/methods to the subclass, such as `extend` and `mixin` - copyOwnProps(superClass, subClass); - - return subClass; -} - - -function mixIntoClass(theClass, members) { - copyOwnProps(members, theClass.prototype); -} -;; - -var Model = Class.extend(EmitterMixin, ListenerMixin, { - - _props: null, - _watchers: null, - _globalWatchArgs: null, - - constructor: function() { - this._watchers = {}; - this._props = {}; - this.applyGlobalWatchers(); - }, - - applyGlobalWatchers: function() { - var argSets = this._globalWatchArgs || []; - var i; - - for (i = 0; i < argSets.length; i++) { - this.watch.apply(this, argSets[i]); - } - }, - - has: function(name) { - return name in this._props; - }, - - get: function(name) { - if (name === undefined) { - return this._props; - } - - return this._props[name]; - }, - - set: function(name, val) { - var newProps; - - if (typeof name === 'string') { - newProps = {}; - newProps[name] = val === undefined ? null : val; - } - else { - newProps = name; - } - - this.setProps(newProps); - }, - - reset: function(newProps) { - var oldProps = this._props; - var changeset = {}; // will have undefined's to signal unsets - var name; - - for (name in oldProps) { - changeset[name] = undefined; - } - - for (name in newProps) { - changeset[name] = newProps[name]; - } - - this.setProps(changeset); - }, - - unset: function(name) { // accepts a string or array of strings - var newProps = {}; - var names; - var i; - - if (typeof name === 'string') { - names = [ name ]; - } - else { - names = name; - } - - for (i = 0; i < names.length; i++) { - newProps[names[i]] = undefined; - } - - this.setProps(newProps); - }, - - setProps: function(newProps) { - var changedProps = {}; - var changedCnt = 0; - var name, val; - - for (name in newProps) { - val = newProps[name]; - - // a change in value? - // if an object, don't check equality, because might have been mutated internally. - // TODO: eventually enforce immutability. - if ( - typeof val === 'object' || - val !== this._props[name] - ) { - changedProps[name] = val; - changedCnt++; - } - } - - if (changedCnt) { - - this.trigger('before:batchChange', changedProps); - - for (name in changedProps) { - val = changedProps[name]; - - this.trigger('before:change', name, val); - this.trigger('before:change:' + name, val); - } - - for (name in changedProps) { - val = changedProps[name]; - - if (val === undefined) { - delete this._props[name]; - } - else { - this._props[name] = val; - } - - this.trigger('change:' + name, val); - this.trigger('change', name, val); - } - - this.trigger('batchChange', changedProps); - } - }, - - watch: function(name, depList, startFunc, stopFunc) { - var _this = this; - - this.unwatch(name); - - this._watchers[name] = this._watchDeps(depList, function(deps) { - var res = startFunc.call(_this, deps); - - if (res && res.then) { - _this.unset(name); // put in an unset state while resolving - res.then(function(val) { - _this.set(name, val); - }); - } - else { - _this.set(name, res); - } - }, function() { - _this.unset(name); - - if (stopFunc) { - stopFunc.call(_this); - } - }); - }, - - unwatch: function(name) { - var watcher = this._watchers[name]; - - if (watcher) { - delete this._watchers[name]; - watcher.teardown(); - } - }, - - _watchDeps: function(depList, startFunc, stopFunc) { - var _this = this; - var queuedChangeCnt = 0; - var depCnt = depList.length; - var satisfyCnt = 0; - var values = {}; // what's passed as the `deps` arguments - var bindTuples = []; // array of [ eventName, handlerFunc ] arrays - var isCallingStop = false; - - function onBeforeDepChange(depName, val, isOptional) { - queuedChangeCnt++; - if (queuedChangeCnt === 1) { // first change to cause a "stop" ? - if (satisfyCnt === depCnt) { // all deps previously satisfied? - isCallingStop = true; - stopFunc(); - isCallingStop = false; - } - } - } - - function onDepChange(depName, val, isOptional) { - - if (val === undefined) { // unsetting a value? - - // required dependency that was previously set? - if (!isOptional && values[depName] !== undefined) { - satisfyCnt--; - } - - delete values[depName]; - } - else { // setting a value? - - // required dependency that was previously unset? - if (!isOptional && values[depName] === undefined) { - satisfyCnt++; - } - - values[depName] = val; - } - - queuedChangeCnt--; - if (!queuedChangeCnt) { // last change to cause a "start"? - - // now finally satisfied or satisfied all along? - if (satisfyCnt === depCnt) { - - // if the stopFunc initiated another value change, ignore it. - // it will be processed by another change event anyway. - if (!isCallingStop) { - startFunc(values); - } - } - } - } - - // intercept for .on() that remembers handlers - function bind(eventName, handler) { - _this.on(eventName, handler); - bindTuples.push([ eventName, handler ]); - } - - // listen to dependency changes - depList.forEach(function(depName) { - var isOptional = false; - - if (depName.charAt(0) === '?') { // TODO: more DRY - depName = depName.substring(1); - isOptional = true; - } - - bind('before:change:' + depName, function(val) { - onBeforeDepChange(depName, val, isOptional); - }); - - bind('change:' + depName, function(val) { - onDepChange(depName, val, isOptional); - }); - }); - - // process current dependency values - depList.forEach(function(depName) { - var isOptional = false; - - if (depName.charAt(0) === '?') { // TODO: more DRY - depName = depName.substring(1); - isOptional = true; - } - - if (_this.has(depName)) { - values[depName] = _this.get(depName); - satisfyCnt++; - } - else if (isOptional) { - satisfyCnt++; - } - }); - - // initially satisfied - if (satisfyCnt === depCnt) { - startFunc(values); - } - - return { - teardown: function() { - // remove all handlers - for (var i = 0; i < bindTuples.length; i++) { - _this.off(bindTuples[i][0], bindTuples[i][1]); - } - bindTuples = null; - - // was satisfied, so call stopFunc - if (satisfyCnt === depCnt) { - stopFunc(); - } - }, - flash: function() { - if (satisfyCnt === depCnt) { - stopFunc(); - startFunc(values); - } - } - }; - }, - - flash: function(name) { - var watcher = this._watchers[name]; - - if (watcher) { - watcher.flash(); - } - } - -}); - - -Model.watch = function(/* same arguments as this.watch() */) { - var proto = this.prototype; - - if (!proto._globalWatchArgs) { - proto._globalWatchArgs = []; - } - - proto._globalWatchArgs.push(arguments); -}; - - -FC.Model = Model; - - -;; - -var Promise = { - - construct: function(executor) { - var deferred = $.Deferred(); - var promise = deferred.promise(); - - if (typeof executor === 'function') { - executor( - function(val) { // resolve - deferred.resolve(val); - attachImmediatelyResolvingThen(promise, val); - }, - function() { // reject - deferred.reject(); - attachImmediatelyRejectingThen(promise); - } - ); - } - - return promise; - }, - - resolve: function(val) { - var deferred = $.Deferred().resolve(val); - var promise = deferred.promise(); - - attachImmediatelyResolvingThen(promise, val); - - return promise; - }, - - reject: function() { - var deferred = $.Deferred().reject(); - var promise = deferred.promise(); - - attachImmediatelyRejectingThen(promise); - - return promise; - } - -}; - - -function attachImmediatelyResolvingThen(promise, val) { - promise.then = function(onResolve) { - if (typeof onResolve === 'function') { - onResolve(val); - } - return promise; // for chaining - }; -} - - -function attachImmediatelyRejectingThen(promise) { - promise.then = function(onResolve, onReject) { - if (typeof onReject === 'function') { - onReject(); - } - return promise; // for chaining - }; -} - - -FC.Promise = Promise; - -;; - -var TaskQueue = Class.extend(EmitterMixin, { - - q: null, - isPaused: false, - isRunning: false, - - - constructor: function() { - this.q = []; - }, - - - queue: function(/* taskFunc, taskFunc... */) { - this.q.push.apply(this.q, arguments); // append - this.tryStart(); - }, - - - pause: function() { - this.isPaused = true; - }, - - - resume: function() { - this.isPaused = false; - this.tryStart(); - }, - - - tryStart: function() { - if (!this.isRunning && this.canRunNext()) { - this.isRunning = true; - this.trigger('start'); - this.runNext(); - } - }, - - - canRunNext: function() { - return !this.isPaused && this.q.length; - }, - - - runNext: function() { // does not check canRunNext - this.runTask(this.q.shift()); - }, - - - runTask: function(task) { - this.runTaskFunc(task); - }, - - - runTaskFunc: function(taskFunc) { - var _this = this; - var res = taskFunc(); - - if (res && res.then) { - res.then(done); - } - else { - done(); - } - - function done() { - if (_this.canRunNext()) { - _this.runNext(); - } - else { - _this.isRunning = false; - _this.trigger('stop'); - } - } - } - -}); - -FC.TaskQueue = TaskQueue; - -;; - -var RenderQueue = TaskQueue.extend({ - - waitsByNamespace: null, - waitNamespace: null, - waitId: null, - - - constructor: function(waitsByNamespace) { - TaskQueue.call(this); // super-constructor - - this.waitsByNamespace = waitsByNamespace || {}; - }, - - - queue: function(taskFunc, namespace, type) { - var task = { - func: taskFunc, - namespace: namespace, - type: type - }; - var waitMs; - - if (namespace) { - waitMs = this.waitsByNamespace[namespace]; - } - - if (this.waitNamespace) { - if (namespace === this.waitNamespace && waitMs != null) { - this.delayWait(waitMs); - } - else { - this.clearWait(); - this.tryStart(); - } - } - - if (this.compoundTask(task)) { // appended to queue? - - if (!this.waitNamespace && waitMs != null) { - this.startWait(namespace, waitMs); - } - else { - this.tryStart(); - } - } - }, - - - startWait: function(namespace, waitMs) { - this.waitNamespace = namespace; - this.spawnWait(waitMs); - }, - - - delayWait: function(waitMs) { - clearTimeout(this.waitId); - this.spawnWait(waitMs); - }, - - - spawnWait: function(waitMs) { - var _this = this; - - this.waitId = setTimeout(function() { - _this.waitNamespace = null; - _this.tryStart(); - }, waitMs); - }, - - - clearWait: function() { - if (this.waitNamespace) { - clearTimeout(this.waitId); - this.waitId = null; - this.waitNamespace = null; - } - }, - - - canRunNext: function() { - if (!TaskQueue.prototype.canRunNext.apply(this, arguments)) { - return false; - } - - // waiting for a certain namespace to stop receiving tasks? - if (this.waitNamespace) { - - // if there was a different namespace task in the meantime, - // that forces all previously-waiting tasks to suddenly execute. - // TODO: find a way to do this in constant time. - for (var q = this.q, i = 0; i < q.length; i++) { - if (q[i].namespace !== this.waitNamespace) { - return true; // allow execution - } - } - - return false; - } - - return true; - }, - - - runTask: function(task) { - this.runTaskFunc(task.func); - }, - - - compoundTask: function(newTask) { - var q = this.q; - var shouldAppend = true; - var i, task; - - if (newTask.namespace) { - - if (newTask.type === 'destroy' || newTask.type === 'init') { - - // remove all add/remove ops with same namespace, regardless of order - for (i = q.length - 1; i >= 0; i--) { - task = q[i]; - - if ( - task.namespace === newTask.namespace && - (task.type === 'add' || task.type === 'remove') - ) { - q.splice(i, 1); // remove task - } - } - - if (newTask.type === 'destroy') { - // eat away final init/destroy operation - if (q.length) { - task = q[q.length - 1]; // last task - - if (task.namespace === newTask.namespace) { - - // the init and our destroy cancel each other out - if (task.type === 'init') { - shouldAppend = false; - q.pop(); - } - // prefer to use the destroy operation that's already present - else if (task.type === 'destroy') { - shouldAppend = false; - } - } - } - } - else if (newTask.type === 'init') { - // eat away final init operation - if (q.length) { - task = q[q.length - 1]; // last task - - if ( - task.namespace === newTask.namespace && - task.type === 'init' - ) { - // our init operation takes precedence - q.pop(); - } - } - } - } - } - - if (shouldAppend) { - q.push(newTask); - } - - return shouldAppend; - } - -}); - -FC.RenderQueue = RenderQueue; - -;; - -var EmitterMixin = FC.EmitterMixin = { - - // jQuery-ification via $(this) allows a non-DOM object to have - // the same event handling capabilities (including namespaces). - - - on: function(types, handler) { - $(this).on(types, this._prepareIntercept(handler)); - return this; // for chaining - }, - - - one: function(types, handler) { - $(this).one(types, this._prepareIntercept(handler)); - return this; // for chaining - }, - - - _prepareIntercept: function(handler) { - // handlers are always called with an "event" object as their first param. - // sneak the `this` context and arguments into the extra parameter object - // and forward them on to the original handler. - var intercept = function(ev, extra) { - return handler.apply( - extra.context || this, - extra.args || [] - ); - }; - - // mimick jQuery's internal "proxy" system (risky, I know) - // causing all functions with the same .guid to appear to be the same. - // https://github.com/jquery/jquery/blob/2.2.4/src/core.js#L448 - // this is needed for calling .off with the original non-intercept handler. - if (!handler.guid) { - handler.guid = $.guid++; - } - intercept.guid = handler.guid; - - return intercept; - }, - - - off: function(types, handler) { - $(this).off(types, handler); - - return this; // for chaining - }, - - - trigger: function(types) { - var args = Array.prototype.slice.call(arguments, 1); // arguments after the first - - // pass in "extra" info to the intercept - $(this).triggerHandler(types, { args: args }); - - return this; // for chaining - }, - - - triggerWith: function(types, context, args) { - - // `triggerHandler` is less reliant on the DOM compared to `trigger`. - // pass in "extra" info to the intercept. - $(this).triggerHandler(types, { context: context, args: args }); - - return this; // for chaining - } - -}; - -;; - -/* -Utility methods for easily listening to events on another object, -and more importantly, easily unlistening from them. -*/ -var ListenerMixin = FC.ListenerMixin = (function() { - var guid = 0; - var ListenerMixin = { - - listenerId: null, - - /* - Given an `other` object that has on/off methods, bind the given `callback` to an event by the given name. - The `callback` will be called with the `this` context of the object that .listenTo is being called on. - Can be called: - .listenTo(other, eventName, callback) - OR - .listenTo(other, { - eventName1: callback1, - eventName2: callback2 - }) - */ - listenTo: function(other, arg, callback) { - if (typeof arg === 'object') { // given dictionary of callbacks - for (var eventName in arg) { - if (arg.hasOwnProperty(eventName)) { - this.listenTo(other, eventName, arg[eventName]); - } - } - } - else if (typeof arg === 'string') { - other.on( - arg + '.' + this.getListenerNamespace(), // use event namespacing to identify this object - $.proxy(callback, this) // always use `this` context - // the usually-undesired jQuery guid behavior doesn't matter, - // because we always unbind via namespace - ); - } - }, - - /* - Causes the current object to stop listening to events on the `other` object. - `eventName` is optional. If omitted, will stop listening to ALL events on `other`. - */ - stopListeningTo: function(other, eventName) { - other.off((eventName || '') + '.' + this.getListenerNamespace()); - }, - - /* - Returns a string, unique to this object, to be used for event namespacing - */ - getListenerNamespace: function() { - if (this.listenerId == null) { - this.listenerId = guid++; - } - return '_listener' + this.listenerId; - } - - }; - return ListenerMixin; -})(); -;; - -/* A rectangular panel that is absolutely positioned over other content ------------------------------------------------------------------------------------------------------------------------- -Options: - - className (string) - - content (HTML string or jQuery element set) - - parentEl - - top - - left - - right (the x coord of where the right edge should be. not a "CSS" right) - - autoHide (boolean) - - show (callback) - - hide (callback) -*/ - -var Popover = Class.extend(ListenerMixin, { - - isHidden: true, - options: null, - el: null, // the container element for the popover. generated by this object - margin: 10, // the space required between the popover and the edges of the scroll container - - - constructor: function(options) { - this.options = options || {}; - }, - - - // Shows the popover on the specified position. Renders it if not already - show: function() { - if (this.isHidden) { - if (!this.el) { - this.render(); - } - this.el.show(); - this.position(); - this.isHidden = false; - this.trigger('show'); - } - }, - - - // Hides the popover, through CSS, but does not remove it from the DOM - hide: function() { - if (!this.isHidden) { - this.el.hide(); - this.isHidden = true; - this.trigger('hide'); - } - }, - - - // Creates `this.el` and renders content inside of it - render: function() { - var _this = this; - var options = this.options; - - this.el = $('<div class="fc-popover"/>') - .addClass(options.className || '') - .css({ - // position initially to the top left to avoid creating scrollbars - top: 0, - left: 0 - }) - .append(options.content) - .appendTo(options.parentEl); - - // when a click happens on anything inside with a 'fc-close' className, hide the popover - this.el.on('click', '.fc-close', function() { - _this.hide(); - }); - - if (options.autoHide) { - this.listenTo($(document), 'mousedown', this.documentMousedown); - } - }, - - - // Triggered when the user clicks *anywhere* in the document, for the autoHide feature - documentMousedown: function(ev) { - // only hide the popover if the click happened outside the popover - if (this.el && !$(ev.target).closest(this.el).length) { - this.hide(); - } - }, - - - // Hides and unregisters any handlers - removeElement: function() { - this.hide(); - - if (this.el) { - this.el.remove(); - this.el = null; - } - - this.stopListeningTo($(document), 'mousedown'); - }, - - - // Positions the popover optimally, using the top/left/right options - position: function() { - var options = this.options; - var origin = this.el.offsetParent().offset(); - var width = this.el.outerWidth(); - var height = this.el.outerHeight(); - var windowEl = $(window); - var viewportEl = getScrollParent(this.el); - var viewportTop; - var viewportLeft; - var viewportOffset; - var top; // the "position" (not "offset") values for the popover - var left; // - - // compute top and left - top = options.top || 0; - if (options.left !== undefined) { - left = options.left; - } - else if (options.right !== undefined) { - left = options.right - width; // derive the left value from the right value - } - else { - left = 0; - } - - if (viewportEl.is(window) || viewportEl.is(document)) { // normalize getScrollParent's result - viewportEl = windowEl; - viewportTop = 0; // the window is always at the top left - viewportLeft = 0; // (and .offset() won't work if called here) - } - else { - viewportOffset = viewportEl.offset(); - viewportTop = viewportOffset.top; - viewportLeft = viewportOffset.left; - } - - // if the window is scrolled, it causes the visible area to be further down - viewportTop += windowEl.scrollTop(); - viewportLeft += windowEl.scrollLeft(); - - // constrain to the view port. if constrained by two edges, give precedence to top/left - if (options.viewportConstrain !== false) { - top = Math.min(top, viewportTop + viewportEl.outerHeight() - height - this.margin); - top = Math.max(top, viewportTop + this.margin); - left = Math.min(left, viewportLeft + viewportEl.outerWidth() - width - this.margin); - left = Math.max(left, viewportLeft + this.margin); - } - - this.el.css({ - top: top - origin.top, - left: left - origin.left - }); - }, - - - // Triggers a callback. Calls a function in the option hash of the same name. - // Arguments beyond the first `name` are forwarded on. - // TODO: better code reuse for this. Repeat code - trigger: function(name) { - if (this.options[name]) { - this.options[name].apply(this, Array.prototype.slice.call(arguments, 1)); - } - } - -}); - -;; - -/* -A cache for the left/right/top/bottom/width/height values for one or more elements. -Works with both offset (from topleft document) and position (from offsetParent). - -options: -- els -- isHorizontal -- isVertical -*/ -var CoordCache = FC.CoordCache = Class.extend({ - - els: null, // jQuery set (assumed to be siblings) - forcedOffsetParentEl: null, // options can override the natural offsetParent - origin: null, // {left,top} position of offsetParent of els - boundingRect: null, // constrain cordinates to this rectangle. {left,right,top,bottom} or null - isHorizontal: false, // whether to query for left/right/width - isVertical: false, // whether to query for top/bottom/height - - // arrays of coordinates (offsets from topleft of document) - lefts: null, - rights: null, - tops: null, - bottoms: null, - - - constructor: function(options) { - this.els = $(options.els); - this.isHorizontal = options.isHorizontal; - this.isVertical = options.isVertical; - this.forcedOffsetParentEl = options.offsetParent ? $(options.offsetParent) : null; - }, - - - // Queries the els for coordinates and stores them. - // Call this method before using and of the get* methods below. - build: function() { - var offsetParentEl = this.forcedOffsetParentEl; - if (!offsetParentEl && this.els.length > 0) { - offsetParentEl = this.els.eq(0).offsetParent(); - } - - this.origin = offsetParentEl ? - offsetParentEl.offset() : - null; - - this.boundingRect = this.queryBoundingRect(); - - if (this.isHorizontal) { - this.buildElHorizontals(); - } - if (this.isVertical) { - this.buildElVerticals(); - } - }, - - - // Destroys all internal data about coordinates, freeing memory - clear: function() { - this.origin = null; - this.boundingRect = null; - this.lefts = null; - this.rights = null; - this.tops = null; - this.bottoms = null; - }, - - - // When called, if coord caches aren't built, builds them - ensureBuilt: function() { - if (!this.origin) { - this.build(); - } - }, - - - // Populates the left/right internal coordinate arrays - buildElHorizontals: function() { - var lefts = []; - var rights = []; - - this.els.each(function(i, node) { - var el = $(node); - var left = el.offset().left; - var width = el.outerWidth(); - - lefts.push(left); - rights.push(left + width); - }); - - this.lefts = lefts; - this.rights = rights; - }, - - - // Populates the top/bottom internal coordinate arrays - buildElVerticals: function() { - var tops = []; - var bottoms = []; - - this.els.each(function(i, node) { - var el = $(node); - var top = el.offset().top; - var height = el.outerHeight(); - - tops.push(top); - bottoms.push(top + height); - }); - - this.tops = tops; - this.bottoms = bottoms; - }, - - - // Given a left offset (from document left), returns the index of the el that it horizontally intersects. - // If no intersection is made, returns undefined. - getHorizontalIndex: function(leftOffset) { - this.ensureBuilt(); - - var lefts = this.lefts; - var rights = this.rights; - var len = lefts.length; - var i; - - for (i = 0; i < len; i++) { - if (leftOffset >= lefts[i] && leftOffset < rights[i]) { - return i; - } - } - }, - - - // Given a top offset (from document top), returns the index of the el that it vertically intersects. - // If no intersection is made, returns undefined. - getVerticalIndex: function(topOffset) { - this.ensureBuilt(); - - var tops = this.tops; - var bottoms = this.bottoms; - var len = tops.length; - var i; - - for (i = 0; i < len; i++) { - if (topOffset >= tops[i] && topOffset < bottoms[i]) { - return i; - } - } - }, - - - // Gets the left offset (from document left) of the element at the given index - getLeftOffset: function(leftIndex) { - this.ensureBuilt(); - return this.lefts[leftIndex]; - }, - - - // Gets the left position (from offsetParent left) of the element at the given index - getLeftPosition: function(leftIndex) { - this.ensureBuilt(); - return this.lefts[leftIndex] - this.origin.left; - }, - - - // Gets the right offset (from document left) of the element at the given index. - // This value is NOT relative to the document's right edge, like the CSS concept of "right" would be. - getRightOffset: function(leftIndex) { - this.ensureBuilt(); - return this.rights[leftIndex]; - }, - - - // Gets the right position (from offsetParent left) of the element at the given index. - // This value is NOT relative to the offsetParent's right edge, like the CSS concept of "right" would be. - getRightPosition: function(leftIndex) { - this.ensureBuilt(); - return this.rights[leftIndex] - this.origin.left; - }, - - - // Gets the width of the element at the given index - getWidth: function(leftIndex) { - this.ensureBuilt(); - return this.rights[leftIndex] - this.lefts[leftIndex]; - }, - - - // Gets the top offset (from document top) of the element at the given index - getTopOffset: function(topIndex) { - this.ensureBuilt(); - return this.tops[topIndex]; - }, - - - // Gets the top position (from offsetParent top) of the element at the given position - getTopPosition: function(topIndex) { - this.ensureBuilt(); - return this.tops[topIndex] - this.origin.top; - }, - - // Gets the bottom offset (from the document top) of the element at the given index. - // This value is NOT relative to the offsetParent's bottom edge, like the CSS concept of "bottom" would be. - getBottomOffset: function(topIndex) { - this.ensureBuilt(); - return this.bottoms[topIndex]; - }, - - - // Gets the bottom position (from the offsetParent top) of the element at the given index. - // This value is NOT relative to the offsetParent's bottom edge, like the CSS concept of "bottom" would be. - getBottomPosition: function(topIndex) { - this.ensureBuilt(); - return this.bottoms[topIndex] - this.origin.top; - }, - - - // Gets the height of the element at the given index - getHeight: function(topIndex) { - this.ensureBuilt(); - return this.bottoms[topIndex] - this.tops[topIndex]; - }, - - - // Bounding Rect - // TODO: decouple this from CoordCache - - // Compute and return what the elements' bounding rectangle is, from the user's perspective. - // Right now, only returns a rectangle if constrained by an overflow:scroll element. - // Returns null if there are no elements - queryBoundingRect: function() { - var scrollParentEl; - - if (this.els.length > 0) { - scrollParentEl = getScrollParent(this.els.eq(0)); - - if (!scrollParentEl.is(document)) { - return getClientRect(scrollParentEl); - } - } - - return null; - }, - - isPointInBounds: function(leftOffset, topOffset) { - return this.isLeftInBounds(leftOffset) && this.isTopInBounds(topOffset); - }, - - isLeftInBounds: function(leftOffset) { - return !this.boundingRect || (leftOffset >= this.boundingRect.left && leftOffset < this.boundingRect.right); - }, - - isTopInBounds: function(topOffset) { - return !this.boundingRect || (topOffset >= this.boundingRect.top && topOffset < this.boundingRect.bottom); - } - -}); - -;; - -/* Tracks a drag's mouse movement, firing various handlers -----------------------------------------------------------------------------------------------------------------------*/ -// TODO: use Emitter - -var DragListener = FC.DragListener = Class.extend(ListenerMixin, { - - options: null, - subjectEl: null, - - // coordinates of the initial mousedown - originX: null, - originY: null, - - // the wrapping element that scrolls, or MIGHT scroll if there's overflow. - // TODO: do this for wrappers that have overflow:hidden as well. - scrollEl: null, - - isInteracting: false, - isDistanceSurpassed: false, - isDelayEnded: false, - isDragging: false, - isTouch: false, - isGeneric: false, // initiated by 'dragstart' (jqui) - - delay: null, - delayTimeoutId: null, - minDistance: null, - - shouldCancelTouchScroll: true, - scrollAlwaysKills: false, - - - constructor: function(options) { - this.options = options || {}; - }, - - - // Interaction (high-level) - // ----------------------------------------------------------------------------------------------------------------- - - - startInteraction: function(ev, extraOptions) { - - if (ev.type === 'mousedown') { - if (GlobalEmitter.get().shouldIgnoreMouse()) { - return; - } - else if (!isPrimaryMouseButton(ev)) { - return; - } - else { - ev.preventDefault(); // prevents native selection in most browsers - } - } - - if (!this.isInteracting) { - - // process options - extraOptions = extraOptions || {}; - this.delay = firstDefined(extraOptions.delay, this.options.delay, 0); - this.minDistance = firstDefined(extraOptions.distance, this.options.distance, 0); - this.subjectEl = this.options.subjectEl; - - preventSelection($('body')); - - this.isInteracting = true; - this.isTouch = getEvIsTouch(ev); - this.isGeneric = ev.type === 'dragstart'; - this.isDelayEnded = false; - this.isDistanceSurpassed = false; - - this.originX = getEvX(ev); - this.originY = getEvY(ev); - this.scrollEl = getScrollParent($(ev.target)); - - this.bindHandlers(); - this.initAutoScroll(); - this.handleInteractionStart(ev); - this.startDelay(ev); - - if (!this.minDistance) { - this.handleDistanceSurpassed(ev); - } - } - }, - - - handleInteractionStart: function(ev) { - this.trigger('interactionStart', ev); - }, - - - endInteraction: function(ev, isCancelled) { - if (this.isInteracting) { - this.endDrag(ev); - - if (this.delayTimeoutId) { - clearTimeout(this.delayTimeoutId); - this.delayTimeoutId = null; - } - - this.destroyAutoScroll(); - this.unbindHandlers(); - - this.isInteracting = false; - this.handleInteractionEnd(ev, isCancelled); - - allowSelection($('body')); - } - }, - - - handleInteractionEnd: function(ev, isCancelled) { - this.trigger('interactionEnd', ev, isCancelled || false); - }, - - - // Binding To DOM - // ----------------------------------------------------------------------------------------------------------------- - - - bindHandlers: function() { - // some browsers (Safari in iOS 10) don't allow preventDefault on touch events that are bound after touchstart, - // so listen to the GlobalEmitter singleton, which is always bound, instead of the document directly. - var globalEmitter = GlobalEmitter.get(); - - if (this.isGeneric) { - this.listenTo($(document), { // might only work on iOS because of GlobalEmitter's bind :( - drag: this.handleMove, - dragstop: this.endInteraction - }); - } - else if (this.isTouch) { - this.listenTo(globalEmitter, { - touchmove: this.handleTouchMove, - touchend: this.endInteraction, - scroll: this.handleTouchScroll - }); - } - else { - this.listenTo(globalEmitter, { - mousemove: this.handleMouseMove, - mouseup: this.endInteraction - }); - } - - this.listenTo(globalEmitter, { - selectstart: preventDefault, // don't allow selection while dragging - contextmenu: preventDefault // long taps would open menu on Chrome dev tools - }); - }, - - - unbindHandlers: function() { - this.stopListeningTo(GlobalEmitter.get()); - this.stopListeningTo($(document)); // for isGeneric - }, - - - // Drag (high-level) - // ----------------------------------------------------------------------------------------------------------------- - - - // extraOptions ignored if drag already started - startDrag: function(ev, extraOptions) { - this.startInteraction(ev, extraOptions); // ensure interaction began - - if (!this.isDragging) { - this.isDragging = true; - this.handleDragStart(ev); - } - }, - - - handleDragStart: function(ev) { - this.trigger('dragStart', ev); - }, - - - handleMove: function(ev) { - var dx = getEvX(ev) - this.originX; - var dy = getEvY(ev) - this.originY; - var minDistance = this.minDistance; - var distanceSq; // current distance from the origin, squared - - if (!this.isDistanceSurpassed) { - distanceSq = dx * dx + dy * dy; - if (distanceSq >= minDistance * minDistance) { // use pythagorean theorem - this.handleDistanceSurpassed(ev); - } - } - - if (this.isDragging) { - this.handleDrag(dx, dy, ev); - } - }, - - - // Called while the mouse is being moved and when we know a legitimate drag is taking place - handleDrag: function(dx, dy, ev) { - this.trigger('drag', dx, dy, ev); - this.updateAutoScroll(ev); // will possibly cause scrolling - }, - - - endDrag: function(ev) { - if (this.isDragging) { - this.isDragging = false; - this.handleDragEnd(ev); - } - }, - - - handleDragEnd: function(ev) { - this.trigger('dragEnd', ev); - }, - - - // Delay - // ----------------------------------------------------------------------------------------------------------------- - - - startDelay: function(initialEv) { - var _this = this; - - if (this.delay) { - this.delayTimeoutId = setTimeout(function() { - _this.handleDelayEnd(initialEv); - }, this.delay); - } - else { - this.handleDelayEnd(initialEv); - } - }, - - - handleDelayEnd: function(initialEv) { - this.isDelayEnded = true; - - if (this.isDistanceSurpassed) { - this.startDrag(initialEv); - } - }, - - - // Distance - // ----------------------------------------------------------------------------------------------------------------- - - - handleDistanceSurpassed: function(ev) { - this.isDistanceSurpassed = true; - - if (this.isDelayEnded) { - this.startDrag(ev); - } - }, - - - // Mouse / Touch - // ----------------------------------------------------------------------------------------------------------------- - - - handleTouchMove: function(ev) { - - // prevent inertia and touchmove-scrolling while dragging - if (this.isDragging && this.shouldCancelTouchScroll) { - ev.preventDefault(); - } - - this.handleMove(ev); - }, - - - handleMouseMove: function(ev) { - this.handleMove(ev); - }, - - - // Scrolling (unrelated to auto-scroll) - // ----------------------------------------------------------------------------------------------------------------- - - - handleTouchScroll: function(ev) { - // if the drag is being initiated by touch, but a scroll happens before - // the drag-initiating delay is over, cancel the drag - if (!this.isDragging || this.scrollAlwaysKills) { - this.endInteraction(ev, true); // isCancelled=true - } - }, - - - // Utils - // ----------------------------------------------------------------------------------------------------------------- - - - // Triggers a callback. Calls a function in the option hash of the same name. - // Arguments beyond the first `name` are forwarded on. - trigger: function(name) { - if (this.options[name]) { - this.options[name].apply(this, Array.prototype.slice.call(arguments, 1)); - } - // makes _methods callable by event name. TODO: kill this - if (this['_' + name]) { - this['_' + name].apply(this, Array.prototype.slice.call(arguments, 1)); - } - } - - -}); - -;; -/* -this.scrollEl is set in DragListener -*/ -DragListener.mixin({ - - isAutoScroll: false, - - scrollBounds: null, // { top, bottom, left, right } - scrollTopVel: null, // pixels per second - scrollLeftVel: null, // pixels per second - scrollIntervalId: null, // ID of setTimeout for scrolling animation loop - - // defaults - scrollSensitivity: 30, // pixels from edge for scrolling to start - scrollSpeed: 200, // pixels per second, at maximum speed - scrollIntervalMs: 50, // millisecond wait between scroll increment - - - initAutoScroll: function() { - var scrollEl = this.scrollEl; - - this.isAutoScroll = - this.options.scroll && - scrollEl && - !scrollEl.is(window) && - !scrollEl.is(document); - - if (this.isAutoScroll) { - // debounce makes sure rapid calls don't happen - this.listenTo(scrollEl, 'scroll', debounce(this.handleDebouncedScroll, 100)); - } - }, - - - destroyAutoScroll: function() { - this.endAutoScroll(); // kill any animation loop - - // remove the scroll handler if there is a scrollEl - if (this.isAutoScroll) { - this.stopListeningTo(this.scrollEl, 'scroll'); // will probably get removed by unbindHandlers too :( - } - }, - - - // Computes and stores the bounding rectangle of scrollEl - computeScrollBounds: function() { - if (this.isAutoScroll) { - this.scrollBounds = getOuterRect(this.scrollEl); - // TODO: use getClientRect in future. but prevents auto scrolling when on top of scrollbars - } - }, - - - // Called when the dragging is in progress and scrolling should be updated - updateAutoScroll: function(ev) { - var sensitivity = this.scrollSensitivity; - var bounds = this.scrollBounds; - var topCloseness, bottomCloseness; - var leftCloseness, rightCloseness; - var topVel = 0; - var leftVel = 0; - - if (bounds) { // only scroll if scrollEl exists - - // compute closeness to edges. valid range is from 0.0 - 1.0 - topCloseness = (sensitivity - (getEvY(ev) - bounds.top)) / sensitivity; - bottomCloseness = (sensitivity - (bounds.bottom - getEvY(ev))) / sensitivity; - leftCloseness = (sensitivity - (getEvX(ev) - bounds.left)) / sensitivity; - rightCloseness = (sensitivity - (bounds.right - getEvX(ev))) / sensitivity; - - // translate vertical closeness into velocity. - // mouse must be completely in bounds for velocity to happen. - if (topCloseness >= 0 && topCloseness <= 1) { - topVel = topCloseness * this.scrollSpeed * -1; // negative. for scrolling up - } - else if (bottomCloseness >= 0 && bottomCloseness <= 1) { - topVel = bottomCloseness * this.scrollSpeed; - } - - // translate horizontal closeness into velocity - if (leftCloseness >= 0 && leftCloseness <= 1) { - leftVel = leftCloseness * this.scrollSpeed * -1; // negative. for scrolling left - } - else if (rightCloseness >= 0 && rightCloseness <= 1) { - leftVel = rightCloseness * this.scrollSpeed; - } - } - - this.setScrollVel(topVel, leftVel); - }, - - - // Sets the speed-of-scrolling for the scrollEl - setScrollVel: function(topVel, leftVel) { - - this.scrollTopVel = topVel; - this.scrollLeftVel = leftVel; - - this.constrainScrollVel(); // massages into realistic values - - // if there is non-zero velocity, and an animation loop hasn't already started, then START - if ((this.scrollTopVel || this.scrollLeftVel) && !this.scrollIntervalId) { - this.scrollIntervalId = setInterval( - proxy(this, 'scrollIntervalFunc'), // scope to `this` - this.scrollIntervalMs - ); - } - }, - - - // Forces scrollTopVel and scrollLeftVel to be zero if scrolling has already gone all the way - constrainScrollVel: function() { - var el = this.scrollEl; - - if (this.scrollTopVel < 0) { // scrolling up? - if (el.scrollTop() <= 0) { // already scrolled all the way up? - this.scrollTopVel = 0; - } - } - else if (this.scrollTopVel > 0) { // scrolling down? - if (el.scrollTop() + el[0].clientHeight >= el[0].scrollHeight) { // already scrolled all the way down? - this.scrollTopVel = 0; - } - } - - if (this.scrollLeftVel < 0) { // scrolling left? - if (el.scrollLeft() <= 0) { // already scrolled all the left? - this.scrollLeftVel = 0; - } - } - else if (this.scrollLeftVel > 0) { // scrolling right? - if (el.scrollLeft() + el[0].clientWidth >= el[0].scrollWidth) { // already scrolled all the way right? - this.scrollLeftVel = 0; - } - } - }, - - - // This function gets called during every iteration of the scrolling animation loop - scrollIntervalFunc: function() { - var el = this.scrollEl; - var frac = this.scrollIntervalMs / 1000; // considering animation frequency, what the vel should be mult'd by - - // change the value of scrollEl's scroll - if (this.scrollTopVel) { - el.scrollTop(el.scrollTop() + this.scrollTopVel * frac); - } - if (this.scrollLeftVel) { - el.scrollLeft(el.scrollLeft() + this.scrollLeftVel * frac); - } - - this.constrainScrollVel(); // since the scroll values changed, recompute the velocities - - // if scrolled all the way, which causes the vels to be zero, stop the animation loop - if (!this.scrollTopVel && !this.scrollLeftVel) { - this.endAutoScroll(); - } - }, - - - // Kills any existing scrolling animation loop - endAutoScroll: function() { - if (this.scrollIntervalId) { - clearInterval(this.scrollIntervalId); - this.scrollIntervalId = null; - - this.handleScrollEnd(); - } - }, - - - // Get called when the scrollEl is scrolled (NOTE: this is delayed via debounce) - handleDebouncedScroll: function() { - // recompute all coordinates, but *only* if this is *not* part of our scrolling animation - if (!this.scrollIntervalId) { - this.handleScrollEnd(); - } - }, - - - // Called when scrolling has stopped, whether through auto scroll, or the user scrolling - handleScrollEnd: function() { - } - -}); -;; - -/* Tracks mouse movements over a component and raises events about which hit the mouse is over. ------------------------------------------------------------------------------------------------------------------------- -options: -- subjectEl -- subjectCenter -*/ - -var HitDragListener = DragListener.extend({ - - component: null, // converts coordinates to hits - // methods: hitsNeeded, hitsNotNeeded, queryHit - - origHit: null, // the hit the mouse was over when listening started - hit: null, // the hit the mouse is over - coordAdjust: null, // delta that will be added to the mouse coordinates when computing collisions - - - constructor: function(component, options) { - DragListener.call(this, options); // call the super-constructor - - this.component = component; - }, - - - // Called when drag listening starts (but a real drag has not necessarily began). - // ev might be undefined if dragging was started manually. - handleInteractionStart: function(ev) { - var subjectEl = this.subjectEl; - var subjectRect; - var origPoint; - var point; - - this.component.hitsNeeded(); - this.computeScrollBounds(); // for autoscroll - - if (ev) { - origPoint = { left: getEvX(ev), top: getEvY(ev) }; - point = origPoint; - - // constrain the point to bounds of the element being dragged - if (subjectEl) { - subjectRect = getOuterRect(subjectEl); // used for centering as well - point = constrainPoint(point, subjectRect); - } - - this.origHit = this.queryHit(point.left, point.top); - - // treat the center of the subject as the collision point? - if (subjectEl && this.options.subjectCenter) { - - // only consider the area the subject overlaps the hit. best for large subjects. - // TODO: skip this if hit didn't supply left/right/top/bottom - if (this.origHit) { - subjectRect = intersectRects(this.origHit, subjectRect) || - subjectRect; // in case there is no intersection - } - - point = getRectCenter(subjectRect); - } - - this.coordAdjust = diffPoints(point, origPoint); // point - origPoint - } - else { - this.origHit = null; - this.coordAdjust = null; - } - - // call the super-method. do it after origHit has been computed - DragListener.prototype.handleInteractionStart.apply(this, arguments); - }, - - - // Called when the actual drag has started - handleDragStart: function(ev) { - var hit; - - DragListener.prototype.handleDragStart.apply(this, arguments); // call the super-method - - // might be different from this.origHit if the min-distance is large - hit = this.queryHit(getEvX(ev), getEvY(ev)); - - // report the initial hit the mouse is over - // especially important if no min-distance and drag starts immediately - if (hit) { - this.handleHitOver(hit); - } - }, - - - // Called when the drag moves - handleDrag: function(dx, dy, ev) { - var hit; - - DragListener.prototype.handleDrag.apply(this, arguments); // call the super-method - - hit = this.queryHit(getEvX(ev), getEvY(ev)); - - if (!isHitsEqual(hit, this.hit)) { // a different hit than before? - if (this.hit) { - this.handleHitOut(); - } - if (hit) { - this.handleHitOver(hit); - } - } - }, - - - // Called when dragging has been stopped - handleDragEnd: function() { - this.handleHitDone(); - DragListener.prototype.handleDragEnd.apply(this, arguments); // call the super-method - }, - - - // Called when a the mouse has just moved over a new hit - handleHitOver: function(hit) { - var isOrig = isHitsEqual(hit, this.origHit); - - this.hit = hit; - - this.trigger('hitOver', this.hit, isOrig, this.origHit); - }, - - - // Called when the mouse has just moved out of a hit - handleHitOut: function() { - if (this.hit) { - this.trigger('hitOut', this.hit); - this.handleHitDone(); - this.hit = null; - } - }, - - - // Called after a hitOut. Also called before a dragStop - handleHitDone: function() { - if (this.hit) { - this.trigger('hitDone', this.hit); - } - }, - - - // Called when the interaction ends, whether there was a real drag or not - handleInteractionEnd: function() { - DragListener.prototype.handleInteractionEnd.apply(this, arguments); // call the super-method - - this.origHit = null; - this.hit = null; - - this.component.hitsNotNeeded(); - }, - - - // Called when scrolling has stopped, whether through auto scroll, or the user scrolling - handleScrollEnd: function() { - DragListener.prototype.handleScrollEnd.apply(this, arguments); // call the super-method - - // hits' absolute positions will be in new places after a user's scroll. - // HACK for recomputing. - if (this.isDragging) { - this.component.releaseHits(); - this.component.prepareHits(); - } - }, - - - // Gets the hit underneath the coordinates for the given mouse event - queryHit: function(left, top) { - - if (this.coordAdjust) { - left += this.coordAdjust.left; - top += this.coordAdjust.top; - } - - return this.component.queryHit(left, top); - } - -}); - - -// Returns `true` if the hits are identically equal. `false` otherwise. Must be from the same component. -// Two null values will be considered equal, as two "out of the component" states are the same. -function isHitsEqual(hit0, hit1) { - - if (!hit0 && !hit1) { - return true; - } - - if (hit0 && hit1) { - return hit0.component === hit1.component && - isHitPropsWithin(hit0, hit1) && - isHitPropsWithin(hit1, hit0); // ensures all props are identical - } - - return false; -} - - -// Returns true if all of subHit's non-standard properties are within superHit -function isHitPropsWithin(subHit, superHit) { - for (var propName in subHit) { - if (!/^(component|left|right|top|bottom)$/.test(propName)) { - if (subHit[propName] !== superHit[propName]) { - return false; - } - } - } - return true; -} - -;; - -/* -Listens to document and window-level user-interaction events, like touch events and mouse events, -and fires these events as-is to whoever is observing a GlobalEmitter. -Best when used as a singleton via GlobalEmitter.get() - -Normalizes mouse/touch events. For examples: -- ignores the the simulated mouse events that happen after a quick tap: mousemove+mousedown+mouseup+click -- compensates for various buggy scenarios where a touchend does not fire -*/ - -FC.touchMouseIgnoreWait = 500; - -var GlobalEmitter = Class.extend(ListenerMixin, EmitterMixin, { - - isTouching: false, - mouseIgnoreDepth: 0, - handleScrollProxy: null, - - - bind: function() { - var _this = this; - - this.listenTo($(document), { - touchstart: this.handleTouchStart, - touchcancel: this.handleTouchCancel, - touchend: this.handleTouchEnd, - mousedown: this.handleMouseDown, - mousemove: this.handleMouseMove, - mouseup: this.handleMouseUp, - click: this.handleClick, - selectstart: this.handleSelectStart, - contextmenu: this.handleContextMenu - }); - - // because we need to call preventDefault - // because https://www.chromestatus.com/features/5093566007214080 - // TODO: investigate performance because this is a global handler - window.addEventListener( - 'touchmove', - this.handleTouchMoveProxy = function(ev) { - _this.handleTouchMove($.Event(ev)); - }, - { passive: false } // allows preventDefault() - ); - - // attach a handler to get called when ANY scroll action happens on the page. - // this was impossible to do with normal on/off because 'scroll' doesn't bubble. - // http://stackoverflow.com/a/32954565/96342 - window.addEventListener( - 'scroll', - this.handleScrollProxy = function(ev) { - _this.handleScroll($.Event(ev)); - }, - true // useCapture - ); - }, - - unbind: function() { - this.stopListeningTo($(document)); - - window.removeEventListener( - 'touchmove', - this.handleTouchMoveProxy - ); - - window.removeEventListener( - 'scroll', - this.handleScrollProxy, - true // useCapture - ); - }, - - - // Touch Handlers - // ----------------------------------------------------------------------------------------------------------------- - - handleTouchStart: function(ev) { - - // if a previous touch interaction never ended with a touchend, then implicitly end it, - // but since a new touch interaction is about to begin, don't start the mouse ignore period. - this.stopTouch(ev, true); // skipMouseIgnore=true - - this.isTouching = true; - this.trigger('touchstart', ev); - }, - - handleTouchMove: function(ev) { - if (this.isTouching) { - this.trigger('touchmove', ev); - } - }, - - handleTouchCancel: function(ev) { - if (this.isTouching) { - this.trigger('touchcancel', ev); - - // Have touchcancel fire an artificial touchend. That way, handlers won't need to listen to both. - // If touchend fires later, it won't have any effect b/c isTouching will be false. - this.stopTouch(ev); - } - }, - - handleTouchEnd: function(ev) { - this.stopTouch(ev); - }, - - - // Mouse Handlers - // ----------------------------------------------------------------------------------------------------------------- - - handleMouseDown: function(ev) { - if (!this.shouldIgnoreMouse()) { - this.trigger('mousedown', ev); - } - }, - - handleMouseMove: function(ev) { - if (!this.shouldIgnoreMouse()) { - this.trigger('mousemove', ev); - } - }, - - handleMouseUp: function(ev) { - if (!this.shouldIgnoreMouse()) { - this.trigger('mouseup', ev); - } - }, - - handleClick: function(ev) { - if (!this.shouldIgnoreMouse()) { - this.trigger('click', ev); - } - }, - - - // Misc Handlers - // ----------------------------------------------------------------------------------------------------------------- - - handleSelectStart: function(ev) { - this.trigger('selectstart', ev); - }, - - handleContextMenu: function(ev) { - this.trigger('contextmenu', ev); - }, - - handleScroll: function(ev) { - this.trigger('scroll', ev); - }, - - - // Utils - // ----------------------------------------------------------------------------------------------------------------- - - stopTouch: function(ev, skipMouseIgnore) { - if (this.isTouching) { - this.isTouching = false; - this.trigger('touchend', ev); - - if (!skipMouseIgnore) { - this.startTouchMouseIgnore(); - } - } - }, - - startTouchMouseIgnore: function() { - var _this = this; - var wait = FC.touchMouseIgnoreWait; - - if (wait) { - this.mouseIgnoreDepth++; - setTimeout(function() { - _this.mouseIgnoreDepth--; - }, wait); - } - }, - - shouldIgnoreMouse: function() { - return this.isTouching || Boolean(this.mouseIgnoreDepth); - } - -}); - - -// Singleton -// --------------------------------------------------------------------------------------------------------------------- - -(function() { - var globalEmitter = null; - var neededCount = 0; - - - // gets the singleton - GlobalEmitter.get = function() { - - if (!globalEmitter) { - globalEmitter = new GlobalEmitter(); - globalEmitter.bind(); - } - - return globalEmitter; - }; - - - // called when an object knows it will need a GlobalEmitter in the near future. - GlobalEmitter.needed = function() { - GlobalEmitter.get(); // ensures globalEmitter - neededCount++; - }; - - - // called when the object that originally called needed() doesn't need a GlobalEmitter anymore. - GlobalEmitter.unneeded = function() { - neededCount--; - - if (!neededCount) { // nobody else needs it - globalEmitter.unbind(); - globalEmitter = null; - } - }; - -})(); - -;; - -/* Creates a clone of an element and lets it track the mouse as it moves -----------------------------------------------------------------------------------------------------------------------*/ - -var MouseFollower = Class.extend(ListenerMixin, { - - options: null, - - sourceEl: null, // the element that will be cloned and made to look like it is dragging - el: null, // the clone of `sourceEl` that will track the mouse - parentEl: null, // the element that `el` (the clone) will be attached to - - // the initial position of el, relative to the offset parent. made to match the initial offset of sourceEl - top0: null, - left0: null, - - // the absolute coordinates of the initiating touch/mouse action - y0: null, - x0: null, - - // the number of pixels the mouse has moved from its initial position - topDelta: null, - leftDelta: null, - - isFollowing: false, - isHidden: false, - isAnimating: false, // doing the revert animation? - - constructor: function(sourceEl, options) { - this.options = options = options || {}; - this.sourceEl = sourceEl; - this.parentEl = options.parentEl ? $(options.parentEl) : sourceEl.parent(); // default to sourceEl's parent - }, - - - // Causes the element to start following the mouse - start: function(ev) { - if (!this.isFollowing) { - this.isFollowing = true; - - this.y0 = getEvY(ev); - this.x0 = getEvX(ev); - this.topDelta = 0; - this.leftDelta = 0; - - if (!this.isHidden) { - this.updatePosition(); - } - - if (getEvIsTouch(ev)) { - this.listenTo($(document), 'touchmove', this.handleMove); - } - else { - this.listenTo($(document), 'mousemove', this.handleMove); - } - } - }, - - - // Causes the element to stop following the mouse. If shouldRevert is true, will animate back to original position. - // `callback` gets invoked when the animation is complete. If no animation, it is invoked immediately. - stop: function(shouldRevert, callback) { - var _this = this; - var revertDuration = this.options.revertDuration; - - function complete() { // might be called by .animate(), which might change `this` context - _this.isAnimating = false; - _this.removeElement(); - - _this.top0 = _this.left0 = null; // reset state for future updatePosition calls - - if (callback) { - callback(); - } - } - - if (this.isFollowing && !this.isAnimating) { // disallow more than one stop animation at a time - this.isFollowing = false; - - this.stopListeningTo($(document)); - - if (shouldRevert && revertDuration && !this.isHidden) { // do a revert animation? - this.isAnimating = true; - this.el.animate({ - top: this.top0, - left: this.left0 - }, { - duration: revertDuration, - complete: complete - }); - } - else { - complete(); - } - } - }, - - - // Gets the tracking element. Create it if necessary - getEl: function() { - var el = this.el; - - if (!el) { - el = this.el = this.sourceEl.clone() - .addClass(this.options.additionalClass || '') - .css({ - position: 'absolute', - visibility: '', // in case original element was hidden (commonly through hideEvents()) - display: this.isHidden ? 'none' : '', // for when initially hidden - margin: 0, - right: 'auto', // erase and set width instead - bottom: 'auto', // erase and set height instead - width: this.sourceEl.width(), // explicit height in case there was a 'right' value - height: this.sourceEl.height(), // explicit width in case there was a 'bottom' value - opacity: this.options.opacity || '', - zIndex: this.options.zIndex - }); - - // we don't want long taps or any mouse interaction causing selection/menus. - // would use preventSelection(), but that prevents selectstart, causing problems. - el.addClass('fc-unselectable'); - - el.appendTo(this.parentEl); - } - - return el; - }, - - - // Removes the tracking element if it has already been created - removeElement: function() { - if (this.el) { - this.el.remove(); - this.el = null; - } - }, - - - // Update the CSS position of the tracking element - updatePosition: function() { - var sourceOffset; - var origin; - - this.getEl(); // ensure this.el - - // make sure origin info was computed - if (this.top0 === null) { - sourceOffset = this.sourceEl.offset(); - origin = this.el.offsetParent().offset(); - this.top0 = sourceOffset.top - origin.top; - this.left0 = sourceOffset.left - origin.left; - } - - this.el.css({ - top: this.top0 + this.topDelta, - left: this.left0 + this.leftDelta - }); - }, - - - // Gets called when the user moves the mouse - handleMove: function(ev) { - this.topDelta = getEvY(ev) - this.y0; - this.leftDelta = getEvX(ev) - this.x0; - - if (!this.isHidden) { - this.updatePosition(); - } - }, - - - // Temporarily makes the tracking element invisible. Can be called before following starts - hide: function() { - if (!this.isHidden) { - this.isHidden = true; - if (this.el) { - this.el.hide(); - } - } - }, - - - // Show the tracking element after it has been temporarily hidden - show: function() { - if (this.isHidden) { - this.isHidden = false; - this.updatePosition(); - this.getEl().show(); - } - } - -}); - -;; - -/* An abstract class comprised of a "grid" of areas that each represent a specific datetime -----------------------------------------------------------------------------------------------------------------------*/ - -var Grid = FC.Grid = Class.extend(ListenerMixin, { - - // self-config, overridable by subclasses - hasDayInteractions: true, // can user click/select ranges of time? - - view: null, // a View object - isRTL: null, // shortcut to the view's isRTL option - - start: null, - end: null, - - el: null, // the containing element - elsByFill: null, // a hash of jQuery element sets used for rendering each fill. Keyed by fill name. - - // derived from options - eventTimeFormat: null, - displayEventTime: null, - displayEventEnd: null, - - minResizeDuration: null, // TODO: hack. set by subclasses. minumum event resize duration - - // if defined, holds the unit identified (ex: "year" or "month") that determines the level of granularity - // of the date areas. if not defined, assumes to be day and time granularity. - // TODO: port isTimeScale into same system? - largeUnit: null, - - dayClickListener: null, - daySelectListener: null, - segDragListener: null, - segResizeListener: null, - externalDragListener: null, - - - constructor: function(view) { - this.view = view; - this.isRTL = view.opt('isRTL'); - this.elsByFill = {}; - - this.dayClickListener = this.buildDayClickListener(); - this.daySelectListener = this.buildDaySelectListener(); - }, - - - /* Options - ------------------------------------------------------------------------------------------------------------------*/ - - - // Generates the format string used for event time text, if not explicitly defined by 'timeFormat' - computeEventTimeFormat: function() { - return this.view.opt('smallTimeFormat'); - }, - - - // Determines whether events should have their end times displayed, if not explicitly defined by 'displayEventTime'. - // Only applies to non-all-day events. - computeDisplayEventTime: function() { - return true; - }, - - - // Determines whether events should have their end times displayed, if not explicitly defined by 'displayEventEnd' - computeDisplayEventEnd: function() { - return true; - }, - - - /* Dates - ------------------------------------------------------------------------------------------------------------------*/ - - - // Tells the grid about what period of time to display. - // Any date-related internal data should be generated. - setRange: function(range) { - this.start = range.start.clone(); - this.end = range.end.clone(); - - this.rangeUpdated(); - this.processRangeOptions(); - }, - - - // Called when internal variables that rely on the range should be updated - rangeUpdated: function() { - }, - - - // Updates values that rely on options and also relate to range - processRangeOptions: function() { - var view = this.view; - var displayEventTime; - var displayEventEnd; - - this.eventTimeFormat = - view.opt('eventTimeFormat') || - view.opt('timeFormat') || // deprecated - this.computeEventTimeFormat(); - - displayEventTime = view.opt('displayEventTime'); - if (displayEventTime == null) { - displayEventTime = this.computeDisplayEventTime(); // might be based off of range - } - - displayEventEnd = view.opt('displayEventEnd'); - if (displayEventEnd == null) { - displayEventEnd = this.computeDisplayEventEnd(); // might be based off of range - } - - this.displayEventTime = displayEventTime; - this.displayEventEnd = displayEventEnd; - }, - - - // Converts a span (has unzoned start/end and any other grid-specific location information) - // into an array of segments (pieces of events whose format is decided by the grid). - spanToSegs: function(span) { - // subclasses must implement - }, - - - // Diffs the two dates, returning a duration, based on granularity of the grid - // TODO: port isTimeScale into this system? - diffDates: function(a, b) { - if (this.largeUnit) { - return diffByUnit(a, b, this.largeUnit); - } - else { - return diffDayTime(a, b); - } - }, - - - /* Hit Area - ------------------------------------------------------------------------------------------------------------------*/ - - hitsNeededDepth: 0, // necessary because multiple callers might need the same hits - - hitsNeeded: function() { - if (!(this.hitsNeededDepth++)) { - this.prepareHits(); - } - }, - - hitsNotNeeded: function() { - if (this.hitsNeededDepth && !(--this.hitsNeededDepth)) { - this.releaseHits(); - } - }, - - - // Called before one or more queryHit calls might happen. Should prepare any cached coordinates for queryHit - prepareHits: function() { - }, - - - // Called when queryHit calls have subsided. Good place to clear any coordinate caches. - releaseHits: function() { - }, - - - // Given coordinates from the topleft of the document, return data about the date-related area underneath. - // Can return an object with arbitrary properties (although top/right/left/bottom are encouraged). - // Must have a `grid` property, a reference to this current grid. TODO: avoid this - // The returned object will be processed by getHitSpan and getHitEl. - queryHit: function(leftOffset, topOffset) { - }, - - - // like getHitSpan, but returns null if the resulting span's range is invalid - getSafeHitSpan: function(hit) { - var hitSpan = this.getHitSpan(hit); - - if (!isRangeWithinRange(hitSpan, this.view.activeRange)) { - return null; - } - - return hitSpan; - }, - - - // Given position-level information about a date-related area within the grid, - // should return an object with at least a start/end date. Can provide other information as well. - getHitSpan: function(hit) { - }, - - - // Given position-level information about a date-related area within the grid, - // should return a jQuery element that best represents it. passed to dayClick callback. - getHitEl: function(hit) { - }, - - - /* Rendering - ------------------------------------------------------------------------------------------------------------------*/ - - - // Sets the container element that the grid should render inside of. - // Does other DOM-related initializations. - setElement: function(el) { - this.el = el; - - if (this.hasDayInteractions) { - preventSelection(el); - - this.bindDayHandler('touchstart', this.dayTouchStart); - this.bindDayHandler('mousedown', this.dayMousedown); - } - - // attach event-element-related handlers. in Grid.events - // same garbage collection note as above. - this.bindSegHandlers(); - - this.bindGlobalHandlers(); - }, - - - bindDayHandler: function(name, handler) { - var _this = this; - - // attach a handler to the grid's root element. - // jQuery will take care of unregistering them when removeElement gets called. - this.el.on(name, function(ev) { - if ( - !$(ev.target).is( - _this.segSelector + ',' + // directly on an event element - _this.segSelector + ' *,' + // within an event element - '.fc-more,' + // a "more.." link - 'a[data-goto]' // a clickable nav link - ) - ) { - return handler.call(_this, ev); - } - }); - }, - - - // Removes the grid's container element from the DOM. Undoes any other DOM-related attachments. - // DOES NOT remove any content beforehand (doesn't clear events or call unrenderDates), unlike View - removeElement: function() { - this.unbindGlobalHandlers(); - this.clearDragListeners(); - - this.el.remove(); - - // NOTE: we don't null-out this.el for the same reasons we don't do it within View::removeElement - }, - - - // Renders the basic structure of grid view before any content is rendered - renderSkeleton: function() { - // subclasses should implement - }, - - - // Renders the grid's date-related content (like areas that represent days/times). - // Assumes setRange has already been called and the skeleton has already been rendered. - renderDates: function() { - // subclasses should implement - }, - - - // Unrenders the grid's date-related content - unrenderDates: function() { - // subclasses should implement - }, - - - /* Handlers - ------------------------------------------------------------------------------------------------------------------*/ - - - // Binds DOM handlers to elements that reside outside the grid, such as the document - bindGlobalHandlers: function() { - this.listenTo($(document), { - dragstart: this.externalDragStart, // jqui - sortstart: this.externalDragStart // jqui - }); - }, - - - // Unbinds DOM handlers from elements that reside outside the grid - unbindGlobalHandlers: function() { - this.stopListeningTo($(document)); - }, - - - // Process a mousedown on an element that represents a day. For day clicking and selecting. - dayMousedown: function(ev) { - var view = this.view; - - // HACK - // This will still work even though bindDayHandler doesn't use GlobalEmitter. - if (GlobalEmitter.get().shouldIgnoreMouse()) { - return; - } - - this.dayClickListener.startInteraction(ev); - - if (view.opt('selectable')) { - this.daySelectListener.startInteraction(ev, { - distance: view.opt('selectMinDistance') - }); - } - }, - - - dayTouchStart: function(ev) { - var view = this.view; - var selectLongPressDelay; - - // On iOS (and Android?) when a new selection is initiated overtop another selection, - // the touchend never fires because the elements gets removed mid-touch-interaction (my theory). - // HACK: simply don't allow this to happen. - // ALSO: prevent selection when an *event* is already raised. - if (view.isSelected || view.selectedEvent) { - return; - } - - selectLongPressDelay = view.opt('selectLongPressDelay'); - if (selectLongPressDelay == null) { - selectLongPressDelay = view.opt('longPressDelay'); // fallback - } - - this.dayClickListener.startInteraction(ev); - - if (view.opt('selectable')) { - this.daySelectListener.startInteraction(ev, { - delay: selectLongPressDelay - }); - } - }, - - - // Creates a listener that tracks the user's drag across day elements, for day clicking. - buildDayClickListener: function() { - var _this = this; - var view = this.view; - var dayClickHit; // null if invalid dayClick - - var dragListener = new HitDragListener(this, { - scroll: view.opt('dragScroll'), - interactionStart: function() { - dayClickHit = dragListener.origHit; - }, - hitOver: function(hit, isOrig, origHit) { - // if user dragged to another cell at any point, it can no longer be a dayClick - if (!isOrig) { - dayClickHit = null; - } - }, - hitOut: function() { // called before mouse moves to a different hit OR moved out of all hits - dayClickHit = null; - }, - interactionEnd: function(ev, isCancelled) { - var hitSpan; - - if (!isCancelled && dayClickHit) { - hitSpan = _this.getSafeHitSpan(dayClickHit); - - if (hitSpan) { - view.triggerDayClick(hitSpan, _this.getHitEl(dayClickHit), ev); - } - } - } - }); - - // because dayClickListener won't be called with any time delay, "dragging" will begin immediately, - // which will kill any touchmoving/scrolling. Prevent this. - dragListener.shouldCancelTouchScroll = false; - - dragListener.scrollAlwaysKills = true; - - return dragListener; - }, - - - // Creates a listener that tracks the user's drag across day elements, for day selecting. - buildDaySelectListener: function() { - var _this = this; - var view = this.view; - var selectionSpan; // null if invalid selection - - var dragListener = new HitDragListener(this, { - scroll: view.opt('dragScroll'), - interactionStart: function() { - selectionSpan = null; - }, - dragStart: function() { - view.unselect(); // since we could be rendering a new selection, we want to clear any old one - }, - hitOver: function(hit, isOrig, origHit) { - var origHitSpan; - var hitSpan; - - if (origHit) { // click needs to have started on a hit - - origHitSpan = _this.getSafeHitSpan(origHit); - hitSpan = _this.getSafeHitSpan(hit); - - if (origHitSpan && hitSpan) { - selectionSpan = _this.computeSelection(origHitSpan, hitSpan); - } - else { - selectionSpan = null; - } - - if (selectionSpan) { - _this.renderSelection(selectionSpan); - } - else if (selectionSpan === false) { - disableCursor(); - } - } - }, - hitOut: function() { // called before mouse moves to a different hit OR moved out of all hits - selectionSpan = null; - _this.unrenderSelection(); - }, - hitDone: function() { // called after a hitOut OR before a dragEnd - enableCursor(); - }, - interactionEnd: function(ev, isCancelled) { - if (!isCancelled && selectionSpan) { - // the selection will already have been rendered. just report it - view.reportSelection(selectionSpan, ev); - } - } - }); - - return dragListener; - }, - - - // Kills all in-progress dragging. - // Useful for when public API methods that result in re-rendering are invoked during a drag. - // Also useful for when touch devices misbehave and don't fire their touchend. - clearDragListeners: function() { - this.dayClickListener.endInteraction(); - this.daySelectListener.endInteraction(); - - if (this.segDragListener) { - this.segDragListener.endInteraction(); // will clear this.segDragListener - } - if (this.segResizeListener) { - this.segResizeListener.endInteraction(); // will clear this.segResizeListener - } - if (this.externalDragListener) { - this.externalDragListener.endInteraction(); // will clear this.externalDragListener - } - }, - - - /* Event Helper - ------------------------------------------------------------------------------------------------------------------*/ - // TODO: should probably move this to Grid.events, like we did event dragging / resizing - - - // Renders a mock event at the given event location, which contains zoned start/end properties. - // Returns all mock event elements. - renderEventLocationHelper: function(eventLocation, sourceSeg) { - var fakeEvent = this.fabricateHelperEvent(eventLocation, sourceSeg); - - return this.renderHelper(fakeEvent, sourceSeg); // do the actual rendering - }, - - - // Builds a fake event given zoned event date properties and a segment is should be inspired from. - // The range's end can be null, in which case the mock event that is rendered will have a null end time. - // `sourceSeg` is the internal segment object involved in the drag. If null, something external is dragging. - fabricateHelperEvent: function(eventLocation, sourceSeg) { - var fakeEvent = sourceSeg ? createObject(sourceSeg.event) : {}; // mask the original event object if possible - - fakeEvent.start = eventLocation.start.clone(); - fakeEvent.end = eventLocation.end ? eventLocation.end.clone() : null; - fakeEvent.allDay = null; // force it to be freshly computed by normalizeEventDates - this.view.calendar.normalizeEventDates(fakeEvent); - - // this extra className will be useful for differentiating real events from mock events in CSS - fakeEvent.className = (fakeEvent.className || []).concat('fc-helper'); - - // if something external is being dragged in, don't render a resizer - if (!sourceSeg) { - fakeEvent.editable = false; - } - - return fakeEvent; - }, - - - // Renders a mock event. Given zoned event date properties. - // Must return all mock event elements. - renderHelper: function(eventLocation, sourceSeg) { - // subclasses must implement - }, - - - // Unrenders a mock event - unrenderHelper: function() { - // subclasses must implement - }, - - - /* Selection - ------------------------------------------------------------------------------------------------------------------*/ - - - // Renders a visual indication of a selection. Will highlight by default but can be overridden by subclasses. - // Given a span (unzoned start/end and other misc data) - renderSelection: function(span) { - this.renderHighlight(span); - }, - - - // Unrenders any visual indications of a selection. Will unrender a highlight by default. - unrenderSelection: function() { - this.unrenderHighlight(); - }, - - - // Given the first and last date-spans of a selection, returns another date-span object. - // Subclasses can override and provide additional data in the span object. Will be passed to renderSelection(). - // Will return false if the selection is invalid and this should be indicated to the user. - // Will return null/undefined if a selection invalid but no error should be reported. - computeSelection: function(span0, span1) { - var span = this.computeSelectionSpan(span0, span1); - - if (span && !this.view.calendar.isSelectionSpanAllowed(span)) { - return false; - } - - return span; - }, - - - // Given two spans, must return the combination of the two. - // TODO: do this separation of concerns (combining VS validation) for event dnd/resize too. - computeSelectionSpan: function(span0, span1) { - var dates = [ span0.start, span0.end, span1.start, span1.end ]; - - dates.sort(compareNumbers); // sorts chronologically. works with Moments - - return { start: dates[0].clone(), end: dates[3].clone() }; - }, - - - /* Highlight - ------------------------------------------------------------------------------------------------------------------*/ - - - // Renders an emphasis on the given date range. Given a span (unzoned start/end and other misc data) - renderHighlight: function(span) { - this.renderFill('highlight', this.spanToSegs(span)); - }, - - - // Unrenders the emphasis on a date range - unrenderHighlight: function() { - this.unrenderFill('highlight'); - }, - - - // Generates an array of classNames for rendering the highlight. Used by the fill system. - highlightSegClasses: function() { - return [ 'fc-highlight' ]; - }, - - - /* Business Hours - ------------------------------------------------------------------------------------------------------------------*/ - - - renderBusinessHours: function() { - }, - - - unrenderBusinessHours: function() { - }, - - - /* Now Indicator - ------------------------------------------------------------------------------------------------------------------*/ - - - getNowIndicatorUnit: function() { - }, - - - renderNowIndicator: function(date) { - }, - - - unrenderNowIndicator: function() { - }, - - - /* Fill System (highlight, background events, business hours) - -------------------------------------------------------------------------------------------------------------------- - TODO: remove this system. like we did in TimeGrid - */ - - - // Renders a set of rectangles over the given segments of time. - // MUST RETURN a subset of segs, the segs that were actually rendered. - // Responsible for populating this.elsByFill. TODO: better API for expressing this requirement - renderFill: function(type, segs) { - // subclasses must implement - }, - - - // Unrenders a specific type of fill that is currently rendered on the grid - unrenderFill: function(type) { - var el = this.elsByFill[type]; - - if (el) { - el.remove(); - delete this.elsByFill[type]; - } - }, - - - // Renders and assigns an `el` property for each fill segment. Generic enough to work with different types. - // Only returns segments that successfully rendered. - // To be harnessed by renderFill (implemented by subclasses). - // Analagous to renderFgSegEls. - renderFillSegEls: function(type, segs) { - var _this = this; - var segElMethod = this[type + 'SegEl']; - var html = ''; - var renderedSegs = []; - var i; - - if (segs.length) { - - // build a large concatenation of segment HTML - for (i = 0; i < segs.length; i++) { - html += this.fillSegHtml(type, segs[i]); - } - - // Grab individual elements from the combined HTML string. Use each as the default rendering. - // Then, compute the 'el' for each segment. - $(html).each(function(i, node) { - var seg = segs[i]; - var el = $(node); - - // allow custom filter methods per-type - if (segElMethod) { - el = segElMethod.call(_this, seg, el); - } - - if (el) { // custom filters did not cancel the render - el = $(el); // allow custom filter to return raw DOM node - - // correct element type? (would be bad if a non-TD were inserted into a table for example) - if (el.is(_this.fillSegTag)) { - seg.el = el; - renderedSegs.push(seg); - } - } - }); - } - - return renderedSegs; - }, - - - fillSegTag: 'div', // subclasses can override - - - // Builds the HTML needed for one fill segment. Generic enough to work with different types. - fillSegHtml: function(type, seg) { - - // custom hooks per-type - var classesMethod = this[type + 'SegClasses']; - var cssMethod = this[type + 'SegCss']; - - var classes = classesMethod ? classesMethod.call(this, seg) : []; - var css = cssToStr(cssMethod ? cssMethod.call(this, seg) : {}); - - return '<' + this.fillSegTag + - (classes.length ? ' class="' + classes.join(' ') + '"' : '') + - (css ? ' style="' + css + '"' : '') + - ' />'; - }, - - - - /* Generic rendering utilities for subclasses - ------------------------------------------------------------------------------------------------------------------*/ - - - // Computes HTML classNames for a single-day element - getDayClasses: function(date, noThemeHighlight) { - var view = this.view; - var classes = []; - var today; - - if (!isDateWithinRange(date, view.activeRange)) { - classes.push('fc-disabled-day'); // TODO: jQuery UI theme? - } - else { - classes.push('fc-' + dayIDs[date.day()]); - - if ( - view.currentRangeAs('months') == 1 && // TODO: somehow get into MonthView - date.month() != view.currentRange.start.month() - ) { - classes.push('fc-other-month'); - } - - today = view.calendar.getNow(); - - if (date.isSame(today, 'day')) { - classes.push('fc-today'); - - if (noThemeHighlight !== true) { - classes.push(view.highlightStateClass); - } - } - else if (date < today) { - classes.push('fc-past'); - } - else { - classes.push('fc-future'); - } - } - - return classes; - } - -}); - -;; - -/* Event-rendering and event-interaction methods for the abstract Grid class ----------------------------------------------------------------------------------------------------------------------- - -Data Types: - event - { title, id, start, (end), whatever } - location - { start, (end), allDay } - rawEventRange - { start, end } - eventRange - { start, end, isStart, isEnd } - eventSpan - { start, end, isStart, isEnd, whatever } - eventSeg - { event, whatever } - seg - { whatever } -*/ - -Grid.mixin({ - - // self-config, overridable by subclasses - segSelector: '.fc-event-container > *', // what constitutes an event element? - - mousedOverSeg: null, // the segment object the user's mouse is over. null if over nothing - isDraggingSeg: false, // is a segment being dragged? boolean - isResizingSeg: false, // is a segment being resized? boolean - isDraggingExternal: false, // jqui-dragging an external element? boolean - segs: null, // the *event* segments currently rendered in the grid. TODO: rename to `eventSegs` - - - // Renders the given events onto the grid - renderEvents: function(events) { - var bgEvents = []; - var fgEvents = []; - var i; - - for (i = 0; i < events.length; i++) { - (isBgEvent(events[i]) ? bgEvents : fgEvents).push(events[i]); - } - - this.segs = [].concat( // record all segs - this.renderBgEvents(bgEvents), - this.renderFgEvents(fgEvents) - ); - }, - - - renderBgEvents: function(events) { - var segs = this.eventsToSegs(events); - - // renderBgSegs might return a subset of segs, segs that were actually rendered - return this.renderBgSegs(segs) || segs; - }, - - - renderFgEvents: function(events) { - var segs = this.eventsToSegs(events); - - // renderFgSegs might return a subset of segs, segs that were actually rendered - return this.renderFgSegs(segs) || segs; - }, - - - // Unrenders all events currently rendered on the grid - unrenderEvents: function() { - this.handleSegMouseout(); // trigger an eventMouseout if user's mouse is over an event - this.clearDragListeners(); - - this.unrenderFgSegs(); - this.unrenderBgSegs(); - - this.segs = null; - }, - - - // Retrieves all rendered segment objects currently rendered on the grid - getEventSegs: function() { - return this.segs || []; - }, - - - /* Foreground Segment Rendering - ------------------------------------------------------------------------------------------------------------------*/ - - - // Renders foreground event segments onto the grid. May return a subset of segs that were rendered. - renderFgSegs: function(segs) { - // subclasses must implement - }, - - - // Unrenders all currently rendered foreground segments - unrenderFgSegs: function() { - // subclasses must implement - }, - - - // Renders and assigns an `el` property for each foreground event segment. - // Only returns segments that successfully rendered. - // A utility that subclasses may use. - renderFgSegEls: function(segs, disableResizing) { - var view = this.view; - var html = ''; - var renderedSegs = []; - var i; - - if (segs.length) { // don't build an empty html string - - // build a large concatenation of event segment HTML - for (i = 0; i < segs.length; i++) { - html += this.fgSegHtml(segs[i], disableResizing); - } - - // Grab individual elements from the combined HTML string. Use each as the default rendering. - // Then, compute the 'el' for each segment. An el might be null if the eventRender callback returned false. - $(html).each(function(i, node) { - var seg = segs[i]; - var el = view.resolveEventEl(seg.event, $(node)); - - if (el) { - el.data('fc-seg', seg); // used by handlers - seg.el = el; - renderedSegs.push(seg); - } - }); - } - - return renderedSegs; - }, - - - // Generates the HTML for the default rendering of a foreground event segment. Used by renderFgSegEls() - fgSegHtml: function(seg, disableResizing) { - // subclasses should implement - }, - - - /* Background Segment Rendering - ------------------------------------------------------------------------------------------------------------------*/ - - - // Renders the given background event segments onto the grid. - // Returns a subset of the segs that were actually rendered. - renderBgSegs: function(segs) { - return this.renderFill('bgEvent', segs); - }, - - - // Unrenders all the currently rendered background event segments - unrenderBgSegs: function() { - this.unrenderFill('bgEvent'); - }, - - - // Renders a background event element, given the default rendering. Called by the fill system. - bgEventSegEl: function(seg, el) { - return this.view.resolveEventEl(seg.event, el); // will filter through eventRender - }, - - - // Generates an array of classNames to be used for the default rendering of a background event. - // Called by fillSegHtml. - bgEventSegClasses: function(seg) { - var event = seg.event; - var source = event.source || {}; - - return [ 'fc-bgevent' ].concat( - event.className, - source.className || [] - ); - }, - - - // Generates a semicolon-separated CSS string to be used for the default rendering of a background event. - // Called by fillSegHtml. - bgEventSegCss: function(seg) { - return { - 'background-color': this.getSegSkinCss(seg)['background-color'] - }; - }, - - - // Generates an array of classNames to be used for the rendering business hours overlay. Called by the fill system. - // Called by fillSegHtml. - businessHoursSegClasses: function(seg) { - return [ 'fc-nonbusiness', 'fc-bgevent' ]; - }, - - - /* Business Hours - ------------------------------------------------------------------------------------------------------------------*/ - - - // Compute business hour segs for the grid's current date range. - // Caller must ask if whole-day business hours are needed. - // If no `businessHours` configuration value is specified, assumes the calendar default. - buildBusinessHourSegs: function(wholeDay, businessHours) { - return this.eventsToSegs( - this.buildBusinessHourEvents(wholeDay, businessHours) - ); - }, - - - // Compute business hour *events* for the grid's current date range. - // Caller must ask if whole-day business hours are needed. - // If no `businessHours` configuration value is specified, assumes the calendar default. - buildBusinessHourEvents: function(wholeDay, businessHours) { - var calendar = this.view.calendar; - var events; - - if (businessHours == null) { - // fallback - // access from calendawr. don't access from view. doesn't update with dynamic options. - businessHours = calendar.opt('businessHours'); - } - - events = calendar.computeBusinessHourEvents(wholeDay, businessHours); - - // HACK. Eventually refactor business hours "events" system. - // If no events are given, but businessHours is activated, this means the entire visible range should be - // marked as *not* business-hours, via inverse-background rendering. - if (!events.length && businessHours) { - events = [ - $.extend({}, BUSINESS_HOUR_EVENT_DEFAULTS, { - start: this.view.activeRange.end, // guaranteed out-of-range - end: this.view.activeRange.end, // " - dow: null - }) - ]; - } - - return events; - }, - - - /* Handlers - ------------------------------------------------------------------------------------------------------------------*/ - - - // Attaches event-element-related handlers for *all* rendered event segments of the view. - bindSegHandlers: function() { - this.bindSegHandlersToEl(this.el); - }, - - - // Attaches event-element-related handlers to an arbitrary container element. leverages bubbling. - bindSegHandlersToEl: function(el) { - this.bindSegHandlerToEl(el, 'touchstart', this.handleSegTouchStart); - this.bindSegHandlerToEl(el, 'mouseenter', this.handleSegMouseover); - this.bindSegHandlerToEl(el, 'mouseleave', this.handleSegMouseout); - this.bindSegHandlerToEl(el, 'mousedown', this.handleSegMousedown); - this.bindSegHandlerToEl(el, 'click', this.handleSegClick); - }, - - - // Executes a handler for any a user-interaction on a segment. - // Handler gets called with (seg, ev), and with the `this` context of the Grid - bindSegHandlerToEl: function(el, name, handler) { - var _this = this; - - el.on(name, this.segSelector, function(ev) { - var seg = $(this).data('fc-seg'); // grab segment data. put there by View::renderEvents - - // only call the handlers if there is not a drag/resize in progress - if (seg && !_this.isDraggingSeg && !_this.isResizingSeg) { - return handler.call(_this, seg, ev); // context will be the Grid - } - }); - }, - - - handleSegClick: function(seg, ev) { - var res = this.view.publiclyTrigger('eventClick', seg.el[0], seg.event, ev); // can return `false` to cancel - if (res === false) { - ev.preventDefault(); - } - }, - - - // Updates internal state and triggers handlers for when an event element is moused over - handleSegMouseover: function(seg, ev) { - if ( - !GlobalEmitter.get().shouldIgnoreMouse() && - !this.mousedOverSeg - ) { - this.mousedOverSeg = seg; - if (this.view.isEventResizable(seg.event)) { - seg.el.addClass('fc-allow-mouse-resize'); - } - this.view.publiclyTrigger('eventMouseover', seg.el[0], seg.event, ev); - } - }, - - - // Updates internal state and triggers handlers for when an event element is moused out. - // Can be given no arguments, in which case it will mouseout the segment that was previously moused over. - handleSegMouseout: function(seg, ev) { - ev = ev || {}; // if given no args, make a mock mouse event - - if (this.mousedOverSeg) { - seg = seg || this.mousedOverSeg; // if given no args, use the currently moused-over segment - this.mousedOverSeg = null; - if (this.view.isEventResizable(seg.event)) { - seg.el.removeClass('fc-allow-mouse-resize'); - } - this.view.publiclyTrigger('eventMouseout', seg.el[0], seg.event, ev); - } - }, - - - handleSegMousedown: function(seg, ev) { - var isResizing = this.startSegResize(seg, ev, { distance: 5 }); - - if (!isResizing && this.view.isEventDraggable(seg.event)) { - this.buildSegDragListener(seg) - .startInteraction(ev, { - distance: 5 - }); - } - }, - - - handleSegTouchStart: function(seg, ev) { - var view = this.view; - var event = seg.event; - var isSelected = view.isEventSelected(event); - var isDraggable = view.isEventDraggable(event); - var isResizable = view.isEventResizable(event); - var isResizing = false; - var dragListener; - var eventLongPressDelay; - - if (isSelected && isResizable) { - // only allow resizing of the event is selected - isResizing = this.startSegResize(seg, ev); - } - - if (!isResizing && (isDraggable || isResizable)) { // allowed to be selected? - - eventLongPressDelay = view.opt('eventLongPressDelay'); - if (eventLongPressDelay == null) { - eventLongPressDelay = view.opt('longPressDelay'); // fallback - } - - dragListener = isDraggable ? - this.buildSegDragListener(seg) : - this.buildSegSelectListener(seg); // seg isn't draggable, but still needs to be selected - - dragListener.startInteraction(ev, { // won't start if already started - delay: isSelected ? 0 : eventLongPressDelay // do delay if not already selected - }); - } - }, - - - // returns boolean whether resizing actually started or not. - // assumes the seg allows resizing. - // `dragOptions` are optional. - startSegResize: function(seg, ev, dragOptions) { - if ($(ev.target).is('.fc-resizer')) { - this.buildSegResizeListener(seg, $(ev.target).is('.fc-start-resizer')) - .startInteraction(ev, dragOptions); - return true; - } - return false; - }, - - - - /* Event Dragging - ------------------------------------------------------------------------------------------------------------------*/ - - - // Builds a listener that will track user-dragging on an event segment. - // Generic enough to work with any type of Grid. - // Has side effect of setting/unsetting `segDragListener` - buildSegDragListener: function(seg) { - var _this = this; - var view = this.view; - var el = seg.el; - var event = seg.event; - var isDragging; - var mouseFollower; // A clone of the original element that will move with the mouse - var dropLocation; // zoned event date properties - - if (this.segDragListener) { - return this.segDragListener; - } - - // Tracks mouse movement over the *view's* coordinate map. Allows dragging and dropping between subcomponents - // of the view. - var dragListener = this.segDragListener = new HitDragListener(view, { - scroll: view.opt('dragScroll'), - subjectEl: el, - subjectCenter: true, - interactionStart: function(ev) { - seg.component = _this; // for renderDrag - isDragging = false; - mouseFollower = new MouseFollower(seg.el, { - additionalClass: 'fc-dragging', - parentEl: view.el, - opacity: dragListener.isTouch ? null : view.opt('dragOpacity'), - revertDuration: view.opt('dragRevertDuration'), - zIndex: 2 // one above the .fc-view - }); - mouseFollower.hide(); // don't show until we know this is a real drag - mouseFollower.start(ev); - }, - dragStart: function(ev) { - if (dragListener.isTouch && !view.isEventSelected(event)) { - // if not previously selected, will fire after a delay. then, select the event - view.selectEvent(event); - } - isDragging = true; - _this.handleSegMouseout(seg, ev); // ensure a mouseout on the manipulated event has been reported - _this.segDragStart(seg, ev); - view.hideEvent(event); // hide all event segments. our mouseFollower will take over - }, - hitOver: function(hit, isOrig, origHit) { - var isAllowed = true; - var origHitSpan; - var hitSpan; - var dragHelperEls; - - // starting hit could be forced (DayGrid.limit) - if (seg.hit) { - origHit = seg.hit; - } - - // hit might not belong to this grid, so query origin grid - origHitSpan = origHit.component.getSafeHitSpan(origHit); - hitSpan = hit.component.getSafeHitSpan(hit); - - if (origHitSpan && hitSpan) { - dropLocation = _this.computeEventDrop(origHitSpan, hitSpan, event); - isAllowed = dropLocation && _this.isEventLocationAllowed(dropLocation, event); - } - else { - isAllowed = false; - } - - if (!isAllowed) { - dropLocation = null; - disableCursor(); - } - - // if a valid drop location, have the subclass render a visual indication - if (dropLocation && (dragHelperEls = view.renderDrag(dropLocation, seg))) { - - dragHelperEls.addClass('fc-dragging'); - if (!dragListener.isTouch) { - _this.applyDragOpacity(dragHelperEls); - } - - mouseFollower.hide(); // if the subclass is already using a mock event "helper", hide our own - } - else { - mouseFollower.show(); // otherwise, have the helper follow the mouse (no snapping) - } - - if (isOrig) { - dropLocation = null; // needs to have moved hits to be a valid drop - } - }, - hitOut: function() { // called before mouse moves to a different hit OR moved out of all hits - view.unrenderDrag(); // unrender whatever was done in renderDrag - mouseFollower.show(); // show in case we are moving out of all hits - dropLocation = null; - }, - hitDone: function() { // Called after a hitOut OR before a dragEnd - enableCursor(); - }, - interactionEnd: function(ev) { - delete seg.component; // prevent side effects - - // do revert animation if hasn't changed. calls a callback when finished (whether animation or not) - mouseFollower.stop(!dropLocation, function() { - if (isDragging) { - view.unrenderDrag(); - _this.segDragStop(seg, ev); - } - - if (dropLocation) { - // no need to re-show original, will rerender all anyways. esp important if eventRenderWait - view.reportSegDrop(seg, dropLocation, _this.largeUnit, el, ev); - } - else { - view.showEvent(event); - } - }); - _this.segDragListener = null; - } - }); - - return dragListener; - }, - - - // seg isn't draggable, but let's use a generic DragListener - // simply for the delay, so it can be selected. - // Has side effect of setting/unsetting `segDragListener` - buildSegSelectListener: function(seg) { - var _this = this; - var view = this.view; - var event = seg.event; - - if (this.segDragListener) { - return this.segDragListener; - } - - var dragListener = this.segDragListener = new DragListener({ - dragStart: function(ev) { - if (dragListener.isTouch && !view.isEventSelected(event)) { - // if not previously selected, will fire after a delay. then, select the event - view.selectEvent(event); - } - }, - interactionEnd: function(ev) { - _this.segDragListener = null; - } - }); - - return dragListener; - }, - - - // Called before event segment dragging starts - segDragStart: function(seg, ev) { - this.isDraggingSeg = true; - this.view.publiclyTrigger('eventDragStart', seg.el[0], seg.event, ev, {}); // last argument is jqui dummy - }, - - - // Called after event segment dragging stops - segDragStop: function(seg, ev) { - this.isDraggingSeg = false; - this.view.publiclyTrigger('eventDragStop', seg.el[0], seg.event, ev, {}); // last argument is jqui dummy - }, - - - // Given the spans an event drag began, and the span event was dropped, calculates the new zoned start/end/allDay - // values for the event. Subclasses may override and set additional properties to be used by renderDrag. - // A falsy returned value indicates an invalid drop. - // DOES NOT consider overlap/constraint. - computeEventDrop: function(startSpan, endSpan, event) { - var calendar = this.view.calendar; - var dragStart = startSpan.start; - var dragEnd = endSpan.start; - var delta; - var dropLocation; // zoned event date properties - - if (dragStart.hasTime() === dragEnd.hasTime()) { - delta = this.diffDates(dragEnd, dragStart); - - // if an all-day event was in a timed area and it was dragged to a different time, - // guarantee an end and adjust start/end to have times - if (event.allDay && durationHasTime(delta)) { - dropLocation = { - start: event.start.clone(), - end: calendar.getEventEnd(event), // will be an ambig day - allDay: false // for normalizeEventTimes - }; - calendar.normalizeEventTimes(dropLocation); - } - // othewise, work off existing values - else { - dropLocation = pluckEventDateProps(event); - } - - dropLocation.start.add(delta); - if (dropLocation.end) { - dropLocation.end.add(delta); - } - } - else { - // if switching from day <-> timed, start should be reset to the dropped date, and the end cleared - dropLocation = { - start: dragEnd.clone(), - end: null, // end should be cleared - allDay: !dragEnd.hasTime() - }; - } - - return dropLocation; - }, - - - // Utility for apply dragOpacity to a jQuery set - applyDragOpacity: function(els) { - var opacity = this.view.opt('dragOpacity'); - - if (opacity != null) { - els.css('opacity', opacity); - } - }, - - - /* External Element Dragging - ------------------------------------------------------------------------------------------------------------------*/ - - - // Called when a jQuery UI drag is initiated anywhere in the DOM - externalDragStart: function(ev, ui) { - var view = this.view; - var el; - var accept; - - if (view.opt('droppable')) { // only listen if this setting is on - el = $((ui ? ui.item : null) || ev.target); - - // Test that the dragged element passes the dropAccept selector or filter function. - // FYI, the default is "*" (matches all) - accept = view.opt('dropAccept'); - if ($.isFunction(accept) ? accept.call(el[0], el) : el.is(accept)) { - if (!this.isDraggingExternal) { // prevent double-listening if fired twice - this.listenToExternalDrag(el, ev, ui); - } - } - } - }, - - - // Called when a jQuery UI drag starts and it needs to be monitored for dropping - listenToExternalDrag: function(el, ev, ui) { - var _this = this; - var view = this.view; - var meta = getDraggedElMeta(el); // extra data about event drop, including possible event to create - var dropLocation; // a null value signals an unsuccessful drag - - // listener that tracks mouse movement over date-associated pixel regions - var dragListener = _this.externalDragListener = new HitDragListener(this, { - interactionStart: function() { - _this.isDraggingExternal = true; - }, - hitOver: function(hit) { - var isAllowed = true; - var hitSpan = hit.component.getSafeHitSpan(hit); // hit might not belong to this grid - - if (hitSpan) { - dropLocation = _this.computeExternalDrop(hitSpan, meta); - isAllowed = dropLocation && _this.isExternalLocationAllowed(dropLocation, meta.eventProps); - } - else { - isAllowed = false; - } - - if (!isAllowed) { - dropLocation = null; - disableCursor(); - } - - if (dropLocation) { - _this.renderDrag(dropLocation); // called without a seg parameter - } - }, - hitOut: function() { - dropLocation = null; // signal unsuccessful - }, - hitDone: function() { // Called after a hitOut OR before a dragEnd - enableCursor(); - _this.unrenderDrag(); - }, - interactionEnd: function(ev) { - if (dropLocation) { // element was dropped on a valid hit - view.reportExternalDrop(meta, dropLocation, el, ev, ui); - } - _this.isDraggingExternal = false; - _this.externalDragListener = null; - } - }); - - dragListener.startDrag(ev); // start listening immediately - }, - - - // Given a hit to be dropped upon, and misc data associated with the jqui drag (guaranteed to be a plain object), - // returns the zoned start/end dates for the event that would result from the hypothetical drop. end might be null. - // Returning a null value signals an invalid drop hit. - // DOES NOT consider overlap/constraint. - computeExternalDrop: function(span, meta) { - var calendar = this.view.calendar; - var dropLocation = { - start: calendar.applyTimezone(span.start), // simulate a zoned event start date - end: null - }; - - // if dropped on an all-day span, and element's metadata specified a time, set it - if (meta.startTime && !dropLocation.start.hasTime()) { - dropLocation.start.time(meta.startTime); - } - - if (meta.duration) { - dropLocation.end = dropLocation.start.clone().add(meta.duration); - } - - return dropLocation; - }, - - - - /* Drag Rendering (for both events and an external elements) - ------------------------------------------------------------------------------------------------------------------*/ - - - // Renders a visual indication of an event or external element being dragged. - // `dropLocation` contains hypothetical start/end/allDay values the event would have if dropped. end can be null. - // `seg` is the internal segment object that is being dragged. If dragging an external element, `seg` is null. - // A truthy returned value indicates this method has rendered a helper element. - // Must return elements used for any mock events. - renderDrag: function(dropLocation, seg) { - // subclasses must implement - }, - - - // Unrenders a visual indication of an event or external element being dragged - unrenderDrag: function() { - // subclasses must implement - }, - - - /* Resizing - ------------------------------------------------------------------------------------------------------------------*/ - - - // Creates a listener that tracks the user as they resize an event segment. - // Generic enough to work with any type of Grid. - buildSegResizeListener: function(seg, isStart) { - var _this = this; - var view = this.view; - var calendar = view.calendar; - var el = seg.el; - var event = seg.event; - var eventEnd = calendar.getEventEnd(event); - var isDragging; - var resizeLocation; // zoned event date properties. falsy if invalid resize - - // Tracks mouse movement over the *grid's* coordinate map - var dragListener = this.segResizeListener = new HitDragListener(this, { - scroll: view.opt('dragScroll'), - subjectEl: el, - interactionStart: function() { - isDragging = false; - }, - dragStart: function(ev) { - isDragging = true; - _this.handleSegMouseout(seg, ev); // ensure a mouseout on the manipulated event has been reported - _this.segResizeStart(seg, ev); - }, - hitOver: function(hit, isOrig, origHit) { - var isAllowed = true; - var origHitSpan = _this.getSafeHitSpan(origHit); - var hitSpan = _this.getSafeHitSpan(hit); - - if (origHitSpan && hitSpan) { - resizeLocation = isStart ? - _this.computeEventStartResize(origHitSpan, hitSpan, event) : - _this.computeEventEndResize(origHitSpan, hitSpan, event); - - isAllowed = resizeLocation && _this.isEventLocationAllowed(resizeLocation, event); - } - else { - isAllowed = false; - } - - if (!isAllowed) { - resizeLocation = null; - disableCursor(); - } - else { - if ( - resizeLocation.start.isSame(event.start.clone().stripZone()) && - resizeLocation.end.isSame(eventEnd.clone().stripZone()) - ) { - // no change. (FYI, event dates might have zones) - resizeLocation = null; - } - } - - if (resizeLocation) { - view.hideEvent(event); - _this.renderEventResize(resizeLocation, seg); - } - }, - hitOut: function() { // called before mouse moves to a different hit OR moved out of all hits - resizeLocation = null; - view.showEvent(event); // for when out-of-bounds. show original - }, - hitDone: function() { // resets the rendering to show the original event - _this.unrenderEventResize(); - enableCursor(); - }, - interactionEnd: function(ev) { - if (isDragging) { - _this.segResizeStop(seg, ev); - } - - if (resizeLocation) { // valid date to resize to? - // no need to re-show original, will rerender all anyways. esp important if eventRenderWait - view.reportSegResize(seg, resizeLocation, _this.largeUnit, el, ev); - } - else { - view.showEvent(event); - } - _this.segResizeListener = null; - } - }); - - return dragListener; - }, - - - // Called before event segment resizing starts - segResizeStart: function(seg, ev) { - this.isResizingSeg = true; - this.view.publiclyTrigger('eventResizeStart', seg.el[0], seg.event, ev, {}); // last argument is jqui dummy - }, - - - // Called after event segment resizing stops - segResizeStop: function(seg, ev) { - this.isResizingSeg = false; - this.view.publiclyTrigger('eventResizeStop', seg.el[0], seg.event, ev, {}); // last argument is jqui dummy - }, - - - // Returns new date-information for an event segment being resized from its start - computeEventStartResize: function(startSpan, endSpan, event) { - return this.computeEventResize('start', startSpan, endSpan, event); - }, - - - // Returns new date-information for an event segment being resized from its end - computeEventEndResize: function(startSpan, endSpan, event) { - return this.computeEventResize('end', startSpan, endSpan, event); - }, - - - // Returns new zoned date information for an event segment being resized from its start OR end - // `type` is either 'start' or 'end'. - // DOES NOT consider overlap/constraint. - computeEventResize: function(type, startSpan, endSpan, event) { - var calendar = this.view.calendar; - var delta = this.diffDates(endSpan[type], startSpan[type]); - var resizeLocation; // zoned event date properties - var defaultDuration; - - // build original values to work from, guaranteeing a start and end - resizeLocation = { - start: event.start.clone(), - end: calendar.getEventEnd(event), - allDay: event.allDay - }; - - // if an all-day event was in a timed area and was resized to a time, adjust start/end to have times - if (resizeLocation.allDay && durationHasTime(delta)) { - resizeLocation.allDay = false; - calendar.normalizeEventTimes(resizeLocation); - } - - resizeLocation[type].add(delta); // apply delta to start or end - - // if the event was compressed too small, find a new reasonable duration for it - if (!resizeLocation.start.isBefore(resizeLocation.end)) { - - defaultDuration = - this.minResizeDuration || // TODO: hack - (event.allDay ? - calendar.defaultAllDayEventDuration : - calendar.defaultTimedEventDuration); - - if (type == 'start') { // resizing the start? - resizeLocation.start = resizeLocation.end.clone().subtract(defaultDuration); - } - else { // resizing the end? - resizeLocation.end = resizeLocation.start.clone().add(defaultDuration); - } - } - - return resizeLocation; - }, - - - // Renders a visual indication of an event being resized. - // `range` has the updated dates of the event. `seg` is the original segment object involved in the drag. - // Must return elements used for any mock events. - renderEventResize: function(range, seg) { - // subclasses must implement - }, - - - // Unrenders a visual indication of an event being resized. - unrenderEventResize: function() { - // subclasses must implement - }, - - - /* Rendering Utils - ------------------------------------------------------------------------------------------------------------------*/ - - - // Compute the text that should be displayed on an event's element. - // `range` can be the Event object itself, or something range-like, with at least a `start`. - // If event times are disabled, or the event has no time, will return a blank string. - // If not specified, formatStr will default to the eventTimeFormat setting, - // and displayEnd will default to the displayEventEnd setting. - getEventTimeText: function(range, formatStr, displayEnd) { - - if (formatStr == null) { - formatStr = this.eventTimeFormat; - } - - if (displayEnd == null) { - displayEnd = this.displayEventEnd; - } - - if (this.displayEventTime && range.start.hasTime()) { - if (displayEnd && range.end) { - return this.view.formatRange(range, formatStr); - } - else { - return range.start.format(formatStr); - } - } - - return ''; - }, - - - // Generic utility for generating the HTML classNames for an event segment's element - getSegClasses: function(seg, isDraggable, isResizable) { - var view = this.view; - var classes = [ - 'fc-event', - seg.isStart ? 'fc-start' : 'fc-not-start', - seg.isEnd ? 'fc-end' : 'fc-not-end' - ].concat(this.getSegCustomClasses(seg)); - - if (isDraggable) { - classes.push('fc-draggable'); - } - if (isResizable) { - classes.push('fc-resizable'); - } - - // event is currently selected? attach a className. - if (view.isEventSelected(seg.event)) { - classes.push('fc-selected'); - } - - return classes; - }, - - - // List of classes that were defined by the caller of the API in some way - getSegCustomClasses: function(seg) { - var event = seg.event; - - return [].concat( - event.className, // guaranteed to be an array - event.source ? event.source.className : [] - ); - }, - - - // Utility for generating event skin-related CSS properties - getSegSkinCss: function(seg) { - return { - 'background-color': this.getSegBackgroundColor(seg), - 'border-color': this.getSegBorderColor(seg), - color: this.getSegTextColor(seg) - }; - }, - - - // Queries for caller-specified color, then falls back to default - getSegBackgroundColor: function(seg) { - return seg.event.backgroundColor || - seg.event.color || - this.getSegDefaultBackgroundColor(seg); - }, - - - getSegDefaultBackgroundColor: function(seg) { - var source = seg.event.source || {}; - - return source.backgroundColor || - source.color || - this.view.opt('eventBackgroundColor') || - this.view.opt('eventColor'); - }, - - - // Queries for caller-specified color, then falls back to default - getSegBorderColor: function(seg) { - return seg.event.borderColor || - seg.event.color || - this.getSegDefaultBorderColor(seg); - }, - - - getSegDefaultBorderColor: function(seg) { - var source = seg.event.source || {}; - - return source.borderColor || - source.color || - this.view.opt('eventBorderColor') || - this.view.opt('eventColor'); - }, - - - // Queries for caller-specified color, then falls back to default - getSegTextColor: function(seg) { - return seg.event.textColor || - this.getSegDefaultTextColor(seg); - }, - - - getSegDefaultTextColor: function(seg) { - var source = seg.event.source || {}; - - return source.textColor || - this.view.opt('eventTextColor'); - }, - - - /* Event Location Validation - ------------------------------------------------------------------------------------------------------------------*/ - - - isEventLocationAllowed: function(eventLocation, event) { - if (this.isEventLocationInRange(eventLocation)) { - var calendar = this.view.calendar; - var eventSpans = this.eventToSpans(eventLocation); - var i; - - if (eventSpans.length) { - for (i = 0; i < eventSpans.length; i++) { - if (!calendar.isEventSpanAllowed(eventSpans[i], event)) { - return false; - } - } - - return true; - } - } - - return false; - }, - - - isExternalLocationAllowed: function(eventLocation, metaProps) { // FOR the external element - if (this.isEventLocationInRange(eventLocation)) { - var calendar = this.view.calendar; - var eventSpans = this.eventToSpans(eventLocation); - var i; - - if (eventSpans.length) { - for (i = 0; i < eventSpans.length; i++) { - if (!calendar.isExternalSpanAllowed(eventSpans[i], eventLocation, metaProps)) { - return false; - } - } - - return true; - } - } - - return false; - }, - - - isEventLocationInRange: function(eventLocation) { - return isRangeWithinRange( - this.eventToRawRange(eventLocation), - this.view.validRange - ); - }, - - - /* Converting events -> eventRange -> eventSpan -> eventSegs - ------------------------------------------------------------------------------------------------------------------*/ - - - // Generates an array of segments for the given single event - // Can accept an event "location" as well (which only has start/end and no allDay) - eventToSegs: function(event) { - return this.eventsToSegs([ event ]); - }, - - - // Generates spans (always unzoned) for the given event. - // Does not do any inverting for inverse-background events. - // Can accept an event "location" as well (which only has start/end and no allDay) - eventToSpans: function(event) { - var eventRange = this.eventToRange(event); // { start, end, isStart, isEnd } - - if (eventRange) { - return this.eventRangeToSpans(eventRange, event); - } - else { // out of view's valid range - return []; - } - }, - - - - // Converts an array of event objects into an array of event segment objects. - // A custom `segSliceFunc` may be given for arbitrarily slicing up events. - // Doesn't guarantee an order for the resulting array. - eventsToSegs: function(allEvents, segSliceFunc) { - var _this = this; - var eventsById = groupEventsById(allEvents); - var segs = []; - - $.each(eventsById, function(id, events) { - var visibleEvents = []; - var eventRanges = []; - var eventRange; // { start, end, isStart, isEnd } - var i; - - for (i = 0; i < events.length; i++) { - eventRange = _this.eventToRange(events[i]); // might be null if completely out of range - - if (eventRange) { - eventRanges.push(eventRange); - visibleEvents.push(events[i]); - } - } - - // inverse-background events (utilize only the first event in calculations) - if (isInverseBgEvent(events[0])) { - eventRanges = _this.invertRanges(eventRanges); // will lose isStart/isEnd - - for (i = 0; i < eventRanges.length; i++) { - segs.push.apply(segs, // append to - _this.eventRangeToSegs(eventRanges[i], events[0], segSliceFunc) - ); - } - } - // normal event ranges - else { - for (i = 0; i < eventRanges.length; i++) { - segs.push.apply(segs, // append to - _this.eventRangeToSegs(eventRanges[i], visibleEvents[i], segSliceFunc) - ); - } - } - }); - - return segs; - }, - - - // Generates the unzoned start/end dates an event appears to occupy - // Can accept an event "location" as well (which only has start/end and no allDay) - // returns { start, end, isStart, isEnd } - // If the event is completely outside of the grid's valid range, will return undefined. - eventToRange: function(event) { - return this.refineRawEventRange( - this.eventToRawRange(event) - ); - }, - - - // Ensures the given range is within the view's activeRange and is correctly localized. - // Always returns a result - refineRawEventRange: function(rawRange) { - var view = this.view; - var calendar = view.calendar; - var range = intersectRanges(rawRange, view.activeRange); - - if (range) { // otherwise, event doesn't have valid range - - // hack: dynamic locale change forgets to upate stored event localed - calendar.localizeMoment(range.start); - calendar.localizeMoment(range.end); - - return range; - } - }, - - - // not constrained to valid dates - // not given localizeMoment hack - eventToRawRange: function(event) { - var calendar = this.view.calendar; - var start = event.start.clone().stripZone(); - var end = ( - event.end ? - event.end.clone() : - // derive the end from the start and allDay. compute allDay if necessary - calendar.getDefaultEventEnd( - event.allDay != null ? - event.allDay : - !event.start.hasTime(), - event.start - ) - ).stripZone(); - - return { start: start, end: end }; - }, - - - // Given an event's range (unzoned start/end), and the event itself, - // slice into segments (using the segSliceFunc function if specified) - // eventRange - { start, end, isStart, isEnd } - eventRangeToSegs: function(eventRange, event, segSliceFunc) { - var eventSpans = this.eventRangeToSpans(eventRange, event); - var segs = []; - var i; - - for (i = 0; i < eventSpans.length; i++) { - segs.push.apply(segs, // append to - this.eventSpanToSegs(eventSpans[i], event, segSliceFunc) - ); - } - - return segs; - }, - - - // Given an event's unzoned date range, return an array of eventSpan objects. - // eventSpan - { start, end, isStart, isEnd, otherthings... } - // Subclasses can override. - // Subclasses are obligated to forward eventRange.isStart/isEnd to the resulting spans. - eventRangeToSpans: function(eventRange, event) { - return [ $.extend({}, eventRange) ]; // copy into a single-item array - }, - - - // Given an event's span (unzoned start/end and other misc data), and the event itself, - // slices into segments and attaches event-derived properties to them. - // eventSpan - { start, end, isStart, isEnd, otherthings... } - eventSpanToSegs: function(eventSpan, event, segSliceFunc) { - var segs = segSliceFunc ? segSliceFunc(eventSpan) : this.spanToSegs(eventSpan); - var i, seg; - - for (i = 0; i < segs.length; i++) { - seg = segs[i]; - - // the eventSpan's isStart/isEnd takes precedence over the seg's - if (!eventSpan.isStart) { - seg.isStart = false; - } - if (!eventSpan.isEnd) { - seg.isEnd = false; - } - - seg.event = event; - seg.eventStartMS = +eventSpan.start; // TODO: not the best name after making spans unzoned - seg.eventDurationMS = eventSpan.end - eventSpan.start; - } - - return segs; - }, - - - // Produces a new array of range objects that will cover all the time NOT covered by the given ranges. - // SIDE EFFECT: will mutate the given array and will use its date references. - invertRanges: function(ranges) { - var view = this.view; - var viewStart = view.activeRange.start.clone(); // need a copy - var viewEnd = view.activeRange.end.clone(); // need a copy - var inverseRanges = []; - var start = viewStart; // the end of the previous range. the start of the new range - var i, range; - - // ranges need to be in order. required for our date-walking algorithm - ranges.sort(compareRanges); - - for (i = 0; i < ranges.length; i++) { - range = ranges[i]; - - // add the span of time before the event (if there is any) - if (range.start > start) { // compare millisecond time (skip any ambig logic) - inverseRanges.push({ - start: start, - end: range.start - }); - } - - if (range.end > start) { - start = range.end; - } - } - - // add the span of time after the last event (if there is any) - if (start < viewEnd) { // compare millisecond time (skip any ambig logic) - inverseRanges.push({ - start: start, - end: viewEnd - }); - } - - return inverseRanges; - }, - - - sortEventSegs: function(segs) { - segs.sort(proxy(this, 'compareEventSegs')); - }, - - - // A cmp function for determining which segments should take visual priority - compareEventSegs: function(seg1, seg2) { - return seg1.eventStartMS - seg2.eventStartMS || // earlier events go first - seg2.eventDurationMS - seg1.eventDurationMS || // tie? longer events go first - seg2.event.allDay - seg1.event.allDay || // tie? put all-day events first (booleans cast to 0/1) - compareByFieldSpecs(seg1.event, seg2.event, this.view.eventOrderSpecs); - } - -}); - - -/* Utilities -----------------------------------------------------------------------------------------------------------------------*/ - - -function pluckEventDateProps(event) { - return { - start: event.start.clone(), - end: event.end ? event.end.clone() : null, - allDay: event.allDay // keep it the same - }; -} -FC.pluckEventDateProps = pluckEventDateProps; - - -function isBgEvent(event) { // returns true if background OR inverse-background - var rendering = getEventRendering(event); - return rendering === 'background' || rendering === 'inverse-background'; -} -FC.isBgEvent = isBgEvent; // export - - -function isInverseBgEvent(event) { - return getEventRendering(event) === 'inverse-background'; -} - - -function getEventRendering(event) { - return firstDefined((event.source || {}).rendering, event.rendering); -} - - -function groupEventsById(events) { - var eventsById = {}; - var i, event; - - for (i = 0; i < events.length; i++) { - event = events[i]; - (eventsById[event._id] || (eventsById[event._id] = [])).push(event); - } - - return eventsById; -} - - -// A cmp function for determining which non-inverted "ranges" (see above) happen earlier -function compareRanges(range1, range2) { - return range1.start - range2.start; // earlier ranges go first -} - - -/* External-Dragging-Element Data -----------------------------------------------------------------------------------------------------------------------*/ - -// Require all HTML5 data-* attributes used by FullCalendar to have this prefix. -// A value of '' will query attributes like data-event. A value of 'fc' will query attributes like data-fc-event. -FC.dataAttrPrefix = ''; - -// Given a jQuery element that might represent a dragged FullCalendar event, returns an intermediate data structure -// to be used for Event Object creation. -// A defined `.eventProps`, even when empty, indicates that an event should be created. -function getDraggedElMeta(el) { - var prefix = FC.dataAttrPrefix; - var eventProps; // properties for creating the event, not related to date/time - var startTime; // a Duration - var duration; - var stick; - - if (prefix) { prefix += '-'; } - eventProps = el.data(prefix + 'event') || null; - - if (eventProps) { - if (typeof eventProps === 'object') { - eventProps = $.extend({}, eventProps); // make a copy - } - else { // something like 1 or true. still signal event creation - eventProps = {}; - } - - // pluck special-cased date/time properties - startTime = eventProps.start; - if (startTime == null) { startTime = eventProps.time; } // accept 'time' as well - duration = eventProps.duration; - stick = eventProps.stick; - delete eventProps.start; - delete eventProps.time; - delete eventProps.duration; - delete eventProps.stick; - } - - // fallback to standalone attribute values for each of the date/time properties - if (startTime == null) { startTime = el.data(prefix + 'start'); } - if (startTime == null) { startTime = el.data(prefix + 'time'); } // accept 'time' as well - if (duration == null) { duration = el.data(prefix + 'duration'); } - if (stick == null) { stick = el.data(prefix + 'stick'); } - - // massage into correct data types - startTime = startTime != null ? moment.duration(startTime) : null; - duration = duration != null ? moment.duration(duration) : null; - stick = Boolean(stick); - - return { eventProps: eventProps, startTime: startTime, duration: duration, stick: stick }; -} - - -;; - -/* -A set of rendering and date-related methods for a visual component comprised of one or more rows of day columns. -Prerequisite: the object being mixed into needs to be a *Grid* -*/ -var DayTableMixin = FC.DayTableMixin = { - - breakOnWeeks: false, // should create a new row for each week? - dayDates: null, // whole-day dates for each column. left to right - dayIndices: null, // for each day from start, the offset - daysPerRow: null, - rowCnt: null, - colCnt: null, - colHeadFormat: null, - - - // Populates internal variables used for date calculation and rendering - updateDayTable: function() { - var view = this.view; - var date = this.start.clone(); - var dayIndex = -1; - var dayIndices = []; - var dayDates = []; - var daysPerRow; - var firstDay; - var rowCnt; - - while (date.isBefore(this.end)) { // loop each day from start to end - if (view.isHiddenDay(date)) { - dayIndices.push(dayIndex + 0.5); // mark that it's between indices - } - else { - dayIndex++; - dayIndices.push(dayIndex); - dayDates.push(date.clone()); - } - date.add(1, 'days'); - } - - if (this.breakOnWeeks) { - // count columns until the day-of-week repeats - firstDay = dayDates[0].day(); - for (daysPerRow = 1; daysPerRow < dayDates.length; daysPerRow++) { - if (dayDates[daysPerRow].day() == firstDay) { - break; - } - } - rowCnt = Math.ceil(dayDates.length / daysPerRow); - } - else { - rowCnt = 1; - daysPerRow = dayDates.length; - } - - this.dayDates = dayDates; - this.dayIndices = dayIndices; - this.daysPerRow = daysPerRow; - this.rowCnt = rowCnt; - - this.updateDayTableCols(); - }, - - - // Computes and assigned the colCnt property and updates any options that may be computed from it - updateDayTableCols: function() { - this.colCnt = this.computeColCnt(); - this.colHeadFormat = this.view.opt('columnFormat') || this.computeColHeadFormat(); - }, - - - // Determines how many columns there should be in the table - computeColCnt: function() { - return this.daysPerRow; - }, - - - // Computes the ambiguously-timed moment for the given cell - getCellDate: function(row, col) { - return this.dayDates[ - this.getCellDayIndex(row, col) - ].clone(); - }, - - - // Computes the ambiguously-timed date range for the given cell - getCellRange: function(row, col) { - var start = this.getCellDate(row, col); - var end = start.clone().add(1, 'days'); - - return { start: start, end: end }; - }, - - - // Returns the number of day cells, chronologically, from the first of the grid (0-based) - getCellDayIndex: function(row, col) { - return row * this.daysPerRow + this.getColDayIndex(col); - }, - - - // Returns the numner of day cells, chronologically, from the first cell in *any given row* - getColDayIndex: function(col) { - if (this.isRTL) { - return this.colCnt - 1 - col; - } - else { - return col; - } - }, - - - // Given a date, returns its chronolocial cell-index from the first cell of the grid. - // If the date lies between cells (because of hiddenDays), returns a floating-point value between offsets. - // If before the first offset, returns a negative number. - // If after the last offset, returns an offset past the last cell offset. - // Only works for *start* dates of cells. Will not work for exclusive end dates for cells. - getDateDayIndex: function(date) { - var dayIndices = this.dayIndices; - var dayOffset = date.diff(this.start, 'days'); - - if (dayOffset < 0) { - return dayIndices[0] - 1; - } - else if (dayOffset >= dayIndices.length) { - return dayIndices[dayIndices.length - 1] + 1; - } - else { - return dayIndices[dayOffset]; - } - }, - - - /* Options - ------------------------------------------------------------------------------------------------------------------*/ - - - // Computes a default column header formatting string if `colFormat` is not explicitly defined - computeColHeadFormat: function() { - // if more than one week row, or if there are a lot of columns with not much space, - // put just the day numbers will be in each cell - if (this.rowCnt > 1 || this.colCnt > 10) { - return 'ddd'; // "Sat" - } - // multiple days, so full single date string WON'T be in title text - else if (this.colCnt > 1) { - return this.view.opt('dayOfMonthFormat'); // "Sat 12/10" - } - // single day, so full single date string will probably be in title text - else { - return 'dddd'; // "Saturday" - } - }, - - - /* Slicing - ------------------------------------------------------------------------------------------------------------------*/ - - - // Slices up a date range into a segment for every week-row it intersects with - sliceRangeByRow: function(range) { - var daysPerRow = this.daysPerRow; - var normalRange = this.view.computeDayRange(range); // make whole-day range, considering nextDayThreshold - var rangeFirst = this.getDateDayIndex(normalRange.start); // inclusive first index - var rangeLast = this.getDateDayIndex(normalRange.end.clone().subtract(1, 'days')); // inclusive last index - var segs = []; - var row; - var rowFirst, rowLast; // inclusive day-index range for current row - var segFirst, segLast; // inclusive day-index range for segment - - for (row = 0; row < this.rowCnt; row++) { - rowFirst = row * daysPerRow; - rowLast = rowFirst + daysPerRow - 1; - - // intersect segment's offset range with the row's - segFirst = Math.max(rangeFirst, rowFirst); - segLast = Math.min(rangeLast, rowLast); - - // deal with in-between indices - segFirst = Math.ceil(segFirst); // in-between starts round to next cell - segLast = Math.floor(segLast); // in-between ends round to prev cell - - if (segFirst <= segLast) { // was there any intersection with the current row? - segs.push({ - row: row, - - // normalize to start of row - firstRowDayIndex: segFirst - rowFirst, - lastRowDayIndex: segLast - rowFirst, - - // must be matching integers to be the segment's start/end - isStart: segFirst === rangeFirst, - isEnd: segLast === rangeLast - }); - } - } - - return segs; - }, - - - // Slices up a date range into a segment for every day-cell it intersects with. - // TODO: make more DRY with sliceRangeByRow somehow. - sliceRangeByDay: function(range) { - var daysPerRow = this.daysPerRow; - var normalRange = this.view.computeDayRange(range); // make whole-day range, considering nextDayThreshold - var rangeFirst = this.getDateDayIndex(normalRange.start); // inclusive first index - var rangeLast = this.getDateDayIndex(normalRange.end.clone().subtract(1, 'days')); // inclusive last index - var segs = []; - var row; - var rowFirst, rowLast; // inclusive day-index range for current row - var i; - var segFirst, segLast; // inclusive day-index range for segment - - for (row = 0; row < this.rowCnt; row++) { - rowFirst = row * daysPerRow; - rowLast = rowFirst + daysPerRow - 1; - - for (i = rowFirst; i <= rowLast; i++) { - - // intersect segment's offset range with the row's - segFirst = Math.max(rangeFirst, i); - segLast = Math.min(rangeLast, i); - - // deal with in-between indices - segFirst = Math.ceil(segFirst); // in-between starts round to next cell - segLast = Math.floor(segLast); // in-between ends round to prev cell - - if (segFirst <= segLast) { // was there any intersection with the current row? - segs.push({ - row: row, - - // normalize to start of row - firstRowDayIndex: segFirst - rowFirst, - lastRowDayIndex: segLast - rowFirst, - - // must be matching integers to be the segment's start/end - isStart: segFirst === rangeFirst, - isEnd: segLast === rangeLast - }); - } - } - } - - return segs; - }, - - - /* Header Rendering - ------------------------------------------------------------------------------------------------------------------*/ - - - renderHeadHtml: function() { - var view = this.view; - - return '' + - '<div class="fc-row ' + view.widgetHeaderClass + '">' + - '<table>' + - '<thead>' + - this.renderHeadTrHtml() + - '</thead>' + - '</table>' + - '</div>'; - }, - - - renderHeadIntroHtml: function() { - return this.renderIntroHtml(); // fall back to generic - }, - - - renderHeadTrHtml: function() { - return '' + - '<tr>' + - (this.isRTL ? '' : this.renderHeadIntroHtml()) + - this.renderHeadDateCellsHtml() + - (this.isRTL ? this.renderHeadIntroHtml() : '') + - '</tr>'; - }, - - - renderHeadDateCellsHtml: function() { - var htmls = []; - var col, date; - - for (col = 0; col < this.colCnt; col++) { - date = this.getCellDate(0, col); - htmls.push(this.renderHeadDateCellHtml(date)); - } - - return htmls.join(''); - }, - - - // TODO: when internalApiVersion, accept an object for HTML attributes - // (colspan should be no different) - renderHeadDateCellHtml: function(date, colspan, otherAttrs) { - var view = this.view; - var isDateValid = isDateWithinRange(date, view.activeRange); // TODO: called too frequently. cache somehow. - var classNames = [ - 'fc-day-header', - view.widgetHeaderClass - ]; - var innerHtml = htmlEscape(date.format(this.colHeadFormat)); - - // if only one row of days, the classNames on the header can represent the specific days beneath - if (this.rowCnt === 1) { - classNames = classNames.concat( - // includes the day-of-week class - // noThemeHighlight=true (don't highlight the header) - this.getDayClasses(date, true) - ); - } - else { - classNames.push('fc-' + dayIDs[date.day()]); // only add the day-of-week class - } - - return '' + - '<th class="' + classNames.join(' ') + '"' + - ((isDateValid && this.rowCnt) === 1 ? - ' data-date="' + date.format('YYYY-MM-DD') + '"' : - '') + - (colspan > 1 ? - ' colspan="' + colspan + '"' : - '') + - (otherAttrs ? - ' ' + otherAttrs : - '') + - '>' + - (isDateValid ? - // don't make a link if the heading could represent multiple days, or if there's only one day (forceOff) - view.buildGotoAnchorHtml( - { date: date, forceOff: this.rowCnt > 1 || this.colCnt === 1 }, - innerHtml - ) : - // if not valid, display text, but no link - innerHtml - ) + - '</th>'; - }, - - - /* Background Rendering - ------------------------------------------------------------------------------------------------------------------*/ - - - renderBgTrHtml: function(row) { - return '' + - '<tr>' + - (this.isRTL ? '' : this.renderBgIntroHtml(row)) + - this.renderBgCellsHtml(row) + - (this.isRTL ? this.renderBgIntroHtml(row) : '') + - '</tr>'; - }, - - - renderBgIntroHtml: function(row) { - return this.renderIntroHtml(); // fall back to generic - }, - - - renderBgCellsHtml: function(row) { - var htmls = []; - var col, date; - - for (col = 0; col < this.colCnt; col++) { - date = this.getCellDate(row, col); - htmls.push(this.renderBgCellHtml(date)); - } - - return htmls.join(''); - }, - - - renderBgCellHtml: function(date, otherAttrs) { - var view = this.view; - var isDateValid = isDateWithinRange(date, view.activeRange); // TODO: called too frequently. cache somehow. - var classes = this.getDayClasses(date); - - classes.unshift('fc-day', view.widgetContentClass); - - return '<td class="' + classes.join(' ') + '"' + - (isDateValid ? - ' data-date="' + date.format('YYYY-MM-DD') + '"' : // if date has a time, won't format it - '') + - (otherAttrs ? - ' ' + otherAttrs : - '') + - '></td>'; - }, - - - /* Generic - ------------------------------------------------------------------------------------------------------------------*/ - - - // Generates the default HTML intro for any row. User classes should override - renderIntroHtml: function() { - }, - - - // TODO: a generic method for dealing with <tr>, RTL, intro - // when increment internalApiVersion - // wrapTr (scheduler) - - - /* Utils - ------------------------------------------------------------------------------------------------------------------*/ - - - // Applies the generic "intro" and "outro" HTML to the given cells. - // Intro means the leftmost cell when the calendar is LTR and the rightmost cell when RTL. Vice-versa for outro. - bookendCells: function(trEl) { - var introHtml = this.renderIntroHtml(); - - if (introHtml) { - if (this.isRTL) { - trEl.append(introHtml); - } - else { - trEl.prepend(introHtml); - } - } - } - -}; - -;; - -/* A component that renders a grid of whole-days that runs horizontally. There can be multiple rows, one per week. -----------------------------------------------------------------------------------------------------------------------*/ - -var DayGrid = FC.DayGrid = Grid.extend(DayTableMixin, { - - numbersVisible: false, // should render a row for day/week numbers? set by outside view. TODO: make internal - bottomCoordPadding: 0, // hack for extending the hit area for the last row of the coordinate grid - - rowEls: null, // set of fake row elements - cellEls: null, // set of whole-day elements comprising the row's background - helperEls: null, // set of cell skeleton elements for rendering the mock event "helper" - - rowCoordCache: null, - colCoordCache: null, - - - // Renders the rows and columns into the component's `this.el`, which should already be assigned. - // isRigid determins whether the individual rows should ignore the contents and be a constant height. - // Relies on the view's colCnt and rowCnt. In the future, this component should probably be self-sufficient. - renderDates: function(isRigid) { - var view = this.view; - var rowCnt = this.rowCnt; - var colCnt = this.colCnt; - var html = ''; - var row; - var col; - - for (row = 0; row < rowCnt; row++) { - html += this.renderDayRowHtml(row, isRigid); - } - this.el.html(html); - - this.rowEls = this.el.find('.fc-row'); - this.cellEls = this.el.find('.fc-day, .fc-disabled-day'); - - this.rowCoordCache = new CoordCache({ - els: this.rowEls, - isVertical: true - }); - this.colCoordCache = new CoordCache({ - els: this.cellEls.slice(0, this.colCnt), // only the first row - isHorizontal: true - }); - - // trigger dayRender with each cell's element - for (row = 0; row < rowCnt; row++) { - for (col = 0; col < colCnt; col++) { - view.publiclyTrigger( - 'dayRender', - null, - this.getCellDate(row, col), - this.getCellEl(row, col) - ); - } - } - }, - - - unrenderDates: function() { - this.removeSegPopover(); - }, - - - renderBusinessHours: function() { - var segs = this.buildBusinessHourSegs(true); // wholeDay=true - this.renderFill('businessHours', segs, 'bgevent'); - }, - - - unrenderBusinessHours: function() { - this.unrenderFill('businessHours'); - }, - - - // Generates the HTML for a single row, which is a div that wraps a table. - // `row` is the row number. - renderDayRowHtml: function(row, isRigid) { - var view = this.view; - var classes = [ 'fc-row', 'fc-week', view.widgetContentClass ]; - - if (isRigid) { - classes.push('fc-rigid'); - } - - return '' + - '<div class="' + classes.join(' ') + '">' + - '<div class="fc-bg">' + - '<table>' + - this.renderBgTrHtml(row) + - '</table>' + - '</div>' + - '<div class="fc-content-skeleton">' + - '<table>' + - (this.numbersVisible ? - '<thead>' + - this.renderNumberTrHtml(row) + - '</thead>' : - '' - ) + - '</table>' + - '</div>' + - '</div>'; - }, - - - /* Grid Number Rendering - ------------------------------------------------------------------------------------------------------------------*/ - - - renderNumberTrHtml: function(row) { - return '' + - '<tr>' + - (this.isRTL ? '' : this.renderNumberIntroHtml(row)) + - this.renderNumberCellsHtml(row) + - (this.isRTL ? this.renderNumberIntroHtml(row) : '') + - '</tr>'; - }, - - - renderNumberIntroHtml: function(row) { - return this.renderIntroHtml(); - }, - - - renderNumberCellsHtml: function(row) { - var htmls = []; - var col, date; - - for (col = 0; col < this.colCnt; col++) { - date = this.getCellDate(row, col); - htmls.push(this.renderNumberCellHtml(date)); - } - - return htmls.join(''); - }, - - - // Generates the HTML for the <td>s of the "number" row in the DayGrid's content skeleton. - // The number row will only exist if either day numbers or week numbers are turned on. - renderNumberCellHtml: function(date) { - var view = this.view; - var html = ''; - var isDateValid = isDateWithinRange(date, view.activeRange); // TODO: called too frequently. cache somehow. - var isDayNumberVisible = view.dayNumbersVisible && isDateValid; - var classes; - var weekCalcFirstDoW; - - if (!isDayNumberVisible && !view.cellWeekNumbersVisible) { - // no numbers in day cell (week number must be along the side) - return '<td/>'; // will create an empty space above events :( - } - - classes = this.getDayClasses(date); - classes.unshift('fc-day-top'); - - if (view.cellWeekNumbersVisible) { - // To determine the day of week number change under ISO, we cannot - // rely on moment.js methods such as firstDayOfWeek() or weekday(), - // because they rely on the locale's dow (possibly overridden by - // our firstDay option), which may not be Monday. We cannot change - // dow, because that would affect the calendar start day as well. - if (date._locale._fullCalendar_weekCalc === 'ISO') { - weekCalcFirstDoW = 1; // Monday by ISO 8601 definition - } - else { - weekCalcFirstDoW = date._locale.firstDayOfWeek(); - } - } - - html += '<td class="' + classes.join(' ') + '"' + - (isDateValid ? - ' data-date="' + date.format() + '"' : - '' - ) + - '>'; - - if (view.cellWeekNumbersVisible && (date.day() == weekCalcFirstDoW)) { - html += view.buildGotoAnchorHtml( - { date: date, type: 'week' }, - { 'class': 'fc-week-number' }, - date.format('w') // inner HTML - ); - } - - if (isDayNumberVisible) { - html += view.buildGotoAnchorHtml( - date, - { 'class': 'fc-day-number' }, - date.date() // inner HTML - ); - } - - html += '</td>'; - - return html; - }, - - - /* Options - ------------------------------------------------------------------------------------------------------------------*/ - - - // Computes a default event time formatting string if `timeFormat` is not explicitly defined - computeEventTimeFormat: function() { - return this.view.opt('extraSmallTimeFormat'); // like "6p" or "6:30p" - }, - - - // Computes a default `displayEventEnd` value if one is not expliclty defined - computeDisplayEventEnd: function() { - return this.colCnt == 1; // we'll likely have space if there's only one day - }, - - - /* Dates - ------------------------------------------------------------------------------------------------------------------*/ - - - rangeUpdated: function() { - this.updateDayTable(); - }, - - - // Slices up the given span (unzoned start/end with other misc data) into an array of segments - spanToSegs: function(span) { - var segs = this.sliceRangeByRow(span); - var i, seg; - - for (i = 0; i < segs.length; i++) { - seg = segs[i]; - if (this.isRTL) { - seg.leftCol = this.daysPerRow - 1 - seg.lastRowDayIndex; - seg.rightCol = this.daysPerRow - 1 - seg.firstRowDayIndex; - } - else { - seg.leftCol = seg.firstRowDayIndex; - seg.rightCol = seg.lastRowDayIndex; - } - } - - return segs; - }, - - - /* Hit System - ------------------------------------------------------------------------------------------------------------------*/ - - - prepareHits: function() { - this.colCoordCache.build(); - this.rowCoordCache.build(); - this.rowCoordCache.bottoms[this.rowCnt - 1] += this.bottomCoordPadding; // hack - }, - - - releaseHits: function() { - this.colCoordCache.clear(); - this.rowCoordCache.clear(); - }, - - - queryHit: function(leftOffset, topOffset) { - if (this.colCoordCache.isLeftInBounds(leftOffset) && this.rowCoordCache.isTopInBounds(topOffset)) { - var col = this.colCoordCache.getHorizontalIndex(leftOffset); - var row = this.rowCoordCache.getVerticalIndex(topOffset); - - if (row != null && col != null) { - return this.getCellHit(row, col); - } - } - }, - - - getHitSpan: function(hit) { - return this.getCellRange(hit.row, hit.col); - }, - - - getHitEl: function(hit) { - return this.getCellEl(hit.row, hit.col); - }, - - - /* Cell System - ------------------------------------------------------------------------------------------------------------------*/ - // FYI: the first column is the leftmost column, regardless of date - - - getCellHit: function(row, col) { - return { - row: row, - col: col, - component: this, // needed unfortunately :( - left: this.colCoordCache.getLeftOffset(col), - right: this.colCoordCache.getRightOffset(col), - top: this.rowCoordCache.getTopOffset(row), - bottom: this.rowCoordCache.getBottomOffset(row) - }; - }, - - - getCellEl: function(row, col) { - return this.cellEls.eq(row * this.colCnt + col); - }, - - - /* Event Drag Visualization - ------------------------------------------------------------------------------------------------------------------*/ - // TODO: move to DayGrid.event, similar to what we did with Grid's drag methods - - - // Renders a visual indication of an event or external element being dragged. - // `eventLocation` has zoned start and end (optional) - renderDrag: function(eventLocation, seg) { - var eventSpans = this.eventToSpans(eventLocation); - var i; - - // always render a highlight underneath - for (i = 0; i < eventSpans.length; i++) { - this.renderHighlight(eventSpans[i]); - } - - // if a segment from the same calendar but another component is being dragged, render a helper event - if (seg && seg.component !== this) { - return this.renderEventLocationHelper(eventLocation, seg); // returns mock event elements - } - }, - - - // Unrenders any visual indication of a hovering event - unrenderDrag: function() { - this.unrenderHighlight(); - this.unrenderHelper(); - }, - - - /* Event Resize Visualization - ------------------------------------------------------------------------------------------------------------------*/ - - - // Renders a visual indication of an event being resized - renderEventResize: function(eventLocation, seg) { - var eventSpans = this.eventToSpans(eventLocation); - var i; - - for (i = 0; i < eventSpans.length; i++) { - this.renderHighlight(eventSpans[i]); - } - - return this.renderEventLocationHelper(eventLocation, seg); // returns mock event elements - }, - - - // Unrenders a visual indication of an event being resized - unrenderEventResize: function() { - this.unrenderHighlight(); - this.unrenderHelper(); - }, - - - /* Event Helper - ------------------------------------------------------------------------------------------------------------------*/ - - - // Renders a mock "helper" event. `sourceSeg` is the associated internal segment object. It can be null. - renderHelper: function(event, sourceSeg) { - var helperNodes = []; - var segs = this.eventToSegs(event); - var rowStructs; - - segs = this.renderFgSegEls(segs); // assigns each seg's el and returns a subset of segs that were rendered - rowStructs = this.renderSegRows(segs); - - // inject each new event skeleton into each associated row - this.rowEls.each(function(row, rowNode) { - var rowEl = $(rowNode); // the .fc-row - var skeletonEl = $('<div class="fc-helper-skeleton"><table/></div>'); // will be absolutely positioned - var skeletonTop; - - // If there is an original segment, match the top position. Otherwise, put it at the row's top level - if (sourceSeg && sourceSeg.row === row) { - skeletonTop = sourceSeg.el.position().top; - } - else { - skeletonTop = rowEl.find('.fc-content-skeleton tbody').position().top; - } - - skeletonEl.css('top', skeletonTop) - .find('table') - .append(rowStructs[row].tbodyEl); - - rowEl.append(skeletonEl); - helperNodes.push(skeletonEl[0]); - }); - - return ( // must return the elements rendered - this.helperEls = $(helperNodes) // array -> jQuery set - ); - }, - - - // Unrenders any visual indication of a mock helper event - unrenderHelper: function() { - if (this.helperEls) { - this.helperEls.remove(); - this.helperEls = null; - } - }, - - - /* Fill System (highlight, background events, business hours) - ------------------------------------------------------------------------------------------------------------------*/ - - - fillSegTag: 'td', // override the default tag name - - - // Renders a set of rectangles over the given segments of days. - // Only returns segments that successfully rendered. - renderFill: function(type, segs, className) { - var nodes = []; - var i, seg; - var skeletonEl; - - segs = this.renderFillSegEls(type, segs); // assignes `.el` to each seg. returns successfully rendered segs - - for (i = 0; i < segs.length; i++) { - seg = segs[i]; - skeletonEl = this.renderFillRow(type, seg, className); - this.rowEls.eq(seg.row).append(skeletonEl); - nodes.push(skeletonEl[0]); - } - - this.elsByFill[type] = $(nodes); - - return segs; - }, - - - // Generates the HTML needed for one row of a fill. Requires the seg's el to be rendered. - renderFillRow: function(type, seg, className) { - var colCnt = this.colCnt; - var startCol = seg.leftCol; - var endCol = seg.rightCol + 1; - var skeletonEl; - var trEl; - - className = className || type.toLowerCase(); - - skeletonEl = $( - '<div class="fc-' + className + '-skeleton">' + - '<table><tr/></table>' + - '</div>' - ); - trEl = skeletonEl.find('tr'); - - if (startCol > 0) { - trEl.append('<td colspan="' + startCol + '"/>'); - } - - trEl.append( - seg.el.attr('colspan', endCol - startCol) - ); - - if (endCol < colCnt) { - trEl.append('<td colspan="' + (colCnt - endCol) + '"/>'); - } - - this.bookendCells(trEl); - - return skeletonEl; - } - -}); - -;; - -/* Event-rendering methods for the DayGrid class -----------------------------------------------------------------------------------------------------------------------*/ - -DayGrid.mixin({ - - rowStructs: null, // an array of objects, each holding information about a row's foreground event-rendering - - - // Unrenders all events currently rendered on the grid - unrenderEvents: function() { - this.removeSegPopover(); // removes the "more.." events popover - Grid.prototype.unrenderEvents.apply(this, arguments); // calls the super-method - }, - - - // Retrieves all rendered segment objects currently rendered on the grid - getEventSegs: function() { - return Grid.prototype.getEventSegs.call(this) // get the segments from the super-method - .concat(this.popoverSegs || []); // append the segments from the "more..." popover - }, - - - // Renders the given background event segments onto the grid - renderBgSegs: function(segs) { - - // don't render timed background events - var allDaySegs = $.grep(segs, function(seg) { - return seg.event.allDay; - }); - - return Grid.prototype.renderBgSegs.call(this, allDaySegs); // call the super-method - }, - - - // Renders the given foreground event segments onto the grid - renderFgSegs: function(segs) { - var rowStructs; - - // render an `.el` on each seg - // returns a subset of the segs. segs that were actually rendered - segs = this.renderFgSegEls(segs); - - rowStructs = this.rowStructs = this.renderSegRows(segs); - - // append to each row's content skeleton - this.rowEls.each(function(i, rowNode) { - $(rowNode).find('.fc-content-skeleton > table').append( - rowStructs[i].tbodyEl - ); - }); - - return segs; // return only the segs that were actually rendered - }, - - - // Unrenders all currently rendered foreground event segments - unrenderFgSegs: function() { - var rowStructs = this.rowStructs || []; - var rowStruct; - - while ((rowStruct = rowStructs.pop())) { - rowStruct.tbodyEl.remove(); - } - - this.rowStructs = null; - }, - - - // Uses the given events array to generate <tbody> elements that should be appended to each row's content skeleton. - // Returns an array of rowStruct objects (see the bottom of `renderSegRow`). - // PRECONDITION: each segment shoud already have a rendered and assigned `.el` - renderSegRows: function(segs) { - var rowStructs = []; - var segRows; - var row; - - segRows = this.groupSegRows(segs); // group into nested arrays - - // iterate each row of segment groupings - for (row = 0; row < segRows.length; row++) { - rowStructs.push( - this.renderSegRow(row, segRows[row]) - ); - } - - return rowStructs; - }, - - - // Builds the HTML to be used for the default element for an individual segment - fgSegHtml: function(seg, disableResizing) { - var view = this.view; - var event = seg.event; - var isDraggable = view.isEventDraggable(event); - var isResizableFromStart = !disableResizing && event.allDay && - seg.isStart && view.isEventResizableFromStart(event); - var isResizableFromEnd = !disableResizing && event.allDay && - seg.isEnd && view.isEventResizableFromEnd(event); - var classes = this.getSegClasses(seg, isDraggable, isResizableFromStart || isResizableFromEnd); - var skinCss = cssToStr(this.getSegSkinCss(seg)); - var timeHtml = ''; - var timeText; - var titleHtml; - - classes.unshift('fc-day-grid-event', 'fc-h-event'); - - // Only display a timed events time if it is the starting segment - if (seg.isStart) { - timeText = this.getEventTimeText(event); - if (timeText) { - timeHtml = '<span class="fc-time">' + htmlEscape(timeText) + '</span>'; - } - } - - titleHtml = - '<span class="fc-title">' + - (htmlEscape(event.title || '') || ' ') + // we always want one line of height - '</span>'; - - return '<a class="' + classes.join(' ') + '"' + - (event.url ? - ' href="' + htmlEscape(event.url) + '"' : - '' - ) + - (skinCss ? - ' style="' + skinCss + '"' : - '' - ) + - '>' + - '<div class="fc-content">' + - (this.isRTL ? - titleHtml + ' ' + timeHtml : // put a natural space in between - timeHtml + ' ' + titleHtml // - ) + - '</div>' + - (isResizableFromStart ? - '<div class="fc-resizer fc-start-resizer" />' : - '' - ) + - (isResizableFromEnd ? - '<div class="fc-resizer fc-end-resizer" />' : - '' - ) + - '</a>'; - }, - - - // Given a row # and an array of segments all in the same row, render a <tbody> element, a skeleton that contains - // the segments. Returns object with a bunch of internal data about how the render was calculated. - // NOTE: modifies rowSegs - renderSegRow: function(row, rowSegs) { - var colCnt = this.colCnt; - var segLevels = this.buildSegLevels(rowSegs); // group into sub-arrays of levels - var levelCnt = Math.max(1, segLevels.length); // ensure at least one level - var tbody = $('<tbody/>'); - var segMatrix = []; // lookup for which segments are rendered into which level+col cells - var cellMatrix = []; // lookup for all <td> elements of the level+col matrix - var loneCellMatrix = []; // lookup for <td> elements that only take up a single column - var i, levelSegs; - var col; - var tr; - var j, seg; - var td; - - // populates empty cells from the current column (`col`) to `endCol` - function emptyCellsUntil(endCol) { - while (col < endCol) { - // try to grab a cell from the level above and extend its rowspan. otherwise, create a fresh cell - td = (loneCellMatrix[i - 1] || [])[col]; - if (td) { - td.attr( - 'rowspan', - parseInt(td.attr('rowspan') || 1, 10) + 1 - ); - } - else { - td = $('<td/>'); - tr.append(td); - } - cellMatrix[i][col] = td; - loneCellMatrix[i][col] = td; - col++; - } - } - - for (i = 0; i < levelCnt; i++) { // iterate through all levels - levelSegs = segLevels[i]; - col = 0; - tr = $('<tr/>'); - - segMatrix.push([]); - cellMatrix.push([]); - loneCellMatrix.push([]); - - // levelCnt might be 1 even though there are no actual levels. protect against this. - // this single empty row is useful for styling. - if (levelSegs) { - for (j = 0; j < levelSegs.length; j++) { // iterate through segments in level - seg = levelSegs[j]; - - emptyCellsUntil(seg.leftCol); - - // create a container that occupies or more columns. append the event element. - td = $('<td class="fc-event-container"/>').append(seg.el); - if (seg.leftCol != seg.rightCol) { - td.attr('colspan', seg.rightCol - seg.leftCol + 1); - } - else { // a single-column segment - loneCellMatrix[i][col] = td; - } - - while (col <= seg.rightCol) { - cellMatrix[i][col] = td; - segMatrix[i][col] = seg; - col++; - } - - tr.append(td); - } - } - - emptyCellsUntil(colCnt); // finish off the row - this.bookendCells(tr); - tbody.append(tr); - } - - return { // a "rowStruct" - row: row, // the row number - tbodyEl: tbody, - cellMatrix: cellMatrix, - segMatrix: segMatrix, - segLevels: segLevels, - segs: rowSegs - }; - }, - - - // Stacks a flat array of segments, which are all assumed to be in the same row, into subarrays of vertical levels. - // NOTE: modifies segs - buildSegLevels: function(segs) { - var levels = []; - var i, seg; - var j; - - // Give preference to elements with certain criteria, so they have - // a chance to be closer to the top. - this.sortEventSegs(segs); - - for (i = 0; i < segs.length; i++) { - seg = segs[i]; - - // loop through levels, starting with the topmost, until the segment doesn't collide with other segments - for (j = 0; j < levels.length; j++) { - if (!isDaySegCollision(seg, levels[j])) { - break; - } - } - // `j` now holds the desired subrow index - seg.level = j; - - // create new level array if needed and append segment - (levels[j] || (levels[j] = [])).push(seg); - } - - // order segments left-to-right. very important if calendar is RTL - for (j = 0; j < levels.length; j++) { - levels[j].sort(compareDaySegCols); - } - - return levels; - }, - - - // Given a flat array of segments, return an array of sub-arrays, grouped by each segment's row - groupSegRows: function(segs) { - var segRows = []; - var i; - - for (i = 0; i < this.rowCnt; i++) { - segRows.push([]); - } - - for (i = 0; i < segs.length; i++) { - segRows[segs[i].row].push(segs[i]); - } - - return segRows; - } - -}); - - -// Computes whether two segments' columns collide. They are assumed to be in the same row. -function isDaySegCollision(seg, otherSegs) { - var i, otherSeg; - - for (i = 0; i < otherSegs.length; i++) { - otherSeg = otherSegs[i]; - - if ( - otherSeg.leftCol <= seg.rightCol && - otherSeg.rightCol >= seg.leftCol - ) { - return true; - } - } - - return false; -} - - -// A cmp function for determining the leftmost event -function compareDaySegCols(a, b) { - return a.leftCol - b.leftCol; -} - -;; - -/* Methods relate to limiting the number events for a given day on a DayGrid -----------------------------------------------------------------------------------------------------------------------*/ -// NOTE: all the segs being passed around in here are foreground segs - -DayGrid.mixin({ - - segPopover: null, // the Popover that holds events that can't fit in a cell. null when not visible - popoverSegs: null, // an array of segment objects that the segPopover holds. null when not visible - - - removeSegPopover: function() { - if (this.segPopover) { - this.segPopover.hide(); // in handler, will call segPopover's removeElement - } - }, - - - // Limits the number of "levels" (vertically stacking layers of events) for each row of the grid. - // `levelLimit` can be false (don't limit), a number, or true (should be computed). - limitRows: function(levelLimit) { - var rowStructs = this.rowStructs || []; - var row; // row # - var rowLevelLimit; - - for (row = 0; row < rowStructs.length; row++) { - this.unlimitRow(row); - - if (!levelLimit) { - rowLevelLimit = false; - } - else if (typeof levelLimit === 'number') { - rowLevelLimit = levelLimit; - } - else { - rowLevelLimit = this.computeRowLevelLimit(row); - } - - if (rowLevelLimit !== false) { - this.limitRow(row, rowLevelLimit); - } - } - }, - - - // Computes the number of levels a row will accomodate without going outside its bounds. - // Assumes the row is "rigid" (maintains a constant height regardless of what is inside). - // `row` is the row number. - computeRowLevelLimit: function(row) { - var rowEl = this.rowEls.eq(row); // the containing "fake" row div - var rowHeight = rowEl.height(); // TODO: cache somehow? - var trEls = this.rowStructs[row].tbodyEl.children(); - var i, trEl; - var trHeight; - - function iterInnerHeights(i, childNode) { - trHeight = Math.max(trHeight, $(childNode).outerHeight()); - } - - // Reveal one level <tr> at a time and stop when we find one out of bounds - for (i = 0; i < trEls.length; i++) { - trEl = trEls.eq(i).removeClass('fc-limited'); // reset to original state (reveal) - - // with rowspans>1 and IE8, trEl.outerHeight() would return the height of the largest cell, - // so instead, find the tallest inner content element. - trHeight = 0; - trEl.find('> td > :first-child').each(iterInnerHeights); - - if (trEl.position().top + trHeight > rowHeight) { - return i; - } - } - - return false; // should not limit at all - }, - - - // Limits the given grid row to the maximum number of levels and injects "more" links if necessary. - // `row` is the row number. - // `levelLimit` is a number for the maximum (inclusive) number of levels allowed. - limitRow: function(row, levelLimit) { - var _this = this; - var rowStruct = this.rowStructs[row]; - var moreNodes = []; // array of "more" <a> links and <td> DOM nodes - var col = 0; // col #, left-to-right (not chronologically) - var levelSegs; // array of segment objects in the last allowable level, ordered left-to-right - var cellMatrix; // a matrix (by level, then column) of all <td> jQuery elements in the row - var limitedNodes; // array of temporarily hidden level <tr> and segment <td> DOM nodes - var i, seg; - var segsBelow; // array of segment objects below `seg` in the current `col` - var totalSegsBelow; // total number of segments below `seg` in any of the columns `seg` occupies - var colSegsBelow; // array of segment arrays, below seg, one for each column (offset from segs's first column) - var td, rowspan; - var segMoreNodes; // array of "more" <td> cells that will stand-in for the current seg's cell - var j; - var moreTd, moreWrap, moreLink; - - // Iterates through empty level cells and places "more" links inside if need be - function emptyCellsUntil(endCol) { // goes from current `col` to `endCol` - while (col < endCol) { - segsBelow = _this.getCellSegs(row, col, levelLimit); - if (segsBelow.length) { - td = cellMatrix[levelLimit - 1][col]; - moreLink = _this.renderMoreLink(row, col, segsBelow); - moreWrap = $('<div/>').append(moreLink); - td.append(moreWrap); - moreNodes.push(moreWrap[0]); - } - col++; - } - } - - if (levelLimit && levelLimit < rowStruct.segLevels.length) { // is it actually over the limit? - levelSegs = rowStruct.segLevels[levelLimit - 1]; - cellMatrix = rowStruct.cellMatrix; - - limitedNodes = rowStruct.tbodyEl.children().slice(levelLimit) // get level <tr> elements past the limit - .addClass('fc-limited').get(); // hide elements and get a simple DOM-nodes array - - // iterate though segments in the last allowable level - for (i = 0; i < levelSegs.length; i++) { - seg = levelSegs[i]; - emptyCellsUntil(seg.leftCol); // process empty cells before the segment - - // determine *all* segments below `seg` that occupy the same columns - colSegsBelow = []; - totalSegsBelow = 0; - while (col <= seg.rightCol) { - segsBelow = this.getCellSegs(row, col, levelLimit); - colSegsBelow.push(segsBelow); - totalSegsBelow += segsBelow.length; - col++; - } - - if (totalSegsBelow) { // do we need to replace this segment with one or many "more" links? - td = cellMatrix[levelLimit - 1][seg.leftCol]; // the segment's parent cell - rowspan = td.attr('rowspan') || 1; - segMoreNodes = []; - - // make a replacement <td> for each column the segment occupies. will be one for each colspan - for (j = 0; j < colSegsBelow.length; j++) { - moreTd = $('<td class="fc-more-cell"/>').attr('rowspan', rowspan); - segsBelow = colSegsBelow[j]; - moreLink = this.renderMoreLink( - row, - seg.leftCol + j, - [ seg ].concat(segsBelow) // count seg as hidden too - ); - moreWrap = $('<div/>').append(moreLink); - moreTd.append(moreWrap); - segMoreNodes.push(moreTd[0]); - moreNodes.push(moreTd[0]); - } - - td.addClass('fc-limited').after($(segMoreNodes)); // hide original <td> and inject replacements - limitedNodes.push(td[0]); - } - } - - emptyCellsUntil(this.colCnt); // finish off the level - rowStruct.moreEls = $(moreNodes); // for easy undoing later - rowStruct.limitedEls = $(limitedNodes); // for easy undoing later - } - }, - - - // Reveals all levels and removes all "more"-related elements for a grid's row. - // `row` is a row number. - unlimitRow: function(row) { - var rowStruct = this.rowStructs[row]; - - if (rowStruct.moreEls) { - rowStruct.moreEls.remove(); - rowStruct.moreEls = null; - } - - if (rowStruct.limitedEls) { - rowStruct.limitedEls.removeClass('fc-limited'); - rowStruct.limitedEls = null; - } - }, - - - // Renders an <a> element that represents hidden event element for a cell. - // Responsible for attaching click handler as well. - renderMoreLink: function(row, col, hiddenSegs) { - var _this = this; - var view = this.view; - - return $('<a class="fc-more"/>') - .text( - this.getMoreLinkText(hiddenSegs.length) - ) - .on('click', function(ev) { - var clickOption = view.opt('eventLimitClick'); - var date = _this.getCellDate(row, col); - var moreEl = $(this); - var dayEl = _this.getCellEl(row, col); - var allSegs = _this.getCellSegs(row, col); - - // rescope the segments to be within the cell's date - var reslicedAllSegs = _this.resliceDaySegs(allSegs, date); - var reslicedHiddenSegs = _this.resliceDaySegs(hiddenSegs, date); - - if (typeof clickOption === 'function') { - // the returned value can be an atomic option - clickOption = view.publiclyTrigger('eventLimitClick', null, { - date: date, - dayEl: dayEl, - moreEl: moreEl, - segs: reslicedAllSegs, - hiddenSegs: reslicedHiddenSegs - }, ev); - } - - if (clickOption === 'popover') { - _this.showSegPopover(row, col, moreEl, reslicedAllSegs); - } - else if (typeof clickOption === 'string') { // a view name - view.calendar.zoomTo(date, clickOption); - } - }); - }, - - - // Reveals the popover that displays all events within a cell - showSegPopover: function(row, col, moreLink, segs) { - var _this = this; - var view = this.view; - var moreWrap = moreLink.parent(); // the <div> wrapper around the <a> - var topEl; // the element we want to match the top coordinate of - var options; - - if (this.rowCnt == 1) { - topEl = view.el; // will cause the popover to cover any sort of header - } - else { - topEl = this.rowEls.eq(row); // will align with top of row - } - - options = { - className: 'fc-more-popover', - content: this.renderSegPopoverContent(row, col, segs), - parentEl: this.view.el, // attach to root of view. guarantees outside of scrollbars. - top: topEl.offset().top, - autoHide: true, // when the user clicks elsewhere, hide the popover - viewportConstrain: view.opt('popoverViewportConstrain'), - hide: function() { - // kill everything when the popover is hidden - // notify events to be removed - if (_this.popoverSegs) { - var seg; - for (var i = 0; i < _this.popoverSegs.length; ++i) { - seg = _this.popoverSegs[i]; - view.publiclyTrigger('eventDestroy', seg.event, seg.event, seg.el); - } - } - _this.segPopover.removeElement(); - _this.segPopover = null; - _this.popoverSegs = null; - } - }; - - // Determine horizontal coordinate. - // We use the moreWrap instead of the <td> to avoid border confusion. - if (this.isRTL) { - options.right = moreWrap.offset().left + moreWrap.outerWidth() + 1; // +1 to be over cell border - } - else { - options.left = moreWrap.offset().left - 1; // -1 to be over cell border - } - - this.segPopover = new Popover(options); - this.segPopover.show(); - - // the popover doesn't live within the grid's container element, and thus won't get the event - // delegated-handlers for free. attach event-related handlers to the popover. - this.bindSegHandlersToEl(this.segPopover.el); - }, - - - // Builds the inner DOM contents of the segment popover - renderSegPopoverContent: function(row, col, segs) { - var view = this.view; - var isTheme = view.opt('theme'); - var title = this.getCellDate(row, col).format(view.opt('dayPopoverFormat')); - var content = $( - '<div class="fc-header ' + view.widgetHeaderClass + '">' + - '<span class="fc-close ' + - (isTheme ? 'ui-icon ui-icon-closethick' : 'fc-icon fc-icon-x') + - '"></span>' + - '<span class="fc-title">' + - htmlEscape(title) + - '</span>' + - '<div class="fc-clear"/>' + - '</div>' + - '<div class="fc-body ' + view.widgetContentClass + '">' + - '<div class="fc-event-container"></div>' + - '</div>' - ); - var segContainer = content.find('.fc-event-container'); - var i; - - // render each seg's `el` and only return the visible segs - segs = this.renderFgSegEls(segs, true); // disableResizing=true - this.popoverSegs = segs; - - for (i = 0; i < segs.length; i++) { - - // because segments in the popover are not part of a grid coordinate system, provide a hint to any - // grids that want to do drag-n-drop about which cell it came from - this.hitsNeeded(); - segs[i].hit = this.getCellHit(row, col); - this.hitsNotNeeded(); - - segContainer.append(segs[i].el); - } - - return content; - }, - - - // Given the events within an array of segment objects, reslice them to be in a single day - resliceDaySegs: function(segs, dayDate) { - - // build an array of the original events - var events = $.map(segs, function(seg) { - return seg.event; - }); - - var dayStart = dayDate.clone(); - var dayEnd = dayStart.clone().add(1, 'days'); - var dayRange = { start: dayStart, end: dayEnd }; - - // slice the events with a custom slicing function - segs = this.eventsToSegs( - events, - function(range) { - var seg = intersectRanges(range, dayRange); // undefind if no intersection - return seg ? [ seg ] : []; // must return an array of segments - } - ); - - // force an order because eventsToSegs doesn't guarantee one - this.sortEventSegs(segs); - - return segs; - }, - - - // Generates the text that should be inside a "more" link, given the number of events it represents - getMoreLinkText: function(num) { - var opt = this.view.opt('eventLimitText'); - - if (typeof opt === 'function') { - return opt(num); - } - else { - return '+' + num + ' ' + opt; - } - }, - - - // Returns segments within a given cell. - // If `startLevel` is specified, returns only events including and below that level. Otherwise returns all segs. - getCellSegs: function(row, col, startLevel) { - var segMatrix = this.rowStructs[row].segMatrix; - var level = startLevel || 0; - var segs = []; - var seg; - - while (level < segMatrix.length) { - seg = segMatrix[level][col]; - if (seg) { - segs.push(seg); - } - level++; - } - - return segs; - } - -}); - -;; - -/* A component that renders one or more columns of vertical time slots -----------------------------------------------------------------------------------------------------------------------*/ -// We mixin DayTable, even though there is only a single row of days - -var TimeGrid = FC.TimeGrid = Grid.extend(DayTableMixin, { - - slotDuration: null, // duration of a "slot", a distinct time segment on given day, visualized by lines - snapDuration: null, // granularity of time for dragging and selecting - snapsPerSlot: null, - labelFormat: null, // formatting string for times running along vertical axis - labelInterval: null, // duration of how often a label should be displayed for a slot - - colEls: null, // cells elements in the day-row background - slatContainerEl: null, // div that wraps all the slat rows - slatEls: null, // elements running horizontally across all columns - nowIndicatorEls: null, - - colCoordCache: null, - slatCoordCache: null, - - - constructor: function() { - Grid.apply(this, arguments); // call the super-constructor - - this.processOptions(); - }, - - - // Renders the time grid into `this.el`, which should already be assigned. - // Relies on the view's colCnt. In the future, this component should probably be self-sufficient. - renderDates: function() { - this.el.html(this.renderHtml()); - this.colEls = this.el.find('.fc-day, .fc-disabled-day'); - this.slatContainerEl = this.el.find('.fc-slats'); - this.slatEls = this.slatContainerEl.find('tr'); - - this.colCoordCache = new CoordCache({ - els: this.colEls, - isHorizontal: true - }); - this.slatCoordCache = new CoordCache({ - els: this.slatEls, - isVertical: true - }); - - this.renderContentSkeleton(); - }, - - - // Renders the basic HTML skeleton for the grid - renderHtml: function() { - return '' + - '<div class="fc-bg">' + - '<table>' + - this.renderBgTrHtml(0) + // row=0 - '</table>' + - '</div>' + - '<div class="fc-slats">' + - '<table>' + - this.renderSlatRowHtml() + - '</table>' + - '</div>'; - }, - - - // Generates the HTML for the horizontal "slats" that run width-wise. Has a time axis on a side. Depends on RTL. - renderSlatRowHtml: function() { - var view = this.view; - var isRTL = this.isRTL; - var html = ''; - var slotTime = moment.duration(+this.view.minTime); // wish there was .clone() for durations - var slotDate; // will be on the view's first day, but we only care about its time - var isLabeled; - var axisHtml; - - // Calculate the time for each slot - while (slotTime < this.view.maxTime) { - slotDate = this.start.clone().time(slotTime); - isLabeled = isInt(divideDurationByDuration(slotTime, this.labelInterval)); - - axisHtml = - '<td class="fc-axis fc-time ' + view.widgetContentClass + '" ' + view.axisStyleAttr() + '>' + - (isLabeled ? - '<span>' + // for matchCellWidths - htmlEscape(slotDate.format(this.labelFormat)) + - '</span>' : - '' - ) + - '</td>'; - - html += - '<tr data-time="' + slotDate.format('HH:mm:ss') + '"' + - (isLabeled ? '' : ' class="fc-minor"') + - '>' + - (!isRTL ? axisHtml : '') + - '<td class="' + view.widgetContentClass + '"/>' + - (isRTL ? axisHtml : '') + - "</tr>"; - - slotTime.add(this.slotDuration); - } - - return html; - }, - - - /* Options - ------------------------------------------------------------------------------------------------------------------*/ - - - // Parses various options into properties of this object - processOptions: function() { - var view = this.view; - var slotDuration = view.opt('slotDuration'); - var snapDuration = view.opt('snapDuration'); - var input; - - slotDuration = moment.duration(slotDuration); - snapDuration = snapDuration ? moment.duration(snapDuration) : slotDuration; - - this.slotDuration = slotDuration; - this.snapDuration = snapDuration; - this.snapsPerSlot = slotDuration / snapDuration; // TODO: ensure an integer multiple? - - this.minResizeDuration = snapDuration; // hack - - // might be an array value (for TimelineView). - // if so, getting the most granular entry (the last one probably). - input = view.opt('slotLabelFormat'); - if ($.isArray(input)) { - input = input[input.length - 1]; - } - - this.labelFormat = - input || - view.opt('smallTimeFormat'); // the computed default - - input = view.opt('slotLabelInterval'); - this.labelInterval = input ? - moment.duration(input) : - this.computeLabelInterval(slotDuration); - }, - - - // Computes an automatic value for slotLabelInterval - computeLabelInterval: function(slotDuration) { - var i; - var labelInterval; - var slotsPerLabel; - - // find the smallest stock label interval that results in more than one slots-per-label - for (i = AGENDA_STOCK_SUB_DURATIONS.length - 1; i >= 0; i--) { - labelInterval = moment.duration(AGENDA_STOCK_SUB_DURATIONS[i]); - slotsPerLabel = divideDurationByDuration(labelInterval, slotDuration); - if (isInt(slotsPerLabel) && slotsPerLabel > 1) { - return labelInterval; - } - } - - return moment.duration(slotDuration); // fall back. clone - }, - - - // Computes a default event time formatting string if `timeFormat` is not explicitly defined - computeEventTimeFormat: function() { - return this.view.opt('noMeridiemTimeFormat'); // like "6:30" (no AM/PM) - }, - - - // Computes a default `displayEventEnd` value if one is not expliclty defined - computeDisplayEventEnd: function() { - return true; - }, - - - /* Hit System - ------------------------------------------------------------------------------------------------------------------*/ - - - prepareHits: function() { - this.colCoordCache.build(); - this.slatCoordCache.build(); - }, - - - releaseHits: function() { - this.colCoordCache.clear(); - // NOTE: don't clear slatCoordCache because we rely on it for computeTimeTop - }, - - - queryHit: function(leftOffset, topOffset) { - var snapsPerSlot = this.snapsPerSlot; - var colCoordCache = this.colCoordCache; - var slatCoordCache = this.slatCoordCache; - - if (colCoordCache.isLeftInBounds(leftOffset) && slatCoordCache.isTopInBounds(topOffset)) { - var colIndex = colCoordCache.getHorizontalIndex(leftOffset); - var slatIndex = slatCoordCache.getVerticalIndex(topOffset); - - if (colIndex != null && slatIndex != null) { - var slatTop = slatCoordCache.getTopOffset(slatIndex); - var slatHeight = slatCoordCache.getHeight(slatIndex); - var partial = (topOffset - slatTop) / slatHeight; // floating point number between 0 and 1 - var localSnapIndex = Math.floor(partial * snapsPerSlot); // the snap # relative to start of slat - var snapIndex = slatIndex * snapsPerSlot + localSnapIndex; - var snapTop = slatTop + (localSnapIndex / snapsPerSlot) * slatHeight; - var snapBottom = slatTop + ((localSnapIndex + 1) / snapsPerSlot) * slatHeight; - - return { - col: colIndex, - snap: snapIndex, - component: this, // needed unfortunately :( - left: colCoordCache.getLeftOffset(colIndex), - right: colCoordCache.getRightOffset(colIndex), - top: snapTop, - bottom: snapBottom - }; - } - } - }, - - - getHitSpan: function(hit) { - var start = this.getCellDate(0, hit.col); // row=0 - var time = this.computeSnapTime(hit.snap); // pass in the snap-index - var end; - - start.time(time); - end = start.clone().add(this.snapDuration); - - return { start: start, end: end }; - }, - - - getHitEl: function(hit) { - return this.colEls.eq(hit.col); - }, - - - /* Dates - ------------------------------------------------------------------------------------------------------------------*/ - - - rangeUpdated: function() { - this.updateDayTable(); - }, - - - // Given a row number of the grid, representing a "snap", returns a time (Duration) from its start-of-day - computeSnapTime: function(snapIndex) { - return moment.duration(this.view.minTime + this.snapDuration * snapIndex); - }, - - - // Slices up the given span (unzoned start/end with other misc data) into an array of segments - spanToSegs: function(span) { - var segs = this.sliceRangeByTimes(span); - var i; - - for (i = 0; i < segs.length; i++) { - if (this.isRTL) { - segs[i].col = this.daysPerRow - 1 - segs[i].dayIndex; - } - else { - segs[i].col = segs[i].dayIndex; - } - } - - return segs; - }, - - - sliceRangeByTimes: function(range) { - var segs = []; - var seg; - var dayIndex; - var dayDate; - var dayRange; - - for (dayIndex = 0; dayIndex < this.daysPerRow; dayIndex++) { - dayDate = this.dayDates[dayIndex].clone().time(0); // TODO: better API for this? - dayRange = { - start: dayDate.clone().add(this.view.minTime), // don't use .time() because it sux with negatives - end: dayDate.clone().add(this.view.maxTime) - }; - seg = intersectRanges(range, dayRange); // both will be ambig timezone - if (seg) { - seg.dayIndex = dayIndex; - segs.push(seg); - } - } - - return segs; - }, - - - /* Coordinates - ------------------------------------------------------------------------------------------------------------------*/ - - - updateSize: function(isResize) { // NOT a standard Grid method - this.slatCoordCache.build(); - - if (isResize) { - this.updateSegVerticals( - [].concat(this.fgSegs || [], this.bgSegs || [], this.businessSegs || []) - ); - } - }, - - - getTotalSlatHeight: function() { - return this.slatContainerEl.outerHeight(); - }, - - - // Computes the top coordinate, relative to the bounds of the grid, of the given date. - // A `startOfDayDate` must be given for avoiding ambiguity over how to treat midnight. - computeDateTop: function(date, startOfDayDate) { - return this.computeTimeTop( - moment.duration( - date - startOfDayDate.clone().stripTime() - ) - ); - }, - - - // Computes the top coordinate, relative to the bounds of the grid, of the given time (a Duration). - computeTimeTop: function(time) { - var len = this.slatEls.length; - var slatCoverage = (time - this.view.minTime) / this.slotDuration; // floating-point value of # of slots covered - var slatIndex; - var slatRemainder; - - // compute a floating-point number for how many slats should be progressed through. - // from 0 to number of slats (inclusive) - // constrained because minTime/maxTime might be customized. - slatCoverage = Math.max(0, slatCoverage); - slatCoverage = Math.min(len, slatCoverage); - - // an integer index of the furthest whole slat - // from 0 to number slats (*exclusive*, so len-1) - slatIndex = Math.floor(slatCoverage); - slatIndex = Math.min(slatIndex, len - 1); - - // how much further through the slatIndex slat (from 0.0-1.0) must be covered in addition. - // could be 1.0 if slatCoverage is covering *all* the slots - slatRemainder = slatCoverage - slatIndex; - - return this.slatCoordCache.getTopPosition(slatIndex) + - this.slatCoordCache.getHeight(slatIndex) * slatRemainder; - }, - - - - /* Event Drag Visualization - ------------------------------------------------------------------------------------------------------------------*/ - - - // Renders a visual indication of an event being dragged over the specified date(s). - // A returned value of `true` signals that a mock "helper" event has been rendered. - renderDrag: function(eventLocation, seg) { - var eventSpans; - var i; - - if (seg) { // if there is event information for this drag, render a helper event - - // returns mock event elements - // signal that a helper has been rendered - return this.renderEventLocationHelper(eventLocation, seg); - } - else { // otherwise, just render a highlight - eventSpans = this.eventToSpans(eventLocation); - - for (i = 0; i < eventSpans.length; i++) { - this.renderHighlight(eventSpans[i]); - } - } - }, - - - // Unrenders any visual indication of an event being dragged - unrenderDrag: function() { - this.unrenderHelper(); - this.unrenderHighlight(); - }, - - - /* Event Resize Visualization - ------------------------------------------------------------------------------------------------------------------*/ - - - // Renders a visual indication of an event being resized - renderEventResize: function(eventLocation, seg) { - return this.renderEventLocationHelper(eventLocation, seg); // returns mock event elements - }, - - - // Unrenders any visual indication of an event being resized - unrenderEventResize: function() { - this.unrenderHelper(); - }, - - - /* Event Helper - ------------------------------------------------------------------------------------------------------------------*/ - - - // Renders a mock "helper" event. `sourceSeg` is the original segment object and might be null (an external drag) - renderHelper: function(event, sourceSeg) { - return this.renderHelperSegs(this.eventToSegs(event), sourceSeg); // returns mock event elements - }, - - - // Unrenders any mock helper event - unrenderHelper: function() { - this.unrenderHelperSegs(); - }, - - - /* Business Hours - ------------------------------------------------------------------------------------------------------------------*/ - - - renderBusinessHours: function() { - this.renderBusinessSegs( - this.buildBusinessHourSegs() - ); - }, - - - unrenderBusinessHours: function() { - this.unrenderBusinessSegs(); - }, - - - /* Now Indicator - ------------------------------------------------------------------------------------------------------------------*/ - - - getNowIndicatorUnit: function() { - return 'minute'; // will refresh on the minute - }, - - - renderNowIndicator: function(date) { - // seg system might be overkill, but it handles scenario where line needs to be rendered - // more than once because of columns with the same date (resources columns for example) - var segs = this.spanToSegs({ start: date, end: date }); - var top = this.computeDateTop(date, date); - var nodes = []; - var i; - - // render lines within the columns - for (i = 0; i < segs.length; i++) { - nodes.push($('<div class="fc-now-indicator fc-now-indicator-line"></div>') - .css('top', top) - .appendTo(this.colContainerEls.eq(segs[i].col))[0]); - } - - // render an arrow over the axis - if (segs.length > 0) { // is the current time in view? - nodes.push($('<div class="fc-now-indicator fc-now-indicator-arrow"></div>') - .css('top', top) - .appendTo(this.el.find('.fc-content-skeleton'))[0]); - } - - this.nowIndicatorEls = $(nodes); - }, - - - unrenderNowIndicator: function() { - if (this.nowIndicatorEls) { - this.nowIndicatorEls.remove(); - this.nowIndicatorEls = null; - } - }, - - - /* Selection - ------------------------------------------------------------------------------------------------------------------*/ - - - // Renders a visual indication of a selection. Overrides the default, which was to simply render a highlight. - renderSelection: function(span) { - if (this.view.opt('selectHelper')) { // this setting signals that a mock helper event should be rendered - - // normally acceps an eventLocation, span has a start/end, which is good enough - this.renderEventLocationHelper(span); - } - else { - this.renderHighlight(span); - } - }, - - - // Unrenders any visual indication of a selection - unrenderSelection: function() { - this.unrenderHelper(); - this.unrenderHighlight(); - }, - - - /* Highlight - ------------------------------------------------------------------------------------------------------------------*/ - - - renderHighlight: function(span) { - this.renderHighlightSegs(this.spanToSegs(span)); - }, - - - unrenderHighlight: function() { - this.unrenderHighlightSegs(); - } - -}); - -;; - -/* Methods for rendering SEGMENTS, pieces of content that live on the view - ( this file is no longer just for events ) -----------------------------------------------------------------------------------------------------------------------*/ - -TimeGrid.mixin({ - - colContainerEls: null, // containers for each column - - // inner-containers for each column where different types of segs live - fgContainerEls: null, - bgContainerEls: null, - helperContainerEls: null, - highlightContainerEls: null, - businessContainerEls: null, - - // arrays of different types of displayed segments - fgSegs: null, - bgSegs: null, - helperSegs: null, - highlightSegs: null, - businessSegs: null, - - - // Renders the DOM that the view's content will live in - renderContentSkeleton: function() { - var cellHtml = ''; - var i; - var skeletonEl; - - for (i = 0; i < this.colCnt; i++) { - cellHtml += - '<td>' + - '<div class="fc-content-col">' + - '<div class="fc-event-container fc-helper-container"></div>' + - '<div class="fc-event-container"></div>' + - '<div class="fc-highlight-container"></div>' + - '<div class="fc-bgevent-container"></div>' + - '<div class="fc-business-container"></div>' + - '</div>' + - '</td>'; - } - - skeletonEl = $( - '<div class="fc-content-skeleton">' + - '<table>' + - '<tr>' + cellHtml + '</tr>' + - '</table>' + - '</div>' - ); - - this.colContainerEls = skeletonEl.find('.fc-content-col'); - this.helperContainerEls = skeletonEl.find('.fc-helper-container'); - this.fgContainerEls = skeletonEl.find('.fc-event-container:not(.fc-helper-container)'); - this.bgContainerEls = skeletonEl.find('.fc-bgevent-container'); - this.highlightContainerEls = skeletonEl.find('.fc-highlight-container'); - this.businessContainerEls = skeletonEl.find('.fc-business-container'); - - this.bookendCells(skeletonEl.find('tr')); // TODO: do this on string level - this.el.append(skeletonEl); - }, - - - /* Foreground Events - ------------------------------------------------------------------------------------------------------------------*/ - - - renderFgSegs: function(segs) { - segs = this.renderFgSegsIntoContainers(segs, this.fgContainerEls); - this.fgSegs = segs; - return segs; // needed for Grid::renderEvents - }, - - - unrenderFgSegs: function() { - this.unrenderNamedSegs('fgSegs'); - }, - - - /* Foreground Helper Events - ------------------------------------------------------------------------------------------------------------------*/ - - - renderHelperSegs: function(segs, sourceSeg) { - var helperEls = []; - var i, seg; - var sourceEl; - - segs = this.renderFgSegsIntoContainers(segs, this.helperContainerEls); - - // Try to make the segment that is in the same row as sourceSeg look the same - for (i = 0; i < segs.length; i++) { - seg = segs[i]; - if (sourceSeg && sourceSeg.col === seg.col) { - sourceEl = sourceSeg.el; - seg.el.css({ - left: sourceEl.css('left'), - right: sourceEl.css('right'), - 'margin-left': sourceEl.css('margin-left'), - 'margin-right': sourceEl.css('margin-right') - }); - } - helperEls.push(seg.el[0]); - } - - this.helperSegs = segs; - - return $(helperEls); // must return rendered helpers - }, - - - unrenderHelperSegs: function() { - this.unrenderNamedSegs('helperSegs'); - }, - - - /* Background Events - ------------------------------------------------------------------------------------------------------------------*/ - - - renderBgSegs: function(segs) { - segs = this.renderFillSegEls('bgEvent', segs); // TODO: old fill system - this.updateSegVerticals(segs); - this.attachSegsByCol(this.groupSegsByCol(segs), this.bgContainerEls); - this.bgSegs = segs; - return segs; // needed for Grid::renderEvents - }, - - - unrenderBgSegs: function() { - this.unrenderNamedSegs('bgSegs'); - }, - - - /* Highlight - ------------------------------------------------------------------------------------------------------------------*/ - - - renderHighlightSegs: function(segs) { - segs = this.renderFillSegEls('highlight', segs); // TODO: old fill system - this.updateSegVerticals(segs); - this.attachSegsByCol(this.groupSegsByCol(segs), this.highlightContainerEls); - this.highlightSegs = segs; - }, - - - unrenderHighlightSegs: function() { - this.unrenderNamedSegs('highlightSegs'); - }, - - - /* Business Hours - ------------------------------------------------------------------------------------------------------------------*/ - - - renderBusinessSegs: function(segs) { - segs = this.renderFillSegEls('businessHours', segs); // TODO: old fill system - this.updateSegVerticals(segs); - this.attachSegsByCol(this.groupSegsByCol(segs), this.businessContainerEls); - this.businessSegs = segs; - }, - - - unrenderBusinessSegs: function() { - this.unrenderNamedSegs('businessSegs'); - }, - - - /* Seg Rendering Utils - ------------------------------------------------------------------------------------------------------------------*/ - - - // Given a flat array of segments, return an array of sub-arrays, grouped by each segment's col - groupSegsByCol: function(segs) { - var segsByCol = []; - var i; - - for (i = 0; i < this.colCnt; i++) { - segsByCol.push([]); - } - - for (i = 0; i < segs.length; i++) { - segsByCol[segs[i].col].push(segs[i]); - } - - return segsByCol; - }, - - - // Given segments grouped by column, insert the segments' elements into a parallel array of container - // elements, each living within a column. - attachSegsByCol: function(segsByCol, containerEls) { - var col; - var segs; - var i; - - for (col = 0; col < this.colCnt; col++) { // iterate each column grouping - segs = segsByCol[col]; - - for (i = 0; i < segs.length; i++) { - containerEls.eq(col).append(segs[i].el); - } - } - }, - - - // Given the name of a property of `this` object, assumed to be an array of segments, - // loops through each segment and removes from DOM. Will null-out the property afterwards. - unrenderNamedSegs: function(propName) { - var segs = this[propName]; - var i; - - if (segs) { - for (i = 0; i < segs.length; i++) { - segs[i].el.remove(); - } - this[propName] = null; - } - }, - - - - /* Foreground Event Rendering Utils - ------------------------------------------------------------------------------------------------------------------*/ - - - // Given an array of foreground segments, render a DOM element for each, computes position, - // and attaches to the column inner-container elements. - renderFgSegsIntoContainers: function(segs, containerEls) { - var segsByCol; - var col; - - segs = this.renderFgSegEls(segs); // will call fgSegHtml - segsByCol = this.groupSegsByCol(segs); - - for (col = 0; col < this.colCnt; col++) { - this.updateFgSegCoords(segsByCol[col]); - } - - this.attachSegsByCol(segsByCol, containerEls); - - return segs; - }, - - - // Renders the HTML for a single event segment's default rendering - fgSegHtml: function(seg, disableResizing) { - var view = this.view; - var event = seg.event; - var isDraggable = view.isEventDraggable(event); - var isResizableFromStart = !disableResizing && seg.isStart && view.isEventResizableFromStart(event); - var isResizableFromEnd = !disableResizing && seg.isEnd && view.isEventResizableFromEnd(event); - var classes = this.getSegClasses(seg, isDraggable, isResizableFromStart || isResizableFromEnd); - var skinCss = cssToStr(this.getSegSkinCss(seg)); - var timeText; - var fullTimeText; // more verbose time text. for the print stylesheet - var startTimeText; // just the start time text - - classes.unshift('fc-time-grid-event', 'fc-v-event'); - - if (view.isMultiDayEvent(event)) { // if the event appears to span more than one day... - // Don't display time text on segments that run entirely through a day. - // That would appear as midnight-midnight and would look dumb. - // Otherwise, display the time text for the *segment's* times (like 6pm-midnight or midnight-10am) - if (seg.isStart || seg.isEnd) { - timeText = this.getEventTimeText(seg); - fullTimeText = this.getEventTimeText(seg, 'LT'); - startTimeText = this.getEventTimeText(seg, null, false); // displayEnd=false - } - } else { - // Display the normal time text for the *event's* times - timeText = this.getEventTimeText(event); - fullTimeText = this.getEventTimeText(event, 'LT'); - startTimeText = this.getEventTimeText(event, null, false); // displayEnd=false - } - - return '<a class="' + classes.join(' ') + '"' + - (event.url ? - ' href="' + htmlEscape(event.url) + '"' : - '' - ) + - (skinCss ? - ' style="' + skinCss + '"' : - '' - ) + - '>' + - '<div class="fc-content">' + - (timeText ? - '<div class="fc-time"' + - ' data-start="' + htmlEscape(startTimeText) + '"' + - ' data-full="' + htmlEscape(fullTimeText) + '"' + - '>' + - '<span>' + htmlEscape(timeText) + '</span>' + - '</div>' : - '' - ) + - (event.title ? - '<div class="fc-title">' + - htmlEscape(event.title) + - '</div>' : - '' - ) + - '</div>' + - '<div class="fc-bg"/>' + - /* TODO: write CSS for this - (isResizableFromStart ? - '<div class="fc-resizer fc-start-resizer" />' : - '' - ) + - */ - (isResizableFromEnd ? - '<div class="fc-resizer fc-end-resizer" />' : - '' - ) + - '</a>'; - }, - - - /* Seg Position Utils - ------------------------------------------------------------------------------------------------------------------*/ - - - // Refreshes the CSS top/bottom coordinates for each segment element. - // Works when called after initial render, after a window resize/zoom for example. - updateSegVerticals: function(segs) { - this.computeSegVerticals(segs); - this.assignSegVerticals(segs); - }, - - - // For each segment in an array, computes and assigns its top and bottom properties - computeSegVerticals: function(segs) { - var i, seg; - var dayDate; - - for (i = 0; i < segs.length; i++) { - seg = segs[i]; - dayDate = this.dayDates[seg.dayIndex]; - - seg.top = this.computeDateTop(seg.start, dayDate); - seg.bottom = this.computeDateTop(seg.end, dayDate); - } - }, - - - // Given segments that already have their top/bottom properties computed, applies those values to - // the segments' elements. - assignSegVerticals: function(segs) { - var i, seg; - - for (i = 0; i < segs.length; i++) { - seg = segs[i]; - seg.el.css(this.generateSegVerticalCss(seg)); - } - }, - - - // Generates an object with CSS properties for the top/bottom coordinates of a segment element - generateSegVerticalCss: function(seg) { - return { - top: seg.top, - bottom: -seg.bottom // flipped because needs to be space beyond bottom edge of event container - }; - }, - - - /* Foreground Event Positioning Utils - ------------------------------------------------------------------------------------------------------------------*/ - - - // Given segments that are assumed to all live in the *same column*, - // compute their verical/horizontal coordinates and assign to their elements. - updateFgSegCoords: function(segs) { - this.computeSegVerticals(segs); // horizontals relies on this - this.computeFgSegHorizontals(segs); // compute horizontal coordinates, z-index's, and reorder the array - this.assignSegVerticals(segs); - this.assignFgSegHorizontals(segs); - }, - - - // Given an array of segments that are all in the same column, sets the backwardCoord and forwardCoord on each. - // NOTE: Also reorders the given array by date! - computeFgSegHorizontals: function(segs) { - var levels; - var level0; - var i; - - this.sortEventSegs(segs); // order by certain criteria - levels = buildSlotSegLevels(segs); - computeForwardSlotSegs(levels); - - if ((level0 = levels[0])) { - - for (i = 0; i < level0.length; i++) { - computeSlotSegPressures(level0[i]); - } - - for (i = 0; i < level0.length; i++) { - this.computeFgSegForwardBack(level0[i], 0, 0); - } - } - }, - - - // Calculate seg.forwardCoord and seg.backwardCoord for the segment, where both values range - // from 0 to 1. If the calendar is left-to-right, the seg.backwardCoord maps to "left" and - // seg.forwardCoord maps to "right" (via percentage). Vice-versa if the calendar is right-to-left. - // - // The segment might be part of a "series", which means consecutive segments with the same pressure - // who's width is unknown until an edge has been hit. `seriesBackwardPressure` is the number of - // segments behind this one in the current series, and `seriesBackwardCoord` is the starting - // coordinate of the first segment in the series. - computeFgSegForwardBack: function(seg, seriesBackwardPressure, seriesBackwardCoord) { - var forwardSegs = seg.forwardSegs; - var i; - - if (seg.forwardCoord === undefined) { // not already computed - - if (!forwardSegs.length) { - - // if there are no forward segments, this segment should butt up against the edge - seg.forwardCoord = 1; - } - else { - - // sort highest pressure first - this.sortForwardSegs(forwardSegs); - - // this segment's forwardCoord will be calculated from the backwardCoord of the - // highest-pressure forward segment. - this.computeFgSegForwardBack(forwardSegs[0], seriesBackwardPressure + 1, seriesBackwardCoord); - seg.forwardCoord = forwardSegs[0].backwardCoord; - } - - // calculate the backwardCoord from the forwardCoord. consider the series - seg.backwardCoord = seg.forwardCoord - - (seg.forwardCoord - seriesBackwardCoord) / // available width for series - (seriesBackwardPressure + 1); // # of segments in the series - - // use this segment's coordinates to computed the coordinates of the less-pressurized - // forward segments - for (i=0; i<forwardSegs.length; i++) { - this.computeFgSegForwardBack(forwardSegs[i], 0, seg.forwardCoord); - } - } - }, - - - sortForwardSegs: function(forwardSegs) { - forwardSegs.sort(proxy(this, 'compareForwardSegs')); - }, - - - // A cmp function for determining which forward segment to rely on more when computing coordinates. - compareForwardSegs: function(seg1, seg2) { - // put higher-pressure first - return seg2.forwardPressure - seg1.forwardPressure || - // put segments that are closer to initial edge first (and favor ones with no coords yet) - (seg1.backwardCoord || 0) - (seg2.backwardCoord || 0) || - // do normal sorting... - this.compareEventSegs(seg1, seg2); - }, - - - // Given foreground event segments that have already had their position coordinates computed, - // assigns position-related CSS values to their elements. - assignFgSegHorizontals: function(segs) { - var i, seg; - - for (i = 0; i < segs.length; i++) { - seg = segs[i]; - seg.el.css(this.generateFgSegHorizontalCss(seg)); - - // if the height is short, add a className for alternate styling - if (seg.bottom - seg.top < 30) { - seg.el.addClass('fc-short'); - } - } - }, - - - // Generates an object with CSS properties/values that should be applied to an event segment element. - // Contains important positioning-related properties that should be applied to any event element, customized or not. - generateFgSegHorizontalCss: function(seg) { - var shouldOverlap = this.view.opt('slotEventOverlap'); - var backwardCoord = seg.backwardCoord; // the left side if LTR. the right side if RTL. floating-point - var forwardCoord = seg.forwardCoord; // the right side if LTR. the left side if RTL. floating-point - var props = this.generateSegVerticalCss(seg); // get top/bottom first - var left; // amount of space from left edge, a fraction of the total width - var right; // amount of space from right edge, a fraction of the total width - - if (shouldOverlap) { - // double the width, but don't go beyond the maximum forward coordinate (1.0) - forwardCoord = Math.min(1, backwardCoord + (forwardCoord - backwardCoord) * 2); - } - - if (this.isRTL) { - left = 1 - forwardCoord; - right = backwardCoord; - } - else { - left = backwardCoord; - right = 1 - forwardCoord; - } - - props.zIndex = seg.level + 1; // convert from 0-base to 1-based - props.left = left * 100 + '%'; - props.right = right * 100 + '%'; - - if (shouldOverlap && seg.forwardPressure) { - // add padding to the edge so that forward stacked events don't cover the resizer's icon - props[this.isRTL ? 'marginLeft' : 'marginRight'] = 10 * 2; // 10 is a guesstimate of the icon's width - } - - return props; - } - -}); - - -// Builds an array of segments "levels". The first level will be the leftmost tier of segments if the calendar is -// left-to-right, or the rightmost if the calendar is right-to-left. Assumes the segments are already ordered by date. -function buildSlotSegLevels(segs) { - var levels = []; - var i, seg; - var j; - - for (i=0; i<segs.length; i++) { - seg = segs[i]; - - // go through all the levels and stop on the first level where there are no collisions - for (j=0; j<levels.length; j++) { - if (!computeSlotSegCollisions(seg, levels[j]).length) { - break; - } - } - - seg.level = j; - - (levels[j] || (levels[j] = [])).push(seg); - } - - return levels; -} - - -// For every segment, figure out the other segments that are in subsequent -// levels that also occupy the same vertical space. Accumulate in seg.forwardSegs -function computeForwardSlotSegs(levels) { - var i, level; - var j, seg; - var k; - - for (i=0; i<levels.length; i++) { - level = levels[i]; - - for (j=0; j<level.length; j++) { - seg = level[j]; - - seg.forwardSegs = []; - for (k=i+1; k<levels.length; k++) { - computeSlotSegCollisions(seg, levels[k], seg.forwardSegs); - } - } - } -} - - -// Figure out which path forward (via seg.forwardSegs) results in the longest path until -// the furthest edge is reached. The number of segments in this path will be seg.forwardPressure -function computeSlotSegPressures(seg) { - var forwardSegs = seg.forwardSegs; - var forwardPressure = 0; - var i, forwardSeg; - - if (seg.forwardPressure === undefined) { // not already computed - - for (i=0; i<forwardSegs.length; i++) { - forwardSeg = forwardSegs[i]; - - // figure out the child's maximum forward path - computeSlotSegPressures(forwardSeg); - - // either use the existing maximum, or use the child's forward pressure - // plus one (for the forwardSeg itself) - forwardPressure = Math.max( - forwardPressure, - 1 + forwardSeg.forwardPressure - ); - } - - seg.forwardPressure = forwardPressure; - } -} - - -// Find all the segments in `otherSegs` that vertically collide with `seg`. -// Append into an optionally-supplied `results` array and return. -function computeSlotSegCollisions(seg, otherSegs, results) { - results = results || []; - - for (var i=0; i<otherSegs.length; i++) { - if (isSlotSegCollision(seg, otherSegs[i])) { - results.push(otherSegs[i]); - } - } - - return results; -} - - -// Do these segments occupy the same vertical space? -function isSlotSegCollision(seg1, seg2) { - return seg1.bottom > seg2.top && seg1.top < seg2.bottom; -} - -;; - -/* An abstract class from which other views inherit from -----------------------------------------------------------------------------------------------------------------------*/ - -var View = FC.View = Model.extend({ - - type: null, // subclass' view name (string) - name: null, // deprecated. use `type` instead - title: null, // the text that will be displayed in the header's title - - calendar: null, // owner Calendar object - viewSpec: null, - options: null, // hash containing all options. already merged with view-specific-options - el: null, // the view's containing element. set by Calendar - - renderQueue: null, - batchRenderDepth: 0, - isDatesRendered: false, - isEventsRendered: false, - isBaseRendered: false, // related to viewRender/viewDestroy triggers - - queuedScroll: null, - - isRTL: false, - isSelected: false, // boolean whether a range of time is user-selected or not - selectedEvent: null, - - eventOrderSpecs: null, // criteria for ordering events when they have same date/time - - // classNames styled by jqui themes - widgetHeaderClass: null, - widgetContentClass: null, - highlightStateClass: null, - - // for date utils, computed from options - nextDayThreshold: null, - isHiddenDayHash: null, - - // now indicator - isNowIndicatorRendered: null, - initialNowDate: null, // result first getNow call - initialNowQueriedMs: null, // ms time the getNow was called - nowIndicatorTimeoutID: null, // for refresh timing of now indicator - nowIndicatorIntervalID: null, // " - - - constructor: function(calendar, viewSpec) { - Model.prototype.constructor.call(this); - - this.calendar = calendar; - this.viewSpec = viewSpec; - - // shortcuts - this.type = viewSpec.type; - this.options = viewSpec.options; - - // .name is deprecated - this.name = this.type; - - this.nextDayThreshold = moment.duration(this.opt('nextDayThreshold')); - this.initThemingProps(); - this.initHiddenDays(); - this.isRTL = this.opt('isRTL'); - - this.eventOrderSpecs = parseFieldSpecs(this.opt('eventOrder')); - - this.renderQueue = this.buildRenderQueue(); - this.initAutoBatchRender(); - - this.initialize(); - }, - - - buildRenderQueue: function() { - var _this = this; - var renderQueue = new RenderQueue({ - event: this.opt('eventRenderWait') - }); - - renderQueue.on('start', function() { - _this.freezeHeight(); - _this.addScroll(_this.queryScroll()); - }); - - renderQueue.on('stop', function() { - _this.thawHeight(); - _this.popScroll(); - }); - - return renderQueue; - }, - - - initAutoBatchRender: function() { - var _this = this; - - this.on('before:change', function() { - _this.startBatchRender(); - }); - - this.on('change', function() { - _this.stopBatchRender(); - }); - }, - - - startBatchRender: function() { - if (!(this.batchRenderDepth++)) { - this.renderQueue.pause(); - } - }, - - - stopBatchRender: function() { - if (!(--this.batchRenderDepth)) { - this.renderQueue.resume(); - } - }, - - - // A good place for subclasses to initialize member variables - initialize: function() { - // subclasses can implement - }, - - - // Retrieves an option with the given name - opt: function(name) { - return this.options[name]; - }, - - - // Triggers handlers that are view-related. Modifies args before passing to calendar. - publiclyTrigger: function(name, thisObj) { // arguments beyond thisObj are passed along - var calendar = this.calendar; - - return calendar.publiclyTrigger.apply( - calendar, - [name, thisObj || this].concat( - Array.prototype.slice.call(arguments, 2), // arguments beyond thisObj - [ this ] // always make the last argument a reference to the view. TODO: deprecate - ) - ); - }, - - - /* Title and Date Formatting - ------------------------------------------------------------------------------------------------------------------*/ - - - // Sets the view's title property to the most updated computed value - updateTitle: function() { - this.title = this.computeTitle(); - this.calendar.setToolbarsTitle(this.title); - }, - - - // Computes what the title at the top of the calendar should be for this view - computeTitle: function() { - var range; - - // for views that span a large unit of time, show the proper interval, ignoring stray days before and after - if (/^(year|month)$/.test(this.currentRangeUnit)) { - range = this.currentRange; - } - else { // for day units or smaller, use the actual day range - range = this.activeRange; - } - - return this.formatRange( - { - // in case currentRange has a time, make sure timezone is correct - start: this.calendar.applyTimezone(range.start), - end: this.calendar.applyTimezone(range.end) - }, - this.opt('titleFormat') || this.computeTitleFormat(), - this.opt('titleRangeSeparator') - ); - }, - - - // Generates the format string that should be used to generate the title for the current date range. - // Attempts to compute the most appropriate format if not explicitly specified with `titleFormat`. - computeTitleFormat: function() { - if (this.currentRangeUnit == 'year') { - return 'YYYY'; - } - else if (this.currentRangeUnit == 'month') { - return this.opt('monthYearFormat'); // like "September 2014" - } - else if (this.currentRangeAs('days') > 1) { - return 'll'; // multi-day range. shorter, like "Sep 9 - 10 2014" - } - else { - return 'LL'; // one day. longer, like "September 9 2014" - } - }, - - - // Utility for formatting a range. Accepts a range object, formatting string, and optional separator. - // Displays all-day ranges naturally, with an inclusive end. Takes the current isRTL into account. - // The timezones of the dates within `range` will be respected. - formatRange: function(range, formatStr, separator) { - var end = range.end; - - if (!end.hasTime()) { // all-day? - end = end.clone().subtract(1); // convert to inclusive. last ms of previous day - } - - return formatRange(range.start, end, formatStr, separator, this.opt('isRTL')); - }, - - - getAllDayHtml: function() { - return this.opt('allDayHtml') || htmlEscape(this.opt('allDayText')); - }, - - - /* Navigation - ------------------------------------------------------------------------------------------------------------------*/ - - - // Generates HTML for an anchor to another view into the calendar. - // Will either generate an <a> tag or a non-clickable <span> tag, depending on enabled settings. - // `gotoOptions` can either be a moment input, or an object with the form: - // { date, type, forceOff } - // `type` is a view-type like "day" or "week". default value is "day". - // `attrs` and `innerHtml` are use to generate the rest of the HTML tag. - buildGotoAnchorHtml: function(gotoOptions, attrs, innerHtml) { - var date, type, forceOff; - var finalOptions; - - if ($.isPlainObject(gotoOptions)) { - date = gotoOptions.date; - type = gotoOptions.type; - forceOff = gotoOptions.forceOff; - } - else { - date = gotoOptions; // a single moment input - } - date = FC.moment(date); // if a string, parse it - - finalOptions = { // for serialization into the link - date: date.format('YYYY-MM-DD'), - type: type || 'day' - }; - - if (typeof attrs === 'string') { - innerHtml = attrs; - attrs = null; - } - - attrs = attrs ? ' ' + attrsToStr(attrs) : ''; // will have a leading space - innerHtml = innerHtml || ''; - - if (!forceOff && this.opt('navLinks')) { - return '<a' + attrs + - ' data-goto="' + htmlEscape(JSON.stringify(finalOptions)) + '">' + - innerHtml + - '</a>'; - } - else { - return '<span' + attrs + '>' + - innerHtml + - '</span>'; - } - }, - - - // Rendering Non-date-related Content - // ----------------------------------------------------------------------------------------------------------------- - - - // Sets the container element that the view should render inside of, does global DOM-related initializations, - // and renders all the non-date-related content inside. - setElement: function(el) { - this.el = el; - this.bindGlobalHandlers(); - this.bindBaseRenderHandlers(); - this.renderSkeleton(); - }, - - - // Removes the view's container element from the DOM, clearing any content beforehand. - // Undoes any other DOM-related attachments. - removeElement: function() { - this.unsetDate(); - this.unrenderSkeleton(); - - this.unbindGlobalHandlers(); - this.unbindBaseRenderHandlers(); - - this.el.remove(); - // NOTE: don't null-out this.el in case the View was destroyed within an API callback. - // We don't null-out the View's other jQuery element references upon destroy, - // so we shouldn't kill this.el either. - }, - - - // Renders the basic structure of the view before any content is rendered - renderSkeleton: function() { - // subclasses should implement - }, - - - // Unrenders the basic structure of the view - unrenderSkeleton: function() { - // subclasses should implement - }, - - - // Date Setting/Unsetting - // ----------------------------------------------------------------------------------------------------------------- - - - setDate: function(date) { - var currentDateProfile = this.get('dateProfile'); - var newDateProfile = this.buildDateProfile(date, null, true); // forceToValid=true - - if ( - !currentDateProfile || - !isRangesEqual(currentDateProfile.activeRange, newDateProfile.activeRange) - ) { - this.set('dateProfile', newDateProfile); - } - - return newDateProfile.date; - }, - - - unsetDate: function() { - this.unset('dateProfile'); - }, - - - // Date Rendering - // ----------------------------------------------------------------------------------------------------------------- - - - requestDateRender: function(dateProfile) { - var _this = this; - - this.renderQueue.queue(function() { - _this.executeDateRender(dateProfile); - }, 'date', 'init'); - }, - - - requestDateUnrender: function() { - var _this = this; - - this.renderQueue.queue(function() { - _this.executeDateUnrender(); - }, 'date', 'destroy'); - }, - - - // Event Data - // ----------------------------------------------------------------------------------------------------------------- - - - fetchInitialEvents: function(dateProfile) { - return this.calendar.requestEvents( - dateProfile.activeRange.start, - dateProfile.activeRange.end - ); - }, - - - bindEventChanges: function() { - this.listenTo(this.calendar, 'eventsReset', this.resetEvents); - }, - - - unbindEventChanges: function() { - this.stopListeningTo(this.calendar, 'eventsReset'); - }, - - - setEvents: function(events) { - this.set('currentEvents', events); - this.set('hasEvents', true); - }, - - - unsetEvents: function() { - this.unset('currentEvents'); - this.unset('hasEvents'); - }, - - - resetEvents: function(events) { - this.startBatchRender(); - this.unsetEvents(); - this.setEvents(events); - this.stopBatchRender(); - }, - - - // Event Rendering - // ----------------------------------------------------------------------------------------------------------------- - - - requestEventsRender: function(events) { - var _this = this; - - this.renderQueue.queue(function() { - _this.executeEventsRender(events); - }, 'event', 'init'); - }, - - - requestEventsUnrender: function() { - var _this = this; - - this.renderQueue.queue(function() { - _this.executeEventsUnrender(); - }, 'event', 'destroy'); - }, - - - // Date High-level Rendering - // ----------------------------------------------------------------------------------------------------------------- - - - // if dateProfile not specified, uses current - executeDateRender: function(dateProfile, skipScroll) { - - this.setDateProfileForRendering(dateProfile); - this.updateTitle(); - this.calendar.updateToolbarButtons(); - - if (this.render) { - this.render(); // TODO: deprecate - } - - this.renderDates(); - this.updateSize(); - this.renderBusinessHours(); // might need coordinates, so should go after updateSize() - this.startNowIndicator(); - - if (!skipScroll) { - this.addScroll(this.computeInitialDateScroll()); - } - - this.isDatesRendered = true; - this.trigger('datesRendered'); - }, - - - executeDateUnrender: function() { - - this.unselect(); - this.stopNowIndicator(); - - this.trigger('before:datesUnrendered'); - - this.unrenderBusinessHours(); - this.unrenderDates(); - - if (this.destroy) { - this.destroy(); // TODO: deprecate - } - - this.isDatesRendered = false; - }, - - - // Date Low-level Rendering - // ----------------------------------------------------------------------------------------------------------------- - - - // date-cell content only - renderDates: function() { - // subclasses should implement - }, - - - // date-cell content only - unrenderDates: function() { - // subclasses should override - }, - - - // Determing when the "meat" of the view is rendered (aka the base) - // ----------------------------------------------------------------------------------------------------------------- - - - bindBaseRenderHandlers: function() { - var _this = this; - - this.on('datesRendered.baseHandler', function() { - _this.onBaseRender(); - }); - - this.on('before:datesUnrendered.baseHandler', function() { - _this.onBeforeBaseUnrender(); - }); - }, - - - unbindBaseRenderHandlers: function() { - this.off('.baseHandler'); - }, - - - onBaseRender: function() { - this.applyScreenState(); - this.publiclyTrigger('viewRender', this, this, this.el); - }, - - - onBeforeBaseUnrender: function() { - this.applyScreenState(); - this.publiclyTrigger('viewDestroy', this, this, this.el); - }, - - - // Misc view rendering utils - // ----------------------------------------------------------------------------------------------------------------- - - - // Binds DOM handlers to elements that reside outside the view container, such as the document - bindGlobalHandlers: function() { - this.listenTo(GlobalEmitter.get(), { - touchstart: this.processUnselect, - mousedown: this.handleDocumentMousedown - }); - }, - - - // Unbinds DOM handlers from elements that reside outside the view container - unbindGlobalHandlers: function() { - this.stopListeningTo(GlobalEmitter.get()); - }, - - - // Initializes internal variables related to theming - initThemingProps: function() { - var tm = this.opt('theme') ? 'ui' : 'fc'; - - this.widgetHeaderClass = tm + '-widget-header'; - this.widgetContentClass = tm + '-widget-content'; - this.highlightStateClass = tm + '-state-highlight'; - }, - - - /* Business Hours - ------------------------------------------------------------------------------------------------------------------*/ - - - // Renders business-hours onto the view. Assumes updateSize has already been called. - renderBusinessHours: function() { - // subclasses should implement - }, - - - // Unrenders previously-rendered business-hours - unrenderBusinessHours: function() { - // subclasses should implement - }, - - - /* Now Indicator - ------------------------------------------------------------------------------------------------------------------*/ - - - // Immediately render the current time indicator and begins re-rendering it at an interval, - // which is defined by this.getNowIndicatorUnit(). - // TODO: somehow do this for the current whole day's background too - startNowIndicator: function() { - var _this = this; - var unit; - var update; - var delay; // ms wait value - - if (this.opt('nowIndicator')) { - unit = this.getNowIndicatorUnit(); - if (unit) { - update = proxy(this, 'updateNowIndicator'); // bind to `this` - - this.initialNowDate = this.calendar.getNow(); - this.initialNowQueriedMs = +new Date(); - this.renderNowIndicator(this.initialNowDate); - this.isNowIndicatorRendered = true; - - // wait until the beginning of the next interval - delay = this.initialNowDate.clone().startOf(unit).add(1, unit) - this.initialNowDate; - this.nowIndicatorTimeoutID = setTimeout(function() { - _this.nowIndicatorTimeoutID = null; - update(); - delay = +moment.duration(1, unit); - delay = Math.max(100, delay); // prevent too frequent - _this.nowIndicatorIntervalID = setInterval(update, delay); // update every interval - }, delay); - } - } - }, - - - // rerenders the now indicator, computing the new current time from the amount of time that has passed - // since the initial getNow call. - updateNowIndicator: function() { - if (this.isNowIndicatorRendered) { - this.unrenderNowIndicator(); - this.renderNowIndicator( - this.initialNowDate.clone().add(new Date() - this.initialNowQueriedMs) // add ms - ); - } - }, - - - // Immediately unrenders the view's current time indicator and stops any re-rendering timers. - // Won't cause side effects if indicator isn't rendered. - stopNowIndicator: function() { - if (this.isNowIndicatorRendered) { - - if (this.nowIndicatorTimeoutID) { - clearTimeout(this.nowIndicatorTimeoutID); - this.nowIndicatorTimeoutID = null; - } - if (this.nowIndicatorIntervalID) { - clearTimeout(this.nowIndicatorIntervalID); - this.nowIndicatorIntervalID = null; - } - - this.unrenderNowIndicator(); - this.isNowIndicatorRendered = false; - } - }, - - - // Returns a string unit, like 'second' or 'minute' that defined how often the current time indicator - // should be refreshed. If something falsy is returned, no time indicator is rendered at all. - getNowIndicatorUnit: function() { - // subclasses should implement - }, - - - // Renders a current time indicator at the given datetime - renderNowIndicator: function(date) { - // subclasses should implement - }, - - - // Undoes the rendering actions from renderNowIndicator - unrenderNowIndicator: function() { - // subclasses should implement - }, - - - /* Dimensions - ------------------------------------------------------------------------------------------------------------------*/ - - - // Refreshes anything dependant upon sizing of the container element of the grid - updateSize: function(isResize) { - var scroll; - - if (isResize) { - scroll = this.queryScroll(); - } - - this.updateHeight(isResize); - this.updateWidth(isResize); - this.updateNowIndicator(); - - if (isResize) { - this.applyScroll(scroll); - } - }, - - - // Refreshes the horizontal dimensions of the calendar - updateWidth: function(isResize) { - // subclasses should implement - }, - - - // Refreshes the vertical dimensions of the calendar - updateHeight: function(isResize) { - var calendar = this.calendar; // we poll the calendar for height information - - this.setHeight( - calendar.getSuggestedViewHeight(), - calendar.isHeightAuto() - ); - }, - - - // Updates the vertical dimensions of the calendar to the specified height. - // if `isAuto` is set to true, height becomes merely a suggestion and the view should use its "natural" height. - setHeight: function(height, isAuto) { - // subclasses should implement - }, - - - /* Scroller - ------------------------------------------------------------------------------------------------------------------*/ - - - addForcedScroll: function(scroll) { - this.addScroll( - $.extend(scroll, { isForced: true }) - ); - }, - - - addScroll: function(scroll) { - var queuedScroll = this.queuedScroll || (this.queuedScroll = {}); - - if (!queuedScroll.isForced) { - $.extend(queuedScroll, scroll); - } - }, - - - popScroll: function() { - this.applyQueuedScroll(); - this.queuedScroll = null; - }, - - - applyQueuedScroll: function() { - if (this.queuedScroll) { - this.applyScroll(this.queuedScroll); - } - }, - - - queryScroll: function() { - var scroll = {}; - - if (this.isDatesRendered) { - $.extend(scroll, this.queryDateScroll()); - } - - return scroll; - }, - - - applyScroll: function(scroll) { - if (this.isDatesRendered) { - this.applyDateScroll(scroll); - } - }, - - - computeInitialDateScroll: function() { - return {}; // subclasses must implement - }, - - - queryDateScroll: function() { - return {}; // subclasses must implement - }, - - - applyDateScroll: function(scroll) { - ; // subclasses must implement - }, - - - /* Height Freezing - ------------------------------------------------------------------------------------------------------------------*/ - - - freezeHeight: function() { - this.calendar.freezeContentHeight(); - }, - - - thawHeight: function() { - this.calendar.thawContentHeight(); - }, - - - // Event High-level Rendering - // ----------------------------------------------------------------------------------------------------------------- - - - executeEventsRender: function(events) { - this.renderEvents(events); - this.isEventsRendered = true; - - this.onEventsRender(); - }, - - - executeEventsUnrender: function() { - this.onBeforeEventsUnrender(); - - if (this.destroyEvents) { - this.destroyEvents(); // TODO: deprecate - } - - this.unrenderEvents(); - this.isEventsRendered = false; - }, - - - // Event Rendering Triggers - // ----------------------------------------------------------------------------------------------------------------- - - - // Signals that all events have been rendered - onEventsRender: function() { - this.applyScreenState(); - - this.renderedEventSegEach(function(seg) { - this.publiclyTrigger('eventAfterRender', seg.event, seg.event, seg.el); - }); - this.publiclyTrigger('eventAfterAllRender'); - }, - - - // Signals that all event elements are about to be removed - onBeforeEventsUnrender: function() { - this.applyScreenState(); - - this.renderedEventSegEach(function(seg) { - this.publiclyTrigger('eventDestroy', seg.event, seg.event, seg.el); - }); - }, - - - applyScreenState: function() { - this.thawHeight(); - this.freezeHeight(); - this.applyQueuedScroll(); - }, - - - // Event Low-level Rendering - // ----------------------------------------------------------------------------------------------------------------- - - - // Renders the events onto the view. - renderEvents: function(events) { - // subclasses should implement - }, - - - // Removes event elements from the view. - unrenderEvents: function() { - // subclasses should implement - }, - - - // Event Rendering Utils - // ----------------------------------------------------------------------------------------------------------------- - - - // Given an event and the default element used for rendering, returns the element that should actually be used. - // Basically runs events and elements through the eventRender hook. - resolveEventEl: function(event, el) { - var custom = this.publiclyTrigger('eventRender', event, event, el); - - if (custom === false) { // means don't render at all - el = null; - } - else if (custom && custom !== true) { - el = $(custom); - } - - return el; - }, - - - // Hides all rendered event segments linked to the given event - showEvent: function(event) { - this.renderedEventSegEach(function(seg) { - seg.el.css('visibility', ''); - }, event); - }, - - - // Shows all rendered event segments linked to the given event - hideEvent: function(event) { - this.renderedEventSegEach(function(seg) { - seg.el.css('visibility', 'hidden'); - }, event); - }, - - - // Iterates through event segments that have been rendered (have an el). Goes through all by default. - // If the optional `event` argument is specified, only iterates through segments linked to that event. - // The `this` value of the callback function will be the view. - renderedEventSegEach: function(func, event) { - var segs = this.getEventSegs(); - var i; - - for (i = 0; i < segs.length; i++) { - if (!event || segs[i].event._id === event._id) { - if (segs[i].el) { - func.call(this, segs[i]); - } - } - } - }, - - - // Retrieves all the rendered segment objects for the view - getEventSegs: function() { - // subclasses must implement - return []; - }, - - - /* Event Drag-n-Drop - ------------------------------------------------------------------------------------------------------------------*/ - - - // Computes if the given event is allowed to be dragged by the user - isEventDraggable: function(event) { - return this.isEventStartEditable(event); - }, - - - isEventStartEditable: function(event) { - return firstDefined( - event.startEditable, - (event.source || {}).startEditable, - this.opt('eventStartEditable'), - this.isEventGenerallyEditable(event) - ); - }, - - - isEventGenerallyEditable: function(event) { - return firstDefined( - event.editable, - (event.source || {}).editable, - this.opt('editable') - ); - }, - - - // Must be called when an event in the view is dropped onto new location. - // `dropLocation` is an object that contains the new zoned start/end/allDay values for the event. - reportSegDrop: function(seg, dropLocation, largeUnit, el, ev) { - var calendar = this.calendar; - var mutateResult = calendar.mutateSeg(seg, dropLocation, largeUnit); - var undoFunc = function() { - mutateResult.undo(); - calendar.reportEventChange(); - }; - - this.triggerEventDrop(seg.event, mutateResult.dateDelta, undoFunc, el, ev); - calendar.reportEventChange(); // will rerender events - }, - - - // Triggers event-drop handlers that have subscribed via the API - triggerEventDrop: function(event, dateDelta, undoFunc, el, ev) { - this.publiclyTrigger('eventDrop', el[0], event, dateDelta, undoFunc, ev, {}); // {} = jqui dummy - }, - - - /* External Element Drag-n-Drop - ------------------------------------------------------------------------------------------------------------------*/ - - - // Must be called when an external element, via jQuery UI, has been dropped onto the calendar. - // `meta` is the parsed data that has been embedded into the dragging event. - // `dropLocation` is an object that contains the new zoned start/end/allDay values for the event. - reportExternalDrop: function(meta, dropLocation, el, ev, ui) { - var eventProps = meta.eventProps; - var eventInput; - var event; - - // Try to build an event object and render it. TODO: decouple the two - if (eventProps) { - eventInput = $.extend({}, eventProps, dropLocation); - event = this.calendar.renderEvent(eventInput, meta.stick)[0]; // renderEvent returns an array - } - - this.triggerExternalDrop(event, dropLocation, el, ev, ui); - }, - - - // Triggers external-drop handlers that have subscribed via the API - triggerExternalDrop: function(event, dropLocation, el, ev, ui) { - - // trigger 'drop' regardless of whether element represents an event - this.publiclyTrigger('drop', el[0], dropLocation.start, ev, ui); - - if (event) { - this.publiclyTrigger('eventReceive', null, event); // signal an external event landed - } - }, - - - /* Drag-n-Drop Rendering (for both events and external elements) - ------------------------------------------------------------------------------------------------------------------*/ - - - // Renders a visual indication of a event or external-element drag over the given drop zone. - // If an external-element, seg will be `null`. - // Must return elements used for any mock events. - renderDrag: function(dropLocation, seg) { - // subclasses must implement - }, - - - // Unrenders a visual indication of an event or external-element being dragged. - unrenderDrag: function() { - // subclasses must implement - }, - - - /* Event Resizing - ------------------------------------------------------------------------------------------------------------------*/ - - - // Computes if the given event is allowed to be resized from its starting edge - isEventResizableFromStart: function(event) { - return this.opt('eventResizableFromStart') && this.isEventResizable(event); - }, - - - // Computes if the given event is allowed to be resized from its ending edge - isEventResizableFromEnd: function(event) { - return this.isEventResizable(event); - }, - - - // Computes if the given event is allowed to be resized by the user at all - isEventResizable: function(event) { - var source = event.source || {}; - - return firstDefined( - event.durationEditable, - source.durationEditable, - this.opt('eventDurationEditable'), - event.editable, - source.editable, - this.opt('editable') - ); - }, - - - // Must be called when an event in the view has been resized to a new length - reportSegResize: function(seg, resizeLocation, largeUnit, el, ev) { - var calendar = this.calendar; - var mutateResult = calendar.mutateSeg(seg, resizeLocation, largeUnit); - var undoFunc = function() { - mutateResult.undo(); - calendar.reportEventChange(); - }; - - this.triggerEventResize(seg.event, mutateResult.durationDelta, undoFunc, el, ev); - calendar.reportEventChange(); // will rerender events - }, - - - // Triggers event-resize handlers that have subscribed via the API - triggerEventResize: function(event, durationDelta, undoFunc, el, ev) { - this.publiclyTrigger('eventResize', el[0], event, durationDelta, undoFunc, ev, {}); // {} = jqui dummy - }, - - - /* Selection (time range) - ------------------------------------------------------------------------------------------------------------------*/ - - - // Selects a date span on the view. `start` and `end` are both Moments. - // `ev` is the native mouse event that begin the interaction. - select: function(span, ev) { - this.unselect(ev); - this.renderSelection(span); - this.reportSelection(span, ev); - }, - - - // Renders a visual indication of the selection - renderSelection: function(span) { - // subclasses should implement - }, - - - // Called when a new selection is made. Updates internal state and triggers handlers. - reportSelection: function(span, ev) { - this.isSelected = true; - this.triggerSelect(span, ev); - }, - - - // Triggers handlers to 'select' - triggerSelect: function(span, ev) { - this.publiclyTrigger( - 'select', - null, - this.calendar.applyTimezone(span.start), // convert to calendar's tz for external API - this.calendar.applyTimezone(span.end), // " - ev - ); - }, - - - // Undoes a selection. updates in the internal state and triggers handlers. - // `ev` is the native mouse event that began the interaction. - unselect: function(ev) { - if (this.isSelected) { - this.isSelected = false; - if (this.destroySelection) { - this.destroySelection(); // TODO: deprecate - } - this.unrenderSelection(); - this.publiclyTrigger('unselect', null, ev); - } - }, - - - // Unrenders a visual indication of selection - unrenderSelection: function() { - // subclasses should implement - }, - - - /* Event Selection - ------------------------------------------------------------------------------------------------------------------*/ - - - selectEvent: function(event) { - if (!this.selectedEvent || this.selectedEvent !== event) { - this.unselectEvent(); - this.renderedEventSegEach(function(seg) { - seg.el.addClass('fc-selected'); - }, event); - this.selectedEvent = event; - } - }, - - - unselectEvent: function() { - if (this.selectedEvent) { - this.renderedEventSegEach(function(seg) { - seg.el.removeClass('fc-selected'); - }, this.selectedEvent); - this.selectedEvent = null; - } - }, - - - isEventSelected: function(event) { - // event references might change on refetchEvents(), while selectedEvent doesn't, - // so compare IDs - return this.selectedEvent && this.selectedEvent._id === event._id; - }, - - - /* Mouse / Touch Unselecting (time range & event unselection) - ------------------------------------------------------------------------------------------------------------------*/ - // TODO: move consistently to down/start or up/end? - // TODO: don't kill previous selection if touch scrolling - - - handleDocumentMousedown: function(ev) { - if (isPrimaryMouseButton(ev)) { - this.processUnselect(ev); - } - }, - - - processUnselect: function(ev) { - this.processRangeUnselect(ev); - this.processEventUnselect(ev); - }, - - - processRangeUnselect: function(ev) { - var ignore; - - // is there a time-range selection? - if (this.isSelected && this.opt('unselectAuto')) { - // only unselect if the clicked element is not identical to or inside of an 'unselectCancel' element - ignore = this.opt('unselectCancel'); - if (!ignore || !$(ev.target).closest(ignore).length) { - this.unselect(ev); - } - } - }, - - - processEventUnselect: function(ev) { - if (this.selectedEvent) { - if (!$(ev.target).closest('.fc-selected').length) { - this.unselectEvent(); - } - } - }, - - - /* Day Click - ------------------------------------------------------------------------------------------------------------------*/ - - - // Triggers handlers to 'dayClick' - // Span has start/end of the clicked area. Only the start is useful. - triggerDayClick: function(span, dayEl, ev) { - this.publiclyTrigger( - 'dayClick', - dayEl, - this.calendar.applyTimezone(span.start), // convert to calendar's timezone for external API - ev - ); - }, - - - /* Date Utils - ------------------------------------------------------------------------------------------------------------------*/ - - - // Returns the date range of the full days the given range visually appears to occupy. - // Returns a new range object. - computeDayRange: function(range) { - var startDay = range.start.clone().stripTime(); // the beginning of the day the range starts - var end = range.end; - var endDay = null; - var endTimeMS; - - if (end) { - endDay = end.clone().stripTime(); // the beginning of the day the range exclusively ends - endTimeMS = +end.time(); // # of milliseconds into `endDay` - - // If the end time is actually inclusively part of the next day and is equal to or - // beyond the next day threshold, adjust the end to be the exclusive end of `endDay`. - // Otherwise, leaving it as inclusive will cause it to exclude `endDay`. - if (endTimeMS && endTimeMS >= this.nextDayThreshold) { - endDay.add(1, 'days'); - } - } - - // If no end was specified, or if it is within `startDay` but not past nextDayThreshold, - // assign the default duration of one day. - if (!end || endDay <= startDay) { - endDay = startDay.clone().add(1, 'days'); - } - - return { start: startDay, end: endDay }; - }, - - - // Does the given event visually appear to occupy more than one day? - isMultiDayEvent: function(event) { - var range = this.computeDayRange(event); // event is range-ish - - return range.end.diff(range.start, 'days') > 1; - } - -}); - - -View.watch('displayingDates', [ 'dateProfile' ], function(deps) { - this.requestDateRender(deps.dateProfile); -}, function() { - this.requestDateUnrender(); -}); - - -View.watch('initialEvents', [ 'dateProfile' ], function(deps) { - return this.fetchInitialEvents(deps.dateProfile); -}); - - -View.watch('bindingEvents', [ 'initialEvents' ], function(deps) { - this.setEvents(deps.initialEvents); - this.bindEventChanges(); -}, function() { - this.unbindEventChanges(); - this.unsetEvents(); -}); - - -View.watch('displayingEvents', [ 'displayingDates', 'hasEvents' ], function() { - this.requestEventsRender(this.get('currentEvents')); // if there were event mutations after initialEvents -}, function() { - this.requestEventsUnrender(); -}); - -;; - -View.mixin({ - - // range the view is formally responsible for. - // for example, a month view might have 1st-31st, excluding padded dates - currentRange: null, - currentRangeUnit: null, // name of largest unit being displayed, like "month" or "week" - - // date range with a rendered skeleton - // includes not-active days that need some sort of DOM - renderRange: null, - - // dates that display events and accept drag-n-drop - activeRange: null, - - // constraint for where prev/next operations can go and where events can be dragged/resized to. - // an object with optional start and end properties. - validRange: null, - - // how far the current date will move for a prev/next operation - dateIncrement: null, - - minTime: null, // Duration object that denotes the first visible time of any given day - maxTime: null, // Duration object that denotes the exclusive visible end time of any given day - usesMinMaxTime: false, // whether minTime/maxTime will affect the activeRange. Views must opt-in. - - // DEPRECATED - start: null, // use activeRange.start - end: null, // use activeRange.end - intervalStart: null, // use currentRange.start - intervalEnd: null, // use currentRange.end - - - /* Date Range Computation - ------------------------------------------------------------------------------------------------------------------*/ - - - setDateProfileForRendering: function(dateProfile) { - this.currentRange = dateProfile.currentRange; - this.currentRangeUnit = dateProfile.currentRangeUnit; - this.renderRange = dateProfile.renderRange; - this.activeRange = dateProfile.activeRange; - this.validRange = dateProfile.validRange; - this.dateIncrement = dateProfile.dateIncrement; - this.minTime = dateProfile.minTime; - this.maxTime = dateProfile.maxTime; - - // DEPRECATED, but we need to keep it updated - this.start = dateProfile.activeRange.start; - this.end = dateProfile.activeRange.end; - this.intervalStart = dateProfile.currentRange.start; - this.intervalEnd = dateProfile.currentRange.end; - }, - - - // Builds a structure with info about what the dates/ranges will be for the "prev" view. - buildPrevDateProfile: function(date) { - var prevDate = date.clone().startOf(this.currentRangeUnit).subtract(this.dateIncrement); - - return this.buildDateProfile(prevDate, -1); - }, - - - // Builds a structure with info about what the dates/ranges will be for the "next" view. - buildNextDateProfile: function(date) { - var nextDate = date.clone().startOf(this.currentRangeUnit).add(this.dateIncrement); - - return this.buildDateProfile(nextDate, 1); - }, - - - // Builds a structure holding dates/ranges for rendering around the given date. - // Optional direction param indicates whether the date is being incremented/decremented - // from its previous value. decremented = -1, incremented = 1 (default). - buildDateProfile: function(date, direction, forceToValid) { - var validRange = this.buildValidRange(); - var minTime = null; - var maxTime = null; - var currentInfo; - var renderRange; - var activeRange; - var isValid; - - if (forceToValid) { - date = constrainDate(date, validRange); - } - - currentInfo = this.buildCurrentRangeInfo(date, direction); - renderRange = this.buildRenderRange(currentInfo.range, currentInfo.unit); - activeRange = cloneRange(renderRange); - - if (!this.opt('showNonCurrentDates')) { - activeRange = constrainRange(activeRange, currentInfo.range); - } - - minTime = moment.duration(this.opt('minTime')); - maxTime = moment.duration(this.opt('maxTime')); - this.adjustActiveRange(activeRange, minTime, maxTime); - - activeRange = constrainRange(activeRange, validRange); - date = constrainDate(date, activeRange); - - // it's invalid if the originally requested date is not contained, - // or if the range is completely outside of the valid range. - isValid = doRangesIntersect(currentInfo.range, validRange); - - return { - validRange: validRange, - currentRange: currentInfo.range, - currentRangeUnit: currentInfo.unit, - activeRange: activeRange, - renderRange: renderRange, - minTime: minTime, - maxTime: maxTime, - isValid: isValid, - date: date, - dateIncrement: this.buildDateIncrement(currentInfo.duration) - // pass a fallback (might be null) ^ - }; - }, - - - // Builds an object with optional start/end properties. - // Indicates the minimum/maximum dates to display. - buildValidRange: function() { - return this.getRangeOption('validRange', this.calendar.getNow()) || {}; - }, - - - // Builds a structure with info about the "current" range, the range that is - // highlighted as being the current month for example. - // See buildDateProfile for a description of `direction`. - // Guaranteed to have `range` and `unit` properties. `duration` is optional. - buildCurrentRangeInfo: function(date, direction) { - var duration = null; - var unit = null; - var range = null; - var dayCount; - - if (this.viewSpec.duration) { - duration = this.viewSpec.duration; - unit = this.viewSpec.durationUnit; - range = this.buildRangeFromDuration(date, direction, duration, unit); - } - else if ((dayCount = this.opt('dayCount'))) { - unit = 'day'; - range = this.buildRangeFromDayCount(date, direction, dayCount); - } - else if ((range = this.buildCustomVisibleRange(date))) { - unit = computeGreatestUnit(range.start, range.end); - } - else { - duration = this.getFallbackDuration(); - unit = computeGreatestUnit(duration); - range = this.buildRangeFromDuration(date, direction, duration, unit); - } - - this.normalizeCurrentRange(range, unit); // modifies in-place - - return { duration: duration, unit: unit, range: range }; - }, - - - getFallbackDuration: function() { - return moment.duration({ days: 1 }); - }, - - - // If the range has day units or larger, remove times. Otherwise, ensure times. - normalizeCurrentRange: function(range, unit) { - - if (/^(year|month|week|day)$/.test(unit)) { // whole-days? - range.start.stripTime(); - range.end.stripTime(); - } - else { // needs to have a time? - if (!range.start.hasTime()) { - range.start.time(0); // give 00:00 time - } - if (!range.end.hasTime()) { - range.end.time(0); // give 00:00 time - } - } - }, - - - // Mutates the given activeRange to have time values (un-ambiguate) - // if the minTime or maxTime causes the range to expand. - // TODO: eventually activeRange should *always* have times. - adjustActiveRange: function(range, minTime, maxTime) { - var hasSpecialTimes = false; - - if (this.usesMinMaxTime) { - - if (minTime < 0) { - range.start.time(0).add(minTime); - hasSpecialTimes = true; - } - - if (maxTime > 24 * 60 * 60 * 1000) { // beyond 24 hours? - range.end.time(maxTime - (24 * 60 * 60 * 1000)); - hasSpecialTimes = true; - } - - if (hasSpecialTimes) { - if (!range.start.hasTime()) { - range.start.time(0); - } - if (!range.end.hasTime()) { - range.end.time(0); - } - } - } - }, - - - // Builds the "current" range when it is specified as an explicit duration. - // `unit` is the already-computed computeGreatestUnit value of duration. - buildRangeFromDuration: function(date, direction, duration, unit) { - var alignment = this.opt('dateAlignment'); - var start = date.clone(); - var end; - var dateIncrementInput; - var dateIncrementDuration; - - // if the view displays a single day or smaller - if (duration.as('days') <= 1) { - if (this.isHiddenDay(start)) { - start = this.skipHiddenDays(start, direction); - start.startOf('day'); - } - } - - // compute what the alignment should be - if (!alignment) { - dateIncrementInput = this.opt('dateIncrement'); - - if (dateIncrementInput) { - dateIncrementDuration = moment.duration(dateIncrementInput); - - // use the smaller of the two units - if (dateIncrementDuration < duration) { - alignment = computeDurationGreatestUnit(dateIncrementDuration, dateIncrementInput); - } - else { - alignment = unit; - } - } - else { - alignment = unit; - } - } - - start.startOf(alignment); - end = start.clone().add(duration); - - return { start: start, end: end }; - }, - - - // Builds the "current" range when a dayCount is specified. - buildRangeFromDayCount: function(date, direction, dayCount) { - var customAlignment = this.opt('dateAlignment'); - var runningCount = 0; - var start = date.clone(); - var end; - - if (customAlignment) { - start.startOf(customAlignment); - } - - start.startOf('day'); - start = this.skipHiddenDays(start, direction); - - end = start.clone(); - do { - end.add(1, 'day'); - if (!this.isHiddenDay(end)) { - runningCount++; - } - } while (runningCount < dayCount); - - return { start: start, end: end }; - }, - - - // Builds a normalized range object for the "visible" range, - // which is a way to define the currentRange and activeRange at the same time. - buildCustomVisibleRange: function(date) { - var visibleRange = this.getRangeOption( - 'visibleRange', - this.calendar.moment(date) // correct zone. also generates new obj that avoids mutations - ); - - if (visibleRange && (!visibleRange.start || !visibleRange.end)) { - return null; - } - - return visibleRange; - }, - - - // Computes the range that will represent the element/cells for *rendering*, - // but which may have voided days/times. - buildRenderRange: function(currentRange, currentRangeUnit) { - // cut off days in the currentRange that are hidden - return this.trimHiddenDays(currentRange); - }, - - - // Compute the duration value that should be added/substracted to the current date - // when a prev/next operation happens. - buildDateIncrement: function(fallback) { - var dateIncrementInput = this.opt('dateIncrement'); - var customAlignment; - - if (dateIncrementInput) { - return moment.duration(dateIncrementInput); - } - else if ((customAlignment = this.opt('dateAlignment'))) { - return moment.duration(1, customAlignment); - } - else if (fallback) { - return fallback; - } - else { - return moment.duration({ days: 1 }); - } - }, - - - // Remove days from the beginning and end of the range that are computed as hidden. - trimHiddenDays: function(inputRange) { - return { - start: this.skipHiddenDays(inputRange.start), - end: this.skipHiddenDays(inputRange.end, -1, true) // exclusively move backwards - }; - }, - - - // Compute the number of the give units in the "current" range. - // Will return a floating-point number. Won't round. - currentRangeAs: function(unit) { - var currentRange = this.currentRange; - return currentRange.end.diff(currentRange.start, unit, true); - }, - - - // Arguments after name will be forwarded to a hypothetical function value - // WARNING: passed-in arguments will be given to generator functions as-is and can cause side-effects. - // Always clone your objects if you fear mutation. - getRangeOption: function(name) { - var val = this.opt(name); - - if (typeof val === 'function') { - val = val.apply( - null, - Array.prototype.slice.call(arguments, 1) - ); - } - - if (val) { - return this.calendar.parseRange(val); - } - }, - - - /* Hidden Days - ------------------------------------------------------------------------------------------------------------------*/ - - - // Initializes internal variables related to calculating hidden days-of-week - initHiddenDays: function() { - var hiddenDays = this.opt('hiddenDays') || []; // array of day-of-week indices that are hidden - var isHiddenDayHash = []; // is the day-of-week hidden? (hash with day-of-week-index -> bool) - var dayCnt = 0; - var i; - - if (this.opt('weekends') === false) { - hiddenDays.push(0, 6); // 0=sunday, 6=saturday - } - - for (i = 0; i < 7; i++) { - if ( - !(isHiddenDayHash[i] = $.inArray(i, hiddenDays) !== -1) - ) { - dayCnt++; - } - } - - if (!dayCnt) { - throw 'invalid hiddenDays'; // all days were hidden? bad. - } - - this.isHiddenDayHash = isHiddenDayHash; - }, - - - // Is the current day hidden? - // `day` is a day-of-week index (0-6), or a Moment - isHiddenDay: function(day) { - if (moment.isMoment(day)) { - day = day.day(); - } - return this.isHiddenDayHash[day]; - }, - - - // Incrementing the current day until it is no longer a hidden day, returning a copy. - // DOES NOT CONSIDER validRange! - // If the initial value of `date` is not a hidden day, don't do anything. - // Pass `isExclusive` as `true` if you are dealing with an end date. - // `inc` defaults to `1` (increment one day forward each time) - skipHiddenDays: function(date, inc, isExclusive) { - var out = date.clone(); - inc = inc || 1; - while ( - this.isHiddenDayHash[(out.day() + (isExclusive ? inc : 0) + 7) % 7] - ) { - out.add(inc, 'days'); - } - return out; - } - -}); - -;; - -/* -Embodies a div that has potential scrollbars -*/ -var Scroller = FC.Scroller = Class.extend({ - - el: null, // the guaranteed outer element - scrollEl: null, // the element with the scrollbars - overflowX: null, - overflowY: null, - - - constructor: function(options) { - options = options || {}; - this.overflowX = options.overflowX || options.overflow || 'auto'; - this.overflowY = options.overflowY || options.overflow || 'auto'; - }, - - - render: function() { - this.el = this.renderEl(); - this.applyOverflow(); - }, - - - renderEl: function() { - return (this.scrollEl = $('<div class="fc-scroller"></div>')); - }, - - - // sets to natural height, unlocks overflow - clear: function() { - this.setHeight('auto'); - this.applyOverflow(); - }, - - - destroy: function() { - this.el.remove(); - }, - - - // Overflow - // ----------------------------------------------------------------------------------------------------------------- - - - applyOverflow: function() { - this.scrollEl.css({ - 'overflow-x': this.overflowX, - 'overflow-y': this.overflowY - }); - }, - - - // Causes any 'auto' overflow values to resolves to 'scroll' or 'hidden'. - // Useful for preserving scrollbar widths regardless of future resizes. - // Can pass in scrollbarWidths for optimization. - lockOverflow: function(scrollbarWidths) { - var overflowX = this.overflowX; - var overflowY = this.overflowY; - - scrollbarWidths = scrollbarWidths || this.getScrollbarWidths(); - - if (overflowX === 'auto') { - overflowX = ( - scrollbarWidths.top || scrollbarWidths.bottom || // horizontal scrollbars? - // OR scrolling pane with massless scrollbars? - this.scrollEl[0].scrollWidth - 1 > this.scrollEl[0].clientWidth - // subtract 1 because of IE off-by-one issue - ) ? 'scroll' : 'hidden'; - } - - if (overflowY === 'auto') { - overflowY = ( - scrollbarWidths.left || scrollbarWidths.right || // vertical scrollbars? - // OR scrolling pane with massless scrollbars? - this.scrollEl[0].scrollHeight - 1 > this.scrollEl[0].clientHeight - // subtract 1 because of IE off-by-one issue - ) ? 'scroll' : 'hidden'; - } - - this.scrollEl.css({ 'overflow-x': overflowX, 'overflow-y': overflowY }); - }, - - - // Getters / Setters - // ----------------------------------------------------------------------------------------------------------------- - - - setHeight: function(height) { - this.scrollEl.height(height); - }, - - - getScrollTop: function() { - return this.scrollEl.scrollTop(); - }, - - - setScrollTop: function(top) { - this.scrollEl.scrollTop(top); - }, - - - getClientWidth: function() { - return this.scrollEl[0].clientWidth; - }, - - - getClientHeight: function() { - return this.scrollEl[0].clientHeight; - }, - - - getScrollbarWidths: function() { - return getScrollbarWidths(this.scrollEl); - } - -}); - -;; -function Iterator(items) { - this.items = items || []; -} - - -/* Calls a method on every item passing the arguments through */ -Iterator.prototype.proxyCall = function(methodName) { - var args = Array.prototype.slice.call(arguments, 1); - var results = []; - - this.items.forEach(function(item) { - results.push(item[methodName].apply(item, args)); - }); - - return results; -}; - -;; - -/* Toolbar with buttons and title -----------------------------------------------------------------------------------------------------------------------*/ - -function Toolbar(calendar, toolbarOptions) { - var t = this; - - // exports - t.setToolbarOptions = setToolbarOptions; - t.render = render; - t.removeElement = removeElement; - t.updateTitle = updateTitle; - t.activateButton = activateButton; - t.deactivateButton = deactivateButton; - t.disableButton = disableButton; - t.enableButton = enableButton; - t.getViewsWithButtons = getViewsWithButtons; - t.el = null; // mirrors local `el` - - // locals - var el; - var viewsWithButtons = []; - var tm; - - // method to update toolbar-specific options, not calendar-wide options - function setToolbarOptions(newToolbarOptions) { - toolbarOptions = newToolbarOptions; - } - - // can be called repeatedly and will rerender - function render() { - var sections = toolbarOptions.layout; - - tm = calendar.opt('theme') ? 'ui' : 'fc'; - - if (sections) { - if (!el) { - el = this.el = $("<div class='fc-toolbar "+ toolbarOptions.extraClasses + "'/>"); - } - else { - el.empty(); - } - el.append(renderSection('left')) - .append(renderSection('right')) - .append(renderSection('center')) - .append('<div class="fc-clear"/>'); - } - else { - removeElement(); - } - } - - - function removeElement() { - if (el) { - el.remove(); - el = t.el = null; - } - } - - - function renderSection(position) { - var sectionEl = $('<div class="fc-' + position + '"/>'); - var buttonStr = toolbarOptions.layout[position]; - var calendarCustomButtons = calendar.opt('customButtons') || {}; - var calendarButtonText = calendar.opt('buttonText') || {}; - - if (buttonStr) { - $.each(buttonStr.split(' '), function(i) { - var groupChildren = $(); - var isOnlyButtons = true; - var groupEl; - - $.each(this.split(','), function(j, buttonName) { - var customButtonProps; - var viewSpec; - var buttonClick; - var overrideText; // text explicitly set by calendar's constructor options. overcomes icons - var defaultText; - var themeIcon; - var normalIcon; - var innerHtml; - var classes; - var button; // the element - - if (buttonName == 'title') { - groupChildren = groupChildren.add($('<h2> </h2>')); // we always want it to take up height - isOnlyButtons = false; - } - else { - if ((customButtonProps = calendarCustomButtons[buttonName])) { - buttonClick = function(ev) { - if (customButtonProps.click) { - customButtonProps.click.call(button[0], ev); - } - }; - overrideText = ''; // icons will override text - defaultText = customButtonProps.text; - } - else if ((viewSpec = calendar.getViewSpec(buttonName))) { - buttonClick = function() { - calendar.changeView(buttonName); - }; - viewsWithButtons.push(buttonName); - overrideText = viewSpec.buttonTextOverride; - defaultText = viewSpec.buttonTextDefault; - } - else if (calendar[buttonName]) { // a calendar method - buttonClick = function() { - calendar[buttonName](); - }; - overrideText = (calendar.overrides.buttonText || {})[buttonName]; - defaultText = calendarButtonText[buttonName]; // everything else is considered default - } - - if (buttonClick) { - - themeIcon = - customButtonProps ? - customButtonProps.themeIcon : - calendar.opt('themeButtonIcons')[buttonName]; - - normalIcon = - customButtonProps ? - customButtonProps.icon : - calendar.opt('buttonIcons')[buttonName]; - - if (overrideText) { - innerHtml = htmlEscape(overrideText); - } - else if (themeIcon && calendar.opt('theme')) { - innerHtml = "<span class='ui-icon ui-icon-" + themeIcon + "'></span>"; - } - else if (normalIcon && !calendar.opt('theme')) { - innerHtml = "<span class='fc-icon fc-icon-" + normalIcon + "'></span>"; - } - else { - innerHtml = htmlEscape(defaultText); - } - - classes = [ - 'fc-' + buttonName + '-button', - tm + '-button', - tm + '-state-default' - ]; - - button = $( // type="button" so that it doesn't submit a form - '<button type="button" class="' + classes.join(' ') + '">' + - innerHtml + - '</button>' - ) - .click(function(ev) { - // don't process clicks for disabled buttons - if (!button.hasClass(tm + '-state-disabled')) { - - buttonClick(ev); - - // after the click action, if the button becomes the "active" tab, or disabled, - // it should never have a hover class, so remove it now. - if ( - button.hasClass(tm + '-state-active') || - button.hasClass(tm + '-state-disabled') - ) { - button.removeClass(tm + '-state-hover'); - } - } - }) - .mousedown(function() { - // the *down* effect (mouse pressed in). - // only on buttons that are not the "active" tab, or disabled - button - .not('.' + tm + '-state-active') - .not('.' + tm + '-state-disabled') - .addClass(tm + '-state-down'); - }) - .mouseup(function() { - // undo the *down* effect - button.removeClass(tm + '-state-down'); - }) - .hover( - function() { - // the *hover* effect. - // only on buttons that are not the "active" tab, or disabled - button - .not('.' + tm + '-state-active') - .not('.' + tm + '-state-disabled') - .addClass(tm + '-state-hover'); - }, - function() { - // undo the *hover* effect - button - .removeClass(tm + '-state-hover') - .removeClass(tm + '-state-down'); // if mouseleave happens before mouseup - } - ); - - groupChildren = groupChildren.add(button); - } - } - }); - - if (isOnlyButtons) { - groupChildren - .first().addClass(tm + '-corner-left').end() - .last().addClass(tm + '-corner-right').end(); - } - - if (groupChildren.length > 1) { - groupEl = $('<div/>'); - if (isOnlyButtons) { - groupEl.addClass('fc-button-group'); - } - groupEl.append(groupChildren); - sectionEl.append(groupEl); - } - else { - sectionEl.append(groupChildren); // 1 or 0 children - } - }); - } - - return sectionEl; - } - - - function updateTitle(text) { - if (el) { - el.find('h2').text(text); - } - } - - - function activateButton(buttonName) { - if (el) { - el.find('.fc-' + buttonName + '-button') - .addClass(tm + '-state-active'); - } - } - - - function deactivateButton(buttonName) { - if (el) { - el.find('.fc-' + buttonName + '-button') - .removeClass(tm + '-state-active'); - } - } - - - function disableButton(buttonName) { - if (el) { - el.find('.fc-' + buttonName + '-button') - .prop('disabled', true) - .addClass(tm + '-state-disabled'); - } - } - - - function enableButton(buttonName) { - if (el) { - el.find('.fc-' + buttonName + '-button') - .prop('disabled', false) - .removeClass(tm + '-state-disabled'); - } - } - - - function getViewsWithButtons() { - return viewsWithButtons; - } - -} - -;; - -var Calendar = FC.Calendar = Class.extend(EmitterMixin, { - - view: null, // current View object - viewsByType: null, // holds all instantiated view instances, current or not - currentDate: null, // unzoned moment. private (public API should use getDate instead) - loadingLevel: 0, // number of simultaneous loading tasks - - - constructor: function(el, overrides) { - - // declare the current calendar instance relies on GlobalEmitter. needed for garbage collection. - // unneeded() is called in destroy. - GlobalEmitter.needed(); - - this.el = el; - this.viewsByType = {}; - this.viewSpecCache = {}; - - this.initOptionsInternals(overrides); - this.initMomentInternals(); // needs to happen after options hash initialized - this.initCurrentDate(); - - EventManager.call(this); // needs options immediately - this.initialize(); - }, - - - // Subclasses can override this for initialization logic after the constructor has been called - initialize: function() { - }, - - - // Public API - // ----------------------------------------------------------------------------------------------------------------- - - - getCalendar: function() { - return this; - }, - - - getView: function() { - return this.view; - }, - - - publiclyTrigger: function(name, thisObj) { - var args = Array.prototype.slice.call(arguments, 2); - var optHandler = this.opt(name); - - thisObj = thisObj || this.el[0]; - this.triggerWith(name, thisObj, args); // Emitter's method - - if (optHandler) { - return optHandler.apply(thisObj, args); - } - }, - - - // View - // ----------------------------------------------------------------------------------------------------------------- - - - // Given a view name for a custom view or a standard view, creates a ready-to-go View object - instantiateView: function(viewType) { - var spec = this.getViewSpec(viewType); - - return new spec['class'](this, spec); - }, - - - // Returns a boolean about whether the view is okay to instantiate at some point - isValidViewType: function(viewType) { - return Boolean(this.getViewSpec(viewType)); - }, - - - changeView: function(viewName, dateOrRange) { - - if (dateOrRange) { - - if (dateOrRange.start && dateOrRange.end) { // a range - this.recordOptionOverrides({ // will not rerender - visibleRange: dateOrRange - }); - } - else { // a date - this.currentDate = this.moment(dateOrRange).stripZone(); // just like gotoDate - } - } - - this.renderView(viewName); - }, - - - // Forces navigation to a view for the given date. - // `viewType` can be a specific view name or a generic one like "week" or "day". - zoomTo: function(newDate, viewType) { - var spec; - - viewType = viewType || 'day'; // day is default zoom - spec = this.getViewSpec(viewType) || this.getUnitViewSpec(viewType); - - this.currentDate = newDate.clone(); - this.renderView(spec ? spec.type : null); - }, - - - // Current Date - // ----------------------------------------------------------------------------------------------------------------- - - - initCurrentDate: function() { - var defaultDateInput = this.opt('defaultDate'); - - // compute the initial ambig-timezone date - if (defaultDateInput != null) { - this.currentDate = this.moment(defaultDateInput).stripZone(); - } - else { - this.currentDate = this.getNow(); // getNow already returns unzoned - } - }, - - - prev: function() { - var prevInfo = this.view.buildPrevDateProfile(this.currentDate); - - if (prevInfo.isValid) { - this.currentDate = prevInfo.date; - this.renderView(); - } - }, - - - next: function() { - var nextInfo = this.view.buildNextDateProfile(this.currentDate); - - if (nextInfo.isValid) { - this.currentDate = nextInfo.date; - this.renderView(); - } - }, - - - prevYear: function() { - this.currentDate.add(-1, 'years'); - this.renderView(); - }, - - - nextYear: function() { - this.currentDate.add(1, 'years'); - this.renderView(); - }, - - - today: function() { - this.currentDate = this.getNow(); // should deny like prev/next? - this.renderView(); - }, - - - gotoDate: function(zonedDateInput) { - this.currentDate = this.moment(zonedDateInput).stripZone(); - this.renderView(); - }, - - - incrementDate: function(delta) { - this.currentDate.add(moment.duration(delta)); - this.renderView(); - }, - - - // for external API - getDate: function() { - return this.applyTimezone(this.currentDate); // infuse the calendar's timezone - }, - - - // Loading Triggering - // ----------------------------------------------------------------------------------------------------------------- - - - // Should be called when any type of async data fetching begins - pushLoading: function() { - if (!(this.loadingLevel++)) { - this.publiclyTrigger('loading', null, true, this.view); - } - }, - - - // Should be called when any type of async data fetching completes - popLoading: function() { - if (!(--this.loadingLevel)) { - this.publiclyTrigger('loading', null, false, this.view); - } - }, - - - // Selection - // ----------------------------------------------------------------------------------------------------------------- - - - // this public method receives start/end dates in any format, with any timezone - select: function(zonedStartInput, zonedEndInput) { - this.view.select( - this.buildSelectSpan.apply(this, arguments) - ); - }, - - - unselect: function() { // safe to be called before renderView - if (this.view) { - this.view.unselect(); - } - }, - - - // Given arguments to the select method in the API, returns a span (unzoned start/end and other info) - buildSelectSpan: function(zonedStartInput, zonedEndInput) { - var start = this.moment(zonedStartInput).stripZone(); - var end; - - if (zonedEndInput) { - end = this.moment(zonedEndInput).stripZone(); - } - else if (start.hasTime()) { - end = start.clone().add(this.defaultTimedEventDuration); - } - else { - end = start.clone().add(this.defaultAllDayEventDuration); - } - - return { start: start, end: end }; - }, - - - // Misc - // ----------------------------------------------------------------------------------------------------------------- - - - // will return `null` if invalid range - parseRange: function(rangeInput) { - var start = null; - var end = null; - - if (rangeInput.start) { - start = this.moment(rangeInput.start).stripZone(); - } - - if (rangeInput.end) { - end = this.moment(rangeInput.end).stripZone(); - } - - if (!start && !end) { - return null; - } - - if (start && end && end.isBefore(start)) { - return null; - } - - return { start: start, end: end }; - }, - - - rerenderEvents: function() { // API method. destroys old events if previously rendered. - if (this.elementVisible()) { - this.reportEventChange(); // will re-trasmit events to the view, causing a rerender - } - } - -}); - -;; -/* -Options binding/triggering system. -*/ -Calendar.mixin({ - - dirDefaults: null, // option defaults related to LTR or RTL - localeDefaults: null, // option defaults related to current locale - overrides: null, // option overrides given to the fullCalendar constructor - dynamicOverrides: null, // options set with dynamic setter method. higher precedence than view overrides. - optionsModel: null, // all defaults combined with overrides - - - initOptionsInternals: function(overrides) { - this.overrides = $.extend({}, overrides); // make a copy - this.dynamicOverrides = {}; - this.optionsModel = new Model(); - - this.populateOptionsHash(); - }, - - - // public getter/setter - option: function(name, value) { - var newOptionHash; - - if (typeof name === 'string') { - if (value === undefined) { // getter - return this.optionsModel.get(name); - } - else { // setter for individual option - newOptionHash = {}; - newOptionHash[name] = value; - this.setOptions(newOptionHash); - } - } - else if (typeof name === 'object') { // compound setter with object input - this.setOptions(name); - } - }, - - - // private getter - opt: function(name) { - return this.optionsModel.get(name); - }, - - - setOptions: function(newOptionHash) { - var optionCnt = 0; - var optionName; - - this.recordOptionOverrides(newOptionHash); - - for (optionName in newOptionHash) { - optionCnt++; - } - - // special-case handling of single option change. - // if only one option change, `optionName` will be its name. - if (optionCnt === 1) { - if (optionName === 'height' || optionName === 'contentHeight' || optionName === 'aspectRatio') { - this.updateSize(true); // true = allow recalculation of height - return; - } - else if (optionName === 'defaultDate') { - return; // can't change date this way. use gotoDate instead - } - else if (optionName === 'businessHours') { - if (this.view) { - this.view.unrenderBusinessHours(); - this.view.renderBusinessHours(); - } - return; - } - else if (optionName === 'timezone') { - this.rezoneArrayEventSources(); - this.refetchEvents(); - return; - } - } - - // catch-all. rerender the header and footer and rebuild/rerender the current view - this.renderHeader(); - this.renderFooter(); - - // even non-current views will be affected by this option change. do before rerender - // TODO: detangle - this.viewsByType = {}; - - this.reinitView(); - }, - - - // Computes the flattened options hash for the calendar and assigns to `this.options`. - // Assumes this.overrides and this.dynamicOverrides have already been initialized. - populateOptionsHash: function() { - var locale, localeDefaults; - var isRTL, dirDefaults; - var rawOptions; - - locale = firstDefined( // explicit locale option given? - this.dynamicOverrides.locale, - this.overrides.locale - ); - localeDefaults = localeOptionHash[locale]; - if (!localeDefaults) { // explicit locale option not given or invalid? - locale = Calendar.defaults.locale; - localeDefaults = localeOptionHash[locale] || {}; - } - - isRTL = firstDefined( // based on options computed so far, is direction RTL? - this.dynamicOverrides.isRTL, - this.overrides.isRTL, - localeDefaults.isRTL, - Calendar.defaults.isRTL - ); - dirDefaults = isRTL ? Calendar.rtlDefaults : {}; - - this.dirDefaults = dirDefaults; - this.localeDefaults = localeDefaults; - - rawOptions = mergeOptions([ // merge defaults and overrides. lowest to highest precedence - Calendar.defaults, // global defaults - dirDefaults, - localeDefaults, - this.overrides, - this.dynamicOverrides - ]); - populateInstanceComputableOptions(rawOptions); // fill in gaps with computed options - - this.optionsModel.reset(rawOptions); - }, - - - // stores the new options internally, but does not rerender anything. - recordOptionOverrides: function(newOptionHash) { - var optionName; - - for (optionName in newOptionHash) { - this.dynamicOverrides[optionName] = newOptionHash[optionName]; - } - - this.viewSpecCache = {}; // the dynamic override invalidates the options in this cache, so just clear it - this.populateOptionsHash(); // this.options needs to be recomputed after the dynamic override - } - -}); - -;; - -Calendar.mixin({ - - defaultAllDayEventDuration: null, - defaultTimedEventDuration: null, - localeData: null, - - - initMomentInternals: function() { - var _this = this; - - this.defaultAllDayEventDuration = moment.duration(this.opt('defaultAllDayEventDuration')); - this.defaultTimedEventDuration = moment.duration(this.opt('defaultTimedEventDuration')); - - // Called immediately, and when any of the options change. - // Happens before any internal objects rebuild or rerender, because this is very core. - this.optionsModel.watch('buildingMomentLocale', [ - '?locale', '?monthNames', '?monthNamesShort', '?dayNames', '?dayNamesShort', - '?firstDay', '?weekNumberCalculation' - ], function(opts) { - var weekNumberCalculation = opts.weekNumberCalculation; - var firstDay = opts.firstDay; - var _week; - - // normalize - if (weekNumberCalculation === 'iso') { - weekNumberCalculation = 'ISO'; // normalize - } - - var localeData = createObject( // make a cheap copy - getMomentLocaleData(opts.locale) // will fall back to en - ); - - if (opts.monthNames) { - localeData._months = opts.monthNames; - } - if (opts.monthNamesShort) { - localeData._monthsShort = opts.monthNamesShort; - } - if (opts.dayNames) { - localeData._weekdays = opts.dayNames; - } - if (opts.dayNamesShort) { - localeData._weekdaysShort = opts.dayNamesShort; - } - - if (firstDay == null && weekNumberCalculation === 'ISO') { - firstDay = 1; - } - if (firstDay != null) { - _week = createObject(localeData._week); // _week: { dow: # } - _week.dow = firstDay; - localeData._week = _week; - } - - if ( // whitelist certain kinds of input - weekNumberCalculation === 'ISO' || - weekNumberCalculation === 'local' || - typeof weekNumberCalculation === 'function' - ) { - localeData._fullCalendar_weekCalc = weekNumberCalculation; // moment-ext will know what to do with it - } - - _this.localeData = localeData; - - // If the internal current date object already exists, move to new locale. - // We do NOT need to do this technique for event dates, because this happens when converting to "segments". - if (_this.currentDate) { - _this.localizeMoment(_this.currentDate); // sets to localeData - } - }); - }, - - - // Builds a moment using the settings of the current calendar: timezone and locale. - // Accepts anything the vanilla moment() constructor accepts. - moment: function() { - var mom; - - if (this.opt('timezone') === 'local') { - mom = FC.moment.apply(null, arguments); - - // Force the moment to be local, because FC.moment doesn't guarantee it. - if (mom.hasTime()) { // don't give ambiguously-timed moments a local zone - mom.local(); - } - } - else if (this.opt('timezone') === 'UTC') { - mom = FC.moment.utc.apply(null, arguments); // process as UTC - } - else { - mom = FC.moment.parseZone.apply(null, arguments); // let the input decide the zone - } - - this.localizeMoment(mom); // TODO - - return mom; - }, - - - // Updates the given moment's locale settings to the current calendar locale settings. - localizeMoment: function(mom) { - mom._locale = this.localeData; - }, - - - // Returns a boolean about whether or not the calendar knows how to calculate - // the timezone offset of arbitrary dates in the current timezone. - getIsAmbigTimezone: function() { - return this.opt('timezone') !== 'local' && this.opt('timezone') !== 'UTC'; - }, - - - // Returns a copy of the given date in the current timezone. Has no effect on dates without times. - applyTimezone: function(date) { - if (!date.hasTime()) { - return date.clone(); - } - - var zonedDate = this.moment(date.toArray()); - var timeAdjust = date.time() - zonedDate.time(); - var adjustedZonedDate; - - // Safari sometimes has problems with this coersion when near DST. Adjust if necessary. (bug #2396) - if (timeAdjust) { // is the time result different than expected? - adjustedZonedDate = zonedDate.clone().add(timeAdjust); // add milliseconds - if (date.time() - adjustedZonedDate.time() === 0) { // does it match perfectly now? - zonedDate = adjustedZonedDate; - } - } - - return zonedDate; - }, - - - // Returns a moment for the current date, as defined by the client's computer or from the `now` option. - // Will return an moment with an ambiguous timezone. - getNow: function() { - var now = this.opt('now'); - if (typeof now === 'function') { - now = now(); - } - return this.moment(now).stripZone(); - }, - - - // Produces a human-readable string for the given duration. - // Side-effect: changes the locale of the given duration. - humanizeDuration: function(duration) { - return duration.locale(this.opt('locale')).humanize(); - }, - - - - // Event-Specific Date Utilities. TODO: move - // ----------------------------------------------------------------------------------------------------------------- - - - // Get an event's normalized end date. If not present, calculate it from the defaults. - getEventEnd: function(event) { - if (event.end) { - return event.end.clone(); - } - else { - return this.getDefaultEventEnd(event.allDay, event.start); - } - }, - - - // Given an event's allDay status and start date, return what its fallback end date should be. - // TODO: rename to computeDefaultEventEnd - getDefaultEventEnd: function(allDay, zonedStart) { - var end = zonedStart.clone(); - - if (allDay) { - end.stripTime().add(this.defaultAllDayEventDuration); - } - else { - end.add(this.defaultTimedEventDuration); - } - - if (this.getIsAmbigTimezone()) { - end.stripZone(); // we don't know what the tzo should be - } - - return end; - } - -}); - -;; - -Calendar.mixin({ - - viewSpecCache: null, // cache of view definitions (initialized in Calendar.js) - - - // Gets information about how to create a view. Will use a cache. - getViewSpec: function(viewType) { - var cache = this.viewSpecCache; - - return cache[viewType] || (cache[viewType] = this.buildViewSpec(viewType)); - }, - - - // Given a duration singular unit, like "week" or "day", finds a matching view spec. - // Preference is given to views that have corresponding buttons. - getUnitViewSpec: function(unit) { - var viewTypes; - var i; - var spec; - - if ($.inArray(unit, unitsDesc) != -1) { - - // put views that have buttons first. there will be duplicates, but oh well - viewTypes = this.header.getViewsWithButtons(); // TODO: include footer as well? - $.each(FC.views, function(viewType) { // all views - viewTypes.push(viewType); - }); - - for (i = 0; i < viewTypes.length; i++) { - spec = this.getViewSpec(viewTypes[i]); - if (spec) { - if (spec.singleUnit == unit) { - return spec; - } - } - } - } - }, - - - // Builds an object with information on how to create a given view - buildViewSpec: function(requestedViewType) { - var viewOverrides = this.overrides.views || {}; - var specChain = []; // for the view. lowest to highest priority - var defaultsChain = []; // for the view. lowest to highest priority - var overridesChain = []; // for the view. lowest to highest priority - var viewType = requestedViewType; - var spec; // for the view - var overrides; // for the view - var durationInput; - var duration; - var unit; - - // iterate from the specific view definition to a more general one until we hit an actual View class - while (viewType) { - spec = fcViews[viewType]; - overrides = viewOverrides[viewType]; - viewType = null; // clear. might repopulate for another iteration - - if (typeof spec === 'function') { // TODO: deprecate - spec = { 'class': spec }; - } - - if (spec) { - specChain.unshift(spec); - defaultsChain.unshift(spec.defaults || {}); - durationInput = durationInput || spec.duration; - viewType = viewType || spec.type; - } - - if (overrides) { - overridesChain.unshift(overrides); // view-specific option hashes have options at zero-level - durationInput = durationInput || overrides.duration; - viewType = viewType || overrides.type; - } - } - - spec = mergeProps(specChain); - spec.type = requestedViewType; - if (!spec['class']) { - return false; - } - - // fall back to top-level `duration` option - durationInput = durationInput || - this.dynamicOverrides.duration || - this.overrides.duration; - - if (durationInput) { - duration = moment.duration(durationInput); - - if (duration.valueOf()) { // valid? - - unit = computeDurationGreatestUnit(duration, durationInput); - - spec.duration = duration; - spec.durationUnit = unit; - - // view is a single-unit duration, like "week" or "day" - // incorporate options for this. lowest priority - if (duration.as(unit) === 1) { - spec.singleUnit = unit; - overridesChain.unshift(viewOverrides[unit] || {}); - } - } - } - - spec.defaults = mergeOptions(defaultsChain); - spec.overrides = mergeOptions(overridesChain); - - this.buildViewSpecOptions(spec); - this.buildViewSpecButtonText(spec, requestedViewType); - - return spec; - }, - - - // Builds and assigns a view spec's options object from its already-assigned defaults and overrides - buildViewSpecOptions: function(spec) { - spec.options = mergeOptions([ // lowest to highest priority - Calendar.defaults, // global defaults - spec.defaults, // view's defaults (from ViewSubclass.defaults) - this.dirDefaults, - this.localeDefaults, // locale and dir take precedence over view's defaults! - this.overrides, // calendar's overrides (options given to constructor) - spec.overrides, // view's overrides (view-specific options) - this.dynamicOverrides // dynamically set via setter. highest precedence - ]); - populateInstanceComputableOptions(spec.options); - }, - - - // Computes and assigns a view spec's buttonText-related options - buildViewSpecButtonText: function(spec, requestedViewType) { - - // given an options object with a possible `buttonText` hash, lookup the buttonText for the - // requested view, falling back to a generic unit entry like "week" or "day" - function queryButtonText(options) { - var buttonText = options.buttonText || {}; - return buttonText[requestedViewType] || - // view can decide to look up a certain key - (spec.buttonTextKey ? buttonText[spec.buttonTextKey] : null) || - // a key like "month" - (spec.singleUnit ? buttonText[spec.singleUnit] : null); - } - - // highest to lowest priority - spec.buttonTextOverride = - queryButtonText(this.dynamicOverrides) || - queryButtonText(this.overrides) || // constructor-specified buttonText lookup hash takes precedence - spec.overrides.buttonText; // `buttonText` for view-specific options is a string - - // highest to lowest priority. mirrors buildViewSpecOptions - spec.buttonTextDefault = - queryButtonText(this.localeDefaults) || - queryButtonText(this.dirDefaults) || - spec.defaults.buttonText || // a single string. from ViewSubclass.defaults - queryButtonText(Calendar.defaults) || - (spec.duration ? this.humanizeDuration(spec.duration) : null) || // like "3 days" - requestedViewType; // fall back to given view name - } - -}); - -;; - -Calendar.mixin({ - - el: null, - contentEl: null, - suggestedViewHeight: null, - windowResizeProxy: null, - ignoreWindowResize: 0, - - - render: function() { - if (!this.contentEl) { - this.initialRender(); - } - else if (this.elementVisible()) { - // mainly for the public API - this.calcSize(); - this.renderView(); - } - }, - - - initialRender: function() { - var _this = this; - var el = this.el; - - el.addClass('fc'); - - // event delegation for nav links - el.on('click.fc', 'a[data-goto]', function(ev) { - var anchorEl = $(this); - var gotoOptions = anchorEl.data('goto'); // will automatically parse JSON - var date = _this.moment(gotoOptions.date); - var viewType = gotoOptions.type; - - // property like "navLinkDayClick". might be a string or a function - var customAction = _this.view.opt('navLink' + capitaliseFirstLetter(viewType) + 'Click'); - - if (typeof customAction === 'function') { - customAction(date, ev); - } - else { - if (typeof customAction === 'string') { - viewType = customAction; - } - _this.zoomTo(date, viewType); - } - }); - - // called immediately, and upon option change - this.optionsModel.watch('applyingThemeClasses', [ '?theme' ], function(opts) { - el.toggleClass('ui-widget', opts.theme); - el.toggleClass('fc-unthemed', !opts.theme); - }); - - // called immediately, and upon option change. - // HACK: locale often affects isRTL, so we explicitly listen to that too. - this.optionsModel.watch('applyingDirClasses', [ '?isRTL', '?locale' ], function(opts) { - el.toggleClass('fc-ltr', !opts.isRTL); - el.toggleClass('fc-rtl', opts.isRTL); - }); - - this.contentEl = $("<div class='fc-view-container'/>").prependTo(el); - - this.initToolbars(); - this.renderHeader(); - this.renderFooter(); - this.renderView(this.opt('defaultView')); - - if (this.opt('handleWindowResize')) { - $(window).resize( - this.windowResizeProxy = debounce( // prevents rapid calls - this.windowResize.bind(this), - this.opt('windowResizeDelay') - ) - ); - } - }, - - - destroy: function() { - - if (this.view) { - this.view.removeElement(); - - // NOTE: don't null-out this.view in case API methods are called after destroy. - // It is still the "current" view, just not rendered. - } - - this.toolbarsManager.proxyCall('removeElement'); - this.contentEl.remove(); - this.el.removeClass('fc fc-ltr fc-rtl fc-unthemed ui-widget'); - - this.el.off('.fc'); // unbind nav link handlers - - if (this.windowResizeProxy) { - $(window).unbind('resize', this.windowResizeProxy); - this.windowResizeProxy = null; - } - - GlobalEmitter.unneeded(); - }, - - - elementVisible: function() { - return this.el.is(':visible'); - }, - - - - // View Rendering - // ----------------------------------------------------------------------------------- - - - // Renders a view because of a date change, view-type change, or for the first time. - // If not given a viewType, keep the current view but render different dates. - // Accepts an optional scroll state to restore to. - renderView: function(viewType, forcedScroll) { - - this.ignoreWindowResize++; - - var needsClearView = this.view && viewType && this.view.type !== viewType; - - // if viewType is changing, remove the old view's rendering - if (needsClearView) { - this.freezeContentHeight(); // prevent a scroll jump when view element is removed - this.clearView(); - } - - // if viewType changed, or the view was never created, create a fresh view - if (!this.view && viewType) { - this.view = - this.viewsByType[viewType] || - (this.viewsByType[viewType] = this.instantiateView(viewType)); - - this.view.setElement( - $("<div class='fc-view fc-" + viewType + "-view' />").appendTo(this.contentEl) - ); - this.toolbarsManager.proxyCall('activateButton', viewType); - } - - if (this.view) { - - if (forcedScroll) { - this.view.addForcedScroll(forcedScroll); - } - - if (this.elementVisible()) { - this.currentDate = this.view.setDate(this.currentDate); - } - } - - if (needsClearView) { - this.thawContentHeight(); - } - - this.ignoreWindowResize--; - }, - - - // Unrenders the current view and reflects this change in the Header. - // Unregsiters the `view`, but does not remove from viewByType hash. - clearView: function() { - this.toolbarsManager.proxyCall('deactivateButton', this.view.type); - this.view.removeElement(); - this.view = null; - }, - - - // Destroys the view, including the view object. Then, re-instantiates it and renders it. - // Maintains the same scroll state. - // TODO: maintain any other user-manipulated state. - reinitView: function() { - this.ignoreWindowResize++; - this.freezeContentHeight(); - - var viewType = this.view.type; - var scrollState = this.view.queryScroll(); - this.clearView(); - this.calcSize(); - this.renderView(viewType, scrollState); - - this.thawContentHeight(); - this.ignoreWindowResize--; - }, - - - // Resizing - // ----------------------------------------------------------------------------------- - - - getSuggestedViewHeight: function() { - if (this.suggestedViewHeight === null) { - this.calcSize(); - } - return this.suggestedViewHeight; - }, - - - isHeightAuto: function() { - return this.opt('contentHeight') === 'auto' || this.opt('height') === 'auto'; - }, - - - updateSize: function(shouldRecalc) { - if (this.elementVisible()) { - - if (shouldRecalc) { - this._calcSize(); - } - - this.ignoreWindowResize++; - this.view.updateSize(true); // isResize=true. will poll getSuggestedViewHeight() and isHeightAuto() - this.ignoreWindowResize--; - - return true; // signal success - } - }, - - - calcSize: function() { - if (this.elementVisible()) { - this._calcSize(); - } - }, - - - _calcSize: function() { // assumes elementVisible - var contentHeightInput = this.opt('contentHeight'); - var heightInput = this.opt('height'); - - if (typeof contentHeightInput === 'number') { // exists and not 'auto' - this.suggestedViewHeight = contentHeightInput; - } - else if (typeof contentHeightInput === 'function') { // exists and is a function - this.suggestedViewHeight = contentHeightInput(); - } - else if (typeof heightInput === 'number') { // exists and not 'auto' - this.suggestedViewHeight = heightInput - this.queryToolbarsHeight(); - } - else if (typeof heightInput === 'function') { // exists and is a function - this.suggestedViewHeight = heightInput() - this.queryToolbarsHeight(); - } - else if (heightInput === 'parent') { // set to height of parent element - this.suggestedViewHeight = this.el.parent().height() - this.queryToolbarsHeight(); - } - else { - this.suggestedViewHeight = Math.round( - this.contentEl.width() / - Math.max(this.opt('aspectRatio'), .5) - ); - } - }, - - - windowResize: function(ev) { - if ( - !this.ignoreWindowResize && - ev.target === window && // so we don't process jqui "resize" events that have bubbled up - this.view.renderRange // view has already been rendered - ) { - if (this.updateSize(true)) { - this.view.publiclyTrigger('windowResize', this.el[0]); - } - } - }, - - - /* Height "Freezing" - -----------------------------------------------------------------------------*/ - - - freezeContentHeight: function() { - this.contentEl.css({ - width: '100%', - height: this.contentEl.height(), - overflow: 'hidden' - }); - }, - - - thawContentHeight: function() { - this.contentEl.css({ - width: '', - height: '', - overflow: '' - }); - } - -}); - -;; - -Calendar.mixin({ - - header: null, - footer: null, - toolbarsManager: null, - - - initToolbars: function() { - this.header = new Toolbar(this, this.computeHeaderOptions()); - this.footer = new Toolbar(this, this.computeFooterOptions()); - this.toolbarsManager = new Iterator([ this.header, this.footer ]); - }, - - - computeHeaderOptions: function() { - return { - extraClasses: 'fc-header-toolbar', - layout: this.opt('header') - }; - }, - - - computeFooterOptions: function() { - return { - extraClasses: 'fc-footer-toolbar', - layout: this.opt('footer') - }; - }, - - - // can be called repeatedly and Header will rerender - renderHeader: function() { - var header = this.header; - - header.setToolbarOptions(this.computeHeaderOptions()); - header.render(); - - if (header.el) { - this.el.prepend(header.el); - } - }, - - - // can be called repeatedly and Footer will rerender - renderFooter: function() { - var footer = this.footer; - - footer.setToolbarOptions(this.computeFooterOptions()); - footer.render(); - - if (footer.el) { - this.el.append(footer.el); - } - }, - - - setToolbarsTitle: function(title) { - this.toolbarsManager.proxyCall('updateTitle', title); - }, - - - updateToolbarButtons: function() { - var now = this.getNow(); - var view = this.view; - var todayInfo = view.buildDateProfile(now); - var prevInfo = view.buildPrevDateProfile(this.currentDate); - var nextInfo = view.buildNextDateProfile(this.currentDate); - - this.toolbarsManager.proxyCall( - (todayInfo.isValid && !isDateWithinRange(now, view.currentRange)) ? - 'enableButton' : - 'disableButton', - 'today' - ); - - this.toolbarsManager.proxyCall( - prevInfo.isValid ? - 'enableButton' : - 'disableButton', - 'prev' - ); - - this.toolbarsManager.proxyCall( - nextInfo.isValid ? - 'enableButton' : - 'disableButton', - 'next' - ); - }, - - - queryToolbarsHeight: function() { - return this.toolbarsManager.items.reduce(function(accumulator, toolbar) { - var toolbarHeight = toolbar.el ? toolbar.el.outerHeight(true) : 0; // includes margin - return accumulator + toolbarHeight; - }, 0); - } - -}); - -;; - -Calendar.defaults = { - - titleRangeSeparator: ' \u2013 ', // en dash - monthYearFormat: 'MMMM YYYY', // required for en. other locales rely on datepicker computable option - - defaultTimedEventDuration: '02:00:00', - defaultAllDayEventDuration: { days: 1 }, - forceEventDuration: false, - nextDayThreshold: '09:00:00', // 9am - - // display - defaultView: 'month', - aspectRatio: 1.35, - header: { - left: 'title', - center: '', - right: 'today prev,next' - }, - weekends: true, - weekNumbers: false, - - weekNumberTitle: 'W', - weekNumberCalculation: 'local', - - //editable: false, - - //nowIndicator: false, - - scrollTime: '06:00:00', - minTime: '00:00:00', - maxTime: '24:00:00', - showNonCurrentDates: true, - - // event ajax - lazyFetching: true, - startParam: 'start', - endParam: 'end', - timezoneParam: 'timezone', - - timezone: false, - - //allDayDefault: undefined, - - // locale - isRTL: false, - buttonText: { - prev: "prev", - next: "next", - prevYear: "prev year", - nextYear: "next year", - year: 'year', // TODO: locale files need to specify this - today: 'today', - month: 'month', - week: 'week', - day: 'day' - }, - - buttonIcons: { - prev: 'left-single-arrow', - next: 'right-single-arrow', - prevYear: 'left-double-arrow', - nextYear: 'right-double-arrow' - }, - - allDayText: 'all-day', - - // jquery-ui theming - theme: false, - themeButtonIcons: { - prev: 'circle-triangle-w', - next: 'circle-triangle-e', - prevYear: 'seek-prev', - nextYear: 'seek-next' - }, - - //eventResizableFromStart: false, - dragOpacity: .75, - dragRevertDuration: 500, - dragScroll: true, - - //selectable: false, - unselectAuto: true, - //selectMinDistance: 0, - - dropAccept: '*', - - eventOrder: 'title', - //eventRenderWait: null, - - eventLimit: false, - eventLimitText: 'more', - eventLimitClick: 'popover', - dayPopoverFormat: 'LL', - - handleWindowResize: true, - windowResizeDelay: 100, // milliseconds before an updateSize happens - - longPressDelay: 1000 - -}; - - -Calendar.englishDefaults = { // used by locale.js - dayPopoverFormat: 'dddd, MMMM D' -}; - - -Calendar.rtlDefaults = { // right-to-left defaults - header: { // TODO: smarter solution (first/center/last ?) - left: 'next,prev today', - center: '', - right: 'title' - }, - buttonIcons: { - prev: 'right-single-arrow', - next: 'left-single-arrow', - prevYear: 'right-double-arrow', - nextYear: 'left-double-arrow' - }, - themeButtonIcons: { - prev: 'circle-triangle-e', - next: 'circle-triangle-w', - nextYear: 'seek-prev', - prevYear: 'seek-next' - } -}; - -;; - -var localeOptionHash = FC.locales = {}; // initialize and expose - - -// TODO: document the structure and ordering of a FullCalendar locale file - - -// Initialize jQuery UI datepicker translations while using some of the translations -// Will set this as the default locales for datepicker. -FC.datepickerLocale = function(localeCode, dpLocaleCode, dpOptions) { - - // get the FullCalendar internal option hash for this locale. create if necessary - var fcOptions = localeOptionHash[localeCode] || (localeOptionHash[localeCode] = {}); - - // transfer some simple options from datepicker to fc - fcOptions.isRTL = dpOptions.isRTL; - fcOptions.weekNumberTitle = dpOptions.weekHeader; - - // compute some more complex options from datepicker - $.each(dpComputableOptions, function(name, func) { - fcOptions[name] = func(dpOptions); - }); - - // is jQuery UI Datepicker is on the page? - if ($.datepicker) { - - // Register the locale data. - // FullCalendar and MomentJS use locale codes like "pt-br" but Datepicker - // does it like "pt-BR" or if it doesn't have the locale, maybe just "pt". - // Make an alias so the locale can be referenced either way. - $.datepicker.regional[dpLocaleCode] = - $.datepicker.regional[localeCode] = // alias - dpOptions; - - // Alias 'en' to the default locale data. Do this every time. - $.datepicker.regional.en = $.datepicker.regional['']; - - // Set as Datepicker's global defaults. - $.datepicker.setDefaults(dpOptions); - } -}; - - -// Sets FullCalendar-specific translations. Will set the locales as the global default. -FC.locale = function(localeCode, newFcOptions) { - var fcOptions; - var momOptions; - - // get the FullCalendar internal option hash for this locale. create if necessary - fcOptions = localeOptionHash[localeCode] || (localeOptionHash[localeCode] = {}); - - // provided new options for this locales? merge them in - if (newFcOptions) { - fcOptions = localeOptionHash[localeCode] = mergeOptions([ fcOptions, newFcOptions ]); - } - - // compute locale options that weren't defined. - // always do this. newFcOptions can be undefined when initializing from i18n file, - // so no way to tell if this is an initialization or a default-setting. - momOptions = getMomentLocaleData(localeCode); // will fall back to en - $.each(momComputableOptions, function(name, func) { - if (fcOptions[name] == null) { - fcOptions[name] = func(momOptions, fcOptions); - } - }); - - // set it as the default locale for FullCalendar - Calendar.defaults.locale = localeCode; -}; - - -// NOTE: can't guarantee any of these computations will run because not every locale has datepicker -// configs, so make sure there are English fallbacks for these in the defaults file. -var dpComputableOptions = { - - buttonText: function(dpOptions) { - return { - // the translations sometimes wrongly contain HTML entities - prev: stripHtmlEntities(dpOptions.prevText), - next: stripHtmlEntities(dpOptions.nextText), - today: stripHtmlEntities(dpOptions.currentText) - }; - }, - - // Produces format strings like "MMMM YYYY" -> "September 2014" - monthYearFormat: function(dpOptions) { - return dpOptions.showMonthAfterYear ? - 'YYYY[' + dpOptions.yearSuffix + '] MMMM' : - 'MMMM YYYY[' + dpOptions.yearSuffix + ']'; - } - -}; - -var momComputableOptions = { - - // Produces format strings like "ddd M/D" -> "Fri 9/15" - dayOfMonthFormat: function(momOptions, fcOptions) { - var format = momOptions.longDateFormat('l'); // for the format like "M/D/YYYY" - - // strip the year off the edge, as well as other misc non-whitespace chars - format = format.replace(/^Y+[^\w\s]*|[^\w\s]*Y+$/g, ''); - - if (fcOptions.isRTL) { - format += ' ddd'; // for RTL, add day-of-week to end - } - else { - format = 'ddd ' + format; // for LTR, add day-of-week to beginning - } - return format; - }, - - // Produces format strings like "h:mma" -> "6:00pm" - mediumTimeFormat: function(momOptions) { // can't be called `timeFormat` because collides with option - return momOptions.longDateFormat('LT') - .replace(/\s*a$/i, 'a'); // convert AM/PM/am/pm to lowercase. remove any spaces beforehand - }, - - // Produces format strings like "h(:mm)a" -> "6pm" / "6:30pm" - smallTimeFormat: function(momOptions) { - return momOptions.longDateFormat('LT') - .replace(':mm', '(:mm)') - .replace(/(\Wmm)$/, '($1)') // like above, but for foreign locales - .replace(/\s*a$/i, 'a'); // convert AM/PM/am/pm to lowercase. remove any spaces beforehand - }, - - // Produces format strings like "h(:mm)t" -> "6p" / "6:30p" - extraSmallTimeFormat: function(momOptions) { - return momOptions.longDateFormat('LT') - .replace(':mm', '(:mm)') - .replace(/(\Wmm)$/, '($1)') // like above, but for foreign locales - .replace(/\s*a$/i, 't'); // convert to AM/PM/am/pm to lowercase one-letter. remove any spaces beforehand - }, - - // Produces format strings like "ha" / "H" -> "6pm" / "18" - hourFormat: function(momOptions) { - return momOptions.longDateFormat('LT') - .replace(':mm', '') - .replace(/(\Wmm)$/, '') // like above, but for foreign locales - .replace(/\s*a$/i, 'a'); // convert AM/PM/am/pm to lowercase. remove any spaces beforehand - }, - - // Produces format strings like "h:mm" -> "6:30" (with no AM/PM) - noMeridiemTimeFormat: function(momOptions) { - return momOptions.longDateFormat('LT') - .replace(/\s*a$/i, ''); // remove trailing AM/PM - } - -}; - - -// options that should be computed off live calendar options (considers override options) -// TODO: best place for this? related to locale? -// TODO: flipping text based on isRTL is a bad idea because the CSS `direction` might want to handle it -var instanceComputableOptions = { - - // Produces format strings for results like "Mo 16" - smallDayDateFormat: function(options) { - return options.isRTL ? - 'D dd' : - 'dd D'; - }, - - // Produces format strings for results like "Wk 5" - weekFormat: function(options) { - return options.isRTL ? - 'w[ ' + options.weekNumberTitle + ']' : - '[' + options.weekNumberTitle + ' ]w'; - }, - - // Produces format strings for results like "Wk5" - smallWeekFormat: function(options) { - return options.isRTL ? - 'w[' + options.weekNumberTitle + ']' : - '[' + options.weekNumberTitle + ']w'; - } - -}; - -// TODO: make these computable properties in optionsModel -function populateInstanceComputableOptions(options) { - $.each(instanceComputableOptions, function(name, func) { - if (options[name] == null) { - options[name] = func(options); - } - }); -} - - -// Returns moment's internal locale data. If doesn't exist, returns English. -function getMomentLocaleData(localeCode) { - return moment.localeData(localeCode) || moment.localeData('en'); -} - - -// Initialize English by forcing computation of moment-derived options. -// Also, sets it as the default. -FC.locale('en', Calendar.englishDefaults); - -;; - -FC.sourceNormalizers = []; -FC.sourceFetchers = []; - -var ajaxDefaults = { - dataType: 'json', - cache: false -}; - -var eventGUID = 1; - - -function EventManager() { // assumed to be a calendar - var t = this; - - - // exports - t.requestEvents = requestEvents; - t.reportEventChange = reportEventChange; - t.isFetchNeeded = isFetchNeeded; - t.fetchEvents = fetchEvents; - t.fetchEventSources = fetchEventSources; - t.refetchEvents = refetchEvents; - t.refetchEventSources = refetchEventSources; - t.getEventSources = getEventSources; - t.getEventSourceById = getEventSourceById; - t.addEventSource = addEventSource; - t.removeEventSource = removeEventSource; - t.removeEventSources = removeEventSources; - t.updateEvent = updateEvent; - t.updateEvents = updateEvents; - t.renderEvent = renderEvent; - t.renderEvents = renderEvents; - t.removeEvents = removeEvents; - t.clientEvents = clientEvents; - t.mutateEvent = mutateEvent; - t.normalizeEventDates = normalizeEventDates; - t.normalizeEventTimes = normalizeEventTimes; - - - // locals - var stickySource = { events: [] }; - var sources = [ stickySource ]; - var rangeStart, rangeEnd; - var pendingSourceCnt = 0; // outstanding fetch requests, max one per source - var cache = []; // holds events that have already been expanded - var prunedCache; // like cache, but only events that intersect with rangeStart/rangeEnd - - - $.each( - (t.opt('events') ? [ t.opt('events') ] : []).concat(t.opt('eventSources') || []), - function(i, sourceInput) { - var source = buildEventSource(sourceInput); - if (source) { - sources.push(source); - } - } - ); - - - - function requestEvents(start, end) { - if (!t.opt('lazyFetching') || isFetchNeeded(start, end)) { - return fetchEvents(start, end); - } - else { - return Promise.resolve(prunedCache); - } - } - - - function reportEventChange() { - prunedCache = filterEventsWithinRange(cache); - t.trigger('eventsReset', prunedCache); - } - - - function filterEventsWithinRange(events) { - var filteredEvents = []; - var i, event; - - for (i = 0; i < events.length; i++) { - event = events[i]; - - if ( - event.start.clone().stripZone() < rangeEnd && - t.getEventEnd(event).stripZone() > rangeStart - ) { - filteredEvents.push(event); - } - } - - return filteredEvents; - } - - - t.getEventCache = function() { - return cache; - }; - - - - /* Fetching - -----------------------------------------------------------------------------*/ - - - // start and end are assumed to be unzoned - function isFetchNeeded(start, end) { - return !rangeStart || // nothing has been fetched yet? - start < rangeStart || end > rangeEnd; // is part of the new range outside of the old range? - } - - - function fetchEvents(start, end) { - rangeStart = start; - rangeEnd = end; - return refetchEvents(); - } - - - // poorly named. fetches all sources with current `rangeStart` and `rangeEnd`. - function refetchEvents() { - return fetchEventSources(sources, 'reset'); - } - - - // poorly named. fetches a subset of event sources. - function refetchEventSources(matchInputs) { - return fetchEventSources(getEventSourcesByMatchArray(matchInputs)); - } - - - // expects an array of event source objects (the originals, not copies) - // `specialFetchType` is an optimization parameter that affects purging of the event cache. - function fetchEventSources(specificSources, specialFetchType) { - var i, source; - - if (specialFetchType === 'reset') { - cache = []; - } - else if (specialFetchType !== 'add') { - cache = excludeEventsBySources(cache, specificSources); - } - - for (i = 0; i < specificSources.length; i++) { - source = specificSources[i]; - - // already-pending sources have already been accounted for in pendingSourceCnt - if (source._status !== 'pending') { - pendingSourceCnt++; - } - - source._fetchId = (source._fetchId || 0) + 1; - source._status = 'pending'; - } - - for (i = 0; i < specificSources.length; i++) { - source = specificSources[i]; - tryFetchEventSource(source, source._fetchId); - } - - if (pendingSourceCnt) { - return Promise.construct(function(resolve) { - t.one('eventsReceived', resolve); // will send prunedCache - }); - } - else { // executed all synchronously, or no sources at all - return Promise.resolve(prunedCache); - } - } - - - // fetches an event source and processes its result ONLY if it is still the current fetch. - // caller is responsible for incrementing pendingSourceCnt first. - function tryFetchEventSource(source, fetchId) { - _fetchEventSource(source, function(eventInputs) { - var isArraySource = $.isArray(source.events); - var i, eventInput; - var abstractEvent; - - if ( - // is this the source's most recent fetch? - // if not, rely on an upcoming fetch of this source to decrement pendingSourceCnt - fetchId === source._fetchId && - // event source no longer valid? - source._status !== 'rejected' - ) { - source._status = 'resolved'; - - if (eventInputs) { - for (i = 0; i < eventInputs.length; i++) { - eventInput = eventInputs[i]; - - if (isArraySource) { // array sources have already been convert to Event Objects - abstractEvent = eventInput; - } - else { - abstractEvent = buildEventFromInput(eventInput, source); - } - - if (abstractEvent) { // not false (an invalid event) - cache.push.apply( // append - cache, - expandEvent(abstractEvent) // add individual expanded events to the cache - ); - } - } - } - - decrementPendingSourceCnt(); - } - }); - } - - - function rejectEventSource(source) { - var wasPending = source._status === 'pending'; - - source._status = 'rejected'; - - if (wasPending) { - decrementPendingSourceCnt(); - } - } - - - function decrementPendingSourceCnt() { - pendingSourceCnt--; - if (!pendingSourceCnt) { - reportEventChange(cache); // updates prunedCache - t.trigger('eventsReceived', prunedCache); - } - } - - - function _fetchEventSource(source, callback) { - var i; - var fetchers = FC.sourceFetchers; - var res; - - for (i=0; i<fetchers.length; i++) { - res = fetchers[i].call( - t, // this, the Calendar object - source, - rangeStart.clone(), - rangeEnd.clone(), - t.opt('timezone'), - callback - ); - - if (res === true) { - // the fetcher is in charge. made its own async request - return; - } - else if (typeof res == 'object') { - // the fetcher returned a new source. process it - _fetchEventSource(res, callback); - return; - } - } - - var events = source.events; - if (events) { - if ($.isFunction(events)) { - t.pushLoading(); - events.call( - t, // this, the Calendar object - rangeStart.clone(), - rangeEnd.clone(), - t.opt('timezone'), - function(events) { - callback(events); - t.popLoading(); - } - ); - } - else if ($.isArray(events)) { - callback(events); - } - else { - callback(); - } - }else{ - var url = source.url; - if (url) { - var success = source.success; - var error = source.error; - var complete = source.complete; - - // retrieve any outbound GET/POST $.ajax data from the options - var customData; - if ($.isFunction(source.data)) { - // supplied as a function that returns a key/value object - customData = source.data(); - } - else { - // supplied as a straight key/value object - customData = source.data; - } - - // use a copy of the custom data so we can modify the parameters - // and not affect the passed-in object. - var data = $.extend({}, customData || {}); - - var startParam = firstDefined(source.startParam, t.opt('startParam')); - var endParam = firstDefined(source.endParam, t.opt('endParam')); - var timezoneParam = firstDefined(source.timezoneParam, t.opt('timezoneParam')); - - if (startParam) { - data[startParam] = rangeStart.format(); - } - if (endParam) { - data[endParam] = rangeEnd.format(); - } - if (t.opt('timezone') && t.opt('timezone') != 'local') { - data[timezoneParam] = t.opt('timezone'); - } - - t.pushLoading(); - $.ajax($.extend({}, ajaxDefaults, source, { - data: data, - success: function(events) { - events = events || []; - var res = applyAll(success, this, arguments); - if ($.isArray(res)) { - events = res; - } - callback(events); - }, - error: function() { - applyAll(error, this, arguments); - callback(); - }, - complete: function() { - applyAll(complete, this, arguments); - t.popLoading(); - } - })); - }else{ - callback(); - } - } - } - - - - /* Sources - -----------------------------------------------------------------------------*/ - - - function addEventSource(sourceInput) { - var source = buildEventSource(sourceInput); - if (source) { - sources.push(source); - fetchEventSources([ source ], 'add'); // will eventually call reportEventChange - } - } - - - function buildEventSource(sourceInput) { // will return undefined if invalid source - var normalizers = FC.sourceNormalizers; - var source; - var i; - - if ($.isFunction(sourceInput) || $.isArray(sourceInput)) { - source = { events: sourceInput }; - } - else if (typeof sourceInput === 'string') { - source = { url: sourceInput }; - } - else if (typeof sourceInput === 'object') { - source = $.extend({}, sourceInput); // shallow copy - } - - if (source) { - - // TODO: repeat code, same code for event classNames - if (source.className) { - if (typeof source.className === 'string') { - source.className = source.className.split(/\s+/); - } - // otherwise, assumed to be an array - } - else { - source.className = []; - } - - // for array sources, we convert to standard Event Objects up front - if ($.isArray(source.events)) { - source.origArray = source.events; // for removeEventSource - source.events = $.map(source.events, function(eventInput) { - return buildEventFromInput(eventInput, source); - }); - } - - for (i=0; i<normalizers.length; i++) { - normalizers[i].call(t, source); - } - - return source; - } - } - - - function removeEventSource(matchInput) { - removeSpecificEventSources( - getEventSourcesByMatch(matchInput) - ); - } - - - // if called with no arguments, removes all. - function removeEventSources(matchInputs) { - if (matchInputs == null) { - removeSpecificEventSources(sources, true); // isAll=true - } - else { - removeSpecificEventSources( - getEventSourcesByMatchArray(matchInputs) - ); - } - } - - - function removeSpecificEventSources(targetSources, isAll) { - var i; - - // cancel pending requests - for (i = 0; i < targetSources.length; i++) { - rejectEventSource(targetSources[i]); - } - - if (isAll) { // an optimization - sources = []; - cache = []; - } - else { - // remove from persisted source list - sources = $.grep(sources, function(source) { - for (i = 0; i < targetSources.length; i++) { - if (source === targetSources[i]) { - return false; // exclude - } - } - return true; // include - }); - - cache = excludeEventsBySources(cache, targetSources); - } - - reportEventChange(); - } - - - function getEventSources() { - return sources.slice(1); // returns a shallow copy of sources with stickySource removed - } - - - function getEventSourceById(id) { - return $.grep(sources, function(source) { - return source.id && source.id === id; - })[0]; - } - - - // like getEventSourcesByMatch, but accepts multple match criteria (like multiple IDs) - function getEventSourcesByMatchArray(matchInputs) { - - // coerce into an array - if (!matchInputs) { - matchInputs = []; - } - else if (!$.isArray(matchInputs)) { - matchInputs = [ matchInputs ]; - } - - var matchingSources = []; - var i; - - // resolve raw inputs to real event source objects - for (i = 0; i < matchInputs.length; i++) { - matchingSources.push.apply( // append - matchingSources, - getEventSourcesByMatch(matchInputs[i]) - ); - } - - return matchingSources; - } - - - // matchInput can either by a real event source object, an ID, or the function/URL for the source. - // returns an array of matching source objects. - function getEventSourcesByMatch(matchInput) { - var i, source; - - // given an proper event source object - for (i = 0; i < sources.length; i++) { - source = sources[i]; - if (source === matchInput) { - return [ source ]; - } - } - - // an ID match - source = getEventSourceById(matchInput); - if (source) { - return [ source ]; - } - - return $.grep(sources, function(source) { - return isSourcesEquivalent(matchInput, source); - }); - } - - - function isSourcesEquivalent(source1, source2) { - return source1 && source2 && getSourcePrimitive(source1) == getSourcePrimitive(source2); - } - - - function getSourcePrimitive(source) { - return ( - (typeof source === 'object') ? // a normalized event source? - (source.origArray || source.googleCalendarId || source.url || source.events) : // get the primitive - null - ) || - source; // the given argument *is* the primitive - } - - - // util - // returns a filtered array without events that are part of any of the given sources - function excludeEventsBySources(specificEvents, specificSources) { - return $.grep(specificEvents, function(event) { - for (var i = 0; i < specificSources.length; i++) { - if (event.source === specificSources[i]) { - return false; // exclude - } - } - return true; // keep - }); - } - - - - /* Manipulation - -----------------------------------------------------------------------------*/ - - - // Only ever called from the externally-facing API - function updateEvent(event) { - updateEvents([ event ]); - } - - - // Only ever called from the externally-facing API - function updateEvents(events) { - var i, event; - - for (i = 0; i < events.length; i++) { - event = events[i]; - - // massage start/end values, even if date string values - event.start = t.moment(event.start); - if (event.end) { - event.end = t.moment(event.end); - } - else { - event.end = null; - } - - mutateEvent(event, getMiscEventProps(event)); // will handle start/end/allDay normalization - } - - reportEventChange(); // reports event modifications (so we can redraw) - } - - - // Returns a hash of misc event properties that should be copied over to related events. - function getMiscEventProps(event) { - var props = {}; - - $.each(event, function(name, val) { - if (isMiscEventPropName(name)) { - if (val !== undefined && isAtomic(val)) { // a defined non-object - props[name] = val; - } - } - }); - - return props; - } - - // non-date-related, non-id-related, non-secret - function isMiscEventPropName(name) { - return !/^_|^(id|allDay|start|end)$/.test(name); - } - - - // returns the expanded events that were created - function renderEvent(eventInput, stick) { - return renderEvents([ eventInput ], stick); - } - - - // returns the expanded events that were created - function renderEvents(eventInputs, stick) { - var renderedEvents = []; - var renderableEvents; - var abstractEvent; - var i, j, event; - - for (i = 0; i < eventInputs.length; i++) { - abstractEvent = buildEventFromInput(eventInputs[i]); - - if (abstractEvent) { // not false (a valid input) - renderableEvents = expandEvent(abstractEvent); - - for (j = 0; j < renderableEvents.length; j++) { - event = renderableEvents[j]; - - if (!event.source) { - if (stick) { - stickySource.events.push(event); - event.source = stickySource; - } - cache.push(event); - } - } - - renderedEvents = renderedEvents.concat(renderableEvents); - } - } - - if (renderedEvents.length) { // any new events rendered? - reportEventChange(); - } - - return renderedEvents; - } - - - function removeEvents(filter) { - var eventID; - var i; - - if (filter == null) { // null or undefined. remove all events - filter = function() { return true; }; // will always match - } - else if (!$.isFunction(filter)) { // an event ID - eventID = filter + ''; - filter = function(event) { - return event._id == eventID; - }; - } - - // Purge event(s) from our local cache - cache = $.grep(cache, filter, true); // inverse=true - - // Remove events from array sources. - // This works because they have been converted to official Event Objects up front. - // (and as a result, event._id has been calculated). - for (i=0; i<sources.length; i++) { - if ($.isArray(sources[i].events)) { - sources[i].events = $.grep(sources[i].events, filter, true); - } - } - - reportEventChange(); - } - - - function clientEvents(filter) { - if ($.isFunction(filter)) { - return $.grep(cache, filter); - } - else if (filter != null) { // not null, not undefined. an event ID - filter += ''; - return $.grep(cache, function(e) { - return e._id == filter; - }); - } - return cache; // else, return all - } - - - // Makes sure all array event sources have their internal event objects - // converted over to the Calendar's current timezone. - t.rezoneArrayEventSources = function() { - var i; - var events; - var j; - - for (i = 0; i < sources.length; i++) { - events = sources[i].events; - if ($.isArray(events)) { - - for (j = 0; j < events.length; j++) { - rezoneEventDates(events[j]); - } - } - } - }; - - function rezoneEventDates(event) { - event.start = t.moment(event.start); - if (event.end) { - event.end = t.moment(event.end); - } - backupEventDates(event); - } - - - /* Event Normalization - -----------------------------------------------------------------------------*/ - - - // Given a raw object with key/value properties, returns an "abstract" Event object. - // An "abstract" event is an event that, if recurring, will not have been expanded yet. - // Will return `false` when input is invalid. - // `source` is optional - function buildEventFromInput(input, source) { - var calendarEventDataTransform = t.opt('eventDataTransform'); - var out = {}; - var start, end; - var allDay; - - if (calendarEventDataTransform) { - input = calendarEventDataTransform(input); - } - if (source && source.eventDataTransform) { - input = source.eventDataTransform(input); - } - - // Copy all properties over to the resulting object. - // The special-case properties will be copied over afterwards. - $.extend(out, input); - - if (source) { - out.source = source; - } - - out._id = input._id || (input.id === undefined ? '_fc' + eventGUID++ : input.id + ''); - - if (input.className) { - if (typeof input.className == 'string') { - out.className = input.className.split(/\s+/); - } - else { // assumed to be an array - out.className = input.className; - } - } - else { - out.className = []; - } - - start = input.start || input.date; // "date" is an alias for "start" - end = input.end; - - // parse as a time (Duration) if applicable - if (isTimeString(start)) { - start = moment.duration(start); - } - if (isTimeString(end)) { - end = moment.duration(end); - } - - if (input.dow || moment.isDuration(start) || moment.isDuration(end)) { - - // the event is "abstract" (recurring) so don't calculate exact start/end dates just yet - out.start = start ? moment.duration(start) : null; // will be a Duration or null - out.end = end ? moment.duration(end) : null; // will be a Duration or null - out._recurring = true; // our internal marker - } - else { - - if (start) { - start = t.moment(start); - if (!start.isValid()) { - return false; - } - } - - if (end) { - end = t.moment(end); - if (!end.isValid()) { - end = null; // let defaults take over - } - } - - allDay = input.allDay; - if (allDay === undefined) { // still undefined? fallback to default - allDay = firstDefined( - source ? source.allDayDefault : undefined, - t.opt('allDayDefault') - ); - // still undefined? normalizeEventDates will calculate it - } - - assignDatesToEvent(start, end, allDay, out); - } - - t.normalizeEvent(out); // hook for external use. a prototype method - - return out; - } - t.buildEventFromInput = buildEventFromInput; - - - // Normalizes and assigns the given dates to the given partially-formed event object. - // NOTE: mutates the given start/end moments. does not make a copy. - function assignDatesToEvent(start, end, allDay, event) { - event.start = start; - event.end = end; - event.allDay = allDay; - normalizeEventDates(event); - backupEventDates(event); - } - - - // Ensures proper values for allDay/start/end. Accepts an Event object, or a plain object with event-ish properties. - // NOTE: Will modify the given object. - function normalizeEventDates(eventProps) { - - normalizeEventTimes(eventProps); - - if (eventProps.end && !eventProps.end.isAfter(eventProps.start)) { - eventProps.end = null; - } - - if (!eventProps.end) { - if (t.opt('forceEventDuration')) { - eventProps.end = t.getDefaultEventEnd(eventProps.allDay, eventProps.start); - } - else { - eventProps.end = null; - } - } - } - - - // Ensures the allDay property exists and the timeliness of the start/end dates are consistent - function normalizeEventTimes(eventProps) { - if (eventProps.allDay == null) { - eventProps.allDay = !(eventProps.start.hasTime() || (eventProps.end && eventProps.end.hasTime())); - } - - if (eventProps.allDay) { - eventProps.start.stripTime(); - if (eventProps.end) { - // TODO: consider nextDayThreshold here? If so, will require a lot of testing and adjustment - eventProps.end.stripTime(); - } - } - else { - if (!eventProps.start.hasTime()) { - eventProps.start = t.applyTimezone(eventProps.start.time(0)); // will assign a 00:00 time - } - if (eventProps.end && !eventProps.end.hasTime()) { - eventProps.end = t.applyTimezone(eventProps.end.time(0)); // will assign a 00:00 time - } - } - } - - - // If the given event is a recurring event, break it down into an array of individual instances. - // If not a recurring event, return an array with the single original event. - // If given a falsy input (probably because of a failed buildEventFromInput call), returns an empty array. - // HACK: can override the recurring window by providing custom rangeStart/rangeEnd (for businessHours). - function expandEvent(abstractEvent, _rangeStart, _rangeEnd) { - var events = []; - var dowHash; - var dow; - var i; - var date; - var startTime, endTime; - var start, end; - var event; - - _rangeStart = _rangeStart || rangeStart; - _rangeEnd = _rangeEnd || rangeEnd; - - if (abstractEvent) { - if (abstractEvent._recurring) { - - // make a boolean hash as to whether the event occurs on each day-of-week - if ((dow = abstractEvent.dow)) { - dowHash = {}; - for (i = 0; i < dow.length; i++) { - dowHash[dow[i]] = true; - } - } - - // iterate through every day in the current range - date = _rangeStart.clone().stripTime(); // holds the date of the current day - while (date.isBefore(_rangeEnd)) { - - if (!dowHash || dowHash[date.day()]) { // if everyday, or this particular day-of-week - - startTime = abstractEvent.start; // the stored start and end properties are times (Durations) - endTime = abstractEvent.end; // " - start = date.clone(); - end = null; - - if (startTime) { - start = start.time(startTime); - } - if (endTime) { - end = date.clone().time(endTime); - } - - event = $.extend({}, abstractEvent); // make a copy of the original - assignDatesToEvent( - start, end, - !startTime && !endTime, // allDay? - event - ); - events.push(event); - } - - date.add(1, 'days'); - } - } - else { - events.push(abstractEvent); // return the original event. will be a one-item array - } - } - - return events; - } - t.expandEvent = expandEvent; - - - - /* Event Modification Math - -----------------------------------------------------------------------------------------*/ - - - // Modifies an event and all related events by applying the given properties. - // Special date-diffing logic is used for manipulation of dates. - // If `props` does not contain start/end dates, the updated values are assumed to be the event's current start/end. - // All date comparisons are done against the event's pristine _start and _end dates. - // Returns an object with delta information and a function to undo all operations. - // For making computations in a granularity greater than day/time, specify largeUnit. - // NOTE: The given `newProps` might be mutated for normalization purposes. - function mutateEvent(event, newProps, largeUnit) { - var miscProps = {}; - var oldProps; - var clearEnd; - var startDelta; - var endDelta; - var durationDelta; - var undoFunc; - - // diffs the dates in the appropriate way, returning a duration - function diffDates(date1, date0) { // date1 - date0 - if (largeUnit) { - return diffByUnit(date1, date0, largeUnit); - } - else if (newProps.allDay) { - return diffDay(date1, date0); - } - else { - return diffDayTime(date1, date0); - } - } - - newProps = newProps || {}; - - // normalize new date-related properties - if (!newProps.start) { - newProps.start = event.start.clone(); - } - if (newProps.end === undefined) { - newProps.end = event.end ? event.end.clone() : null; - } - if (newProps.allDay == null) { // is null or undefined? - newProps.allDay = event.allDay; - } - normalizeEventDates(newProps); - - // create normalized versions of the original props to compare against - // need a real end value, for diffing - oldProps = { - start: event._start.clone(), - end: event._end ? event._end.clone() : t.getDefaultEventEnd(event._allDay, event._start), - allDay: newProps.allDay // normalize the dates in the same regard as the new properties - }; - normalizeEventDates(oldProps); - - // need to clear the end date if explicitly changed to null - clearEnd = event._end !== null && newProps.end === null; - - // compute the delta for moving the start date - startDelta = diffDates(newProps.start, oldProps.start); - - // compute the delta for moving the end date - if (newProps.end) { - endDelta = diffDates(newProps.end, oldProps.end); - durationDelta = endDelta.subtract(startDelta); - } - else { - durationDelta = null; - } - - // gather all non-date-related properties - $.each(newProps, function(name, val) { - if (isMiscEventPropName(name)) { - if (val !== undefined) { - miscProps[name] = val; - } - } - }); - - // apply the operations to the event and all related events - undoFunc = mutateEvents( - clientEvents(event._id), // get events with this ID - clearEnd, - newProps.allDay, - startDelta, - durationDelta, - miscProps - ); - - return { - dateDelta: startDelta, - durationDelta: durationDelta, - undo: undoFunc - }; - } - - - // Modifies an array of events in the following ways (operations are in order): - // - clear the event's `end` - // - convert the event to allDay - // - add `dateDelta` to the start and end - // - add `durationDelta` to the event's duration - // - assign `miscProps` to the event - // - // Returns a function that can be called to undo all the operations. - // - // TODO: don't use so many closures. possible memory issues when lots of events with same ID. - // - function mutateEvents(events, clearEnd, allDay, dateDelta, durationDelta, miscProps) { - var isAmbigTimezone = t.getIsAmbigTimezone(); - var undoFunctions = []; - - // normalize zero-length deltas to be null - if (dateDelta && !dateDelta.valueOf()) { dateDelta = null; } - if (durationDelta && !durationDelta.valueOf()) { durationDelta = null; } - - $.each(events, function(i, event) { - var oldProps; - var newProps; - - // build an object holding all the old values, both date-related and misc. - // for the undo function. - oldProps = { - start: event.start.clone(), - end: event.end ? event.end.clone() : null, - allDay: event.allDay - }; - $.each(miscProps, function(name) { - oldProps[name] = event[name]; - }); - - // new date-related properties. work off the original date snapshot. - // ok to use references because they will be thrown away when backupEventDates is called. - newProps = { - start: event._start, - end: event._end, - allDay: allDay // normalize the dates in the same regard as the new properties - }; - normalizeEventDates(newProps); // massages start/end/allDay - - // strip or ensure the end date - if (clearEnd) { - newProps.end = null; - } - else if (durationDelta && !newProps.end) { // the duration translation requires an end date - newProps.end = t.getDefaultEventEnd(newProps.allDay, newProps.start); - } - - if (dateDelta) { - newProps.start.add(dateDelta); - if (newProps.end) { - newProps.end.add(dateDelta); - } - } - - if (durationDelta) { - newProps.end.add(durationDelta); // end already ensured above - } - - // if the dates have changed, and we know it is impossible to recompute the - // timezone offsets, strip the zone. - if ( - isAmbigTimezone && - !newProps.allDay && - (dateDelta || durationDelta) - ) { - newProps.start.stripZone(); - if (newProps.end) { - newProps.end.stripZone(); - } - } - - $.extend(event, miscProps, newProps); // copy over misc props, then date-related props - backupEventDates(event); // regenerate internal _start/_end/_allDay - - undoFunctions.push(function() { - $.extend(event, oldProps); - backupEventDates(event); // regenerate internal _start/_end/_allDay - }); - }); - - return function() { - for (var i = 0; i < undoFunctions.length; i++) { - undoFunctions[i](); - } - }; - } - -} - - -// returns an undo function -Calendar.prototype.mutateSeg = function(seg, newProps) { - return this.mutateEvent(seg.event, newProps); -}; - - -// hook for external libs to manipulate event properties upon creation. -// should manipulate the event in-place. -Calendar.prototype.normalizeEvent = function(event) { -}; - - -// Does the given span (start, end, and other location information) -// fully contain the other? -Calendar.prototype.spanContainsSpan = function(outerSpan, innerSpan) { - var eventStart = outerSpan.start.clone().stripZone(); - var eventEnd = this.getEventEnd(outerSpan).stripZone(); - - return innerSpan.start >= eventStart && innerSpan.end <= eventEnd; -}; - - -// Returns a list of events that the given event should be compared against when being considered for a move to -// the specified span. Attached to the Calendar's prototype because EventManager is a mixin for a Calendar. -Calendar.prototype.getPeerEvents = function(span, event) { - var cache = this.getEventCache(); - var peerEvents = []; - var i, otherEvent; - - for (i = 0; i < cache.length; i++) { - otherEvent = cache[i]; - if ( - !event || - event._id !== otherEvent._id // don't compare the event to itself or other related [repeating] events - ) { - peerEvents.push(otherEvent); - } - } - - return peerEvents; -}; - - -// updates the "backup" properties, which are preserved in order to compute diffs later on. -function backupEventDates(event) { - event._allDay = event.allDay; - event._start = event.start.clone(); - event._end = event.end ? event.end.clone() : null; -} - - -/* Overlapping / Constraining ------------------------------------------------------------------------------------------*/ - - -// Determines if the given event can be relocated to the given span (unzoned start/end with other misc data) -Calendar.prototype.isEventSpanAllowed = function(span, event) { - var source = event.source || {}; - var eventAllowFunc = this.opt('eventAllow'); - - var constraint = firstDefined( - event.constraint, - source.constraint, - this.opt('eventConstraint') - ); - - var overlap = firstDefined( - event.overlap, - source.overlap, - this.opt('eventOverlap') - ); - - return this.isSpanAllowed(span, constraint, overlap, event) && - (!eventAllowFunc || eventAllowFunc(span, event) !== false); -}; - - -// Determines if an external event can be relocated to the given span (unzoned start/end with other misc data) -Calendar.prototype.isExternalSpanAllowed = function(eventSpan, eventLocation, eventProps) { - var eventInput; - var event; - - // note: very similar logic is in View's reportExternalDrop - if (eventProps) { - eventInput = $.extend({}, eventProps, eventLocation); - event = this.expandEvent( - this.buildEventFromInput(eventInput) - )[0]; - } - - if (event) { - return this.isEventSpanAllowed(eventSpan, event); - } - else { // treat it as a selection - - return this.isSelectionSpanAllowed(eventSpan); - } -}; - - -// Determines the given span (unzoned start/end with other misc data) can be selected. -Calendar.prototype.isSelectionSpanAllowed = function(span) { - var selectAllowFunc = this.opt('selectAllow'); - - return this.isSpanAllowed(span, this.opt('selectConstraint'), this.opt('selectOverlap')) && - (!selectAllowFunc || selectAllowFunc(span) !== false); -}; - - -// Returns true if the given span (caused by an event drop/resize or a selection) is allowed to exist -// according to the constraint/overlap settings. -// `event` is not required if checking a selection. -Calendar.prototype.isSpanAllowed = function(span, constraint, overlap, event) { - var constraintEvents; - var anyContainment; - var peerEvents; - var i, peerEvent; - var peerOverlap; - - // the range must be fully contained by at least one of produced constraint events - if (constraint != null) { - - // not treated as an event! intermediate data structure - // TODO: use ranges in the future - constraintEvents = this.constraintToEvents(constraint); - if (constraintEvents) { // not invalid - - anyContainment = false; - for (i = 0; i < constraintEvents.length; i++) { - if (this.spanContainsSpan(constraintEvents[i], span)) { - anyContainment = true; - break; - } - } - - if (!anyContainment) { - return false; - } - } - } - - peerEvents = this.getPeerEvents(span, event); - - for (i = 0; i < peerEvents.length; i++) { - peerEvent = peerEvents[i]; - - // there needs to be an actual intersection before disallowing anything - if (this.eventIntersectsRange(peerEvent, span)) { - - // evaluate overlap for the given range and short-circuit if necessary - if (overlap === false) { - return false; - } - // if the event's overlap is a test function, pass the peer event in question as the first param - else if (typeof overlap === 'function' && !overlap(peerEvent, event)) { - return false; - } - - // if we are computing if the given range is allowable for an event, consider the other event's - // EventObject-specific or Source-specific `overlap` property - if (event) { - peerOverlap = firstDefined( - peerEvent.overlap, - (peerEvent.source || {}).overlap - // we already considered the global `eventOverlap` - ); - if (peerOverlap === false) { - return false; - } - // if the peer event's overlap is a test function, pass the subject event as the first param - if (typeof peerOverlap === 'function' && !peerOverlap(event, peerEvent)) { - return false; - } - } - } - } - - return true; -}; - - -// Given an event input from the API, produces an array of event objects. Possible event inputs: -// 'businessHours' -// An event ID (number or string) -// An object with specific start/end dates or a recurring event (like what businessHours accepts) -Calendar.prototype.constraintToEvents = function(constraintInput) { - - if (constraintInput === 'businessHours') { - return this.getCurrentBusinessHourEvents(); - } - - if (typeof constraintInput === 'object') { - if (constraintInput.start != null) { // needs to be event-like input - return this.expandEvent(this.buildEventFromInput(constraintInput)); - } - else { - return null; // invalid - } - } - - return this.clientEvents(constraintInput); // probably an ID -}; - - -// Does the event's date range intersect with the given range? -// start/end already assumed to have stripped zones :( -Calendar.prototype.eventIntersectsRange = function(event, range) { - var eventStart = event.start.clone().stripZone(); - var eventEnd = this.getEventEnd(event).stripZone(); - - return range.start < eventEnd && range.end > eventStart; -}; - - -/* Business Hours ------------------------------------------------------------------------------------------*/ - -var BUSINESS_HOUR_EVENT_DEFAULTS = { - id: '_fcBusinessHours', // will relate events from different calls to expandEvent - start: '09:00', - end: '17:00', - dow: [ 1, 2, 3, 4, 5 ], // monday - friday - rendering: 'inverse-background' - // classNames are defined in businessHoursSegClasses -}; - -// Return events objects for business hours within the current view. -// Abuse of our event system :( -Calendar.prototype.getCurrentBusinessHourEvents = function(wholeDay) { - return this.computeBusinessHourEvents(wholeDay, this.opt('businessHours')); -}; - -// Given a raw input value from options, return events objects for business hours within the current view. -Calendar.prototype.computeBusinessHourEvents = function(wholeDay, input) { - if (input === true) { - return this.expandBusinessHourEvents(wholeDay, [ {} ]); - } - else if ($.isPlainObject(input)) { - return this.expandBusinessHourEvents(wholeDay, [ input ]); - } - else if ($.isArray(input)) { - return this.expandBusinessHourEvents(wholeDay, input, true); - } - else { - return []; - } -}; - -// inputs expected to be an array of objects. -// if ignoreNoDow is true, will ignore entries that don't specify a day-of-week (dow) key. -Calendar.prototype.expandBusinessHourEvents = function(wholeDay, inputs, ignoreNoDow) { - var view = this.getView(); - var events = []; - var i, input; - - for (i = 0; i < inputs.length; i++) { - input = inputs[i]; - - if (ignoreNoDow && !input.dow) { - continue; - } - - // give defaults. will make a copy - input = $.extend({}, BUSINESS_HOUR_EVENT_DEFAULTS, input); - - // if a whole-day series is requested, clear the start/end times - if (wholeDay) { - input.start = null; - input.end = null; - } - - events.push.apply(events, // append - this.expandEvent( - this.buildEventFromInput(input), - view.activeRange.start, - view.activeRange.end - ) - ); - } - - return events; -}; - -;; - -/* An abstract class for the "basic" views, as well as month view. Renders one or more rows of day cells. -----------------------------------------------------------------------------------------------------------------------*/ -// It is a manager for a DayGrid subcomponent, which does most of the heavy lifting. -// It is responsible for managing width/height. - -var BasicView = FC.BasicView = View.extend({ - - scroller: null, - - dayGridClass: DayGrid, // class the dayGrid will be instantiated from (overridable by subclasses) - dayGrid: null, // the main subcomponent that does most of the heavy lifting - - dayNumbersVisible: false, // display day numbers on each day cell? - colWeekNumbersVisible: false, // display week numbers along the side? - cellWeekNumbersVisible: false, // display week numbers in day cell? - - weekNumberWidth: null, // width of all the week-number cells running down the side - - headContainerEl: null, // div that hold's the dayGrid's rendered date header - headRowEl: null, // the fake row element of the day-of-week header - - - initialize: function() { - this.dayGrid = this.instantiateDayGrid(); - - this.scroller = new Scroller({ - overflowX: 'hidden', - overflowY: 'auto' - }); - }, - - - // Generates the DayGrid object this view needs. Draws from this.dayGridClass - instantiateDayGrid: function() { - // generate a subclass on the fly with BasicView-specific behavior - // TODO: cache this subclass - var subclass = this.dayGridClass.extend(basicDayGridMethods); - - return new subclass(this); - }, - - - // Computes the date range that will be rendered. - buildRenderRange: function(currentRange, currentRangeUnit) { - var renderRange = View.prototype.buildRenderRange.apply(this, arguments); - - // year and month views should be aligned with weeks. this is already done for week - if (/^(year|month)$/.test(currentRangeUnit)) { - renderRange.start.startOf('week'); - - // make end-of-week if not already - if (renderRange.end.weekday()) { - renderRange.end.add(1, 'week').startOf('week'); // exclusively move backwards - } - } - - return this.trimHiddenDays(renderRange); - }, - - - // Renders the view into `this.el`, which should already be assigned - renderDates: function() { - - this.dayGrid.breakOnWeeks = /year|month|week/.test(this.currentRangeUnit); // do before Grid::setRange - this.dayGrid.setRange(this.renderRange); - - this.dayNumbersVisible = this.dayGrid.rowCnt > 1; // TODO: make grid responsible - if (this.opt('weekNumbers')) { - if (this.opt('weekNumbersWithinDays')) { - this.cellWeekNumbersVisible = true; - this.colWeekNumbersVisible = false; - } - else { - this.cellWeekNumbersVisible = false; - this.colWeekNumbersVisible = true; - }; - } - this.dayGrid.numbersVisible = this.dayNumbersVisible || - this.cellWeekNumbersVisible || this.colWeekNumbersVisible; - - this.el.addClass('fc-basic-view').html(this.renderSkeletonHtml()); - this.renderHead(); - - this.scroller.render(); - var dayGridContainerEl = this.scroller.el.addClass('fc-day-grid-container'); - var dayGridEl = $('<div class="fc-day-grid" />').appendTo(dayGridContainerEl); - this.el.find('.fc-body > tr > td').append(dayGridContainerEl); - - this.dayGrid.setElement(dayGridEl); - this.dayGrid.renderDates(this.hasRigidRows()); - }, - - - // render the day-of-week headers - renderHead: function() { - this.headContainerEl = - this.el.find('.fc-head-container') - .html(this.dayGrid.renderHeadHtml()); - this.headRowEl = this.headContainerEl.find('.fc-row'); - }, - - - // Unrenders the content of the view. Since we haven't separated skeleton rendering from date rendering, - // always completely kill the dayGrid's rendering. - unrenderDates: function() { - this.dayGrid.unrenderDates(); - this.dayGrid.removeElement(); - this.scroller.destroy(); - }, - - - renderBusinessHours: function() { - this.dayGrid.renderBusinessHours(); - }, - - - unrenderBusinessHours: function() { - this.dayGrid.unrenderBusinessHours(); - }, - - - // Builds the HTML skeleton for the view. - // The day-grid component will render inside of a container defined by this HTML. - renderSkeletonHtml: function() { - return '' + - '<table>' + - '<thead class="fc-head">' + - '<tr>' + - '<td class="fc-head-container ' + this.widgetHeaderClass + '"></td>' + - '</tr>' + - '</thead>' + - '<tbody class="fc-body">' + - '<tr>' + - '<td class="' + this.widgetContentClass + '"></td>' + - '</tr>' + - '</tbody>' + - '</table>'; - }, - - - // Generates an HTML attribute string for setting the width of the week number column, if it is known - weekNumberStyleAttr: function() { - if (this.weekNumberWidth !== null) { - return 'style="width:' + this.weekNumberWidth + 'px"'; - } - return ''; - }, - - - // Determines whether each row should have a constant height - hasRigidRows: function() { - var eventLimit = this.opt('eventLimit'); - return eventLimit && typeof eventLimit !== 'number'; - }, - - - /* Dimensions - ------------------------------------------------------------------------------------------------------------------*/ - - - // Refreshes the horizontal dimensions of the view - updateWidth: function() { - if (this.colWeekNumbersVisible) { - // Make sure all week number cells running down the side have the same width. - // Record the width for cells created later. - this.weekNumberWidth = matchCellWidths( - this.el.find('.fc-week-number') - ); - } - }, - - - // Adjusts the vertical dimensions of the view to the specified values - setHeight: function(totalHeight, isAuto) { - var eventLimit = this.opt('eventLimit'); - var scrollerHeight; - var scrollbarWidths; - - // reset all heights to be natural - this.scroller.clear(); - uncompensateScroll(this.headRowEl); - - this.dayGrid.removeSegPopover(); // kill the "more" popover if displayed - - // is the event limit a constant level number? - if (eventLimit && typeof eventLimit === 'number') { - this.dayGrid.limitRows(eventLimit); // limit the levels first so the height can redistribute after - } - - // distribute the height to the rows - // (totalHeight is a "recommended" value if isAuto) - scrollerHeight = this.computeScrollerHeight(totalHeight); - this.setGridHeight(scrollerHeight, isAuto); - - // is the event limit dynamically calculated? - if (eventLimit && typeof eventLimit !== 'number') { - this.dayGrid.limitRows(eventLimit); // limit the levels after the grid's row heights have been set - } - - if (!isAuto) { // should we force dimensions of the scroll container? - - this.scroller.setHeight(scrollerHeight); - scrollbarWidths = this.scroller.getScrollbarWidths(); - - if (scrollbarWidths.left || scrollbarWidths.right) { // using scrollbars? - - compensateScroll(this.headRowEl, scrollbarWidths); - - // doing the scrollbar compensation might have created text overflow which created more height. redo - scrollerHeight = this.computeScrollerHeight(totalHeight); - this.scroller.setHeight(scrollerHeight); - } - - // guarantees the same scrollbar widths - this.scroller.lockOverflow(scrollbarWidths); - } - }, - - - // given a desired total height of the view, returns what the height of the scroller should be - computeScrollerHeight: function(totalHeight) { - return totalHeight - - subtractInnerElHeight(this.el, this.scroller.el); // everything that's NOT the scroller - }, - - - // Sets the height of just the DayGrid component in this view - setGridHeight: function(height, isAuto) { - if (isAuto) { - undistributeHeight(this.dayGrid.rowEls); // let the rows be their natural height with no expanding - } - else { - distributeHeight(this.dayGrid.rowEls, height, true); // true = compensate for height-hogging rows - } - }, - - - /* Scroll - ------------------------------------------------------------------------------------------------------------------*/ - - - computeInitialDateScroll: function() { - return { top: 0 }; - }, - - - queryDateScroll: function() { - return { top: this.scroller.getScrollTop() }; - }, - - - applyDateScroll: function(scroll) { - if (scroll.top !== undefined) { - this.scroller.setScrollTop(scroll.top); - } - }, - - - /* Hit Areas - ------------------------------------------------------------------------------------------------------------------*/ - // forward all hit-related method calls to dayGrid - - - hitsNeeded: function() { - this.dayGrid.hitsNeeded(); - }, - - - hitsNotNeeded: function() { - this.dayGrid.hitsNotNeeded(); - }, - - - prepareHits: function() { - this.dayGrid.prepareHits(); - }, - - - releaseHits: function() { - this.dayGrid.releaseHits(); - }, - - - queryHit: function(left, top) { - return this.dayGrid.queryHit(left, top); - }, - - - getHitSpan: function(hit) { - return this.dayGrid.getHitSpan(hit); - }, - - - getHitEl: function(hit) { - return this.dayGrid.getHitEl(hit); - }, - - - /* Events - ------------------------------------------------------------------------------------------------------------------*/ - - - // Renders the given events onto the view and populates the segments array - renderEvents: function(events) { - this.dayGrid.renderEvents(events); - - this.updateHeight(); // must compensate for events that overflow the row - }, - - - // Retrieves all segment objects that are rendered in the view - getEventSegs: function() { - return this.dayGrid.getEventSegs(); - }, - - - // Unrenders all event elements and clears internal segment data - unrenderEvents: function() { - this.dayGrid.unrenderEvents(); - - // we DON'T need to call updateHeight() because - // a renderEvents() call always happens after this, which will eventually call updateHeight() - }, - - - /* Dragging (for both events and external elements) - ------------------------------------------------------------------------------------------------------------------*/ - - - // A returned value of `true` signals that a mock "helper" event has been rendered. - renderDrag: function(dropLocation, seg) { - return this.dayGrid.renderDrag(dropLocation, seg); - }, - - - unrenderDrag: function() { - this.dayGrid.unrenderDrag(); - }, - - - /* Selection - ------------------------------------------------------------------------------------------------------------------*/ - - - // Renders a visual indication of a selection - renderSelection: function(span) { - this.dayGrid.renderSelection(span); - }, - - - // Unrenders a visual indications of a selection - unrenderSelection: function() { - this.dayGrid.unrenderSelection(); - } - -}); - - -// Methods that will customize the rendering behavior of the BasicView's dayGrid -var basicDayGridMethods = { - - - // Generates the HTML that will go before the day-of week header cells - renderHeadIntroHtml: function() { - var view = this.view; - - if (view.colWeekNumbersVisible) { - return '' + - '<th class="fc-week-number ' + view.widgetHeaderClass + '" ' + view.weekNumberStyleAttr() + '>' + - '<span>' + // needed for matchCellWidths - htmlEscape(view.opt('weekNumberTitle')) + - '</span>' + - '</th>'; - } - - return ''; - }, - - - // Generates the HTML that will go before content-skeleton cells that display the day/week numbers - renderNumberIntroHtml: function(row) { - var view = this.view; - var weekStart = this.getCellDate(row, 0); - - if (view.colWeekNumbersVisible) { - return '' + - '<td class="fc-week-number" ' + view.weekNumberStyleAttr() + '>' + - view.buildGotoAnchorHtml( // aside from link, important for matchCellWidths - { date: weekStart, type: 'week', forceOff: this.colCnt === 1 }, - weekStart.format('w') // inner HTML - ) + - '</td>'; - } - - return ''; - }, - - - // Generates the HTML that goes before the day bg cells for each day-row - renderBgIntroHtml: function() { - var view = this.view; - - if (view.colWeekNumbersVisible) { - return '<td class="fc-week-number ' + view.widgetContentClass + '" ' + - view.weekNumberStyleAttr() + '></td>'; - } - - return ''; - }, - - - // Generates the HTML that goes before every other type of row generated by DayGrid. - // Affects helper-skeleton and highlight-skeleton rows. - renderIntroHtml: function() { - var view = this.view; - - if (view.colWeekNumbersVisible) { - return '<td class="fc-week-number" ' + view.weekNumberStyleAttr() + '></td>'; - } - - return ''; - } - -}; - -;; - -/* A month view with day cells running in rows (one-per-week) and columns -----------------------------------------------------------------------------------------------------------------------*/ - -var MonthView = FC.MonthView = BasicView.extend({ - - - // Computes the date range that will be rendered. - buildRenderRange: function() { - var renderRange = BasicView.prototype.buildRenderRange.apply(this, arguments); - var rowCnt; - - // ensure 6 weeks - if (this.isFixedWeeks()) { - rowCnt = Math.ceil( // could be partial weeks due to hiddenDays - renderRange.end.diff(renderRange.start, 'weeks', true) // dontRound=true - ); - renderRange.end.add(6 - rowCnt, 'weeks'); - } - - return renderRange; - }, - - - // Overrides the default BasicView behavior to have special multi-week auto-height logic - setGridHeight: function(height, isAuto) { - - // if auto, make the height of each row the height that it would be if there were 6 weeks - if (isAuto) { - height *= this.rowCnt / 6; - } - - distributeHeight(this.dayGrid.rowEls, height, !isAuto); // if auto, don't compensate for height-hogging rows - }, - - - isFixedWeeks: function() { - return this.opt('fixedWeekCount'); - } - -}); - -;; - -fcViews.basic = { - 'class': BasicView -}; - -fcViews.basicDay = { - type: 'basic', - duration: { days: 1 } -}; - -fcViews.basicWeek = { - type: 'basic', - duration: { weeks: 1 } -}; - -fcViews.month = { - 'class': MonthView, - duration: { months: 1 }, // important for prev/next - defaults: { - fixedWeekCount: true - } -}; -;; - -/* An abstract class for all agenda-related views. Displays one more columns with time slots running vertically. -----------------------------------------------------------------------------------------------------------------------*/ -// Is a manager for the TimeGrid subcomponent and possibly the DayGrid subcomponent (if allDaySlot is on). -// Responsible for managing width/height. - -var AgendaView = FC.AgendaView = View.extend({ - - scroller: null, - - timeGridClass: TimeGrid, // class used to instantiate the timeGrid. subclasses can override - timeGrid: null, // the main time-grid subcomponent of this view - - dayGridClass: DayGrid, // class used to instantiate the dayGrid. subclasses can override - dayGrid: null, // the "all-day" subcomponent. if all-day is turned off, this will be null - - axisWidth: null, // the width of the time axis running down the side - - headContainerEl: null, // div that hold's the timeGrid's rendered date header - noScrollRowEls: null, // set of fake row elements that must compensate when scroller has scrollbars - - // when the time-grid isn't tall enough to occupy the given height, we render an <hr> underneath - bottomRuleEl: null, - - // indicates that minTime/maxTime affects rendering - usesMinMaxTime: true, - - - initialize: function() { - this.timeGrid = this.instantiateTimeGrid(); - - if (this.opt('allDaySlot')) { // should we display the "all-day" area? - this.dayGrid = this.instantiateDayGrid(); // the all-day subcomponent of this view - } - - this.scroller = new Scroller({ - overflowX: 'hidden', - overflowY: 'auto' - }); - }, - - - // Instantiates the TimeGrid object this view needs. Draws from this.timeGridClass - instantiateTimeGrid: function() { - var subclass = this.timeGridClass.extend(agendaTimeGridMethods); - - return new subclass(this); - }, - - - // Instantiates the DayGrid object this view might need. Draws from this.dayGridClass - instantiateDayGrid: function() { - var subclass = this.dayGridClass.extend(agendaDayGridMethods); - - return new subclass(this); - }, - - - /* Rendering - ------------------------------------------------------------------------------------------------------------------*/ - - - // Renders the view into `this.el`, which has already been assigned - renderDates: function() { - - this.timeGrid.setRange(this.renderRange); - - if (this.dayGrid) { - this.dayGrid.setRange(this.renderRange); - } - - this.el.addClass('fc-agenda-view').html(this.renderSkeletonHtml()); - this.renderHead(); - - this.scroller.render(); - var timeGridWrapEl = this.scroller.el.addClass('fc-time-grid-container'); - var timeGridEl = $('<div class="fc-time-grid" />').appendTo(timeGridWrapEl); - this.el.find('.fc-body > tr > td').append(timeGridWrapEl); - - this.timeGrid.setElement(timeGridEl); - this.timeGrid.renderDates(); - - // the <hr> that sometimes displays under the time-grid - this.bottomRuleEl = $('<hr class="fc-divider ' + this.widgetHeaderClass + '"/>') - .appendTo(this.timeGrid.el); // inject it into the time-grid - - if (this.dayGrid) { - this.dayGrid.setElement(this.el.find('.fc-day-grid')); - this.dayGrid.renderDates(); - - // have the day-grid extend it's coordinate area over the <hr> dividing the two grids - this.dayGrid.bottomCoordPadding = this.dayGrid.el.next('hr').outerHeight(); - } - - this.noScrollRowEls = this.el.find('.fc-row:not(.fc-scroller *)'); // fake rows not within the scroller - }, - - - // render the day-of-week headers - renderHead: function() { - this.headContainerEl = - this.el.find('.fc-head-container') - .html(this.timeGrid.renderHeadHtml()); - }, - - - // Unrenders the content of the view. Since we haven't separated skeleton rendering from date rendering, - // always completely kill each grid's rendering. - unrenderDates: function() { - this.timeGrid.unrenderDates(); - this.timeGrid.removeElement(); - - if (this.dayGrid) { - this.dayGrid.unrenderDates(); - this.dayGrid.removeElement(); - } - - this.scroller.destroy(); - }, - - - // Builds the HTML skeleton for the view. - // The day-grid and time-grid components will render inside containers defined by this HTML. - renderSkeletonHtml: function() { - return '' + - '<table>' + - '<thead class="fc-head">' + - '<tr>' + - '<td class="fc-head-container ' + this.widgetHeaderClass + '"></td>' + - '</tr>' + - '</thead>' + - '<tbody class="fc-body">' + - '<tr>' + - '<td class="' + this.widgetContentClass + '">' + - (this.dayGrid ? - '<div class="fc-day-grid"/>' + - '<hr class="fc-divider ' + this.widgetHeaderClass + '"/>' : - '' - ) + - '</td>' + - '</tr>' + - '</tbody>' + - '</table>'; - }, - - - // Generates an HTML attribute string for setting the width of the axis, if it is known - axisStyleAttr: function() { - if (this.axisWidth !== null) { - return 'style="width:' + this.axisWidth + 'px"'; - } - return ''; - }, - - - /* Business Hours - ------------------------------------------------------------------------------------------------------------------*/ - - - renderBusinessHours: function() { - this.timeGrid.renderBusinessHours(); - - if (this.dayGrid) { - this.dayGrid.renderBusinessHours(); - } - }, - - - unrenderBusinessHours: function() { - this.timeGrid.unrenderBusinessHours(); - - if (this.dayGrid) { - this.dayGrid.unrenderBusinessHours(); - } - }, - - - /* Now Indicator - ------------------------------------------------------------------------------------------------------------------*/ - - - getNowIndicatorUnit: function() { - return this.timeGrid.getNowIndicatorUnit(); - }, - - - renderNowIndicator: function(date) { - this.timeGrid.renderNowIndicator(date); - }, - - - unrenderNowIndicator: function() { - this.timeGrid.unrenderNowIndicator(); - }, - - - /* Dimensions - ------------------------------------------------------------------------------------------------------------------*/ - - - updateSize: function(isResize) { - this.timeGrid.updateSize(isResize); - - View.prototype.updateSize.call(this, isResize); // call the super-method - }, - - - // Refreshes the horizontal dimensions of the view - updateWidth: function() { - // make all axis cells line up, and record the width so newly created axis cells will have it - this.axisWidth = matchCellWidths(this.el.find('.fc-axis')); - }, - - - // Adjusts the vertical dimensions of the view to the specified values - setHeight: function(totalHeight, isAuto) { - var eventLimit; - var scrollerHeight; - var scrollbarWidths; - - // reset all dimensions back to the original state - this.bottomRuleEl.hide(); // .show() will be called later if this <hr> is necessary - this.scroller.clear(); // sets height to 'auto' and clears overflow - uncompensateScroll(this.noScrollRowEls); - - // limit number of events in the all-day area - if (this.dayGrid) { - this.dayGrid.removeSegPopover(); // kill the "more" popover if displayed - - eventLimit = this.opt('eventLimit'); - if (eventLimit && typeof eventLimit !== 'number') { - eventLimit = AGENDA_ALL_DAY_EVENT_LIMIT; // make sure "auto" goes to a real number - } - if (eventLimit) { - this.dayGrid.limitRows(eventLimit); - } - } - - if (!isAuto) { // should we force dimensions of the scroll container? - - scrollerHeight = this.computeScrollerHeight(totalHeight); - this.scroller.setHeight(scrollerHeight); - scrollbarWidths = this.scroller.getScrollbarWidths(); - - if (scrollbarWidths.left || scrollbarWidths.right) { // using scrollbars? - - // make the all-day and header rows lines up - compensateScroll(this.noScrollRowEls, scrollbarWidths); - - // the scrollbar compensation might have changed text flow, which might affect height, so recalculate - // and reapply the desired height to the scroller. - scrollerHeight = this.computeScrollerHeight(totalHeight); - this.scroller.setHeight(scrollerHeight); - } - - // guarantees the same scrollbar widths - this.scroller.lockOverflow(scrollbarWidths); - - // if there's any space below the slats, show the horizontal rule. - // this won't cause any new overflow, because lockOverflow already called. - if (this.timeGrid.getTotalSlatHeight() < scrollerHeight) { - this.bottomRuleEl.show(); - } - } - }, - - - // given a desired total height of the view, returns what the height of the scroller should be - computeScrollerHeight: function(totalHeight) { - return totalHeight - - subtractInnerElHeight(this.el, this.scroller.el); // everything that's NOT the scroller - }, - - - /* Scroll - ------------------------------------------------------------------------------------------------------------------*/ - - - // Computes the initial pre-configured scroll state prior to allowing the user to change it - computeInitialDateScroll: function() { - var scrollTime = moment.duration(this.opt('scrollTime')); - var top = this.timeGrid.computeTimeTop(scrollTime); - - // zoom can give weird floating-point values. rather scroll a little bit further - top = Math.ceil(top); - - if (top) { - top++; // to overcome top border that slots beyond the first have. looks better - } - - return { top: top }; - }, - - - queryDateScroll: function() { - return { top: this.scroller.getScrollTop() }; - }, - - - applyDateScroll: function(scroll) { - if (scroll.top !== undefined) { - this.scroller.setScrollTop(scroll.top); - } - }, - - - /* Hit Areas - ------------------------------------------------------------------------------------------------------------------*/ - // forward all hit-related method calls to the grids (dayGrid might not be defined) - - - hitsNeeded: function() { - this.timeGrid.hitsNeeded(); - if (this.dayGrid) { - this.dayGrid.hitsNeeded(); - } - }, - - - hitsNotNeeded: function() { - this.timeGrid.hitsNotNeeded(); - if (this.dayGrid) { - this.dayGrid.hitsNotNeeded(); - } - }, - - - prepareHits: function() { - this.timeGrid.prepareHits(); - if (this.dayGrid) { - this.dayGrid.prepareHits(); - } - }, - - - releaseHits: function() { - this.timeGrid.releaseHits(); - if (this.dayGrid) { - this.dayGrid.releaseHits(); - } - }, - - - queryHit: function(left, top) { - var hit = this.timeGrid.queryHit(left, top); - - if (!hit && this.dayGrid) { - hit = this.dayGrid.queryHit(left, top); - } - - return hit; - }, - - - getHitSpan: function(hit) { - // TODO: hit.component is set as a hack to identify where the hit came from - return hit.component.getHitSpan(hit); - }, - - - getHitEl: function(hit) { - // TODO: hit.component is set as a hack to identify where the hit came from - return hit.component.getHitEl(hit); - }, - - - /* Events - ------------------------------------------------------------------------------------------------------------------*/ - - - // Renders events onto the view and populates the View's segment array - renderEvents: function(events) { - var dayEvents = []; - var timedEvents = []; - var daySegs = []; - var timedSegs; - var i; - - // separate the events into all-day and timed - for (i = 0; i < events.length; i++) { - if (events[i].allDay) { - dayEvents.push(events[i]); - } - else { - timedEvents.push(events[i]); - } - } - - // render the events in the subcomponents - timedSegs = this.timeGrid.renderEvents(timedEvents); - if (this.dayGrid) { - daySegs = this.dayGrid.renderEvents(dayEvents); - } - - // the all-day area is flexible and might have a lot of events, so shift the height - this.updateHeight(); - }, - - - // Retrieves all segment objects that are rendered in the view - getEventSegs: function() { - return this.timeGrid.getEventSegs().concat( - this.dayGrid ? this.dayGrid.getEventSegs() : [] - ); - }, - - - // Unrenders all event elements and clears internal segment data - unrenderEvents: function() { - - // unrender the events in the subcomponents - this.timeGrid.unrenderEvents(); - if (this.dayGrid) { - this.dayGrid.unrenderEvents(); - } - - // we DON'T need to call updateHeight() because - // a renderEvents() call always happens after this, which will eventually call updateHeight() - }, - - - /* Dragging (for events and external elements) - ------------------------------------------------------------------------------------------------------------------*/ - - - // A returned value of `true` signals that a mock "helper" event has been rendered. - renderDrag: function(dropLocation, seg) { - if (dropLocation.start.hasTime()) { - return this.timeGrid.renderDrag(dropLocation, seg); - } - else if (this.dayGrid) { - return this.dayGrid.renderDrag(dropLocation, seg); - } - }, - - - unrenderDrag: function() { - this.timeGrid.unrenderDrag(); - if (this.dayGrid) { - this.dayGrid.unrenderDrag(); - } - }, - - - /* Selection - ------------------------------------------------------------------------------------------------------------------*/ - - - // Renders a visual indication of a selection - renderSelection: function(span) { - if (span.start.hasTime() || span.end.hasTime()) { - this.timeGrid.renderSelection(span); - } - else if (this.dayGrid) { - this.dayGrid.renderSelection(span); - } - }, - - - // Unrenders a visual indications of a selection - unrenderSelection: function() { - this.timeGrid.unrenderSelection(); - if (this.dayGrid) { - this.dayGrid.unrenderSelection(); - } - } - -}); - - -// Methods that will customize the rendering behavior of the AgendaView's timeGrid -// TODO: move into TimeGrid -var agendaTimeGridMethods = { - - - // Generates the HTML that will go before the day-of week header cells - renderHeadIntroHtml: function() { - var view = this.view; - var weekText; - - if (view.opt('weekNumbers')) { - weekText = this.start.format(view.opt('smallWeekFormat')); - - return '' + - '<th class="fc-axis fc-week-number ' + view.widgetHeaderClass + '" ' + view.axisStyleAttr() + '>' + - view.buildGotoAnchorHtml( // aside from link, important for matchCellWidths - { date: this.start, type: 'week', forceOff: this.colCnt > 1 }, - htmlEscape(weekText) // inner HTML - ) + - '</th>'; - } - else { - return '<th class="fc-axis ' + view.widgetHeaderClass + '" ' + view.axisStyleAttr() + '></th>'; - } - }, - - - // Generates the HTML that goes before the bg of the TimeGrid slot area. Long vertical column. - renderBgIntroHtml: function() { - var view = this.view; - - return '<td class="fc-axis ' + view.widgetContentClass + '" ' + view.axisStyleAttr() + '></td>'; - }, - - - // Generates the HTML that goes before all other types of cells. - // Affects content-skeleton, helper-skeleton, highlight-skeleton for both the time-grid and day-grid. - renderIntroHtml: function() { - var view = this.view; - - return '<td class="fc-axis" ' + view.axisStyleAttr() + '></td>'; - } - -}; - - -// Methods that will customize the rendering behavior of the AgendaView's dayGrid -var agendaDayGridMethods = { - - - // Generates the HTML that goes before the all-day cells - renderBgIntroHtml: function() { - var view = this.view; - - return '' + - '<td class="fc-axis ' + view.widgetContentClass + '" ' + view.axisStyleAttr() + '>' + - '<span>' + // needed for matchCellWidths - view.getAllDayHtml() + - '</span>' + - '</td>'; - }, - - - // Generates the HTML that goes before all other types of cells. - // Affects content-skeleton, helper-skeleton, highlight-skeleton for both the time-grid and day-grid. - renderIntroHtml: function() { - var view = this.view; - - return '<td class="fc-axis" ' + view.axisStyleAttr() + '></td>'; - } - -}; - -;; - -var AGENDA_ALL_DAY_EVENT_LIMIT = 5; - -// potential nice values for the slot-duration and interval-duration -// from largest to smallest -var AGENDA_STOCK_SUB_DURATIONS = [ - { hours: 1 }, - { minutes: 30 }, - { minutes: 15 }, - { seconds: 30 }, - { seconds: 15 } -]; - -fcViews.agenda = { - 'class': AgendaView, - defaults: { - allDaySlot: true, - slotDuration: '00:30:00', - slotEventOverlap: true // a bad name. confused with overlap/constraint system - } -}; - -fcViews.agendaDay = { - type: 'agenda', - duration: { days: 1 } -}; - -fcViews.agendaWeek = { - type: 'agenda', - duration: { weeks: 1 } -}; -;; - -/* -Responsible for the scroller, and forwarding event-related actions into the "grid" -*/ -var ListView = View.extend({ - - grid: null, - scroller: null, - - initialize: function() { - this.grid = new ListViewGrid(this); - this.scroller = new Scroller({ - overflowX: 'hidden', - overflowY: 'auto' - }); - }, - - renderSkeleton: function() { - this.el.addClass( - 'fc-list-view ' + - this.widgetContentClass - ); - - this.scroller.render(); - this.scroller.el.appendTo(this.el); - - this.grid.setElement(this.scroller.scrollEl); - }, - - unrenderSkeleton: function() { - this.scroller.destroy(); // will remove the Grid too - }, - - setHeight: function(totalHeight, isAuto) { - this.scroller.setHeight(this.computeScrollerHeight(totalHeight)); - }, - - computeScrollerHeight: function(totalHeight) { - return totalHeight - - subtractInnerElHeight(this.el, this.scroller.el); // everything that's NOT the scroller - }, - - renderDates: function() { - this.grid.setRange(this.renderRange); // needs to process range-related options - }, - - renderEvents: function(events) { - this.grid.renderEvents(events); - }, - - unrenderEvents: function() { - this.grid.unrenderEvents(); - }, - - isEventResizable: function(event) { - return false; - }, - - isEventDraggable: function(event) { - return false; - } - -}); - -/* -Responsible for event rendering and user-interaction. -Its "el" is the inner-content of the above view's scroller. -*/ -var ListViewGrid = Grid.extend({ - - segSelector: '.fc-list-item', // which elements accept event actions - hasDayInteractions: false, // no day selection or day clicking - - // slices by day - spanToSegs: function(span) { - var view = this.view; - var dayStart = view.renderRange.start.clone().time(0); // timed, so segs get times! - var dayIndex = 0; - var seg; - var segs = []; - - while (dayStart < view.renderRange.end) { - - seg = intersectRanges(span, { - start: dayStart, - end: dayStart.clone().add(1, 'day') - }); - - if (seg) { - seg.dayIndex = dayIndex; - segs.push(seg); - } - - dayStart.add(1, 'day'); - dayIndex++; - - // detect when span won't go fully into the next day, - // and mutate the latest seg to the be the end. - if ( - seg && !seg.isEnd && span.end.hasTime() && - span.end < dayStart.clone().add(this.view.nextDayThreshold) - ) { - seg.end = span.end.clone(); - seg.isEnd = true; - break; - } - } - - return segs; - }, - - // like "4:00am" - computeEventTimeFormat: function() { - return this.view.opt('mediumTimeFormat'); - }, - - // for events with a url, the whole <tr> should be clickable, - // but it's impossible to wrap with an <a> tag. simulate this. - handleSegClick: function(seg, ev) { - var url; - - Grid.prototype.handleSegClick.apply(this, arguments); // super. might prevent the default action - - // not clicking on or within an <a> with an href - if (!$(ev.target).closest('a[href]').length) { - url = seg.event.url; - if (url && !ev.isDefaultPrevented()) { // jsEvent not cancelled in handler - window.location.href = url; // simulate link click - } - } - }, - - // returns list of foreground segs that were actually rendered - renderFgSegs: function(segs) { - segs = this.renderFgSegEls(segs); // might filter away hidden events - - if (!segs.length) { - this.renderEmptyMessage(); - } - else { - this.renderSegList(segs); - } - - return segs; - }, - - renderEmptyMessage: function() { - this.el.html( - '<div class="fc-list-empty-wrap2">' + // TODO: try less wraps - '<div class="fc-list-empty-wrap1">' + - '<div class="fc-list-empty">' + - htmlEscape(this.view.opt('noEventsMessage')) + - '</div>' + - '</div>' + - '</div>' - ); - }, - - // render the event segments in the view - renderSegList: function(allSegs) { - var segsByDay = this.groupSegsByDay(allSegs); // sparse array - var dayIndex; - var daySegs; - var i; - var tableEl = $('<table class="fc-list-table"><tbody/></table>'); - var tbodyEl = tableEl.find('tbody'); - - for (dayIndex = 0; dayIndex < segsByDay.length; dayIndex++) { - daySegs = segsByDay[dayIndex]; - if (daySegs) { // sparse array, so might be undefined - - // append a day header - tbodyEl.append(this.dayHeaderHtml( - this.view.renderRange.start.clone().add(dayIndex, 'days') - )); - - this.sortEventSegs(daySegs); - - for (i = 0; i < daySegs.length; i++) { - tbodyEl.append(daySegs[i].el); // append event row - } - } - } - - this.el.empty().append(tableEl); - }, - - // Returns a sparse array of arrays, segs grouped by their dayIndex - groupSegsByDay: function(segs) { - var segsByDay = []; // sparse array - var i, seg; - - for (i = 0; i < segs.length; i++) { - seg = segs[i]; - (segsByDay[seg.dayIndex] || (segsByDay[seg.dayIndex] = [])) - .push(seg); - } - - return segsByDay; - }, - - // generates the HTML for the day headers that live amongst the event rows - dayHeaderHtml: function(dayDate) { - var view = this.view; - var mainFormat = view.opt('listDayFormat'); - var altFormat = view.opt('listDayAltFormat'); - - return '<tr class="fc-list-heading" data-date="' + dayDate.format('YYYY-MM-DD') + '">' + - '<td class="' + view.widgetHeaderClass + '" colspan="3">' + - (mainFormat ? - view.buildGotoAnchorHtml( - dayDate, - { 'class': 'fc-list-heading-main' }, - htmlEscape(dayDate.format(mainFormat)) // inner HTML - ) : - '') + - (altFormat ? - view.buildGotoAnchorHtml( - dayDate, - { 'class': 'fc-list-heading-alt' }, - htmlEscape(dayDate.format(altFormat)) // inner HTML - ) : - '') + - '</td>' + - '</tr>'; - }, - - // generates the HTML for a single event row - fgSegHtml: function(seg) { - var view = this.view; - var classes = [ 'fc-list-item' ].concat(this.getSegCustomClasses(seg)); - var bgColor = this.getSegBackgroundColor(seg); - var event = seg.event; - var url = event.url; - var timeHtml; - - if (event.allDay) { - timeHtml = view.getAllDayHtml(); - } - else if (view.isMultiDayEvent(event)) { // if the event appears to span more than one day - if (seg.isStart || seg.isEnd) { // outer segment that probably lasts part of the day - timeHtml = htmlEscape(this.getEventTimeText(seg)); - } - else { // inner segment that lasts the whole day - timeHtml = view.getAllDayHtml(); - } - } - else { - // Display the normal time text for the *event's* times - timeHtml = htmlEscape(this.getEventTimeText(event)); - } - - if (url) { - classes.push('fc-has-url'); - } - - return '<tr class="' + classes.join(' ') + '">' + - (this.displayEventTime ? - '<td class="fc-list-item-time ' + view.widgetContentClass + '">' + - (timeHtml || '') + - '</td>' : - '') + - '<td class="fc-list-item-marker ' + view.widgetContentClass + '">' + - '<span class="fc-event-dot"' + - (bgColor ? - ' style="background-color:' + bgColor + '"' : - '') + - '></span>' + - '</td>' + - '<td class="fc-list-item-title ' + view.widgetContentClass + '">' + - '<a' + (url ? ' href="' + htmlEscape(url) + '"' : '') + '>' + - htmlEscape(seg.event.title || '') + - '</a>' + - '</td>' + - '</tr>'; - } - -}); - -;; - -fcViews.list = { - 'class': ListView, - buttonTextKey: 'list', // what to lookup in locale files - defaults: { - buttonText: 'list', // text to display for English - listDayFormat: 'LL', // like "January 1, 2016" - noEventsMessage: 'No events to display' - } -}; - -fcViews.listDay = { - type: 'list', - duration: { days: 1 }, - defaults: { - listDayFormat: 'dddd' // day-of-week is all we need. full date is probably in header - } -}; - -fcViews.listWeek = { - type: 'list', - duration: { weeks: 1 }, - defaults: { - listDayFormat: 'dddd', // day-of-week is more important - listDayAltFormat: 'LL' - } -}; - -fcViews.listMonth = { - type: 'list', - duration: { month: 1 }, - defaults: { - listDayAltFormat: 'dddd' // day-of-week is nice-to-have - } -}; - -fcViews.listYear = { - type: 'list', - duration: { year: 1 }, - defaults: { - listDayAltFormat: 'dddd' // day-of-week is nice-to-have - } -}; - -;; - -return FC; // export for Node/CommonJS -}); \ No newline at end of file diff --git a/src/UI/JsLibraries/handlebars.helpers.js b/src/UI/JsLibraries/handlebars.helpers.js deleted file mode 100644 index 56df9b642..000000000 --- a/src/UI/JsLibraries/handlebars.helpers.js +++ /dev/null @@ -1,145 +0,0 @@ -/* Handlebars Helpers - Dan Harper (http://github.com/danharper) */ - -/* This program is free software. It comes without any warranty, to - * the extent permitted by applicable law. You can redistribute it - * and/or modify it under the terms of the Do What The Fuck You Want - * To Public License, Version 2, as published by Sam Hocevar. See - * http://sam.zoy.org/wtfpl/COPYING for more details. */ - -/** - * Following lines make Handlebars helper function to work with all - * three such as Direct web, RequireJS AMD and Node JS. - * This concepts derived from UMD. - * @courtesy - https://github.com/umdjs/umd/blob/master/returnExports.js - */ - -(function (root, factory) { - if (typeof exports === 'object') { - // Node. Does not work with strict CommonJS, but - // only CommonJS-like enviroments that support module.exports, - // like Node. - module.exports = factory(require('handlebars')); - } else if (typeof define === 'function' && define.amd) { - // AMD. Register as an anonymous module. - define(['handlebars'], factory); - } else { - // Browser globals (root is window) - root.returnExports = factory(root.Handlebars); - } -}(this, function (Handlebars) { - - /** - * If Equals - * if_eq this compare=that - */ - Handlebars.registerHelper('if_eq', function(context, options) { - if (context == options.hash.compare) - return options.fn(this); - return options.inverse(this); - }); - - /** - * Unless Equals - * unless_eq this compare=that - */ - Handlebars.registerHelper('unless_eq', function(context, options) { - if (context == options.hash.compare) - return options.inverse(this); - return options.fn(this); - }); - - - /** - * If Greater Than - * if_gt this compare=that - */ - Handlebars.registerHelper('if_gt', function(context, options) { - if (context > options.hash.compare) - return options.fn(this); - return options.inverse(this); - }); - - /** - * Unless Greater Than - * unless_gt this compare=that - */ - Handlebars.registerHelper('unless_gt', function(context, options) { - if (context > options.hash.compare) - return options.inverse(this); - return options.fn(this); - }); - - - /** - * If Less Than - * if_lt this compare=that - */ - Handlebars.registerHelper('if_lt', function(context, options) { - if (context < options.hash.compare) - return options.fn(this); - return options.inverse(this); - }); - - /** - * Unless Less Than - * unless_lt this compare=that - */ - Handlebars.registerHelper('unless_lt', function(context, options) { - if (context < options.hash.compare) - return options.inverse(this); - return options.fn(this); - }); - - - /** - * If Greater Than or Equal To - * if_gteq this compare=that - */ - Handlebars.registerHelper('if_gteq', function(context, options) { - if (context >= options.hash.compare) - return options.fn(this); - return options.inverse(this); - }); - - /** - * Unless Greater Than or Equal To - * unless_gteq this compare=that - */ - Handlebars.registerHelper('unless_gteq', function(context, options) { - if (context >= options.hash.compare) - return options.inverse(this); - return options.fn(this); - }); - - - /** - * If Less Than or Equal To - * if_lteq this compare=that - */ - Handlebars.registerHelper('if_lteq', function(context, options) { - if (context <= options.hash.compare) - return options.fn(this); - return options.inverse(this); - }); - - /** - * Unless Less Than or Equal To - * unless_lteq this compare=that - */ - Handlebars.registerHelper('unless_lteq', function(context, options) { - if (context <= options.hash.compare) - return options.inverse(this); - return options.fn(this); - }); - - /** - * Convert new line (\n\r) to <br> - * from http://phpjs.org/functions/nl2br:480 - */ - Handlebars.registerHelper('nl2br', function(text) { - text = Handlebars.Utils.escapeExpression(text); - var nl2br = (text + '').replace(/([^>\r\n]?)(\r\n|\n\r|\r|\n)/g, '$1' + '<br>' + '$2'); - return new Handlebars.SafeString(nl2br); - }); - -})); \ No newline at end of file diff --git a/src/UI/JsLibraries/handlebars.runtime.js b/src/UI/JsLibraries/handlebars.runtime.js deleted file mode 100644 index 94af5a379..000000000 --- a/src/UI/JsLibraries/handlebars.runtime.js +++ /dev/null @@ -1,660 +0,0 @@ -/*! - - handlebars v2.0.0 - - Copyright (C) 2011-2014 by Yehuda Katz - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in - all copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - THE SOFTWARE. - - @license - */ -/* exported Handlebars */ -(function (root, factory) { - if (typeof define === 'function' && define.amd) { - define([], factory); - } else if (typeof exports === 'object') { - module.exports = factory(); - } else { - root.Handlebars = root.Handlebars || factory(); - } -}(this, function () { - // handlebars/safe-string.js - var __module3__ = (function() { - "use strict"; - var __exports__; - // Build out our basic SafeString type - function SafeString(string) { - this.string = string; - } - - SafeString.prototype.toString = function() { - return "" + this.string; - }; - - __exports__ = SafeString; - return __exports__; - })(); - - // handlebars/utils.js - var __module2__ = (function(__dependency1__) { - "use strict"; - var __exports__ = {}; - /*jshint -W004 */ - var SafeString = __dependency1__; - - var escape = { - "&": "&", - "<": "<", - ">": ">", - '"': """, - "'": "'", - "`": "`" - }; - - var badChars = /[&<>"'`]/g; - var possible = /[&<>"'`]/; - - function escapeChar(chr) { - return escape[chr]; - } - - function extend(obj /* , ...source */) { - for (var i = 1; i < arguments.length; i++) { - for (var key in arguments[i]) { - if (Object.prototype.hasOwnProperty.call(arguments[i], key)) { - obj[key] = arguments[i][key]; - } - } - } - - return obj; - } - - __exports__.extend = extend;var toString = Object.prototype.toString; - __exports__.toString = toString; - // Sourced from lodash - // https://github.com/bestiejs/lodash/blob/master/LICENSE.txt - var isFunction = function(value) { - return typeof value === 'function'; - }; - // fallback for older versions of Chrome and Safari - /* istanbul ignore next */ - if (isFunction(/x/)) { - isFunction = function(value) { - return typeof value === 'function' && toString.call(value) === '[object Function]'; - }; - } - var isFunction; - __exports__.isFunction = isFunction; - /* istanbul ignore next */ - var isArray = Array.isArray || function(value) { - return (value && typeof value === 'object') ? toString.call(value) === '[object Array]' : false; - }; - __exports__.isArray = isArray; - - function escapeExpression(string) { - // don't escape SafeStrings, since they're already safe - if (string instanceof SafeString) { - return string.toString(); - } else if (string == null) { - return ""; - } else if (!string) { - return string + ''; - } - - // Force a string conversion as this will be done by the append regardless and - // the regex test will do this transparently behind the scenes, causing issues if - // an object's to string has escaped characters in it. - string = "" + string; - - if(!possible.test(string)) { return string; } - return string.replace(badChars, escapeChar); - } - - __exports__.escapeExpression = escapeExpression;function isEmpty(value) { - if (!value && value !== 0) { - return true; - } else if (isArray(value) && value.length === 0) { - return true; - } else { - return false; - } - } - - __exports__.isEmpty = isEmpty;function appendContextPath(contextPath, id) { - return (contextPath ? contextPath + '.' : '') + id; - } - - __exports__.appendContextPath = appendContextPath; - return __exports__; - })(__module3__); - - // handlebars/exception.js - var __module4__ = (function() { - "use strict"; - var __exports__; - - var errorProps = ['description', 'fileName', 'lineNumber', 'message', 'name', 'number', 'stack']; - - function Exception(message, node) { - var line; - if (node && node.firstLine) { - line = node.firstLine; - - message += ' - ' + line + ':' + node.firstColumn; - } - - var tmp = Error.prototype.constructor.call(this, message); - - // Unfortunately errors are not enumerable in Chrome (at least), so `for prop in tmp` doesn't work. - for (var idx = 0; idx < errorProps.length; idx++) { - this[errorProps[idx]] = tmp[errorProps[idx]]; - } - - if (line) { - this.lineNumber = line; - this.column = node.firstColumn; - } - } - - Exception.prototype = new Error(); - - __exports__ = Exception; - return __exports__; - })(); - - // handlebars/base.js - var __module1__ = (function(__dependency1__, __dependency2__) { - "use strict"; - var __exports__ = {}; - var Utils = __dependency1__; - var Exception = __dependency2__; - - var VERSION = "2.0.0"; - __exports__.VERSION = VERSION;var COMPILER_REVISION = 6; - __exports__.COMPILER_REVISION = COMPILER_REVISION; - var REVISION_CHANGES = { - 1: '<= 1.0.rc.2', // 1.0.rc.2 is actually rev2 but doesn't report it - 2: '== 1.0.0-rc.3', - 3: '== 1.0.0-rc.4', - 4: '== 1.x.x', - 5: '== 2.0.0-alpha.x', - 6: '>= 2.0.0-beta.1' - }; - __exports__.REVISION_CHANGES = REVISION_CHANGES; - var isArray = Utils.isArray, - isFunction = Utils.isFunction, - toString = Utils.toString, - objectType = '[object Object]'; - - function HandlebarsEnvironment(helpers, partials) { - this.helpers = helpers || {}; - this.partials = partials || {}; - - registerDefaultHelpers(this); - } - - __exports__.HandlebarsEnvironment = HandlebarsEnvironment;HandlebarsEnvironment.prototype = { - constructor: HandlebarsEnvironment, - - logger: logger, - log: log, - - registerHelper: function(name, fn) { - if (toString.call(name) === objectType) { - if (fn) { throw new Exception('Arg not supported with multiple helpers'); } - Utils.extend(this.helpers, name); - } else { - this.helpers[name] = fn; - } - }, - unregisterHelper: function(name) { - delete this.helpers[name]; - }, - - registerPartial: function(name, partial) { - if (toString.call(name) === objectType) { - Utils.extend(this.partials, name); - } else { - this.partials[name] = partial; - } - }, - unregisterPartial: function(name) { - delete this.partials[name]; - } - }; - - function registerDefaultHelpers(instance) { - instance.registerHelper('helperMissing', function(/* [args, ]options */) { - if(arguments.length === 1) { - // A missing field in a {{foo}} constuct. - return undefined; - } else { - // Someone is actually trying to call something, blow up. - throw new Exception("Missing helper: '" + arguments[arguments.length-1].name + "'"); - } - }); - - instance.registerHelper('blockHelperMissing', function(context, options) { - var inverse = options.inverse, - fn = options.fn; - - if(context === true) { - return fn(this); - } else if(context === false || context == null) { - return inverse(this); - } else if (isArray(context)) { - if(context.length > 0) { - if (options.ids) { - options.ids = [options.name]; - } - - return instance.helpers.each(context, options); - } else { - return inverse(this); - } - } else { - if (options.data && options.ids) { - var data = createFrame(options.data); - data.contextPath = Utils.appendContextPath(options.data.contextPath, options.name); - options = {data: data}; - } - - return fn(context, options); - } - }); - - instance.registerHelper('each', function(context, options) { - if (!options) { - throw new Exception('Must pass iterator to #each'); - } - - var fn = options.fn, inverse = options.inverse; - var i = 0, ret = "", data; - - var contextPath; - if (options.data && options.ids) { - contextPath = Utils.appendContextPath(options.data.contextPath, options.ids[0]) + '.'; - } - - if (isFunction(context)) { context = context.call(this); } - - if (options.data) { - data = createFrame(options.data); - } - - if(context && typeof context === 'object') { - if (isArray(context)) { - for(var j = context.length; i<j; i++) { - if (data) { - data.index = i; - data.first = (i === 0); - data.last = (i === (context.length-1)); - - if (contextPath) { - data.contextPath = contextPath + i; - } - } - ret = ret + fn(context[i], { data: data }); - } - } else { - for(var key in context) { - if(context.hasOwnProperty(key)) { - if(data) { - data.key = key; - data.index = i; - data.first = (i === 0); - - if (contextPath) { - data.contextPath = contextPath + key; - } - } - ret = ret + fn(context[key], {data: data}); - i++; - } - } - } - } - - if(i === 0){ - ret = inverse(this); - } - - return ret; - }); - - instance.registerHelper('if', function(conditional, options) { - if (isFunction(conditional)) { conditional = conditional.call(this); } - - // Default behavior is to render the positive path if the value is truthy and not empty. - // The `includeZero` option may be set to treat the condtional as purely not empty based on the - // behavior of isEmpty. Effectively this determines if 0 is handled by the positive path or negative. - if ((!options.hash.includeZero && !conditional) || Utils.isEmpty(conditional)) { - return options.inverse(this); - } else { - return options.fn(this); - } - }); - - instance.registerHelper('unless', function(conditional, options) { - return instance.helpers['if'].call(this, conditional, {fn: options.inverse, inverse: options.fn, hash: options.hash}); - }); - - instance.registerHelper('with', function(context, options) { - if (isFunction(context)) { context = context.call(this); } - - var fn = options.fn; - - if (!Utils.isEmpty(context)) { - if (options.data && options.ids) { - var data = createFrame(options.data); - data.contextPath = Utils.appendContextPath(options.data.contextPath, options.ids[0]); - options = {data:data}; - } - - return fn(context, options); - } else { - return options.inverse(this); - } - }); - - instance.registerHelper('log', function(message, options) { - var level = options.data && options.data.level != null ? parseInt(options.data.level, 10) : 1; - instance.log(level, message); - }); - - instance.registerHelper('lookup', function(obj, field) { - return obj && obj[field]; - }); - } - - var logger = { - methodMap: { 0: 'debug', 1: 'info', 2: 'warn', 3: 'error' }, - - // State enum - DEBUG: 0, - INFO: 1, - WARN: 2, - ERROR: 3, - level: 3, - - // can be overridden in the host environment - log: function(level, message) { - if (logger.level <= level) { - var method = logger.methodMap[level]; - if (typeof console !== 'undefined' && console[method]) { - console[method].call(console, message); - } - } - } - }; - __exports__.logger = logger; - var log = logger.log; - __exports__.log = log; - var createFrame = function(object) { - var frame = Utils.extend({}, object); - frame._parent = object; - return frame; - }; - __exports__.createFrame = createFrame; - return __exports__; - })(__module2__, __module4__); - - // handlebars/runtime.js - var __module5__ = (function(__dependency1__, __dependency2__, __dependency3__) { - "use strict"; - var __exports__ = {}; - var Utils = __dependency1__; - var Exception = __dependency2__; - var COMPILER_REVISION = __dependency3__.COMPILER_REVISION; - var REVISION_CHANGES = __dependency3__.REVISION_CHANGES; - var createFrame = __dependency3__.createFrame; - - function checkRevision(compilerInfo) { - var compilerRevision = compilerInfo && compilerInfo[0] || 1, - currentRevision = COMPILER_REVISION; - - if (compilerRevision !== currentRevision) { - if (compilerRevision < currentRevision) { - var runtimeVersions = REVISION_CHANGES[currentRevision], - compilerVersions = REVISION_CHANGES[compilerRevision]; - throw new Exception("Template was precompiled with an older version of Handlebars than the current runtime. "+ - "Please update your precompiler to a newer version ("+runtimeVersions+") or downgrade your runtime to an older version ("+compilerVersions+")."); - } else { - // Use the embedded version info since the runtime doesn't know about this revision yet - throw new Exception("Template was precompiled with a newer version of Handlebars than the current runtime. "+ - "Please update your runtime to a newer version ("+compilerInfo[1]+")."); - } - } - } - - __exports__.checkRevision = checkRevision;// TODO: Remove this line and break up compilePartial - - function template(templateSpec, env) { - /* istanbul ignore next */ - if (!env) { - throw new Exception("No environment passed to template"); - } - if (!templateSpec || !templateSpec.main) { - throw new Exception('Unknown template object: ' + typeof templateSpec); - } - - // Note: Using env.VM references rather than local var references throughout this section to allow - // for external users to override these as psuedo-supported APIs. - env.VM.checkRevision(templateSpec.compiler); - - var invokePartialWrapper = function(partial, indent, name, context, hash, helpers, partials, data, depths) { - if (hash) { - context = Utils.extend({}, context, hash); - } - - var result = env.VM.invokePartial.call(this, partial, name, context, helpers, partials, data, depths); - - if (result == null && env.compile) { - var options = { helpers: helpers, partials: partials, data: data, depths: depths }; - partials[name] = env.compile(partial, { data: data !== undefined, compat: templateSpec.compat }, env); - result = partials[name](context, options); - } - if (result != null) { - if (indent) { - var lines = result.split('\n'); - for (var i = 0, l = lines.length; i < l; i++) { - if (!lines[i] && i + 1 === l) { - break; - } - - lines[i] = indent + lines[i]; - } - result = lines.join('\n'); - } - return result; - } else { - throw new Exception("The partial " + name + " could not be compiled when running in runtime-only mode"); - } - }; - - // Just add water - var container = { - lookup: function(depths, name) { - var len = depths.length; - for (var i = 0; i < len; i++) { - if (depths[i] && depths[i][name] != null) { - return depths[i][name]; - } - } - }, - lambda: function(current, context) { - return typeof current === 'function' ? current.call(context) : current; - }, - - escapeExpression: Utils.escapeExpression, - invokePartial: invokePartialWrapper, - - fn: function(i) { - return templateSpec[i]; - }, - - programs: [], - program: function(i, data, depths) { - var programWrapper = this.programs[i], - fn = this.fn(i); - if (data || depths) { - programWrapper = program(this, i, fn, data, depths); - } else if (!programWrapper) { - programWrapper = this.programs[i] = program(this, i, fn); - } - return programWrapper; - }, - - data: function(data, depth) { - while (data && depth--) { - data = data._parent; - } - return data; - }, - merge: function(param, common) { - var ret = param || common; - - if (param && common && (param !== common)) { - ret = Utils.extend({}, common, param); - } - - return ret; - }, - - noop: env.VM.noop, - compilerInfo: templateSpec.compiler - }; - - var ret = function(context, options) { - options = options || {}; - var data = options.data; - - ret._setup(options); - if (!options.partial && templateSpec.useData) { - data = initData(context, data); - } - var depths; - if (templateSpec.useDepths) { - depths = options.depths ? [context].concat(options.depths) : [context]; - } - - return templateSpec.main.call(container, context, container.helpers, container.partials, data, depths); - }; - ret.isTop = true; - - ret._setup = function(options) { - if (!options.partial) { - container.helpers = container.merge(options.helpers, env.helpers); - - if (templateSpec.usePartial) { - container.partials = container.merge(options.partials, env.partials); - } - } else { - container.helpers = options.helpers; - container.partials = options.partials; - } - }; - - ret._child = function(i, data, depths) { - if (templateSpec.useDepths && !depths) { - throw new Exception('must pass parent depths'); - } - - return program(container, i, templateSpec[i], data, depths); - }; - return ret; - } - - __exports__.template = template;function program(container, i, fn, data, depths) { - var prog = function(context, options) { - options = options || {}; - - return fn.call(container, context, container.helpers, container.partials, options.data || data, depths && [context].concat(depths)); - }; - prog.program = i; - prog.depth = depths ? depths.length : 0; - return prog; - } - - __exports__.program = program;function invokePartial(partial, name, context, helpers, partials, data, depths) { - var options = { partial: true, helpers: helpers, partials: partials, data: data, depths: depths }; - - if(partial === undefined) { - throw new Exception("The partial " + name + " could not be found"); - } else if(partial instanceof Function) { - return partial(context, options); - } - } - - __exports__.invokePartial = invokePartial;function noop() { return ""; } - - __exports__.noop = noop;function initData(context, data) { - if (!data || !('root' in data)) { - data = data ? createFrame(data) : {}; - data.root = context; - } - return data; - } - return __exports__; - })(__module2__, __module4__, __module1__); - - // handlebars.runtime.js - var __module0__ = (function(__dependency1__, __dependency2__, __dependency3__, __dependency4__, __dependency5__) { - "use strict"; - var __exports__; - /*globals Handlebars: true */ - var base = __dependency1__; - - // Each of these augment the Handlebars object. No need to setup here. - // (This is done to easily share code between commonjs and browse envs) - var SafeString = __dependency2__; - var Exception = __dependency3__; - var Utils = __dependency4__; - var runtime = __dependency5__; - - // For compatibility and usage outside of module systems, make the Handlebars object a namespace - var create = function() { - var hb = new base.HandlebarsEnvironment(); - - Utils.extend(hb, base); - hb.SafeString = SafeString; - hb.Exception = Exception; - hb.Utils = Utils; - hb.escapeExpression = Utils.escapeExpression; - - hb.VM = runtime; - hb.template = function(spec) { - return runtime.template(spec, hb); - }; - - return hb; - }; - - var Handlebars = create(); - Handlebars.create = create; - - Handlebars['default'] = Handlebars; - - __exports__ = Handlebars; - return __exports__; - })(__module1__, __module3__, __module4__, __module2__, __module5__); - - return __module0__; -})); diff --git a/src/UI/JsLibraries/jquery-ui.js b/src/UI/JsLibraries/jquery-ui.js deleted file mode 100644 index fe44a9c84..000000000 --- a/src/UI/JsLibraries/jquery-ui.js +++ /dev/null @@ -1,4233 +0,0 @@ -/*! jQuery UI - v1.10.4 - 2014-01-22 -* http://jqueryui.com -* Includes: jquery.ui.core.js, jquery.ui.widget.js, jquery.ui.mouse.js, jquery.ui.draggable.js, jquery.ui.droppable.js, jquery.ui.sortable.js, jquery.ui.slider.js -* Copyright 2014 jQuery Foundation and other contributors; Licensed MIT */ - -(function( $, undefined ) { - -var uuid = 0, - runiqueId = /^ui-id-\d+$/; - -// $.ui might exist from components with no dependencies, e.g., $.ui.position -$.ui = $.ui || {}; - -$.extend( $.ui, { - version: "1.10.4", - - keyCode: { - BACKSPACE: 8, - COMMA: 188, - DELETE: 46, - DOWN: 40, - END: 35, - ENTER: 13, - ESCAPE: 27, - HOME: 36, - LEFT: 37, - NUMPAD_ADD: 107, - NUMPAD_DECIMAL: 110, - NUMPAD_DIVIDE: 111, - NUMPAD_ENTER: 108, - NUMPAD_MULTIPLY: 106, - NUMPAD_SUBTRACT: 109, - PAGE_DOWN: 34, - PAGE_UP: 33, - PERIOD: 190, - RIGHT: 39, - SPACE: 32, - TAB: 9, - UP: 38 - } -}); - -// plugins -$.fn.extend({ - focus: (function( orig ) { - return function( delay, fn ) { - return typeof delay === "number" ? - this.each(function() { - var elem = this; - setTimeout(function() { - $( elem ).focus(); - if ( fn ) { - fn.call( elem ); - } - }, delay ); - }) : - orig.apply( this, arguments ); - }; - })( $.fn.focus ), - - scrollParent: function() { - var scrollParent; - if (($.ui.ie && (/(static|relative)/).test(this.css("position"))) || (/absolute/).test(this.css("position"))) { - scrollParent = this.parents().filter(function() { - return (/(relative|absolute|fixed)/).test($.css(this,"position")) && (/(auto|scroll)/).test($.css(this,"overflow")+$.css(this,"overflow-y")+$.css(this,"overflow-x")); - }).eq(0); - } else { - scrollParent = this.parents().filter(function() { - return (/(auto|scroll)/).test($.css(this,"overflow")+$.css(this,"overflow-y")+$.css(this,"overflow-x")); - }).eq(0); - } - - return (/fixed/).test(this.css("position")) || !scrollParent.length ? $(document) : scrollParent; - }, - - zIndex: function( zIndex ) { - if ( zIndex !== undefined ) { - return this.css( "zIndex", zIndex ); - } - - if ( this.length ) { - var elem = $( this[ 0 ] ), position, value; - while ( elem.length && elem[ 0 ] !== document ) { - // Ignore z-index if position is set to a value where z-index is ignored by the browser - // This makes behavior of this function consistent across browsers - // WebKit always returns auto if the element is positioned - position = elem.css( "position" ); - if ( position === "absolute" || position === "relative" || position === "fixed" ) { - // IE returns 0 when zIndex is not specified - // other browsers return a string - // we ignore the case of nested elements with an explicit value of 0 - // <div style="z-index: -10;"><div style="z-index: 0;"></div></div> - value = parseInt( elem.css( "zIndex" ), 10 ); - if ( !isNaN( value ) && value !== 0 ) { - return value; - } - } - elem = elem.parent(); - } - } - - return 0; - }, - - uniqueId: function() { - return this.each(function() { - if ( !this.id ) { - this.id = "ui-id-" + (++uuid); - } - }); - }, - - removeUniqueId: function() { - return this.each(function() { - if ( runiqueId.test( this.id ) ) { - $( this ).removeAttr( "id" ); - } - }); - } -}); - -// selectors -function focusable( element, isTabIndexNotNaN ) { - var map, mapName, img, - nodeName = element.nodeName.toLowerCase(); - if ( "area" === nodeName ) { - map = element.parentNode; - mapName = map.name; - if ( !element.href || !mapName || map.nodeName.toLowerCase() !== "map" ) { - return false; - } - img = $( "img[usemap=#" + mapName + "]" )[0]; - return !!img && visible( img ); - } - return ( /input|select|textarea|button|object/.test( nodeName ) ? - !element.disabled : - "a" === nodeName ? - element.href || isTabIndexNotNaN : - isTabIndexNotNaN) && - // the element and all of its ancestors must be visible - visible( element ); -} - -function visible( element ) { - return $.expr.filters.visible( element ) && - !$( element ).parents().addBack().filter(function() { - return $.css( this, "visibility" ) === "hidden"; - }).length; -} - -$.extend( $.expr[ ":" ], { - data: $.expr.createPseudo ? - $.expr.createPseudo(function( dataName ) { - return function( elem ) { - return !!$.data( elem, dataName ); - }; - }) : - // support: jQuery <1.8 - function( elem, i, match ) { - return !!$.data( elem, match[ 3 ] ); - }, - - focusable: function( element ) { - return focusable( element, !isNaN( $.attr( element, "tabindex" ) ) ); - }, - - tabbable: function( element ) { - var tabIndex = $.attr( element, "tabindex" ), - isTabIndexNaN = isNaN( tabIndex ); - return ( isTabIndexNaN || tabIndex >= 0 ) && focusable( element, !isTabIndexNaN ); - } -}); - -// support: jQuery <1.8 -if ( !$( "<a>" ).outerWidth( 1 ).jquery ) { - $.each( [ "Width", "Height" ], function( i, name ) { - var side = name === "Width" ? [ "Left", "Right" ] : [ "Top", "Bottom" ], - type = name.toLowerCase(), - orig = { - innerWidth: $.fn.innerWidth, - innerHeight: $.fn.innerHeight, - outerWidth: $.fn.outerWidth, - outerHeight: $.fn.outerHeight - }; - - function reduce( elem, size, border, margin ) { - $.each( side, function() { - size -= parseFloat( $.css( elem, "padding" + this ) ) || 0; - if ( border ) { - size -= parseFloat( $.css( elem, "border" + this + "Width" ) ) || 0; - } - if ( margin ) { - size -= parseFloat( $.css( elem, "margin" + this ) ) || 0; - } - }); - return size; - } - - $.fn[ "inner" + name ] = function( size ) { - if ( size === undefined ) { - return orig[ "inner" + name ].call( this ); - } - - return this.each(function() { - $( this ).css( type, reduce( this, size ) + "px" ); - }); - }; - - $.fn[ "outer" + name] = function( size, margin ) { - if ( typeof size !== "number" ) { - return orig[ "outer" + name ].call( this, size ); - } - - return this.each(function() { - $( this).css( type, reduce( this, size, true, margin ) + "px" ); - }); - }; - }); -} - -// support: jQuery <1.8 -if ( !$.fn.addBack ) { - $.fn.addBack = function( selector ) { - return this.add( selector == null ? - this.prevObject : this.prevObject.filter( selector ) - ); - }; -} - -// support: jQuery 1.6.1, 1.6.2 (http://bugs.jquery.com/ticket/9413) -if ( $( "<a>" ).data( "a-b", "a" ).removeData( "a-b" ).data( "a-b" ) ) { - $.fn.removeData = (function( removeData ) { - return function( key ) { - if ( arguments.length ) { - return removeData.call( this, $.camelCase( key ) ); - } else { - return removeData.call( this ); - } - }; - })( $.fn.removeData ); -} - - - - - -// deprecated -$.ui.ie = !!/msie [\w.]+/.exec( navigator.userAgent.toLowerCase() ); - -$.support.selectstart = "onselectstart" in document.createElement( "div" ); -$.fn.extend({ - disableSelection: function() { - return this.bind( ( $.support.selectstart ? "selectstart" : "mousedown" ) + - ".ui-disableSelection", function( event ) { - event.preventDefault(); - }); - }, - - enableSelection: function() { - return this.unbind( ".ui-disableSelection" ); - } -}); - -$.extend( $.ui, { - // $.ui.plugin is deprecated. Use $.widget() extensions instead. - plugin: { - add: function( module, option, set ) { - var i, - proto = $.ui[ module ].prototype; - for ( i in set ) { - proto.plugins[ i ] = proto.plugins[ i ] || []; - proto.plugins[ i ].push( [ option, set[ i ] ] ); - } - }, - call: function( instance, name, args ) { - var i, - set = instance.plugins[ name ]; - if ( !set || !instance.element[ 0 ].parentNode || instance.element[ 0 ].parentNode.nodeType === 11 ) { - return; - } - - for ( i = 0; i < set.length; i++ ) { - if ( instance.options[ set[ i ][ 0 ] ] ) { - set[ i ][ 1 ].apply( instance.element, args ); - } - } - } - }, - - // only used by resizable - hasScroll: function( el, a ) { - - //If overflow is hidden, the element might have extra content, but the user wants to hide it - if ( $( el ).css( "overflow" ) === "hidden") { - return false; - } - - var scroll = ( a && a === "left" ) ? "scrollLeft" : "scrollTop", - has = false; - - if ( el[ scroll ] > 0 ) { - return true; - } - - // TODO: determine which cases actually cause this to happen - // if the element doesn't have the scroll set, see if it's possible to - // set the scroll - el[ scroll ] = 1; - has = ( el[ scroll ] > 0 ); - el[ scroll ] = 0; - return has; - } -}); - -})( jQuery ); -(function( $, undefined ) { - -var uuid = 0, - slice = Array.prototype.slice, - _cleanData = $.cleanData; -$.cleanData = function( elems ) { - for ( var i = 0, elem; (elem = elems[i]) != null; i++ ) { - try { - $( elem ).triggerHandler( "remove" ); - // http://bugs.jquery.com/ticket/8235 - } catch( e ) {} - } - _cleanData( elems ); -}; - -$.widget = function( name, base, prototype ) { - var fullName, existingConstructor, constructor, basePrototype, - // proxiedPrototype allows the provided prototype to remain unmodified - // so that it can be used as a mixin for multiple widgets (#8876) - proxiedPrototype = {}, - namespace = name.split( "." )[ 0 ]; - - name = name.split( "." )[ 1 ]; - fullName = namespace + "-" + name; - - if ( !prototype ) { - prototype = base; - base = $.Widget; - } - - // create selector for plugin - $.expr[ ":" ][ fullName.toLowerCase() ] = function( elem ) { - return !!$.data( elem, fullName ); - }; - - $[ namespace ] = $[ namespace ] || {}; - existingConstructor = $[ namespace ][ name ]; - constructor = $[ namespace ][ name ] = function( options, element ) { - // allow instantiation without "new" keyword - if ( !this._createWidget ) { - return new constructor( options, element ); - } - - // allow instantiation without initializing for simple inheritance - // must use "new" keyword (the code above always passes args) - if ( arguments.length ) { - this._createWidget( options, element ); - } - }; - // extend with the existing constructor to carry over any static properties - $.extend( constructor, existingConstructor, { - version: prototype.version, - // copy the object used to create the prototype in case we need to - // redefine the widget later - _proto: $.extend( {}, prototype ), - // track widgets that inherit from this widget in case this widget is - // redefined after a widget inherits from it - _childConstructors: [] - }); - - basePrototype = new base(); - // we need to make the options hash a property directly on the new instance - // otherwise we'll modify the options hash on the prototype that we're - // inheriting from - basePrototype.options = $.widget.extend( {}, basePrototype.options ); - $.each( prototype, function( prop, value ) { - if ( !$.isFunction( value ) ) { - proxiedPrototype[ prop ] = value; - return; - } - proxiedPrototype[ prop ] = (function() { - var _super = function() { - return base.prototype[ prop ].apply( this, arguments ); - }, - _superApply = function( args ) { - return base.prototype[ prop ].apply( this, args ); - }; - return function() { - var __super = this._super, - __superApply = this._superApply, - returnValue; - - this._super = _super; - this._superApply = _superApply; - - returnValue = value.apply( this, arguments ); - - this._super = __super; - this._superApply = __superApply; - - return returnValue; - }; - })(); - }); - constructor.prototype = $.widget.extend( basePrototype, { - // TODO: remove support for widgetEventPrefix - // always use the name + a colon as the prefix, e.g., draggable:start - // don't prefix for widgets that aren't DOM-based - widgetEventPrefix: existingConstructor ? (basePrototype.widgetEventPrefix || name) : name - }, proxiedPrototype, { - constructor: constructor, - namespace: namespace, - widgetName: name, - widgetFullName: fullName - }); - - // If this widget is being redefined then we need to find all widgets that - // are inheriting from it and redefine all of them so that they inherit from - // the new version of this widget. We're essentially trying to replace one - // level in the prototype chain. - if ( existingConstructor ) { - $.each( existingConstructor._childConstructors, function( i, child ) { - var childPrototype = child.prototype; - - // redefine the child widget using the same prototype that was - // originally used, but inherit from the new version of the base - $.widget( childPrototype.namespace + "." + childPrototype.widgetName, constructor, child._proto ); - }); - // remove the list of existing child constructors from the old constructor - // so the old child constructors can be garbage collected - delete existingConstructor._childConstructors; - } else { - base._childConstructors.push( constructor ); - } - - $.widget.bridge( name, constructor ); -}; - -$.widget.extend = function( target ) { - var input = slice.call( arguments, 1 ), - inputIndex = 0, - inputLength = input.length, - key, - value; - for ( ; inputIndex < inputLength; inputIndex++ ) { - for ( key in input[ inputIndex ] ) { - value = input[ inputIndex ][ key ]; - if ( input[ inputIndex ].hasOwnProperty( key ) && value !== undefined ) { - // Clone objects - if ( $.isPlainObject( value ) ) { - target[ key ] = $.isPlainObject( target[ key ] ) ? - $.widget.extend( {}, target[ key ], value ) : - // Don't extend strings, arrays, etc. with objects - $.widget.extend( {}, value ); - // Copy everything else by reference - } else { - target[ key ] = value; - } - } - } - } - return target; -}; - -$.widget.bridge = function( name, object ) { - var fullName = object.prototype.widgetFullName || name; - $.fn[ name ] = function( options ) { - var isMethodCall = typeof options === "string", - args = slice.call( arguments, 1 ), - returnValue = this; - - // allow multiple hashes to be passed on init - options = !isMethodCall && args.length ? - $.widget.extend.apply( null, [ options ].concat(args) ) : - options; - - if ( isMethodCall ) { - this.each(function() { - var methodValue, - instance = $.data( this, fullName ); - if ( !instance ) { - return $.error( "cannot call methods on " + name + " prior to initialization; " + - "attempted to call method '" + options + "'" ); - } - if ( !$.isFunction( instance[options] ) || options.charAt( 0 ) === "_" ) { - return $.error( "no such method '" + options + "' for " + name + " widget instance" ); - } - methodValue = instance[ options ].apply( instance, args ); - if ( methodValue !== instance && methodValue !== undefined ) { - returnValue = methodValue && methodValue.jquery ? - returnValue.pushStack( methodValue.get() ) : - methodValue; - return false; - } - }); - } else { - this.each(function() { - var instance = $.data( this, fullName ); - if ( instance ) { - instance.option( options || {} )._init(); - } else { - $.data( this, fullName, new object( options, this ) ); - } - }); - } - - return returnValue; - }; -}; - -$.Widget = function( /* options, element */ ) {}; -$.Widget._childConstructors = []; - -$.Widget.prototype = { - widgetName: "widget", - widgetEventPrefix: "", - defaultElement: "<div>", - options: { - disabled: false, - - // callbacks - create: null - }, - _createWidget: function( options, element ) { - element = $( element || this.defaultElement || this )[ 0 ]; - this.element = $( element ); - this.uuid = uuid++; - this.eventNamespace = "." + this.widgetName + this.uuid; - this.options = $.widget.extend( {}, - this.options, - this._getCreateOptions(), - options ); - - this.bindings = $(); - this.hoverable = $(); - this.focusable = $(); - - if ( element !== this ) { - $.data( element, this.widgetFullName, this ); - this._on( true, this.element, { - remove: function( event ) { - if ( event.target === element ) { - this.destroy(); - } - } - }); - this.document = $( element.style ? - // element within the document - element.ownerDocument : - // element is window or document - element.document || element ); - this.window = $( this.document[0].defaultView || this.document[0].parentWindow ); - } - - this._create(); - this._trigger( "create", null, this._getCreateEventData() ); - this._init(); - }, - _getCreateOptions: $.noop, - _getCreateEventData: $.noop, - _create: $.noop, - _init: $.noop, - - destroy: function() { - this._destroy(); - // we can probably remove the unbind calls in 2.0 - // all event bindings should go through this._on() - this.element - .unbind( this.eventNamespace ) - // 1.9 BC for #7810 - // TODO remove dual storage - .removeData( this.widgetName ) - .removeData( this.widgetFullName ) - // support: jquery <1.6.3 - // http://bugs.jquery.com/ticket/9413 - .removeData( $.camelCase( this.widgetFullName ) ); - this.widget() - .unbind( this.eventNamespace ) - .removeAttr( "aria-disabled" ) - .removeClass( - this.widgetFullName + "-disabled " + - "ui-state-disabled" ); - - // clean up events and states - this.bindings.unbind( this.eventNamespace ); - this.hoverable.removeClass( "ui-state-hover" ); - this.focusable.removeClass( "ui-state-focus" ); - }, - _destroy: $.noop, - - widget: function() { - return this.element; - }, - - option: function( key, value ) { - var options = key, - parts, - curOption, - i; - - if ( arguments.length === 0 ) { - // don't return a reference to the internal hash - return $.widget.extend( {}, this.options ); - } - - if ( typeof key === "string" ) { - // handle nested keys, e.g., "foo.bar" => { foo: { bar: ___ } } - options = {}; - parts = key.split( "." ); - key = parts.shift(); - if ( parts.length ) { - curOption = options[ key ] = $.widget.extend( {}, this.options[ key ] ); - for ( i = 0; i < parts.length - 1; i++ ) { - curOption[ parts[ i ] ] = curOption[ parts[ i ] ] || {}; - curOption = curOption[ parts[ i ] ]; - } - key = parts.pop(); - if ( arguments.length === 1 ) { - return curOption[ key ] === undefined ? null : curOption[ key ]; - } - curOption[ key ] = value; - } else { - if ( arguments.length === 1 ) { - return this.options[ key ] === undefined ? null : this.options[ key ]; - } - options[ key ] = value; - } - } - - this._setOptions( options ); - - return this; - }, - _setOptions: function( options ) { - var key; - - for ( key in options ) { - this._setOption( key, options[ key ] ); - } - - return this; - }, - _setOption: function( key, value ) { - this.options[ key ] = value; - - if ( key === "disabled" ) { - this.widget() - .toggleClass( this.widgetFullName + "-disabled ui-state-disabled", !!value ) - .attr( "aria-disabled", value ); - this.hoverable.removeClass( "ui-state-hover" ); - this.focusable.removeClass( "ui-state-focus" ); - } - - return this; - }, - - enable: function() { - return this._setOption( "disabled", false ); - }, - disable: function() { - return this._setOption( "disabled", true ); - }, - - _on: function( suppressDisabledCheck, element, handlers ) { - var delegateElement, - instance = this; - - // no suppressDisabledCheck flag, shuffle arguments - if ( typeof suppressDisabledCheck !== "boolean" ) { - handlers = element; - element = suppressDisabledCheck; - suppressDisabledCheck = false; - } - - // no element argument, shuffle and use this.element - if ( !handlers ) { - handlers = element; - element = this.element; - delegateElement = this.widget(); - } else { - // accept selectors, DOM elements - element = delegateElement = $( element ); - this.bindings = this.bindings.add( element ); - } - - $.each( handlers, function( event, handler ) { - function handlerProxy() { - // allow widgets to customize the disabled handling - // - disabled as an array instead of boolean - // - disabled class as method for disabling individual parts - if ( !suppressDisabledCheck && - ( instance.options.disabled === true || - $( this ).hasClass( "ui-state-disabled" ) ) ) { - return; - } - return ( typeof handler === "string" ? instance[ handler ] : handler ) - .apply( instance, arguments ); - } - - // copy the guid so direct unbinding works - if ( typeof handler !== "string" ) { - handlerProxy.guid = handler.guid = - handler.guid || handlerProxy.guid || $.guid++; - } - - var match = event.match( /^(\w+)\s*(.*)$/ ), - eventName = match[1] + instance.eventNamespace, - selector = match[2]; - if ( selector ) { - delegateElement.delegate( selector, eventName, handlerProxy ); - } else { - element.bind( eventName, handlerProxy ); - } - }); - }, - - _off: function( element, eventName ) { - eventName = (eventName || "").split( " " ).join( this.eventNamespace + " " ) + this.eventNamespace; - element.unbind( eventName ).undelegate( eventName ); - }, - - _delay: function( handler, delay ) { - function handlerProxy() { - return ( typeof handler === "string" ? instance[ handler ] : handler ) - .apply( instance, arguments ); - } - var instance = this; - return setTimeout( handlerProxy, delay || 0 ); - }, - - _hoverable: function( element ) { - this.hoverable = this.hoverable.add( element ); - this._on( element, { - mouseenter: function( event ) { - $( event.currentTarget ).addClass( "ui-state-hover" ); - }, - mouseleave: function( event ) { - $( event.currentTarget ).removeClass( "ui-state-hover" ); - } - }); - }, - - _focusable: function( element ) { - this.focusable = this.focusable.add( element ); - this._on( element, { - focusin: function( event ) { - $( event.currentTarget ).addClass( "ui-state-focus" ); - }, - focusout: function( event ) { - $( event.currentTarget ).removeClass( "ui-state-focus" ); - } - }); - }, - - _trigger: function( type, event, data ) { - var prop, orig, - callback = this.options[ type ]; - - data = data || {}; - event = $.Event( event ); - event.type = ( type === this.widgetEventPrefix ? - type : - this.widgetEventPrefix + type ).toLowerCase(); - // the original event may come from any element - // so we need to reset the target on the new event - event.target = this.element[ 0 ]; - - // copy original event properties over to the new event - orig = event.originalEvent; - if ( orig ) { - for ( prop in orig ) { - if ( !( prop in event ) ) { - event[ prop ] = orig[ prop ]; - } - } - } - - this.element.trigger( event, data ); - return !( $.isFunction( callback ) && - callback.apply( this.element[0], [ event ].concat( data ) ) === false || - event.isDefaultPrevented() ); - } -}; - -$.each( { show: "fadeIn", hide: "fadeOut" }, function( method, defaultEffect ) { - $.Widget.prototype[ "_" + method ] = function( element, options, callback ) { - if ( typeof options === "string" ) { - options = { effect: options }; - } - var hasOptions, - effectName = !options ? - method : - options === true || typeof options === "number" ? - defaultEffect : - options.effect || defaultEffect; - options = options || {}; - if ( typeof options === "number" ) { - options = { duration: options }; - } - hasOptions = !$.isEmptyObject( options ); - options.complete = callback; - if ( options.delay ) { - element.delay( options.delay ); - } - if ( hasOptions && $.effects && $.effects.effect[ effectName ] ) { - element[ method ]( options ); - } else if ( effectName !== method && element[ effectName ] ) { - element[ effectName ]( options.duration, options.easing, callback ); - } else { - element.queue(function( next ) { - $( this )[ method ](); - if ( callback ) { - callback.call( element[ 0 ] ); - } - next(); - }); - } - }; -}); - -})( jQuery ); -(function( $, undefined ) { - -var mouseHandled = false; -$( document ).mouseup( function() { - mouseHandled = false; -}); - -$.widget("ui.mouse", { - version: "1.10.4", - options: { - cancel: "input,textarea,button,select,option", - distance: 1, - delay: 0 - }, - _mouseInit: function() { - var that = this; - - this.element - .bind("mousedown."+this.widgetName, function(event) { - return that._mouseDown(event); - }) - .bind("click."+this.widgetName, function(event) { - if (true === $.data(event.target, that.widgetName + ".preventClickEvent")) { - $.removeData(event.target, that.widgetName + ".preventClickEvent"); - event.stopImmediatePropagation(); - return false; - } - }); - - this.started = false; - }, - - // TODO: make sure destroying one instance of mouse doesn't mess with - // other instances of mouse - _mouseDestroy: function() { - this.element.unbind("."+this.widgetName); - if ( this._mouseMoveDelegate ) { - $(document) - .unbind("mousemove."+this.widgetName, this._mouseMoveDelegate) - .unbind("mouseup."+this.widgetName, this._mouseUpDelegate); - } - }, - - _mouseDown: function(event) { - // don't let more than one widget handle mouseStart - if( mouseHandled ) { return; } - - // we may have missed mouseup (out of window) - (this._mouseStarted && this._mouseUp(event)); - - this._mouseDownEvent = event; - - var that = this, - btnIsLeft = (event.which === 1), - // event.target.nodeName works around a bug in IE 8 with - // disabled inputs (#7620) - elIsCancel = (typeof this.options.cancel === "string" && event.target.nodeName ? $(event.target).closest(this.options.cancel).length : false); - if (!btnIsLeft || elIsCancel || !this._mouseCapture(event)) { - return true; - } - - this.mouseDelayMet = !this.options.delay; - if (!this.mouseDelayMet) { - this._mouseDelayTimer = setTimeout(function() { - that.mouseDelayMet = true; - }, this.options.delay); - } - - if (this._mouseDistanceMet(event) && this._mouseDelayMet(event)) { - this._mouseStarted = (this._mouseStart(event) !== false); - if (!this._mouseStarted) { - event.preventDefault(); - return true; - } - } - - // Click event may never have fired (Gecko & Opera) - if (true === $.data(event.target, this.widgetName + ".preventClickEvent")) { - $.removeData(event.target, this.widgetName + ".preventClickEvent"); - } - - // these delegates are required to keep context - this._mouseMoveDelegate = function(event) { - return that._mouseMove(event); - }; - this._mouseUpDelegate = function(event) { - return that._mouseUp(event); - }; - $(document) - .bind("mousemove."+this.widgetName, this._mouseMoveDelegate) - .bind("mouseup."+this.widgetName, this._mouseUpDelegate); - - event.preventDefault(); - - mouseHandled = true; - return true; - }, - - _mouseMove: function(event) { - // IE mouseup check - mouseup happened when mouse was out of window - if ($.ui.ie && ( !document.documentMode || document.documentMode < 9 ) && !event.button) { - return this._mouseUp(event); - } - - if (this._mouseStarted) { - this._mouseDrag(event); - return event.preventDefault(); - } - - if (this._mouseDistanceMet(event) && this._mouseDelayMet(event)) { - this._mouseStarted = - (this._mouseStart(this._mouseDownEvent, event) !== false); - (this._mouseStarted ? this._mouseDrag(event) : this._mouseUp(event)); - } - - return !this._mouseStarted; - }, - - _mouseUp: function(event) { - $(document) - .unbind("mousemove."+this.widgetName, this._mouseMoveDelegate) - .unbind("mouseup."+this.widgetName, this._mouseUpDelegate); - - if (this._mouseStarted) { - this._mouseStarted = false; - - if (event.target === this._mouseDownEvent.target) { - $.data(event.target, this.widgetName + ".preventClickEvent", true); - } - - this._mouseStop(event); - } - - return false; - }, - - _mouseDistanceMet: function(event) { - return (Math.max( - Math.abs(this._mouseDownEvent.pageX - event.pageX), - Math.abs(this._mouseDownEvent.pageY - event.pageY) - ) >= this.options.distance - ); - }, - - _mouseDelayMet: function(/* event */) { - return this.mouseDelayMet; - }, - - // These are placeholder methods, to be overriden by extending plugin - _mouseStart: function(/* event */) {}, - _mouseDrag: function(/* event */) {}, - _mouseStop: function(/* event */) {}, - _mouseCapture: function(/* event */) { return true; } -}); - -})(jQuery); -(function( $, undefined ) { - -$.widget("ui.draggable", $.ui.mouse, { - version: "1.10.4", - widgetEventPrefix: "drag", - options: { - addClasses: true, - appendTo: "parent", - axis: false, - connectToSortable: false, - containment: false, - cursor: "auto", - cursorAt: false, - grid: false, - handle: false, - helper: "original", - iframeFix: false, - opacity: false, - refreshPositions: false, - revert: false, - revertDuration: 500, - scope: "default", - scroll: true, - scrollSensitivity: 20, - scrollSpeed: 20, - snap: false, - snapMode: "both", - snapTolerance: 20, - stack: false, - zIndex: false, - - // callbacks - drag: null, - start: null, - stop: null - }, - _create: function() { - - if (this.options.helper === "original" && !(/^(?:r|a|f)/).test(this.element.css("position"))) { - this.element[0].style.position = "relative"; - } - if (this.options.addClasses){ - this.element.addClass("ui-draggable"); - } - if (this.options.disabled){ - this.element.addClass("ui-draggable-disabled"); - } - - this._mouseInit(); - - }, - - _destroy: function() { - this.element.removeClass( "ui-draggable ui-draggable-dragging ui-draggable-disabled" ); - this._mouseDestroy(); - }, - - _mouseCapture: function(event) { - - var o = this.options; - - // among others, prevent a drag on a resizable-handle - if (this.helper || o.disabled || $(event.target).closest(".ui-resizable-handle").length > 0) { - return false; - } - - //Quit if we're not on a valid handle - this.handle = this._getHandle(event); - if (!this.handle) { - return false; - } - - $(o.iframeFix === true ? "iframe" : o.iframeFix).each(function() { - $("<div class='ui-draggable-iframeFix' style='background: #fff;'></div>") - .css({ - width: this.offsetWidth+"px", height: this.offsetHeight+"px", - position: "absolute", opacity: "0.001", zIndex: 1000 - }) - .css($(this).offset()) - .appendTo("body"); - }); - - return true; - - }, - - _mouseStart: function(event) { - - var o = this.options; - - //Create and append the visible helper - this.helper = this._createHelper(event); - - this.helper.addClass("ui-draggable-dragging"); - - //Cache the helper size - this._cacheHelperProportions(); - - //If ddmanager is used for droppables, set the global draggable - if($.ui.ddmanager) { - $.ui.ddmanager.current = this; - } - - /* - * - Position generation - - * This block generates everything position related - it's the core of draggables. - */ - - //Cache the margins of the original element - this._cacheMargins(); - - //Store the helper's css position - this.cssPosition = this.helper.css( "position" ); - this.scrollParent = this.helper.scrollParent(); - this.offsetParent = this.helper.offsetParent(); - this.offsetParentCssPosition = this.offsetParent.css( "position" ); - - //The element's absolute position on the page minus margins - this.offset = this.positionAbs = this.element.offset(); - this.offset = { - top: this.offset.top - this.margins.top, - left: this.offset.left - this.margins.left - }; - - //Reset scroll cache - this.offset.scroll = false; - - $.extend(this.offset, { - click: { //Where the click happened, relative to the element - left: event.pageX - this.offset.left, - top: event.pageY - this.offset.top - }, - parent: this._getParentOffset(), - relative: this._getRelativeOffset() //This is a relative to absolute position minus the actual position calculation - only used for relative positioned helper - }); - - //Generate the original position - this.originalPosition = this.position = this._generatePosition(event); - this.originalPageX = event.pageX; - this.originalPageY = event.pageY; - - //Adjust the mouse offset relative to the helper if "cursorAt" is supplied - (o.cursorAt && this._adjustOffsetFromHelper(o.cursorAt)); - - //Set a containment if given in the options - this._setContainment(); - - //Trigger event + callbacks - if(this._trigger("start", event) === false) { - this._clear(); - return false; - } - - //Recache the helper size - this._cacheHelperProportions(); - - //Prepare the droppable offsets - if ($.ui.ddmanager && !o.dropBehaviour) { - $.ui.ddmanager.prepareOffsets(this, event); - } - - - this._mouseDrag(event, true); //Execute the drag once - this causes the helper not to be visible before getting its correct position - - //If the ddmanager is used for droppables, inform the manager that dragging has started (see #5003) - if ( $.ui.ddmanager ) { - $.ui.ddmanager.dragStart(this, event); - } - - return true; - }, - - _mouseDrag: function(event, noPropagation) { - // reset any necessary cached properties (see #5009) - if ( this.offsetParentCssPosition === "fixed" ) { - this.offset.parent = this._getParentOffset(); - } - - //Compute the helpers position - this.position = this._generatePosition(event); - this.positionAbs = this._convertPositionTo("absolute"); - - //Call plugins and callbacks and use the resulting position if something is returned - if (!noPropagation) { - var ui = this._uiHash(); - if(this._trigger("drag", event, ui) === false) { - this._mouseUp({}); - return false; - } - this.position = ui.position; - } - - if(!this.options.axis || this.options.axis !== "y") { - this.helper[0].style.left = this.position.left+"px"; - } - if(!this.options.axis || this.options.axis !== "x") { - this.helper[0].style.top = this.position.top+"px"; - } - if($.ui.ddmanager) { - $.ui.ddmanager.drag(this, event); - } - - return false; - }, - - _mouseStop: function(event) { - - //If we are using droppables, inform the manager about the drop - var that = this, - dropped = false; - if ($.ui.ddmanager && !this.options.dropBehaviour) { - dropped = $.ui.ddmanager.drop(this, event); - } - - //if a drop comes from outside (a sortable) - if(this.dropped) { - dropped = this.dropped; - this.dropped = false; - } - - //if the original element is no longer in the DOM don't bother to continue (see #8269) - if ( this.options.helper === "original" && !$.contains( this.element[ 0 ].ownerDocument, this.element[ 0 ] ) ) { - return false; - } - - if((this.options.revert === "invalid" && !dropped) || (this.options.revert === "valid" && dropped) || this.options.revert === true || ($.isFunction(this.options.revert) && this.options.revert.call(this.element, dropped))) { - $(this.helper).animate(this.originalPosition, parseInt(this.options.revertDuration, 10), function() { - if(that._trigger("stop", event) !== false) { - that._clear(); - } - }); - } else { - if(this._trigger("stop", event) !== false) { - this._clear(); - } - } - - return false; - }, - - _mouseUp: function(event) { - //Remove frame helpers - $("div.ui-draggable-iframeFix").each(function() { - this.parentNode.removeChild(this); - }); - - //If the ddmanager is used for droppables, inform the manager that dragging has stopped (see #5003) - if( $.ui.ddmanager ) { - $.ui.ddmanager.dragStop(this, event); - } - - return $.ui.mouse.prototype._mouseUp.call(this, event); - }, - - cancel: function() { - - if(this.helper.is(".ui-draggable-dragging")) { - this._mouseUp({}); - } else { - this._clear(); - } - - return this; - - }, - - _getHandle: function(event) { - return this.options.handle ? - !!$( event.target ).closest( this.element.find( this.options.handle ) ).length : - true; - }, - - _createHelper: function(event) { - - var o = this.options, - helper = $.isFunction(o.helper) ? $(o.helper.apply(this.element[0], [event])) : (o.helper === "clone" ? this.element.clone().removeAttr("id") : this.element); - - if(!helper.parents("body").length) { - helper.appendTo((o.appendTo === "parent" ? this.element[0].parentNode : o.appendTo)); - } - - if(helper[0] !== this.element[0] && !(/(fixed|absolute)/).test(helper.css("position"))) { - helper.css("position", "absolute"); - } - - return helper; - - }, - - _adjustOffsetFromHelper: function(obj) { - if (typeof obj === "string") { - obj = obj.split(" "); - } - if ($.isArray(obj)) { - obj = {left: +obj[0], top: +obj[1] || 0}; - } - if ("left" in obj) { - this.offset.click.left = obj.left + this.margins.left; - } - if ("right" in obj) { - this.offset.click.left = this.helperProportions.width - obj.right + this.margins.left; - } - if ("top" in obj) { - this.offset.click.top = obj.top + this.margins.top; - } - if ("bottom" in obj) { - this.offset.click.top = this.helperProportions.height - obj.bottom + this.margins.top; - } - }, - - _getParentOffset: function() { - - //Get the offsetParent and cache its position - var po = this.offsetParent.offset(); - - // This is a special case where we need to modify a offset calculated on start, since the following happened: - // 1. The position of the helper is absolute, so it's position is calculated based on the next positioned parent - // 2. The actual offset parent is a child of the scroll parent, and the scroll parent isn't the document, which means that - // the scroll is included in the initial calculation of the offset of the parent, and never recalculated upon drag - if(this.cssPosition === "absolute" && this.scrollParent[0] !== document && $.contains(this.scrollParent[0], this.offsetParent[0])) { - po.left += this.scrollParent.scrollLeft(); - po.top += this.scrollParent.scrollTop(); - } - - //This needs to be actually done for all browsers, since pageX/pageY includes this information - //Ugly IE fix - if((this.offsetParent[0] === document.body) || - (this.offsetParent[0].tagName && this.offsetParent[0].tagName.toLowerCase() === "html" && $.ui.ie)) { - po = { top: 0, left: 0 }; - } - - return { - top: po.top + (parseInt(this.offsetParent.css("borderTopWidth"),10) || 0), - left: po.left + (parseInt(this.offsetParent.css("borderLeftWidth"),10) || 0) - }; - - }, - - _getRelativeOffset: function() { - - if(this.cssPosition === "relative") { - var p = this.element.position(); - return { - top: p.top - (parseInt(this.helper.css("top"),10) || 0) + this.scrollParent.scrollTop(), - left: p.left - (parseInt(this.helper.css("left"),10) || 0) + this.scrollParent.scrollLeft() - }; - } else { - return { top: 0, left: 0 }; - } - - }, - - _cacheMargins: function() { - this.margins = { - left: (parseInt(this.element.css("marginLeft"),10) || 0), - top: (parseInt(this.element.css("marginTop"),10) || 0), - right: (parseInt(this.element.css("marginRight"),10) || 0), - bottom: (parseInt(this.element.css("marginBottom"),10) || 0) - }; - }, - - _cacheHelperProportions: function() { - this.helperProportions = { - width: this.helper.outerWidth(), - height: this.helper.outerHeight() - }; - }, - - _setContainment: function() { - - var over, c, ce, - o = this.options; - - if ( !o.containment ) { - this.containment = null; - return; - } - - if ( o.containment === "window" ) { - this.containment = [ - $( window ).scrollLeft() - this.offset.relative.left - this.offset.parent.left, - $( window ).scrollTop() - this.offset.relative.top - this.offset.parent.top, - $( window ).scrollLeft() + $( window ).width() - this.helperProportions.width - this.margins.left, - $( window ).scrollTop() + ( $( window ).height() || document.body.parentNode.scrollHeight ) - this.helperProportions.height - this.margins.top - ]; - return; - } - - if ( o.containment === "document") { - this.containment = [ - 0, - 0, - $( document ).width() - this.helperProportions.width - this.margins.left, - ( $( document ).height() || document.body.parentNode.scrollHeight ) - this.helperProportions.height - this.margins.top - ]; - return; - } - - if ( o.containment.constructor === Array ) { - this.containment = o.containment; - return; - } - - if ( o.containment === "parent" ) { - o.containment = this.helper[ 0 ].parentNode; - } - - c = $( o.containment ); - ce = c[ 0 ]; - - if( !ce ) { - return; - } - - over = c.css( "overflow" ) !== "hidden"; - - this.containment = [ - ( parseInt( c.css( "borderLeftWidth" ), 10 ) || 0 ) + ( parseInt( c.css( "paddingLeft" ), 10 ) || 0 ), - ( parseInt( c.css( "borderTopWidth" ), 10 ) || 0 ) + ( parseInt( c.css( "paddingTop" ), 10 ) || 0 ) , - ( over ? Math.max( ce.scrollWidth, ce.offsetWidth ) : ce.offsetWidth ) - ( parseInt( c.css( "borderRightWidth" ), 10 ) || 0 ) - ( parseInt( c.css( "paddingRight" ), 10 ) || 0 ) - this.helperProportions.width - this.margins.left - this.margins.right, - ( over ? Math.max( ce.scrollHeight, ce.offsetHeight ) : ce.offsetHeight ) - ( parseInt( c.css( "borderBottomWidth" ), 10 ) || 0 ) - ( parseInt( c.css( "paddingBottom" ), 10 ) || 0 ) - this.helperProportions.height - this.margins.top - this.margins.bottom - ]; - this.relative_container = c; - }, - - _convertPositionTo: function(d, pos) { - - if(!pos) { - pos = this.position; - } - - var mod = d === "absolute" ? 1 : -1, - scroll = this.cssPosition === "absolute" && !( this.scrollParent[ 0 ] !== document && $.contains( this.scrollParent[ 0 ], this.offsetParent[ 0 ] ) ) ? this.offsetParent : this.scrollParent; - - //Cache the scroll - if (!this.offset.scroll) { - this.offset.scroll = {top : scroll.scrollTop(), left : scroll.scrollLeft()}; - } - - return { - top: ( - pos.top + // The absolute mouse position - this.offset.relative.top * mod + // Only for relative positioned nodes: Relative offset from element to offset parent - this.offset.parent.top * mod - // The offsetParent's offset without borders (offset + border) - ( ( this.cssPosition === "fixed" ? -this.scrollParent.scrollTop() : this.offset.scroll.top ) * mod ) - ), - left: ( - pos.left + // The absolute mouse position - this.offset.relative.left * mod + // Only for relative positioned nodes: Relative offset from element to offset parent - this.offset.parent.left * mod - // The offsetParent's offset without borders (offset + border) - ( ( this.cssPosition === "fixed" ? -this.scrollParent.scrollLeft() : this.offset.scroll.left ) * mod ) - ) - }; - - }, - - _generatePosition: function(event) { - - var containment, co, top, left, - o = this.options, - scroll = this.cssPosition === "absolute" && !( this.scrollParent[ 0 ] !== document && $.contains( this.scrollParent[ 0 ], this.offsetParent[ 0 ] ) ) ? this.offsetParent : this.scrollParent, - pageX = event.pageX, - pageY = event.pageY; - - //Cache the scroll - if (!this.offset.scroll) { - this.offset.scroll = {top : scroll.scrollTop(), left : scroll.scrollLeft()}; - } - - /* - * - Position constraining - - * Constrain the position to a mix of grid, containment. - */ - - // If we are not dragging yet, we won't check for options - if ( this.originalPosition ) { - if ( this.containment ) { - if ( this.relative_container ){ - co = this.relative_container.offset(); - containment = [ - this.containment[ 0 ] + co.left, - this.containment[ 1 ] + co.top, - this.containment[ 2 ] + co.left, - this.containment[ 3 ] + co.top - ]; - } - else { - containment = this.containment; - } - - if(event.pageX - this.offset.click.left < containment[0]) { - pageX = containment[0] + this.offset.click.left; - } - if(event.pageY - this.offset.click.top < containment[1]) { - pageY = containment[1] + this.offset.click.top; - } - if(event.pageX - this.offset.click.left > containment[2]) { - pageX = containment[2] + this.offset.click.left; - } - if(event.pageY - this.offset.click.top > containment[3]) { - pageY = containment[3] + this.offset.click.top; - } - } - - if(o.grid) { - //Check for grid elements set to 0 to prevent divide by 0 error causing invalid argument errors in IE (see ticket #6950) - top = o.grid[1] ? this.originalPageY + Math.round((pageY - this.originalPageY) / o.grid[1]) * o.grid[1] : this.originalPageY; - pageY = containment ? ((top - this.offset.click.top >= containment[1] || top - this.offset.click.top > containment[3]) ? top : ((top - this.offset.click.top >= containment[1]) ? top - o.grid[1] : top + o.grid[1])) : top; - - left = o.grid[0] ? this.originalPageX + Math.round((pageX - this.originalPageX) / o.grid[0]) * o.grid[0] : this.originalPageX; - pageX = containment ? ((left - this.offset.click.left >= containment[0] || left - this.offset.click.left > containment[2]) ? left : ((left - this.offset.click.left >= containment[0]) ? left - o.grid[0] : left + o.grid[0])) : left; - } - - } - - return { - top: ( - pageY - // The absolute mouse position - this.offset.click.top - // Click offset (relative to the element) - this.offset.relative.top - // Only for relative positioned nodes: Relative offset from element to offset parent - this.offset.parent.top + // The offsetParent's offset without borders (offset + border) - ( this.cssPosition === "fixed" ? -this.scrollParent.scrollTop() : this.offset.scroll.top ) - ), - left: ( - pageX - // The absolute mouse position - this.offset.click.left - // Click offset (relative to the element) - this.offset.relative.left - // Only for relative positioned nodes: Relative offset from element to offset parent - this.offset.parent.left + // The offsetParent's offset without borders (offset + border) - ( this.cssPosition === "fixed" ? -this.scrollParent.scrollLeft() : this.offset.scroll.left ) - ) - }; - - }, - - _clear: function() { - this.helper.removeClass("ui-draggable-dragging"); - if(this.helper[0] !== this.element[0] && !this.cancelHelperRemoval) { - this.helper.remove(); - } - this.helper = null; - this.cancelHelperRemoval = false; - }, - - // From now on bulk stuff - mainly helpers - - _trigger: function(type, event, ui) { - ui = ui || this._uiHash(); - $.ui.plugin.call(this, type, [event, ui]); - //The absolute position has to be recalculated after plugins - if(type === "drag") { - this.positionAbs = this._convertPositionTo("absolute"); - } - return $.Widget.prototype._trigger.call(this, type, event, ui); - }, - - plugins: {}, - - _uiHash: function() { - return { - helper: this.helper, - position: this.position, - originalPosition: this.originalPosition, - offset: this.positionAbs - }; - } - -}); - -$.ui.plugin.add("draggable", "connectToSortable", { - start: function(event, ui) { - - var inst = $(this).data("ui-draggable"), o = inst.options, - uiSortable = $.extend({}, ui, { item: inst.element }); - inst.sortables = []; - $(o.connectToSortable).each(function() { - var sortable = $.data(this, "ui-sortable"); - if (sortable && !sortable.options.disabled) { - inst.sortables.push({ - instance: sortable, - shouldRevert: sortable.options.revert - }); - sortable.refreshPositions(); // Call the sortable's refreshPositions at drag start to refresh the containerCache since the sortable container cache is used in drag and needs to be up to date (this will ensure it's initialised as well as being kept in step with any changes that might have happened on the page). - sortable._trigger("activate", event, uiSortable); - } - }); - - }, - stop: function(event, ui) { - - //If we are still over the sortable, we fake the stop event of the sortable, but also remove helper - var inst = $(this).data("ui-draggable"), - uiSortable = $.extend({}, ui, { item: inst.element }); - - $.each(inst.sortables, function() { - if(this.instance.isOver) { - - this.instance.isOver = 0; - - inst.cancelHelperRemoval = true; //Don't remove the helper in the draggable instance - this.instance.cancelHelperRemoval = false; //Remove it in the sortable instance (so sortable plugins like revert still work) - - //The sortable revert is supported, and we have to set a temporary dropped variable on the draggable to support revert: "valid/invalid" - if(this.shouldRevert) { - this.instance.options.revert = this.shouldRevert; - } - - //Trigger the stop of the sortable - this.instance._mouseStop(event); - - this.instance.options.helper = this.instance.options._helper; - - //If the helper has been the original item, restore properties in the sortable - if(inst.options.helper === "original") { - this.instance.currentItem.css({ top: "auto", left: "auto" }); - } - - } else { - this.instance.cancelHelperRemoval = false; //Remove the helper in the sortable instance - this.instance._trigger("deactivate", event, uiSortable); - } - - }); - - }, - drag: function(event, ui) { - - var inst = $(this).data("ui-draggable"), that = this; - - $.each(inst.sortables, function() { - - var innermostIntersecting = false, - thisSortable = this; - - //Copy over some variables to allow calling the sortable's native _intersectsWith - this.instance.positionAbs = inst.positionAbs; - this.instance.helperProportions = inst.helperProportions; - this.instance.offset.click = inst.offset.click; - - if(this.instance._intersectsWith(this.instance.containerCache)) { - innermostIntersecting = true; - $.each(inst.sortables, function () { - this.instance.positionAbs = inst.positionAbs; - this.instance.helperProportions = inst.helperProportions; - this.instance.offset.click = inst.offset.click; - if (this !== thisSortable && - this.instance._intersectsWith(this.instance.containerCache) && - $.contains(thisSortable.instance.element[0], this.instance.element[0]) - ) { - innermostIntersecting = false; - } - return innermostIntersecting; - }); - } - - - if(innermostIntersecting) { - //If it intersects, we use a little isOver variable and set it once, so our move-in stuff gets fired only once - if(!this.instance.isOver) { - - this.instance.isOver = 1; - //Now we fake the start of dragging for the sortable instance, - //by cloning the list group item, appending it to the sortable and using it as inst.currentItem - //We can then fire the start event of the sortable with our passed browser event, and our own helper (so it doesn't create a new one) - this.instance.currentItem = $(that).clone().removeAttr("id").appendTo(this.instance.element).data("ui-sortable-item", true); - this.instance.options._helper = this.instance.options.helper; //Store helper option to later restore it - this.instance.options.helper = function() { return ui.helper[0]; }; - - event.target = this.instance.currentItem[0]; - this.instance._mouseCapture(event, true); - this.instance._mouseStart(event, true, true); - - //Because the browser event is way off the new appended portlet, we modify a couple of variables to reflect the changes - this.instance.offset.click.top = inst.offset.click.top; - this.instance.offset.click.left = inst.offset.click.left; - this.instance.offset.parent.left -= inst.offset.parent.left - this.instance.offset.parent.left; - this.instance.offset.parent.top -= inst.offset.parent.top - this.instance.offset.parent.top; - - inst._trigger("toSortable", event); - inst.dropped = this.instance.element; //draggable revert needs that - //hack so receive/update callbacks work (mostly) - inst.currentItem = inst.element; - this.instance.fromOutside = inst; - - } - - //Provided we did all the previous steps, we can fire the drag event of the sortable on every draggable drag, when it intersects with the sortable - if(this.instance.currentItem) { - this.instance._mouseDrag(event); - } - - } else { - - //If it doesn't intersect with the sortable, and it intersected before, - //we fake the drag stop of the sortable, but make sure it doesn't remove the helper by using cancelHelperRemoval - if(this.instance.isOver) { - - this.instance.isOver = 0; - this.instance.cancelHelperRemoval = true; - - //Prevent reverting on this forced stop - this.instance.options.revert = false; - - // The out event needs to be triggered independently - this.instance._trigger("out", event, this.instance._uiHash(this.instance)); - - this.instance._mouseStop(event, true); - this.instance.options.helper = this.instance.options._helper; - - //Now we remove our currentItem, the list group clone again, and the placeholder, and animate the helper back to it's original size - this.instance.currentItem.remove(); - if(this.instance.placeholder) { - this.instance.placeholder.remove(); - } - - inst._trigger("fromSortable", event); - inst.dropped = false; //draggable revert needs that - } - - } - - }); - - } -}); - -$.ui.plugin.add("draggable", "cursor", { - start: function() { - var t = $("body"), o = $(this).data("ui-draggable").options; - if (t.css("cursor")) { - o._cursor = t.css("cursor"); - } - t.css("cursor", o.cursor); - }, - stop: function() { - var o = $(this).data("ui-draggable").options; - if (o._cursor) { - $("body").css("cursor", o._cursor); - } - } -}); - -$.ui.plugin.add("draggable", "opacity", { - start: function(event, ui) { - var t = $(ui.helper), o = $(this).data("ui-draggable").options; - if(t.css("opacity")) { - o._opacity = t.css("opacity"); - } - t.css("opacity", o.opacity); - }, - stop: function(event, ui) { - var o = $(this).data("ui-draggable").options; - if(o._opacity) { - $(ui.helper).css("opacity", o._opacity); - } - } -}); - -$.ui.plugin.add("draggable", "scroll", { - start: function() { - var i = $(this).data("ui-draggable"); - if(i.scrollParent[0] !== document && i.scrollParent[0].tagName !== "HTML") { - i.overflowOffset = i.scrollParent.offset(); - } - }, - drag: function( event ) { - - var i = $(this).data("ui-draggable"), o = i.options, scrolled = false; - - if(i.scrollParent[0] !== document && i.scrollParent[0].tagName !== "HTML") { - - if(!o.axis || o.axis !== "x") { - if((i.overflowOffset.top + i.scrollParent[0].offsetHeight) - event.pageY < o.scrollSensitivity) { - i.scrollParent[0].scrollTop = scrolled = i.scrollParent[0].scrollTop + o.scrollSpeed; - } else if(event.pageY - i.overflowOffset.top < o.scrollSensitivity) { - i.scrollParent[0].scrollTop = scrolled = i.scrollParent[0].scrollTop - o.scrollSpeed; - } - } - - if(!o.axis || o.axis !== "y") { - if((i.overflowOffset.left + i.scrollParent[0].offsetWidth) - event.pageX < o.scrollSensitivity) { - i.scrollParent[0].scrollLeft = scrolled = i.scrollParent[0].scrollLeft + o.scrollSpeed; - } else if(event.pageX - i.overflowOffset.left < o.scrollSensitivity) { - i.scrollParent[0].scrollLeft = scrolled = i.scrollParent[0].scrollLeft - o.scrollSpeed; - } - } - - } else { - - if(!o.axis || o.axis !== "x") { - if(event.pageY - $(document).scrollTop() < o.scrollSensitivity) { - scrolled = $(document).scrollTop($(document).scrollTop() - o.scrollSpeed); - } else if($(window).height() - (event.pageY - $(document).scrollTop()) < o.scrollSensitivity) { - scrolled = $(document).scrollTop($(document).scrollTop() + o.scrollSpeed); - } - } - - if(!o.axis || o.axis !== "y") { - if(event.pageX - $(document).scrollLeft() < o.scrollSensitivity) { - scrolled = $(document).scrollLeft($(document).scrollLeft() - o.scrollSpeed); - } else if($(window).width() - (event.pageX - $(document).scrollLeft()) < o.scrollSensitivity) { - scrolled = $(document).scrollLeft($(document).scrollLeft() + o.scrollSpeed); - } - } - - } - - if(scrolled !== false && $.ui.ddmanager && !o.dropBehaviour) { - $.ui.ddmanager.prepareOffsets(i, event); - } - - } -}); - -$.ui.plugin.add("draggable", "snap", { - start: function() { - - var i = $(this).data("ui-draggable"), - o = i.options; - - i.snapElements = []; - - $(o.snap.constructor !== String ? ( o.snap.items || ":data(ui-draggable)" ) : o.snap).each(function() { - var $t = $(this), - $o = $t.offset(); - if(this !== i.element[0]) { - i.snapElements.push({ - item: this, - width: $t.outerWidth(), height: $t.outerHeight(), - top: $o.top, left: $o.left - }); - } - }); - - }, - drag: function(event, ui) { - - var ts, bs, ls, rs, l, r, t, b, i, first, - inst = $(this).data("ui-draggable"), - o = inst.options, - d = o.snapTolerance, - x1 = ui.offset.left, x2 = x1 + inst.helperProportions.width, - y1 = ui.offset.top, y2 = y1 + inst.helperProportions.height; - - for (i = inst.snapElements.length - 1; i >= 0; i--){ - - l = inst.snapElements[i].left; - r = l + inst.snapElements[i].width; - t = inst.snapElements[i].top; - b = t + inst.snapElements[i].height; - - if ( x2 < l - d || x1 > r + d || y2 < t - d || y1 > b + d || !$.contains( inst.snapElements[ i ].item.ownerDocument, inst.snapElements[ i ].item ) ) { - if(inst.snapElements[i].snapping) { - (inst.options.snap.release && inst.options.snap.release.call(inst.element, event, $.extend(inst._uiHash(), { snapItem: inst.snapElements[i].item }))); - } - inst.snapElements[i].snapping = false; - continue; - } - - if(o.snapMode !== "inner") { - ts = Math.abs(t - y2) <= d; - bs = Math.abs(b - y1) <= d; - ls = Math.abs(l - x2) <= d; - rs = Math.abs(r - x1) <= d; - if(ts) { - ui.position.top = inst._convertPositionTo("relative", { top: t - inst.helperProportions.height, left: 0 }).top - inst.margins.top; - } - if(bs) { - ui.position.top = inst._convertPositionTo("relative", { top: b, left: 0 }).top - inst.margins.top; - } - if(ls) { - ui.position.left = inst._convertPositionTo("relative", { top: 0, left: l - inst.helperProportions.width }).left - inst.margins.left; - } - if(rs) { - ui.position.left = inst._convertPositionTo("relative", { top: 0, left: r }).left - inst.margins.left; - } - } - - first = (ts || bs || ls || rs); - - if(o.snapMode !== "outer") { - ts = Math.abs(t - y1) <= d; - bs = Math.abs(b - y2) <= d; - ls = Math.abs(l - x1) <= d; - rs = Math.abs(r - x2) <= d; - if(ts) { - ui.position.top = inst._convertPositionTo("relative", { top: t, left: 0 }).top - inst.margins.top; - } - if(bs) { - ui.position.top = inst._convertPositionTo("relative", { top: b - inst.helperProportions.height, left: 0 }).top - inst.margins.top; - } - if(ls) { - ui.position.left = inst._convertPositionTo("relative", { top: 0, left: l }).left - inst.margins.left; - } - if(rs) { - ui.position.left = inst._convertPositionTo("relative", { top: 0, left: r - inst.helperProportions.width }).left - inst.margins.left; - } - } - - if(!inst.snapElements[i].snapping && (ts || bs || ls || rs || first)) { - (inst.options.snap.snap && inst.options.snap.snap.call(inst.element, event, $.extend(inst._uiHash(), { snapItem: inst.snapElements[i].item }))); - } - inst.snapElements[i].snapping = (ts || bs || ls || rs || first); - - } - - } -}); - -$.ui.plugin.add("draggable", "stack", { - start: function() { - var min, - o = this.data("ui-draggable").options, - group = $.makeArray($(o.stack)).sort(function(a,b) { - return (parseInt($(a).css("zIndex"),10) || 0) - (parseInt($(b).css("zIndex"),10) || 0); - }); - - if (!group.length) { return; } - - min = parseInt($(group[0]).css("zIndex"), 10) || 0; - $(group).each(function(i) { - $(this).css("zIndex", min + i); - }); - this.css("zIndex", (min + group.length)); - } -}); - -$.ui.plugin.add("draggable", "zIndex", { - start: function(event, ui) { - var t = $(ui.helper), o = $(this).data("ui-draggable").options; - if(t.css("zIndex")) { - o._zIndex = t.css("zIndex"); - } - t.css("zIndex", o.zIndex); - }, - stop: function(event, ui) { - var o = $(this).data("ui-draggable").options; - if(o._zIndex) { - $(ui.helper).css("zIndex", o._zIndex); - } - } -}); - -})(jQuery); -(function( $, undefined ) { - -function isOverAxis( x, reference, size ) { - return ( x > reference ) && ( x < ( reference + size ) ); -} - -$.widget("ui.droppable", { - version: "1.10.4", - widgetEventPrefix: "drop", - options: { - accept: "*", - activeClass: false, - addClasses: true, - greedy: false, - hoverClass: false, - scope: "default", - tolerance: "intersect", - - // callbacks - activate: null, - deactivate: null, - drop: null, - out: null, - over: null - }, - _create: function() { - - var proportions, - o = this.options, - accept = o.accept; - - this.isover = false; - this.isout = true; - - this.accept = $.isFunction(accept) ? accept : function(d) { - return d.is(accept); - }; - - this.proportions = function( /* valueToWrite */ ) { - if ( arguments.length ) { - // Store the droppable's proportions - proportions = arguments[ 0 ]; - } else { - // Retrieve or derive the droppable's proportions - return proportions ? - proportions : - proportions = { - width: this.element[ 0 ].offsetWidth, - height: this.element[ 0 ].offsetHeight - }; - } - }; - - // Add the reference and positions to the manager - $.ui.ddmanager.droppables[o.scope] = $.ui.ddmanager.droppables[o.scope] || []; - $.ui.ddmanager.droppables[o.scope].push(this); - - (o.addClasses && this.element.addClass("ui-droppable")); - - }, - - _destroy: function() { - var i = 0, - drop = $.ui.ddmanager.droppables[this.options.scope]; - - for ( ; i < drop.length; i++ ) { - if ( drop[i] === this ) { - drop.splice(i, 1); - } - } - - this.element.removeClass("ui-droppable ui-droppable-disabled"); - }, - - _setOption: function(key, value) { - - if(key === "accept") { - this.accept = $.isFunction(value) ? value : function(d) { - return d.is(value); - }; - } - $.Widget.prototype._setOption.apply(this, arguments); - }, - - _activate: function(event) { - var draggable = $.ui.ddmanager.current; - if(this.options.activeClass) { - this.element.addClass(this.options.activeClass); - } - if(draggable){ - this._trigger("activate", event, this.ui(draggable)); - } - }, - - _deactivate: function(event) { - var draggable = $.ui.ddmanager.current; - if(this.options.activeClass) { - this.element.removeClass(this.options.activeClass); - } - if(draggable){ - this._trigger("deactivate", event, this.ui(draggable)); - } - }, - - _over: function(event) { - - var draggable = $.ui.ddmanager.current; - - // Bail if draggable and droppable are same element - if (!draggable || (draggable.currentItem || draggable.element)[0] === this.element[0]) { - return; - } - - if (this.accept.call(this.element[0],(draggable.currentItem || draggable.element))) { - if(this.options.hoverClass) { - this.element.addClass(this.options.hoverClass); - } - this._trigger("over", event, this.ui(draggable)); - } - - }, - - _out: function(event) { - - var draggable = $.ui.ddmanager.current; - - // Bail if draggable and droppable are same element - if (!draggable || (draggable.currentItem || draggable.element)[0] === this.element[0]) { - return; - } - - if (this.accept.call(this.element[0],(draggable.currentItem || draggable.element))) { - if(this.options.hoverClass) { - this.element.removeClass(this.options.hoverClass); - } - this._trigger("out", event, this.ui(draggable)); - } - - }, - - _drop: function(event,custom) { - - var draggable = custom || $.ui.ddmanager.current, - childrenIntersection = false; - - // Bail if draggable and droppable are same element - if (!draggable || (draggable.currentItem || draggable.element)[0] === this.element[0]) { - return false; - } - - this.element.find(":data(ui-droppable)").not(".ui-draggable-dragging").each(function() { - var inst = $.data(this, "ui-droppable"); - if( - inst.options.greedy && - !inst.options.disabled && - inst.options.scope === draggable.options.scope && - inst.accept.call(inst.element[0], (draggable.currentItem || draggable.element)) && - $.ui.intersect(draggable, $.extend(inst, { offset: inst.element.offset() }), inst.options.tolerance) - ) { childrenIntersection = true; return false; } - }); - if(childrenIntersection) { - return false; - } - - if(this.accept.call(this.element[0],(draggable.currentItem || draggable.element))) { - if(this.options.activeClass) { - this.element.removeClass(this.options.activeClass); - } - if(this.options.hoverClass) { - this.element.removeClass(this.options.hoverClass); - } - this._trigger("drop", event, this.ui(draggable)); - return this.element; - } - - return false; - - }, - - ui: function(c) { - return { - draggable: (c.currentItem || c.element), - helper: c.helper, - position: c.position, - offset: c.positionAbs - }; - } - -}); - -$.ui.intersect = function(draggable, droppable, toleranceMode) { - - if (!droppable.offset) { - return false; - } - - var draggableLeft, draggableTop, - x1 = (draggable.positionAbs || draggable.position.absolute).left, - y1 = (draggable.positionAbs || draggable.position.absolute).top, - x2 = x1 + draggable.helperProportions.width, - y2 = y1 + draggable.helperProportions.height, - l = droppable.offset.left, - t = droppable.offset.top, - r = l + droppable.proportions().width, - b = t + droppable.proportions().height; - - switch (toleranceMode) { - case "fit": - return (l <= x1 && x2 <= r && t <= y1 && y2 <= b); - case "intersect": - return (l < x1 + (draggable.helperProportions.width / 2) && // Right Half - x2 - (draggable.helperProportions.width / 2) < r && // Left Half - t < y1 + (draggable.helperProportions.height / 2) && // Bottom Half - y2 - (draggable.helperProportions.height / 2) < b ); // Top Half - case "pointer": - draggableLeft = ((draggable.positionAbs || draggable.position.absolute).left + (draggable.clickOffset || draggable.offset.click).left); - draggableTop = ((draggable.positionAbs || draggable.position.absolute).top + (draggable.clickOffset || draggable.offset.click).top); - return isOverAxis( draggableTop, t, droppable.proportions().height ) && isOverAxis( draggableLeft, l, droppable.proportions().width ); - case "touch": - return ( - (y1 >= t && y1 <= b) || // Top edge touching - (y2 >= t && y2 <= b) || // Bottom edge touching - (y1 < t && y2 > b) // Surrounded vertically - ) && ( - (x1 >= l && x1 <= r) || // Left edge touching - (x2 >= l && x2 <= r) || // Right edge touching - (x1 < l && x2 > r) // Surrounded horizontally - ); - default: - return false; - } - -}; - -/* - This manager tracks offsets of draggables and droppables -*/ -$.ui.ddmanager = { - current: null, - droppables: { "default": [] }, - prepareOffsets: function(t, event) { - - var i, j, - m = $.ui.ddmanager.droppables[t.options.scope] || [], - type = event ? event.type : null, // workaround for #2317 - list = (t.currentItem || t.element).find(":data(ui-droppable)").addBack(); - - droppablesLoop: for (i = 0; i < m.length; i++) { - - //No disabled and non-accepted - if(m[i].options.disabled || (t && !m[i].accept.call(m[i].element[0],(t.currentItem || t.element)))) { - continue; - } - - // Filter out elements in the current dragged item - for (j=0; j < list.length; j++) { - if(list[j] === m[i].element[0]) { - m[i].proportions().height = 0; - continue droppablesLoop; - } - } - - m[i].visible = m[i].element.css("display") !== "none"; - if(!m[i].visible) { - continue; - } - - //Activate the droppable if used directly from draggables - if(type === "mousedown") { - m[i]._activate.call(m[i], event); - } - - m[ i ].offset = m[ i ].element.offset(); - m[ i ].proportions({ width: m[ i ].element[ 0 ].offsetWidth, height: m[ i ].element[ 0 ].offsetHeight }); - - } - - }, - drop: function(draggable, event) { - - var dropped = false; - // Create a copy of the droppables in case the list changes during the drop (#9116) - $.each(($.ui.ddmanager.droppables[draggable.options.scope] || []).slice(), function() { - - if(!this.options) { - return; - } - if (!this.options.disabled && this.visible && $.ui.intersect(draggable, this, this.options.tolerance)) { - dropped = this._drop.call(this, event) || dropped; - } - - if (!this.options.disabled && this.visible && this.accept.call(this.element[0],(draggable.currentItem || draggable.element))) { - this.isout = true; - this.isover = false; - this._deactivate.call(this, event); - } - - }); - return dropped; - - }, - dragStart: function( draggable, event ) { - //Listen for scrolling so that if the dragging causes scrolling the position of the droppables can be recalculated (see #5003) - draggable.element.parentsUntil( "body" ).bind( "scroll.droppable", function() { - if( !draggable.options.refreshPositions ) { - $.ui.ddmanager.prepareOffsets( draggable, event ); - } - }); - }, - drag: function(draggable, event) { - - //If you have a highly dynamic page, you might try this option. It renders positions every time you move the mouse. - if(draggable.options.refreshPositions) { - $.ui.ddmanager.prepareOffsets(draggable, event); - } - - //Run through all droppables and check their positions based on specific tolerance options - $.each($.ui.ddmanager.droppables[draggable.options.scope] || [], function() { - - if(this.options.disabled || this.greedyChild || !this.visible) { - return; - } - - var parentInstance, scope, parent, - intersects = $.ui.intersect(draggable, this, this.options.tolerance), - c = !intersects && this.isover ? "isout" : (intersects && !this.isover ? "isover" : null); - if(!c) { - return; - } - - if (this.options.greedy) { - // find droppable parents with same scope - scope = this.options.scope; - parent = this.element.parents(":data(ui-droppable)").filter(function () { - return $.data(this, "ui-droppable").options.scope === scope; - }); - - if (parent.length) { - parentInstance = $.data(parent[0], "ui-droppable"); - parentInstance.greedyChild = (c === "isover"); - } - } - - // we just moved into a greedy child - if (parentInstance && c === "isover") { - parentInstance.isover = false; - parentInstance.isout = true; - parentInstance._out.call(parentInstance, event); - } - - this[c] = true; - this[c === "isout" ? "isover" : "isout"] = false; - this[c === "isover" ? "_over" : "_out"].call(this, event); - - // we just moved out of a greedy child - if (parentInstance && c === "isout") { - parentInstance.isout = false; - parentInstance.isover = true; - parentInstance._over.call(parentInstance, event); - } - }); - - }, - dragStop: function( draggable, event ) { - draggable.element.parentsUntil( "body" ).unbind( "scroll.droppable" ); - //Call prepareOffsets one final time since IE does not fire return scroll events when overflow was caused by drag (see #5003) - if( !draggable.options.refreshPositions ) { - $.ui.ddmanager.prepareOffsets( draggable, event ); - } - } -}; - -})(jQuery); -(function( $, undefined ) { - -function isOverAxis( x, reference, size ) { - return ( x > reference ) && ( x < ( reference + size ) ); -} - -function isFloating(item) { - return (/left|right/).test(item.css("float")) || (/inline|table-cell/).test(item.css("display")); -} - -$.widget("ui.sortable", $.ui.mouse, { - version: "1.10.4", - widgetEventPrefix: "sort", - ready: false, - options: { - appendTo: "parent", - axis: false, - connectWith: false, - containment: false, - cursor: "auto", - cursorAt: false, - dropOnEmpty: true, - forcePlaceholderSize: false, - forceHelperSize: false, - grid: false, - handle: false, - helper: "original", - items: "> *", - opacity: false, - placeholder: false, - revert: false, - scroll: true, - scrollSensitivity: 20, - scrollSpeed: 20, - scope: "default", - tolerance: "intersect", - zIndex: 1000, - - // callbacks - activate: null, - beforeStop: null, - change: null, - deactivate: null, - out: null, - over: null, - receive: null, - remove: null, - sort: null, - start: null, - stop: null, - update: null - }, - _create: function() { - - var o = this.options; - this.containerCache = {}; - this.element.addClass("ui-sortable"); - - //Get the items - this.refresh(); - - //Let's determine if the items are being displayed horizontally - this.floating = this.items.length ? o.axis === "x" || isFloating(this.items[0].item) : false; - - //Let's determine the parent's offset - this.offset = this.element.offset(); - - //Initialize mouse events for interaction - this._mouseInit(); - - //We're ready to go - this.ready = true; - - }, - - _destroy: function() { - this.element - .removeClass("ui-sortable ui-sortable-disabled"); - this._mouseDestroy(); - - for ( var i = this.items.length - 1; i >= 0; i-- ) { - this.items[i].item.removeData(this.widgetName + "-item"); - } - - return this; - }, - - _setOption: function(key, value){ - if ( key === "disabled" ) { - this.options[ key ] = value; - - this.widget().toggleClass( "ui-sortable-disabled", !!value ); - } else { - // Don't call widget base _setOption for disable as it adds ui-state-disabled class - $.Widget.prototype._setOption.apply(this, arguments); - } - }, - - _mouseCapture: function(event, overrideHandle) { - var currentItem = null, - validHandle = false, - that = this; - - if (this.reverting) { - return false; - } - - if(this.options.disabled || this.options.type === "static") { - return false; - } - - //We have to refresh the items data once first - this._refreshItems(event); - - //Find out if the clicked node (or one of its parents) is a actual item in this.items - $(event.target).parents().each(function() { - if($.data(this, that.widgetName + "-item") === that) { - currentItem = $(this); - return false; - } - }); - if($.data(event.target, that.widgetName + "-item") === that) { - currentItem = $(event.target); - } - - if(!currentItem) { - return false; - } - if(this.options.handle && !overrideHandle) { - $(this.options.handle, currentItem).find("*").addBack().each(function() { - if(this === event.target) { - validHandle = true; - } - }); - if(!validHandle) { - return false; - } - } - - this.currentItem = currentItem; - this._removeCurrentsFromItems(); - return true; - - }, - - _mouseStart: function(event, overrideHandle, noActivation) { - - var i, body, - o = this.options; - - this.currentContainer = this; - - //We only need to call refreshPositions, because the refreshItems call has been moved to mouseCapture - this.refreshPositions(); - - //Create and append the visible helper - this.helper = this._createHelper(event); - - //Cache the helper size - this._cacheHelperProportions(); - - /* - * - Position generation - - * This block generates everything position related - it's the core of draggables. - */ - - //Cache the margins of the original element - this._cacheMargins(); - - //Get the next scrolling parent - this.scrollParent = this.helper.scrollParent(); - - //The element's absolute position on the page minus margins - this.offset = this.currentItem.offset(); - this.offset = { - top: this.offset.top - this.margins.top, - left: this.offset.left - this.margins.left - }; - - $.extend(this.offset, { - click: { //Where the click happened, relative to the element - left: event.pageX - this.offset.left, - top: event.pageY - this.offset.top - }, - parent: this._getParentOffset(), - relative: this._getRelativeOffset() //This is a relative to absolute position minus the actual position calculation - only used for relative positioned helper - }); - - // Only after we got the offset, we can change the helper's position to absolute - // TODO: Still need to figure out a way to make relative sorting possible - this.helper.css("position", "absolute"); - this.cssPosition = this.helper.css("position"); - - //Generate the original position - this.originalPosition = this._generatePosition(event); - this.originalPageX = event.pageX; - this.originalPageY = event.pageY; - - //Adjust the mouse offset relative to the helper if "cursorAt" is supplied - (o.cursorAt && this._adjustOffsetFromHelper(o.cursorAt)); - - //Cache the former DOM position - this.domPosition = { prev: this.currentItem.prev()[0], parent: this.currentItem.parent()[0] }; - - //If the helper is not the original, hide the original so it's not playing any role during the drag, won't cause anything bad this way - if(this.helper[0] !== this.currentItem[0]) { - this.currentItem.hide(); - } - - //Create the placeholder - this._createPlaceholder(); - - //Set a containment if given in the options - if(o.containment) { - this._setContainment(); - } - - if( o.cursor && o.cursor !== "auto" ) { // cursor option - body = this.document.find( "body" ); - - // support: IE - this.storedCursor = body.css( "cursor" ); - body.css( "cursor", o.cursor ); - - this.storedStylesheet = $( "<style>*{ cursor: "+o.cursor+" !important; }</style>" ).appendTo( body ); - } - - if(o.opacity) { // opacity option - if (this.helper.css("opacity")) { - this._storedOpacity = this.helper.css("opacity"); - } - this.helper.css("opacity", o.opacity); - } - - if(o.zIndex) { // zIndex option - if (this.helper.css("zIndex")) { - this._storedZIndex = this.helper.css("zIndex"); - } - this.helper.css("zIndex", o.zIndex); - } - - //Prepare scrolling - if(this.scrollParent[0] !== document && this.scrollParent[0].tagName !== "HTML") { - this.overflowOffset = this.scrollParent.offset(); - } - - //Call callbacks - this._trigger("start", event, this._uiHash()); - - //Recache the helper size - if(!this._preserveHelperProportions) { - this._cacheHelperProportions(); - } - - - //Post "activate" events to possible containers - if( !noActivation ) { - for ( i = this.containers.length - 1; i >= 0; i-- ) { - this.containers[ i ]._trigger( "activate", event, this._uiHash( this ) ); - } - } - - //Prepare possible droppables - if($.ui.ddmanager) { - $.ui.ddmanager.current = this; - } - - if ($.ui.ddmanager && !o.dropBehaviour) { - $.ui.ddmanager.prepareOffsets(this, event); - } - - this.dragging = true; - - this.helper.addClass("ui-sortable-helper"); - this._mouseDrag(event); //Execute the drag once - this causes the helper not to be visible before getting its correct position - return true; - - }, - - _mouseDrag: function(event) { - var i, item, itemElement, intersection, - o = this.options, - scrolled = false; - - //Compute the helpers position - this.position = this._generatePosition(event); - this.positionAbs = this._convertPositionTo("absolute"); - - if (!this.lastPositionAbs) { - this.lastPositionAbs = this.positionAbs; - } - - //Do scrolling - if(this.options.scroll) { - if(this.scrollParent[0] !== document && this.scrollParent[0].tagName !== "HTML") { - - if((this.overflowOffset.top + this.scrollParent[0].offsetHeight) - event.pageY < o.scrollSensitivity) { - this.scrollParent[0].scrollTop = scrolled = this.scrollParent[0].scrollTop + o.scrollSpeed; - } else if(event.pageY - this.overflowOffset.top < o.scrollSensitivity) { - this.scrollParent[0].scrollTop = scrolled = this.scrollParent[0].scrollTop - o.scrollSpeed; - } - - if((this.overflowOffset.left + this.scrollParent[0].offsetWidth) - event.pageX < o.scrollSensitivity) { - this.scrollParent[0].scrollLeft = scrolled = this.scrollParent[0].scrollLeft + o.scrollSpeed; - } else if(event.pageX - this.overflowOffset.left < o.scrollSensitivity) { - this.scrollParent[0].scrollLeft = scrolled = this.scrollParent[0].scrollLeft - o.scrollSpeed; - } - - } else { - - if(event.pageY - $(document).scrollTop() < o.scrollSensitivity) { - scrolled = $(document).scrollTop($(document).scrollTop() - o.scrollSpeed); - } else if($(window).height() - (event.pageY - $(document).scrollTop()) < o.scrollSensitivity) { - scrolled = $(document).scrollTop($(document).scrollTop() + o.scrollSpeed); - } - - if(event.pageX - $(document).scrollLeft() < o.scrollSensitivity) { - scrolled = $(document).scrollLeft($(document).scrollLeft() - o.scrollSpeed); - } else if($(window).width() - (event.pageX - $(document).scrollLeft()) < o.scrollSensitivity) { - scrolled = $(document).scrollLeft($(document).scrollLeft() + o.scrollSpeed); - } - - } - - if(scrolled !== false && $.ui.ddmanager && !o.dropBehaviour) { - $.ui.ddmanager.prepareOffsets(this, event); - } - } - - //Regenerate the absolute position used for position checks - this.positionAbs = this._convertPositionTo("absolute"); - - //Set the helper position - if(!this.options.axis || this.options.axis !== "y") { - this.helper[0].style.left = this.position.left+"px"; - } - if(!this.options.axis || this.options.axis !== "x") { - this.helper[0].style.top = this.position.top+"px"; - } - - //Rearrange - for (i = this.items.length - 1; i >= 0; i--) { - - //Cache variables and intersection, continue if no intersection - item = this.items[i]; - itemElement = item.item[0]; - intersection = this._intersectsWithPointer(item); - if (!intersection) { - continue; - } - - // Only put the placeholder inside the current Container, skip all - // items from other containers. This works because when moving - // an item from one container to another the - // currentContainer is switched before the placeholder is moved. - // - // Without this, moving items in "sub-sortables" can cause - // the placeholder to jitter beetween the outer and inner container. - if (item.instance !== this.currentContainer) { - continue; - } - - // cannot intersect with itself - // no useless actions that have been done before - // no action if the item moved is the parent of the item checked - if (itemElement !== this.currentItem[0] && - this.placeholder[intersection === 1 ? "next" : "prev"]()[0] !== itemElement && - !$.contains(this.placeholder[0], itemElement) && - (this.options.type === "semi-dynamic" ? !$.contains(this.element[0], itemElement) : true) - ) { - - this.direction = intersection === 1 ? "down" : "up"; - - if (this.options.tolerance === "pointer" || this._intersectsWithSides(item)) { - this._rearrange(event, item); - } else { - break; - } - - this._trigger("change", event, this._uiHash()); - break; - } - } - - //Post events to containers - this._contactContainers(event); - - //Interconnect with droppables - if($.ui.ddmanager) { - $.ui.ddmanager.drag(this, event); - } - - //Call callbacks - this._trigger("sort", event, this._uiHash()); - - this.lastPositionAbs = this.positionAbs; - return false; - - }, - - _mouseStop: function(event, noPropagation) { - - if(!event) { - return; - } - - //If we are using droppables, inform the manager about the drop - if ($.ui.ddmanager && !this.options.dropBehaviour) { - $.ui.ddmanager.drop(this, event); - } - - if(this.options.revert) { - var that = this, - cur = this.placeholder.offset(), - axis = this.options.axis, - animation = {}; - - if ( !axis || axis === "x" ) { - animation.left = cur.left - this.offset.parent.left - this.margins.left + (this.offsetParent[0] === document.body ? 0 : this.offsetParent[0].scrollLeft); - } - if ( !axis || axis === "y" ) { - animation.top = cur.top - this.offset.parent.top - this.margins.top + (this.offsetParent[0] === document.body ? 0 : this.offsetParent[0].scrollTop); - } - this.reverting = true; - $(this.helper).animate( animation, parseInt(this.options.revert, 10) || 500, function() { - that._clear(event); - }); - } else { - this._clear(event, noPropagation); - } - - return false; - - }, - - cancel: function() { - - if(this.dragging) { - - this._mouseUp({ target: null }); - - if(this.options.helper === "original") { - this.currentItem.css(this._storedCSS).removeClass("ui-sortable-helper"); - } else { - this.currentItem.show(); - } - - //Post deactivating events to containers - for (var i = this.containers.length - 1; i >= 0; i--){ - this.containers[i]._trigger("deactivate", null, this._uiHash(this)); - if(this.containers[i].containerCache.over) { - this.containers[i]._trigger("out", null, this._uiHash(this)); - this.containers[i].containerCache.over = 0; - } - } - - } - - if (this.placeholder) { - //$(this.placeholder[0]).remove(); would have been the jQuery way - unfortunately, it unbinds ALL events from the original node! - if(this.placeholder[0].parentNode) { - this.placeholder[0].parentNode.removeChild(this.placeholder[0]); - } - if(this.options.helper !== "original" && this.helper && this.helper[0].parentNode) { - this.helper.remove(); - } - - $.extend(this, { - helper: null, - dragging: false, - reverting: false, - _noFinalSort: null - }); - - if(this.domPosition.prev) { - $(this.domPosition.prev).after(this.currentItem); - } else { - $(this.domPosition.parent).prepend(this.currentItem); - } - } - - return this; - - }, - - serialize: function(o) { - - var items = this._getItemsAsjQuery(o && o.connected), - str = []; - o = o || {}; - - $(items).each(function() { - var res = ($(o.item || this).attr(o.attribute || "id") || "").match(o.expression || (/(.+)[\-=_](.+)/)); - if (res) { - str.push((o.key || res[1]+"[]")+"="+(o.key && o.expression ? res[1] : res[2])); - } - }); - - if(!str.length && o.key) { - str.push(o.key + "="); - } - - return str.join("&"); - - }, - - toArray: function(o) { - - var items = this._getItemsAsjQuery(o && o.connected), - ret = []; - - o = o || {}; - - items.each(function() { ret.push($(o.item || this).attr(o.attribute || "id") || ""); }); - return ret; - - }, - - /* Be careful with the following core functions */ - _intersectsWith: function(item) { - - var x1 = this.positionAbs.left, - x2 = x1 + this.helperProportions.width, - y1 = this.positionAbs.top, - y2 = y1 + this.helperProportions.height, - l = item.left, - r = l + item.width, - t = item.top, - b = t + item.height, - dyClick = this.offset.click.top, - dxClick = this.offset.click.left, - isOverElementHeight = ( this.options.axis === "x" ) || ( ( y1 + dyClick ) > t && ( y1 + dyClick ) < b ), - isOverElementWidth = ( this.options.axis === "y" ) || ( ( x1 + dxClick ) > l && ( x1 + dxClick ) < r ), - isOverElement = isOverElementHeight && isOverElementWidth; - - if ( this.options.tolerance === "pointer" || - this.options.forcePointerForContainers || - (this.options.tolerance !== "pointer" && this.helperProportions[this.floating ? "width" : "height"] > item[this.floating ? "width" : "height"]) - ) { - return isOverElement; - } else { - - return (l < x1 + (this.helperProportions.width / 2) && // Right Half - x2 - (this.helperProportions.width / 2) < r && // Left Half - t < y1 + (this.helperProportions.height / 2) && // Bottom Half - y2 - (this.helperProportions.height / 2) < b ); // Top Half - - } - }, - - _intersectsWithPointer: function(item) { - - var isOverElementHeight = (this.options.axis === "x") || isOverAxis(this.positionAbs.top + this.offset.click.top, item.top, item.height), - isOverElementWidth = (this.options.axis === "y") || isOverAxis(this.positionAbs.left + this.offset.click.left, item.left, item.width), - isOverElement = isOverElementHeight && isOverElementWidth, - verticalDirection = this._getDragVerticalDirection(), - horizontalDirection = this._getDragHorizontalDirection(); - - if (!isOverElement) { - return false; - } - - return this.floating ? - ( ((horizontalDirection && horizontalDirection === "right") || verticalDirection === "down") ? 2 : 1 ) - : ( verticalDirection && (verticalDirection === "down" ? 2 : 1) ); - - }, - - _intersectsWithSides: function(item) { - - var isOverBottomHalf = isOverAxis(this.positionAbs.top + this.offset.click.top, item.top + (item.height/2), item.height), - isOverRightHalf = isOverAxis(this.positionAbs.left + this.offset.click.left, item.left + (item.width/2), item.width), - verticalDirection = this._getDragVerticalDirection(), - horizontalDirection = this._getDragHorizontalDirection(); - - if (this.floating && horizontalDirection) { - return ((horizontalDirection === "right" && isOverRightHalf) || (horizontalDirection === "left" && !isOverRightHalf)); - } else { - return verticalDirection && ((verticalDirection === "down" && isOverBottomHalf) || (verticalDirection === "up" && !isOverBottomHalf)); - } - - }, - - _getDragVerticalDirection: function() { - var delta = this.positionAbs.top - this.lastPositionAbs.top; - return delta !== 0 && (delta > 0 ? "down" : "up"); - }, - - _getDragHorizontalDirection: function() { - var delta = this.positionAbs.left - this.lastPositionAbs.left; - return delta !== 0 && (delta > 0 ? "right" : "left"); - }, - - refresh: function(event) { - this._refreshItems(event); - this.refreshPositions(); - return this; - }, - - _connectWith: function() { - var options = this.options; - return options.connectWith.constructor === String ? [options.connectWith] : options.connectWith; - }, - - _getItemsAsjQuery: function(connected) { - - var i, j, cur, inst, - items = [], - queries = [], - connectWith = this._connectWith(); - - if(connectWith && connected) { - for (i = connectWith.length - 1; i >= 0; i--){ - cur = $(connectWith[i]); - for ( j = cur.length - 1; j >= 0; j--){ - inst = $.data(cur[j], this.widgetFullName); - if(inst && inst !== this && !inst.options.disabled) { - queries.push([$.isFunction(inst.options.items) ? inst.options.items.call(inst.element) : $(inst.options.items, inst.element).not(".ui-sortable-helper").not(".ui-sortable-placeholder"), inst]); - } - } - } - } - - queries.push([$.isFunction(this.options.items) ? this.options.items.call(this.element, null, { options: this.options, item: this.currentItem }) : $(this.options.items, this.element).not(".ui-sortable-helper").not(".ui-sortable-placeholder"), this]); - - function addItems() { - items.push( this ); - } - for (i = queries.length - 1; i >= 0; i--){ - queries[i][0].each( addItems ); - } - - return $(items); - - }, - - _removeCurrentsFromItems: function() { - - var list = this.currentItem.find(":data(" + this.widgetName + "-item)"); - - this.items = $.grep(this.items, function (item) { - for (var j=0; j < list.length; j++) { - if(list[j] === item.item[0]) { - return false; - } - } - return true; - }); - - }, - - _refreshItems: function(event) { - - this.items = []; - this.containers = [this]; - - var i, j, cur, inst, targetData, _queries, item, queriesLength, - items = this.items, - queries = [[$.isFunction(this.options.items) ? this.options.items.call(this.element[0], event, { item: this.currentItem }) : $(this.options.items, this.element), this]], - connectWith = this._connectWith(); - - if(connectWith && this.ready) { //Shouldn't be run the first time through due to massive slow-down - for (i = connectWith.length - 1; i >= 0; i--){ - cur = $(connectWith[i]); - for (j = cur.length - 1; j >= 0; j--){ - inst = $.data(cur[j], this.widgetFullName); - if(inst && inst !== this && !inst.options.disabled) { - queries.push([$.isFunction(inst.options.items) ? inst.options.items.call(inst.element[0], event, { item: this.currentItem }) : $(inst.options.items, inst.element), inst]); - this.containers.push(inst); - } - } - } - } - - for (i = queries.length - 1; i >= 0; i--) { - targetData = queries[i][1]; - _queries = queries[i][0]; - - for (j=0, queriesLength = _queries.length; j < queriesLength; j++) { - item = $(_queries[j]); - - item.data(this.widgetName + "-item", targetData); // Data for target checking (mouse manager) - - items.push({ - item: item, - instance: targetData, - width: 0, height: 0, - left: 0, top: 0 - }); - } - } - - }, - - refreshPositions: function(fast) { - - //This has to be redone because due to the item being moved out/into the offsetParent, the offsetParent's position will change - if(this.offsetParent && this.helper) { - this.offset.parent = this._getParentOffset(); - } - - var i, item, t, p; - - for (i = this.items.length - 1; i >= 0; i--){ - item = this.items[i]; - - //We ignore calculating positions of all connected containers when we're not over them - if(item.instance !== this.currentContainer && this.currentContainer && item.item[0] !== this.currentItem[0]) { - continue; - } - - t = this.options.toleranceElement ? $(this.options.toleranceElement, item.item) : item.item; - - if (!fast) { - item.width = t.outerWidth(); - item.height = t.outerHeight(); - } - - p = t.offset(); - item.left = p.left; - item.top = p.top; - } - - if(this.options.custom && this.options.custom.refreshContainers) { - this.options.custom.refreshContainers.call(this); - } else { - for (i = this.containers.length - 1; i >= 0; i--){ - p = this.containers[i].element.offset(); - this.containers[i].containerCache.left = p.left; - this.containers[i].containerCache.top = p.top; - this.containers[i].containerCache.width = this.containers[i].element.outerWidth(); - this.containers[i].containerCache.height = this.containers[i].element.outerHeight(); - } - } - - return this; - }, - - _createPlaceholder: function(that) { - that = that || this; - var className, - o = that.options; - - if(!o.placeholder || o.placeholder.constructor === String) { - className = o.placeholder; - o.placeholder = { - element: function() { - - var nodeName = that.currentItem[0].nodeName.toLowerCase(), - element = $( "<" + nodeName + ">", that.document[0] ) - .addClass(className || that.currentItem[0].className+" ui-sortable-placeholder") - .removeClass("ui-sortable-helper"); - - if ( nodeName === "tr" ) { - that.currentItem.children().each(function() { - $( "<td> </td>", that.document[0] ) - .attr( "colspan", $( this ).attr( "colspan" ) || 1 ) - .appendTo( element ); - }); - } else if ( nodeName === "img" ) { - element.attr( "src", that.currentItem.attr( "src" ) ); - } - - if ( !className ) { - element.css( "visibility", "hidden" ); - } - - return element; - }, - update: function(container, p) { - - // 1. If a className is set as 'placeholder option, we don't force sizes - the class is responsible for that - // 2. The option 'forcePlaceholderSize can be enabled to force it even if a class name is specified - if(className && !o.forcePlaceholderSize) { - return; - } - - //If the element doesn't have a actual height by itself (without styles coming from a stylesheet), it receives the inline height from the dragged item - if(!p.height()) { p.height(that.currentItem.innerHeight() - parseInt(that.currentItem.css("paddingTop")||0, 10) - parseInt(that.currentItem.css("paddingBottom")||0, 10)); } - if(!p.width()) { p.width(that.currentItem.innerWidth() - parseInt(that.currentItem.css("paddingLeft")||0, 10) - parseInt(that.currentItem.css("paddingRight")||0, 10)); } - } - }; - } - - //Create the placeholder - that.placeholder = $(o.placeholder.element.call(that.element, that.currentItem)); - - //Append it after the actual current item - that.currentItem.after(that.placeholder); - - //Update the size of the placeholder (TODO: Logic to fuzzy, see line 316/317) - o.placeholder.update(that, that.placeholder); - - }, - - _contactContainers: function(event) { - var i, j, dist, itemWithLeastDistance, posProperty, sizeProperty, base, cur, nearBottom, floating, - innermostContainer = null, - innermostIndex = null; - - // get innermost container that intersects with item - for (i = this.containers.length - 1; i >= 0; i--) { - - // never consider a container that's located within the item itself - if($.contains(this.currentItem[0], this.containers[i].element[0])) { - continue; - } - - if(this._intersectsWith(this.containers[i].containerCache)) { - - // if we've already found a container and it's more "inner" than this, then continue - if(innermostContainer && $.contains(this.containers[i].element[0], innermostContainer.element[0])) { - continue; - } - - innermostContainer = this.containers[i]; - innermostIndex = i; - - } else { - // container doesn't intersect. trigger "out" event if necessary - if(this.containers[i].containerCache.over) { - this.containers[i]._trigger("out", event, this._uiHash(this)); - this.containers[i].containerCache.over = 0; - } - } - - } - - // if no intersecting containers found, return - if(!innermostContainer) { - return; - } - - // move the item into the container if it's not there already - if(this.containers.length === 1) { - if (!this.containers[innermostIndex].containerCache.over) { - this.containers[innermostIndex]._trigger("over", event, this._uiHash(this)); - this.containers[innermostIndex].containerCache.over = 1; - } - } else { - - //When entering a new container, we will find the item with the least distance and append our item near it - dist = 10000; - itemWithLeastDistance = null; - floating = innermostContainer.floating || isFloating(this.currentItem); - posProperty = floating ? "left" : "top"; - sizeProperty = floating ? "width" : "height"; - base = this.positionAbs[posProperty] + this.offset.click[posProperty]; - for (j = this.items.length - 1; j >= 0; j--) { - if(!$.contains(this.containers[innermostIndex].element[0], this.items[j].item[0])) { - continue; - } - if(this.items[j].item[0] === this.currentItem[0]) { - continue; - } - if (floating && !isOverAxis(this.positionAbs.top + this.offset.click.top, this.items[j].top, this.items[j].height)) { - continue; - } - cur = this.items[j].item.offset()[posProperty]; - nearBottom = false; - if(Math.abs(cur - base) > Math.abs(cur + this.items[j][sizeProperty] - base)){ - nearBottom = true; - cur += this.items[j][sizeProperty]; - } - - if(Math.abs(cur - base) < dist) { - dist = Math.abs(cur - base); itemWithLeastDistance = this.items[j]; - this.direction = nearBottom ? "up": "down"; - } - } - - //Check if dropOnEmpty is enabled - if(!itemWithLeastDistance && !this.options.dropOnEmpty) { - return; - } - - if(this.currentContainer === this.containers[innermostIndex]) { - return; - } - - itemWithLeastDistance ? this._rearrange(event, itemWithLeastDistance, null, true) : this._rearrange(event, null, this.containers[innermostIndex].element, true); - this._trigger("change", event, this._uiHash()); - this.containers[innermostIndex]._trigger("change", event, this._uiHash(this)); - this.currentContainer = this.containers[innermostIndex]; - - //Update the placeholder - this.options.placeholder.update(this.currentContainer, this.placeholder); - - this.containers[innermostIndex]._trigger("over", event, this._uiHash(this)); - this.containers[innermostIndex].containerCache.over = 1; - } - - - }, - - _createHelper: function(event) { - - var o = this.options, - helper = $.isFunction(o.helper) ? $(o.helper.apply(this.element[0], [event, this.currentItem])) : (o.helper === "clone" ? this.currentItem.clone() : this.currentItem); - - //Add the helper to the DOM if that didn't happen already - if(!helper.parents("body").length) { - $(o.appendTo !== "parent" ? o.appendTo : this.currentItem[0].parentNode)[0].appendChild(helper[0]); - } - - if(helper[0] === this.currentItem[0]) { - this._storedCSS = { width: this.currentItem[0].style.width, height: this.currentItem[0].style.height, position: this.currentItem.css("position"), top: this.currentItem.css("top"), left: this.currentItem.css("left") }; - } - - if(!helper[0].style.width || o.forceHelperSize) { - helper.width(this.currentItem.width()); - } - if(!helper[0].style.height || o.forceHelperSize) { - helper.height(this.currentItem.height()); - } - - return helper; - - }, - - _adjustOffsetFromHelper: function(obj) { - if (typeof obj === "string") { - obj = obj.split(" "); - } - if ($.isArray(obj)) { - obj = {left: +obj[0], top: +obj[1] || 0}; - } - if ("left" in obj) { - this.offset.click.left = obj.left + this.margins.left; - } - if ("right" in obj) { - this.offset.click.left = this.helperProportions.width - obj.right + this.margins.left; - } - if ("top" in obj) { - this.offset.click.top = obj.top + this.margins.top; - } - if ("bottom" in obj) { - this.offset.click.top = this.helperProportions.height - obj.bottom + this.margins.top; - } - }, - - _getParentOffset: function() { - - - //Get the offsetParent and cache its position - this.offsetParent = this.helper.offsetParent(); - var po = this.offsetParent.offset(); - - // This is a special case where we need to modify a offset calculated on start, since the following happened: - // 1. The position of the helper is absolute, so it's position is calculated based on the next positioned parent - // 2. The actual offset parent is a child of the scroll parent, and the scroll parent isn't the document, which means that - // the scroll is included in the initial calculation of the offset of the parent, and never recalculated upon drag - if(this.cssPosition === "absolute" && this.scrollParent[0] !== document && $.contains(this.scrollParent[0], this.offsetParent[0])) { - po.left += this.scrollParent.scrollLeft(); - po.top += this.scrollParent.scrollTop(); - } - - // This needs to be actually done for all browsers, since pageX/pageY includes this information - // with an ugly IE fix - if( this.offsetParent[0] === document.body || (this.offsetParent[0].tagName && this.offsetParent[0].tagName.toLowerCase() === "html" && $.ui.ie)) { - po = { top: 0, left: 0 }; - } - - return { - top: po.top + (parseInt(this.offsetParent.css("borderTopWidth"),10) || 0), - left: po.left + (parseInt(this.offsetParent.css("borderLeftWidth"),10) || 0) - }; - - }, - - _getRelativeOffset: function() { - - if(this.cssPosition === "relative") { - var p = this.currentItem.position(); - return { - top: p.top - (parseInt(this.helper.css("top"),10) || 0) + this.scrollParent.scrollTop(), - left: p.left - (parseInt(this.helper.css("left"),10) || 0) + this.scrollParent.scrollLeft() - }; - } else { - return { top: 0, left: 0 }; - } - - }, - - _cacheMargins: function() { - this.margins = { - left: (parseInt(this.currentItem.css("marginLeft"),10) || 0), - top: (parseInt(this.currentItem.css("marginTop"),10) || 0) - }; - }, - - _cacheHelperProportions: function() { - this.helperProportions = { - width: this.helper.outerWidth(), - height: this.helper.outerHeight() - }; - }, - - _setContainment: function() { - - var ce, co, over, - o = this.options; - if(o.containment === "parent") { - o.containment = this.helper[0].parentNode; - } - if(o.containment === "document" || o.containment === "window") { - this.containment = [ - 0 - this.offset.relative.left - this.offset.parent.left, - 0 - this.offset.relative.top - this.offset.parent.top, - $(o.containment === "document" ? document : window).width() - this.helperProportions.width - this.margins.left, - ($(o.containment === "document" ? document : window).height() || document.body.parentNode.scrollHeight) - this.helperProportions.height - this.margins.top - ]; - } - - if(!(/^(document|window|parent)$/).test(o.containment)) { - ce = $(o.containment)[0]; - co = $(o.containment).offset(); - over = ($(ce).css("overflow") !== "hidden"); - - this.containment = [ - co.left + (parseInt($(ce).css("borderLeftWidth"),10) || 0) + (parseInt($(ce).css("paddingLeft"),10) || 0) - this.margins.left, - co.top + (parseInt($(ce).css("borderTopWidth"),10) || 0) + (parseInt($(ce).css("paddingTop"),10) || 0) - this.margins.top, - co.left+(over ? Math.max(ce.scrollWidth,ce.offsetWidth) : ce.offsetWidth) - (parseInt($(ce).css("borderLeftWidth"),10) || 0) - (parseInt($(ce).css("paddingRight"),10) || 0) - this.helperProportions.width - this.margins.left, - co.top+(over ? Math.max(ce.scrollHeight,ce.offsetHeight) : ce.offsetHeight) - (parseInt($(ce).css("borderTopWidth"),10) || 0) - (parseInt($(ce).css("paddingBottom"),10) || 0) - this.helperProportions.height - this.margins.top - ]; - } - - }, - - _convertPositionTo: function(d, pos) { - - if(!pos) { - pos = this.position; - } - var mod = d === "absolute" ? 1 : -1, - scroll = this.cssPosition === "absolute" && !(this.scrollParent[0] !== document && $.contains(this.scrollParent[0], this.offsetParent[0])) ? this.offsetParent : this.scrollParent, - scrollIsRootNode = (/(html|body)/i).test(scroll[0].tagName); - - return { - top: ( - pos.top + // The absolute mouse position - this.offset.relative.top * mod + // Only for relative positioned nodes: Relative offset from element to offset parent - this.offset.parent.top * mod - // The offsetParent's offset without borders (offset + border) - ( ( this.cssPosition === "fixed" ? -this.scrollParent.scrollTop() : ( scrollIsRootNode ? 0 : scroll.scrollTop() ) ) * mod) - ), - left: ( - pos.left + // The absolute mouse position - this.offset.relative.left * mod + // Only for relative positioned nodes: Relative offset from element to offset parent - this.offset.parent.left * mod - // The offsetParent's offset without borders (offset + border) - ( ( this.cssPosition === "fixed" ? -this.scrollParent.scrollLeft() : scrollIsRootNode ? 0 : scroll.scrollLeft() ) * mod) - ) - }; - - }, - - _generatePosition: function(event) { - - var top, left, - o = this.options, - pageX = event.pageX, - pageY = event.pageY, - scroll = this.cssPosition === "absolute" && !(this.scrollParent[0] !== document && $.contains(this.scrollParent[0], this.offsetParent[0])) ? this.offsetParent : this.scrollParent, scrollIsRootNode = (/(html|body)/i).test(scroll[0].tagName); - - // This is another very weird special case that only happens for relative elements: - // 1. If the css position is relative - // 2. and the scroll parent is the document or similar to the offset parent - // we have to refresh the relative offset during the scroll so there are no jumps - if(this.cssPosition === "relative" && !(this.scrollParent[0] !== document && this.scrollParent[0] !== this.offsetParent[0])) { - this.offset.relative = this._getRelativeOffset(); - } - - /* - * - Position constraining - - * Constrain the position to a mix of grid, containment. - */ - - if(this.originalPosition) { //If we are not dragging yet, we won't check for options - - if(this.containment) { - if(event.pageX - this.offset.click.left < this.containment[0]) { - pageX = this.containment[0] + this.offset.click.left; - } - if(event.pageY - this.offset.click.top < this.containment[1]) { - pageY = this.containment[1] + this.offset.click.top; - } - if(event.pageX - this.offset.click.left > this.containment[2]) { - pageX = this.containment[2] + this.offset.click.left; - } - if(event.pageY - this.offset.click.top > this.containment[3]) { - pageY = this.containment[3] + this.offset.click.top; - } - } - - if(o.grid) { - top = this.originalPageY + Math.round((pageY - this.originalPageY) / o.grid[1]) * o.grid[1]; - pageY = this.containment ? ( (top - this.offset.click.top >= this.containment[1] && top - this.offset.click.top <= this.containment[3]) ? top : ((top - this.offset.click.top >= this.containment[1]) ? top - o.grid[1] : top + o.grid[1])) : top; - - left = this.originalPageX + Math.round((pageX - this.originalPageX) / o.grid[0]) * o.grid[0]; - pageX = this.containment ? ( (left - this.offset.click.left >= this.containment[0] && left - this.offset.click.left <= this.containment[2]) ? left : ((left - this.offset.click.left >= this.containment[0]) ? left - o.grid[0] : left + o.grid[0])) : left; - } - - } - - return { - top: ( - pageY - // The absolute mouse position - this.offset.click.top - // Click offset (relative to the element) - this.offset.relative.top - // Only for relative positioned nodes: Relative offset from element to offset parent - this.offset.parent.top + // The offsetParent's offset without borders (offset + border) - ( ( this.cssPosition === "fixed" ? -this.scrollParent.scrollTop() : ( scrollIsRootNode ? 0 : scroll.scrollTop() ) )) - ), - left: ( - pageX - // The absolute mouse position - this.offset.click.left - // Click offset (relative to the element) - this.offset.relative.left - // Only for relative positioned nodes: Relative offset from element to offset parent - this.offset.parent.left + // The offsetParent's offset without borders (offset + border) - ( ( this.cssPosition === "fixed" ? -this.scrollParent.scrollLeft() : scrollIsRootNode ? 0 : scroll.scrollLeft() )) - ) - }; - - }, - - _rearrange: function(event, i, a, hardRefresh) { - - a ? a[0].appendChild(this.placeholder[0]) : i.item[0].parentNode.insertBefore(this.placeholder[0], (this.direction === "down" ? i.item[0] : i.item[0].nextSibling)); - - //Various things done here to improve the performance: - // 1. we create a setTimeout, that calls refreshPositions - // 2. on the instance, we have a counter variable, that get's higher after every append - // 3. on the local scope, we copy the counter variable, and check in the timeout, if it's still the same - // 4. this lets only the last addition to the timeout stack through - this.counter = this.counter ? ++this.counter : 1; - var counter = this.counter; - - this._delay(function() { - if(counter === this.counter) { - this.refreshPositions(!hardRefresh); //Precompute after each DOM insertion, NOT on mousemove - } - }); - - }, - - _clear: function(event, noPropagation) { - - this.reverting = false; - // We delay all events that have to be triggered to after the point where the placeholder has been removed and - // everything else normalized again - var i, - delayedTriggers = []; - - // We first have to update the dom position of the actual currentItem - // Note: don't do it if the current item is already removed (by a user), or it gets reappended (see #4088) - if(!this._noFinalSort && this.currentItem.parent().length) { - this.placeholder.before(this.currentItem); - } - this._noFinalSort = null; - - if(this.helper[0] === this.currentItem[0]) { - for(i in this._storedCSS) { - if(this._storedCSS[i] === "auto" || this._storedCSS[i] === "static") { - this._storedCSS[i] = ""; - } - } - this.currentItem.css(this._storedCSS).removeClass("ui-sortable-helper"); - } else { - this.currentItem.show(); - } - - if(this.fromOutside && !noPropagation) { - delayedTriggers.push(function(event) { this._trigger("receive", event, this._uiHash(this.fromOutside)); }); - } - if((this.fromOutside || this.domPosition.prev !== this.currentItem.prev().not(".ui-sortable-helper")[0] || this.domPosition.parent !== this.currentItem.parent()[0]) && !noPropagation) { - delayedTriggers.push(function(event) { this._trigger("update", event, this._uiHash()); }); //Trigger update callback if the DOM position has changed - } - - // Check if the items Container has Changed and trigger appropriate - // events. - if (this !== this.currentContainer) { - if(!noPropagation) { - delayedTriggers.push(function(event) { this._trigger("remove", event, this._uiHash()); }); - delayedTriggers.push((function(c) { return function(event) { c._trigger("receive", event, this._uiHash(this)); }; }).call(this, this.currentContainer)); - delayedTriggers.push((function(c) { return function(event) { c._trigger("update", event, this._uiHash(this)); }; }).call(this, this.currentContainer)); - } - } - - - //Post events to containers - function delayEvent( type, instance, container ) { - return function( event ) { - container._trigger( type, event, instance._uiHash( instance ) ); - }; - } - for (i = this.containers.length - 1; i >= 0; i--){ - if (!noPropagation) { - delayedTriggers.push( delayEvent( "deactivate", this, this.containers[ i ] ) ); - } - if(this.containers[i].containerCache.over) { - delayedTriggers.push( delayEvent( "out", this, this.containers[ i ] ) ); - this.containers[i].containerCache.over = 0; - } - } - - //Do what was originally in plugins - if ( this.storedCursor ) { - this.document.find( "body" ).css( "cursor", this.storedCursor ); - this.storedStylesheet.remove(); - } - if(this._storedOpacity) { - this.helper.css("opacity", this._storedOpacity); - } - if(this._storedZIndex) { - this.helper.css("zIndex", this._storedZIndex === "auto" ? "" : this._storedZIndex); - } - - this.dragging = false; - if(this.cancelHelperRemoval) { - if(!noPropagation) { - this._trigger("beforeStop", event, this._uiHash()); - for (i=0; i < delayedTriggers.length; i++) { - delayedTriggers[i].call(this, event); - } //Trigger all delayed events - this._trigger("stop", event, this._uiHash()); - } - - this.fromOutside = false; - return false; - } - - if(!noPropagation) { - this._trigger("beforeStop", event, this._uiHash()); - } - - //$(this.placeholder[0]).remove(); would have been the jQuery way - unfortunately, it unbinds ALL events from the original node! - this.placeholder[0].parentNode.removeChild(this.placeholder[0]); - - if(this.helper[0] !== this.currentItem[0]) { - this.helper.remove(); - } - this.helper = null; - - if(!noPropagation) { - for (i=0; i < delayedTriggers.length; i++) { - delayedTriggers[i].call(this, event); - } //Trigger all delayed events - this._trigger("stop", event, this._uiHash()); - } - - this.fromOutside = false; - return true; - - }, - - _trigger: function() { - if ($.Widget.prototype._trigger.apply(this, arguments) === false) { - this.cancel(); - } - }, - - _uiHash: function(_inst) { - var inst = _inst || this; - return { - helper: inst.helper, - placeholder: inst.placeholder || $([]), - position: inst.position, - originalPosition: inst.originalPosition, - offset: inst.positionAbs, - item: inst.currentItem, - sender: _inst ? _inst.element : null - }; - } - -}); - -})(jQuery); -(function( $, undefined ) { - -// number of pages in a slider -// (how many times can you page up/down to go through the whole range) -var numPages = 5; - -$.widget( "ui.slider", $.ui.mouse, { - version: "1.10.4", - widgetEventPrefix: "slide", - - options: { - animate: false, - distance: 0, - max: 100, - min: 0, - orientation: "horizontal", - range: false, - step: 1, - value: 0, - values: null, - - // callbacks - change: null, - slide: null, - start: null, - stop: null - }, - - _create: function() { - this._keySliding = false; - this._mouseSliding = false; - this._animateOff = true; - this._handleIndex = null; - this._detectOrientation(); - this._mouseInit(); - - this.element - .addClass( "ui-slider" + - " ui-slider-" + this.orientation + - " ui-widget" + - " ui-widget-content" + - " ui-corner-all"); - - this._refresh(); - this._setOption( "disabled", this.options.disabled ); - - this._animateOff = false; - }, - - _refresh: function() { - this._createRange(); - this._createHandles(); - this._setupEvents(); - this._refreshValue(); - }, - - _createHandles: function() { - var i, handleCount, - options = this.options, - existingHandles = this.element.find( ".ui-slider-handle" ).addClass( "ui-state-default ui-corner-all" ), - handle = "<a class='ui-slider-handle ui-state-default ui-corner-all' href='#'></a>", - handles = []; - - handleCount = ( options.values && options.values.length ) || 1; - - if ( existingHandles.length > handleCount ) { - existingHandles.slice( handleCount ).remove(); - existingHandles = existingHandles.slice( 0, handleCount ); - } - - for ( i = existingHandles.length; i < handleCount; i++ ) { - handles.push( handle ); - } - - this.handles = existingHandles.add( $( handles.join( "" ) ).appendTo( this.element ) ); - - this.handle = this.handles.eq( 0 ); - - this.handles.each(function( i ) { - $( this ).data( "ui-slider-handle-index", i ); - }); - }, - - _createRange: function() { - var options = this.options, - classes = ""; - - if ( options.range ) { - if ( options.range === true ) { - if ( !options.values ) { - options.values = [ this._valueMin(), this._valueMin() ]; - } else if ( options.values.length && options.values.length !== 2 ) { - options.values = [ options.values[0], options.values[0] ]; - } else if ( $.isArray( options.values ) ) { - options.values = options.values.slice(0); - } - } - - if ( !this.range || !this.range.length ) { - this.range = $( "<div></div>" ) - .appendTo( this.element ); - - classes = "ui-slider-range" + - // note: this isn't the most fittingly semantic framework class for this element, - // but worked best visually with a variety of themes - " ui-widget-header ui-corner-all"; - } else { - this.range.removeClass( "ui-slider-range-min ui-slider-range-max" ) - // Handle range switching from true to min/max - .css({ - "left": "", - "bottom": "" - }); - } - - this.range.addClass( classes + - ( ( options.range === "min" || options.range === "max" ) ? " ui-slider-range-" + options.range : "" ) ); - } else { - if ( this.range ) { - this.range.remove(); - } - this.range = null; - } - }, - - _setupEvents: function() { - var elements = this.handles.add( this.range ).filter( "a" ); - this._off( elements ); - this._on( elements, this._handleEvents ); - this._hoverable( elements ); - this._focusable( elements ); - }, - - _destroy: function() { - this.handles.remove(); - if ( this.range ) { - this.range.remove(); - } - - this.element - .removeClass( "ui-slider" + - " ui-slider-horizontal" + - " ui-slider-vertical" + - " ui-widget" + - " ui-widget-content" + - " ui-corner-all" ); - - this._mouseDestroy(); - }, - - _mouseCapture: function( event ) { - var position, normValue, distance, closestHandle, index, allowed, offset, mouseOverHandle, - that = this, - o = this.options; - - if ( o.disabled ) { - return false; - } - - this.elementSize = { - width: this.element.outerWidth(), - height: this.element.outerHeight() - }; - this.elementOffset = this.element.offset(); - - position = { x: event.pageX, y: event.pageY }; - normValue = this._normValueFromMouse( position ); - distance = this._valueMax() - this._valueMin() + 1; - this.handles.each(function( i ) { - var thisDistance = Math.abs( normValue - that.values(i) ); - if (( distance > thisDistance ) || - ( distance === thisDistance && - (i === that._lastChangedValue || that.values(i) === o.min ))) { - distance = thisDistance; - closestHandle = $( this ); - index = i; - } - }); - - allowed = this._start( event, index ); - if ( allowed === false ) { - return false; - } - this._mouseSliding = true; - - this._handleIndex = index; - - closestHandle - .addClass( "ui-state-active" ) - .focus(); - - offset = closestHandle.offset(); - mouseOverHandle = !$( event.target ).parents().addBack().is( ".ui-slider-handle" ); - this._clickOffset = mouseOverHandle ? { left: 0, top: 0 } : { - left: event.pageX - offset.left - ( closestHandle.width() / 2 ), - top: event.pageY - offset.top - - ( closestHandle.height() / 2 ) - - ( parseInt( closestHandle.css("borderTopWidth"), 10 ) || 0 ) - - ( parseInt( closestHandle.css("borderBottomWidth"), 10 ) || 0) + - ( parseInt( closestHandle.css("marginTop"), 10 ) || 0) - }; - - if ( !this.handles.hasClass( "ui-state-hover" ) ) { - this._slide( event, index, normValue ); - } - this._animateOff = true; - return true; - }, - - _mouseStart: function() { - return true; - }, - - _mouseDrag: function( event ) { - var position = { x: event.pageX, y: event.pageY }, - normValue = this._normValueFromMouse( position ); - - this._slide( event, this._handleIndex, normValue ); - - return false; - }, - - _mouseStop: function( event ) { - this.handles.removeClass( "ui-state-active" ); - this._mouseSliding = false; - - this._stop( event, this._handleIndex ); - this._change( event, this._handleIndex ); - - this._handleIndex = null; - this._clickOffset = null; - this._animateOff = false; - - return false; - }, - - _detectOrientation: function() { - this.orientation = ( this.options.orientation === "vertical" ) ? "vertical" : "horizontal"; - }, - - _normValueFromMouse: function( position ) { - var pixelTotal, - pixelMouse, - percentMouse, - valueTotal, - valueMouse; - - if ( this.orientation === "horizontal" ) { - pixelTotal = this.elementSize.width; - pixelMouse = position.x - this.elementOffset.left - ( this._clickOffset ? this._clickOffset.left : 0 ); - } else { - pixelTotal = this.elementSize.height; - pixelMouse = position.y - this.elementOffset.top - ( this._clickOffset ? this._clickOffset.top : 0 ); - } - - percentMouse = ( pixelMouse / pixelTotal ); - if ( percentMouse > 1 ) { - percentMouse = 1; - } - if ( percentMouse < 0 ) { - percentMouse = 0; - } - if ( this.orientation === "vertical" ) { - percentMouse = 1 - percentMouse; - } - - valueTotal = this._valueMax() - this._valueMin(); - valueMouse = this._valueMin() + percentMouse * valueTotal; - - return this._trimAlignValue( valueMouse ); - }, - - _start: function( event, index ) { - var uiHash = { - handle: this.handles[ index ], - value: this.value() - }; - if ( this.options.values && this.options.values.length ) { - uiHash.value = this.values( index ); - uiHash.values = this.values(); - } - return this._trigger( "start", event, uiHash ); - }, - - _slide: function( event, index, newVal ) { - var otherVal, - newValues, - allowed; - - if ( this.options.values && this.options.values.length ) { - otherVal = this.values( index ? 0 : 1 ); - - if ( ( this.options.values.length === 2 && this.options.range === true ) && - ( ( index === 0 && newVal > otherVal) || ( index === 1 && newVal < otherVal ) ) - ) { - newVal = otherVal; - } - - if ( newVal !== this.values( index ) ) { - newValues = this.values(); - newValues[ index ] = newVal; - // A slide can be canceled by returning false from the slide callback - allowed = this._trigger( "slide", event, { - handle: this.handles[ index ], - value: newVal, - values: newValues - } ); - otherVal = this.values( index ? 0 : 1 ); - if ( allowed !== false ) { - this.values( index, newVal ); - } - } - } else { - if ( newVal !== this.value() ) { - // A slide can be canceled by returning false from the slide callback - allowed = this._trigger( "slide", event, { - handle: this.handles[ index ], - value: newVal - } ); - if ( allowed !== false ) { - this.value( newVal ); - } - } - } - }, - - _stop: function( event, index ) { - var uiHash = { - handle: this.handles[ index ], - value: this.value() - }; - if ( this.options.values && this.options.values.length ) { - uiHash.value = this.values( index ); - uiHash.values = this.values(); - } - - this._trigger( "stop", event, uiHash ); - }, - - _change: function( event, index ) { - if ( !this._keySliding && !this._mouseSliding ) { - var uiHash = { - handle: this.handles[ index ], - value: this.value() - }; - if ( this.options.values && this.options.values.length ) { - uiHash.value = this.values( index ); - uiHash.values = this.values(); - } - - //store the last changed value index for reference when handles overlap - this._lastChangedValue = index; - - this._trigger( "change", event, uiHash ); - } - }, - - value: function( newValue ) { - if ( arguments.length ) { - this.options.value = this._trimAlignValue( newValue ); - this._refreshValue(); - this._change( null, 0 ); - return; - } - - return this._value(); - }, - - values: function( index, newValue ) { - var vals, - newValues, - i; - - if ( arguments.length > 1 ) { - this.options.values[ index ] = this._trimAlignValue( newValue ); - this._refreshValue(); - this._change( null, index ); - return; - } - - if ( arguments.length ) { - if ( $.isArray( arguments[ 0 ] ) ) { - vals = this.options.values; - newValues = arguments[ 0 ]; - for ( i = 0; i < vals.length; i += 1 ) { - vals[ i ] = this._trimAlignValue( newValues[ i ] ); - this._change( null, i ); - } - this._refreshValue(); - } else { - if ( this.options.values && this.options.values.length ) { - return this._values( index ); - } else { - return this.value(); - } - } - } else { - return this._values(); - } - }, - - _setOption: function( key, value ) { - var i, - valsLength = 0; - - if ( key === "range" && this.options.range === true ) { - if ( value === "min" ) { - this.options.value = this._values( 0 ); - this.options.values = null; - } else if ( value === "max" ) { - this.options.value = this._values( this.options.values.length-1 ); - this.options.values = null; - } - } - - if ( $.isArray( this.options.values ) ) { - valsLength = this.options.values.length; - } - - $.Widget.prototype._setOption.apply( this, arguments ); - - switch ( key ) { - case "orientation": - this._detectOrientation(); - this.element - .removeClass( "ui-slider-horizontal ui-slider-vertical" ) - .addClass( "ui-slider-" + this.orientation ); - this._refreshValue(); - break; - case "value": - this._animateOff = true; - this._refreshValue(); - this._change( null, 0 ); - this._animateOff = false; - break; - case "values": - this._animateOff = true; - this._refreshValue(); - for ( i = 0; i < valsLength; i += 1 ) { - this._change( null, i ); - } - this._animateOff = false; - break; - case "min": - case "max": - this._animateOff = true; - this._refreshValue(); - this._animateOff = false; - break; - case "range": - this._animateOff = true; - this._refresh(); - this._animateOff = false; - break; - } - }, - - //internal value getter - // _value() returns value trimmed by min and max, aligned by step - _value: function() { - var val = this.options.value; - val = this._trimAlignValue( val ); - - return val; - }, - - //internal values getter - // _values() returns array of values trimmed by min and max, aligned by step - // _values( index ) returns single value trimmed by min and max, aligned by step - _values: function( index ) { - var val, - vals, - i; - - if ( arguments.length ) { - val = this.options.values[ index ]; - val = this._trimAlignValue( val ); - - return val; - } else if ( this.options.values && this.options.values.length ) { - // .slice() creates a copy of the array - // this copy gets trimmed by min and max and then returned - vals = this.options.values.slice(); - for ( i = 0; i < vals.length; i+= 1) { - vals[ i ] = this._trimAlignValue( vals[ i ] ); - } - - return vals; - } else { - return []; - } - }, - - // returns the step-aligned value that val is closest to, between (inclusive) min and max - _trimAlignValue: function( val ) { - if ( val <= this._valueMin() ) { - return this._valueMin(); - } - if ( val >= this._valueMax() ) { - return this._valueMax(); - } - var step = ( this.options.step > 0 ) ? this.options.step : 1, - valModStep = (val - this._valueMin()) % step, - alignValue = val - valModStep; - - if ( Math.abs(valModStep) * 2 >= step ) { - alignValue += ( valModStep > 0 ) ? step : ( -step ); - } - - // Since JavaScript has problems with large floats, round - // the final value to 5 digits after the decimal point (see #4124) - return parseFloat( alignValue.toFixed(5) ); - }, - - _valueMin: function() { - return this.options.min; - }, - - _valueMax: function() { - return this.options.max; - }, - - _refreshValue: function() { - var lastValPercent, valPercent, value, valueMin, valueMax, - oRange = this.options.range, - o = this.options, - that = this, - animate = ( !this._animateOff ) ? o.animate : false, - _set = {}; - - if ( this.options.values && this.options.values.length ) { - this.handles.each(function( i ) { - valPercent = ( that.values(i) - that._valueMin() ) / ( that._valueMax() - that._valueMin() ) * 100; - _set[ that.orientation === "horizontal" ? "left" : "bottom" ] = valPercent + "%"; - $( this ).stop( 1, 1 )[ animate ? "animate" : "css" ]( _set, o.animate ); - if ( that.options.range === true ) { - if ( that.orientation === "horizontal" ) { - if ( i === 0 ) { - that.range.stop( 1, 1 )[ animate ? "animate" : "css" ]( { left: valPercent + "%" }, o.animate ); - } - if ( i === 1 ) { - that.range[ animate ? "animate" : "css" ]( { width: ( valPercent - lastValPercent ) + "%" }, { queue: false, duration: o.animate } ); - } - } else { - if ( i === 0 ) { - that.range.stop( 1, 1 )[ animate ? "animate" : "css" ]( { bottom: ( valPercent ) + "%" }, o.animate ); - } - if ( i === 1 ) { - that.range[ animate ? "animate" : "css" ]( { height: ( valPercent - lastValPercent ) + "%" }, { queue: false, duration: o.animate } ); - } - } - } - lastValPercent = valPercent; - }); - } else { - value = this.value(); - valueMin = this._valueMin(); - valueMax = this._valueMax(); - valPercent = ( valueMax !== valueMin ) ? - ( value - valueMin ) / ( valueMax - valueMin ) * 100 : - 0; - _set[ this.orientation === "horizontal" ? "left" : "bottom" ] = valPercent + "%"; - this.handle.stop( 1, 1 )[ animate ? "animate" : "css" ]( _set, o.animate ); - - if ( oRange === "min" && this.orientation === "horizontal" ) { - this.range.stop( 1, 1 )[ animate ? "animate" : "css" ]( { width: valPercent + "%" }, o.animate ); - } - if ( oRange === "max" && this.orientation === "horizontal" ) { - this.range[ animate ? "animate" : "css" ]( { width: ( 100 - valPercent ) + "%" }, { queue: false, duration: o.animate } ); - } - if ( oRange === "min" && this.orientation === "vertical" ) { - this.range.stop( 1, 1 )[ animate ? "animate" : "css" ]( { height: valPercent + "%" }, o.animate ); - } - if ( oRange === "max" && this.orientation === "vertical" ) { - this.range[ animate ? "animate" : "css" ]( { height: ( 100 - valPercent ) + "%" }, { queue: false, duration: o.animate } ); - } - } - }, - - _handleEvents: { - keydown: function( event ) { - var allowed, curVal, newVal, step, - index = $( event.target ).data( "ui-slider-handle-index" ); - - switch ( event.keyCode ) { - case $.ui.keyCode.HOME: - case $.ui.keyCode.END: - case $.ui.keyCode.PAGE_UP: - case $.ui.keyCode.PAGE_DOWN: - case $.ui.keyCode.UP: - case $.ui.keyCode.RIGHT: - case $.ui.keyCode.DOWN: - case $.ui.keyCode.LEFT: - event.preventDefault(); - if ( !this._keySliding ) { - this._keySliding = true; - $( event.target ).addClass( "ui-state-active" ); - allowed = this._start( event, index ); - if ( allowed === false ) { - return; - } - } - break; - } - - step = this.options.step; - if ( this.options.values && this.options.values.length ) { - curVal = newVal = this.values( index ); - } else { - curVal = newVal = this.value(); - } - - switch ( event.keyCode ) { - case $.ui.keyCode.HOME: - newVal = this._valueMin(); - break; - case $.ui.keyCode.END: - newVal = this._valueMax(); - break; - case $.ui.keyCode.PAGE_UP: - newVal = this._trimAlignValue( curVal + ( (this._valueMax() - this._valueMin()) / numPages ) ); - break; - case $.ui.keyCode.PAGE_DOWN: - newVal = this._trimAlignValue( curVal - ( (this._valueMax() - this._valueMin()) / numPages ) ); - break; - case $.ui.keyCode.UP: - case $.ui.keyCode.RIGHT: - if ( curVal === this._valueMax() ) { - return; - } - newVal = this._trimAlignValue( curVal + step ); - break; - case $.ui.keyCode.DOWN: - case $.ui.keyCode.LEFT: - if ( curVal === this._valueMin() ) { - return; - } - newVal = this._trimAlignValue( curVal - step ); - break; - } - - this._slide( event, index, newVal ); - }, - click: function( event ) { - event.preventDefault(); - }, - keyup: function( event ) { - var index = $( event.target ).data( "ui-slider-handle-index" ); - - if ( this._keySliding ) { - this._keySliding = false; - this._stop( event, index ); - this._change( event, index ); - $( event.target ).removeClass( "ui-state-active" ); - } - } - } - -}); - -}(jQuery)); diff --git a/src/UI/JsLibraries/jquery.backstretch.js b/src/UI/JsLibraries/jquery.backstretch.js deleted file mode 100644 index 7ac0875f8..000000000 --- a/src/UI/JsLibraries/jquery.backstretch.js +++ /dev/null @@ -1,377 +0,0 @@ -/*! Backstretch - v2.0.4 - 2013-06-19 -* http://srobbin.com/jquery-plugins/backstretch/ -* Copyright (c) 2013 Scott Robbin; Licensed MIT */ - -;(function ($, window, undefined) { - 'use strict'; - - /* PLUGIN DEFINITION - * ========================= */ - - $.fn.backstretch = function (images, options) { - // We need at least one image or method name - if (images === undefined || images.length === 0) { - $.error("No images were supplied for Backstretch"); - } - - /* - * Scroll the page one pixel to get the right window height on iOS - * Pretty harmless for everyone else - */ - if ($(window).scrollTop() === 0 ) { - window.scrollTo(0, 0); - } - - return this.each(function () { - var $this = $(this) - , obj = $this.data('backstretch'); - - // Do we already have an instance attached to this element? - if (obj) { - - // Is this a method they're trying to execute? - if (typeof images == 'string' && typeof obj[images] == 'function') { - // Call the method - obj[images](options); - - // No need to do anything further - return; - } - - // Merge the old options with the new - options = $.extend(obj.options, options); - - // Remove the old instance - obj.destroy(true); - } - - obj = new Backstretch(this, images, options); - $this.data('backstretch', obj); - }); - }; - - // If no element is supplied, we'll attach to body - $.backstretch = function (images, options) { - // Return the instance - return $('body') - .backstretch(images, options) - .data('backstretch'); - }; - - // Custom selector - $.expr[':'].backstretch = function(elem) { - return $(elem).data('backstretch') !== undefined; - }; - - /* DEFAULTS - * ========================= */ - - $.fn.backstretch.defaults = { - centeredX: true // Should we center the image on the X axis? - , centeredY: true // Should we center the image on the Y axis? - , duration: 5000 // Amount of time in between slides (if slideshow) - , fade: 0 // Speed of fade transition between slides - }; - - /* STYLES - * - * Baked-in styles that we'll apply to our elements. - * In an effort to keep the plugin simple, these are not exposed as options. - * That said, anyone can override these in their own stylesheet. - * ========================= */ - var styles = { - wrap: { - left: 0 - , top: 0 - , overflow: 'hidden' - , margin: 0 - , padding: 0 - , height: '100%' - , width: '100%' - , zIndex: -999999 - } - , img: { - position: 'absolute' - , display: 'none' - , margin: 0 - , padding: 0 - , border: 'none' - , width: 'auto' - , height: 'auto' - , maxHeight: 'none' - , maxWidth: 'none' - , zIndex: -999999 - } - }; - - /* CLASS DEFINITION - * ========================= */ - var Backstretch = function (container, images, options) { - this.options = $.extend({}, $.fn.backstretch.defaults, options || {}); - - /* In its simplest form, we allow Backstretch to be called on an image path. - * e.g. $.backstretch('/path/to/image.jpg') - * So, we need to turn this back into an array. - */ - this.images = $.isArray(images) ? images : [images]; - - // Preload images - $.each(this.images, function () { - $('<img />')[0].src = this; - }); - - // Convenience reference to know if the container is body. - this.isBody = container === document.body; - - /* We're keeping track of a few different elements - * - * Container: the element that Backstretch was called on. - * Wrap: a DIV that we place the image into, so we can hide the overflow. - * Root: Convenience reference to help calculate the correct height. - */ - this.$container = $(container); - this.$root = this.isBody ? supportsFixedPosition ? $(window) : $(document) : this.$container; - - // Don't create a new wrap if one already exists (from a previous instance of Backstretch) - var $existing = this.$container.children(".backstretch").first(); - this.$wrap = $existing.length ? $existing : $('<div class="backstretch"></div>').css(styles.wrap).appendTo(this.$container); - - // Non-body elements need some style adjustments - if (!this.isBody) { - // If the container is statically positioned, we need to make it relative, - // and if no zIndex is defined, we should set it to zero. - var position = this.$container.css('position') - , zIndex = this.$container.css('zIndex'); - - this.$container.css({ - position: position === 'static' ? 'relative' : position - , zIndex: zIndex === 'auto' ? 0 : zIndex - , background: 'none' - }); - - // Needs a higher z-index - this.$wrap.css({zIndex: -999998}); - } - - // Fixed or absolute positioning? - this.$wrap.css({ - position: this.isBody && supportsFixedPosition ? 'fixed' : 'absolute' - }); - - // Set the first image - this.index = 0; - this.show(this.index); - - // Listen for resize - $(window).on('resize.backstretch', $.proxy(this.resize, this)) - .on('orientationchange.backstretch', $.proxy(function () { - // Need to do this in order to get the right window height - if (this.isBody && window.pageYOffset === 0) { - window.scrollTo(0, 1); - this.resize(); - } - }, this)); - }; - - /* PUBLIC METHODS - * ========================= */ - Backstretch.prototype = { - resize: function () { - try { - var bgCSS = {left: 0, top: 0} - , rootWidth = this.isBody ? this.$root.width() : this.$root.innerWidth() - , bgWidth = rootWidth - , rootHeight = this.isBody ? ( window.innerHeight ? window.innerHeight : this.$root.height() ) : this.$root.innerHeight() - , bgHeight = bgWidth / this.$img.data('ratio') - , bgOffset; - - // Make adjustments based on image ratio - if (bgHeight >= rootHeight) { - bgOffset = (bgHeight - rootHeight) / 2; - if(this.options.centeredY) { - bgCSS.top = '-' + bgOffset + 'px'; - } - } else { - bgHeight = rootHeight; - bgWidth = bgHeight * this.$img.data('ratio'); - bgOffset = (bgWidth - rootWidth) / 2; - if(this.options.centeredX) { - bgCSS.left = '-' + bgOffset + 'px'; - } - } - - this.$wrap.css({width: rootWidth, height: rootHeight}) - .find('img:not(.deleteable)').css({width: bgWidth, height: bgHeight}).css(bgCSS); - } catch(err) { - // IE7 seems to trigger resize before the image is loaded. - // This try/catch block is a hack to let it fail gracefully. - } - - return this; - } - - // Show the slide at a certain position - , show: function (newIndex) { - - // Validate index - if (Math.abs(newIndex) > this.images.length - 1) { - return; - } - - // Vars - var self = this - , oldImage = self.$wrap.find('img').addClass('deleteable') - , evtOptions = { relatedTarget: self.$container[0] }; - - // Trigger the "before" event - self.$container.trigger($.Event('backstretch.before', evtOptions), [self, newIndex]); - - // Set the new index - this.index = newIndex; - - // Pause the slideshow - clearInterval(self.interval); - - // New image - self.$img = $('<img />') - .css(styles.img) - .bind('load', function (e) { - var imgWidth = this.width || $(e.target).width() - , imgHeight = this.height || $(e.target).height(); - - // Save the ratio - $(this).data('ratio', imgWidth / imgHeight); - - // Show the image, then delete the old one - // "speed" option has been deprecated, but we want backwards compatibilty - $(this).fadeIn(self.options.speed || self.options.fade, function () { - oldImage.remove(); - - // Resume the slideshow - if (!self.paused) { - self.cycle(); - } - - // Trigger the "after" and "show" events - // "show" is being deprecated - $(['after', 'show']).each(function () { - self.$container.trigger($.Event('backstretch.' + this, evtOptions), [self, newIndex]); - }); - }); - - // Resize - self.resize(); - }) - .appendTo(self.$wrap); - - // Hack for IE img onload event - self.$img.attr('src', self.images[newIndex]); - return self; - } - - , next: function () { - // Next slide - return this.show(this.index < this.images.length - 1 ? this.index + 1 : 0); - } - - , prev: function () { - // Previous slide - return this.show(this.index === 0 ? this.images.length - 1 : this.index - 1); - } - - , pause: function () { - // Pause the slideshow - this.paused = true; - return this; - } - - , resume: function () { - // Resume the slideshow - this.paused = false; - this.next(); - return this; - } - - , cycle: function () { - // Start/resume the slideshow - if(this.images.length > 1) { - // Clear the interval, just in case - clearInterval(this.interval); - - this.interval = setInterval($.proxy(function () { - // Check for paused slideshow - if (!this.paused) { - this.next(); - } - }, this), this.options.duration); - } - return this; - } - - , destroy: function (preserveBackground) { - // Stop the resize events - $(window).off('resize.backstretch orientationchange.backstretch'); - - // Clear the interval - clearInterval(this.interval); - - // Remove Backstretch - if(!preserveBackground) { - this.$wrap.remove(); - } - this.$container.removeData('backstretch'); - } - }; - - /* SUPPORTS FIXED POSITION? - * - * Based on code from jQuery Mobile 1.1.0 - * http://jquerymobile.com/ - * - * In a nutshell, we need to figure out if fixed positioning is supported. - * Unfortunately, this is very difficult to do on iOS, and usually involves - * injecting content, scrolling the page, etc.. It's ugly. - * jQuery Mobile uses this workaround. It's not ideal, but works. - * - * Modified to detect IE6 - * ========================= */ - - var supportsFixedPosition = (function () { - var ua = navigator.userAgent - , platform = navigator.platform - // Rendering engine is Webkit, and capture major version - , wkmatch = ua.match( /AppleWebKit\/([0-9]+)/ ) - , wkversion = !!wkmatch && wkmatch[ 1 ] - , ffmatch = ua.match( /Fennec\/([0-9]+)/ ) - , ffversion = !!ffmatch && ffmatch[ 1 ] - , operammobilematch = ua.match( /Opera Mobi\/([0-9]+)/ ) - , omversion = !!operammobilematch && operammobilematch[ 1 ] - , iematch = ua.match( /MSIE ([0-9]+)/ ) - , ieversion = !!iematch && iematch[ 1 ]; - - return !( - // iOS 4.3 and older : Platform is iPhone/Pad/Touch and Webkit version is less than 534 (ios5) - ((platform.indexOf( "iPhone" ) > -1 || platform.indexOf( "iPad" ) > -1 || platform.indexOf( "iPod" ) > -1 ) && wkversion && wkversion < 534) || - - // Opera Mini - (window.operamini && ({}).toString.call( window.operamini ) === "[object OperaMini]") || - (operammobilematch && omversion < 7458) || - - //Android lte 2.1: Platform is Android and Webkit version is less than 533 (Android 2.2) - (ua.indexOf( "Android" ) > -1 && wkversion && wkversion < 533) || - - // Firefox Mobile before 6.0 - - (ffversion && ffversion < 6) || - - // WebOS less than 3 - ("palmGetResource" in window && wkversion && wkversion < 534) || - - // MeeGo - (ua.indexOf( "MeeGo" ) > -1 && ua.indexOf( "NokiaBrowser/8.5.0" ) > -1) || - - // IE6 - (ieversion && ieversion <= 6) - ); - }()); - -}(jQuery, window)); \ No newline at end of file diff --git a/src/UI/JsLibraries/jquery.dotdotdot.js b/src/UI/JsLibraries/jquery.dotdotdot.js deleted file mode 100644 index c7f58d039..000000000 --- a/src/UI/JsLibraries/jquery.dotdotdot.js +++ /dev/null @@ -1,632 +0,0 @@ -/* - * jQuery dotdotdot 1.6.1 - * - * Copyright (c) 2013 Fred Heusschen - * www.frebsite.nl - * - * Plugin website: - * dotdotdot.frebsite.nl - * - * Dual licensed under the MIT and GPL licenses. - * http://en.wikipedia.org/wiki/MIT_License - * http://en.wikipedia.org/wiki/GNU_General_Public_License - */ - -(function( $ ) -{ - if ( $.fn.dotdotdot ) - { - return; - } - - $.fn.dotdotdot = function( o ) - { - if ( this.length == 0 ) - { - if ( !o || o.debug !== false ) - { - debug( true, 'No element found for "' + this.selector + '".' ); - } - return this; - } - if ( this.length > 1 ) - { - return this.each( - function() - { - $(this).dotdotdot( o ); - } - ); - } - - - var $dot = this; - - if ( $dot.data( 'dotdotdot' ) ) - { - $dot.trigger( 'destroy.dot' ); - } - - $dot.data( 'dotdotdot-style', $dot.attr( 'style' ) ); - $dot.css( 'word-wrap', 'break-word' ); - if ($dot.css( 'white-space' ) === 'nowrap') - { - $dot.css( 'white-space', 'normal' ); - } - - $dot.bind_events = function() - { - $dot.bind( - 'update.dot', - function( e, c ) - { - e.preventDefault(); - e.stopPropagation(); - - opts.maxHeight = ( typeof opts.height == 'number' ) - ? opts.height - : getTrueInnerHeight( $dot ); - - opts.maxHeight += opts.tolerance; - - if ( typeof c != 'undefined' ) - { - if ( typeof c == 'string' || c instanceof HTMLElement ) - { - c = $('<div />').append( c ).contents(); - } - if ( c instanceof $ ) - { - orgContent = c; - } - } - - $inr = $dot.wrapInner( '<div class="dotdotdot" />' ).children(); - $inr.empty() - .append( orgContent.clone( true ) ) - .css({ - 'height' : 'auto', - 'width' : 'auto', - 'border' : 'none', - 'padding' : 0, - 'margin' : 0 - }); - - var after = false, - trunc = false; - - if ( conf.afterElement ) - { - after = conf.afterElement.clone( true ); - conf.afterElement.remove(); - } - if ( test( $inr, opts ) ) - { - if ( opts.wrap == 'children' ) - { - trunc = children( $inr, opts, after ); - } - else - { - trunc = ellipsis( $inr, $dot, $inr, opts, after ); - } - } - $inr.replaceWith( $inr.contents() ); - $inr = null; - - if ( $.isFunction( opts.callback ) ) - { - opts.callback.call( $dot[ 0 ], trunc, orgContent ); - } - - conf.isTruncated = trunc; - return trunc; - } - - ).bind( - 'isTruncated.dot', - function( e, fn ) - { - e.preventDefault(); - e.stopPropagation(); - - if ( typeof fn == 'function' ) - { - fn.call( $dot[ 0 ], conf.isTruncated ); - } - return conf.isTruncated; - } - - ).bind( - 'originalContent.dot', - function( e, fn ) - { - e.preventDefault(); - e.stopPropagation(); - - if ( typeof fn == 'function' ) - { - fn.call( $dot[ 0 ], orgContent ); - } - return orgContent; - } - - ).bind( - 'destroy.dot', - function( e ) - { - e.preventDefault(); - e.stopPropagation(); - - $dot.unwatch() - .unbind_events() - .empty() - .append( orgContent ) - .attr( 'style', $dot.data( 'dotdotdot-style' ) ) - .data( 'dotdotdot', false ); - } - ); - return $dot; - }; // /bind_events - - $dot.unbind_events = function() - { - $dot.unbind('.dot'); - return $dot; - }; // /unbind_events - - $dot.watch = function() - { - $dot.unwatch(); - if ( opts.watch == 'window' ) - { - var $window = $(window), - _wWidth = $window.width(), - _wHeight = $window.height(); - - $window.bind( - 'resize.dot' + conf.dotId, - function() - { - if ( _wWidth != $window.width() || _wHeight != $window.height() || !opts.windowResizeFix ) - { - _wWidth = $window.width(); - _wHeight = $window.height(); - - if ( watchInt ) - { - clearInterval( watchInt ); - } - watchInt = setTimeout( - function() - { - $dot.trigger( 'update.dot' ); - }, 10 - ); - } - } - ); - } - else - { - watchOrg = getSizes( $dot ); - watchInt = setInterval( - function() - { - var watchNew = getSizes( $dot ); - if ( watchOrg.width != watchNew.width || - watchOrg.height != watchNew.height ) - { - $dot.trigger( 'update.dot' ); - watchOrg = getSizes( $dot ); - } - }, 100 - ); - } - return $dot; - }; - $dot.unwatch = function() - { - $(window).unbind( 'resize.dot' + conf.dotId ); - if ( watchInt ) - { - clearInterval( watchInt ); - } - return $dot; - }; - - var orgContent = $dot.contents(), - opts = $.extend( true, {}, $.fn.dotdotdot.defaults, o ), - conf = {}, - watchOrg = {}, - watchInt = null, - $inr = null; - - - if ( !( opts.lastCharacter.remove instanceof Array ) ) - { - opts.lastCharacter.remove = $.fn.dotdotdot.defaultArrays.lastCharacter.remove; - } - if ( !( opts.lastCharacter.noEllipsis instanceof Array ) ) - { - opts.lastCharacter.noEllipsis = $.fn.dotdotdot.defaultArrays.lastCharacter.noEllipsis; - } - - - conf.afterElement = getElement( opts.after, $dot ); - conf.isTruncated = false; - conf.dotId = dotId++; - - - $dot.data( 'dotdotdot', true ) - .bind_events() - .trigger( 'update.dot' ); - - if ( opts.watch ) - { - $dot.watch(); - } - - return $dot; - }; - - - // public - $.fn.dotdotdot.defaults = { - 'ellipsis' : '... ', - 'wrap' : 'word', - 'fallbackToLetter' : true, - 'lastCharacter' : {}, - 'tolerance' : 0, - 'callback' : null, - 'after' : null, - 'height' : null, - 'watch' : false, - 'windowResizeFix' : true, - 'debug' : false - }; - $.fn.dotdotdot.defaultArrays = { - 'lastCharacter' : { - 'remove' : [ ' ', '\u3000', ',', ';', '.', '!', '?' ], - 'noEllipsis' : [] - } - }; - - - // private - var dotId = 1; - - function children( $elem, o, after ) - { - var $elements = $elem.children(), - isTruncated = false; - - $elem.empty(); - - for ( var a = 0, l = $elements.length; a < l; a++ ) - { - var $e = $elements.eq( a ); - $elem.append( $e ); - if ( after ) - { - $elem.append( after ); - } - if ( test( $elem, o ) ) - { - $e.remove(); - isTruncated = true; - break; - } - else - { - if ( after ) - { - after.detach(); - } - } - } - return isTruncated; - } - function ellipsis( $elem, $d, $i, o, after ) - { - var $elements = $elem.contents(), - isTruncated = false; - - $elem.empty(); - - var notx = 'table, thead, tbody, tfoot, tr, col, colgroup, object, embed, param, ol, ul, dl, blockquote, select, optgroup, option, textarea, script, style'; - for ( var a = 0, l = $elements.length; a < l; a++ ) - { - - if ( isTruncated ) - { - break; - } - - var e = $elements[ a ], - $e = $(e); - - if ( typeof e == 'undefined' ) - { - continue; - } - - $elem.append( $e ); - if ( after ) - { - $elem[ ( $elem.is( notx ) ) ? 'after' : 'append' ]( after ); - } - if ( e.nodeType == 3 ) - { - if ( test( $i, o ) ) - { - isTruncated = ellipsisElement( $e, $d, $i, o, after ); - } - } - else - { - isTruncated = ellipsis( $e, $d, $i, o, after ); - } - - if ( !isTruncated ) - { - if ( after ) - { - after.detach(); - } - } - } - return isTruncated; - } - function ellipsisElement( $e, $d, $i, o, after ) - { - var isTruncated = false, - e = $e[ 0 ]; - - if ( typeof e == 'undefined' ) - { - return false; - } - - var txt = getTextContent( e ), - space = ( txt.indexOf(' ') !== -1 ) ? ' ' : '\u3000', - separator = ( o.wrap == 'letter' ) ? '' : space, - textArr = txt.split( separator ), - position = -1, - midPos = -1, - startPos = 0, - endPos = textArr.length - 1; - - while ( startPos <= endPos && !( startPos == 0 && endPos == 0 ) ) - { - var m = Math.floor( ( startPos + endPos ) / 2 ); - if ( m == midPos ) - { - break; - } - midPos = m; - - setTextContent( e, textArr.slice( 0, midPos + 1 ).join( separator ) + o.ellipsis ); - - if ( !test( $i, o ) ) - { - position = midPos; - startPos = midPos; - } - else - { - endPos = midPos; - } - if( endPos == startPos && endPos == 0 && o.fallbackToLetter ) - { - separator = ''; - textArr = textArr[0].split(separator); - position = -1; - midPos = -1; - startPos = 0; - endPos = textArr.length - 1; - } - } - - if ( position != -1 && !( textArr.length == 1 && textArr[ 0 ].length == 0 ) ) - { - txt = addEllipsis( textArr.slice( 0, position + 1 ).join( separator ), o ); - isTruncated = true; - setTextContent( e, txt ); - } - else - { - var $w = $e.parent(); - $e.remove(); - - var afterLength = ( after ) ? after.length : 0 ; - - if ( $w.contents().size() > afterLength ) - { - var $n = $w.contents().eq( -1 - afterLength ); - isTruncated = ellipsisElement( $n, $d, $i, o, after ); - } - else - { - var $p = $w.prev() - var e = $p.contents().eq( -1 )[ 0 ]; - - if ( typeof e != 'undefined' ) - { - var txt = addEllipsis( getTextContent( e ), o ); - setTextContent( e, txt ); - if ( after ) - { - $p.append( after ); - } - $w.remove(); - isTruncated = true; - } - - } - } - - return isTruncated; - } - function test( $i, o ) - { - return $i.innerHeight() > o.maxHeight; - } - function addEllipsis( txt, o ) - { - while( $.inArray( txt.slice( -1 ), o.lastCharacter.remove ) > -1 ) - { - txt = txt.slice( 0, -1 ); - } - if ( $.inArray( txt.slice( -1 ), o.lastCharacter.noEllipsis ) < 0 ) - { - txt += o.ellipsis; - } - return txt; - } - function getSizes( $d ) - { - return { - 'width' : $d.innerWidth(), - 'height': $d.innerHeight() - }; - } - function setTextContent( e, content ) - { - if ( e.innerText ) - { - e.innerText = content; - } - else if ( e.nodeValue ) - { - e.nodeValue = content; - } - else if (e.textContent) - { - e.textContent = content; - } - - } - function getTextContent( e ) - { - if ( e.innerText ) - { - return e.innerText; - } - else if ( e.nodeValue ) - { - return e.nodeValue; - } - else if ( e.textContent ) - { - return e.textContent; - } - else - { - return ""; - } - } - function getElement( e, $i ) - { - if ( typeof e == 'undefined' ) - { - return false; - } - if ( !e ) - { - return false; - } - if ( typeof e == 'string' ) - { - e = $(e, $i); - return ( e.length ) - ? e - : false; - } - if ( typeof e == 'object' ) - { - return ( typeof e.jquery == 'undefined' ) - ? false - : e; - } - return false; - } - function getTrueInnerHeight( $el ) - { - var h = $el.innerHeight(), - a = [ 'paddingTop', 'paddingBottom' ]; - - for ( var z = 0, l = a.length; z < l; z++ ) { - var m = parseInt( $el.css( a[ z ] ), 10 ); - if ( isNaN( m ) ) - { - m = 0; - } - h -= m; - } - return h; - } - function debug( d, m ) - { - if ( !d ) - { - return false; - } - if ( typeof m == 'string' ) - { - m = 'dotdotdot: ' + m; - } - else - { - m = [ 'dotdotdot:', m ]; - } - - if ( typeof window.console != 'undefined' ) - { - if ( typeof window.console.log != 'undefined' ) - { - window.console.log( m ); - } - } - return false; - } - - - // override jQuery.html - var _orgHtml = $.fn.html; - $.fn.html = function( str ) { - if ( typeof str != 'undefined' ) - { - if ( this.data( 'dotdotdot' ) ) - { - if ( typeof str != 'function' ) - { - return this.trigger( 'update', [ str ] ); - } - } - return _orgHtml.call( this, str ); - } - return _orgHtml.call( this ); - }; - - - // override jQuery.text - var _orgText = $.fn.text; - $.fn.text = function( str ) { - if ( typeof str != 'undefined' ) - { - if ( this.data( 'dotdotdot' ) ) - { - var temp = $( '<div />' ); - temp.text( str ); - str = temp.html(); - temp.remove(); - return this.trigger( 'update', [ str ] ); - } - return _orgText.call( this, str ); - } - return _orgText.call( this ); - }; - - -})( jQuery ); diff --git a/src/UI/JsLibraries/jquery.easypiechart.js b/src/UI/JsLibraries/jquery.easypiechart.js deleted file mode 100644 index c600fb85f..000000000 --- a/src/UI/JsLibraries/jquery.easypiechart.js +++ /dev/null @@ -1,357 +0,0 @@ -/**! - * easyPieChart - * Lightweight plugin to render simple, animated and retina optimized pie charts - * - * @license - * @author Robert Fleischmann <rendro87@gmail.com> (http://robert-fleischmann.de) - * @version 2.1.3 - **/ - -(function(root, factory) { - if(typeof exports === 'object') { - module.exports = factory(require('jquery')); - } - else if(typeof define === 'function' && define.amd) { - define(['jquery'], factory); - } - else { - factory(root.jQuery); - } -}(this, function($) { -/** - * Renderer to render the chart on a canvas object - * @param {DOMElement} el DOM element to host the canvas (root of the plugin) - * @param {object} options options object of the plugin - */ -var CanvasRenderer = function(el, options) { - var cachedBackground; - var canvas = document.createElement('canvas'); - - el.appendChild(canvas); - - if (typeof(G_vmlCanvasManager) !== 'undefined') { - G_vmlCanvasManager.initElement(canvas); - } - - var ctx = canvas.getContext('2d'); - - canvas.width = canvas.height = options.size; - - // canvas on retina devices - var scaleBy = 1; - if (window.devicePixelRatio > 1) { - scaleBy = window.devicePixelRatio; - canvas.style.width = canvas.style.height = [options.size, 'px'].join(''); - canvas.width = canvas.height = options.size * scaleBy; - ctx.scale(scaleBy, scaleBy); - } - - // move 0,0 coordinates to the center - ctx.translate(options.size / 2, options.size / 2); - - // rotate canvas -90deg - ctx.rotate((-1 / 2 + options.rotate / 180) * Math.PI); - - var radius = (options.size - options.lineWidth) / 2; - if (options.scaleColor && options.scaleLength) { - radius -= options.scaleLength + 2; // 2 is the distance between scale and bar - } - - // IE polyfill for Date - Date.now = Date.now || function() { - return +(new Date()); - }; - - /** - * Draw a circle around the center of the canvas - * @param {strong} color Valid CSS color string - * @param {number} lineWidth Width of the line in px - * @param {number} percent Percentage to draw (float between -1 and 1) - */ - var drawCircle = function(color, lineWidth, percent) { - percent = Math.min(Math.max(-1, percent || 0), 1); - var isNegative = percent <= 0 ? true : false; - - ctx.beginPath(); - ctx.arc(0, 0, radius, 0, Math.PI * 2 * percent, isNegative); - - ctx.strokeStyle = color; - ctx.lineWidth = lineWidth; - - ctx.stroke(); - }; - - /** - * Draw the scale of the chart - */ - var drawScale = function() { - var offset; - var length; - - ctx.lineWidth = 1; - ctx.fillStyle = options.scaleColor; - - ctx.save(); - for (var i = 24; i > 0; --i) { - if (i % 6 === 0) { - length = options.scaleLength; - offset = 0; - } else { - length = options.scaleLength * 0.6; - offset = options.scaleLength - length; - } - ctx.fillRect(-options.size/2 + offset, 0, length, 1); - ctx.rotate(Math.PI / 12); - } - ctx.restore(); - }; - - /** - * Request animation frame wrapper with polyfill - * @return {function} Request animation frame method or timeout fallback - */ - var reqAnimationFrame = (function() { - return window.requestAnimationFrame || - window.webkitRequestAnimationFrame || - window.mozRequestAnimationFrame || - function(callback) { - window.setTimeout(callback, 1000 / 60); - }; - }()); - - /** - * Draw the background of the plugin including the scale and the track - */ - var drawBackground = function() { - if(options.scaleColor) drawScale(); - if(options.trackColor) drawCircle(options.trackColor, options.lineWidth, 1); - }; - - /** - * Canvas accessor - */ - this.getCanvas = function() { - return canvas; - }; - - /** - * Canvas 2D context 'ctx' accessor - */ - this.getCtx = function() { - return ctx; - }; - - /** - * Clear the complete canvas - */ - this.clear = function() { - ctx.clearRect(options.size / -2, options.size / -2, options.size, options.size); - }; - - /** - * Draw the complete chart - * @param {number} percent Percent shown by the chart between -100 and 100 - */ - this.draw = function(percent) { - // do we need to render a background - if (!!options.scaleColor || !!options.trackColor) { - // getImageData and putImageData are supported - if (ctx.getImageData && ctx.putImageData) { - if (!cachedBackground) { - drawBackground(); - cachedBackground = ctx.getImageData(0, 0, options.size * scaleBy, options.size * scaleBy); - } else { - ctx.putImageData(cachedBackground, 0, 0); - } - } else { - this.clear(); - drawBackground(); - } - } else { - this.clear(); - } - - ctx.lineCap = options.lineCap; - - // if barcolor is a function execute it and pass the percent as a value - var color; - if (typeof(options.barColor) === 'function') { - color = options.barColor(percent); - } else { - color = options.barColor; - } - - // draw bar - drawCircle(color, options.lineWidth, percent / 100); - }.bind(this); - - /** - * Animate from some percent to some other percentage - * @param {number} from Starting percentage - * @param {number} to Final percentage - */ - this.animate = function(from, to) { - var startTime = Date.now(); - options.onStart(from, to); - var animation = function() { - var process = Math.min(Date.now() - startTime, options.animate.duration); - var currentValue = options.easing(this, process, from, to - from, options.animate.duration); - this.draw(currentValue); - options.onStep(from, to, currentValue); - if (process >= options.animate.duration) { - options.onStop(from, to); - } else { - reqAnimationFrame(animation); - } - }.bind(this); - - reqAnimationFrame(animation); - }.bind(this); -}; - -var EasyPieChart = function(el, opts) { - var defaultOptions = { - barColor: '#ef1e25', - trackColor: '#f9f9f9', - scaleColor: '#dfe0e0', - scaleLength: 5, - lineCap: 'round', - lineWidth: 3, - size: 110, - rotate: 0, - animate: { - duration: 1000, - enabled: true - }, - easing: function (x, t, b, c, d) { // more can be found here: http://gsgd.co.uk/sandbox/jquery/easing/ - t = t / (d/2); - if (t < 1) { - return c / 2 * t * t + b; - } - return -c/2 * ((--t)*(t-2) - 1) + b; - }, - onStart: function(from, to) { - return; - }, - onStep: function(from, to, currentValue) { - return; - }, - onStop: function(from, to) { - return; - } - }; - - // detect present renderer - if (typeof(CanvasRenderer) !== 'undefined') { - defaultOptions.renderer = CanvasRenderer; - } else if (typeof(SVGRenderer) !== 'undefined') { - defaultOptions.renderer = SVGRenderer; - } else { - throw new Error('Please load either the SVG- or the CanvasRenderer'); - } - - var options = {}; - var currentValue = 0; - - /** - * Initialize the plugin by creating the options object and initialize rendering - */ - var init = function() { - this.el = el; - this.options = options; - - // merge user options into default options - for (var i in defaultOptions) { - if (defaultOptions.hasOwnProperty(i)) { - options[i] = opts && typeof(opts[i]) !== 'undefined' ? opts[i] : defaultOptions[i]; - if (typeof(options[i]) === 'function') { - options[i] = options[i].bind(this); - } - } - } - - // check for jQuery easing - if (typeof(options.easing) === 'string' && typeof(jQuery) !== 'undefined' && jQuery.isFunction(jQuery.easing[options.easing])) { - options.easing = jQuery.easing[options.easing]; - } else { - options.easing = defaultOptions.easing; - } - - // process earlier animate option to avoid bc breaks - if (typeof(options.animate) === 'number') { - options.animate = { - duration: options.animate, - enabled: true - }; - } - - if (typeof(options.animate) === 'boolean' && !options.animate) { - options.animate = { - duration: 1000, - enabled: options.animate - }; - } - - // create renderer - this.renderer = new options.renderer(el, options); - - // initial draw - this.renderer.draw(currentValue); - - // initial update - if (el.dataset && el.dataset.percent) { - this.update(parseFloat(el.dataset.percent)); - } else if (el.getAttribute && el.getAttribute('data-percent')) { - this.update(parseFloat(el.getAttribute('data-percent'))); - } - }.bind(this); - - /** - * Update the value of the chart - * @param {number} newValue Number between 0 and 100 - * @return {object} Instance of the plugin for method chaining - */ - this.update = function(newValue) { - newValue = parseFloat(newValue); - if (options.animate.enabled) { - this.renderer.animate(currentValue, newValue); - } else { - this.renderer.draw(newValue); - } - currentValue = newValue; - return this; - }.bind(this); - - /** - * Disable animation - * @return {object} Instance of the plugin for method chaining - */ - this.disableAnimation = function() { - options.animate.enabled = false; - return this; - }; - - /** - * Enable animation - * @return {object} Instance of the plugin for method chaining - */ - this.enableAnimation = function() { - options.animate.enabled = true; - return this; - }; - - init(); -}; - -$.fn.easyPieChart = function(options) { - return this.each(function() { - var instanceOptions; - - if (!$.data(this, 'easyPieChart')) { - instanceOptions = $.extend({}, options, $(this).data()); - $.data(this, 'easyPieChart', new EasyPieChart(this, instanceOptions)); - } - }); -}; - -})); diff --git a/src/UI/JsLibraries/jquery.js b/src/UI/JsLibraries/jquery.js deleted file mode 100644 index 87dd04093..000000000 --- a/src/UI/JsLibraries/jquery.js +++ /dev/null @@ -1,10351 +0,0 @@ -/*! - * jQuery JavaScript Library v1.11.3 - * http://jquery.com/ - * - * Includes Sizzle.js - * http://sizzlejs.com/ - * - * Copyright 2005, 2014 jQuery Foundation, Inc. and other contributors - * Released under the MIT license - * http://jquery.org/license - * - * Date: 2015-04-28T16:19Z - */ - -(function( global, factory ) { - - if ( typeof module === "object" && typeof module.exports === "object" ) { - // For CommonJS and CommonJS-like environments where a proper window is present, - // execute the factory and get jQuery - // For environments that do not inherently posses a window with a document - // (such as Node.js), expose a jQuery-making factory as module.exports - // This accentuates the need for the creation of a real window - // e.g. var jQuery = require("jquery")(window); - // See ticket #14549 for more info - module.exports = global.document ? - factory( global, true ) : - function( w ) { - if ( !w.document ) { - throw new Error( "jQuery requires a window with a document" ); - } - return factory( w ); - }; - } else { - factory( global ); - } - -// Pass this if window is not defined yet -}(typeof window !== "undefined" ? window : this, function( window, noGlobal ) { - -// Can't do this because several apps including ASP.NET trace -// the stack via arguments.caller.callee and Firefox dies if -// you try to trace through "use strict" call chains. (#13335) -// Support: Firefox 18+ -// - -var deletedIds = []; - -var slice = deletedIds.slice; - -var concat = deletedIds.concat; - -var push = deletedIds.push; - -var indexOf = deletedIds.indexOf; - -var class2type = {}; - -var toString = class2type.toString; - -var hasOwn = class2type.hasOwnProperty; - -var support = {}; - - - -var - version = "1.11.3", - - // Define a local copy of jQuery - jQuery = function( selector, context ) { - // The jQuery object is actually just the init constructor 'enhanced' - // Need init if jQuery is called (just allow error to be thrown if not included) - return new jQuery.fn.init( selector, context ); - }, - - // Support: Android<4.1, IE<9 - // Make sure we trim BOM and NBSP - rtrim = /^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g, - - // Matches dashed string for camelizing - rmsPrefix = /^-ms-/, - rdashAlpha = /-([\da-z])/gi, - - // Used by jQuery.camelCase as callback to replace() - fcamelCase = function( all, letter ) { - return letter.toUpperCase(); - }; - -jQuery.fn = jQuery.prototype = { - // The current version of jQuery being used - jquery: version, - - constructor: jQuery, - - // Start with an empty selector - selector: "", - - // The default length of a jQuery object is 0 - length: 0, - - toArray: function() { - return slice.call( this ); - }, - - // Get the Nth element in the matched element set OR - // Get the whole matched element set as a clean array - get: function( num ) { - return num != null ? - - // Return just the one element from the set - ( num < 0 ? this[ num + this.length ] : this[ num ] ) : - - // Return all the elements in a clean array - slice.call( this ); - }, - - // Take an array of elements and push it onto the stack - // (returning the new matched element set) - pushStack: function( elems ) { - - // Build a new jQuery matched element set - var ret = jQuery.merge( this.constructor(), elems ); - - // Add the old object onto the stack (as a reference) - ret.prevObject = this; - ret.context = this.context; - - // Return the newly-formed element set - return ret; - }, - - // Execute a callback for every element in the matched set. - // (You can seed the arguments with an array of args, but this is - // only used internally.) - each: function( callback, args ) { - return jQuery.each( this, callback, args ); - }, - - map: function( callback ) { - return this.pushStack( jQuery.map(this, function( elem, i ) { - return callback.call( elem, i, elem ); - })); - }, - - slice: function() { - return this.pushStack( slice.apply( this, arguments ) ); - }, - - first: function() { - return this.eq( 0 ); - }, - - last: function() { - return this.eq( -1 ); - }, - - eq: function( i ) { - var len = this.length, - j = +i + ( i < 0 ? len : 0 ); - return this.pushStack( j >= 0 && j < len ? [ this[j] ] : [] ); - }, - - end: function() { - return this.prevObject || this.constructor(null); - }, - - // For internal use only. - // Behaves like an Array's method, not like a jQuery method. - push: push, - sort: deletedIds.sort, - splice: deletedIds.splice -}; - -jQuery.extend = jQuery.fn.extend = function() { - var src, copyIsArray, copy, name, options, clone, - target = arguments[0] || {}, - i = 1, - length = arguments.length, - deep = false; - - // Handle a deep copy situation - if ( typeof target === "boolean" ) { - deep = target; - - // skip the boolean and the target - target = arguments[ i ] || {}; - i++; - } - - // Handle case when target is a string or something (possible in deep copy) - if ( typeof target !== "object" && !jQuery.isFunction(target) ) { - target = {}; - } - - // extend jQuery itself if only one argument is passed - if ( i === length ) { - target = this; - i--; - } - - for ( ; i < length; i++ ) { - // Only deal with non-null/undefined values - if ( (options = arguments[ i ]) != null ) { - // Extend the base object - for ( name in options ) { - src = target[ name ]; - copy = options[ name ]; - - // Prevent never-ending loop - if ( target === copy ) { - continue; - } - - // Recurse if we're merging plain objects or arrays - if ( deep && copy && ( jQuery.isPlainObject(copy) || (copyIsArray = jQuery.isArray(copy)) ) ) { - if ( copyIsArray ) { - copyIsArray = false; - clone = src && jQuery.isArray(src) ? src : []; - - } else { - clone = src && jQuery.isPlainObject(src) ? src : {}; - } - - // Never move original objects, clone them - target[ name ] = jQuery.extend( deep, clone, copy ); - - // Don't bring in undefined values - } else if ( copy !== undefined ) { - target[ name ] = copy; - } - } - } - } - - // Return the modified object - return target; -}; - -jQuery.extend({ - // Unique for each copy of jQuery on the page - expando: "jQuery" + ( version + Math.random() ).replace( /\D/g, "" ), - - // Assume jQuery is ready without the ready module - isReady: true, - - error: function( msg ) { - throw new Error( msg ); - }, - - noop: function() {}, - - // See test/unit/core.js for details concerning isFunction. - // Since version 1.3, DOM methods and functions like alert - // aren't supported. They return false on IE (#2968). - isFunction: function( obj ) { - return jQuery.type(obj) === "function"; - }, - - isArray: Array.isArray || function( obj ) { - return jQuery.type(obj) === "array"; - }, - - isWindow: function( obj ) { - /* jshint eqeqeq: false */ - return obj != null && obj == obj.window; - }, - - isNumeric: function( obj ) { - // parseFloat NaNs numeric-cast false positives (null|true|false|"") - // ...but misinterprets leading-number strings, particularly hex literals ("0x...") - // subtraction forces infinities to NaN - // adding 1 corrects loss of precision from parseFloat (#15100) - return !jQuery.isArray( obj ) && (obj - parseFloat( obj ) + 1) >= 0; - }, - - isEmptyObject: function( obj ) { - var name; - for ( name in obj ) { - return false; - } - return true; - }, - - isPlainObject: function( obj ) { - var key; - - // Must be an Object. - // Because of IE, we also have to check the presence of the constructor property. - // Make sure that DOM nodes and window objects don't pass through, as well - if ( !obj || jQuery.type(obj) !== "object" || obj.nodeType || jQuery.isWindow( obj ) ) { - return false; - } - - try { - // Not own constructor property must be Object - if ( obj.constructor && - !hasOwn.call(obj, "constructor") && - !hasOwn.call(obj.constructor.prototype, "isPrototypeOf") ) { - return false; - } - } catch ( e ) { - // IE8,9 Will throw exceptions on certain host objects #9897 - return false; - } - - // Support: IE<9 - // Handle iteration over inherited properties before own properties. - if ( support.ownLast ) { - for ( key in obj ) { - return hasOwn.call( obj, key ); - } - } - - // Own properties are enumerated firstly, so to speed up, - // if last one is own, then all properties are own. - for ( key in obj ) {} - - return key === undefined || hasOwn.call( obj, key ); - }, - - type: function( obj ) { - if ( obj == null ) { - return obj + ""; - } - return typeof obj === "object" || typeof obj === "function" ? - class2type[ toString.call(obj) ] || "object" : - typeof obj; - }, - - // Evaluates a script in a global context - // Workarounds based on findings by Jim Driscoll - // http://weblogs.java.net/blog/driscoll/archive/2009/09/08/eval-javascript-global-context - globalEval: function( data ) { - if ( data && jQuery.trim( data ) ) { - // We use execScript on Internet Explorer - // We use an anonymous function so that context is window - // rather than jQuery in Firefox - ( window.execScript || function( data ) { - window[ "eval" ].call( window, data ); - } )( data ); - } - }, - - // Convert dashed to camelCase; used by the css and data modules - // Microsoft forgot to hump their vendor prefix (#9572) - camelCase: function( string ) { - return string.replace( rmsPrefix, "ms-" ).replace( rdashAlpha, fcamelCase ); - }, - - nodeName: function( elem, name ) { - return elem.nodeName && elem.nodeName.toLowerCase() === name.toLowerCase(); - }, - - // args is for internal usage only - each: function( obj, callback, args ) { - var value, - i = 0, - length = obj.length, - isArray = isArraylike( obj ); - - if ( args ) { - if ( isArray ) { - for ( ; i < length; i++ ) { - value = callback.apply( obj[ i ], args ); - - if ( value === false ) { - break; - } - } - } else { - for ( i in obj ) { - value = callback.apply( obj[ i ], args ); - - if ( value === false ) { - break; - } - } - } - - // A special, fast, case for the most common use of each - } else { - if ( isArray ) { - for ( ; i < length; i++ ) { - value = callback.call( obj[ i ], i, obj[ i ] ); - - if ( value === false ) { - break; - } - } - } else { - for ( i in obj ) { - value = callback.call( obj[ i ], i, obj[ i ] ); - - if ( value === false ) { - break; - } - } - } - } - - return obj; - }, - - // Support: Android<4.1, IE<9 - trim: function( text ) { - return text == null ? - "" : - ( text + "" ).replace( rtrim, "" ); - }, - - // results is for internal usage only - makeArray: function( arr, results ) { - var ret = results || []; - - if ( arr != null ) { - if ( isArraylike( Object(arr) ) ) { - jQuery.merge( ret, - typeof arr === "string" ? - [ arr ] : arr - ); - } else { - push.call( ret, arr ); - } - } - - return ret; - }, - - inArray: function( elem, arr, i ) { - var len; - - if ( arr ) { - if ( indexOf ) { - return indexOf.call( arr, elem, i ); - } - - len = arr.length; - i = i ? i < 0 ? Math.max( 0, len + i ) : i : 0; - - for ( ; i < len; i++ ) { - // Skip accessing in sparse arrays - if ( i in arr && arr[ i ] === elem ) { - return i; - } - } - } - - return -1; - }, - - merge: function( first, second ) { - var len = +second.length, - j = 0, - i = first.length; - - while ( j < len ) { - first[ i++ ] = second[ j++ ]; - } - - // Support: IE<9 - // Workaround casting of .length to NaN on otherwise arraylike objects (e.g., NodeLists) - if ( len !== len ) { - while ( second[j] !== undefined ) { - first[ i++ ] = second[ j++ ]; - } - } - - first.length = i; - - return first; - }, - - grep: function( elems, callback, invert ) { - var callbackInverse, - matches = [], - i = 0, - length = elems.length, - callbackExpect = !invert; - - // Go through the array, only saving the items - // that pass the validator function - for ( ; i < length; i++ ) { - callbackInverse = !callback( elems[ i ], i ); - if ( callbackInverse !== callbackExpect ) { - matches.push( elems[ i ] ); - } - } - - return matches; - }, - - // arg is for internal usage only - map: function( elems, callback, arg ) { - var value, - i = 0, - length = elems.length, - isArray = isArraylike( elems ), - ret = []; - - // Go through the array, translating each of the items to their new values - if ( isArray ) { - for ( ; i < length; i++ ) { - value = callback( elems[ i ], i, arg ); - - if ( value != null ) { - ret.push( value ); - } - } - - // Go through every key on the object, - } else { - for ( i in elems ) { - value = callback( elems[ i ], i, arg ); - - if ( value != null ) { - ret.push( value ); - } - } - } - - // Flatten any nested arrays - return concat.apply( [], ret ); - }, - - // A global GUID counter for objects - guid: 1, - - // Bind a function to a context, optionally partially applying any - // arguments. - proxy: function( fn, context ) { - var args, proxy, tmp; - - if ( typeof context === "string" ) { - tmp = fn[ context ]; - context = fn; - fn = tmp; - } - - // Quick check to determine if target is callable, in the spec - // this throws a TypeError, but we will just return undefined. - if ( !jQuery.isFunction( fn ) ) { - return undefined; - } - - // Simulated bind - args = slice.call( arguments, 2 ); - proxy = function() { - return fn.apply( context || this, args.concat( slice.call( arguments ) ) ); - }; - - // Set the guid of unique handler to the same of original handler, so it can be removed - proxy.guid = fn.guid = fn.guid || jQuery.guid++; - - return proxy; - }, - - now: function() { - return +( new Date() ); - }, - - // jQuery.support is not used in Core but other projects attach their - // properties to it so it needs to exist. - support: support -}); - -// Populate the class2type map -jQuery.each("Boolean Number String Function Array Date RegExp Object Error".split(" "), function(i, name) { - class2type[ "[object " + name + "]" ] = name.toLowerCase(); -}); - -function isArraylike( obj ) { - - // Support: iOS 8.2 (not reproducible in simulator) - // `in` check used to prevent JIT error (gh-2145) - // hasOwn isn't used here due to false negatives - // regarding Nodelist length in IE - var length = "length" in obj && obj.length, - type = jQuery.type( obj ); - - if ( type === "function" || jQuery.isWindow( obj ) ) { - return false; - } - - if ( obj.nodeType === 1 && length ) { - return true; - } - - return type === "array" || length === 0 || - typeof length === "number" && length > 0 && ( length - 1 ) in obj; -} -var Sizzle = -/*! - * Sizzle CSS Selector Engine v2.2.0-pre - * http://sizzlejs.com/ - * - * Copyright 2008, 2014 jQuery Foundation, Inc. and other contributors - * Released under the MIT license - * http://jquery.org/license - * - * Date: 2014-12-16 - */ -(function( window ) { - -var i, - support, - Expr, - getText, - isXML, - tokenize, - compile, - select, - outermostContext, - sortInput, - hasDuplicate, - - // Local document vars - setDocument, - document, - docElem, - documentIsHTML, - rbuggyQSA, - rbuggyMatches, - matches, - contains, - - // Instance-specific data - expando = "sizzle" + 1 * new Date(), - preferredDoc = window.document, - dirruns = 0, - done = 0, - classCache = createCache(), - tokenCache = createCache(), - compilerCache = createCache(), - sortOrder = function( a, b ) { - if ( a === b ) { - hasDuplicate = true; - } - return 0; - }, - - // General-purpose constants - MAX_NEGATIVE = 1 << 31, - - // Instance methods - hasOwn = ({}).hasOwnProperty, - arr = [], - pop = arr.pop, - push_native = arr.push, - push = arr.push, - slice = arr.slice, - // Use a stripped-down indexOf as it's faster than native - // http://jsperf.com/thor-indexof-vs-for/5 - indexOf = function( list, elem ) { - var i = 0, - len = list.length; - for ( ; i < len; i++ ) { - if ( list[i] === elem ) { - return i; - } - } - return -1; - }, - - booleans = "checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped", - - // Regular expressions - - // Whitespace characters http://www.w3.org/TR/css3-selectors/#whitespace - whitespace = "[\\x20\\t\\r\\n\\f]", - // http://www.w3.org/TR/css3-syntax/#characters - characterEncoding = "(?:\\\\.|[\\w-]|[^\\x00-\\xa0])+", - - // Loosely modeled on CSS identifier characters - // An unquoted value should be a CSS identifier http://www.w3.org/TR/css3-selectors/#attribute-selectors - // Proper syntax: http://www.w3.org/TR/CSS21/syndata.html#value-def-identifier - identifier = characterEncoding.replace( "w", "w#" ), - - // Attribute selectors: http://www.w3.org/TR/selectors/#attribute-selectors - attributes = "\\[" + whitespace + "*(" + characterEncoding + ")(?:" + whitespace + - // Operator (capture 2) - "*([*^$|!~]?=)" + whitespace + - // "Attribute values must be CSS identifiers [capture 5] or strings [capture 3 or capture 4]" - "*(?:'((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\"|(" + identifier + "))|)" + whitespace + - "*\\]", - - pseudos = ":(" + characterEncoding + ")(?:\\((" + - // To reduce the number of selectors needing tokenize in the preFilter, prefer arguments: - // 1. quoted (capture 3; capture 4 or capture 5) - "('((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\")|" + - // 2. simple (capture 6) - "((?:\\\\.|[^\\\\()[\\]]|" + attributes + ")*)|" + - // 3. anything else (capture 2) - ".*" + - ")\\)|)", - - // Leading and non-escaped trailing whitespace, capturing some non-whitespace characters preceding the latter - rwhitespace = new RegExp( whitespace + "+", "g" ), - rtrim = new RegExp( "^" + whitespace + "+|((?:^|[^\\\\])(?:\\\\.)*)" + whitespace + "+$", "g" ), - - rcomma = new RegExp( "^" + whitespace + "*," + whitespace + "*" ), - rcombinators = new RegExp( "^" + whitespace + "*([>+~]|" + whitespace + ")" + whitespace + "*" ), - - rattributeQuotes = new RegExp( "=" + whitespace + "*([^\\]'\"]*?)" + whitespace + "*\\]", "g" ), - - rpseudo = new RegExp( pseudos ), - ridentifier = new RegExp( "^" + identifier + "$" ), - - matchExpr = { - "ID": new RegExp( "^#(" + characterEncoding + ")" ), - "CLASS": new RegExp( "^\\.(" + characterEncoding + ")" ), - "TAG": new RegExp( "^(" + characterEncoding.replace( "w", "w*" ) + ")" ), - "ATTR": new RegExp( "^" + attributes ), - "PSEUDO": new RegExp( "^" + pseudos ), - "CHILD": new RegExp( "^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\(" + whitespace + - "*(even|odd|(([+-]|)(\\d*)n|)" + whitespace + "*(?:([+-]|)" + whitespace + - "*(\\d+)|))" + whitespace + "*\\)|)", "i" ), - "bool": new RegExp( "^(?:" + booleans + ")$", "i" ), - // For use in libraries implementing .is() - // We use this for POS matching in `select` - "needsContext": new RegExp( "^" + whitespace + "*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\(" + - whitespace + "*((?:-\\d)?\\d*)" + whitespace + "*\\)|)(?=[^-]|$)", "i" ) - }, - - rinputs = /^(?:input|select|textarea|button)$/i, - rheader = /^h\d$/i, - - rnative = /^[^{]+\{\s*\[native \w/, - - // Easily-parseable/retrievable ID or TAG or CLASS selectors - rquickExpr = /^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/, - - rsibling = /[+~]/, - rescape = /'|\\/g, - - // CSS escapes http://www.w3.org/TR/CSS21/syndata.html#escaped-characters - runescape = new RegExp( "\\\\([\\da-f]{1,6}" + whitespace + "?|(" + whitespace + ")|.)", "ig" ), - funescape = function( _, escaped, escapedWhitespace ) { - var high = "0x" + escaped - 0x10000; - // NaN means non-codepoint - // Support: Firefox<24 - // Workaround erroneous numeric interpretation of +"0x" - return high !== high || escapedWhitespace ? - escaped : - high < 0 ? - // BMP codepoint - String.fromCharCode( high + 0x10000 ) : - // Supplemental Plane codepoint (surrogate pair) - String.fromCharCode( high >> 10 | 0xD800, high & 0x3FF | 0xDC00 ); - }, - - // Used for iframes - // See setDocument() - // Removing the function wrapper causes a "Permission Denied" - // error in IE - unloadHandler = function() { - setDocument(); - }; - -// Optimize for push.apply( _, NodeList ) -try { - push.apply( - (arr = slice.call( preferredDoc.childNodes )), - preferredDoc.childNodes - ); - // Support: Android<4.0 - // Detect silently failing push.apply - arr[ preferredDoc.childNodes.length ].nodeType; -} catch ( e ) { - push = { apply: arr.length ? - - // Leverage slice if possible - function( target, els ) { - push_native.apply( target, slice.call(els) ); - } : - - // Support: IE<9 - // Otherwise append directly - function( target, els ) { - var j = target.length, - i = 0; - // Can't trust NodeList.length - while ( (target[j++] = els[i++]) ) {} - target.length = j - 1; - } - }; -} - -function Sizzle( selector, context, results, seed ) { - var match, elem, m, nodeType, - // QSA vars - i, groups, old, nid, newContext, newSelector; - - if ( ( context ? context.ownerDocument || context : preferredDoc ) !== document ) { - setDocument( context ); - } - - context = context || document; - results = results || []; - nodeType = context.nodeType; - - if ( typeof selector !== "string" || !selector || - nodeType !== 1 && nodeType !== 9 && nodeType !== 11 ) { - - return results; - } - - if ( !seed && documentIsHTML ) { - - // Try to shortcut find operations when possible (e.g., not under DocumentFragment) - if ( nodeType !== 11 && (match = rquickExpr.exec( selector )) ) { - // Speed-up: Sizzle("#ID") - if ( (m = match[1]) ) { - if ( nodeType === 9 ) { - elem = context.getElementById( m ); - // Check parentNode to catch when Blackberry 4.6 returns - // nodes that are no longer in the document (jQuery #6963) - if ( elem && elem.parentNode ) { - // Handle the case where IE, Opera, and Webkit return items - // by name instead of ID - if ( elem.id === m ) { - results.push( elem ); - return results; - } - } else { - return results; - } - } else { - // Context is not a document - if ( context.ownerDocument && (elem = context.ownerDocument.getElementById( m )) && - contains( context, elem ) && elem.id === m ) { - results.push( elem ); - return results; - } - } - - // Speed-up: Sizzle("TAG") - } else if ( match[2] ) { - push.apply( results, context.getElementsByTagName( selector ) ); - return results; - - // Speed-up: Sizzle(".CLASS") - } else if ( (m = match[3]) && support.getElementsByClassName ) { - push.apply( results, context.getElementsByClassName( m ) ); - return results; - } - } - - // QSA path - if ( support.qsa && (!rbuggyQSA || !rbuggyQSA.test( selector )) ) { - nid = old = expando; - newContext = context; - newSelector = nodeType !== 1 && selector; - - // qSA works strangely on Element-rooted queries - // We can work around this by specifying an extra ID on the root - // and working up from there (Thanks to Andrew Dupont for the technique) - // IE 8 doesn't work on object elements - if ( nodeType === 1 && context.nodeName.toLowerCase() !== "object" ) { - groups = tokenize( selector ); - - if ( (old = context.getAttribute("id")) ) { - nid = old.replace( rescape, "\\$&" ); - } else { - context.setAttribute( "id", nid ); - } - nid = "[id='" + nid + "'] "; - - i = groups.length; - while ( i-- ) { - groups[i] = nid + toSelector( groups[i] ); - } - newContext = rsibling.test( selector ) && testContext( context.parentNode ) || context; - newSelector = groups.join(","); - } - - if ( newSelector ) { - try { - push.apply( results, - newContext.querySelectorAll( newSelector ) - ); - return results; - } catch(qsaError) { - } finally { - if ( !old ) { - context.removeAttribute("id"); - } - } - } - } - } - - // All others - return select( selector.replace( rtrim, "$1" ), context, results, seed ); -} - -/** - * Create key-value caches of limited size - * @returns {Function(string, Object)} Returns the Object data after storing it on itself with - * property name the (space-suffixed) string and (if the cache is larger than Expr.cacheLength) - * deleting the oldest entry - */ -function createCache() { - var keys = []; - - function cache( key, value ) { - // Use (key + " ") to avoid collision with native prototype properties (see Issue #157) - if ( keys.push( key + " " ) > Expr.cacheLength ) { - // Only keep the most recent entries - delete cache[ keys.shift() ]; - } - return (cache[ key + " " ] = value); - } - return cache; -} - -/** - * Mark a function for special use by Sizzle - * @param {Function} fn The function to mark - */ -function markFunction( fn ) { - fn[ expando ] = true; - return fn; -} - -/** - * Support testing using an element - * @param {Function} fn Passed the created div and expects a boolean result - */ -function assert( fn ) { - var div = document.createElement("div"); - - try { - return !!fn( div ); - } catch (e) { - return false; - } finally { - // Remove from its parent by default - if ( div.parentNode ) { - div.parentNode.removeChild( div ); - } - // release memory in IE - div = null; - } -} - -/** - * Adds the same handler for all of the specified attrs - * @param {String} attrs Pipe-separated list of attributes - * @param {Function} handler The method that will be applied - */ -function addHandle( attrs, handler ) { - var arr = attrs.split("|"), - i = attrs.length; - - while ( i-- ) { - Expr.attrHandle[ arr[i] ] = handler; - } -} - -/** - * Checks document order of two siblings - * @param {Element} a - * @param {Element} b - * @returns {Number} Returns less than 0 if a precedes b, greater than 0 if a follows b - */ -function siblingCheck( a, b ) { - var cur = b && a, - diff = cur && a.nodeType === 1 && b.nodeType === 1 && - ( ~b.sourceIndex || MAX_NEGATIVE ) - - ( ~a.sourceIndex || MAX_NEGATIVE ); - - // Use IE sourceIndex if available on both nodes - if ( diff ) { - return diff; - } - - // Check if b follows a - if ( cur ) { - while ( (cur = cur.nextSibling) ) { - if ( cur === b ) { - return -1; - } - } - } - - return a ? 1 : -1; -} - -/** - * Returns a function to use in pseudos for input types - * @param {String} type - */ -function createInputPseudo( type ) { - return function( elem ) { - var name = elem.nodeName.toLowerCase(); - return name === "input" && elem.type === type; - }; -} - -/** - * Returns a function to use in pseudos for buttons - * @param {String} type - */ -function createButtonPseudo( type ) { - return function( elem ) { - var name = elem.nodeName.toLowerCase(); - return (name === "input" || name === "button") && elem.type === type; - }; -} - -/** - * Returns a function to use in pseudos for positionals - * @param {Function} fn - */ -function createPositionalPseudo( fn ) { - return markFunction(function( argument ) { - argument = +argument; - return markFunction(function( seed, matches ) { - var j, - matchIndexes = fn( [], seed.length, argument ), - i = matchIndexes.length; - - // Match elements found at the specified indexes - while ( i-- ) { - if ( seed[ (j = matchIndexes[i]) ] ) { - seed[j] = !(matches[j] = seed[j]); - } - } - }); - }); -} - -/** - * Checks a node for validity as a Sizzle context - * @param {Element|Object=} context - * @returns {Element|Object|Boolean} The input node if acceptable, otherwise a falsy value - */ -function testContext( context ) { - return context && typeof context.getElementsByTagName !== "undefined" && context; -} - -// Expose support vars for convenience -support = Sizzle.support = {}; - -/** - * Detects XML nodes - * @param {Element|Object} elem An element or a document - * @returns {Boolean} True iff elem is a non-HTML XML node - */ -isXML = Sizzle.isXML = function( elem ) { - // documentElement is verified for cases where it doesn't yet exist - // (such as loading iframes in IE - #4833) - var documentElement = elem && (elem.ownerDocument || elem).documentElement; - return documentElement ? documentElement.nodeName !== "HTML" : false; -}; - -/** - * Sets document-related variables once based on the current document - * @param {Element|Object} [doc] An element or document object to use to set the document - * @returns {Object} Returns the current document - */ -setDocument = Sizzle.setDocument = function( node ) { - var hasCompare, parent, - doc = node ? node.ownerDocument || node : preferredDoc; - - // If no document and documentElement is available, return - if ( doc === document || doc.nodeType !== 9 || !doc.documentElement ) { - return document; - } - - // Set our document - document = doc; - docElem = doc.documentElement; - parent = doc.defaultView; - - // Support: IE>8 - // If iframe document is assigned to "document" variable and if iframe has been reloaded, - // IE will throw "permission denied" error when accessing "document" variable, see jQuery #13936 - // IE6-8 do not support the defaultView property so parent will be undefined - if ( parent && parent !== parent.top ) { - // IE11 does not have attachEvent, so all must suffer - if ( parent.addEventListener ) { - parent.addEventListener( "unload", unloadHandler, false ); - } else if ( parent.attachEvent ) { - parent.attachEvent( "onunload", unloadHandler ); - } - } - - /* Support tests - ---------------------------------------------------------------------- */ - documentIsHTML = !isXML( doc ); - - /* Attributes - ---------------------------------------------------------------------- */ - - // Support: IE<8 - // Verify that getAttribute really returns attributes and not properties - // (excepting IE8 booleans) - support.attributes = assert(function( div ) { - div.className = "i"; - return !div.getAttribute("className"); - }); - - /* getElement(s)By* - ---------------------------------------------------------------------- */ - - // Check if getElementsByTagName("*") returns only elements - support.getElementsByTagName = assert(function( div ) { - div.appendChild( doc.createComment("") ); - return !div.getElementsByTagName("*").length; - }); - - // Support: IE<9 - support.getElementsByClassName = rnative.test( doc.getElementsByClassName ); - - // Support: IE<10 - // Check if getElementById returns elements by name - // The broken getElementById methods don't pick up programatically-set names, - // so use a roundabout getElementsByName test - support.getById = assert(function( div ) { - docElem.appendChild( div ).id = expando; - return !doc.getElementsByName || !doc.getElementsByName( expando ).length; - }); - - // ID find and filter - if ( support.getById ) { - Expr.find["ID"] = function( id, context ) { - if ( typeof context.getElementById !== "undefined" && documentIsHTML ) { - var m = context.getElementById( id ); - // Check parentNode to catch when Blackberry 4.6 returns - // nodes that are no longer in the document #6963 - return m && m.parentNode ? [ m ] : []; - } - }; - Expr.filter["ID"] = function( id ) { - var attrId = id.replace( runescape, funescape ); - return function( elem ) { - return elem.getAttribute("id") === attrId; - }; - }; - } else { - // Support: IE6/7 - // getElementById is not reliable as a find shortcut - delete Expr.find["ID"]; - - Expr.filter["ID"] = function( id ) { - var attrId = id.replace( runescape, funescape ); - return function( elem ) { - var node = typeof elem.getAttributeNode !== "undefined" && elem.getAttributeNode("id"); - return node && node.value === attrId; - }; - }; - } - - // Tag - Expr.find["TAG"] = support.getElementsByTagName ? - function( tag, context ) { - if ( typeof context.getElementsByTagName !== "undefined" ) { - return context.getElementsByTagName( tag ); - - // DocumentFragment nodes don't have gEBTN - } else if ( support.qsa ) { - return context.querySelectorAll( tag ); - } - } : - - function( tag, context ) { - var elem, - tmp = [], - i = 0, - // By happy coincidence, a (broken) gEBTN appears on DocumentFragment nodes too - results = context.getElementsByTagName( tag ); - - // Filter out possible comments - if ( tag === "*" ) { - while ( (elem = results[i++]) ) { - if ( elem.nodeType === 1 ) { - tmp.push( elem ); - } - } - - return tmp; - } - return results; - }; - - // Class - Expr.find["CLASS"] = support.getElementsByClassName && function( className, context ) { - if ( documentIsHTML ) { - return context.getElementsByClassName( className ); - } - }; - - /* QSA/matchesSelector - ---------------------------------------------------------------------- */ - - // QSA and matchesSelector support - - // matchesSelector(:active) reports false when true (IE9/Opera 11.5) - rbuggyMatches = []; - - // qSa(:focus) reports false when true (Chrome 21) - // We allow this because of a bug in IE8/9 that throws an error - // whenever `document.activeElement` is accessed on an iframe - // So, we allow :focus to pass through QSA all the time to avoid the IE error - // See http://bugs.jquery.com/ticket/13378 - rbuggyQSA = []; - - if ( (support.qsa = rnative.test( doc.querySelectorAll )) ) { - // Build QSA regex - // Regex strategy adopted from Diego Perini - assert(function( div ) { - // Select is set to empty string on purpose - // This is to test IE's treatment of not explicitly - // setting a boolean content attribute, - // since its presence should be enough - // http://bugs.jquery.com/ticket/12359 - docElem.appendChild( div ).innerHTML = "<a id='" + expando + "'></a>" + - "<select id='" + expando + "-\f]' msallowcapture=''>" + - "<option selected=''></option></select>"; - - // Support: IE8, Opera 11-12.16 - // Nothing should be selected when empty strings follow ^= or $= or *= - // The test attribute must be unknown in Opera but "safe" for WinRT - // http://msdn.microsoft.com/en-us/library/ie/hh465388.aspx#attribute_section - if ( div.querySelectorAll("[msallowcapture^='']").length ) { - rbuggyQSA.push( "[*^$]=" + whitespace + "*(?:''|\"\")" ); - } - - // Support: IE8 - // Boolean attributes and "value" are not treated correctly - if ( !div.querySelectorAll("[selected]").length ) { - rbuggyQSA.push( "\\[" + whitespace + "*(?:value|" + booleans + ")" ); - } - - // Support: Chrome<29, Android<4.2+, Safari<7.0+, iOS<7.0+, PhantomJS<1.9.7+ - if ( !div.querySelectorAll( "[id~=" + expando + "-]" ).length ) { - rbuggyQSA.push("~="); - } - - // Webkit/Opera - :checked should return selected option elements - // http://www.w3.org/TR/2011/REC-css3-selectors-20110929/#checked - // IE8 throws error here and will not see later tests - if ( !div.querySelectorAll(":checked").length ) { - rbuggyQSA.push(":checked"); - } - - // Support: Safari 8+, iOS 8+ - // https://bugs.webkit.org/show_bug.cgi?id=136851 - // In-page `selector#id sibing-combinator selector` fails - if ( !div.querySelectorAll( "a#" + expando + "+*" ).length ) { - rbuggyQSA.push(".#.+[+~]"); - } - }); - - assert(function( div ) { - // Support: Windows 8 Native Apps - // The type and name attributes are restricted during .innerHTML assignment - var input = doc.createElement("input"); - input.setAttribute( "type", "hidden" ); - div.appendChild( input ).setAttribute( "name", "D" ); - - // Support: IE8 - // Enforce case-sensitivity of name attribute - if ( div.querySelectorAll("[name=d]").length ) { - rbuggyQSA.push( "name" + whitespace + "*[*^$|!~]?=" ); - } - - // FF 3.5 - :enabled/:disabled and hidden elements (hidden elements are still enabled) - // IE8 throws error here and will not see later tests - if ( !div.querySelectorAll(":enabled").length ) { - rbuggyQSA.push( ":enabled", ":disabled" ); - } - - // Opera 10-11 does not throw on post-comma invalid pseudos - div.querySelectorAll("*,:x"); - rbuggyQSA.push(",.*:"); - }); - } - - if ( (support.matchesSelector = rnative.test( (matches = docElem.matches || - docElem.webkitMatchesSelector || - docElem.mozMatchesSelector || - docElem.oMatchesSelector || - docElem.msMatchesSelector) )) ) { - - assert(function( div ) { - // Check to see if it's possible to do matchesSelector - // on a disconnected node (IE 9) - support.disconnectedMatch = matches.call( div, "div" ); - - // This should fail with an exception - // Gecko does not error, returns false instead - matches.call( div, "[s!='']:x" ); - rbuggyMatches.push( "!=", pseudos ); - }); - } - - rbuggyQSA = rbuggyQSA.length && new RegExp( rbuggyQSA.join("|") ); - rbuggyMatches = rbuggyMatches.length && new RegExp( rbuggyMatches.join("|") ); - - /* Contains - ---------------------------------------------------------------------- */ - hasCompare = rnative.test( docElem.compareDocumentPosition ); - - // Element contains another - // Purposefully does not implement inclusive descendent - // As in, an element does not contain itself - contains = hasCompare || rnative.test( docElem.contains ) ? - function( a, b ) { - var adown = a.nodeType === 9 ? a.documentElement : a, - bup = b && b.parentNode; - return a === bup || !!( bup && bup.nodeType === 1 && ( - adown.contains ? - adown.contains( bup ) : - a.compareDocumentPosition && a.compareDocumentPosition( bup ) & 16 - )); - } : - function( a, b ) { - if ( b ) { - while ( (b = b.parentNode) ) { - if ( b === a ) { - return true; - } - } - } - return false; - }; - - /* Sorting - ---------------------------------------------------------------------- */ - - // Document order sorting - sortOrder = hasCompare ? - function( a, b ) { - - // Flag for duplicate removal - if ( a === b ) { - hasDuplicate = true; - return 0; - } - - // Sort on method existence if only one input has compareDocumentPosition - var compare = !a.compareDocumentPosition - !b.compareDocumentPosition; - if ( compare ) { - return compare; - } - - // Calculate position if both inputs belong to the same document - compare = ( a.ownerDocument || a ) === ( b.ownerDocument || b ) ? - a.compareDocumentPosition( b ) : - - // Otherwise we know they are disconnected - 1; - - // Disconnected nodes - if ( compare & 1 || - (!support.sortDetached && b.compareDocumentPosition( a ) === compare) ) { - - // Choose the first element that is related to our preferred document - if ( a === doc || a.ownerDocument === preferredDoc && contains(preferredDoc, a) ) { - return -1; - } - if ( b === doc || b.ownerDocument === preferredDoc && contains(preferredDoc, b) ) { - return 1; - } - - // Maintain original order - return sortInput ? - ( indexOf( sortInput, a ) - indexOf( sortInput, b ) ) : - 0; - } - - return compare & 4 ? -1 : 1; - } : - function( a, b ) { - // Exit early if the nodes are identical - if ( a === b ) { - hasDuplicate = true; - return 0; - } - - var cur, - i = 0, - aup = a.parentNode, - bup = b.parentNode, - ap = [ a ], - bp = [ b ]; - - // Parentless nodes are either documents or disconnected - if ( !aup || !bup ) { - return a === doc ? -1 : - b === doc ? 1 : - aup ? -1 : - bup ? 1 : - sortInput ? - ( indexOf( sortInput, a ) - indexOf( sortInput, b ) ) : - 0; - - // If the nodes are siblings, we can do a quick check - } else if ( aup === bup ) { - return siblingCheck( a, b ); - } - - // Otherwise we need full lists of their ancestors for comparison - cur = a; - while ( (cur = cur.parentNode) ) { - ap.unshift( cur ); - } - cur = b; - while ( (cur = cur.parentNode) ) { - bp.unshift( cur ); - } - - // Walk down the tree looking for a discrepancy - while ( ap[i] === bp[i] ) { - i++; - } - - return i ? - // Do a sibling check if the nodes have a common ancestor - siblingCheck( ap[i], bp[i] ) : - - // Otherwise nodes in our document sort first - ap[i] === preferredDoc ? -1 : - bp[i] === preferredDoc ? 1 : - 0; - }; - - return doc; -}; - -Sizzle.matches = function( expr, elements ) { - return Sizzle( expr, null, null, elements ); -}; - -Sizzle.matchesSelector = function( elem, expr ) { - // Set document vars if needed - if ( ( elem.ownerDocument || elem ) !== document ) { - setDocument( elem ); - } - - // Make sure that attribute selectors are quoted - expr = expr.replace( rattributeQuotes, "='$1']" ); - - if ( support.matchesSelector && documentIsHTML && - ( !rbuggyMatches || !rbuggyMatches.test( expr ) ) && - ( !rbuggyQSA || !rbuggyQSA.test( expr ) ) ) { - - try { - var ret = matches.call( elem, expr ); - - // IE 9's matchesSelector returns false on disconnected nodes - if ( ret || support.disconnectedMatch || - // As well, disconnected nodes are said to be in a document - // fragment in IE 9 - elem.document && elem.document.nodeType !== 11 ) { - return ret; - } - } catch (e) {} - } - - return Sizzle( expr, document, null, [ elem ] ).length > 0; -}; - -Sizzle.contains = function( context, elem ) { - // Set document vars if needed - if ( ( context.ownerDocument || context ) !== document ) { - setDocument( context ); - } - return contains( context, elem ); -}; - -Sizzle.attr = function( elem, name ) { - // Set document vars if needed - if ( ( elem.ownerDocument || elem ) !== document ) { - setDocument( elem ); - } - - var fn = Expr.attrHandle[ name.toLowerCase() ], - // Don't get fooled by Object.prototype properties (jQuery #13807) - val = fn && hasOwn.call( Expr.attrHandle, name.toLowerCase() ) ? - fn( elem, name, !documentIsHTML ) : - undefined; - - return val !== undefined ? - val : - support.attributes || !documentIsHTML ? - elem.getAttribute( name ) : - (val = elem.getAttributeNode(name)) && val.specified ? - val.value : - null; -}; - -Sizzle.error = function( msg ) { - throw new Error( "Syntax error, unrecognized expression: " + msg ); -}; - -/** - * Document sorting and removing duplicates - * @param {ArrayLike} results - */ -Sizzle.uniqueSort = function( results ) { - var elem, - duplicates = [], - j = 0, - i = 0; - - // Unless we *know* we can detect duplicates, assume their presence - hasDuplicate = !support.detectDuplicates; - sortInput = !support.sortStable && results.slice( 0 ); - results.sort( sortOrder ); - - if ( hasDuplicate ) { - while ( (elem = results[i++]) ) { - if ( elem === results[ i ] ) { - j = duplicates.push( i ); - } - } - while ( j-- ) { - results.splice( duplicates[ j ], 1 ); - } - } - - // Clear input after sorting to release objects - // See https://github.com/jquery/sizzle/pull/225 - sortInput = null; - - return results; -}; - -/** - * Utility function for retrieving the text value of an array of DOM nodes - * @param {Array|Element} elem - */ -getText = Sizzle.getText = function( elem ) { - var node, - ret = "", - i = 0, - nodeType = elem.nodeType; - - if ( !nodeType ) { - // If no nodeType, this is expected to be an array - while ( (node = elem[i++]) ) { - // Do not traverse comment nodes - ret += getText( node ); - } - } else if ( nodeType === 1 || nodeType === 9 || nodeType === 11 ) { - // Use textContent for elements - // innerText usage removed for consistency of new lines (jQuery #11153) - if ( typeof elem.textContent === "string" ) { - return elem.textContent; - } else { - // Traverse its children - for ( elem = elem.firstChild; elem; elem = elem.nextSibling ) { - ret += getText( elem ); - } - } - } else if ( nodeType === 3 || nodeType === 4 ) { - return elem.nodeValue; - } - // Do not include comment or processing instruction nodes - - return ret; -}; - -Expr = Sizzle.selectors = { - - // Can be adjusted by the user - cacheLength: 50, - - createPseudo: markFunction, - - match: matchExpr, - - attrHandle: {}, - - find: {}, - - relative: { - ">": { dir: "parentNode", first: true }, - " ": { dir: "parentNode" }, - "+": { dir: "previousSibling", first: true }, - "~": { dir: "previousSibling" } - }, - - preFilter: { - "ATTR": function( match ) { - match[1] = match[1].replace( runescape, funescape ); - - // Move the given value to match[3] whether quoted or unquoted - match[3] = ( match[3] || match[4] || match[5] || "" ).replace( runescape, funescape ); - - if ( match[2] === "~=" ) { - match[3] = " " + match[3] + " "; - } - - return match.slice( 0, 4 ); - }, - - "CHILD": function( match ) { - /* matches from matchExpr["CHILD"] - 1 type (only|nth|...) - 2 what (child|of-type) - 3 argument (even|odd|\d*|\d*n([+-]\d+)?|...) - 4 xn-component of xn+y argument ([+-]?\d*n|) - 5 sign of xn-component - 6 x of xn-component - 7 sign of y-component - 8 y of y-component - */ - match[1] = match[1].toLowerCase(); - - if ( match[1].slice( 0, 3 ) === "nth" ) { - // nth-* requires argument - if ( !match[3] ) { - Sizzle.error( match[0] ); - } - - // numeric x and y parameters for Expr.filter.CHILD - // remember that false/true cast respectively to 0/1 - match[4] = +( match[4] ? match[5] + (match[6] || 1) : 2 * ( match[3] === "even" || match[3] === "odd" ) ); - match[5] = +( ( match[7] + match[8] ) || match[3] === "odd" ); - - // other types prohibit arguments - } else if ( match[3] ) { - Sizzle.error( match[0] ); - } - - return match; - }, - - "PSEUDO": function( match ) { - var excess, - unquoted = !match[6] && match[2]; - - if ( matchExpr["CHILD"].test( match[0] ) ) { - return null; - } - - // Accept quoted arguments as-is - if ( match[3] ) { - match[2] = match[4] || match[5] || ""; - - // Strip excess characters from unquoted arguments - } else if ( unquoted && rpseudo.test( unquoted ) && - // Get excess from tokenize (recursively) - (excess = tokenize( unquoted, true )) && - // advance to the next closing parenthesis - (excess = unquoted.indexOf( ")", unquoted.length - excess ) - unquoted.length) ) { - - // excess is a negative index - match[0] = match[0].slice( 0, excess ); - match[2] = unquoted.slice( 0, excess ); - } - - // Return only captures needed by the pseudo filter method (type and argument) - return match.slice( 0, 3 ); - } - }, - - filter: { - - "TAG": function( nodeNameSelector ) { - var nodeName = nodeNameSelector.replace( runescape, funescape ).toLowerCase(); - return nodeNameSelector === "*" ? - function() { return true; } : - function( elem ) { - return elem.nodeName && elem.nodeName.toLowerCase() === nodeName; - }; - }, - - "CLASS": function( className ) { - var pattern = classCache[ className + " " ]; - - return pattern || - (pattern = new RegExp( "(^|" + whitespace + ")" + className + "(" + whitespace + "|$)" )) && - classCache( className, function( elem ) { - return pattern.test( typeof elem.className === "string" && elem.className || typeof elem.getAttribute !== "undefined" && elem.getAttribute("class") || "" ); - }); - }, - - "ATTR": function( name, operator, check ) { - return function( elem ) { - var result = Sizzle.attr( elem, name ); - - if ( result == null ) { - return operator === "!="; - } - if ( !operator ) { - return true; - } - - result += ""; - - return operator === "=" ? result === check : - operator === "!=" ? result !== check : - operator === "^=" ? check && result.indexOf( check ) === 0 : - operator === "*=" ? check && result.indexOf( check ) > -1 : - operator === "$=" ? check && result.slice( -check.length ) === check : - operator === "~=" ? ( " " + result.replace( rwhitespace, " " ) + " " ).indexOf( check ) > -1 : - operator === "|=" ? result === check || result.slice( 0, check.length + 1 ) === check + "-" : - false; - }; - }, - - "CHILD": function( type, what, argument, first, last ) { - var simple = type.slice( 0, 3 ) !== "nth", - forward = type.slice( -4 ) !== "last", - ofType = what === "of-type"; - - return first === 1 && last === 0 ? - - // Shortcut for :nth-*(n) - function( elem ) { - return !!elem.parentNode; - } : - - function( elem, context, xml ) { - var cache, outerCache, node, diff, nodeIndex, start, - dir = simple !== forward ? "nextSibling" : "previousSibling", - parent = elem.parentNode, - name = ofType && elem.nodeName.toLowerCase(), - useCache = !xml && !ofType; - - if ( parent ) { - - // :(first|last|only)-(child|of-type) - if ( simple ) { - while ( dir ) { - node = elem; - while ( (node = node[ dir ]) ) { - if ( ofType ? node.nodeName.toLowerCase() === name : node.nodeType === 1 ) { - return false; - } - } - // Reverse direction for :only-* (if we haven't yet done so) - start = dir = type === "only" && !start && "nextSibling"; - } - return true; - } - - start = [ forward ? parent.firstChild : parent.lastChild ]; - - // non-xml :nth-child(...) stores cache data on `parent` - if ( forward && useCache ) { - // Seek `elem` from a previously-cached index - outerCache = parent[ expando ] || (parent[ expando ] = {}); - cache = outerCache[ type ] || []; - nodeIndex = cache[0] === dirruns && cache[1]; - diff = cache[0] === dirruns && cache[2]; - node = nodeIndex && parent.childNodes[ nodeIndex ]; - - while ( (node = ++nodeIndex && node && node[ dir ] || - - // Fallback to seeking `elem` from the start - (diff = nodeIndex = 0) || start.pop()) ) { - - // When found, cache indexes on `parent` and break - if ( node.nodeType === 1 && ++diff && node === elem ) { - outerCache[ type ] = [ dirruns, nodeIndex, diff ]; - break; - } - } - - // Use previously-cached element index if available - } else if ( useCache && (cache = (elem[ expando ] || (elem[ expando ] = {}))[ type ]) && cache[0] === dirruns ) { - diff = cache[1]; - - // xml :nth-child(...) or :nth-last-child(...) or :nth(-last)?-of-type(...) - } else { - // Use the same loop as above to seek `elem` from the start - while ( (node = ++nodeIndex && node && node[ dir ] || - (diff = nodeIndex = 0) || start.pop()) ) { - - if ( ( ofType ? node.nodeName.toLowerCase() === name : node.nodeType === 1 ) && ++diff ) { - // Cache the index of each encountered element - if ( useCache ) { - (node[ expando ] || (node[ expando ] = {}))[ type ] = [ dirruns, diff ]; - } - - if ( node === elem ) { - break; - } - } - } - } - - // Incorporate the offset, then check against cycle size - diff -= last; - return diff === first || ( diff % first === 0 && diff / first >= 0 ); - } - }; - }, - - "PSEUDO": function( pseudo, argument ) { - // pseudo-class names are case-insensitive - // http://www.w3.org/TR/selectors/#pseudo-classes - // Prioritize by case sensitivity in case custom pseudos are added with uppercase letters - // Remember that setFilters inherits from pseudos - var args, - fn = Expr.pseudos[ pseudo ] || Expr.setFilters[ pseudo.toLowerCase() ] || - Sizzle.error( "unsupported pseudo: " + pseudo ); - - // The user may use createPseudo to indicate that - // arguments are needed to create the filter function - // just as Sizzle does - if ( fn[ expando ] ) { - return fn( argument ); - } - - // But maintain support for old signatures - if ( fn.length > 1 ) { - args = [ pseudo, pseudo, "", argument ]; - return Expr.setFilters.hasOwnProperty( pseudo.toLowerCase() ) ? - markFunction(function( seed, matches ) { - var idx, - matched = fn( seed, argument ), - i = matched.length; - while ( i-- ) { - idx = indexOf( seed, matched[i] ); - seed[ idx ] = !( matches[ idx ] = matched[i] ); - } - }) : - function( elem ) { - return fn( elem, 0, args ); - }; - } - - return fn; - } - }, - - pseudos: { - // Potentially complex pseudos - "not": markFunction(function( selector ) { - // Trim the selector passed to compile - // to avoid treating leading and trailing - // spaces as combinators - var input = [], - results = [], - matcher = compile( selector.replace( rtrim, "$1" ) ); - - return matcher[ expando ] ? - markFunction(function( seed, matches, context, xml ) { - var elem, - unmatched = matcher( seed, null, xml, [] ), - i = seed.length; - - // Match elements unmatched by `matcher` - while ( i-- ) { - if ( (elem = unmatched[i]) ) { - seed[i] = !(matches[i] = elem); - } - } - }) : - function( elem, context, xml ) { - input[0] = elem; - matcher( input, null, xml, results ); - // Don't keep the element (issue #299) - input[0] = null; - return !results.pop(); - }; - }), - - "has": markFunction(function( selector ) { - return function( elem ) { - return Sizzle( selector, elem ).length > 0; - }; - }), - - "contains": markFunction(function( text ) { - text = text.replace( runescape, funescape ); - return function( elem ) { - return ( elem.textContent || elem.innerText || getText( elem ) ).indexOf( text ) > -1; - }; - }), - - // "Whether an element is represented by a :lang() selector - // is based solely on the element's language value - // being equal to the identifier C, - // or beginning with the identifier C immediately followed by "-". - // The matching of C against the element's language value is performed case-insensitively. - // The identifier C does not have to be a valid language name." - // http://www.w3.org/TR/selectors/#lang-pseudo - "lang": markFunction( function( lang ) { - // lang value must be a valid identifier - if ( !ridentifier.test(lang || "") ) { - Sizzle.error( "unsupported lang: " + lang ); - } - lang = lang.replace( runescape, funescape ).toLowerCase(); - return function( elem ) { - var elemLang; - do { - if ( (elemLang = documentIsHTML ? - elem.lang : - elem.getAttribute("xml:lang") || elem.getAttribute("lang")) ) { - - elemLang = elemLang.toLowerCase(); - return elemLang === lang || elemLang.indexOf( lang + "-" ) === 0; - } - } while ( (elem = elem.parentNode) && elem.nodeType === 1 ); - return false; - }; - }), - - // Miscellaneous - "target": function( elem ) { - var hash = window.location && window.location.hash; - return hash && hash.slice( 1 ) === elem.id; - }, - - "root": function( elem ) { - return elem === docElem; - }, - - "focus": function( elem ) { - return elem === document.activeElement && (!document.hasFocus || document.hasFocus()) && !!(elem.type || elem.href || ~elem.tabIndex); - }, - - // Boolean properties - "enabled": function( elem ) { - return elem.disabled === false; - }, - - "disabled": function( elem ) { - return elem.disabled === true; - }, - - "checked": function( elem ) { - // In CSS3, :checked should return both checked and selected elements - // http://www.w3.org/TR/2011/REC-css3-selectors-20110929/#checked - var nodeName = elem.nodeName.toLowerCase(); - return (nodeName === "input" && !!elem.checked) || (nodeName === "option" && !!elem.selected); - }, - - "selected": function( elem ) { - // Accessing this property makes selected-by-default - // options in Safari work properly - if ( elem.parentNode ) { - elem.parentNode.selectedIndex; - } - - return elem.selected === true; - }, - - // Contents - "empty": function( elem ) { - // http://www.w3.org/TR/selectors/#empty-pseudo - // :empty is negated by element (1) or content nodes (text: 3; cdata: 4; entity ref: 5), - // but not by others (comment: 8; processing instruction: 7; etc.) - // nodeType < 6 works because attributes (2) do not appear as children - for ( elem = elem.firstChild; elem; elem = elem.nextSibling ) { - if ( elem.nodeType < 6 ) { - return false; - } - } - return true; - }, - - "parent": function( elem ) { - return !Expr.pseudos["empty"]( elem ); - }, - - // Element/input types - "header": function( elem ) { - return rheader.test( elem.nodeName ); - }, - - "input": function( elem ) { - return rinputs.test( elem.nodeName ); - }, - - "button": function( elem ) { - var name = elem.nodeName.toLowerCase(); - return name === "input" && elem.type === "button" || name === "button"; - }, - - "text": function( elem ) { - var attr; - return elem.nodeName.toLowerCase() === "input" && - elem.type === "text" && - - // Support: IE<8 - // New HTML5 attribute values (e.g., "search") appear with elem.type === "text" - ( (attr = elem.getAttribute("type")) == null || attr.toLowerCase() === "text" ); - }, - - // Position-in-collection - "first": createPositionalPseudo(function() { - return [ 0 ]; - }), - - "last": createPositionalPseudo(function( matchIndexes, length ) { - return [ length - 1 ]; - }), - - "eq": createPositionalPseudo(function( matchIndexes, length, argument ) { - return [ argument < 0 ? argument + length : argument ]; - }), - - "even": createPositionalPseudo(function( matchIndexes, length ) { - var i = 0; - for ( ; i < length; i += 2 ) { - matchIndexes.push( i ); - } - return matchIndexes; - }), - - "odd": createPositionalPseudo(function( matchIndexes, length ) { - var i = 1; - for ( ; i < length; i += 2 ) { - matchIndexes.push( i ); - } - return matchIndexes; - }), - - "lt": createPositionalPseudo(function( matchIndexes, length, argument ) { - var i = argument < 0 ? argument + length : argument; - for ( ; --i >= 0; ) { - matchIndexes.push( i ); - } - return matchIndexes; - }), - - "gt": createPositionalPseudo(function( matchIndexes, length, argument ) { - var i = argument < 0 ? argument + length : argument; - for ( ; ++i < length; ) { - matchIndexes.push( i ); - } - return matchIndexes; - }) - } -}; - -Expr.pseudos["nth"] = Expr.pseudos["eq"]; - -// Add button/input type pseudos -for ( i in { radio: true, checkbox: true, file: true, password: true, image: true } ) { - Expr.pseudos[ i ] = createInputPseudo( i ); -} -for ( i in { submit: true, reset: true } ) { - Expr.pseudos[ i ] = createButtonPseudo( i ); -} - -// Easy API for creating new setFilters -function setFilters() {} -setFilters.prototype = Expr.filters = Expr.pseudos; -Expr.setFilters = new setFilters(); - -tokenize = Sizzle.tokenize = function( selector, parseOnly ) { - var matched, match, tokens, type, - soFar, groups, preFilters, - cached = tokenCache[ selector + " " ]; - - if ( cached ) { - return parseOnly ? 0 : cached.slice( 0 ); - } - - soFar = selector; - groups = []; - preFilters = Expr.preFilter; - - while ( soFar ) { - - // Comma and first run - if ( !matched || (match = rcomma.exec( soFar )) ) { - if ( match ) { - // Don't consume trailing commas as valid - soFar = soFar.slice( match[0].length ) || soFar; - } - groups.push( (tokens = []) ); - } - - matched = false; - - // Combinators - if ( (match = rcombinators.exec( soFar )) ) { - matched = match.shift(); - tokens.push({ - value: matched, - // Cast descendant combinators to space - type: match[0].replace( rtrim, " " ) - }); - soFar = soFar.slice( matched.length ); - } - - // Filters - for ( type in Expr.filter ) { - if ( (match = matchExpr[ type ].exec( soFar )) && (!preFilters[ type ] || - (match = preFilters[ type ]( match ))) ) { - matched = match.shift(); - tokens.push({ - value: matched, - type: type, - matches: match - }); - soFar = soFar.slice( matched.length ); - } - } - - if ( !matched ) { - break; - } - } - - // Return the length of the invalid excess - // if we're just parsing - // Otherwise, throw an error or return tokens - return parseOnly ? - soFar.length : - soFar ? - Sizzle.error( selector ) : - // Cache the tokens - tokenCache( selector, groups ).slice( 0 ); -}; - -function toSelector( tokens ) { - var i = 0, - len = tokens.length, - selector = ""; - for ( ; i < len; i++ ) { - selector += tokens[i].value; - } - return selector; -} - -function addCombinator( matcher, combinator, base ) { - var dir = combinator.dir, - checkNonElements = base && dir === "parentNode", - doneName = done++; - - return combinator.first ? - // Check against closest ancestor/preceding element - function( elem, context, xml ) { - while ( (elem = elem[ dir ]) ) { - if ( elem.nodeType === 1 || checkNonElements ) { - return matcher( elem, context, xml ); - } - } - } : - - // Check against all ancestor/preceding elements - function( elem, context, xml ) { - var oldCache, outerCache, - newCache = [ dirruns, doneName ]; - - // We can't set arbitrary data on XML nodes, so they don't benefit from dir caching - if ( xml ) { - while ( (elem = elem[ dir ]) ) { - if ( elem.nodeType === 1 || checkNonElements ) { - if ( matcher( elem, context, xml ) ) { - return true; - } - } - } - } else { - while ( (elem = elem[ dir ]) ) { - if ( elem.nodeType === 1 || checkNonElements ) { - outerCache = elem[ expando ] || (elem[ expando ] = {}); - if ( (oldCache = outerCache[ dir ]) && - oldCache[ 0 ] === dirruns && oldCache[ 1 ] === doneName ) { - - // Assign to newCache so results back-propagate to previous elements - return (newCache[ 2 ] = oldCache[ 2 ]); - } else { - // Reuse newcache so results back-propagate to previous elements - outerCache[ dir ] = newCache; - - // A match means we're done; a fail means we have to keep checking - if ( (newCache[ 2 ] = matcher( elem, context, xml )) ) { - return true; - } - } - } - } - } - }; -} - -function elementMatcher( matchers ) { - return matchers.length > 1 ? - function( elem, context, xml ) { - var i = matchers.length; - while ( i-- ) { - if ( !matchers[i]( elem, context, xml ) ) { - return false; - } - } - return true; - } : - matchers[0]; -} - -function multipleContexts( selector, contexts, results ) { - var i = 0, - len = contexts.length; - for ( ; i < len; i++ ) { - Sizzle( selector, contexts[i], results ); - } - return results; -} - -function condense( unmatched, map, filter, context, xml ) { - var elem, - newUnmatched = [], - i = 0, - len = unmatched.length, - mapped = map != null; - - for ( ; i < len; i++ ) { - if ( (elem = unmatched[i]) ) { - if ( !filter || filter( elem, context, xml ) ) { - newUnmatched.push( elem ); - if ( mapped ) { - map.push( i ); - } - } - } - } - - return newUnmatched; -} - -function setMatcher( preFilter, selector, matcher, postFilter, postFinder, postSelector ) { - if ( postFilter && !postFilter[ expando ] ) { - postFilter = setMatcher( postFilter ); - } - if ( postFinder && !postFinder[ expando ] ) { - postFinder = setMatcher( postFinder, postSelector ); - } - return markFunction(function( seed, results, context, xml ) { - var temp, i, elem, - preMap = [], - postMap = [], - preexisting = results.length, - - // Get initial elements from seed or context - elems = seed || multipleContexts( selector || "*", context.nodeType ? [ context ] : context, [] ), - - // Prefilter to get matcher input, preserving a map for seed-results synchronization - matcherIn = preFilter && ( seed || !selector ) ? - condense( elems, preMap, preFilter, context, xml ) : - elems, - - matcherOut = matcher ? - // If we have a postFinder, or filtered seed, or non-seed postFilter or preexisting results, - postFinder || ( seed ? preFilter : preexisting || postFilter ) ? - - // ...intermediate processing is necessary - [] : - - // ...otherwise use results directly - results : - matcherIn; - - // Find primary matches - if ( matcher ) { - matcher( matcherIn, matcherOut, context, xml ); - } - - // Apply postFilter - if ( postFilter ) { - temp = condense( matcherOut, postMap ); - postFilter( temp, [], context, xml ); - - // Un-match failing elements by moving them back to matcherIn - i = temp.length; - while ( i-- ) { - if ( (elem = temp[i]) ) { - matcherOut[ postMap[i] ] = !(matcherIn[ postMap[i] ] = elem); - } - } - } - - if ( seed ) { - if ( postFinder || preFilter ) { - if ( postFinder ) { - // Get the final matcherOut by condensing this intermediate into postFinder contexts - temp = []; - i = matcherOut.length; - while ( i-- ) { - if ( (elem = matcherOut[i]) ) { - // Restore matcherIn since elem is not yet a final match - temp.push( (matcherIn[i] = elem) ); - } - } - postFinder( null, (matcherOut = []), temp, xml ); - } - - // Move matched elements from seed to results to keep them synchronized - i = matcherOut.length; - while ( i-- ) { - if ( (elem = matcherOut[i]) && - (temp = postFinder ? indexOf( seed, elem ) : preMap[i]) > -1 ) { - - seed[temp] = !(results[temp] = elem); - } - } - } - - // Add elements to results, through postFinder if defined - } else { - matcherOut = condense( - matcherOut === results ? - matcherOut.splice( preexisting, matcherOut.length ) : - matcherOut - ); - if ( postFinder ) { - postFinder( null, results, matcherOut, xml ); - } else { - push.apply( results, matcherOut ); - } - } - }); -} - -function matcherFromTokens( tokens ) { - var checkContext, matcher, j, - len = tokens.length, - leadingRelative = Expr.relative[ tokens[0].type ], - implicitRelative = leadingRelative || Expr.relative[" "], - i = leadingRelative ? 1 : 0, - - // The foundational matcher ensures that elements are reachable from top-level context(s) - matchContext = addCombinator( function( elem ) { - return elem === checkContext; - }, implicitRelative, true ), - matchAnyContext = addCombinator( function( elem ) { - return indexOf( checkContext, elem ) > -1; - }, implicitRelative, true ), - matchers = [ function( elem, context, xml ) { - var ret = ( !leadingRelative && ( xml || context !== outermostContext ) ) || ( - (checkContext = context).nodeType ? - matchContext( elem, context, xml ) : - matchAnyContext( elem, context, xml ) ); - // Avoid hanging onto element (issue #299) - checkContext = null; - return ret; - } ]; - - for ( ; i < len; i++ ) { - if ( (matcher = Expr.relative[ tokens[i].type ]) ) { - matchers = [ addCombinator(elementMatcher( matchers ), matcher) ]; - } else { - matcher = Expr.filter[ tokens[i].type ].apply( null, tokens[i].matches ); - - // Return special upon seeing a positional matcher - if ( matcher[ expando ] ) { - // Find the next relative operator (if any) for proper handling - j = ++i; - for ( ; j < len; j++ ) { - if ( Expr.relative[ tokens[j].type ] ) { - break; - } - } - return setMatcher( - i > 1 && elementMatcher( matchers ), - i > 1 && toSelector( - // If the preceding token was a descendant combinator, insert an implicit any-element `*` - tokens.slice( 0, i - 1 ).concat({ value: tokens[ i - 2 ].type === " " ? "*" : "" }) - ).replace( rtrim, "$1" ), - matcher, - i < j && matcherFromTokens( tokens.slice( i, j ) ), - j < len && matcherFromTokens( (tokens = tokens.slice( j )) ), - j < len && toSelector( tokens ) - ); - } - matchers.push( matcher ); - } - } - - return elementMatcher( matchers ); -} - -function matcherFromGroupMatchers( elementMatchers, setMatchers ) { - var bySet = setMatchers.length > 0, - byElement = elementMatchers.length > 0, - superMatcher = function( seed, context, xml, results, outermost ) { - var elem, j, matcher, - matchedCount = 0, - i = "0", - unmatched = seed && [], - setMatched = [], - contextBackup = outermostContext, - // We must always have either seed elements or outermost context - elems = seed || byElement && Expr.find["TAG"]( "*", outermost ), - // Use integer dirruns iff this is the outermost matcher - dirrunsUnique = (dirruns += contextBackup == null ? 1 : Math.random() || 0.1), - len = elems.length; - - if ( outermost ) { - outermostContext = context !== document && context; - } - - // Add elements passing elementMatchers directly to results - // Keep `i` a string if there are no elements so `matchedCount` will be "00" below - // Support: IE<9, Safari - // Tolerate NodeList properties (IE: "length"; Safari: <number>) matching elements by id - for ( ; i !== len && (elem = elems[i]) != null; i++ ) { - if ( byElement && elem ) { - j = 0; - while ( (matcher = elementMatchers[j++]) ) { - if ( matcher( elem, context, xml ) ) { - results.push( elem ); - break; - } - } - if ( outermost ) { - dirruns = dirrunsUnique; - } - } - - // Track unmatched elements for set filters - if ( bySet ) { - // They will have gone through all possible matchers - if ( (elem = !matcher && elem) ) { - matchedCount--; - } - - // Lengthen the array for every element, matched or not - if ( seed ) { - unmatched.push( elem ); - } - } - } - - // Apply set filters to unmatched elements - matchedCount += i; - if ( bySet && i !== matchedCount ) { - j = 0; - while ( (matcher = setMatchers[j++]) ) { - matcher( unmatched, setMatched, context, xml ); - } - - if ( seed ) { - // Reintegrate element matches to eliminate the need for sorting - if ( matchedCount > 0 ) { - while ( i-- ) { - if ( !(unmatched[i] || setMatched[i]) ) { - setMatched[i] = pop.call( results ); - } - } - } - - // Discard index placeholder values to get only actual matches - setMatched = condense( setMatched ); - } - - // Add matches to results - push.apply( results, setMatched ); - - // Seedless set matches succeeding multiple successful matchers stipulate sorting - if ( outermost && !seed && setMatched.length > 0 && - ( matchedCount + setMatchers.length ) > 1 ) { - - Sizzle.uniqueSort( results ); - } - } - - // Override manipulation of globals by nested matchers - if ( outermost ) { - dirruns = dirrunsUnique; - outermostContext = contextBackup; - } - - return unmatched; - }; - - return bySet ? - markFunction( superMatcher ) : - superMatcher; -} - -compile = Sizzle.compile = function( selector, match /* Internal Use Only */ ) { - var i, - setMatchers = [], - elementMatchers = [], - cached = compilerCache[ selector + " " ]; - - if ( !cached ) { - // Generate a function of recursive functions that can be used to check each element - if ( !match ) { - match = tokenize( selector ); - } - i = match.length; - while ( i-- ) { - cached = matcherFromTokens( match[i] ); - if ( cached[ expando ] ) { - setMatchers.push( cached ); - } else { - elementMatchers.push( cached ); - } - } - - // Cache the compiled function - cached = compilerCache( selector, matcherFromGroupMatchers( elementMatchers, setMatchers ) ); - - // Save selector and tokenization - cached.selector = selector; - } - return cached; -}; - -/** - * A low-level selection function that works with Sizzle's compiled - * selector functions - * @param {String|Function} selector A selector or a pre-compiled - * selector function built with Sizzle.compile - * @param {Element} context - * @param {Array} [results] - * @param {Array} [seed] A set of elements to match against - */ -select = Sizzle.select = function( selector, context, results, seed ) { - var i, tokens, token, type, find, - compiled = typeof selector === "function" && selector, - match = !seed && tokenize( (selector = compiled.selector || selector) ); - - results = results || []; - - // Try to minimize operations if there is no seed and only one group - if ( match.length === 1 ) { - - // Take a shortcut and set the context if the root selector is an ID - tokens = match[0] = match[0].slice( 0 ); - if ( tokens.length > 2 && (token = tokens[0]).type === "ID" && - support.getById && context.nodeType === 9 && documentIsHTML && - Expr.relative[ tokens[1].type ] ) { - - context = ( Expr.find["ID"]( token.matches[0].replace(runescape, funescape), context ) || [] )[0]; - if ( !context ) { - return results; - - // Precompiled matchers will still verify ancestry, so step up a level - } else if ( compiled ) { - context = context.parentNode; - } - - selector = selector.slice( tokens.shift().value.length ); - } - - // Fetch a seed set for right-to-left matching - i = matchExpr["needsContext"].test( selector ) ? 0 : tokens.length; - while ( i-- ) { - token = tokens[i]; - - // Abort if we hit a combinator - if ( Expr.relative[ (type = token.type) ] ) { - break; - } - if ( (find = Expr.find[ type ]) ) { - // Search, expanding context for leading sibling combinators - if ( (seed = find( - token.matches[0].replace( runescape, funescape ), - rsibling.test( tokens[0].type ) && testContext( context.parentNode ) || context - )) ) { - - // If seed is empty or no tokens remain, we can return early - tokens.splice( i, 1 ); - selector = seed.length && toSelector( tokens ); - if ( !selector ) { - push.apply( results, seed ); - return results; - } - - break; - } - } - } - } - - // Compile and execute a filtering function if one is not provided - // Provide `match` to avoid retokenization if we modified the selector above - ( compiled || compile( selector, match ) )( - seed, - context, - !documentIsHTML, - results, - rsibling.test( selector ) && testContext( context.parentNode ) || context - ); - return results; -}; - -// One-time assignments - -// Sort stability -support.sortStable = expando.split("").sort( sortOrder ).join("") === expando; - -// Support: Chrome 14-35+ -// Always assume duplicates if they aren't passed to the comparison function -support.detectDuplicates = !!hasDuplicate; - -// Initialize against the default document -setDocument(); - -// Support: Webkit<537.32 - Safari 6.0.3/Chrome 25 (fixed in Chrome 27) -// Detached nodes confoundingly follow *each other* -support.sortDetached = assert(function( div1 ) { - // Should return 1, but returns 4 (following) - return div1.compareDocumentPosition( document.createElement("div") ) & 1; -}); - -// Support: IE<8 -// Prevent attribute/property "interpolation" -// http://msdn.microsoft.com/en-us/library/ms536429%28VS.85%29.aspx -if ( !assert(function( div ) { - div.innerHTML = "<a href='#'></a>"; - return div.firstChild.getAttribute("href") === "#" ; -}) ) { - addHandle( "type|href|height|width", function( elem, name, isXML ) { - if ( !isXML ) { - return elem.getAttribute( name, name.toLowerCase() === "type" ? 1 : 2 ); - } - }); -} - -// Support: IE<9 -// Use defaultValue in place of getAttribute("value") -if ( !support.attributes || !assert(function( div ) { - div.innerHTML = "<input/>"; - div.firstChild.setAttribute( "value", "" ); - return div.firstChild.getAttribute( "value" ) === ""; -}) ) { - addHandle( "value", function( elem, name, isXML ) { - if ( !isXML && elem.nodeName.toLowerCase() === "input" ) { - return elem.defaultValue; - } - }); -} - -// Support: IE<9 -// Use getAttributeNode to fetch booleans when getAttribute lies -if ( !assert(function( div ) { - return div.getAttribute("disabled") == null; -}) ) { - addHandle( booleans, function( elem, name, isXML ) { - var val; - if ( !isXML ) { - return elem[ name ] === true ? name.toLowerCase() : - (val = elem.getAttributeNode( name )) && val.specified ? - val.value : - null; - } - }); -} - -return Sizzle; - -})( window ); - - - -jQuery.find = Sizzle; -jQuery.expr = Sizzle.selectors; -jQuery.expr[":"] = jQuery.expr.pseudos; -jQuery.unique = Sizzle.uniqueSort; -jQuery.text = Sizzle.getText; -jQuery.isXMLDoc = Sizzle.isXML; -jQuery.contains = Sizzle.contains; - - - -var rneedsContext = jQuery.expr.match.needsContext; - -var rsingleTag = (/^<(\w+)\s*\/?>(?:<\/\1>|)$/); - - - -var risSimple = /^.[^:#\[\.,]*$/; - -// Implement the identical functionality for filter and not -function winnow( elements, qualifier, not ) { - if ( jQuery.isFunction( qualifier ) ) { - return jQuery.grep( elements, function( elem, i ) { - /* jshint -W018 */ - return !!qualifier.call( elem, i, elem ) !== not; - }); - - } - - if ( qualifier.nodeType ) { - return jQuery.grep( elements, function( elem ) { - return ( elem === qualifier ) !== not; - }); - - } - - if ( typeof qualifier === "string" ) { - if ( risSimple.test( qualifier ) ) { - return jQuery.filter( qualifier, elements, not ); - } - - qualifier = jQuery.filter( qualifier, elements ); - } - - return jQuery.grep( elements, function( elem ) { - return ( jQuery.inArray( elem, qualifier ) >= 0 ) !== not; - }); -} - -jQuery.filter = function( expr, elems, not ) { - var elem = elems[ 0 ]; - - if ( not ) { - expr = ":not(" + expr + ")"; - } - - return elems.length === 1 && elem.nodeType === 1 ? - jQuery.find.matchesSelector( elem, expr ) ? [ elem ] : [] : - jQuery.find.matches( expr, jQuery.grep( elems, function( elem ) { - return elem.nodeType === 1; - })); -}; - -jQuery.fn.extend({ - find: function( selector ) { - var i, - ret = [], - self = this, - len = self.length; - - if ( typeof selector !== "string" ) { - return this.pushStack( jQuery( selector ).filter(function() { - for ( i = 0; i < len; i++ ) { - if ( jQuery.contains( self[ i ], this ) ) { - return true; - } - } - }) ); - } - - for ( i = 0; i < len; i++ ) { - jQuery.find( selector, self[ i ], ret ); - } - - // Needed because $( selector, context ) becomes $( context ).find( selector ) - ret = this.pushStack( len > 1 ? jQuery.unique( ret ) : ret ); - ret.selector = this.selector ? this.selector + " " + selector : selector; - return ret; - }, - filter: function( selector ) { - return this.pushStack( winnow(this, selector || [], false) ); - }, - not: function( selector ) { - return this.pushStack( winnow(this, selector || [], true) ); - }, - is: function( selector ) { - return !!winnow( - this, - - // If this is a positional/relative selector, check membership in the returned set - // so $("p:first").is("p:last") won't return true for a doc with two "p". - typeof selector === "string" && rneedsContext.test( selector ) ? - jQuery( selector ) : - selector || [], - false - ).length; - } -}); - - -// Initialize a jQuery object - - -// A central reference to the root jQuery(document) -var rootjQuery, - - // Use the correct document accordingly with window argument (sandbox) - document = window.document, - - // A simple way to check for HTML strings - // Prioritize #id over <tag> to avoid XSS via location.hash (#9521) - // Strict HTML recognition (#11290: must start with <) - rquickExpr = /^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]*))$/, - - init = jQuery.fn.init = function( selector, context ) { - var match, elem; - - // HANDLE: $(""), $(null), $(undefined), $(false) - if ( !selector ) { - return this; - } - - // Handle HTML strings - if ( typeof selector === "string" ) { - if ( selector.charAt(0) === "<" && selector.charAt( selector.length - 1 ) === ">" && selector.length >= 3 ) { - // Assume that strings that start and end with <> are HTML and skip the regex check - match = [ null, selector, null ]; - - } else { - match = rquickExpr.exec( selector ); - } - - // Match html or make sure no context is specified for #id - if ( match && (match[1] || !context) ) { - - // HANDLE: $(html) -> $(array) - if ( match[1] ) { - context = context instanceof jQuery ? context[0] : context; - - // scripts is true for back-compat - // Intentionally let the error be thrown if parseHTML is not present - jQuery.merge( this, jQuery.parseHTML( - match[1], - context && context.nodeType ? context.ownerDocument || context : document, - true - ) ); - - // HANDLE: $(html, props) - if ( rsingleTag.test( match[1] ) && jQuery.isPlainObject( context ) ) { - for ( match in context ) { - // Properties of context are called as methods if possible - if ( jQuery.isFunction( this[ match ] ) ) { - this[ match ]( context[ match ] ); - - // ...and otherwise set as attributes - } else { - this.attr( match, context[ match ] ); - } - } - } - - return this; - - // HANDLE: $(#id) - } else { - elem = document.getElementById( match[2] ); - - // Check parentNode to catch when Blackberry 4.6 returns - // nodes that are no longer in the document #6963 - if ( elem && elem.parentNode ) { - // Handle the case where IE and Opera return items - // by name instead of ID - if ( elem.id !== match[2] ) { - return rootjQuery.find( selector ); - } - - // Otherwise, we inject the element directly into the jQuery object - this.length = 1; - this[0] = elem; - } - - this.context = document; - this.selector = selector; - return this; - } - - // HANDLE: $(expr, $(...)) - } else if ( !context || context.jquery ) { - return ( context || rootjQuery ).find( selector ); - - // HANDLE: $(expr, context) - // (which is just equivalent to: $(context).find(expr) - } else { - return this.constructor( context ).find( selector ); - } - - // HANDLE: $(DOMElement) - } else if ( selector.nodeType ) { - this.context = this[0] = selector; - this.length = 1; - return this; - - // HANDLE: $(function) - // Shortcut for document ready - } else if ( jQuery.isFunction( selector ) ) { - return typeof rootjQuery.ready !== "undefined" ? - rootjQuery.ready( selector ) : - // Execute immediately if ready is not present - selector( jQuery ); - } - - if ( selector.selector !== undefined ) { - this.selector = selector.selector; - this.context = selector.context; - } - - return jQuery.makeArray( selector, this ); - }; - -// Give the init function the jQuery prototype for later instantiation -init.prototype = jQuery.fn; - -// Initialize central reference -rootjQuery = jQuery( document ); - - -var rparentsprev = /^(?:parents|prev(?:Until|All))/, - // methods guaranteed to produce a unique set when starting from a unique set - guaranteedUnique = { - children: true, - contents: true, - next: true, - prev: true - }; - -jQuery.extend({ - dir: function( elem, dir, until ) { - var matched = [], - cur = elem[ dir ]; - - while ( cur && cur.nodeType !== 9 && (until === undefined || cur.nodeType !== 1 || !jQuery( cur ).is( until )) ) { - if ( cur.nodeType === 1 ) { - matched.push( cur ); - } - cur = cur[dir]; - } - return matched; - }, - - sibling: function( n, elem ) { - var r = []; - - for ( ; n; n = n.nextSibling ) { - if ( n.nodeType === 1 && n !== elem ) { - r.push( n ); - } - } - - return r; - } -}); - -jQuery.fn.extend({ - has: function( target ) { - var i, - targets = jQuery( target, this ), - len = targets.length; - - return this.filter(function() { - for ( i = 0; i < len; i++ ) { - if ( jQuery.contains( this, targets[i] ) ) { - return true; - } - } - }); - }, - - closest: function( selectors, context ) { - var cur, - i = 0, - l = this.length, - matched = [], - pos = rneedsContext.test( selectors ) || typeof selectors !== "string" ? - jQuery( selectors, context || this.context ) : - 0; - - for ( ; i < l; i++ ) { - for ( cur = this[i]; cur && cur !== context; cur = cur.parentNode ) { - // Always skip document fragments - if ( cur.nodeType < 11 && (pos ? - pos.index(cur) > -1 : - - // Don't pass non-elements to Sizzle - cur.nodeType === 1 && - jQuery.find.matchesSelector(cur, selectors)) ) { - - matched.push( cur ); - break; - } - } - } - - return this.pushStack( matched.length > 1 ? jQuery.unique( matched ) : matched ); - }, - - // Determine the position of an element within - // the matched set of elements - index: function( elem ) { - - // No argument, return index in parent - if ( !elem ) { - return ( this[0] && this[0].parentNode ) ? this.first().prevAll().length : -1; - } - - // index in selector - if ( typeof elem === "string" ) { - return jQuery.inArray( this[0], jQuery( elem ) ); - } - - // Locate the position of the desired element - return jQuery.inArray( - // If it receives a jQuery object, the first element is used - elem.jquery ? elem[0] : elem, this ); - }, - - add: function( selector, context ) { - return this.pushStack( - jQuery.unique( - jQuery.merge( this.get(), jQuery( selector, context ) ) - ) - ); - }, - - addBack: function( selector ) { - return this.add( selector == null ? - this.prevObject : this.prevObject.filter(selector) - ); - } -}); - -function sibling( cur, dir ) { - do { - cur = cur[ dir ]; - } while ( cur && cur.nodeType !== 1 ); - - return cur; -} - -jQuery.each({ - parent: function( elem ) { - var parent = elem.parentNode; - return parent && parent.nodeType !== 11 ? parent : null; - }, - parents: function( elem ) { - return jQuery.dir( elem, "parentNode" ); - }, - parentsUntil: function( elem, i, until ) { - return jQuery.dir( elem, "parentNode", until ); - }, - next: function( elem ) { - return sibling( elem, "nextSibling" ); - }, - prev: function( elem ) { - return sibling( elem, "previousSibling" ); - }, - nextAll: function( elem ) { - return jQuery.dir( elem, "nextSibling" ); - }, - prevAll: function( elem ) { - return jQuery.dir( elem, "previousSibling" ); - }, - nextUntil: function( elem, i, until ) { - return jQuery.dir( elem, "nextSibling", until ); - }, - prevUntil: function( elem, i, until ) { - return jQuery.dir( elem, "previousSibling", until ); - }, - siblings: function( elem ) { - return jQuery.sibling( ( elem.parentNode || {} ).firstChild, elem ); - }, - children: function( elem ) { - return jQuery.sibling( elem.firstChild ); - }, - contents: function( elem ) { - return jQuery.nodeName( elem, "iframe" ) ? - elem.contentDocument || elem.contentWindow.document : - jQuery.merge( [], elem.childNodes ); - } -}, function( name, fn ) { - jQuery.fn[ name ] = function( until, selector ) { - var ret = jQuery.map( this, fn, until ); - - if ( name.slice( -5 ) !== "Until" ) { - selector = until; - } - - if ( selector && typeof selector === "string" ) { - ret = jQuery.filter( selector, ret ); - } - - if ( this.length > 1 ) { - // Remove duplicates - if ( !guaranteedUnique[ name ] ) { - ret = jQuery.unique( ret ); - } - - // Reverse order for parents* and prev-derivatives - if ( rparentsprev.test( name ) ) { - ret = ret.reverse(); - } - } - - return this.pushStack( ret ); - }; -}); -var rnotwhite = (/\S+/g); - - - -// String to Object options format cache -var optionsCache = {}; - -// Convert String-formatted options into Object-formatted ones and store in cache -function createOptions( options ) { - var object = optionsCache[ options ] = {}; - jQuery.each( options.match( rnotwhite ) || [], function( _, flag ) { - object[ flag ] = true; - }); - return object; -} - -/* - * Create a callback list using the following parameters: - * - * options: an optional list of space-separated options that will change how - * the callback list behaves or a more traditional option object - * - * By default a callback list will act like an event callback list and can be - * "fired" multiple times. - * - * Possible options: - * - * once: will ensure the callback list can only be fired once (like a Deferred) - * - * memory: will keep track of previous values and will call any callback added - * after the list has been fired right away with the latest "memorized" - * values (like a Deferred) - * - * unique: will ensure a callback can only be added once (no duplicate in the list) - * - * stopOnFalse: interrupt callings when a callback returns false - * - */ -jQuery.Callbacks = function( options ) { - - // Convert options from String-formatted to Object-formatted if needed - // (we check in cache first) - options = typeof options === "string" ? - ( optionsCache[ options ] || createOptions( options ) ) : - jQuery.extend( {}, options ); - - var // Flag to know if list is currently firing - firing, - // Last fire value (for non-forgettable lists) - memory, - // Flag to know if list was already fired - fired, - // End of the loop when firing - firingLength, - // Index of currently firing callback (modified by remove if needed) - firingIndex, - // First callback to fire (used internally by add and fireWith) - firingStart, - // Actual callback list - list = [], - // Stack of fire calls for repeatable lists - stack = !options.once && [], - // Fire callbacks - fire = function( data ) { - memory = options.memory && data; - fired = true; - firingIndex = firingStart || 0; - firingStart = 0; - firingLength = list.length; - firing = true; - for ( ; list && firingIndex < firingLength; firingIndex++ ) { - if ( list[ firingIndex ].apply( data[ 0 ], data[ 1 ] ) === false && options.stopOnFalse ) { - memory = false; // To prevent further calls using add - break; - } - } - firing = false; - if ( list ) { - if ( stack ) { - if ( stack.length ) { - fire( stack.shift() ); - } - } else if ( memory ) { - list = []; - } else { - self.disable(); - } - } - }, - // Actual Callbacks object - self = { - // Add a callback or a collection of callbacks to the list - add: function() { - if ( list ) { - // First, we save the current length - var start = list.length; - (function add( args ) { - jQuery.each( args, function( _, arg ) { - var type = jQuery.type( arg ); - if ( type === "function" ) { - if ( !options.unique || !self.has( arg ) ) { - list.push( arg ); - } - } else if ( arg && arg.length && type !== "string" ) { - // Inspect recursively - add( arg ); - } - }); - })( arguments ); - // Do we need to add the callbacks to the - // current firing batch? - if ( firing ) { - firingLength = list.length; - // With memory, if we're not firing then - // we should call right away - } else if ( memory ) { - firingStart = start; - fire( memory ); - } - } - return this; - }, - // Remove a callback from the list - remove: function() { - if ( list ) { - jQuery.each( arguments, function( _, arg ) { - var index; - while ( ( index = jQuery.inArray( arg, list, index ) ) > -1 ) { - list.splice( index, 1 ); - // Handle firing indexes - if ( firing ) { - if ( index <= firingLength ) { - firingLength--; - } - if ( index <= firingIndex ) { - firingIndex--; - } - } - } - }); - } - return this; - }, - // Check if a given callback is in the list. - // If no argument is given, return whether or not list has callbacks attached. - has: function( fn ) { - return fn ? jQuery.inArray( fn, list ) > -1 : !!( list && list.length ); - }, - // Remove all callbacks from the list - empty: function() { - list = []; - firingLength = 0; - return this; - }, - // Have the list do nothing anymore - disable: function() { - list = stack = memory = undefined; - return this; - }, - // Is it disabled? - disabled: function() { - return !list; - }, - // Lock the list in its current state - lock: function() { - stack = undefined; - if ( !memory ) { - self.disable(); - } - return this; - }, - // Is it locked? - locked: function() { - return !stack; - }, - // Call all callbacks with the given context and arguments - fireWith: function( context, args ) { - if ( list && ( !fired || stack ) ) { - args = args || []; - args = [ context, args.slice ? args.slice() : args ]; - if ( firing ) { - stack.push( args ); - } else { - fire( args ); - } - } - return this; - }, - // Call all the callbacks with the given arguments - fire: function() { - self.fireWith( this, arguments ); - return this; - }, - // To know if the callbacks have already been called at least once - fired: function() { - return !!fired; - } - }; - - return self; -}; - - -jQuery.extend({ - - Deferred: function( func ) { - var tuples = [ - // action, add listener, listener list, final state - [ "resolve", "done", jQuery.Callbacks("once memory"), "resolved" ], - [ "reject", "fail", jQuery.Callbacks("once memory"), "rejected" ], - [ "notify", "progress", jQuery.Callbacks("memory") ] - ], - state = "pending", - promise = { - state: function() { - return state; - }, - always: function() { - deferred.done( arguments ).fail( arguments ); - return this; - }, - then: function( /* fnDone, fnFail, fnProgress */ ) { - var fns = arguments; - return jQuery.Deferred(function( newDefer ) { - jQuery.each( tuples, function( i, tuple ) { - var fn = jQuery.isFunction( fns[ i ] ) && fns[ i ]; - // deferred[ done | fail | progress ] for forwarding actions to newDefer - deferred[ tuple[1] ](function() { - var returned = fn && fn.apply( this, arguments ); - if ( returned && jQuery.isFunction( returned.promise ) ) { - returned.promise() - .done( newDefer.resolve ) - .fail( newDefer.reject ) - .progress( newDefer.notify ); - } else { - newDefer[ tuple[ 0 ] + "With" ]( this === promise ? newDefer.promise() : this, fn ? [ returned ] : arguments ); - } - }); - }); - fns = null; - }).promise(); - }, - // Get a promise for this deferred - // If obj is provided, the promise aspect is added to the object - promise: function( obj ) { - return obj != null ? jQuery.extend( obj, promise ) : promise; - } - }, - deferred = {}; - - // Keep pipe for back-compat - promise.pipe = promise.then; - - // Add list-specific methods - jQuery.each( tuples, function( i, tuple ) { - var list = tuple[ 2 ], - stateString = tuple[ 3 ]; - - // promise[ done | fail | progress ] = list.add - promise[ tuple[1] ] = list.add; - - // Handle state - if ( stateString ) { - list.add(function() { - // state = [ resolved | rejected ] - state = stateString; - - // [ reject_list | resolve_list ].disable; progress_list.lock - }, tuples[ i ^ 1 ][ 2 ].disable, tuples[ 2 ][ 2 ].lock ); - } - - // deferred[ resolve | reject | notify ] - deferred[ tuple[0] ] = function() { - deferred[ tuple[0] + "With" ]( this === deferred ? promise : this, arguments ); - return this; - }; - deferred[ tuple[0] + "With" ] = list.fireWith; - }); - - // Make the deferred a promise - promise.promise( deferred ); - - // Call given func if any - if ( func ) { - func.call( deferred, deferred ); - } - - // All done! - return deferred; - }, - - // Deferred helper - when: function( subordinate /* , ..., subordinateN */ ) { - var i = 0, - resolveValues = slice.call( arguments ), - length = resolveValues.length, - - // the count of uncompleted subordinates - remaining = length !== 1 || ( subordinate && jQuery.isFunction( subordinate.promise ) ) ? length : 0, - - // the master Deferred. If resolveValues consist of only a single Deferred, just use that. - deferred = remaining === 1 ? subordinate : jQuery.Deferred(), - - // Update function for both resolve and progress values - updateFunc = function( i, contexts, values ) { - return function( value ) { - contexts[ i ] = this; - values[ i ] = arguments.length > 1 ? slice.call( arguments ) : value; - if ( values === progressValues ) { - deferred.notifyWith( contexts, values ); - - } else if ( !(--remaining) ) { - deferred.resolveWith( contexts, values ); - } - }; - }, - - progressValues, progressContexts, resolveContexts; - - // add listeners to Deferred subordinates; treat others as resolved - if ( length > 1 ) { - progressValues = new Array( length ); - progressContexts = new Array( length ); - resolveContexts = new Array( length ); - for ( ; i < length; i++ ) { - if ( resolveValues[ i ] && jQuery.isFunction( resolveValues[ i ].promise ) ) { - resolveValues[ i ].promise() - .done( updateFunc( i, resolveContexts, resolveValues ) ) - .fail( deferred.reject ) - .progress( updateFunc( i, progressContexts, progressValues ) ); - } else { - --remaining; - } - } - } - - // if we're not waiting on anything, resolve the master - if ( !remaining ) { - deferred.resolveWith( resolveContexts, resolveValues ); - } - - return deferred.promise(); - } -}); - - -// The deferred used on DOM ready -var readyList; - -jQuery.fn.ready = function( fn ) { - // Add the callback - jQuery.ready.promise().done( fn ); - - return this; -}; - -jQuery.extend({ - // Is the DOM ready to be used? Set to true once it occurs. - isReady: false, - - // A counter to track how many items to wait for before - // the ready event fires. See #6781 - readyWait: 1, - - // Hold (or release) the ready event - holdReady: function( hold ) { - if ( hold ) { - jQuery.readyWait++; - } else { - jQuery.ready( true ); - } - }, - - // Handle when the DOM is ready - ready: function( wait ) { - - // Abort if there are pending holds or we're already ready - if ( wait === true ? --jQuery.readyWait : jQuery.isReady ) { - return; - } - - // Make sure body exists, at least, in case IE gets a little overzealous (ticket #5443). - if ( !document.body ) { - return setTimeout( jQuery.ready ); - } - - // Remember that the DOM is ready - jQuery.isReady = true; - - // If a normal DOM Ready event fired, decrement, and wait if need be - if ( wait !== true && --jQuery.readyWait > 0 ) { - return; - } - - // If there are functions bound, to execute - readyList.resolveWith( document, [ jQuery ] ); - - // Trigger any bound ready events - if ( jQuery.fn.triggerHandler ) { - jQuery( document ).triggerHandler( "ready" ); - jQuery( document ).off( "ready" ); - } - } -}); - -/** - * Clean-up method for dom ready events - */ -function detach() { - if ( document.addEventListener ) { - document.removeEventListener( "DOMContentLoaded", completed, false ); - window.removeEventListener( "load", completed, false ); - - } else { - document.detachEvent( "onreadystatechange", completed ); - window.detachEvent( "onload", completed ); - } -} - -/** - * The ready event handler and self cleanup method - */ -function completed() { - // readyState === "complete" is good enough for us to call the dom ready in oldIE - if ( document.addEventListener || event.type === "load" || document.readyState === "complete" ) { - detach(); - jQuery.ready(); - } -} - -jQuery.ready.promise = function( obj ) { - if ( !readyList ) { - - readyList = jQuery.Deferred(); - - // Catch cases where $(document).ready() is called after the browser event has already occurred. - // we once tried to use readyState "interactive" here, but it caused issues like the one - // discovered by ChrisS here: http://bugs.jquery.com/ticket/12282#comment:15 - if ( document.readyState === "complete" ) { - // Handle it asynchronously to allow scripts the opportunity to delay ready - setTimeout( jQuery.ready ); - - // Standards-based browsers support DOMContentLoaded - } else if ( document.addEventListener ) { - // Use the handy event callback - document.addEventListener( "DOMContentLoaded", completed, false ); - - // A fallback to window.onload, that will always work - window.addEventListener( "load", completed, false ); - - // If IE event model is used - } else { - // Ensure firing before onload, maybe late but safe also for iframes - document.attachEvent( "onreadystatechange", completed ); - - // A fallback to window.onload, that will always work - window.attachEvent( "onload", completed ); - - // If IE and not a frame - // continually check to see if the document is ready - var top = false; - - try { - top = window.frameElement == null && document.documentElement; - } catch(e) {} - - if ( top && top.doScroll ) { - (function doScrollCheck() { - if ( !jQuery.isReady ) { - - try { - // Use the trick by Diego Perini - // http://javascript.nwbox.com/IEContentLoaded/ - top.doScroll("left"); - } catch(e) { - return setTimeout( doScrollCheck, 50 ); - } - - // detach all dom ready events - detach(); - - // and execute any waiting functions - jQuery.ready(); - } - })(); - } - } - } - return readyList.promise( obj ); -}; - - -var strundefined = typeof undefined; - - - -// Support: IE<9 -// Iteration over object's inherited properties before its own -var i; -for ( i in jQuery( support ) ) { - break; -} -support.ownLast = i !== "0"; - -// Note: most support tests are defined in their respective modules. -// false until the test is run -support.inlineBlockNeedsLayout = false; - -// Execute ASAP in case we need to set body.style.zoom -jQuery(function() { - // Minified: var a,b,c,d - var val, div, body, container; - - body = document.getElementsByTagName( "body" )[ 0 ]; - if ( !body || !body.style ) { - // Return for frameset docs that don't have a body - return; - } - - // Setup - div = document.createElement( "div" ); - container = document.createElement( "div" ); - container.style.cssText = "position:absolute;border:0;width:0;height:0;top:0;left:-9999px"; - body.appendChild( container ).appendChild( div ); - - if ( typeof div.style.zoom !== strundefined ) { - // Support: IE<8 - // Check if natively block-level elements act like inline-block - // elements when setting their display to 'inline' and giving - // them layout - div.style.cssText = "display:inline;margin:0;border:0;padding:1px;width:1px;zoom:1"; - - support.inlineBlockNeedsLayout = val = div.offsetWidth === 3; - if ( val ) { - // Prevent IE 6 from affecting layout for positioned elements #11048 - // Prevent IE from shrinking the body in IE 7 mode #12869 - // Support: IE<8 - body.style.zoom = 1; - } - } - - body.removeChild( container ); -}); - - - - -(function() { - var div = document.createElement( "div" ); - - // Execute the test only if not already executed in another module. - if (support.deleteExpando == null) { - // Support: IE<9 - support.deleteExpando = true; - try { - delete div.test; - } catch( e ) { - support.deleteExpando = false; - } - } - - // Null elements to avoid leaks in IE. - div = null; -})(); - - -/** - * Determines whether an object can have data - */ -jQuery.acceptData = function( elem ) { - var noData = jQuery.noData[ (elem.nodeName + " ").toLowerCase() ], - nodeType = +elem.nodeType || 1; - - // Do not set data on non-element DOM nodes because it will not be cleared (#8335). - return nodeType !== 1 && nodeType !== 9 ? - false : - - // Nodes accept data unless otherwise specified; rejection can be conditional - !noData || noData !== true && elem.getAttribute("classid") === noData; -}; - - -var rbrace = /^(?:\{[\w\W]*\}|\[[\w\W]*\])$/, - rmultiDash = /([A-Z])/g; - -function dataAttr( elem, key, data ) { - // If nothing was found internally, try to fetch any - // data from the HTML5 data-* attribute - if ( data === undefined && elem.nodeType === 1 ) { - - var name = "data-" + key.replace( rmultiDash, "-$1" ).toLowerCase(); - - data = elem.getAttribute( name ); - - if ( typeof data === "string" ) { - try { - data = data === "true" ? true : - data === "false" ? false : - data === "null" ? null : - // Only convert to a number if it doesn't change the string - +data + "" === data ? +data : - rbrace.test( data ) ? jQuery.parseJSON( data ) : - data; - } catch( e ) {} - - // Make sure we set the data so it isn't changed later - jQuery.data( elem, key, data ); - - } else { - data = undefined; - } - } - - return data; -} - -// checks a cache object for emptiness -function isEmptyDataObject( obj ) { - var name; - for ( name in obj ) { - - // if the public data object is empty, the private is still empty - if ( name === "data" && jQuery.isEmptyObject( obj[name] ) ) { - continue; - } - if ( name !== "toJSON" ) { - return false; - } - } - - return true; -} - -function internalData( elem, name, data, pvt /* Internal Use Only */ ) { - if ( !jQuery.acceptData( elem ) ) { - return; - } - - var ret, thisCache, - internalKey = jQuery.expando, - - // We have to handle DOM nodes and JS objects differently because IE6-7 - // can't GC object references properly across the DOM-JS boundary - isNode = elem.nodeType, - - // Only DOM nodes need the global jQuery cache; JS object data is - // attached directly to the object so GC can occur automatically - cache = isNode ? jQuery.cache : elem, - - // Only defining an ID for JS objects if its cache already exists allows - // the code to shortcut on the same path as a DOM node with no cache - id = isNode ? elem[ internalKey ] : elem[ internalKey ] && internalKey; - - // Avoid doing any more work than we need to when trying to get data on an - // object that has no data at all - if ( (!id || !cache[id] || (!pvt && !cache[id].data)) && data === undefined && typeof name === "string" ) { - return; - } - - if ( !id ) { - // Only DOM nodes need a new unique ID for each element since their data - // ends up in the global cache - if ( isNode ) { - id = elem[ internalKey ] = deletedIds.pop() || jQuery.guid++; - } else { - id = internalKey; - } - } - - if ( !cache[ id ] ) { - // Avoid exposing jQuery metadata on plain JS objects when the object - // is serialized using JSON.stringify - cache[ id ] = isNode ? {} : { toJSON: jQuery.noop }; - } - - // An object can be passed to jQuery.data instead of a key/value pair; this gets - // shallow copied over onto the existing cache - if ( typeof name === "object" || typeof name === "function" ) { - if ( pvt ) { - cache[ id ] = jQuery.extend( cache[ id ], name ); - } else { - cache[ id ].data = jQuery.extend( cache[ id ].data, name ); - } - } - - thisCache = cache[ id ]; - - // jQuery data() is stored in a separate object inside the object's internal data - // cache in order to avoid key collisions between internal data and user-defined - // data. - if ( !pvt ) { - if ( !thisCache.data ) { - thisCache.data = {}; - } - - thisCache = thisCache.data; - } - - if ( data !== undefined ) { - thisCache[ jQuery.camelCase( name ) ] = data; - } - - // Check for both converted-to-camel and non-converted data property names - // If a data property was specified - if ( typeof name === "string" ) { - - // First Try to find as-is property data - ret = thisCache[ name ]; - - // Test for null|undefined property data - if ( ret == null ) { - - // Try to find the camelCased property - ret = thisCache[ jQuery.camelCase( name ) ]; - } - } else { - ret = thisCache; - } - - return ret; -} - -function internalRemoveData( elem, name, pvt ) { - if ( !jQuery.acceptData( elem ) ) { - return; - } - - var thisCache, i, - isNode = elem.nodeType, - - // See jQuery.data for more information - cache = isNode ? jQuery.cache : elem, - id = isNode ? elem[ jQuery.expando ] : jQuery.expando; - - // If there is already no cache entry for this object, there is no - // purpose in continuing - if ( !cache[ id ] ) { - return; - } - - if ( name ) { - - thisCache = pvt ? cache[ id ] : cache[ id ].data; - - if ( thisCache ) { - - // Support array or space separated string names for data keys - if ( !jQuery.isArray( name ) ) { - - // try the string as a key before any manipulation - if ( name in thisCache ) { - name = [ name ]; - } else { - - // split the camel cased version by spaces unless a key with the spaces exists - name = jQuery.camelCase( name ); - if ( name in thisCache ) { - name = [ name ]; - } else { - name = name.split(" "); - } - } - } else { - // If "name" is an array of keys... - // When data is initially created, via ("key", "val") signature, - // keys will be converted to camelCase. - // Since there is no way to tell _how_ a key was added, remove - // both plain key and camelCase key. #12786 - // This will only penalize the array argument path. - name = name.concat( jQuery.map( name, jQuery.camelCase ) ); - } - - i = name.length; - while ( i-- ) { - delete thisCache[ name[i] ]; - } - - // If there is no data left in the cache, we want to continue - // and let the cache object itself get destroyed - if ( pvt ? !isEmptyDataObject(thisCache) : !jQuery.isEmptyObject(thisCache) ) { - return; - } - } - } - - // See jQuery.data for more information - if ( !pvt ) { - delete cache[ id ].data; - - // Don't destroy the parent cache unless the internal data object - // had been the only thing left in it - if ( !isEmptyDataObject( cache[ id ] ) ) { - return; - } - } - - // Destroy the cache - if ( isNode ) { - jQuery.cleanData( [ elem ], true ); - - // Use delete when supported for expandos or `cache` is not a window per isWindow (#10080) - /* jshint eqeqeq: false */ - } else if ( support.deleteExpando || cache != cache.window ) { - /* jshint eqeqeq: true */ - delete cache[ id ]; - - // When all else fails, null - } else { - cache[ id ] = null; - } -} - -jQuery.extend({ - cache: {}, - - // The following elements (space-suffixed to avoid Object.prototype collisions) - // throw uncatchable exceptions if you attempt to set expando properties - noData: { - "applet ": true, - "embed ": true, - // ...but Flash objects (which have this classid) *can* handle expandos - "object ": "clsid:D27CDB6E-AE6D-11cf-96B8-444553540000" - }, - - hasData: function( elem ) { - elem = elem.nodeType ? jQuery.cache[ elem[jQuery.expando] ] : elem[ jQuery.expando ]; - return !!elem && !isEmptyDataObject( elem ); - }, - - data: function( elem, name, data ) { - return internalData( elem, name, data ); - }, - - removeData: function( elem, name ) { - return internalRemoveData( elem, name ); - }, - - // For internal use only. - _data: function( elem, name, data ) { - return internalData( elem, name, data, true ); - }, - - _removeData: function( elem, name ) { - return internalRemoveData( elem, name, true ); - } -}); - -jQuery.fn.extend({ - data: function( key, value ) { - var i, name, data, - elem = this[0], - attrs = elem && elem.attributes; - - // Special expections of .data basically thwart jQuery.access, - // so implement the relevant behavior ourselves - - // Gets all values - if ( key === undefined ) { - if ( this.length ) { - data = jQuery.data( elem ); - - if ( elem.nodeType === 1 && !jQuery._data( elem, "parsedAttrs" ) ) { - i = attrs.length; - while ( i-- ) { - - // Support: IE11+ - // The attrs elements can be null (#14894) - if ( attrs[ i ] ) { - name = attrs[ i ].name; - if ( name.indexOf( "data-" ) === 0 ) { - name = jQuery.camelCase( name.slice(5) ); - dataAttr( elem, name, data[ name ] ); - } - } - } - jQuery._data( elem, "parsedAttrs", true ); - } - } - - return data; - } - - // Sets multiple values - if ( typeof key === "object" ) { - return this.each(function() { - jQuery.data( this, key ); - }); - } - - return arguments.length > 1 ? - - // Sets one value - this.each(function() { - jQuery.data( this, key, value ); - }) : - - // Gets one value - // Try to fetch any internally stored data first - elem ? dataAttr( elem, key, jQuery.data( elem, key ) ) : undefined; - }, - - removeData: function( key ) { - return this.each(function() { - jQuery.removeData( this, key ); - }); - } -}); - - -jQuery.extend({ - queue: function( elem, type, data ) { - var queue; - - if ( elem ) { - type = ( type || "fx" ) + "queue"; - queue = jQuery._data( elem, type ); - - // Speed up dequeue by getting out quickly if this is just a lookup - if ( data ) { - if ( !queue || jQuery.isArray(data) ) { - queue = jQuery._data( elem, type, jQuery.makeArray(data) ); - } else { - queue.push( data ); - } - } - return queue || []; - } - }, - - dequeue: function( elem, type ) { - type = type || "fx"; - - var queue = jQuery.queue( elem, type ), - startLength = queue.length, - fn = queue.shift(), - hooks = jQuery._queueHooks( elem, type ), - next = function() { - jQuery.dequeue( elem, type ); - }; - - // If the fx queue is dequeued, always remove the progress sentinel - if ( fn === "inprogress" ) { - fn = queue.shift(); - startLength--; - } - - if ( fn ) { - - // Add a progress sentinel to prevent the fx queue from being - // automatically dequeued - if ( type === "fx" ) { - queue.unshift( "inprogress" ); - } - - // clear up the last queue stop function - delete hooks.stop; - fn.call( elem, next, hooks ); - } - - if ( !startLength && hooks ) { - hooks.empty.fire(); - } - }, - - // not intended for public consumption - generates a queueHooks object, or returns the current one - _queueHooks: function( elem, type ) { - var key = type + "queueHooks"; - return jQuery._data( elem, key ) || jQuery._data( elem, key, { - empty: jQuery.Callbacks("once memory").add(function() { - jQuery._removeData( elem, type + "queue" ); - jQuery._removeData( elem, key ); - }) - }); - } -}); - -jQuery.fn.extend({ - queue: function( type, data ) { - var setter = 2; - - if ( typeof type !== "string" ) { - data = type; - type = "fx"; - setter--; - } - - if ( arguments.length < setter ) { - return jQuery.queue( this[0], type ); - } - - return data === undefined ? - this : - this.each(function() { - var queue = jQuery.queue( this, type, data ); - - // ensure a hooks for this queue - jQuery._queueHooks( this, type ); - - if ( type === "fx" && queue[0] !== "inprogress" ) { - jQuery.dequeue( this, type ); - } - }); - }, - dequeue: function( type ) { - return this.each(function() { - jQuery.dequeue( this, type ); - }); - }, - clearQueue: function( type ) { - return this.queue( type || "fx", [] ); - }, - // Get a promise resolved when queues of a certain type - // are emptied (fx is the type by default) - promise: function( type, obj ) { - var tmp, - count = 1, - defer = jQuery.Deferred(), - elements = this, - i = this.length, - resolve = function() { - if ( !( --count ) ) { - defer.resolveWith( elements, [ elements ] ); - } - }; - - if ( typeof type !== "string" ) { - obj = type; - type = undefined; - } - type = type || "fx"; - - while ( i-- ) { - tmp = jQuery._data( elements[ i ], type + "queueHooks" ); - if ( tmp && tmp.empty ) { - count++; - tmp.empty.add( resolve ); - } - } - resolve(); - return defer.promise( obj ); - } -}); -var pnum = (/[+-]?(?:\d*\.|)\d+(?:[eE][+-]?\d+|)/).source; - -var cssExpand = [ "Top", "Right", "Bottom", "Left" ]; - -var isHidden = function( elem, el ) { - // isHidden might be called from jQuery#filter function; - // in that case, element will be second argument - elem = el || elem; - return jQuery.css( elem, "display" ) === "none" || !jQuery.contains( elem.ownerDocument, elem ); - }; - - - -// Multifunctional method to get and set values of a collection -// The value/s can optionally be executed if it's a function -var access = jQuery.access = function( elems, fn, key, value, chainable, emptyGet, raw ) { - var i = 0, - length = elems.length, - bulk = key == null; - - // Sets many values - if ( jQuery.type( key ) === "object" ) { - chainable = true; - for ( i in key ) { - jQuery.access( elems, fn, i, key[i], true, emptyGet, raw ); - } - - // Sets one value - } else if ( value !== undefined ) { - chainable = true; - - if ( !jQuery.isFunction( value ) ) { - raw = true; - } - - if ( bulk ) { - // Bulk operations run against the entire set - if ( raw ) { - fn.call( elems, value ); - fn = null; - - // ...except when executing function values - } else { - bulk = fn; - fn = function( elem, key, value ) { - return bulk.call( jQuery( elem ), value ); - }; - } - } - - if ( fn ) { - for ( ; i < length; i++ ) { - fn( elems[i], key, raw ? value : value.call( elems[i], i, fn( elems[i], key ) ) ); - } - } - } - - return chainable ? - elems : - - // Gets - bulk ? - fn.call( elems ) : - length ? fn( elems[0], key ) : emptyGet; -}; -var rcheckableType = (/^(?:checkbox|radio)$/i); - - - -(function() { - // Minified: var a,b,c - var input = document.createElement( "input" ), - div = document.createElement( "div" ), - fragment = document.createDocumentFragment(); - - // Setup - div.innerHTML = " <link/><table></table><a href='/a'>a</a><input type='checkbox'/>"; - - // IE strips leading whitespace when .innerHTML is used - support.leadingWhitespace = div.firstChild.nodeType === 3; - - // Make sure that tbody elements aren't automatically inserted - // IE will insert them into empty tables - support.tbody = !div.getElementsByTagName( "tbody" ).length; - - // Make sure that link elements get serialized correctly by innerHTML - // This requires a wrapper element in IE - support.htmlSerialize = !!div.getElementsByTagName( "link" ).length; - - // Makes sure cloning an html5 element does not cause problems - // Where outerHTML is undefined, this still works - support.html5Clone = - document.createElement( "nav" ).cloneNode( true ).outerHTML !== "<:nav></:nav>"; - - // Check if a disconnected checkbox will retain its checked - // value of true after appended to the DOM (IE6/7) - input.type = "checkbox"; - input.checked = true; - fragment.appendChild( input ); - support.appendChecked = input.checked; - - // Make sure textarea (and checkbox) defaultValue is properly cloned - // Support: IE6-IE11+ - div.innerHTML = "<textarea>x</textarea>"; - support.noCloneChecked = !!div.cloneNode( true ).lastChild.defaultValue; - - // #11217 - WebKit loses check when the name is after the checked attribute - fragment.appendChild( div ); - div.innerHTML = "<input type='radio' checked='checked' name='t'/>"; - - // Support: Safari 5.1, iOS 5.1, Android 4.x, Android 2.3 - // old WebKit doesn't clone checked state correctly in fragments - support.checkClone = div.cloneNode( true ).cloneNode( true ).lastChild.checked; - - // Support: IE<9 - // Opera does not clone events (and typeof div.attachEvent === undefined). - // IE9-10 clones events bound via attachEvent, but they don't trigger with .click() - support.noCloneEvent = true; - if ( div.attachEvent ) { - div.attachEvent( "onclick", function() { - support.noCloneEvent = false; - }); - - div.cloneNode( true ).click(); - } - - // Execute the test only if not already executed in another module. - if (support.deleteExpando == null) { - // Support: IE<9 - support.deleteExpando = true; - try { - delete div.test; - } catch( e ) { - support.deleteExpando = false; - } - } -})(); - - -(function() { - var i, eventName, - div = document.createElement( "div" ); - - // Support: IE<9 (lack submit/change bubble), Firefox 23+ (lack focusin event) - for ( i in { submit: true, change: true, focusin: true }) { - eventName = "on" + i; - - if ( !(support[ i + "Bubbles" ] = eventName in window) ) { - // Beware of CSP restrictions (https://developer.mozilla.org/en/Security/CSP) - div.setAttribute( eventName, "t" ); - support[ i + "Bubbles" ] = div.attributes[ eventName ].expando === false; - } - } - - // Null elements to avoid leaks in IE. - div = null; -})(); - - -var rformElems = /^(?:input|select|textarea)$/i, - rkeyEvent = /^key/, - rmouseEvent = /^(?:mouse|pointer|contextmenu)|click/, - rfocusMorph = /^(?:focusinfocus|focusoutblur)$/, - rtypenamespace = /^([^.]*)(?:\.(.+)|)$/; - -function returnTrue() { - return true; -} - -function returnFalse() { - return false; -} - -function safeActiveElement() { - try { - return document.activeElement; - } catch ( err ) { } -} - -/* - * Helper functions for managing events -- not part of the public interface. - * Props to Dean Edwards' addEvent library for many of the ideas. - */ -jQuery.event = { - - global: {}, - - add: function( elem, types, handler, data, selector ) { - var tmp, events, t, handleObjIn, - special, eventHandle, handleObj, - handlers, type, namespaces, origType, - elemData = jQuery._data( elem ); - - // Don't attach events to noData or text/comment nodes (but allow plain objects) - if ( !elemData ) { - return; - } - - // Caller can pass in an object of custom data in lieu of the handler - if ( handler.handler ) { - handleObjIn = handler; - handler = handleObjIn.handler; - selector = handleObjIn.selector; - } - - // Make sure that the handler has a unique ID, used to find/remove it later - if ( !handler.guid ) { - handler.guid = jQuery.guid++; - } - - // Init the element's event structure and main handler, if this is the first - if ( !(events = elemData.events) ) { - events = elemData.events = {}; - } - if ( !(eventHandle = elemData.handle) ) { - eventHandle = elemData.handle = function( e ) { - // Discard the second event of a jQuery.event.trigger() and - // when an event is called after a page has unloaded - return typeof jQuery !== strundefined && (!e || jQuery.event.triggered !== e.type) ? - jQuery.event.dispatch.apply( eventHandle.elem, arguments ) : - undefined; - }; - // Add elem as a property of the handle fn to prevent a memory leak with IE non-native events - eventHandle.elem = elem; - } - - // Handle multiple events separated by a space - types = ( types || "" ).match( rnotwhite ) || [ "" ]; - t = types.length; - while ( t-- ) { - tmp = rtypenamespace.exec( types[t] ) || []; - type = origType = tmp[1]; - namespaces = ( tmp[2] || "" ).split( "." ).sort(); - - // There *must* be a type, no attaching namespace-only handlers - if ( !type ) { - continue; - } - - // If event changes its type, use the special event handlers for the changed type - special = jQuery.event.special[ type ] || {}; - - // If selector defined, determine special event api type, otherwise given type - type = ( selector ? special.delegateType : special.bindType ) || type; - - // Update special based on newly reset type - special = jQuery.event.special[ type ] || {}; - - // handleObj is passed to all event handlers - handleObj = jQuery.extend({ - type: type, - origType: origType, - data: data, - handler: handler, - guid: handler.guid, - selector: selector, - needsContext: selector && jQuery.expr.match.needsContext.test( selector ), - namespace: namespaces.join(".") - }, handleObjIn ); - - // Init the event handler queue if we're the first - if ( !(handlers = events[ type ]) ) { - handlers = events[ type ] = []; - handlers.delegateCount = 0; - - // Only use addEventListener/attachEvent if the special events handler returns false - if ( !special.setup || special.setup.call( elem, data, namespaces, eventHandle ) === false ) { - // Bind the global event handler to the element - if ( elem.addEventListener ) { - elem.addEventListener( type, eventHandle, false ); - - } else if ( elem.attachEvent ) { - elem.attachEvent( "on" + type, eventHandle ); - } - } - } - - if ( special.add ) { - special.add.call( elem, handleObj ); - - if ( !handleObj.handler.guid ) { - handleObj.handler.guid = handler.guid; - } - } - - // Add to the element's handler list, delegates in front - if ( selector ) { - handlers.splice( handlers.delegateCount++, 0, handleObj ); - } else { - handlers.push( handleObj ); - } - - // Keep track of which events have ever been used, for event optimization - jQuery.event.global[ type ] = true; - } - - // Nullify elem to prevent memory leaks in IE - elem = null; - }, - - // Detach an event or set of events from an element - remove: function( elem, types, handler, selector, mappedTypes ) { - var j, handleObj, tmp, - origCount, t, events, - special, handlers, type, - namespaces, origType, - elemData = jQuery.hasData( elem ) && jQuery._data( elem ); - - if ( !elemData || !(events = elemData.events) ) { - return; - } - - // Once for each type.namespace in types; type may be omitted - types = ( types || "" ).match( rnotwhite ) || [ "" ]; - t = types.length; - while ( t-- ) { - tmp = rtypenamespace.exec( types[t] ) || []; - type = origType = tmp[1]; - namespaces = ( tmp[2] || "" ).split( "." ).sort(); - - // Unbind all events (on this namespace, if provided) for the element - if ( !type ) { - for ( type in events ) { - jQuery.event.remove( elem, type + types[ t ], handler, selector, true ); - } - continue; - } - - special = jQuery.event.special[ type ] || {}; - type = ( selector ? special.delegateType : special.bindType ) || type; - handlers = events[ type ] || []; - tmp = tmp[2] && new RegExp( "(^|\\.)" + namespaces.join("\\.(?:.*\\.|)") + "(\\.|$)" ); - - // Remove matching events - origCount = j = handlers.length; - while ( j-- ) { - handleObj = handlers[ j ]; - - if ( ( mappedTypes || origType === handleObj.origType ) && - ( !handler || handler.guid === handleObj.guid ) && - ( !tmp || tmp.test( handleObj.namespace ) ) && - ( !selector || selector === handleObj.selector || selector === "**" && handleObj.selector ) ) { - handlers.splice( j, 1 ); - - if ( handleObj.selector ) { - handlers.delegateCount--; - } - if ( special.remove ) { - special.remove.call( elem, handleObj ); - } - } - } - - // Remove generic event handler if we removed something and no more handlers exist - // (avoids potential for endless recursion during removal of special event handlers) - if ( origCount && !handlers.length ) { - if ( !special.teardown || special.teardown.call( elem, namespaces, elemData.handle ) === false ) { - jQuery.removeEvent( elem, type, elemData.handle ); - } - - delete events[ type ]; - } - } - - // Remove the expando if it's no longer used - if ( jQuery.isEmptyObject( events ) ) { - delete elemData.handle; - - // removeData also checks for emptiness and clears the expando if empty - // so use it instead of delete - jQuery._removeData( elem, "events" ); - } - }, - - trigger: function( event, data, elem, onlyHandlers ) { - var handle, ontype, cur, - bubbleType, special, tmp, i, - eventPath = [ elem || document ], - type = hasOwn.call( event, "type" ) ? event.type : event, - namespaces = hasOwn.call( event, "namespace" ) ? event.namespace.split(".") : []; - - cur = tmp = elem = elem || document; - - // Don't do events on text and comment nodes - if ( elem.nodeType === 3 || elem.nodeType === 8 ) { - return; - } - - // focus/blur morphs to focusin/out; ensure we're not firing them right now - if ( rfocusMorph.test( type + jQuery.event.triggered ) ) { - return; - } - - if ( type.indexOf(".") >= 0 ) { - // Namespaced trigger; create a regexp to match event type in handle() - namespaces = type.split("."); - type = namespaces.shift(); - namespaces.sort(); - } - ontype = type.indexOf(":") < 0 && "on" + type; - - // Caller can pass in a jQuery.Event object, Object, or just an event type string - event = event[ jQuery.expando ] ? - event : - new jQuery.Event( type, typeof event === "object" && event ); - - // Trigger bitmask: & 1 for native handlers; & 2 for jQuery (always true) - event.isTrigger = onlyHandlers ? 2 : 3; - event.namespace = namespaces.join("."); - event.namespace_re = event.namespace ? - new RegExp( "(^|\\.)" + namespaces.join("\\.(?:.*\\.|)") + "(\\.|$)" ) : - null; - - // Clean up the event in case it is being reused - event.result = undefined; - if ( !event.target ) { - event.target = elem; - } - - // Clone any incoming data and prepend the event, creating the handler arg list - data = data == null ? - [ event ] : - jQuery.makeArray( data, [ event ] ); - - // Allow special events to draw outside the lines - special = jQuery.event.special[ type ] || {}; - if ( !onlyHandlers && special.trigger && special.trigger.apply( elem, data ) === false ) { - return; - } - - // Determine event propagation path in advance, per W3C events spec (#9951) - // Bubble up to document, then to window; watch for a global ownerDocument var (#9724) - if ( !onlyHandlers && !special.noBubble && !jQuery.isWindow( elem ) ) { - - bubbleType = special.delegateType || type; - if ( !rfocusMorph.test( bubbleType + type ) ) { - cur = cur.parentNode; - } - for ( ; cur; cur = cur.parentNode ) { - eventPath.push( cur ); - tmp = cur; - } - - // Only add window if we got to document (e.g., not plain obj or detached DOM) - if ( tmp === (elem.ownerDocument || document) ) { - eventPath.push( tmp.defaultView || tmp.parentWindow || window ); - } - } - - // Fire handlers on the event path - i = 0; - while ( (cur = eventPath[i++]) && !event.isPropagationStopped() ) { - - event.type = i > 1 ? - bubbleType : - special.bindType || type; - - // jQuery handler - handle = ( jQuery._data( cur, "events" ) || {} )[ event.type ] && jQuery._data( cur, "handle" ); - if ( handle ) { - handle.apply( cur, data ); - } - - // Native handler - handle = ontype && cur[ ontype ]; - if ( handle && handle.apply && jQuery.acceptData( cur ) ) { - event.result = handle.apply( cur, data ); - if ( event.result === false ) { - event.preventDefault(); - } - } - } - event.type = type; - - // If nobody prevented the default action, do it now - if ( !onlyHandlers && !event.isDefaultPrevented() ) { - - if ( (!special._default || special._default.apply( eventPath.pop(), data ) === false) && - jQuery.acceptData( elem ) ) { - - // Call a native DOM method on the target with the same name name as the event. - // Can't use an .isFunction() check here because IE6/7 fails that test. - // Don't do default actions on window, that's where global variables be (#6170) - if ( ontype && elem[ type ] && !jQuery.isWindow( elem ) ) { - - // Don't re-trigger an onFOO event when we call its FOO() method - tmp = elem[ ontype ]; - - if ( tmp ) { - elem[ ontype ] = null; - } - - // Prevent re-triggering of the same event, since we already bubbled it above - jQuery.event.triggered = type; - try { - elem[ type ](); - } catch ( e ) { - // IE<9 dies on focus/blur to hidden element (#1486,#12518) - // only reproducible on winXP IE8 native, not IE9 in IE8 mode - } - jQuery.event.triggered = undefined; - - if ( tmp ) { - elem[ ontype ] = tmp; - } - } - } - } - - return event.result; - }, - - dispatch: function( event ) { - - // Make a writable jQuery.Event from the native event object - event = jQuery.event.fix( event ); - - var i, ret, handleObj, matched, j, - handlerQueue = [], - args = slice.call( arguments ), - handlers = ( jQuery._data( this, "events" ) || {} )[ event.type ] || [], - special = jQuery.event.special[ event.type ] || {}; - - // Use the fix-ed jQuery.Event rather than the (read-only) native event - args[0] = event; - event.delegateTarget = this; - - // Call the preDispatch hook for the mapped type, and let it bail if desired - if ( special.preDispatch && special.preDispatch.call( this, event ) === false ) { - return; - } - - // Determine handlers - handlerQueue = jQuery.event.handlers.call( this, event, handlers ); - - // Run delegates first; they may want to stop propagation beneath us - i = 0; - while ( (matched = handlerQueue[ i++ ]) && !event.isPropagationStopped() ) { - event.currentTarget = matched.elem; - - j = 0; - while ( (handleObj = matched.handlers[ j++ ]) && !event.isImmediatePropagationStopped() ) { - - // Triggered event must either 1) have no namespace, or - // 2) have namespace(s) a subset or equal to those in the bound event (both can have no namespace). - if ( !event.namespace_re || event.namespace_re.test( handleObj.namespace ) ) { - - event.handleObj = handleObj; - event.data = handleObj.data; - - ret = ( (jQuery.event.special[ handleObj.origType ] || {}).handle || handleObj.handler ) - .apply( matched.elem, args ); - - if ( ret !== undefined ) { - if ( (event.result = ret) === false ) { - event.preventDefault(); - event.stopPropagation(); - } - } - } - } - } - - // Call the postDispatch hook for the mapped type - if ( special.postDispatch ) { - special.postDispatch.call( this, event ); - } - - return event.result; - }, - - handlers: function( event, handlers ) { - var sel, handleObj, matches, i, - handlerQueue = [], - delegateCount = handlers.delegateCount, - cur = event.target; - - // Find delegate handlers - // Black-hole SVG <use> instance trees (#13180) - // Avoid non-left-click bubbling in Firefox (#3861) - if ( delegateCount && cur.nodeType && (!event.button || event.type !== "click") ) { - - /* jshint eqeqeq: false */ - for ( ; cur != this; cur = cur.parentNode || this ) { - /* jshint eqeqeq: true */ - - // Don't check non-elements (#13208) - // Don't process clicks on disabled elements (#6911, #8165, #11382, #11764) - if ( cur.nodeType === 1 && (cur.disabled !== true || event.type !== "click") ) { - matches = []; - for ( i = 0; i < delegateCount; i++ ) { - handleObj = handlers[ i ]; - - // Don't conflict with Object.prototype properties (#13203) - sel = handleObj.selector + " "; - - if ( matches[ sel ] === undefined ) { - matches[ sel ] = handleObj.needsContext ? - jQuery( sel, this ).index( cur ) >= 0 : - jQuery.find( sel, this, null, [ cur ] ).length; - } - if ( matches[ sel ] ) { - matches.push( handleObj ); - } - } - if ( matches.length ) { - handlerQueue.push({ elem: cur, handlers: matches }); - } - } - } - } - - // Add the remaining (directly-bound) handlers - if ( delegateCount < handlers.length ) { - handlerQueue.push({ elem: this, handlers: handlers.slice( delegateCount ) }); - } - - return handlerQueue; - }, - - fix: function( event ) { - if ( event[ jQuery.expando ] ) { - return event; - } - - // Create a writable copy of the event object and normalize some properties - var i, prop, copy, - type = event.type, - originalEvent = event, - fixHook = this.fixHooks[ type ]; - - if ( !fixHook ) { - this.fixHooks[ type ] = fixHook = - rmouseEvent.test( type ) ? this.mouseHooks : - rkeyEvent.test( type ) ? this.keyHooks : - {}; - } - copy = fixHook.props ? this.props.concat( fixHook.props ) : this.props; - - event = new jQuery.Event( originalEvent ); - - i = copy.length; - while ( i-- ) { - prop = copy[ i ]; - event[ prop ] = originalEvent[ prop ]; - } - - // Support: IE<9 - // Fix target property (#1925) - if ( !event.target ) { - event.target = originalEvent.srcElement || document; - } - - // Support: Chrome 23+, Safari? - // Target should not be a text node (#504, #13143) - if ( event.target.nodeType === 3 ) { - event.target = event.target.parentNode; - } - - // Support: IE<9 - // For mouse/key events, metaKey==false if it's undefined (#3368, #11328) - event.metaKey = !!event.metaKey; - - return fixHook.filter ? fixHook.filter( event, originalEvent ) : event; - }, - - // Includes some event props shared by KeyEvent and MouseEvent - props: "altKey bubbles cancelable ctrlKey currentTarget eventPhase metaKey relatedTarget shiftKey target timeStamp view which".split(" "), - - fixHooks: {}, - - keyHooks: { - props: "char charCode key keyCode".split(" "), - filter: function( event, original ) { - - // Add which for key events - if ( event.which == null ) { - event.which = original.charCode != null ? original.charCode : original.keyCode; - } - - return event; - } - }, - - mouseHooks: { - props: "button buttons clientX clientY fromElement offsetX offsetY pageX pageY screenX screenY toElement".split(" "), - filter: function( event, original ) { - var body, eventDoc, doc, - button = original.button, - fromElement = original.fromElement; - - // Calculate pageX/Y if missing and clientX/Y available - if ( event.pageX == null && original.clientX != null ) { - eventDoc = event.target.ownerDocument || document; - doc = eventDoc.documentElement; - body = eventDoc.body; - - event.pageX = original.clientX + ( doc && doc.scrollLeft || body && body.scrollLeft || 0 ) - ( doc && doc.clientLeft || body && body.clientLeft || 0 ); - event.pageY = original.clientY + ( doc && doc.scrollTop || body && body.scrollTop || 0 ) - ( doc && doc.clientTop || body && body.clientTop || 0 ); - } - - // Add relatedTarget, if necessary - if ( !event.relatedTarget && fromElement ) { - event.relatedTarget = fromElement === event.target ? original.toElement : fromElement; - } - - // Add which for click: 1 === left; 2 === middle; 3 === right - // Note: button is not normalized, so don't use it - if ( !event.which && button !== undefined ) { - event.which = ( button & 1 ? 1 : ( button & 2 ? 3 : ( button & 4 ? 2 : 0 ) ) ); - } - - return event; - } - }, - - special: { - load: { - // Prevent triggered image.load events from bubbling to window.load - noBubble: true - }, - focus: { - // Fire native event if possible so blur/focus sequence is correct - trigger: function() { - if ( this !== safeActiveElement() && this.focus ) { - try { - this.focus(); - return false; - } catch ( e ) { - // Support: IE<9 - // If we error on focus to hidden element (#1486, #12518), - // let .trigger() run the handlers - } - } - }, - delegateType: "focusin" - }, - blur: { - trigger: function() { - if ( this === safeActiveElement() && this.blur ) { - this.blur(); - return false; - } - }, - delegateType: "focusout" - }, - click: { - // For checkbox, fire native event so checked state will be right - trigger: function() { - if ( jQuery.nodeName( this, "input" ) && this.type === "checkbox" && this.click ) { - this.click(); - return false; - } - }, - - // For cross-browser consistency, don't fire native .click() on links - _default: function( event ) { - return jQuery.nodeName( event.target, "a" ); - } - }, - - beforeunload: { - postDispatch: function( event ) { - - // Support: Firefox 20+ - // Firefox doesn't alert if the returnValue field is not set. - if ( event.result !== undefined && event.originalEvent ) { - event.originalEvent.returnValue = event.result; - } - } - } - }, - - simulate: function( type, elem, event, bubble ) { - // Piggyback on a donor event to simulate a different one. - // Fake originalEvent to avoid donor's stopPropagation, but if the - // simulated event prevents default then we do the same on the donor. - var e = jQuery.extend( - new jQuery.Event(), - event, - { - type: type, - isSimulated: true, - originalEvent: {} - } - ); - if ( bubble ) { - jQuery.event.trigger( e, null, elem ); - } else { - jQuery.event.dispatch.call( elem, e ); - } - if ( e.isDefaultPrevented() ) { - event.preventDefault(); - } - } -}; - -jQuery.removeEvent = document.removeEventListener ? - function( elem, type, handle ) { - if ( elem.removeEventListener ) { - elem.removeEventListener( type, handle, false ); - } - } : - function( elem, type, handle ) { - var name = "on" + type; - - if ( elem.detachEvent ) { - - // #8545, #7054, preventing memory leaks for custom events in IE6-8 - // detachEvent needed property on element, by name of that event, to properly expose it to GC - if ( typeof elem[ name ] === strundefined ) { - elem[ name ] = null; - } - - elem.detachEvent( name, handle ); - } - }; - -jQuery.Event = function( src, props ) { - // Allow instantiation without the 'new' keyword - if ( !(this instanceof jQuery.Event) ) { - return new jQuery.Event( src, props ); - } - - // Event object - if ( src && src.type ) { - this.originalEvent = src; - this.type = src.type; - - // Events bubbling up the document may have been marked as prevented - // by a handler lower down the tree; reflect the correct value. - this.isDefaultPrevented = src.defaultPrevented || - src.defaultPrevented === undefined && - // Support: IE < 9, Android < 4.0 - src.returnValue === false ? - returnTrue : - returnFalse; - - // Event type - } else { - this.type = src; - } - - // Put explicitly provided properties onto the event object - if ( props ) { - jQuery.extend( this, props ); - } - - // Create a timestamp if incoming event doesn't have one - this.timeStamp = src && src.timeStamp || jQuery.now(); - - // Mark it as fixed - this[ jQuery.expando ] = true; -}; - -// jQuery.Event is based on DOM3 Events as specified by the ECMAScript Language Binding -// http://www.w3.org/TR/2003/WD-DOM-Level-3-Events-20030331/ecma-script-binding.html -jQuery.Event.prototype = { - isDefaultPrevented: returnFalse, - isPropagationStopped: returnFalse, - isImmediatePropagationStopped: returnFalse, - - preventDefault: function() { - var e = this.originalEvent; - - this.isDefaultPrevented = returnTrue; - if ( !e ) { - return; - } - - // If preventDefault exists, run it on the original event - if ( e.preventDefault ) { - e.preventDefault(); - - // Support: IE - // Otherwise set the returnValue property of the original event to false - } else { - e.returnValue = false; - } - }, - stopPropagation: function() { - var e = this.originalEvent; - - this.isPropagationStopped = returnTrue; - if ( !e ) { - return; - } - // If stopPropagation exists, run it on the original event - if ( e.stopPropagation ) { - e.stopPropagation(); - } - - // Support: IE - // Set the cancelBubble property of the original event to true - e.cancelBubble = true; - }, - stopImmediatePropagation: function() { - var e = this.originalEvent; - - this.isImmediatePropagationStopped = returnTrue; - - if ( e && e.stopImmediatePropagation ) { - e.stopImmediatePropagation(); - } - - this.stopPropagation(); - } -}; - -// Create mouseenter/leave events using mouseover/out and event-time checks -jQuery.each({ - mouseenter: "mouseover", - mouseleave: "mouseout", - pointerenter: "pointerover", - pointerleave: "pointerout" -}, function( orig, fix ) { - jQuery.event.special[ orig ] = { - delegateType: fix, - bindType: fix, - - handle: function( event ) { - var ret, - target = this, - related = event.relatedTarget, - handleObj = event.handleObj; - - // For mousenter/leave call the handler if related is outside the target. - // NB: No relatedTarget if the mouse left/entered the browser window - if ( !related || (related !== target && !jQuery.contains( target, related )) ) { - event.type = handleObj.origType; - ret = handleObj.handler.apply( this, arguments ); - event.type = fix; - } - return ret; - } - }; -}); - -// IE submit delegation -if ( !support.submitBubbles ) { - - jQuery.event.special.submit = { - setup: function() { - // Only need this for delegated form submit events - if ( jQuery.nodeName( this, "form" ) ) { - return false; - } - - // Lazy-add a submit handler when a descendant form may potentially be submitted - jQuery.event.add( this, "click._submit keypress._submit", function( e ) { - // Node name check avoids a VML-related crash in IE (#9807) - var elem = e.target, - form = jQuery.nodeName( elem, "input" ) || jQuery.nodeName( elem, "button" ) ? elem.form : undefined; - if ( form && !jQuery._data( form, "submitBubbles" ) ) { - jQuery.event.add( form, "submit._submit", function( event ) { - event._submit_bubble = true; - }); - jQuery._data( form, "submitBubbles", true ); - } - }); - // return undefined since we don't need an event listener - }, - - postDispatch: function( event ) { - // If form was submitted by the user, bubble the event up the tree - if ( event._submit_bubble ) { - delete event._submit_bubble; - if ( this.parentNode && !event.isTrigger ) { - jQuery.event.simulate( "submit", this.parentNode, event, true ); - } - } - }, - - teardown: function() { - // Only need this for delegated form submit events - if ( jQuery.nodeName( this, "form" ) ) { - return false; - } - - // Remove delegated handlers; cleanData eventually reaps submit handlers attached above - jQuery.event.remove( this, "._submit" ); - } - }; -} - -// IE change delegation and checkbox/radio fix -if ( !support.changeBubbles ) { - - jQuery.event.special.change = { - - setup: function() { - - if ( rformElems.test( this.nodeName ) ) { - // IE doesn't fire change on a check/radio until blur; trigger it on click - // after a propertychange. Eat the blur-change in special.change.handle. - // This still fires onchange a second time for check/radio after blur. - if ( this.type === "checkbox" || this.type === "radio" ) { - jQuery.event.add( this, "propertychange._change", function( event ) { - if ( event.originalEvent.propertyName === "checked" ) { - this._just_changed = true; - } - }); - jQuery.event.add( this, "click._change", function( event ) { - if ( this._just_changed && !event.isTrigger ) { - this._just_changed = false; - } - // Allow triggered, simulated change events (#11500) - jQuery.event.simulate( "change", this, event, true ); - }); - } - return false; - } - // Delegated event; lazy-add a change handler on descendant inputs - jQuery.event.add( this, "beforeactivate._change", function( e ) { - var elem = e.target; - - if ( rformElems.test( elem.nodeName ) && !jQuery._data( elem, "changeBubbles" ) ) { - jQuery.event.add( elem, "change._change", function( event ) { - if ( this.parentNode && !event.isSimulated && !event.isTrigger ) { - jQuery.event.simulate( "change", this.parentNode, event, true ); - } - }); - jQuery._data( elem, "changeBubbles", true ); - } - }); - }, - - handle: function( event ) { - var elem = event.target; - - // Swallow native change events from checkbox/radio, we already triggered them above - if ( this !== elem || event.isSimulated || event.isTrigger || (elem.type !== "radio" && elem.type !== "checkbox") ) { - return event.handleObj.handler.apply( this, arguments ); - } - }, - - teardown: function() { - jQuery.event.remove( this, "._change" ); - - return !rformElems.test( this.nodeName ); - } - }; -} - -// Create "bubbling" focus and blur events -if ( !support.focusinBubbles ) { - jQuery.each({ focus: "focusin", blur: "focusout" }, function( orig, fix ) { - - // Attach a single capturing handler on the document while someone wants focusin/focusout - var handler = function( event ) { - jQuery.event.simulate( fix, event.target, jQuery.event.fix( event ), true ); - }; - - jQuery.event.special[ fix ] = { - setup: function() { - var doc = this.ownerDocument || this, - attaches = jQuery._data( doc, fix ); - - if ( !attaches ) { - doc.addEventListener( orig, handler, true ); - } - jQuery._data( doc, fix, ( attaches || 0 ) + 1 ); - }, - teardown: function() { - var doc = this.ownerDocument || this, - attaches = jQuery._data( doc, fix ) - 1; - - if ( !attaches ) { - doc.removeEventListener( orig, handler, true ); - jQuery._removeData( doc, fix ); - } else { - jQuery._data( doc, fix, attaches ); - } - } - }; - }); -} - -jQuery.fn.extend({ - - on: function( types, selector, data, fn, /*INTERNAL*/ one ) { - var type, origFn; - - // Types can be a map of types/handlers - if ( typeof types === "object" ) { - // ( types-Object, selector, data ) - if ( typeof selector !== "string" ) { - // ( types-Object, data ) - data = data || selector; - selector = undefined; - } - for ( type in types ) { - this.on( type, selector, data, types[ type ], one ); - } - return this; - } - - if ( data == null && fn == null ) { - // ( types, fn ) - fn = selector; - data = selector = undefined; - } else if ( fn == null ) { - if ( typeof selector === "string" ) { - // ( types, selector, fn ) - fn = data; - data = undefined; - } else { - // ( types, data, fn ) - fn = data; - data = selector; - selector = undefined; - } - } - if ( fn === false ) { - fn = returnFalse; - } else if ( !fn ) { - return this; - } - - if ( one === 1 ) { - origFn = fn; - fn = function( event ) { - // Can use an empty set, since event contains the info - jQuery().off( event ); - return origFn.apply( this, arguments ); - }; - // Use same guid so caller can remove using origFn - fn.guid = origFn.guid || ( origFn.guid = jQuery.guid++ ); - } - return this.each( function() { - jQuery.event.add( this, types, fn, data, selector ); - }); - }, - one: function( types, selector, data, fn ) { - return this.on( types, selector, data, fn, 1 ); - }, - off: function( types, selector, fn ) { - var handleObj, type; - if ( types && types.preventDefault && types.handleObj ) { - // ( event ) dispatched jQuery.Event - handleObj = types.handleObj; - jQuery( types.delegateTarget ).off( - handleObj.namespace ? handleObj.origType + "." + handleObj.namespace : handleObj.origType, - handleObj.selector, - handleObj.handler - ); - return this; - } - if ( typeof types === "object" ) { - // ( types-object [, selector] ) - for ( type in types ) { - this.off( type, selector, types[ type ] ); - } - return this; - } - if ( selector === false || typeof selector === "function" ) { - // ( types [, fn] ) - fn = selector; - selector = undefined; - } - if ( fn === false ) { - fn = returnFalse; - } - return this.each(function() { - jQuery.event.remove( this, types, fn, selector ); - }); - }, - - trigger: function( type, data ) { - return this.each(function() { - jQuery.event.trigger( type, data, this ); - }); - }, - triggerHandler: function( type, data ) { - var elem = this[0]; - if ( elem ) { - return jQuery.event.trigger( type, data, elem, true ); - } - } -}); - - -function createSafeFragment( document ) { - var list = nodeNames.split( "|" ), - safeFrag = document.createDocumentFragment(); - - if ( safeFrag.createElement ) { - while ( list.length ) { - safeFrag.createElement( - list.pop() - ); - } - } - return safeFrag; -} - -var nodeNames = "abbr|article|aside|audio|bdi|canvas|data|datalist|details|figcaption|figure|footer|" + - "header|hgroup|mark|meter|nav|output|progress|section|summary|time|video", - rinlinejQuery = / jQuery\d+="(?:null|\d+)"/g, - rnoshimcache = new RegExp("<(?:" + nodeNames + ")[\\s/>]", "i"), - rleadingWhitespace = /^\s+/, - rxhtmlTag = /<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi, - rtagName = /<([\w:]+)/, - rtbody = /<tbody/i, - rhtml = /<|&#?\w+;/, - rnoInnerhtml = /<(?:script|style|link)/i, - // checked="checked" or checked - rchecked = /checked\s*(?:[^=]|=\s*.checked.)/i, - rscriptType = /^$|\/(?:java|ecma)script/i, - rscriptTypeMasked = /^true\/(.*)/, - rcleanScript = /^\s*<!(?:\[CDATA\[|--)|(?:\]\]|--)>\s*$/g, - - // We have to close these tags to support XHTML (#13200) - wrapMap = { - option: [ 1, "<select multiple='multiple'>", "</select>" ], - legend: [ 1, "<fieldset>", "</fieldset>" ], - area: [ 1, "<map>", "</map>" ], - param: [ 1, "<object>", "</object>" ], - thead: [ 1, "<table>", "</table>" ], - tr: [ 2, "<table><tbody>", "</tbody></table>" ], - col: [ 2, "<table><tbody></tbody><colgroup>", "</colgroup></table>" ], - td: [ 3, "<table><tbody><tr>", "</tr></tbody></table>" ], - - // IE6-8 can't serialize link, script, style, or any html5 (NoScope) tags, - // unless wrapped in a div with non-breaking characters in front of it. - _default: support.htmlSerialize ? [ 0, "", "" ] : [ 1, "X<div>", "</div>" ] - }, - safeFragment = createSafeFragment( document ), - fragmentDiv = safeFragment.appendChild( document.createElement("div") ); - -wrapMap.optgroup = wrapMap.option; -wrapMap.tbody = wrapMap.tfoot = wrapMap.colgroup = wrapMap.caption = wrapMap.thead; -wrapMap.th = wrapMap.td; - -function getAll( context, tag ) { - var elems, elem, - i = 0, - found = typeof context.getElementsByTagName !== strundefined ? context.getElementsByTagName( tag || "*" ) : - typeof context.querySelectorAll !== strundefined ? context.querySelectorAll( tag || "*" ) : - undefined; - - if ( !found ) { - for ( found = [], elems = context.childNodes || context; (elem = elems[i]) != null; i++ ) { - if ( !tag || jQuery.nodeName( elem, tag ) ) { - found.push( elem ); - } else { - jQuery.merge( found, getAll( elem, tag ) ); - } - } - } - - return tag === undefined || tag && jQuery.nodeName( context, tag ) ? - jQuery.merge( [ context ], found ) : - found; -} - -// Used in buildFragment, fixes the defaultChecked property -function fixDefaultChecked( elem ) { - if ( rcheckableType.test( elem.type ) ) { - elem.defaultChecked = elem.checked; - } -} - -// Support: IE<8 -// Manipulating tables requires a tbody -function manipulationTarget( elem, content ) { - return jQuery.nodeName( elem, "table" ) && - jQuery.nodeName( content.nodeType !== 11 ? content : content.firstChild, "tr" ) ? - - elem.getElementsByTagName("tbody")[0] || - elem.appendChild( elem.ownerDocument.createElement("tbody") ) : - elem; -} - -// Replace/restore the type attribute of script elements for safe DOM manipulation -function disableScript( elem ) { - elem.type = (jQuery.find.attr( elem, "type" ) !== null) + "/" + elem.type; - return elem; -} -function restoreScript( elem ) { - var match = rscriptTypeMasked.exec( elem.type ); - if ( match ) { - elem.type = match[1]; - } else { - elem.removeAttribute("type"); - } - return elem; -} - -// Mark scripts as having already been evaluated -function setGlobalEval( elems, refElements ) { - var elem, - i = 0; - for ( ; (elem = elems[i]) != null; i++ ) { - jQuery._data( elem, "globalEval", !refElements || jQuery._data( refElements[i], "globalEval" ) ); - } -} - -function cloneCopyEvent( src, dest ) { - - if ( dest.nodeType !== 1 || !jQuery.hasData( src ) ) { - return; - } - - var type, i, l, - oldData = jQuery._data( src ), - curData = jQuery._data( dest, oldData ), - events = oldData.events; - - if ( events ) { - delete curData.handle; - curData.events = {}; - - for ( type in events ) { - for ( i = 0, l = events[ type ].length; i < l; i++ ) { - jQuery.event.add( dest, type, events[ type ][ i ] ); - } - } - } - - // make the cloned public data object a copy from the original - if ( curData.data ) { - curData.data = jQuery.extend( {}, curData.data ); - } -} - -function fixCloneNodeIssues( src, dest ) { - var nodeName, e, data; - - // We do not need to do anything for non-Elements - if ( dest.nodeType !== 1 ) { - return; - } - - nodeName = dest.nodeName.toLowerCase(); - - // IE6-8 copies events bound via attachEvent when using cloneNode. - if ( !support.noCloneEvent && dest[ jQuery.expando ] ) { - data = jQuery._data( dest ); - - for ( e in data.events ) { - jQuery.removeEvent( dest, e, data.handle ); - } - - // Event data gets referenced instead of copied if the expando gets copied too - dest.removeAttribute( jQuery.expando ); - } - - // IE blanks contents when cloning scripts, and tries to evaluate newly-set text - if ( nodeName === "script" && dest.text !== src.text ) { - disableScript( dest ).text = src.text; - restoreScript( dest ); - - // IE6-10 improperly clones children of object elements using classid. - // IE10 throws NoModificationAllowedError if parent is null, #12132. - } else if ( nodeName === "object" ) { - if ( dest.parentNode ) { - dest.outerHTML = src.outerHTML; - } - - // This path appears unavoidable for IE9. When cloning an object - // element in IE9, the outerHTML strategy above is not sufficient. - // If the src has innerHTML and the destination does not, - // copy the src.innerHTML into the dest.innerHTML. #10324 - if ( support.html5Clone && ( src.innerHTML && !jQuery.trim(dest.innerHTML) ) ) { - dest.innerHTML = src.innerHTML; - } - - } else if ( nodeName === "input" && rcheckableType.test( src.type ) ) { - // IE6-8 fails to persist the checked state of a cloned checkbox - // or radio button. Worse, IE6-7 fail to give the cloned element - // a checked appearance if the defaultChecked value isn't also set - - dest.defaultChecked = dest.checked = src.checked; - - // IE6-7 get confused and end up setting the value of a cloned - // checkbox/radio button to an empty string instead of "on" - if ( dest.value !== src.value ) { - dest.value = src.value; - } - - // IE6-8 fails to return the selected option to the default selected - // state when cloning options - } else if ( nodeName === "option" ) { - dest.defaultSelected = dest.selected = src.defaultSelected; - - // IE6-8 fails to set the defaultValue to the correct value when - // cloning other types of input fields - } else if ( nodeName === "input" || nodeName === "textarea" ) { - dest.defaultValue = src.defaultValue; - } -} - -jQuery.extend({ - clone: function( elem, dataAndEvents, deepDataAndEvents ) { - var destElements, node, clone, i, srcElements, - inPage = jQuery.contains( elem.ownerDocument, elem ); - - if ( support.html5Clone || jQuery.isXMLDoc(elem) || !rnoshimcache.test( "<" + elem.nodeName + ">" ) ) { - clone = elem.cloneNode( true ); - - // IE<=8 does not properly clone detached, unknown element nodes - } else { - fragmentDiv.innerHTML = elem.outerHTML; - fragmentDiv.removeChild( clone = fragmentDiv.firstChild ); - } - - if ( (!support.noCloneEvent || !support.noCloneChecked) && - (elem.nodeType === 1 || elem.nodeType === 11) && !jQuery.isXMLDoc(elem) ) { - - // We eschew Sizzle here for performance reasons: http://jsperf.com/getall-vs-sizzle/2 - destElements = getAll( clone ); - srcElements = getAll( elem ); - - // Fix all IE cloning issues - for ( i = 0; (node = srcElements[i]) != null; ++i ) { - // Ensure that the destination node is not null; Fixes #9587 - if ( destElements[i] ) { - fixCloneNodeIssues( node, destElements[i] ); - } - } - } - - // Copy the events from the original to the clone - if ( dataAndEvents ) { - if ( deepDataAndEvents ) { - srcElements = srcElements || getAll( elem ); - destElements = destElements || getAll( clone ); - - for ( i = 0; (node = srcElements[i]) != null; i++ ) { - cloneCopyEvent( node, destElements[i] ); - } - } else { - cloneCopyEvent( elem, clone ); - } - } - - // Preserve script evaluation history - destElements = getAll( clone, "script" ); - if ( destElements.length > 0 ) { - setGlobalEval( destElements, !inPage && getAll( elem, "script" ) ); - } - - destElements = srcElements = node = null; - - // Return the cloned set - return clone; - }, - - buildFragment: function( elems, context, scripts, selection ) { - var j, elem, contains, - tmp, tag, tbody, wrap, - l = elems.length, - - // Ensure a safe fragment - safe = createSafeFragment( context ), - - nodes = [], - i = 0; - - for ( ; i < l; i++ ) { - elem = elems[ i ]; - - if ( elem || elem === 0 ) { - - // Add nodes directly - if ( jQuery.type( elem ) === "object" ) { - jQuery.merge( nodes, elem.nodeType ? [ elem ] : elem ); - - // Convert non-html into a text node - } else if ( !rhtml.test( elem ) ) { - nodes.push( context.createTextNode( elem ) ); - - // Convert html into DOM nodes - } else { - tmp = tmp || safe.appendChild( context.createElement("div") ); - - // Deserialize a standard representation - tag = (rtagName.exec( elem ) || [ "", "" ])[ 1 ].toLowerCase(); - wrap = wrapMap[ tag ] || wrapMap._default; - - tmp.innerHTML = wrap[1] + elem.replace( rxhtmlTag, "<$1></$2>" ) + wrap[2]; - - // Descend through wrappers to the right content - j = wrap[0]; - while ( j-- ) { - tmp = tmp.lastChild; - } - - // Manually add leading whitespace removed by IE - if ( !support.leadingWhitespace && rleadingWhitespace.test( elem ) ) { - nodes.push( context.createTextNode( rleadingWhitespace.exec( elem )[0] ) ); - } - - // Remove IE's autoinserted <tbody> from table fragments - if ( !support.tbody ) { - - // String was a <table>, *may* have spurious <tbody> - elem = tag === "table" && !rtbody.test( elem ) ? - tmp.firstChild : - - // String was a bare <thead> or <tfoot> - wrap[1] === "<table>" && !rtbody.test( elem ) ? - tmp : - 0; - - j = elem && elem.childNodes.length; - while ( j-- ) { - if ( jQuery.nodeName( (tbody = elem.childNodes[j]), "tbody" ) && !tbody.childNodes.length ) { - elem.removeChild( tbody ); - } - } - } - - jQuery.merge( nodes, tmp.childNodes ); - - // Fix #12392 for WebKit and IE > 9 - tmp.textContent = ""; - - // Fix #12392 for oldIE - while ( tmp.firstChild ) { - tmp.removeChild( tmp.firstChild ); - } - - // Remember the top-level container for proper cleanup - tmp = safe.lastChild; - } - } - } - - // Fix #11356: Clear elements from fragment - if ( tmp ) { - safe.removeChild( tmp ); - } - - // Reset defaultChecked for any radios and checkboxes - // about to be appended to the DOM in IE 6/7 (#8060) - if ( !support.appendChecked ) { - jQuery.grep( getAll( nodes, "input" ), fixDefaultChecked ); - } - - i = 0; - while ( (elem = nodes[ i++ ]) ) { - - // #4087 - If origin and destination elements are the same, and this is - // that element, do not do anything - if ( selection && jQuery.inArray( elem, selection ) !== -1 ) { - continue; - } - - contains = jQuery.contains( elem.ownerDocument, elem ); - - // Append to fragment - tmp = getAll( safe.appendChild( elem ), "script" ); - - // Preserve script evaluation history - if ( contains ) { - setGlobalEval( tmp ); - } - - // Capture executables - if ( scripts ) { - j = 0; - while ( (elem = tmp[ j++ ]) ) { - if ( rscriptType.test( elem.type || "" ) ) { - scripts.push( elem ); - } - } - } - } - - tmp = null; - - return safe; - }, - - cleanData: function( elems, /* internal */ acceptData ) { - var elem, type, id, data, - i = 0, - internalKey = jQuery.expando, - cache = jQuery.cache, - deleteExpando = support.deleteExpando, - special = jQuery.event.special; - - for ( ; (elem = elems[i]) != null; i++ ) { - if ( acceptData || jQuery.acceptData( elem ) ) { - - id = elem[ internalKey ]; - data = id && cache[ id ]; - - if ( data ) { - if ( data.events ) { - for ( type in data.events ) { - if ( special[ type ] ) { - jQuery.event.remove( elem, type ); - - // This is a shortcut to avoid jQuery.event.remove's overhead - } else { - jQuery.removeEvent( elem, type, data.handle ); - } - } - } - - // Remove cache only if it was not already removed by jQuery.event.remove - if ( cache[ id ] ) { - - delete cache[ id ]; - - // IE does not allow us to delete expando properties from nodes, - // nor does it have a removeAttribute function on Document nodes; - // we must handle all of these cases - if ( deleteExpando ) { - delete elem[ internalKey ]; - - } else if ( typeof elem.removeAttribute !== strundefined ) { - elem.removeAttribute( internalKey ); - - } else { - elem[ internalKey ] = null; - } - - deletedIds.push( id ); - } - } - } - } - } -}); - -jQuery.fn.extend({ - text: function( value ) { - return access( this, function( value ) { - return value === undefined ? - jQuery.text( this ) : - this.empty().append( ( this[0] && this[0].ownerDocument || document ).createTextNode( value ) ); - }, null, value, arguments.length ); - }, - - append: function() { - return this.domManip( arguments, function( elem ) { - if ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) { - var target = manipulationTarget( this, elem ); - target.appendChild( elem ); - } - }); - }, - - prepend: function() { - return this.domManip( arguments, function( elem ) { - if ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) { - var target = manipulationTarget( this, elem ); - target.insertBefore( elem, target.firstChild ); - } - }); - }, - - before: function() { - return this.domManip( arguments, function( elem ) { - if ( this.parentNode ) { - this.parentNode.insertBefore( elem, this ); - } - }); - }, - - after: function() { - return this.domManip( arguments, function( elem ) { - if ( this.parentNode ) { - this.parentNode.insertBefore( elem, this.nextSibling ); - } - }); - }, - - remove: function( selector, keepData /* Internal Use Only */ ) { - var elem, - elems = selector ? jQuery.filter( selector, this ) : this, - i = 0; - - for ( ; (elem = elems[i]) != null; i++ ) { - - if ( !keepData && elem.nodeType === 1 ) { - jQuery.cleanData( getAll( elem ) ); - } - - if ( elem.parentNode ) { - if ( keepData && jQuery.contains( elem.ownerDocument, elem ) ) { - setGlobalEval( getAll( elem, "script" ) ); - } - elem.parentNode.removeChild( elem ); - } - } - - return this; - }, - - empty: function() { - var elem, - i = 0; - - for ( ; (elem = this[i]) != null; i++ ) { - // Remove element nodes and prevent memory leaks - if ( elem.nodeType === 1 ) { - jQuery.cleanData( getAll( elem, false ) ); - } - - // Remove any remaining nodes - while ( elem.firstChild ) { - elem.removeChild( elem.firstChild ); - } - - // If this is a select, ensure that it displays empty (#12336) - // Support: IE<9 - if ( elem.options && jQuery.nodeName( elem, "select" ) ) { - elem.options.length = 0; - } - } - - return this; - }, - - clone: function( dataAndEvents, deepDataAndEvents ) { - dataAndEvents = dataAndEvents == null ? false : dataAndEvents; - deepDataAndEvents = deepDataAndEvents == null ? dataAndEvents : deepDataAndEvents; - - return this.map(function() { - return jQuery.clone( this, dataAndEvents, deepDataAndEvents ); - }); - }, - - html: function( value ) { - return access( this, function( value ) { - var elem = this[ 0 ] || {}, - i = 0, - l = this.length; - - if ( value === undefined ) { - return elem.nodeType === 1 ? - elem.innerHTML.replace( rinlinejQuery, "" ) : - undefined; - } - - // See if we can take a shortcut and just use innerHTML - if ( typeof value === "string" && !rnoInnerhtml.test( value ) && - ( support.htmlSerialize || !rnoshimcache.test( value ) ) && - ( support.leadingWhitespace || !rleadingWhitespace.test( value ) ) && - !wrapMap[ (rtagName.exec( value ) || [ "", "" ])[ 1 ].toLowerCase() ] ) { - - value = value.replace( rxhtmlTag, "<$1></$2>" ); - - try { - for (; i < l; i++ ) { - // Remove element nodes and prevent memory leaks - elem = this[i] || {}; - if ( elem.nodeType === 1 ) { - jQuery.cleanData( getAll( elem, false ) ); - elem.innerHTML = value; - } - } - - elem = 0; - - // If using innerHTML throws an exception, use the fallback method - } catch(e) {} - } - - if ( elem ) { - this.empty().append( value ); - } - }, null, value, arguments.length ); - }, - - replaceWith: function() { - var arg = arguments[ 0 ]; - - // Make the changes, replacing each context element with the new content - this.domManip( arguments, function( elem ) { - arg = this.parentNode; - - jQuery.cleanData( getAll( this ) ); - - if ( arg ) { - arg.replaceChild( elem, this ); - } - }); - - // Force removal if there was no new content (e.g., from empty arguments) - return arg && (arg.length || arg.nodeType) ? this : this.remove(); - }, - - detach: function( selector ) { - return this.remove( selector, true ); - }, - - domManip: function( args, callback ) { - - // Flatten any nested arrays - args = concat.apply( [], args ); - - var first, node, hasScripts, - scripts, doc, fragment, - i = 0, - l = this.length, - set = this, - iNoClone = l - 1, - value = args[0], - isFunction = jQuery.isFunction( value ); - - // We can't cloneNode fragments that contain checked, in WebKit - if ( isFunction || - ( l > 1 && typeof value === "string" && - !support.checkClone && rchecked.test( value ) ) ) { - return this.each(function( index ) { - var self = set.eq( index ); - if ( isFunction ) { - args[0] = value.call( this, index, self.html() ); - } - self.domManip( args, callback ); - }); - } - - if ( l ) { - fragment = jQuery.buildFragment( args, this[ 0 ].ownerDocument, false, this ); - first = fragment.firstChild; - - if ( fragment.childNodes.length === 1 ) { - fragment = first; - } - - if ( first ) { - scripts = jQuery.map( getAll( fragment, "script" ), disableScript ); - hasScripts = scripts.length; - - // Use the original fragment for the last item instead of the first because it can end up - // being emptied incorrectly in certain situations (#8070). - for ( ; i < l; i++ ) { - node = fragment; - - if ( i !== iNoClone ) { - node = jQuery.clone( node, true, true ); - - // Keep references to cloned scripts for later restoration - if ( hasScripts ) { - jQuery.merge( scripts, getAll( node, "script" ) ); - } - } - - callback.call( this[i], node, i ); - } - - if ( hasScripts ) { - doc = scripts[ scripts.length - 1 ].ownerDocument; - - // Reenable scripts - jQuery.map( scripts, restoreScript ); - - // Evaluate executable scripts on first document insertion - for ( i = 0; i < hasScripts; i++ ) { - node = scripts[ i ]; - if ( rscriptType.test( node.type || "" ) && - !jQuery._data( node, "globalEval" ) && jQuery.contains( doc, node ) ) { - - if ( node.src ) { - // Optional AJAX dependency, but won't run scripts if not present - if ( jQuery._evalUrl ) { - jQuery._evalUrl( node.src ); - } - } else { - jQuery.globalEval( ( node.text || node.textContent || node.innerHTML || "" ).replace( rcleanScript, "" ) ); - } - } - } - } - - // Fix #11809: Avoid leaking memory - fragment = first = null; - } - } - - return this; - } -}); - -jQuery.each({ - appendTo: "append", - prependTo: "prepend", - insertBefore: "before", - insertAfter: "after", - replaceAll: "replaceWith" -}, function( name, original ) { - jQuery.fn[ name ] = function( selector ) { - var elems, - i = 0, - ret = [], - insert = jQuery( selector ), - last = insert.length - 1; - - for ( ; i <= last; i++ ) { - elems = i === last ? this : this.clone(true); - jQuery( insert[i] )[ original ]( elems ); - - // Modern browsers can apply jQuery collections as arrays, but oldIE needs a .get() - push.apply( ret, elems.get() ); - } - - return this.pushStack( ret ); - }; -}); - - -var iframe, - elemdisplay = {}; - -/** - * Retrieve the actual display of a element - * @param {String} name nodeName of the element - * @param {Object} doc Document object - */ -// Called only from within defaultDisplay -function actualDisplay( name, doc ) { - var style, - elem = jQuery( doc.createElement( name ) ).appendTo( doc.body ), - - // getDefaultComputedStyle might be reliably used only on attached element - display = window.getDefaultComputedStyle && ( style = window.getDefaultComputedStyle( elem[ 0 ] ) ) ? - - // Use of this method is a temporary fix (more like optmization) until something better comes along, - // since it was removed from specification and supported only in FF - style.display : jQuery.css( elem[ 0 ], "display" ); - - // We don't have any data stored on the element, - // so use "detach" method as fast way to get rid of the element - elem.detach(); - - return display; -} - -/** - * Try to determine the default display value of an element - * @param {String} nodeName - */ -function defaultDisplay( nodeName ) { - var doc = document, - display = elemdisplay[ nodeName ]; - - if ( !display ) { - display = actualDisplay( nodeName, doc ); - - // If the simple way fails, read from inside an iframe - if ( display === "none" || !display ) { - - // Use the already-created iframe if possible - iframe = (iframe || jQuery( "<iframe frameborder='0' width='0' height='0'/>" )).appendTo( doc.documentElement ); - - // Always write a new HTML skeleton so Webkit and Firefox don't choke on reuse - doc = ( iframe[ 0 ].contentWindow || iframe[ 0 ].contentDocument ).document; - - // Support: IE - doc.write(); - doc.close(); - - display = actualDisplay( nodeName, doc ); - iframe.detach(); - } - - // Store the correct default display - elemdisplay[ nodeName ] = display; - } - - return display; -} - - -(function() { - var shrinkWrapBlocksVal; - - support.shrinkWrapBlocks = function() { - if ( shrinkWrapBlocksVal != null ) { - return shrinkWrapBlocksVal; - } - - // Will be changed later if needed. - shrinkWrapBlocksVal = false; - - // Minified: var b,c,d - var div, body, container; - - body = document.getElementsByTagName( "body" )[ 0 ]; - if ( !body || !body.style ) { - // Test fired too early or in an unsupported environment, exit. - return; - } - - // Setup - div = document.createElement( "div" ); - container = document.createElement( "div" ); - container.style.cssText = "position:absolute;border:0;width:0;height:0;top:0;left:-9999px"; - body.appendChild( container ).appendChild( div ); - - // Support: IE6 - // Check if elements with layout shrink-wrap their children - if ( typeof div.style.zoom !== strundefined ) { - // Reset CSS: box-sizing; display; margin; border - div.style.cssText = - // Support: Firefox<29, Android 2.3 - // Vendor-prefix box-sizing - "-webkit-box-sizing:content-box;-moz-box-sizing:content-box;" + - "box-sizing:content-box;display:block;margin:0;border:0;" + - "padding:1px;width:1px;zoom:1"; - div.appendChild( document.createElement( "div" ) ).style.width = "5px"; - shrinkWrapBlocksVal = div.offsetWidth !== 3; - } - - body.removeChild( container ); - - return shrinkWrapBlocksVal; - }; - -})(); -var rmargin = (/^margin/); - -var rnumnonpx = new RegExp( "^(" + pnum + ")(?!px)[a-z%]+$", "i" ); - - - -var getStyles, curCSS, - rposition = /^(top|right|bottom|left)$/; - -if ( window.getComputedStyle ) { - getStyles = function( elem ) { - // Support: IE<=11+, Firefox<=30+ (#15098, #14150) - // IE throws on elements created in popups - // FF meanwhile throws on frame elements through "defaultView.getComputedStyle" - if ( elem.ownerDocument.defaultView.opener ) { - return elem.ownerDocument.defaultView.getComputedStyle( elem, null ); - } - - return window.getComputedStyle( elem, null ); - }; - - curCSS = function( elem, name, computed ) { - var width, minWidth, maxWidth, ret, - style = elem.style; - - computed = computed || getStyles( elem ); - - // getPropertyValue is only needed for .css('filter') in IE9, see #12537 - ret = computed ? computed.getPropertyValue( name ) || computed[ name ] : undefined; - - if ( computed ) { - - if ( ret === "" && !jQuery.contains( elem.ownerDocument, elem ) ) { - ret = jQuery.style( elem, name ); - } - - // A tribute to the "awesome hack by Dean Edwards" - // Chrome < 17 and Safari 5.0 uses "computed value" instead of "used value" for margin-right - // Safari 5.1.7 (at least) returns percentage for a larger set of values, but width seems to be reliably pixels - // this is against the CSSOM draft spec: http://dev.w3.org/csswg/cssom/#resolved-values - if ( rnumnonpx.test( ret ) && rmargin.test( name ) ) { - - // Remember the original values - width = style.width; - minWidth = style.minWidth; - maxWidth = style.maxWidth; - - // Put in the new values to get a computed value out - style.minWidth = style.maxWidth = style.width = ret; - ret = computed.width; - - // Revert the changed values - style.width = width; - style.minWidth = minWidth; - style.maxWidth = maxWidth; - } - } - - // Support: IE - // IE returns zIndex value as an integer. - return ret === undefined ? - ret : - ret + ""; - }; -} else if ( document.documentElement.currentStyle ) { - getStyles = function( elem ) { - return elem.currentStyle; - }; - - curCSS = function( elem, name, computed ) { - var left, rs, rsLeft, ret, - style = elem.style; - - computed = computed || getStyles( elem ); - ret = computed ? computed[ name ] : undefined; - - // Avoid setting ret to empty string here - // so we don't default to auto - if ( ret == null && style && style[ name ] ) { - ret = style[ name ]; - } - - // From the awesome hack by Dean Edwards - // http://erik.eae.net/archives/2007/07/27/18.54.15/#comment-102291 - - // If we're not dealing with a regular pixel number - // but a number that has a weird ending, we need to convert it to pixels - // but not position css attributes, as those are proportional to the parent element instead - // and we can't measure the parent instead because it might trigger a "stacking dolls" problem - if ( rnumnonpx.test( ret ) && !rposition.test( name ) ) { - - // Remember the original values - left = style.left; - rs = elem.runtimeStyle; - rsLeft = rs && rs.left; - - // Put in the new values to get a computed value out - if ( rsLeft ) { - rs.left = elem.currentStyle.left; - } - style.left = name === "fontSize" ? "1em" : ret; - ret = style.pixelLeft + "px"; - - // Revert the changed values - style.left = left; - if ( rsLeft ) { - rs.left = rsLeft; - } - } - - // Support: IE - // IE returns zIndex value as an integer. - return ret === undefined ? - ret : - ret + "" || "auto"; - }; -} - - - - -function addGetHookIf( conditionFn, hookFn ) { - // Define the hook, we'll check on the first run if it's really needed. - return { - get: function() { - var condition = conditionFn(); - - if ( condition == null ) { - // The test was not ready at this point; screw the hook this time - // but check again when needed next time. - return; - } - - if ( condition ) { - // Hook not needed (or it's not possible to use it due to missing dependency), - // remove it. - // Since there are no other hooks for marginRight, remove the whole object. - delete this.get; - return; - } - - // Hook needed; redefine it so that the support test is not executed again. - - return (this.get = hookFn).apply( this, arguments ); - } - }; -} - - -(function() { - // Minified: var b,c,d,e,f,g, h,i - var div, style, a, pixelPositionVal, boxSizingReliableVal, - reliableHiddenOffsetsVal, reliableMarginRightVal; - - // Setup - div = document.createElement( "div" ); - div.innerHTML = " <link/><table></table><a href='/a'>a</a><input type='checkbox'/>"; - a = div.getElementsByTagName( "a" )[ 0 ]; - style = a && a.style; - - // Finish early in limited (non-browser) environments - if ( !style ) { - return; - } - - style.cssText = "float:left;opacity:.5"; - - // Support: IE<9 - // Make sure that element opacity exists (as opposed to filter) - support.opacity = style.opacity === "0.5"; - - // Verify style float existence - // (IE uses styleFloat instead of cssFloat) - support.cssFloat = !!style.cssFloat; - - div.style.backgroundClip = "content-box"; - div.cloneNode( true ).style.backgroundClip = ""; - support.clearCloneStyle = div.style.backgroundClip === "content-box"; - - // Support: Firefox<29, Android 2.3 - // Vendor-prefix box-sizing - support.boxSizing = style.boxSizing === "" || style.MozBoxSizing === "" || - style.WebkitBoxSizing === ""; - - jQuery.extend(support, { - reliableHiddenOffsets: function() { - if ( reliableHiddenOffsetsVal == null ) { - computeStyleTests(); - } - return reliableHiddenOffsetsVal; - }, - - boxSizingReliable: function() { - if ( boxSizingReliableVal == null ) { - computeStyleTests(); - } - return boxSizingReliableVal; - }, - - pixelPosition: function() { - if ( pixelPositionVal == null ) { - computeStyleTests(); - } - return pixelPositionVal; - }, - - // Support: Android 2.3 - reliableMarginRight: function() { - if ( reliableMarginRightVal == null ) { - computeStyleTests(); - } - return reliableMarginRightVal; - } - }); - - function computeStyleTests() { - // Minified: var b,c,d,j - var div, body, container, contents; - - body = document.getElementsByTagName( "body" )[ 0 ]; - if ( !body || !body.style ) { - // Test fired too early or in an unsupported environment, exit. - return; - } - - // Setup - div = document.createElement( "div" ); - container = document.createElement( "div" ); - container.style.cssText = "position:absolute;border:0;width:0;height:0;top:0;left:-9999px"; - body.appendChild( container ).appendChild( div ); - - div.style.cssText = - // Support: Firefox<29, Android 2.3 - // Vendor-prefix box-sizing - "-webkit-box-sizing:border-box;-moz-box-sizing:border-box;" + - "box-sizing:border-box;display:block;margin-top:1%;top:1%;" + - "border:1px;padding:1px;width:4px;position:absolute"; - - // Support: IE<9 - // Assume reasonable values in the absence of getComputedStyle - pixelPositionVal = boxSizingReliableVal = false; - reliableMarginRightVal = true; - - // Check for getComputedStyle so that this code is not run in IE<9. - if ( window.getComputedStyle ) { - pixelPositionVal = ( window.getComputedStyle( div, null ) || {} ).top !== "1%"; - boxSizingReliableVal = - ( window.getComputedStyle( div, null ) || { width: "4px" } ).width === "4px"; - - // Support: Android 2.3 - // Div with explicit width and no margin-right incorrectly - // gets computed margin-right based on width of container (#3333) - // WebKit Bug 13343 - getComputedStyle returns wrong value for margin-right - contents = div.appendChild( document.createElement( "div" ) ); - - // Reset CSS: box-sizing; display; margin; border; padding - contents.style.cssText = div.style.cssText = - // Support: Firefox<29, Android 2.3 - // Vendor-prefix box-sizing - "-webkit-box-sizing:content-box;-moz-box-sizing:content-box;" + - "box-sizing:content-box;display:block;margin:0;border:0;padding:0"; - contents.style.marginRight = contents.style.width = "0"; - div.style.width = "1px"; - - reliableMarginRightVal = - !parseFloat( ( window.getComputedStyle( contents, null ) || {} ).marginRight ); - - div.removeChild( contents ); - } - - // Support: IE8 - // Check if table cells still have offsetWidth/Height when they are set - // to display:none and there are still other visible table cells in a - // table row; if so, offsetWidth/Height are not reliable for use when - // determining if an element has been hidden directly using - // display:none (it is still safe to use offsets if a parent element is - // hidden; don safety goggles and see bug #4512 for more information). - div.innerHTML = "<table><tr><td></td><td>t</td></tr></table>"; - contents = div.getElementsByTagName( "td" ); - contents[ 0 ].style.cssText = "margin:0;border:0;padding:0;display:none"; - reliableHiddenOffsetsVal = contents[ 0 ].offsetHeight === 0; - if ( reliableHiddenOffsetsVal ) { - contents[ 0 ].style.display = ""; - contents[ 1 ].style.display = "none"; - reliableHiddenOffsetsVal = contents[ 0 ].offsetHeight === 0; - } - - body.removeChild( container ); - } - -})(); - - -// A method for quickly swapping in/out CSS properties to get correct calculations. -jQuery.swap = function( elem, options, callback, args ) { - var ret, name, - old = {}; - - // Remember the old values, and insert the new ones - for ( name in options ) { - old[ name ] = elem.style[ name ]; - elem.style[ name ] = options[ name ]; - } - - ret = callback.apply( elem, args || [] ); - - // Revert the old values - for ( name in options ) { - elem.style[ name ] = old[ name ]; - } - - return ret; -}; - - -var - ralpha = /alpha\([^)]*\)/i, - ropacity = /opacity\s*=\s*([^)]*)/, - - // swappable if display is none or starts with table except "table", "table-cell", or "table-caption" - // see here for display values: https://developer.mozilla.org/en-US/docs/CSS/display - rdisplayswap = /^(none|table(?!-c[ea]).+)/, - rnumsplit = new RegExp( "^(" + pnum + ")(.*)$", "i" ), - rrelNum = new RegExp( "^([+-])=(" + pnum + ")", "i" ), - - cssShow = { position: "absolute", visibility: "hidden", display: "block" }, - cssNormalTransform = { - letterSpacing: "0", - fontWeight: "400" - }, - - cssPrefixes = [ "Webkit", "O", "Moz", "ms" ]; - - -// return a css property mapped to a potentially vendor prefixed property -function vendorPropName( style, name ) { - - // shortcut for names that are not vendor prefixed - if ( name in style ) { - return name; - } - - // check for vendor prefixed names - var capName = name.charAt(0).toUpperCase() + name.slice(1), - origName = name, - i = cssPrefixes.length; - - while ( i-- ) { - name = cssPrefixes[ i ] + capName; - if ( name in style ) { - return name; - } - } - - return origName; -} - -function showHide( elements, show ) { - var display, elem, hidden, - values = [], - index = 0, - length = elements.length; - - for ( ; index < length; index++ ) { - elem = elements[ index ]; - if ( !elem.style ) { - continue; - } - - values[ index ] = jQuery._data( elem, "olddisplay" ); - display = elem.style.display; - if ( show ) { - // Reset the inline display of this element to learn if it is - // being hidden by cascaded rules or not - if ( !values[ index ] && display === "none" ) { - elem.style.display = ""; - } - - // Set elements which have been overridden with display: none - // in a stylesheet to whatever the default browser style is - // for such an element - if ( elem.style.display === "" && isHidden( elem ) ) { - values[ index ] = jQuery._data( elem, "olddisplay", defaultDisplay(elem.nodeName) ); - } - } else { - hidden = isHidden( elem ); - - if ( display && display !== "none" || !hidden ) { - jQuery._data( elem, "olddisplay", hidden ? display : jQuery.css( elem, "display" ) ); - } - } - } - - // Set the display of most of the elements in a second loop - // to avoid the constant reflow - for ( index = 0; index < length; index++ ) { - elem = elements[ index ]; - if ( !elem.style ) { - continue; - } - if ( !show || elem.style.display === "none" || elem.style.display === "" ) { - elem.style.display = show ? values[ index ] || "" : "none"; - } - } - - return elements; -} - -function setPositiveNumber( elem, value, subtract ) { - var matches = rnumsplit.exec( value ); - return matches ? - // Guard against undefined "subtract", e.g., when used as in cssHooks - Math.max( 0, matches[ 1 ] - ( subtract || 0 ) ) + ( matches[ 2 ] || "px" ) : - value; -} - -function augmentWidthOrHeight( elem, name, extra, isBorderBox, styles ) { - var i = extra === ( isBorderBox ? "border" : "content" ) ? - // If we already have the right measurement, avoid augmentation - 4 : - // Otherwise initialize for horizontal or vertical properties - name === "width" ? 1 : 0, - - val = 0; - - for ( ; i < 4; i += 2 ) { - // both box models exclude margin, so add it if we want it - if ( extra === "margin" ) { - val += jQuery.css( elem, extra + cssExpand[ i ], true, styles ); - } - - if ( isBorderBox ) { - // border-box includes padding, so remove it if we want content - if ( extra === "content" ) { - val -= jQuery.css( elem, "padding" + cssExpand[ i ], true, styles ); - } - - // at this point, extra isn't border nor margin, so remove border - if ( extra !== "margin" ) { - val -= jQuery.css( elem, "border" + cssExpand[ i ] + "Width", true, styles ); - } - } else { - // at this point, extra isn't content, so add padding - val += jQuery.css( elem, "padding" + cssExpand[ i ], true, styles ); - - // at this point, extra isn't content nor padding, so add border - if ( extra !== "padding" ) { - val += jQuery.css( elem, "border" + cssExpand[ i ] + "Width", true, styles ); - } - } - } - - return val; -} - -function getWidthOrHeight( elem, name, extra ) { - - // Start with offset property, which is equivalent to the border-box value - var valueIsBorderBox = true, - val = name === "width" ? elem.offsetWidth : elem.offsetHeight, - styles = getStyles( elem ), - isBorderBox = support.boxSizing && jQuery.css( elem, "boxSizing", false, styles ) === "border-box"; - - // some non-html elements return undefined for offsetWidth, so check for null/undefined - // svg - https://bugzilla.mozilla.org/show_bug.cgi?id=649285 - // MathML - https://bugzilla.mozilla.org/show_bug.cgi?id=491668 - if ( val <= 0 || val == null ) { - // Fall back to computed then uncomputed css if necessary - val = curCSS( elem, name, styles ); - if ( val < 0 || val == null ) { - val = elem.style[ name ]; - } - - // Computed unit is not pixels. Stop here and return. - if ( rnumnonpx.test(val) ) { - return val; - } - - // we need the check for style in case a browser which returns unreliable values - // for getComputedStyle silently falls back to the reliable elem.style - valueIsBorderBox = isBorderBox && ( support.boxSizingReliable() || val === elem.style[ name ] ); - - // Normalize "", auto, and prepare for extra - val = parseFloat( val ) || 0; - } - - // use the active box-sizing model to add/subtract irrelevant styles - return ( val + - augmentWidthOrHeight( - elem, - name, - extra || ( isBorderBox ? "border" : "content" ), - valueIsBorderBox, - styles - ) - ) + "px"; -} - -jQuery.extend({ - // Add in style property hooks for overriding the default - // behavior of getting and setting a style property - cssHooks: { - opacity: { - get: function( elem, computed ) { - if ( computed ) { - // We should always get a number back from opacity - var ret = curCSS( elem, "opacity" ); - return ret === "" ? "1" : ret; - } - } - } - }, - - // Don't automatically add "px" to these possibly-unitless properties - cssNumber: { - "columnCount": true, - "fillOpacity": true, - "flexGrow": true, - "flexShrink": true, - "fontWeight": true, - "lineHeight": true, - "opacity": true, - "order": true, - "orphans": true, - "widows": true, - "zIndex": true, - "zoom": true - }, - - // Add in properties whose names you wish to fix before - // setting or getting the value - cssProps: { - // normalize float css property - "float": support.cssFloat ? "cssFloat" : "styleFloat" - }, - - // Get and set the style property on a DOM Node - style: function( elem, name, value, extra ) { - // Don't set styles on text and comment nodes - if ( !elem || elem.nodeType === 3 || elem.nodeType === 8 || !elem.style ) { - return; - } - - // Make sure that we're working with the right name - var ret, type, hooks, - origName = jQuery.camelCase( name ), - style = elem.style; - - name = jQuery.cssProps[ origName ] || ( jQuery.cssProps[ origName ] = vendorPropName( style, origName ) ); - - // gets hook for the prefixed version - // followed by the unprefixed version - hooks = jQuery.cssHooks[ name ] || jQuery.cssHooks[ origName ]; - - // Check if we're setting a value - if ( value !== undefined ) { - type = typeof value; - - // convert relative number strings (+= or -=) to relative numbers. #7345 - if ( type === "string" && (ret = rrelNum.exec( value )) ) { - value = ( ret[1] + 1 ) * ret[2] + parseFloat( jQuery.css( elem, name ) ); - // Fixes bug #9237 - type = "number"; - } - - // Make sure that null and NaN values aren't set. See: #7116 - if ( value == null || value !== value ) { - return; - } - - // If a number was passed in, add 'px' to the (except for certain CSS properties) - if ( type === "number" && !jQuery.cssNumber[ origName ] ) { - value += "px"; - } - - // Fixes #8908, it can be done more correctly by specifing setters in cssHooks, - // but it would mean to define eight (for every problematic property) identical functions - if ( !support.clearCloneStyle && value === "" && name.indexOf("background") === 0 ) { - style[ name ] = "inherit"; - } - - // If a hook was provided, use that value, otherwise just set the specified value - if ( !hooks || !("set" in hooks) || (value = hooks.set( elem, value, extra )) !== undefined ) { - - // Support: IE - // Swallow errors from 'invalid' CSS values (#5509) - try { - style[ name ] = value; - } catch(e) {} - } - - } else { - // If a hook was provided get the non-computed value from there - if ( hooks && "get" in hooks && (ret = hooks.get( elem, false, extra )) !== undefined ) { - return ret; - } - - // Otherwise just get the value from the style object - return style[ name ]; - } - }, - - css: function( elem, name, extra, styles ) { - var num, val, hooks, - origName = jQuery.camelCase( name ); - - // Make sure that we're working with the right name - name = jQuery.cssProps[ origName ] || ( jQuery.cssProps[ origName ] = vendorPropName( elem.style, origName ) ); - - // gets hook for the prefixed version - // followed by the unprefixed version - hooks = jQuery.cssHooks[ name ] || jQuery.cssHooks[ origName ]; - - // If a hook was provided get the computed value from there - if ( hooks && "get" in hooks ) { - val = hooks.get( elem, true, extra ); - } - - // Otherwise, if a way to get the computed value exists, use that - if ( val === undefined ) { - val = curCSS( elem, name, styles ); - } - - //convert "normal" to computed value - if ( val === "normal" && name in cssNormalTransform ) { - val = cssNormalTransform[ name ]; - } - - // Return, converting to number if forced or a qualifier was provided and val looks numeric - if ( extra === "" || extra ) { - num = parseFloat( val ); - return extra === true || jQuery.isNumeric( num ) ? num || 0 : val; - } - return val; - } -}); - -jQuery.each([ "height", "width" ], function( i, name ) { - jQuery.cssHooks[ name ] = { - get: function( elem, computed, extra ) { - if ( computed ) { - // certain elements can have dimension info if we invisibly show them - // however, it must have a current display style that would benefit from this - return rdisplayswap.test( jQuery.css( elem, "display" ) ) && elem.offsetWidth === 0 ? - jQuery.swap( elem, cssShow, function() { - return getWidthOrHeight( elem, name, extra ); - }) : - getWidthOrHeight( elem, name, extra ); - } - }, - - set: function( elem, value, extra ) { - var styles = extra && getStyles( elem ); - return setPositiveNumber( elem, value, extra ? - augmentWidthOrHeight( - elem, - name, - extra, - support.boxSizing && jQuery.css( elem, "boxSizing", false, styles ) === "border-box", - styles - ) : 0 - ); - } - }; -}); - -if ( !support.opacity ) { - jQuery.cssHooks.opacity = { - get: function( elem, computed ) { - // IE uses filters for opacity - return ropacity.test( (computed && elem.currentStyle ? elem.currentStyle.filter : elem.style.filter) || "" ) ? - ( 0.01 * parseFloat( RegExp.$1 ) ) + "" : - computed ? "1" : ""; - }, - - set: function( elem, value ) { - var style = elem.style, - currentStyle = elem.currentStyle, - opacity = jQuery.isNumeric( value ) ? "alpha(opacity=" + value * 100 + ")" : "", - filter = currentStyle && currentStyle.filter || style.filter || ""; - - // IE has trouble with opacity if it does not have layout - // Force it by setting the zoom level - style.zoom = 1; - - // if setting opacity to 1, and no other filters exist - attempt to remove filter attribute #6652 - // if value === "", then remove inline opacity #12685 - if ( ( value >= 1 || value === "" ) && - jQuery.trim( filter.replace( ralpha, "" ) ) === "" && - style.removeAttribute ) { - - // Setting style.filter to null, "" & " " still leave "filter:" in the cssText - // if "filter:" is present at all, clearType is disabled, we want to avoid this - // style.removeAttribute is IE Only, but so apparently is this code path... - style.removeAttribute( "filter" ); - - // if there is no filter style applied in a css rule or unset inline opacity, we are done - if ( value === "" || currentStyle && !currentStyle.filter ) { - return; - } - } - - // otherwise, set new filter values - style.filter = ralpha.test( filter ) ? - filter.replace( ralpha, opacity ) : - filter + " " + opacity; - } - }; -} - -jQuery.cssHooks.marginRight = addGetHookIf( support.reliableMarginRight, - function( elem, computed ) { - if ( computed ) { - // WebKit Bug 13343 - getComputedStyle returns wrong value for margin-right - // Work around by temporarily setting element display to inline-block - return jQuery.swap( elem, { "display": "inline-block" }, - curCSS, [ elem, "marginRight" ] ); - } - } -); - -// These hooks are used by animate to expand properties -jQuery.each({ - margin: "", - padding: "", - border: "Width" -}, function( prefix, suffix ) { - jQuery.cssHooks[ prefix + suffix ] = { - expand: function( value ) { - var i = 0, - expanded = {}, - - // assumes a single number if not a string - parts = typeof value === "string" ? value.split(" ") : [ value ]; - - for ( ; i < 4; i++ ) { - expanded[ prefix + cssExpand[ i ] + suffix ] = - parts[ i ] || parts[ i - 2 ] || parts[ 0 ]; - } - - return expanded; - } - }; - - if ( !rmargin.test( prefix ) ) { - jQuery.cssHooks[ prefix + suffix ].set = setPositiveNumber; - } -}); - -jQuery.fn.extend({ - css: function( name, value ) { - return access( this, function( elem, name, value ) { - var styles, len, - map = {}, - i = 0; - - if ( jQuery.isArray( name ) ) { - styles = getStyles( elem ); - len = name.length; - - for ( ; i < len; i++ ) { - map[ name[ i ] ] = jQuery.css( elem, name[ i ], false, styles ); - } - - return map; - } - - return value !== undefined ? - jQuery.style( elem, name, value ) : - jQuery.css( elem, name ); - }, name, value, arguments.length > 1 ); - }, - show: function() { - return showHide( this, true ); - }, - hide: function() { - return showHide( this ); - }, - toggle: function( state ) { - if ( typeof state === "boolean" ) { - return state ? this.show() : this.hide(); - } - - return this.each(function() { - if ( isHidden( this ) ) { - jQuery( this ).show(); - } else { - jQuery( this ).hide(); - } - }); - } -}); - - -function Tween( elem, options, prop, end, easing ) { - return new Tween.prototype.init( elem, options, prop, end, easing ); -} -jQuery.Tween = Tween; - -Tween.prototype = { - constructor: Tween, - init: function( elem, options, prop, end, easing, unit ) { - this.elem = elem; - this.prop = prop; - this.easing = easing || "swing"; - this.options = options; - this.start = this.now = this.cur(); - this.end = end; - this.unit = unit || ( jQuery.cssNumber[ prop ] ? "" : "px" ); - }, - cur: function() { - var hooks = Tween.propHooks[ this.prop ]; - - return hooks && hooks.get ? - hooks.get( this ) : - Tween.propHooks._default.get( this ); - }, - run: function( percent ) { - var eased, - hooks = Tween.propHooks[ this.prop ]; - - if ( this.options.duration ) { - this.pos = eased = jQuery.easing[ this.easing ]( - percent, this.options.duration * percent, 0, 1, this.options.duration - ); - } else { - this.pos = eased = percent; - } - this.now = ( this.end - this.start ) * eased + this.start; - - if ( this.options.step ) { - this.options.step.call( this.elem, this.now, this ); - } - - if ( hooks && hooks.set ) { - hooks.set( this ); - } else { - Tween.propHooks._default.set( this ); - } - return this; - } -}; - -Tween.prototype.init.prototype = Tween.prototype; - -Tween.propHooks = { - _default: { - get: function( tween ) { - var result; - - if ( tween.elem[ tween.prop ] != null && - (!tween.elem.style || tween.elem.style[ tween.prop ] == null) ) { - return tween.elem[ tween.prop ]; - } - - // passing an empty string as a 3rd parameter to .css will automatically - // attempt a parseFloat and fallback to a string if the parse fails - // so, simple values such as "10px" are parsed to Float. - // complex values such as "rotate(1rad)" are returned as is. - result = jQuery.css( tween.elem, tween.prop, "" ); - // Empty strings, null, undefined and "auto" are converted to 0. - return !result || result === "auto" ? 0 : result; - }, - set: function( tween ) { - // use step hook for back compat - use cssHook if its there - use .style if its - // available and use plain properties where available - if ( jQuery.fx.step[ tween.prop ] ) { - jQuery.fx.step[ tween.prop ]( tween ); - } else if ( tween.elem.style && ( tween.elem.style[ jQuery.cssProps[ tween.prop ] ] != null || jQuery.cssHooks[ tween.prop ] ) ) { - jQuery.style( tween.elem, tween.prop, tween.now + tween.unit ); - } else { - tween.elem[ tween.prop ] = tween.now; - } - } - } -}; - -// Support: IE <=9 -// Panic based approach to setting things on disconnected nodes - -Tween.propHooks.scrollTop = Tween.propHooks.scrollLeft = { - set: function( tween ) { - if ( tween.elem.nodeType && tween.elem.parentNode ) { - tween.elem[ tween.prop ] = tween.now; - } - } -}; - -jQuery.easing = { - linear: function( p ) { - return p; - }, - swing: function( p ) { - return 0.5 - Math.cos( p * Math.PI ) / 2; - } -}; - -jQuery.fx = Tween.prototype.init; - -// Back Compat <1.8 extension point -jQuery.fx.step = {}; - - - - -var - fxNow, timerId, - rfxtypes = /^(?:toggle|show|hide)$/, - rfxnum = new RegExp( "^(?:([+-])=|)(" + pnum + ")([a-z%]*)$", "i" ), - rrun = /queueHooks$/, - animationPrefilters = [ defaultPrefilter ], - tweeners = { - "*": [ function( prop, value ) { - var tween = this.createTween( prop, value ), - target = tween.cur(), - parts = rfxnum.exec( value ), - unit = parts && parts[ 3 ] || ( jQuery.cssNumber[ prop ] ? "" : "px" ), - - // Starting value computation is required for potential unit mismatches - start = ( jQuery.cssNumber[ prop ] || unit !== "px" && +target ) && - rfxnum.exec( jQuery.css( tween.elem, prop ) ), - scale = 1, - maxIterations = 20; - - if ( start && start[ 3 ] !== unit ) { - // Trust units reported by jQuery.css - unit = unit || start[ 3 ]; - - // Make sure we update the tween properties later on - parts = parts || []; - - // Iteratively approximate from a nonzero starting point - start = +target || 1; - - do { - // If previous iteration zeroed out, double until we get *something* - // Use a string for doubling factor so we don't accidentally see scale as unchanged below - scale = scale || ".5"; - - // Adjust and apply - start = start / scale; - jQuery.style( tween.elem, prop, start + unit ); - - // Update scale, tolerating zero or NaN from tween.cur() - // And breaking the loop if scale is unchanged or perfect, or if we've just had enough - } while ( scale !== (scale = tween.cur() / target) && scale !== 1 && --maxIterations ); - } - - // Update tween properties - if ( parts ) { - start = tween.start = +start || +target || 0; - tween.unit = unit; - // If a +=/-= token was provided, we're doing a relative animation - tween.end = parts[ 1 ] ? - start + ( parts[ 1 ] + 1 ) * parts[ 2 ] : - +parts[ 2 ]; - } - - return tween; - } ] - }; - -// Animations created synchronously will run synchronously -function createFxNow() { - setTimeout(function() { - fxNow = undefined; - }); - return ( fxNow = jQuery.now() ); -} - -// Generate parameters to create a standard animation -function genFx( type, includeWidth ) { - var which, - attrs = { height: type }, - i = 0; - - // if we include width, step value is 1 to do all cssExpand values, - // if we don't include width, step value is 2 to skip over Left and Right - includeWidth = includeWidth ? 1 : 0; - for ( ; i < 4 ; i += 2 - includeWidth ) { - which = cssExpand[ i ]; - attrs[ "margin" + which ] = attrs[ "padding" + which ] = type; - } - - if ( includeWidth ) { - attrs.opacity = attrs.width = type; - } - - return attrs; -} - -function createTween( value, prop, animation ) { - var tween, - collection = ( tweeners[ prop ] || [] ).concat( tweeners[ "*" ] ), - index = 0, - length = collection.length; - for ( ; index < length; index++ ) { - if ( (tween = collection[ index ].call( animation, prop, value )) ) { - - // we're done with this property - return tween; - } - } -} - -function defaultPrefilter( elem, props, opts ) { - /* jshint validthis: true */ - var prop, value, toggle, tween, hooks, oldfire, display, checkDisplay, - anim = this, - orig = {}, - style = elem.style, - hidden = elem.nodeType && isHidden( elem ), - dataShow = jQuery._data( elem, "fxshow" ); - - // handle queue: false promises - if ( !opts.queue ) { - hooks = jQuery._queueHooks( elem, "fx" ); - if ( hooks.unqueued == null ) { - hooks.unqueued = 0; - oldfire = hooks.empty.fire; - hooks.empty.fire = function() { - if ( !hooks.unqueued ) { - oldfire(); - } - }; - } - hooks.unqueued++; - - anim.always(function() { - // doing this makes sure that the complete handler will be called - // before this completes - anim.always(function() { - hooks.unqueued--; - if ( !jQuery.queue( elem, "fx" ).length ) { - hooks.empty.fire(); - } - }); - }); - } - - // height/width overflow pass - if ( elem.nodeType === 1 && ( "height" in props || "width" in props ) ) { - // Make sure that nothing sneaks out - // Record all 3 overflow attributes because IE does not - // change the overflow attribute when overflowX and - // overflowY are set to the same value - opts.overflow = [ style.overflow, style.overflowX, style.overflowY ]; - - // Set display property to inline-block for height/width - // animations on inline elements that are having width/height animated - display = jQuery.css( elem, "display" ); - - // Test default display if display is currently "none" - checkDisplay = display === "none" ? - jQuery._data( elem, "olddisplay" ) || defaultDisplay( elem.nodeName ) : display; - - if ( checkDisplay === "inline" && jQuery.css( elem, "float" ) === "none" ) { - - // inline-level elements accept inline-block; - // block-level elements need to be inline with layout - if ( !support.inlineBlockNeedsLayout || defaultDisplay( elem.nodeName ) === "inline" ) { - style.display = "inline-block"; - } else { - style.zoom = 1; - } - } - } - - if ( opts.overflow ) { - style.overflow = "hidden"; - if ( !support.shrinkWrapBlocks() ) { - anim.always(function() { - style.overflow = opts.overflow[ 0 ]; - style.overflowX = opts.overflow[ 1 ]; - style.overflowY = opts.overflow[ 2 ]; - }); - } - } - - // show/hide pass - for ( prop in props ) { - value = props[ prop ]; - if ( rfxtypes.exec( value ) ) { - delete props[ prop ]; - toggle = toggle || value === "toggle"; - if ( value === ( hidden ? "hide" : "show" ) ) { - - // If there is dataShow left over from a stopped hide or show and we are going to proceed with show, we should pretend to be hidden - if ( value === "show" && dataShow && dataShow[ prop ] !== undefined ) { - hidden = true; - } else { - continue; - } - } - orig[ prop ] = dataShow && dataShow[ prop ] || jQuery.style( elem, prop ); - - // Any non-fx value stops us from restoring the original display value - } else { - display = undefined; - } - } - - if ( !jQuery.isEmptyObject( orig ) ) { - if ( dataShow ) { - if ( "hidden" in dataShow ) { - hidden = dataShow.hidden; - } - } else { - dataShow = jQuery._data( elem, "fxshow", {} ); - } - - // store state if its toggle - enables .stop().toggle() to "reverse" - if ( toggle ) { - dataShow.hidden = !hidden; - } - if ( hidden ) { - jQuery( elem ).show(); - } else { - anim.done(function() { - jQuery( elem ).hide(); - }); - } - anim.done(function() { - var prop; - jQuery._removeData( elem, "fxshow" ); - for ( prop in orig ) { - jQuery.style( elem, prop, orig[ prop ] ); - } - }); - for ( prop in orig ) { - tween = createTween( hidden ? dataShow[ prop ] : 0, prop, anim ); - - if ( !( prop in dataShow ) ) { - dataShow[ prop ] = tween.start; - if ( hidden ) { - tween.end = tween.start; - tween.start = prop === "width" || prop === "height" ? 1 : 0; - } - } - } - - // If this is a noop like .hide().hide(), restore an overwritten display value - } else if ( (display === "none" ? defaultDisplay( elem.nodeName ) : display) === "inline" ) { - style.display = display; - } -} - -function propFilter( props, specialEasing ) { - var index, name, easing, value, hooks; - - // camelCase, specialEasing and expand cssHook pass - for ( index in props ) { - name = jQuery.camelCase( index ); - easing = specialEasing[ name ]; - value = props[ index ]; - if ( jQuery.isArray( value ) ) { - easing = value[ 1 ]; - value = props[ index ] = value[ 0 ]; - } - - if ( index !== name ) { - props[ name ] = value; - delete props[ index ]; - } - - hooks = jQuery.cssHooks[ name ]; - if ( hooks && "expand" in hooks ) { - value = hooks.expand( value ); - delete props[ name ]; - - // not quite $.extend, this wont overwrite keys already present. - // also - reusing 'index' from above because we have the correct "name" - for ( index in value ) { - if ( !( index in props ) ) { - props[ index ] = value[ index ]; - specialEasing[ index ] = easing; - } - } - } else { - specialEasing[ name ] = easing; - } - } -} - -function Animation( elem, properties, options ) { - var result, - stopped, - index = 0, - length = animationPrefilters.length, - deferred = jQuery.Deferred().always( function() { - // don't match elem in the :animated selector - delete tick.elem; - }), - tick = function() { - if ( stopped ) { - return false; - } - var currentTime = fxNow || createFxNow(), - remaining = Math.max( 0, animation.startTime + animation.duration - currentTime ), - // archaic crash bug won't allow us to use 1 - ( 0.5 || 0 ) (#12497) - temp = remaining / animation.duration || 0, - percent = 1 - temp, - index = 0, - length = animation.tweens.length; - - for ( ; index < length ; index++ ) { - animation.tweens[ index ].run( percent ); - } - - deferred.notifyWith( elem, [ animation, percent, remaining ]); - - if ( percent < 1 && length ) { - return remaining; - } else { - deferred.resolveWith( elem, [ animation ] ); - return false; - } - }, - animation = deferred.promise({ - elem: elem, - props: jQuery.extend( {}, properties ), - opts: jQuery.extend( true, { specialEasing: {} }, options ), - originalProperties: properties, - originalOptions: options, - startTime: fxNow || createFxNow(), - duration: options.duration, - tweens: [], - createTween: function( prop, end ) { - var tween = jQuery.Tween( elem, animation.opts, prop, end, - animation.opts.specialEasing[ prop ] || animation.opts.easing ); - animation.tweens.push( tween ); - return tween; - }, - stop: function( gotoEnd ) { - var index = 0, - // if we are going to the end, we want to run all the tweens - // otherwise we skip this part - length = gotoEnd ? animation.tweens.length : 0; - if ( stopped ) { - return this; - } - stopped = true; - for ( ; index < length ; index++ ) { - animation.tweens[ index ].run( 1 ); - } - - // resolve when we played the last frame - // otherwise, reject - if ( gotoEnd ) { - deferred.resolveWith( elem, [ animation, gotoEnd ] ); - } else { - deferred.rejectWith( elem, [ animation, gotoEnd ] ); - } - return this; - } - }), - props = animation.props; - - propFilter( props, animation.opts.specialEasing ); - - for ( ; index < length ; index++ ) { - result = animationPrefilters[ index ].call( animation, elem, props, animation.opts ); - if ( result ) { - return result; - } - } - - jQuery.map( props, createTween, animation ); - - if ( jQuery.isFunction( animation.opts.start ) ) { - animation.opts.start.call( elem, animation ); - } - - jQuery.fx.timer( - jQuery.extend( tick, { - elem: elem, - anim: animation, - queue: animation.opts.queue - }) - ); - - // attach callbacks from options - return animation.progress( animation.opts.progress ) - .done( animation.opts.done, animation.opts.complete ) - .fail( animation.opts.fail ) - .always( animation.opts.always ); -} - -jQuery.Animation = jQuery.extend( Animation, { - tweener: function( props, callback ) { - if ( jQuery.isFunction( props ) ) { - callback = props; - props = [ "*" ]; - } else { - props = props.split(" "); - } - - var prop, - index = 0, - length = props.length; - - for ( ; index < length ; index++ ) { - prop = props[ index ]; - tweeners[ prop ] = tweeners[ prop ] || []; - tweeners[ prop ].unshift( callback ); - } - }, - - prefilter: function( callback, prepend ) { - if ( prepend ) { - animationPrefilters.unshift( callback ); - } else { - animationPrefilters.push( callback ); - } - } -}); - -jQuery.speed = function( speed, easing, fn ) { - var opt = speed && typeof speed === "object" ? jQuery.extend( {}, speed ) : { - complete: fn || !fn && easing || - jQuery.isFunction( speed ) && speed, - duration: speed, - easing: fn && easing || easing && !jQuery.isFunction( easing ) && easing - }; - - opt.duration = jQuery.fx.off ? 0 : typeof opt.duration === "number" ? opt.duration : - opt.duration in jQuery.fx.speeds ? jQuery.fx.speeds[ opt.duration ] : jQuery.fx.speeds._default; - - // normalize opt.queue - true/undefined/null -> "fx" - if ( opt.queue == null || opt.queue === true ) { - opt.queue = "fx"; - } - - // Queueing - opt.old = opt.complete; - - opt.complete = function() { - if ( jQuery.isFunction( opt.old ) ) { - opt.old.call( this ); - } - - if ( opt.queue ) { - jQuery.dequeue( this, opt.queue ); - } - }; - - return opt; -}; - -jQuery.fn.extend({ - fadeTo: function( speed, to, easing, callback ) { - - // show any hidden elements after setting opacity to 0 - return this.filter( isHidden ).css( "opacity", 0 ).show() - - // animate to the value specified - .end().animate({ opacity: to }, speed, easing, callback ); - }, - animate: function( prop, speed, easing, callback ) { - var empty = jQuery.isEmptyObject( prop ), - optall = jQuery.speed( speed, easing, callback ), - doAnimation = function() { - // Operate on a copy of prop so per-property easing won't be lost - var anim = Animation( this, jQuery.extend( {}, prop ), optall ); - - // Empty animations, or finishing resolves immediately - if ( empty || jQuery._data( this, "finish" ) ) { - anim.stop( true ); - } - }; - doAnimation.finish = doAnimation; - - return empty || optall.queue === false ? - this.each( doAnimation ) : - this.queue( optall.queue, doAnimation ); - }, - stop: function( type, clearQueue, gotoEnd ) { - var stopQueue = function( hooks ) { - var stop = hooks.stop; - delete hooks.stop; - stop( gotoEnd ); - }; - - if ( typeof type !== "string" ) { - gotoEnd = clearQueue; - clearQueue = type; - type = undefined; - } - if ( clearQueue && type !== false ) { - this.queue( type || "fx", [] ); - } - - return this.each(function() { - var dequeue = true, - index = type != null && type + "queueHooks", - timers = jQuery.timers, - data = jQuery._data( this ); - - if ( index ) { - if ( data[ index ] && data[ index ].stop ) { - stopQueue( data[ index ] ); - } - } else { - for ( index in data ) { - if ( data[ index ] && data[ index ].stop && rrun.test( index ) ) { - stopQueue( data[ index ] ); - } - } - } - - for ( index = timers.length; index--; ) { - if ( timers[ index ].elem === this && (type == null || timers[ index ].queue === type) ) { - timers[ index ].anim.stop( gotoEnd ); - dequeue = false; - timers.splice( index, 1 ); - } - } - - // start the next in the queue if the last step wasn't forced - // timers currently will call their complete callbacks, which will dequeue - // but only if they were gotoEnd - if ( dequeue || !gotoEnd ) { - jQuery.dequeue( this, type ); - } - }); - }, - finish: function( type ) { - if ( type !== false ) { - type = type || "fx"; - } - return this.each(function() { - var index, - data = jQuery._data( this ), - queue = data[ type + "queue" ], - hooks = data[ type + "queueHooks" ], - timers = jQuery.timers, - length = queue ? queue.length : 0; - - // enable finishing flag on private data - data.finish = true; - - // empty the queue first - jQuery.queue( this, type, [] ); - - if ( hooks && hooks.stop ) { - hooks.stop.call( this, true ); - } - - // look for any active animations, and finish them - for ( index = timers.length; index--; ) { - if ( timers[ index ].elem === this && timers[ index ].queue === type ) { - timers[ index ].anim.stop( true ); - timers.splice( index, 1 ); - } - } - - // look for any animations in the old queue and finish them - for ( index = 0; index < length; index++ ) { - if ( queue[ index ] && queue[ index ].finish ) { - queue[ index ].finish.call( this ); - } - } - - // turn off finishing flag - delete data.finish; - }); - } -}); - -jQuery.each([ "toggle", "show", "hide" ], function( i, name ) { - var cssFn = jQuery.fn[ name ]; - jQuery.fn[ name ] = function( speed, easing, callback ) { - return speed == null || typeof speed === "boolean" ? - cssFn.apply( this, arguments ) : - this.animate( genFx( name, true ), speed, easing, callback ); - }; -}); - -// Generate shortcuts for custom animations -jQuery.each({ - slideDown: genFx("show"), - slideUp: genFx("hide"), - slideToggle: genFx("toggle"), - fadeIn: { opacity: "show" }, - fadeOut: { opacity: "hide" }, - fadeToggle: { opacity: "toggle" } -}, function( name, props ) { - jQuery.fn[ name ] = function( speed, easing, callback ) { - return this.animate( props, speed, easing, callback ); - }; -}); - -jQuery.timers = []; -jQuery.fx.tick = function() { - var timer, - timers = jQuery.timers, - i = 0; - - fxNow = jQuery.now(); - - for ( ; i < timers.length; i++ ) { - timer = timers[ i ]; - // Checks the timer has not already been removed - if ( !timer() && timers[ i ] === timer ) { - timers.splice( i--, 1 ); - } - } - - if ( !timers.length ) { - jQuery.fx.stop(); - } - fxNow = undefined; -}; - -jQuery.fx.timer = function( timer ) { - jQuery.timers.push( timer ); - if ( timer() ) { - jQuery.fx.start(); - } else { - jQuery.timers.pop(); - } -}; - -jQuery.fx.interval = 13; - -jQuery.fx.start = function() { - if ( !timerId ) { - timerId = setInterval( jQuery.fx.tick, jQuery.fx.interval ); - } -}; - -jQuery.fx.stop = function() { - clearInterval( timerId ); - timerId = null; -}; - -jQuery.fx.speeds = { - slow: 600, - fast: 200, - // Default speed - _default: 400 -}; - - -// Based off of the plugin by Clint Helfers, with permission. -// http://blindsignals.com/index.php/2009/07/jquery-delay/ -jQuery.fn.delay = function( time, type ) { - time = jQuery.fx ? jQuery.fx.speeds[ time ] || time : time; - type = type || "fx"; - - return this.queue( type, function( next, hooks ) { - var timeout = setTimeout( next, time ); - hooks.stop = function() { - clearTimeout( timeout ); - }; - }); -}; - - -(function() { - // Minified: var a,b,c,d,e - var input, div, select, a, opt; - - // Setup - div = document.createElement( "div" ); - div.setAttribute( "className", "t" ); - div.innerHTML = " <link/><table></table><a href='/a'>a</a><input type='checkbox'/>"; - a = div.getElementsByTagName("a")[ 0 ]; - - // First batch of tests. - select = document.createElement("select"); - opt = select.appendChild( document.createElement("option") ); - input = div.getElementsByTagName("input")[ 0 ]; - - a.style.cssText = "top:1px"; - - // Test setAttribute on camelCase class. If it works, we need attrFixes when doing get/setAttribute (ie6/7) - support.getSetAttribute = div.className !== "t"; - - // Get the style information from getAttribute - // (IE uses .cssText instead) - support.style = /top/.test( a.getAttribute("style") ); - - // Make sure that URLs aren't manipulated - // (IE normalizes it by default) - support.hrefNormalized = a.getAttribute("href") === "/a"; - - // Check the default checkbox/radio value ("" on WebKit; "on" elsewhere) - support.checkOn = !!input.value; - - // Make sure that a selected-by-default option has a working selected property. - // (WebKit defaults to false instead of true, IE too, if it's in an optgroup) - support.optSelected = opt.selected; - - // Tests for enctype support on a form (#6743) - support.enctype = !!document.createElement("form").enctype; - - // Make sure that the options inside disabled selects aren't marked as disabled - // (WebKit marks them as disabled) - select.disabled = true; - support.optDisabled = !opt.disabled; - - // Support: IE8 only - // Check if we can trust getAttribute("value") - input = document.createElement( "input" ); - input.setAttribute( "value", "" ); - support.input = input.getAttribute( "value" ) === ""; - - // Check if an input maintains its value after becoming a radio - input.value = "t"; - input.setAttribute( "type", "radio" ); - support.radioValue = input.value === "t"; -})(); - - -var rreturn = /\r/g; - -jQuery.fn.extend({ - val: function( value ) { - var hooks, ret, isFunction, - elem = this[0]; - - if ( !arguments.length ) { - if ( elem ) { - hooks = jQuery.valHooks[ elem.type ] || jQuery.valHooks[ elem.nodeName.toLowerCase() ]; - - if ( hooks && "get" in hooks && (ret = hooks.get( elem, "value" )) !== undefined ) { - return ret; - } - - ret = elem.value; - - return typeof ret === "string" ? - // handle most common string cases - ret.replace(rreturn, "") : - // handle cases where value is null/undef or number - ret == null ? "" : ret; - } - - return; - } - - isFunction = jQuery.isFunction( value ); - - return this.each(function( i ) { - var val; - - if ( this.nodeType !== 1 ) { - return; - } - - if ( isFunction ) { - val = value.call( this, i, jQuery( this ).val() ); - } else { - val = value; - } - - // Treat null/undefined as ""; convert numbers to string - if ( val == null ) { - val = ""; - } else if ( typeof val === "number" ) { - val += ""; - } else if ( jQuery.isArray( val ) ) { - val = jQuery.map( val, function( value ) { - return value == null ? "" : value + ""; - }); - } - - hooks = jQuery.valHooks[ this.type ] || jQuery.valHooks[ this.nodeName.toLowerCase() ]; - - // If set returns undefined, fall back to normal setting - if ( !hooks || !("set" in hooks) || hooks.set( this, val, "value" ) === undefined ) { - this.value = val; - } - }); - } -}); - -jQuery.extend({ - valHooks: { - option: { - get: function( elem ) { - var val = jQuery.find.attr( elem, "value" ); - return val != null ? - val : - // Support: IE10-11+ - // option.text throws exceptions (#14686, #14858) - jQuery.trim( jQuery.text( elem ) ); - } - }, - select: { - get: function( elem ) { - var value, option, - options = elem.options, - index = elem.selectedIndex, - one = elem.type === "select-one" || index < 0, - values = one ? null : [], - max = one ? index + 1 : options.length, - i = index < 0 ? - max : - one ? index : 0; - - // Loop through all the selected options - for ( ; i < max; i++ ) { - option = options[ i ]; - - // oldIE doesn't update selected after form reset (#2551) - if ( ( option.selected || i === index ) && - // Don't return options that are disabled or in a disabled optgroup - ( support.optDisabled ? !option.disabled : option.getAttribute("disabled") === null ) && - ( !option.parentNode.disabled || !jQuery.nodeName( option.parentNode, "optgroup" ) ) ) { - - // Get the specific value for the option - value = jQuery( option ).val(); - - // We don't need an array for one selects - if ( one ) { - return value; - } - - // Multi-Selects return an array - values.push( value ); - } - } - - return values; - }, - - set: function( elem, value ) { - var optionSet, option, - options = elem.options, - values = jQuery.makeArray( value ), - i = options.length; - - while ( i-- ) { - option = options[ i ]; - - if ( jQuery.inArray( jQuery.valHooks.option.get( option ), values ) >= 0 ) { - - // Support: IE6 - // When new option element is added to select box we need to - // force reflow of newly added node in order to workaround delay - // of initialization properties - try { - option.selected = optionSet = true; - - } catch ( _ ) { - - // Will be executed only in IE6 - option.scrollHeight; - } - - } else { - option.selected = false; - } - } - - // Force browsers to behave consistently when non-matching value is set - if ( !optionSet ) { - elem.selectedIndex = -1; - } - - return options; - } - } - } -}); - -// Radios and checkboxes getter/setter -jQuery.each([ "radio", "checkbox" ], function() { - jQuery.valHooks[ this ] = { - set: function( elem, value ) { - if ( jQuery.isArray( value ) ) { - return ( elem.checked = jQuery.inArray( jQuery(elem).val(), value ) >= 0 ); - } - } - }; - if ( !support.checkOn ) { - jQuery.valHooks[ this ].get = function( elem ) { - // Support: Webkit - // "" is returned instead of "on" if a value isn't specified - return elem.getAttribute("value") === null ? "on" : elem.value; - }; - } -}); - - - - -var nodeHook, boolHook, - attrHandle = jQuery.expr.attrHandle, - ruseDefault = /^(?:checked|selected)$/i, - getSetAttribute = support.getSetAttribute, - getSetInput = support.input; - -jQuery.fn.extend({ - attr: function( name, value ) { - return access( this, jQuery.attr, name, value, arguments.length > 1 ); - }, - - removeAttr: function( name ) { - return this.each(function() { - jQuery.removeAttr( this, name ); - }); - } -}); - -jQuery.extend({ - attr: function( elem, name, value ) { - var hooks, ret, - nType = elem.nodeType; - - // don't get/set attributes on text, comment and attribute nodes - if ( !elem || nType === 3 || nType === 8 || nType === 2 ) { - return; - } - - // Fallback to prop when attributes are not supported - if ( typeof elem.getAttribute === strundefined ) { - return jQuery.prop( elem, name, value ); - } - - // All attributes are lowercase - // Grab necessary hook if one is defined - if ( nType !== 1 || !jQuery.isXMLDoc( elem ) ) { - name = name.toLowerCase(); - hooks = jQuery.attrHooks[ name ] || - ( jQuery.expr.match.bool.test( name ) ? boolHook : nodeHook ); - } - - if ( value !== undefined ) { - - if ( value === null ) { - jQuery.removeAttr( elem, name ); - - } else if ( hooks && "set" in hooks && (ret = hooks.set( elem, value, name )) !== undefined ) { - return ret; - - } else { - elem.setAttribute( name, value + "" ); - return value; - } - - } else if ( hooks && "get" in hooks && (ret = hooks.get( elem, name )) !== null ) { - return ret; - - } else { - ret = jQuery.find.attr( elem, name ); - - // Non-existent attributes return null, we normalize to undefined - return ret == null ? - undefined : - ret; - } - }, - - removeAttr: function( elem, value ) { - var name, propName, - i = 0, - attrNames = value && value.match( rnotwhite ); - - if ( attrNames && elem.nodeType === 1 ) { - while ( (name = attrNames[i++]) ) { - propName = jQuery.propFix[ name ] || name; - - // Boolean attributes get special treatment (#10870) - if ( jQuery.expr.match.bool.test( name ) ) { - // Set corresponding property to false - if ( getSetInput && getSetAttribute || !ruseDefault.test( name ) ) { - elem[ propName ] = false; - // Support: IE<9 - // Also clear defaultChecked/defaultSelected (if appropriate) - } else { - elem[ jQuery.camelCase( "default-" + name ) ] = - elem[ propName ] = false; - } - - // See #9699 for explanation of this approach (setting first, then removal) - } else { - jQuery.attr( elem, name, "" ); - } - - elem.removeAttribute( getSetAttribute ? name : propName ); - } - } - }, - - attrHooks: { - type: { - set: function( elem, value ) { - if ( !support.radioValue && value === "radio" && jQuery.nodeName(elem, "input") ) { - // Setting the type on a radio button after the value resets the value in IE6-9 - // Reset value to default in case type is set after value during creation - var val = elem.value; - elem.setAttribute( "type", value ); - if ( val ) { - elem.value = val; - } - return value; - } - } - } - } -}); - -// Hook for boolean attributes -boolHook = { - set: function( elem, value, name ) { - if ( value === false ) { - // Remove boolean attributes when set to false - jQuery.removeAttr( elem, name ); - } else if ( getSetInput && getSetAttribute || !ruseDefault.test( name ) ) { - // IE<8 needs the *property* name - elem.setAttribute( !getSetAttribute && jQuery.propFix[ name ] || name, name ); - - // Use defaultChecked and defaultSelected for oldIE - } else { - elem[ jQuery.camelCase( "default-" + name ) ] = elem[ name ] = true; - } - - return name; - } -}; - -// Retrieve booleans specially -jQuery.each( jQuery.expr.match.bool.source.match( /\w+/g ), function( i, name ) { - - var getter = attrHandle[ name ] || jQuery.find.attr; - - attrHandle[ name ] = getSetInput && getSetAttribute || !ruseDefault.test( name ) ? - function( elem, name, isXML ) { - var ret, handle; - if ( !isXML ) { - // Avoid an infinite loop by temporarily removing this function from the getter - handle = attrHandle[ name ]; - attrHandle[ name ] = ret; - ret = getter( elem, name, isXML ) != null ? - name.toLowerCase() : - null; - attrHandle[ name ] = handle; - } - return ret; - } : - function( elem, name, isXML ) { - if ( !isXML ) { - return elem[ jQuery.camelCase( "default-" + name ) ] ? - name.toLowerCase() : - null; - } - }; -}); - -// fix oldIE attroperties -if ( !getSetInput || !getSetAttribute ) { - jQuery.attrHooks.value = { - set: function( elem, value, name ) { - if ( jQuery.nodeName( elem, "input" ) ) { - // Does not return so that setAttribute is also used - elem.defaultValue = value; - } else { - // Use nodeHook if defined (#1954); otherwise setAttribute is fine - return nodeHook && nodeHook.set( elem, value, name ); - } - } - }; -} - -// IE6/7 do not support getting/setting some attributes with get/setAttribute -if ( !getSetAttribute ) { - - // Use this for any attribute in IE6/7 - // This fixes almost every IE6/7 issue - nodeHook = { - set: function( elem, value, name ) { - // Set the existing or create a new attribute node - var ret = elem.getAttributeNode( name ); - if ( !ret ) { - elem.setAttributeNode( - (ret = elem.ownerDocument.createAttribute( name )) - ); - } - - ret.value = value += ""; - - // Break association with cloned elements by also using setAttribute (#9646) - if ( name === "value" || value === elem.getAttribute( name ) ) { - return value; - } - } - }; - - // Some attributes are constructed with empty-string values when not defined - attrHandle.id = attrHandle.name = attrHandle.coords = - function( elem, name, isXML ) { - var ret; - if ( !isXML ) { - return (ret = elem.getAttributeNode( name )) && ret.value !== "" ? - ret.value : - null; - } - }; - - // Fixing value retrieval on a button requires this module - jQuery.valHooks.button = { - get: function( elem, name ) { - var ret = elem.getAttributeNode( name ); - if ( ret && ret.specified ) { - return ret.value; - } - }, - set: nodeHook.set - }; - - // Set contenteditable to false on removals(#10429) - // Setting to empty string throws an error as an invalid value - jQuery.attrHooks.contenteditable = { - set: function( elem, value, name ) { - nodeHook.set( elem, value === "" ? false : value, name ); - } - }; - - // Set width and height to auto instead of 0 on empty string( Bug #8150 ) - // This is for removals - jQuery.each([ "width", "height" ], function( i, name ) { - jQuery.attrHooks[ name ] = { - set: function( elem, value ) { - if ( value === "" ) { - elem.setAttribute( name, "auto" ); - return value; - } - } - }; - }); -} - -if ( !support.style ) { - jQuery.attrHooks.style = { - get: function( elem ) { - // Return undefined in the case of empty string - // Note: IE uppercases css property names, but if we were to .toLowerCase() - // .cssText, that would destroy case senstitivity in URL's, like in "background" - return elem.style.cssText || undefined; - }, - set: function( elem, value ) { - return ( elem.style.cssText = value + "" ); - } - }; -} - - - - -var rfocusable = /^(?:input|select|textarea|button|object)$/i, - rclickable = /^(?:a|area)$/i; - -jQuery.fn.extend({ - prop: function( name, value ) { - return access( this, jQuery.prop, name, value, arguments.length > 1 ); - }, - - removeProp: function( name ) { - name = jQuery.propFix[ name ] || name; - return this.each(function() { - // try/catch handles cases where IE balks (such as removing a property on window) - try { - this[ name ] = undefined; - delete this[ name ]; - } catch( e ) {} - }); - } -}); - -jQuery.extend({ - propFix: { - "for": "htmlFor", - "class": "className" - }, - - prop: function( elem, name, value ) { - var ret, hooks, notxml, - nType = elem.nodeType; - - // don't get/set properties on text, comment and attribute nodes - if ( !elem || nType === 3 || nType === 8 || nType === 2 ) { - return; - } - - notxml = nType !== 1 || !jQuery.isXMLDoc( elem ); - - if ( notxml ) { - // Fix name and attach hooks - name = jQuery.propFix[ name ] || name; - hooks = jQuery.propHooks[ name ]; - } - - if ( value !== undefined ) { - return hooks && "set" in hooks && (ret = hooks.set( elem, value, name )) !== undefined ? - ret : - ( elem[ name ] = value ); - - } else { - return hooks && "get" in hooks && (ret = hooks.get( elem, name )) !== null ? - ret : - elem[ name ]; - } - }, - - propHooks: { - tabIndex: { - get: function( elem ) { - // elem.tabIndex doesn't always return the correct value when it hasn't been explicitly set - // http://fluidproject.org/blog/2008/01/09/getting-setting-and-removing-tabindex-values-with-javascript/ - // Use proper attribute retrieval(#12072) - var tabindex = jQuery.find.attr( elem, "tabindex" ); - - return tabindex ? - parseInt( tabindex, 10 ) : - rfocusable.test( elem.nodeName ) || rclickable.test( elem.nodeName ) && elem.href ? - 0 : - -1; - } - } - } -}); - -// Some attributes require a special call on IE -// http://msdn.microsoft.com/en-us/library/ms536429%28VS.85%29.aspx -if ( !support.hrefNormalized ) { - // href/src property should get the full normalized URL (#10299/#12915) - jQuery.each([ "href", "src" ], function( i, name ) { - jQuery.propHooks[ name ] = { - get: function( elem ) { - return elem.getAttribute( name, 4 ); - } - }; - }); -} - -// Support: Safari, IE9+ -// mis-reports the default selected property of an option -// Accessing the parent's selectedIndex property fixes it -if ( !support.optSelected ) { - jQuery.propHooks.selected = { - get: function( elem ) { - var parent = elem.parentNode; - - if ( parent ) { - parent.selectedIndex; - - // Make sure that it also works with optgroups, see #5701 - if ( parent.parentNode ) { - parent.parentNode.selectedIndex; - } - } - return null; - } - }; -} - -jQuery.each([ - "tabIndex", - "readOnly", - "maxLength", - "cellSpacing", - "cellPadding", - "rowSpan", - "colSpan", - "useMap", - "frameBorder", - "contentEditable" -], function() { - jQuery.propFix[ this.toLowerCase() ] = this; -}); - -// IE6/7 call enctype encoding -if ( !support.enctype ) { - jQuery.propFix.enctype = "encoding"; -} - - - - -var rclass = /[\t\r\n\f]/g; - -jQuery.fn.extend({ - addClass: function( value ) { - var classes, elem, cur, clazz, j, finalValue, - i = 0, - len = this.length, - proceed = typeof value === "string" && value; - - if ( jQuery.isFunction( value ) ) { - return this.each(function( j ) { - jQuery( this ).addClass( value.call( this, j, this.className ) ); - }); - } - - if ( proceed ) { - // The disjunction here is for better compressibility (see removeClass) - classes = ( value || "" ).match( rnotwhite ) || []; - - for ( ; i < len; i++ ) { - elem = this[ i ]; - cur = elem.nodeType === 1 && ( elem.className ? - ( " " + elem.className + " " ).replace( rclass, " " ) : - " " - ); - - if ( cur ) { - j = 0; - while ( (clazz = classes[j++]) ) { - if ( cur.indexOf( " " + clazz + " " ) < 0 ) { - cur += clazz + " "; - } - } - - // only assign if different to avoid unneeded rendering. - finalValue = jQuery.trim( cur ); - if ( elem.className !== finalValue ) { - elem.className = finalValue; - } - } - } - } - - return this; - }, - - removeClass: function( value ) { - var classes, elem, cur, clazz, j, finalValue, - i = 0, - len = this.length, - proceed = arguments.length === 0 || typeof value === "string" && value; - - if ( jQuery.isFunction( value ) ) { - return this.each(function( j ) { - jQuery( this ).removeClass( value.call( this, j, this.className ) ); - }); - } - if ( proceed ) { - classes = ( value || "" ).match( rnotwhite ) || []; - - for ( ; i < len; i++ ) { - elem = this[ i ]; - // This expression is here for better compressibility (see addClass) - cur = elem.nodeType === 1 && ( elem.className ? - ( " " + elem.className + " " ).replace( rclass, " " ) : - "" - ); - - if ( cur ) { - j = 0; - while ( (clazz = classes[j++]) ) { - // Remove *all* instances - while ( cur.indexOf( " " + clazz + " " ) >= 0 ) { - cur = cur.replace( " " + clazz + " ", " " ); - } - } - - // only assign if different to avoid unneeded rendering. - finalValue = value ? jQuery.trim( cur ) : ""; - if ( elem.className !== finalValue ) { - elem.className = finalValue; - } - } - } - } - - return this; - }, - - toggleClass: function( value, stateVal ) { - var type = typeof value; - - if ( typeof stateVal === "boolean" && type === "string" ) { - return stateVal ? this.addClass( value ) : this.removeClass( value ); - } - - if ( jQuery.isFunction( value ) ) { - return this.each(function( i ) { - jQuery( this ).toggleClass( value.call(this, i, this.className, stateVal), stateVal ); - }); - } - - return this.each(function() { - if ( type === "string" ) { - // toggle individual class names - var className, - i = 0, - self = jQuery( this ), - classNames = value.match( rnotwhite ) || []; - - while ( (className = classNames[ i++ ]) ) { - // check each className given, space separated list - if ( self.hasClass( className ) ) { - self.removeClass( className ); - } else { - self.addClass( className ); - } - } - - // Toggle whole class name - } else if ( type === strundefined || type === "boolean" ) { - if ( this.className ) { - // store className if set - jQuery._data( this, "__className__", this.className ); - } - - // If the element has a class name or if we're passed "false", - // then remove the whole classname (if there was one, the above saved it). - // Otherwise bring back whatever was previously saved (if anything), - // falling back to the empty string if nothing was stored. - this.className = this.className || value === false ? "" : jQuery._data( this, "__className__" ) || ""; - } - }); - }, - - hasClass: function( selector ) { - var className = " " + selector + " ", - i = 0, - l = this.length; - for ( ; i < l; i++ ) { - if ( this[i].nodeType === 1 && (" " + this[i].className + " ").replace(rclass, " ").indexOf( className ) >= 0 ) { - return true; - } - } - - return false; - } -}); - - - - -// Return jQuery for attributes-only inclusion - - -jQuery.each( ("blur focus focusin focusout load resize scroll unload click dblclick " + - "mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave " + - "change select submit keydown keypress keyup error contextmenu").split(" "), function( i, name ) { - - // Handle event binding - jQuery.fn[ name ] = function( data, fn ) { - return arguments.length > 0 ? - this.on( name, null, data, fn ) : - this.trigger( name ); - }; -}); - -jQuery.fn.extend({ - hover: function( fnOver, fnOut ) { - return this.mouseenter( fnOver ).mouseleave( fnOut || fnOver ); - }, - - bind: function( types, data, fn ) { - return this.on( types, null, data, fn ); - }, - unbind: function( types, fn ) { - return this.off( types, null, fn ); - }, - - delegate: function( selector, types, data, fn ) { - return this.on( types, selector, data, fn ); - }, - undelegate: function( selector, types, fn ) { - // ( namespace ) or ( selector, types [, fn] ) - return arguments.length === 1 ? this.off( selector, "**" ) : this.off( types, selector || "**", fn ); - } -}); - - -var nonce = jQuery.now(); - -var rquery = (/\?/); - - - -var rvalidtokens = /(,)|(\[|{)|(}|])|"(?:[^"\\\r\n]|\\["\\\/bfnrt]|\\u[\da-fA-F]{4})*"\s*:?|true|false|null|-?(?!0\d)\d+(?:\.\d+|)(?:[eE][+-]?\d+|)/g; - -jQuery.parseJSON = function( data ) { - // Attempt to parse using the native JSON parser first - if ( window.JSON && window.JSON.parse ) { - // Support: Android 2.3 - // Workaround failure to string-cast null input - return window.JSON.parse( data + "" ); - } - - var requireNonComma, - depth = null, - str = jQuery.trim( data + "" ); - - // Guard against invalid (and possibly dangerous) input by ensuring that nothing remains - // after removing valid tokens - return str && !jQuery.trim( str.replace( rvalidtokens, function( token, comma, open, close ) { - - // Force termination if we see a misplaced comma - if ( requireNonComma && comma ) { - depth = 0; - } - - // Perform no more replacements after returning to outermost depth - if ( depth === 0 ) { - return token; - } - - // Commas must not follow "[", "{", or "," - requireNonComma = open || comma; - - // Determine new depth - // array/object open ("[" or "{"): depth += true - false (increment) - // array/object close ("]" or "}"): depth += false - true (decrement) - // other cases ("," or primitive): depth += true - true (numeric cast) - depth += !close - !open; - - // Remove this token - return ""; - }) ) ? - ( Function( "return " + str ) )() : - jQuery.error( "Invalid JSON: " + data ); -}; - - -// Cross-browser xml parsing -jQuery.parseXML = function( data ) { - var xml, tmp; - if ( !data || typeof data !== "string" ) { - return null; - } - try { - if ( window.DOMParser ) { // Standard - tmp = new DOMParser(); - xml = tmp.parseFromString( data, "text/xml" ); - } else { // IE - xml = new ActiveXObject( "Microsoft.XMLDOM" ); - xml.async = "false"; - xml.loadXML( data ); - } - } catch( e ) { - xml = undefined; - } - if ( !xml || !xml.documentElement || xml.getElementsByTagName( "parsererror" ).length ) { - jQuery.error( "Invalid XML: " + data ); - } - return xml; -}; - - -var - // Document location - ajaxLocParts, - ajaxLocation, - - rhash = /#.*$/, - rts = /([?&])_=[^&]*/, - rheaders = /^(.*?):[ \t]*([^\r\n]*)\r?$/mg, // IE leaves an \r character at EOL - // #7653, #8125, #8152: local protocol detection - rlocalProtocol = /^(?:about|app|app-storage|.+-extension|file|res|widget):$/, - rnoContent = /^(?:GET|HEAD)$/, - rprotocol = /^\/\//, - rurl = /^([\w.+-]+:)(?:\/\/(?:[^\/?#]*@|)([^\/?#:]*)(?::(\d+)|)|)/, - - /* Prefilters - * 1) They are useful to introduce custom dataTypes (see ajax/jsonp.js for an example) - * 2) These are called: - * - BEFORE asking for a transport - * - AFTER param serialization (s.data is a string if s.processData is true) - * 3) key is the dataType - * 4) the catchall symbol "*" can be used - * 5) execution will start with transport dataType and THEN continue down to "*" if needed - */ - prefilters = {}, - - /* Transports bindings - * 1) key is the dataType - * 2) the catchall symbol "*" can be used - * 3) selection will start with transport dataType and THEN go to "*" if needed - */ - transports = {}, - - // Avoid comment-prolog char sequence (#10098); must appease lint and evade compression - allTypes = "*/".concat("*"); - -// #8138, IE may throw an exception when accessing -// a field from window.location if document.domain has been set -try { - ajaxLocation = location.href; -} catch( e ) { - // Use the href attribute of an A element - // since IE will modify it given document.location - ajaxLocation = document.createElement( "a" ); - ajaxLocation.href = ""; - ajaxLocation = ajaxLocation.href; -} - -// Segment location into parts -ajaxLocParts = rurl.exec( ajaxLocation.toLowerCase() ) || []; - -// Base "constructor" for jQuery.ajaxPrefilter and jQuery.ajaxTransport -function addToPrefiltersOrTransports( structure ) { - - // dataTypeExpression is optional and defaults to "*" - return function( dataTypeExpression, func ) { - - if ( typeof dataTypeExpression !== "string" ) { - func = dataTypeExpression; - dataTypeExpression = "*"; - } - - var dataType, - i = 0, - dataTypes = dataTypeExpression.toLowerCase().match( rnotwhite ) || []; - - if ( jQuery.isFunction( func ) ) { - // For each dataType in the dataTypeExpression - while ( (dataType = dataTypes[i++]) ) { - // Prepend if requested - if ( dataType.charAt( 0 ) === "+" ) { - dataType = dataType.slice( 1 ) || "*"; - (structure[ dataType ] = structure[ dataType ] || []).unshift( func ); - - // Otherwise append - } else { - (structure[ dataType ] = structure[ dataType ] || []).push( func ); - } - } - } - }; -} - -// Base inspection function for prefilters and transports -function inspectPrefiltersOrTransports( structure, options, originalOptions, jqXHR ) { - - var inspected = {}, - seekingTransport = ( structure === transports ); - - function inspect( dataType ) { - var selected; - inspected[ dataType ] = true; - jQuery.each( structure[ dataType ] || [], function( _, prefilterOrFactory ) { - var dataTypeOrTransport = prefilterOrFactory( options, originalOptions, jqXHR ); - if ( typeof dataTypeOrTransport === "string" && !seekingTransport && !inspected[ dataTypeOrTransport ] ) { - options.dataTypes.unshift( dataTypeOrTransport ); - inspect( dataTypeOrTransport ); - return false; - } else if ( seekingTransport ) { - return !( selected = dataTypeOrTransport ); - } - }); - return selected; - } - - return inspect( options.dataTypes[ 0 ] ) || !inspected[ "*" ] && inspect( "*" ); -} - -// A special extend for ajax options -// that takes "flat" options (not to be deep extended) -// Fixes #9887 -function ajaxExtend( target, src ) { - var deep, key, - flatOptions = jQuery.ajaxSettings.flatOptions || {}; - - for ( key in src ) { - if ( src[ key ] !== undefined ) { - ( flatOptions[ key ] ? target : ( deep || (deep = {}) ) )[ key ] = src[ key ]; - } - } - if ( deep ) { - jQuery.extend( true, target, deep ); - } - - return target; -} - -/* Handles responses to an ajax request: - * - finds the right dataType (mediates between content-type and expected dataType) - * - returns the corresponding response - */ -function ajaxHandleResponses( s, jqXHR, responses ) { - var firstDataType, ct, finalDataType, type, - contents = s.contents, - dataTypes = s.dataTypes; - - // Remove auto dataType and get content-type in the process - while ( dataTypes[ 0 ] === "*" ) { - dataTypes.shift(); - if ( ct === undefined ) { - ct = s.mimeType || jqXHR.getResponseHeader("Content-Type"); - } - } - - // Check if we're dealing with a known content-type - if ( ct ) { - for ( type in contents ) { - if ( contents[ type ] && contents[ type ].test( ct ) ) { - dataTypes.unshift( type ); - break; - } - } - } - - // Check to see if we have a response for the expected dataType - if ( dataTypes[ 0 ] in responses ) { - finalDataType = dataTypes[ 0 ]; - } else { - // Try convertible dataTypes - for ( type in responses ) { - if ( !dataTypes[ 0 ] || s.converters[ type + " " + dataTypes[0] ] ) { - finalDataType = type; - break; - } - if ( !firstDataType ) { - firstDataType = type; - } - } - // Or just use first one - finalDataType = finalDataType || firstDataType; - } - - // If we found a dataType - // We add the dataType to the list if needed - // and return the corresponding response - if ( finalDataType ) { - if ( finalDataType !== dataTypes[ 0 ] ) { - dataTypes.unshift( finalDataType ); - } - return responses[ finalDataType ]; - } -} - -/* Chain conversions given the request and the original response - * Also sets the responseXXX fields on the jqXHR instance - */ -function ajaxConvert( s, response, jqXHR, isSuccess ) { - var conv2, current, conv, tmp, prev, - converters = {}, - // Work with a copy of dataTypes in case we need to modify it for conversion - dataTypes = s.dataTypes.slice(); - - // Create converters map with lowercased keys - if ( dataTypes[ 1 ] ) { - for ( conv in s.converters ) { - converters[ conv.toLowerCase() ] = s.converters[ conv ]; - } - } - - current = dataTypes.shift(); - - // Convert to each sequential dataType - while ( current ) { - - if ( s.responseFields[ current ] ) { - jqXHR[ s.responseFields[ current ] ] = response; - } - - // Apply the dataFilter if provided - if ( !prev && isSuccess && s.dataFilter ) { - response = s.dataFilter( response, s.dataType ); - } - - prev = current; - current = dataTypes.shift(); - - if ( current ) { - - // There's only work to do if current dataType is non-auto - if ( current === "*" ) { - - current = prev; - - // Convert response if prev dataType is non-auto and differs from current - } else if ( prev !== "*" && prev !== current ) { - - // Seek a direct converter - conv = converters[ prev + " " + current ] || converters[ "* " + current ]; - - // If none found, seek a pair - if ( !conv ) { - for ( conv2 in converters ) { - - // If conv2 outputs current - tmp = conv2.split( " " ); - if ( tmp[ 1 ] === current ) { - - // If prev can be converted to accepted input - conv = converters[ prev + " " + tmp[ 0 ] ] || - converters[ "* " + tmp[ 0 ] ]; - if ( conv ) { - // Condense equivalence converters - if ( conv === true ) { - conv = converters[ conv2 ]; - - // Otherwise, insert the intermediate dataType - } else if ( converters[ conv2 ] !== true ) { - current = tmp[ 0 ]; - dataTypes.unshift( tmp[ 1 ] ); - } - break; - } - } - } - } - - // Apply converter (if not an equivalence) - if ( conv !== true ) { - - // Unless errors are allowed to bubble, catch and return them - if ( conv && s[ "throws" ] ) { - response = conv( response ); - } else { - try { - response = conv( response ); - } catch ( e ) { - return { state: "parsererror", error: conv ? e : "No conversion from " + prev + " to " + current }; - } - } - } - } - } - } - - return { state: "success", data: response }; -} - -jQuery.extend({ - - // Counter for holding the number of active queries - active: 0, - - // Last-Modified header cache for next request - lastModified: {}, - etag: {}, - - ajaxSettings: { - url: ajaxLocation, - type: "GET", - isLocal: rlocalProtocol.test( ajaxLocParts[ 1 ] ), - global: true, - processData: true, - async: true, - contentType: "application/x-www-form-urlencoded; charset=UTF-8", - /* - timeout: 0, - data: null, - dataType: null, - username: null, - password: null, - cache: null, - throws: false, - traditional: false, - headers: {}, - */ - - accepts: { - "*": allTypes, - text: "text/plain", - html: "text/html", - xml: "application/xml, text/xml", - json: "application/json, text/javascript" - }, - - contents: { - xml: /xml/, - html: /html/, - json: /json/ - }, - - responseFields: { - xml: "responseXML", - text: "responseText", - json: "responseJSON" - }, - - // Data converters - // Keys separate source (or catchall "*") and destination types with a single space - converters: { - - // Convert anything to text - "* text": String, - - // Text to html (true = no transformation) - "text html": true, - - // Evaluate text as a json expression - "text json": jQuery.parseJSON, - - // Parse text as xml - "text xml": jQuery.parseXML - }, - - // For options that shouldn't be deep extended: - // you can add your own custom options here if - // and when you create one that shouldn't be - // deep extended (see ajaxExtend) - flatOptions: { - url: true, - context: true - } - }, - - // Creates a full fledged settings object into target - // with both ajaxSettings and settings fields. - // If target is omitted, writes into ajaxSettings. - ajaxSetup: function( target, settings ) { - return settings ? - - // Building a settings object - ajaxExtend( ajaxExtend( target, jQuery.ajaxSettings ), settings ) : - - // Extending ajaxSettings - ajaxExtend( jQuery.ajaxSettings, target ); - }, - - ajaxPrefilter: addToPrefiltersOrTransports( prefilters ), - ajaxTransport: addToPrefiltersOrTransports( transports ), - - // Main method - ajax: function( url, options ) { - - // If url is an object, simulate pre-1.5 signature - if ( typeof url === "object" ) { - options = url; - url = undefined; - } - - // Force options to be an object - options = options || {}; - - var // Cross-domain detection vars - parts, - // Loop variable - i, - // URL without anti-cache param - cacheURL, - // Response headers as string - responseHeadersString, - // timeout handle - timeoutTimer, - - // To know if global events are to be dispatched - fireGlobals, - - transport, - // Response headers - responseHeaders, - // Create the final options object - s = jQuery.ajaxSetup( {}, options ), - // Callbacks context - callbackContext = s.context || s, - // Context for global events is callbackContext if it is a DOM node or jQuery collection - globalEventContext = s.context && ( callbackContext.nodeType || callbackContext.jquery ) ? - jQuery( callbackContext ) : - jQuery.event, - // Deferreds - deferred = jQuery.Deferred(), - completeDeferred = jQuery.Callbacks("once memory"), - // Status-dependent callbacks - statusCode = s.statusCode || {}, - // Headers (they are sent all at once) - requestHeaders = {}, - requestHeadersNames = {}, - // The jqXHR state - state = 0, - // Default abort message - strAbort = "canceled", - // Fake xhr - jqXHR = { - readyState: 0, - - // Builds headers hashtable if needed - getResponseHeader: function( key ) { - var match; - if ( state === 2 ) { - if ( !responseHeaders ) { - responseHeaders = {}; - while ( (match = rheaders.exec( responseHeadersString )) ) { - responseHeaders[ match[1].toLowerCase() ] = match[ 2 ]; - } - } - match = responseHeaders[ key.toLowerCase() ]; - } - return match == null ? null : match; - }, - - // Raw string - getAllResponseHeaders: function() { - return state === 2 ? responseHeadersString : null; - }, - - // Caches the header - setRequestHeader: function( name, value ) { - var lname = name.toLowerCase(); - if ( !state ) { - name = requestHeadersNames[ lname ] = requestHeadersNames[ lname ] || name; - requestHeaders[ name ] = value; - } - return this; - }, - - // Overrides response content-type header - overrideMimeType: function( type ) { - if ( !state ) { - s.mimeType = type; - } - return this; - }, - - // Status-dependent callbacks - statusCode: function( map ) { - var code; - if ( map ) { - if ( state < 2 ) { - for ( code in map ) { - // Lazy-add the new callback in a way that preserves old ones - statusCode[ code ] = [ statusCode[ code ], map[ code ] ]; - } - } else { - // Execute the appropriate callbacks - jqXHR.always( map[ jqXHR.status ] ); - } - } - return this; - }, - - // Cancel the request - abort: function( statusText ) { - var finalText = statusText || strAbort; - if ( transport ) { - transport.abort( finalText ); - } - done( 0, finalText ); - return this; - } - }; - - // Attach deferreds - deferred.promise( jqXHR ).complete = completeDeferred.add; - jqXHR.success = jqXHR.done; - jqXHR.error = jqXHR.fail; - - // Remove hash character (#7531: and string promotion) - // Add protocol if not provided (#5866: IE7 issue with protocol-less urls) - // Handle falsy url in the settings object (#10093: consistency with old signature) - // We also use the url parameter if available - s.url = ( ( url || s.url || ajaxLocation ) + "" ).replace( rhash, "" ).replace( rprotocol, ajaxLocParts[ 1 ] + "//" ); - - // Alias method option to type as per ticket #12004 - s.type = options.method || options.type || s.method || s.type; - - // Extract dataTypes list - s.dataTypes = jQuery.trim( s.dataType || "*" ).toLowerCase().match( rnotwhite ) || [ "" ]; - - // A cross-domain request is in order when we have a protocol:host:port mismatch - if ( s.crossDomain == null ) { - parts = rurl.exec( s.url.toLowerCase() ); - s.crossDomain = !!( parts && - ( parts[ 1 ] !== ajaxLocParts[ 1 ] || parts[ 2 ] !== ajaxLocParts[ 2 ] || - ( parts[ 3 ] || ( parts[ 1 ] === "http:" ? "80" : "443" ) ) !== - ( ajaxLocParts[ 3 ] || ( ajaxLocParts[ 1 ] === "http:" ? "80" : "443" ) ) ) - ); - } - - // Convert data if not already a string - if ( s.data && s.processData && typeof s.data !== "string" ) { - s.data = jQuery.param( s.data, s.traditional ); - } - - // Apply prefilters - inspectPrefiltersOrTransports( prefilters, s, options, jqXHR ); - - // If request was aborted inside a prefilter, stop there - if ( state === 2 ) { - return jqXHR; - } - - // We can fire global events as of now if asked to - // Don't fire events if jQuery.event is undefined in an AMD-usage scenario (#15118) - fireGlobals = jQuery.event && s.global; - - // Watch for a new set of requests - if ( fireGlobals && jQuery.active++ === 0 ) { - jQuery.event.trigger("ajaxStart"); - } - - // Uppercase the type - s.type = s.type.toUpperCase(); - - // Determine if request has content - s.hasContent = !rnoContent.test( s.type ); - - // Save the URL in case we're toying with the If-Modified-Since - // and/or If-None-Match header later on - cacheURL = s.url; - - // More options handling for requests with no content - if ( !s.hasContent ) { - - // If data is available, append data to url - if ( s.data ) { - cacheURL = ( s.url += ( rquery.test( cacheURL ) ? "&" : "?" ) + s.data ); - // #9682: remove data so that it's not used in an eventual retry - delete s.data; - } - - // Add anti-cache in url if needed - if ( s.cache === false ) { - s.url = rts.test( cacheURL ) ? - - // If there is already a '_' parameter, set its value - cacheURL.replace( rts, "$1_=" + nonce++ ) : - - // Otherwise add one to the end - cacheURL + ( rquery.test( cacheURL ) ? "&" : "?" ) + "_=" + nonce++; - } - } - - // Set the If-Modified-Since and/or If-None-Match header, if in ifModified mode. - if ( s.ifModified ) { - if ( jQuery.lastModified[ cacheURL ] ) { - jqXHR.setRequestHeader( "If-Modified-Since", jQuery.lastModified[ cacheURL ] ); - } - if ( jQuery.etag[ cacheURL ] ) { - jqXHR.setRequestHeader( "If-None-Match", jQuery.etag[ cacheURL ] ); - } - } - - // Set the correct header, if data is being sent - if ( s.data && s.hasContent && s.contentType !== false || options.contentType ) { - jqXHR.setRequestHeader( "Content-Type", s.contentType ); - } - - // Set the Accepts header for the server, depending on the dataType - jqXHR.setRequestHeader( - "Accept", - s.dataTypes[ 0 ] && s.accepts[ s.dataTypes[0] ] ? - s.accepts[ s.dataTypes[0] ] + ( s.dataTypes[ 0 ] !== "*" ? ", " + allTypes + "; q=0.01" : "" ) : - s.accepts[ "*" ] - ); - - // Check for headers option - for ( i in s.headers ) { - jqXHR.setRequestHeader( i, s.headers[ i ] ); - } - - // Allow custom headers/mimetypes and early abort - if ( s.beforeSend && ( s.beforeSend.call( callbackContext, jqXHR, s ) === false || state === 2 ) ) { - // Abort if not done already and return - return jqXHR.abort(); - } - - // aborting is no longer a cancellation - strAbort = "abort"; - - // Install callbacks on deferreds - for ( i in { success: 1, error: 1, complete: 1 } ) { - jqXHR[ i ]( s[ i ] ); - } - - // Get transport - transport = inspectPrefiltersOrTransports( transports, s, options, jqXHR ); - - // If no transport, we auto-abort - if ( !transport ) { - done( -1, "No Transport" ); - } else { - jqXHR.readyState = 1; - - // Send global event - if ( fireGlobals ) { - globalEventContext.trigger( "ajaxSend", [ jqXHR, s ] ); - } - // Timeout - if ( s.async && s.timeout > 0 ) { - timeoutTimer = setTimeout(function() { - jqXHR.abort("timeout"); - }, s.timeout ); - } - - try { - state = 1; - transport.send( requestHeaders, done ); - } catch ( e ) { - // Propagate exception as error if not done - if ( state < 2 ) { - done( -1, e ); - // Simply rethrow otherwise - } else { - throw e; - } - } - } - - // Callback for when everything is done - function done( status, nativeStatusText, responses, headers ) { - var isSuccess, success, error, response, modified, - statusText = nativeStatusText; - - // Called once - if ( state === 2 ) { - return; - } - - // State is "done" now - state = 2; - - // Clear timeout if it exists - if ( timeoutTimer ) { - clearTimeout( timeoutTimer ); - } - - // Dereference transport for early garbage collection - // (no matter how long the jqXHR object will be used) - transport = undefined; - - // Cache response headers - responseHeadersString = headers || ""; - - // Set readyState - jqXHR.readyState = status > 0 ? 4 : 0; - - // Determine if successful - isSuccess = status >= 200 && status < 300 || status === 304; - - // Get response data - if ( responses ) { - response = ajaxHandleResponses( s, jqXHR, responses ); - } - - // Convert no matter what (that way responseXXX fields are always set) - response = ajaxConvert( s, response, jqXHR, isSuccess ); - - // If successful, handle type chaining - if ( isSuccess ) { - - // Set the If-Modified-Since and/or If-None-Match header, if in ifModified mode. - if ( s.ifModified ) { - modified = jqXHR.getResponseHeader("Last-Modified"); - if ( modified ) { - jQuery.lastModified[ cacheURL ] = modified; - } - modified = jqXHR.getResponseHeader("etag"); - if ( modified ) { - jQuery.etag[ cacheURL ] = modified; - } - } - - // if no content - if ( status === 204 || s.type === "HEAD" ) { - statusText = "nocontent"; - - // if not modified - } else if ( status === 304 ) { - statusText = "notmodified"; - - // If we have data, let's convert it - } else { - statusText = response.state; - success = response.data; - error = response.error; - isSuccess = !error; - } - } else { - // We extract error from statusText - // then normalize statusText and status for non-aborts - error = statusText; - if ( status || !statusText ) { - statusText = "error"; - if ( status < 0 ) { - status = 0; - } - } - } - - // Set data for the fake xhr object - jqXHR.status = status; - jqXHR.statusText = ( nativeStatusText || statusText ) + ""; - - // Success/Error - if ( isSuccess ) { - deferred.resolveWith( callbackContext, [ success, statusText, jqXHR ] ); - } else { - deferred.rejectWith( callbackContext, [ jqXHR, statusText, error ] ); - } - - // Status-dependent callbacks - jqXHR.statusCode( statusCode ); - statusCode = undefined; - - if ( fireGlobals ) { - globalEventContext.trigger( isSuccess ? "ajaxSuccess" : "ajaxError", - [ jqXHR, s, isSuccess ? success : error ] ); - } - - // Complete - completeDeferred.fireWith( callbackContext, [ jqXHR, statusText ] ); - - if ( fireGlobals ) { - globalEventContext.trigger( "ajaxComplete", [ jqXHR, s ] ); - // Handle the global AJAX counter - if ( !( --jQuery.active ) ) { - jQuery.event.trigger("ajaxStop"); - } - } - } - - return jqXHR; - }, - - getJSON: function( url, data, callback ) { - return jQuery.get( url, data, callback, "json" ); - }, - - getScript: function( url, callback ) { - return jQuery.get( url, undefined, callback, "script" ); - } -}); - -jQuery.each( [ "get", "post" ], function( i, method ) { - jQuery[ method ] = function( url, data, callback, type ) { - // shift arguments if data argument was omitted - if ( jQuery.isFunction( data ) ) { - type = type || callback; - callback = data; - data = undefined; - } - - return jQuery.ajax({ - url: url, - type: method, - dataType: type, - data: data, - success: callback - }); - }; -}); - - -jQuery._evalUrl = function( url ) { - return jQuery.ajax({ - url: url, - type: "GET", - dataType: "script", - async: false, - global: false, - "throws": true - }); -}; - - -jQuery.fn.extend({ - wrapAll: function( html ) { - if ( jQuery.isFunction( html ) ) { - return this.each(function(i) { - jQuery(this).wrapAll( html.call(this, i) ); - }); - } - - if ( this[0] ) { - // The elements to wrap the target around - var wrap = jQuery( html, this[0].ownerDocument ).eq(0).clone(true); - - if ( this[0].parentNode ) { - wrap.insertBefore( this[0] ); - } - - wrap.map(function() { - var elem = this; - - while ( elem.firstChild && elem.firstChild.nodeType === 1 ) { - elem = elem.firstChild; - } - - return elem; - }).append( this ); - } - - return this; - }, - - wrapInner: function( html ) { - if ( jQuery.isFunction( html ) ) { - return this.each(function(i) { - jQuery(this).wrapInner( html.call(this, i) ); - }); - } - - return this.each(function() { - var self = jQuery( this ), - contents = self.contents(); - - if ( contents.length ) { - contents.wrapAll( html ); - - } else { - self.append( html ); - } - }); - }, - - wrap: function( html ) { - var isFunction = jQuery.isFunction( html ); - - return this.each(function(i) { - jQuery( this ).wrapAll( isFunction ? html.call(this, i) : html ); - }); - }, - - unwrap: function() { - return this.parent().each(function() { - if ( !jQuery.nodeName( this, "body" ) ) { - jQuery( this ).replaceWith( this.childNodes ); - } - }).end(); - } -}); - - -jQuery.expr.filters.hidden = function( elem ) { - // Support: Opera <= 12.12 - // Opera reports offsetWidths and offsetHeights less than zero on some elements - return elem.offsetWidth <= 0 && elem.offsetHeight <= 0 || - (!support.reliableHiddenOffsets() && - ((elem.style && elem.style.display) || jQuery.css( elem, "display" )) === "none"); -}; - -jQuery.expr.filters.visible = function( elem ) { - return !jQuery.expr.filters.hidden( elem ); -}; - - - - -var r20 = /%20/g, - rbracket = /\[\]$/, - rCRLF = /\r?\n/g, - rsubmitterTypes = /^(?:submit|button|image|reset|file)$/i, - rsubmittable = /^(?:input|select|textarea|keygen)/i; - -function buildParams( prefix, obj, traditional, add ) { - var name; - - if ( jQuery.isArray( obj ) ) { - // Serialize array item. - jQuery.each( obj, function( i, v ) { - if ( traditional || rbracket.test( prefix ) ) { - // Treat each array item as a scalar. - add( prefix, v ); - - } else { - // Item is non-scalar (array or object), encode its numeric index. - buildParams( prefix + "[" + ( typeof v === "object" ? i : "" ) + "]", v, traditional, add ); - } - }); - - } else if ( !traditional && jQuery.type( obj ) === "object" ) { - // Serialize object item. - for ( name in obj ) { - buildParams( prefix + "[" + name + "]", obj[ name ], traditional, add ); - } - - } else { - // Serialize scalar item. - add( prefix, obj ); - } -} - -// Serialize an array of form elements or a set of -// key/values into a query string -jQuery.param = function( a, traditional ) { - var prefix, - s = [], - add = function( key, value ) { - // If value is a function, invoke it and return its value - value = jQuery.isFunction( value ) ? value() : ( value == null ? "" : value ); - s[ s.length ] = encodeURIComponent( key ) + "=" + encodeURIComponent( value ); - }; - - // Set traditional to true for jQuery <= 1.3.2 behavior. - if ( traditional === undefined ) { - traditional = jQuery.ajaxSettings && jQuery.ajaxSettings.traditional; - } - - // If an array was passed in, assume that it is an array of form elements. - if ( jQuery.isArray( a ) || ( a.jquery && !jQuery.isPlainObject( a ) ) ) { - // Serialize the form elements - jQuery.each( a, function() { - add( this.name, this.value ); - }); - - } else { - // If traditional, encode the "old" way (the way 1.3.2 or older - // did it), otherwise encode params recursively. - for ( prefix in a ) { - buildParams( prefix, a[ prefix ], traditional, add ); - } - } - - // Return the resulting serialization - return s.join( "&" ).replace( r20, "+" ); -}; - -jQuery.fn.extend({ - serialize: function() { - return jQuery.param( this.serializeArray() ); - }, - serializeArray: function() { - return this.map(function() { - // Can add propHook for "elements" to filter or add form elements - var elements = jQuery.prop( this, "elements" ); - return elements ? jQuery.makeArray( elements ) : this; - }) - .filter(function() { - var type = this.type; - // Use .is(":disabled") so that fieldset[disabled] works - return this.name && !jQuery( this ).is( ":disabled" ) && - rsubmittable.test( this.nodeName ) && !rsubmitterTypes.test( type ) && - ( this.checked || !rcheckableType.test( type ) ); - }) - .map(function( i, elem ) { - var val = jQuery( this ).val(); - - return val == null ? - null : - jQuery.isArray( val ) ? - jQuery.map( val, function( val ) { - return { name: elem.name, value: val.replace( rCRLF, "\r\n" ) }; - }) : - { name: elem.name, value: val.replace( rCRLF, "\r\n" ) }; - }).get(); - } -}); - - -// Create the request object -// (This is still attached to ajaxSettings for backward compatibility) -jQuery.ajaxSettings.xhr = window.ActiveXObject !== undefined ? - // Support: IE6+ - function() { - - // XHR cannot access local files, always use ActiveX for that case - return !this.isLocal && - - // Support: IE7-8 - // oldIE XHR does not support non-RFC2616 methods (#13240) - // See http://msdn.microsoft.com/en-us/library/ie/ms536648(v=vs.85).aspx - // and http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9 - // Although this check for six methods instead of eight - // since IE also does not support "trace" and "connect" - /^(get|post|head|put|delete|options)$/i.test( this.type ) && - - createStandardXHR() || createActiveXHR(); - } : - // For all other browsers, use the standard XMLHttpRequest object - createStandardXHR; - -var xhrId = 0, - xhrCallbacks = {}, - xhrSupported = jQuery.ajaxSettings.xhr(); - -// Support: IE<10 -// Open requests must be manually aborted on unload (#5280) -// See https://support.microsoft.com/kb/2856746 for more info -if ( window.attachEvent ) { - window.attachEvent( "onunload", function() { - for ( var key in xhrCallbacks ) { - xhrCallbacks[ key ]( undefined, true ); - } - }); -} - -// Determine support properties -support.cors = !!xhrSupported && ( "withCredentials" in xhrSupported ); -xhrSupported = support.ajax = !!xhrSupported; - -// Create transport if the browser can provide an xhr -if ( xhrSupported ) { - - jQuery.ajaxTransport(function( options ) { - // Cross domain only allowed if supported through XMLHttpRequest - if ( !options.crossDomain || support.cors ) { - - var callback; - - return { - send: function( headers, complete ) { - var i, - xhr = options.xhr(), - id = ++xhrId; - - // Open the socket - xhr.open( options.type, options.url, options.async, options.username, options.password ); - - // Apply custom fields if provided - if ( options.xhrFields ) { - for ( i in options.xhrFields ) { - xhr[ i ] = options.xhrFields[ i ]; - } - } - - // Override mime type if needed - if ( options.mimeType && xhr.overrideMimeType ) { - xhr.overrideMimeType( options.mimeType ); - } - - // X-Requested-With header - // For cross-domain requests, seeing as conditions for a preflight are - // akin to a jigsaw puzzle, we simply never set it to be sure. - // (it can always be set on a per-request basis or even using ajaxSetup) - // For same-domain requests, won't change header if already provided. - if ( !options.crossDomain && !headers["X-Requested-With"] ) { - headers["X-Requested-With"] = "XMLHttpRequest"; - } - - // Set headers - for ( i in headers ) { - // Support: IE<9 - // IE's ActiveXObject throws a 'Type Mismatch' exception when setting - // request header to a null-value. - // - // To keep consistent with other XHR implementations, cast the value - // to string and ignore `undefined`. - if ( headers[ i ] !== undefined ) { - xhr.setRequestHeader( i, headers[ i ] + "" ); - } - } - - // Do send the request - // This may raise an exception which is actually - // handled in jQuery.ajax (so no try/catch here) - xhr.send( ( options.hasContent && options.data ) || null ); - - // Listener - callback = function( _, isAbort ) { - var status, statusText, responses; - - // Was never called and is aborted or complete - if ( callback && ( isAbort || xhr.readyState === 4 ) ) { - // Clean up - delete xhrCallbacks[ id ]; - callback = undefined; - xhr.onreadystatechange = jQuery.noop; - - // Abort manually if needed - if ( isAbort ) { - if ( xhr.readyState !== 4 ) { - xhr.abort(); - } - } else { - responses = {}; - status = xhr.status; - - // Support: IE<10 - // Accessing binary-data responseText throws an exception - // (#11426) - if ( typeof xhr.responseText === "string" ) { - responses.text = xhr.responseText; - } - - // Firefox throws an exception when accessing - // statusText for faulty cross-domain requests - try { - statusText = xhr.statusText; - } catch( e ) { - // We normalize with Webkit giving an empty statusText - statusText = ""; - } - - // Filter status for non standard behaviors - - // If the request is local and we have data: assume a success - // (success with no data won't get notified, that's the best we - // can do given current implementations) - if ( !status && options.isLocal && !options.crossDomain ) { - status = responses.text ? 200 : 404; - // IE - #1450: sometimes returns 1223 when it should be 204 - } else if ( status === 1223 ) { - status = 204; - } - } - } - - // Call complete if needed - if ( responses ) { - complete( status, statusText, responses, xhr.getAllResponseHeaders() ); - } - }; - - if ( !options.async ) { - // if we're in sync mode we fire the callback - callback(); - } else if ( xhr.readyState === 4 ) { - // (IE6 & IE7) if it's in cache and has been - // retrieved directly we need to fire the callback - setTimeout( callback ); - } else { - // Add to the list of active xhr callbacks - xhr.onreadystatechange = xhrCallbacks[ id ] = callback; - } - }, - - abort: function() { - if ( callback ) { - callback( undefined, true ); - } - } - }; - } - }); -} - -// Functions to create xhrs -function createStandardXHR() { - try { - return new window.XMLHttpRequest(); - } catch( e ) {} -} - -function createActiveXHR() { - try { - return new window.ActiveXObject( "Microsoft.XMLHTTP" ); - } catch( e ) {} -} - - - - -// Install script dataType -jQuery.ajaxSetup({ - accepts: { - script: "text/javascript, application/javascript, application/ecmascript, application/x-ecmascript" - }, - contents: { - script: /(?:java|ecma)script/ - }, - converters: { - "text script": function( text ) { - jQuery.globalEval( text ); - return text; - } - } -}); - -// Handle cache's special case and global -jQuery.ajaxPrefilter( "script", function( s ) { - if ( s.cache === undefined ) { - s.cache = false; - } - if ( s.crossDomain ) { - s.type = "GET"; - s.global = false; - } -}); - -// Bind script tag hack transport -jQuery.ajaxTransport( "script", function(s) { - - // This transport only deals with cross domain requests - if ( s.crossDomain ) { - - var script, - head = document.head || jQuery("head")[0] || document.documentElement; - - return { - - send: function( _, callback ) { - - script = document.createElement("script"); - - script.async = true; - - if ( s.scriptCharset ) { - script.charset = s.scriptCharset; - } - - script.src = s.url; - - // Attach handlers for all browsers - script.onload = script.onreadystatechange = function( _, isAbort ) { - - if ( isAbort || !script.readyState || /loaded|complete/.test( script.readyState ) ) { - - // Handle memory leak in IE - script.onload = script.onreadystatechange = null; - - // Remove the script - if ( script.parentNode ) { - script.parentNode.removeChild( script ); - } - - // Dereference the script - script = null; - - // Callback if not abort - if ( !isAbort ) { - callback( 200, "success" ); - } - } - }; - - // Circumvent IE6 bugs with base elements (#2709 and #4378) by prepending - // Use native DOM manipulation to avoid our domManip AJAX trickery - head.insertBefore( script, head.firstChild ); - }, - - abort: function() { - if ( script ) { - script.onload( undefined, true ); - } - } - }; - } -}); - - - - -var oldCallbacks = [], - rjsonp = /(=)\?(?=&|$)|\?\?/; - -// Default jsonp settings -jQuery.ajaxSetup({ - jsonp: "callback", - jsonpCallback: function() { - var callback = oldCallbacks.pop() || ( jQuery.expando + "_" + ( nonce++ ) ); - this[ callback ] = true; - return callback; - } -}); - -// Detect, normalize options and install callbacks for jsonp requests -jQuery.ajaxPrefilter( "json jsonp", function( s, originalSettings, jqXHR ) { - - var callbackName, overwritten, responseContainer, - jsonProp = s.jsonp !== false && ( rjsonp.test( s.url ) ? - "url" : - typeof s.data === "string" && !( s.contentType || "" ).indexOf("application/x-www-form-urlencoded") && rjsonp.test( s.data ) && "data" - ); - - // Handle iff the expected data type is "jsonp" or we have a parameter to set - if ( jsonProp || s.dataTypes[ 0 ] === "jsonp" ) { - - // Get callback name, remembering preexisting value associated with it - callbackName = s.jsonpCallback = jQuery.isFunction( s.jsonpCallback ) ? - s.jsonpCallback() : - s.jsonpCallback; - - // Insert callback into url or form data - if ( jsonProp ) { - s[ jsonProp ] = s[ jsonProp ].replace( rjsonp, "$1" + callbackName ); - } else if ( s.jsonp !== false ) { - s.url += ( rquery.test( s.url ) ? "&" : "?" ) + s.jsonp + "=" + callbackName; - } - - // Use data converter to retrieve json after script execution - s.converters["script json"] = function() { - if ( !responseContainer ) { - jQuery.error( callbackName + " was not called" ); - } - return responseContainer[ 0 ]; - }; - - // force json dataType - s.dataTypes[ 0 ] = "json"; - - // Install callback - overwritten = window[ callbackName ]; - window[ callbackName ] = function() { - responseContainer = arguments; - }; - - // Clean-up function (fires after converters) - jqXHR.always(function() { - // Restore preexisting value - window[ callbackName ] = overwritten; - - // Save back as free - if ( s[ callbackName ] ) { - // make sure that re-using the options doesn't screw things around - s.jsonpCallback = originalSettings.jsonpCallback; - - // save the callback name for future use - oldCallbacks.push( callbackName ); - } - - // Call if it was a function and we have a response - if ( responseContainer && jQuery.isFunction( overwritten ) ) { - overwritten( responseContainer[ 0 ] ); - } - - responseContainer = overwritten = undefined; - }); - - // Delegate to script - return "script"; - } -}); - - - - -// data: string of html -// context (optional): If specified, the fragment will be created in this context, defaults to document -// keepScripts (optional): If true, will include scripts passed in the html string -jQuery.parseHTML = function( data, context, keepScripts ) { - if ( !data || typeof data !== "string" ) { - return null; - } - if ( typeof context === "boolean" ) { - keepScripts = context; - context = false; - } - context = context || document; - - var parsed = rsingleTag.exec( data ), - scripts = !keepScripts && []; - - // Single tag - if ( parsed ) { - return [ context.createElement( parsed[1] ) ]; - } - - parsed = jQuery.buildFragment( [ data ], context, scripts ); - - if ( scripts && scripts.length ) { - jQuery( scripts ).remove(); - } - - return jQuery.merge( [], parsed.childNodes ); -}; - - -// Keep a copy of the old load method -var _load = jQuery.fn.load; - -/** - * Load a url into a page - */ -jQuery.fn.load = function( url, params, callback ) { - if ( typeof url !== "string" && _load ) { - return _load.apply( this, arguments ); - } - - var selector, response, type, - self = this, - off = url.indexOf(" "); - - if ( off >= 0 ) { - selector = jQuery.trim( url.slice( off, url.length ) ); - url = url.slice( 0, off ); - } - - // If it's a function - if ( jQuery.isFunction( params ) ) { - - // We assume that it's the callback - callback = params; - params = undefined; - - // Otherwise, build a param string - } else if ( params && typeof params === "object" ) { - type = "POST"; - } - - // If we have elements to modify, make the request - if ( self.length > 0 ) { - jQuery.ajax({ - url: url, - - // if "type" variable is undefined, then "GET" method will be used - type: type, - dataType: "html", - data: params - }).done(function( responseText ) { - - // Save response for use in complete callback - response = arguments; - - self.html( selector ? - - // If a selector was specified, locate the right elements in a dummy div - // Exclude scripts to avoid IE 'Permission Denied' errors - jQuery("<div>").append( jQuery.parseHTML( responseText ) ).find( selector ) : - - // Otherwise use the full result - responseText ); - - }).complete( callback && function( jqXHR, status ) { - self.each( callback, response || [ jqXHR.responseText, status, jqXHR ] ); - }); - } - - return this; -}; - - - - -// Attach a bunch of functions for handling common AJAX events -jQuery.each( [ "ajaxStart", "ajaxStop", "ajaxComplete", "ajaxError", "ajaxSuccess", "ajaxSend" ], function( i, type ) { - jQuery.fn[ type ] = function( fn ) { - return this.on( type, fn ); - }; -}); - - - - -jQuery.expr.filters.animated = function( elem ) { - return jQuery.grep(jQuery.timers, function( fn ) { - return elem === fn.elem; - }).length; -}; - - - - - -var docElem = window.document.documentElement; - -/** - * Gets a window from an element - */ -function getWindow( elem ) { - return jQuery.isWindow( elem ) ? - elem : - elem.nodeType === 9 ? - elem.defaultView || elem.parentWindow : - false; -} - -jQuery.offset = { - setOffset: function( elem, options, i ) { - var curPosition, curLeft, curCSSTop, curTop, curOffset, curCSSLeft, calculatePosition, - position = jQuery.css( elem, "position" ), - curElem = jQuery( elem ), - props = {}; - - // set position first, in-case top/left are set even on static elem - if ( position === "static" ) { - elem.style.position = "relative"; - } - - curOffset = curElem.offset(); - curCSSTop = jQuery.css( elem, "top" ); - curCSSLeft = jQuery.css( elem, "left" ); - calculatePosition = ( position === "absolute" || position === "fixed" ) && - jQuery.inArray("auto", [ curCSSTop, curCSSLeft ] ) > -1; - - // need to be able to calculate position if either top or left is auto and position is either absolute or fixed - if ( calculatePosition ) { - curPosition = curElem.position(); - curTop = curPosition.top; - curLeft = curPosition.left; - } else { - curTop = parseFloat( curCSSTop ) || 0; - curLeft = parseFloat( curCSSLeft ) || 0; - } - - if ( jQuery.isFunction( options ) ) { - options = options.call( elem, i, curOffset ); - } - - if ( options.top != null ) { - props.top = ( options.top - curOffset.top ) + curTop; - } - if ( options.left != null ) { - props.left = ( options.left - curOffset.left ) + curLeft; - } - - if ( "using" in options ) { - options.using.call( elem, props ); - } else { - curElem.css( props ); - } - } -}; - -jQuery.fn.extend({ - offset: function( options ) { - if ( arguments.length ) { - return options === undefined ? - this : - this.each(function( i ) { - jQuery.offset.setOffset( this, options, i ); - }); - } - - var docElem, win, - box = { top: 0, left: 0 }, - elem = this[ 0 ], - doc = elem && elem.ownerDocument; - - if ( !doc ) { - return; - } - - docElem = doc.documentElement; - - // Make sure it's not a disconnected DOM node - if ( !jQuery.contains( docElem, elem ) ) { - return box; - } - - // If we don't have gBCR, just use 0,0 rather than error - // BlackBerry 5, iOS 3 (original iPhone) - if ( typeof elem.getBoundingClientRect !== strundefined ) { - box = elem.getBoundingClientRect(); - } - win = getWindow( doc ); - return { - top: box.top + ( win.pageYOffset || docElem.scrollTop ) - ( docElem.clientTop || 0 ), - left: box.left + ( win.pageXOffset || docElem.scrollLeft ) - ( docElem.clientLeft || 0 ) - }; - }, - - position: function() { - if ( !this[ 0 ] ) { - return; - } - - var offsetParent, offset, - parentOffset = { top: 0, left: 0 }, - elem = this[ 0 ]; - - // fixed elements are offset from window (parentOffset = {top:0, left: 0}, because it is its only offset parent - if ( jQuery.css( elem, "position" ) === "fixed" ) { - // we assume that getBoundingClientRect is available when computed position is fixed - offset = elem.getBoundingClientRect(); - } else { - // Get *real* offsetParent - offsetParent = this.offsetParent(); - - // Get correct offsets - offset = this.offset(); - if ( !jQuery.nodeName( offsetParent[ 0 ], "html" ) ) { - parentOffset = offsetParent.offset(); - } - - // Add offsetParent borders - parentOffset.top += jQuery.css( offsetParent[ 0 ], "borderTopWidth", true ); - parentOffset.left += jQuery.css( offsetParent[ 0 ], "borderLeftWidth", true ); - } - - // Subtract parent offsets and element margins - // note: when an element has margin: auto the offsetLeft and marginLeft - // are the same in Safari causing offset.left to incorrectly be 0 - return { - top: offset.top - parentOffset.top - jQuery.css( elem, "marginTop", true ), - left: offset.left - parentOffset.left - jQuery.css( elem, "marginLeft", true) - }; - }, - - offsetParent: function() { - return this.map(function() { - var offsetParent = this.offsetParent || docElem; - - while ( offsetParent && ( !jQuery.nodeName( offsetParent, "html" ) && jQuery.css( offsetParent, "position" ) === "static" ) ) { - offsetParent = offsetParent.offsetParent; - } - return offsetParent || docElem; - }); - } -}); - -// Create scrollLeft and scrollTop methods -jQuery.each( { scrollLeft: "pageXOffset", scrollTop: "pageYOffset" }, function( method, prop ) { - var top = /Y/.test( prop ); - - jQuery.fn[ method ] = function( val ) { - return access( this, function( elem, method, val ) { - var win = getWindow( elem ); - - if ( val === undefined ) { - return win ? (prop in win) ? win[ prop ] : - win.document.documentElement[ method ] : - elem[ method ]; - } - - if ( win ) { - win.scrollTo( - !top ? val : jQuery( win ).scrollLeft(), - top ? val : jQuery( win ).scrollTop() - ); - - } else { - elem[ method ] = val; - } - }, method, val, arguments.length, null ); - }; -}); - -// Add the top/left cssHooks using jQuery.fn.position -// Webkit bug: https://bugs.webkit.org/show_bug.cgi?id=29084 -// getComputedStyle returns percent when specified for top/left/bottom/right -// rather than make the css module depend on the offset module, we just check for it here -jQuery.each( [ "top", "left" ], function( i, prop ) { - jQuery.cssHooks[ prop ] = addGetHookIf( support.pixelPosition, - function( elem, computed ) { - if ( computed ) { - computed = curCSS( elem, prop ); - // if curCSS returns percentage, fallback to offset - return rnumnonpx.test( computed ) ? - jQuery( elem ).position()[ prop ] + "px" : - computed; - } - } - ); -}); - - -// Create innerHeight, innerWidth, height, width, outerHeight and outerWidth methods -jQuery.each( { Height: "height", Width: "width" }, function( name, type ) { - jQuery.each( { padding: "inner" + name, content: type, "": "outer" + name }, function( defaultExtra, funcName ) { - // margin is only for outerHeight, outerWidth - jQuery.fn[ funcName ] = function( margin, value ) { - var chainable = arguments.length && ( defaultExtra || typeof margin !== "boolean" ), - extra = defaultExtra || ( margin === true || value === true ? "margin" : "border" ); - - return access( this, function( elem, type, value ) { - var doc; - - if ( jQuery.isWindow( elem ) ) { - // As of 5/8/2012 this will yield incorrect results for Mobile Safari, but there - // isn't a whole lot we can do. See pull request at this URL for discussion: - // https://github.com/jquery/jquery/pull/764 - return elem.document.documentElement[ "client" + name ]; - } - - // Get document width or height - if ( elem.nodeType === 9 ) { - doc = elem.documentElement; - - // Either scroll[Width/Height] or offset[Width/Height] or client[Width/Height], whichever is greatest - // unfortunately, this causes bug #3838 in IE6/8 only, but there is currently no good, small way to fix it. - return Math.max( - elem.body[ "scroll" + name ], doc[ "scroll" + name ], - elem.body[ "offset" + name ], doc[ "offset" + name ], - doc[ "client" + name ] - ); - } - - return value === undefined ? - // Get width or height on the element, requesting but not forcing parseFloat - jQuery.css( elem, type, extra ) : - - // Set width or height on the element - jQuery.style( elem, type, value, extra ); - }, type, chainable ? margin : undefined, chainable, null ); - }; - }); -}); - - -// The number of elements contained in the matched element set -jQuery.fn.size = function() { - return this.length; -}; - -jQuery.fn.andSelf = jQuery.fn.addBack; - - - - -// Register as a named AMD module, since jQuery can be concatenated with other -// files that may use define, but not via a proper concatenation script that -// understands anonymous AMD modules. A named AMD is safest and most robust -// way to register. Lowercase jquery is used because AMD module names are -// derived from file names, and jQuery is normally delivered in a lowercase -// file name. Do this after creating the global so that if an AMD module wants -// to call noConflict to hide this version of jQuery, it will work. - -// Note that for maximum portability, libraries that are not jQuery should -// declare themselves as anonymous modules, and avoid setting a global if an -// AMD loader is present. jQuery is a special case. For more information, see -// https://github.com/jrburke/requirejs/wiki/Updating-existing-libraries#wiki-anon - -if ( typeof define === "function" && define.amd ) { - define( "jquery", [], function() { - return jQuery; - }); -} - - - - -var - // Map over jQuery in case of overwrite - _jQuery = window.jQuery, - - // Map over the $ in case of overwrite - _$ = window.$; - -jQuery.noConflict = function( deep ) { - if ( window.$ === jQuery ) { - window.$ = _$; - } - - if ( deep && window.jQuery === jQuery ) { - window.jQuery = _jQuery; - } - - return jQuery; -}; - -// Expose jQuery and $ identifiers, even in -// AMD (#7102#comment:10, https://github.com/jquery/jquery/pull/557) -// and CommonJS for browser emulators (#13566) -if ( typeof noGlobal === strundefined ) { - window.jQuery = window.$ = jQuery; -} - - - - -return jQuery; - -})); diff --git a/src/UI/JsLibraries/jquery.knob.js b/src/UI/JsLibraries/jquery.knob.js deleted file mode 100644 index a657773d4..000000000 --- a/src/UI/JsLibraries/jquery.knob.js +++ /dev/null @@ -1,672 +0,0 @@ -/*!jQuery Knob*/ -/** - * Downward compatible, touchable dial - * - * Version: 1.2.0 (15/07/2012) - * Requires: jQuery v1.7+ - * - * Copyright (c) 2012 Anthony Terrien - * Under MIT and GPL licenses: - * http://www.opensource.org/licenses/mit-license.php - * http://www.gnu.org/licenses/gpl.html - * - * Thanks to vor, eskimoblood, spiffistan, FabrizioC - */ -(function($) { - - /** - * Kontrol library - */ - "use strict"; - - /** - * Definition of globals and core - */ - var k = {}, // kontrol - max = Math.max, - min = Math.min; - - k.c = {}; - k.c.d = $(document); - k.c.t = function (e) { - return e.originalEvent.touches.length - 1; - }; - - /** - * Kontrol Object - * - * Definition of an abstract UI control - * - * Each concrete component must call this one. - * <code> - * k.o.call(this); - * </code> - */ - k.o = function () { - var s = this; - - this.o = null; // array of options - this.$ = null; // jQuery wrapped element - this.i = null; // mixed HTMLInputElement or array of HTMLInputElement - this.g = null; // 2D graphics context for 'pre-rendering' - this.v = null; // value ; mixed array or integer - this.cv = null; // change value ; not commited value - this.x = 0; // canvas x position - this.y = 0; // canvas y position - this.$c = null; // jQuery canvas element - this.c = null; // rendered canvas context - this.t = 0; // touches index - this.isInit = false; - this.fgColor = null; // main color - this.pColor = null; // previous color - this.dH = null; // draw hook - this.cH = null; // change hook - this.eH = null; // cancel hook - this.rH = null; // release hook - - this.run = function () { - var cf = function (e, conf) { - var k; - for (k in conf) { - s.o[k] = conf[k]; - } - s.init(); - s._configure() - ._draw(); - }; - - if(this.$.data('kontroled')) return; - this.$.data('kontroled', true); - - this.extend(); - this.o = $.extend( - { - // Config - min : this.$.data('min') || 0, - max : this.$.data('max') || 100, - stopper : true, - readOnly : this.$.data('readonly'), - - // UI - cursor : (this.$.data('cursor') === true && 30) - || this.$.data('cursor') - || 0, - thickness : this.$.data('thickness') || 0.35, - lineCap : this.$.data('linecap') || 'butt', - width : this.$.data('width') || 200, - height : this.$.data('height') || 200, - displayInput : this.$.data('displayinput') == null || this.$.data('displayinput'), - displayPrevious : this.$.data('displayprevious'), - fgColor : this.$.data('fgcolor') || '#87CEEB', - inputColor: this.$.data('inputcolor') || this.$.data('fgcolor') || '#87CEEB', - inline : false, - step : this.$.data('step') || 1, - - // Hooks - draw : null, // function () {} - change : null, // function (value) {} - cancel : null, // function () {} - release : null, // function (value) {} - error : null // function () {} - }, this.o - ); - - // routing value - if(this.$.is('fieldset')) { - - // fieldset = array of integer - this.v = {}; - this.i = this.$.find('input') - this.i.each(function(k) { - var $this = $(this); - s.i[k] = $this; - s.v[k] = $this.val(); - - $this.bind( - 'change' - , function () { - var val = {}; - val[k] = $this.val(); - s.val(val); - } - ); - }); - this.$.find('legend').remove(); - - } else { - // input = integer - this.i = this.$; - this.v = this.$.val(); - (this.v == '') && (this.v = this.o.min); - - this.$.bind( - 'change' - , function () { - s.val(s._validate(s.$.val())); - } - ); - } - - (!this.o.displayInput) && this.$.hide(); - - this.$c = $('<canvas width="' + - this.o.width + 'px" height="' + - this.o.height + 'px"></canvas>'); - - this.c = this.$c[0].getContext? this.$c[0].getContext('2d') : null; - - if (!this.c) { - this.o.error && this.o.error(); - return; - } - - this.$ - .wrap($('<div style="' + (this.o.inline ? 'display:inline;' : '') + - 'width:' + this.o.width + 'px;height:' + - this.o.height + 'px;"></div>')) - .before(this.$c); - - if (this.v instanceof Object) { - this.cv = {}; - this.copy(this.v, this.cv); - } else { - this.cv = this.v; - } - - this.$ - .bind("configure", cf) - .parent() - .bind("configure", cf); - - this._listen() - ._configure() - ._xy() - .init(); - - this.isInit = true; - - this._draw(); - - return this; - }; - - this._draw = function () { - - // canvas pre-rendering - var d = true, - c = document.createElement('canvas'); - - c.width = s.o.width; - c.height = s.o.height; - s.g = c.getContext('2d'); - - s.clear(); - - s.dH - && (d = s.dH()); - - (d !== false) && s.draw(); - - s.c.drawImage(c, 0, 0); - c = null; - }; - - this._touch = function (e) { - - var touchMove = function (e) { - - var v = s.xy2val( - e.originalEvent.touches[s.t].pageX, - e.originalEvent.touches[s.t].pageY - ); - - if (v == s.cv) return; - - if ( - s.cH - && (s.cH(v) === false) - ) return; - - - s.change(s._validate(v)); - s.$.trigger('change', v); - s._draw(); - }; - - // get touches index - this.t = k.c.t(e); - - // First touch - touchMove(e); - - // Touch events listeners - k.c.d - .bind("touchmove.k", touchMove) - .bind( - "touchend.k" - , function () { - k.c.d.unbind('touchmove.k touchend.k'); - - if ( - s.rH - && (s.rH(s.cv) === false) - ) return; - - s.val(s.cv); - } - ); - - return this; - }; - - this._mouse = function (e) { - - var mouseMove = function (e) { - var v = s.xy2val(e.pageX, e.pageY); - if (v == s.cv) return; - - if ( - s.cH - && (s.cH(v) === false) - ) return; - - s.change(s._validate(v)); - s.$.trigger('change', v); - s._draw(); - }; - - // First click - mouseMove(e); - - // Mouse events listeners - k.c.d - .bind("mousemove.k", mouseMove) - .bind( - // Escape key cancel current change - "keyup.k" - , function (e) { - if (e.keyCode === 27) { - k.c.d.unbind("mouseup.k mousemove.k keyup.k"); - - if ( - s.eH - && (s.eH() === false) - ) return; - - s.cancel(); - } - } - ) - .bind( - "mouseup.k" - , function (e) { - k.c.d.unbind('mousemove.k mouseup.k keyup.k'); - - if ( - s.rH - && (s.rH(s.cv) === false) - ) return; - - s.val(s.cv); - } - ); - - return this; - }; - - this._xy = function () { - var o = this.$c.offset(); - this.x = o.left; - this.y = o.top; - return this; - }; - - this._listen = function () { - - if (!this.o.readOnly) { - this.$c - .bind( - "mousedown" - , function (e) { - e.preventDefault(); - s._xy()._mouse(e); - } - ) - .bind( - "touchstart" - , function (e) { - e.preventDefault(); - s._xy()._touch(e); - } - ); - this.listen(); - } else { - this.$.attr('readonly', 'readonly'); - } - - return this; - }; - - this._configure = function () { - - // Hooks - if (this.o.draw) this.dH = this.o.draw; - if (this.o.change) this.cH = this.o.change; - if (this.o.cancel) this.eH = this.o.cancel; - if (this.o.release) this.rH = this.o.release; - - if (this.o.displayPrevious) { - this.pColor = this.h2rgba(this.o.fgColor, "0.4"); - this.fgColor = this.h2rgba(this.o.fgColor, "0.6"); - } else { - this.fgColor = this.o.fgColor; - } - - return this; - }; - - this._clear = function () { - this.$c[0].width = this.$c[0].width; - }; - - this._validate = function(v) { - return (~~ (((v < 0) ? -0.5 : 0.5) + (v/this.o.step))) * this.o.step; - }; - - // Abstract methods - this.listen = function () {}; // on start, one time - this.extend = function () {}; // each time configure triggered - this.init = function () {}; // each time configure triggered - this.change = function (v) {}; // on change - this.val = function (v) {}; // on release - this.xy2val = function (x, y) {}; // - this.draw = function () {}; // on change / on release - this.clear = function () { this._clear(); }; - - // Utils - this.h2rgba = function (h, a) { - var rgb; - h = h.substring(1,7) - rgb = [parseInt(h.substring(0,2),16) - ,parseInt(h.substring(2,4),16) - ,parseInt(h.substring(4,6),16)]; - return "rgba(" + rgb[0] + "," + rgb[1] + "," + rgb[2] + "," + a + ")"; - }; - - this.copy = function (f, t) { - for (var i in f) { t[i] = f[i]; } - }; - }; - - - /** - * k.Dial - */ - k.Dial = function () { - k.o.call(this); - - this.startAngle = null; - this.xy = null; - this.radius = null; - this.lineWidth = null; - this.cursorExt = null; - this.w2 = null; - this.PI2 = 2*Math.PI; - - this.extend = function () { - this.o = $.extend( - { - bgColor : this.$.data('bgcolor') || '#EEEEEE', - angleOffset : this.$.data('angleoffset') || 0, - angleArc : this.$.data('anglearc') || 360, - inline : true - }, this.o - ); - }; - - this.val = function (v) { - if (null != v) { - this.cv = this.o.stopper ? max(min(v, this.o.max), this.o.min) : v; - this.v = this.cv; - this.$.val(this.v); - this._draw(); - } else { - return this.v; - } - }; - - this.xy2val = function (x, y) { - var a, ret; - - a = Math.atan2( - x - (this.x + this.w2) - , - (y - this.y - this.w2) - ) - this.angleOffset; - - if(this.angleArc != this.PI2 && (a < 0) && (a > -0.5)) { - // if isset angleArc option, set to min if .5 under min - a = 0; - } else if (a < 0) { - a += this.PI2; - } - - ret = ~~ (0.5 + (a * (this.o.max - this.o.min) / this.angleArc)) - + this.o.min; - - this.o.stopper - && (ret = max(min(ret, this.o.max), this.o.min)); - - return ret; - }; - - this.listen = function () { - // bind MouseWheel - var s = this, - mw = function (e) { - e.preventDefault(); - var ori = e.originalEvent - ,deltaX = ori.detail || ori.wheelDeltaX - ,deltaY = ori.detail || ori.wheelDeltaY - ,v = parseInt(s.$.val()) + (deltaX>0 || deltaY>0 ? s.o.step : deltaX<0 || deltaY<0 ? -s.o.step : 0); - - if ( - s.cH - && (s.cH(v) === false) - ) return; - - s.val(v); - s.$.trigger('change', v); - } - , kval, to, m = 1, kv = {37:-s.o.step, 38:s.o.step, 39:s.o.step, 40:-s.o.step}; - - this.$ - .bind( - "keydown" - ,function (e) { - var kc = e.keyCode; - - // numpad support - if(kc >= 96 && kc <= 105) { - kc = e.keyCode = kc - 48; - } - - kval = parseInt(String.fromCharCode(kc)); - - if (isNaN(kval)) { - - (kc !== 13) // enter - && (kc !== 8) // bs - && (kc !== 9) // tab - && (kc !== 189) // - - && e.preventDefault(); - - // arrows - if ($.inArray(kc,[37,38,39,40]) > -1) { - e.preventDefault(); - - var v = parseInt(s.$.val()) + kv[kc] * m; - - s.o.stopper - && (v = max(min(v, s.o.max), s.o.min)); - - s.change(v); - s.$.trigger('change', v); - s._draw(); - - // long time keydown speed-up - to = window.setTimeout( - function () { m*=2; } - ,30 - ); - } - } - } - ) - .bind( - "keyup" - ,function (e) { - if (isNaN(kval)) { - if (to) { - window.clearTimeout(to); - to = null; - m = 1; - s.val(s.$.val()); - } - } else { - // kval postcond - (s.$.val() > s.o.max && s.$.val(s.o.max)) - || (s.$.val() < s.o.min && s.$.val(s.o.min)); - } - - } - ); - - this.$c.bind("mousewheel DOMMouseScroll", mw); - this.$.bind("mousewheel DOMMouseScroll", mw) - }; - - this.init = function () { - - if ( - this.v < this.o.min - || this.v > this.o.max - ) this.v = this.o.min; - - this.$.val(this.v); - this.w2 = this.o.width / 2; - this.cursorExt = this.o.cursor / 100; - this.xy = this.w2; - this.lineWidth = this.xy * this.o.thickness; - this.lineCap = this.o.lineCap; - this.radius = this.xy - this.lineWidth / 2; - - this.o.angleOffset - && (this.o.angleOffset = isNaN(this.o.angleOffset) ? 0 : this.o.angleOffset); - - this.o.angleArc - && (this.o.angleArc = isNaN(this.o.angleArc) ? this.PI2 : this.o.angleArc); - - // deg to rad - this.angleOffset = this.o.angleOffset * Math.PI / 180; - this.angleArc = this.o.angleArc * Math.PI / 180; - - // compute start and end angles - this.startAngle = 1.5 * Math.PI + this.angleOffset; - this.endAngle = 1.5 * Math.PI + this.angleOffset + this.angleArc; - - var s = max( - String(Math.abs(this.o.max)).length - , String(Math.abs(this.o.min)).length - , 2 - ) + 2; - - this.o.displayInput - && this.i.css({ - 'width' : ((this.o.width / 2 + 4) >> 0) + 'px' - ,'height' : ((this.o.width / 3) >> 0) + 'px' - ,'position' : 'absolute' - ,'vertical-align' : 'middle' - ,'margin-top' : ((this.o.width / 3) >> 0) + 'px' - ,'margin-left' : '-' + ((this.o.width * 3 / 4 + 2) >> 0) + 'px' - ,'border' : 0 - ,'background' : 'none' - ,'font' : 'bold ' + ((this.o.width / s) >> 0) + 'px Arial' - ,'text-align' : 'center' - ,'color' : this.o.inputColor || this.o.fgColor - ,'padding' : '0px' - ,'-webkit-appearance': 'none' - }) - || this.i.css({ - 'width' : '0px' - ,'visibility' : 'hidden' - }); - }; - - this.change = function (v) { - this.cv = v; - this.$.val(v); - }; - - this.angle = function (v) { - return (v - this.o.min) * this.angleArc / (this.o.max - this.o.min); - }; - - this.draw = function () { - - var c = this.g, // context - a = this.angle(this.cv) // Angle - , sat = this.startAngle // Start angle - , eat = sat + a // End angle - , sa, ea // Previous angles - , r = 1; - - c.lineWidth = this.lineWidth; - - c.lineCap = this.lineCap; - - this.o.cursor - && (sat = eat - this.cursorExt) - && (eat = eat + this.cursorExt); - - c.beginPath(); - c.strokeStyle = this.o.bgColor; - c.arc(this.xy, this.xy, this.radius, this.endAngle, this.startAngle, true); - c.stroke(); - - if (this.o.displayPrevious) { - ea = this.startAngle + this.angle(this.v); - sa = this.startAngle; - this.o.cursor - && (sa = ea - this.cursorExt) - && (ea = ea + this.cursorExt); - - c.beginPath(); - c.strokeStyle = this.pColor; - c.arc(this.xy, this.xy, this.radius, sa, ea, false); - c.stroke(); - r = (this.cv == this.v); - } - - c.beginPath(); - c.strokeStyle = r ? this.o.fgColor : this.fgColor ; - c.arc(this.xy, this.xy, this.radius, sat, eat, false); - c.stroke(); - }; - - this.cancel = function () { - this.val(this.v); - }; - }; - - $.fn.dial = $.fn.knob = function (o) { - return this.each( - function () { - var d = new k.Dial(); - d.o = o; - d.$ = $(this); - d.run(); - } - ).parent(); - }; - -})(jQuery); \ No newline at end of file diff --git a/src/UI/JsLibraries/jquery.signalR.js b/src/UI/JsLibraries/jquery.signalR.js deleted file mode 100644 index fcacbc371..000000000 --- a/src/UI/JsLibraries/jquery.signalR.js +++ /dev/null @@ -1,2193 +0,0 @@ -/* jquery.signalR.core.js */ -/*global window:false */ -/*! - * ASP.NET SignalR JavaScript Library v1.1.3 - * http://signalr.net/ - * - * Copyright Microsoft Open Technologies, Inc. All rights reserved. - * Licensed under the Apache 2.0 - * https://github.com/SignalR/SignalR/blob/master/LICENSE.md - * - */ - -/// <reference path="Scripts/jquery-1.6.4.js" /> -(function ($, window) { - "use strict"; - - if (typeof ($) !== "function") { - // no jQuery! - throw new Error("SignalR: jQuery not found. Please ensure jQuery is referenced before the SignalR.js file."); - } - - if (!window.JSON) { - // no JSON! - throw new Error("SignalR: No JSON parser found. Please ensure json2.js is referenced before the SignalR.js file if you need to support clients without native JSON parsing support, e.g. IE<8."); - } - - var signalR, - _connection, - _pageLoaded = (window.document.readyState === "complete"), - _pageWindow = $(window), - - events = { - onStart: "onStart", - onStarting: "onStarting", - onReceived: "onReceived", - onError: "onError", - onConnectionSlow: "onConnectionSlow", - onReconnecting: "onReconnecting", - onReconnect: "onReconnect", - onStateChanged: "onStateChanged", - onDisconnect: "onDisconnect" - }, - - log = function (msg, logging) { - if (logging === false) { - return; - } - var m; - if (typeof (window.console) === "undefined") { - return; - } - m = "[" + new Date().toTimeString() + "] SignalR: " + msg; - if (window.console.debug) { - window.console.debug(m); - } else if (window.console.log) { - window.console.log(m); - } - }, - - changeState = function (connection, expectedState, newState) { - if (expectedState === connection.state) { - connection.state = newState; - - $(connection).triggerHandler(events.onStateChanged, [{ oldState: expectedState, newState: newState }]); - return true; - } - - return false; - }, - - isDisconnecting = function (connection) { - return connection.state === signalR.connectionState.disconnected; - }, - - configureStopReconnectingTimeout = function (connection) { - var stopReconnectingTimeout, - onReconnectTimeout; - - // Check if this connection has already been configured to stop reconnecting after a specified timeout. - // Without this check if a connection is stopped then started events will be bound multiple times. - if (!connection._.configuredStopReconnectingTimeout) { - onReconnectTimeout = function (connection) { - connection.log("Couldn't reconnect within the configured timeout (" + connection.disconnectTimeout + "ms), disconnecting."); - connection.stop(/* async */ false, /* notifyServer */ false); - }; - - connection.reconnecting(function () { - var connection = this; - - // Guard against state changing in a previous user defined even handler - if (connection.state === signalR.connectionState.reconnecting) { - stopReconnectingTimeout = window.setTimeout(function () { onReconnectTimeout(connection); }, connection.disconnectTimeout); - } - }); - - connection.stateChanged(function (data) { - if (data.oldState === signalR.connectionState.reconnecting) { - // Clear the pending reconnect timeout check - window.clearTimeout(stopReconnectingTimeout); - } - }); - - connection._.configuredStopReconnectingTimeout = true; - } - }; - - signalR = function (url, qs, logging) { - /// <summary>Creates a new SignalR connection for the given url</summary> - /// <param name="url" type="String">The URL of the long polling endpoint</param> - /// <param name="qs" type="Object"> - /// [Optional] Custom querystring parameters to add to the connection URL. - /// If an object, every non-function member will be added to the querystring. - /// If a string, it's added to the QS as specified. - /// </param> - /// <param name="logging" type="Boolean"> - /// [Optional] A flag indicating whether connection logging is enabled to the browser - /// console/log. Defaults to false. - /// </param> - - return new signalR.fn.init(url, qs, logging); - }; - - signalR._ = { - defaultContentType: "application/x-www-form-urlencoded; charset=UTF-8", - ieVersion: (function () { - var version, - matches; - - if (window.navigator.appName === 'Microsoft Internet Explorer') { - // Check if the user agent has the pattern "MSIE (one or more numbers).(one or more numbers)"; - matches = /MSIE ([0-9]+\.[0-9]+)/.exec(window.navigator.userAgent); - - if (matches) { - version = window.parseFloat(matches[1]); - } - } - - // undefined value means not IE - return version; - })() - }; - - signalR.events = events; - - signalR.changeState = changeState; - - signalR.isDisconnecting = isDisconnecting; - - signalR.connectionState = { - connecting: 0, - connected: 1, - reconnecting: 2, - disconnected: 4 - }; - - signalR.hub = { - start: function () { - // This will get replaced with the real hub connection start method when hubs is referenced correctly - throw new Error("SignalR: Error loading hubs. Ensure your hubs reference is correct, e.g. <script src='/signalr/hubs'></script>."); - } - }; - - _pageWindow.load(function () { _pageLoaded = true; }); - - function validateTransport(requestedTransport, connection) { - /// <summary>Validates the requested transport by cross checking it with the pre-defined signalR.transports</summary> - /// <param name="requestedTransport" type="Object">The designated transports that the user has specified.</param> - /// <param name="connection" type="signalR">The connection that will be using the requested transports. Used for logging purposes.</param> - /// <returns type="Object" /> - - if ($.isArray(requestedTransport)) { - // Go through transport array and remove an "invalid" tranports - for (var i = requestedTransport.length - 1; i >= 0; i--) { - var transport = requestedTransport[i]; - if ($.type(requestedTransport) !== "object" && ($.type(transport) !== "string" || !signalR.transports[transport])) { - connection.log("Invalid transport: " + transport + ", removing it from the transports list."); - requestedTransport.splice(i, 1); - } - } - - // Verify we still have transports left, if we dont then we have invalid transports - if (requestedTransport.length === 0) { - connection.log("No transports remain within the specified transport array."); - requestedTransport = null; - } - } else if ($.type(requestedTransport) !== "object" && !signalR.transports[requestedTransport] && requestedTransport !== "auto") { - connection.log("Invalid transport: " + requestedTransport.toString()); - requestedTransport = null; - } - else if (requestedTransport === "auto" && signalR._.ieVersion <= 8) - { - // If we're doing an auto transport and we're IE8 then force longPolling, #1764 - return ["longPolling"]; - - } - - return requestedTransport; - } - - function getDefaultPort(protocol) { - if(protocol === "http:") { - return 80; - } - else if (protocol === "https:") { - return 443; - } - } - - function addDefaultPort(protocol, url) { - // Remove ports from url. We have to check if there's a / or end of line - // following the port in order to avoid removing ports such as 8080. - if(url.match(/:\d+$/)) { - return url; - } else { - return url + ":" + getDefaultPort(protocol); - } - } - - signalR.fn = signalR.prototype = { - init: function (url, qs, logging) { - this.url = url; - this.qs = qs; - this._ = {}; - if (typeof (logging) === "boolean") { - this.logging = logging; - } - }, - - isCrossDomain: function (url, against) { - /// <summary>Checks if url is cross domain</summary> - /// <param name="url" type="String">The base URL</param> - /// <param name="against" type="Object"> - /// An optional argument to compare the URL against, if not specified it will be set to window.location. - /// If specified it must contain a protocol and a host property. - /// </param> - var link; - - url = $.trim(url); - if (url.indexOf("http") !== 0) { - return false; - } - - against = against || window.location; - - // Create an anchor tag. - link = window.document.createElement("a"); - link.href = url; - - // When checking for cross domain we have to special case port 80 because the window.location will remove the - return link.protocol + addDefaultPort(link.protocol, link.host) !== against.protocol + addDefaultPort(against.protocol, against.host); - }, - - ajaxDataType: "json", - - contentType: "application/json; charset=UTF-8", - - logging: false, - - state: signalR.connectionState.disconnected, - - keepAliveData: {}, - - reconnectDelay: 2000, - - disconnectTimeout: 30000, // This should be set by the server in response to the negotiate request (30s default) - - keepAliveWarnAt: 2 / 3, // Warn user of slow connection if we breach the X% mark of the keep alive timeout - - start: function (options, callback) { - /// <summary>Starts the connection</summary> - /// <param name="options" type="Object">Options map</param> - /// <param name="callback" type="Function">A callback function to execute when the connection has started</param> - var connection = this, - config = { - waitForPageLoad: true, - transport: "auto", - jsonp: false - }, - initialize, - deferred = connection._deferral || $.Deferred(), // Check to see if there is a pre-existing deferral that's being built on, if so we want to keep using it - parser = window.document.createElement("a"); - - if ($.type(options) === "function") { - // Support calling with single callback parameter - callback = options; - } else if ($.type(options) === "object") { - $.extend(config, options); - if ($.type(config.callback) === "function") { - callback = config.callback; - } - } - - config.transport = validateTransport(config.transport, connection); - - // If the transport is invalid throw an error and abort start - if (!config.transport) { - throw new Error("SignalR: Invalid transport(s) specified, aborting start."); - } - - // Check to see if start is being called prior to page load - // If waitForPageLoad is true we then want to re-direct function call to the window load event - if (!_pageLoaded && config.waitForPageLoad === true) { - _pageWindow.load(function () { - connection._deferral = deferred; - connection.start(options, callback); - }); - return deferred.promise(); - } - - configureStopReconnectingTimeout(connection); - - if (changeState(connection, - signalR.connectionState.disconnected, - signalR.connectionState.connecting) === false) { - // Already started, just return - deferred.resolve(connection); - return deferred.promise(); - } - - // Resolve the full url - parser.href = connection.url; - if (!parser.protocol || parser.protocol === ":") { - connection.protocol = window.document.location.protocol; - connection.host = window.document.location.host; - connection.baseUrl = connection.protocol + "//" + connection.host; - } - else { - connection.protocol = parser.protocol; - connection.host = parser.host; - connection.baseUrl = parser.protocol + "//" + parser.host; - } - - // Set the websocket protocol - connection.wsProtocol = connection.protocol === "https:" ? "wss://" : "ws://"; - - // If jsonp with no/auto transport is specified, then set the transport to long polling - // since that is the only transport for which jsonp really makes sense. - // Some developers might actually choose to specify jsonp for same origin requests - // as demonstrated by Issue #623. - if (config.transport === "auto" && config.jsonp === true) { - config.transport = "longPolling"; - } - - if (this.isCrossDomain(connection.url)) { - connection.log("Auto detected cross domain url."); - - if (config.transport === "auto") { - // Try webSockets and longPolling since SSE doesn't support CORS - // TODO: Support XDM with foreverFrame - config.transport = ["webSockets", "longPolling"]; - } - - // Determine if jsonp is the only choice for negotiation, ajaxSend and ajaxAbort. - // i.e. if the browser doesn't supports CORS - // If it is, ignore any preference to the contrary, and switch to jsonp. - if (!config.jsonp) { - config.jsonp = !$.support.cors; - - if (config.jsonp) { - connection.log("Using jsonp because this browser doesn't support CORS"); - } - } - - connection.contentType = signalR._.defaultContentType; - } - - connection.ajaxDataType = config.jsonp ? "jsonp" : "json"; - - $(connection).bind(events.onStart, function (e, data) { - if ($.type(callback) === "function") { - callback.call(connection); - } - deferred.resolve(connection); - }); - - initialize = function (transports, index) { - index = index || 0; - if (index >= transports.length) { - if (!connection.transport) { - // No transport initialized successfully - $(connection).triggerHandler(events.onError, ["SignalR: No transport could be initialized successfully. Try specifying a different transport or none at all for auto initialization."]); - deferred.reject("SignalR: No transport could be initialized successfully. Try specifying a different transport or none at all for auto initialization."); - // Stop the connection if it has connected and move it into the disconnected state - connection.stop(); - } - return; - } - - var transportName = transports[index], - transport = $.type(transportName) === "object" ? transportName : signalR.transports[transportName]; - - if (transportName.indexOf("_") === 0) { - // Private member - initialize(transports, index + 1); - return; - } - - transport.start(connection, function () { // success - if (transport.supportsKeepAlive && connection.keepAliveData.activated) { - signalR.transports._logic.monitorKeepAlive(connection); - } - - connection.transport = transport; - - changeState(connection, - signalR.connectionState.connecting, - signalR.connectionState.connected); - - $(connection).triggerHandler(events.onStart); - - _pageWindow.unload(function () { // failure - connection.stop(false /* async */); - }); - - }, function () { - initialize(transports, index + 1); - }); - }; - - var url = connection.url + "/negotiate"; - - url = signalR.transports._logic.addQs(url, connection); - - connection.log("Negotiating with '" + url + "'."); - $.ajax({ - url: url, - global: true, - cache: false, - type: "GET", - contentType: connection.contentType, - data: {}, - dataType: connection.ajaxDataType, - error: function (error) { - $(connection).triggerHandler(events.onError, [error.responseText]); - deferred.reject("SignalR: Error during negotiation request: " + error.responseText); - // Stop the connection if negotiate failed - connection.stop(); - }, - success: function (res) { - var keepAliveData = connection.keepAliveData; - - connection.appRelativeUrl = res.Url; - connection.id = res.ConnectionId; - connection.token = res.ConnectionToken; - connection.webSocketServerUrl = res.WebSocketServerUrl; - - // Once the server has labeled the PersistentConnection as Disconnected, we should stop attempting to reconnect - // after res.DisconnectTimeout seconds. - connection.disconnectTimeout = res.DisconnectTimeout * 1000; // in ms - - - // If we have a keep alive - if (res.KeepAliveTimeout) { - // Register the keep alive data as activated - keepAliveData.activated = true; - - // Timeout to designate when to force the connection into reconnecting converted to milliseconds - keepAliveData.timeout = res.KeepAliveTimeout * 1000; - - // Timeout to designate when to warn the developer that the connection may be dead or is not responding. - keepAliveData.timeoutWarning = keepAliveData.timeout * connection.keepAliveWarnAt; - - // Instantiate the frequency in which we check the keep alive. It must be short in order to not miss/pick up any changes - keepAliveData.checkInterval = (keepAliveData.timeout - keepAliveData.timeoutWarning) / 3; - } - else { - keepAliveData.activated = false; - } - - if (!res.ProtocolVersion || res.ProtocolVersion !== "1.2") { - $(connection).triggerHandler(events.onError, ["You are using a version of the client that isn't compatible with the server. Client version 1.2, server version " + res.ProtocolVersion + "."]); - deferred.reject("You are using a version of the client that isn't compatible with the server. Client version 1.2, server version " + res.ProtocolVersion + "."); - return; - } - - $(connection).triggerHandler(events.onStarting); - - var transports = [], - supportedTransports = []; - - $.each(signalR.transports, function (key) { - if (key === "webSockets" && !res.TryWebSockets) { - // Server said don't even try WebSockets, but keep processing the loop - return true; - } - supportedTransports.push(key); - }); - - if ($.isArray(config.transport)) { - // ordered list provided - $.each(config.transport, function () { - var transport = this; - if ($.type(transport) === "object" || ($.type(transport) === "string" && $.inArray("" + transport, supportedTransports) >= 0)) { - transports.push($.type(transport) === "string" ? "" + transport : transport); - } - }); - } else if ($.type(config.transport) === "object" || - $.inArray(config.transport, supportedTransports) >= 0) { - // specific transport provided, as object or a named transport, e.g. "longPolling" - transports.push(config.transport); - } else { // default "auto" - transports = supportedTransports; - } - initialize(transports); - } - }); - - return deferred.promise(); - }, - - starting: function (callback) { - /// <summary>Adds a callback that will be invoked before anything is sent over the connection</summary> - /// <param name="callback" type="Function">A callback function to execute before each time data is sent on the connection</param> - /// <returns type="signalR" /> - var connection = this; - $(connection).bind(events.onStarting, function (e, data) { - callback.call(connection); - }); - return connection; - }, - - send: function (data) { - /// <summary>Sends data over the connection</summary> - /// <param name="data" type="String">The data to send over the connection</param> - /// <returns type="signalR" /> - var connection = this; - - if (connection.state === signalR.connectionState.disconnected) { - // Connection hasn't been started yet - throw new Error("SignalR: Connection must be started before data can be sent. Call .start() before .send()"); - } - - if (connection.state === signalR.connectionState.connecting) { - // Connection hasn't been started yet - throw new Error("SignalR: Connection has not been fully initialized. Use .start().done() or .start().fail() to run logic after the connection has started."); - } - - connection.transport.send(connection, data); - // REVIEW: Should we return deferred here? - return connection; - }, - - received: function (callback) { - /// <summary>Adds a callback that will be invoked after anything is received over the connection</summary> - /// <param name="callback" type="Function">A callback function to execute when any data is received on the connection</param> - /// <returns type="signalR" /> - var connection = this; - $(connection).bind(events.onReceived, function (e, data) { - callback.call(connection, data); - }); - return connection; - }, - - stateChanged: function (callback) { - /// <summary>Adds a callback that will be invoked when the connection state changes</summary> - /// <param name="callback" type="Function">A callback function to execute when the connection state changes</param> - /// <returns type="signalR" /> - var connection = this; - $(connection).bind(events.onStateChanged, function (e, data) { - callback.call(connection, data); - }); - return connection; - }, - - error: function (callback) { - /// <summary>Adds a callback that will be invoked after an error occurs with the connection</summary> - /// <param name="callback" type="Function">A callback function to execute when an error occurs on the connection</param> - /// <returns type="signalR" /> - var connection = this; - $(connection).bind(events.onError, function (e, data) { - callback.call(connection, data); - }); - return connection; - }, - - disconnected: function (callback) { - /// <summary>Adds a callback that will be invoked when the client disconnects</summary> - /// <param name="callback" type="Function">A callback function to execute when the connection is broken</param> - /// <returns type="signalR" /> - var connection = this; - $(connection).bind(events.onDisconnect, function (e, data) { - callback.call(connection); - }); - return connection; - }, - - connectionSlow: function (callback) { - /// <summary>Adds a callback that will be invoked when the client detects a slow connection</summary> - /// <param name="callback" type="Function">A callback function to execute when the connection is slow</param> - /// <returns type="signalR" /> - var connection = this; - $(connection).bind(events.onConnectionSlow, function(e, data) { - callback.call(connection); - }); - - return connection; - }, - - reconnecting: function (callback) { - /// <summary>Adds a callback that will be invoked when the underlying transport begins reconnecting</summary> - /// <param name="callback" type="Function">A callback function to execute when the connection enters a reconnecting state</param> - /// <returns type="signalR" /> - var connection = this; - $(connection).bind(events.onReconnecting, function (e, data) { - callback.call(connection); - }); - return connection; - }, - - reconnected: function (callback) { - /// <summary>Adds a callback that will be invoked when the underlying transport reconnects</summary> - /// <param name="callback" type="Function">A callback function to execute when the connection is restored</param> - /// <returns type="signalR" /> - var connection = this; - $(connection).bind(events.onReconnect, function (e, data) { - callback.call(connection); - }); - return connection; - }, - - stop: function (async, notifyServer) { - /// <summary>Stops listening</summary> - /// <param name="async" type="Boolean">Whether or not to asynchronously abort the connection</param> - /// <param name="notifyServer" type="Boolean">Whether we want to notify the server that we are aborting the connection</param> - /// <returns type="signalR" /> - var connection = this; - - if (connection.state === signalR.connectionState.disconnected) { - return; - } - - try { - if (connection.transport) { - if (notifyServer !== false) { - connection.transport.abort(connection, async); - } - - if (connection.transport.supportsKeepAlive && connection.keepAliveData.activated) { - signalR.transports._logic.stopMonitoringKeepAlive(connection); - } - - connection.transport.stop(connection); - connection.transport = null; - } - - // Trigger the disconnect event - $(connection).triggerHandler(events.onDisconnect); - - delete connection.messageId; - delete connection.groupsToken; - - // Remove the ID and the deferral on stop, this is to ensure that if a connection is restarted it takes on a new id/deferral. - delete connection.id; - delete connection._deferral; - } - finally { - changeState(connection, connection.state, signalR.connectionState.disconnected); - } - - return connection; - }, - - log: function (msg) { - log(msg, this.logging); - } - }; - - signalR.fn.init.prototype = signalR.fn; - - signalR.noConflict = function () { - /// <summary>Reinstates the original value of $.connection and returns the signalR object for manual assignment</summary> - /// <returns type="signalR" /> - if ($.connection === signalR) { - $.connection = _connection; - } - return signalR; - }; - - if ($.connection) { - _connection = $.connection; - } - - $.connection = $.signalR = signalR; - -}(window.jQuery, window)); -/* jquery.signalR.transports.common.js */ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -/*global window:false */ -/// <reference path="jquery.signalR.core.js" /> - -(function ($, window) { - "use strict"; - - var signalR = $.signalR, - events = $.signalR.events, - changeState = $.signalR.changeState; - - signalR.transports = {}; - - function checkIfAlive(connection) { - var keepAliveData = connection.keepAliveData, - diff, - timeElapsed; - - // Only check if we're connected - if (connection.state === signalR.connectionState.connected) { - diff = new Date(); - - diff.setTime(diff - keepAliveData.lastKeepAlive); - timeElapsed = diff.getTime(); - - // Check if the keep alive has completely timed out - if (timeElapsed >= keepAliveData.timeout) { - connection.log("Keep alive timed out. Notifying transport that connection has been lost."); - - // Notify transport that the connection has been lost - connection.transport.lostConnection(connection); - } - else if (timeElapsed >= keepAliveData.timeoutWarning) { - // This is to assure that the user only gets a single warning - if (!keepAliveData.userNotified) { - connection.log("Keep alive has been missed, connection may be dead/slow."); - $(connection).triggerHandler(events.onConnectionSlow); - keepAliveData.userNotified = true; - } - } - else { - keepAliveData.userNotified = false; - } - } - - // Verify we're monitoring the keep alive - // We don't want this as a part of the inner if statement above because we want keep alives to continue to be checked - // in the event that the server comes back online (if it goes offline). - if (keepAliveData.monitoring) { - window.setTimeout(function () { - checkIfAlive(connection); - }, keepAliveData.checkInterval); - } - } - - function isConnectedOrReconnecting(connection) { - return connection.state === signalR.connectionState.connected || - connection.state === signalR.connectionState.reconnecting; - } - - signalR.transports._logic = { - pingServer: function (connection, transport) { - /// <summary>Pings the server</summary> - /// <param name="connection" type="signalr">Connection associated with the server ping</param> - /// <returns type="signalR" /> - var baseUrl = transport === "webSockets" ? "" : connection.baseUrl, - url = baseUrl + connection.appRelativeUrl + "/ping", - deferral = $.Deferred(); - - url = this.addQs(url, connection); - - $.ajax({ - url: url, - global: true, - cache: false, - type: "GET", - contentType: connection.contentType, - data: {}, - dataType: connection.ajaxDataType, - success: function (data) { - if (data.Response === "pong") { - deferral.resolve(); - } - else { - deferral.reject("SignalR: Invalid ping response when pinging server: " + (data.responseText || data.statusText)); - } - }, - error: function (data) { - deferral.reject("SignalR: Error pinging server: " + (data.responseText || data.statusText)); - } - }); - - return deferral.promise(); - }, - - addQs: function (url, connection) { - var appender = url.indexOf("?") !== -1 ? "&" : "?", - firstChar; - - if (!connection.qs) { - return url; - } - - if (typeof (connection.qs) === "object") { - return url + appender + $.param(connection.qs); - } - - if (typeof (connection.qs) === "string") { - firstChar = connection.qs.charAt(0); - - if (firstChar === "?" || firstChar === "&") { - appender = ""; - } - - return url + appender + connection.qs; - } - - throw new Error("Connections query string property must be either a string or object."); - }, - - getUrl: function (connection, transport, reconnecting, poll) { - /// <summary>Gets the url for making a GET based connect request</summary> - var baseUrl = transport === "webSockets" ? "" : connection.baseUrl, - url = baseUrl + connection.appRelativeUrl, - qs = "transport=" + transport + "&connectionToken=" + window.encodeURIComponent(connection.token); - - if (connection.data) { - qs += "&connectionData=" + window.encodeURIComponent(connection.data); - } - - if (connection.groupsToken) { - qs += "&groupsToken=" + window.encodeURIComponent(connection.groupsToken); - } - - if (!reconnecting) { - url += "/connect"; - } else { - if (poll) { - // longPolling transport specific - url += "/poll"; - } else { - url += "/reconnect"; - } - - if (connection.messageId) { - qs += "&messageId=" + window.encodeURIComponent(connection.messageId); - } - } - url += "?" + qs; - url = this.addQs(url, connection); - url += "&tid=" + Math.floor(Math.random() * 11); - return url; - }, - - maximizePersistentResponse: function (minPersistentResponse) { - return { - MessageId: minPersistentResponse.C, - Messages: minPersistentResponse.M, - Disconnect: typeof (minPersistentResponse.D) !== "undefined" ? true : false, - TimedOut: typeof (minPersistentResponse.T) !== "undefined" ? true : false, - LongPollDelay: minPersistentResponse.L, - GroupsToken: minPersistentResponse.G - }; - }, - - updateGroups: function (connection, groupsToken) { - if (groupsToken) { - connection.groupsToken = groupsToken; - } - }, - - ajaxSend: function (connection, data) { - var url = connection.url + "/send" + "?transport=" + connection.transport.name + "&connectionToken=" + window.encodeURIComponent(connection.token); - url = this.addQs(url, connection); - return $.ajax({ - url: url, - global: true, - type: connection.ajaxDataType === "jsonp" ? "GET" : "POST", - contentType: signalR._.defaultContentType, - dataType: connection.ajaxDataType, - data: { - data: data - }, - success: function (result) { - if (result) { - $(connection).triggerHandler(events.onReceived, [result]); - } - }, - error: function (errData, textStatus) { - if (textStatus === "abort" || textStatus === "parsererror") { - // The parsererror happens for sends that don't return any data, and hence - // don't write the jsonp callback to the response. This is harder to fix on the server - // so just hack around it on the client for now. - return; - } - $(connection).triggerHandler(events.onError, [errData, data]); - } - }); - }, - - ajaxAbort: function (connection, async) { - if (typeof (connection.transport) === "undefined") { - return; - } - - // Async by default unless explicitly overidden - async = typeof async === "undefined" ? true : async; - - var url = connection.url + "/abort" + "?transport=" + connection.transport.name + "&connectionToken=" + window.encodeURIComponent(connection.token); - url = this.addQs(url, connection); - $.ajax({ - url: url, - async: async, - timeout: 1000, - global: true, - type: "POST", - contentType: connection.contentType, - dataType: connection.ajaxDataType, - data: {} - }); - - connection.log("Fired ajax abort async = " + async); - }, - - processMessages: function (connection, minData) { - var data; - // Transport can be null if we've just closed the connection - if (connection.transport) { - var $connection = $(connection); - - // If our transport supports keep alive then we need to update the last keep alive time stamp. - // Very rarely the transport can be null. - if (connection.transport.supportsKeepAlive && connection.keepAliveData.activated) { - this.updateKeepAlive(connection); - } - - if (!minData) { - return; - } - - data = this.maximizePersistentResponse(minData); - - if (data.Disconnect) { - connection.log("Disconnect command received from server"); - - // Disconnected by the server - connection.stop(false, false); - return; - } - - this.updateGroups(connection, data.GroupsToken); - - if (data.Messages) { - $.each(data.Messages, function (index, message) { - $connection.triggerHandler(events.onReceived, [message]); - }); - } - - if (data.MessageId) { - connection.messageId = data.MessageId; - } - } - }, - - monitorKeepAlive: function (connection) { - var keepAliveData = connection.keepAliveData, - that = this; - - // If we haven't initiated the keep alive timeouts then we need to - if (!keepAliveData.monitoring) { - keepAliveData.monitoring = true; - - // Initialize the keep alive time stamp ping - that.updateKeepAlive(connection); - - // Save the function so we can unbind it on stop - connection.keepAliveData.reconnectKeepAliveUpdate = function () { - that.updateKeepAlive(connection); - }; - - // Update Keep alive on reconnect - $(connection).bind(events.onReconnect, connection.keepAliveData.reconnectKeepAliveUpdate); - - connection.log("Now monitoring keep alive with a warning timeout of " + keepAliveData.timeoutWarning + " and a connection lost timeout of " + keepAliveData.timeout); - // Start the monitoring of the keep alive - checkIfAlive(connection); - } - else { - connection.log("Tried to monitor keep alive but it's already being monitored"); - } - }, - - stopMonitoringKeepAlive: function (connection) { - var keepAliveData = connection.keepAliveData; - - // Only attempt to stop the keep alive monitoring if its being monitored - if (keepAliveData.monitoring) { - // Stop monitoring - keepAliveData.monitoring = false; - - // Remove the updateKeepAlive function from the reconnect event - $(connection).unbind(events.onReconnect, connection.keepAliveData.reconnectKeepAliveUpdate); - - // Clear all the keep alive data - connection.keepAliveData = {}; - connection.log("Stopping the monitoring of the keep alive"); - } - }, - - updateKeepAlive: function (connection) { - connection.keepAliveData.lastKeepAlive = new Date(); - }, - - ensureReconnectingState: function (connection) { - if (changeState(connection, - signalR.connectionState.connected, - signalR.connectionState.reconnecting) === true) { - $(connection).triggerHandler(events.onReconnecting); - } - return connection.state === signalR.connectionState.reconnecting; - }, - - clearReconnectTimeout: function (connection) { - if (connection && connection._.reconnectTimeout) { - window.clearTimeout(connection._.reconnectTimeout); - delete connection._.reconnectTimeout; - } - }, - - reconnect: function (connection, transportName) { - var transport = signalR.transports[transportName], - that = this; - - // We should only set a reconnectTimeout if we are currently connected - // and a reconnectTimeout isn't already set. - if (isConnectedOrReconnecting(connection) && !connection._.reconnectTimeout) { - - connection._.reconnectTimeout = window.setTimeout(function () { - transport.stop(connection); - - if (that.ensureReconnectingState(connection)) { - connection.log(transportName + " reconnecting"); - transport.start(connection); - } - }, connection.reconnectDelay); - } - }, - - foreverFrame: { - count: 0, - connections: {} - } - }; - -}(window.jQuery, window)); -/* jquery.signalR.transports.webSockets.js */ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -/*global window:false */ -/// <reference path="jquery.signalR.transports.common.js" /> - -(function ($, window) { - "use strict"; - - var signalR = $.signalR, - events = $.signalR.events, - changeState = $.signalR.changeState, - transportLogic = signalR.transports._logic; - - signalR.transports.webSockets = { - name: "webSockets", - - supportsKeepAlive: true, - - send: function (connection, data) { - connection.socket.send(data); - }, - - start: function (connection, onSuccess, onFailed) { - var url, - opened = false, - that = this, - reconnecting = !onSuccess, - $connection = $(connection); - - if (!window.WebSocket) { - onFailed(); - return; - } - - if (!connection.socket) { - if (connection.webSocketServerUrl) { - url = connection.webSocketServerUrl; - } - else { - url = connection.wsProtocol + connection.host; - } - - url += transportLogic.getUrl(connection, this.name, reconnecting); - - connection.log("Connecting to websocket endpoint '" + url + "'"); - connection.socket = new window.WebSocket(url); - connection.socket.onopen = function () { - opened = true; - connection.log("Websocket opened"); - - transportLogic.clearReconnectTimeout(connection); - - if (onSuccess) { - onSuccess(); - } else if (changeState(connection, - signalR.connectionState.reconnecting, - signalR.connectionState.connected) === true) { - $connection.triggerHandler(events.onReconnect); - } - }; - - connection.socket.onclose = function (event) { - // Only handle a socket close if the close is from the current socket. - // Sometimes on disconnect the server will push down an onclose event - // to an expired socket. - if (this === connection.socket) { - if (!opened) { - if (onFailed) { - onFailed(); - } - else if (reconnecting) { - that.reconnect(connection); - } - return; - } - else if (typeof event.wasClean !== "undefined" && event.wasClean === false) { - // Ideally this would use the websocket.onerror handler (rather than checking wasClean in onclose) but - // I found in some circumstances Chrome won't call onerror. This implementation seems to work on all browsers. - $(connection).triggerHandler(events.onError, [event.reason]); - connection.log("Unclean disconnect from websocket." + event.reason); - } - else { - connection.log("Websocket closed"); - } - - that.reconnect(connection); - } - }; - - connection.socket.onmessage = function (event) { - var data = window.JSON.parse(event.data), - $connection = $(connection); - - if (data) { - // data.M is PersistentResponse.Messages - if ($.isEmptyObject(data) || data.M) { - transportLogic.processMessages(connection, data); - } else { - // For websockets we need to trigger onReceived - // for callbacks to outgoing hub calls. - $connection.triggerHandler(events.onReceived, [data]); - } - } - }; - } - }, - - reconnect: function (connection) { - transportLogic.reconnect(connection, this.name); - }, - - lostConnection: function (connection) { - this.reconnect(connection); - - }, - - stop: function (connection) { - // Don't trigger a reconnect after stopping - transportLogic.clearReconnectTimeout(connection); - - if (connection.socket !== null) { - connection.log("Closing the Websocket"); - connection.socket.close(); - connection.socket = null; - } - }, - - abort: function (connection) { - } - }; - -}(window.jQuery, window)); -/* jquery.signalR.transports.serverSentEvents.js */ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -/*global window:false */ -/// <reference path="jquery.signalR.transports.common.js" /> - -(function ($, window) { - "use strict"; - - var signalR = $.signalR, - events = $.signalR.events, - changeState = $.signalR.changeState, - transportLogic = signalR.transports._logic; - - signalR.transports.serverSentEvents = { - name: "serverSentEvents", - - supportsKeepAlive: true, - - timeOut: 3000, - - start: function (connection, onSuccess, onFailed) { - var that = this, - opened = false, - $connection = $(connection), - reconnecting = !onSuccess, - url, - connectTimeOut; - - if (connection.eventSource) { - connection.log("The connection already has an event source. Stopping it."); - connection.stop(); - } - - if (!window.EventSource) { - if (onFailed) { - connection.log("This browser doesn't support SSE."); - onFailed(); - } - return; - } - - url = transportLogic.getUrl(connection, this.name, reconnecting); - - try { - connection.log("Attempting to connect to SSE endpoint '" + url + "'"); - connection.eventSource = new window.EventSource(url); - } - catch (e) { - connection.log("EventSource failed trying to connect with error " + e.Message); - if (onFailed) { - // The connection failed, call the failed callback - onFailed(); - } - else { - $connection.triggerHandler(events.onError, [e]); - if (reconnecting) { - // If we were reconnecting, rather than doing initial connect, then try reconnect again - that.reconnect(connection); - } - } - return; - } - - // After connecting, if after the specified timeout there's no response stop the connection - // and raise on failed - connectTimeOut = window.setTimeout(function () { - if (opened === false) { - connection.log("EventSource timed out trying to connect"); - connection.log("EventSource readyState: " + connection.eventSource.readyState); - - if (!reconnecting) { - that.stop(connection); - } - - if (reconnecting) { - // If we're reconnecting and the event source is attempting to connect, - // don't keep retrying. This causes duplicate connections to spawn. - if (connection.eventSource.readyState !== window.EventSource.CONNECTING && - connection.eventSource.readyState !== window.EventSource.OPEN) { - // If we were reconnecting, rather than doing initial connect, then try reconnect again - that.reconnect(connection); - } - } else if (onFailed) { - onFailed(); - } - } - }, - that.timeOut); - - connection.eventSource.addEventListener("open", function (e) { - connection.log("EventSource connected"); - - if (connectTimeOut) { - window.clearTimeout(connectTimeOut); - } - - transportLogic.clearReconnectTimeout(connection); - - if (opened === false) { - opened = true; - - if (onSuccess) { - onSuccess(); - } else if (changeState(connection, - signalR.connectionState.reconnecting, - signalR.connectionState.connected) === true) { - // If there's no onSuccess handler we assume this is a reconnect - $connection.triggerHandler(events.onReconnect); - } - } - }, false); - - connection.eventSource.addEventListener("message", function (e) { - // process messages - if (e.data === "initialized") { - return; - } - - transportLogic.processMessages(connection, window.JSON.parse(e.data)); - }, false); - - connection.eventSource.addEventListener("error", function (e) { - // Only handle an error if the error is from the current Event Source. - // Sometimes on disconnect the server will push down an error event - // to an expired Event Source. - if (this === connection.eventSource) { - if (!opened) { - if (onFailed) { - onFailed(); - } - - return; - } - - connection.log("EventSource readyState: " + connection.eventSource.readyState); - - if (e.eventPhase === window.EventSource.CLOSED) { - // We don't use the EventSource's native reconnect function as it - // doesn't allow us to change the URL when reconnecting. We need - // to change the URL to not include the /connect suffix, and pass - // the last message id we received. - connection.log("EventSource reconnecting due to the server connection ending"); - that.reconnect(connection); - } else { - // connection error - connection.log("EventSource error"); - $connection.triggerHandler(events.onError); - } - } - }, false); - }, - - reconnect: function (connection) { - transportLogic.reconnect(connection, this.name); - }, - - lostConnection: function (connection) { - this.reconnect(connection); - }, - - send: function (connection, data) { - transportLogic.ajaxSend(connection, data); - }, - - stop: function (connection) { - // Don't trigger a reconnect after stopping - transportLogic.clearReconnectTimeout(connection); - - if (connection && connection.eventSource) { - connection.log("EventSource calling close()"); - connection.eventSource.close(); - connection.eventSource = null; - delete connection.eventSource; - } - }, - - abort: function (connection, async) { - transportLogic.ajaxAbort(connection, async); - } - }; - -}(window.jQuery, window)); -/* jquery.signalR.transports.foreverFrame.js */ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -/*global window:false */ -/// <reference path="jquery.signalR.transports.common.js" /> - -(function ($, window) { - "use strict"; - - var signalR = $.signalR, - events = $.signalR.events, - changeState = $.signalR.changeState, - transportLogic = signalR.transports._logic, - // Used to prevent infinite loading icon spins in older versions of ie - // We build this object inside a closure so we don't pollute the rest of - // the foreverFrame transport with unnecessary functions/utilities. - loadPreventer = (function () { - var loadingFixIntervalId = null, - loadingFixInterval = 1000, - attachedTo = 0; - - return { - prevent: function () { - // Prevent additional iframe removal procedures from newer browsers - if (signalR._.ieVersion <= 8) { - // We only ever want to set the interval one time, so on the first attachedTo - if (attachedTo === 0) { - // Create and destroy iframe every 3 seconds to prevent loading icon, super hacky - loadingFixIntervalId = window.setInterval(function () { - var tempFrame = $("<iframe style='position:absolute;top:0;left:0;width:0;height:0;visibility:hidden;' src=''></iframe>"); - - $("body").append(tempFrame); - tempFrame.remove(); - tempFrame = null; - }, loadingFixInterval); - } - - attachedTo++; - } - }, - cancel: function () { - // Only clear the interval if there's only one more object that the loadPreventer is attachedTo - if (attachedTo === 1) { - window.clearInterval(loadingFixIntervalId); - } - - if (attachedTo > 0) { - attachedTo--; - } - } - }; - })(); - - signalR.transports.foreverFrame = { - name: "foreverFrame", - - supportsKeepAlive: true, - - timeOut: 3000, - - start: function (connection, onSuccess, onFailed) { - var that = this, - frameId = (transportLogic.foreverFrame.count += 1), - url, - frame = $("<iframe data-signalr-connection-id='" + connection.id + "' style='position:absolute;top:0;left:0;width:0;height:0;visibility:hidden;' src=''></iframe>"); - - if (window.EventSource) { - // If the browser supports SSE, don't use Forever Frame - if (onFailed) { - connection.log("This browser supports SSE, skipping Forever Frame."); - onFailed(); - } - return; - } - - // Start preventing loading icon - // This will only perform work if the loadPreventer is not attached to another connection. - loadPreventer.prevent(); - - // Build the url - url = transportLogic.getUrl(connection, this.name); - url += "&frameId=" + frameId; - - // Set body prior to setting URL to avoid caching issues. - $("body").append(frame); - - frame.prop("src", url); - transportLogic.foreverFrame.connections[frameId] = connection; - - connection.log("Binding to iframe's readystatechange event."); - frame.bind("readystatechange", function () { - if ($.inArray(this.readyState, ["loaded", "complete"]) >= 0) { - connection.log("Forever frame iframe readyState changed to " + this.readyState + ", reconnecting"); - - that.reconnect(connection); - } - }); - - connection.frame = frame[0]; - connection.frameId = frameId; - - if (onSuccess) { - connection.onSuccess = onSuccess; - } - - // After connecting, if after the specified timeout there's no response stop the connection - // and raise on failed - window.setTimeout(function () { - if (connection.onSuccess) { - connection.log("Failed to connect using forever frame source, it timed out after " + that.timeOut + "ms."); - that.stop(connection); - - if (onFailed) { - onFailed(); - } - } - }, that.timeOut); - }, - - reconnect: function (connection) { - var that = this; - window.setTimeout(function () { - if (connection.frame && transportLogic.ensureReconnectingState(connection)) { - var frame = connection.frame, - src = transportLogic.getUrl(connection, that.name, true) + "&frameId=" + connection.frameId; - connection.log("Updating iframe src to '" + src + "'."); - frame.src = src; - } - }, connection.reconnectDelay); - }, - - lostConnection: function (connection) { - this.reconnect(connection); - }, - - send: function (connection, data) { - transportLogic.ajaxSend(connection, data); - }, - - receive: function (connection, data) { - var cw; - - transportLogic.processMessages(connection, data); - // Delete the script & div elements - connection.frameMessageCount = (connection.frameMessageCount || 0) + 1; - if (connection.frameMessageCount > 50) { - connection.frameMessageCount = 0; - cw = connection.frame.contentWindow || connection.frame.contentDocument; - if (cw && cw.document) { - $("body", cw.document).empty(); - } - } - }, - - stop: function (connection) { - var cw = null; - - // Stop attempting to prevent loading icon - loadPreventer.cancel(); - - if (connection.frame) { - if (connection.frame.stop) { - connection.frame.stop(); - } else { - try { - cw = connection.frame.contentWindow || connection.frame.contentDocument; - if (cw.document && cw.document.execCommand) { - cw.document.execCommand("Stop"); - } - } - catch (e) { - connection.log("SignalR: Error occured when stopping foreverFrame transport. Message = " + e.message); - } - } - $(connection.frame).remove(); - delete transportLogic.foreverFrame.connections[connection.frameId]; - connection.frame = null; - connection.frameId = null; - delete connection.frame; - delete connection.frameId; - connection.log("Stopping forever frame"); - } - }, - - abort: function (connection, async) { - transportLogic.ajaxAbort(connection, async); - }, - - getConnection: function (id) { - return transportLogic.foreverFrame.connections[id]; - }, - - started: function (connection) { - if (connection.onSuccess) { - connection.onSuccess(); - connection.onSuccess = null; - delete connection.onSuccess; - } else if (changeState(connection, - signalR.connectionState.reconnecting, - signalR.connectionState.connected) === true) { - // If there's no onSuccess handler we assume this is a reconnect - $(connection).triggerHandler(events.onReconnect); - } - } - }; - -}(window.jQuery, window)); -/* jquery.signalR.transports.longPolling.js */ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -/*global window:false */ -/// <reference path="jquery.signalR.transports.common.js" /> - -(function ($, window) { - "use strict"; - - var signalR = $.signalR, - events = $.signalR.events, - changeState = $.signalR.changeState, - isDisconnecting = $.signalR.isDisconnecting, - transportLogic = signalR.transports._logic; - - signalR.transports.longPolling = { - name: "longPolling", - - supportsKeepAlive: false, - - reconnectDelay: 3000, - - init: function (connection, onComplete) { - /// <summary>Pings the server to ensure availability</summary> - /// <param name="connection" type="signalr">Connection associated with the server ping</param> - /// <param name="onComplete" type="Function">Callback to call once initialization has completed</param> - - var that = this, - pingLoop, - // pingFail is used to loop the re-ping behavior. When we fail we want to re-try. - pingFail = function (reason) { - if (isDisconnecting(connection) === false) { - connection.log("SignalR: Server ping failed because '" + reason + "', re-trying ping."); - window.setTimeout(pingLoop, that.reconnectDelay); - } - }; - - connection.log("SignalR: Initializing long polling connection with server."); - pingLoop = function () { - // Ping the server, on successful ping call the onComplete method, otherwise if we fail call the pingFail - transportLogic.pingServer(connection, that.name).done(onComplete).fail(pingFail); - }; - - pingLoop(); - }, - - start: function (connection, onSuccess, onFailed) { - /// <summary>Starts the long polling connection</summary> - /// <param name="connection" type="signalR">The SignalR connection to start</param> - var that = this, - initialConnectedFired = false, - fireConnect = function () { - if (initialConnectedFired) { - return; - } - initialConnectedFired = true; - onSuccess(); - connection.log("Longpolling connected"); - }, - reconnectErrors = 0, - reconnectTimeoutId = null, - fireReconnected = function (instance) { - window.clearTimeout(reconnectTimeoutId); - reconnectTimeoutId = null; - - if (changeState(connection, - signalR.connectionState.reconnecting, - signalR.connectionState.connected) === true) { - // Successfully reconnected! - connection.log("Raising the reconnect event"); - $(instance).triggerHandler(events.onReconnect); - } - }, - // 1 hour - maxFireReconnectedTimeout = 3600000; - - if (connection.pollXhr) { - connection.log("Polling xhr requests already exists, aborting."); - connection.stop(); - } - - // We start with an initialization procedure which pings the server to verify that it is there. - // On scucessful initialization we'll then proceed with starting the transport. - that.init(connection, function () { - connection.messageId = null; - - window.setTimeout(function () { - (function poll(instance, raiseReconnect) { - var messageId = instance.messageId, - connect = (messageId === null), - reconnecting = !connect, - polling = !raiseReconnect, - url = transportLogic.getUrl(instance, that.name, reconnecting, polling); - - // If we've disconnected during the time we've tried to re-instantiate the poll then stop. - if (isDisconnecting(instance) === true) { - return; - } - - connection.log("Attempting to connect to '" + url + "' using longPolling."); - instance.pollXhr = $.ajax({ - url: url, - global: true, - cache: false, - type: "GET", - dataType: connection.ajaxDataType, - contentType: connection.contentType, - success: function (minData) { - var delay = 0, - data; - - // Reset our reconnect errors so if we transition into a reconnecting state again we trigger - // reconnected quickly - reconnectErrors = 0; - - // If there's currently a timeout to trigger reconnect, fire it now before processing messages - if (reconnectTimeoutId !== null) { - fireReconnected(); - } - - fireConnect(); - - if (minData) { - data = transportLogic.maximizePersistentResponse(minData); - } - - transportLogic.processMessages(instance, minData); - - if (data && - $.type(data.LongPollDelay) === "number") { - delay = data.LongPollDelay; - } - - if (data && data.Disconnect) { - return; - } - - if (isDisconnecting(instance) === true) { - return; - } - - // We never want to pass a raiseReconnect flag after a successful poll. This is handled via the error function - if (delay > 0) { - window.setTimeout(function () { - poll(instance, false); - }, delay); - } else { - poll(instance, false); - } - }, - - error: function (data, textStatus) { - // Stop trying to trigger reconnect, connection is in an error state - // If we're not in the reconnect state this will noop - window.clearTimeout(reconnectTimeoutId); - reconnectTimeoutId = null; - - if (textStatus === "abort") { - connection.log("Aborted xhr requst."); - return; - } - - // Increment our reconnect errors, we assume all errors to be reconnect errors - // In the case that it's our first error this will cause Reconnect to be fired - // after 1 second due to reconnectErrors being = 1. - reconnectErrors++; - - if (connection.state !== signalR.connectionState.reconnecting) { - connection.log("An error occurred using longPolling. Status = " + textStatus + ". " + data.responseText); - $(instance).triggerHandler(events.onError, [data.responseText]); - } - - // Transition into the reconnecting state - transportLogic.ensureReconnectingState(instance); - - // If we've errored out we need to verify that the server is still there, so re-start initialization process - // This will ping the server until it successfully gets a response. - that.init(instance, function () { - // Call poll with the raiseReconnect flag as true - poll(instance, true); - }); - } - }); - - - // This will only ever pass after an error has occured via the poll ajax procedure. - if (reconnecting && raiseReconnect === true) { - // We wait to reconnect depending on how many times we've failed to reconnect. - // This is essentially a heuristic that will exponentially increase in wait time before - // triggering reconnected. This depends on the "error" handler of Poll to cancel this - // timeout if it triggers before the Reconnected event fires. - // The Math.min at the end is to ensure that the reconnect timeout does not overflow. - reconnectTimeoutId = window.setTimeout(function () { fireReconnected(instance); }, Math.min(1000 * (Math.pow(2, reconnectErrors) - 1), maxFireReconnectedTimeout)); - } - }(connection)); - - // Set an arbitrary timeout to trigger onSuccess, this will alot for enough time on the server to wire up the connection. - // Will be fixed by #1189 and this code can be modified to not be a timeout - window.setTimeout(function () { - // Trigger the onSuccess() method because we've now instantiated a connection - fireConnect(); - }, 250); - }, 250); // Have to delay initial poll so Chrome doesn't show loader spinner in tab - }); - }, - - lostConnection: function (connection) { - throw new Error("Lost Connection not handled for LongPolling"); - }, - - send: function (connection, data) { - transportLogic.ajaxSend(connection, data); - }, - - stop: function (connection) { - /// <summary>Stops the long polling connection</summary> - /// <param name="connection" type="signalR">The SignalR connection to stop</param> - if (connection.pollXhr) { - connection.pollXhr.abort(); - connection.pollXhr = null; - delete connection.pollXhr; - } - }, - - abort: function (connection, async) { - transportLogic.ajaxAbort(connection, async); - } - }; - -}(window.jQuery, window)); -/* jquery.signalR.hubs.js */ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -/*global window:false */ -/// <reference path="jquery.signalR.core.js" /> - -(function ($, window) { - "use strict"; - - // we use a global id for tracking callbacks so the server doesn't have to send extra info like hub name - var eventNamespace = ".hubProxy"; - - function makeEventName(event) { - return event + eventNamespace; - } - - // Equivalent to Array.prototype.map - function map(arr, fun, thisp) { - var i, - length = arr.length, - result = []; - for (i = 0; i < length; i += 1) { - if (arr.hasOwnProperty(i)) { - result[i] = fun.call(thisp, arr[i], i, arr); - } - } - return result; - } - - function getArgValue(a) { - return $.isFunction(a) ? null : ($.type(a) === "undefined" ? null : a); - } - - function hasMembers(obj) { - for (var key in obj) { - // If we have any properties in our callback map then we have callbacks and can exit the loop via return - if (obj.hasOwnProperty(key)) { - return true; - } - } - - return false; - } - - function clearInvocationCallbacks(connection, error) { - /// <param name="connection" type="hubConnection" /> - var callbacks = connection._.invocationCallbacks, - callback; - - connection.log("Clearing hub invocation callbacks with error: " + error); - - // Reset the callback cache now as we have a local var referencing it - connection._.invocationCallbackId = 0; - delete connection._.invocationCallbacks; - connection._.invocationCallbacks = {}; - - // Loop over the callbacks and invoke them. - // We do this using a local var reference and *after* we've cleared the cache - // so that if a fail callback itself tries to invoke another method we don't - // end up with its callback in the list we're looping over. - for (var callbackId in callbacks) { - callback = callbacks[callbackId]; - callback.method.call(callback.scope, { E: error }); - } - } - - // hubProxy - function hubProxy(hubConnection, hubName) { - /// <summary> - /// Creates a new proxy object for the given hub connection that can be used to invoke - /// methods on server hubs and handle client method invocation requests from the server. - /// </summary> - return new hubProxy.fn.init(hubConnection, hubName); - } - - hubProxy.fn = hubProxy.prototype = { - init: function (connection, hubName) { - this.state = {}; - this.connection = connection; - this.hubName = hubName; - this._ = { - callbackMap: {} - }; - }, - - hasSubscriptions: function () { - return hasMembers(this._.callbackMap); - }, - - on: function (eventName, callback) { - /// <summary>Wires up a callback to be invoked when a invocation request is received from the server hub.</summary> - /// <param name="eventName" type="String">The name of the hub event to register the callback for.</param> - /// <param name="callback" type="Function">The callback to be invoked.</param> - var self = this, - callbackMap = self._.callbackMap; - - // Normalize the event name to lowercase - eventName = eventName.toLowerCase(); - - // If there is not an event registered for this callback yet we want to create its event space in the callback map. - if (!callbackMap[eventName]) { - callbackMap[eventName] = {}; - } - - // Map the callback to our encompassed function - callbackMap[eventName][callback] = function (e, data) { - callback.apply(self, data); - }; - - $(self).bind(makeEventName(eventName), callbackMap[eventName][callback]); - - return self; - }, - - off: function (eventName, callback) { - /// <summary>Removes the callback invocation request from the server hub for the given event name.</summary> - /// <param name="eventName" type="String">The name of the hub event to unregister the callback for.</param> - /// <param name="callback" type="Function">The callback to be invoked.</param> - var self = this, - callbackMap = self._.callbackMap, - callbackSpace; - - // Normalize the event name to lowercase - eventName = eventName.toLowerCase(); - - callbackSpace = callbackMap[eventName]; - - // Verify that there is an event space to unbind - if (callbackSpace) { - // Only unbind if there's an event bound with eventName and a callback with the specified callback - if (callbackSpace[callback]) { - $(self).unbind(makeEventName(eventName), callbackSpace[callback]); - - // Remove the callback from the callback map - delete callbackSpace[callback]; - - // Check if there are any members left on the event, if not we need to destroy it. - if (!hasMembers(callbackSpace)) { - delete callbackMap[eventName]; - } - } - else if (!callback) { // Check if we're removing the whole event and we didn't error because of an invalid callback - $(self).unbind(makeEventName(eventName)); - - delete callbackMap[eventName]; - } - } - - return self; - }, - - invoke: function (methodName) { - /// <summary>Invokes a server hub method with the given arguments.</summary> - /// <param name="methodName" type="String">The name of the server hub method.</param> - - var self = this, - connection = self.connection, - args = $.makeArray(arguments).slice(1), - argValues = map(args, getArgValue), - data = { H: self.hubName, M: methodName, A: argValues, I: connection._.invocationCallbackId }, - d = $.Deferred(), - callback = function (minResult) { - var result = self._maximizeHubResponse(minResult); - - // Update the hub state - $.extend(self.state, result.State); - - if (result.Error) { - // Server hub method threw an exception, log it & reject the deferred - if (result.StackTrace) { - connection.log(result.Error + "\n" + result.StackTrace); - } - d.rejectWith(self, [result.Error]); - } else { - // Server invocation succeeded, resolve the deferred - d.resolveWith(self, [result.Result]); - } - }; - - connection._.invocationCallbacks[connection._.invocationCallbackId.toString()] = { scope: self, method: callback }; - connection._.invocationCallbackId += 1; - - if (!$.isEmptyObject(self.state)) { - data.S = self.state; - } - - connection.send(window.JSON.stringify(data)); - - return d.promise(); - }, - - _maximizeHubResponse: function (minHubResponse) { - return { - State: minHubResponse.S, - Result: minHubResponse.R, - Id: minHubResponse.I, - Error: minHubResponse.E, - StackTrace: minHubResponse.T - }; - } - }; - - hubProxy.fn.init.prototype = hubProxy.fn; - - // hubConnection - function hubConnection(url, options) { - /// <summary>Creates a new hub connection.</summary> - /// <param name="url" type="String">[Optional] The hub route url, defaults to "/signalr".</param> - /// <param name="options" type="Object">[Optional] Settings to use when creating the hubConnection.</param> - var settings = { - qs: null, - logging: false, - useDefaultPath: true - }; - - $.extend(settings, options); - - if (!url || settings.useDefaultPath) { - url = (url || "") + "/signalr"; - } - return new hubConnection.fn.init(url, settings); - } - - hubConnection.fn = hubConnection.prototype = $.connection(); - - hubConnection.fn.init = function (url, options) { - var settings = { - qs: null, - logging: false, - useDefaultPath: true - }, - connection = this; - - $.extend(settings, options); - - // Call the base constructor - $.signalR.fn.init.call(connection, url, settings.qs, settings.logging); - - // Object to store hub proxies for this connection - connection.proxies = {}; - - connection._.invocationCallbackId = 0; - connection._.invocationCallbacks = {}; - - // Wire up the received handler - connection.received(function (minData) { - var data, proxy, dataCallbackId, callback, hubName, eventName; - if (!minData) { - return; - } - - if (typeof (minData.I) !== "undefined") { - // We received the return value from a server method invocation, look up callback by id and call it - dataCallbackId = minData.I.toString(); - callback = connection._.invocationCallbacks[dataCallbackId]; - if (callback) { - // Delete the callback from the proxy - connection._.invocationCallbacks[dataCallbackId] = null; - delete connection._.invocationCallbacks[dataCallbackId]; - - // Invoke the callback - callback.method.call(callback.scope, minData); - } - } else { - data = this._maximizeClientHubInvocation(minData); - - // We received a client invocation request, i.e. broadcast from server hub - connection.log("Triggering client hub event '" + data.Method + "' on hub '" + data.Hub + "'."); - - // Normalize the names to lowercase - hubName = data.Hub.toLowerCase(); - eventName = data.Method.toLowerCase(); - - // Trigger the local invocation event - proxy = this.proxies[hubName]; - - // Update the hub state - $.extend(proxy.state, data.State); - $(proxy).triggerHandler(makeEventName(eventName), [data.Args]); - } - }); - - connection.error(function (errData, origData) { - var data, callbackId, callback; - - if (connection.transport && connection.transport.name === "webSockets") { - // WebSockets connections have all callbacks removed on reconnect instead - // as WebSockets sends are fire & forget - return; - } - - if (!origData) { - // No original data passed so this is not a send error - return; - } - - try { - data = window.JSON.parse(origData); - if (!data.I) { - // The original data doesn't have a callback ID so not a send error - return; - } - } catch (e) { - // The original data is not a JSON payload so this is not a send error - return; - } - - callbackId = data.I; - callback = connection._.invocationCallbacks[callbackId]; - - // Invoke the callback with an error to reject the promise - callback.method.call(callback.scope, { E: errData }); - - // Delete the callback - connection._.invocationCallbacks[callbackId] = null; - delete connection._.invocationCallbacks[callbackId]; - }); - - connection.reconnecting(function () { - if (connection.transport && connection.transport.name === "webSockets") { - clearInvocationCallbacks(connection, "Connection started reconnecting before invocation result was received."); - } - }); - - connection.disconnected(function () { - clearInvocationCallbacks(connection, "Connection was disconnected before invocation result was received."); - }); - }; - - hubConnection.fn._maximizeClientHubInvocation = function (minClientHubInvocation) { - return { - Hub: minClientHubInvocation.H, - Method: minClientHubInvocation.M, - Args: minClientHubInvocation.A, - State: minClientHubInvocation.S - }; - }; - - hubConnection.fn._registerSubscribedHubs = function () { - /// <summary> - /// Sets the starting event to loop through the known hubs and register any new hubs - /// that have been added to the proxy. - /// </summary> - - if (!this._subscribedToHubs) { - this._subscribedToHubs = true; - this.starting(function () { - // Set the connection's data object with all the hub proxies with active subscriptions. - // These proxies will receive notifications from the server. - var subscribedHubs = []; - - $.each(this.proxies, function (key) { - if (this.hasSubscriptions()) { - subscribedHubs.push({ name: key }); - } - }); - - this.data = window.JSON.stringify(subscribedHubs); - }); - } - }; - - hubConnection.fn.createHubProxy = function (hubName) { - /// <summary> - /// Creates a new proxy object for the given hub connection that can be used to invoke - /// methods on server hubs and handle client method invocation requests from the server. - /// </summary> - /// <param name="hubName" type="String"> - /// The name of the hub on the server to create the proxy for. - /// </param> - - // Normalize the name to lowercase - hubName = hubName.toLowerCase(); - - var proxy = this.proxies[hubName]; - if (!proxy) { - proxy = hubProxy(this, hubName); - this.proxies[hubName] = proxy; - } - - this._registerSubscribedHubs(); - - return proxy; - }; - - hubConnection.fn.init.prototype = hubConnection.fn; - - $.hubConnection = hubConnection; - -}(window.jQuery, window)); -/* jquery.signalR.version.js */ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -/*global window:false */ -/// <reference path="jquery.signalR.core.js" /> -(function ($) { - $.signalR.version = "1.1.3"; -}(window.jQuery)); diff --git a/src/UI/JsLibraries/locale/placeholder.txt b/src/UI/JsLibraries/locale/placeholder.txt deleted file mode 100644 index 89326d0d4..000000000 --- a/src/UI/JsLibraries/locale/placeholder.txt +++ /dev/null @@ -1 +0,0 @@ -//Need this directory for moment/webpack, but git doesn't like empty directories. \ No newline at end of file diff --git a/src/UI/JsLibraries/lodash.underscore.js b/src/UI/JsLibraries/lodash.underscore.js deleted file mode 100644 index 02fc342c5..000000000 --- a/src/UI/JsLibraries/lodash.underscore.js +++ /dev/null @@ -1,4619 +0,0 @@ -/** - * @license - * Lo-Dash 1.3.1 (Custom Build) <http://lodash.com/> - * Build: `lodash underscore exports="amd,commonjs,global,node" -o ./dist/lodash.underscore.js` - * Copyright 2012-2013 The Dojo Foundation <http://dojofoundation.org/> - * Based on Underscore.js 1.5.1 <http://underscorejs.org/LICENSE> - * Copyright 2009-2013 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors - * Available under MIT license <http://lodash.com/license> - */ -;(function(window) { - - /** Used as a safe reference for `undefined` in pre ES5 environments */ - var undefined; - - /** Used to generate unique IDs */ - var idCounter = 0; - - /** Used internally to indicate various things */ - var indicatorObject = {}; - - /** Used to prefix keys to avoid issues with `__proto__` and properties on `Object.prototype` */ - var keyPrefix = +new Date + ''; - - /** Used to match "interpolate" template delimiters */ - var reInterpolate = /<%=([\s\S]+?)%>/g; - - /** Used to ensure capturing order of template delimiters */ - var reNoMatch = /($^)/; - - /** Used to match unescaped characters in compiled string literals */ - var reUnescapedString = /['\n\r\t\u2028\u2029\\]/g; - - /** `Object#toString` result shortcuts */ - var argsClass = '[object Arguments]', - arrayClass = '[object Array]', - boolClass = '[object Boolean]', - dateClass = '[object Date]', - funcClass = '[object Function]', - numberClass = '[object Number]', - objectClass = '[object Object]', - regexpClass = '[object RegExp]', - stringClass = '[object String]'; - - /** Used to determine if values are of the language type Object */ - var objectTypes = { - 'boolean': false, - 'function': true, - 'object': true, - 'number': false, - 'string': false, - 'undefined': false - }; - - /** Used to escape characters for inclusion in compiled string literals */ - var stringEscapes = { - '\\': '\\', - "'": "'", - '\n': 'n', - '\r': 'r', - '\t': 't', - '\u2028': 'u2028', - '\u2029': 'u2029' - }; - - /** Detect free variable `exports` */ - var freeExports = objectTypes[typeof exports] && exports; - - /** Detect free variable `module` */ - var freeModule = objectTypes[typeof module] && module && module.exports == freeExports && module; - - /** Detect free variable `global` from Node.js or Browserified code and use it as `window` */ - var freeGlobal = objectTypes[typeof global] && global; - if (freeGlobal && (freeGlobal.global === freeGlobal || freeGlobal.window === freeGlobal)) { - window = freeGlobal; - } - - /*--------------------------------------------------------------------------*/ - - /** - * The base implementation of `_.indexOf` without support for binary searches - * or `fromIndex` constraints. - * - * @private - * @param {Array} array The array to search. - * @param {Mixed} value The value to search for. - * @param {Number} [fromIndex=0] The index to search from. - * @returns {Number} Returns the index of the matched value or `-1`. - */ - function baseIndexOf(array, value, fromIndex) { - var index = (fromIndex || 0) - 1, - length = array ? array.length : 0; - - while (++index < length) { - if (array[index] === value) { - return index; - } - } - return -1; - } - - /** - * Used by `sortBy` to compare transformed `collection` elements, stable sorting - * them in ascending order. - * - * @private - * @param {Object} a The object to compare to `b`. - * @param {Object} b The object to compare to `a`. - * @returns {Number} Returns the sort order indicator of `1` or `-1`. - */ - function compareAscending(a, b) { - var ac = a.criteria, - bc = b.criteria; - - // ensure a stable sort in V8 and other engines - // http://code.google.com/p/v8/issues/detail?id=90 - if (ac !== bc) { - if (ac > bc || typeof ac == 'undefined') { - return 1; - } - if (ac < bc || typeof bc == 'undefined') { - return -1; - } - } - // The JS engine embedded in Adobe applications like InDesign has a buggy - // `Array#sort` implementation that causes it, under certain circumstances, - // to return the same value for `a` and `b`. - // See https://github.com/jashkenas/underscore/pull/1247 - return a.index - b.index; - } - - /** - * Used by `template` to escape characters for inclusion in compiled - * string literals. - * - * @private - * @param {String} match The matched character to escape. - * @returns {String} Returns the escaped character. - */ - function escapeStringChar(match) { - return '\\' + stringEscapes[match]; - } - - /** - * A no-operation function. - * - * @private - */ - function noop() { - // no operation performed - } - - /*--------------------------------------------------------------------------*/ - - /** - * Used for `Array` method references. - * - * Normally `Array.prototype` would suffice, however, using an array literal - * avoids issues in Narwhal. - */ - var arrayRef = []; - - /** Used for native method references */ - var objectProto = Object.prototype; - - /** Used to restore the original `_` reference in `noConflict` */ - var oldDash = window._; - - /** Used to detect if a method is native */ - var reNative = RegExp('^' + - String(objectProto.valueOf) - .replace(/[.*+?^${}()|[\]\\]/g, '\\$&') - .replace(/valueOf|for [^\]]+/g, '.+?') + '$' - ); - - /** Native method shortcuts */ - var ceil = Math.ceil, - floor = Math.floor, - hasOwnProperty = objectProto.hasOwnProperty, - push = arrayRef.push, - toString = objectProto.toString, - unshift = arrayRef.unshift; - - /* Native method shortcuts for methods with the same name as other `lodash` methods */ - var nativeBind = reNative.test(nativeBind = toString.bind) && nativeBind, - nativeCreate = reNative.test(nativeCreate = Object.create) && nativeCreate, - nativeIsArray = reNative.test(nativeIsArray = Array.isArray) && nativeIsArray, - nativeIsFinite = window.isFinite, - nativeIsNaN = window.isNaN, - nativeKeys = reNative.test(nativeKeys = Object.keys) && nativeKeys, - nativeMax = Math.max, - nativeMin = Math.min, - nativeRandom = Math.random, - nativeSlice = arrayRef.slice; - - /** Detect various environments */ - var isIeOpera = reNative.test(window.attachEvent), - isV8 = nativeBind && !/\n|true/.test(nativeBind + isIeOpera); - - /*--------------------------------------------------------------------------*/ - - /** - * Creates a `lodash` object which wraps the given value to enable method - * chaining. - * - * In addition to Lo-Dash methods, wrappers also have the following `Array` methods: - * `concat`, `join`, `pop`, `push`, `reverse`, `shift`, `slice`, `sort`, `splice`, - * and `unshift` - * - * Chaining is supported in custom builds as long as the `value` method is - * implicitly or explicitly included in the build. - * - * The chainable wrapper functions are: - * `after`, `assign`, `bind`, `bindAll`, `bindKey`, `chain`, `compact`, - * `compose`, `concat`, `countBy`, `createCallback`, `curry`, `debounce`, - * `defaults`, `defer`, `delay`, `difference`, `filter`, `flatten`, `forEach`, - * `forEachRight`, `forIn`, `forInRight`, `forOwn`, `forOwnRight`, `functions`, - * `groupBy`, `indexBy`, `initial`, `intersection`, `invert`, `invoke`, `keys`, - * `map`, `max`, `memoize`, `merge`, `min`, `object`, `omit`, `once`, `pairs`, - * `partial`, `partialRight`, `pick`, `pluck`, `pull`, `push`, `range`, `reject`, - * `remove`, `rest`, `reverse`, `shuffle`, `slice`, `sort`, `sortBy`, `splice`, - * `tap`, `throttle`, `times`, `toArray`, `transform`, `union`, `uniq`, `unshift`, - * `unzip`, `values`, `where`, `without`, `wrap`, and `zip` - * - * The non-chainable wrapper functions are: - * `clone`, `cloneDeep`, `contains`, `escape`, `every`, `find`, `findIndex`, - * `findKey`, `findLast`, `findLastIndex`, `findLastKey`, `has`, `identity`, - * `indexOf`, `isArguments`, `isArray`, `isBoolean`, `isDate`, `isElement`, - * `isEmpty`, `isEqual`, `isFinite`, `isFunction`, `isNaN`, `isNull`, `isNumber`, - * `isObject`, `isPlainObject`, `isRegExp`, `isString`, `isUndefined`, `join`, - * `lastIndexOf`, `mixin`, `noConflict`, `parseInt`, `pop`, `random`, `reduce`, - * `reduceRight`, `result`, `shift`, `size`, `some`, `sortedIndex`, `runInContext`, - * `template`, `unescape`, `uniqueId`, and `value` - * - * The wrapper functions `first` and `last` return wrapped values when `n` is - * provided, otherwise they return unwrapped values. - * - * @name _ - * @constructor - * @category Chaining - * @param {Mixed} value The value to wrap in a `lodash` instance. - * @returns {Object} Returns a `lodash` instance. - * @example - * - * var wrapped = _([1, 2, 3]); - * - * // returns an unwrapped value - * wrapped.reduce(function(sum, num) { - * return sum + num; - * }); - * // => 6 - * - * // returns a wrapped value - * var squares = wrapped.map(function(num) { - * return num * num; - * }); - * - * _.isArray(squares); - * // => false - * - * _.isArray(squares.value()); - * // => true - */ - function lodash(value) { - return (value instanceof lodash) - ? value - : new lodashWrapper(value); - } - - /** - * A fast path for creating `lodash` wrapper objects. - * - * @private - * @param {Mixed} value The value to wrap in a `lodash` instance. - * @param {Boolean} chainAll A flag to enable chaining for all methods - * @returns {Object} Returns a `lodash` instance. - */ - function lodashWrapper(value, chainAll) { - this.__chain__ = !!chainAll; - this.__wrapped__ = value; - } - // ensure `new lodashWrapper` is an instance of `lodash` - lodashWrapper.prototype = lodash.prototype; - - /** - * An object used to flag environments features. - * - * @static - * @memberOf _ - * @type Object - */ - var support = {}; - - (function() { - var object = { '0': 1, 'length': 1 }; - - /** - * Detect if `Function#bind` exists and is inferred to be fast (all but V8). - * - * @memberOf _.support - * @type Boolean - */ - support.fastBind = nativeBind && !isV8; - - /** - * Detect if `Array#shift` and `Array#splice` augment array-like objects correctly. - * - * Firefox < 10, IE compatibility mode, and IE < 9 have buggy Array `shift()` - * and `splice()` functions that fail to remove the last element, `value[0]`, - * of array-like objects even though the `length` property is set to `0`. - * The `shift()` method is buggy in IE 8 compatibility mode, while `splice()` - * is buggy regardless of mode in IE < 9 and buggy in compatibility mode in IE 9. - * - * @memberOf _.support - * @type Boolean - */ - support.spliceObjects = (arrayRef.splice.call(object, 0, 1), !object[0]); - }(1)); - - /** - * By default, the template delimiters used by Lo-Dash are similar to those in - * embedded Ruby (ERB). Change the following template settings to use alternative - * delimiters. - * - * @static - * @memberOf _ - * @type Object - */ - lodash.templateSettings = { - - /** - * Used to detect `data` property values to be HTML-escaped. - * - * @memberOf _.templateSettings - * @type RegExp - */ - 'escape': /<%-([\s\S]+?)%>/g, - - /** - * Used to detect code to be evaluated. - * - * @memberOf _.templateSettings - * @type RegExp - */ - 'evaluate': /<%([\s\S]+?)%>/g, - - /** - * Used to detect `data` property values to inject. - * - * @memberOf _.templateSettings - * @type RegExp - */ - 'interpolate': reInterpolate, - - /** - * Used to reference the data object in the template text. - * - * @memberOf _.templateSettings - * @type String - */ - 'variable': '' - }; - - /*--------------------------------------------------------------------------*/ - - /** - * The base implementation of `_.createCallback` without support for creating - * "_.pluck" or "_.where" style callbacks. - * - * @private - * @param {Mixed} [func=identity] The value to convert to a callback. - * @param {Mixed} [thisArg] The `this` binding of the created callback. - * @param {Number} [argCount] The number of arguments the callback accepts. - * @returns {Function} Returns a callback function. - */ - function baseCreateCallback(func, thisArg, argCount) { - if (typeof func != 'function') { - return identity; - } - // exit early if there is no `thisArg` - if (typeof thisArg == 'undefined') { - return func; - } - switch (argCount) { - case 1: return function(value) { - return func.call(thisArg, value); - }; - case 2: return function(a, b) { - return func.call(thisArg, a, b); - }; - case 3: return function(value, index, collection) { - return func.call(thisArg, value, index, collection); - }; - case 4: return function(accumulator, value, index, collection) { - return func.call(thisArg, accumulator, value, index, collection); - }; - } - return bind(func, thisArg); - } - - /** - * The base implementation of `_.flatten` without support for callback - * shorthands or `thisArg` binding. - * - * @private - * @param {Array} array The array to flatten. - * @param {Boolean} [isShallow=false] A flag to restrict flattening to a single level. - * @param {Boolean} [isArgArrays=false] A flag to restrict flattening to arrays and `arguments` objects. - * @param {Number} [fromIndex=0] The index to start from. - * @returns {Array} Returns a new flattened array. - */ - function baseFlatten(array, isShallow, isArgArrays, fromIndex) { - var index = (fromIndex || 0) - 1, - length = array ? array.length : 0, - result = []; - - while (++index < length) { - var value = array[index]; - // recursively flatten arrays (susceptible to call stack limits) - if (value && typeof value == 'object' && (isArray(value) || isArguments(value))) { - push.apply(result, isShallow ? value : baseFlatten(value, isShallow, isArgArrays)); - } else if (!isArgArrays) { - result.push(value); - } - } - return result; - } - - /** - * The base implementation of `_.isEqual`, without support for `thisArg` binding, - * that allows partial "_.where" style comparisons. - * - * @private - * @param {Mixed} a The value to compare. - * @param {Mixed} b The other value to compare. - * @param {Function} [callback] The function to customize comparing values. - * @param {Function} [isWhere=false] A flag to indicate performing partial comparisons. - * @param {Array} [stackA=[]] Tracks traversed `a` objects. - * @param {Array} [stackB=[]] Tracks traversed `b` objects. - * @returns {Boolean} Returns `true` if the values are equivalent, else `false`. - */ - function baseIsEqual(a, b, stackA, stackB) { - if (a === b) { - return a !== 0 || (1 / a == 1 / b); - } - var type = typeof a, - otherType = typeof b; - - if (a === a && - !(a && objectTypes[type]) && - !(b && objectTypes[otherType])) { - return false; - } - if (a == null || b == null) { - return a === b; - } - var className = toString.call(a), - otherClass = toString.call(b); - - if (className != otherClass) { - return false; - } - switch (className) { - case boolClass: - case dateClass: - return +a == +b; - - case numberClass: - return a != +a - ? b != +b - : (a == 0 ? (1 / a == 1 / b) : a == +b); - - case regexpClass: - case stringClass: - return a == String(b); - } - var isArr = className == arrayClass; - if (!isArr) { - if (hasOwnProperty.call(a, '__wrapped__ ') || b instanceof lodash) { - return baseIsEqual(a.__wrapped__ || a, b.__wrapped__ || b, stackA, stackB); - } - if (className != objectClass) { - return false; - } - var ctorA = a.constructor, - ctorB = b.constructor; - - if (ctorA != ctorB && !( - isFunction(ctorA) && ctorA instanceof ctorA && - isFunction(ctorB) && ctorB instanceof ctorB - )) { - return false; - } - } - stackA || (stackA = []); - stackB || (stackB = []); - - var length = stackA.length; - while (length--) { - if (stackA[length] == a) { - return stackB[length] == b; - } - } - var result = true, - size = 0; - - stackA.push(a); - stackB.push(b); - - if (isArr) { - size = b.length; - result = size == a.length; - - if (result) { - while (size--) { - if (!(result = baseIsEqual(a[size], b[size], stackA, stackB))) { - break; - } - } - } - return result; - } - forIn(b, function(value, key, b) { - if (hasOwnProperty.call(b, key)) { - size++; - return !(result = hasOwnProperty.call(a, key) && baseIsEqual(a[key], value, stackA, stackB)) && indicatorObject; - } - }); - - if (result) { - forIn(a, function(value, key, a) { - if (hasOwnProperty.call(a, key)) { - return !(result = --size > -1) && indicatorObject; - } - }); - } - return result; - } - - /** - * The base implementation of `_.uniq` without support for callback shorthands - * or `thisArg` binding. - * - * @private - * @param {Array} array The array to process. - * @param {Boolean} [isSorted=false] A flag to indicate that `array` is sorted. - * @param {Function} [callback] The function called per iteration. - * @returns {Array} Returns a duplicate-value-free array. - */ - function baseUniq(array, isSorted, callback) { - var index = -1, - indexOf = getIndexOf(), - length = array ? array.length : 0, - result = [], - seen = callback ? [] : result; - - while (++index < length) { - var value = array[index], - computed = callback ? callback(value, index, array) : value; - - if (isSorted - ? !index || seen[seen.length - 1] !== computed - : indexOf(seen, computed) < 0 - ) { - if (callback) { - seen.push(computed); - } - result.push(value); - } - } - return result; - } - - /** - * Creates a function that aggregates a collection, creating an object composed - * of keys generated from the results of running each element of the collection - * through a callback. The given `setter` function sets the keys and values - * of the composed object. - * - * @private - * @param {Function} setter The setter function. - * @returns {Function} Returns the new aggregator function. - */ - function createAggregator(setter) { - return function(collection, callback, thisArg) { - var result = {}; - callback = createCallback(callback, thisArg, 3); - forEach(collection, function(value, key, collection) { - key = String(callback(value, key, collection)); - setter(result, value, key, collection); - }); - return result; - }; - } - - /** - * Creates a function that, when called, either curries or invokes `func` - * with an optional `this` binding and partially applied arguments. - * - * @private - * @param {Function|String} func The function or method name to reference. - * @param {Number} bitmask The bitmask of method flags to compose. - * The bitmask may be composed of the following flags: - * 1 - `_.bind` - * 2 - `_.bindKey` - * 4 - `_.curry` - * 8 - `_.curry` (bound) - * 16 - `_.partial` - * 32 - `_.partialRight` - * @param {Array} [partialArgs] An array of arguments to prepend to those - * provided to the new function. - * @param {Array} [partialRightArgs] An array of arguments to append to those - * provided to the new function. - * @param {Mixed} [thisArg] The `this` binding of `func`. - * @param {Number} [arity] The arity of `func`. - * @returns {Function} Returns the new bound function. - */ - function createBound(func, bitmask, partialArgs, partialRightArgs, thisArg, arity) { - var isBind = bitmask & 1, - isBindKey = bitmask & 2, - isCurry = bitmask & 4, - isCurryBound = bitmask & 8, - isPartial = bitmask & 16, - isPartialRight = bitmask & 32; - - if (!isBindKey && !isFunction(func)) { - throw new TypeError; - } - // use `Function#bind` if it exists and is fast - // (in V8 `Function#bind` is slower except when partially applied) - if (isBind && !(isBindKey || isCurry || isPartialRight) && - (support.fastBind || (nativeBind && partialArgs.length))) { - var args = [func, thisArg]; - push.apply(args, partialArgs); - var bound = nativeBind.call.apply(nativeBind, args); - } - else { - bound = function() { - // `Function#bind` spec - // http://es5.github.io/#x15.3.4.5 - var args = arguments, - thisBinding = isBind ? thisArg : this; - - if (partialArgs) { - unshift.apply(args, partialArgs); - } - if (partialRightArgs) { - push.apply(args, partialRightArgs); - } - if (isCurry && args.length < arity) { - bitmask |= 16 & ~32 - return createBound(func, (isCurryBound ? bitmask : bitmask & ~3), args, null, thisArg, arity); - } - if (isBindKey) { - func = thisBinding[key]; - } - if (this instanceof bound) { - // ensure `new bound` is an instance of `func` - thisBinding = createObject(func.prototype); - - // mimic the constructor's `return` behavior - // http://es5.github.io/#x13.2.2 - var result = func.apply(thisBinding, args); - return isObject(result) ? result : thisBinding; - } - return func.apply(thisBinding, args); - }; - } - if (isBindKey) { - var key = thisArg; - thisArg = func; - } - return bound; - } - - /** - * Creates a new object with the specified `prototype`. - * - * @private - * @param {Object} prototype The prototype object. - * @returns {Object} Returns the new object. - */ - function createObject(prototype) { - return isObject(prototype) ? nativeCreate(prototype) : {}; - } - // fallback for browsers without `Object.create` - if (!nativeCreate) { - createObject = function(prototype) { - if (isObject(prototype)) { - noop.prototype = prototype; - var result = new noop; - noop.prototype = null; - } - return result || {}; - }; - } - - /** - * Used by `escape` to convert characters to HTML entities. - * - * @private - * @param {String} match The matched character to escape. - * @returns {String} Returns the escaped character. - */ - function escapeHtmlChar(match) { - return htmlEscapes[match]; - } - - /** - * Gets the appropriate "indexOf" function. If the `_.indexOf` method is - * customized, this method returns the custom method, otherwise it returns - * the `baseIndexOf` function. - * - * @private - * @returns {Function} Returns the "indexOf" function. - */ - function getIndexOf() { - var result = (result = lodash.indexOf) === indexOf ? baseIndexOf : result; - return result; - } - - /** - * Used by `unescape` to convert HTML entities to characters. - * - * @private - * @param {String} match The matched character to unescape. - * @returns {String} Returns the unescaped character. - */ - function unescapeHtmlChar(match) { - return htmlUnescapes[match]; - } - - /*--------------------------------------------------------------------------*/ - - /** - * Checks if `value` is an `arguments` object. - * - * @static - * @memberOf _ - * @category Objects - * @param {Mixed} value The value to check. - * @returns {Boolean} Returns `true` if the `value` is an `arguments` object, else `false`. - * @example - * - * (function() { return _.isArguments(arguments); })(1, 2, 3); - * // => true - * - * _.isArguments([1, 2, 3]); - * // => false - */ - function isArguments(value) { - return (value && typeof value == 'object') ? toString.call(value) == argsClass : false; - } - // fallback for browsers that can't detect `arguments` objects by [[Class]] - if (!isArguments(arguments)) { - isArguments = function(value) { - return (value && typeof value == 'object') ? hasOwnProperty.call(value, 'callee') : false; - }; - } - - /** - * Checks if `value` is an array. - * - * @static - * @memberOf _ - * @type Function - * @category Objects - * @param {Mixed} value The value to check. - * @returns {Boolean} Returns `true` if the `value` is an array, else `false`. - * @example - * - * (function() { return _.isArray(arguments); })(); - * // => false - * - * _.isArray([1, 2, 3]); - * // => true - */ - var isArray = nativeIsArray || function(value) { - return (value && typeof value == 'object') ? toString.call(value) == arrayClass : false; - }; - - /** - * A fallback implementation of `Object.keys` which produces an array of the - * given object's own enumerable property names. - * - * @private - * @type Function - * @param {Object} object The object to inspect. - * @returns {Array} Returns an array of property names. - */ - var shimKeys = function(object) { - var index, iterable = object, result = []; - if (!iterable) return result; - if (!(objectTypes[typeof object])) return result; - for (index in iterable) { - if (hasOwnProperty.call(iterable, index)) { - result.push(index); - } - } - return result - }; - - /** - * Creates an array composed of the own enumerable property names of an object. - * - * @static - * @memberOf _ - * @category Objects - * @param {Object} object The object to inspect. - * @returns {Array} Returns an array of property names. - * @example - * - * _.keys({ 'one': 1, 'two': 2, 'three': 3 }); - * // => ['one', 'two', 'three'] (order is not guaranteed) - */ - var keys = !nativeKeys ? shimKeys : function(object) { - if (!isObject(object)) { - return []; - } - return nativeKeys(object); - }; - - /** - * Used to convert characters to HTML entities: - * - * Though the `>` character is escaped for symmetry, characters like `>` and `/` - * don't require escaping in HTML and have no special meaning unless they're part - * of a tag or an unquoted attribute value. - * http://mathiasbynens.be/notes/ambiguous-ampersands (under "semi-related fun fact") - */ - var htmlEscapes = { - '&': '&', - '<': '<', - '>': '>', - '"': '"', - "'": ''', - '/': '/' - }; - - /** Used to convert HTML entities to characters */ - var htmlUnescapes = invert(htmlEscapes); - - /** Used to match HTML entities and HTML characters */ - var reEscapedHtml = RegExp('(' + keys(htmlUnescapes).join('|') + ')', 'g'), - reUnescapedHtml = RegExp('[' + keys(htmlEscapes).join('') + ']', 'g'); - - /*--------------------------------------------------------------------------*/ - - /** - * Assigns own enumerable properties of source object(s) to the destination - * object. Subsequent sources will overwrite property assignments of previous - * sources. If a callback is provided it will be executed to produce the - * assigned values. The callback is bound to `thisArg` and invoked with two - * arguments; (objectValue, sourceValue). - * - * @static - * @memberOf _ - * @type Function - * @alias extend - * @category Objects - * @param {Object} object The destination object. - * @param {Object} [source1, source2, ...] The source objects. - * @param {Function} [callback] The function to customize assigning values. - * @param {Mixed} [thisArg] The `this` binding of `callback`. - * @returns {Object} Returns the destination object. - * @example - * - * _.assign({ 'name': 'moe' }, { 'age': 40 }); - * // => { 'name': 'moe', 'age': 40 } - * - * var defaults = _.partialRight(_.assign, function(a, b) { - * return typeof a == 'undefined' ? b : a; - * }); - * - * var food = { 'name': 'apple' }; - * defaults(food, { 'name': 'banana', 'type': 'fruit' }); - * // => { 'name': 'apple', 'type': 'fruit' } - */ - function assign(object) { - if (!object) { - return object; - } - for (var argsIndex = 1, argsLength = arguments.length; argsIndex < argsLength; argsIndex++) { - var iterable = arguments[argsIndex]; - if (iterable) { - for (var key in iterable) { - object[key] = iterable[key]; - } - } - } - return object; - } - - /** - * Creates a clone of `value`. If `deep` is `true` nested objects will also - * be cloned, otherwise they will be assigned by reference. If a callback - * is provided it will be executed to produce the cloned values. If the - * callback returns `undefined` cloning will be handled by the method instead. - * The callback is bound to `thisArg` and invoked with one argument; (value). - * - * @static - * @memberOf _ - * @category Objects - * @param {Mixed} value The value to clone. - * @param {Boolean} [deep=false] A flag to indicate a deep clone. - * @param {Function} [callback] The function to customize cloning values. - * @param {Mixed} [thisArg] The `this` binding of `callback`. - * @returns {Mixed} Returns the cloned `value`. - * @example - * - * var stooges = [ - * { 'name': 'moe', 'age': 40 }, - * { 'name': 'larry', 'age': 50 } - * ]; - * - * var shallow = _.clone(stooges); - * shallow[0] === stooges[0]; - * // => true - * - * var deep = _.clone(stooges, true); - * deep[0] === stooges[0]; - * // => false - * - * _.mixin({ - * 'clone': _.partialRight(_.clone, function(value) { - * return _.isElement(value) ? value.cloneNode(false) : undefined; - * }) - * }); - * - * var clone = _.clone(document.body); - * clone.childNodes.length; - * // => 0 - */ - function clone(value) { - return isObject(value) - ? (isArray(value) ? nativeSlice.call(value) : assign({}, value)) - : value; - } - - /** - * Assigns own enumerable properties of source object(s) to the destination - * object for all destination properties that resolve to `undefined`. Once a - * property is set, additional defaults of the same property will be ignored. - * - * @static - * @memberOf _ - * @type Function - * @category Objects - * @param {Object} object The destination object. - * @param {Object} [source1, source2, ...] The source objects. - * @param- {Object} [guard] Allows working with `_.reduce` without using - * their `key` and `object` arguments as sources. - * @returns {Object} Returns the destination object. - * @example - * - * var food = { 'name': 'apple' }; - * _.defaults(food, { 'name': 'banana', 'type': 'fruit' }); - * // => { 'name': 'apple', 'type': 'fruit' } - */ - function defaults(object) { - if (!object) { - return object; - } - for (var argsIndex = 1, argsLength = arguments.length; argsIndex < argsLength; argsIndex++) { - var iterable = arguments[argsIndex]; - if (iterable) { - for (var key in iterable) { - if (typeof object[key] == 'undefined') { - object[key] = iterable[key]; - } - } - } - } - return object; - } - - /** - * Iterates over own and inherited enumerable properties of an object, - * executing the callback for each property. The callback is bound to `thisArg` - * and invoked with three arguments; (value, key, object). Callbacks may exit - * iteration early by explicitly returning `false`. - * - * @static - * @memberOf _ - * @type Function - * @category Objects - * @param {Object} object The object to iterate over. - * @param {Function} [callback=identity] The function called per iteration. - * @param {Mixed} [thisArg] The `this` binding of `callback`. - * @returns {Object} Returns `object`. - * @example - * - * function Dog(name) { - * this.name = name; - * } - * - * Dog.prototype.bark = function() { - * console.log('Woof, woof!'); - * }; - * - * _.forIn(new Dog('Dagny'), function(value, key) { - * console.log(key); - * }); - * // => logs 'bark' and 'name' (order is not guaranteed) - */ - var forIn = function(collection, callback) { - var index, iterable = collection, result = iterable; - if (!iterable) return result; - if (!objectTypes[typeof iterable]) return result; - for (index in iterable) { - if (callback(iterable[index], index, collection) === indicatorObject) return result; - } - return result - }; - - /** - * Iterates over own enumerable properties of an object, executing the callback - * for each property. The callback is bound to `thisArg` and invoked with three - * arguments; (value, key, object). Callbacks may exit iteration early by - * explicitly returning `false`. - * - * @static - * @memberOf _ - * @type Function - * @category Objects - * @param {Object} object The object to iterate over. - * @param {Function} [callback=identity] The function called per iteration. - * @param {Mixed} [thisArg] The `this` binding of `callback`. - * @returns {Object} Returns `object`. - * @example - * - * _.forOwn({ '0': 'zero', '1': 'one', 'length': 2 }, function(num, key) { - * console.log(key); - * }); - * // => logs '0', '1', and 'length' (order is not guaranteed) - */ - var forOwn = function(collection, callback) { - var index, iterable = collection, result = iterable; - if (!iterable) return result; - if (!objectTypes[typeof iterable]) return result; - for (index in iterable) { - if (hasOwnProperty.call(iterable, index)) { - if (callback(iterable[index], index, collection) === indicatorObject) return result; - } - } - return result - }; - - /** - * Creates a sorted array of property names of all enumerable properties, - * own and inherited, of `object` that have function values. - * - * @static - * @memberOf _ - * @alias methods - * @category Objects - * @param {Object} object The object to inspect. - * @returns {Array} Returns an array of property names that have function values. - * @example - * - * _.functions(_); - * // => ['all', 'any', 'bind', 'bindAll', 'clone', 'compact', 'compose', ...] - */ - function functions(object) { - var result = []; - forIn(object, function(value, key) { - if (isFunction(value)) { - result.push(key); - } - }); - return result.sort(); - } - - /** - * Checks if the specified object `property` exists and is a direct property, - * instead of an inherited property. - * - * @static - * @memberOf _ - * @category Objects - * @param {Object} object The object to check. - * @param {String} property The property to check for. - * @returns {Boolean} Returns `true` if key is a direct property, else `false`. - * @example - * - * _.has({ 'a': 1, 'b': 2, 'c': 3 }, 'b'); - * // => true - */ - function has(object, property) { - return object ? hasOwnProperty.call(object, property) : false; - } - - /** - * Creates an object composed of the inverted keys and values of the given object. - * - * @static - * @memberOf _ - * @category Objects - * @param {Object} object The object to invert. - * @returns {Object} Returns the created inverted object. - * @example - * - * _.invert({ 'first': 'moe', 'second': 'larry' }); - * // => { 'moe': 'first', 'larry': 'second' } - */ - function invert(object) { - var index = -1, - props = keys(object), - length = props.length, - result = {}; - - while (++index < length) { - var key = props[index]; - result[object[key]] = key; - } - return result; - } - - /** - * Checks if `value` is a boolean value. - * - * @static - * @memberOf _ - * @category Objects - * @param {Mixed} value The value to check. - * @returns {Boolean} Returns `true` if the `value` is a boolean value, else `false`. - * @example - * - * _.isBoolean(null); - * // => false - */ - function isBoolean(value) { - return value === true || value === false || toString.call(value) == boolClass; - } - - /** - * Checks if `value` is a date. - * - * @static - * @memberOf _ - * @category Objects - * @param {Mixed} value The value to check. - * @returns {Boolean} Returns `true` if the `value` is a date, else `false`. - * @example - * - * _.isDate(new Date); - * // => true - */ - function isDate(value) { - return value ? (typeof value == 'object' && toString.call(value) == dateClass) : false; - } - - /** - * Checks if `value` is a DOM element. - * - * @static - * @memberOf _ - * @category Objects - * @param {Mixed} value The value to check. - * @returns {Boolean} Returns `true` if the `value` is a DOM element, else `false`. - * @example - * - * _.isElement(document.body); - * // => true - */ - function isElement(value) { - return value ? value.nodeType === 1 : false; - } - - /** - * Checks if `value` is empty. Arrays, strings, or `arguments` objects with a - * length of `0` and objects with no own enumerable properties are considered - * "empty". - * - * @static - * @memberOf _ - * @category Objects - * @param {Array|Object|String} value The value to inspect. - * @returns {Boolean} Returns `true` if the `value` is empty, else `false`. - * @example - * - * _.isEmpty([1, 2, 3]); - * // => false - * - * _.isEmpty({}); - * // => true - * - * _.isEmpty(''); - * // => true - */ - function isEmpty(value) { - if (!value) { - return true; - } - if (isArray(value) || isString(value)) { - return !value.length; - } - for (var key in value) { - if (hasOwnProperty.call(value, key)) { - return false; - } - } - return true; - } - - /** - * Performs a deep comparison between two values to determine if they are - * equivalent to each other. If a callback is provided it will be executed - * to compare values. If the callback returns `undefined` comparisons will - * be handled by the method instead. The callback is bound to `thisArg` and - * invoked with two arguments; (a, b). - * - * @static - * @memberOf _ - * @category Objects - * @param {Mixed} a The value to compare. - * @param {Mixed} b The other value to compare. - * @param {Function} [callback] The function to customize comparing values. - * @param {Mixed} [thisArg] The `this` binding of `callback`. - * @returns {Boolean} Returns `true` if the values are equivalent, else `false`. - * @example - * - * var moe = { 'name': 'moe', 'age': 40 }; - * var copy = { 'name': 'moe', 'age': 40 }; - * - * moe == copy; - * // => false - * - * _.isEqual(moe, copy); - * // => true - * - * var words = ['hello', 'goodbye']; - * var otherWords = ['hi', 'goodbye']; - * - * _.isEqual(words, otherWords, function(a, b) { - * var reGreet = /^(?:hello|hi)$/i, - * aGreet = _.isString(a) && reGreet.test(a), - * bGreet = _.isString(b) && reGreet.test(b); - * - * return (aGreet || bGreet) ? (aGreet == bGreet) : undefined; - * }); - * // => true - */ - function isEqual(a, b) { - return baseIsEqual(a, b); - } - - /** - * Checks if `value` is, or can be coerced to, a finite number. - * - * Note: This is not the same as native `isFinite` which will return true for - * booleans and empty strings. See http://es5.github.io/#x15.1.2.5. - * - * @static - * @memberOf _ - * @category Objects - * @param {Mixed} value The value to check. - * @returns {Boolean} Returns `true` if the `value` is finite, else `false`. - * @example - * - * _.isFinite(-101); - * // => true - * - * _.isFinite('10'); - * // => true - * - * _.isFinite(true); - * // => false - * - * _.isFinite(''); - * // => false - * - * _.isFinite(Infinity); - * // => false - */ - function isFinite(value) { - return nativeIsFinite(value) && !nativeIsNaN(parseFloat(value)); - } - - /** - * Checks if `value` is a function. - * - * @static - * @memberOf _ - * @category Objects - * @param {Mixed} value The value to check. - * @returns {Boolean} Returns `true` if the `value` is a function, else `false`. - * @example - * - * _.isFunction(_); - * // => true - */ - function isFunction(value) { - return typeof value == 'function'; - } - // fallback for older versions of Chrome and Safari - if (isFunction(/x/)) { - isFunction = function(value) { - return typeof value == 'function' && toString.call(value) == funcClass; - }; - } - - /** - * Checks if `value` is the language type of Object. - * (e.g. arrays, functions, objects, regexes, `new Number(0)`, and `new String('')`) - * - * @static - * @memberOf _ - * @category Objects - * @param {Mixed} value The value to check. - * @returns {Boolean} Returns `true` if the `value` is an object, else `false`. - * @example - * - * _.isObject({}); - * // => true - * - * _.isObject([1, 2, 3]); - * // => true - * - * _.isObject(1); - * // => false - */ - function isObject(value) { - // check if the value is the ECMAScript language type of Object - // http://es5.github.io/#x8 - // and avoid a V8 bug - // http://code.google.com/p/v8/issues/detail?id=2291 - return !!(value && objectTypes[typeof value]); - } - - /** - * Checks if `value` is `NaN`. - * - * Note: This is not the same as native `isNaN` which will return `true` for - * `undefined` and other non-numeric values. See http://es5.github.io/#x15.1.2.4. - * - * @static - * @memberOf _ - * @category Objects - * @param {Mixed} value The value to check. - * @returns {Boolean} Returns `true` if the `value` is `NaN`, else `false`. - * @example - * - * _.isNaN(NaN); - * // => true - * - * _.isNaN(new Number(NaN)); - * // => true - * - * isNaN(undefined); - * // => true - * - * _.isNaN(undefined); - * // => false - */ - function isNaN(value) { - // `NaN` as a primitive is the only value that is not equal to itself - // (perform the [[Class]] check first to avoid errors with some host objects in IE) - return isNumber(value) && value != +value; - } - - /** - * Checks if `value` is `null`. - * - * @static - * @memberOf _ - * @category Objects - * @param {Mixed} value The value to check. - * @returns {Boolean} Returns `true` if the `value` is `null`, else `false`. - * @example - * - * _.isNull(null); - * // => true - * - * _.isNull(undefined); - * // => false - */ - function isNull(value) { - return value === null; - } - - /** - * Checks if `value` is a number. - * - * Note: `NaN` is considered a number. See http://es5.github.io/#x8.5. - * - * @static - * @memberOf _ - * @category Objects - * @param {Mixed} value The value to check. - * @returns {Boolean} Returns `true` if the `value` is a number, else `false`. - * @example - * - * _.isNumber(8.4 * 5); - * // => true - */ - function isNumber(value) { - return typeof value == 'number' || toString.call(value) == numberClass; - } - - /** - * Checks if `value` is a regular expression. - * - * @static - * @memberOf _ - * @category Objects - * @param {Mixed} value The value to check. - * @returns {Boolean} Returns `true` if the `value` is a regular expression, else `false`. - * @example - * - * _.isRegExp(/moe/); - * // => true - */ - function isRegExp(value) { - return (value && objectTypes[typeof value]) ? toString.call(value) == regexpClass : false; - } - - /** - * Checks if `value` is a string. - * - * @static - * @memberOf _ - * @category Objects - * @param {Mixed} value The value to check. - * @returns {Boolean} Returns `true` if the `value` is a string, else `false`. - * @example - * - * _.isString('moe'); - * // => true - */ - function isString(value) { - return typeof value == 'string' || toString.call(value) == stringClass; - } - - /** - * Checks if `value` is `undefined`. - * - * @static - * @memberOf _ - * @category Objects - * @param {Mixed} value The value to check. - * @returns {Boolean} Returns `true` if the `value` is `undefined`, else `false`. - * @example - * - * _.isUndefined(void 0); - * // => true - */ - function isUndefined(value) { - return typeof value == 'undefined'; - } - - /** - * Creates a shallow clone of `object` excluding the specified properties. - * Property names may be specified as individual arguments or as arrays of - * property names. If a callback is provided it will be executed for each - * property of `object` omitting the properties the callback returns truthy - * for. The callback is bound to `thisArg` and invoked with three arguments; - * (value, key, object). - * - * @static - * @memberOf _ - * @category Objects - * @param {Object} object The source object. - * @param {Function|String} callback|[prop1, prop2, ...] The properties to omit - * or the function called per iteration. - * @param {Mixed} [thisArg] The `this` binding of `callback`. - * @returns {Object} Returns an object without the omitted properties. - * @example - * - * _.omit({ 'name': 'moe', 'age': 40 }, 'age'); - * // => { 'name': 'moe' } - * - * _.omit({ 'name': 'moe', 'age': 40 }, function(value) { - * return typeof value == 'number'; - * }); - * // => { 'name': 'moe' } - */ - function omit(object) { - var indexOf = getIndexOf(), - props = baseFlatten(arguments, true, false, 1), - result = {}; - - forIn(object, function(value, key) { - if (indexOf(props, key) < 0) { - result[key] = value; - } - }); - return result; - } - - /** - * Creates a two dimensional array of an object's key-value pairs, - * i.e. `[[key1, value1], [key2, value2]]`. - * - * @static - * @memberOf _ - * @category Objects - * @param {Object} object The object to inspect. - * @returns {Array} Returns new array of key-value pairs. - * @example - * - * _.pairs({ 'moe': 30, 'larry': 40 }); - * // => [['moe', 30], ['larry', 40]] (order is not guaranteed) - */ - function pairs(object) { - var index = -1, - props = keys(object), - length = props.length, - result = Array(length); - - while (++index < length) { - var key = props[index]; - result[index] = [key, object[key]]; - } - return result; - } - - /** - * Creates a shallow clone of `object` composed of the specified properties. - * Property names may be specified as individual arguments or as arrays of - * property names. If a callback is provided it will be executed for each - * property of `object` picking the properties the callback returns truthy - * for. The callback is bound to `thisArg` and invoked with three arguments; - * (value, key, object). - * - * @static - * @memberOf _ - * @category Objects - * @param {Object} object The source object. - * @param {Array|Function|String} callback|[prop1, prop2, ...] The function - * called per iteration or property names to pick, specified as individual - * property names or arrays of property names. - * @param {Mixed} [thisArg] The `this` binding of `callback`. - * @returns {Object} Returns an object composed of the picked properties. - * @example - * - * _.pick({ 'name': 'moe', '_userid': 'moe1' }, 'name'); - * // => { 'name': 'moe' } - * - * _.pick({ 'name': 'moe', '_userid': 'moe1' }, function(value, key) { - * return key.charAt(0) != '_'; - * }); - * // => { 'name': 'moe' } - */ - function pick(object) { - var index = -1, - props = baseFlatten(arguments, true, false, 1), - length = props.length, - result = {}; - - while (++index < length) { - var prop = props[index]; - if (prop in object) { - result[prop] = object[prop]; - } - } - return result; - } - - /** - * Creates an array composed of the own enumerable property values of `object`. - * - * @static - * @memberOf _ - * @category Objects - * @param {Object} object The object to inspect. - * @returns {Array} Returns an array of property values. - * @example - * - * _.values({ 'one': 1, 'two': 2, 'three': 3 }); - * // => [1, 2, 3] (order is not guaranteed) - */ - function values(object) { - var index = -1, - props = keys(object), - length = props.length, - result = Array(length); - - while (++index < length) { - result[index] = object[props[index]]; - } - return result; - } - - /*--------------------------------------------------------------------------*/ - - /** - * Checks if a given value is present in a collection using strict equality - * for comparisons, i.e. `===`. If `fromIndex` is negative, it is used as the - * offset from the end of the collection. - * - * @static - * @memberOf _ - * @alias include - * @category Collections - * @param {Array|Object|String} collection The collection to iterate over. - * @param {Mixed} target The value to check for. - * @param {Number} [fromIndex=0] The index to search from. - * @returns {Boolean} Returns `true` if the `target` element is found, else `false`. - * @example - * - * _.contains([1, 2, 3], 1); - * // => true - * - * _.contains([1, 2, 3], 1, 2); - * // => false - * - * _.contains({ 'name': 'moe', 'age': 40 }, 'moe'); - * // => true - * - * _.contains('curly', 'ur'); - * // => true - */ - function contains(collection, target) { - var indexOf = getIndexOf(), - length = collection ? collection.length : 0, - result = false; - if (length && typeof length == 'number') { - result = indexOf(collection, target) > -1; - } else { - forOwn(collection, function(value) { - return (result = value === target) && indicatorObject; - }); - } - return result; - } - - /** - * Creates an object composed of keys generated from the results of running - * each element of `collection` through the callback. The corresponding value - * of each key is the number of times the key was returned by the callback. - * The callback is bound to `thisArg` and invoked with three arguments; - * (value, index|key, collection). - * - * If a property name is provided for `callback` the created "_.pluck" style - * callback will return the property value of the given element. - * - * If an object is provided for `callback` the created "_.where" style callback - * will return `true` for elements that have the properties of the given object, - * else `false`. - * - * @static - * @memberOf _ - * @category Collections - * @param {Array|Object|String} collection The collection to iterate over. - * @param {Function|Object|String} [callback=identity] The function called - * per iteration. If a property name or object is provided it will be used - * to create a "_.pluck" or "_.where" style callback, respectively. - * @param {Mixed} [thisArg] The `this` binding of `callback`. - * @returns {Object} Returns the composed aggregate object. - * @example - * - * _.countBy([4.3, 6.1, 6.4], function(num) { return Math.floor(num); }); - * // => { '4': 1, '6': 2 } - * - * _.countBy([4.3, 6.1, 6.4], function(num) { return this.floor(num); }, Math); - * // => { '4': 1, '6': 2 } - * - * _.countBy(['one', 'two', 'three'], 'length'); - * // => { '3': 2, '5': 1 } - */ - var countBy = createAggregator(function(result, value, key) { - (hasOwnProperty.call(result, key) ? result[key]++ : result[key] = 1); - }); - - /** - * Checks if the given callback returns truthy value for **all** elements of - * a collection. The callback is bound to `thisArg` and invoked with three - * arguments; (value, index|key, collection). - * - * If a property name is provided for `callback` the created "_.pluck" style - * callback will return the property value of the given element. - * - * If an object is provided for `callback` the created "_.where" style callback - * will return `true` for elements that have the properties of the given object, - * else `false`. - * - * @static - * @memberOf _ - * @alias all - * @category Collections - * @param {Array|Object|String} collection The collection to iterate over. - * @param {Function|Object|String} [callback=identity] The function called - * per iteration. If a property name or object is provided it will be used - * to create a "_.pluck" or "_.where" style callback, respectively. - * @param {Mixed} [thisArg] The `this` binding of `callback`. - * @returns {Boolean} Returns `true` if all elements passed the callback check, - * else `false`. - * @example - * - * _.every([true, 1, null, 'yes'], Boolean); - * // => false - * - * var stooges = [ - * { 'name': 'moe', 'age': 40 }, - * { 'name': 'larry', 'age': 50 } - * ]; - * - * // using "_.pluck" callback shorthand - * _.every(stooges, 'age'); - * // => true - * - * // using "_.where" callback shorthand - * _.every(stooges, { 'age': 50 }); - * // => false - */ - function every(collection, callback, thisArg) { - var result = true; - callback = createCallback(callback, thisArg, 3); - - var index = -1, - length = collection ? collection.length : 0; - - if (typeof length == 'number') { - while (++index < length) { - if (!(result = !!callback(collection[index], index, collection))) { - break; - } - } - } else { - forOwn(collection, function(value, index, collection) { - return !(result = !!callback(value, index, collection)) && indicatorObject; - }); - } - return result; - } - - /** - * Iterates over elements of a collection, returning an array of all elements - * the callback returns truthy for. The callback is bound to `thisArg` and - * invoked with three arguments; (value, index|key, collection). - * - * If a property name is provided for `callback` the created "_.pluck" style - * callback will return the property value of the given element. - * - * If an object is provided for `callback` the created "_.where" style callback - * will return `true` for elements that have the properties of the given object, - * else `false`. - * - * @static - * @memberOf _ - * @alias select - * @category Collections - * @param {Array|Object|String} collection The collection to iterate over. - * @param {Function|Object|String} [callback=identity] The function called - * per iteration. If a property name or object is provided it will be used - * to create a "_.pluck" or "_.where" style callback, respectively. - * @param {Mixed} [thisArg] The `this` binding of `callback`. - * @returns {Array} Returns a new array of elements that passed the callback check. - * @example - * - * var evens = _.filter([1, 2, 3, 4, 5, 6], function(num) { return num % 2 == 0; }); - * // => [2, 4, 6] - * - * var food = [ - * { 'name': 'apple', 'organic': false, 'type': 'fruit' }, - * { 'name': 'carrot', 'organic': true, 'type': 'vegetable' } - * ]; - * - * // using "_.pluck" callback shorthand - * _.filter(food, 'organic'); - * // => [{ 'name': 'carrot', 'organic': true, 'type': 'vegetable' }] - * - * // using "_.where" callback shorthand - * _.filter(food, { 'type': 'fruit' }); - * // => [{ 'name': 'apple', 'organic': false, 'type': 'fruit' }] - */ - function filter(collection, callback, thisArg) { - var result = []; - callback = createCallback(callback, thisArg, 3); - - var index = -1, - length = collection ? collection.length : 0; - - if (typeof length == 'number') { - while (++index < length) { - var value = collection[index]; - if (callback(value, index, collection)) { - result.push(value); - } - } - } else { - forOwn(collection, function(value, index, collection) { - if (callback(value, index, collection)) { - result.push(value); - } - }); - } - return result; - } - - /** - * Iterates over elements of a collection, returning the first element that - * the callback returns truthy for. The callback is bound to `thisArg` and - * invoked with three arguments; (value, index|key, collection). - * - * If a property name is provided for `callback` the created "_.pluck" style - * callback will return the property value of the given element. - * - * If an object is provided for `callback` the created "_.where" style callback - * will return `true` for elements that have the properties of the given object, - * else `false`. - * - * @static - * @memberOf _ - * @alias detect, findWhere - * @category Collections - * @param {Array|Object|String} collection The collection to iterate over. - * @param {Function|Object|String} [callback=identity] The function called - * per iteration. If a property name or object is provided it will be used - * to create a "_.pluck" or "_.where" style callback, respectively. - * @param {Mixed} [thisArg] The `this` binding of `callback`. - * @returns {Mixed} Returns the found element, else `undefined`. - * @example - * - * _.find([1, 2, 3, 4], function(num) { - * return num % 2 == 0; - * }); - * // => 2 - * - * var food = [ - * { 'name': 'apple', 'organic': false, 'type': 'fruit' }, - * { 'name': 'banana', 'organic': true, 'type': 'fruit' }, - * { 'name': 'beet', 'organic': false, 'type': 'vegetable' } - * ]; - * - * // using "_.where" callback shorthand - * _.find(food, { 'type': 'vegetable' }); - * // => { 'name': 'beet', 'organic': false, 'type': 'vegetable' } - * - * // using "_.pluck" callback shorthand - * _.find(food, 'organic'); - * // => { 'name': 'banana', 'organic': true, 'type': 'fruit' } - */ - function find(collection, callback, thisArg) { - callback = createCallback(callback, thisArg, 3); - - var index = -1, - length = collection ? collection.length : 0; - - if (typeof length == 'number') { - while (++index < length) { - var value = collection[index]; - if (callback(value, index, collection)) { - return value; - } - } - } else { - var result; - forOwn(collection, function(value, index, collection) { - if (callback(value, index, collection)) { - result = value; - return indicatorObject; - } - }); - return result; - } - } - - /** - * Examines each element in a `collection`, returning the first that - * has the given `properties`. When checking `properties`, this method - * performs a deep comparison between values to determine if they are - * equivalent to each other. - * - * @static - * @memberOf _ - * @category Collections - * @param {Array|Object|String} collection The collection to iterate over. - * @param {Object} properties The object of property values to filter by. - * @returns {Mixed} Returns the found element, else `undefined`. - * @example - * - * var food = [ - * { 'name': 'apple', 'organic': false, 'type': 'fruit' }, - * { 'name': 'banana', 'organic': true, 'type': 'fruit' }, - * { 'name': 'beet', 'organic': false, 'type': 'vegetable' } - * ]; - * - * _.findWhere(food, { 'type': 'vegetable' }); - * // => { 'name': 'beet', 'organic': false, 'type': 'vegetable' } - */ - function findWhere(object, properties) { - return where(object, properties, true); - } - - /** - * Iterates over elements of a collection, executing the callback for each - * element. The callback is bound to `thisArg` and invoked with three arguments; - * (value, index|key, collection). Callbacks may exit iteration early by - * explicitly returning `false`. - * - * @static - * @memberOf _ - * @alias each - * @category Collections - * @param {Array|Object|String} collection The collection to iterate over. - * @param {Function} [callback=identity] The function called per iteration. - * @param {Mixed} [thisArg] The `this` binding of `callback`. - * @returns {Array|Object|String} Returns `collection`. - * @example - * - * _([1, 2, 3]).forEach(function(num) { console.log(num); }).join(','); - * // => logs each number and returns '1,2,3' - * - * _.forEach({ 'one': 1, 'two': 2, 'three': 3 }, function(num) { console.log(num); }); - * // => logs each number value and returns the object (order is not guaranteed) - */ - function forEach(collection, callback, thisArg) { - var index = -1, - length = collection ? collection.length : 0; - - callback = callback && typeof thisArg == 'undefined' ? callback : baseCreateCallback(callback, thisArg, 3); - if (typeof length == 'number') { - while (++index < length) { - if (callback(collection[index], index, collection) === indicatorObject) { - break; - } - } - } else { - forOwn(collection, callback); - } - } - - /** - * This method is like `_.forEach` except that it iterates over elements - * of a `collection` from right to left. - * - * @static - * @memberOf _ - * @alias eachRight - * @category Collections - * @param {Array|Object|String} collection The collection to iterate over. - * @param {Function} [callback=identity] The function called per iteration. - * @param {Mixed} [thisArg] The `this` binding of `callback`. - * @returns {Array|Object|String} Returns `collection`. - * @example - * - * _([1, 2, 3]).forEachRight(function(num) { console.log(num); }).join(','); - * // => logs each number from right to left and returns '3,2,1' - */ - function forEachRight(collection, callback) { - var iterable = collection, - length = collection ? collection.length : 0; - - if (typeof length != 'number') { - var props = keys(collection); - length = props.length; - } - forEach(collection, function(value, index, collection) { - index = props ? props[--length] : --length; - return callback(iterable[index], index, collection) === false && indicatorObject; - }); - } - - /** - * Creates an object composed of keys generated from the results of running - * each element of a collection through the callback. The corresponding value - * of each key is an array of the elements responsible for generating the key. - * The callback is bound to `thisArg` and invoked with three arguments; - * (value, index|key, collection). - * - * If a property name is provided for `callback` the created "_.pluck" style - * callback will return the property value of the given element. - * - * If an object is provided for `callback` the created "_.where" style callback - * will return `true` for elements that have the properties of the given object, - * else `false` - * - * @static - * @memberOf _ - * @category Collections - * @param {Array|Object|String} collection The collection to iterate over. - * @param {Function|Object|String} [callback=identity] The function called - * per iteration. If a property name or object is provided it will be used - * to create a "_.pluck" or "_.where" style callback, respectively. - * @param {Mixed} [thisArg] The `this` binding of `callback`. - * @returns {Object} Returns the composed aggregate object. - * @example - * - * _.groupBy([4.2, 6.1, 6.4], function(num) { return Math.floor(num); }); - * // => { '4': [4.2], '6': [6.1, 6.4] } - * - * _.groupBy([4.2, 6.1, 6.4], function(num) { return this.floor(num); }, Math); - * // => { '4': [4.2], '6': [6.1, 6.4] } - * - * // using "_.pluck" callback shorthand - * _.groupBy(['one', 'two', 'three'], 'length'); - * // => { '3': ['one', 'two'], '5': ['three'] } - */ - var groupBy = createAggregator(function(result, value, key) { - (hasOwnProperty.call(result, key) ? result[key] : result[key] = []).push(value); - }); - - /** - * Invokes the method named by `methodName` on each element in the `collection` - * returning an array of the results of each invoked method. Additional arguments - * will be provided to each invoked method. If `methodName` is a function it - * will be invoked for, and `this` bound to, each element in the `collection`. - * - * @static - * @memberOf _ - * @category Collections - * @param {Array|Object|String} collection The collection to iterate over. - * @param {Function|String} methodName The name of the method to invoke or - * the function invoked per iteration. - * @param {Mixed} [arg1, arg2, ...] Arguments to invoke the method with. - * @returns {Array} Returns a new array of the results of each invoked method. - * @example - * - * _.invoke([[5, 1, 7], [3, 2, 1]], 'sort'); - * // => [[1, 5, 7], [1, 2, 3]] - * - * _.invoke([123, 456], String.prototype.split, ''); - * // => [['1', '2', '3'], ['4', '5', '6']] - */ - function invoke(collection, methodName) { - var args = nativeSlice.call(arguments, 2), - index = -1, - isFunc = typeof methodName == 'function', - length = collection ? collection.length : 0, - result = Array(typeof length == 'number' ? length : 0); - - forEach(collection, function(value) { - result[++index] = (isFunc ? methodName : value[methodName]).apply(value, args); - }); - return result; - } - - /** - * Creates an array of values by running each element in the collection - * through the callback. The callback is bound to `thisArg` and invoked with - * three arguments; (value, index|key, collection). - * - * If a property name is provided for `callback` the created "_.pluck" style - * callback will return the property value of the given element. - * - * If an object is provided for `callback` the created "_.where" style callback - * will return `true` for elements that have the properties of the given object, - * else `false`. - * - * @static - * @memberOf _ - * @alias collect - * @category Collections - * @param {Array|Object|String} collection The collection to iterate over. - * @param {Function|Object|String} [callback=identity] The function called - * per iteration. If a property name or object is provided it will be used - * to create a "_.pluck" or "_.where" style callback, respectively. - * @param {Mixed} [thisArg] The `this` binding of `callback`. - * @returns {Array} Returns a new array of the results of each `callback` execution. - * @example - * - * _.map([1, 2, 3], function(num) { return num * 3; }); - * // => [3, 6, 9] - * - * _.map({ 'one': 1, 'two': 2, 'three': 3 }, function(num) { return num * 3; }); - * // => [3, 6, 9] (order is not guaranteed) - * - * var stooges = [ - * { 'name': 'moe', 'age': 40 }, - * { 'name': 'larry', 'age': 50 } - * ]; - * - * // using "_.pluck" callback shorthand - * _.map(stooges, 'name'); - * // => ['moe', 'larry'] - */ - function map(collection, callback, thisArg) { - var index = -1, - length = collection ? collection.length : 0; - - callback = createCallback(callback, thisArg, 3); - if (typeof length == 'number') { - var result = Array(length); - while (++index < length) { - result[index] = callback(collection[index], index, collection); - } - } else { - result = []; - forOwn(collection, function(value, key, collection) { - result[++index] = callback(value, key, collection); - }); - } - return result; - } - - /** - * Retrieves the maximum value of an array. If a callback is provided it - * will be executed for each value in the array to generate the criterion by - * which the value is ranked. The callback is bound to `thisArg` and invoked - * with three arguments; (value, index, collection). - * - * If a property name is provided for `callback` the created "_.pluck" style - * callback will return the property value of the given element. - * - * If an object is provided for `callback` the created "_.where" style callback - * will return `true` for elements that have the properties of the given object, - * else `false`. - * - * @static - * @memberOf _ - * @category Collections - * @param {Array|Object|String} collection The collection to iterate over. - * @param {Function|Object|String} [callback=identity] The function called - * per iteration. If a property name or object is provided it will be used - * to create a "_.pluck" or "_.where" style callback, respectively. - * @param {Mixed} [thisArg] The `this` binding of `callback`. - * @returns {Mixed} Returns the maximum value. - * @example - * - * _.max([4, 2, 8, 6]); - * // => 8 - * - * var stooges = [ - * { 'name': 'moe', 'age': 40 }, - * { 'name': 'larry', 'age': 50 } - * ]; - * - * _.max(stooges, function(stooge) { return stooge.age; }); - * // => { 'name': 'larry', 'age': 50 }; - * - * // using "_.pluck" callback shorthand - * _.max(stooges, 'age'); - * // => { 'name': 'larry', 'age': 50 }; - */ - function max(collection, callback, thisArg) { - var computed = -Infinity, - result = computed; - - var index = -1, - length = collection ? collection.length : 0; - - if (!callback && typeof length == 'number') { - while (++index < length) { - var value = collection[index]; - if (value > result) { - result = value; - } - } - } else { - callback = createCallback(callback, thisArg, 3); - - forEach(collection, function(value, index, collection) { - var current = callback(value, index, collection); - if (current > computed) { - computed = current; - result = value; - } - }); - } - return result; - } - - /** - * Retrieves the minimum value of an array. If a callback is provided it - * will be executed for each value in the array to generate the criterion by - * which the value is ranked. The callback is bound to `thisArg` and invoked - * with three arguments; (value, index, collection). - * - * If a property name is provided for `callback` the created "_.pluck" style - * callback will return the property value of the given element. - * - * If an object is provided for `callback` the created "_.where" style callback - * will return `true` for elements that have the properties of the given object, - * else `false`. - * - * @static - * @memberOf _ - * @category Collections - * @param {Array|Object|String} collection The collection to iterate over. - * @param {Function|Object|String} [callback=identity] The function called - * per iteration. If a property name or object is provided it will be used - * to create a "_.pluck" or "_.where" style callback, respectively. - * @param {Mixed} [thisArg] The `this` binding of `callback`. - * @returns {Mixed} Returns the minimum value. - * @example - * - * _.min([4, 2, 8, 6]); - * // => 2 - * - * var stooges = [ - * { 'name': 'moe', 'age': 40 }, - * { 'name': 'larry', 'age': 50 } - * ]; - * - * _.min(stooges, function(stooge) { return stooge.age; }); - * // => { 'name': 'moe', 'age': 40 }; - * - * // using "_.pluck" callback shorthand - * _.min(stooges, 'age'); - * // => { 'name': 'moe', 'age': 40 }; - */ - function min(collection, callback, thisArg) { - var computed = Infinity, - result = computed; - - var index = -1, - length = collection ? collection.length : 0; - - if (!callback && typeof length == 'number') { - while (++index < length) { - var value = collection[index]; - if (value < result) { - result = value; - } - } - } else { - callback = createCallback(callback, thisArg, 3); - - forEach(collection, function(value, index, collection) { - var current = callback(value, index, collection); - if (current < computed) { - computed = current; - result = value; - } - }); - } - return result; - } - - /** - * Retrieves the value of a specified property from all elements in the `collection`. - * - * @static - * @memberOf _ - * @type Function - * @category Collections - * @param {Array|Object|String} collection The collection to iterate over. - * @param {String} property The property to pluck. - * @returns {Array} Returns a new array of property values. - * @example - * - * var stooges = [ - * { 'name': 'moe', 'age': 40 }, - * { 'name': 'larry', 'age': 50 } - * ]; - * - * _.pluck(stooges, 'name'); - * // => ['moe', 'larry'] - */ - function pluck(collection, property) { - var index = -1, - length = collection ? collection.length : 0; - - if (typeof length == 'number') { - var result = Array(length); - while (++index < length) { - result[index] = collection[index][property]; - } - } - return result || map(collection, property); - } - - /** - * Reduces a collection to a value which is the accumulated result of running - * each element in the collection through the callback, where each successive - * callback execution consumes the return value of the previous execution. If - * `accumulator` is not provided the first element of the collection will be - * used as the initial `accumulator` value. The callback is bound to `thisArg` - * and invoked with four arguments; (accumulator, value, index|key, collection). - * - * @static - * @memberOf _ - * @alias foldl, inject - * @category Collections - * @param {Array|Object|String} collection The collection to iterate over. - * @param {Function} [callback=identity] The function called per iteration. - * @param {Mixed} [accumulator] Initial value of the accumulator. - * @param {Mixed} [thisArg] The `this` binding of `callback`. - * @returns {Mixed} Returns the accumulated value. - * @example - * - * var sum = _.reduce([1, 2, 3], function(sum, num) { - * return sum + num; - * }); - * // => 6 - * - * var mapped = _.reduce({ 'a': 1, 'b': 2, 'c': 3 }, function(result, num, key) { - * result[key] = num * 3; - * return result; - * }, {}); - * // => { 'a': 3, 'b': 6, 'c': 9 } - */ - function reduce(collection, callback, accumulator, thisArg) { - if (!collection) return accumulator; - var noaccum = arguments.length < 3; - callback = baseCreateCallback(callback, thisArg, 4); - - var index = -1, - length = collection.length; - - if (typeof length == 'number') { - if (noaccum) { - accumulator = collection[++index]; - } - while (++index < length) { - accumulator = callback(accumulator, collection[index], index, collection); - } - } else { - forOwn(collection, function(value, index, collection) { - accumulator = noaccum - ? (noaccum = false, value) - : callback(accumulator, value, index, collection) - }); - } - return accumulator; - } - - /** - * This method is like `_.reduce` except that it iterates over elements - * of a `collection` from right to left. - * - * @static - * @memberOf _ - * @alias foldr - * @category Collections - * @param {Array|Object|String} collection The collection to iterate over. - * @param {Function} [callback=identity] The function called per iteration. - * @param {Mixed} [accumulator] Initial value of the accumulator. - * @param {Mixed} [thisArg] The `this` binding of `callback`. - * @returns {Mixed} Returns the accumulated value. - * @example - * - * var list = [[0, 1], [2, 3], [4, 5]]; - * var flat = _.reduceRight(list, function(a, b) { return a.concat(b); }, []); - * // => [4, 5, 2, 3, 0, 1] - */ - function reduceRight(collection, callback, accumulator, thisArg) { - var noaccum = arguments.length < 3; - callback = baseCreateCallback(callback, thisArg, 4); - forEachRight(collection, function(value, index, collection) { - accumulator = noaccum - ? (noaccum = false, value) - : callback(accumulator, value, index, collection); - }); - return accumulator; - } - - /** - * The opposite of `_.filter` this method returns the elements of a - * collection that the callback does **not** return truthy for. - * - * If a property name is provided for `callback` the created "_.pluck" style - * callback will return the property value of the given element. - * - * If an object is provided for `callback` the created "_.where" style callback - * will return `true` for elements that have the properties of the given object, - * else `false`. - * - * @static - * @memberOf _ - * @category Collections - * @param {Array|Object|String} collection The collection to iterate over. - * @param {Function|Object|String} [callback=identity] The function called - * per iteration. If a property name or object is provided it will be used - * to create a "_.pluck" or "_.where" style callback, respectively. - * @param {Mixed} [thisArg] The `this` binding of `callback`. - * @returns {Array} Returns a new array of elements that failed the callback check. - * @example - * - * var odds = _.reject([1, 2, 3, 4, 5, 6], function(num) { return num % 2 == 0; }); - * // => [1, 3, 5] - * - * var food = [ - * { 'name': 'apple', 'organic': false, 'type': 'fruit' }, - * { 'name': 'carrot', 'organic': true, 'type': 'vegetable' } - * ]; - * - * // using "_.pluck" callback shorthand - * _.reject(food, 'organic'); - * // => [{ 'name': 'apple', 'organic': false, 'type': 'fruit' }] - * - * // using "_.where" callback shorthand - * _.reject(food, { 'type': 'fruit' }); - * // => [{ 'name': 'carrot', 'organic': true, 'type': 'vegetable' }] - */ - function reject(collection, callback, thisArg) { - callback = createCallback(callback, thisArg, 3); - return filter(collection, function(value, index, collection) { - return !callback(value, index, collection); - }); - } - - /** - * Creates an array of shuffled values, using a version of the Fisher-Yates - * shuffle. See http://en.wikipedia.org/wiki/Fisher-Yates_shuffle. - * - * @static - * @memberOf _ - * @category Collections - * @param {Array|Object|String} collection The collection to shuffle. - * @returns {Array} Returns a new shuffled collection. - * @example - * - * _.shuffle([1, 2, 3, 4, 5, 6]); - * // => [4, 1, 6, 3, 5, 2] - */ - function shuffle(collection) { - var index = -1, - length = collection ? collection.length : 0, - result = Array(typeof length == 'number' ? length : 0); - - forEach(collection, function(value) { - var rand = floor(nativeRandom() * (++index + 1)); - result[index] = result[rand]; - result[rand] = value; - }); - return result; - } - - /** - * Gets the size of the `collection` by returning `collection.length` for arrays - * and array-like objects or the number of own enumerable properties for objects. - * - * @static - * @memberOf _ - * @category Collections - * @param {Array|Object|String} collection The collection to inspect. - * @returns {Number} Returns `collection.length` or number of own enumerable properties. - * @example - * - * _.size([1, 2]); - * // => 2 - * - * _.size({ 'one': 1, 'two': 2, 'three': 3 }); - * // => 3 - * - * _.size('curly'); - * // => 5 - */ - function size(collection) { - var length = collection ? collection.length : 0; - return typeof length == 'number' ? length : keys(collection).length; - } - - /** - * Checks if the callback returns a truthy value for **any** element of a - * collection. The function returns as soon as it finds a passing value and - * does not iterate over the entire collection. The callback is bound to - * `thisArg` and invoked with three arguments; (value, index|key, collection). - * - * If a property name is provided for `callback` the created "_.pluck" style - * callback will return the property value of the given element. - * - * If an object is provided for `callback` the created "_.where" style callback - * will return `true` for elements that have the properties of the given object, - * else `false`. - * - * @static - * @memberOf _ - * @alias any - * @category Collections - * @param {Array|Object|String} collection The collection to iterate over. - * @param {Function|Object|String} [callback=identity] The function called - * per iteration. If a property name or object is provided it will be used - * to create a "_.pluck" or "_.where" style callback, respectively. - * @param {Mixed} [thisArg] The `this` binding of `callback`. - * @returns {Boolean} Returns `true` if any element passed the callback check, - * else `false`. - * @example - * - * _.some([null, 0, 'yes', false], Boolean); - * // => true - * - * var food = [ - * { 'name': 'apple', 'organic': false, 'type': 'fruit' }, - * { 'name': 'carrot', 'organic': true, 'type': 'vegetable' } - * ]; - * - * // using "_.pluck" callback shorthand - * _.some(food, 'organic'); - * // => true - * - * // using "_.where" callback shorthand - * _.some(food, { 'type': 'meat' }); - * // => false - */ - function some(collection, callback, thisArg) { - var result; - callback = createCallback(callback, thisArg, 3); - - var index = -1, - length = collection ? collection.length : 0; - - if (typeof length == 'number') { - while (++index < length) { - if ((result = callback(collection[index], index, collection))) { - break; - } - } - } else { - forOwn(collection, function(value, index, collection) { - return (result = callback(value, index, collection)) && indicatorObject; - }); - } - return !!result; - } - - /** - * Creates an array of elements, sorted in ascending order by the results of - * running each element in a collection through the callback. This method - * performs a stable sort, that is, it will preserve the original sort order - * of equal elements. The callback is bound to `thisArg` and invoked with - * three arguments; (value, index|key, collection). - * - * If a property name is provided for `callback` the created "_.pluck" style - * callback will return the property value of the given element. - * - * If an object is provided for `callback` the created "_.where" style callback - * will return `true` for elements that have the properties of the given object, - * else `false`. - * - * @static - * @memberOf _ - * @category Collections - * @param {Array|Object|String} collection The collection to iterate over. - * @param {Function|Object|String} [callback=identity] The function called - * per iteration. If a property name or object is provided it will be used - * to create a "_.pluck" or "_.where" style callback, respectively. - * @param {Mixed} [thisArg] The `this` binding of `callback`. - * @returns {Array} Returns a new array of sorted elements. - * @example - * - * _.sortBy([1, 2, 3], function(num) { return Math.sin(num); }); - * // => [3, 1, 2] - * - * _.sortBy([1, 2, 3], function(num) { return this.sin(num); }, Math); - * // => [3, 1, 2] - * - * // using "_.pluck" callback shorthand - * _.sortBy(['banana', 'strawberry', 'apple'], 'length'); - * // => ['apple', 'banana', 'strawberry'] - */ - function sortBy(collection, callback, thisArg) { - var index = -1, - length = collection ? collection.length : 0, - result = Array(typeof length == 'number' ? length : 0); - - callback = createCallback(callback, thisArg, 3); - forEach(collection, function(value, key, collection) { - result[++index] = { - 'criteria': callback(value, key, collection), - 'index': index, - 'value': value - }; - }); - - length = result.length; - result.sort(compareAscending); - while (length--) { - result[length] = result[length].value; - } - return result; - } - - /** - * Converts the `collection` to an array. - * - * @static - * @memberOf _ - * @category Collections - * @param {Array|Object|String} collection The collection to convert. - * @returns {Array} Returns the new converted array. - * @example - * - * (function() { return _.toArray(arguments).slice(1); })(1, 2, 3, 4); - * // => [2, 3, 4] - */ - function toArray(collection) { - if (isArray(collection)) { - return nativeSlice.call(collection); - } - if (collection && typeof collection.length == 'number') { - return map(collection); - } - return values(collection); - } - - /** - * Performs a deep comparison of each element in a `collection` to the given - * `properties` object, returning an array of all elements that have equivalent - * property values. - * - * @static - * @memberOf _ - * @type Function - * @category Collections - * @param {Array|Object|String} collection The collection to iterate over. - * @param {Object} properties The object of property values to filter by. - * @returns {Array} Returns a new array of elements that have the given `properties`. - * @example - * - * var stooges = [ - * { 'name': 'curly', 'age': 30, 'quotes': ['Oh, a wise guy, eh?', 'Poifect!'] }, - * { 'name': 'moe', 'age': '40', 'quotes': ['Spread out!', 'You knucklehead!'] } - * ]; - * - * _.where(stooges, { 'age': 40 }); - * // => [{ 'name': 'moe', 'age': '40', 'quotes': ['Spread out!', 'You knucklehead!'] }] - * - * _.where(stooges, { 'quotes': ['Poifect!'] }); - * // => [{ 'name': 'curly', 'age': 30, 'quotes': ['Oh, a wise guy, eh?', 'Poifect!'] }] - */ - function where(collection, properties, first) { - return (first && isEmpty(properties)) - ? undefined - : (first ? find : filter)(collection, properties); - } - - /*--------------------------------------------------------------------------*/ - - /** - * Creates an array with all falsey values removed. The values `false`, `null`, - * `0`, `""`, `undefined`, and `NaN` are all falsey. - * - * @static - * @memberOf _ - * @category Arrays - * @param {Array} array The array to compact. - * @returns {Array} Returns a new array of filtered values. - * @example - * - * _.compact([0, 1, false, 2, '', 3]); - * // => [1, 2, 3] - */ - function compact(array) { - var index = -1, - length = array ? array.length : 0, - result = []; - - while (++index < length) { - var value = array[index]; - if (value) { - result.push(value); - } - } - return result; - } - - /** - * Creates an array excluding all values of the provided arrays using strict - * equality for comparisons, i.e. `===`. - * - * @static - * @memberOf _ - * @category Arrays - * @param {Array} array The array to process. - * @param {Array} [array1, array2, ...] The arrays of values to exclude. - * @returns {Array} Returns a new array of filtered values. - * @example - * - * _.difference([1, 2, 3, 4, 5], [5, 2, 10]); - * // => [1, 3, 4] - */ - function difference(array) { - var index = -1, - indexOf = getIndexOf(), - length = array.length, - flattened = baseFlatten(arguments, true, true, 1), - result = []; - - while (++index < length) { - var value = array[index]; - if (indexOf(flattened, value) < 0) { - result.push(value); - } - } - return result; - } - - /** - * Gets the first element of an array. If a number `n` is provided the first - * `n` elements of the array are returned. If a callback is provided elements - * at the beginning of the array are returned as long as the callback returns - * truthy. The callback is bound to `thisArg` and invoked with three arguments; - * (value, index, array). - * - * If a property name is provided for `callback` the created "_.pluck" style - * callback will return the property value of the given element. - * - * If an object is provided for `callback` the created "_.where" style callback - * will return `true` for elements that have the properties of the given object, - * else `false`. - * - * @static - * @memberOf _ - * @alias head, take - * @category Arrays - * @param {Array} array The array to query. - * @param {Function|Object|Number|String} [callback|n] The function called - * per element or the number of elements to return. If a property name or - * object is provided it will be used to create a "_.pluck" or "_.where" - * style callback, respectively. - * @param {Mixed} [thisArg] The `this` binding of `callback`. - * @returns {Mixed} Returns the first element(s) of `array`. - * @example - * - * _.first([1, 2, 3]); - * // => 1 - * - * _.first([1, 2, 3], 2); - * // => [1, 2] - * - * _.first([1, 2, 3], function(num) { - * return num < 3; - * }); - * // => [1, 2] - * - * var food = [ - * { 'name': 'banana', 'organic': true }, - * { 'name': 'beet', 'organic': false }, - * ]; - * - * // using "_.pluck" callback shorthand - * _.first(food, 'organic'); - * // => [{ 'name': 'banana', 'organic': true }] - * - * var food = [ - * { 'name': 'apple', 'type': 'fruit' }, - * { 'name': 'banana', 'type': 'fruit' }, - * { 'name': 'beet', 'type': 'vegetable' } - * ]; - * - * // using "_.where" callback shorthand - * _.first(food, { 'type': 'fruit' }); - * // => [{ 'name': 'apple', 'type': 'fruit' }, { 'name': 'banana', 'type': 'fruit' }] - */ - function first(array, callback, thisArg) { - if (array) { - var n = 0, - length = array.length; - - if (typeof callback != 'number' && callback != null) { - var index = -1; - callback = createCallback(callback, thisArg, 3); - while (++index < length && callback(array[index], index, array)) { - n++; - } - } else { - n = callback; - if (n == null || thisArg) { - return array[0]; - } - } - return nativeSlice.call(array, 0, nativeMin(nativeMax(0, n), length)); - } - } - - /** - * Flattens a nested array (the nesting can be to any depth). If `isShallow` - * is truthy, the array will only be flattened a single level. If a callback - * is provided each element of the array is passed through the callback before - * flattening. The callback is bound to `thisArg` and invoked with three - * arguments; (value, index, array). - * - * If a property name is provided for `callback` the created "_.pluck" style - * callback will return the property value of the given element. - * - * If an object is provided for `callback` the created "_.where" style callback - * will return `true` for elements that have the properties of the given object, - * else `false`. - * - * @static - * @memberOf _ - * @category Arrays - * @param {Array} array The array to flatten. - * @param {Boolean} [isShallow=false] A flag to restrict flattening to a single level. - * @param {Function|Object|String} [callback=identity] The function called - * per iteration. If a property name or object is provided it will be used - * to create a "_.pluck" or "_.where" style callback, respectively. - * @param {Mixed} [thisArg] The `this` binding of `callback`. - * @returns {Array} Returns a new flattened array. - * @example - * - * _.flatten([1, [2], [3, [[4]]]]); - * // => [1, 2, 3, 4]; - * - * _.flatten([1, [2], [3, [[4]]]], true); - * // => [1, 2, 3, [[4]]]; - * - * var stooges = [ - * { 'name': 'curly', 'quotes': ['Oh, a wise guy, eh?', 'Poifect!'] }, - * { 'name': 'moe', 'quotes': ['Spread out!', 'You knucklehead!'] } - * ]; - * - * // using "_.pluck" callback shorthand - * _.flatten(stooges, 'quotes'); - * // => ['Oh, a wise guy, eh?', 'Poifect!', 'Spread out!', 'You knucklehead!'] - */ - function flatten(array, isShallow) { - return baseFlatten(array, isShallow); - } - - /** - * Gets the index at which the first occurrence of `value` is found using - * strict equality for comparisons, i.e. `===`. If the array is already sorted - * providing `true` for `fromIndex` will run a faster binary search. - * - * @static - * @memberOf _ - * @category Arrays - * @param {Array} array The array to search. - * @param {Mixed} value The value to search for. - * @param {Boolean|Number} [fromIndex=0] The index to search from or `true` - * to perform a binary search on a sorted array. - * @returns {Number} Returns the index of the matched value or `-1`. - * @example - * - * _.indexOf([1, 2, 3, 1, 2, 3], 2); - * // => 1 - * - * _.indexOf([1, 2, 3, 1, 2, 3], 2, 3); - * // => 4 - * - * _.indexOf([1, 1, 2, 2, 3, 3], 2, true); - * // => 2 - */ - function indexOf(array, value, fromIndex) { - if (typeof fromIndex == 'number') { - var length = array ? array.length : 0; - fromIndex = (fromIndex < 0 ? nativeMax(0, length + fromIndex) : fromIndex || 0); - } else if (fromIndex) { - var index = sortedIndex(array, value); - return array[index] === value ? index : -1; - } - return array ? baseIndexOf(array, value, fromIndex) : -1; - } - - /** - * Gets all but the last element of an array. If a number `n` is provided - * the last `n` elements are excluded from the result. If a callback is - * provided elements at the end of the array are excluded from the result - * as long as the callback returns truthy. The callback is bound to `thisArg` - * and invoked with three arguments; (value, index, array). - * - * If a property name is provided for `callback` the created "_.pluck" style - * callback will return the property value of the given element. - * - * If an object is provided for `callback` the created "_.where" style callback - * will return `true` for elements that have the properties of the given object, - * else `false`. - * - * @static - * @memberOf _ - * @category Arrays - * @param {Array} array The array to query. - * @param {Function|Object|Number|String} [callback|n=1] The function called - * per element or the number of elements to exclude. If a property name or - * object is provided it will be used to create a "_.pluck" or "_.where" - * style callback, respectively. - * @param {Mixed} [thisArg] The `this` binding of `callback`. - * @returns {Array} Returns a slice of `array`. - * @example - * - * _.initial([1, 2, 3]); - * // => [1, 2] - * - * _.initial([1, 2, 3], 2); - * // => [1] - * - * _.initial([1, 2, 3], function(num) { - * return num > 1; - * }); - * // => [1] - * - * var food = [ - * { 'name': 'beet', 'organic': false }, - * { 'name': 'carrot', 'organic': true } - * ]; - * - * // using "_.pluck" callback shorthand - * _.initial(food, 'organic'); - * // => [{ 'name': 'beet', 'organic': false }] - * - * var food = [ - * { 'name': 'banana', 'type': 'fruit' }, - * { 'name': 'beet', 'type': 'vegetable' }, - * { 'name': 'carrot', 'type': 'vegetable' } - * ]; - * - * // using "_.where" callback shorthand - * _.initial(food, { 'type': 'vegetable' }); - * // => [{ 'name': 'banana', 'type': 'fruit' }] - */ - function initial(array, callback, thisArg) { - if (!array) { - return []; - } - var n = 0, - length = array.length; - - if (typeof callback != 'number' && callback != null) { - var index = length; - callback = createCallback(callback, thisArg, 3); - while (index-- && callback(array[index], index, array)) { - n++; - } - } else { - n = (callback == null || thisArg) ? 1 : callback || n; - } - return nativeSlice.call(array, 0, nativeMin(nativeMax(0, length - n), length)); - } - - /** - * Creates an array of unique values present in all provided arrays using - * strict equality for comparisons, i.e. `===`. - * - * @static - * @memberOf _ - * @category Arrays - * @param {Array} [array1, array2, ...] The arrays to inspect. - * @returns {Array} Returns an array of composite values. - * @example - * - * _.intersection([1, 2, 3], [101, 2, 1, 10], [2, 1]); - * // => [1, 2] - */ - function intersection(array) { - var args = arguments, - argsLength = args.length, - index = -1, - indexOf = getIndexOf(), - length = array ? array.length : 0, - result = []; - - outer: - while (++index < length) { - var value = array[index]; - if (indexOf(result, value) < 0) { - var argsIndex = argsLength; - while (--argsIndex) { - if (indexOf(args[argsIndex], value) < 0) { - continue outer; - } - } - result.push(value); - } - } - return result; - } - - /** - * Gets the last element of an array. If a number `n` is provided the last - * `n` elements of the array are returned. If a callback is provided elements - * at the end of the array are returned as long as the callback returns truthy. - * The callback is bound to `thisArg` and invoked with three arguments; - * (value, index, array). - * - * - * If a property name is provided for `callback` the created "_.pluck" style - * callback will return the property value of the given element. - * - * If an object is provided for `callback` the created "_.where" style callback - * will return `true` for elements that have the properties of the given object, - * else `false`. - * - * @static - * @memberOf _ - * @category Arrays - * @param {Array} array The array to query. - * @param {Function|Object|Number|String} [callback|n] The function called - * per element or the number of elements to return. If a property name or - * object is provided it will be used to create a "_.pluck" or "_.where" - * style callback, respectively. - * @param {Mixed} [thisArg] The `this` binding of `callback`. - * @returns {Mixed} Returns the last element(s) of `array`. - * @example - * - * _.last([1, 2, 3]); - * // => 3 - * - * _.last([1, 2, 3], 2); - * // => [2, 3] - * - * _.last([1, 2, 3], function(num) { - * return num > 1; - * }); - * // => [2, 3] - * - * var food = [ - * { 'name': 'beet', 'organic': false }, - * { 'name': 'carrot', 'organic': true } - * ]; - * - * // using "_.pluck" callback shorthand - * _.last(food, 'organic'); - * // => [{ 'name': 'carrot', 'organic': true }] - * - * var food = [ - * { 'name': 'banana', 'type': 'fruit' }, - * { 'name': 'beet', 'type': 'vegetable' }, - * { 'name': 'carrot', 'type': 'vegetable' } - * ]; - * - * // using "_.where" callback shorthand - * _.last(food, { 'type': 'vegetable' }); - * // => [{ 'name': 'beet', 'type': 'vegetable' }, { 'name': 'carrot', 'type': 'vegetable' }] - */ - function last(array, callback, thisArg) { - if (array) { - var n = 0, - length = array.length; - - if (typeof callback != 'number' && callback != null) { - var index = length; - callback = createCallback(callback, thisArg, 3); - while (index-- && callback(array[index], index, array)) { - n++; - } - } else { - n = callback; - if (n == null || thisArg) { - return array[length - 1]; - } - } - return nativeSlice.call(array, nativeMax(0, length - n)); - } - } - - /** - * Gets the index at which the last occurrence of `value` is found using strict - * equality for comparisons, i.e. `===`. If `fromIndex` is negative, it is used - * as the offset from the end of the collection. - * - * @static - * @memberOf _ - * @category Arrays - * @param {Array} array The array to search. - * @param {Mixed} value The value to search for. - * @param {Number} [fromIndex=array.length-1] The index to search from. - * @returns {Number} Returns the index of the matched value or `-1`. - * @example - * - * _.lastIndexOf([1, 2, 3, 1, 2, 3], 2); - * // => 4 - * - * _.lastIndexOf([1, 2, 3, 1, 2, 3], 2, 3); - * // => 1 - */ - function lastIndexOf(array, value, fromIndex) { - var index = array ? array.length : 0; - if (typeof fromIndex == 'number') { - index = (fromIndex < 0 ? nativeMax(0, index + fromIndex) : nativeMin(fromIndex, index - 1)) + 1; - } - while (index--) { - if (array[index] === value) { - return index; - } - } - return -1; - } - - /** - * Creates an array of numbers (positive and/or negative) progressing from - * `start` up to but not including `end`. If `start` is less than `stop` a - * zero-length range is created unless a negative `step` is specified. - * - * @static - * @memberOf _ - * @category Arrays - * @param {Number} [start=0] The start of the range. - * @param {Number} end The end of the range. - * @param {Number} [step=1] The value to increment or decrement by. - * @returns {Array} Returns a new range array. - * @example - * - * _.range(10); - * // => [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] - * - * _.range(1, 11); - * // => [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] - * - * _.range(0, 30, 5); - * // => [0, 5, 10, 15, 20, 25] - * - * _.range(0, -10, -1); - * // => [0, -1, -2, -3, -4, -5, -6, -7, -8, -9] - * - * _.range(1, 4, 0); - * // => [1, 1, 1] - * - * _.range(0); - * // => [] - */ - function range(start, end, step) { - start = +start || 0; - step = (+step || 1); - - if (end == null) { - end = start; - start = 0; - } - // use `Array(length)` so engines, like Chakra and V8, avoid slower modes - // http://youtu.be/XAqIpGU8ZZk#t=17m25s - var index = -1, - length = nativeMax(0, ceil((end - start) / step)), - result = Array(length); - - while (++index < length) { - result[index] = start; - start += step; - } - return result; - } - - /** - * The opposite of `_.initial` this method gets all but the first value of - * an array. If a number `n` is provided the first `n` values are excluded - * from the result. If a callback function is provided elements at the beginning - * of the array are excluded from the result as long as the callback returns - * truthy. The callback is bound to `thisArg` and invoked with three - * arguments; (value, index, array). - * - * If a property name is provided for `callback` the created "_.pluck" style - * callback will return the property value of the given element. - * - * If an object is provided for `callback` the created "_.where" style callback - * will return `true` for elements that have the properties of the given object, - * else `false`. - * - * @static - * @memberOf _ - * @alias drop, tail - * @category Arrays - * @param {Array} array The array to query. - * @param {Function|Object|Number|String} [callback|n=1] The function called - * per element or the number of elements to exclude. If a property name or - * object is provided it will be used to create a "_.pluck" or "_.where" - * style callback, respectively. - * @param {Mixed} [thisArg] The `this` binding of `callback`. - * @returns {Array} Returns a slice of `array`. - * @example - * - * _.rest([1, 2, 3]); - * // => [2, 3] - * - * _.rest([1, 2, 3], 2); - * // => [3] - * - * _.rest([1, 2, 3], function(num) { - * return num < 3; - * }); - * // => [3] - * - * var food = [ - * { 'name': 'banana', 'organic': true }, - * { 'name': 'beet', 'organic': false }, - * ]; - * - * // using "_.pluck" callback shorthand - * _.rest(food, 'organic'); - * // => [{ 'name': 'beet', 'organic': false }] - * - * var food = [ - * { 'name': 'apple', 'type': 'fruit' }, - * { 'name': 'banana', 'type': 'fruit' }, - * { 'name': 'beet', 'type': 'vegetable' } - * ]; - * - * // using "_.where" callback shorthand - * _.rest(food, { 'type': 'fruit' }); - * // => [{ 'name': 'beet', 'type': 'vegetable' }] - */ - function rest(array, callback, thisArg) { - if (typeof callback != 'number' && callback != null) { - var n = 0, - index = -1, - length = array ? array.length : 0; - - callback = createCallback(callback, thisArg, 3); - while (++index < length && callback(array[index], index, array)) { - n++; - } - } else { - n = (callback == null || thisArg) ? 1 : nativeMax(0, callback); - } - return nativeSlice.call(array, n); - } - - /** - * Uses a binary search to determine the smallest index at which a value - * should be inserted into a given sorted array in order to maintain the sort - * order of the array. If a callback is provided it will be executed for - * `value` and each element of `array` to compute their sort ranking. The - * callback is bound to `thisArg` and invoked with one argument; (value). - * - * If a property name is provided for `callback` the created "_.pluck" style - * callback will return the property value of the given element. - * - * If an object is provided for `callback` the created "_.where" style callback - * will return `true` for elements that have the properties of the given object, - * else `false`. - * - * @static - * @memberOf _ - * @category Arrays - * @param {Array} array The array to inspect. - * @param {Mixed} value The value to evaluate. - * @param {Function|Object|String} [callback=identity] The function called - * per iteration. If a property name or object is provided it will be used - * to create a "_.pluck" or "_.where" style callback, respectively. - * @param {Mixed} [thisArg] The `this` binding of `callback`. - * @returns {Number} Returns the index at which `value` should be inserted - * into `array`. - * @example - * - * _.sortedIndex([20, 30, 50], 40); - * // => 2 - * - * // using "_.pluck" callback shorthand - * _.sortedIndex([{ 'x': 20 }, { 'x': 30 }, { 'x': 50 }], { 'x': 40 }, 'x'); - * // => 2 - * - * var dict = { - * 'wordToNumber': { 'twenty': 20, 'thirty': 30, 'fourty': 40, 'fifty': 50 } - * }; - * - * _.sortedIndex(['twenty', 'thirty', 'fifty'], 'fourty', function(word) { - * return dict.wordToNumber[word]; - * }); - * // => 2 - * - * _.sortedIndex(['twenty', 'thirty', 'fifty'], 'fourty', function(word) { - * return this.wordToNumber[word]; - * }, dict); - * // => 2 - */ - function sortedIndex(array, value, callback, thisArg) { - var low = 0, - high = array ? array.length : low; - - // explicitly reference `identity` for better inlining in Firefox - callback = callback ? createCallback(callback, thisArg, 1) : identity; - value = callback(value); - - while (low < high) { - var mid = (low + high) >>> 1; - (callback(array[mid]) < value) - ? low = mid + 1 - : high = mid; - } - return low; - } - - /** - * Creates an array of unique values, in order, of the provided arrays using - * strict equality for comparisons, i.e. `===`. - * - * @static - * @memberOf _ - * @category Arrays - * @param {Array} [array1, array2, ...] The arrays to inspect. - * @returns {Array} Returns an array of composite values. - * @example - * - * _.union([1, 2, 3], [101, 2, 1, 10], [2, 1]); - * // => [1, 2, 3, 101, 10] - */ - function union(array) { - return baseUniq(baseFlatten(arguments, true, true)); - } - - /** - * Creates a duplicate-value-free version of an array using strict equality - * for comparisons, i.e. `===`. If the array is sorted, providing - * `true` for `isSorted` will use a faster algorithm. If a callback is provided - * each element of `array` is passed through the callback before uniqueness - * is computed. The callback is bound to `thisArg` and invoked with three - * arguments; (value, index, array). - * - * If a property name is provided for `callback` the created "_.pluck" style - * callback will return the property value of the given element. - * - * If an object is provided for `callback` the created "_.where" style callback - * will return `true` for elements that have the properties of the given object, - * else `false`. - * - * @static - * @memberOf _ - * @alias unique - * @category Arrays - * @param {Array} array The array to process. - * @param {Boolean} [isSorted=false] A flag to indicate that `array` is sorted. - * @param {Function|Object|String} [callback=identity] The function called - * per iteration. If a property name or object is provided it will be used - * to create a "_.pluck" or "_.where" style callback, respectively. - * @param {Mixed} [thisArg] The `this` binding of `callback`. - * @returns {Array} Returns a duplicate-value-free array. - * @example - * - * _.uniq([1, 2, 1, 3, 1]); - * // => [1, 2, 3] - * - * _.uniq([1, 1, 2, 2, 3], true); - * // => [1, 2, 3] - * - * _.uniq(['A', 'b', 'C', 'a', 'B', 'c'], function(letter) { return letter.toLowerCase(); }); - * // => ['A', 'b', 'C'] - * - * _.uniq([1, 2.5, 3, 1.5, 2, 3.5], function(num) { return this.floor(num); }, Math); - * // => [1, 2.5, 3] - * - * // using "_.pluck" callback shorthand - * _.uniq([{ 'x': 1 }, { 'x': 2 }, { 'x': 1 }], 'x'); - * // => [{ 'x': 1 }, { 'x': 2 }] - */ - function uniq(array, isSorted, callback, thisArg) { - // juggle arguments - if (typeof isSorted != 'boolean' && isSorted != null) { - thisArg = callback; - callback = !(thisArg && thisArg[isSorted] === array) ? isSorted : undefined; - isSorted = false; - } - if (callback != null) { - callback = createCallback(callback, thisArg, 3); - } - return baseUniq(array, isSorted, callback); - } - - /** - * Creates an array excluding all provided values using strict equality for - * comparisons, i.e. `===`. - * - * @static - * @memberOf _ - * @category Arrays - * @param {Array} array The array to filter. - * @param {Mixed} [value1, value2, ...] The values to exclude. - * @returns {Array} Returns a new array of filtered values. - * @example - * - * _.without([1, 2, 1, 0, 3, 1, 4], 0, 1); - * // => [2, 3, 4] - */ - function without(array) { - return difference(array, nativeSlice.call(arguments, 1)); - } - - /** - * Creates an array of grouped elements, the first of which contains the first - * elements of the given arrays, the second of which contains the second - * elements of the given arrays, and so on. - * - * @static - * @memberOf _ - * @alias unzip - * @category Arrays - * @param {Array} [array1, array2, ...] Arrays to process. - * @returns {Array} Returns a new array of grouped elements. - * @example - * - * _.zip(['moe', 'larry'], [30, 40], [true, false]); - * // => [['moe', 30, true], ['larry', 40, false]] - */ - function zip() { - var index = -1, - length = max(pluck(arguments, 'length')), - result = Array(length < 0 ? 0 : length); - - while (++index < length) { - result[index] = pluck(arguments, index); - } - return result; - } - - /** - * Creates an object composed from arrays of `keys` and `values`. Provide - * either a single two dimensional array, i.e. `[[key1, value1], [key2, value2]]` - * or two arrays, one of `keys` and one of corresponding `values`. - * - * @static - * @memberOf _ - * @alias object - * @category Arrays - * @param {Array} keys The array of keys. - * @param {Array} [values=[]] The array of values. - * @returns {Object} Returns an object composed of the given keys and - * corresponding values. - * @example - * - * _.zipObject(['moe', 'larry'], [30, 40]); - * // => { 'moe': 30, 'larry': 40 } - */ - function zipObject(keys, values) { - var index = -1, - length = keys ? keys.length : 0, - result = {}; - - while (++index < length) { - var key = keys[index]; - if (values) { - result[key] = values[index]; - } else if (key) { - result[key[0]] = key[1]; - } - } - return result; - } - - /*--------------------------------------------------------------------------*/ - - /** - * Creates a function this is restricted to executing `func` with the `this` - * binding and arguments of the created function, only after it is called `n` times. - * - * @static - * @memberOf _ - * @category Functions - * @param {Number} n The number of times the function must be called before - * `func` is executed. - * @param {Function} func The function to restrict. - * @returns {Function} Returns the new restricted function. - * @example - * - * var renderNotes = _.after(notes.length, render); - * _.forEach(notes, function(note) { - * note.asyncSave({ 'success': renderNotes }); - * }); - * // `renderNotes` is run once, after all notes have saved - */ - function after(n, func) { - if (!isFunction(func)) { - throw new TypeError; - } - return function() { - if (--n < 1) { - return func.apply(this, arguments); - } - }; - } - - /** - * Creates a function that, when called, invokes `func` with the `this` - * binding of `thisArg` and prepends any additional `bind` arguments to those - * provided to the bound function. - * - * @static - * @memberOf _ - * @category Functions - * @param {Function} func The function to bind. - * @param {Mixed} [thisArg] The `this` binding of `func`. - * @param {Mixed} [arg1, arg2, ...] Arguments to be partially applied. - * @returns {Function} Returns the new bound function. - * @example - * - * var func = function(greeting) { - * return greeting + ' ' + this.name; - * }; - * - * func = _.bind(func, { 'name': 'moe' }, 'hi'); - * func(); - * // => 'hi moe' - */ - function bind(func, thisArg) { - return createBound(func, 17, nativeSlice.call(arguments, 2), null, thisArg); - } - - /** - * Binds methods of an object to the object itself, overwriting the existing - * method. Method names may be specified as individual arguments or as arrays - * of method names. If no method names are provided all the function properties - * of `object` will be bound. - * - * @static - * @memberOf _ - * @category Functions - * @param {Object} object The object to bind and assign the bound methods to. - * @param {String} [methodName1, methodName2, ...] The object method names to - * bind, specified as individual method names or arrays of method names. - * @returns {Object} Returns `object`. - * @example - * - * var view = { - * 'label': 'docs', - * 'onClick': function() { console.log('clicked ' + this.label); } - * }; - * - * _.bindAll(view); - * jQuery('#docs').on('click', view.onClick); - * // => logs 'clicked docs', when the button is clicked - */ - function bindAll(object) { - var funcs = arguments.length > 1 ? baseFlatten(arguments, true, false, 1) : functions(object), - index = -1, - length = funcs.length; - - while (++index < length) { - var key = funcs[index]; - object[key] = bind(object[key], object); - } - return object; - } - - /** - * Creates a function that is the composition of the provided functions, - * where each function consumes the return value of the function that follows. - * For example, composing the functions `f()`, `g()`, and `h()` produces `f(g(h()))`. - * Each function is executed with the `this` binding of the composed function. - * - * @static - * @memberOf _ - * @category Functions - * @param {Function} [func1, func2, ...] Functions to compose. - * @returns {Function} Returns the new composed function. - * @example - * - * var realNameMap = { - * 'curly': 'jerome' - * }; - * - * var format = function(name) { - * name = realNameMap[name.toLowerCase()] || name; - * return name.charAt(0).toUpperCase() + name.slice(1).toLowerCase(); - * }; - * - * var greet = function(formatted) { - * return 'Hiya ' + formatted + '!'; - * }; - * - * var welcome = _.compose(greet, format); - * welcome('curly'); - * // => 'Hiya Jerome!' - */ - function compose() { - var funcs = arguments, - length = funcs.length || 1; - - while (length--) { - if (!isFunction(funcs[length])) { - throw new TypeError; - } - } - return function() { - var args = arguments, - length = funcs.length; - - while (length--) { - args = [funcs[length].apply(this, args)]; - } - return args[0]; - }; - } - - /** - * Produces a callback bound to an optional `thisArg`. If `func` is a property - * name the created callback will return the property value for a given element. - * If `func` is an object the created callback will return `true` for elements - * that contain the equivalent object properties, otherwise it will return `false`. - * - * @static - * @memberOf _ - * @category Functions - * @param {Mixed} [func=identity] The value to convert to a callback. - * @param {Mixed} [thisArg] The `this` binding of the created callback. - * @param {Number} [argCount] The number of arguments the callback accepts. - * @returns {Function} Returns a callback function. - * @example - * - * var stooges = [ - * { 'name': 'moe', 'age': 40 }, - * { 'name': 'larry', 'age': 50 } - * ]; - * - * // wrap to create custom callback shorthands - * _.createCallback = _.wrap(_.createCallback, function(func, callback, thisArg) { - * var match = /^(.+?)__([gl]t)(.+)$/.exec(callback); - * return !match ? func(callback, thisArg) : function(object) { - * return match[2] == 'gt' ? object[match[1]] > match[3] : object[match[1]] < match[3]; - * }; - * }); - * - * _.filter(stooges, 'age__gt45'); - * // => [{ 'name': 'larry', 'age': 50 }] - */ - function createCallback(func, thisArg, argCount) { - var type = typeof func; - if (func == null || type == 'function') { - return baseCreateCallback(func, thisArg, argCount); - } - // handle "_.pluck" style callback shorthands - if (type != 'object') { - return function(object) { - return object[func]; - }; - } - var props = keys(func); - return function(object) { - var length = props.length, - result = false; - - while (length--) { - if (!(result = object[props[length]] === func[props[length]])) { - break; - } - } - return result; - }; - } - - /** - * Creates a function that will delay the execution of `func` until after - * `wait` milliseconds have elapsed since the last time it was invoked. - * Provide an options object to indicate that `func` should be invoked on - * the leading and/or trailing edge of the `wait` timeout. Subsequent calls - * to the debounced function will return the result of the last `func` call. - * - * Note: If `leading` and `trailing` options are `true` `func` will be called - * on the trailing edge of the timeout only if the the debounced function is - * invoked more than once during the `wait` timeout. - * - * @static - * @memberOf _ - * @category Functions - * @param {Function} func The function to debounce. - * @param {Number} wait The number of milliseconds to delay. - * @param {Object} options The options object. - * [leading=false] A boolean to specify execution on the leading edge of the timeout. - * [maxWait] The maximum time `func` is allowed to be delayed before it's called. - * [trailing=true] A boolean to specify execution on the trailing edge of the timeout. - * @returns {Function} Returns the new debounced function. - * @example - * - * // avoid costly calculations while the window size is in flux - * var lazyLayout = _.debounce(calculateLayout, 150); - * jQuery(window).on('resize', lazyLayout); - * - * // execute `sendMail` when the click event is fired, debouncing subsequent calls - * jQuery('#postbox').on('click', _.debounce(sendMail, 300, { - * 'leading': true, - * 'trailing': false - * }); - * - * // ensure `batchLog` is executed once after 1 second of debounced calls - * var source = new EventSource('/stream'); - * source.addEventListener('message', _.debounce(batchLog, 250, { - * 'maxWait': 1000 - * }, false); - */ - function debounce(func, wait, options) { - var args, - result, - thisArg, - callCount = 0, - lastCalled = 0, - maxWait = false, - maxTimeoutId = null, - timeoutId = null, - trailing = true; - - if (!isFunction(func)) { - throw new TypeError; - } - wait = nativeMax(0, wait || 0); - if (options === true) { - var leading = true; - trailing = false; - } else if (isObject(options)) { - leading = options.leading; - maxWait = 'maxWait' in options && nativeMax(wait, options.maxWait || 0); - trailing = 'trailing' in options ? options.trailing : trailing; - } - var clear = function() { - clearTimeout(maxTimeoutId); - clearTimeout(timeoutId); - callCount = 0; - maxTimeoutId = timeoutId = null; - }; - - var delayed = function() { - var isCalled = trailing && (!leading || callCount > 1); - clear(); - if (isCalled) { - if (maxWait !== false) { - lastCalled = +new Date; - } - result = func.apply(thisArg, args); - } - }; - - var maxDelayed = function() { - clear(); - if (trailing || (maxWait !== wait)) { - lastCalled = +new Date; - result = func.apply(thisArg, args); - } - }; - - return function() { - args = arguments; - thisArg = this; - callCount++; - - // avoid issues with Titanium and `undefined` timeout ids - // https://github.com/appcelerator/titanium_mobile/blob/3_1_0_GA/android/titanium/src/java/ti/modules/titanium/TitaniumModule.java#L185-L192 - clearTimeout(timeoutId); - - if (maxWait === false) { - if (leading && callCount < 2) { - result = func.apply(thisArg, args); - } - } else { - var stamp = +new Date; - if (!maxTimeoutId && !leading) { - lastCalled = stamp; - } - var remaining = maxWait - (stamp - lastCalled); - if (remaining <= 0) { - clearTimeout(maxTimeoutId); - maxTimeoutId = null; - lastCalled = stamp; - result = func.apply(thisArg, args); - } - else if (!maxTimeoutId) { - maxTimeoutId = setTimeout(maxDelayed, remaining); - } - } - if (wait !== maxWait) { - timeoutId = setTimeout(delayed, wait); - } - return result; - }; - } - - /** - * Defers executing the `func` function until the current call stack has cleared. - * Additional arguments will be provided to `func` when it is invoked. - * - * @static - * @memberOf _ - * @category Functions - * @param {Function} func The function to defer. - * @param {Mixed} [arg1, arg2, ...] Arguments to invoke the function with. - * @returns {Number} Returns the timer id. - * @example - * - * _.defer(function() { console.log('deferred'); }); - * // returns from the function before 'deferred' is logged - */ - function defer(func) { - if (!isFunction(func)) { - throw new TypeError; - } - var args = nativeSlice.call(arguments, 1); - return setTimeout(function() { func.apply(undefined, args); }, 1); - } - - /** - * Executes the `func` function after `wait` milliseconds. Additional arguments - * will be provided to `func` when it is invoked. - * - * @static - * @memberOf _ - * @category Functions - * @param {Function} func The function to delay. - * @param {Number} wait The number of milliseconds to delay execution. - * @param {Mixed} [arg1, arg2, ...] Arguments to invoke the function with. - * @returns {Number} Returns the timer id. - * @example - * - * var log = _.bind(console.log, console); - * _.delay(log, 1000, 'logged later'); - * // => 'logged later' (Appears after one second.) - */ - function delay(func, wait) { - if (!isFunction(func)) { - throw new TypeError; - } - var args = nativeSlice.call(arguments, 2); - return setTimeout(function() { func.apply(undefined, args); }, wait); - } - - /** - * Creates a function that memoizes the result of `func`. If `resolver` is - * provided it will be used to determine the cache key for storing the result - * based on the arguments provided to the memoized function. By default, the - * first argument provided to the memoized function is used as the cache key. - * The `func` is executed with the `this` binding of the memoized function. - * The result cache is exposed as the `cache` property on the memoized function. - * - * @static - * @memberOf _ - * @category Functions - * @param {Function} func The function to have its output memoized. - * @param {Function} [resolver] A function used to resolve the cache key. - * @returns {Function} Returns the new memoizing function. - * @example - * - * var fibonacci = _.memoize(function(n) { - * return n < 2 ? n : fibonacci(n - 1) + fibonacci(n - 2); - * }); - */ - function memoize(func, resolver) { - var cache = {}; - return function() { - var key = keyPrefix + (resolver ? resolver.apply(this, arguments) : arguments[0]); - return hasOwnProperty.call(cache, key) - ? cache[key] - : (cache[key] = func.apply(this, arguments)); - }; - } - - /** - * Creates a function that is restricted to execute `func` once. Repeat calls to - * the function will return the value of the first call. The `func` is executed - * with the `this` binding of the created function. - * - * @static - * @memberOf _ - * @category Functions - * @param {Function} func The function to restrict. - * @returns {Function} Returns the new restricted function. - * @example - * - * var initialize = _.once(createApplication); - * initialize(); - * initialize(); - * // `initialize` executes `createApplication` once - */ - function once(func) { - var ran, - result; - - if (!isFunction(func)) { - throw new TypeError; - } - return function() { - if (ran) { - return result; - } - ran = true; - result = func.apply(this, arguments); - - // clear the `func` variable so the function may be garbage collected - func = null; - return result; - }; - } - - /** - * Creates a function that, when called, invokes `func` with any additional - * `partial` arguments prepended to those provided to the new function. This - * method is similar to `_.bind` except it does **not** alter the `this` binding. - * - * @static - * @memberOf _ - * @category Functions - * @param {Function} func The function to partially apply arguments to. - * @param {Mixed} [arg1, arg2, ...] Arguments to be partially applied. - * @returns {Function} Returns the new partially applied function. - * @example - * - * var greet = function(greeting, name) { return greeting + ' ' + name; }; - * var hi = _.partial(greet, 'hi'); - * hi('moe'); - * // => 'hi moe' - */ - function partial(func) { - return createBound(func, 16, nativeSlice.call(arguments, 1)); - } - - /** - * Creates a function that, when executed, will only call the `func` function - * at most once per every `wait` milliseconds. Provide an options object to - * indicate that `func` should be invoked on the leading and/or trailing edge - * of the `wait` timeout. Subsequent calls to the throttled function will - * return the result of the last `func` call. - * - * Note: If `leading` and `trailing` options are `true` `func` will be called - * on the trailing edge of the timeout only if the the throttled function is - * invoked more than once during the `wait` timeout. - * - * @static - * @memberOf _ - * @category Functions - * @param {Function} func The function to throttle. - * @param {Number} wait The number of milliseconds to throttle executions to. - * @param {Object} options The options object. - * [leading=true] A boolean to specify execution on the leading edge of the timeout. - * [trailing=true] A boolean to specify execution on the trailing edge of the timeout. - * @returns {Function} Returns the new throttled function. - * @example - * - * // avoid excessively updating the position while scrolling - * var throttled = _.throttle(updatePosition, 100); - * jQuery(window).on('scroll', throttled); - * - * // execute `renewToken` when the click event is fired, but not more than once every 5 minutes - * jQuery('.interactive').on('click', _.throttle(renewToken, 300000, { - * 'trailing': false - * })); - */ - function throttle(func, wait, options) { - var leading = true, - trailing = true; - - if (options === false) { - leading = false; - } else if (isObject(options)) { - leading = 'leading' in options ? options.leading : leading; - trailing = 'trailing' in options ? options.trailing : trailing; - } - options = {}; - options.leading = leading; - options.maxWait = wait; - options.trailing = trailing; - - return debounce(func, wait, options); - } - - /** - * Creates a function that provides `value` to the wrapper function as its - * first argument. Additional arguments provided to the function are appended - * to those provided to the wrapper function. The wrapper is executed with - * the `this` binding of the created function. - * - * @static - * @memberOf _ - * @category Functions - * @param {Mixed} value The value to wrap. - * @param {Function} wrapper The wrapper function. - * @returns {Function} Returns the new function. - * @example - * - * var hello = function(name) { return 'hello ' + name; }; - * hello = _.wrap(hello, function(func) { - * return 'before, ' + func('moe') + ', after'; - * }); - * hello(); - * // => 'before, hello moe, after' - */ - function wrap(value, wrapper) { - if (!isFunction(wrapper)) { - throw new TypeError; - } - return function() { - var args = [value]; - push.apply(args, arguments); - return wrapper.apply(this, args); - }; - } - - /*--------------------------------------------------------------------------*/ - - /** - * Converts the characters `&`, `<`, `>`, `"`, and `'` in `string` to their - * corresponding HTML entities. - * - * @static - * @memberOf _ - * @category Utilities - * @param {String} string The string to escape. - * @returns {String} Returns the escaped string. - * @example - * - * _.escape('Moe, Larry & Curly'); - * // => 'Moe, Larry & Curly' - */ - function escape(string) { - return string == null ? '' : String(string).replace(reUnescapedHtml, escapeHtmlChar); - } - - /** - * This method returns the first argument provided to it. - * - * @static - * @memberOf _ - * @category Utilities - * @param {Mixed} value Any value. - * @returns {Mixed} Returns `value`. - * @example - * - * var moe = { 'name': 'moe' }; - * moe === _.identity(moe); - * // => true - */ - function identity(value) { - return value; - } - - /** - * Adds function properties of a source object to the `lodash` function and - * chainable wrapper. - * - * @static - * @memberOf _ - * @category Utilities - * @param {Object} object The object of function properties to add to `lodash`. - * @param {Object} object The object of function properties to add to `lodash`. - * @example - * - * _.mixin({ - * 'capitalize': function(string) { - * return string.charAt(0).toUpperCase() + string.slice(1).toLowerCase(); - * } - * }); - * - * _.capitalize('moe'); - * // => 'Moe' - * - * _('moe').capitalize(); - * // => 'Moe' - */ - function mixin(object) { - forEach(functions(object), function(methodName) { - var func = lodash[methodName] = object[methodName]; - - lodash.prototype[methodName] = function() { - var args = [this.__wrapped__]; - push.apply(args, arguments); - - var result = func.apply(lodash, args); - if (this.__chain__) { - result = new lodashWrapper(result); - result.__chain__ = true; - } - return result; - }; - }); - } - - /** - * Reverts the '_' variable to its previous value and returns a reference to - * the `lodash` function. - * - * @static - * @memberOf _ - * @category Utilities - * @returns {Function} Returns the `lodash` function. - * @example - * - * var lodash = _.noConflict(); - */ - function noConflict() { - window._ = oldDash; - return this; - } - - /** - * Produces a random number between `min` and `max` (inclusive). If only one - * argument is provided a number between `0` and the given number will be - * returned. - * - * @static - * @memberOf _ - * @category Utilities - * @param {Number} [min=0] The minimum possible value. - * @param {Number} [max=1] The maximum possible value. - * @returns {Number} Returns a random number. - * @example - * - * _.random(0, 5); - * // => a number between 0 and 5 - * - * _.random(5); - * // => also a number between 0 and 5 - */ - function random(min, max) { - if (min == null && max == null) { - max = 1; - } - min = +min || 0; - if (max == null) { - max = min; - min = 0; - } else { - max = +max || 0; - } - var rand = nativeRandom(); - return (min % 1 || max % 1) - ? min + nativeMin(rand * (max - min + parseFloat('1e-' + ((rand +'').length - 1))), max) - : min + floor(rand * (max - min + 1)); - } - - /** - * Resolves the value of `property` on `object`. If `property` is a function - * it will be invoked with the `this` binding of `object` and its result returned, - * else the property value is returned. If `object` is falsey then `undefined` - * is returned. - * - * @static - * @memberOf _ - * @category Utilities - * @param {Object} object The object to inspect. - * @param {String} property The property to get the value of. - * @returns {Mixed} Returns the resolved value. - * @example - * - * var object = { - * 'cheese': 'crumpets', - * 'stuff': function() { - * return 'nonsense'; - * } - * }; - * - * _.result(object, 'cheese'); - * // => 'crumpets' - * - * _.result(object, 'stuff'); - * // => 'nonsense' - */ - function result(object, property) { - var value = object ? object[property] : undefined; - return isFunction(value) ? object[property]() : value; - } - - /** - * A micro-templating method that handles arbitrary delimiters, preserves - * whitespace, and correctly escapes quotes within interpolated code. - * - * Note: In the development build, `_.template` utilizes sourceURLs for easier - * debugging. See http://www.html5rocks.com/en/tutorials/developertools/sourcemaps/#toc-sourceurl - * - * For more information on precompiling templates see: - * http://lodash.com/#custom-builds - * - * For more information on Chrome extension sandboxes see: - * http://developer.chrome.com/stable/extensions/sandboxingEval.html - * - * @static - * @memberOf _ - * @category Utilities - * @param {String} text The template text. - * @param {Object} data The data object used to populate the text. - * @param {Object} options The options object. - * escape - The "escape" delimiter regexp. - * evaluate - The "evaluate" delimiter regexp. - * imports - An object of properties to import into the compiled template as local variables. - * interpolate - The "interpolate" delimiter regexp. - * sourceURL - The sourceURL of the template's compiled source. - * variable - The data object variable name. - * @returns {Function|String} Returns a compiled function when no `data` object - * is given, else it returns the interpolated text. - * @example - * - * // using a compiled template - * var compiled = _.template('hello <%= name %>'); - * compiled({ 'name': 'moe' }); - * // => 'hello moe' - * - * // using the "escape" delimiter to escape HTML in data property values - * _.template('<b><%- value %></b>', { 'value': '<script>' }); - * // => '<b><script></b>' - * - * // using the "evaluate" delimiter to generate HTML - * var list = '<% _.forEach(people, function(name) { %><li><%= name %></li><% }); %>'; - * _.template(list, { 'people': ['moe', 'larry'] }); - * // => '<li>moe</li><li>larry</li>' - * - * // using the ES6 delimiter as an alternative to the default "interpolate" delimiter - * _.template('hello ${ name }', { 'name': 'curly' }); - * // => 'hello curly' - * - * // using the internal `print` function in "evaluate" delimiters - * _.template('<% print("hello " + epithet); %>!', { 'epithet': 'stooge' }); - * // => 'hello stooge!' - * - * // using a custom template delimiters - * _.templateSettings = { - * 'interpolate': /{{([\s\S]+?)}}/g - * }; - * - * _.template('hello {{ name }}!', { 'name': 'mustache' }); - * // => 'hello mustache!' - * - * // using the `imports` option to import jQuery - * var list = '<% $.each(people, function(name) { %><li><%= name %></li><% }); %>'; - * _.template(list, { 'people': ['moe', 'larry'] }, { 'imports': { '$': jQuery }); - * // => '<li>moe</li><li>larry</li>' - * - * // using the `sourceURL` option to specify a custom sourceURL for the template - * var compiled = _.template('hello <%= name %>', null, { 'sourceURL': '/basic/greeting.jst' }); - * compiled(data); - * // => find the source of "greeting.jst" under the Sources tab or Resources panel of the web inspector - * - * // using the `variable` option to ensure a with-statement isn't used in the compiled template - * var compiled = _.template('hi <%= data.name %>!', null, { 'variable': 'data' }); - * compiled.source; - * // => function(data) { - * var __t, __p = '', __e = _.escape; - * __p += 'hi ' + ((__t = ( data.name )) == null ? '' : __t) + '!'; - * return __p; - * } - * - * // using the `source` property to inline compiled templates for meaningful - * // line numbers in error messages and a stack trace - * fs.writeFileSync(path.join(cwd, 'jst.js'), '\ - * var JST = {\ - * "main": ' + _.template(mainText).source + '\ - * };\ - * '); - */ - function template(text, data, options) { - var _ = lodash, - settings = _.templateSettings; - - text || (text = ''); - options = defaults({}, options, settings); - - var index = 0, - source = "__p += '", - variable = options.variable; - - var reDelimiters = RegExp( - (options.escape || reNoMatch).source + '|' + - (options.interpolate || reNoMatch).source + '|' + - (options.evaluate || reNoMatch).source + '|$' - , 'g'); - - text.replace(reDelimiters, function(match, escapeValue, interpolateValue, evaluateValue, offset) { - source += text.slice(index, offset).replace(reUnescapedString, escapeStringChar); - if (escapeValue) { - source += "' +\n_.escape(" + escapeValue + ") +\n'"; - } - if (evaluateValue) { - source += "';\n" + evaluateValue + ";\n__p += '"; - } - if (interpolateValue) { - source += "' +\n((__t = (" + interpolateValue + ")) == null ? '' : __t) +\n'"; - } - index = offset + match.length; - return match; - }); - - source += "';\n"; - if (!variable) { - variable = 'obj'; - source = 'with (' + variable + ' || {}) {\n' + source + '\n}\n'; - } - source = 'function(' + variable + ') {\n' + - "var __t, __p = '', __j = Array.prototype.join;\n" + - "function print() { __p += __j.call(arguments, '') }\n" + - source + - 'return __p\n}'; - - try { - var result = Function('_', 'return ' + source)(_); - } catch(e) { - e.source = source; - throw e; - } - if (data) { - return result(data); - } - result.source = source; - return result; - } - - /** - * Executes the callback `n` times, returning an array of the results - * of each callback execution. The callback is bound to `thisArg` and invoked - * with one argument; (index). - * - * @static - * @memberOf _ - * @category Utilities - * @param {Number} n The number of times to execute the callback. - * @param {Function} callback The function called per iteration. - * @param {Mixed} [thisArg] The `this` binding of `callback`. - * @returns {Array} Returns an array of the results of each `callback` execution. - * @example - * - * var diceRolls = _.times(3, _.partial(_.random, 1, 6)); - * // => [3, 6, 4] - * - * _.times(3, function(n) { mage.castSpell(n); }); - * // => calls `mage.castSpell(n)` three times, passing `n` of `0`, `1`, and `2` respectively - * - * _.times(3, function(n) { this.cast(n); }, mage); - * // => also calls `mage.castSpell(n)` three times - */ - function times(n, callback, thisArg) { - var index = -1, - result = Array(n > -1 ? n : 0); - - while (++index < n) { - result[index] = callback.call(thisArg, index); - } - return result; - } - - /** - * The inverse of `_.escape` this method converts the HTML entities - * `&`, `<`, `>`, `"`, and `'` in `string` to their - * corresponding characters. - * - * @static - * @memberOf _ - * @category Utilities - * @param {String} string The string to unescape. - * @returns {String} Returns the unescaped string. - * @example - * - * _.unescape('Moe, Larry & Curly'); - * // => 'Moe, Larry & Curly' - */ - function unescape(string) { - return string == null ? '' : String(string).replace(reEscapedHtml, unescapeHtmlChar); - } - - /** - * Generates a unique ID. If `prefix` is provided the ID will be appended to it. - * - * @static - * @memberOf _ - * @category Utilities - * @param {String} [prefix] The value to prefix the ID with. - * @returns {String} Returns the unique ID. - * @example - * - * _.uniqueId('contact_'); - * // => 'contact_104' - * - * _.uniqueId(); - * // => '105' - */ - function uniqueId(prefix) { - var id = ++idCounter + ''; - return prefix ? prefix + id : id; - } - - /*--------------------------------------------------------------------------*/ - - /** - * Creates a `lodash` object that wraps the given `value`. - * - * @static - * @memberOf _ - * @category Chaining - * @param {Mixed} value The value to wrap. - * @returns {Object} Returns the wrapper object. - * @example - * - * var stooges = [ - * { 'name': 'moe', 'age': 40 }, - * { 'name': 'larry', 'age': 50 }, - * { 'name': 'curly', 'age': 60 } - * ]; - * - * var youngest = _.chain(stooges) - * .sortBy(function(stooge) { return stooge.age; }) - * .map(function(stooge) { return stooge.name + ' is ' + stooge.age; }) - * .first(); - * // => 'moe is 40' - */ - function chain(value) { - value = new lodashWrapper(value); - value.__chain__ = true; - return value; - } - - /** - * Invokes `interceptor` with the `value` as the first argument and then - * returns `value`. The purpose of this method is to "tap into" a method - * chain in order to perform operations on intermediate results within - * the chain. - * - * @static - * @memberOf _ - * @category Chaining - * @param {Mixed} value The value to provide to `interceptor`. - * @param {Function} interceptor The function to invoke. - * @returns {Mixed} Returns `value`. - * @example - * - * _([1, 2, 3, 4]) - * .filter(function(num) { return num % 2 == 0; }) - * .tap(function(array) { console.log(array); }) - * .map(function(num) { return num * num; }) - * .value(); - * // => // [2, 4] (logged) - * // => [4, 16] - */ - function tap(value, interceptor) { - interceptor(value); - return value; - } - - /** - * Enables method chaining on the wrapper object. - * - * @name chain - * @memberOf _ - * @category Chaining - * @returns {Mixed} Returns the wrapper object. - * @example - * - * var sum = _([1, 2, 3]) - * .chain() - * .reduce(function(sum, num) { return sum + num; }) - * .value() - * // => 6` - */ - function wrapperChain() { - this.__chain__ = true; - return this; - } - - /** - * Extracts the wrapped value. - * - * @name valueOf - * @memberOf _ - * @alias value - * @category Chaining - * @returns {Mixed} Returns the wrapped value. - * @example - * - * _([1, 2, 3]).valueOf(); - * // => [1, 2, 3] - */ - function wrapperValueOf() { - return this.__wrapped__; - } - - /*--------------------------------------------------------------------------*/ - - // add functions that return wrapped values when chaining - lodash.after = after; - lodash.bind = bind; - lodash.bindAll = bindAll; - lodash.chain = chain; - lodash.compact = compact; - lodash.compose = compose; - lodash.countBy = countBy; - lodash.debounce = debounce; - lodash.defaults = defaults; - lodash.defer = defer; - lodash.delay = delay; - lodash.difference = difference; - lodash.filter = filter; - lodash.flatten = flatten; - lodash.forEach = forEach; - lodash.functions = functions; - lodash.groupBy = groupBy; - lodash.initial = initial; - lodash.intersection = intersection; - lodash.invert = invert; - lodash.invoke = invoke; - lodash.keys = keys; - lodash.map = map; - lodash.max = max; - lodash.memoize = memoize; - lodash.min = min; - lodash.omit = omit; - lodash.once = once; - lodash.pairs = pairs; - lodash.partial = partial; - lodash.pick = pick; - lodash.pluck = pluck; - lodash.range = range; - lodash.reject = reject; - lodash.rest = rest; - lodash.shuffle = shuffle; - lodash.sortBy = sortBy; - lodash.tap = tap; - lodash.throttle = throttle; - lodash.times = times; - lodash.toArray = toArray; - lodash.union = union; - lodash.uniq = uniq; - lodash.values = values; - lodash.where = where; - lodash.without = without; - lodash.wrap = wrap; - lodash.zip = zip; - - // add aliases - lodash.collect = map; - lodash.drop = rest; - lodash.each = forEach; - lodash.extend = assign; - lodash.methods = functions; - lodash.object = zipObject; - lodash.select = filter; - lodash.tail = rest; - lodash.unique = uniq; - - /*--------------------------------------------------------------------------*/ - - // add functions that return unwrapped values when chaining - lodash.clone = clone; - lodash.contains = contains; - lodash.escape = escape; - lodash.every = every; - lodash.find = find; - lodash.has = has; - lodash.identity = identity; - lodash.indexOf = indexOf; - lodash.isArguments = isArguments; - lodash.isArray = isArray; - lodash.isBoolean = isBoolean; - lodash.isDate = isDate; - lodash.isElement = isElement; - lodash.isEmpty = isEmpty; - lodash.isEqual = isEqual; - lodash.isFinite = isFinite; - lodash.isFunction = isFunction; - lodash.isNaN = isNaN; - lodash.isNull = isNull; - lodash.isNumber = isNumber; - lodash.isObject = isObject; - lodash.isRegExp = isRegExp; - lodash.isString = isString; - lodash.isUndefined = isUndefined; - lodash.lastIndexOf = lastIndexOf; - lodash.mixin = mixin; - lodash.noConflict = noConflict; - lodash.random = random; - lodash.reduce = reduce; - lodash.reduceRight = reduceRight; - lodash.result = result; - lodash.size = size; - lodash.some = some; - lodash.sortedIndex = sortedIndex; - lodash.template = template; - lodash.unescape = unescape; - lodash.uniqueId = uniqueId; - - // add aliases - lodash.all = every; - lodash.any = some; - lodash.detect = find; - lodash.findWhere = findWhere; - lodash.foldl = reduce; - lodash.foldr = reduceRight; - lodash.include = contains; - lodash.inject = reduce; - - /*--------------------------------------------------------------------------*/ - - // add functions capable of returning wrapped and unwrapped values when chaining - lodash.first = first; - lodash.last = last; - - // add aliases - lodash.take = first; - lodash.head = first; - - /*--------------------------------------------------------------------------*/ - - // add functions to `lodash.prototype` - mixin(lodash); - - /** - * The semantic version number. - * - * @static - * @memberOf _ - * @type String - */ - lodash.VERSION = '1.3.1'; - - // add "Chaining" functions to the wrapper - lodash.prototype.chain = wrapperChain; - lodash.prototype.value = wrapperValueOf; - - // add `Array` mutator functions to the wrapper - forEach(['pop', 'push', 'reverse', 'shift', 'sort', 'splice', 'unshift'], function(methodName) { - var func = arrayRef[methodName]; - lodash.prototype[methodName] = function() { - var value = this.__wrapped__; - func.apply(value, arguments); - - // avoid array-like object bugs with `Array#shift` and `Array#splice` - // in Firefox < 10 and IE < 9 - if (!support.spliceObjects && value.length === 0) { - delete value[0]; - } - return this; - }; - }); - - // add `Array` accessor functions to the wrapper - forEach(['concat', 'join', 'slice'], function(methodName) { - var func = arrayRef[methodName]; - lodash.prototype[methodName] = function() { - var value = this.__wrapped__, - result = func.apply(value, arguments); - - if (this.__chain__) { - result = new lodashWrapper(result); - result.__chain__ = true; - } - return result; - }; - }); - - /*--------------------------------------------------------------------------*/ - - // some AMD build optimizers, like r.js, check for specific condition patterns like the following: - if (typeof define == 'function' && typeof define.amd == 'object' && define.amd) { - // Expose Lo-Dash to the global object even when an AMD loader is present in - // case Lo-Dash was injected by a third-party script and not intended to be - // loaded as a module. The global assignment can be reverted in the Lo-Dash - // module via its `noConflict()` method. - window._ = lodash; - - // define as an anonymous module so, through path mapping, it can be - // referenced as the "underscore" module - define(function() { - return lodash; - }); - } - // check for `exports` after `define` in case a build optimizer adds an `exports` object - else if (freeExports && !freeExports.nodeType) { - // in Node.js or RingoJS v0.8.0+ - if (freeModule) { - (freeModule.exports = lodash)._ = lodash; - } - // in Narwhal or RingoJS v0.7.0- - else { - freeExports._ = lodash; - } - } - else { - // in a browser or Rhino - window._ = lodash; - } -}(this)); diff --git a/src/UI/JsLibraries/messenger.js b/src/UI/JsLibraries/messenger.js deleted file mode 100644 index 8acdbcff3..000000000 --- a/src/UI/JsLibraries/messenger.js +++ /dev/null @@ -1,1263 +0,0 @@ -/*! messenger 1.4.1 */ -/* - * This file begins the output concatenated into messenger.js - * - * It establishes the Messenger object while preserving whatever it was before - * (for noConflict), and making it a callable function. - */ - -(function(){ - var _prevMessenger = window.Messenger; - var localMessenger; - - localMessenger = window.Messenger = function(){ - return localMessenger._call.apply(this, arguments); - } - - window.Messenger.noConflict = function(){ - window.Messenger = _prevMessenger; - - return localMessenger; - } -})(); - -/* - * This file contains shims for when Underscore and Backbone - * are not included. - * - * Portions taken from Underscore.js and Backbone.js - * Both of which are Copyright (c) 2009-2013 Jeremy Ashkenas, DocumentCloud - */ -window.Messenger._ = (function() { - if (window._) - return window._ - - var ArrayProto = Array.prototype, ObjProto = Object.prototype, FuncProto = Function.prototype; - - // Create quick reference variables for speed access to core prototypes. - var push = ArrayProto.push, - slice = ArrayProto.slice, - concat = ArrayProto.concat, - toString = ObjProto.toString, - hasOwnProperty = ObjProto.hasOwnProperty; - - // All **ECMAScript 5** native function implementations that we hope to use - // are declared here. - var - nativeForEach = ArrayProto.forEach, - nativeMap = ArrayProto.map, - nativeReduce = ArrayProto.reduce, - nativeReduceRight = ArrayProto.reduceRight, - nativeFilter = ArrayProto.filter, - nativeEvery = ArrayProto.every, - nativeSome = ArrayProto.some, - nativeIndexOf = ArrayProto.indexOf, - nativeLastIndexOf = ArrayProto.lastIndexOf, - nativeIsArray = Array.isArray, - nativeKeys = Object.keys, - nativeBind = FuncProto.bind; - - // Create a safe reference to the Underscore object for use below. - var _ = {}; - - // Establish the object that gets returned to break out of a loop iteration. - var breaker = {}; - - var each = _.each = _.forEach = function(obj, iterator, context) { - if (obj == null) return; - if (nativeForEach && obj.forEach === nativeForEach) { - obj.forEach(iterator, context); - } else if (obj.length === +obj.length) { - for (var i = 0, l = obj.length; i < l; i++) { - if (iterator.call(context, obj[i], i, obj) === breaker) return; - } - } else { - for (var key in obj) { - if (_.has(obj, key)) { - if (iterator.call(context, obj[key], key, obj) === breaker) return; - } - } - } - }; - - _.result = function(object, property) { - if (object == null) return null; - var value = object[property]; - return _.isFunction(value) ? value.call(object) : value; - }; - - _.once = function(func) { - var ran = false, memo; - return function() { - if (ran) return memo; - ran = true; - memo = func.apply(this, arguments); - func = null; - return memo; - }; - }; - - var idCounter = 0; - _.uniqueId = function(prefix) { - var id = ++idCounter + ''; - return prefix ? prefix + id : id; - }; - - _.filter = _.select = function(obj, iterator, context) { - var results = []; - if (obj == null) return results; - if (nativeFilter && obj.filter === nativeFilter) return obj.filter(iterator, context); - each(obj, function(value, index, list) { - if (iterator.call(context, value, index, list)) results[results.length] = value; - }); - return results; - }; - - // Add some isType methods: isArguments, isFunction, isString, isNumber, isDate, isRegExp. - each(['Arguments', 'Function', 'String', 'Number', 'Date', 'RegExp'], function(name) { - _['is' + name] = function(obj) { - return toString.call(obj) == '[object ' + name + ']'; - }; - }); - - _.defaults = function(obj) { - each(slice.call(arguments, 1), function(source) { - if (source) { - for (var prop in source) { - if (obj[prop] == null) obj[prop] = source[prop]; - } - } - }); - return obj; - }; - - _.extend = function(obj) { - each(slice.call(arguments, 1), function(source) { - if (source) { - for (var prop in source) { - obj[prop] = source[prop]; - } - } - }); - return obj; - }; - - _.keys = nativeKeys || function(obj) { - if (obj !== Object(obj)) throw new TypeError('Invalid object'); - var keys = []; - for (var key in obj) if (_.has(obj, key)) keys[keys.length] = key; - return keys; - }; - - _.bind = function(func, context) { - if (func.bind === nativeBind && nativeBind) return nativeBind.apply(func, slice.call(arguments, 1)); - var args = slice.call(arguments, 2); - return function() { - return func.apply(context, args.concat(slice.call(arguments))); - }; - }; - - _.isObject = function(obj) { - return obj === Object(obj); - }; - - return _; -})(); - -window.Messenger.Events = (function() { - if (window.Backbone && Backbone.Events) { - return Backbone.Events; - } - - var eventsShim = function() { - var eventSplitter = /\s+/; - - var eventsApi = function(obj, action, name, rest) { - if (!name) return true; - if (typeof name === 'object') { - for (var key in name) { - obj[action].apply(obj, [key, name[key]].concat(rest)); - } - } else if (eventSplitter.test(name)) { - var names = name.split(eventSplitter); - for (var i = 0, l = names.length; i < l; i++) { - obj[action].apply(obj, [names[i]].concat(rest)); - } - } else { - return true; - } - }; - - var triggerEvents = function(events, args) { - var ev, i = -1, l = events.length; - switch (args.length) { - case 0: while (++i < l) (ev = events[i]).callback.call(ev.ctx); - return; - case 1: while (++i < l) (ev = events[i]).callback.call(ev.ctx, args[0]); - return; - case 2: while (++i < l) (ev = events[i]).callback.call(ev.ctx, args[0], args[1]); - return; - case 3: while (++i < l) (ev = events[i]).callback.call(ev.ctx, args[0], args[1], args[2]); - return; - default: while (++i < l) (ev = events[i]).callback.apply(ev.ctx, args); - } - }; - - var Events = { - - on: function(name, callback, context) { - if (!(eventsApi(this, 'on', name, [callback, context]) && callback)) return this; - this._events || (this._events = {}); - var list = this._events[name] || (this._events[name] = []); - list.push({callback: callback, context: context, ctx: context || this}); - return this; - }, - - once: function(name, callback, context) { - if (!(eventsApi(this, 'once', name, [callback, context]) && callback)) return this; - var self = this; - var once = _.once(function() { - self.off(name, once); - callback.apply(this, arguments); - }); - once._callback = callback; - this.on(name, once, context); - return this; - }, - - off: function(name, callback, context) { - var list, ev, events, names, i, l, j, k; - if (!this._events || !eventsApi(this, 'off', name, [callback, context])) return this; - if (!name && !callback && !context) { - this._events = {}; - return this; - } - - names = name ? [name] : _.keys(this._events); - for (i = 0, l = names.length; i < l; i++) { - name = names[i]; - if (list = this._events[name]) { - events = []; - if (callback || context) { - for (j = 0, k = list.length; j < k; j++) { - ev = list[j]; - if ((callback && callback !== ev.callback && - callback !== ev.callback._callback) || - (context && context !== ev.context)) { - events.push(ev); - } - } - } - this._events[name] = events; - } - } - - return this; - }, - - trigger: function(name) { - if (!this._events) return this; - var args = Array.prototype.slice.call(arguments, 1); - if (!eventsApi(this, 'trigger', name, args)) return this; - var events = this._events[name]; - var allEvents = this._events.all; - if (events) triggerEvents(events, args); - if (allEvents) triggerEvents(allEvents, arguments); - return this; - }, - - listenTo: function(obj, name, callback) { - var listeners = this._listeners || (this._listeners = {}); - var id = obj._listenerId || (obj._listenerId = _.uniqueId('l')); - listeners[id] = obj; - obj.on(name, typeof name === 'object' ? this : callback, this); - return this; - }, - - stopListening: function(obj, name, callback) { - var listeners = this._listeners; - if (!listeners) return; - if (obj) { - obj.off(name, typeof name === 'object' ? this : callback, this); - if (!name && !callback) delete listeners[obj._listenerId]; - } else { - if (typeof name === 'object') callback = this; - for (var id in listeners) { - listeners[id].off(name, callback, this); - } - this._listeners = {}; - } - return this; - } - }; - - Events.bind = Events.on; - Events.unbind = Events.off; - return Events; - }; - return eventsShim(); -})(); - -(function() { - var $, ActionMessenger, BaseView, Events, RetryingMessage, _, _Message, _Messenger, _ref, _ref1, _ref2, - __hasProp = {}.hasOwnProperty, - __extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }, - __slice = [].slice, - __indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; }; - - $ = jQuery; - - _ = (_ref = window._) != null ? _ref : window.Messenger._; - - Events = (_ref1 = typeof Backbone !== "undefined" && Backbone !== null ? Backbone.Events : void 0) != null ? _ref1 : window.Messenger.Events; - - BaseView = (function() { - - function BaseView(options) { - $.extend(this, Events); - if (_.isObject(options)) { - if (options.el) { - this.setElement(options.el); - } - this.model = options.model; - } - this.initialize.apply(this, arguments); - } - - BaseView.prototype.setElement = function(el) { - this.$el = $(el); - return this.el = this.$el[0]; - }; - - BaseView.prototype.delegateEvents = function(events) { - var delegateEventSplitter, eventName, key, match, method, selector, _results; - if (!(events || (events = _.result(this, "events")))) { - return; - } - this.undelegateEvents(); - delegateEventSplitter = /^(\S+)\s*(.*)$/; - _results = []; - for (key in events) { - method = events[key]; - if (!_.isFunction(method)) { - method = this[events[key]]; - } - if (!method) { - throw new Error("Method \"" + events[key] + "\" does not exist"); - } - match = key.match(delegateEventSplitter); - eventName = match[1]; - selector = match[2]; - method = _.bind(method, this); - eventName += ".delegateEvents" + this.cid; - if (selector === '') { - _results.push(this.jqon(eventName, method)); - } else { - _results.push(this.jqon(eventName, selector, method)); - } - } - return _results; - }; - - BaseView.prototype.jqon = function(eventName, selector, method) { - var _ref2; - if (this.$el.on != null) { - return (_ref2 = this.$el).on.apply(_ref2, arguments); - } else { - if (!(method != null)) { - method = selector; - selector = void 0; - } - if (selector != null) { - return this.$el.delegate(selector, eventName, method); - } else { - return this.$el.bind(eventName, method); - } - } - }; - - BaseView.prototype.jqoff = function(eventName) { - var _ref2; - if (this.$el.off != null) { - return (_ref2 = this.$el).off.apply(_ref2, arguments); - } else { - this.$el.undelegate(); - return this.$el.unbind(eventName); - } - }; - - BaseView.prototype.undelegateEvents = function() { - return this.jqoff(".delegateEvents" + this.cid); - }; - - BaseView.prototype.remove = function() { - this.undelegateEvents(); - return this.$el.remove(); - }; - - return BaseView; - - })(); - - _Message = (function(_super) { - - __extends(_Message, _super); - - function _Message() { - return _Message.__super__.constructor.apply(this, arguments); - } - - _Message.prototype.defaults = { - hideAfter: 10, - scroll: true, - closeButtonText: "×" - }; - - _Message.prototype.initialize = function(opts) { - if (opts == null) { - opts = {}; - } - this.shown = false; - this.rendered = false; - this.messenger = opts.messenger; - return this.options = $.extend({}, this.options, opts, this.defaults); - }; - - _Message.prototype.show = function() { - var wasShown; - if (!this.rendered) { - this.render(); - } - this.$message.removeClass('messenger-hidden'); - wasShown = this.shown; - this.shown = true; - if (!wasShown) { - return this.trigger('show'); - } - }; - - _Message.prototype.hide = function() { - var wasShown; - if (!this.rendered) { - return; - } - this.$message.addClass('messenger-hidden'); - wasShown = this.shown; - this.shown = false; - if (wasShown) { - return this.trigger('hide'); - } - }; - - _Message.prototype.cancel = function() { - return this.hide(); - }; - - _Message.prototype.update = function(opts) { - var _ref2, - _this = this; - if (_.isString(opts)) { - opts = { - message: opts - }; - } - $.extend(this.options, opts); - this.lastUpdate = new Date(); - this.rendered = false; - this.events = (_ref2 = this.options.events) != null ? _ref2 : {}; - this.render(); - this.actionsToEvents(); - this.delegateEvents(); - this.checkClickable(); - if (this.options.hideAfter) { - this.$message.addClass('messenger-will-hide-after'); - if (this._hideTimeout != null) { - clearTimeout(this._hideTimeout); - } - this._hideTimeout = setTimeout(function() { - return _this.hide(); - }, this.options.hideAfter * 1000); - } else { - this.$message.removeClass('messenger-will-hide-after'); - } - if (this.options.hideOnNavigate) { - this.$message.addClass('messenger-will-hide-on-navigate'); - if ((typeof Backbone !== "undefined" && Backbone !== null ? Backbone.history : void 0) != null) { - Backbone.history.on('route', function() { - return _this.hide(); - }); - } - } else { - this.$message.removeClass('messenger-will-hide-on-navigate'); - } - return this.trigger('update', this); - }; - - _Message.prototype.scrollTo = function() { - if (!this.options.scroll) { - return; - } - return $.scrollTo(this.$el, { - duration: 400, - offset: { - left: 0, - top: -20 - } - }); - }; - - _Message.prototype.timeSinceUpdate = function() { - if (this.lastUpdate) { - return (new Date) - this.lastUpdate; - } else { - return null; - } - }; - - _Message.prototype.actionsToEvents = function() { - var act, name, _ref2, _results, - _this = this; - _ref2 = this.options.actions; - _results = []; - for (name in _ref2) { - act = _ref2[name]; - _results.push(this.events["click [data-action=\"" + name + "\"] a"] = (function(act) { - return function(e) { - e.preventDefault(); - e.stopPropagation(); - _this.trigger("action:" + name, act, e); - return act.action.call(_this, e, _this); - }; - })(act)); - } - return _results; - }; - - _Message.prototype.checkClickable = function() { - var evt, name, _ref2, _results; - _ref2 = this.events; - _results = []; - for (name in _ref2) { - evt = _ref2[name]; - if (name === 'click') { - _results.push(this.$message.addClass('messenger-clickable')); - } else { - _results.push(void 0); - } - } - return _results; - }; - - _Message.prototype.undelegateEvents = function() { - var _ref2; - _Message.__super__.undelegateEvents.apply(this, arguments); - return (_ref2 = this.$message) != null ? _ref2.removeClass('messenger-clickable') : void 0; - }; - - _Message.prototype.parseActions = function() { - var act, actions, n_act, name, _ref2, _ref3; - actions = []; - _ref2 = this.options.actions; - for (name in _ref2) { - act = _ref2[name]; - n_act = $.extend({}, act); - n_act.name = name; - if ((_ref3 = n_act.label) == null) { - n_act.label = name; - } - actions.push(n_act); - } - return actions; - }; - - _Message.prototype.template = function(opts) { - var $action, $actions, $cancel, $link, $message, $text, action, _i, _len, _ref2, - _this = this; - $message = $("<div class='messenger-message message alert " + opts.type + " message-" + opts.type + " alert-" + opts.type + "'>"); - if (opts.showCloseButton) { - $cancel = $('<button type="button" class="messenger-close" data-dismiss="alert">'); - $cancel.html(opts.closeButtonText); - $cancel.click(function() { - _this.cancel(); - return true; - }); - $message.append($cancel); - } - $text = $("<div class=\"messenger-message-inner\">" + opts.message + "</div>"); - $message.append($text); - if (opts.actions.length) { - $actions = $('<div class="messenger-actions">'); - } - _ref2 = opts.actions; - for (_i = 0, _len = _ref2.length; _i < _len; _i++) { - action = _ref2[_i]; - $action = $('<span>'); - $action.attr('data-action', "" + action.name); - $link = $('<a>'); - $link.html(action.label); - $action.append($('<span class="messenger-phrase">')); - $action.append($link); - $actions.append($action); - } - $message.append($actions); - return $message; - }; - - _Message.prototype.render = function() { - var opts; - if (this.rendered) { - return; - } - if (!this._hasSlot) { - this.setElement(this.messenger._reserveMessageSlot(this)); - this._hasSlot = true; - } - opts = $.extend({}, this.options, { - actions: this.parseActions() - }); - this.$message = $(this.template(opts)); - this.$el.html(this.$message); - this.shown = true; - this.rendered = true; - return this.trigger('render'); - }; - - return _Message; - - })(BaseView); - - RetryingMessage = (function(_super) { - - __extends(RetryingMessage, _super); - - function RetryingMessage() { - return RetryingMessage.__super__.constructor.apply(this, arguments); - } - - RetryingMessage.prototype.initialize = function() { - RetryingMessage.__super__.initialize.apply(this, arguments); - return this._timers = {}; - }; - - RetryingMessage.prototype.cancel = function() { - this.clearTimers(); - this.hide(); - if ((this._actionInstance != null) && (this._actionInstance.abort != null)) { - return this._actionInstance.abort(); - } - }; - - RetryingMessage.prototype.clearTimers = function() { - var name, timer, _ref2, _ref3; - _ref2 = this._timers; - for (name in _ref2) { - timer = _ref2[name]; - clearTimeout(timer); - } - this._timers = {}; - return (_ref3 = this.$message) != null ? _ref3.removeClass('messenger-retry-soon messenger-retry-later') : void 0; - }; - - RetryingMessage.prototype.render = function() { - var action, name, _ref2, _results; - RetryingMessage.__super__.render.apply(this, arguments); - this.clearTimers(); - _ref2 = this.options.actions; - _results = []; - for (name in _ref2) { - action = _ref2[name]; - if (action.auto) { - _results.push(this.startCountdown(name, action)); - } else { - _results.push(void 0); - } - } - return _results; - }; - - RetryingMessage.prototype.renderPhrase = function(action, time) { - var phrase; - phrase = action.phrase.replace('TIME', this.formatTime(time)); - return phrase; - }; - - RetryingMessage.prototype.formatTime = function(time) { - var pluralize; - pluralize = function(num, str) { - num = Math.floor(num); - if (num !== 1) { - str = str + 's'; - } - return 'in ' + num + ' ' + str; - }; - if (Math.floor(time) === 0) { - return 'now...'; - } - if (time < 60) { - return pluralize(time, 'second'); - } - time /= 60; - if (time < 60) { - return pluralize(time, 'minute'); - } - time /= 60; - return pluralize(time, 'hour'); - }; - - RetryingMessage.prototype.startCountdown = function(name, action) { - var $phrase, remaining, tick, _ref2, - _this = this; - if (this._timers[name] != null) { - return; - } - $phrase = this.$message.find("[data-action='" + name + "'] .messenger-phrase"); - remaining = (_ref2 = action.delay) != null ? _ref2 : 3; - if (remaining <= 10) { - this.$message.removeClass('messenger-retry-later'); - this.$message.addClass('messenger-retry-soon'); - } else { - this.$message.removeClass('messenger-retry-soon'); - this.$message.addClass('messenger-retry-later'); - } - tick = function() { - var delta; - $phrase.text(_this.renderPhrase(action, remaining)); - if (remaining > 0) { - delta = Math.min(remaining, 1); - remaining -= delta; - return _this._timers[name] = setTimeout(tick, delta * 1000); - } else { - _this.$message.removeClass('messenger-retry-soon messenger-retry-later'); - delete _this._timers[name]; - return action.action(); - } - }; - return tick(); - }; - - return RetryingMessage; - - })(_Message); - - _Messenger = (function(_super) { - - __extends(_Messenger, _super); - - function _Messenger() { - return _Messenger.__super__.constructor.apply(this, arguments); - } - - _Messenger.prototype.tagName = 'ul'; - - _Messenger.prototype.className = 'messenger'; - - _Messenger.prototype.messageDefaults = { - type: 'info' - }; - - _Messenger.prototype.initialize = function(options) { - this.options = options != null ? options : {}; - this.history = []; - return this.messageDefaults = $.extend({}, this.messageDefaults, this.options.messageDefaults); - }; - - _Messenger.prototype.render = function() { - return this.updateMessageSlotClasses(); - }; - - _Messenger.prototype.findById = function(id) { - return _.filter(this.history, function(rec) { - return rec.msg.options.id === id; - }); - }; - - _Messenger.prototype._reserveMessageSlot = function(msg) { - var $slot, dmsg, - _this = this; - $slot = $('<li>'); - $slot.addClass('messenger-message-slot'); - this.$el.prepend($slot); - this.history.push({ - msg: msg, - $slot: $slot - }); - this._enforceIdConstraint(msg); - msg.on('update', function() { - return _this._enforceIdConstraint(msg); - }); - while (this.options.maxMessages && this.history.length > this.options.maxMessages) { - dmsg = this.history.shift(); - dmsg.msg.remove(); - dmsg.$slot.remove(); - } - return $slot; - }; - - _Messenger.prototype._enforceIdConstraint = function(msg) { - var entry, _i, _len, _msg, _ref2; - if (msg.options.id == null) { - return; - } - _ref2 = this.history; - for (_i = 0, _len = _ref2.length; _i < _len; _i++) { - entry = _ref2[_i]; - _msg = entry.msg; - if ((_msg.options.id != null) && _msg.options.id === msg.options.id && msg !== _msg) { - if (msg.options.singleton) { - msg.hide(); - return; - } else { - _msg.hide(); - } - } - } - }; - - _Messenger.prototype.newMessage = function(opts) { - var msg, _ref2, _ref3, _ref4, - _this = this; - if (opts == null) { - opts = {}; - } - opts.messenger = this; - _Message = (_ref2 = (_ref3 = Messenger.themes[(_ref4 = opts.theme) != null ? _ref4 : this.options.theme]) != null ? _ref3.Message : void 0) != null ? _ref2 : RetryingMessage; - msg = new _Message(opts); - msg.on('show', function() { - if (opts.scrollTo && _this.$el.css('position') !== 'fixed') { - return msg.scrollTo(); - } - }); - msg.on('hide show render', this.updateMessageSlotClasses, this); - return msg; - }; - - _Messenger.prototype.updateMessageSlotClasses = function() { - var anyShown, last, rec, willBeFirst, _i, _len, _ref2; - willBeFirst = true; - last = null; - anyShown = false; - _ref2 = this.history; - for (_i = 0, _len = _ref2.length; _i < _len; _i++) { - rec = _ref2[_i]; - rec.$slot.removeClass('messenger-first messenger-last messenger-shown'); - if (rec.msg.shown && rec.msg.rendered) { - rec.$slot.addClass('messenger-shown'); - anyShown = true; - last = rec; - if (willBeFirst) { - willBeFirst = false; - rec.$slot.addClass('messenger-first'); - } - } - } - if (last != null) { - last.$slot.addClass('messenger-last'); - } - return this.$el["" + (anyShown ? 'remove' : 'add') + "Class"]('messenger-empty'); - }; - - _Messenger.prototype.hideAll = function() { - var rec, _i, _len, _ref2, _results; - _ref2 = this.history; - _results = []; - for (_i = 0, _len = _ref2.length; _i < _len; _i++) { - rec = _ref2[_i]; - _results.push(rec.msg.hide()); - } - return _results; - }; - - _Messenger.prototype.post = function(opts) { - var msg; - if (_.isString(opts)) { - opts = { - message: opts - }; - } - opts = $.extend(true, {}, this.messageDefaults, opts); - msg = this.newMessage(opts); - msg.update(opts); - return msg; - }; - - return _Messenger; - - })(BaseView); - - ActionMessenger = (function(_super) { - - __extends(ActionMessenger, _super); - - function ActionMessenger() { - return ActionMessenger.__super__.constructor.apply(this, arguments); - } - - ActionMessenger.prototype.doDefaults = { - progressMessage: null, - successMessage: null, - errorMessage: "Error connecting to the server.", - showSuccessWithoutError: true, - retry: { - auto: true, - allow: true - }, - action: $.ajax - }; - - ActionMessenger.prototype.hookBackboneAjax = function(msgr_opts) { - var _ajax, - _this = this; - if (msgr_opts == null) { - msgr_opts = {}; - } - if (!(window.Backbone != null)) { - throw 'Expected Backbone to be defined'; - } - msgr_opts = _.defaults(msgr_opts, { - id: 'BACKBONE_ACTION', - errorMessage: false, - successMessage: "Request completed successfully.", - showSuccessWithoutError: false - }); - _ajax = function(options) { - var sync_msgr_opts; - sync_msgr_opts = _.extend({}, msgr_opts, options.messenger); - return _this["do"](sync_msgr_opts, options); - }; - if (Backbone.ajax != null) { - if (Backbone.ajax._withoutMessenger) { - Backbone.ajax = Backbone.ajax._withoutMessenger; - } - if (!(msgr_opts.action != null) || msgr_opts.action === this.doDefaults.action) { - msgr_opts.action = Backbone.ajax; - } - _ajax._withoutMessenger = Backbone.ajax; - return Backbone.ajax = _ajax; - } else { - return Backbone.sync = _.wrap(Backbone.sync, function() { - var args, _old_ajax, _old_sync; - _old_sync = arguments[0], args = 2 <= arguments.length ? __slice.call(arguments, 1) : []; - _old_ajax = $.ajax; - $.ajax = _ajax; - _old_sync.call.apply(_old_sync, [this].concat(__slice.call(args))); - return $.ajax = _old_ajax; - }); - } - }; - - ActionMessenger.prototype._getHandlerResponse = function(returnVal) { - if (returnVal === false) { - return false; - } - if (returnVal === true || !(returnVal != null)) { - return true; - } - return returnVal; - }; - - ActionMessenger.prototype._parseEvents = function(events) { - var desc, firstSpace, func, label, out, type, _ref2; - if (events == null) { - events = {}; - } - out = {}; - for (label in events) { - func = events[label]; - firstSpace = label.indexOf(' '); - type = label.substring(0, firstSpace); - desc = label.substring(firstSpace + 1); - if ((_ref2 = out[type]) == null) { - out[type] = {}; - } - out[type][desc] = func; - } - return out; - }; - - ActionMessenger.prototype._normalizeResponse = function() { - var data, elem, resp, type, xhr, _i, _len; - resp = 1 <= arguments.length ? __slice.call(arguments, 0) : []; - type = null; - xhr = null; - data = null; - for (_i = 0, _len = resp.length; _i < _len; _i++) { - elem = resp[_i]; - if (elem === 'success' || elem === 'timeout' || elem === 'abort') { - type = elem; - } else if (((elem != null ? elem.readyState : void 0) != null) && ((elem != null ? elem.responseText : void 0) != null)) { - xhr = elem; - } else if (_.isObject(elem)) { - data = elem; - } - } - return [type, data, xhr]; - }; - - ActionMessenger.prototype.run = function() { - var args, events, getMessageText, handler, handlers, m_opts, msg, old, opts, type, _ref2, - _this = this; - m_opts = arguments[0], opts = arguments[1], args = 3 <= arguments.length ? __slice.call(arguments, 2) : []; - if (opts == null) { - opts = {}; - } - m_opts = $.extend(true, {}, this.messageDefaults, this.doDefaults, m_opts != null ? m_opts : {}); - events = this._parseEvents(m_opts.events); - getMessageText = function(type, xhr) { - var message; - message = m_opts[type + 'Message']; - if (_.isFunction(message)) { - return message.call(_this, type, xhr); - } - return message; - }; - msg = (_ref2 = m_opts.messageInstance) != null ? _ref2 : this.newMessage(m_opts); - if (m_opts.id != null) { - msg.options.id = m_opts.id; - } - if (m_opts.progressMessage != null) { - msg.update($.extend({}, m_opts, { - message: getMessageText('progress', null), - type: 'info' - })); - } - handlers = {}; - _.each(['error', 'success'], function(type) { - var originalHandler; - originalHandler = opts[type]; - return handlers[type] = function() { - var data, defaultOpts, handlerResp, msgOpts, reason, resp, responseOpts, xhr, _ref3, _ref4, _ref5, _ref6, _ref7, _ref8, _ref9; - resp = 1 <= arguments.length ? __slice.call(arguments, 0) : []; - _ref3 = _this._normalizeResponse.apply(_this, resp), reason = _ref3[0], data = _ref3[1], xhr = _ref3[2]; - if (type === 'success' && !(msg.errorCount != null) && m_opts.showSuccessWithoutError === false) { - m_opts['successMessage'] = null; - } - if (type === 'error') { - if ((_ref4 = m_opts.errorCount) == null) { - m_opts.errorCount = 0; - } - m_opts.errorCount += 1; - } - handlerResp = m_opts.returnsPromise ? resp[0] : typeof originalHandler === "function" ? originalHandler.apply(null, resp) : void 0; - responseOpts = _this._getHandlerResponse(handlerResp); - if (_.isString(responseOpts)) { - responseOpts = { - message: responseOpts - }; - } - if (type === 'error' && ((xhr != null ? xhr.status : void 0) === 0 || reason === 'abort')) { - msg.hide(); - return; - } - if (type === 'error' && ((m_opts.ignoredErrorCodes != null) && (_ref5 = xhr != null ? xhr.status : void 0, __indexOf.call(m_opts.ignoredErrorCodes, _ref5) >= 0))) { - msg.hide(); - return; - } - defaultOpts = { - message: getMessageText(type, xhr), - type: type, - events: (_ref6 = events[type]) != null ? _ref6 : {}, - hideOnNavigate: type === 'success' - }; - msgOpts = $.extend({}, m_opts, defaultOpts, responseOpts); - if (typeof ((_ref7 = msgOpts.retry) != null ? _ref7.allow : void 0) === 'number') { - msgOpts.retry.allow--; - } - if (type === 'error' && (xhr != null ? xhr.status : void 0) >= 500 && ((_ref8 = msgOpts.retry) != null ? _ref8.allow : void 0)) { - if (msgOpts.retry.delay == null) { - if (msgOpts.errorCount < 4) { - msgOpts.retry.delay = 10; - } else { - msgOpts.retry.delay = 5 * 60; - } - } - if (msgOpts.hideAfter) { - if ((_ref9 = msgOpts._hideAfter) == null) { - msgOpts._hideAfter = msgOpts.hideAfter; - } - msgOpts.hideAfter = msgOpts._hideAfter + msgOpts.retry.delay; - } - msgOpts._retryActions = true; - msgOpts.actions = { - retry: { - label: 'retry now', - phrase: 'Retrying TIME', - auto: msgOpts.retry.auto, - delay: msgOpts.retry.delay, - action: function() { - msgOpts.messageInstance = msg; - return setTimeout(function() { - return _this["do"].apply(_this, [msgOpts, opts].concat(__slice.call(args))); - }, 0); - } - }, - cancel: { - action: function() { - return msg.cancel(); - } - } - }; - } else if (msgOpts._retryActions) { - delete msgOpts.actions.retry; - delete msgOpts.actions.cancel; - delete m_opts._retryActions; - } - msg.update(msgOpts); - if (responseOpts && msgOpts.message) { - Messenger(_.extend({}, _this.options, { - instance: _this - })); - return msg.show(); - } else { - return msg.hide(); - } - }; - }); - if (!m_opts.returnsPromise) { - for (type in handlers) { - handler = handlers[type]; - old = opts[type]; - opts[type] = handler; - } - } - msg._actionInstance = m_opts.action.apply(m_opts, [opts].concat(__slice.call(args))); - if (m_opts.returnsPromise) { - msg._actionInstance.then(handlers.success, handlers.error); - } - return msg; - }; - - ActionMessenger.prototype["do"] = ActionMessenger.prototype.run; - - ActionMessenger.prototype.ajax = function() { - var args, m_opts; - m_opts = arguments[0], args = 2 <= arguments.length ? __slice.call(arguments, 1) : []; - m_opts.action = $.ajax; - return this.run.apply(this, [m_opts].concat(__slice.call(args))); - }; - - ActionMessenger.prototype.expectPromise = function(action, m_opts) { - m_opts = _.extend({}, m_opts, { - action: action, - returnsPromise: true - }); - return this.run(m_opts); - }; - - ActionMessenger.prototype.error = function(m_opts) { - if (m_opts == null) { - m_opts = {}; - } - if (typeof m_opts === 'string') { - m_opts = { - message: m_opts - }; - } - m_opts.type = 'error'; - return this.post(m_opts); - }; - - ActionMessenger.prototype.info = function(m_opts) { - if (m_opts == null) { - m_opts = {}; - } - if (typeof m_opts === 'string') { - m_opts = { - message: m_opts - }; - } - m_opts.type = 'info'; - return this.post(m_opts); - }; - - ActionMessenger.prototype.success = function(m_opts) { - if (m_opts == null) { - m_opts = {}; - } - if (typeof m_opts === 'string') { - m_opts = { - message: m_opts - }; - } - m_opts.type = 'success'; - return this.post(m_opts); - }; - - return ActionMessenger; - - })(_Messenger); - - $.fn.messenger = function() { - var $el, args, func, instance, opts, _ref2, _ref3, _ref4; - func = arguments[0], args = 2 <= arguments.length ? __slice.call(arguments, 1) : []; - if (func == null) { - func = {}; - } - $el = this; - if (!(func != null) || !_.isString(func)) { - opts = func; - if (!($el.data('messenger') != null)) { - _Messenger = (_ref2 = (_ref3 = Messenger.themes[opts.theme]) != null ? _ref3.Messenger : void 0) != null ? _ref2 : ActionMessenger; - $el.data('messenger', instance = new _Messenger($.extend({ - el: $el - }, opts))); - instance.render(); - } - return $el.data('messenger'); - } else { - return (_ref4 = $el.data('messenger'))[func].apply(_ref4, args); - } - }; - - window.Messenger._call = function(opts) { - var $el, $parent, choosen_loc, chosen_loc, classes, defaultOpts, inst, loc, locations, _i, _len; - defaultOpts = { - extraClasses: 'messenger-fixed messenger-on-bottom messenger-on-right', - theme: 'future', - maxMessages: 9, - parentLocations: ['body'] - }; - opts = $.extend(defaultOpts, $._messengerDefaults, Messenger.options, opts); - if (opts.theme != null) { - opts.extraClasses += " messenger-theme-" + opts.theme; - } - inst = opts.instance || Messenger.instance; - if (opts.instance == null) { - locations = opts.parentLocations; - $parent = null; - choosen_loc = null; - for (_i = 0, _len = locations.length; _i < _len; _i++) { - loc = locations[_i]; - $parent = $(loc); - if ($parent.length) { - chosen_loc = loc; - break; - } - } - if (!inst) { - $el = $('<ul>'); - $parent.prepend($el); - inst = $el.messenger(opts); - inst._location = chosen_loc; - Messenger.instance = inst; - } else if (!$(inst._location).is($(chosen_loc))) { - inst.$el.detach(); - $parent.prepend(inst.$el); - } - } - if (inst._addedClasses != null) { - inst.$el.removeClass(inst._addedClasses); - } - inst.$el.addClass(classes = "" + inst.className + " " + opts.extraClasses); - inst._addedClasses = classes; - return inst; - }; - - $.extend(Messenger, { - Message: RetryingMessage, - Messenger: ActionMessenger, - themes: (_ref2 = Messenger.themes) != null ? _ref2 : {} - }); - - $.globalMessenger = window.Messenger = Messenger; - -}).call(this); diff --git a/src/UI/JsLibraries/moment.js b/src/UI/JsLibraries/moment.js deleted file mode 100644 index 275a3c324..000000000 --- a/src/UI/JsLibraries/moment.js +++ /dev/null @@ -1,3111 +0,0 @@ -//! moment.js -//! version : 2.10.3 -//! authors : Tim Wood, Iskren Chernev, Moment.js contributors -//! license : MIT -//! momentjs.com - -(function (global, factory) { - typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : - typeof define === 'function' && define.amd ? define(factory) : - global.moment = factory() -}(this, function () { 'use strict'; - - var hookCallback; - - function utils_hooks__hooks () { - return hookCallback.apply(null, arguments); - } - - // This is done to register the method called with moment() - // without creating circular dependencies. - function setHookCallback (callback) { - hookCallback = callback; - } - - function isArray(input) { - return Object.prototype.toString.call(input) === '[object Array]'; - } - - function isDate(input) { - return input instanceof Date || Object.prototype.toString.call(input) === '[object Date]'; - } - - function map(arr, fn) { - var res = [], i; - for (i = 0; i < arr.length; ++i) { - res.push(fn(arr[i], i)); - } - return res; - } - - function hasOwnProp(a, b) { - return Object.prototype.hasOwnProperty.call(a, b); - } - - function extend(a, b) { - for (var i in b) { - if (hasOwnProp(b, i)) { - a[i] = b[i]; - } - } - - if (hasOwnProp(b, 'toString')) { - a.toString = b.toString; - } - - if (hasOwnProp(b, 'valueOf')) { - a.valueOf = b.valueOf; - } - - return a; - } - - function create_utc__createUTC (input, format, locale, strict) { - return createLocalOrUTC(input, format, locale, strict, true).utc(); - } - - function defaultParsingFlags() { - // We need to deep clone this object. - return { - empty : false, - unusedTokens : [], - unusedInput : [], - overflow : -2, - charsLeftOver : 0, - nullInput : false, - invalidMonth : null, - invalidFormat : false, - userInvalidated : false, - iso : false - }; - } - - function getParsingFlags(m) { - if (m._pf == null) { - m._pf = defaultParsingFlags(); - } - return m._pf; - } - - function valid__isValid(m) { - if (m._isValid == null) { - var flags = getParsingFlags(m); - m._isValid = !isNaN(m._d.getTime()) && - flags.overflow < 0 && - !flags.empty && - !flags.invalidMonth && - !flags.nullInput && - !flags.invalidFormat && - !flags.userInvalidated; - - if (m._strict) { - m._isValid = m._isValid && - flags.charsLeftOver === 0 && - flags.unusedTokens.length === 0 && - flags.bigHour === undefined; - } - } - return m._isValid; - } - - function valid__createInvalid (flags) { - var m = create_utc__createUTC(NaN); - if (flags != null) { - extend(getParsingFlags(m), flags); - } - else { - getParsingFlags(m).userInvalidated = true; - } - - return m; - } - - var momentProperties = utils_hooks__hooks.momentProperties = []; - - function copyConfig(to, from) { - var i, prop, val; - - if (typeof from._isAMomentObject !== 'undefined') { - to._isAMomentObject = from._isAMomentObject; - } - if (typeof from._i !== 'undefined') { - to._i = from._i; - } - if (typeof from._f !== 'undefined') { - to._f = from._f; - } - if (typeof from._l !== 'undefined') { - to._l = from._l; - } - if (typeof from._strict !== 'undefined') { - to._strict = from._strict; - } - if (typeof from._tzm !== 'undefined') { - to._tzm = from._tzm; - } - if (typeof from._isUTC !== 'undefined') { - to._isUTC = from._isUTC; - } - if (typeof from._offset !== 'undefined') { - to._offset = from._offset; - } - if (typeof from._pf !== 'undefined') { - to._pf = getParsingFlags(from); - } - if (typeof from._locale !== 'undefined') { - to._locale = from._locale; - } - - if (momentProperties.length > 0) { - for (i in momentProperties) { - prop = momentProperties[i]; - val = from[prop]; - if (typeof val !== 'undefined') { - to[prop] = val; - } - } - } - - return to; - } - - var updateInProgress = false; - - // Moment prototype object - function Moment(config) { - copyConfig(this, config); - this._d = new Date(+config._d); - // Prevent infinite loop in case updateOffset creates new moment - // objects. - if (updateInProgress === false) { - updateInProgress = true; - utils_hooks__hooks.updateOffset(this); - updateInProgress = false; - } - } - - function isMoment (obj) { - return obj instanceof Moment || (obj != null && obj._isAMomentObject != null); - } - - function toInt(argumentForCoercion) { - var coercedNumber = +argumentForCoercion, - value = 0; - - if (coercedNumber !== 0 && isFinite(coercedNumber)) { - if (coercedNumber >= 0) { - value = Math.floor(coercedNumber); - } else { - value = Math.ceil(coercedNumber); - } - } - - return value; - } - - function compareArrays(array1, array2, dontConvert) { - var len = Math.min(array1.length, array2.length), - lengthDiff = Math.abs(array1.length - array2.length), - diffs = 0, - i; - for (i = 0; i < len; i++) { - if ((dontConvert && array1[i] !== array2[i]) || - (!dontConvert && toInt(array1[i]) !== toInt(array2[i]))) { - diffs++; - } - } - return diffs + lengthDiff; - } - - function Locale() { - } - - var locales = {}; - var globalLocale; - - function normalizeLocale(key) { - return key ? key.toLowerCase().replace('_', '-') : key; - } - - // pick the locale from the array - // try ['en-au', 'en-gb'] as 'en-au', 'en-gb', 'en', as in move through the list trying each - // substring from most specific to least, but move to the next array item if it's a more specific variant than the current root - function chooseLocale(names) { - var i = 0, j, next, locale, split; - - while (i < names.length) { - split = normalizeLocale(names[i]).split('-'); - j = split.length; - next = normalizeLocale(names[i + 1]); - next = next ? next.split('-') : null; - while (j > 0) { - locale = loadLocale(split.slice(0, j).join('-')); - if (locale) { - return locale; - } - if (next && next.length >= j && compareArrays(split, next, true) >= j - 1) { - //the next array item is better than a shallower substring of this one - break; - } - j--; - } - i++; - } - return null; - } - - function loadLocale(name) { - var oldLocale = null; - // TODO: Find a better way to register and load all the locales in Node - if (!locales[name] && typeof module !== 'undefined' && - module && module.exports) { - try { - oldLocale = globalLocale._abbr; - require('./locale/' + name); - // because defineLocale currently also sets the global locale, we - // want to undo that for lazy loaded locales - locale_locales__getSetGlobalLocale(oldLocale); - } catch (e) { } - } - return locales[name]; - } - - // This function will load locale and then set the global locale. If - // no arguments are passed in, it will simply return the current global - // locale key. - function locale_locales__getSetGlobalLocale (key, values) { - var data; - if (key) { - if (typeof values === 'undefined') { - data = locale_locales__getLocale(key); - } - else { - data = defineLocale(key, values); - } - - if (data) { - // moment.duration._locale = moment._locale = data; - globalLocale = data; - } - } - - return globalLocale._abbr; - } - - function defineLocale (name, values) { - if (values !== null) { - values.abbr = name; - if (!locales[name]) { - locales[name] = new Locale(); - } - locales[name].set(values); - - // backwards compat for now: also set the locale - locale_locales__getSetGlobalLocale(name); - - return locales[name]; - } else { - // useful for testing - delete locales[name]; - return null; - } - } - - // returns locale data - function locale_locales__getLocale (key) { - var locale; - - if (key && key._locale && key._locale._abbr) { - key = key._locale._abbr; - } - - if (!key) { - return globalLocale; - } - - if (!isArray(key)) { - //short-circuit everything else - locale = loadLocale(key); - if (locale) { - return locale; - } - key = [key]; - } - - return chooseLocale(key); - } - - var aliases = {}; - - function addUnitAlias (unit, shorthand) { - var lowerCase = unit.toLowerCase(); - aliases[lowerCase] = aliases[lowerCase + 's'] = aliases[shorthand] = unit; - } - - function normalizeUnits(units) { - return typeof units === 'string' ? aliases[units] || aliases[units.toLowerCase()] : undefined; - } - - function normalizeObjectUnits(inputObject) { - var normalizedInput = {}, - normalizedProp, - prop; - - for (prop in inputObject) { - if (hasOwnProp(inputObject, prop)) { - normalizedProp = normalizeUnits(prop); - if (normalizedProp) { - normalizedInput[normalizedProp] = inputObject[prop]; - } - } - } - - return normalizedInput; - } - - function makeGetSet (unit, keepTime) { - return function (value) { - if (value != null) { - get_set__set(this, unit, value); - utils_hooks__hooks.updateOffset(this, keepTime); - return this; - } else { - return get_set__get(this, unit); - } - }; - } - - function get_set__get (mom, unit) { - return mom._d['get' + (mom._isUTC ? 'UTC' : '') + unit](); - } - - function get_set__set (mom, unit, value) { - return mom._d['set' + (mom._isUTC ? 'UTC' : '') + unit](value); - } - - // MOMENTS - - function getSet (units, value) { - var unit; - if (typeof units === 'object') { - for (unit in units) { - this.set(unit, units[unit]); - } - } else { - units = normalizeUnits(units); - if (typeof this[units] === 'function') { - return this[units](value); - } - } - return this; - } - - function zeroFill(number, targetLength, forceSign) { - var output = '' + Math.abs(number), - sign = number >= 0; - - while (output.length < targetLength) { - output = '0' + output; - } - return (sign ? (forceSign ? '+' : '') : '-') + output; - } - - var formattingTokens = /(\[[^\[]*\])|(\\)?(Mo|MM?M?M?|Do|DDDo|DD?D?D?|ddd?d?|do?|w[o|w]?|W[o|W]?|Q|YYYYYY|YYYYY|YYYY|YY|gg(ggg?)?|GG(GGG?)?|e|E|a|A|hh?|HH?|mm?|ss?|S{1,4}|x|X|zz?|ZZ?|.)/g; - - var localFormattingTokens = /(\[[^\[]*\])|(\\)?(LTS|LT|LL?L?L?|l{1,4})/g; - - var formatFunctions = {}; - - var formatTokenFunctions = {}; - - // token: 'M' - // padded: ['MM', 2] - // ordinal: 'Mo' - // callback: function () { this.month() + 1 } - function addFormatToken (token, padded, ordinal, callback) { - var func = callback; - if (typeof callback === 'string') { - func = function () { - return this[callback](); - }; - } - if (token) { - formatTokenFunctions[token] = func; - } - if (padded) { - formatTokenFunctions[padded[0]] = function () { - return zeroFill(func.apply(this, arguments), padded[1], padded[2]); - }; - } - if (ordinal) { - formatTokenFunctions[ordinal] = function () { - return this.localeData().ordinal(func.apply(this, arguments), token); - }; - } - } - - function removeFormattingTokens(input) { - if (input.match(/\[[\s\S]/)) { - return input.replace(/^\[|\]$/g, ''); - } - return input.replace(/\\/g, ''); - } - - function makeFormatFunction(format) { - var array = format.match(formattingTokens), i, length; - - for (i = 0, length = array.length; i < length; i++) { - if (formatTokenFunctions[array[i]]) { - array[i] = formatTokenFunctions[array[i]]; - } else { - array[i] = removeFormattingTokens(array[i]); - } - } - - return function (mom) { - var output = ''; - for (i = 0; i < length; i++) { - output += array[i] instanceof Function ? array[i].call(mom, format) : array[i]; - } - return output; - }; - } - - // format date using native date object - function formatMoment(m, format) { - if (!m.isValid()) { - return m.localeData().invalidDate(); - } - - format = expandFormat(format, m.localeData()); - - if (!formatFunctions[format]) { - formatFunctions[format] = makeFormatFunction(format); - } - - return formatFunctions[format](m); - } - - function expandFormat(format, locale) { - var i = 5; - - function replaceLongDateFormatTokens(input) { - return locale.longDateFormat(input) || input; - } - - localFormattingTokens.lastIndex = 0; - while (i >= 0 && localFormattingTokens.test(format)) { - format = format.replace(localFormattingTokens, replaceLongDateFormatTokens); - localFormattingTokens.lastIndex = 0; - i -= 1; - } - - return format; - } - - var match1 = /\d/; // 0 - 9 - var match2 = /\d\d/; // 00 - 99 - var match3 = /\d{3}/; // 000 - 999 - var match4 = /\d{4}/; // 0000 - 9999 - var match6 = /[+-]?\d{6}/; // -999999 - 999999 - var match1to2 = /\d\d?/; // 0 - 99 - var match1to3 = /\d{1,3}/; // 0 - 999 - var match1to4 = /\d{1,4}/; // 0 - 9999 - var match1to6 = /[+-]?\d{1,6}/; // -999999 - 999999 - - var matchUnsigned = /\d+/; // 0 - inf - var matchSigned = /[+-]?\d+/; // -inf - inf - - var matchOffset = /Z|[+-]\d\d:?\d\d/gi; // +00:00 -00:00 +0000 -0000 or Z - - var matchTimestamp = /[+-]?\d+(\.\d{1,3})?/; // 123456789 123456789.123 - - // any word (or two) characters or numbers including two/three word month in arabic. - var matchWord = /[0-9]*['a-z\u00A0-\u05FF\u0700-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]+|[\u0600-\u06FF\/]+(\s*?[\u0600-\u06FF]+){1,2}/i; - - var regexes = {}; - - function addRegexToken (token, regex, strictRegex) { - regexes[token] = typeof regex === 'function' ? regex : function (isStrict) { - return (isStrict && strictRegex) ? strictRegex : regex; - }; - } - - function getParseRegexForToken (token, config) { - if (!hasOwnProp(regexes, token)) { - return new RegExp(unescapeFormat(token)); - } - - return regexes[token](config._strict, config._locale); - } - - // Code from http://stackoverflow.com/questions/3561493/is-there-a-regexp-escape-function-in-javascript - function unescapeFormat(s) { - return s.replace('\\', '').replace(/\\(\[)|\\(\])|\[([^\]\[]*)\]|\\(.)/g, function (matched, p1, p2, p3, p4) { - return p1 || p2 || p3 || p4; - }).replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'); - } - - var tokens = {}; - - function addParseToken (token, callback) { - var i, func = callback; - if (typeof token === 'string') { - token = [token]; - } - if (typeof callback === 'number') { - func = function (input, array) { - array[callback] = toInt(input); - }; - } - for (i = 0; i < token.length; i++) { - tokens[token[i]] = func; - } - } - - function addWeekParseToken (token, callback) { - addParseToken(token, function (input, array, config, token) { - config._w = config._w || {}; - callback(input, config._w, config, token); - }); - } - - function addTimeToArrayFromToken(token, input, config) { - if (input != null && hasOwnProp(tokens, token)) { - tokens[token](input, config._a, config, token); - } - } - - var YEAR = 0; - var MONTH = 1; - var DATE = 2; - var HOUR = 3; - var MINUTE = 4; - var SECOND = 5; - var MILLISECOND = 6; - - function daysInMonth(year, month) { - return new Date(Date.UTC(year, month + 1, 0)).getUTCDate(); - } - - // FORMATTING - - addFormatToken('M', ['MM', 2], 'Mo', function () { - return this.month() + 1; - }); - - addFormatToken('MMM', 0, 0, function (format) { - return this.localeData().monthsShort(this, format); - }); - - addFormatToken('MMMM', 0, 0, function (format) { - return this.localeData().months(this, format); - }); - - // ALIASES - - addUnitAlias('month', 'M'); - - // PARSING - - addRegexToken('M', match1to2); - addRegexToken('MM', match1to2, match2); - addRegexToken('MMM', matchWord); - addRegexToken('MMMM', matchWord); - - addParseToken(['M', 'MM'], function (input, array) { - array[MONTH] = toInt(input) - 1; - }); - - addParseToken(['MMM', 'MMMM'], function (input, array, config, token) { - var month = config._locale.monthsParse(input, token, config._strict); - // if we didn't find a month name, mark the date as invalid. - if (month != null) { - array[MONTH] = month; - } else { - getParsingFlags(config).invalidMonth = input; - } - }); - - // LOCALES - - var defaultLocaleMonths = 'January_February_March_April_May_June_July_August_September_October_November_December'.split('_'); - function localeMonths (m) { - return this._months[m.month()]; - } - - var defaultLocaleMonthsShort = 'Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec'.split('_'); - function localeMonthsShort (m) { - return this._monthsShort[m.month()]; - } - - function localeMonthsParse (monthName, format, strict) { - var i, mom, regex; - - if (!this._monthsParse) { - this._monthsParse = []; - this._longMonthsParse = []; - this._shortMonthsParse = []; - } - - for (i = 0; i < 12; i++) { - // make the regex if we don't have it already - mom = create_utc__createUTC([2000, i]); - if (strict && !this._longMonthsParse[i]) { - this._longMonthsParse[i] = new RegExp('^' + this.months(mom, '').replace('.', '') + '$', 'i'); - this._shortMonthsParse[i] = new RegExp('^' + this.monthsShort(mom, '').replace('.', '') + '$', 'i'); - } - if (!strict && !this._monthsParse[i]) { - regex = '^' + this.months(mom, '') + '|^' + this.monthsShort(mom, ''); - this._monthsParse[i] = new RegExp(regex.replace('.', ''), 'i'); - } - // test the regex - if (strict && format === 'MMMM' && this._longMonthsParse[i].test(monthName)) { - return i; - } else if (strict && format === 'MMM' && this._shortMonthsParse[i].test(monthName)) { - return i; - } else if (!strict && this._monthsParse[i].test(monthName)) { - return i; - } - } - } - - // MOMENTS - - function setMonth (mom, value) { - var dayOfMonth; - - // TODO: Move this out of here! - if (typeof value === 'string') { - value = mom.localeData().monthsParse(value); - // TODO: Another silent failure? - if (typeof value !== 'number') { - return mom; - } - } - - dayOfMonth = Math.min(mom.date(), daysInMonth(mom.year(), value)); - mom._d['set' + (mom._isUTC ? 'UTC' : '') + 'Month'](value, dayOfMonth); - return mom; - } - - function getSetMonth (value) { - if (value != null) { - setMonth(this, value); - utils_hooks__hooks.updateOffset(this, true); - return this; - } else { - return get_set__get(this, 'Month'); - } - } - - function getDaysInMonth () { - return daysInMonth(this.year(), this.month()); - } - - function checkOverflow (m) { - var overflow; - var a = m._a; - - if (a && getParsingFlags(m).overflow === -2) { - overflow = - a[MONTH] < 0 || a[MONTH] > 11 ? MONTH : - a[DATE] < 1 || a[DATE] > daysInMonth(a[YEAR], a[MONTH]) ? DATE : - a[HOUR] < 0 || a[HOUR] > 24 || (a[HOUR] === 24 && (a[MINUTE] !== 0 || a[SECOND] !== 0 || a[MILLISECOND] !== 0)) ? HOUR : - a[MINUTE] < 0 || a[MINUTE] > 59 ? MINUTE : - a[SECOND] < 0 || a[SECOND] > 59 ? SECOND : - a[MILLISECOND] < 0 || a[MILLISECOND] > 999 ? MILLISECOND : - -1; - - if (getParsingFlags(m)._overflowDayOfYear && (overflow < YEAR || overflow > DATE)) { - overflow = DATE; - } - - getParsingFlags(m).overflow = overflow; - } - - return m; - } - - function warn(msg) { - if (utils_hooks__hooks.suppressDeprecationWarnings === false && typeof console !== 'undefined' && console.warn) { - console.warn('Deprecation warning: ' + msg); - } - } - - function deprecate(msg, fn) { - var firstTime = true, - msgWithStack = msg + '\n' + (new Error()).stack; - - return extend(function () { - if (firstTime) { - warn(msgWithStack); - firstTime = false; - } - return fn.apply(this, arguments); - }, fn); - } - - var deprecations = {}; - - function deprecateSimple(name, msg) { - if (!deprecations[name]) { - warn(msg); - deprecations[name] = true; - } - } - - utils_hooks__hooks.suppressDeprecationWarnings = false; - - var from_string__isoRegex = /^\s*(?:[+-]\d{6}|\d{4})-(?:(\d\d-\d\d)|(W\d\d$)|(W\d\d-\d)|(\d\d\d))((T| )(\d\d(:\d\d(:\d\d(\.\d+)?)?)?)?([\+\-]\d\d(?::?\d\d)?|\s*Z)?)?$/; - - var isoDates = [ - ['YYYYYY-MM-DD', /[+-]\d{6}-\d{2}-\d{2}/], - ['YYYY-MM-DD', /\d{4}-\d{2}-\d{2}/], - ['GGGG-[W]WW-E', /\d{4}-W\d{2}-\d/], - ['GGGG-[W]WW', /\d{4}-W\d{2}/], - ['YYYY-DDD', /\d{4}-\d{3}/] - ]; - - // iso time formats and regexes - var isoTimes = [ - ['HH:mm:ss.SSSS', /(T| )\d\d:\d\d:\d\d\.\d+/], - ['HH:mm:ss', /(T| )\d\d:\d\d:\d\d/], - ['HH:mm', /(T| )\d\d:\d\d/], - ['HH', /(T| )\d\d/] - ]; - - var aspNetJsonRegex = /^\/?Date\((\-?\d+)/i; - - // date from iso format - function configFromISO(config) { - var i, l, - string = config._i, - match = from_string__isoRegex.exec(string); - - if (match) { - getParsingFlags(config).iso = true; - for (i = 0, l = isoDates.length; i < l; i++) { - if (isoDates[i][1].exec(string)) { - // match[5] should be 'T' or undefined - config._f = isoDates[i][0] + (match[6] || ' '); - break; - } - } - for (i = 0, l = isoTimes.length; i < l; i++) { - if (isoTimes[i][1].exec(string)) { - config._f += isoTimes[i][0]; - break; - } - } - if (string.match(matchOffset)) { - config._f += 'Z'; - } - configFromStringAndFormat(config); - } else { - config._isValid = false; - } - } - - // date from iso format or fallback - function configFromString(config) { - var matched = aspNetJsonRegex.exec(config._i); - - if (matched !== null) { - config._d = new Date(+matched[1]); - return; - } - - configFromISO(config); - if (config._isValid === false) { - delete config._isValid; - utils_hooks__hooks.createFromInputFallback(config); - } - } - - utils_hooks__hooks.createFromInputFallback = deprecate( - 'moment construction falls back to js Date. This is ' + - 'discouraged and will be removed in upcoming major ' + - 'release. Please refer to ' + - 'https://github.com/moment/moment/issues/1407 for more info.', - function (config) { - config._d = new Date(config._i + (config._useUTC ? ' UTC' : '')); - } - ); - - function createDate (y, m, d, h, M, s, ms) { - //can't just apply() to create a date: - //http://stackoverflow.com/questions/181348/instantiating-a-javascript-object-by-calling-prototype-constructor-apply - var date = new Date(y, m, d, h, M, s, ms); - - //the date constructor doesn't accept years < 1970 - if (y < 1970) { - date.setFullYear(y); - } - return date; - } - - function createUTCDate (y) { - var date = new Date(Date.UTC.apply(null, arguments)); - if (y < 1970) { - date.setUTCFullYear(y); - } - return date; - } - - addFormatToken(0, ['YY', 2], 0, function () { - return this.year() % 100; - }); - - addFormatToken(0, ['YYYY', 4], 0, 'year'); - addFormatToken(0, ['YYYYY', 5], 0, 'year'); - addFormatToken(0, ['YYYYYY', 6, true], 0, 'year'); - - // ALIASES - - addUnitAlias('year', 'y'); - - // PARSING - - addRegexToken('Y', matchSigned); - addRegexToken('YY', match1to2, match2); - addRegexToken('YYYY', match1to4, match4); - addRegexToken('YYYYY', match1to6, match6); - addRegexToken('YYYYYY', match1to6, match6); - - addParseToken(['YYYY', 'YYYYY', 'YYYYYY'], YEAR); - addParseToken('YY', function (input, array) { - array[YEAR] = utils_hooks__hooks.parseTwoDigitYear(input); - }); - - // HELPERS - - function daysInYear(year) { - return isLeapYear(year) ? 366 : 365; - } - - function isLeapYear(year) { - return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0; - } - - // HOOKS - - utils_hooks__hooks.parseTwoDigitYear = function (input) { - return toInt(input) + (toInt(input) > 68 ? 1900 : 2000); - }; - - // MOMENTS - - var getSetYear = makeGetSet('FullYear', false); - - function getIsLeapYear () { - return isLeapYear(this.year()); - } - - addFormatToken('w', ['ww', 2], 'wo', 'week'); - addFormatToken('W', ['WW', 2], 'Wo', 'isoWeek'); - - // ALIASES - - addUnitAlias('week', 'w'); - addUnitAlias('isoWeek', 'W'); - - // PARSING - - addRegexToken('w', match1to2); - addRegexToken('ww', match1to2, match2); - addRegexToken('W', match1to2); - addRegexToken('WW', match1to2, match2); - - addWeekParseToken(['w', 'ww', 'W', 'WW'], function (input, week, config, token) { - week[token.substr(0, 1)] = toInt(input); - }); - - // HELPERS - - // firstDayOfWeek 0 = sun, 6 = sat - // the day of the week that starts the week - // (usually sunday or monday) - // firstDayOfWeekOfYear 0 = sun, 6 = sat - // the first week is the week that contains the first - // of this day of the week - // (eg. ISO weeks use thursday (4)) - function weekOfYear(mom, firstDayOfWeek, firstDayOfWeekOfYear) { - var end = firstDayOfWeekOfYear - firstDayOfWeek, - daysToDayOfWeek = firstDayOfWeekOfYear - mom.day(), - adjustedMoment; - - - if (daysToDayOfWeek > end) { - daysToDayOfWeek -= 7; - } - - if (daysToDayOfWeek < end - 7) { - daysToDayOfWeek += 7; - } - - adjustedMoment = local__createLocal(mom).add(daysToDayOfWeek, 'd'); - return { - week: Math.ceil(adjustedMoment.dayOfYear() / 7), - year: adjustedMoment.year() - }; - } - - // LOCALES - - function localeWeek (mom) { - return weekOfYear(mom, this._week.dow, this._week.doy).week; - } - - var defaultLocaleWeek = { - dow : 0, // Sunday is the first day of the week. - doy : 6 // The week that contains Jan 1st is the first week of the year. - }; - - function localeFirstDayOfWeek () { - return this._week.dow; - } - - function localeFirstDayOfYear () { - return this._week.doy; - } - - // MOMENTS - - function getSetWeek (input) { - var week = this.localeData().week(this); - return input == null ? week : this.add((input - week) * 7, 'd'); - } - - function getSetISOWeek (input) { - var week = weekOfYear(this, 1, 4).week; - return input == null ? week : this.add((input - week) * 7, 'd'); - } - - addFormatToken('DDD', ['DDDD', 3], 'DDDo', 'dayOfYear'); - - // ALIASES - - addUnitAlias('dayOfYear', 'DDD'); - - // PARSING - - addRegexToken('DDD', match1to3); - addRegexToken('DDDD', match3); - addParseToken(['DDD', 'DDDD'], function (input, array, config) { - config._dayOfYear = toInt(input); - }); - - // HELPERS - - //http://en.wikipedia.org/wiki/ISO_week_date#Calculating_a_date_given_the_year.2C_week_number_and_weekday - function dayOfYearFromWeeks(year, week, weekday, firstDayOfWeekOfYear, firstDayOfWeek) { - var d = createUTCDate(year, 0, 1).getUTCDay(); - var daysToAdd; - var dayOfYear; - - d = d === 0 ? 7 : d; - weekday = weekday != null ? weekday : firstDayOfWeek; - daysToAdd = firstDayOfWeek - d + (d > firstDayOfWeekOfYear ? 7 : 0) - (d < firstDayOfWeek ? 7 : 0); - dayOfYear = 7 * (week - 1) + (weekday - firstDayOfWeek) + daysToAdd + 1; - - return { - year : dayOfYear > 0 ? year : year - 1, - dayOfYear : dayOfYear > 0 ? dayOfYear : daysInYear(year - 1) + dayOfYear - }; - } - - // MOMENTS - - function getSetDayOfYear (input) { - var dayOfYear = Math.round((this.clone().startOf('day') - this.clone().startOf('year')) / 864e5) + 1; - return input == null ? dayOfYear : this.add((input - dayOfYear), 'd'); - } - - // Pick the first defined of two or three arguments. - function defaults(a, b, c) { - if (a != null) { - return a; - } - if (b != null) { - return b; - } - return c; - } - - function currentDateArray(config) { - var now = new Date(); - if (config._useUTC) { - return [now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate()]; - } - return [now.getFullYear(), now.getMonth(), now.getDate()]; - } - - // convert an array to a date. - // the array should mirror the parameters below - // note: all values past the year are optional and will default to the lowest possible value. - // [year, month, day , hour, minute, second, millisecond] - function configFromArray (config) { - var i, date, input = [], currentDate, yearToUse; - - if (config._d) { - return; - } - - currentDate = currentDateArray(config); - - //compute day of the year from weeks and weekdays - if (config._w && config._a[DATE] == null && config._a[MONTH] == null) { - dayOfYearFromWeekInfo(config); - } - - //if the day of the year is set, figure out what it is - if (config._dayOfYear) { - yearToUse = defaults(config._a[YEAR], currentDate[YEAR]); - - if (config._dayOfYear > daysInYear(yearToUse)) { - getParsingFlags(config)._overflowDayOfYear = true; - } - - date = createUTCDate(yearToUse, 0, config._dayOfYear); - config._a[MONTH] = date.getUTCMonth(); - config._a[DATE] = date.getUTCDate(); - } - - // Default to current date. - // * if no year, month, day of month are given, default to today - // * if day of month is given, default month and year - // * if month is given, default only year - // * if year is given, don't default anything - for (i = 0; i < 3 && config._a[i] == null; ++i) { - config._a[i] = input[i] = currentDate[i]; - } - - // Zero out whatever was not defaulted, including time - for (; i < 7; i++) { - config._a[i] = input[i] = (config._a[i] == null) ? (i === 2 ? 1 : 0) : config._a[i]; - } - - // Check for 24:00:00.000 - if (config._a[HOUR] === 24 && - config._a[MINUTE] === 0 && - config._a[SECOND] === 0 && - config._a[MILLISECOND] === 0) { - config._nextDay = true; - config._a[HOUR] = 0; - } - - config._d = (config._useUTC ? createUTCDate : createDate).apply(null, input); - // Apply timezone offset from input. The actual utcOffset can be changed - // with parseZone. - if (config._tzm != null) { - config._d.setUTCMinutes(config._d.getUTCMinutes() - config._tzm); - } - - if (config._nextDay) { - config._a[HOUR] = 24; - } - } - - function dayOfYearFromWeekInfo(config) { - var w, weekYear, week, weekday, dow, doy, temp; - - w = config._w; - if (w.GG != null || w.W != null || w.E != null) { - dow = 1; - doy = 4; - - // TODO: We need to take the current isoWeekYear, but that depends on - // how we interpret now (local, utc, fixed offset). So create - // a now version of current config (take local/utc/offset flags, and - // create now). - weekYear = defaults(w.GG, config._a[YEAR], weekOfYear(local__createLocal(), 1, 4).year); - week = defaults(w.W, 1); - weekday = defaults(w.E, 1); - } else { - dow = config._locale._week.dow; - doy = config._locale._week.doy; - - weekYear = defaults(w.gg, config._a[YEAR], weekOfYear(local__createLocal(), dow, doy).year); - week = defaults(w.w, 1); - - if (w.d != null) { - // weekday -- low day numbers are considered next week - weekday = w.d; - if (weekday < dow) { - ++week; - } - } else if (w.e != null) { - // local weekday -- counting starts from begining of week - weekday = w.e + dow; - } else { - // default to begining of week - weekday = dow; - } - } - temp = dayOfYearFromWeeks(weekYear, week, weekday, doy, dow); - - config._a[YEAR] = temp.year; - config._dayOfYear = temp.dayOfYear; - } - - utils_hooks__hooks.ISO_8601 = function () {}; - - // date from string and format string - function configFromStringAndFormat(config) { - // TODO: Move this to another part of the creation flow to prevent circular deps - if (config._f === utils_hooks__hooks.ISO_8601) { - configFromISO(config); - return; - } - - config._a = []; - getParsingFlags(config).empty = true; - - // This array is used to make a Date, either with `new Date` or `Date.UTC` - var string = '' + config._i, - i, parsedInput, tokens, token, skipped, - stringLength = string.length, - totalParsedInputLength = 0; - - tokens = expandFormat(config._f, config._locale).match(formattingTokens) || []; - - for (i = 0; i < tokens.length; i++) { - token = tokens[i]; - parsedInput = (string.match(getParseRegexForToken(token, config)) || [])[0]; - if (parsedInput) { - skipped = string.substr(0, string.indexOf(parsedInput)); - if (skipped.length > 0) { - getParsingFlags(config).unusedInput.push(skipped); - } - string = string.slice(string.indexOf(parsedInput) + parsedInput.length); - totalParsedInputLength += parsedInput.length; - } - // don't parse if it's not a known token - if (formatTokenFunctions[token]) { - if (parsedInput) { - getParsingFlags(config).empty = false; - } - else { - getParsingFlags(config).unusedTokens.push(token); - } - addTimeToArrayFromToken(token, parsedInput, config); - } - else if (config._strict && !parsedInput) { - getParsingFlags(config).unusedTokens.push(token); - } - } - - // add remaining unparsed input length to the string - getParsingFlags(config).charsLeftOver = stringLength - totalParsedInputLength; - if (string.length > 0) { - getParsingFlags(config).unusedInput.push(string); - } - - // clear _12h flag if hour is <= 12 - if (getParsingFlags(config).bigHour === true && - config._a[HOUR] <= 12 && - config._a[HOUR] > 0) { - getParsingFlags(config).bigHour = undefined; - } - // handle meridiem - config._a[HOUR] = meridiemFixWrap(config._locale, config._a[HOUR], config._meridiem); - - configFromArray(config); - checkOverflow(config); - } - - - function meridiemFixWrap (locale, hour, meridiem) { - var isPm; - - if (meridiem == null) { - // nothing to do - return hour; - } - if (locale.meridiemHour != null) { - return locale.meridiemHour(hour, meridiem); - } else if (locale.isPM != null) { - // Fallback - isPm = locale.isPM(meridiem); - if (isPm && hour < 12) { - hour += 12; - } - if (!isPm && hour === 12) { - hour = 0; - } - return hour; - } else { - // this is not supposed to happen - return hour; - } - } - - function configFromStringAndArray(config) { - var tempConfig, - bestMoment, - - scoreToBeat, - i, - currentScore; - - if (config._f.length === 0) { - getParsingFlags(config).invalidFormat = true; - config._d = new Date(NaN); - return; - } - - for (i = 0; i < config._f.length; i++) { - currentScore = 0; - tempConfig = copyConfig({}, config); - if (config._useUTC != null) { - tempConfig._useUTC = config._useUTC; - } - tempConfig._f = config._f[i]; - configFromStringAndFormat(tempConfig); - - if (!valid__isValid(tempConfig)) { - continue; - } - - // if there is any input that was not parsed add a penalty for that format - currentScore += getParsingFlags(tempConfig).charsLeftOver; - - //or tokens - currentScore += getParsingFlags(tempConfig).unusedTokens.length * 10; - - getParsingFlags(tempConfig).score = currentScore; - - if (scoreToBeat == null || currentScore < scoreToBeat) { - scoreToBeat = currentScore; - bestMoment = tempConfig; - } - } - - extend(config, bestMoment || tempConfig); - } - - function configFromObject(config) { - if (config._d) { - return; - } - - var i = normalizeObjectUnits(config._i); - config._a = [i.year, i.month, i.day || i.date, i.hour, i.minute, i.second, i.millisecond]; - - configFromArray(config); - } - - function createFromConfig (config) { - var input = config._i, - format = config._f, - res; - - config._locale = config._locale || locale_locales__getLocale(config._l); - - if (input === null || (format === undefined && input === '')) { - return valid__createInvalid({nullInput: true}); - } - - if (typeof input === 'string') { - config._i = input = config._locale.preparse(input); - } - - if (isMoment(input)) { - return new Moment(checkOverflow(input)); - } else if (isArray(format)) { - configFromStringAndArray(config); - } else if (format) { - configFromStringAndFormat(config); - } else if (isDate(input)) { - config._d = input; - } else { - configFromInput(config); - } - - res = new Moment(checkOverflow(config)); - if (res._nextDay) { - // Adding is smart enough around DST - res.add(1, 'd'); - res._nextDay = undefined; - } - - return res; - } - - function configFromInput(config) { - var input = config._i; - if (input === undefined) { - config._d = new Date(); - } else if (isDate(input)) { - config._d = new Date(+input); - } else if (typeof input === 'string') { - configFromString(config); - } else if (isArray(input)) { - config._a = map(input.slice(0), function (obj) { - return parseInt(obj, 10); - }); - configFromArray(config); - } else if (typeof(input) === 'object') { - configFromObject(config); - } else if (typeof(input) === 'number') { - // from milliseconds - config._d = new Date(input); - } else { - utils_hooks__hooks.createFromInputFallback(config); - } - } - - function createLocalOrUTC (input, format, locale, strict, isUTC) { - var c = {}; - - if (typeof(locale) === 'boolean') { - strict = locale; - locale = undefined; - } - // object construction must be done this way. - // https://github.com/moment/moment/issues/1423 - c._isAMomentObject = true; - c._useUTC = c._isUTC = isUTC; - c._l = locale; - c._i = input; - c._f = format; - c._strict = strict; - - return createFromConfig(c); - } - - function local__createLocal (input, format, locale, strict) { - return createLocalOrUTC(input, format, locale, strict, false); - } - - var prototypeMin = deprecate( - 'moment().min is deprecated, use moment.min instead. https://github.com/moment/moment/issues/1548', - function () { - var other = local__createLocal.apply(null, arguments); - return other < this ? this : other; - } - ); - - var prototypeMax = deprecate( - 'moment().max is deprecated, use moment.max instead. https://github.com/moment/moment/issues/1548', - function () { - var other = local__createLocal.apply(null, arguments); - return other > this ? this : other; - } - ); - - // Pick a moment m from moments so that m[fn](other) is true for all - // other. This relies on the function fn to be transitive. - // - // moments should either be an array of moment objects or an array, whose - // first element is an array of moment objects. - function pickBy(fn, moments) { - var res, i; - if (moments.length === 1 && isArray(moments[0])) { - moments = moments[0]; - } - if (!moments.length) { - return local__createLocal(); - } - res = moments[0]; - for (i = 1; i < moments.length; ++i) { - if (moments[i][fn](res)) { - res = moments[i]; - } - } - return res; - } - - // TODO: Use [].sort instead? - function min () { - var args = [].slice.call(arguments, 0); - - return pickBy('isBefore', args); - } - - function max () { - var args = [].slice.call(arguments, 0); - - return pickBy('isAfter', args); - } - - function Duration (duration) { - var normalizedInput = normalizeObjectUnits(duration), - years = normalizedInput.year || 0, - quarters = normalizedInput.quarter || 0, - months = normalizedInput.month || 0, - weeks = normalizedInput.week || 0, - days = normalizedInput.day || 0, - hours = normalizedInput.hour || 0, - minutes = normalizedInput.minute || 0, - seconds = normalizedInput.second || 0, - milliseconds = normalizedInput.millisecond || 0; - - // representation for dateAddRemove - this._milliseconds = +milliseconds + - seconds * 1e3 + // 1000 - minutes * 6e4 + // 1000 * 60 - hours * 36e5; // 1000 * 60 * 60 - // Because of dateAddRemove treats 24 hours as different from a - // day when working around DST, we need to store them separately - this._days = +days + - weeks * 7; - // It is impossible translate months into days without knowing - // which months you are are talking about, so we have to store - // it separately. - this._months = +months + - quarters * 3 + - years * 12; - - this._data = {}; - - this._locale = locale_locales__getLocale(); - - this._bubble(); - } - - function isDuration (obj) { - return obj instanceof Duration; - } - - function offset (token, separator) { - addFormatToken(token, 0, 0, function () { - var offset = this.utcOffset(); - var sign = '+'; - if (offset < 0) { - offset = -offset; - sign = '-'; - } - return sign + zeroFill(~~(offset / 60), 2) + separator + zeroFill(~~(offset) % 60, 2); - }); - } - - offset('Z', ':'); - offset('ZZ', ''); - - // PARSING - - addRegexToken('Z', matchOffset); - addRegexToken('ZZ', matchOffset); - addParseToken(['Z', 'ZZ'], function (input, array, config) { - config._useUTC = true; - config._tzm = offsetFromString(input); - }); - - // HELPERS - - // timezone chunker - // '+10:00' > ['10', '00'] - // '-1530' > ['-15', '30'] - var chunkOffset = /([\+\-]|\d\d)/gi; - - function offsetFromString(string) { - var matches = ((string || '').match(matchOffset) || []); - var chunk = matches[matches.length - 1] || []; - var parts = (chunk + '').match(chunkOffset) || ['-', 0, 0]; - var minutes = +(parts[1] * 60) + toInt(parts[2]); - - return parts[0] === '+' ? minutes : -minutes; - } - - // Return a moment from input, that is local/utc/zone equivalent to model. - function cloneWithOffset(input, model) { - var res, diff; - if (model._isUTC) { - res = model.clone(); - diff = (isMoment(input) || isDate(input) ? +input : +local__createLocal(input)) - (+res); - // Use low-level api, because this fn is low-level api. - res._d.setTime(+res._d + diff); - utils_hooks__hooks.updateOffset(res, false); - return res; - } else { - return local__createLocal(input).local(); - } - return model._isUTC ? local__createLocal(input).zone(model._offset || 0) : local__createLocal(input).local(); - } - - function getDateOffset (m) { - // On Firefox.24 Date#getTimezoneOffset returns a floating point. - // https://github.com/moment/moment/pull/1871 - return -Math.round(m._d.getTimezoneOffset() / 15) * 15; - } - - // HOOKS - - // This function will be called whenever a moment is mutated. - // It is intended to keep the offset in sync with the timezone. - utils_hooks__hooks.updateOffset = function () {}; - - // MOMENTS - - // keepLocalTime = true means only change the timezone, without - // affecting the local hour. So 5:31:26 +0300 --[utcOffset(2, true)]--> - // 5:31:26 +0200 It is possible that 5:31:26 doesn't exist with offset - // +0200, so we adjust the time as needed, to be valid. - // - // Keeping the time actually adds/subtracts (one hour) - // from the actual represented time. That is why we call updateOffset - // a second time. In case it wants us to change the offset again - // _changeInProgress == true case, then we have to adjust, because - // there is no such time in the given timezone. - function getSetOffset (input, keepLocalTime) { - var offset = this._offset || 0, - localAdjust; - if (input != null) { - if (typeof input === 'string') { - input = offsetFromString(input); - } - if (Math.abs(input) < 16) { - input = input * 60; - } - if (!this._isUTC && keepLocalTime) { - localAdjust = getDateOffset(this); - } - this._offset = input; - this._isUTC = true; - if (localAdjust != null) { - this.add(localAdjust, 'm'); - } - if (offset !== input) { - if (!keepLocalTime || this._changeInProgress) { - add_subtract__addSubtract(this, create__createDuration(input - offset, 'm'), 1, false); - } else if (!this._changeInProgress) { - this._changeInProgress = true; - utils_hooks__hooks.updateOffset(this, true); - this._changeInProgress = null; - } - } - return this; - } else { - return this._isUTC ? offset : getDateOffset(this); - } - } - - function getSetZone (input, keepLocalTime) { - if (input != null) { - if (typeof input !== 'string') { - input = -input; - } - - this.utcOffset(input, keepLocalTime); - - return this; - } else { - return -this.utcOffset(); - } - } - - function setOffsetToUTC (keepLocalTime) { - return this.utcOffset(0, keepLocalTime); - } - - function setOffsetToLocal (keepLocalTime) { - if (this._isUTC) { - this.utcOffset(0, keepLocalTime); - this._isUTC = false; - - if (keepLocalTime) { - this.subtract(getDateOffset(this), 'm'); - } - } - return this; - } - - function setOffsetToParsedOffset () { - if (this._tzm) { - this.utcOffset(this._tzm); - } else if (typeof this._i === 'string') { - this.utcOffset(offsetFromString(this._i)); - } - return this; - } - - function hasAlignedHourOffset (input) { - if (!input) { - input = 0; - } - else { - input = local__createLocal(input).utcOffset(); - } - - return (this.utcOffset() - input) % 60 === 0; - } - - function isDaylightSavingTime () { - return ( - this.utcOffset() > this.clone().month(0).utcOffset() || - this.utcOffset() > this.clone().month(5).utcOffset() - ); - } - - function isDaylightSavingTimeShifted () { - if (this._a) { - var other = this._isUTC ? create_utc__createUTC(this._a) : local__createLocal(this._a); - return this.isValid() && compareArrays(this._a, other.toArray()) > 0; - } - - return false; - } - - function isLocal () { - return !this._isUTC; - } - - function isUtcOffset () { - return this._isUTC; - } - - function isUtc () { - return this._isUTC && this._offset === 0; - } - - var aspNetRegex = /(\-)?(?:(\d*)\.)?(\d+)\:(\d+)(?:\:(\d+)\.?(\d{3})?)?/; - - // from http://docs.closure-library.googlecode.com/git/closure_goog_date_date.js.source.html - // somewhat more in line with 4.4.3.2 2004 spec, but allows decimal anywhere - var create__isoRegex = /^(-)?P(?:(?:([0-9,.]*)Y)?(?:([0-9,.]*)M)?(?:([0-9,.]*)D)?(?:T(?:([0-9,.]*)H)?(?:([0-9,.]*)M)?(?:([0-9,.]*)S)?)?|([0-9,.]*)W)$/; - - function create__createDuration (input, key) { - var duration = input, - // matching against regexp is expensive, do it on demand - match = null, - sign, - ret, - diffRes; - - if (isDuration(input)) { - duration = { - ms : input._milliseconds, - d : input._days, - M : input._months - }; - } else if (typeof input === 'number') { - duration = {}; - if (key) { - duration[key] = input; - } else { - duration.milliseconds = input; - } - } else if (!!(match = aspNetRegex.exec(input))) { - sign = (match[1] === '-') ? -1 : 1; - duration = { - y : 0, - d : toInt(match[DATE]) * sign, - h : toInt(match[HOUR]) * sign, - m : toInt(match[MINUTE]) * sign, - s : toInt(match[SECOND]) * sign, - ms : toInt(match[MILLISECOND]) * sign - }; - } else if (!!(match = create__isoRegex.exec(input))) { - sign = (match[1] === '-') ? -1 : 1; - duration = { - y : parseIso(match[2], sign), - M : parseIso(match[3], sign), - d : parseIso(match[4], sign), - h : parseIso(match[5], sign), - m : parseIso(match[6], sign), - s : parseIso(match[7], sign), - w : parseIso(match[8], sign) - }; - } else if (duration == null) {// checks for null or undefined - duration = {}; - } else if (typeof duration === 'object' && ('from' in duration || 'to' in duration)) { - diffRes = momentsDifference(local__createLocal(duration.from), local__createLocal(duration.to)); - - duration = {}; - duration.ms = diffRes.milliseconds; - duration.M = diffRes.months; - } - - ret = new Duration(duration); - - if (isDuration(input) && hasOwnProp(input, '_locale')) { - ret._locale = input._locale; - } - - return ret; - } - - create__createDuration.fn = Duration.prototype; - - function parseIso (inp, sign) { - // We'd normally use ~~inp for this, but unfortunately it also - // converts floats to ints. - // inp may be undefined, so careful calling replace on it. - var res = inp && parseFloat(inp.replace(',', '.')); - // apply sign while we're at it - return (isNaN(res) ? 0 : res) * sign; - } - - function positiveMomentsDifference(base, other) { - var res = {milliseconds: 0, months: 0}; - - res.months = other.month() - base.month() + - (other.year() - base.year()) * 12; - if (base.clone().add(res.months, 'M').isAfter(other)) { - --res.months; - } - - res.milliseconds = +other - +(base.clone().add(res.months, 'M')); - - return res; - } - - function momentsDifference(base, other) { - var res; - other = cloneWithOffset(other, base); - if (base.isBefore(other)) { - res = positiveMomentsDifference(base, other); - } else { - res = positiveMomentsDifference(other, base); - res.milliseconds = -res.milliseconds; - res.months = -res.months; - } - - return res; - } - - function createAdder(direction, name) { - return function (val, period) { - var dur, tmp; - //invert the arguments, but complain about it - if (period !== null && !isNaN(+period)) { - deprecateSimple(name, 'moment().' + name + '(period, number) is deprecated. Please use moment().' + name + '(number, period).'); - tmp = val; val = period; period = tmp; - } - - val = typeof val === 'string' ? +val : val; - dur = create__createDuration(val, period); - add_subtract__addSubtract(this, dur, direction); - return this; - }; - } - - function add_subtract__addSubtract (mom, duration, isAdding, updateOffset) { - var milliseconds = duration._milliseconds, - days = duration._days, - months = duration._months; - updateOffset = updateOffset == null ? true : updateOffset; - - if (milliseconds) { - mom._d.setTime(+mom._d + milliseconds * isAdding); - } - if (days) { - get_set__set(mom, 'Date', get_set__get(mom, 'Date') + days * isAdding); - } - if (months) { - setMonth(mom, get_set__get(mom, 'Month') + months * isAdding); - } - if (updateOffset) { - utils_hooks__hooks.updateOffset(mom, days || months); - } - } - - var add_subtract__add = createAdder(1, 'add'); - var add_subtract__subtract = createAdder(-1, 'subtract'); - - function moment_calendar__calendar (time) { - // We want to compare the start of today, vs this. - // Getting start-of-today depends on whether we're local/utc/offset or not. - var now = time || local__createLocal(), - sod = cloneWithOffset(now, this).startOf('day'), - diff = this.diff(sod, 'days', true), - format = diff < -6 ? 'sameElse' : - diff < -1 ? 'lastWeek' : - diff < 0 ? 'lastDay' : - diff < 1 ? 'sameDay' : - diff < 2 ? 'nextDay' : - diff < 7 ? 'nextWeek' : 'sameElse'; - return this.format(this.localeData().calendar(format, this, local__createLocal(now))); - } - - function clone () { - return new Moment(this); - } - - function isAfter (input, units) { - var inputMs; - units = normalizeUnits(typeof units !== 'undefined' ? units : 'millisecond'); - if (units === 'millisecond') { - input = isMoment(input) ? input : local__createLocal(input); - return +this > +input; - } else { - inputMs = isMoment(input) ? +input : +local__createLocal(input); - return inputMs < +this.clone().startOf(units); - } - } - - function isBefore (input, units) { - var inputMs; - units = normalizeUnits(typeof units !== 'undefined' ? units : 'millisecond'); - if (units === 'millisecond') { - input = isMoment(input) ? input : local__createLocal(input); - return +this < +input; - } else { - inputMs = isMoment(input) ? +input : +local__createLocal(input); - return +this.clone().endOf(units) < inputMs; - } - } - - function isBetween (from, to, units) { - return this.isAfter(from, units) && this.isBefore(to, units); - } - - function isSame (input, units) { - var inputMs; - units = normalizeUnits(units || 'millisecond'); - if (units === 'millisecond') { - input = isMoment(input) ? input : local__createLocal(input); - return +this === +input; - } else { - inputMs = +local__createLocal(input); - return +(this.clone().startOf(units)) <= inputMs && inputMs <= +(this.clone().endOf(units)); - } - } - - function absFloor (number) { - if (number < 0) { - return Math.ceil(number); - } else { - return Math.floor(number); - } - } - - function diff (input, units, asFloat) { - var that = cloneWithOffset(input, this), - zoneDelta = (that.utcOffset() - this.utcOffset()) * 6e4, - delta, output; - - units = normalizeUnits(units); - - if (units === 'year' || units === 'month' || units === 'quarter') { - output = monthDiff(this, that); - if (units === 'quarter') { - output = output / 3; - } else if (units === 'year') { - output = output / 12; - } - } else { - delta = this - that; - output = units === 'second' ? delta / 1e3 : // 1000 - units === 'minute' ? delta / 6e4 : // 1000 * 60 - units === 'hour' ? delta / 36e5 : // 1000 * 60 * 60 - units === 'day' ? (delta - zoneDelta) / 864e5 : // 1000 * 60 * 60 * 24, negate dst - units === 'week' ? (delta - zoneDelta) / 6048e5 : // 1000 * 60 * 60 * 24 * 7, negate dst - delta; - } - return asFloat ? output : absFloor(output); - } - - function monthDiff (a, b) { - // difference in months - var wholeMonthDiff = ((b.year() - a.year()) * 12) + (b.month() - a.month()), - // b is in (anchor - 1 month, anchor + 1 month) - anchor = a.clone().add(wholeMonthDiff, 'months'), - anchor2, adjust; - - if (b - anchor < 0) { - anchor2 = a.clone().add(wholeMonthDiff - 1, 'months'); - // linear across the month - adjust = (b - anchor) / (anchor - anchor2); - } else { - anchor2 = a.clone().add(wholeMonthDiff + 1, 'months'); - // linear across the month - adjust = (b - anchor) / (anchor2 - anchor); - } - - return -(wholeMonthDiff + adjust); - } - - utils_hooks__hooks.defaultFormat = 'YYYY-MM-DDTHH:mm:ssZ'; - - function toString () { - return this.clone().locale('en').format('ddd MMM DD YYYY HH:mm:ss [GMT]ZZ'); - } - - function moment_format__toISOString () { - var m = this.clone().utc(); - if (0 < m.year() && m.year() <= 9999) { - if ('function' === typeof Date.prototype.toISOString) { - // native implementation is ~50x faster, use it when we can - return this.toDate().toISOString(); - } else { - return formatMoment(m, 'YYYY-MM-DD[T]HH:mm:ss.SSS[Z]'); - } - } else { - return formatMoment(m, 'YYYYYY-MM-DD[T]HH:mm:ss.SSS[Z]'); - } - } - - function format (inputString) { - var output = formatMoment(this, inputString || utils_hooks__hooks.defaultFormat); - return this.localeData().postformat(output); - } - - function from (time, withoutSuffix) { - if (!this.isValid()) { - return this.localeData().invalidDate(); - } - return create__createDuration({to: this, from: time}).locale(this.locale()).humanize(!withoutSuffix); - } - - function fromNow (withoutSuffix) { - return this.from(local__createLocal(), withoutSuffix); - } - - function to (time, withoutSuffix) { - if (!this.isValid()) { - return this.localeData().invalidDate(); - } - return create__createDuration({from: this, to: time}).locale(this.locale()).humanize(!withoutSuffix); - } - - function toNow (withoutSuffix) { - return this.to(local__createLocal(), withoutSuffix); - } - - function locale (key) { - var newLocaleData; - - if (key === undefined) { - return this._locale._abbr; - } else { - newLocaleData = locale_locales__getLocale(key); - if (newLocaleData != null) { - this._locale = newLocaleData; - } - return this; - } - } - - var lang = deprecate( - 'moment().lang() is deprecated. Instead, use moment().localeData() to get the language configuration. Use moment().locale() to change languages.', - function (key) { - if (key === undefined) { - return this.localeData(); - } else { - return this.locale(key); - } - } - ); - - function localeData () { - return this._locale; - } - - function startOf (units) { - units = normalizeUnits(units); - // the following switch intentionally omits break keywords - // to utilize falling through the cases. - switch (units) { - case 'year': - this.month(0); - /* falls through */ - case 'quarter': - case 'month': - this.date(1); - /* falls through */ - case 'week': - case 'isoWeek': - case 'day': - this.hours(0); - /* falls through */ - case 'hour': - this.minutes(0); - /* falls through */ - case 'minute': - this.seconds(0); - /* falls through */ - case 'second': - this.milliseconds(0); - } - - // weeks are a special case - if (units === 'week') { - this.weekday(0); - } - if (units === 'isoWeek') { - this.isoWeekday(1); - } - - // quarters are also special - if (units === 'quarter') { - this.month(Math.floor(this.month() / 3) * 3); - } - - return this; - } - - function endOf (units) { - units = normalizeUnits(units); - if (units === undefined || units === 'millisecond') { - return this; - } - return this.startOf(units).add(1, (units === 'isoWeek' ? 'week' : units)).subtract(1, 'ms'); - } - - function to_type__valueOf () { - return +this._d - ((this._offset || 0) * 60000); - } - - function unix () { - return Math.floor(+this / 1000); - } - - function toDate () { - return this._offset ? new Date(+this) : this._d; - } - - function toArray () { - var m = this; - return [m.year(), m.month(), m.date(), m.hour(), m.minute(), m.second(), m.millisecond()]; - } - - function moment_valid__isValid () { - return valid__isValid(this); - } - - function parsingFlags () { - return extend({}, getParsingFlags(this)); - } - - function invalidAt () { - return getParsingFlags(this).overflow; - } - - addFormatToken(0, ['gg', 2], 0, function () { - return this.weekYear() % 100; - }); - - addFormatToken(0, ['GG', 2], 0, function () { - return this.isoWeekYear() % 100; - }); - - function addWeekYearFormatToken (token, getter) { - addFormatToken(0, [token, token.length], 0, getter); - } - - addWeekYearFormatToken('gggg', 'weekYear'); - addWeekYearFormatToken('ggggg', 'weekYear'); - addWeekYearFormatToken('GGGG', 'isoWeekYear'); - addWeekYearFormatToken('GGGGG', 'isoWeekYear'); - - // ALIASES - - addUnitAlias('weekYear', 'gg'); - addUnitAlias('isoWeekYear', 'GG'); - - // PARSING - - addRegexToken('G', matchSigned); - addRegexToken('g', matchSigned); - addRegexToken('GG', match1to2, match2); - addRegexToken('gg', match1to2, match2); - addRegexToken('GGGG', match1to4, match4); - addRegexToken('gggg', match1to4, match4); - addRegexToken('GGGGG', match1to6, match6); - addRegexToken('ggggg', match1to6, match6); - - addWeekParseToken(['gggg', 'ggggg', 'GGGG', 'GGGGG'], function (input, week, config, token) { - week[token.substr(0, 2)] = toInt(input); - }); - - addWeekParseToken(['gg', 'GG'], function (input, week, config, token) { - week[token] = utils_hooks__hooks.parseTwoDigitYear(input); - }); - - // HELPERS - - function weeksInYear(year, dow, doy) { - return weekOfYear(local__createLocal([year, 11, 31 + dow - doy]), dow, doy).week; - } - - // MOMENTS - - function getSetWeekYear (input) { - var year = weekOfYear(this, this.localeData()._week.dow, this.localeData()._week.doy).year; - return input == null ? year : this.add((input - year), 'y'); - } - - function getSetISOWeekYear (input) { - var year = weekOfYear(this, 1, 4).year; - return input == null ? year : this.add((input - year), 'y'); - } - - function getISOWeeksInYear () { - return weeksInYear(this.year(), 1, 4); - } - - function getWeeksInYear () { - var weekInfo = this.localeData()._week; - return weeksInYear(this.year(), weekInfo.dow, weekInfo.doy); - } - - addFormatToken('Q', 0, 0, 'quarter'); - - // ALIASES - - addUnitAlias('quarter', 'Q'); - - // PARSING - - addRegexToken('Q', match1); - addParseToken('Q', function (input, array) { - array[MONTH] = (toInt(input) - 1) * 3; - }); - - // MOMENTS - - function getSetQuarter (input) { - return input == null ? Math.ceil((this.month() + 1) / 3) : this.month((input - 1) * 3 + this.month() % 3); - } - - addFormatToken('D', ['DD', 2], 'Do', 'date'); - - // ALIASES - - addUnitAlias('date', 'D'); - - // PARSING - - addRegexToken('D', match1to2); - addRegexToken('DD', match1to2, match2); - addRegexToken('Do', function (isStrict, locale) { - return isStrict ? locale._ordinalParse : locale._ordinalParseLenient; - }); - - addParseToken(['D', 'DD'], DATE); - addParseToken('Do', function (input, array) { - array[DATE] = toInt(input.match(match1to2)[0], 10); - }); - - // MOMENTS - - var getSetDayOfMonth = makeGetSet('Date', true); - - addFormatToken('d', 0, 'do', 'day'); - - addFormatToken('dd', 0, 0, function (format) { - return this.localeData().weekdaysMin(this, format); - }); - - addFormatToken('ddd', 0, 0, function (format) { - return this.localeData().weekdaysShort(this, format); - }); - - addFormatToken('dddd', 0, 0, function (format) { - return this.localeData().weekdays(this, format); - }); - - addFormatToken('e', 0, 0, 'weekday'); - addFormatToken('E', 0, 0, 'isoWeekday'); - - // ALIASES - - addUnitAlias('day', 'd'); - addUnitAlias('weekday', 'e'); - addUnitAlias('isoWeekday', 'E'); - - // PARSING - - addRegexToken('d', match1to2); - addRegexToken('e', match1to2); - addRegexToken('E', match1to2); - addRegexToken('dd', matchWord); - addRegexToken('ddd', matchWord); - addRegexToken('dddd', matchWord); - - addWeekParseToken(['dd', 'ddd', 'dddd'], function (input, week, config) { - var weekday = config._locale.weekdaysParse(input); - // if we didn't get a weekday name, mark the date as invalid - if (weekday != null) { - week.d = weekday; - } else { - getParsingFlags(config).invalidWeekday = input; - } - }); - - addWeekParseToken(['d', 'e', 'E'], function (input, week, config, token) { - week[token] = toInt(input); - }); - - // HELPERS - - function parseWeekday(input, locale) { - if (typeof input === 'string') { - if (!isNaN(input)) { - input = parseInt(input, 10); - } - else { - input = locale.weekdaysParse(input); - if (typeof input !== 'number') { - return null; - } - } - } - return input; - } - - // LOCALES - - var defaultLocaleWeekdays = 'Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday'.split('_'); - function localeWeekdays (m) { - return this._weekdays[m.day()]; - } - - var defaultLocaleWeekdaysShort = 'Sun_Mon_Tue_Wed_Thu_Fri_Sat'.split('_'); - function localeWeekdaysShort (m) { - return this._weekdaysShort[m.day()]; - } - - var defaultLocaleWeekdaysMin = 'Su_Mo_Tu_We_Th_Fr_Sa'.split('_'); - function localeWeekdaysMin (m) { - return this._weekdaysMin[m.day()]; - } - - function localeWeekdaysParse (weekdayName) { - var i, mom, regex; - - if (!this._weekdaysParse) { - this._weekdaysParse = []; - } - - for (i = 0; i < 7; i++) { - // make the regex if we don't have it already - if (!this._weekdaysParse[i]) { - mom = local__createLocal([2000, 1]).day(i); - regex = '^' + this.weekdays(mom, '') + '|^' + this.weekdaysShort(mom, '') + '|^' + this.weekdaysMin(mom, ''); - this._weekdaysParse[i] = new RegExp(regex.replace('.', ''), 'i'); - } - // test the regex - if (this._weekdaysParse[i].test(weekdayName)) { - return i; - } - } - } - - // MOMENTS - - function getSetDayOfWeek (input) { - var day = this._isUTC ? this._d.getUTCDay() : this._d.getDay(); - if (input != null) { - input = parseWeekday(input, this.localeData()); - return this.add(input - day, 'd'); - } else { - return day; - } - } - - function getSetLocaleDayOfWeek (input) { - var weekday = (this.day() + 7 - this.localeData()._week.dow) % 7; - return input == null ? weekday : this.add(input - weekday, 'd'); - } - - function getSetISODayOfWeek (input) { - // behaves the same as moment#day except - // as a getter, returns 7 instead of 0 (1-7 range instead of 0-6) - // as a setter, sunday should belong to the previous week. - return input == null ? this.day() || 7 : this.day(this.day() % 7 ? input : input - 7); - } - - addFormatToken('H', ['HH', 2], 0, 'hour'); - addFormatToken('h', ['hh', 2], 0, function () { - return this.hours() % 12 || 12; - }); - - function meridiem (token, lowercase) { - addFormatToken(token, 0, 0, function () { - return this.localeData().meridiem(this.hours(), this.minutes(), lowercase); - }); - } - - meridiem('a', true); - meridiem('A', false); - - // ALIASES - - addUnitAlias('hour', 'h'); - - // PARSING - - function matchMeridiem (isStrict, locale) { - return locale._meridiemParse; - } - - addRegexToken('a', matchMeridiem); - addRegexToken('A', matchMeridiem); - addRegexToken('H', match1to2); - addRegexToken('h', match1to2); - addRegexToken('HH', match1to2, match2); - addRegexToken('hh', match1to2, match2); - - addParseToken(['H', 'HH'], HOUR); - addParseToken(['a', 'A'], function (input, array, config) { - config._isPm = config._locale.isPM(input); - config._meridiem = input; - }); - addParseToken(['h', 'hh'], function (input, array, config) { - array[HOUR] = toInt(input); - getParsingFlags(config).bigHour = true; - }); - - // LOCALES - - function localeIsPM (input) { - // IE8 Quirks Mode & IE7 Standards Mode do not allow accessing strings like arrays - // Using charAt should be more compatible. - return ((input + '').toLowerCase().charAt(0) === 'p'); - } - - var defaultLocaleMeridiemParse = /[ap]\.?m?\.?/i; - function localeMeridiem (hours, minutes, isLower) { - if (hours > 11) { - return isLower ? 'pm' : 'PM'; - } else { - return isLower ? 'am' : 'AM'; - } - } - - - // MOMENTS - - // Setting the hour should keep the time, because the user explicitly - // specified which hour he wants. So trying to maintain the same hour (in - // a new timezone) makes sense. Adding/subtracting hours does not follow - // this rule. - var getSetHour = makeGetSet('Hours', true); - - addFormatToken('m', ['mm', 2], 0, 'minute'); - - // ALIASES - - addUnitAlias('minute', 'm'); - - // PARSING - - addRegexToken('m', match1to2); - addRegexToken('mm', match1to2, match2); - addParseToken(['m', 'mm'], MINUTE); - - // MOMENTS - - var getSetMinute = makeGetSet('Minutes', false); - - addFormatToken('s', ['ss', 2], 0, 'second'); - - // ALIASES - - addUnitAlias('second', 's'); - - // PARSING - - addRegexToken('s', match1to2); - addRegexToken('ss', match1to2, match2); - addParseToken(['s', 'ss'], SECOND); - - // MOMENTS - - var getSetSecond = makeGetSet('Seconds', false); - - addFormatToken('S', 0, 0, function () { - return ~~(this.millisecond() / 100); - }); - - addFormatToken(0, ['SS', 2], 0, function () { - return ~~(this.millisecond() / 10); - }); - - function millisecond__milliseconds (token) { - addFormatToken(0, [token, 3], 0, 'millisecond'); - } - - millisecond__milliseconds('SSS'); - millisecond__milliseconds('SSSS'); - - // ALIASES - - addUnitAlias('millisecond', 'ms'); - - // PARSING - - addRegexToken('S', match1to3, match1); - addRegexToken('SS', match1to3, match2); - addRegexToken('SSS', match1to3, match3); - addRegexToken('SSSS', matchUnsigned); - addParseToken(['S', 'SS', 'SSS', 'SSSS'], function (input, array) { - array[MILLISECOND] = toInt(('0.' + input) * 1000); - }); - - // MOMENTS - - var getSetMillisecond = makeGetSet('Milliseconds', false); - - addFormatToken('z', 0, 0, 'zoneAbbr'); - addFormatToken('zz', 0, 0, 'zoneName'); - - // MOMENTS - - function getZoneAbbr () { - return this._isUTC ? 'UTC' : ''; - } - - function getZoneName () { - return this._isUTC ? 'Coordinated Universal Time' : ''; - } - - var momentPrototype__proto = Moment.prototype; - - momentPrototype__proto.add = add_subtract__add; - momentPrototype__proto.calendar = moment_calendar__calendar; - momentPrototype__proto.clone = clone; - momentPrototype__proto.diff = diff; - momentPrototype__proto.endOf = endOf; - momentPrototype__proto.format = format; - momentPrototype__proto.from = from; - momentPrototype__proto.fromNow = fromNow; - momentPrototype__proto.to = to; - momentPrototype__proto.toNow = toNow; - momentPrototype__proto.get = getSet; - momentPrototype__proto.invalidAt = invalidAt; - momentPrototype__proto.isAfter = isAfter; - momentPrototype__proto.isBefore = isBefore; - momentPrototype__proto.isBetween = isBetween; - momentPrototype__proto.isSame = isSame; - momentPrototype__proto.isValid = moment_valid__isValid; - momentPrototype__proto.lang = lang; - momentPrototype__proto.locale = locale; - momentPrototype__proto.localeData = localeData; - momentPrototype__proto.max = prototypeMax; - momentPrototype__proto.min = prototypeMin; - momentPrototype__proto.parsingFlags = parsingFlags; - momentPrototype__proto.set = getSet; - momentPrototype__proto.startOf = startOf; - momentPrototype__proto.subtract = add_subtract__subtract; - momentPrototype__proto.toArray = toArray; - momentPrototype__proto.toDate = toDate; - momentPrototype__proto.toISOString = moment_format__toISOString; - momentPrototype__proto.toJSON = moment_format__toISOString; - momentPrototype__proto.toString = toString; - momentPrototype__proto.unix = unix; - momentPrototype__proto.valueOf = to_type__valueOf; - - // Year - momentPrototype__proto.year = getSetYear; - momentPrototype__proto.isLeapYear = getIsLeapYear; - - // Week Year - momentPrototype__proto.weekYear = getSetWeekYear; - momentPrototype__proto.isoWeekYear = getSetISOWeekYear; - - // Quarter - momentPrototype__proto.quarter = momentPrototype__proto.quarters = getSetQuarter; - - // Month - momentPrototype__proto.month = getSetMonth; - momentPrototype__proto.daysInMonth = getDaysInMonth; - - // Week - momentPrototype__proto.week = momentPrototype__proto.weeks = getSetWeek; - momentPrototype__proto.isoWeek = momentPrototype__proto.isoWeeks = getSetISOWeek; - momentPrototype__proto.weeksInYear = getWeeksInYear; - momentPrototype__proto.isoWeeksInYear = getISOWeeksInYear; - - // Day - momentPrototype__proto.date = getSetDayOfMonth; - momentPrototype__proto.day = momentPrototype__proto.days = getSetDayOfWeek; - momentPrototype__proto.weekday = getSetLocaleDayOfWeek; - momentPrototype__proto.isoWeekday = getSetISODayOfWeek; - momentPrototype__proto.dayOfYear = getSetDayOfYear; - - // Hour - momentPrototype__proto.hour = momentPrototype__proto.hours = getSetHour; - - // Minute - momentPrototype__proto.minute = momentPrototype__proto.minutes = getSetMinute; - - // Second - momentPrototype__proto.second = momentPrototype__proto.seconds = getSetSecond; - - // Millisecond - momentPrototype__proto.millisecond = momentPrototype__proto.milliseconds = getSetMillisecond; - - // Offset - momentPrototype__proto.utcOffset = getSetOffset; - momentPrototype__proto.utc = setOffsetToUTC; - momentPrototype__proto.local = setOffsetToLocal; - momentPrototype__proto.parseZone = setOffsetToParsedOffset; - momentPrototype__proto.hasAlignedHourOffset = hasAlignedHourOffset; - momentPrototype__proto.isDST = isDaylightSavingTime; - momentPrototype__proto.isDSTShifted = isDaylightSavingTimeShifted; - momentPrototype__proto.isLocal = isLocal; - momentPrototype__proto.isUtcOffset = isUtcOffset; - momentPrototype__proto.isUtc = isUtc; - momentPrototype__proto.isUTC = isUtc; - - // Timezone - momentPrototype__proto.zoneAbbr = getZoneAbbr; - momentPrototype__proto.zoneName = getZoneName; - - // Deprecations - momentPrototype__proto.dates = deprecate('dates accessor is deprecated. Use date instead.', getSetDayOfMonth); - momentPrototype__proto.months = deprecate('months accessor is deprecated. Use month instead', getSetMonth); - momentPrototype__proto.years = deprecate('years accessor is deprecated. Use year instead', getSetYear); - momentPrototype__proto.zone = deprecate('moment().zone is deprecated, use moment().utcOffset instead. https://github.com/moment/moment/issues/1779', getSetZone); - - var momentPrototype = momentPrototype__proto; - - function moment__createUnix (input) { - return local__createLocal(input * 1000); - } - - function moment__createInZone () { - return local__createLocal.apply(null, arguments).parseZone(); - } - - var defaultCalendar = { - sameDay : '[Today at] LT', - nextDay : '[Tomorrow at] LT', - nextWeek : 'dddd [at] LT', - lastDay : '[Yesterday at] LT', - lastWeek : '[Last] dddd [at] LT', - sameElse : 'L' - }; - - function locale_calendar__calendar (key, mom, now) { - var output = this._calendar[key]; - return typeof output === 'function' ? output.call(mom, now) : output; - } - - var defaultLongDateFormat = { - LTS : 'h:mm:ss A', - LT : 'h:mm A', - L : 'MM/DD/YYYY', - LL : 'MMMM D, YYYY', - LLL : 'MMMM D, YYYY LT', - LLLL : 'dddd, MMMM D, YYYY LT' - }; - - function longDateFormat (key) { - var output = this._longDateFormat[key]; - if (!output && this._longDateFormat[key.toUpperCase()]) { - output = this._longDateFormat[key.toUpperCase()].replace(/MMMM|MM|DD|dddd/g, function (val) { - return val.slice(1); - }); - this._longDateFormat[key] = output; - } - return output; - } - - var defaultInvalidDate = 'Invalid date'; - - function invalidDate () { - return this._invalidDate; - } - - var defaultOrdinal = '%d'; - var defaultOrdinalParse = /\d{1,2}/; - - function ordinal (number) { - return this._ordinal.replace('%d', number); - } - - function preParsePostFormat (string) { - return string; - } - - var defaultRelativeTime = { - future : 'in %s', - past : '%s ago', - s : 'a few seconds', - m : 'a minute', - mm : '%d minutes', - h : 'an hour', - hh : '%d hours', - d : 'a day', - dd : '%d days', - M : 'a month', - MM : '%d months', - y : 'a year', - yy : '%d years' - }; - - function relative__relativeTime (number, withoutSuffix, string, isFuture) { - var output = this._relativeTime[string]; - return (typeof output === 'function') ? - output(number, withoutSuffix, string, isFuture) : - output.replace(/%d/i, number); - } - - function pastFuture (diff, output) { - var format = this._relativeTime[diff > 0 ? 'future' : 'past']; - return typeof format === 'function' ? format(output) : format.replace(/%s/i, output); - } - - function locale_set__set (config) { - var prop, i; - for (i in config) { - prop = config[i]; - if (typeof prop === 'function') { - this[i] = prop; - } else { - this['_' + i] = prop; - } - } - // Lenient ordinal parsing accepts just a number in addition to - // number + (possibly) stuff coming from _ordinalParseLenient. - this._ordinalParseLenient = new RegExp(this._ordinalParse.source + '|' + (/\d{1,2}/).source); - } - - var prototype__proto = Locale.prototype; - - prototype__proto._calendar = defaultCalendar; - prototype__proto.calendar = locale_calendar__calendar; - prototype__proto._longDateFormat = defaultLongDateFormat; - prototype__proto.longDateFormat = longDateFormat; - prototype__proto._invalidDate = defaultInvalidDate; - prototype__proto.invalidDate = invalidDate; - prototype__proto._ordinal = defaultOrdinal; - prototype__proto.ordinal = ordinal; - prototype__proto._ordinalParse = defaultOrdinalParse; - prototype__proto.preparse = preParsePostFormat; - prototype__proto.postformat = preParsePostFormat; - prototype__proto._relativeTime = defaultRelativeTime; - prototype__proto.relativeTime = relative__relativeTime; - prototype__proto.pastFuture = pastFuture; - prototype__proto.set = locale_set__set; - - // Month - prototype__proto.months = localeMonths; - prototype__proto._months = defaultLocaleMonths; - prototype__proto.monthsShort = localeMonthsShort; - prototype__proto._monthsShort = defaultLocaleMonthsShort; - prototype__proto.monthsParse = localeMonthsParse; - - // Week - prototype__proto.week = localeWeek; - prototype__proto._week = defaultLocaleWeek; - prototype__proto.firstDayOfYear = localeFirstDayOfYear; - prototype__proto.firstDayOfWeek = localeFirstDayOfWeek; - - // Day of Week - prototype__proto.weekdays = localeWeekdays; - prototype__proto._weekdays = defaultLocaleWeekdays; - prototype__proto.weekdaysMin = localeWeekdaysMin; - prototype__proto._weekdaysMin = defaultLocaleWeekdaysMin; - prototype__proto.weekdaysShort = localeWeekdaysShort; - prototype__proto._weekdaysShort = defaultLocaleWeekdaysShort; - prototype__proto.weekdaysParse = localeWeekdaysParse; - - // Hours - prototype__proto.isPM = localeIsPM; - prototype__proto._meridiemParse = defaultLocaleMeridiemParse; - prototype__proto.meridiem = localeMeridiem; - - function lists__get (format, index, field, setter) { - var locale = locale_locales__getLocale(); - var utc = create_utc__createUTC().set(setter, index); - return locale[field](utc, format); - } - - function list (format, index, field, count, setter) { - if (typeof format === 'number') { - index = format; - format = undefined; - } - - format = format || ''; - - if (index != null) { - return lists__get(format, index, field, setter); - } - - var i; - var out = []; - for (i = 0; i < count; i++) { - out[i] = lists__get(format, i, field, setter); - } - return out; - } - - function lists__listMonths (format, index) { - return list(format, index, 'months', 12, 'month'); - } - - function lists__listMonthsShort (format, index) { - return list(format, index, 'monthsShort', 12, 'month'); - } - - function lists__listWeekdays (format, index) { - return list(format, index, 'weekdays', 7, 'day'); - } - - function lists__listWeekdaysShort (format, index) { - return list(format, index, 'weekdaysShort', 7, 'day'); - } - - function lists__listWeekdaysMin (format, index) { - return list(format, index, 'weekdaysMin', 7, 'day'); - } - - locale_locales__getSetGlobalLocale('en', { - ordinalParse: /\d{1,2}(th|st|nd|rd)/, - ordinal : function (number) { - var b = number % 10, - output = (toInt(number % 100 / 10) === 1) ? 'th' : - (b === 1) ? 'st' : - (b === 2) ? 'nd' : - (b === 3) ? 'rd' : 'th'; - return number + output; - } - }); - - // Side effect imports - utils_hooks__hooks.lang = deprecate('moment.lang is deprecated. Use moment.locale instead.', locale_locales__getSetGlobalLocale); - utils_hooks__hooks.langData = deprecate('moment.langData is deprecated. Use moment.localeData instead.', locale_locales__getLocale); - - var mathAbs = Math.abs; - - function duration_abs__abs () { - var data = this._data; - - this._milliseconds = mathAbs(this._milliseconds); - this._days = mathAbs(this._days); - this._months = mathAbs(this._months); - - data.milliseconds = mathAbs(data.milliseconds); - data.seconds = mathAbs(data.seconds); - data.minutes = mathAbs(data.minutes); - data.hours = mathAbs(data.hours); - data.months = mathAbs(data.months); - data.years = mathAbs(data.years); - - return this; - } - - function duration_add_subtract__addSubtract (duration, input, value, direction) { - var other = create__createDuration(input, value); - - duration._milliseconds += direction * other._milliseconds; - duration._days += direction * other._days; - duration._months += direction * other._months; - - return duration._bubble(); - } - - // supports only 2.0-style add(1, 's') or add(duration) - function duration_add_subtract__add (input, value) { - return duration_add_subtract__addSubtract(this, input, value, 1); - } - - // supports only 2.0-style subtract(1, 's') or subtract(duration) - function duration_add_subtract__subtract (input, value) { - return duration_add_subtract__addSubtract(this, input, value, -1); - } - - function bubble () { - var milliseconds = this._milliseconds; - var days = this._days; - var months = this._months; - var data = this._data; - var seconds, minutes, hours, years = 0; - - // The following code bubbles up values, see the tests for - // examples of what that means. - data.milliseconds = milliseconds % 1000; - - seconds = absFloor(milliseconds / 1000); - data.seconds = seconds % 60; - - minutes = absFloor(seconds / 60); - data.minutes = minutes % 60; - - hours = absFloor(minutes / 60); - data.hours = hours % 24; - - days += absFloor(hours / 24); - - // Accurately convert days to years, assume start from year 0. - years = absFloor(daysToYears(days)); - days -= absFloor(yearsToDays(years)); - - // 30 days to a month - // TODO (iskren): Use anchor date (like 1st Jan) to compute this. - months += absFloor(days / 30); - days %= 30; - - // 12 months -> 1 year - years += absFloor(months / 12); - months %= 12; - - data.days = days; - data.months = months; - data.years = years; - - return this; - } - - function daysToYears (days) { - // 400 years have 146097 days (taking into account leap year rules) - return days * 400 / 146097; - } - - function yearsToDays (years) { - // years * 365 + absFloor(years / 4) - - // absFloor(years / 100) + absFloor(years / 400); - return years * 146097 / 400; - } - - function as (units) { - var days; - var months; - var milliseconds = this._milliseconds; - - units = normalizeUnits(units); - - if (units === 'month' || units === 'year') { - days = this._days + milliseconds / 864e5; - months = this._months + daysToYears(days) * 12; - return units === 'month' ? months : months / 12; - } else { - // handle milliseconds separately because of floating point math errors (issue #1867) - days = this._days + Math.round(yearsToDays(this._months / 12)); - switch (units) { - case 'week' : return days / 7 + milliseconds / 6048e5; - case 'day' : return days + milliseconds / 864e5; - case 'hour' : return days * 24 + milliseconds / 36e5; - case 'minute' : return days * 1440 + milliseconds / 6e4; - case 'second' : return days * 86400 + milliseconds / 1000; - // Math.floor prevents floating point math errors here - case 'millisecond': return Math.floor(days * 864e5) + milliseconds; - default: throw new Error('Unknown unit ' + units); - } - } - } - - // TODO: Use this.as('ms')? - function duration_as__valueOf () { - return ( - this._milliseconds + - this._days * 864e5 + - (this._months % 12) * 2592e6 + - toInt(this._months / 12) * 31536e6 - ); - } - - function makeAs (alias) { - return function () { - return this.as(alias); - }; - } - - var asMilliseconds = makeAs('ms'); - var asSeconds = makeAs('s'); - var asMinutes = makeAs('m'); - var asHours = makeAs('h'); - var asDays = makeAs('d'); - var asWeeks = makeAs('w'); - var asMonths = makeAs('M'); - var asYears = makeAs('y'); - - function duration_get__get (units) { - units = normalizeUnits(units); - return this[units + 's'](); - } - - function makeGetter(name) { - return function () { - return this._data[name]; - }; - } - - var duration_get__milliseconds = makeGetter('milliseconds'); - var seconds = makeGetter('seconds'); - var minutes = makeGetter('minutes'); - var hours = makeGetter('hours'); - var days = makeGetter('days'); - var months = makeGetter('months'); - var years = makeGetter('years'); - - function weeks () { - return absFloor(this.days() / 7); - } - - var round = Math.round; - var thresholds = { - s: 45, // seconds to minute - m: 45, // minutes to hour - h: 22, // hours to day - d: 26, // days to month - M: 11 // months to year - }; - - // helper function for moment.fn.from, moment.fn.fromNow, and moment.duration.fn.humanize - function substituteTimeAgo(string, number, withoutSuffix, isFuture, locale) { - return locale.relativeTime(number || 1, !!withoutSuffix, string, isFuture); - } - - function duration_humanize__relativeTime (posNegDuration, withoutSuffix, locale) { - var duration = create__createDuration(posNegDuration).abs(); - var seconds = round(duration.as('s')); - var minutes = round(duration.as('m')); - var hours = round(duration.as('h')); - var days = round(duration.as('d')); - var months = round(duration.as('M')); - var years = round(duration.as('y')); - - var a = seconds < thresholds.s && ['s', seconds] || - minutes === 1 && ['m'] || - minutes < thresholds.m && ['mm', minutes] || - hours === 1 && ['h'] || - hours < thresholds.h && ['hh', hours] || - days === 1 && ['d'] || - days < thresholds.d && ['dd', days] || - months === 1 && ['M'] || - months < thresholds.M && ['MM', months] || - years === 1 && ['y'] || ['yy', years]; - - a[2] = withoutSuffix; - a[3] = +posNegDuration > 0; - a[4] = locale; - return substituteTimeAgo.apply(null, a); - } - - // This function allows you to set a threshold for relative time strings - function duration_humanize__getSetRelativeTimeThreshold (threshold, limit) { - if (thresholds[threshold] === undefined) { - return false; - } - if (limit === undefined) { - return thresholds[threshold]; - } - thresholds[threshold] = limit; - return true; - } - - function humanize (withSuffix) { - var locale = this.localeData(); - var output = duration_humanize__relativeTime(this, !withSuffix, locale); - - if (withSuffix) { - output = locale.pastFuture(+this, output); - } - - return locale.postformat(output); - } - - var iso_string__abs = Math.abs; - - function iso_string__toISOString() { - // inspired by https://github.com/dordille/moment-isoduration/blob/master/moment.isoduration.js - var Y = iso_string__abs(this.years()); - var M = iso_string__abs(this.months()); - var D = iso_string__abs(this.days()); - var h = iso_string__abs(this.hours()); - var m = iso_string__abs(this.minutes()); - var s = iso_string__abs(this.seconds() + this.milliseconds() / 1000); - var total = this.asSeconds(); - - if (!total) { - // this is the same as C#'s (Noda) and python (isodate)... - // but not other JS (goog.date) - return 'P0D'; - } - - return (total < 0 ? '-' : '') + - 'P' + - (Y ? Y + 'Y' : '') + - (M ? M + 'M' : '') + - (D ? D + 'D' : '') + - ((h || m || s) ? 'T' : '') + - (h ? h + 'H' : '') + - (m ? m + 'M' : '') + - (s ? s + 'S' : ''); - } - - var duration_prototype__proto = Duration.prototype; - - duration_prototype__proto.abs = duration_abs__abs; - duration_prototype__proto.add = duration_add_subtract__add; - duration_prototype__proto.subtract = duration_add_subtract__subtract; - duration_prototype__proto.as = as; - duration_prototype__proto.asMilliseconds = asMilliseconds; - duration_prototype__proto.asSeconds = asSeconds; - duration_prototype__proto.asMinutes = asMinutes; - duration_prototype__proto.asHours = asHours; - duration_prototype__proto.asDays = asDays; - duration_prototype__proto.asWeeks = asWeeks; - duration_prototype__proto.asMonths = asMonths; - duration_prototype__proto.asYears = asYears; - duration_prototype__proto.valueOf = duration_as__valueOf; - duration_prototype__proto._bubble = bubble; - duration_prototype__proto.get = duration_get__get; - duration_prototype__proto.milliseconds = duration_get__milliseconds; - duration_prototype__proto.seconds = seconds; - duration_prototype__proto.minutes = minutes; - duration_prototype__proto.hours = hours; - duration_prototype__proto.days = days; - duration_prototype__proto.weeks = weeks; - duration_prototype__proto.months = months; - duration_prototype__proto.years = years; - duration_prototype__proto.humanize = humanize; - duration_prototype__proto.toISOString = iso_string__toISOString; - duration_prototype__proto.toString = iso_string__toISOString; - duration_prototype__proto.toJSON = iso_string__toISOString; - duration_prototype__proto.locale = locale; - duration_prototype__proto.localeData = localeData; - - // Deprecations - duration_prototype__proto.toIsoString = deprecate('toIsoString() is deprecated. Please use toISOString() instead (notice the capitals)', iso_string__toISOString); - duration_prototype__proto.lang = lang; - - // Side effect imports - - addFormatToken('X', 0, 0, 'unix'); - addFormatToken('x', 0, 0, 'valueOf'); - - // PARSING - - addRegexToken('x', matchSigned); - addRegexToken('X', matchTimestamp); - addParseToken('X', function (input, array, config) { - config._d = new Date(parseFloat(input, 10) * 1000); - }); - addParseToken('x', function (input, array, config) { - config._d = new Date(toInt(input)); - }); - - // Side effect imports - - - utils_hooks__hooks.version = '2.10.3'; - - setHookCallback(local__createLocal); - - utils_hooks__hooks.fn = momentPrototype; - utils_hooks__hooks.min = min; - utils_hooks__hooks.max = max; - utils_hooks__hooks.utc = create_utc__createUTC; - utils_hooks__hooks.unix = moment__createUnix; - utils_hooks__hooks.months = lists__listMonths; - utils_hooks__hooks.isDate = isDate; - utils_hooks__hooks.locale = locale_locales__getSetGlobalLocale; - utils_hooks__hooks.invalid = valid__createInvalid; - utils_hooks__hooks.duration = create__createDuration; - utils_hooks__hooks.isMoment = isMoment; - utils_hooks__hooks.weekdays = lists__listWeekdays; - utils_hooks__hooks.parseZone = moment__createInZone; - utils_hooks__hooks.localeData = locale_locales__getLocale; - utils_hooks__hooks.isDuration = isDuration; - utils_hooks__hooks.monthsShort = lists__listMonthsShort; - utils_hooks__hooks.weekdaysMin = lists__listWeekdaysMin; - utils_hooks__hooks.defineLocale = defineLocale; - utils_hooks__hooks.weekdaysShort = lists__listWeekdaysShort; - utils_hooks__hooks.normalizeUnits = normalizeUnits; - utils_hooks__hooks.relativeTimeThreshold = duration_humanize__getSetRelativeTimeThreshold; - - var _moment = utils_hooks__hooks; - - return _moment; - -})); \ No newline at end of file diff --git a/src/UI/JsLibraries/typeahead.js b/src/UI/JsLibraries/typeahead.js deleted file mode 100644 index 450a6ca43..000000000 --- a/src/UI/JsLibraries/typeahead.js +++ /dev/null @@ -1,1716 +0,0 @@ -/*! - * typeahead.js 0.10.2 - * https://github.com/twitter/typeahead.js - * Copyright 2013-2014 Twitter, Inc. and other contributors; Licensed MIT - */ - -(function($) { - var _ = { - isMsie: function() { - return /(msie|trident)/i.test(navigator.userAgent) ? navigator.userAgent.match(/(msie |rv:)(\d+(.\d+)?)/i)[2] : false; - }, - isBlankString: function(str) { - return !str || /^\s*$/.test(str); - }, - escapeRegExChars: function(str) { - return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&"); - }, - isString: function(obj) { - return typeof obj === "string"; - }, - isNumber: function(obj) { - return typeof obj === "number"; - }, - isArray: $.isArray, - isFunction: $.isFunction, - isObject: $.isPlainObject, - isUndefined: function(obj) { - return typeof obj === "undefined"; - }, - bind: $.proxy, - each: function(collection, cb) { - $.each(collection, reverseArgs); - function reverseArgs(index, value) { - return cb(value, index); - } - }, - map: $.map, - filter: $.grep, - every: function(obj, test) { - var result = true; - if (!obj) { - return result; - } - $.each(obj, function(key, val) { - if (!(result = test.call(null, val, key, obj))) { - return false; - } - }); - return !!result; - }, - some: function(obj, test) { - var result = false; - if (!obj) { - return result; - } - $.each(obj, function(key, val) { - if (result = test.call(null, val, key, obj)) { - return false; - } - }); - return !!result; - }, - mixin: $.extend, - getUniqueId: function() { - var counter = 0; - return function() { - return counter++; - }; - }(), - templatify: function templatify(obj) { - return $.isFunction(obj) ? obj : template; - function template() { - return String(obj); - } - }, - defer: function(fn) { - setTimeout(fn, 0); - }, - debounce: function(func, wait, immediate) { - var timeout, result; - return function() { - var context = this, args = arguments, later, callNow; - later = function() { - timeout = null; - if (!immediate) { - result = func.apply(context, args); - } - }; - callNow = immediate && !timeout; - clearTimeout(timeout); - timeout = setTimeout(later, wait); - if (callNow) { - result = func.apply(context, args); - } - return result; - }; - }, - throttle: function(func, wait) { - var context, args, timeout, result, previous, later; - previous = 0; - later = function() { - previous = new Date(); - timeout = null; - result = func.apply(context, args); - }; - return function() { - var now = new Date(), remaining = wait - (now - previous); - context = this; - args = arguments; - if (remaining <= 0) { - clearTimeout(timeout); - timeout = null; - previous = now; - result = func.apply(context, args); - } else if (!timeout) { - timeout = setTimeout(later, remaining); - } - return result; - }; - }, - noop: function() {} - }; - var VERSION = "0.10.2"; - var tokenizers = function(root) { - return { - nonword: nonword, - whitespace: whitespace, - obj: { - nonword: getObjTokenizer(nonword), - whitespace: getObjTokenizer(whitespace) - } - }; - function whitespace(s) { - return s.split(/\s+/); - } - function nonword(s) { - return s.split(/\W+/); - } - function getObjTokenizer(tokenizer) { - return function setKey(key) { - return function tokenize(o) { - return tokenizer(o[key]); - }; - }; - } - }(); - var LruCache = function() { - function LruCache(maxSize) { - this.maxSize = maxSize || 100; - this.size = 0; - this.hash = {}; - this.list = new List(); - } - _.mixin(LruCache.prototype, { - set: function set(key, val) { - var tailItem = this.list.tail, node; - if (this.size >= this.maxSize) { - this.list.remove(tailItem); - delete this.hash[tailItem.key]; - } - if (node = this.hash[key]) { - node.val = val; - this.list.moveToFront(node); - } else { - node = new Node(key, val); - this.list.add(node); - this.hash[key] = node; - this.size++; - } - }, - get: function get(key) { - var node = this.hash[key]; - if (node) { - this.list.moveToFront(node); - return node.val; - } - } - }); - function List() { - this.head = this.tail = null; - } - _.mixin(List.prototype, { - add: function add(node) { - if (this.head) { - node.next = this.head; - this.head.prev = node; - } - this.head = node; - this.tail = this.tail || node; - }, - remove: function remove(node) { - node.prev ? node.prev.next = node.next : this.head = node.next; - node.next ? node.next.prev = node.prev : this.tail = node.prev; - }, - moveToFront: function(node) { - this.remove(node); - this.add(node); - } - }); - function Node(key, val) { - this.key = key; - this.val = val; - this.prev = this.next = null; - } - return LruCache; - }(); - var PersistentStorage = function() { - var ls, methods; - try { - ls = window.localStorage; - ls.setItem("~~~", "!"); - ls.removeItem("~~~"); - } catch (err) { - ls = null; - } - function PersistentStorage(namespace) { - this.prefix = [ "__", namespace, "__" ].join(""); - this.ttlKey = "__ttl__"; - this.keyMatcher = new RegExp("^" + this.prefix); - } - if (ls && window.JSON) { - methods = { - _prefix: function(key) { - return this.prefix + key; - }, - _ttlKey: function(key) { - return this._prefix(key) + this.ttlKey; - }, - get: function(key) { - if (this.isExpired(key)) { - this.remove(key); - } - return decode(ls.getItem(this._prefix(key))); - }, - set: function(key, val, ttl) { - if (_.isNumber(ttl)) { - ls.setItem(this._ttlKey(key), encode(now() + ttl)); - } else { - ls.removeItem(this._ttlKey(key)); - } - return ls.setItem(this._prefix(key), encode(val)); - }, - remove: function(key) { - ls.removeItem(this._ttlKey(key)); - ls.removeItem(this._prefix(key)); - return this; - }, - clear: function() { - var i, key, keys = [], len = ls.length; - for (i = 0; i < len; i++) { - if ((key = ls.key(i)).match(this.keyMatcher)) { - keys.push(key.replace(this.keyMatcher, "")); - } - } - for (i = keys.length; i--; ) { - this.remove(keys[i]); - } - return this; - }, - isExpired: function(key) { - var ttl = decode(ls.getItem(this._ttlKey(key))); - return _.isNumber(ttl) && now() > ttl ? true : false; - } - }; - } else { - methods = { - get: _.noop, - set: _.noop, - remove: _.noop, - clear: _.noop, - isExpired: _.noop - }; - } - _.mixin(PersistentStorage.prototype, methods); - return PersistentStorage; - function now() { - return new Date().getTime(); - } - function encode(val) { - return JSON.stringify(_.isUndefined(val) ? null : val); - } - function decode(val) { - return JSON.parse(val); - } - }(); - var Transport = function() { - var pendingRequestsCount = 0, pendingRequests = {}, maxPendingRequests = 6, requestCache = new LruCache(10); - function Transport(o) { - o = o || {}; - this._send = o.transport ? callbackToDeferred(o.transport) : $.ajax; - this._get = o.rateLimiter ? o.rateLimiter(this._get) : this._get; - } - Transport.setMaxPendingRequests = function setMaxPendingRequests(num) { - maxPendingRequests = num; - }; - Transport.resetCache = function clearCache() { - requestCache = new LruCache(10); - }; - _.mixin(Transport.prototype, { - _get: function(url, o, cb) { - var that = this, jqXhr; - if (jqXhr = pendingRequests[url]) { - jqXhr.done(done).fail(fail); - } else if (pendingRequestsCount < maxPendingRequests) { - pendingRequestsCount++; - pendingRequests[url] = this._send(url, o).done(done).fail(fail).always(always); - } else { - this.onDeckRequestArgs = [].slice.call(arguments, 0); - } - function done(resp) { - cb && cb(null, resp); - requestCache.set(url, resp); - } - function fail() { - cb && cb(true); - } - function always() { - pendingRequestsCount--; - delete pendingRequests[url]; - if (that.onDeckRequestArgs) { - that._get.apply(that, that.onDeckRequestArgs); - that.onDeckRequestArgs = null; - } - } - }, - get: function(url, o, cb) { - var resp; - if (_.isFunction(o)) { - cb = o; - o = {}; - } - if (resp = requestCache.get(url)) { - _.defer(function() { - cb && cb(null, resp); - }); - } else { - this._get(url, o, cb); - } - return !!resp; - } - }); - return Transport; - function callbackToDeferred(fn) { - return function customSendWrapper(url, o) { - var deferred = $.Deferred(); - fn(url, o, onSuccess, onError); - return deferred; - function onSuccess(resp) { - _.defer(function() { - deferred.resolve(resp); - }); - } - function onError(err) { - _.defer(function() { - deferred.reject(err); - }); - } - }; - } - }(); - var SearchIndex = function() { - function SearchIndex(o) { - o = o || {}; - if (!o.datumTokenizer || !o.queryTokenizer) { - $.error("datumTokenizer and queryTokenizer are both required"); - } - this.datumTokenizer = o.datumTokenizer; - this.queryTokenizer = o.queryTokenizer; - this.reset(); - } - _.mixin(SearchIndex.prototype, { - bootstrap: function bootstrap(o) { - this.datums = o.datums; - this.trie = o.trie; - }, - add: function(data) { - var that = this; - data = _.isArray(data) ? data : [ data ]; - _.each(data, function(datum) { - var id, tokens; - id = that.datums.push(datum) - 1; - tokens = normalizeTokens(that.datumTokenizer(datum)); - _.each(tokens, function(token) { - var node, chars, ch; - node = that.trie; - chars = token.split(""); - while (ch = chars.shift()) { - node = node.children[ch] || (node.children[ch] = newNode()); - node.ids.push(id); - } - }); - }); - }, - get: function get(query) { - var that = this, tokens, matches; - tokens = normalizeTokens(this.queryTokenizer(query)); - _.each(tokens, function(token) { - var node, chars, ch, ids; - if (matches && matches.length === 0) { - return false; - } - node = that.trie; - chars = token.split(""); - while (node && (ch = chars.shift())) { - node = node.children[ch]; - } - if (node && chars.length === 0) { - ids = node.ids.slice(0); - matches = matches ? getIntersection(matches, ids) : ids; - } else { - matches = []; - return false; - } - }); - return matches ? _.map(unique(matches), function(id) { - return that.datums[id]; - }) : []; - }, - reset: function reset() { - this.datums = []; - this.trie = newNode(); - }, - serialize: function serialize() { - return { - datums: this.datums, - trie: this.trie - }; - } - }); - return SearchIndex; - function normalizeTokens(tokens) { - tokens = _.filter(tokens, function(token) { - return !!token; - }); - tokens = _.map(tokens, function(token) { - return token.toLowerCase(); - }); - return tokens; - } - function newNode() { - return { - ids: [], - children: {} - }; - } - function unique(array) { - var seen = {}, uniques = []; - for (var i = 0; i < array.length; i++) { - if (!seen[array[i]]) { - seen[array[i]] = true; - uniques.push(array[i]); - } - } - return uniques; - } - function getIntersection(arrayA, arrayB) { - var ai = 0, bi = 0, intersection = []; - arrayA = arrayA.sort(compare); - arrayB = arrayB.sort(compare); - while (ai < arrayA.length && bi < arrayB.length) { - if (arrayA[ai] < arrayB[bi]) { - ai++; - } else if (arrayA[ai] > arrayB[bi]) { - bi++; - } else { - intersection.push(arrayA[ai]); - ai++; - bi++; - } - } - return intersection; - function compare(a, b) { - return a - b; - } - } - }(); - var oParser = function() { - return { - local: getLocal, - prefetch: getPrefetch, - remote: getRemote - }; - function getLocal(o) { - return o.local || null; - } - function getPrefetch(o) { - var prefetch, defaults; - defaults = { - url: null, - thumbprint: "", - ttl: 24 * 60 * 60 * 1e3, - filter: null, - ajax: {} - }; - if (prefetch = o.prefetch || null) { - prefetch = _.isString(prefetch) ? { - url: prefetch - } : prefetch; - prefetch = _.mixin(defaults, prefetch); - prefetch.thumbprint = VERSION + prefetch.thumbprint; - prefetch.ajax.type = prefetch.ajax.type || "GET"; - prefetch.ajax.dataType = prefetch.ajax.dataType || "json"; - !prefetch.url && $.error("prefetch requires url to be set"); - } - return prefetch; - } - function getRemote(o) { - var remote, defaults; - defaults = { - url: null, - wildcard: "%QUERY", - replace: null, - rateLimitBy: "debounce", - rateLimitWait: 300, - send: null, - filter: null, - ajax: {} - }; - if (remote = o.remote || null) { - remote = _.isString(remote) ? { - url: remote - } : remote; - remote = _.mixin(defaults, remote); - remote.rateLimiter = /^throttle$/i.test(remote.rateLimitBy) ? byThrottle(remote.rateLimitWait) : byDebounce(remote.rateLimitWait); - remote.ajax.type = remote.ajax.type || "GET"; - remote.ajax.dataType = remote.ajax.dataType || "json"; - delete remote.rateLimitBy; - delete remote.rateLimitWait; - !remote.url && $.error("remote requires url to be set"); - } - return remote; - function byDebounce(wait) { - return function(fn) { - return _.debounce(fn, wait); - }; - } - function byThrottle(wait) { - return function(fn) { - return _.throttle(fn, wait); - }; - } - } - }(); - (function(root) { - var old, keys; - old = root.Bloodhound; - keys = { - data: "data", - protocol: "protocol", - thumbprint: "thumbprint" - }; - root.Bloodhound = Bloodhound; - function Bloodhound(o) { - if (!o || !o.local && !o.prefetch && !o.remote) { - $.error("one of local, prefetch, or remote is required"); - } - this.limit = o.limit || 5; - this.sorter = getSorter(o.sorter); - this.dupDetector = o.dupDetector || ignoreDuplicates; - this.local = oParser.local(o); - this.prefetch = oParser.prefetch(o); - this.remote = oParser.remote(o); - this.cacheKey = this.prefetch ? this.prefetch.cacheKey || this.prefetch.url : null; - this.index = new SearchIndex({ - datumTokenizer: o.datumTokenizer, - queryTokenizer: o.queryTokenizer - }); - this.storage = this.cacheKey ? new PersistentStorage(this.cacheKey) : null; - } - Bloodhound.noConflict = function noConflict() { - root.Bloodhound = old; - return Bloodhound; - }; - Bloodhound.tokenizers = tokenizers; - _.mixin(Bloodhound.prototype, { - _loadPrefetch: function loadPrefetch(o) { - var that = this, serialized, deferred; - if (serialized = this._readFromStorage(o.thumbprint)) { - this.index.bootstrap(serialized); - deferred = $.Deferred().resolve(); - } else { - deferred = $.ajax(o.url, o.ajax).done(handlePrefetchResponse); - } - return deferred; - function handlePrefetchResponse(resp) { - that.clear(); - that.add(o.filter ? o.filter(resp) : resp); - that._saveToStorage(that.index.serialize(), o.thumbprint, o.ttl); - } - }, - _getFromRemote: function getFromRemote(query, cb) { - var that = this, url, uriEncodedQuery; - query = query || ""; - uriEncodedQuery = encodeURIComponent(query); - url = this.remote.replace ? this.remote.replace(this.remote.url, query) : this.remote.url.replace(this.remote.wildcard, uriEncodedQuery); - return this.transport.get(url, this.remote.ajax, handleRemoteResponse); - function handleRemoteResponse(err, resp) { - err ? cb([]) : cb(that.remote.filter ? that.remote.filter(resp) : resp); - } - }, - _saveToStorage: function saveToStorage(data, thumbprint, ttl) { - if (this.storage) { - this.storage.set(keys.data, data, ttl); - this.storage.set(keys.protocol, location.protocol, ttl); - this.storage.set(keys.thumbprint, thumbprint, ttl); - } - }, - _readFromStorage: function readFromStorage(thumbprint) { - var stored = {}, isExpired; - if (this.storage) { - stored.data = this.storage.get(keys.data); - stored.protocol = this.storage.get(keys.protocol); - stored.thumbprint = this.storage.get(keys.thumbprint); - } - isExpired = stored.thumbprint !== thumbprint || stored.protocol !== location.protocol; - return stored.data && !isExpired ? stored.data : null; - }, - _initialize: function initialize() { - var that = this, local = this.local, deferred; - deferred = this.prefetch ? this._loadPrefetch(this.prefetch) : $.Deferred().resolve(); - local && deferred.done(addLocalToIndex); - this.transport = this.remote ? new Transport(this.remote) : null; - return this.initPromise = deferred.promise(); - function addLocalToIndex() { - that.add(_.isFunction(local) ? local() : local); - } - }, - initialize: function initialize(force) { - return !this.initPromise || force ? this._initialize() : this.initPromise; - }, - add: function add(data) { - this.index.add(data); - }, - get: function get(query, cb) { - var that = this, matches = [], cacheHit = false; - matches = this.index.get(query); - matches = this.sorter(matches).slice(0, this.limit); - if (matches.length < this.limit && this.transport) { - cacheHit = this._getFromRemote(query, returnRemoteMatches); - } - if (!cacheHit) { - (matches.length > 0 || !this.transport) && cb && cb(matches); - } - function returnRemoteMatches(remoteMatches) { - var matchesWithBackfill = matches.slice(0); - _.each(remoteMatches, function(remoteMatch) { - var isDuplicate; - isDuplicate = _.some(matchesWithBackfill, function(match) { - return that.dupDetector(remoteMatch, match); - }); - !isDuplicate && matchesWithBackfill.push(remoteMatch); - return matchesWithBackfill.length < that.limit; - }); - cb && cb(that.sorter(matchesWithBackfill)); - } - }, - clear: function clear() { - this.index.reset(); - }, - clearPrefetchCache: function clearPrefetchCache() { - this.storage && this.storage.clear(); - }, - clearRemoteCache: function clearRemoteCache() { - this.transport && Transport.resetCache(); - }, - ttAdapter: function ttAdapter() { - return _.bind(this.get, this); - } - }); - return Bloodhound; - function getSorter(sortFn) { - return _.isFunction(sortFn) ? sort : noSort; - function sort(array) { - return array.sort(sortFn); - } - function noSort(array) { - return array; - } - } - function ignoreDuplicates() { - return false; - } - })(this); - var html = { - wrapper: '<span class="twitter-typeahead"></span>', - dropdown: '<span class="tt-dropdown-menu"></span>', - dataset: '<div class="tt-dataset-%CLASS%"></div>', - suggestions: '<span class="tt-suggestions"></span>', - suggestion: '<div class="tt-suggestion"></div>' - }; - var css = { - wrapper: { - position: "relative", - display: "inline-block" - }, - hint: { - position: "absolute", - top: "0", - left: "0", - borderColor: "transparent", - boxShadow: "none" - }, - input: { - position: "relative", - verticalAlign: "top", - backgroundColor: "transparent" - }, - inputWithNoHint: { - position: "relative", - verticalAlign: "top" - }, - dropdown: { - position: "absolute", - top: "100%", - left: "0", - zIndex: "100", - display: "none" - }, - suggestions: { - display: "block" - }, - suggestion: { - whiteSpace: "nowrap", - cursor: "pointer" - }, - suggestionChild: { - whiteSpace: "normal" - }, - ltr: { - left: "0", - right: "auto" - }, - rtl: { - left: "auto", - right: " 0" - } - }; - if (_.isMsie()) { - _.mixin(css.input, { - backgroundImage: "url(data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7)" - }); - } - if (_.isMsie() && _.isMsie() <= 7) { - _.mixin(css.input, { - marginTop: "-1px" - }); - } - var EventBus = function() { - var namespace = "typeahead:"; - function EventBus(o) { - if (!o || !o.el) { - $.error("EventBus initialized without el"); - } - this.$el = $(o.el); - } - _.mixin(EventBus.prototype, { - trigger: function(type) { - var args = [].slice.call(arguments, 1); - this.$el.trigger(namespace + type, args); - } - }); - return EventBus; - }(); - var EventEmitter = function() { - var splitter = /\s+/, nextTick = getNextTick(); - return { - onSync: onSync, - onAsync: onAsync, - off: off, - trigger: trigger - }; - function on(method, types, cb, context) { - var type; - if (!cb) { - return this; - } - types = types.split(splitter); - cb = context ? bindContext(cb, context) : cb; - this._callbacks = this._callbacks || {}; - while (type = types.shift()) { - this._callbacks[type] = this._callbacks[type] || { - sync: [], - async: [] - }; - this._callbacks[type][method].push(cb); - } - return this; - } - function onAsync(types, cb, context) { - return on.call(this, "async", types, cb, context); - } - function onSync(types, cb, context) { - return on.call(this, "sync", types, cb, context); - } - function off(types) { - var type; - if (!this._callbacks) { - return this; - } - types = types.split(splitter); - while (type = types.shift()) { - delete this._callbacks[type]; - } - return this; - } - function trigger(types) { - var type, callbacks, args, syncFlush, asyncFlush; - if (!this._callbacks) { - return this; - } - types = types.split(splitter); - args = [].slice.call(arguments, 1); - while ((type = types.shift()) && (callbacks = this._callbacks[type])) { - syncFlush = getFlush(callbacks.sync, this, [ type ].concat(args)); - asyncFlush = getFlush(callbacks.async, this, [ type ].concat(args)); - syncFlush() && nextTick(asyncFlush); - } - return this; - } - function getFlush(callbacks, context, args) { - return flush; - function flush() { - var cancelled; - for (var i = 0; !cancelled && i < callbacks.length; i += 1) { - cancelled = callbacks[i].apply(context, args) === false; - } - return !cancelled; - } - } - function getNextTick() { - var nextTickFn; - if (window.setImmediate) { - nextTickFn = function nextTickSetImmediate(fn) { - setImmediate(function() { - fn(); - }); - }; - } else { - nextTickFn = function nextTickSetTimeout(fn) { - setTimeout(function() { - fn(); - }, 0); - }; - } - return nextTickFn; - } - function bindContext(fn, context) { - return fn.bind ? fn.bind(context) : function() { - fn.apply(context, [].slice.call(arguments, 0)); - }; - } - }(); - var highlight = function(doc) { - var defaults = { - node: null, - pattern: null, - tagName: "strong", - className: null, - wordsOnly: false, - caseSensitive: false - }; - return function hightlight(o) { - var regex; - o = _.mixin({}, defaults, o); - if (!o.node || !o.pattern) { - return; - } - o.pattern = _.isArray(o.pattern) ? o.pattern : [ o.pattern ]; - regex = getRegex(o.pattern, o.caseSensitive, o.wordsOnly); - traverse(o.node, hightlightTextNode); - function hightlightTextNode(textNode) { - var match, patternNode; - if (match = regex.exec(textNode.data)) { - wrapperNode = doc.createElement(o.tagName); - o.className && (wrapperNode.className = o.className); - patternNode = textNode.splitText(match.index); - patternNode.splitText(match[0].length); - wrapperNode.appendChild(patternNode.cloneNode(true)); - textNode.parentNode.replaceChild(wrapperNode, patternNode); - } - return !!match; - } - function traverse(el, hightlightTextNode) { - var childNode, TEXT_NODE_TYPE = 3; - for (var i = 0; i < el.childNodes.length; i++) { - childNode = el.childNodes[i]; - if (childNode.nodeType === TEXT_NODE_TYPE) { - i += hightlightTextNode(childNode) ? 1 : 0; - } else { - traverse(childNode, hightlightTextNode); - } - } - } - }; - function getRegex(patterns, caseSensitive, wordsOnly) { - var escapedPatterns = [], regexStr; - for (var i = 0; i < patterns.length; i++) { - escapedPatterns.push(_.escapeRegExChars(patterns[i])); - } - regexStr = wordsOnly ? "\\b(" + escapedPatterns.join("|") + ")\\b" : "(" + escapedPatterns.join("|") + ")"; - return caseSensitive ? new RegExp(regexStr) : new RegExp(regexStr, "i"); - } - }(window.document); - var Input = function() { - var specialKeyCodeMap; - specialKeyCodeMap = { - 9: "tab", - 27: "esc", - 37: "left", - 39: "right", - 13: "enter", - 38: "up", - 40: "down" - }; - function Input(o) { - var that = this, onBlur, onFocus, onKeydown, onInput; - o = o || {}; - if (!o.input) { - $.error("input is missing"); - } - onBlur = _.bind(this._onBlur, this); - onFocus = _.bind(this._onFocus, this); - onKeydown = _.bind(this._onKeydown, this); - onInput = _.bind(this._onInput, this); - this.$hint = $(o.hint); - this.$input = $(o.input).on("blur.tt", onBlur).on("focus.tt", onFocus).on("keydown.tt", onKeydown); - if (this.$hint.length === 0) { - this.setHint = this.getHint = this.clearHint = this.clearHintIfInvalid = _.noop; - } - if (!_.isMsie()) { - this.$input.on("input.tt", onInput); - } else { - this.$input.on("keydown.tt keypress.tt cut.tt paste.tt", function($e) { - if (specialKeyCodeMap[$e.which || $e.keyCode]) { - return; - } - _.defer(_.bind(that._onInput, that, $e)); - }); - } - this.query = this.$input.val(); - this.$overflowHelper = buildOverflowHelper(this.$input); - } - Input.normalizeQuery = function(str) { - return (str || "").replace(/^\s*/g, "").replace(/\s{2,}/g, " "); - }; - _.mixin(Input.prototype, EventEmitter, { - _onBlur: function onBlur() { - this.resetInputValue(); - this.trigger("blurred"); - }, - _onFocus: function onFocus() { - this.trigger("focused"); - }, - _onKeydown: function onKeydown($e) { - var keyName = specialKeyCodeMap[$e.which || $e.keyCode]; - this._managePreventDefault(keyName, $e); - if (keyName && this._shouldTrigger(keyName, $e)) { - this.trigger(keyName + "Keyed", $e); - } - }, - _onInput: function onInput() { - this._checkInputValue(); - }, - _managePreventDefault: function managePreventDefault(keyName, $e) { - var preventDefault, hintValue, inputValue; - switch (keyName) { - case "tab": - hintValue = this.getHint(); - inputValue = this.getInputValue(); - preventDefault = hintValue && hintValue !== inputValue && !withModifier($e); - break; - - case "up": - case "down": - preventDefault = !withModifier($e); - break; - - default: - preventDefault = false; - } - preventDefault && $e.preventDefault(); - }, - _shouldTrigger: function shouldTrigger(keyName, $e) { - var trigger; - switch (keyName) { - case "tab": - trigger = !withModifier($e); - break; - - default: - trigger = true; - } - return trigger; - }, - _checkInputValue: function checkInputValue() { - var inputValue, areEquivalent, hasDifferentWhitespace; - inputValue = this.getInputValue(); - areEquivalent = areQueriesEquivalent(inputValue, this.query); - hasDifferentWhitespace = areEquivalent ? this.query.length !== inputValue.length : false; - if (!areEquivalent) { - this.trigger("queryChanged", this.query = inputValue); - } else if (hasDifferentWhitespace) { - this.trigger("whitespaceChanged", this.query); - } - }, - focus: function focus() { - this.$input.focus(); - }, - blur: function blur() { - this.$input.blur(); - }, - getQuery: function getQuery() { - return this.query; - }, - setQuery: function setQuery(query) { - this.query = query; - }, - getInputValue: function getInputValue() { - return this.$input.val(); - }, - setInputValue: function setInputValue(value, silent) { - this.$input.val(value); - silent ? this.clearHint() : this._checkInputValue(); - }, - resetInputValue: function resetInputValue() { - this.setInputValue(this.query, true); - }, - getHint: function getHint() { - return this.$hint.val(); - }, - setHint: function setHint(value) { - this.$hint.val(value); - }, - clearHint: function clearHint() { - this.setHint(""); - }, - clearHintIfInvalid: function clearHintIfInvalid() { - var val, hint, valIsPrefixOfHint, isValid; - val = this.getInputValue(); - hint = this.getHint(); - valIsPrefixOfHint = val !== hint && hint.indexOf(val) === 0; - isValid = val !== "" && valIsPrefixOfHint && !this.hasOverflow(); - !isValid && this.clearHint(); - }, - getLanguageDirection: function getLanguageDirection() { - return (this.$input.css("direction") || "ltr").toLowerCase(); - }, - hasOverflow: function hasOverflow() { - var constraint = this.$input.width() - 2; - this.$overflowHelper.text(this.getInputValue()); - return this.$overflowHelper.width() >= constraint; - }, - isCursorAtEnd: function() { - var valueLength, selectionStart, range; - valueLength = this.$input.val().length; - selectionStart = this.$input[0].selectionStart; - if (_.isNumber(selectionStart)) { - return selectionStart === valueLength; - } else if (document.selection) { - range = document.selection.createRange(); - range.moveStart("character", -valueLength); - return valueLength === range.text.length; - } - return true; - }, - destroy: function destroy() { - this.$hint.off(".tt"); - this.$input.off(".tt"); - this.$hint = this.$input = this.$overflowHelper = null; - } - }); - return Input; - function buildOverflowHelper($input) { - return $('<pre aria-hidden="true"></pre>').css({ - position: "absolute", - visibility: "hidden", - whiteSpace: "pre", - fontFamily: $input.css("font-family"), - fontSize: $input.css("font-size"), - fontStyle: $input.css("font-style"), - fontVariant: $input.css("font-variant"), - fontWeight: $input.css("font-weight"), - wordSpacing: $input.css("word-spacing"), - letterSpacing: $input.css("letter-spacing"), - textIndent: $input.css("text-indent"), - textRendering: $input.css("text-rendering"), - textTransform: $input.css("text-transform") - }).insertAfter($input); - } - function areQueriesEquivalent(a, b) { - return Input.normalizeQuery(a) === Input.normalizeQuery(b); - } - function withModifier($e) { - return $e.altKey || $e.ctrlKey || $e.metaKey || $e.shiftKey; - } - }(); - var Dataset = function() { - var datasetKey = "ttDataset", valueKey = "ttValue", datumKey = "ttDatum"; - function Dataset(o) { - o = o || {}; - o.templates = o.templates || {}; - if (!o.source) { - $.error("missing source"); - } - if (o.name && !isValidName(o.name)) { - $.error("invalid dataset name: " + o.name); - } - this.query = null; - this.highlight = !!o.highlight; - this.name = o.name || _.getUniqueId(); - this.source = o.source; - this.displayFn = getDisplayFn(o.display || o.displayKey); - this.templates = getTemplates(o.templates, this.displayFn); - this.$el = $(html.dataset.replace("%CLASS%", this.name)); - } - Dataset.extractDatasetName = function extractDatasetName(el) { - return $(el).data(datasetKey); - }; - Dataset.extractValue = function extractDatum(el) { - return $(el).data(valueKey); - }; - Dataset.extractDatum = function extractDatum(el) { - return $(el).data(datumKey); - }; - _.mixin(Dataset.prototype, EventEmitter, { - _render: function render(query, suggestions) { - if (!this.$el) { - return; - } - var that = this, hasSuggestions; - this.$el.empty(); - hasSuggestions = suggestions && suggestions.length; - if (!hasSuggestions && this.templates.empty) { - this.$el.html(getEmptyHtml()).prepend(that.templates.header ? getHeaderHtml() : null).append(that.templates.footer ? getFooterHtml() : null); - } else if (hasSuggestions) { - this.$el.html(getSuggestionsHtml()).prepend(that.templates.header ? getHeaderHtml() : null).append(that.templates.footer ? getFooterHtml() : null); - } - this.trigger("rendered"); - function getEmptyHtml() { - return that.templates.empty({ - query: query, - isEmpty: true - }); - } - function getSuggestionsHtml() { - var $suggestions, nodes; - $suggestions = $(html.suggestions).css(css.suggestions); - nodes = _.map(suggestions, getSuggestionNode); - $suggestions.append.apply($suggestions, nodes); - that.highlight && highlight({ - node: $suggestions[0], - pattern: query - }); - return $suggestions; - function getSuggestionNode(suggestion) { - var $el; - $el = $(html.suggestion).append(that.templates.suggestion(suggestion)).data(datasetKey, that.name).data(valueKey, that.displayFn(suggestion)).data(datumKey, suggestion); - $el.children().each(function() { - $(this).css(css.suggestionChild); - }); - return $el; - } - } - function getHeaderHtml() { - return that.templates.header({ - query: query, - isEmpty: !hasSuggestions - }); - } - function getFooterHtml() { - return that.templates.footer({ - query: query, - isEmpty: !hasSuggestions - }); - } - }, - getRoot: function getRoot() { - return this.$el; - }, - update: function update(query) { - var that = this; - this.query = query; - this.canceled = false; - this.source(query, render); - function render(suggestions) { - if (!that.canceled && query === that.query) { - that._render(query, suggestions); - } - } - }, - cancel: function cancel() { - this.canceled = true; - }, - clear: function clear() { - this.cancel(); - this.$el.empty(); - this.trigger("rendered"); - }, - isEmpty: function isEmpty() { - return this.$el.is(":empty"); - }, - destroy: function destroy() { - this.$el = null; - } - }); - return Dataset; - function getDisplayFn(display) { - display = display || "value"; - return _.isFunction(display) ? display : displayFn; - function displayFn(obj) { - return obj[display]; - } - } - function getTemplates(templates, displayFn) { - return { - empty: templates.empty && _.templatify(templates.empty), - header: templates.header && _.templatify(templates.header), - footer: templates.footer && _.templatify(templates.footer), - suggestion: templates.suggestion || suggestionTemplate - }; - function suggestionTemplate(context) { - return "<p>" + displayFn(context) + "</p>"; - } - } - function isValidName(str) { - return /^[_a-zA-Z0-9-]+$/.test(str); - } - }(); - var Dropdown = function() { - function Dropdown(o) { - var that = this, onSuggestionClick, onSuggestionMouseEnter, onSuggestionMouseLeave; - o = o || {}; - if (!o.menu) { - $.error("menu is required"); - } - this.isOpen = false; - this.isEmpty = true; - this.datasets = _.map(o.datasets, initializeDataset); - onSuggestionClick = _.bind(this._onSuggestionClick, this); - onSuggestionMouseEnter = _.bind(this._onSuggestionMouseEnter, this); - onSuggestionMouseLeave = _.bind(this._onSuggestionMouseLeave, this); - this.$menu = $(o.menu).on("click.tt", ".tt-suggestion", onSuggestionClick).on("mouseenter.tt", ".tt-suggestion", onSuggestionMouseEnter).on("mouseleave.tt", ".tt-suggestion", onSuggestionMouseLeave); - _.each(this.datasets, function(dataset) { - that.$menu.append(dataset.getRoot()); - dataset.onSync("rendered", that._onRendered, that); - }); - } - _.mixin(Dropdown.prototype, EventEmitter, { - _onSuggestionClick: function onSuggestionClick($e) { - this.trigger("suggestionClicked", $($e.currentTarget)); - }, - _onSuggestionMouseEnter: function onSuggestionMouseEnter($e) { - this._removeCursor(); - this._setCursor($($e.currentTarget), true); - }, - _onSuggestionMouseLeave: function onSuggestionMouseLeave() { - this._removeCursor(); - }, - _onRendered: function onRendered() { - this.isEmpty = _.every(this.datasets, isDatasetEmpty); - this.isEmpty ? this._hide() : this.isOpen && this._show(); - this.trigger("datasetRendered"); - function isDatasetEmpty(dataset) { - return dataset.isEmpty(); - } - }, - _hide: function() { - this.$menu.hide(); - }, - _show: function() { - this.$menu.css("display", "block"); - }, - _getSuggestions: function getSuggestions() { - return this.$menu.find(".tt-suggestion"); - }, - _getCursor: function getCursor() { - return this.$menu.find(".tt-cursor").first(); - }, - _setCursor: function setCursor($el, silent) { - $el.first().addClass("tt-cursor"); - !silent && this.trigger("cursorMoved"); - }, - _removeCursor: function removeCursor() { - this._getCursor().removeClass("tt-cursor"); - }, - _moveCursor: function moveCursor(increment) { - var $suggestions, $oldCursor, newCursorIndex, $newCursor; - if (!this.isOpen) { - return; - } - $oldCursor = this._getCursor(); - $suggestions = this._getSuggestions(); - this._removeCursor(); - newCursorIndex = $suggestions.index($oldCursor) + increment; - newCursorIndex = (newCursorIndex + 1) % ($suggestions.length + 1) - 1; - if (newCursorIndex === -1) { - this.trigger("cursorRemoved"); - return; - } else if (newCursorIndex < -1) { - newCursorIndex = $suggestions.length - 1; - } - this._setCursor($newCursor = $suggestions.eq(newCursorIndex)); - this._ensureVisible($newCursor); - }, - _ensureVisible: function ensureVisible($el) { - var elTop, elBottom, menuScrollTop, menuHeight; - elTop = $el.position().top; - elBottom = elTop + $el.outerHeight(true); - menuScrollTop = this.$menu.scrollTop(); - menuHeight = this.$menu.height() + parseInt(this.$menu.css("paddingTop"), 10) + parseInt(this.$menu.css("paddingBottom"), 10); - if (elTop < 0) { - this.$menu.scrollTop(menuScrollTop + elTop); - } else if (menuHeight < elBottom) { - this.$menu.scrollTop(menuScrollTop + (elBottom - menuHeight)); - } - }, - close: function close() { - if (this.isOpen) { - this.isOpen = false; - this._removeCursor(); - this._hide(); - this.trigger("closed"); - } - }, - open: function open() { - if (!this.isOpen) { - this.isOpen = true; - !this.isEmpty && this._show(); - this.trigger("opened"); - } - }, - setLanguageDirection: function setLanguageDirection(dir) { - this.$menu.css(dir === "ltr" ? css.ltr : css.rtl); - }, - moveCursorUp: function moveCursorUp() { - this._moveCursor(-1); - }, - moveCursorDown: function moveCursorDown() { - this._moveCursor(+1); - }, - getDatumForSuggestion: function getDatumForSuggestion($el) { - var datum = null; - if ($el.length) { - datum = { - raw: Dataset.extractDatum($el), - value: Dataset.extractValue($el), - datasetName: Dataset.extractDatasetName($el) - }; - } - return datum; - }, - getDatumForCursor: function getDatumForCursor() { - return this.getDatumForSuggestion(this._getCursor().first()); - }, - getDatumForTopSuggestion: function getDatumForTopSuggestion() { - return this.getDatumForSuggestion(this._getSuggestions().first()); - }, - update: function update(query) { - _.each(this.datasets, updateDataset); - function updateDataset(dataset) { - dataset.update(query); - } - }, - empty: function empty() { - _.each(this.datasets, clearDataset); - this.isEmpty = true; - function clearDataset(dataset) { - dataset.clear(); - } - }, - isVisible: function isVisible() { - return this.isOpen && !this.isEmpty; - }, - destroy: function destroy() { - this.$menu.off(".tt"); - this.$menu = null; - _.each(this.datasets, destroyDataset); - function destroyDataset(dataset) { - dataset.destroy(); - } - } - }); - return Dropdown; - function initializeDataset(oDataset) { - return new Dataset(oDataset); - } - }(); - var Typeahead = function() { - var attrsKey = "ttAttrs"; - function Typeahead(o) { - var $menu, $input, $hint; - o = o || {}; - if (!o.input) { - $.error("missing input"); - } - this.isActivated = false; - this.autoselect = !!o.autoselect; - this.minLength = _.isNumber(o.minLength) ? o.minLength : 1; - this.$node = buildDomStructure(o.input, o.withHint); - $menu = this.$node.find(".tt-dropdown-menu"); - $input = this.$node.find(".tt-input"); - $hint = this.$node.find(".tt-hint"); - $input.on("blur.tt", function($e) { - var active, isActive, hasActive; - active = document.activeElement; - isActive = $menu.is(active); - hasActive = $menu.has(active).length > 0; - if (_.isMsie() && (isActive || hasActive)) { - $e.preventDefault(); - $e.stopImmediatePropagation(); - _.defer(function() { - $input.focus(); - }); - } - }); - $menu.on("mousedown.tt", function($e) { - $e.preventDefault(); - }); - this.eventBus = o.eventBus || new EventBus({ - el: $input - }); - this.dropdown = new Dropdown({ - menu: $menu, - datasets: o.datasets - }).onSync("suggestionClicked", this._onSuggestionClicked, this).onSync("cursorMoved", this._onCursorMoved, this).onSync("cursorRemoved", this._onCursorRemoved, this).onSync("opened", this._onOpened, this).onSync("closed", this._onClosed, this).onAsync("datasetRendered", this._onDatasetRendered, this); - this.input = new Input({ - input: $input, - hint: $hint - }).onSync("focused", this._onFocused, this).onSync("blurred", this._onBlurred, this).onSync("enterKeyed", this._onEnterKeyed, this).onSync("tabKeyed", this._onTabKeyed, this).onSync("escKeyed", this._onEscKeyed, this).onSync("upKeyed", this._onUpKeyed, this).onSync("downKeyed", this._onDownKeyed, this).onSync("leftKeyed", this._onLeftKeyed, this).onSync("rightKeyed", this._onRightKeyed, this).onSync("queryChanged", this._onQueryChanged, this).onSync("whitespaceChanged", this._onWhitespaceChanged, this); - this._setLanguageDirection(); - } - _.mixin(Typeahead.prototype, { - _onSuggestionClicked: function onSuggestionClicked(type, $el) { - var datum; - if (datum = this.dropdown.getDatumForSuggestion($el)) { - this._select(datum); - } - }, - _onCursorMoved: function onCursorMoved() { - var datum = this.dropdown.getDatumForCursor(); - this.input.setInputValue(datum.value, true); - this.eventBus.trigger("cursorchanged", datum.raw, datum.datasetName); - }, - _onCursorRemoved: function onCursorRemoved() { - this.input.resetInputValue(); - this._updateHint(); - }, - _onDatasetRendered: function onDatasetRendered() { - this._updateHint(); - }, - _onOpened: function onOpened() { - this._updateHint(); - this.eventBus.trigger("opened"); - }, - _onClosed: function onClosed() { - this.input.clearHint(); - this.eventBus.trigger("closed"); - }, - _onFocused: function onFocused() { - this.isActivated = true; - this.dropdown.open(); - }, - _onBlurred: function onBlurred() { - this.isActivated = false; - this.dropdown.empty(); - this.dropdown.close(); - }, - _onEnterKeyed: function onEnterKeyed(type, $e) { - var cursorDatum, topSuggestionDatum; - cursorDatum = this.dropdown.getDatumForCursor(); - topSuggestionDatum = this.dropdown.getDatumForTopSuggestion(); - if (cursorDatum) { - this._select(cursorDatum); - $e.preventDefault(); - } else if (this.autoselect && topSuggestionDatum) { - this._select(topSuggestionDatum); - $e.preventDefault(); - } - }, - _onTabKeyed: function onTabKeyed(type, $e) { - var datum; - if (datum = this.dropdown.getDatumForCursor()) { - this._select(datum); - $e.preventDefault(); - } else { - this._autocomplete(true); - } - }, - _onEscKeyed: function onEscKeyed() { - this.dropdown.close(); - this.input.resetInputValue(); - }, - _onUpKeyed: function onUpKeyed() { - var query = this.input.getQuery(); - this.dropdown.isEmpty && query.length >= this.minLength ? this.dropdown.update(query) : this.dropdown.moveCursorUp(); - this.dropdown.open(); - }, - _onDownKeyed: function onDownKeyed() { - var query = this.input.getQuery(); - this.dropdown.isEmpty && query.length >= this.minLength ? this.dropdown.update(query) : this.dropdown.moveCursorDown(); - this.dropdown.open(); - }, - _onLeftKeyed: function onLeftKeyed() { - this.dir === "rtl" && this._autocomplete(); - }, - _onRightKeyed: function onRightKeyed() { - this.dir === "ltr" && this._autocomplete(); - }, - _onQueryChanged: function onQueryChanged(e, query) { - this.input.clearHintIfInvalid(); - query.length >= this.minLength ? this.dropdown.update(query) : this.dropdown.empty(); - this.dropdown.open(); - this._setLanguageDirection(); - }, - _onWhitespaceChanged: function onWhitespaceChanged() { - this._updateHint(); - this.dropdown.open(); - }, - _setLanguageDirection: function setLanguageDirection() { - var dir; - if (this.dir !== (dir = this.input.getLanguageDirection())) { - this.dir = dir; - this.$node.css("direction", dir); - this.dropdown.setLanguageDirection(dir); - } - }, - _updateHint: function updateHint() { - var datum, val, query, escapedQuery, frontMatchRegEx, match; - datum = this.dropdown.getDatumForTopSuggestion(); - if (datum && this.dropdown.isVisible() && !this.input.hasOverflow()) { - val = this.input.getInputValue(); - query = Input.normalizeQuery(val); - escapedQuery = _.escapeRegExChars(query); - frontMatchRegEx = new RegExp("^(?:" + escapedQuery + ")(.+$)", "i"); - match = frontMatchRegEx.exec(datum.value); - match ? this.input.setHint(val + match[1]) : this.input.clearHint(); - } else { - this.input.clearHint(); - } - }, - _autocomplete: function autocomplete(laxCursor) { - var hint, query, isCursorAtEnd, datum; - hint = this.input.getHint(); - query = this.input.getQuery(); - isCursorAtEnd = laxCursor || this.input.isCursorAtEnd(); - if (hint && query !== hint && isCursorAtEnd) { - datum = this.dropdown.getDatumForTopSuggestion(); - datum && this.input.setInputValue(datum.value); - this.eventBus.trigger("autocompleted", datum.raw, datum.datasetName); - } - }, - _select: function select(datum) { - this.input.setQuery(datum.value); - this.input.setInputValue(datum.value, true); - this._setLanguageDirection(); - this.eventBus.trigger("selected", datum.raw, datum.datasetName); - this.dropdown.close(); - _.defer(_.bind(this.dropdown.empty, this.dropdown)); - }, - open: function open() { - this.dropdown.open(); - }, - close: function close() { - this.dropdown.close(); - }, - setVal: function setVal(val) { - if (this.isActivated) { - this.input.setInputValue(val); - } else { - this.input.setQuery(val); - this.input.setInputValue(val, true); - } - this._setLanguageDirection(); - }, - getVal: function getVal() { - return this.input.getQuery(); - }, - destroy: function destroy() { - this.input.destroy(); - this.dropdown.destroy(); - destroyDomStructure(this.$node); - this.$node = null; - } - }); - return Typeahead; - function buildDomStructure(input, withHint) { - var $input, $wrapper, $dropdown, $hint; - $input = $(input); - $wrapper = $(html.wrapper).css(css.wrapper); - $dropdown = $(html.dropdown).css(css.dropdown); - $hint = $input.clone().css(css.hint).css(getBackgroundStyles($input)); - $hint.val("").removeData().addClass("tt-hint").removeAttr("id name placeholder").prop("disabled", true).attr({ - autocomplete: "off", - spellcheck: "false" - }); - $input.data(attrsKey, { - dir: $input.attr("dir"), - autocomplete: $input.attr("autocomplete"), - spellcheck: $input.attr("spellcheck"), - style: $input.attr("style") - }); - $input.addClass("tt-input").attr({ - autocomplete: "off", - spellcheck: false - }).css(withHint ? css.input : css.inputWithNoHint); - try { - !$input.attr("dir") && $input.attr("dir", "auto"); - } catch (e) {} - return $input.wrap($wrapper).parent().prepend(withHint ? $hint : null).append($dropdown); - } - function getBackgroundStyles($el) { - return { - backgroundAttachment: $el.css("background-attachment"), - backgroundClip: $el.css("background-clip"), - backgroundColor: $el.css("background-color"), - backgroundImage: $el.css("background-image"), - backgroundOrigin: $el.css("background-origin"), - backgroundPosition: $el.css("background-position"), - backgroundRepeat: $el.css("background-repeat"), - backgroundSize: $el.css("background-size") - }; - } - function destroyDomStructure($node) { - var $input = $node.find(".tt-input"); - _.each($input.data(attrsKey), function(val, key) { - _.isUndefined(val) ? $input.removeAttr(key) : $input.attr(key, val); - }); - $input.detach().removeData(attrsKey).removeClass("tt-input").insertAfter($node); - $node.remove(); - } - }(); - (function() { - var old, typeaheadKey, methods; - old = $.fn.typeahead; - typeaheadKey = "ttTypeahead"; - methods = { - initialize: function initialize(o, datasets) { - datasets = _.isArray(datasets) ? datasets : [].slice.call(arguments, 1); - o = o || {}; - return this.each(attach); - function attach() { - var $input = $(this), eventBus, typeahead; - _.each(datasets, function(d) { - d.highlight = !!o.highlight; - }); - typeahead = new Typeahead({ - input: $input, - eventBus: eventBus = new EventBus({ - el: $input - }), - withHint: _.isUndefined(o.hint) ? true : !!o.hint, - minLength: o.minLength, - autoselect: o.autoselect, - datasets: datasets - }); - $input.data(typeaheadKey, typeahead); - } - }, - open: function open() { - return this.each(openTypeahead); - function openTypeahead() { - var $input = $(this), typeahead; - if (typeahead = $input.data(typeaheadKey)) { - typeahead.open(); - } - } - }, - close: function close() { - return this.each(closeTypeahead); - function closeTypeahead() { - var $input = $(this), typeahead; - if (typeahead = $input.data(typeaheadKey)) { - typeahead.close(); - } - } - }, - val: function val(newVal) { - return !arguments.length ? getVal(this.first()) : this.each(setVal); - function setVal() { - var $input = $(this), typeahead; - if (typeahead = $input.data(typeaheadKey)) { - typeahead.setVal(newVal); - } - } - function getVal($input) { - var typeahead, query; - if (typeahead = $input.data(typeaheadKey)) { - query = typeahead.getVal(); - } - return query; - } - }, - destroy: function destroy() { - return this.each(unattach); - function unattach() { - var $input = $(this), typeahead; - if (typeahead = $input.data(typeaheadKey)) { - typeahead.destroy(); - $input.removeData(typeaheadKey); - } - } - } - }; - $.fn.typeahead = function(method) { - if (methods[method]) { - return methods[method].apply(this, [].slice.call(arguments, 1)); - } else { - return methods.initialize.apply(this, arguments); - } - }; - $.fn.typeahead.noConflict = function noConflict() { - $.fn.typeahead = old; - return this; - }; - })(); -})(window.jQuery); \ No newline at end of file diff --git a/src/UI/JsLibraries/zero.clipboard.js b/src/UI/JsLibraries/zero.clipboard.js deleted file mode 100644 index dd44ac46a..000000000 --- a/src/UI/JsLibraries/zero.clipboard.js +++ /dev/null @@ -1,2581 +0,0 @@ -/*! - * ZeroClipboard - * The ZeroClipboard library provides an easy way to copy text to the clipboard using an invisible Adobe Flash movie and a JavaScript interface. - * Copyright (c) 2009-2014 Jon Rohan, James M. Greene - * Licensed MIT - * http://zeroclipboard.org/ - * v2.2.0 - */ -(function(window, undefined) { - "use strict"; - /** - * Store references to critically important global functions that may be - * overridden on certain web pages. - */ - var _window = window, _document = _window.document, _navigator = _window.navigator, _setTimeout = _window.setTimeout, _clearTimeout = _window.clearTimeout, _setInterval = _window.setInterval, _clearInterval = _window.clearInterval, _getComputedStyle = _window.getComputedStyle, _encodeURIComponent = _window.encodeURIComponent, _ActiveXObject = _window.ActiveXObject, _Error = _window.Error, _parseInt = _window.Number.parseInt || _window.parseInt, _parseFloat = _window.Number.parseFloat || _window.parseFloat, _isNaN = _window.Number.isNaN || _window.isNaN, _now = _window.Date.now, _keys = _window.Object.keys, _defineProperty = _window.Object.defineProperty, _hasOwn = _window.Object.prototype.hasOwnProperty, _slice = _window.Array.prototype.slice, _unwrap = function() { - var unwrapper = function(el) { - return el; - }; - if (typeof _window.wrap === "function" && typeof _window.unwrap === "function") { - try { - var div = _document.createElement("div"); - var unwrappedDiv = _window.unwrap(div); - if (div.nodeType === 1 && unwrappedDiv && unwrappedDiv.nodeType === 1) { - unwrapper = _window.unwrap; - } - } catch (e) {} - } - return unwrapper; - }(); - /** - * Convert an `arguments` object into an Array. - * - * @returns The arguments as an Array - * @private - */ - var _args = function(argumentsObj) { - return _slice.call(argumentsObj, 0); - }; - /** - * Shallow-copy the owned, enumerable properties of one object over to another, similar to jQuery's `$.extend`. - * - * @returns The target object, augmented - * @private - */ - var _extend = function() { - var i, len, arg, prop, src, copy, args = _args(arguments), target = args[0] || {}; - for (i = 1, len = args.length; i < len; i++) { - if ((arg = args[i]) != null) { - for (prop in arg) { - if (_hasOwn.call(arg, prop)) { - src = target[prop]; - copy = arg[prop]; - if (target !== copy && copy !== undefined) { - target[prop] = copy; - } - } - } - } - } - return target; - }; - /** - * Return a deep copy of the source object or array. - * - * @returns Object or Array - * @private - */ - var _deepCopy = function(source) { - var copy, i, len, prop; - if (typeof source !== "object" || source == null || typeof source.nodeType === "number") { - copy = source; - } else if (typeof source.length === "number") { - copy = []; - for (i = 0, len = source.length; i < len; i++) { - if (_hasOwn.call(source, i)) { - copy[i] = _deepCopy(source[i]); - } - } - } else { - copy = {}; - for (prop in source) { - if (_hasOwn.call(source, prop)) { - copy[prop] = _deepCopy(source[prop]); - } - } - } - return copy; - }; - /** - * Makes a shallow copy of `obj` (like `_extend`) but filters its properties based on a list of `keys` to keep. - * The inverse of `_omit`, mostly. The big difference is that these properties do NOT need to be enumerable to - * be kept. - * - * @returns A new filtered object. - * @private - */ - var _pick = function(obj, keys) { - var newObj = {}; - for (var i = 0, len = keys.length; i < len; i++) { - if (keys[i] in obj) { - newObj[keys[i]] = obj[keys[i]]; - } - } - return newObj; - }; - /** - * Makes a shallow copy of `obj` (like `_extend`) but filters its properties based on a list of `keys` to omit. - * The inverse of `_pick`. - * - * @returns A new filtered object. - * @private - */ - var _omit = function(obj, keys) { - var newObj = {}; - for (var prop in obj) { - if (keys.indexOf(prop) === -1) { - newObj[prop] = obj[prop]; - } - } - return newObj; - }; - /** - * Remove all owned, enumerable properties from an object. - * - * @returns The original object without its owned, enumerable properties. - * @private - */ - var _deleteOwnProperties = function(obj) { - if (obj) { - for (var prop in obj) { - if (_hasOwn.call(obj, prop)) { - delete obj[prop]; - } - } - } - return obj; - }; - /** - * Determine if an element is contained within another element. - * - * @returns Boolean - * @private - */ - var _containedBy = function(el, ancestorEl) { - if (el && el.nodeType === 1 && el.ownerDocument && ancestorEl && (ancestorEl.nodeType === 1 && ancestorEl.ownerDocument && ancestorEl.ownerDocument === el.ownerDocument || ancestorEl.nodeType === 9 && !ancestorEl.ownerDocument && ancestorEl === el.ownerDocument)) { - do { - if (el === ancestorEl) { - return true; - } - el = el.parentNode; - } while (el); - } - return false; - }; - /** - * Get the URL path's parent directory. - * - * @returns String or `undefined` - * @private - */ - var _getDirPathOfUrl = function(url) { - var dir; - if (typeof url === "string" && url) { - dir = url.split("#")[0].split("?")[0]; - dir = url.slice(0, url.lastIndexOf("/") + 1); - } - return dir; - }; - /** - * Get the current script's URL by throwing an `Error` and analyzing it. - * - * @returns String or `undefined` - * @private - */ - var _getCurrentScriptUrlFromErrorStack = function(stack) { - var url, matches; - if (typeof stack === "string" && stack) { - matches = stack.match(/^(?:|[^:@]*@|.+\)@(?=http[s]?|file)|.+?\s+(?: at |@)(?:[^:\(]+ )*[\(]?)((?:http[s]?|file):\/\/[\/]?.+?\/[^:\)]*?)(?::\d+)(?::\d+)?/); - if (matches && matches[1]) { - url = matches[1]; - } else { - matches = stack.match(/\)@((?:http[s]?|file):\/\/[\/]?.+?\/[^:\)]*?)(?::\d+)(?::\d+)?/); - if (matches && matches[1]) { - url = matches[1]; - } - } - } - return url; - }; - /** - * Get the current script's URL by throwing an `Error` and analyzing it. - * - * @returns String or `undefined` - * @private - */ - var _getCurrentScriptUrlFromError = function() { - var url, err; - try { - throw new _Error(); - } catch (e) { - err = e; - } - if (err) { - url = err.sourceURL || err.fileName || _getCurrentScriptUrlFromErrorStack(err.stack); - } - return url; - }; - /** - * Get the current script's URL. - * - * @returns String or `undefined` - * @private - */ - var _getCurrentScriptUrl = function() { - var jsPath, scripts, i; - if (_document.currentScript && (jsPath = _document.currentScript.src)) { - return jsPath; - } - scripts = _document.getElementsByTagName("script"); - if (scripts.length === 1) { - return scripts[0].src || undefined; - } - if ("readyState" in scripts[0]) { - for (i = scripts.length; i--; ) { - if (scripts[i].readyState === "interactive" && (jsPath = scripts[i].src)) { - return jsPath; - } - } - } - if (_document.readyState === "loading" && (jsPath = scripts[scripts.length - 1].src)) { - return jsPath; - } - if (jsPath = _getCurrentScriptUrlFromError()) { - return jsPath; - } - return undefined; - }; - /** - * Get the unanimous parent directory of ALL script tags. - * If any script tags are either (a) inline or (b) from differing parent - * directories, this method must return `undefined`. - * - * @returns String or `undefined` - * @private - */ - var _getUnanimousScriptParentDir = function() { - var i, jsDir, jsPath, scripts = _document.getElementsByTagName("script"); - for (i = scripts.length; i--; ) { - if (!(jsPath = scripts[i].src)) { - jsDir = null; - break; - } - jsPath = _getDirPathOfUrl(jsPath); - if (jsDir == null) { - jsDir = jsPath; - } else if (jsDir !== jsPath) { - jsDir = null; - break; - } - } - return jsDir || undefined; - }; - /** - * Get the presumed location of the "ZeroClipboard.swf" file, based on the location - * of the executing JavaScript file (e.g. "ZeroClipboard.js", etc.). - * - * @returns String - * @private - */ - var _getDefaultSwfPath = function() { - var jsDir = _getDirPathOfUrl(_getCurrentScriptUrl()) || _getUnanimousScriptParentDir() || ""; - return jsDir + "ZeroClipboard.swf"; - }; - /** - * Keep track of if the page is framed (in an `iframe`). This can never change. - * @private - */ - var _pageIsFramed = function() { - return window.opener == null && (!!window.top && window != window.top || !!window.parent && window != window.parent); - }(); - /** - * Keep track of the state of the Flash object. - * @private - */ - var _flashState = { - bridge: null, - version: "0.0.0", - pluginType: "unknown", - disabled: null, - outdated: null, - sandboxed: null, - unavailable: null, - degraded: null, - deactivated: null, - overdue: null, - ready: null - }; - /** - * The minimum Flash Player version required to use ZeroClipboard completely. - * @readonly - * @private - */ - var _minimumFlashVersion = "11.0.0"; - /** - * The ZeroClipboard library version number, as reported by Flash, at the time the SWF was compiled. - */ - var _zcSwfVersion; - /** - * Keep track of all event listener registrations. - * @private - */ - var _handlers = {}; - /** - * Keep track of the currently activated element. - * @private - */ - var _currentElement; - /** - * Keep track of the element that was activated when a `copy` process started. - * @private - */ - var _copyTarget; - /** - * Keep track of data for the pending clipboard transaction. - * @private - */ - var _clipData = {}; - /** - * Keep track of data formats for the pending clipboard transaction. - * @private - */ - var _clipDataFormatMap = null; - /** - * Keep track of the Flash availability check timeout. - * @private - */ - var _flashCheckTimeout = 0; - /** - * Keep track of SWF network errors interval polling. - * @private - */ - var _swfFallbackCheckInterval = 0; - /** - * The `message` store for events - * @private - */ - var _eventMessages = { - ready: "Flash communication is established", - error: { - "flash-disabled": "Flash is disabled or not installed. May also be attempting to run Flash in a sandboxed iframe, which is impossible.", - "flash-outdated": "Flash is too outdated to support ZeroClipboard", - "flash-sandboxed": "Attempting to run Flash in a sandboxed iframe, which is impossible", - "flash-unavailable": "Flash is unable to communicate bidirectionally with JavaScript", - "flash-degraded": "Flash is unable to preserve data fidelity when communicating with JavaScript", - "flash-deactivated": "Flash is too outdated for your browser and/or is configured as click-to-activate.\nThis may also mean that the ZeroClipboard SWF object could not be loaded, so please check your `swfPath` configuration and/or network connectivity.\nMay also be attempting to run Flash in a sandboxed iframe, which is impossible.", - "flash-overdue": "Flash communication was established but NOT within the acceptable time limit", - "version-mismatch": "ZeroClipboard JS version number does not match ZeroClipboard SWF version number", - "clipboard-error": "At least one error was thrown while ZeroClipboard was attempting to inject your data into the clipboard", - "config-mismatch": "ZeroClipboard configuration does not match Flash's reality", - "swf-not-found": "The ZeroClipboard SWF object could not be loaded, so please check your `swfPath` configuration and/or network connectivity" - } - }; - /** - * The `name`s of `error` events that can only occur is Flash has at least - * been able to load the SWF successfully. - * @private - */ - var _errorsThatOnlyOccurAfterFlashLoads = [ "flash-unavailable", "flash-degraded", "flash-overdue", "version-mismatch", "config-mismatch", "clipboard-error" ]; - /** - * The `name`s of `error` events that should likely result in the `_flashState` - * variable's property values being updated. - * @private - */ - var _flashStateErrorNames = [ "flash-disabled", "flash-outdated", "flash-sandboxed", "flash-unavailable", "flash-degraded", "flash-deactivated", "flash-overdue" ]; - /** - * A RegExp to match the `name` property of `error` events related to Flash. - * @private - */ - var _flashStateErrorNameMatchingRegex = new RegExp("^flash-(" + _flashStateErrorNames.map(function(errorName) { - return errorName.replace(/^flash-/, ""); - }).join("|") + ")$"); - /** - * A RegExp to match the `name` property of `error` events related to Flash, - * which is enabled. - * @private - */ - var _flashStateEnabledErrorNameMatchingRegex = new RegExp("^flash-(" + _flashStateErrorNames.slice(1).map(function(errorName) { - return errorName.replace(/^flash-/, ""); - }).join("|") + ")$"); - /** - * ZeroClipboard configuration defaults for the Core module. - * @private - */ - var _globalConfig = { - swfPath: _getDefaultSwfPath(), - trustedDomains: window.location.host ? [ window.location.host ] : [], - cacheBust: true, - forceEnhancedClipboard: false, - flashLoadTimeout: 3e4, - autoActivate: true, - bubbleEvents: true, - containerId: "global-zeroclipboard-html-bridge", - containerClass: "global-zeroclipboard-container", - swfObjectId: "global-zeroclipboard-flash-bridge", - hoverClass: "zeroclipboard-is-hover", - activeClass: "zeroclipboard-is-active", - forceHandCursor: false, - title: null, - zIndex: 999999999 - }; - /** - * The underlying implementation of `ZeroClipboard.config`. - * @private - */ - var _config = function(options) { - if (typeof options === "object" && options !== null) { - for (var prop in options) { - if (_hasOwn.call(options, prop)) { - if (/^(?:forceHandCursor|title|zIndex|bubbleEvents)$/.test(prop)) { - _globalConfig[prop] = options[prop]; - } else if (_flashState.bridge == null) { - if (prop === "containerId" || prop === "swfObjectId") { - if (_isValidHtml4Id(options[prop])) { - _globalConfig[prop] = options[prop]; - } else { - throw new Error("The specified `" + prop + "` value is not valid as an HTML4 Element ID"); - } - } else { - _globalConfig[prop] = options[prop]; - } - } - } - } - } - if (typeof options === "string" && options) { - if (_hasOwn.call(_globalConfig, options)) { - return _globalConfig[options]; - } - return; - } - return _deepCopy(_globalConfig); - }; - /** - * The underlying implementation of `ZeroClipboard.state`. - * @private - */ - var _state = function() { - _detectSandbox(); - return { - browser: _pick(_navigator, [ "userAgent", "platform", "appName" ]), - flash: _omit(_flashState, [ "bridge" ]), - zeroclipboard: { - version: ZeroClipboard.version, - config: ZeroClipboard.config() - } - }; - }; - /** - * The underlying implementation of `ZeroClipboard.isFlashUnusable`. - * @private - */ - var _isFlashUnusable = function() { - return !!(_flashState.disabled || _flashState.outdated || _flashState.sandboxed || _flashState.unavailable || _flashState.degraded || _flashState.deactivated); - }; - /** - * The underlying implementation of `ZeroClipboard.on`. - * @private - */ - var _on = function(eventType, listener) { - var i, len, events, added = {}; - if (typeof eventType === "string" && eventType) { - events = eventType.toLowerCase().split(/\s+/); - } else if (typeof eventType === "object" && eventType && typeof listener === "undefined") { - for (i in eventType) { - if (_hasOwn.call(eventType, i) && typeof i === "string" && i && typeof eventType[i] === "function") { - ZeroClipboard.on(i, eventType[i]); - } - } - } - if (events && events.length) { - for (i = 0, len = events.length; i < len; i++) { - eventType = events[i].replace(/^on/, ""); - added[eventType] = true; - if (!_handlers[eventType]) { - _handlers[eventType] = []; - } - _handlers[eventType].push(listener); - } - if (added.ready && _flashState.ready) { - ZeroClipboard.emit({ - type: "ready" - }); - } - if (added.error) { - for (i = 0, len = _flashStateErrorNames.length; i < len; i++) { - if (_flashState[_flashStateErrorNames[i].replace(/^flash-/, "")] === true) { - ZeroClipboard.emit({ - type: "error", - name: _flashStateErrorNames[i] - }); - break; - } - } - if (_zcSwfVersion !== undefined && ZeroClipboard.version !== _zcSwfVersion) { - ZeroClipboard.emit({ - type: "error", - name: "version-mismatch", - jsVersion: ZeroClipboard.version, - swfVersion: _zcSwfVersion - }); - } - } - } - return ZeroClipboard; - }; - /** - * The underlying implementation of `ZeroClipboard.off`. - * @private - */ - var _off = function(eventType, listener) { - var i, len, foundIndex, events, perEventHandlers; - if (arguments.length === 0) { - events = _keys(_handlers); - } else if (typeof eventType === "string" && eventType) { - events = eventType.split(/\s+/); - } else if (typeof eventType === "object" && eventType && typeof listener === "undefined") { - for (i in eventType) { - if (_hasOwn.call(eventType, i) && typeof i === "string" && i && typeof eventType[i] === "function") { - ZeroClipboard.off(i, eventType[i]); - } - } - } - if (events && events.length) { - for (i = 0, len = events.length; i < len; i++) { - eventType = events[i].toLowerCase().replace(/^on/, ""); - perEventHandlers = _handlers[eventType]; - if (perEventHandlers && perEventHandlers.length) { - if (listener) { - foundIndex = perEventHandlers.indexOf(listener); - while (foundIndex !== -1) { - perEventHandlers.splice(foundIndex, 1); - foundIndex = perEventHandlers.indexOf(listener, foundIndex); - } - } else { - perEventHandlers.length = 0; - } - } - } - } - return ZeroClipboard; - }; - /** - * The underlying implementation of `ZeroClipboard.handlers`. - * @private - */ - var _listeners = function(eventType) { - var copy; - if (typeof eventType === "string" && eventType) { - copy = _deepCopy(_handlers[eventType]) || null; - } else { - copy = _deepCopy(_handlers); - } - return copy; - }; - /** - * The underlying implementation of `ZeroClipboard.emit`. - * @private - */ - var _emit = function(event) { - var eventCopy, returnVal, tmp; - event = _createEvent(event); - if (!event) { - return; - } - if (_preprocessEvent(event)) { - return; - } - if (event.type === "ready" && _flashState.overdue === true) { - return ZeroClipboard.emit({ - type: "error", - name: "flash-overdue" - }); - } - eventCopy = _extend({}, event); - _dispatchCallbacks.call(this, eventCopy); - if (event.type === "copy") { - tmp = _mapClipDataToFlash(_clipData); - returnVal = tmp.data; - _clipDataFormatMap = tmp.formatMap; - } - return returnVal; - }; - /** - * The underlying implementation of `ZeroClipboard.create`. - * @private - */ - var _create = function() { - var previousState = _flashState.sandboxed; - _detectSandbox(); - if (typeof _flashState.ready !== "boolean") { - _flashState.ready = false; - } - if (_flashState.sandboxed !== previousState && _flashState.sandboxed === true) { - _flashState.ready = false; - ZeroClipboard.emit({ - type: "error", - name: "flash-sandboxed" - }); - } else if (!ZeroClipboard.isFlashUnusable() && _flashState.bridge === null) { - var maxWait = _globalConfig.flashLoadTimeout; - if (typeof maxWait === "number" && maxWait >= 0) { - _flashCheckTimeout = _setTimeout(function() { - if (typeof _flashState.deactivated !== "boolean") { - _flashState.deactivated = true; - } - if (_flashState.deactivated === true) { - ZeroClipboard.emit({ - type: "error", - name: "flash-deactivated" - }); - } - }, maxWait); - } - _flashState.overdue = false; - _embedSwf(); - } - }; - /** - * The underlying implementation of `ZeroClipboard.destroy`. - * @private - */ - var _destroy = function() { - ZeroClipboard.clearData(); - ZeroClipboard.blur(); - ZeroClipboard.emit("destroy"); - _unembedSwf(); - ZeroClipboard.off(); - }; - /** - * The underlying implementation of `ZeroClipboard.setData`. - * @private - */ - var _setData = function(format, data) { - var dataObj; - if (typeof format === "object" && format && typeof data === "undefined") { - dataObj = format; - ZeroClipboard.clearData(); - } else if (typeof format === "string" && format) { - dataObj = {}; - dataObj[format] = data; - } else { - return; - } - for (var dataFormat in dataObj) { - if (typeof dataFormat === "string" && dataFormat && _hasOwn.call(dataObj, dataFormat) && typeof dataObj[dataFormat] === "string" && dataObj[dataFormat]) { - _clipData[dataFormat] = dataObj[dataFormat]; - } - } - }; - /** - * The underlying implementation of `ZeroClipboard.clearData`. - * @private - */ - var _clearData = function(format) { - if (typeof format === "undefined") { - _deleteOwnProperties(_clipData); - _clipDataFormatMap = null; - } else if (typeof format === "string" && _hasOwn.call(_clipData, format)) { - delete _clipData[format]; - } - }; - /** - * The underlying implementation of `ZeroClipboard.getData`. - * @private - */ - var _getData = function(format) { - if (typeof format === "undefined") { - return _deepCopy(_clipData); - } else if (typeof format === "string" && _hasOwn.call(_clipData, format)) { - return _clipData[format]; - } - }; - /** - * The underlying implementation of `ZeroClipboard.focus`/`ZeroClipboard.activate`. - * @private - */ - var _focus = function(element) { - if (!(element && element.nodeType === 1)) { - return; - } - if (_currentElement) { - _removeClass(_currentElement, _globalConfig.activeClass); - if (_currentElement !== element) { - _removeClass(_currentElement, _globalConfig.hoverClass); - } - } - _currentElement = element; - _addClass(element, _globalConfig.hoverClass); - var newTitle = element.getAttribute("title") || _globalConfig.title; - if (typeof newTitle === "string" && newTitle) { - var htmlBridge = _getHtmlBridge(_flashState.bridge); - if (htmlBridge) { - htmlBridge.setAttribute("title", newTitle); - } - } - var useHandCursor = _globalConfig.forceHandCursor === true || _getStyle(element, "cursor") === "pointer"; - _setHandCursor(useHandCursor); - _reposition(); - }; - /** - * The underlying implementation of `ZeroClipboard.blur`/`ZeroClipboard.deactivate`. - * @private - */ - var _blur = function() { - var htmlBridge = _getHtmlBridge(_flashState.bridge); - if (htmlBridge) { - htmlBridge.removeAttribute("title"); - htmlBridge.style.left = "0px"; - htmlBridge.style.top = "-9999px"; - htmlBridge.style.width = "1px"; - htmlBridge.style.height = "1px"; - } - if (_currentElement) { - _removeClass(_currentElement, _globalConfig.hoverClass); - _removeClass(_currentElement, _globalConfig.activeClass); - _currentElement = null; - } - }; - /** - * The underlying implementation of `ZeroClipboard.activeElement`. - * @private - */ - var _activeElement = function() { - return _currentElement || null; - }; - /** - * Check if a value is a valid HTML4 `ID` or `Name` token. - * @private - */ - var _isValidHtml4Id = function(id) { - return typeof id === "string" && id && /^[A-Za-z][A-Za-z0-9_:\-\.]*$/.test(id); - }; - /** - * Create or update an `event` object, based on the `eventType`. - * @private - */ - var _createEvent = function(event) { - var eventType; - if (typeof event === "string" && event) { - eventType = event; - event = {}; - } else if (typeof event === "object" && event && typeof event.type === "string" && event.type) { - eventType = event.type; - } - if (!eventType) { - return; - } - eventType = eventType.toLowerCase(); - if (!event.target && (/^(copy|aftercopy|_click)$/.test(eventType) || eventType === "error" && event.name === "clipboard-error")) { - event.target = _copyTarget; - } - _extend(event, { - type: eventType, - target: event.target || _currentElement || null, - relatedTarget: event.relatedTarget || null, - currentTarget: _flashState && _flashState.bridge || null, - timeStamp: event.timeStamp || _now() || null - }); - var msg = _eventMessages[event.type]; - if (event.type === "error" && event.name && msg) { - msg = msg[event.name]; - } - if (msg) { - event.message = msg; - } - if (event.type === "ready") { - _extend(event, { - target: null, - version: _flashState.version - }); - } - if (event.type === "error") { - if (_flashStateErrorNameMatchingRegex.test(event.name)) { - _extend(event, { - target: null, - minimumVersion: _minimumFlashVersion - }); - } - if (_flashStateEnabledErrorNameMatchingRegex.test(event.name)) { - _extend(event, { - version: _flashState.version - }); - } - } - if (event.type === "copy") { - event.clipboardData = { - setData: ZeroClipboard.setData, - clearData: ZeroClipboard.clearData - }; - } - if (event.type === "aftercopy") { - event = _mapClipResultsFromFlash(event, _clipDataFormatMap); - } - if (event.target && !event.relatedTarget) { - event.relatedTarget = _getRelatedTarget(event.target); - } - return _addMouseData(event); - }; - /** - * Get a relatedTarget from the target's `data-clipboard-target` attribute - * @private - */ - var _getRelatedTarget = function(targetEl) { - var relatedTargetId = targetEl && targetEl.getAttribute && targetEl.getAttribute("data-clipboard-target"); - return relatedTargetId ? _document.getElementById(relatedTargetId) : null; - }; - /** - * Add element and position data to `MouseEvent` instances - * @private - */ - var _addMouseData = function(event) { - if (event && /^_(?:click|mouse(?:over|out|down|up|move))$/.test(event.type)) { - var srcElement = event.target; - var fromElement = event.type === "_mouseover" && event.relatedTarget ? event.relatedTarget : undefined; - var toElement = event.type === "_mouseout" && event.relatedTarget ? event.relatedTarget : undefined; - var pos = _getElementPosition(srcElement); - var screenLeft = _window.screenLeft || _window.screenX || 0; - var screenTop = _window.screenTop || _window.screenY || 0; - var scrollLeft = _document.body.scrollLeft + _document.documentElement.scrollLeft; - var scrollTop = _document.body.scrollTop + _document.documentElement.scrollTop; - var pageX = pos.left + (typeof event._stageX === "number" ? event._stageX : 0); - var pageY = pos.top + (typeof event._stageY === "number" ? event._stageY : 0); - var clientX = pageX - scrollLeft; - var clientY = pageY - scrollTop; - var screenX = screenLeft + clientX; - var screenY = screenTop + clientY; - var moveX = typeof event.movementX === "number" ? event.movementX : 0; - var moveY = typeof event.movementY === "number" ? event.movementY : 0; - delete event._stageX; - delete event._stageY; - _extend(event, { - srcElement: srcElement, - fromElement: fromElement, - toElement: toElement, - screenX: screenX, - screenY: screenY, - pageX: pageX, - pageY: pageY, - clientX: clientX, - clientY: clientY, - x: clientX, - y: clientY, - movementX: moveX, - movementY: moveY, - offsetX: 0, - offsetY: 0, - layerX: 0, - layerY: 0 - }); - } - return event; - }; - /** - * Determine if an event's registered handlers should be execute synchronously or asynchronously. - * - * @returns {boolean} - * @private - */ - var _shouldPerformAsync = function(event) { - var eventType = event && typeof event.type === "string" && event.type || ""; - return !/^(?:(?:before)?copy|destroy)$/.test(eventType); - }; - /** - * Control if a callback should be executed asynchronously or not. - * - * @returns `undefined` - * @private - */ - var _dispatchCallback = function(func, context, args, async) { - if (async) { - _setTimeout(function() { - func.apply(context, args); - }, 0); - } else { - func.apply(context, args); - } - }; - /** - * Handle the actual dispatching of events to client instances. - * - * @returns `undefined` - * @private - */ - var _dispatchCallbacks = function(event) { - if (!(typeof event === "object" && event && event.type)) { - return; - } - var async = _shouldPerformAsync(event); - var wildcardTypeHandlers = _handlers["*"] || []; - var specificTypeHandlers = _handlers[event.type] || []; - var handlers = wildcardTypeHandlers.concat(specificTypeHandlers); - if (handlers && handlers.length) { - var i, len, func, context, eventCopy, originalContext = this; - for (i = 0, len = handlers.length; i < len; i++) { - func = handlers[i]; - context = originalContext; - if (typeof func === "string" && typeof _window[func] === "function") { - func = _window[func]; - } - if (typeof func === "object" && func && typeof func.handleEvent === "function") { - context = func; - func = func.handleEvent; - } - if (typeof func === "function") { - eventCopy = _extend({}, event); - _dispatchCallback(func, context, [ eventCopy ], async); - } - } - } - return this; - }; - /** - * Check an `error` event's `name` property to see if Flash has - * already loaded, which rules out possible `iframe` sandboxing. - * @private - */ - var _getSandboxStatusFromErrorEvent = function(event) { - var isSandboxed = null; - if (_pageIsFramed === false || event && event.type === "error" && event.name && _errorsThatOnlyOccurAfterFlashLoads.indexOf(event.name) !== -1) { - isSandboxed = false; - } - return isSandboxed; - }; - /** - * Preprocess any special behaviors, reactions, or state changes after receiving this event. - * Executes only once per event emitted, NOT once per client. - * @private - */ - var _preprocessEvent = function(event) { - var element = event.target || _currentElement || null; - var sourceIsSwf = event._source === "swf"; - delete event._source; - switch (event.type) { - case "error": - var isSandboxed = event.name === "flash-sandboxed" || _getSandboxStatusFromErrorEvent(event); - if (typeof isSandboxed === "boolean") { - _flashState.sandboxed = isSandboxed; - } - if (_flashStateErrorNames.indexOf(event.name) !== -1) { - _extend(_flashState, { - disabled: event.name === "flash-disabled", - outdated: event.name === "flash-outdated", - unavailable: event.name === "flash-unavailable", - degraded: event.name === "flash-degraded", - deactivated: event.name === "flash-deactivated", - overdue: event.name === "flash-overdue", - ready: false - }); - } else if (event.name === "version-mismatch") { - _zcSwfVersion = event.swfVersion; - _extend(_flashState, { - disabled: false, - outdated: false, - unavailable: false, - degraded: false, - deactivated: false, - overdue: false, - ready: false - }); - } - _clearTimeoutsAndPolling(); - break; - - case "ready": - _zcSwfVersion = event.swfVersion; - var wasDeactivated = _flashState.deactivated === true; - _extend(_flashState, { - disabled: false, - outdated: false, - sandboxed: false, - unavailable: false, - degraded: false, - deactivated: false, - overdue: wasDeactivated, - ready: !wasDeactivated - }); - _clearTimeoutsAndPolling(); - break; - - case "beforecopy": - _copyTarget = element; - break; - - case "copy": - var textContent, htmlContent, targetEl = event.relatedTarget; - if (!(_clipData["text/html"] || _clipData["text/plain"]) && targetEl && (htmlContent = targetEl.value || targetEl.outerHTML || targetEl.innerHTML) && (textContent = targetEl.value || targetEl.textContent || targetEl.innerText)) { - event.clipboardData.clearData(); - event.clipboardData.setData("text/plain", textContent); - if (htmlContent !== textContent) { - event.clipboardData.setData("text/html", htmlContent); - } - } else if (!_clipData["text/plain"] && event.target && (textContent = event.target.getAttribute("data-clipboard-text"))) { - event.clipboardData.clearData(); - event.clipboardData.setData("text/plain", textContent); - } - break; - - case "aftercopy": - _queueEmitClipboardErrors(event); - ZeroClipboard.clearData(); - if (element && element !== _safeActiveElement() && element.focus) { - element.focus(); - } - break; - - case "_mouseover": - ZeroClipboard.focus(element); - if (_globalConfig.bubbleEvents === true && sourceIsSwf) { - if (element && element !== event.relatedTarget && !_containedBy(event.relatedTarget, element)) { - _fireMouseEvent(_extend({}, event, { - type: "mouseenter", - bubbles: false, - cancelable: false - })); - } - _fireMouseEvent(_extend({}, event, { - type: "mouseover" - })); - } - break; - - case "_mouseout": - ZeroClipboard.blur(); - if (_globalConfig.bubbleEvents === true && sourceIsSwf) { - if (element && element !== event.relatedTarget && !_containedBy(event.relatedTarget, element)) { - _fireMouseEvent(_extend({}, event, { - type: "mouseleave", - bubbles: false, - cancelable: false - })); - } - _fireMouseEvent(_extend({}, event, { - type: "mouseout" - })); - } - break; - - case "_mousedown": - _addClass(element, _globalConfig.activeClass); - if (_globalConfig.bubbleEvents === true && sourceIsSwf) { - _fireMouseEvent(_extend({}, event, { - type: event.type.slice(1) - })); - } - break; - - case "_mouseup": - _removeClass(element, _globalConfig.activeClass); - if (_globalConfig.bubbleEvents === true && sourceIsSwf) { - _fireMouseEvent(_extend({}, event, { - type: event.type.slice(1) - })); - } - break; - - case "_click": - _copyTarget = null; - if (_globalConfig.bubbleEvents === true && sourceIsSwf) { - _fireMouseEvent(_extend({}, event, { - type: event.type.slice(1) - })); - } - break; - - case "_mousemove": - if (_globalConfig.bubbleEvents === true && sourceIsSwf) { - _fireMouseEvent(_extend({}, event, { - type: event.type.slice(1) - })); - } - break; - } - if (/^_(?:click|mouse(?:over|out|down|up|move))$/.test(event.type)) { - return true; - } - }; - /** - * Check an "aftercopy" event for clipboard errors and emit a corresponding "error" event. - * @private - */ - var _queueEmitClipboardErrors = function(aftercopyEvent) { - if (aftercopyEvent.errors && aftercopyEvent.errors.length > 0) { - var errorEvent = _deepCopy(aftercopyEvent); - _extend(errorEvent, { - type: "error", - name: "clipboard-error" - }); - delete errorEvent.success; - _setTimeout(function() { - ZeroClipboard.emit(errorEvent); - }, 0); - } - }; - /** - * Dispatch a synthetic MouseEvent. - * - * @returns `undefined` - * @private - */ - var _fireMouseEvent = function(event) { - if (!(event && typeof event.type === "string" && event)) { - return; - } - var e, target = event.target || null, doc = target && target.ownerDocument || _document, defaults = { - view: doc.defaultView || _window, - canBubble: true, - cancelable: true, - detail: event.type === "click" ? 1 : 0, - button: typeof event.which === "number" ? event.which - 1 : typeof event.button === "number" ? event.button : doc.createEvent ? 0 : 1 - }, args = _extend(defaults, event); - if (!target) { - return; - } - if (doc.createEvent && target.dispatchEvent) { - args = [ args.type, args.canBubble, args.cancelable, args.view, args.detail, args.screenX, args.screenY, args.clientX, args.clientY, args.ctrlKey, args.altKey, args.shiftKey, args.metaKey, args.button, args.relatedTarget ]; - e = doc.createEvent("MouseEvents"); - if (e.initMouseEvent) { - e.initMouseEvent.apply(e, args); - e._source = "js"; - target.dispatchEvent(e); - } - } - }; - /** - * Continuously poll the DOM until either: - * (a) the fallback content becomes visible, or - * (b) we receive an event from SWF (handled elsewhere) - * - * IMPORTANT: - * This is NOT a necessary check but it can result in significantly faster - * detection of bad `swfPath` configuration and/or network/server issues [in - * supported browsers] than waiting for the entire `flashLoadTimeout` duration - * to elapse before detecting that the SWF cannot be loaded. The detection - * duration can be anywhere from 10-30 times faster [in supported browsers] by - * using this approach. - * - * @returns `undefined` - * @private - */ - var _watchForSwfFallbackContent = function() { - var maxWait = _globalConfig.flashLoadTimeout; - if (typeof maxWait === "number" && maxWait >= 0) { - var pollWait = Math.min(1e3, maxWait / 10); - var fallbackContentId = _globalConfig.swfObjectId + "_fallbackContent"; - _swfFallbackCheckInterval = _setInterval(function() { - var el = _document.getElementById(fallbackContentId); - if (_isElementVisible(el)) { - _clearTimeoutsAndPolling(); - _flashState.deactivated = null; - ZeroClipboard.emit({ - type: "error", - name: "swf-not-found" - }); - } - }, pollWait); - } - }; - /** - * Create the HTML bridge element to embed the Flash object into. - * @private - */ - var _createHtmlBridge = function() { - var container = _document.createElement("div"); - container.id = _globalConfig.containerId; - container.className = _globalConfig.containerClass; - container.style.position = "absolute"; - container.style.left = "0px"; - container.style.top = "-9999px"; - container.style.width = "1px"; - container.style.height = "1px"; - container.style.zIndex = "" + _getSafeZIndex(_globalConfig.zIndex); - return container; - }; - /** - * Get the HTML element container that wraps the Flash bridge object/element. - * @private - */ - var _getHtmlBridge = function(flashBridge) { - var htmlBridge = flashBridge && flashBridge.parentNode; - while (htmlBridge && htmlBridge.nodeName === "OBJECT" && htmlBridge.parentNode) { - htmlBridge = htmlBridge.parentNode; - } - return htmlBridge || null; - }; - /** - * Create the SWF object. - * - * @returns The SWF object reference. - * @private - */ - var _embedSwf = function() { - var len, flashBridge = _flashState.bridge, container = _getHtmlBridge(flashBridge); - if (!flashBridge) { - var allowScriptAccess = _determineScriptAccess(_window.location.host, _globalConfig); - var allowNetworking = allowScriptAccess === "never" ? "none" : "all"; - var flashvars = _vars(_extend({ - jsVersion: ZeroClipboard.version - }, _globalConfig)); - var swfUrl = _globalConfig.swfPath + _cacheBust(_globalConfig.swfPath, _globalConfig); - container = _createHtmlBridge(); - var divToBeReplaced = _document.createElement("div"); - container.appendChild(divToBeReplaced); - _document.body.appendChild(container); - var tmpDiv = _document.createElement("div"); - var usingActiveX = _flashState.pluginType === "activex"; - tmpDiv.innerHTML = '<object id="' + _globalConfig.swfObjectId + '" name="' + _globalConfig.swfObjectId + '" ' + 'width="100%" height="100%" ' + (usingActiveX ? 'classid="clsid:d27cdb6e-ae6d-11cf-96b8-444553540000"' : 'type="application/x-shockwave-flash" data="' + swfUrl + '"') + ">" + (usingActiveX ? '<param name="movie" value="' + swfUrl + '"/>' : "") + '<param name="allowScriptAccess" value="' + allowScriptAccess + '"/>' + '<param name="allowNetworking" value="' + allowNetworking + '"/>' + '<param name="menu" value="false"/>' + '<param name="wmode" value="transparent"/>' + '<param name="flashvars" value="' + flashvars + '"/>' + '<div id="' + _globalConfig.swfObjectId + '_fallbackContent"> </div>' + "</object>"; - flashBridge = tmpDiv.firstChild; - tmpDiv = null; - _unwrap(flashBridge).ZeroClipboard = ZeroClipboard; - container.replaceChild(flashBridge, divToBeReplaced); - _watchForSwfFallbackContent(); - } - if (!flashBridge) { - flashBridge = _document[_globalConfig.swfObjectId]; - if (flashBridge && (len = flashBridge.length)) { - flashBridge = flashBridge[len - 1]; - } - if (!flashBridge && container) { - flashBridge = container.firstChild; - } - } - _flashState.bridge = flashBridge || null; - return flashBridge; - }; - /** - * Destroy the SWF object. - * @private - */ - var _unembedSwf = function() { - var flashBridge = _flashState.bridge; - if (flashBridge) { - var htmlBridge = _getHtmlBridge(flashBridge); - if (htmlBridge) { - if (_flashState.pluginType === "activex" && "readyState" in flashBridge) { - flashBridge.style.display = "none"; - (function removeSwfFromIE() { - if (flashBridge.readyState === 4) { - for (var prop in flashBridge) { - if (typeof flashBridge[prop] === "function") { - flashBridge[prop] = null; - } - } - if (flashBridge.parentNode) { - flashBridge.parentNode.removeChild(flashBridge); - } - if (htmlBridge.parentNode) { - htmlBridge.parentNode.removeChild(htmlBridge); - } - } else { - _setTimeout(removeSwfFromIE, 10); - } - })(); - } else { - if (flashBridge.parentNode) { - flashBridge.parentNode.removeChild(flashBridge); - } - if (htmlBridge.parentNode) { - htmlBridge.parentNode.removeChild(htmlBridge); - } - } - } - _clearTimeoutsAndPolling(); - _flashState.ready = null; - _flashState.bridge = null; - _flashState.deactivated = null; - _zcSwfVersion = undefined; - } - }; - /** - * Map the data format names of the "clipData" to Flash-friendly names. - * - * @returns A new transformed object. - * @private - */ - var _mapClipDataToFlash = function(clipData) { - var newClipData = {}, formatMap = {}; - if (!(typeof clipData === "object" && clipData)) { - return; - } - for (var dataFormat in clipData) { - if (dataFormat && _hasOwn.call(clipData, dataFormat) && typeof clipData[dataFormat] === "string" && clipData[dataFormat]) { - switch (dataFormat.toLowerCase()) { - case "text/plain": - case "text": - case "air:text": - case "flash:text": - newClipData.text = clipData[dataFormat]; - formatMap.text = dataFormat; - break; - - case "text/html": - case "html": - case "air:html": - case "flash:html": - newClipData.html = clipData[dataFormat]; - formatMap.html = dataFormat; - break; - - case "application/rtf": - case "text/rtf": - case "rtf": - case "richtext": - case "air:rtf": - case "flash:rtf": - newClipData.rtf = clipData[dataFormat]; - formatMap.rtf = dataFormat; - break; - - default: - break; - } - } - } - return { - data: newClipData, - formatMap: formatMap - }; - }; - /** - * Map the data format names from Flash-friendly names back to their original "clipData" names (via a format mapping). - * - * @returns A new transformed object. - * @private - */ - var _mapClipResultsFromFlash = function(clipResults, formatMap) { - if (!(typeof clipResults === "object" && clipResults && typeof formatMap === "object" && formatMap)) { - return clipResults; - } - var newResults = {}; - for (var prop in clipResults) { - if (_hasOwn.call(clipResults, prop)) { - if (prop === "errors") { - newResults[prop] = clipResults[prop] ? clipResults[prop].slice() : []; - for (var i = 0, len = newResults[prop].length; i < len; i++) { - newResults[prop][i].format = formatMap[newResults[prop][i].format]; - } - } else if (prop !== "success" && prop !== "data") { - newResults[prop] = clipResults[prop]; - } else { - newResults[prop] = {}; - var tmpHash = clipResults[prop]; - for (var dataFormat in tmpHash) { - if (dataFormat && _hasOwn.call(tmpHash, dataFormat) && _hasOwn.call(formatMap, dataFormat)) { - newResults[prop][formatMap[dataFormat]] = tmpHash[dataFormat]; - } - } - } - } - } - return newResults; - }; - /** - * Will look at a path, and will create a "?noCache={time}" or "&noCache={time}" - * query param string to return. Does NOT append that string to the original path. - * This is useful because ExternalInterface often breaks when a Flash SWF is cached. - * - * @returns The `noCache` query param with necessary "?"/"&" prefix. - * @private - */ - var _cacheBust = function(path, options) { - var cacheBust = options == null || options && options.cacheBust === true; - if (cacheBust) { - return (path.indexOf("?") === -1 ? "?" : "&") + "noCache=" + _now(); - } else { - return ""; - } - }; - /** - * Creates a query string for the FlashVars param. - * Does NOT include the cache-busting query param. - * - * @returns FlashVars query string - * @private - */ - var _vars = function(options) { - var i, len, domain, domains, str = "", trustedOriginsExpanded = []; - if (options.trustedDomains) { - if (typeof options.trustedDomains === "string") { - domains = [ options.trustedDomains ]; - } else if (typeof options.trustedDomains === "object" && "length" in options.trustedDomains) { - domains = options.trustedDomains; - } - } - if (domains && domains.length) { - for (i = 0, len = domains.length; i < len; i++) { - if (_hasOwn.call(domains, i) && domains[i] && typeof domains[i] === "string") { - domain = _extractDomain(domains[i]); - if (!domain) { - continue; - } - if (domain === "*") { - trustedOriginsExpanded.length = 0; - trustedOriginsExpanded.push(domain); - break; - } - trustedOriginsExpanded.push.apply(trustedOriginsExpanded, [ domain, "//" + domain, _window.location.protocol + "//" + domain ]); - } - } - } - if (trustedOriginsExpanded.length) { - str += "trustedOrigins=" + _encodeURIComponent(trustedOriginsExpanded.join(",")); - } - if (options.forceEnhancedClipboard === true) { - str += (str ? "&" : "") + "forceEnhancedClipboard=true"; - } - if (typeof options.swfObjectId === "string" && options.swfObjectId) { - str += (str ? "&" : "") + "swfObjectId=" + _encodeURIComponent(options.swfObjectId); - } - if (typeof options.jsVersion === "string" && options.jsVersion) { - str += (str ? "&" : "") + "jsVersion=" + _encodeURIComponent(options.jsVersion); - } - return str; - }; - /** - * Extract the domain (e.g. "github.com") from an origin (e.g. "https://github.com") or - * URL (e.g. "https://github.com/zeroclipboard/zeroclipboard/"). - * - * @returns the domain - * @private - */ - var _extractDomain = function(originOrUrl) { - if (originOrUrl == null || originOrUrl === "") { - return null; - } - originOrUrl = originOrUrl.replace(/^\s+|\s+$/g, ""); - if (originOrUrl === "") { - return null; - } - var protocolIndex = originOrUrl.indexOf("//"); - originOrUrl = protocolIndex === -1 ? originOrUrl : originOrUrl.slice(protocolIndex + 2); - var pathIndex = originOrUrl.indexOf("/"); - originOrUrl = pathIndex === -1 ? originOrUrl : protocolIndex === -1 || pathIndex === 0 ? null : originOrUrl.slice(0, pathIndex); - if (originOrUrl && originOrUrl.slice(-4).toLowerCase() === ".swf") { - return null; - } - return originOrUrl || null; - }; - /** - * Set `allowScriptAccess` based on `trustedDomains` and `window.location.host` vs. `swfPath`. - * - * @returns The appropriate script access level. - * @private - */ - var _determineScriptAccess = function() { - var _extractAllDomains = function(origins) { - var i, len, tmp, resultsArray = []; - if (typeof origins === "string") { - origins = [ origins ]; - } - if (!(typeof origins === "object" && origins && typeof origins.length === "number")) { - return resultsArray; - } - for (i = 0, len = origins.length; i < len; i++) { - if (_hasOwn.call(origins, i) && (tmp = _extractDomain(origins[i]))) { - if (tmp === "*") { - resultsArray.length = 0; - resultsArray.push("*"); - break; - } - if (resultsArray.indexOf(tmp) === -1) { - resultsArray.push(tmp); - } - } - } - return resultsArray; - }; - return function(currentDomain, configOptions) { - var swfDomain = _extractDomain(configOptions.swfPath); - if (swfDomain === null) { - swfDomain = currentDomain; - } - var trustedDomains = _extractAllDomains(configOptions.trustedDomains); - var len = trustedDomains.length; - if (len > 0) { - if (len === 1 && trustedDomains[0] === "*") { - return "always"; - } - if (trustedDomains.indexOf(currentDomain) !== -1) { - if (len === 1 && currentDomain === swfDomain) { - return "sameDomain"; - } - return "always"; - } - } - return "never"; - }; - }(); - /** - * Get the currently active/focused DOM element. - * - * @returns the currently active/focused element, or `null` - * @private - */ - var _safeActiveElement = function() { - try { - return _document.activeElement; - } catch (err) { - return null; - } - }; - /** - * Add a class to an element, if it doesn't already have it. - * - * @returns The element, with its new class added. - * @private - */ - var _addClass = function(element, value) { - var c, cl, className, classNames = []; - if (typeof value === "string" && value) { - classNames = value.split(/\s+/); - } - if (element && element.nodeType === 1 && classNames.length > 0) { - if (element.classList) { - for (c = 0, cl = classNames.length; c < cl; c++) { - element.classList.add(classNames[c]); - } - } else if (element.hasOwnProperty("className")) { - className = " " + element.className + " "; - for (c = 0, cl = classNames.length; c < cl; c++) { - if (className.indexOf(" " + classNames[c] + " ") === -1) { - className += classNames[c] + " "; - } - } - element.className = className.replace(/^\s+|\s+$/g, ""); - } - } - return element; - }; - /** - * Remove a class from an element, if it has it. - * - * @returns The element, with its class removed. - * @private - */ - var _removeClass = function(element, value) { - var c, cl, className, classNames = []; - if (typeof value === "string" && value) { - classNames = value.split(/\s+/); - } - if (element && element.nodeType === 1 && classNames.length > 0) { - if (element.classList && element.classList.length > 0) { - for (c = 0, cl = classNames.length; c < cl; c++) { - element.classList.remove(classNames[c]); - } - } else if (element.className) { - className = (" " + element.className + " ").replace(/[\r\n\t]/g, " "); - for (c = 0, cl = classNames.length; c < cl; c++) { - className = className.replace(" " + classNames[c] + " ", " "); - } - element.className = className.replace(/^\s+|\s+$/g, ""); - } - } - return element; - }; - /** - * Attempt to interpret the element's CSS styling. If `prop` is `"cursor"`, - * then we assume that it should be a hand ("pointer") cursor if the element - * is an anchor element ("a" tag). - * - * @returns The computed style property. - * @private - */ - var _getStyle = function(el, prop) { - var value = _getComputedStyle(el, null).getPropertyValue(prop); - if (prop === "cursor") { - if (!value || value === "auto") { - if (el.nodeName === "A") { - return "pointer"; - } - } - } - return value; - }; - /** - * Get the absolutely positioned coordinates of a DOM element. - * - * @returns Object containing the element's position, width, and height. - * @private - */ - var _getElementPosition = function(el) { - var pos = { - left: 0, - top: 0, - width: 0, - height: 0 - }; - if (el.getBoundingClientRect) { - var elRect = el.getBoundingClientRect(); - var pageXOffset = _window.pageXOffset; - var pageYOffset = _window.pageYOffset; - var leftBorderWidth = _document.documentElement.clientLeft || 0; - var topBorderWidth = _document.documentElement.clientTop || 0; - var leftBodyOffset = 0; - var topBodyOffset = 0; - if (_getStyle(_document.body, "position") === "relative") { - var bodyRect = _document.body.getBoundingClientRect(); - var htmlRect = _document.documentElement.getBoundingClientRect(); - leftBodyOffset = bodyRect.left - htmlRect.left || 0; - topBodyOffset = bodyRect.top - htmlRect.top || 0; - } - pos.left = elRect.left + pageXOffset - leftBorderWidth - leftBodyOffset; - pos.top = elRect.top + pageYOffset - topBorderWidth - topBodyOffset; - pos.width = "width" in elRect ? elRect.width : elRect.right - elRect.left; - pos.height = "height" in elRect ? elRect.height : elRect.bottom - elRect.top; - } - return pos; - }; - /** - * Determine is an element is visible somewhere within the document (page). - * - * @returns Boolean - * @private - */ - var _isElementVisible = function(el) { - if (!el) { - return false; - } - var styles = _getComputedStyle(el, null); - var hasCssHeight = _parseFloat(styles.height) > 0; - var hasCssWidth = _parseFloat(styles.width) > 0; - var hasCssTop = _parseFloat(styles.top) >= 0; - var hasCssLeft = _parseFloat(styles.left) >= 0; - var cssKnows = hasCssHeight && hasCssWidth && hasCssTop && hasCssLeft; - var rect = cssKnows ? null : _getElementPosition(el); - var isVisible = styles.display !== "none" && styles.visibility !== "collapse" && (cssKnows || !!rect && (hasCssHeight || rect.height > 0) && (hasCssWidth || rect.width > 0) && (hasCssTop || rect.top >= 0) && (hasCssLeft || rect.left >= 0)); - return isVisible; - }; - /** - * Clear all existing timeouts and interval polling delegates. - * - * @returns `undefined` - * @private - */ - var _clearTimeoutsAndPolling = function() { - _clearTimeout(_flashCheckTimeout); - _flashCheckTimeout = 0; - _clearInterval(_swfFallbackCheckInterval); - _swfFallbackCheckInterval = 0; - }; - /** - * Reposition the Flash object to cover the currently activated element. - * - * @returns `undefined` - * @private - */ - var _reposition = function() { - var htmlBridge; - if (_currentElement && (htmlBridge = _getHtmlBridge(_flashState.bridge))) { - var pos = _getElementPosition(_currentElement); - _extend(htmlBridge.style, { - width: pos.width + "px", - height: pos.height + "px", - top: pos.top + "px", - left: pos.left + "px", - zIndex: "" + _getSafeZIndex(_globalConfig.zIndex) - }); - } - }; - /** - * Sends a signal to the Flash object to display the hand cursor if `true`. - * - * @returns `undefined` - * @private - */ - var _setHandCursor = function(enabled) { - if (_flashState.ready === true) { - if (_flashState.bridge && typeof _flashState.bridge.setHandCursor === "function") { - _flashState.bridge.setHandCursor(enabled); - } else { - _flashState.ready = false; - } - } - }; - /** - * Get a safe value for `zIndex` - * - * @returns an integer, or "auto" - * @private - */ - var _getSafeZIndex = function(val) { - if (/^(?:auto|inherit)$/.test(val)) { - return val; - } - var zIndex; - if (typeof val === "number" && !_isNaN(val)) { - zIndex = val; - } else if (typeof val === "string") { - zIndex = _getSafeZIndex(_parseInt(val, 10)); - } - return typeof zIndex === "number" ? zIndex : "auto"; - }; - /** - * Attempt to detect if ZeroClipboard is executing inside of a sandboxed iframe. - * If it is, Flash Player cannot be used, so ZeroClipboard is dead in the water. - * - * @see {@link http://lists.w3.org/Archives/Public/public-whatwg-archive/2014Dec/0002.html} - * @see {@link https://github.com/zeroclipboard/zeroclipboard/issues/511} - * @see {@link http://zeroclipboard.org/test-iframes.html} - * - * @returns `true` (is sandboxed), `false` (is not sandboxed), or `null` (uncertain) - * @private - */ - var _detectSandbox = function(doNotReassessFlashSupport) { - var effectiveScriptOrigin, frame, frameError, previousState = _flashState.sandboxed, isSandboxed = null; - doNotReassessFlashSupport = doNotReassessFlashSupport === true; - if (_pageIsFramed === false) { - isSandboxed = false; - } else { - try { - frame = window.frameElement || null; - } catch (e) { - frameError = { - name: e.name, - message: e.message - }; - } - if (frame && frame.nodeType === 1 && frame.nodeName === "IFRAME") { - try { - isSandboxed = frame.hasAttribute("sandbox"); - } catch (e) { - isSandboxed = null; - } - } else { - try { - effectiveScriptOrigin = document.domain || null; - } catch (e) { - effectiveScriptOrigin = null; - } - if (effectiveScriptOrigin === null || frameError && frameError.name === "SecurityError" && /(^|[\s\(\[@])sandbox(es|ed|ing|[\s\.,!\)\]@]|$)/.test(frameError.message.toLowerCase())) { - isSandboxed = true; - } - } - } - _flashState.sandboxed = isSandboxed; - if (previousState !== isSandboxed && !doNotReassessFlashSupport) { - _detectFlashSupport(_ActiveXObject); - } - return isSandboxed; - }; - /** - * Detect the Flash Player status, version, and plugin type. - * - * @see {@link https://code.google.com/p/doctype-mirror/wiki/ArticleDetectFlash#The_code} - * @see {@link http://stackoverflow.com/questions/12866060/detecting-pepper-ppapi-flash-with-javascript} - * - * @returns `undefined` - * @private - */ - var _detectFlashSupport = function(ActiveXObject) { - var plugin, ax, mimeType, hasFlash = false, isActiveX = false, isPPAPI = false, flashVersion = ""; - /** - * Derived from Apple's suggested sniffer. - * @param {String} desc e.g. "Shockwave Flash 7.0 r61" - * @returns {String} "7.0.61" - * @private - */ - function parseFlashVersion(desc) { - var matches = desc.match(/[\d]+/g); - matches.length = 3; - return matches.join("."); - } - function isPepperFlash(flashPlayerFileName) { - return !!flashPlayerFileName && (flashPlayerFileName = flashPlayerFileName.toLowerCase()) && (/^(pepflashplayer\.dll|libpepflashplayer\.so|pepperflashplayer\.plugin)$/.test(flashPlayerFileName) || flashPlayerFileName.slice(-13) === "chrome.plugin"); - } - function inspectPlugin(plugin) { - if (plugin) { - hasFlash = true; - if (plugin.version) { - flashVersion = parseFlashVersion(plugin.version); - } - if (!flashVersion && plugin.description) { - flashVersion = parseFlashVersion(plugin.description); - } - if (plugin.filename) { - isPPAPI = isPepperFlash(plugin.filename); - } - } - } - if (_navigator.plugins && _navigator.plugins.length) { - plugin = _navigator.plugins["Shockwave Flash"]; - inspectPlugin(plugin); - if (_navigator.plugins["Shockwave Flash 2.0"]) { - hasFlash = true; - flashVersion = "2.0.0.11"; - } - } else if (_navigator.mimeTypes && _navigator.mimeTypes.length) { - mimeType = _navigator.mimeTypes["application/x-shockwave-flash"]; - plugin = mimeType && mimeType.enabledPlugin; - inspectPlugin(plugin); - } else if (typeof ActiveXObject !== "undefined") { - isActiveX = true; - try { - ax = new ActiveXObject("ShockwaveFlash.ShockwaveFlash.7"); - hasFlash = true; - flashVersion = parseFlashVersion(ax.GetVariable("$version")); - } catch (e1) { - try { - ax = new ActiveXObject("ShockwaveFlash.ShockwaveFlash.6"); - hasFlash = true; - flashVersion = "6.0.21"; - } catch (e2) { - try { - ax = new ActiveXObject("ShockwaveFlash.ShockwaveFlash"); - hasFlash = true; - flashVersion = parseFlashVersion(ax.GetVariable("$version")); - } catch (e3) { - isActiveX = false; - } - } - } - } - _flashState.disabled = hasFlash !== true; - _flashState.outdated = flashVersion && _parseFloat(flashVersion) < _parseFloat(_minimumFlashVersion); - _flashState.version = flashVersion || "0.0.0"; - _flashState.pluginType = isPPAPI ? "pepper" : isActiveX ? "activex" : hasFlash ? "netscape" : "unknown"; - }; - /** - * Invoke the Flash detection algorithms immediately upon inclusion so we're not waiting later. - */ - _detectFlashSupport(_ActiveXObject); - /** - * Always assess the `sandboxed` state of the page at important Flash-related moments. - */ - _detectSandbox(true); - /** - * A shell constructor for `ZeroClipboard` client instances. - * - * @constructor - */ - var ZeroClipboard = function() { - if (!(this instanceof ZeroClipboard)) { - return new ZeroClipboard(); - } - if (typeof ZeroClipboard._createClient === "function") { - ZeroClipboard._createClient.apply(this, _args(arguments)); - } - }; - /** - * The ZeroClipboard library's version number. - * - * @static - * @readonly - * @property {string} - */ - _defineProperty(ZeroClipboard, "version", { - value: "2.2.0", - writable: false, - configurable: true, - enumerable: true - }); - /** - * Update or get a copy of the ZeroClipboard global configuration. - * Returns a copy of the current/updated configuration. - * - * @returns Object - * @static - */ - ZeroClipboard.config = function() { - return _config.apply(this, _args(arguments)); - }; - /** - * Diagnostic method that describes the state of the browser, Flash Player, and ZeroClipboard. - * - * @returns Object - * @static - */ - ZeroClipboard.state = function() { - return _state.apply(this, _args(arguments)); - }; - /** - * Check if Flash is unusable for any reason: disabled, outdated, deactivated, etc. - * - * @returns Boolean - * @static - */ - ZeroClipboard.isFlashUnusable = function() { - return _isFlashUnusable.apply(this, _args(arguments)); - }; - /** - * Register an event listener. - * - * @returns `ZeroClipboard` - * @static - */ - ZeroClipboard.on = function() { - return _on.apply(this, _args(arguments)); - }; - /** - * Unregister an event listener. - * If no `listener` function/object is provided, it will unregister all listeners for the provided `eventType`. - * If no `eventType` is provided, it will unregister all listeners for every event type. - * - * @returns `ZeroClipboard` - * @static - */ - ZeroClipboard.off = function() { - return _off.apply(this, _args(arguments)); - }; - /** - * Retrieve event listeners for an `eventType`. - * If no `eventType` is provided, it will retrieve all listeners for every event type. - * - * @returns array of listeners for the `eventType`; if no `eventType`, then a map/hash object of listeners for all event types; or `null` - */ - ZeroClipboard.handlers = function() { - return _listeners.apply(this, _args(arguments)); - }; - /** - * Event emission receiver from the Flash object, forwarding to any registered JavaScript event listeners. - * - * @returns For the "copy" event, returns the Flash-friendly "clipData" object; otherwise `undefined`. - * @static - */ - ZeroClipboard.emit = function() { - return _emit.apply(this, _args(arguments)); - }; - /** - * Create and embed the Flash object. - * - * @returns The Flash object - * @static - */ - ZeroClipboard.create = function() { - return _create.apply(this, _args(arguments)); - }; - /** - * Self-destruct and clean up everything, including the embedded Flash object. - * - * @returns `undefined` - * @static - */ - ZeroClipboard.destroy = function() { - return _destroy.apply(this, _args(arguments)); - }; - /** - * Set the pending data for clipboard injection. - * - * @returns `undefined` - * @static - */ - ZeroClipboard.setData = function() { - return _setData.apply(this, _args(arguments)); - }; - /** - * Clear the pending data for clipboard injection. - * If no `format` is provided, all pending data formats will be cleared. - * - * @returns `undefined` - * @static - */ - ZeroClipboard.clearData = function() { - return _clearData.apply(this, _args(arguments)); - }; - /** - * Get a copy of the pending data for clipboard injection. - * If no `format` is provided, a copy of ALL pending data formats will be returned. - * - * @returns `String` or `Object` - * @static - */ - ZeroClipboard.getData = function() { - return _getData.apply(this, _args(arguments)); - }; - /** - * Sets the current HTML object that the Flash object should overlay. This will put the global - * Flash object on top of the current element; depending on the setup, this may also set the - * pending clipboard text data as well as the Flash object's wrapping element's title attribute - * based on the underlying HTML element and ZeroClipboard configuration. - * - * @returns `undefined` - * @static - */ - ZeroClipboard.focus = ZeroClipboard.activate = function() { - return _focus.apply(this, _args(arguments)); - }; - /** - * Un-overlays the Flash object. This will put the global Flash object off-screen; depending on - * the setup, this may also unset the Flash object's wrapping element's title attribute based on - * the underlying HTML element and ZeroClipboard configuration. - * - * @returns `undefined` - * @static - */ - ZeroClipboard.blur = ZeroClipboard.deactivate = function() { - return _blur.apply(this, _args(arguments)); - }; - /** - * Returns the currently focused/"activated" HTML element that the Flash object is wrapping. - * - * @returns `HTMLElement` or `null` - * @static - */ - ZeroClipboard.activeElement = function() { - return _activeElement.apply(this, _args(arguments)); - }; - /** - * Keep track of the ZeroClipboard client instance counter. - */ - var _clientIdCounter = 0; - /** - * Keep track of the state of the client instances. - * - * Entry structure: - * _clientMeta[client.id] = { - * instance: client, - * elements: [], - * handlers: {} - * }; - */ - var _clientMeta = {}; - /** - * Keep track of the ZeroClipboard clipped elements counter. - */ - var _elementIdCounter = 0; - /** - * Keep track of the state of the clipped element relationships to clients. - * - * Entry structure: - * _elementMeta[element.zcClippingId] = [client1.id, client2.id]; - */ - var _elementMeta = {}; - /** - * Keep track of the state of the mouse event handlers for clipped elements. - * - * Entry structure: - * _mouseHandlers[element.zcClippingId] = { - * mouseover: function(event) {}, - * mouseout: function(event) {}, - * mouseenter: function(event) {}, - * mouseleave: function(event) {}, - * mousemove: function(event) {} - * }; - */ - var _mouseHandlers = {}; - /** - * Extending the ZeroClipboard configuration defaults for the Client module. - */ - _extend(_globalConfig, { - autoActivate: true - }); - /** - * The real constructor for `ZeroClipboard` client instances. - * @private - */ - var _clientConstructor = function(elements) { - var client = this; - client.id = "" + _clientIdCounter++; - _clientMeta[client.id] = { - instance: client, - elements: [], - handlers: {} - }; - if (elements) { - client.clip(elements); - } - ZeroClipboard.on("*", function(event) { - return client.emit(event); - }); - ZeroClipboard.on("destroy", function() { - client.destroy(); - }); - ZeroClipboard.create(); - }; - /** - * The underlying implementation of `ZeroClipboard.Client.prototype.on`. - * @private - */ - var _clientOn = function(eventType, listener) { - var i, len, events, added = {}, meta = _clientMeta[this.id], handlers = meta && meta.handlers; - if (!meta) { - throw new Error("Attempted to add new listener(s) to a destroyed ZeroClipboard client instance"); - } - if (typeof eventType === "string" && eventType) { - events = eventType.toLowerCase().split(/\s+/); - } else if (typeof eventType === "object" && eventType && typeof listener === "undefined") { - for (i in eventType) { - if (_hasOwn.call(eventType, i) && typeof i === "string" && i && typeof eventType[i] === "function") { - this.on(i, eventType[i]); - } - } - } - if (events && events.length) { - for (i = 0, len = events.length; i < len; i++) { - eventType = events[i].replace(/^on/, ""); - added[eventType] = true; - if (!handlers[eventType]) { - handlers[eventType] = []; - } - handlers[eventType].push(listener); - } - if (added.ready && _flashState.ready) { - this.emit({ - type: "ready", - client: this - }); - } - if (added.error) { - for (i = 0, len = _flashStateErrorNames.length; i < len; i++) { - if (_flashState[_flashStateErrorNames[i].replace(/^flash-/, "")]) { - this.emit({ - type: "error", - name: _flashStateErrorNames[i], - client: this - }); - break; - } - } - if (_zcSwfVersion !== undefined && ZeroClipboard.version !== _zcSwfVersion) { - this.emit({ - type: "error", - name: "version-mismatch", - jsVersion: ZeroClipboard.version, - swfVersion: _zcSwfVersion - }); - } - } - } - return this; - }; - /** - * The underlying implementation of `ZeroClipboard.Client.prototype.off`. - * @private - */ - var _clientOff = function(eventType, listener) { - var i, len, foundIndex, events, perEventHandlers, meta = _clientMeta[this.id], handlers = meta && meta.handlers; - if (!handlers) { - return this; - } - if (arguments.length === 0) { - events = _keys(handlers); - } else if (typeof eventType === "string" && eventType) { - events = eventType.split(/\s+/); - } else if (typeof eventType === "object" && eventType && typeof listener === "undefined") { - for (i in eventType) { - if (_hasOwn.call(eventType, i) && typeof i === "string" && i && typeof eventType[i] === "function") { - this.off(i, eventType[i]); - } - } - } - if (events && events.length) { - for (i = 0, len = events.length; i < len; i++) { - eventType = events[i].toLowerCase().replace(/^on/, ""); - perEventHandlers = handlers[eventType]; - if (perEventHandlers && perEventHandlers.length) { - if (listener) { - foundIndex = perEventHandlers.indexOf(listener); - while (foundIndex !== -1) { - perEventHandlers.splice(foundIndex, 1); - foundIndex = perEventHandlers.indexOf(listener, foundIndex); - } - } else { - perEventHandlers.length = 0; - } - } - } - } - return this; - }; - /** - * The underlying implementation of `ZeroClipboard.Client.prototype.handlers`. - * @private - */ - var _clientListeners = function(eventType) { - var copy = null, handlers = _clientMeta[this.id] && _clientMeta[this.id].handlers; - if (handlers) { - if (typeof eventType === "string" && eventType) { - copy = handlers[eventType] ? handlers[eventType].slice(0) : []; - } else { - copy = _deepCopy(handlers); - } - } - return copy; - }; - /** - * The underlying implementation of `ZeroClipboard.Client.prototype.emit`. - * @private - */ - var _clientEmit = function(event) { - if (_clientShouldEmit.call(this, event)) { - if (typeof event === "object" && event && typeof event.type === "string" && event.type) { - event = _extend({}, event); - } - var eventCopy = _extend({}, _createEvent(event), { - client: this - }); - _clientDispatchCallbacks.call(this, eventCopy); - } - return this; - }; - /** - * The underlying implementation of `ZeroClipboard.Client.prototype.clip`. - * @private - */ - var _clientClip = function(elements) { - if (!_clientMeta[this.id]) { - throw new Error("Attempted to clip element(s) to a destroyed ZeroClipboard client instance"); - } - elements = _prepClip(elements); - for (var i = 0; i < elements.length; i++) { - if (_hasOwn.call(elements, i) && elements[i] && elements[i].nodeType === 1) { - if (!elements[i].zcClippingId) { - elements[i].zcClippingId = "zcClippingId_" + _elementIdCounter++; - _elementMeta[elements[i].zcClippingId] = [ this.id ]; - if (_globalConfig.autoActivate === true) { - _addMouseHandlers(elements[i]); - } - } else if (_elementMeta[elements[i].zcClippingId].indexOf(this.id) === -1) { - _elementMeta[elements[i].zcClippingId].push(this.id); - } - var clippedElements = _clientMeta[this.id] && _clientMeta[this.id].elements; - if (clippedElements.indexOf(elements[i]) === -1) { - clippedElements.push(elements[i]); - } - } - } - return this; - }; - /** - * The underlying implementation of `ZeroClipboard.Client.prototype.unclip`. - * @private - */ - var _clientUnclip = function(elements) { - var meta = _clientMeta[this.id]; - if (!meta) { - return this; - } - var clippedElements = meta.elements; - var arrayIndex; - if (typeof elements === "undefined") { - elements = clippedElements.slice(0); - } else { - elements = _prepClip(elements); - } - for (var i = elements.length; i--; ) { - if (_hasOwn.call(elements, i) && elements[i] && elements[i].nodeType === 1) { - arrayIndex = 0; - while ((arrayIndex = clippedElements.indexOf(elements[i], arrayIndex)) !== -1) { - clippedElements.splice(arrayIndex, 1); - } - var clientIds = _elementMeta[elements[i].zcClippingId]; - if (clientIds) { - arrayIndex = 0; - while ((arrayIndex = clientIds.indexOf(this.id, arrayIndex)) !== -1) { - clientIds.splice(arrayIndex, 1); - } - if (clientIds.length === 0) { - if (_globalConfig.autoActivate === true) { - _removeMouseHandlers(elements[i]); - } - delete elements[i].zcClippingId; - } - } - } - } - return this; - }; - /** - * The underlying implementation of `ZeroClipboard.Client.prototype.elements`. - * @private - */ - var _clientElements = function() { - var meta = _clientMeta[this.id]; - return meta && meta.elements ? meta.elements.slice(0) : []; - }; - /** - * The underlying implementation of `ZeroClipboard.Client.prototype.destroy`. - * @private - */ - var _clientDestroy = function() { - if (!_clientMeta[this.id]) { - return; - } - this.unclip(); - this.off(); - delete _clientMeta[this.id]; - }; - /** - * Inspect an Event to see if the Client (`this`) should honor it for emission. - * @private - */ - var _clientShouldEmit = function(event) { - if (!(event && event.type)) { - return false; - } - if (event.client && event.client !== this) { - return false; - } - var meta = _clientMeta[this.id]; - var clippedEls = meta && meta.elements; - var hasClippedEls = !!clippedEls && clippedEls.length > 0; - var goodTarget = !event.target || hasClippedEls && clippedEls.indexOf(event.target) !== -1; - var goodRelTarget = event.relatedTarget && hasClippedEls && clippedEls.indexOf(event.relatedTarget) !== -1; - var goodClient = event.client && event.client === this; - if (!meta || !(goodTarget || goodRelTarget || goodClient)) { - return false; - } - return true; - }; - /** - * Handle the actual dispatching of events to a client instance. - * - * @returns `undefined` - * @private - */ - var _clientDispatchCallbacks = function(event) { - var meta = _clientMeta[this.id]; - if (!(typeof event === "object" && event && event.type && meta)) { - return; - } - var async = _shouldPerformAsync(event); - var wildcardTypeHandlers = meta && meta.handlers["*"] || []; - var specificTypeHandlers = meta && meta.handlers[event.type] || []; - var handlers = wildcardTypeHandlers.concat(specificTypeHandlers); - if (handlers && handlers.length) { - var i, len, func, context, eventCopy, originalContext = this; - for (i = 0, len = handlers.length; i < len; i++) { - func = handlers[i]; - context = originalContext; - if (typeof func === "string" && typeof _window[func] === "function") { - func = _window[func]; - } - if (typeof func === "object" && func && typeof func.handleEvent === "function") { - context = func; - func = func.handleEvent; - } - if (typeof func === "function") { - eventCopy = _extend({}, event); - _dispatchCallback(func, context, [ eventCopy ], async); - } - } - } - }; - /** - * Prepares the elements for clipping/unclipping. - * - * @returns An Array of elements. - * @private - */ - var _prepClip = function(elements) { - if (typeof elements === "string") { - elements = []; - } - return typeof elements.length !== "number" ? [ elements ] : elements; - }; - /** - * Add a `mouseover` handler function for a clipped element. - * - * @returns `undefined` - * @private - */ - var _addMouseHandlers = function(element) { - if (!(element && element.nodeType === 1)) { - return; - } - var _suppressMouseEvents = function(event) { - if (!(event || (event = _window.event))) { - return; - } - if (event._source !== "js") { - event.stopImmediatePropagation(); - event.preventDefault(); - } - delete event._source; - }; - var _elementMouseOver = function(event) { - if (!(event || (event = _window.event))) { - return; - } - _suppressMouseEvents(event); - ZeroClipboard.focus(element); - }; - element.addEventListener("mouseover", _elementMouseOver, false); - element.addEventListener("mouseout", _suppressMouseEvents, false); - element.addEventListener("mouseenter", _suppressMouseEvents, false); - element.addEventListener("mouseleave", _suppressMouseEvents, false); - element.addEventListener("mousemove", _suppressMouseEvents, false); - _mouseHandlers[element.zcClippingId] = { - mouseover: _elementMouseOver, - mouseout: _suppressMouseEvents, - mouseenter: _suppressMouseEvents, - mouseleave: _suppressMouseEvents, - mousemove: _suppressMouseEvents - }; - }; - /** - * Remove a `mouseover` handler function for a clipped element. - * - * @returns `undefined` - * @private - */ - var _removeMouseHandlers = function(element) { - if (!(element && element.nodeType === 1)) { - return; - } - var mouseHandlers = _mouseHandlers[element.zcClippingId]; - if (!(typeof mouseHandlers === "object" && mouseHandlers)) { - return; - } - var key, val, mouseEvents = [ "move", "leave", "enter", "out", "over" ]; - for (var i = 0, len = mouseEvents.length; i < len; i++) { - key = "mouse" + mouseEvents[i]; - val = mouseHandlers[key]; - if (typeof val === "function") { - element.removeEventListener(key, val, false); - } - } - delete _mouseHandlers[element.zcClippingId]; - }; - /** - * Creates a new ZeroClipboard client instance. - * Optionally, auto-`clip` an element or collection of elements. - * - * @constructor - */ - ZeroClipboard._createClient = function() { - _clientConstructor.apply(this, _args(arguments)); - }; - /** - * Register an event listener to the client. - * - * @returns `this` - */ - ZeroClipboard.prototype.on = function() { - return _clientOn.apply(this, _args(arguments)); - }; - /** - * Unregister an event handler from the client. - * If no `listener` function/object is provided, it will unregister all handlers for the provided `eventType`. - * If no `eventType` is provided, it will unregister all handlers for every event type. - * - * @returns `this` - */ - ZeroClipboard.prototype.off = function() { - return _clientOff.apply(this, _args(arguments)); - }; - /** - * Retrieve event listeners for an `eventType` from the client. - * If no `eventType` is provided, it will retrieve all listeners for every event type. - * - * @returns array of listeners for the `eventType`; if no `eventType`, then a map/hash object of listeners for all event types; or `null` - */ - ZeroClipboard.prototype.handlers = function() { - return _clientListeners.apply(this, _args(arguments)); - }; - /** - * Event emission receiver from the Flash object for this client's registered JavaScript event listeners. - * - * @returns For the "copy" event, returns the Flash-friendly "clipData" object; otherwise `undefined`. - */ - ZeroClipboard.prototype.emit = function() { - return _clientEmit.apply(this, _args(arguments)); - }; - /** - * Register clipboard actions for new element(s) to the client. - * - * @returns `this` - */ - ZeroClipboard.prototype.clip = function() { - return _clientClip.apply(this, _args(arguments)); - }; - /** - * Unregister the clipboard actions of previously registered element(s) on the page. - * If no elements are provided, ALL registered elements will be unregistered. - * - * @returns `this` - */ - ZeroClipboard.prototype.unclip = function() { - return _clientUnclip.apply(this, _args(arguments)); - }; - /** - * Get all of the elements to which this client is clipped. - * - * @returns array of clipped elements - */ - ZeroClipboard.prototype.elements = function() { - return _clientElements.apply(this, _args(arguments)); - }; - /** - * Self-destruct and clean up everything for a single client. - * This will NOT destroy the embedded Flash object. - * - * @returns `undefined` - */ - ZeroClipboard.prototype.destroy = function() { - return _clientDestroy.apply(this, _args(arguments)); - }; - /** - * Stores the pending plain text to inject into the clipboard. - * - * @returns `this` - */ - ZeroClipboard.prototype.setText = function(text) { - if (!_clientMeta[this.id]) { - throw new Error("Attempted to set pending clipboard data from a destroyed ZeroClipboard client instance"); - } - ZeroClipboard.setData("text/plain", text); - return this; - }; - /** - * Stores the pending HTML text to inject into the clipboard. - * - * @returns `this` - */ - ZeroClipboard.prototype.setHtml = function(html) { - if (!_clientMeta[this.id]) { - throw new Error("Attempted to set pending clipboard data from a destroyed ZeroClipboard client instance"); - } - ZeroClipboard.setData("text/html", html); - return this; - }; - /** - * Stores the pending rich text (RTF) to inject into the clipboard. - * - * @returns `this` - */ - ZeroClipboard.prototype.setRichText = function(richText) { - if (!_clientMeta[this.id]) { - throw new Error("Attempted to set pending clipboard data from a destroyed ZeroClipboard client instance"); - } - ZeroClipboard.setData("application/rtf", richText); - return this; - }; - /** - * Stores the pending data to inject into the clipboard. - * - * @returns `this` - */ - ZeroClipboard.prototype.setData = function() { - if (!_clientMeta[this.id]) { - throw new Error("Attempted to set pending clipboard data from a destroyed ZeroClipboard client instance"); - } - ZeroClipboard.setData.apply(this, _args(arguments)); - return this; - }; - /** - * Clears the pending data to inject into the clipboard. - * If no `format` is provided, all pending data formats will be cleared. - * - * @returns `this` - */ - ZeroClipboard.prototype.clearData = function() { - if (!_clientMeta[this.id]) { - throw new Error("Attempted to clear pending clipboard data from a destroyed ZeroClipboard client instance"); - } - ZeroClipboard.clearData.apply(this, _args(arguments)); - return this; - }; - /** - * Gets a copy of the pending data to inject into the clipboard. - * If no `format` is provided, a copy of ALL pending data formats will be returned. - * - * @returns `String` or `Object` - */ - ZeroClipboard.prototype.getData = function() { - if (!_clientMeta[this.id]) { - throw new Error("Attempted to get pending clipboard data from a destroyed ZeroClipboard client instance"); - } - return ZeroClipboard.getData.apply(this, _args(arguments)); - }; - if (typeof define === "function" && define.amd) { - define(function() { - return ZeroClipboard; - }); - } else if (typeof module === "object" && module && typeof module.exports === "object" && module.exports) { - module.exports = ZeroClipboard; - } else { - window.ZeroClipboard = ZeroClipboard; - } -})(function() { - return this || window; -}()); \ No newline at end of file diff --git a/src/UI/LifeCycle.js b/src/UI/LifeCycle.js deleted file mode 100644 index 59a237340..000000000 --- a/src/UI/LifeCycle.js +++ /dev/null @@ -1,3 +0,0 @@ -window.onbeforeunload = function() { - window.NzbDrone.unloading = true; -}; \ No newline at end of file diff --git a/src/UI/ManualImport/Cells/EpisodesCell.js b/src/UI/ManualImport/Cells/EpisodesCell.js deleted file mode 100644 index 68c4b5166..000000000 --- a/src/UI/ManualImport/Cells/EpisodesCell.js +++ /dev/null @@ -1,46 +0,0 @@ -var _ = require('underscore'); -var vent = require('../../vent'); -var NzbDroneCell = require('../../Cells/NzbDroneCell'); -var SelectEpisodeLayout = require('../Episode/SelectEpisodeLayout'); - -module.exports = NzbDroneCell.extend({ - className : 'episodes-cell', - - events : { - 'click' : '_onClick' - }, - - render : function() { - this.$el.empty(); - - var episodes = this.model.get('episodes'); - - if (episodes) - { - var episodeNumbers = _.map(episodes, 'episodeNumber'); - - this.$el.html(episodeNumbers.join(', ')); - } - - return this; - }, - - _onClick : function () { - var series = this.model.get('series'); - var seasonNumber = this.model.get('seasonNumber'); - - if (series === undefined || seasonNumber === undefined) { - return; - } - - var view = new SelectEpisodeLayout({ series: series, seasonNumber: seasonNumber }); - - this.listenTo(view, 'manualimport:selected:episodes', this._setEpisodes); - - vent.trigger(vent.Commands.OpenModal2Command, view); - }, - - _setEpisodes : function (e) { - this.model.set('episodes', e.episodes); - } -}); \ No newline at end of file diff --git a/src/UI/ManualImport/Cells/PathCell.js b/src/UI/ManualImport/Cells/PathCell.js deleted file mode 100644 index 7397d1623..000000000 --- a/src/UI/ManualImport/Cells/PathCell.js +++ /dev/null @@ -1,16 +0,0 @@ -var NzbDroneCell = require('../../Cells/NzbDroneCell'); - -module.exports = NzbDroneCell.extend({ - className : 'path-cell', - - render : function() { - this.$el.empty(); - - var relativePath = this.model.get('relativePath'); - var path = this.model.get('path'); - - this.$el.html('<div title="{0}">{1}</div>'.format(path, relativePath)); - - return this; - } -}); \ No newline at end of file diff --git a/src/UI/ManualImport/Cells/QualityCell.js b/src/UI/ManualImport/Cells/QualityCell.js deleted file mode 100644 index 181ebf254..000000000 --- a/src/UI/ManualImport/Cells/QualityCell.js +++ /dev/null @@ -1,23 +0,0 @@ -var vent = require('../../vent'); -var QualityCell = require('../../Cells/QualityCell'); -var SelectQualityLayout = require('../Quality/SelectQualityLayout'); - -module.exports = QualityCell.extend({ - className : 'quality-cell editable', - - events : { - 'click' : '_onClick' - }, - - _onClick : function () { - var view = new SelectQualityLayout(); - - this.listenTo(view, 'manualimport:selected:quality', this._setQuality); - - vent.trigger(vent.Commands.OpenModal2Command, view); - }, - - _setQuality : function (e) { - this.model.set('quality', e.quality); - } -}); \ No newline at end of file diff --git a/src/UI/ManualImport/Cells/SeasonCell.js b/src/UI/ManualImport/Cells/SeasonCell.js deleted file mode 100644 index 6120055ea..000000000 --- a/src/UI/ManualImport/Cells/SeasonCell.js +++ /dev/null @@ -1,47 +0,0 @@ -var vent = require('../../vent'); -var NzbDroneCell = require('../../Cells/NzbDroneCell'); -var SelectSeasonLayout = require('../Season/SelectSeasonLayout'); - -module.exports = NzbDroneCell.extend({ - className : 'season-cell', - - events : { - 'click' : '_onClick' - }, - - render : function() { - this.$el.empty(); - - if (this.model.has('seasonNumber')) { - this.$el.html(this.model.get('seasonNumber')); - } - - this.delegateEvents(); - return this; - }, - - _onClick : function () { - var series = this.model.get('series'); - - if (!series) { - return; - } - - var view = new SelectSeasonLayout({ seasons: series.seasons }); - - this.listenTo(view, 'manualimport:selected:season', this._setSeason); - - vent.trigger(vent.Commands.OpenModal2Command, view); - }, - - _setSeason : function (e) { - if (this.model.has('seasonNumber') && e.seasonNumber === this.model.get('seasonNumber')) { - return; - } - - this.model.set({ - seasonNumber : e.seasonNumber, - episodes : [] - }); - } -}); \ No newline at end of file diff --git a/src/UI/ManualImport/Cells/SeriesCell.js b/src/UI/ManualImport/Cells/SeriesCell.js deleted file mode 100644 index cb66f6826..000000000 --- a/src/UI/ManualImport/Cells/SeriesCell.js +++ /dev/null @@ -1,45 +0,0 @@ -var vent = require('../../vent'); -var NzbDroneCell = require('../../Cells/NzbDroneCell'); -var SelectSeriesLayout = require('../Series/SelectSeriesLayout'); - -module.exports = NzbDroneCell.extend({ - className : 'series-title-cell editable', - - events : { - 'click' : '_onClick' - }, - - render : function() { - this.$el.empty(); - - var series = this.model.get('series'); - - if (series) - { - this.$el.html(series.title); - } - - this.delegateEvents(); - return this; - }, - - _onClick : function () { - var view = new SelectSeriesLayout(); - - this.listenTo(view, 'manualimport:selected:series', this._setSeries); - - vent.trigger(vent.Commands.OpenModal2Command, view); - }, - - _setSeries : function (e) { - if (this.model.has('series') && e.model.id === this.model.get('series').id) { - return; - } - - this.model.set({ - series : e.model.toJSON(), - seasonNumber : undefined, - episodes : [] - }); - } -}); \ No newline at end of file diff --git a/src/UI/ManualImport/EmptyView.js b/src/UI/ManualImport/EmptyView.js deleted file mode 100644 index 2b4394d3f..000000000 --- a/src/UI/ManualImport/EmptyView.js +++ /dev/null @@ -1,5 +0,0 @@ -var Marionette = require('marionette'); - -module.exports = Marionette.CompositeView.extend({ - template : 'ManualImport/EmptyViewTemplate' -}); \ No newline at end of file diff --git a/src/UI/ManualImport/EmptyViewTemplate.hbs b/src/UI/ManualImport/EmptyViewTemplate.hbs deleted file mode 100644 index fe59eb600..000000000 --- a/src/UI/ManualImport/EmptyViewTemplate.hbs +++ /dev/null @@ -1 +0,0 @@ -No video files were found in the selected folder. \ No newline at end of file diff --git a/src/UI/ManualImport/Episode/SelectEpisodeLayout.js b/src/UI/ManualImport/Episode/SelectEpisodeLayout.js deleted file mode 100644 index 04617a0bc..000000000 --- a/src/UI/ManualImport/Episode/SelectEpisodeLayout.js +++ /dev/null @@ -1,81 +0,0 @@ -var _ = require('underscore'); -var vent = require('vent'); -var Marionette = require('marionette'); -var Backgrid = require('backgrid'); -var EpisodeCollection = require('../../Series/EpisodeCollection'); -var LoadingView = require('../../Shared/LoadingView'); -var SelectAllCell = require('../../Cells/SelectAllCell'); -var EpisodeNumberCell = require('../../Series/Details/EpisodeNumberCell'); -var RelativeDateCell = require('../../Cells/RelativeDateCell'); -var SelectEpisodeRow = require('./SelectEpisodeRow'); - -module.exports = Marionette.Layout.extend({ - template : 'ManualImport/Episode/SelectEpisodeLayoutTemplate', - - regions : { - episodes : '.x-episodes' - }, - - events : { - 'click .x-select' : '_selectEpisodes' - }, - - columns : [ - { - name : '', - cell : SelectAllCell, - headerCell : 'select-all', - sortable : false - }, - { - name : 'episodeNumber', - label : '#', - cell : EpisodeNumberCell - }, - { - name : 'title', - label : 'Title', - hideSeriesLink : true, - cell : 'string', - sortable : false - }, - { - name : 'airDateUtc', - label : 'Air Date', - cell : RelativeDateCell - } - ], - - initialize : function(options) { - this.series = options.series; - this.seasonNumber = options.seasonNumber; - }, - - onRender : function() { - this.episodes.show(new LoadingView()); - - this.episodeCollection = new EpisodeCollection({ seriesId : this.series.id }); - this.episodeCollection.fetch(); - - this.listenToOnce(this.episodeCollection, 'sync', function () { - - this.episodeView = new Backgrid.Grid({ - columns : this.columns, - collection : this.episodeCollection.bySeason(this.seasonNumber), - className : 'table table-hover season-grid', - row : SelectEpisodeRow - }); - - this.episodes.show(this.episodeView); - }); - }, - - _selectEpisodes : function () { - var episodes = _.map(this.episodeView.getSelectedModels(), function (episode) { - return episode.toJSON(); - }); - - this.trigger('manualimport:selected:episodes', { episodes: episodes }); - vent.trigger(vent.Commands.CloseModal2Command); - } -}); diff --git a/src/UI/ManualImport/Episode/SelectEpisodeLayoutTemplate.hbs b/src/UI/ManualImport/Episode/SelectEpisodeLayoutTemplate.hbs deleted file mode 100644 index 68a9af81a..000000000 --- a/src/UI/ManualImport/Episode/SelectEpisodeLayoutTemplate.hbs +++ /dev/null @@ -1,21 +0,0 @@ -<div class="modal-content"> - <div class="manual-import-modal"> - <div class="modal-header"> - <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> - - <h3> - Manual Import - Select Episode(s) - </h3> - - </div> - <div class="modal-body"> - <div class="row"> - <div class="col-md-12 x-episodes"></div> - </div> - </div> - <div class="modal-footer"> - <button class="btn btn-default" data-dismiss="modal">Cancel</button> - <button class="btn btn-success x-select" data-dismiss="modal">Select Episodes</button> - </div> - </div> -</div> diff --git a/src/UI/ManualImport/Episode/SelectEpisodeRow.js b/src/UI/ManualImport/Episode/SelectEpisodeRow.js deleted file mode 100644 index 6dc90fc99..000000000 --- a/src/UI/ManualImport/Episode/SelectEpisodeRow.js +++ /dev/null @@ -1,20 +0,0 @@ -var Backgrid = require('backgrid'); - -module.exports = Backgrid.Row.extend({ - className : 'select-episode-row', - - events : { - 'click' : '_toggle' - }, - - _toggle : function(e) { - - if (e.target.type === 'checkbox') { - return; - } - - var checked = this.$el.find('.select-row-cell :checkbox').prop('checked'); - - this.model.trigger('backgrid:select', this.model, !checked); - } -}); \ No newline at end of file diff --git a/src/UI/ManualImport/Folder/SelectFolderView.js b/src/UI/ManualImport/Folder/SelectFolderView.js deleted file mode 100644 index 0a2c066c2..000000000 --- a/src/UI/ManualImport/Folder/SelectFolderView.js +++ /dev/null @@ -1,84 +0,0 @@ -var _ = require('underscore'); -var $ = require('jquery'); -var Config = require('../../Config'); -var Marionette = require('marionette'); -var moment = require('moment'); -require('../../Mixins/FileBrowser'); - -module.exports = Marionette.ItemView.extend({ - template : 'ManualImport/Folder/SelectFolderViewTemplate', - - ui : { - path : '.x-path', - buttons : '.x-button' - }, - - events: { - 'click .x-manual-import' : '_manualImport', - 'click .x-automatic-import' : '_automaticImport', - 'change .x-path' : '_updateButtons', - 'keyup .x-path' : '_updateButtons', - 'click .x-recent-folder' : '_selectRecentFolder' - }, - - initialize : function () { - this.templateHelpers = { - recentFolders: Config.getValueJson('manualimport.recentfolders', []) - }; - }, - - onRender : function() { - this.ui.path.fileBrowser(); - this._updateButtons(); - }, - - path : function() { - return this.ui.path.val(); - }, - - _manualImport : function () { - var path = this.ui.path.val(); - - if (path) { - this._setRecentFolders(path); - this.trigger('manualImport', { folder: path }); - } - }, - - _automaticImport : function () { - var path = this.ui.path.val(); - - if (path) { - this._setRecentFolders(path); - this.trigger('automaticImport', { folder: path }); - } - }, - - _updateButtons : function () { - if (this.ui.path.val()) { - this.ui.buttons.removeAttr('disabled'); - } - - else { - this.ui.buttons.attr('disabled', 'disabled'); - } - }, - - _selectRecentFolder : function (e) { - var path = $(e.target).closest('tr').data('path'); - this.ui.path.val(path); - this.ui.path.trigger('change'); - }, - - _setRecentFolders : function (path) { - var recentFolders = Config.getValueJson('manualimport.recentfolders', []); - - recentFolders = _.filter(recentFolders, function (folder) { - return folder.path.toLowerCase() !== path.toLowerCase(); - }); - - recentFolders.unshift({ path: path, lastUsed: moment.utc().toISOString() }); - - Config.setValueJson('manualimport.recentfolders', _.take(recentFolders, 5)); - } -}); diff --git a/src/UI/ManualImport/Folder/SelectFolderViewTemplate.hbs b/src/UI/ManualImport/Folder/SelectFolderViewTemplate.hbs deleted file mode 100644 index 4f681aecb..000000000 --- a/src/UI/ManualImport/Folder/SelectFolderViewTemplate.hbs +++ /dev/null @@ -1,43 +0,0 @@ -<div class="select-folder"> - <div class="row"> - <div class="form-group"> - <div class="col-md-12"> - <input type="text" class="form-control x-path" placeholder="Select a folder to import" name="path"> - </div> - </div> - </div> - <div class="recent-folders"> - {{#if recentFolders}} - <h4>Recent Folders</h4> - - <table class="table table-hover"> - <thead> - <tr> - <th>Path</th> - <th>Last Used</th> - </tr> - </thead> - <tbody> - {{#each recentFolders}} - <tr class="recent-folder x-recent-folder" data-path="{{path}}"> - <td>{{path}}</td> - <td>{{RelativeDate lastUsed}}</td> - </tr> - {{/each}} - </tbody> - </table> - {{/if}} - </div> - <div class="buttons"> - <div class="row"> - <div class="col-md-4 col-md-offset-4"> - <button class="btn btn-primary btn-lg btn-block x-automatic-import x-button"><i class="icon-lidarr-search-automatic"></i> Import File(s) Automatically</button> - </div> - </div> - <div class="row"> - <div class="col-md-4 col-md-offset-4"> - <button class="btn btn-primary btn-lg btn-block x-manual-import x-button"><i class="icon-lidarr-search-manual"></i> Manual Import</button> - </div> - </div> - </div> -</div> \ No newline at end of file diff --git a/src/UI/ManualImport/ManualImportCollection.js b/src/UI/ManualImport/ManualImportCollection.js deleted file mode 100644 index c7cff70f7..000000000 --- a/src/UI/ManualImport/ManualImportCollection.js +++ /dev/null @@ -1,74 +0,0 @@ -var PageableCollection = require('backbone.pageable'); -var ManualImportModel = require('./ManualImportModel'); -var AsSortedCollection = require('../Mixins/AsSortedCollection'); - -var Collection = PageableCollection.extend({ - model : ManualImportModel, - url : window.NzbDrone.ApiRoot + '/manualimport', - - state : { - sortKey : 'quality', - order : 1, - pageSize : 100000 - }, - - mode : 'client', - - originalFetch : PageableCollection.prototype.fetch, - - initialize : function (options) { - options = options || {}; - - if (!options.folder && !options.downloadId) { - throw 'folder or downloadId is required'; - } - - this.folder = options.folder; - this.downloadId = options.downloadId; - }, - - fetch : function(options) { - options = options || {}; - - options.data = { folder : this.folder, downloadId : this.downloadId }; - - return this.originalFetch.call(this, options); - }, - - sortMappings : { - series : { - sortValue : function(model, attr, order) { - var series = model.get(attr); - - if (series) { - return series.sortTitle; - } - - return ''; - } - }, - - quality : { - sortKey : 'qualityWeight' - } - }, - - comparator : function(model1, model2) { - var quality1 = model1.get('quality'); - var quality2 = model2.get('quality'); - - if (quality1 < quality2) { - return 1; - } - - if (quality1 > quality2) { - return -1; - } - - return 0; - } -}); - -Collection = AsSortedCollection.call(Collection); - -module.exports = Collection; \ No newline at end of file diff --git a/src/UI/ManualImport/ManualImportLayout.js b/src/UI/ManualImport/ManualImportLayout.js deleted file mode 100644 index 9d32c2769..000000000 --- a/src/UI/ManualImport/ManualImportLayout.js +++ /dev/null @@ -1,259 +0,0 @@ -var _ = require('underscore'); -var vent = require('vent'); -var Marionette = require('marionette'); -var Backgrid = require('backgrid'); -var CommandController = require('../Commands/CommandController'); -var EmptyView = require('./EmptyView'); -var SelectFolderView = require('./Folder/SelectFolderView'); -var LoadingView = require('../Shared/LoadingView'); -var ManualImportRow = require('./ManualImportRow'); -var SelectAllCell = require('../Cells/SelectAllCell'); -var PathCell = require('./Cells/PathCell'); -var SeriesCell = require('./Cells/SeriesCell'); -var SeasonCell = require('./Cells/SeasonCell'); -var EpisodesCell = require('./Cells/EpisodesCell'); -var QualityCell = require('./Cells/QualityCell'); -var FileSizeCell = require('../Cells/FileSizeCell'); -var ApprovalStatusCell = require('../Cells/ApprovalStatusCell'); -var ManualImportCollection = require('./ManualImportCollection'); -var Messenger = require('../Shared/Messenger'); - -module.exports = Marionette.Layout.extend({ - className : 'modal-lg', - template : 'ManualImport/ManualImportLayoutTemplate', - - regions : { - workspace : '.x-workspace' - }, - - ui : { - importButton : '.x-import', - importMode : '.x-importmode' - }, - - events : { - 'click .x-import' : '_import' - }, - - columns : [ - { - name : '', - cell : SelectAllCell, - headerCell : 'select-all', - sortable : false - }, - { - name : 'relativePath', - label : 'Relative Path', - cell : PathCell, - sortable : true - }, - { - name : 'series', - label : 'Series', - cell : SeriesCell, - sortable : true - }, - { - name : 'seasonNumber', - label : 'Season', - cell : SeasonCell, - sortable : true - }, - { - name : 'episodes', - label : 'Episode(s)', - cell : EpisodesCell, - sortable : false - }, - { - name : 'quality', - label : 'Quality', - cell : QualityCell, - sortable : true - - }, - { - name : 'size', - label : 'Size', - cell : FileSizeCell, - sortable : true - }, - { - name : 'rejections', - label : '<i class="icon-lidarr-header-rejections" />', - tooltip : 'Rejections', - cell : ApprovalStatusCell, - sortable : false, - sortType : 'fixed', - direction : 'ascending', - title : 'Import Rejected' - } - ], - - initialize : function(options) { - this.folder = options.folder; - this.downloadId = options.downloadId; - this.title = options.title; - this.importMode = options.importMode || 'Move'; - - this.templateHelpers = { - title : this.title || this.folder - }; - }, - - onRender : function() { - - if (this.folder || this.downloadId) { - this._showLoading(); - this._loadCollection(); - this.ui.importMode.val(this.importMode); - } - - else { - this._showSelectFolder(); - this.ui.importButton.hide(); - this.ui.importMode.hide(); - } - }, - - _showLoading : function () { - this.workspace.show(new LoadingView()); - }, - - _loadCollection : function () { - this.manualImportCollection = new ManualImportCollection({ folder: this.folder, downloadId: this.downloadId }); - this.manualImportCollection.fetch(); - - this.listenTo(this.manualImportCollection, 'sync', this._showTable); - this.listenTo(this.manualImportCollection, 'backgrid:selected', this._updateButtons); - }, - - _showTable : function () { - if (this.manualImportCollection.length === 0) { - this.workspace.show(new EmptyView()); - return; - } - - this.fileView = new Backgrid.Grid({ - columns : this.columns, - collection : this.manualImportCollection, - className : 'table table-hover', - row : ManualImportRow - }); - - this.workspace.show(this.fileView); - this._updateButtons(); - }, - - _showSelectFolder : function () { - this.selectFolderView = new SelectFolderView(); - this.workspace.show(this.selectFolderView); - - this.listenTo(this.selectFolderView, 'manualImport', this._manualImport); - this.listenTo(this.selectFolderView, 'automaticImport', this._automaticImport); - }, - - _manualImport : function (e) { - this.folder = e.folder; - this.templateHelpers.title = this.folder; - this.render(); - }, - - _automaticImport : function (e) { - CommandController.Execute('downloadedAlbumsScan', { - name : 'downloadedAlbumsScan', - path : e.folder - }); - - vent.trigger(vent.Commands.CloseModalCommand); - }, - - _import : function () { - var selected = this.fileView.getSelectedModels(); - - if (selected.length === 0) { - return; - } - - if (_.any(selected, function (model) { - return !model.has('series'); - })) { - - this._showErrorMessage('Series must be chosen for each selected file'); - return; - } - - if (_.any(selected, function (model) { - return !model.has('seasonNumber'); - })) { - - this._showErrorMessage('Season must be chosen for each selected file'); - return; - } - - if (_.any(selected, function (model) { - return !model.has('episodes') || model.get('episodes').length === 0; - })) { - - this._showErrorMessage('One or more episodes must be chosen for each selected file'); - return; - } - - var importMode = this.ui.importMode.val(); - - CommandController.Execute('manualImport', { - name : 'manualImport', - files : _.map(selected, function (file) { - return { - path : file.get('path'), - seriesId : file.get('series').id, - episodeIds : _.map(file.get('episodes'), 'id'), - quality : file.get('quality'), - downloadId : file.get('downloadId') - }; - }), - importMode : importMode - }); - - vent.trigger(vent.Commands.CloseModalCommand); - }, - - _updateButtons : function (model, selected) { - if (!this.fileView) { - this.ui.importButton.attr('disabled', 'disabled'); - return; - } - - if (!model) { - return; - } - - var selectedModels = this.fileView.getSelectedModels(); - var selectedCount = 0; - - if (selected) { - selectedCount = _.any(selectedModels, { id : model.id }) ? selectedModels.length : selectedModels.length + 1; - } - - else { - selectedCount = _.any(selectedModels, { id : model.id }) ? selectedModels.length - 1 : selectedModels.length; - } - - if (selectedCount === 0) { - this.ui.importButton.attr('disabled', 'disabled'); - } - - else { - this.ui.importButton.removeAttr('disabled'); - } - }, - - _showErrorMessage : function (message) { - Messenger.show({ - message : message, - type : 'error', - hideAfter : 5 - }); - } -}); \ No newline at end of file diff --git a/src/UI/ManualImport/ManualImportLayoutTemplate.hbs b/src/UI/ManualImport/ManualImportLayoutTemplate.hbs deleted file mode 100644 index 194e094e9..000000000 --- a/src/UI/ManualImport/ManualImportLayoutTemplate.hbs +++ /dev/null @@ -1,26 +0,0 @@ -<div class="modal-content"> - <div class="manual-import-modal"> - <div class="modal-header"> - <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> - - <h3> - Manual Import - {{#if title}}{{title}}{{else}}Select Folder{{/if}} - </h3> - - </div> - <div class="modal-body"> - <div class="x-workspace"></div> - <div class="x-footer"></div> - </div> - <div class="modal-footer"> - <div class="col-md-2 pull-left"> - <select class="form-control x-importmode"> - <option value="Move">Move Files</option> - <option value="Copy">Copy Files</option> - </select> - </div> - <button class="btn btn-default" data-dismiss="modal">Cancel</button> - <button class="btn btn-success x-import" disabled="disabled">Import</button> - </div> - </div> -</div> diff --git a/src/UI/ManualImport/ManualImportModel.js b/src/UI/ManualImport/ManualImportModel.js deleted file mode 100644 index dfd34cead..000000000 --- a/src/UI/ManualImport/ManualImportModel.js +++ /dev/null @@ -1,4 +0,0 @@ -var Backbone = require('backbone'); - -module.exports = Backbone.Model.extend({ -}); \ No newline at end of file diff --git a/src/UI/ManualImport/ManualImportRow.js b/src/UI/ManualImport/ManualImportRow.js deleted file mode 100644 index 5699e83c3..000000000 --- a/src/UI/ManualImport/ManualImportRow.js +++ /dev/null @@ -1,41 +0,0 @@ -var Backgrid = require('backgrid'); - -module.exports = Backgrid.Row.extend({ - className : 'manual-import-row', - - _originalInit : Backgrid.Row.prototype.initialize, - _originalRender : Backgrid.Row.prototype.render, - - initialize : function () { - this._originalInit.apply(this, arguments); - - this.listenTo(this.model, 'change', this._setError); - this.listenTo(this.model, 'change', this._setClasses); - }, - - render : function () { - this._originalRender.apply(this, arguments); - this._setError(); - this._setClasses(); - - return this; - }, - - _setError : function () { - if (this.model.has('series') && - this.model.has('seasonNumber') && - (this.model.has('episodes') && this.model.get('episodes').length > 0)&& - this.model.has('quality')) { - this.$el.removeClass('manual-import-error'); - } - - else { - this.$el.addClass('manual-import-error'); - } - }, - - _setClasses : function () { - this.$el.toggleClass('has-series', this.model.has('series')); - this.$el.toggleClass('has-season', this.model.has('seasonNumber')); - } -}); \ No newline at end of file diff --git a/src/UI/ManualImport/Quality/SelectQualityLayout.js b/src/UI/ManualImport/Quality/SelectQualityLayout.js deleted file mode 100644 index beba005e9..000000000 --- a/src/UI/ManualImport/Quality/SelectQualityLayout.js +++ /dev/null @@ -1,43 +0,0 @@ -var _ = require('underscore'); -var vent = require('../../vent'); -var Marionette = require('marionette'); -var LoadingView = require('../../Shared/LoadingView'); -var ProfileSchemaCollection = require('../../Settings/Profile/ProfileSchemaCollection'); -var SelectQualityView = require('./SelectQualityView'); - -module.exports = Marionette.Layout.extend({ - template : 'ManualImport/Quality/SelectQualityLayoutTemplate', - - regions : { - quality : '.x-quality' - }, - - events : { - 'click .x-select' : '_selectQuality' - }, - - initialize : function() { - this.profileSchemaCollection = new ProfileSchemaCollection(); - this.profileSchemaCollection.fetch(); - - this.listenTo(this.profileSchemaCollection, 'sync', this._showQuality); - }, - - onRender : function() { - this.quality.show(new LoadingView()); - }, - - _showQuality : function () { - var qualities = _.map(this.profileSchemaCollection.first().get('items'), function (quality) { - return quality.quality; - }); - - this.selectQualityView = new SelectQualityView({ qualities: qualities }); - this.quality.show(this.selectQualityView); - }, - - _selectQuality : function () { - this.trigger('manualimport:selected:quality', { quality: this.selectQualityView.selectedQuality() }); - vent.trigger(vent.Commands.CloseModal2Command); - } -}); \ No newline at end of file diff --git a/src/UI/ManualImport/Quality/SelectQualityLayoutTemplate.hbs b/src/UI/ManualImport/Quality/SelectQualityLayoutTemplate.hbs deleted file mode 100644 index d5d2098e6..000000000 --- a/src/UI/ManualImport/Quality/SelectQualityLayoutTemplate.hbs +++ /dev/null @@ -1,19 +0,0 @@ -<div class="modal-content"> - <div class="manual-import-modal"> - <div class="modal-header"> - <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> - - <h3> - Manual Import - Select Quality - </h3> - - </div> - <div class="modal-body"> - <div class="x-quality"></div> - </div> - <div class="modal-footer"> - <button class="btn btn-default" data-dismiss="modal">Cancel</button> - <button class="btn btn-success x-select" data-dismiss="modal">Select Quality</button> - </div> - </div> -</div> diff --git a/src/UI/ManualImport/Quality/SelectQualityView.js b/src/UI/ManualImport/Quality/SelectQualityView.js deleted file mode 100644 index 8a39fab82..000000000 --- a/src/UI/ManualImport/Quality/SelectQualityView.js +++ /dev/null @@ -1,37 +0,0 @@ -var _ = require('underscore'); -var Marionette = require('marionette'); - -module.exports = Marionette.ItemView.extend({ - template : 'ManualImport/Quality/SelectQualityViewTemplate', - - ui : { - select : '.x-select-quality', - proper : 'x-proper' - }, - - initialize : function(options) { - this.qualities = options.qualities; - - this.templateHelpers = { - qualities: this.qualities - }; - }, - - selectedQuality : function () { - var selected = parseInt(this.ui.select.val(), 10); - var proper = this.ui.proper.prop('checked'); - - var quality = _.find(this.qualities, function(q) { - return q.id === selected; - }); - - - return { - quality : quality, - revision : { - version : proper ? 2 : 1, - real : 0 - } - }; - } -}); \ No newline at end of file diff --git a/src/UI/ManualImport/Quality/SelectQualityViewTemplate.hbs b/src/UI/ManualImport/Quality/SelectQualityViewTemplate.hbs deleted file mode 100644 index a04342280..000000000 --- a/src/UI/ManualImport/Quality/SelectQualityViewTemplate.hbs +++ /dev/null @@ -1,33 +0,0 @@ -<div class="form-horizontal"> - <div class="form-group"> - <label class="col-sm-4 control-label">Quality</label> - - <div class="col-sm-4"> - <select class="form-control x-select-quality"> - <option value="-1">Select Quality</option> - {{#each qualities}} - <option value="{{id}}">{{name}}</option> - {{/each}} - </select> - - </div> - </div> - - <div class="form-group"> - <label class="col-sm-4 control-label">Proper</label> - - <div class="col-sm-8"> - <div class="input-group"> - <label class="checkbox toggle well"> - <input type="checkbox" class="x-proper"/> - <p> - <span>Yes</span> - <span>No</span> - </p> - - <div class="btn btn-primary slide-button"/> - </label> - </div> - </div> - </div> -</div> diff --git a/src/UI/ManualImport/Season/SelectSeasonLayout.js b/src/UI/ManualImport/Season/SelectSeasonLayout.js deleted file mode 100644 index 6f46f9cd9..000000000 --- a/src/UI/ManualImport/Season/SelectSeasonLayout.js +++ /dev/null @@ -1,28 +0,0 @@ -var vent = require('vent'); -var Marionette = require('marionette'); - -module.exports = Marionette.Layout.extend({ - template : 'ManualImport/Season/SelectSeasonLayoutTemplate', - - events : { - 'change .x-select-season' : '_selectSeason' - }, - - initialize : function(options) { - - this.templateHelpers = { - seasons : options.seasons - }; - }, - - _selectSeason : function (e) { - var seasonNumber = parseInt(e.target.value, 10); - - if (seasonNumber === -1) { - return; - } - - this.trigger('manualimport:selected:season', { seasonNumber: seasonNumber }); - vent.trigger(vent.Commands.CloseModal2Command); - } -}); \ No newline at end of file diff --git a/src/UI/ManualImport/Season/SelectSeasonLayoutTemplate.hbs b/src/UI/ManualImport/Season/SelectSeasonLayoutTemplate.hbs deleted file mode 100644 index b459c6bf5..000000000 --- a/src/UI/ManualImport/Season/SelectSeasonLayoutTemplate.hbs +++ /dev/null @@ -1,29 +0,0 @@ -<div class="modal-content"> - <div class="manual-import-modal"> - <div class="modal-header"> - <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> - - <h3> - Manual Import - Select Season - </h3> - - </div> - <div class="modal-body"> - <div class="row"> - <div class="form-group col-md-4 col-md-offset-4"> - <select class="form-control x-select-season"> - <option value="-1">Select Season</option> - {{#each seasons}} - <option value="{{seasonNumber}}">Season {{seasonNumber}}</option> - {{/each}} - </select> - </div> - </div> - </div> - <div class="modal-footer"> - <button class="btn btn-default" data-dismiss="modal">Cancel</button> - </div> - </div> -</div> - - diff --git a/src/UI/ManualImport/Series/SelectSeriesLayout.js b/src/UI/ManualImport/Series/SelectSeriesLayout.js deleted file mode 100644 index 2d0ea1487..000000000 --- a/src/UI/ManualImport/Series/SelectSeriesLayout.js +++ /dev/null @@ -1,101 +0,0 @@ -var _ = require('underscore'); -var vent = require('vent'); -var Marionette = require('marionette'); -var Backgrid = require('backgrid'); -var SeriesCollection = require('../../Series/SeriesCollection'); -var SelectRow = require('./SelectSeriesRow'); - -module.exports = Marionette.Layout.extend({ - template : 'ManualImport/Series/SelectSeriesLayoutTemplate', - - regions : { - series : '.x-series' - }, - - ui : { - filter : '.x-filter' - }, - - columns : [ - { - name : 'title', - label : 'Title', - cell : 'String', - sortValue : 'sortTitle' - } - ], - - initialize : function() { - this.seriesCollection = SeriesCollection.clone(); - this._setModelCollection(); - - this.listenTo(this.seriesCollection, 'row:selected', this._onSelected); - this.listenTo(this, 'modal:afterShow', this._setFocus); - }, - - onRender : function() { - this.seriesView = new Backgrid.Grid({ - columns : this.columns, - collection : this.seriesCollection, - className : 'table table-hover season-grid', - row : SelectRow - }); - - this.series.show(this.seriesView); - this._setupFilter(); - }, - - _setupFilter : function () { - var self = this; - - //TODO: This should be a mixin (same as Add Series searching) - this.ui.filter.keyup(function(e) { - if (_.contains([ - 9, - 16, - 17, - 18, - 19, - 20, - 33, - 34, - 35, - 36, - 37, - 38, - 39, - 40, - 91, - 92, - 93 - ], e.keyCode)) { - return; - } - - self._filter(self.ui.filter.val()); - }); - }, - - _filter : function (term) { - this.seriesCollection.setFilter(['title', term, 'contains']); - this._setModelCollection(); - }, - - _onSelected : function (e) { - this.trigger('manualimport:selected:series', { model: e.model }); - - vent.trigger(vent.Commands.CloseModal2Command); - }, - - _setFocus : function () { - this.ui.filter.focus(); - }, - - _setModelCollection: function () { - var self = this; - - _.each(this.seriesCollection.models, function (model) { - model.collection = self.seriesCollection; - }); - } -}); diff --git a/src/UI/ManualImport/Series/SelectSeriesLayoutTemplate.hbs b/src/UI/ManualImport/Series/SelectSeriesLayoutTemplate.hbs deleted file mode 100644 index 0db951d99..000000000 --- a/src/UI/ManualImport/Series/SelectSeriesLayoutTemplate.hbs +++ /dev/null @@ -1,30 +0,0 @@ -<div class="modal-content"> - <div class="manual-import-modal"> - <div class="modal-header"> - <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> - - <h3> - Manual Import - Select Series - </h3> - - </div> - <div class="modal-body"> - <div class="row"> - <div class="col-md-12"> - <div class="form-group"> - <input type="text" class="form-control x-filter" placeholder="Filter series" /> - </div> - </div> - </div> - - <div class="row"> - <div class="col-md-12 x-series"></div> - </div> - </div> - <div class="modal-footer"> - <button class="btn btn-default" data-dismiss="modal">Cancel</button> - </div> - </div> -</div> - - diff --git a/src/UI/ManualImport/Series/SelectSeriesRow.js b/src/UI/ManualImport/Series/SelectSeriesRow.js deleted file mode 100644 index 38a2d5ca6..000000000 --- a/src/UI/ManualImport/Series/SelectSeriesRow.js +++ /dev/null @@ -1,13 +0,0 @@ -var Backgrid = require('backgrid'); - -module.exports = Backgrid.Row.extend({ - className : 'select-row select-series-row', - - events : { - 'click' : '_onClick' - }, - - _onClick : function() { - this.model.collection.trigger('row:selected', { model: this.model }); - } -}); \ No newline at end of file diff --git a/src/UI/ManualImport/Summary/ManualImportSummaryView.js b/src/UI/ManualImport/Summary/ManualImportSummaryView.js deleted file mode 100644 index a4ab847c2..000000000 --- a/src/UI/ManualImport/Summary/ManualImportSummaryView.js +++ /dev/null @@ -1,20 +0,0 @@ -var _ = require('underscore'); -var Marionette = require('marionette'); - -module.exports = Marionette.ItemView.extend({ - template : 'ManualImport/Summary/ManualImportSummaryViewTemplate', - - initialize : function (options) { - var episodes = _.map(options.episodes, function (episode) { - return episode.toJSON(); - }); - - this.templateHelpers = { - file : options.file, - series : options.series, - season : options.season, - episodes : episodes, - quality : options.quality - }; - } -}); \ No newline at end of file diff --git a/src/UI/ManualImport/Summary/ManualImportSummaryViewTemplate.hbs b/src/UI/ManualImport/Summary/ManualImportSummaryViewTemplate.hbs deleted file mode 100644 index d65ff52f1..000000000 --- a/src/UI/ManualImport/Summary/ManualImportSummaryViewTemplate.hbs +++ /dev/null @@ -1,19 +0,0 @@ -<dl class="dl-horizontal"> - - <dt>Path:</dt> - <dd>{{file}}</dd> - - <dt>Series:</dt> - <dd>{{series.title}}</dd> - - <dt>Season:</dt> - <dd>{{season.seasonNumber}}</dd> - - {{#each episodes}} - <dt>Episode:</dt> - <dd>{{episodeNumber}} - {{title}}</dd> - {{/each}} - - <dt>Quality:</dt> - <dd>{{quality.name}}</dd> -</dl> diff --git a/src/UI/ManualImport/manualimport.less b/src/UI/ManualImport/manualimport.less deleted file mode 100644 index c1d7af5a2..000000000 --- a/src/UI/ManualImport/manualimport.less +++ /dev/null @@ -1,63 +0,0 @@ -@import "../Shared/Styles/card.less"; -@import "../Shared/Styles/clickable.less"; -@import "../Content/Bootstrap/variables"; - -.manual-import-modal { - .path-cell { - word-break : break-all; - } - - .file-size-cell { - min-width : 80px; - } - - .has-series { - .season-cell { - .clickable(); - } - } - - .has-season { - .episodes-cell { - .clickable(); - } - } - - .editable { - .clickable(); - - .badge { - .clickable(); - } - } - - .select-row { - .clickable(); - } - - .select-folder { - .buttons { - margin-top: 20px; - - .row { - margin-top: 10px; - } - } - - .recent-folders { - margin-top: 20px; - } - - .recent-folder { - .clickable(); - } - } - - .manual-import-error { - background-color : #fdefef; - } - - .recent-folder { - .clickable(); - } -} diff --git a/src/UI/Mixins/AsChangeTrackingModel.js b/src/UI/Mixins/AsChangeTrackingModel.js deleted file mode 100644 index b3524b244..000000000 --- a/src/UI/Mixins/AsChangeTrackingModel.js +++ /dev/null @@ -1,22 +0,0 @@ -module.exports = function() { - var originalInit = this.prototype.initialize; - - this.prototype.initialize = function() { - - this.isSaved = true; - - this.on('change', function() { - this.isSaved = false; - }, this); - - this.on('sync', function() { - this.isSaved = true; - }, this); - - if (originalInit) { - originalInit.call(this); - } - }; - - return this; -}; \ No newline at end of file diff --git a/src/UI/Mixins/AsEditModalView.js b/src/UI/Mixins/AsEditModalView.js deleted file mode 100644 index 383e0315b..000000000 --- a/src/UI/Mixins/AsEditModalView.js +++ /dev/null @@ -1,114 +0,0 @@ -var AppLayout = require('../AppLayout'); - -module.exports = function() { - var originalInitialize = this.prototype.initialize; - var originalOnBeforeClose = this.prototype.onBeforeClose; - - var saveInternal = function() { - var self = this; - - if (this.saving) { - return this.savePromise; - } - - this.saving = true; - this.ui.indicator.show(); - - if (this._onBeforeSave) { - this._onBeforeSave.call(this); - } - - this.savePromise = this.model.save(); - - this.savePromise.always(function() { - self.saving = false; - - if (!self.isClosed) { - self.ui.indicator.hide(); - } - }); - - this.savePromise.done(function() { - self.originalModelData = JSON.stringify(self.model.toJSON()); - }); - - return this.savePromise; - }; - - this.prototype.initialize = function(options) { - if (!this.model) { - throw 'View has no model'; - } - - this.testing = false; - this.saving = false; - - this.originalModelData = JSON.stringify(this.model.toJSON()); - - this.events = this.events || {}; - this.events['click .x-save'] = '_save'; - this.events['click .x-save-and-add'] = '_saveAndAdd'; - this.events['click .x-test'] = '_test'; - this.events['click .x-delete'] = '_delete'; - - this.ui = this.ui || {}; - this.ui.indicator = '.x-indicator'; - - if (originalInitialize) { - originalInitialize.call(this, options); - } - }; - - this.prototype._save = function() { - var self = this; - var promise = saveInternal.call(this); - - promise.done(function() { - if (self._onAfterSave) { - self._onAfterSave.call(self); - } - }); - }; - - this.prototype._saveAndAdd = function() { - var self = this; - var promise = saveInternal.call(this); - - promise.done(function() { - if (self._onAfterSaveAndAdd) { - self._onAfterSaveAndAdd.call(self); - } - }); - }; - - this.prototype._test = function() { - var self = this; - - if (this.testing) { - return; - } - - this.testing = true; - this.ui.indicator.show(); - - this.model.test().always(function() { - self.testing = false; - self.ui.indicator.hide(); - }); - }; - - this.prototype._delete = function() { - var view = new this._deleteView({ model : this.model }); - AppLayout.modalRegion.show(view); - }; - - this.prototype.onBeforeClose = function() { - this.model.set(JSON.parse(this.originalModelData)); - - if (originalOnBeforeClose) { - originalOnBeforeClose.call(this); - } - }; - - return this; -}; diff --git a/src/UI/Mixins/AsFilteredCollection.js b/src/UI/Mixins/AsFilteredCollection.js deleted file mode 100644 index 4b3fd3272..000000000 --- a/src/UI/Mixins/AsFilteredCollection.js +++ /dev/null @@ -1,79 +0,0 @@ -var _ = require('underscore'); -var Backbone = require('backbone'); - -module.exports = function() { - - this.prototype.setFilter = function(filter, options) { - options = _.extend({ reset : true }, options || {}); - - this.state.filterKey = filter[0]; - this.state.filterValue = filter[1]; - this.state.filterType = filter[2] || 'equal'; - - if (options.reset) { - if (this.mode !== 'server') { - this.fullCollection.resetFiltered(); - } else { - return this.fetch(); - } - } - }; - - this.prototype.setFilterMode = function(mode, options) { - return this.setFilter(this.filterModes[mode], options); - }; - - var originalMakeFullCollection = this.prototype._makeFullCollection; - - this.prototype._makeFullCollection = function(models, options) { - var self = this; - - self.shadowCollection = originalMakeFullCollection.call(this, models, options); - - var filterModel = function(model) { - if (_.isFunction(self.state.filterType)) { - return self.state.filterType(model); - } - - if (!self.state.filterKey) { - return true; - } - else if (self.state.filterType === 'contains') { - return model.get(self.state.filterKey).toLowerCase().indexOf(self.state.filterValue.toLowerCase()) > -1; - } - else { - return model.get(self.state.filterKey) === self.state.filterValue; - } - }; - - self.shadowCollection.filtered = function() { - return this.filter(filterModel); - }; - - var filteredModels = self.shadowCollection.filtered(); - var fullCollection = originalMakeFullCollection.call(this, filteredModels, options); - - fullCollection.resetFiltered = function(options) { - Backbone.Collection.prototype.reset.call(this, self.shadowCollection.filtered(), options); - }; - - fullCollection.reset = function(models, options) { - self.shadowCollection.reset(models, options); - self.fullCollection.resetFiltered(); - }; - - return fullCollection; - }; - - _.extend(this.prototype.state, { - filterKey : null, - filterValue : null - }); - - _.extend(this.prototype.queryParams, { - filterKey : 'filterKey', - filterValue : 'filterValue' - }); - - return this; -}; diff --git a/src/UI/Mixins/AsModelBoundView.js b/src/UI/Mixins/AsModelBoundView.js deleted file mode 100644 index 12d3fcca3..000000000 --- a/src/UI/Mixins/AsModelBoundView.js +++ /dev/null @@ -1,46 +0,0 @@ -var ModelBinder = require('backbone.modelbinder'); - -module.exports = function() { - - var originalOnRender = this.prototype.onRender; - var originalBeforeClose = this.prototype.onBeforeClose; - - this.prototype.onRender = function() { - - if (!this.model) { - throw 'View has no model for binding'; - } - - if (!this._modelBinder) { - this._modelBinder = new ModelBinder(); - } - - var options = { - changeTriggers : { - '' : 'change typeahead:selected typeahead:autocompleted', - '[contenteditable]' : 'blur', - '[data-onkeyup]' : 'keyup' - } - }; - - this._modelBinder.bind(this.model, this.el, null, options); - - if (originalOnRender) { - originalOnRender.call(this); - } - }; - - this.prototype.onBeforeClose = function() { - - if (this._modelBinder) { - this._modelBinder.unbind(); - delete this._modelBinder; - } - - if (originalBeforeClose) { - originalBeforeClose.call(this); - } - }; - - return this; -}; diff --git a/src/UI/Mixins/AsNamedView.js b/src/UI/Mixins/AsNamedView.js deleted file mode 100644 index 8bdd4b604..000000000 --- a/src/UI/Mixins/AsNamedView.js +++ /dev/null @@ -1,31 +0,0 @@ -module.exports = function() { - - window.NzbDrone.NameViews = window.NzbDrone.NameViews || !window.NzbDrone.Production; - - var regex = new RegExp('/', 'g'); - - var _getViewName = function(template) { - if (template) { - return template.toLocaleLowerCase().replace('template', '').replace(regex, '-'); - } - - return undefined; - }; - - var originalOnRender = this.onRender; - - this.onRender = function() { - - if (window.NzbDrone.NameViews) { - this.$el.addClass('iv-' + _getViewName(this.template)); - } - - if (originalOnRender) { - return originalOnRender.call(this); - } - - return undefined; - }; - - return this; -}; \ No newline at end of file diff --git a/src/UI/Mixins/AsPageableCollection.js b/src/UI/Mixins/AsPageableCollection.js deleted file mode 100644 index 60145e569..000000000 --- a/src/UI/Mixins/AsPageableCollection.js +++ /dev/null @@ -1,45 +0,0 @@ -var _ = require('underscore'); - -module.exports = function() { - var originalMakeCollectionEventHandler = this.prototype._makeCollectionEventHandler; - - this.prototype._makeCollectionEventHandler = function (pageCollection, fullCollection) { - var self = this; - this.pageCollection = pageCollection; - this.fullCollection = fullCollection; - var eventHandler = originalMakeCollectionEventHandler.apply(this, arguments); - - return _.wrap(eventHandler, _.bind(self._resetEventHandler, self)); - }; - - this.prototype._resetEventHandler = function (originalEventHandler, event, model, collection, options) { - if (event === 'reset') { - var currentPage = this.state.currentPage; - var pageSize = this.state.pageSize; - - originalEventHandler.apply(this, [].slice.call(arguments, 1)); - - var totalPages = Math.max(1,Math.ceil(this.state.totalRecords / pageSize)); - var newPage = Math.min(currentPage, totalPages); - - if (newPage !== this.state.currentPage) { - this.state.currentPage = newPage; - - // If backbone pageable fixes their reset bug - // (they reset the page number, but not the range), - // we'll want to do this for all resets where the page number changed - if (currentPage !== newPage) { - var pageStart = (newPage - 1) * pageSize; - var pageEnd = pageStart + pageSize; - - this.pageCollection.reset(this.fullCollection.models.slice(pageStart, pageEnd), - _.extend({}, options, { parse : false })); - } - } - } else { - originalEventHandler.call(this, [].slice.call(arguments, 1)); - } - }; - - return this; -}; diff --git a/src/UI/Mixins/AsPersistedStateCollection.js b/src/UI/Mixins/AsPersistedStateCollection.js deleted file mode 100644 index cecdeb2d8..000000000 --- a/src/UI/Mixins/AsPersistedStateCollection.js +++ /dev/null @@ -1,72 +0,0 @@ -var _ = require('underscore'); -var Config = require('../Config'); - -module.exports = function() { - - var originalInit = this.prototype.initialize; - this.prototype.initialize = function(options) { - - options = options || {}; - - if (options.tableName) { - this.tableName = options.tableName; - } - - if (!this.tableName && !options.tableName) { - throw 'tableName is required'; - } - - _setInitialState.call(this); - - this.on('backgrid:sort', _storeStateFromBackgrid, this); - this.on('drone:sort', _storeState, this); - - if (originalInit) { - originalInit.call(this, options); - } - }; - - if (!this.prototype._getSortMapping) { - this.prototype._getSortMapping = function(key) { - return { - name : key, - sortKey : key - }; - }; - } - - var _setInitialState = function() { - var key = Config.getValue('{0}.sortKey'.format(this.tableName), this.state.sortKey); - var direction = Config.getValue('{0}.sortDirection'.format(this.tableName), this.state.order); - var order = parseInt(direction, 10); - - this.state.sortKey = this._getSortMapping(key).sortKey; - this.state.order = order; - }; - - var _storeStateFromBackgrid = function(column, sortDirection) { - var order = _convertDirectionToInt(sortDirection); - var sortKey = this._getSortMapping(column.get('name')).sortKey; - - Config.setValue('{0}.sortKey'.format(this.tableName), sortKey); - Config.setValue('{0}.sortDirection'.format(this.tableName), order); - }; - - var _storeState = function(sortModel, sortDirection) { - var order = _convertDirectionToInt(sortDirection); - var sortKey = this._getSortMapping(sortModel.get('name')).sortKey; - - Config.setValue('{0}.sortKey'.format(this.tableName), sortKey); - Config.setValue('{0}.sortDirection'.format(this.tableName), order); - }; - - var _convertDirectionToInt = function(dir) { - if (dir === 'ascending') { - return '-1'; - } - - return '1'; - }; - - return this; -}; diff --git a/src/UI/Mixins/AsSortedCollection.js b/src/UI/Mixins/AsSortedCollection.js deleted file mode 100644 index 78a31edb6..000000000 --- a/src/UI/Mixins/AsSortedCollection.js +++ /dev/null @@ -1,130 +0,0 @@ -var _ = require('underscore'); -var Config = require('../Config'); - -module.exports = function() { - - var originalSetSorting = this.prototype.setSorting; - - this.prototype.setSorting = function(sortKey, order, options) { - var sortMapping = this._getSortMapping(sortKey); - - options = _.defaults({ sortValue : sortMapping.sortValue }, options || {}); - - return originalSetSorting.call(this, sortMapping.sortKey, order, options); - }; - - this.prototype._getSortMappings = function() { - var result = {}; - - if (this.sortMappings) { - _.each(this.sortMappings, function(values, key) { - var item = { - name : key, - sortKey : values.sortKey || key, - sortValue : values.sortValue - }; - result[key] = item; - result[item.sortKey] = item; - }); - } - - return result; - }; - - this.prototype._getSortMapping = function(key) { - var sortMappings = this._getSortMappings(); - - return sortMappings[key] || { - name : key, - sortKey : key - }; - }; - - this.prototype._getSecondarySorting = function() { - var sortKey = this.state.secondarySortKey; - var sortOrder = this.state.secondarySortOrder || -1; - - if (!sortKey || sortKey === this.state.sortKey) { - return null; - } - - var sortMapping = this._getSortMapping(sortKey); - - if (!sortMapping.sortValue) { - sortMapping.sortValue = function(model, attr) { - return model.get(attr); - }; - } - - return { - key : sortKey, - order : sortOrder, - sortValue : sortMapping.sortValue - }; - }; - - this.prototype._makeComparator = function(sortKey, order, sortValue) { - var state = this.state; - var secondarySorting = this._getSecondarySorting(); - - sortKey = sortKey || state.sortKey; - order = order || state.order; - - if (!sortKey || !order) { - return; - } - - if (!sortValue) { - sortValue = function(model, attr) { - return model.get(attr); - }; - } - - return function(left, right) { - var l = sortValue(left, sortKey, order); - var r = sortValue(right, sortKey, order); - var t; - - if (order === 1) { - t = l; - l = r; - r = t; - } - - if (l === r) { - - if (secondarySorting) { - var ls = secondarySorting.sortValue(left, secondarySorting.key, order); - var rs = secondarySorting.sortValue(right, secondarySorting.key, order); - var ts; - - if (secondarySorting.order === 1) { - ts = ls; - ls = rs; - rs = ts; - } - - if (ls === rs) { - return 0; - } - - if (ls < rs) { - return -1; - } - - return 1; - } - - return 0; - } - - else if (l < r) { - return -1; - } - - return 1; - }; - }; - - return this; -}; diff --git a/src/UI/Mixins/AsSortedCollectionView.js b/src/UI/Mixins/AsSortedCollectionView.js deleted file mode 100644 index e68b833a7..000000000 --- a/src/UI/Mixins/AsSortedCollectionView.js +++ /dev/null @@ -1,24 +0,0 @@ -module.exports = function() { - this.prototype.appendHtml = function(collectionView, itemView, index) { - var childrenContainer = collectionView.itemViewContainer ? collectionView.$(collectionView.itemViewContainer) : collectionView.$el; - var collection = collectionView.collection; - - // If the index of the model is at the end of the collection append, else insert at proper index - if (index >= collection.size() - 1) { - childrenContainer.append(itemView.el); - } else { - var previousModel = collection.at(index + 1); - var previousView = this.children.findByModel(previousModel); - - if (previousView) { - previousView.$el.before(itemView.$el); - } - - else { - childrenContainer.append(itemView.el); - } - } - }; - - return this; -}; \ No newline at end of file diff --git a/src/UI/Mixins/AsValidatedView.js b/src/UI/Mixins/AsValidatedView.js deleted file mode 100644 index 22e3c0844..000000000 --- a/src/UI/Mixins/AsValidatedView.js +++ /dev/null @@ -1,93 +0,0 @@ -var Validation = require('backbone.validation'); -var _ = require('underscore'); - -module.exports = (function() { - 'use strict'; - return function() { - - var originalInitialize = this.prototype.initialize; - var originalOnRender = this.prototype.onRender; - var originalBeforeClose = this.prototype.onBeforeClose; - - var errorHandler = function(response) { - if (this.model) { - this.model.trigger('validation:failed', response); - } else { - this.trigger('validation:failed', response); - } - }; - - var validatedSync = function(method, model, options) { - model.trigger('validation:sync'); - - arguments[2].isValidatedCall = true; - return model._originalSync.apply(this, arguments).fail(errorHandler.bind(this)); - }; - - var bindToModel = function(model) { - if (!model._originalSync) { - model._originalSync = model.sync; - model.sync = validatedSync.bind(this); - } - }; - - var validationFailed = function(response) { - if (response.status === 400) { - var view = this; - var validationErrors = JSON.parse(response.responseText); - _.each(validationErrors, function(error) { - view.$el.processServerError(error); - }); - } - }; - - this.prototype.initialize = function(options) { - if (this.model) { - this.listenTo(this.model, 'validation:sync', function() { - this.$el.removeAllErrors(); - }); - - this.listenTo(this.model, 'validation:failed', validationFailed); - } else { - this.listenTo(this, 'validation:sync', function() { - this.$el.removeAllErrors(); - }); - - this.listenTo(this, 'validation:failed', validationFailed); - } - - if (originalInitialize) { - originalInitialize.call(this, options); - } - }; - - this.prototype.onRender = function() { - Validation.bind(this); - this.bindToModelValidation = bindToModel.bind(this); - - if (this.model) { - this.bindToModelValidation(this.model); - } - - if (originalOnRender) { - originalOnRender.call(this); - } - }; - - this.prototype.onBeforeClose = function() { - if (this.model) { - Validation.unbind(this); - - //If we don't do this the next time the model is used the sync is bound to an old view - this.model.sync = this.model._originalSync; - this.model._originalSync = undefined; - } - - if (originalBeforeClose) { - originalBeforeClose.call(this); - } - }; - - return this; - }; -}).call(this); \ No newline at end of file diff --git a/src/UI/Mixins/AutoComplete.js b/src/UI/Mixins/AutoComplete.js deleted file mode 100644 index f0499d373..000000000 --- a/src/UI/Mixins/AutoComplete.js +++ /dev/null @@ -1,51 +0,0 @@ -var $ = require('jquery'); -require('typeahead'); - -$.fn.autoComplete = function(options) { - if (!options) { - throw 'options are required'; - } - - if (!options.resource) { - throw 'resource is required'; - } - - if (!options.query) { - throw 'query is required'; - } - - $(this).typeahead({ - hint : true, - highlight : true, - minLength : 3, - items : 20 - }, { - name : options.resource.replace('/'), - displayKey : '', - source : function(filter, callback) { - var data = options.data || {}; - data[options.query] = filter; - $.ajax({ - url : window.NzbDrone.ApiRoot + options.resource, - dataType : 'json', - type : 'GET', - data : data, - success : function(response) { - if (options.filter) { - options.filter.call(this, filter, response, callback); - } else { - var matches = []; - - $.each(response, function(i, d) { - if (d[options.query] && d[options.property].startsWith(filter)) { - matches.push({ value : d[options.property] }); - } - }); - - callback(matches); - } - } - }); - } - }); -}; \ No newline at end of file diff --git a/src/UI/Mixins/CopyToClipboard.js b/src/UI/Mixins/CopyToClipboard.js deleted file mode 100644 index 77db6e39a..000000000 --- a/src/UI/Mixins/CopyToClipboard.js +++ /dev/null @@ -1,22 +0,0 @@ -var $ = require('jquery'); -var StatusModel = require('../System/StatusModel'); -var ZeroClipboard = require('zero.clipboard'); -var Messenger = require('../Shared/Messenger'); - -$.fn.copyToClipboard = function(input) { - - ZeroClipboard.config({ - swfPath : StatusModel.get('urlBase') + '/Content/zero.clipboard.swf' - }); - - var client = new ZeroClipboard(this); - - client.on('ready', function(e) { - client.on('copy', function(e) { - e.clipboardData.setData("text/plain", input.val()); - }); - client.on('aftercopy', function() { - Messenger.show({ message : 'Copied text to clipboard' }); - }); - }); -}; \ No newline at end of file diff --git a/src/UI/Mixins/DirectoryAutoComplete.js b/src/UI/Mixins/DirectoryAutoComplete.js deleted file mode 100644 index f18ed35de..000000000 --- a/src/UI/Mixins/DirectoryAutoComplete.js +++ /dev/null @@ -1,29 +0,0 @@ -var $ = require('jquery'); -require('./AutoComplete'); - -$.fn.directoryAutoComplete = function(options) { - options = options || {}; - - var query = 'path'; - var data = { - includeFiles: options.includeFiles || false - }; - - $(this).autoComplete({ - resource : '/filesystem', - query : query, - data : data, - filter : function(filter, response, callback) { - var matches = []; - var results = response.directories.concat(response.files); - - $.each(results, function(i, d) { - if (d[query] && d[query].startsWith(filter)) { - matches.push({ value : d[query] }); - } - }); - - callback(matches); - } - }); -}; \ No newline at end of file diff --git a/src/UI/Mixins/FileBrowser.js b/src/UI/Mixins/FileBrowser.js deleted file mode 100644 index a19318fb4..000000000 --- a/src/UI/Mixins/FileBrowser.js +++ /dev/null @@ -1,32 +0,0 @@ -var $ = require('jquery'); -var vent = require('vent'); -require('../Shared/FileBrowser/FileBrowserLayout'); -require('./DirectoryAutoComplete'); - -$.fn.fileBrowser = function(options) { - var inputs = $(this); - - inputs.each(function() { - var input = $(this); - var inputOptions = $.extend({ input : input, showFiles: input.hasClass('x-filepath') }, options); - var inputGroup = $('<div class="input-group"></div>'); - var inputGroupButton = $('<span class="input-group-btn"></span>'); - - var button = $('<button class="btn btn-primary x-file-browser" title="Browse"><i class="icon-lidarr-folder-open"/></button>'); - - if (input.parent('.input-group').length > 0) { - input.parent('.input-group').find('.input-group-btn').prepend(button); - } else { - inputGroupButton.append(button); - input.wrap(inputGroup); - input.after(inputGroupButton); - } - - button.on('click', function() { - vent.trigger(vent.Commands.ShowFileBrowser, inputOptions); - }); - - input.directoryAutoComplete({ includeFiles: inputOptions.showFiles }); - }); - -}; diff --git a/src/UI/Mixins/TagInput.js b/src/UI/Mixins/TagInput.js deleted file mode 100644 index 0f6a542b4..000000000 --- a/src/UI/Mixins/TagInput.js +++ /dev/null @@ -1,156 +0,0 @@ -var $ = require('jquery'); -var _ = require('underscore'); -var TagCollection = require('../Tags/TagCollection'); -var TagModel = require('../Tags/TagModel'); -require('bootstrap.tagsinput'); - -var substringMatcher = function(tagCollection) { - return function findMatches (q, cb) { - q = q.replace(/[^-_a-z0-9]/gi, '').toLowerCase(); - var matches = _.select(tagCollection.toJSON(), function(tag) { - return tag.label.toLowerCase().indexOf(q) > -1; - }); - cb(matches); - }; -}; -var getExistingTags = function(tagValues) { - return _.select(TagCollection.toJSON(), function(tag) { - return _.contains(tagValues, tag.id); - }); -}; - -var testTag = function(item) { - var tagLimitations = new RegExp('[^-_a-z0-9]', 'i'); - try { - return !tagLimitations.test(item); - } - catch (e) { - return false; - } -}; - -var originalAdd = $.fn.tagsinput.Constructor.prototype.add; -var originalRemove = $.fn.tagsinput.Constructor.prototype.remove; -var originalBuild = $.fn.tagsinput.Constructor.prototype.build; - -$.fn.tagsinput.Constructor.prototype.add = function(item, dontPushVal) { - var tagCollection = this.options.tagCollection; - - if (!tagCollection) { - originalAdd.call(this, item, dontPushVal); - return; - } - var self = this; - - if (typeof item === 'string') { - var existing = _.find(tagCollection.toJSON(), { label : item }); - - if (existing) { - originalAdd.call(this, existing, dontPushVal); - } else if (this.options.allowNew) { - if (item === null || item === '' || !testTag(item)) { - return; - } - - var newTag = new TagModel(); - newTag.set({ label : item.toLowerCase() }); - tagCollection.add(newTag); - - newTag.save().done(function() { - item = newTag.toJSON(); - originalAdd.call(self, item, dontPushVal); - }); - } - } else { - originalAdd.call(self, item, dontPushVal); - } - - self.$input.typeahead('val', ''); -}; - -$.fn.tagsinput.Constructor.prototype.remove = function(item, dontPushVal) { - if (item === null) { - return; - } - - originalRemove.call(this, item, dontPushVal); -}; - -$.fn.tagsinput.Constructor.prototype.build = function(options) { - var self = this; - var defaults = { - confirmKeys : [ - 9, - 13, - 32, - 44, - 59 - ] //tab, enter, space, comma, semi-colon - }; - - options = $.extend({}, defaults, options); - - self.$input.on('keydown', function(event) { - if (event.which === 9) { - var e = $.Event('keypress'); - e.which = 9; - self.$input.trigger(e); - event.preventDefault(); - } - }); - - self.$input.on('focusout', function() { - self.add(self.$input.val()); - self.$input.val(''); - }); - - originalBuild.call(this, options); -}; - -$.fn.tagInput = function(options) { - options = $.extend({}, { allowNew : true }, options); - - var input = this; - var model = options.model; - var property = options.property; - - var tagInput = $(this).tagsinput({ - tagCollection : TagCollection, - freeInput : true, - allowNew : options.allowNew, - itemValue : 'id', - itemText : 'label', - trimValue : true, - typeaheadjs : { - name : 'tags', - displayKey : 'label', - source : substringMatcher(TagCollection) - } - }); - - //Override the free input being set to false because we're using objects - $(tagInput)[0].options.freeInput = true; - - if (model) { - var tags = getExistingTags(model.get(property)); - - //Remove any existing tags and re-add them - $(this).tagsinput('removeAll'); - _.each(tags, function(tag) { - $(input).tagsinput('add', tag); - }); - $(this).tagsinput('refresh'); - $(this).on('itemAdded', function(event) { - var tags = model.get(property); - tags.push(event.item.id); - model.set(property, tags); - }); - $(this).on('itemRemoved', function(event) { - if (!event.item) { - return; - } - var tags = _.without(model.get(property), event.item.id); - model.set(property, tags); - }); - } -}; \ No newline at end of file diff --git a/src/UI/Mixins/backbone.signalr.mixin.js b/src/UI/Mixins/backbone.signalr.mixin.js deleted file mode 100644 index 8aad9b71c..000000000 --- a/src/UI/Mixins/backbone.signalr.mixin.js +++ /dev/null @@ -1,46 +0,0 @@ -var vent = require('vent'); -var _ = require('underscore'); -var Backbone = require('backbone'); - -require('signalR'); - -module.exports = _.extend(Backbone.Collection.prototype, { - bindSignalR : function(bindOptions) { - - var collection = this; - bindOptions = bindOptions || {}; - - var processMessage = function(options) { - if (options.action === 'sync') { - console.log('sync received, re-fetching collection'); - collection.fetch(); - - return; - } - - if (options.action === 'deleted') { - collection.remove(new collection.model(options.resource, { parse : true })); - - return; - } - - var model = new collection.model(options.resource, { parse : true }); - - //updateOnly will prevent the collection from adding a new item - if (bindOptions.updateOnly && !collection.get(model.get('id'))) { - return; - } - - collection.add(model, { - merge : true, - changeSource : 'signalr' - }); - - console.log(options.action + ': {0}}'.format(options.resource)); - }; - - collection.listenTo(vent, 'server:' + collection.url.split('/api/')[1], processMessage); - - return this; - } -}); \ No newline at end of file diff --git a/src/UI/Navbar/NavbarLayout.js b/src/UI/Navbar/NavbarLayout.js deleted file mode 100644 index c364a0923..000000000 --- a/src/UI/Navbar/NavbarLayout.js +++ /dev/null @@ -1,63 +0,0 @@ -var Marionette = require('marionette'); -var $ = require('jquery'); -var HealthView = require('../Health/HealthView'); -var QueueView = require('../Activity/Queue/QueueView'); -require('./Search'); - -module.exports = Marionette.Layout.extend({ - template : 'Navbar/NavbarLayoutTemplate', - - regions : { - health : '#x-health', - queue : '#x-queue-count' - }, - - ui : { - search : '.x-series-search', - collapse : '.x-navbar-collapse' - }, - - events : { - 'click a' : 'onClick' - }, - - onRender : function() { - this.ui.search.bindSearch(); - this.health.show(new HealthView()); - this.queue.show(new QueueView()); - }, - - onClick : function(event) { - var target = $(event.target); - - //look down for <a/> - var href = event.target.getAttribute('href'); - - if (href && href.startsWith("http")) { - return; - } - - event.preventDefault(); - - //if couldn't find it look up' - if (!href && target.closest('a') && target.closest('a')[0]) { - - var linkElement = target.closest('a')[0]; - - href = linkElement.getAttribute('href'); - this.setActive(linkElement); - } else { - this.setActive(event.target); - } - - if ($(window).width() < 768) { - this.ui.collapse.collapse('hide'); - } - }, - - setActive : function(element) { - //Todo: Set active on first load - this.$('a').removeClass('active'); - $(element).addClass('active'); - } -}); \ No newline at end of file diff --git a/src/UI/Navbar/NavbarLayoutTemplate.hbs b/src/UI/Navbar/NavbarLayoutTemplate.hbs deleted file mode 100644 index 4fb80fabd..000000000 --- a/src/UI/Navbar/NavbarLayoutTemplate.hbs +++ /dev/null @@ -1,44 +0,0 @@ -<!-- Static navbar --> -<div class="navbar navbar-nzbdrone" role="navigation"> - <div class="container-fluid"> - <div class="navbar-header"> - <button type="button" class="navbar-toggle navbar-inverse" data-toggle="collapse" data-target=".navbar-collapse"> - <span class="sr-only">Toggle navigation</span> - <span class="icon-lidarr-navbar-collapsed fa-lg"></span> - </button> - <a class="navbar-brand" href="{{UrlBase}}/"> - <!--<img src="{{UrlBase}}/Content/Images/logo.png?v=2" alt="Lidarr">--> - <img src="{{UrlBase}}/Content/Images/logos/128.png" class="visible-lg"/> - <img src="{{UrlBase}}/Content/Images/logos/64.png" class="visible-md visible-sm"/> - <span class="visible-xs"> - <img src="{{UrlBase}}/Content/Images/logos/32.png"/> - <span class="logo-text">lidarr</span> - </span> - - </a> - </div> - <div class="navbar-collapse collapse x-navbar-collapse"> - <ul class="nav navbar-nav"> - <li><a href="{{UrlBase}}/" class="x-series-nav"><i class="icon-lidarr-navbar-icon icon-lidarr-navbar-artist"></i> Artists</a></li> - <li><a href="{{UrlBase}}/calendar" class="x-calendar-nav"><i class="icon-lidarr-navbar-icon icon-lidarr-navbar-calendar"></i> Calendar</a></li> - <li><a href="{{UrlBase}}/activity" class="x-activity-nav"><i class="icon-lidarr-navbar-icon icon-lidarr-navbar-activity"></i> Activity<span id="x-queue-count" class="navbar-info"></span></a></li> - <li><a href="{{UrlBase}}/wanted" class="x-wanted-nav"><i class="icon-lidarr-navbar-icon icon-lidarr-navbar-wanted"></i> Wanted</a></li> - <li><a href="{{UrlBase}}/settings" class="x-settings-nav"><i class="icon-lidarr-navbar-icon icon-lidarr-navbar-settings"></i> Settings</a></li> - <li><a href="{{UrlBase}}/system" class="x-system-nav"><i class="icon-lidarr-navbar-icon icon-lidarr-navbar-system"></i> System<span id="x-health" class="navbar-info"></span></a></li> - <li><a href="https://paypal.me/Lidarr" target="_blank"><i class="icon-lidarr-navbar-icon icon-lidarr-navbar-donate"></i> Donate</a></li> - </ul> - <ul class="nav navbar-nav navbar-right"> - <li class="active screen-size"></li> - </ul> - </div><!--/.nav-collapse --> - </div><!--/.container-fluid --> - - <div class="col-md-12 search"> - <div class="col-md-6 col-md-offset-3"> - <div class="input-group"> - <span class="input-group-addon"><i class="fa fa-search"></i></span> - <input type="text" class="col-md-6 form-control x-series-search" placeholder="Search the artist in your library"> - </div> - </div> - </div> -</div> \ No newline at end of file diff --git a/src/UI/Navbar/Search.js b/src/UI/Navbar/Search.js deleted file mode 100644 index dfc2ae8fb..000000000 --- a/src/UI/Navbar/Search.js +++ /dev/null @@ -1,38 +0,0 @@ -var _ = require('underscore'); -var $ = require('jquery'); -var vent = require('vent'); -var Backbone = require('backbone'); -var ArtistCollection = require('../Artist/ArtistCollection'); -require('typeahead'); - -vent.on(vent.Hotkeys.NavbarSearch, function() { - $('.x-artist-search').focus(); -}); - -var substringMatcher = function() { - - return function findMatches (q, cb) { - var matches = _.select(ArtistCollection.toJSON(), function(artist) { - return artist.name.toLowerCase().indexOf(q.toLowerCase()) > -1; - }); - cb(matches); - }; -}; - -$.fn.bindSearch = function() { - $(this).typeahead({ - hint : true, - highlight : true, - minLength : 1 - }, { - name : 'artist', - displayKey : 'name', - source : substringMatcher() - }); - - $(this).on('typeahead:selected typeahead:autocompleted', function(e, artist) { - this.blur(); - $(this).val(''); - Backbone.history.navigate('/artist/{0}'.format(artist.nameSlug), { trigger : true }); - }); -}; \ No newline at end of file diff --git a/src/UI/Profile/ProfileCollection.js b/src/UI/Profile/ProfileCollection.js deleted file mode 100644 index 838ac3c9c..000000000 --- a/src/UI/Profile/ProfileCollection.js +++ /dev/null @@ -1,13 +0,0 @@ -var Backbone = require('backbone'); -var ProfileModel = require('./ProfileModel'); - -var ProfileCollection = Backbone.Collection.extend({ - model : ProfileModel, - url : window.NzbDrone.ApiRoot + '/profile' -}); - -var profiles = new ProfileCollection(); - -profiles.fetch(); - -module.exports = profiles; diff --git a/src/UI/Profile/ProfileModel.js b/src/UI/Profile/ProfileModel.js deleted file mode 100644 index 259e4be5f..000000000 --- a/src/UI/Profile/ProfileModel.js +++ /dev/null @@ -1,9 +0,0 @@ -var DeepModel = require('backbone.deepmodel'); - -module.exports = DeepModel.extend({ - defaults : { - id : null, - name : '', - cutoff : null - } -}); \ No newline at end of file diff --git a/src/UI/Profile/ProfileSelectionPartial.hbs b/src/UI/Profile/ProfileSelectionPartial.hbs deleted file mode 100644 index 5526b361d..000000000 --- a/src/UI/Profile/ProfileSelectionPartial.hbs +++ /dev/null @@ -1,5 +0,0 @@ -<select class="col-md-2 form-control x-profile"> - {{#each this}} - <option value="{{id}}">{{name}}</option> - {{/each}} -</select> \ No newline at end of file diff --git a/src/UI/Quality/QualityDefinitionCollection.js b/src/UI/Quality/QualityDefinitionCollection.js deleted file mode 100644 index 7f111f2fd..000000000 --- a/src/UI/Quality/QualityDefinitionCollection.js +++ /dev/null @@ -1,7 +0,0 @@ -var Backbone = require('backbone'); -var QualityDefinitionModel = require('./QualityDefinitionModel'); - -module.exports = Backbone.Collection.extend({ - model : QualityDefinitionModel, - url : window.NzbDrone.ApiRoot + '/qualitydefinition' -}); \ No newline at end of file diff --git a/src/UI/Quality/QualityDefinitionModel.js b/src/UI/Quality/QualityDefinitionModel.js deleted file mode 100644 index e5a901b6d..000000000 --- a/src/UI/Quality/QualityDefinitionModel.js +++ /dev/null @@ -1,14 +0,0 @@ -var ModelBase = require('../Settings/SettingsModelBase'); - -module.exports = ModelBase.extend({ - baseInitialize : ModelBase.prototype.initialize, - - initialize : function() { - var name = this.get('quality').name; - - this.successMessage = 'Saved ' + name + ' quality settings'; - this.errorMessage = 'Couldn\'t save ' + name + ' quality settings'; - - this.baseInitialize.call(this); - } -}); \ No newline at end of file diff --git a/src/UI/Release/AgeCell.js b/src/UI/Release/AgeCell.js deleted file mode 100644 index f5a4bc7de..000000000 --- a/src/UI/Release/AgeCell.js +++ /dev/null @@ -1,33 +0,0 @@ -var moment = require('moment'); -var Backgrid = require('backgrid'); -var UiSettings = require('../Shared/UiSettingsModel'); -var FormatHelpers = require('../Shared/FormatHelpers'); - -module.exports = Backgrid.Cell.extend({ - className : 'age-cell', - - render : function() { - var age = this.model.get('age'); - var ageHours = this.model.get('ageHours'); - var ageMinutes = this.model.get('ageMinutes'); - var published = moment(this.model.get('publishDate')); - var publishedFormatted = published.format('{0} {1}'.format(UiSettings.get('shortDateFormat'), UiSettings.time(true, true))); - var formatted = age; - var suffix = FormatHelpers.plural(age, 'day'); - - if (age < 2) { - formatted = ageHours.toFixed(1); - suffix = FormatHelpers.plural(Math.round(ageHours), 'hour'); - } - - if (ageHours < 2) { - formatted = ageMinutes.toFixed(1); - suffix = FormatHelpers.plural(Math.round(ageMinutes), 'minute'); - } - - this.$el.html('<div title="{2}">{0} {1}</div>'.format(formatted, suffix, publishedFormatted)); - - this.delegateEvents(); - return this; - } -}); \ No newline at end of file diff --git a/src/UI/Release/DownloadReportCell.js b/src/UI/Release/DownloadReportCell.js deleted file mode 100644 index 3c54d55ba..000000000 --- a/src/UI/Release/DownloadReportCell.js +++ /dev/null @@ -1,49 +0,0 @@ -var Backgrid = require('backgrid'); - -module.exports = Backgrid.Cell.extend({ - className : 'download-report-cell', - - events : { - 'click' : '_onClick' - }, - - _onClick : function() { - if (!this.model.get('downloadAllowed')) { - return; - } - - var self = this; - - this.$el.html('<i class="icon-lidarr-spinner fa-spin" title="Adding to download queue" />'); - - //Using success callback instead of promise so it - //gets called before the sync event is triggered - var promise = this.model.save(null, { - success : function() { - self.model.set('queued', true); - } - }); - - promise.fail(function (xhr) { - if (xhr.responseJSON && xhr.responseJSON.message) { - self.$el.html('<i class="icon-lidarr-download-failed" title="{0}" />'.format(xhr.responseJSON.message)); - } else { - self.$el.html('<i class="icon-lidarr-download-failed" title="Failed to add to download queue" />'); - } - }); - }, - - render : function() { - this.$el.empty(); - - if (this.model.get('queued')) { - this.$el.html('<i class="icon-lidarr-downloading" title="Added to downloaded queue" />'); - } else if (this.model.get('downloadAllowed')) { - this.$el.html('<i class="icon-lidarr-download" title="Add to download queue" />'); - } else { - this.className = 'no-download-report-cell'; - } - - return this; - } -}); \ No newline at end of file diff --git a/src/UI/Release/PeersCell.js b/src/UI/Release/PeersCell.js deleted file mode 100644 index 033c69115..000000000 --- a/src/UI/Release/PeersCell.js +++ /dev/null @@ -1,28 +0,0 @@ -var Backgrid = require('backgrid'); - -module.exports = Backgrid.Cell.extend({ - className : 'peers-cell', - - render : function() { - if (this.model.get('protocol') === 'torrent') { - var seeders = this.model.get('seeders') || 0; - var leechers = this.model.get('leechers') || 0; - - var level = 'danger'; - - if (seeders > 0) { - level = 'warning'; - } else if (seeders > 10) { - level = 'info'; - } else if (seeders > 50) { - level = 'primary'; - } - - this.$el.html('<div class="label label-{2}" title="{0} seeders, {1} leechers">{0} / {1}</div>'.format(seeders, leechers, level)); - } - - this.delegateEvents(); - - return this; - } -}); \ No newline at end of file diff --git a/src/UI/Release/ProtocolCell.js b/src/UI/Release/ProtocolCell.js deleted file mode 100644 index ede35f9b3..000000000 --- a/src/UI/Release/ProtocolCell.js +++ /dev/null @@ -1,24 +0,0 @@ -var Backgrid = require('backgrid'); - -module.exports = Backgrid.Cell.extend({ - className : 'protocol-cell', - - render : function() { - var protocol = this.model.get('protocol') || 'Unknown'; - var label = '??'; - - if (protocol) { - if (protocol === 'torrent') { - label = 'torrent'; - } else if (protocol === 'usenet') { - label = 'nzb'; - } - - this.$el.html('<div class="label label-default protocol-{0}" title="{0}">{1}</div>'.format(protocol, label)); - } - - this.delegateEvents(); - - return this; - } -}); \ No newline at end of file diff --git a/src/UI/Release/ReleaseCollection.js b/src/UI/Release/ReleaseCollection.js deleted file mode 100644 index a2dbb13ed..000000000 --- a/src/UI/Release/ReleaseCollection.js +++ /dev/null @@ -1,56 +0,0 @@ -var PagableCollection = require('backbone.pageable'); -var ReleaseModel = require('./ReleaseModel'); -var AsSortedCollection = require('../Mixins/AsSortedCollection'); - -var Collection = PagableCollection.extend({ - url : window.NzbDrone.ApiRoot + '/release', - model : ReleaseModel, - - state : { - pageSize : 2000, - sortKey : 'download', - order : -1 - }, - - mode : 'client', - - sortMappings : { - 'quality' : { - sortKey : 'qualityWeight' - }, - 'rejections' : { - sortValue : function(model) { - var rejections = model.get('rejections'); - var releaseWeight = model.get('releaseWeight'); - - if (rejections.length !== 0) { - return releaseWeight + 1000000; - } - - return releaseWeight; - } - }, - 'download' : { - sortKey : 'releaseWeight' - }, - 'seeders' : { - sortValue : function(model) { - var seeders = model.get('seeders') || 0; - var leechers = model.get('leechers') || 0; - - return seeders * 1000000 + leechers; - } - }, - 'age' : { - sortKey : 'ageMinutes' - } - }, - - fetchAlbumReleases : function(albumId) { - return this.fetch({ data : { albumId : albumId } }); - } -}); - -Collection = AsSortedCollection.call(Collection); - -module.exports = Collection; \ No newline at end of file diff --git a/src/UI/Release/ReleaseLayout.js b/src/UI/Release/ReleaseLayout.js deleted file mode 100644 index 07f4a1af6..000000000 --- a/src/UI/Release/ReleaseLayout.js +++ /dev/null @@ -1,78 +0,0 @@ -var Marionette = require('marionette'); -var Backgrid = require('backgrid'); -var ReleaseCollection = require('./ReleaseCollection'); -var IndexerCell = require('../Cells/IndexerCell'); -var EpisodeNumberCell = require('../Cells/EpisodeNumberCell'); -var FileSizeCell = require('../Cells/FileSizeCell'); -var QualityCell = require('../Cells/QualityCell'); -var ApprovalStatusCell = require('../Cells/ApprovalStatusCell'); -var LoadingView = require('../Shared/LoadingView'); - -module.exports = Marionette.Layout.extend({ - template : 'Release/ReleaseLayoutTemplate', - - regions : { - grid : '#x-grid', - toolbar : '#x-toolbar' - }, - - columns : [ - { - name : 'indexer', - label : 'Indexer', - sortable : true, - cell : IndexerCell - }, - { - name : 'title', - label : 'Title', - sortable : true, - cell : Backgrid.StringCell - }, - { - name : 'episodeNumbers', - episodes : 'episodeNumbers', - label : 'season', - cell : EpisodeNumberCell - }, - { - name : 'size', - label : 'Size', - sortable : true, - cell : FileSizeCell - }, - { - name : 'quality', - label : 'Quality', - sortable : true, - cell : QualityCell - }, - { - name : 'rejections', - label : '', - cell : ApprovalStatusCell, - title : 'Release Rejected' - } - ], - - initialize : function() { - this.collection = new ReleaseCollection(); - this.listenTo(this.collection, 'sync', this._showTable); - }, - - onRender : function() { - this.grid.show(new LoadingView()); - this.collection.fetch(); - }, - - _showTable : function() { - if (!this.isClosed) { - this.grid.show(new Backgrid.Grid({ - row : Backgrid.Row, - columns : this.columns, - collection : this.collection, - className : 'table table-hover' - })); - } - } -}); \ No newline at end of file diff --git a/src/UI/Release/ReleaseLayoutTemplate.hbs b/src/UI/Release/ReleaseLayoutTemplate.hbs deleted file mode 100644 index 429260d74..000000000 --- a/src/UI/Release/ReleaseLayoutTemplate.hbs +++ /dev/null @@ -1,7 +0,0 @@ -<div id="x-toolbar"/> -<div class="row"> - <div class="col-md-12"> - <div id="x-grid"/> - </div> -</div> - diff --git a/src/UI/Release/ReleaseModel.js b/src/UI/Release/ReleaseModel.js deleted file mode 100644 index 3986a5948..000000000 --- a/src/UI/Release/ReleaseModel.js +++ /dev/null @@ -1,3 +0,0 @@ -var Backbone = require('backbone'); - -module.exports = Backbone.Model.extend({}); \ No newline at end of file diff --git a/src/UI/Rename/RenamePreviewCollection.js b/src/UI/Rename/RenamePreviewCollection.js deleted file mode 100644 index 3d5325933..000000000 --- a/src/UI/Rename/RenamePreviewCollection.js +++ /dev/null @@ -1,34 +0,0 @@ -var Backbone = require('backbone'); -var RenamePreviewModel = require('./RenamePreviewModel'); - -module.exports = Backbone.Collection.extend({ - url : window.NzbDrone.ApiRoot + '/rename', - model : RenamePreviewModel, - - originalFetch : Backbone.Collection.prototype.fetch, - - initialize : function(options) { - if (!options.artistId) { - throw 'artistId is required'; - } - - this.artistId = options.artistId; - this.albumId = options.albumId; - }, - - fetch : function(options) { - if (!this.artistId) { - throw 'artistId is required'; - } - - options = options || {}; - options.data = {}; - options.data.artistId = this.artistId; - - if (this.albumId !== undefined) { - options.data.albumId = this.albumId; - } - - return this.originalFetch.call(this, options); - } -}); \ No newline at end of file diff --git a/src/UI/Rename/RenamePreviewCollectionView.js b/src/UI/Rename/RenamePreviewCollectionView.js deleted file mode 100644 index 7751b666d..000000000 --- a/src/UI/Rename/RenamePreviewCollectionView.js +++ /dev/null @@ -1,6 +0,0 @@ -var Marionette = require('marionette'); -var RenamePreviewItemView = require('./RenamePreviewItemView'); - -module.exports = Marionette.CollectionView.extend({ - itemView : RenamePreviewItemView -}); \ No newline at end of file diff --git a/src/UI/Rename/RenamePreviewEmptyCollectionView.js b/src/UI/Rename/RenamePreviewEmptyCollectionView.js deleted file mode 100644 index f7b7a5166..000000000 --- a/src/UI/Rename/RenamePreviewEmptyCollectionView.js +++ /dev/null @@ -1,6 +0,0 @@ -var vent = require('vent'); -var Marionette = require('marionette'); - -module.exports = Marionette.ItemView.extend({ - template : 'Rename/RenamePreviewEmptyCollectionViewTemplate' -}); \ No newline at end of file diff --git a/src/UI/Rename/RenamePreviewEmptyCollectionViewTemplate.hbs b/src/UI/Rename/RenamePreviewEmptyCollectionViewTemplate.hbs deleted file mode 100644 index eeb0e3d0c..000000000 --- a/src/UI/Rename/RenamePreviewEmptyCollectionViewTemplate.hbs +++ /dev/null @@ -1,3 +0,0 @@ -<div class="alert alert-success"> - Success! My work is done, no files to rename. -</div> \ No newline at end of file diff --git a/src/UI/Rename/RenamePreviewFormatView.js b/src/UI/Rename/RenamePreviewFormatView.js deleted file mode 100644 index 8baab7d0e..000000000 --- a/src/UI/Rename/RenamePreviewFormatView.js +++ /dev/null @@ -1,22 +0,0 @@ -var vent = require('vent'); -var Marionette = require('marionette'); -var NamingModel = require('../Settings/MediaManagement/Naming/NamingModel'); - -module.exports = Marionette.ItemView.extend({ - template : 'Rename/RenamePreviewFormatViewTemplate', - - templateHelpers : function() { - var type = this.model.get('seriesType'); - - return { - rename : this.naming.get('renameTracks'), - format : this.naming.get('standardTrackFormat') - }; - }, - - initialize : function() { - this.naming = new NamingModel(); - this.naming.fetch(); - this.listenTo(this.naming, 'sync', this.render); - } -}); \ No newline at end of file diff --git a/src/UI/Rename/RenamePreviewFormatViewTemplate.hbs b/src/UI/Rename/RenamePreviewFormatViewTemplate.hbs deleted file mode 100644 index 77297f56b..000000000 --- a/src/UI/Rename/RenamePreviewFormatViewTemplate.hbs +++ /dev/null @@ -1,3 +0,0 @@ -{{#if rename}} -Naming pattern: {{format}} -{{/if}} diff --git a/src/UI/Rename/RenamePreviewItemView.js b/src/UI/Rename/RenamePreviewItemView.js deleted file mode 100644 index 394cd6045..000000000 --- a/src/UI/Rename/RenamePreviewItemView.js +++ /dev/null @@ -1,39 +0,0 @@ -var vent = require('vent'); -var Marionette = require('marionette'); -var AsModelBoundView = require('../Mixins/AsModelBoundView'); - -var view = Marionette.ItemView.extend({ - template : 'Rename/RenamePreviewItemViewTemplate', - - ui : { - itemDiv : '.rename-preview-item', - checkboxIcon : '.rename-checkbox i' - }, - - onRender : function() { - this._setItemState(); - this.listenTo(this.model, 'change', this._setItemState); - this.listenTo(this.model, 'rename:select', this._onRenameAll); - }, - - _setItemState : function() { - var checked = this.model.get('rename'); - this.model.trigger('rename:select', this.model, checked); - - if (checked) { - this.ui.itemDiv.removeClass('do-not-rename'); - this.ui.checkboxIcon.addClass('icon-lidarr-checked'); - this.ui.checkboxIcon.removeClass('icon-lidarr-unchecked'); - } else { - this.ui.itemDiv.addClass('do-not-rename'); - this.ui.checkboxIcon.addClass('icon-lidarr-unchecked'); - this.ui.checkboxIcon.removeClass('icon-lidarr-checked'); - } - }, - - _onRenameAll : function(model, checked) { - this.model.set('rename', checked); - } -}); - -module.exports = AsModelBoundView.apply(view); \ No newline at end of file diff --git a/src/UI/Rename/RenamePreviewItemViewTemplate.hbs b/src/UI/Rename/RenamePreviewItemViewTemplate.hbs deleted file mode 100644 index 57ef7200f..000000000 --- a/src/UI/Rename/RenamePreviewItemViewTemplate.hbs +++ /dev/null @@ -1,20 +0,0 @@ -<div class="rename-preview-item"> - <div class="row"> - <div class="rename-checkbox col-md-1"> - <label class="checkbox-button" title="Rename File"> - <input type="checkbox" name="rename"/> - <div class="btn"> - <i></i> - </div> - </label> - </div> - <div class="col-md-11"> - <div class="row"> - <div class="col-md-12 file-path"><i class="icon-lidarr-existing" title="Existing path" /> {{existingPath}}</div> - </div> - <div class="row"> - <div class="col-md-12 file-path"><i class="icon-lidarr-suggested" title="Suggested path" /> {{newPath}}</div> - </div> - </div> - </div> -</div> \ No newline at end of file diff --git a/src/UI/Rename/RenamePreviewLayout.js b/src/UI/Rename/RenamePreviewLayout.js deleted file mode 100644 index 9ea4f6c05..000000000 --- a/src/UI/Rename/RenamePreviewLayout.js +++ /dev/null @@ -1,124 +0,0 @@ -var _ = require('underscore'); -var vent = require('vent'); -var Marionette = require('marionette'); -var RenamePreviewCollection = require('./RenamePreviewCollection'); -var RenamePreviewCollectionView = require('./RenamePreviewCollectionView'); -var EmptyCollectionView = require('./RenamePreviewEmptyCollectionView'); -var RenamePreviewFormatView = require('./RenamePreviewFormatView'); -var LoadingView = require('../Shared/LoadingView'); -var CommandController = require('../Commands/CommandController'); - -module.exports = Marionette.Layout.extend({ - className : 'modal-lg', - template : 'Rename/RenamePreviewLayoutTemplate', - - regions : { - renamePreviews : '#rename-previews', - formatRegion : '.x-format-region' - }, - - ui : { - pathInfo : '.x-path-info', - renameAll : '.x-rename-all', - checkboxIcon : '.x-rename-all-button i' - }, - - events : { - 'click .x-organize' : '_organizeFiles', - 'change .x-rename-all' : '_toggleAll' - }, - - initialize : function(options) { - this.model = options.artist; - this.albumId = options.albumId; - - var viewOptions = {}; - viewOptions.artistId = this.model.id; - viewOptions.albumId = this.albumId; - - this.collection = new RenamePreviewCollection(viewOptions); - this.listenTo(this.collection, 'sync', this._showPreviews); - this.listenTo(this.collection, 'rename:select', this._itemRenameChanged); - - this.collection.fetch(); - }, - - onRender : function() { - this.renamePreviews.show(new LoadingView()); - this.formatRegion.show(new RenamePreviewFormatView({ model : this.model })); - }, - - _showPreviews : function() { - if (this.collection.length === 0) { - this.ui.pathInfo.hide(); - this.renamePreviews.show(new EmptyCollectionView()); - return; - } - - this.ui.pathInfo.show(); - this.collection.invoke('set', { rename : true }); - this.renamePreviews.show(new RenamePreviewCollectionView({ collection : this.collection })); - }, - - _organizeFiles : function() { - if (this.collection.length === 0) { - vent.trigger(vent.Commands.CloseModalCommand); - } - - var files = _.map(this.collection.where({ rename : true }), function(model) { - return model.get('trackFileId'); - }); - - if (files.length === 0) { - vent.trigger(vent.Commands.CloseModalCommand); - return; - } - - if (this.albumId) { - CommandController.Execute('renameFiles', { - name : 'renameFiles', - artistId : this.model.id, - albumId : this.albumId, - files : files - }); - } else { - CommandController.Execute('renameFiles', { - name : 'renameFiles', - artistId : this.model.id, - albumId : -1, - files : files - }); - } - - vent.trigger(vent.Commands.CloseModalCommand); - }, - - _setCheckedState : function(checked) { - if (checked) { - this.ui.checkboxIcon.addClass('icon-lidarr-checked'); - this.ui.checkboxIcon.removeClass('icon-lidarr-unchecked'); - } else { - this.ui.checkboxIcon.addClass('icon-lidarr-unchecked'); - this.ui.checkboxIcon.removeClass('icon-lidarr-checked'); - } - }, - - _toggleAll : function() { - var checked = this.ui.renameAll.prop('checked'); - this._setCheckedState(checked); - - this.collection.each(function(model) { - model.trigger('rename:select', model, checked); - }); - }, - - _itemRenameChanged : function(model, checked) { - var allChecked = this.collection.all(function(m) { - return m.get('rename'); - }); - - if (!checked || allChecked) { - this._setCheckedState(checked); - } - } -}); \ No newline at end of file diff --git a/src/UI/Rename/RenamePreviewLayoutTemplate.hbs b/src/UI/Rename/RenamePreviewLayoutTemplate.hbs deleted file mode 100644 index ccb5d9b3d..000000000 --- a/src/UI/Rename/RenamePreviewLayoutTemplate.hbs +++ /dev/null @@ -1,34 +0,0 @@ -<div class="modal-content"> - <div class="rename-preview-modal"> - <div class="modal-header"> - <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> - <h3> - <i class="icon-lidarr-rename"></i> Organize & Rename - </h3> - - </div> - <div class="modal-body"> - <div class="alert alert-info"> - <div class="path-info x-path-info">All paths are relative to: <strong>{{path}}</strong></div> - <div class="x-format-region"></div> - </div> - - <div id="rename-previews"></div> - - </div> - <div class="modal-footer"> - - <span class="rename-all-button x-rename-all-button pull-left"> - <label class="checkbox-button" title="Toggle all"> - <input type="checkbox" checked="checked" class="x-rename-all"/> - <div class="btn btn-icon-only"> - <i class="icon-lidarr-checked"></i> - </div> - </label> - </span> - - <button class="btn" data-dismiss="modal">Close</button> - <button class="btn btn-primary x-organize">Organize</button> - </div> - </div> -</div> diff --git a/src/UI/Rename/RenamePreviewModel.js b/src/UI/Rename/RenamePreviewModel.js deleted file mode 100644 index 3986a5948..000000000 --- a/src/UI/Rename/RenamePreviewModel.js +++ /dev/null @@ -1,3 +0,0 @@ -var Backbone = require('backbone'); - -module.exports = Backbone.Model.extend({}); \ No newline at end of file diff --git a/src/UI/Rename/rename.less b/src/UI/Rename/rename.less deleted file mode 100644 index 07e694016..000000000 --- a/src/UI/Rename/rename.less +++ /dev/null @@ -1,42 +0,0 @@ -@import "../Content/FontAwesome/font-awesome"; -@import "../Content/Bootstrap/variables"; -@import "../Content/variables"; - -.rename-preview-item { - margin-bottom: 5px; - padding: 5px; - border-bottom: 1px solid #e5e5e5; - - &.do-not-rename { - background-color: #aaaaaa; - opacity: 0.7; - } - - .rename-checkbox { - width: 40px; - padding-top: 5px; - margin-right: 10px; - - .checkbox-button { - .btn { - text-align: left; - width: 38px; - } - } - } -} - -.path-info { - display: none; -} - -.rename-all-button { - display: inline-block; - - .checkbox-button { - .btn { - text-align: left; - width: 38px; - } - } -} diff --git a/src/UI/Router.js b/src/UI/Router.js deleted file mode 100644 index e72f0af93..000000000 --- a/src/UI/Router.js +++ /dev/null @@ -1,25 +0,0 @@ -var Marionette = require('marionette'); -var Controller = require('./Controller'); - -module.exports = Marionette.AppRouter.extend({ - controller : new Controller(), - appRoutes : { - 'addartist' : 'addArtist', - 'addartist/:action(/:query)' : 'addArtist', - 'calendar' : 'calendar', - 'settings' : 'settings', - 'settings/:action(/:query)' : 'settings', - 'wanted' : 'wanted', - 'wanted/:action' : 'wanted', - 'history' : 'activity', - 'history/:action' : 'activity', - 'activity' : 'activity', - 'activity/:action' : 'activity', - 'rss' : 'rss', - 'system' : 'system', - 'system/:action' : 'system', - 'albumstudio' : 'albumStudio', - 'artisteditor' : 'artistEditor', - ':whatever' : 'showNotFound' - } -}); \ No newline at end of file diff --git a/src/UI/Series/Delete/DeleteSeriesTemplate.hbs b/src/UI/Series/Delete/DeleteSeriesTemplate.hbs deleted file mode 100644 index caccec733..000000000 --- a/src/UI/Series/Delete/DeleteSeriesTemplate.hbs +++ /dev/null @@ -1,50 +0,0 @@ -<div class="modal-content"> - <div class="modal-header"> - <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> - <h3>Delete {{title}}</h3> - </div> - <div class="modal-body delete-series-modal"> - - <div class="row"> - <div class="col-sm-3 hidden-xs"> - {{poster}} - </div> - <div class="col-sm-9"> - <div class="form-horizontal"> - <h3 class="path">{{path}}</h3> - - <div class="form-group"> - <label class="col-sm-4 control-label">Delete all files</label> - - <div class="col-sm-8"> - <div class="input-group"> - <label class="checkbox toggle well"> - <input type="checkbox" class="x-delete-files"/> - <p> - <span>Yes</span> - <span>No</span> - </p> - - <div class="btn slide-button btn-danger"/> - </label> - - <span class="help-inline-checkbox"> - <i class="icon-lidarr-form-info" title="Do you want to delete all files from disk?"/> - <i class="icon-lidarr-form-warning" title="This option is irreversible, use with extreme caution"/> - </span> - </div> - </div> - </div> - <div class="col-md-offset-1 col-md-5 delete-files-info x-delete-files-info"> - {{episodeFileCount}} episode files will be deleted - </div> - </div> - </div> - </div> - </div> - <div class="modal-footer"> - <span class="indicator x-indicator"><i class="icon-lidarr-spinner fa-spin"></i></span> - <button class="btn" data-dismiss="modal">Cancel</button> - <button class="btn btn-danger x-confirm-delete">Delete</button> - </div> -</div> diff --git a/src/UI/Series/Delete/DeleteSeriesView.js b/src/UI/Series/Delete/DeleteSeriesView.js deleted file mode 100644 index de6640b5e..000000000 --- a/src/UI/Series/Delete/DeleteSeriesView.js +++ /dev/null @@ -1,41 +0,0 @@ -var vent = require('vent'); -var Marionette = require('marionette'); - -module.exports = Marionette.ItemView.extend({ - template : 'Series/Delete/DeleteSeriesTemplate', - - events : { - 'click .x-confirm-delete' : 'removeSeries', - 'change .x-delete-files' : 'changeDeletedFiles' - }, - - ui : { - deleteFiles : '.x-delete-files', - deleteFilesInfo : '.x-delete-files-info', - indicator : '.x-indicator' - }, - - removeSeries : function() { - var self = this; - var deleteFiles = this.ui.deleteFiles.prop('checked'); - this.ui.indicator.show(); - - this.model.destroy({ - data : { 'deleteFiles' : deleteFiles }, - wait : true - }).done(function() { - vent.trigger(vent.Events.SeriesDeleted, { series : self.model }); - vent.trigger(vent.Commands.CloseModalCommand); - }); - }, - - changeDeletedFiles : function() { - var deleteFiles = this.ui.deleteFiles.prop('checked'); - - if (deleteFiles) { - this.ui.deleteFilesInfo.show(); - } else { - this.ui.deleteFilesInfo.hide(); - } - } -}); \ No newline at end of file diff --git a/src/UI/Series/Details/EpisodeNumberCell.js b/src/UI/Series/Details/EpisodeNumberCell.js deleted file mode 100644 index 9a84e644e..000000000 --- a/src/UI/Series/Details/EpisodeNumberCell.js +++ /dev/null @@ -1,47 +0,0 @@ -var Marionette = require('marionette'); -var NzbDroneCell = require('../../Cells/NzbDroneCell'); -var reqres = require('../../reqres'); -var SeriesCollection = require('../SeriesCollection'); - -module.exports = NzbDroneCell.extend({ - className : 'episode-number-cell', - template : 'Series/Details/EpisodeNumberCellTemplate', - - render : function() { - this.$el.empty(); - this.$el.html(this.model.get('episodeNumber')); - - var series = SeriesCollection.get(this.model.get('seriesId')); - - if (series.get('seriesType') === 'anime' && this.model.has('absoluteEpisodeNumber')) { - this.$el.html('{0} ({1})'.format(this.model.get('episodeNumber'), this.model.get('absoluteEpisodeNumber'))); - } - - var alternateTitles = []; - - if (reqres.hasHandler(reqres.Requests.GetAlternateNameBySeasonNumber)) { - alternateTitles = reqres.request(reqres.Requests.GetAlternateNameBySeasonNumber, this.model.get('seriesId'), this.model.get('seasonNumber'), this.model.get('sceneSeasonNumber')); - } - - if (this.model.get('sceneSeasonNumber') > 0 || this.model.get('sceneEpisodeNumber') > 0 || this.model.has('sceneAbsoluteEpisodeNumber') || alternateTitles.length > 0) { - this.templateFunction = Marionette.TemplateCache.get(this.template); - - var json = this.model.toJSON(); - json.alternateTitles = alternateTitles; - - var html = this.templateFunction(json); - - this.$el.popover({ - content : html, - html : true, - trigger : 'hover', - title : 'Scene Information', - placement : 'right', - container : this.$el - }); - } - - this.delegateEvents(); - return this; - } -}); \ No newline at end of file diff --git a/src/UI/Series/Details/EpisodeNumberCellTemplate.hbs b/src/UI/Series/Details/EpisodeNumberCellTemplate.hbs deleted file mode 100644 index a9028a423..000000000 --- a/src/UI/Series/Details/EpisodeNumberCellTemplate.hbs +++ /dev/null @@ -1,39 +0,0 @@ -<div class="scene-info"> - {{#if sceneSeasonNumber}} - <div class="row"> - <div class="key">Season</div> - <div class="value">{{sceneSeasonNumber}}</div> - </div> - {{/if}} - - {{#if sceneEpisodeNumber}} - <div class="row"> - <div class="key">Episode</div> - <div class="value">{{sceneEpisodeNumber}}</div> - </div> - {{/if}} - - {{#if sceneAbsoluteEpisodeNumber}} - <div class="row"> - <div class="key">Absolute</div> - <div class="value">{{sceneAbsoluteEpisodeNumber}}</div> - </div> - {{/if}} - - {{#if alternateTitles}} - <div class="row"> - {{#if_gt alternateTitles.length compare="1"}} - <div class="key">Titles</div> - {{else}} - <div class="key">Title</div> - {{/if_gt}} - <div class="value"> - <ul> - {{#each alternateTitles}} - <li>{{title}}</li> - {{/each}} - </ul> - </div> - </div> - {{/if}} -</div> \ No newline at end of file diff --git a/src/UI/Series/Details/EpisodeWarningCell.js b/src/UI/Series/Details/EpisodeWarningCell.js deleted file mode 100644 index 1178ac4ab..000000000 --- a/src/UI/Series/Details/EpisodeWarningCell.js +++ /dev/null @@ -1,21 +0,0 @@ -var NzbDroneCell = require('../../Cells/NzbDroneCell'); -var SeriesCollection = require('../SeriesCollection'); - -module.exports = NzbDroneCell.extend({ - className : 'episode-warning-cell', - - render : function() { - this.$el.empty(); - - if (this.model.get('unverifiedSceneNumbering')) { - this.$el.html('<i class="icon-lidarr-form-warning" title="Scene number hasn\'t been verified yet."></i>'); - } - - else if (SeriesCollection.get(this.model.get('seriesId')).get('seriesType') === 'anime' && this.model.get('seasonNumber') > 0 && !this.model.has('absoluteEpisodeNumber')) { - this.$el.html('<i class="icon-lidarr-form-warning" title="Episode does not have an absolute episode number"></i>'); - } - - this.delegateEvents(); - return this; - } -}); \ No newline at end of file diff --git a/src/UI/Series/Details/InfoView.js b/src/UI/Series/Details/InfoView.js deleted file mode 100644 index c7fab9fc4..000000000 --- a/src/UI/Series/Details/InfoView.js +++ /dev/null @@ -1,18 +0,0 @@ -var Marionette = require('marionette'); - -module.exports = Marionette.ItemView.extend({ - template : 'Series/Details/InfoViewTemplate', - - initialize : function(options) { - this.episodeFileCollection = options.episodeFileCollection; - - this.listenTo(this.model, 'change', this.render); - this.listenTo(this.episodeFileCollection, 'sync', this.render); - }, - - templateHelpers : function() { - return { - fileCount : this.episodeFileCollection.length - }; - } -}); \ No newline at end of file diff --git a/src/UI/Series/Details/InfoViewTemplate.hbs b/src/UI/Series/Details/InfoViewTemplate.hbs deleted file mode 100644 index b52130246..000000000 --- a/src/UI/Series/Details/InfoViewTemplate.hbs +++ /dev/null @@ -1,73 +0,0 @@ -<div class="row"> - <div class="col-md-9"> - {{profile profileId}} - - {{#if network}} - <span class="label label-info">{{network}}</span> - {{/if}} - - <span class="label label-info">{{runtime}} minutes</span> - <span class="label label-info">{{path}}</span> - - {{#if ratings}} - <span class="label label-info" title="{{ratings.votes}} vote{{#if_gt ratings.votes compare="1"}}s{{/if_gt}}">{{ratings.value}}</span> - {{/if}} - - <span class="label label-info">{{Bytes sizeOnDisk}}</span> - - {{#if_eq fileCount compare="1"}} - <span class="label label-info"> 1 file</span> - {{else}} - <span class="label label-info"> {{fileCount}} files</span> - {{/if_eq}} - - {{#if_eq status compare="continuing"}} - <span class="label label-info">Continuing</span> - {{else}} - <span class="label label-default">Ended</span> - {{/if_eq}} - </div> - <div class="col-md-3"> - <span class="series-info-links"> - <a href="{{traktUrl}}" class="label label-info">Trakt</a> - - <a href="{{tvdbUrl}}" class="label label-info">The TVDB</a> - - {{#if imdbId}} - <a href="{{imdbUrl}}" class="label label-info">IMDB</a> - {{/if}} - - {{#if tvRageId}} - <a href="{{tvRageUrl}}" class="label label-info">TV Rage</a> - {{/if}} - - {{#if tvMazeId}} - <a href="{{tvMazeUrl}}" class="label label-info">TV Maze</a> - {{/if}} - </span> - </div> -</div> - -{{#if alternateTitles}} -<div class="row"> - <div class="col-md-12"> - {{#each alternateTitles}} - {{#if_eq seasonNumber compare="-1"}} - <span class="label label-default">{{title}}</span> - {{/if_eq}} - - {{#if_eq sceneSeasonNumber compare="-1"}} - <span class="label label-default">{{title}}</span> - {{/if_eq}} - {{/each}} - </div> -</div> -{{/if}} - -{{#if tags}} -<div class="row"> - <div class="col-md-12"> - {{tagDisplay tags}} - </div> -</div> -{{/if}} diff --git a/src/UI/Series/Details/SeasonCollectionView.js b/src/UI/Series/Details/SeasonCollectionView.js deleted file mode 100644 index 24da6171c..000000000 --- a/src/UI/Series/Details/SeasonCollectionView.js +++ /dev/null @@ -1,44 +0,0 @@ -var _ = require('underscore'); -var Marionette = require('marionette'); -var SeasonLayout = require('./SeasonLayout'); -var AsSortedCollectionView = require('../../Mixins/AsSortedCollectionView'); - -var view = Marionette.CollectionView.extend({ - - itemView : SeasonLayout, - - initialize : function(options) { - if (!options.episodeCollection) { - throw 'episodeCollection is needed'; - } - - this.episodeCollection = options.episodeCollection; - this.series = options.series; - }, - - itemViewOptions : function() { - return { - episodeCollection : this.episodeCollection, - series : this.series - }; - }, - - onEpisodeGrabbed : function(message) { - if (message.episode.series.id !== this.episodeCollection.seriesId) { - return; - } - - var self = this; - - _.each(message.episode.episodes, function(episode) { - var ep = self.episodeCollection.get(episode.id); - ep.set('downloading', true); - }); - - this.render(); - } -}); - -AsSortedCollectionView.call(view); - -module.exports = view; \ No newline at end of file diff --git a/src/UI/Series/Details/SeasonLayout.js b/src/UI/Series/Details/SeasonLayout.js deleted file mode 100644 index f87553e79..000000000 --- a/src/UI/Series/Details/SeasonLayout.js +++ /dev/null @@ -1,301 +0,0 @@ -var vent = require('vent'); -var Marionette = require('marionette'); -var Backgrid = require('backgrid'); -var ToggleCell = require('../../Cells/EpisodeMonitoredCell'); -var EpisodeTitleCell = require('../../Cells/EpisodeTitleCell'); -var RelativeDateCell = require('../../Cells/RelativeDateCell'); -var EpisodeStatusCell = require('../../Cells/EpisodeStatusCell'); -var EpisodeActionsCell = require('../../Cells/EpisodeActionsCell'); -var EpisodeNumberCell = require('./EpisodeNumberCell'); -var EpisodeWarningCell = require('./EpisodeWarningCell'); -var CommandController = require('../../Commands/CommandController'); -var EpisodeFileEditorLayout = require('../../EpisodeFile/Editor/EpisodeFileEditorLayout'); -var moment = require('moment'); -var _ = require('underscore'); -var Messenger = require('../../Shared/Messenger'); - -module.exports = Marionette.Layout.extend({ - template : 'Series/Details/SeasonLayoutTemplate', - - ui : { - seasonSearch : '.x-season-search', - seasonMonitored : '.x-season-monitored', - seasonRename : '.x-season-rename' - }, - - events : { - 'click .x-season-episode-file-editor' : '_openEpisodeFileEditor', - 'click .x-season-monitored' : '_seasonMonitored', - 'click .x-season-search' : '_seasonSearch', - 'click .x-season-rename' : '_seasonRename', - 'click .x-show-hide-episodes' : '_showHideEpisodes', - 'dblclick .series-season h2' : '_showHideEpisodes' - }, - - regions : { - episodeGrid : '.x-episode-grid' - }, - - columns : [ - { - name : 'monitored', - label : '', - cell : ToggleCell, - trueClass : 'icon-lidarr-monitored', - falseClass : 'icon-lidarr-unmonitored', - tooltip : 'Toggle monitored status', - sortable : false - }, - { - name : 'episodeNumber', - label : '#', - cell : EpisodeNumberCell - }, - { - name : 'this', - label : '', - cell : EpisodeWarningCell, - sortable : false, - className : 'episode-warning-cell' - }, - { - name : 'this', - label : 'Title', - hideSeriesLink : true, - cell : EpisodeTitleCell, - sortable : false - }, - { - name : 'airDateUtc', - label : 'Air Date', - cell : RelativeDateCell - }, - { - name : 'status', - label : 'Status', - cell : EpisodeStatusCell, - sortable : false - }, - { - name : 'this', - label : '', - cell : EpisodeActionsCell, - sortable : false - } - ], - - templateHelpers : function() { - var episodeCount = this.episodeCollection.filter(function(episode) { - return episode.get('hasFile') || episode.get('monitored') && moment(episode.get('airDateUtc')).isBefore(moment()); - }).length; - - var episodeFileCount = this.episodeCollection.where({ hasFile : true }).length; - var percentOfEpisodes = 100; - - if (episodeCount > 0) { - percentOfEpisodes = episodeFileCount / episodeCount * 100; - } - - return { - showingEpisodes : this.showingEpisodes, - episodeCount : episodeCount, - episodeFileCount : episodeFileCount, - percentOfEpisodes : percentOfEpisodes - }; - }, - - initialize : function(options) { - if (!options.episodeCollection) { - throw 'episodeCollection is required'; - } - - this.series = options.series; - this.fullEpisodeCollection = options.episodeCollection; - this.episodeCollection = this.fullEpisodeCollection.bySeason(this.model.get('seasonNumber')); - this._updateEpisodeCollection(); - - this.showingEpisodes = this._shouldShowEpisodes(); - - this.listenTo(this.model, 'sync', this._afterSeasonMonitored); - this.listenTo(this.episodeCollection, 'sync', this.render); - - this.listenTo(this.fullEpisodeCollection, 'sync', this._refreshEpisodes); - }, - - onRender : function() { - if (this.showingEpisodes) { - this._showEpisodes(); - } - - this._setSeasonMonitoredState(); - - CommandController.bindToCommand({ - element : this.ui.seasonSearch, - command : { - name : 'seasonSearch', - seriesId : this.series.id, - seasonNumber : this.model.get('seasonNumber') - } - }); - - CommandController.bindToCommand({ - element : this.ui.seasonRename, - command : { - name : 'renameFiles', - seriesId : this.series.id, - seasonNumber : this.model.get('seasonNumber') - } - }); - }, - - _seasonSearch : function() { - CommandController.Execute('seasonSearch', { - name : 'seasonSearch', - seriesId : this.series.id, - seasonNumber : this.model.get('seasonNumber') - }); - }, - - _seasonRename : function() { - vent.trigger(vent.Commands.ShowRenamePreview, { - series : this.series, - seasonNumber : this.model.get('seasonNumber') - }); - }, - - _seasonMonitored : function() { - if (!this.series.get('monitored')) { - - Messenger.show({ - message : 'Unable to change monitored state when series is not monitored', - type : 'error' - }); - - return; - } - - var name = 'monitored'; - this.model.set(name, !this.model.get(name)); - this.series.setSeasonMonitored(this.model.get('seasonNumber')); - - var savePromise = this.series.save().always(this._afterSeasonMonitored.bind(this)); - - this.ui.seasonMonitored.spinForPromise(savePromise); - }, - - _afterSeasonMonitored : function() { - var self = this; - - _.each(this.episodeCollection.models, function(episode) { - episode.set({ monitored : self.model.get('monitored') }); - }); - - this.render(); - }, - - _setSeasonMonitoredState : function() { - this.ui.seasonMonitored.removeClass('icon-lidarr-spinner fa-spin'); - - if (this.model.get('monitored')) { - this.ui.seasonMonitored.addClass('icon-lidarr-monitored'); - this.ui.seasonMonitored.removeClass('icon-lidarr-unmonitored'); - } else { - this.ui.seasonMonitored.addClass('icon-lidarr-unmonitored'); - this.ui.seasonMonitored.removeClass('icon-lidarr-monitored'); - } - }, - - _showEpisodes : function() { - this.episodeGrid.show(new Backgrid.Grid({ - columns : this.columns, - collection : this.episodeCollection, - className : 'table table-hover season-grid' - })); - }, - - _shouldShowEpisodes : function() { - var startDate = moment().add('month', -1); - var endDate = moment().add('year', 1); - - return this.episodeCollection.some(function(episode) { - var airDate = episode.get('airDateUtc'); - - if (airDate) { - var airDateMoment = moment(airDate); - - if (airDateMoment.isAfter(startDate) && airDateMoment.isBefore(endDate)) { - return true; - } - } - - return false; - }); - }, - - _showHideEpisodes : function() { - if (this.showingEpisodes) { - this.showingEpisodes = false; - this.episodeGrid.close(); - } else { - this.showingEpisodes = true; - this._showEpisodes(); - } - - this.templateHelpers.showingEpisodes = this.showingEpisodes; - this.render(); - }, - - _episodeMonitoredToggled : function(options) { - var model = options.model; - var shiftKey = options.shiftKey; - - if (!this.episodeCollection.get(model.get('id'))) { - return; - } - - if (!shiftKey) { - return; - } - - var lastToggled = this.episodeCollection.lastToggled; - - if (!lastToggled) { - return; - } - - var currentIndex = this.episodeCollection.indexOf(model); - var lastIndex = this.episodeCollection.indexOf(lastToggled); - - var low = Math.min(currentIndex, lastIndex); - var high = Math.max(currentIndex, lastIndex); - var range = _.range(low + 1, high); - - this.episodeCollection.lastToggled = model; - }, - - _updateEpisodeCollection : function() { - var self = this; - - this.episodeCollection.add(this.fullEpisodeCollection.bySeason(this.model.get('seasonNumber')).models, { merge : true }); - - this.episodeCollection.each(function(model) { - model.episodeCollection = self.episodeCollection; - }); - }, - - _refreshEpisodes : function() { - this._updateEpisodeCollection(); - this.episodeCollection.fullCollection.sort(); - this.render(); - }, - - _openEpisodeFileEditor : function() { - var view = new EpisodeFileEditorLayout({ - model : this.model, - series : this.series, - episodeCollection : this.episodeCollection - }); - - vent.trigger(vent.Commands.OpenModalCommand, view); - } -}); \ No newline at end of file diff --git a/src/UI/Series/Details/SeasonLayoutTemplate.hbs b/src/UI/Series/Details/SeasonLayoutTemplate.hbs deleted file mode 100644 index f16e6439d..000000000 --- a/src/UI/Series/Details/SeasonLayoutTemplate.hbs +++ /dev/null @@ -1,50 +0,0 @@ -<div class="series-season" id="season-{{seasonNumber}}"> - <h2> - <i class="x-season-monitored season-monitored clickable" title="Toggle season monitored status"/> - - {{#if seasonNumber}} - Season {{seasonNumber}} - {{else}} - Specials - {{/if}} - - - {{#if_eq episodeCount compare=0}} - {{#if monitored}} - <span class="badge badge-primary season-status" title="No aired episodes"> </span> - {{else}} - <span class="badge badge-warning season-status" title="Season is not monitored"> </span> - {{/if}} - {{else}} - {{#if_eq percentOfEpisodes compare=100}} - <span class="badge badge-success season-status" title="{{episodeFileCount}}/{{episodeCount}} episodes downloaded">{{episodeFileCount}} / {{episodeCount}}</span> - {{else}} - <span class="badge badge-danger season-status" title="{{episodeFileCount}}/{{episodeCount}} episodes downloaded">{{episodeFileCount}} / {{episodeCount}}</span> - {{/if_eq}} - {{/if_eq}} - - <span class="season-actions pull-right"> - <div class="x-season-episode-file-editor"> - <i class="icon-lidarr-episode-file" title="Modify episode files for season"/> - </div> - <div class="x-season-rename"> - <i class="icon-lidarr-rename" title="Preview rename for season {{seasonNumber}}"/> - </div> - <div class="x-season-search"> - <i class="icon-lidarr-search" title="Search for monitored episodes in season {{seasonNumber}}"/> - </div> - </span> - </h2> - <div class="show-hide-episodes x-show-hide-episodes"> - <h4> - {{#if showingEpisodes}} - <i class="icon-lidarr-panel-hide"/> - Hide Episodes - {{else}} - <i class="icon-lidarr-panel-show"/> - Show Episodes - {{/if}} - </h4> - </div> - <div class="x-episode-grid table-responsive"></div> -</div> diff --git a/src/UI/Series/Details/SeriesDetailsLayout.js b/src/UI/Series/Details/SeriesDetailsLayout.js deleted file mode 100644 index 4b3d34297..000000000 --- a/src/UI/Series/Details/SeriesDetailsLayout.js +++ /dev/null @@ -1,263 +0,0 @@ -var $ = require('jquery'); -var _ = require('underscore'); -var vent = require('vent'); -var reqres = require('../../reqres'); -var Marionette = require('marionette'); -var Backbone = require('backbone'); -var ArtistCollection = require('../../Artist/ArtistCollection'); -var EpisodeCollection = require('../EpisodeCollection'); -var EpisodeFileCollection = require('../EpisodeFileCollection'); -var SeasonCollection = require('../SeasonCollection'); -var SeasonCollectionView = require('./SeasonCollectionView'); -var InfoView = require('./InfoView'); -var CommandController = require('../../Commands/CommandController'); -var LoadingView = require('../../Shared/LoadingView'); -var EpisodeFileEditorLayout = require('../../EpisodeFile/Editor/EpisodeFileEditorLayout'); -require('backstrech'); -require('../../Mixins/backbone.signalr.mixin'); - -module.exports = Marionette.Layout.extend({ - itemViewContainer : '.x-series-seasons', - template : 'Series/Details/SeriesDetailsTemplate', - - regions : { - seasons : '#seasons', - info : '#info' - }, - - ui : { - header : '.x-header', - monitored : '.x-monitored', - edit : '.x-edit', - refresh : '.x-refresh', - rename : '.x-rename', - search : '.x-search', - poster : '.x-series-poster' - }, - - events : { - 'click .x-episode-file-editor' : '_openEpisodeFileEditor', - 'click .x-monitored' : '_toggleMonitored', - 'click .x-edit' : '_editSeries', - 'click .x-refresh' : '_refreshSeries', - 'click .x-rename' : '_renameSeries', - 'click .x-search' : '_seriesSearch' - }, - - initialize : function() { - this.seriesCollection = ArtistCollection.clone(); - this.seriesCollection.shadowCollection.bindSignalR(); - - - this.listenTo(this.model, 'change:monitored', this._setMonitoredState); - this.listenTo(this.model, 'remove', this._seriesRemoved); - //this.listenTo(vent, vent.Events.CommandComplete, this._commandComplete); - - this.listenTo(this.model, 'change', function(model, options) { - if (options && options.changeSource === 'signalr') { - this._refresh(); - } - }); - - this.listenTo(this.model, 'change:images', this._updateImages); - - }, - - onShow : function() { - this._showBackdrop(); - this._showSeasons(); - this._setMonitoredState(); - this._showInfo(); - }, - - onRender : function() { - CommandController.bindToCommand({ - element : this.ui.refresh, - command : { - name : 'refreshSeries' - } - }); - CommandController.bindToCommand({ - element : this.ui.search, - command : { - name : 'seriesSearch' - } - }); - console.log(this.model); - - /*CommandController.bindToCommand({ - element : this.ui.rename, - command : { - name : 'renameFiles', - seriesId : this.model.spotifyId, - seasonNumber : -1 - } - });*/ - }, - - onClose : function() { - if (this._backstrech) { - this._backstrech.destroy(); - delete this._backstrech; - } - - $('body').removeClass('backdrop'); - reqres.removeHandler(reqres.Requests.GetEpisodeFileById); - }, - - _getImage : function(type) { - var image = _.where(this.model.get('images'), { coverType : type }); - - if (image && image[0]) { - return image[0].url; - } - - return undefined; - }, - - _toggleMonitored : function() { - var savePromise = this.model.save('monitored', !this.model.get('monitored'), { wait : true }); - - this.ui.monitored.spinForPromise(savePromise); - }, - - _setMonitoredState : function() { - var monitored = this.model.get('monitored'); - - this.ui.monitored.removeAttr('data-idle-icon'); - this.ui.monitored.removeClass('fa-spin icon-lidarr-spinner'); - - if (monitored) { - this.ui.monitored.addClass('icon-lidarr-monitored'); - this.ui.monitored.removeClass('icon-lidarr-unmonitored'); - this.$el.removeClass('series-not-monitored'); - } else { - this.ui.monitored.addClass('icon-lidarr-unmonitored'); - this.ui.monitored.removeClass('icon-lidarr-monitored'); - this.$el.addClass('series-not-monitored'); - } - }, - - _editSeries : function() { - vent.trigger(vent.Commands.EditSeriesCommand, { series : this.model }); - }, - - _refreshSeries : function() { - CommandController.Execute('refreshSeries', { - name : 'refreshSeries', - seriesId : this.model.id - }); - }, - - _seriesRemoved : function() { - Backbone.history.navigate('/', { trigger : true }); - }, - - _renameSeries : function() { - vent.trigger(vent.Commands.ShowRenamePreview, { series : this.model }); - }, - - _seriesSearch : function() { - console.log('_seriesSearch:', this.model); - CommandController.Execute('seriesSearch', { - name : 'seriesSearch', - seriesId : this.model.id - }); - }, - - _showSeasons : function() { - var self = this; - return; - - this.seasons.show(new LoadingView()); - - this.seasonCollection = new SeasonCollection(this.model.get('seasons')); - this.episodeCollection = new EpisodeCollection({ seriesId : this.model.id }).bindSignalR(); - this.episodeFileCollection = new EpisodeFileCollection({ seriesId : this.model.id }).bindSignalR(); - - reqres.setHandler(reqres.Requests.GetEpisodeFileById, function(episodeFileId) { - return self.episodeFileCollection.get(episodeFileId); - }); - - reqres.setHandler(reqres.Requests.GetAlternateNameBySeasonNumber, function(seriesId, seasonNumber, sceneSeasonNumber) { - if (self.model.get('id') !== seriesId) { - return []; - } - - if (sceneSeasonNumber === undefined) { - sceneSeasonNumber = seasonNumber; - } - - return _.where(self.model.get('alternateTitles'), - function(alt) { - return alt.sceneSeasonNumber === sceneSeasonNumber || alt.seasonNumber === seasonNumber; - }); - }); - - $.when(this.episodeCollection.fetch(), this.episodeFileCollection.fetch()).done(function() { - var seasonCollectionView = new SeasonCollectionView({ - collection : self.seasonCollection, - episodeCollection : self.episodeCollection, - series : self.model - }); - - if (!self.isClosed) { - self.seasons.show(seasonCollectionView); - } - }); - }, - - _showInfo : function() { - this.info.show(new InfoView({ - model : this.model, - episodeFileCollection : this.episodeFileCollection - })); - }, - - _commandComplete : function(options) { - if (options.command.get('name') === 'renamefiles') { - if (options.command.get('seriesId') === this.model.get('id')) { - this._refresh(); - } - } - }, - - _refresh : function() { - this.seasonCollection.add(this.model.get('seasons'), { merge : true }); - this.episodeCollection.fetch(); - this.episodeFileCollection.fetch(); - - this._setMonitoredState(); - this._showInfo(); - }, - - _openEpisodeFileEditor : function() { - var view = new EpisodeFileEditorLayout({ - series : this.model, - episodeCollection : this.episodeCollection - }); - - vent.trigger(vent.Commands.OpenModalCommand, view); - }, - - _updateImages : function () { - var poster = this._getImage('poster'); - - if (poster) { - this.ui.poster.attr('src', poster); - } - - this._showBackdrop(); - }, - - _showBackdrop : function () { - $('body').addClass('backdrop'); - var fanArt = this._getImage('fanart'); - - if (fanArt) { - this._backstrech = $.backstretch(fanArt); - } else { - $('body').removeClass('backdrop'); - } - } -}); \ No newline at end of file diff --git a/src/UI/Series/Details/SeriesDetailsTemplate.hbs b/src/UI/Series/Details/SeriesDetailsTemplate.hbs deleted file mode 100644 index 605ead424..000000000 --- a/src/UI/Series/Details/SeriesDetailsTemplate.hbs +++ /dev/null @@ -1,35 +0,0 @@ -<div class="row series-page-header"> - <div class="visible-lg col-lg-2 poster"> - {{poster}} - </div> - <div class="col-md-12 col-lg-10"> - <div> - <h1 class="header-text"> - <i class="x-monitored" title="Toggle monitored state for entire series"/> - {{title}} - <div class="series-actions pull-right"> - <div class="x-episode-file-editor"> - <i class="icon-lidarr-episode-file" title="Modify episode files for series"/> - </div> - <div class="x-refresh"> - <i class="icon-lidarr-refresh icon-can-spin" title="Update series info and scan disk"/> - </div> - <div class="x-rename"> - <i class="icon-lidarr-rename" title="Preview rename for all episodes"/> - </div> - <div class="x-search"> - <i class="icon-lidarr-search" title="Search for monitored episodes in this series"/> - </div> - <div class="x-edit"> - <i class="icon-lidarr-edit" title="Edit series"/> - </div> - </div> - </h1> - </div> - <div class="series-detail-overview"> - {{overview}} - </div> - <div id="info" class="series-info"></div> - </div> -</div> -<div id="seasons"></div> diff --git a/src/UI/Series/Edit/EditSeriesView.js b/src/UI/Series/Edit/EditSeriesView.js deleted file mode 100644 index 3f8c789e8..000000000 --- a/src/UI/Series/Edit/EditSeriesView.js +++ /dev/null @@ -1,54 +0,0 @@ -var vent = require('vent'); -var Marionette = require('marionette'); -var Profiles = require('../../Profile/ProfileCollection'); -var AsModelBoundView = require('../../Mixins/AsModelBoundView'); -var AsValidatedView = require('../../Mixins/AsValidatedView'); -var AsEditModalView = require('../../Mixins/AsEditModalView'); -require('../../Mixins/TagInput'); -require('../../Mixins/FileBrowser'); - -var view = Marionette.ItemView.extend({ - template : 'Series/Edit/EditSeriesViewTemplate', - - ui : { - profile : '.x-profile', - path : '.x-path', - tags : '.x-tags' - }, - - events : { - 'click .x-remove' : '_removeSeries' - }, - - initialize : function() { - this.model.set('profiles', Profiles); - }, - - onRender : function() { - this.ui.path.fileBrowser(); - this.ui.tags.tagInput({ - model : this.model, - property : 'tags' - }); - }, - - _onBeforeSave : function() { - var profileId = this.ui.profile.val(); - this.model.set({ profileId : profileId }); - }, - - _onAfterSave : function() { - this.trigger('saved'); - vent.trigger(vent.Commands.CloseModalCommand); - }, - - _removeSeries : function() { - vent.trigger(vent.Commands.DeleteSeriesCommand, { series : this.model }); - } -}); - -AsModelBoundView.call(view); -AsValidatedView.call(view); -AsEditModalView.call(view); - -module.exports = view; \ No newline at end of file diff --git a/src/UI/Series/Edit/EditSeriesViewTemplate.hbs b/src/UI/Series/Edit/EditSeriesViewTemplate.hbs deleted file mode 100644 index a85058ed3..000000000 --- a/src/UI/Series/Edit/EditSeriesViewTemplate.hbs +++ /dev/null @@ -1,104 +0,0 @@ -<div class="modal-content"> - <div class="modal-header"> - <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> - <h3>{{title}}</h3> - </div> - <div class="modal-body edit-series-modal"> - <div class="row"> - <div class="col-sm-3 hidden-xs"> - {{poster}} - </div> - <div class="col-sm-9"> - <div class="form-horizontal"> - - <div class="form-group"> - <label class="col-sm-4 control-label">Monitored</label> - - <div class="col-sm-8"> - <div class="input-group"> - <label class="checkbox toggle well"> - <input type="checkbox" name="monitored"/> - <p> - <span>Yes</span> - <span>No</span> - </p> - - <div class="btn btn-primary slide-button"/> - </label> - - <span class="help-inline-checkbox"> - <i class="icon-lidarr-form-info" title="Should Lidarr download episodes for this series?"/> - </span> - </div> - </div> - </div> - - <div class="form-group"> - <label class="col-sm-4 control-label">Use Season Folder</label> - - <div class="col-sm-8"> - <div class="input-group"> - <label class="checkbox toggle well"> - <input type="checkbox" name="seasonFolder"/> - <p> - <span>Yes</span> - <span>No</span> - </p> - - <div class="btn btn-primary slide-button"/> - </label> - - <span class="help-inline-checkbox"> - <i class="icon-lidarr-form-info" title="Should downloaded episodes be stored in season folders?"/> - </span> - </div> - </div> - </div> - - <div class="form-group"> - <label class="col-sm-4 control-label">Profile</label> - - <div class="col-sm-4"> - <select class="form-control x-profile" id="inputProfile" name="profileId"> - {{#each profiles.models}} - <option value="{{id}}">{{attributes.name}}</option> - {{/each}} - </select> - - </div> - </div> - - <div class="form-group"> - <label class="col-sm-4 control-label">Series Type</label> - <div class="col-sm-4"> - {{> SeriesTypeSelectionPartial}} - </div> - </div> - - <div class="form-group"> - <label class="col-sm-4 control-label">Path</label> - - <div class="col-sm-6"> - <input type="text" class="form-control x-path" placeholder="Path" name="path"> - </div> - </div> - - <div class="form-group"> - <label class="col-sm-4 control-label">Tags</label> - - <div class="col-sm-6"> - <input type="text" class="form-control x-tags"> - </div> - </div> - </div> - </div> - </div> - </div> - <div class="modal-footer"> - <button class="btn btn-danger pull-left x-remove">Delete</button> - - <span class="indicator x-indicator"><i class="icon-lidarr-spinner fa-spin"></i></span> - <button class="btn" data-dismiss="modal">Cancel</button> - <button class="btn btn-primary x-save">Save</button> - </div> -</div> diff --git a/src/UI/Series/Editor/Organize/OrganizeFilesView.js b/src/UI/Series/Editor/Organize/OrganizeFilesView.js deleted file mode 100644 index 25534fb21..000000000 --- a/src/UI/Series/Editor/Organize/OrganizeFilesView.js +++ /dev/null @@ -1,33 +0,0 @@ -var _ = require('underscore'); -var vent = require('vent'); -var Backbone = require('backbone'); -var Marionette = require('marionette'); -var CommandController = require('../../../Commands/CommandController'); - -module.exports = Marionette.ItemView.extend({ - template : 'Series/Editor/Organize/OrganizeFilesViewTemplate', - - events : { - 'click .x-confirm-organize' : '_organize' - }, - - initialize : function(options) { - this.series = options.series; - this.templateHelpers = { - numberOfSeries : this.series.length, - series : new Backbone.Collection(this.series).toJSON() - }; - }, - - _organize : function() { - var seriesIds = _.pluck(this.series, 'id'); - - CommandController.Execute('renameSeries', { - name : 'renameSeries', - seriesIds : seriesIds - }); - - this.trigger('organizingFiles'); - vent.trigger(vent.Commands.CloseModalCommand); - } -}); \ No newline at end of file diff --git a/src/UI/Series/Editor/Organize/OrganizeFilesViewTemplate.hbs b/src/UI/Series/Editor/Organize/OrganizeFilesViewTemplate.hbs deleted file mode 100644 index 356db72db..000000000 --- a/src/UI/Series/Editor/Organize/OrganizeFilesViewTemplate.hbs +++ /dev/null @@ -1,25 +0,0 @@ -<div class="modal-content"> - <div class="modal-header"> - <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> - <h3>Organize of Selected Series</h3> - </div> - <div class="modal-body update-files-series-modal"> - <div class="alert alert-info"> - <button type="button" class="close" data-dismiss="alert">×</button> - Tip: To preview a rename... select "Cancel" then any series title and use the <i data-original-title="" class="icon-lidarr-rename" title=""></i> - </div> - - Are you sure you want to update all files in the {{numberOfSeries}} selected series? - - {{debug}} - <ul class="selected-series"> - {{#each series}} - <li>{{title}}</li> - {{/each}} - </ul> - </div> - <div class="modal-footer"> - <button class="btn" data-dismiss="modal">Cancel</button> - <button class="btn btn-danger x-confirm-organize">Organize</button> - </div> -</div> diff --git a/src/UI/Series/Editor/SeriesEditorFooterView.js b/src/UI/Series/Editor/SeriesEditorFooterView.js deleted file mode 100644 index e2783bbeb..000000000 --- a/src/UI/Series/Editor/SeriesEditorFooterView.js +++ /dev/null @@ -1,126 +0,0 @@ -var _ = require('underscore'); -var Marionette = require('marionette'); -var vent = require('vent'); -var Profiles = require('../../Profile/ProfileCollection'); -var RootFolders = require('../../AddArtist/RootFolders/RootFolderCollection'); -var RootFolderLayout = require('../../AddArtist/RootFolders/RootFolderLayout'); -var UpdateFilesSeriesView = require('./Organize/OrganizeFilesView'); -var Config = require('../../Config'); - -module.exports = Marionette.ItemView.extend({ - template : 'Series/Editor/SeriesEditorFooterViewTemplate', - - ui : { - monitored : '.x-monitored', - profile : '.x-profiles', - seasonFolder : '.x-season-folder', - rootFolder : '.x-root-folder', - selectedCount : '.x-selected-count', - container : '.series-editor-footer', - actions : '.x-action' - }, - - events : { - 'click .x-save' : '_updateAndSave', - 'change .x-root-folder' : '_rootFolderChanged', - 'click .x-organize-files' : '_organizeFiles' - }, - - templateHelpers : function() { - return { - profiles : Profiles, - rootFolders : RootFolders.toJSON() - }; - }, - - initialize : function(options) { - this.seriesCollection = options.collection; - - RootFolders.fetch().done(function() { - RootFolders.synced = true; - }); - - this.editorGrid = options.editorGrid; - this.listenTo(this.seriesCollection, 'backgrid:selected', this._updateInfo); - this.listenTo(RootFolders, 'all', this.render); - }, - - onRender : function() { - this._updateInfo(); - }, - - _updateAndSave : function() { - var selected = this.editorGrid.getSelectedModels(); - - var monitored = this.ui.monitored.val(); - var profile = this.ui.profile.val(); - var seasonFolder = this.ui.seasonFolder.val(); - var rootFolder = this.ui.rootFolder.val(); - - _.each(selected, function(model) { - if (monitored === 'true') { - model.set('monitored', true); - } else if (monitored === 'false') { - model.set('monitored', false); - } - - if (profile !== 'noChange') { - model.set('profileId', parseInt(profile, 10)); - } - - if (seasonFolder === 'true') { - model.set('seasonFolder', true); - } else if (seasonFolder === 'false') { - model.set('seasonFolder', false); - } - - if (rootFolder !== 'noChange') { - var rootFolderPath = RootFolders.get(parseInt(rootFolder, 10)); - - model.set('rootFolderPath', rootFolderPath.get('path')); - } - - model.edited = true; - }); - - this.seriesCollection.save(); - }, - - _updateInfo : function() { - var selected = this.editorGrid.getSelectedModels(); - var selectedCount = selected.length; - - this.ui.selectedCount.html('{0} series selected'.format(selectedCount)); - - if (selectedCount === 0) { - this.ui.actions.attr('disabled', 'disabled'); - } else { - this.ui.actions.removeAttr('disabled'); - } - }, - - _rootFolderChanged : function() { - var rootFolderValue = this.ui.rootFolder.val(); - if (rootFolderValue === 'addNew') { - var rootFolderLayout = new RootFolderLayout(); - this.listenToOnce(rootFolderLayout, 'folderSelected', this._setRootFolder); - vent.trigger(vent.Commands.OpenModalCommand, rootFolderLayout); - } else { - Config.setValue(Config.Keys.DefaultRootFolderId, rootFolderValue); - } - }, - - _setRootFolder : function(options) { - vent.trigger(vent.Commands.CloseModalCommand); - this.ui.rootFolder.val(options.model.id); - this._rootFolderChanged(); - }, - - _organizeFiles : function() { - var selected = this.editorGrid.getSelectedModels(); - var updateFilesSeriesView = new UpdateFilesSeriesView({ series : selected }); - this.listenToOnce(updateFilesSeriesView, 'updatingFiles', this._afterSave); - - vent.trigger(vent.Commands.OpenModalCommand, updateFilesSeriesView); - } -}); \ No newline at end of file diff --git a/src/UI/Series/Editor/SeriesEditorFooterViewTemplate.hbs b/src/UI/Series/Editor/SeriesEditorFooterViewTemplate.hbs deleted file mode 100644 index c47b3c50a..000000000 --- a/src/UI/Series/Editor/SeriesEditorFooterViewTemplate.hbs +++ /dev/null @@ -1,54 +0,0 @@ -<div class="series-editor-footer"> - <div class="row"> - <div class="form-group col-md-2"> - <label>Monitored</label> - - <select class="form-control x-action x-monitored"> - <option value="noChange">No change</option> - <option value="true">Monitored</option> - <option value="false">Unmonitored</option> - </select> - </div> - - <div class="form-group col-md-2"> - <label>Profile</label> - - <select class="form-control x-action x-profiles"> - <option value="noChange">No change</option> - {{#each profiles.models}} - <option value="{{id}}">{{attributes.name}}</option> - {{/each}} - </select> - </div> - - <div class="form-group col-md-2"> - <label>Season Folder</label> - - <select class="form-control x-action x-season-folder"> - <option value="noChange">No change</option> - <option value="true">Yes</option> - <option value="false">No</option> - </select> - </div> - - <div class="form-group col-md-3"> - <label>Root Folder</label> - - <select class="form-control x-action x-root-folder" validation-name="RootFolderPath"> - <option value="noChange">No change</option> - {{#each rootFolders}} - <option value="{{id}}">{{path}}</option> - {{/each}} - <option value="addNew">Add a different path</option> - </select> - </div> - - <div class="form-group col-md-3 actions"> - <label class="x-selected-count">0 series selected</label> - <div> - <button class="btn btn-primary x-action x-save">Save</button> - <button class="btn btn-danger x-action x-organize-files" title="Organize and rename episode files">Organize</button> - </div> - </div> - </div> -</div> diff --git a/src/UI/Series/Editor/SeriesEditorLayout.js b/src/UI/Series/Editor/SeriesEditorLayout.js deleted file mode 100644 index 6927699d5..000000000 --- a/src/UI/Series/Editor/SeriesEditorLayout.js +++ /dev/null @@ -1,184 +0,0 @@ -var vent = require('vent'); -var Marionette = require('marionette'); -var Backgrid = require('backgrid'); -var EmptyView = require('../Index/EmptyView'); -var SeriesCollection = require('../SeriesCollection'); -var SeriesTitleCell = require('../../Cells/SeriesTitleCell'); -var ProfileCell = require('../../Cells/ProfileCell'); -var SeriesStatusCell = require('../../Cells/SeriesStatusCell'); -var SeasonFolderCell = require('../../Cells/SeasonFolderCell'); -var SelectAllCell = require('../../Cells/SelectAllCell'); -var ToolbarLayout = require('../../Shared/Toolbar/ToolbarLayout'); -var FooterView = require('./SeriesEditorFooterView'); -require('../../Mixins/backbone.signalr.mixin'); - -module.exports = Marionette.Layout.extend({ - template : 'Series/Editor/SeriesEditorLayoutTemplate', - - regions : { - seriesRegion : '#x-series-editor', - toolbar : '#x-toolbar' - }, - - ui : { - monitored : '.x-monitored', - profiles : '.x-profiles', - rootFolder : '.x-root-folder', - selectedCount : '.x-selected-count' - }, - - events : { - 'click .x-save' : '_updateAndSave', - 'change .x-root-folder' : '_rootFolderChanged' - }, - - columns : [ - { - name : '', - cell : SelectAllCell, - headerCell : 'select-all', - sortable : false - }, - { - name : 'statusWeight', - label : '', - cell : SeriesStatusCell - }, - { - name : 'title', - label : 'Title', - cell : SeriesTitleCell, - cellValue : 'this' - }, - { - name : 'profileId', - label : 'Profile', - cell : ProfileCell - }, - { - name : 'seasonFolder', - label : 'Season Folder', - cell : SeasonFolderCell - }, - { - name : 'path', - label : 'Path', - cell : 'string' - } - ], - - leftSideButtons : { - type : 'default', - storeState : false, - items : [ - { - title : 'Season Pass', - icon : 'icon-lidarr-monitored', - route : 'seasonpass' - }, - { - title : 'Update Library', - icon : 'icon-lidarr-refresh', - command : 'refreshseries', - successMessage : 'Library was updated!', - errorMessage : 'Library update failed!' - } - ] - }, - - initialize : function() { - this.seriesCollection = SeriesCollection.clone(); - this.seriesCollection.shadowCollection.bindSignalR(); - this.listenTo(this.seriesCollection, 'save', this.render); - - this.filteringOptions = { - type : 'radio', - storeState : true, - menuKey : 'serieseditor.filterMode', - defaultAction : 'all', - items : [ - { - key : 'all', - title : '', - tooltip : 'All', - icon : 'icon-lidarr-all', - callback : this._setFilter - }, - { - key : 'monitored', - title : '', - tooltip : 'Monitored Only', - icon : 'icon-lidarr-monitored', - callback : this._setFilter - }, - { - key : 'continuing', - title : '', - tooltip : 'Continuing Only', - icon : 'icon-lidarr-series-continuing', - callback : this._setFilter - }, - { - key : 'ended', - title : '', - tooltip : 'Ended Only', - icon : 'icon-lidarr-series-ended', - callback : this._setFilter - } - ] - }; - }, - - onRender : function() { - this._showToolbar(); - this._showTable(); - }, - - onClose : function() { - vent.trigger(vent.Commands.CloseControlPanelCommand); - }, - - _showTable : function() { - if (this.seriesCollection.shadowCollection.length === 0) { - this.seriesRegion.show(new EmptyView()); - this.toolbar.close(); - return; - } - - this.columns[0].sortedCollection = this.seriesCollection; - - this.editorGrid = new Backgrid.Grid({ - collection : this.seriesCollection, - columns : this.columns, - className : 'table table-hover' - }); - - this.seriesRegion.show(this.editorGrid); - this._showFooter(); - }, - - _showToolbar : function() { - this.toolbar.show(new ToolbarLayout({ - left : [ - this.leftSideButtons - ], - right : [ - this.filteringOptions - ], - context : this - })); - }, - - _showFooter : function() { - vent.trigger(vent.Commands.OpenControlPanelCommand, new FooterView({ - editorGrid : this.editorGrid, - collection : this.seriesCollection - })); - }, - - _setFilter : function(buttonContext) { - var mode = buttonContext.model.get('key'); - - this.seriesCollection.setFilterMode(mode); - } -}); \ No newline at end of file diff --git a/src/UI/Series/Editor/SeriesEditorLayoutTemplate.hbs b/src/UI/Series/Editor/SeriesEditorLayoutTemplate.hbs deleted file mode 100644 index 1d0519894..000000000 --- a/src/UI/Series/Editor/SeriesEditorLayoutTemplate.hbs +++ /dev/null @@ -1,7 +0,0 @@ -<div id="x-toolbar"></div> - -<div class="row"> - <div class="col-md-12"> - <div id="x-series-editor" class="table-responsive"></div> - </div> -</div> \ No newline at end of file diff --git a/src/UI/Series/EpisodeCollection.js b/src/UI/Series/EpisodeCollection.js deleted file mode 100644 index a6794394b..000000000 --- a/src/UI/Series/EpisodeCollection.js +++ /dev/null @@ -1,62 +0,0 @@ -var Backbone = require('backbone'); -var PageableCollection = require('backbone.pageable'); -var EpisodeModel = require('./EpisodeModel'); -require('./EpisodeCollection'); - -module.exports = PageableCollection.extend({ - url : window.NzbDrone.ApiRoot + '/episode', - model : EpisodeModel, - - state : { - sortKey : 'episodeNumber', - order : 1, - pageSize : 100000 - }, - - mode : 'client', - - originalFetch : Backbone.Collection.prototype.fetch, - - initialize : function(options) { - this.seriesId = options.seriesId; - }, - - bySeason : function(season) { - var filtered = this.filter(function(episode) { - return episode.get('seasonNumber') === season; - }); - - var EpisodeCollection = require('./EpisodeCollection'); - - return new EpisodeCollection(filtered); - }, - - comparator : function(model1, model2) { - var episode1 = model1.get('episodeNumber'); - var episode2 = model2.get('episodeNumber'); - - if (episode1 < episode2) { - return 1; - } - - if (episode1 > episode2) { - return -1; - } - - return 0; - }, - - fetch : function(options) { - if (!this.seriesId) { - throw 'seriesId is required'; - } - - if (!options) { - options = {}; - } - - options.data = { seriesId : this.seriesId }; - - return this.originalFetch.call(this, options); - } -}); \ No newline at end of file diff --git a/src/UI/Series/EpisodeFileCollection.js b/src/UI/Series/EpisodeFileCollection.js deleted file mode 100644 index dff988512..000000000 --- a/src/UI/Series/EpisodeFileCollection.js +++ /dev/null @@ -1,28 +0,0 @@ -var Backbone = require('backbone'); -var EpisodeFileModel = require('./EpisodeFileModel'); - -module.exports = Backbone.Collection.extend({ - url : window.NzbDrone.ApiRoot + '/episodefile', - model : EpisodeFileModel, - - originalFetch : Backbone.Collection.prototype.fetch, - - initialize : function(options) { - this.seriesId = options.seriesId; - this.models = []; - }, - - fetch : function(options) { - if (!this.seriesId) { - throw 'seriesId is required'; - } - - if (!options) { - options = {}; - } - - options.data = { seriesId : this.seriesId }; - - return this.originalFetch.call(this, options); - } -}); \ No newline at end of file diff --git a/src/UI/Series/EpisodeFileModel.js b/src/UI/Series/EpisodeFileModel.js deleted file mode 100644 index 3986a5948..000000000 --- a/src/UI/Series/EpisodeFileModel.js +++ /dev/null @@ -1,3 +0,0 @@ -var Backbone = require('backbone'); - -module.exports = Backbone.Model.extend({}); \ No newline at end of file diff --git a/src/UI/Series/EpisodeModel.js b/src/UI/Series/EpisodeModel.js deleted file mode 100644 index ebb72cf29..000000000 --- a/src/UI/Series/EpisodeModel.js +++ /dev/null @@ -1,20 +0,0 @@ -var Backbone = require('backbone'); - -module.exports = Backbone.Model.extend({ - defaults : { - seasonNumber : 0, - status : 0 - }, - - methodUrls : { - 'update' : window.NzbDrone.ApiRoot + '/episode' - }, - - sync : function(method, model, options) { - if (model.methodUrls && model.methodUrls[method.toLowerCase()]) { - options = options || {}; - options.url = model.methodUrls[method.toLowerCase()]; - } - return Backbone.sync(method, model, options); - } -}); \ No newline at end of file diff --git a/src/UI/Series/Index/EmptyTemplate.hbs b/src/UI/Series/Index/EmptyTemplate.hbs deleted file mode 100644 index 23b8b513c..000000000 --- a/src/UI/Series/Index/EmptyTemplate.hbs +++ /dev/null @@ -1,16 +0,0 @@ -<div class="no-series"> - <div class="row"> - <div class="well col-md-12"> - <i class="icon-lidarr-comment"/> - You must be new around here. You should add some music. - </div> - </div> - <div class="row"> - <div class="col-md-4 col-md-offset-4"> - <a href="/addartist" class='btn btn-lg btn-block btn-success x-add-artist'> - <i class='icon-lidarr-add'></i> - Add Music - </a> - </div> - </div> -</div> diff --git a/src/UI/Series/Index/EmptyView.js b/src/UI/Series/Index/EmptyView.js deleted file mode 100644 index 01dcc07a4..000000000 --- a/src/UI/Series/Index/EmptyView.js +++ /dev/null @@ -1,5 +0,0 @@ -var Marionette = require('marionette'); - -module.exports = Marionette.CompositeView.extend({ - template : 'Series/Index/EmptyTemplate' -}); \ No newline at end of file diff --git a/src/UI/Series/Index/EpisodeProgressPartial.hbs b/src/UI/Series/Index/EpisodeProgressPartial.hbs deleted file mode 100644 index db5c49a2b..000000000 --- a/src/UI/Series/Index/EpisodeProgressPartial.hbs +++ /dev/null @@ -1,4 +0,0 @@ -<div class="progress episode-progress"> - <span class="progressbar-back-text">{{episodeFileCount}} / {{episodeCount}}</span> - <div class="progress-bar {{EpisodeProgressClass}} episode-progress" style="width:{{percentOfEpisodes}}%"><span class="progressbar-front-text">{{episodeFileCount}} / {{episodeCount}}</span></div> -</div> \ No newline at end of file diff --git a/src/UI/Series/Index/FooterModel.js b/src/UI/Series/Index/FooterModel.js deleted file mode 100644 index 235552061..000000000 --- a/src/UI/Series/Index/FooterModel.js +++ /dev/null @@ -1,4 +0,0 @@ -var Backbone = require('backbone'); -var _ = require('underscore'); - -module.exports = Backbone.Model.extend({}); \ No newline at end of file diff --git a/src/UI/Series/Index/FooterView.js b/src/UI/Series/Index/FooterView.js deleted file mode 100644 index 1d31cc404..000000000 --- a/src/UI/Series/Index/FooterView.js +++ /dev/null @@ -1,5 +0,0 @@ -var Marionette = require('marionette'); - -module.exports = Marionette.CompositeView.extend({ - template : 'Series/Index/FooterViewTemplate' -}); \ No newline at end of file diff --git a/src/UI/Series/Index/FooterViewTemplate.hbs b/src/UI/Series/Index/FooterViewTemplate.hbs deleted file mode 100644 index 1b45fa747..000000000 --- a/src/UI/Series/Index/FooterViewTemplate.hbs +++ /dev/null @@ -1,46 +0,0 @@ -<div class="row"> - <div class="series-legend legend col-xs-6 col-sm-4"> - <ul class='legend-labels'> - <li><span class="progress-bar"></span>Continuing (All episodes downloaded)</li> - <li><span class="progress-bar-success"></span>Ended (All episodes downloaded)</li> - <li><span class="progress-bar-danger"></span>Missing Episodes (Series monitored)</li> - <li><span class="progress-bar-warning"></span>Missing Episodes (Series not monitored)</li> - </ul> - </div> - <div class="col-xs-5 col-sm-7"> - <div class="row"> - <div class="series-stats col-sm-4"> - <dl class="dl-horizontal"> - <dt>Series</dt> - <dd>{{series}}</dd> - - <dt>Ended</dt> - <dd>{{ended}}</dd> - - <dt>Continuing</dt> - <dd>{{continuing}}</dd> - </dl> - </div> - - <div class="series-stats col-sm-4"> - <dl class="dl-horizontal"> - <dt>Monitored</dt> - <dd>{{monitored}}</dd> - - <dt>Unmonitored</dt> - <dd>{{unmonitored}}</dd> - </dl> - </div> - - <div class="series-stats col-sm-4"> - <dl class="dl-horizontal"> - <dt>Episodes</dt> - <dd>{{episodes}}</dd> - - <dt>Files</dt> - <dd>{{episodeFiles}}</dd> - </dl> - </div> - </div> - </div> -</div> diff --git a/src/UI/Series/Index/Overview/SeriesOverviewCollectionView.js b/src/UI/Series/Index/Overview/SeriesOverviewCollectionView.js deleted file mode 100644 index 7db4b76f0..000000000 --- a/src/UI/Series/Index/Overview/SeriesOverviewCollectionView.js +++ /dev/null @@ -1,8 +0,0 @@ -var Marionette = require('marionette'); -var ListItemView = require('./SeriesOverviewItemView'); - -module.exports = Marionette.CompositeView.extend({ - itemView : ListItemView, - itemViewContainer : '#x-series-list', - template : 'Series/Index/Overview/SeriesOverviewCollectionViewTemplate' -}); \ No newline at end of file diff --git a/src/UI/Series/Index/Overview/SeriesOverviewCollectionViewTemplate.hbs b/src/UI/Series/Index/Overview/SeriesOverviewCollectionViewTemplate.hbs deleted file mode 100644 index 046bb3348..000000000 --- a/src/UI/Series/Index/Overview/SeriesOverviewCollectionViewTemplate.hbs +++ /dev/null @@ -1 +0,0 @@ -<div id="x-series-list"/> diff --git a/src/UI/Series/Index/Overview/SeriesOverviewItemView.js b/src/UI/Series/Index/Overview/SeriesOverviewItemView.js deleted file mode 100644 index bb780480b..000000000 --- a/src/UI/Series/Index/Overview/SeriesOverviewItemView.js +++ /dev/null @@ -1,7 +0,0 @@ -var vent = require('vent'); -var Marionette = require('marionette'); -var SeriesIndexItemView = require('../SeriesIndexItemView'); - -module.exports = SeriesIndexItemView.extend({ - template : 'Series/Index/Overview/SeriesOverviewItemViewTemplate' -}); \ No newline at end of file diff --git a/src/UI/Series/Index/Overview/SeriesOverviewItemViewTemplate.hbs b/src/UI/Series/Index/Overview/SeriesOverviewItemViewTemplate.hbs deleted file mode 100644 index 6fb80ec50..000000000 --- a/src/UI/Series/Index/Overview/SeriesOverviewItemViewTemplate.hbs +++ /dev/null @@ -1,59 +0,0 @@ -<div class="series-item"> - <div class="row"> - <div class="col-md-2 col-xs-3"> - <a href="{{route}}"> - {{poster}} - </a> - </div> - <div class="col-md-10 col-xs-9"> - <div class="row"> - <div class="col-md-10 col-xs-10"> - <a href="artist/{{artistSlug}}" target="_blank"> - <h2>{{artistName}}</h2> - </a> - </div> - <div class="col-md-2 col-xs-2"> - <div class="pull-right series-overview-list-actions"> - <i class="icon-lidarr-refresh x-refresh" title="Update artist info and scan disk"/> - <i class="icon-lidarr-edit x-edit" title="Edit Artist"/> - </div> - </div> - </div> - <div class="row"> - <div class="col-md-10 col-xs-12"> - <div> - {{truncate overview 600}} - </div> - </div> - </div> - <div class="row"> - <div class="col-md-12"> -   - </div> - </div> - <div class="row"> - <div class="col-md-10 col-xs-8"> - <!--{{#if_eq status compare="ended"}} - <span class="label label-danger">Ended</span> - {{/if_eq}}--> - - <!-- - NOTE: We can show next drop date of album in future - {{#if nextAiring}} - <span class="label label-default">{{RelativeDate nextAiring}}</span> - {{/if}}--> - - {{albumCountHelper}} - - {{profile profileId}} - </div> - <div class="col-md-2 col-xs-4"> - {{> EpisodeProgressPartial }} - </div> - <div class="col-md-8 col-xs-10"> - Path {{path}} - </div> - </div> - </div> - </div> -</div> diff --git a/src/UI/Series/Index/Posters/SeriesPostersCollectionView.js b/src/UI/Series/Index/Posters/SeriesPostersCollectionView.js deleted file mode 100644 index 0d6094f1c..000000000 --- a/src/UI/Series/Index/Posters/SeriesPostersCollectionView.js +++ /dev/null @@ -1,8 +0,0 @@ -var Marionette = require('marionette'); -var PosterItemView = require('./SeriesPostersItemView'); - -module.exports = Marionette.CompositeView.extend({ - itemView : PosterItemView, - itemViewContainer : '#x-series-posters', - template : 'Series/Index/Posters/SeriesPostersCollectionViewTemplate' -}); \ No newline at end of file diff --git a/src/UI/Series/Index/Posters/SeriesPostersCollectionViewTemplate.hbs b/src/UI/Series/Index/Posters/SeriesPostersCollectionViewTemplate.hbs deleted file mode 100644 index 11b8e8ac7..000000000 --- a/src/UI/Series/Index/Posters/SeriesPostersCollectionViewTemplate.hbs +++ /dev/null @@ -1 +0,0 @@ -<ul id="x-series-posters" class="series-posters"></ul> \ No newline at end of file diff --git a/src/UI/Series/Index/Posters/SeriesPostersItemView.js b/src/UI/Series/Index/Posters/SeriesPostersItemView.js deleted file mode 100644 index 9a42b4655..000000000 --- a/src/UI/Series/Index/Posters/SeriesPostersItemView.js +++ /dev/null @@ -1,19 +0,0 @@ -var SeriesIndexItemView = require('../SeriesIndexItemView'); - -module.exports = SeriesIndexItemView.extend({ - tagName : 'li', - template : 'Series/Index/Posters/SeriesPostersItemViewTemplate', - - initialize : function() { - this.events['mouseenter .x-series-poster-container'] = 'posterHoverAction'; - this.events['mouseleave .x-series-poster-container'] = 'posterHoverAction'; - - this.ui.controls = '.x-series-controls'; - this.ui.title = '.x-title'; - }, - - posterHoverAction : function() { - this.ui.controls.slideToggle(); - this.ui.title.slideToggle(); - } -}); \ No newline at end of file diff --git a/src/UI/Series/Index/Posters/SeriesPostersItemViewTemplate.hbs b/src/UI/Series/Index/Posters/SeriesPostersItemViewTemplate.hbs deleted file mode 100644 index 7b217b779..000000000 --- a/src/UI/Series/Index/Posters/SeriesPostersItemViewTemplate.hbs +++ /dev/null @@ -1,30 +0,0 @@ -<div class="series-posters-item"> - <div class="center"> - <div class="series-poster-container x-series-poster-container"> - <div class="series-controls x-series-controls"> - <i class="icon-lidarr-refresh x-refresh" title="Refresh Series"/> - <i class="icon-lidarr-edit x-edit" title="Edit Series"/> - </div> - {{#unless_eq status compare="continuing"}} - <div class="ended-banner">Ended</div> - {{/unless_eq}} - <a href="{{route}}"> - {{poster}} - <div class="center title">{{title}}</div> - </a> - <div class="hidden-title x-title"> - {{title}} - </div> - </div> - </div> - - <div class="center"> - <div class="labels"> - {{> EpisodeProgressPartial }} - - {{#if nextAiring}} - <span class="label label-default">{{RelativeDate nextAiring}}</span> - {{/if}} - </div> - </div> -</div> diff --git a/src/UI/Series/Index/SeriesIndexItemView.js b/src/UI/Series/Index/SeriesIndexItemView.js deleted file mode 100644 index 91d3b329b..000000000 --- a/src/UI/Series/Index/SeriesIndexItemView.js +++ /dev/null @@ -1,35 +0,0 @@ -var vent = require('vent'); -var Marionette = require('marionette'); -var CommandController = require('../../Commands/CommandController'); - -module.exports = Marionette.ItemView.extend({ - ui : { - refresh : '.x-refresh' - }, - - events : { - 'click .x-edit' : '_editSeries', - 'click .x-refresh' : '_refreshArtist' - }, - - onRender : function() { - CommandController.bindToCommand({ - element : this.ui.refresh, - command : { - name : 'refreshArtist', - seriesId : this.model.get('id') - } - }); - }, - - _editSeries : function() { - vent.trigger(vent.Commands.EditSeriesCommand, { series : this.model }); - }, - - _refreshArtist : function() { - CommandController.Execute('refreshArtist', { - name : 'refreshArtist', - seriesId : this.model.id - }); - } -}); \ No newline at end of file diff --git a/src/UI/Series/Index/SeriesIndexLayout.js b/src/UI/Series/Index/SeriesIndexLayout.js deleted file mode 100644 index 09be2418c..000000000 --- a/src/UI/Series/Index/SeriesIndexLayout.js +++ /dev/null @@ -1,357 +0,0 @@ -var _ = require('underscore'); -var Marionette = require('marionette'); -var Backgrid = require('backgrid'); -var PosterCollectionView = require('./Posters/SeriesPostersCollectionView'); -var ListCollectionView = require('./Overview/SeriesOverviewCollectionView'); -var EmptyView = require('./EmptyView'); -var ArtistCollection = require('../../Artist/ArtistCollection'); -var RelativeDateCell = require('../../Cells/RelativeDateCell'); -var SeriesTitleCell = require('../../Cells/SeriesTitleCell'); -var TemplatedCell = require('../../Cells/TemplatedCell'); -var ProfileCell = require('../../Cells/ProfileCell'); -var EpisodeProgressCell = require('../../Cells/EpisodeProgressCell'); -var SeriesActionsCell = require('../../Cells/SeriesActionsCell'); -var SeriesStatusCell = require('../../Cells/SeriesStatusCell'); -var FooterView = require('./FooterView'); -var FooterModel = require('./FooterModel'); -var ToolbarLayout = require('../../Shared/Toolbar/ToolbarLayout'); -require('../../Mixins/backbone.signalr.mixin'); - -module.exports = Marionette.Layout.extend({ - template : 'Series/Index/SeriesIndexLayoutTemplate', - - - regions : { - seriesRegion : '#x-series', - toolbar : '#x-toolbar', - toolbar2 : '#x-toolbar2', - footer : '#x-series-footer' - }, - - columns : [ - { - name : 'statusWeight', - label : '', - cell : SeriesStatusCell - }, - { - name : 'title', - label : 'Title', - cell : SeriesTitleCell, - cellValue : 'this', - sortValue : 'sortTitle' - }, - { - name : 'seasonCount', - label : 'Seasons', - cell : 'integer' - }, - { - name : 'profileId', - label : 'Profile', - cell : ProfileCell - }, - { - name : 'network', - label : 'Network', - cell : 'string' - }, - { - name : 'nextAiring', - label : 'Next Airing', - cell : RelativeDateCell - }, - { - name : 'percentOfEpisodes', - label : 'Episodes', - cell : EpisodeProgressCell, - className : 'episode-progress-cell' - }, - { - name : 'this', - label : '', - sortable : false, - cell : SeriesActionsCell - } - ], - - leftSideButtons : { - type : 'default', - storeState : false, - collapse : true, - items : [ - { - title : 'Add Artist', - icon : 'icon-lidarr-add', - route : 'addartist' - }, - { - title : 'Season Pass', - icon : 'icon-lidarr-monitored', - route : 'seasonpass' - }, - { - title : 'Series Editor', - icon : 'icon-lidarr-edit', - route : 'serieseditor' - }, - { - title : 'RSS Sync', - icon : 'icon-lidarr-rss', - command : 'rsssync', - errorMessage : 'RSS Sync Failed!' - }, - { - title : 'Update Library', - icon : 'icon-lidarr-refresh', - command : 'refreshseries', - successMessage : 'Library was updated!', - errorMessage : 'Library update failed!' - } - ] - }, - - initialize : function() { - this.artistCollection = ArtistCollection.clone(); - this.artistCollection.shadowCollection.bindSignalR(); - - this.listenTo(this.artistCollection, 'sync', function(model, collection, options) { - this.artistCollection.fullCollection.resetFiltered(); - this._renderView(); - }); - - this.listenTo(this.artistCollection, 'add', function(model, collection, options) { - this.artistCollection.fullCollection.resetFiltered(); - this._renderView(); - }); - - this.listenTo(this.artistCollection, 'remove', function(model, collection, options) { - this.artistCollection.fullCollection.resetFiltered(); - this._renderView(); - }); - - this.sortingOptions = { - type : 'sorting', - storeState : false, - viewCollection : this.artistCollection, - items : [ - { - title : 'Title', - name : 'title' - }, - { - title : 'Seasons', - name : 'seasonCount' - }, - { - title : 'Quality', - name : 'profileId' - }, - { - title : 'Network', - name : 'network' - }, - { - title : 'Next Airing', - name : 'nextAiring' - }, - { - title : 'Episodes', - name : 'percentOfEpisodes' - } - ] - }; - - this.filteringOptions = { - type : 'radio', - storeState : true, - menuKey : 'series.filterMode', - defaultAction : 'all', - items : [ - { - key : 'all', - title : '', - tooltip : 'All', - icon : 'icon-lidarr-all', - callback : this._setFilter - }, - { - key : 'monitored', - title : '', - tooltip : 'Monitored Only', - icon : 'icon-lidarr-monitored', - callback : this._setFilter - }, - { - key : 'continuing', - title : '', - tooltip : 'Continuing Only', - icon : 'icon-lidarr-series-continuing', - callback : this._setFilter - }, - { - key : 'ended', - title : '', - tooltip : 'Ended Only', - icon : 'icon-lidarr-series-ended', - callback : this._setFilter - }, - { - key : 'missing', - title : '', - tooltip : 'Missing', - icon : 'icon-lidarr-missing', - callback : this._setFilter - } - ] - }; - - this.viewButtons = { - type : 'radio', - storeState : true, - menuKey : 'seriesViewMode', - defaultAction : 'listView', - items : [ - { - key : 'posterView', - title : '', - tooltip : 'Posters', - icon : 'icon-lidarr-view-poster', - callback : this._showPosters - }, - { - key : 'listView', - title : '', - tooltip : 'Overview List', - icon : 'icon-lidarr-view-list', - callback : this._showList - }, - { - key : 'tableView', - title : '', - tooltip : 'Table', - icon : 'icon-lidarr-view-table', - callback : this._showTable - } - ] - }; - }, - - onShow : function() { - this._showToolbar(); - this._fetchCollection(); - }, - - _showTable : function() { - this.currentView = new Backgrid.Grid({ - collection : this.artistCollection, - columns : this.columns, - className : 'table table-hover' - }); - - this._renderView(); - }, - - _showList : function() { - this.currentView = new ListCollectionView({ - collection : this.artistCollection - }); - - this._renderView(); - }, - - _showPosters : function() { - this.currentView = new PosterCollectionView({ - collection : this.artistCollection - }); - - this._renderView(); - }, - - _renderView : function() { - // Problem is this is calling before artistCollection has updated. Where are the promises with backbone? - if (this.artistCollection.length === 0) { - this.seriesRegion.show(new EmptyView()); - - this.toolbar.close(); - this.toolbar2.close(); - } else { - this.seriesRegion.show(this.currentView); - - this._showToolbar(); - this._showFooter(); - } - }, - - _fetchCollection : function() { - this.artistCollection.fetch(); - console.log('index page, collection: ', this.artistCollection); - }, - - _setFilter : function(buttonContext) { - var mode = buttonContext.model.get('key'); - - this.artistCollection.setFilterMode(mode); - }, - - _showToolbar : function() { - if (this.toolbar.currentView) { - return; - } - - this.toolbar2.show(new ToolbarLayout({ - right : [ - this.filteringOptions - ], - context : this - })); - - this.toolbar.show(new ToolbarLayout({ - right : [ - this.sortingOptions, - this.viewButtons - ], - left : [ - this.leftSideButtons - ], - context : this - })); - }, - - _showFooter : function() { - var footerModel = new FooterModel(); - var series = this.artistCollection.models.length; - var episodes = 0; - var episodeFiles = 0; - var ended = 0; - var continuing = 0; - var monitored = 0; - - _.each(this.artistCollection.models, function(model) { - episodes += model.get('episodeCount'); // TODO: Refactor to Seasons and Tracks - episodeFiles += model.get('episodeFileCount'); - - /*if (model.get('status').toLowerCase() === 'ended') { - ended++; - } else { - continuing++; - }*/ - - if (model.get('monitored')) { - monitored++; - } - }); - - footerModel.set({ - series : series, - ended : ended, - continuing : continuing, - monitored : monitored, - unmonitored : series - monitored, - episodes : episodes, - episodeFiles : episodeFiles - }); - - this.footer.show(new FooterView({ model : footerModel })); - } -}); diff --git a/src/UI/Series/Index/SeriesIndexLayoutTemplate.hbs b/src/UI/Series/Index/SeriesIndexLayoutTemplate.hbs deleted file mode 100644 index d9e6b3263..000000000 --- a/src/UI/Series/Index/SeriesIndexLayoutTemplate.hbs +++ /dev/null @@ -1,12 +0,0 @@ -<div class="toolbars"> - <div id="x-toolbar"></div> - <div id="x-toolbar2"></div> -</div> - -<div class="row"> - <div class="col-md-12"> - <div id="x-series" class="table-responsive"></div> - </div> -</div> - -<div id="x-series-footer"></div> \ No newline at end of file diff --git a/src/UI/Series/SeasonCollection.js b/src/UI/Series/SeasonCollection.js deleted file mode 100644 index ed661af2b..000000000 --- a/src/UI/Series/SeasonCollection.js +++ /dev/null @@ -1,10 +0,0 @@ -var Backbone = require('backbone'); -var SeasonModel = require('./SeasonModel'); - -module.exports = Backbone.Collection.extend({ - model : SeasonModel, - - comparator : function(season) { - return -season.get('seasonNumber'); - } -}); \ No newline at end of file diff --git a/src/UI/Series/SeasonModel.js b/src/UI/Series/SeasonModel.js deleted file mode 100644 index 1ba049eb6..000000000 --- a/src/UI/Series/SeasonModel.js +++ /dev/null @@ -1,11 +0,0 @@ -var Backbone = require('backbone'); - -module.exports = Backbone.Model.extend({ - defaults : { - seasonNumber : 0 - }, - - initialize : function() { - this.set('id', this.get('seasonNumber')); - } -}); \ No newline at end of file diff --git a/src/UI/Series/SeriesCollection.js b/src/UI/Series/SeriesCollection.js deleted file mode 100644 index bef8fe338..000000000 --- a/src/UI/Series/SeriesCollection.js +++ /dev/null @@ -1,120 +0,0 @@ -var _ = require('underscore'); -var Backbone = require('backbone'); -var PageableCollection = require('backbone.pageable'); -var SeriesModel = require('./SeriesModel'); -var ApiData = require('../Shared/ApiData'); -var AsFilteredCollection = require('../Mixins/AsFilteredCollection'); -var AsSortedCollection = require('../Mixins/AsSortedCollection'); -var AsPersistedStateCollection = require('../Mixins/AsPersistedStateCollection'); -var moment = require('moment'); -require('../Mixins/backbone.signalr.mixin'); - -var Collection = PageableCollection.extend({ - url : window.NzbDrone.ApiRoot + '/series', - model : SeriesModel, - tableName : 'series', - - state : { - sortKey : 'sortTitle', - order : -1, - pageSize : 100000, - secondarySortKey : 'sortTitle', - secondarySortOrder : -1 - }, - - mode : 'client', - - save : function() { - var self = this; - - var proxy = _.extend(new Backbone.Model(), { - id : '', - - url : self.url + '/editor', - - toJSON : function() { - return self.filter(function(model) { - return model.edited; - }); - } - }); - - this.listenTo(proxy, 'sync', function(proxyModel, models) { - this.add(models, { merge : true }); - this.trigger('save', this); - }); - - return proxy.save(); - }, - - filterModes : { - 'all' : [ - null, - null - ], - 'continuing' : [ - 'status', - 'continuing' - ], - 'ended' : [ - 'status', - 'ended' - ], - 'monitored' : [ - 'monitored', - true - ], - 'missing' : [ - null, - null, - function(model) { return model.get('episodeCount') !== model.get('episodeFileCount'); } - ] - }, - - sortMappings : { - title : { - sortKey : 'sortTitle' - }, - - nextAiring : { - sortValue : function(model, attr, order) { - var nextAiring = model.get(attr); - - if (nextAiring) { - return moment(nextAiring).unix(); - } - - if (order === 1) { - return 0; - } - - return Number.MAX_VALUE; - } - }, - - percentOfEpisodes : { - sortValue : function(model, attr) { - var percentOfEpisodes = model.get(attr); - var episodeCount = model.get('episodeCount'); - - return percentOfEpisodes + episodeCount / 1000000; - } - }, - - path : { - sortValue : function(model) { - var path = model.get('path'); - - return path.toLowerCase(); - } - } - } -}); - -Collection = AsFilteredCollection.call(Collection); -Collection = AsSortedCollection.call(Collection); -Collection = AsPersistedStateCollection.call(Collection); - -var data = ApiData.get('series'); - -module.exports = new Collection(data, { full : true }).bindSignalR(); diff --git a/src/UI/Series/SeriesController.js b/src/UI/Series/SeriesController.js deleted file mode 100644 index 3216e64c3..000000000 --- a/src/UI/Series/SeriesController.js +++ /dev/null @@ -1,34 +0,0 @@ -var NzbDroneController = require('../Shared/NzbDroneController'); -var AppLayout = require('../AppLayout'); -var SeriesCollection = require('./SeriesCollection'); -var SeriesIndexLayout = require('./Index/SeriesIndexLayout'); -var SeriesDetailsLayout = require('./Details/SeriesDetailsLayout'); - -module.exports = NzbDroneController.extend({ - _originalInit : NzbDroneController.prototype.initialize, - - initialize : function() { - this.route('', this.series); - this.route('series', this.series); - this.route('series/:query', this.seriesDetails); - - this._originalInit.apply(this, arguments); - }, - - series : function() { - this.setTitle('Lidarr'); - this.showMainRegion(new SeriesIndexLayout()); - }, - - seriesDetails : function(query) { - var series = SeriesCollection.where({ titleSlug : query }); - - if (series.length !== 0) { - var targetSeries = series[0]; - this.setTitle(targetSeries.get('title')); - this.showMainRegion(new SeriesDetailsLayout({ model : targetSeries })); - } else { - this.showNotFound(); - } - } -}); \ No newline at end of file diff --git a/src/UI/Series/SeriesModel.js b/src/UI/Series/SeriesModel.js deleted file mode 100644 index 9d154fa7d..000000000 --- a/src/UI/Series/SeriesModel.js +++ /dev/null @@ -1,31 +0,0 @@ -var Backbone = require('backbone'); -var _ = require('underscore'); - -module.exports = Backbone.Model.extend({ - urlRoot : window.NzbDrone.ApiRoot + '/series', - - defaults : { - episodeFileCount : 0, - episodeCount : 0, - isExisting : false, - status : 0 - }, - - setSeasonMonitored : function(seasonNumber) { - _.each(this.get('seasons'), function(season) { - if (season.seasonNumber === seasonNumber) { - season.monitored = !season.monitored; - } - }); - }, - - setSeasonPass : function(seasonNumber) { - _.each(this.get('seasons'), function(season) { - if (season.seasonNumber >= seasonNumber) { - season.monitored = true; - } else { - season.monitored = false; - } - }); - } -}); \ No newline at end of file diff --git a/src/UI/Series/series.less b/src/UI/Series/series.less deleted file mode 100644 index f17229f6f..000000000 --- a/src/UI/Series/series.less +++ /dev/null @@ -1,477 +0,0 @@ -@import "../Content/Bootstrap/variables"; -@import "../Shared/Styles/card.less"; -@import "../Shared/Styles/clickable.less"; -@import "../Content/prefixer"; - -.series-poster { - min-width: 56px; - max-width: 100%; -} - -.truncate { - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -.edit-series-modal, .delete-series-modal { - overflow : visible; - - .series-poster { - padding-left : 20px; - width : 168px; - } - - .form-horizontal { - margin-top : 10px; - } - - .twitter-typeahead { - .form-control[disabled] { - background-color: #ffffff; - } - } -} - -.delete-series-modal { - .path { - margin-left : 30px; - } - - .delete-files-info { - margin-top : 10px; - display : none; - } -} - -.series-item { - padding-bottom : 30px; - - :hover { - text-decoration : none; - } - - h2 { - margin-top : 0px; - } - - a { - color : #000000; - } -} - -.series-page-header { - .card(black); - .opacity(0.9); - background : #000000; - color : #ffffff; - padding : 30px 15px; - margin : 50px 10px; - - .poster { - margin-top : 4px; - } - - .header-text { - margin-top : 0px; - } -} - -.series-season { - .card; - .opacity(0.9); - margin : 30px 10px; - padding : 10px 25px; - - .show-hide-episodes { - .clickable(); - text-align : center; - - i { - .clickable(); - } - } -} - -.series-posters { - list-style-type: none; - - @media (max-width: @screen-xs-max) { - padding : 0px; - } - - li { - display : inline-block; - vertical-align : top; - } - - .series-posters-item { - - .card; - .clickable; - margin-bottom : 20px; - height : 315px; - - .center { - display : block; - margin-left : auto; - margin-right : auto; - text-align : center; - - .progress { - text-align : left; - margin-top : 5px; - left : 0px; - width : 170px; - - .progressbar-front-text, .progressbar-back-text { - width : 170px; - } - } - } - - .labels { - display : inline-block; - .opacity(0.75); - width : 170px; - - :hover { - cursor : default; - } - - .label { - margin-top : 3px; - display : block; - } - - .tooltip { - .opacity(1); - } - } - - @media (max-width: @screen-xs-max) { - height : 235px; - margin : 5px; - padding : 6px 5px; - - .center { - .progress { - width : 125px; - - .progressbar-front-text, .progressbar-back-text { - width : 125px - } - } - } - - .labels { - width: 125px; - } - } - } - - .series-poster-container { - position : relative; - overflow : hidden; - display : inline-block; - - .placeholder-image ~ .title { - opacity: 1.0; - } - - .title { - position : absolute; - top : 25px; - color : #f5f5f5; - width : 100%; - font-size : 22px; - line-height: 24px; - opacity : 0.0; - font-weight: 100; - } - - .ended-banner { - color : #eeeeee; - background-color : #b94a48; - .box-shadow(2px 2px 20px #888888); - -moz-transform-origin : 50% 50%; - -webkit-transform-origin : 50% 50%; - position : absolute; - width : 320px; - top : 200px; - left : -122px; - text-align : center; - .opacity(0.9); - - .transform(rotate(45deg)); - } - - .series-controls { - position : absolute;; - top : 0px; - overflow : hidden; - background-color : #eeeeee; - width : 100%; - text-align : right; - padding-right : 10px; - display : none; - .opacity(0.8); - - i { - .clickable(); - } - } - - .hidden-title { - position : absolute;; - bottom : 0px; - overflow : hidden; - background-color : #eeeeee; - width : 100%; - text-align : center; - .opacity(0.8); - display : none; - } - - .series-poster { - width : 168px; - height : 247px; - display : block; - font-size : 34px; - line-height : 34px; - } - - @media (max-width: @screen-xs-max) { - .series-poster { - width : 120px; - height : 176px; - } - - .ended-banner { - top : 145px; - left : -137px; - } - } - } -} - -.series-detail-overview { - margin-bottom : 50px; -} - -.series-season { - - .episode-number-cell { - width : 40px; - white-space: nowrap; - } - .episode-air-date-cell { - width : 150px; - } - - .episode-status-cell { - width : 100px; - } - - .episode-title-cell { - cursor : pointer; - } -} - -.episode-detail-modal { - - .episode-info { - margin-bottom : 10px; - } - - .episode-overview { - font-style : italic; - } - - .episode-file-info { - margin-top : 30px; - font-size : 12px; - } - - .episode-history-details-cell .popover { - max-width: 800px; - } - - .hidden-series-title { - display : none; - } -} - -.season-grid { - .toggle-cell { - width : 28px; - text-align : center; - padding-left : 0px; - padding-right : 0px; - } - - .toggle-cell { - i { - .clickable; - } - } -} - -.season-actions { - width: 100px; -} - -.season-actions, .series-actions { - - div { - display : inline-block - } - - text-transform : none; - - i { - .clickable(); - font-size : 24px; - margin-left : 5px; - } -} - -.series-stats { - font-size : 11px; -} - -.series-legend { - padding-top : 5px; -} - -.seasonpass-series { - .card; - margin : 20px 0px; - - .title { - font-weight : 300; - font-size : 24px; - line-height : 30px; - margin-left : 5px; - } - - .season-select { - margin-bottom : 0px; - } - - .expander { - .clickable; - line-height : 30px; - margin-left : 8px; - width : 16px; - } - - .season-grid { - margin-top : 10px; - } - - .season-pass-button { - display : inline-block; - } - - .series-monitor-toggle { - font-size : 24px; - margin-top : 3px; - } - - .help-inline { - margin-top : 7px; - display : inline-block; - } -} - -.season-status { - font-size : 11px; - vertical-align : middle !important; -} - -//Overview List -.series-overview-list-actions { - min-width: 56px; - max-width: 56px; - - i { - .clickable(); - } -} - -//Editor - -.series-editor-footer { - max-width: 1160px; - color: #f5f5f5; - margin-left: auto; - margin-right: auto; - - .form-group { - padding-top: 0px; - } -} - -.update-files-series-modal { - .selected-series { - margin-top: 15px; - } -} - -//Series Details - -.series-not-monitored { - .season-monitored, .episode-monitored { - color: #888888; - cursor: not-allowed; - - i { - cursor: not-allowed; - } - } -} - -.series-info { - .row { - margin-bottom : 3px; - - .label { - display : inline-block; - margin-bottom : 2px; - padding : 4px 6px 3px 6px; - max-width : 100%; - white-space : normal; - word-wrap : break-word; - } - } - - .series-info-links { - @media (max-width: @screen-sm-max) { - display : inline-block; - margin-top : 5px; - } - } -} - -.scene-info { - .key, .value { - display : inline-block; - } - - .key { - width : 80px; - margin-left : 10px; - vertical-align : top; - } - - .value { - margin-right : 10px; - max-width : 170px; - } - - ul { - padding-left : 0px; - list-style-type : none; - } -} diff --git a/src/UI/Settings/DownloadClient/Add/DownloadClientAddCollectionView.js b/src/UI/Settings/DownloadClient/Add/DownloadClientAddCollectionView.js deleted file mode 100644 index 9efced249..000000000 --- a/src/UI/Settings/DownloadClient/Add/DownloadClientAddCollectionView.js +++ /dev/null @@ -1,9 +0,0 @@ -var ThingyAddCollectionView = require('../../ThingyAddCollectionView'); -var ThingyHeaderGroupView = require('../../ThingyHeaderGroupView'); -var AddItemView = require('./DownloadClientAddItemView'); - -module.exports = ThingyAddCollectionView.extend({ - itemView : ThingyHeaderGroupView.extend({ itemView : AddItemView }), - itemViewContainer : '.add-download-client .items', - template : 'Settings/DownloadClient/Add/DownloadClientAddCollectionViewTemplate' -}); \ No newline at end of file diff --git a/src/UI/Settings/DownloadClient/Add/DownloadClientAddCollectionViewTemplate.hbs b/src/UI/Settings/DownloadClient/Add/DownloadClientAddCollectionViewTemplate.hbs deleted file mode 100644 index f3a823f4a..000000000 --- a/src/UI/Settings/DownloadClient/Add/DownloadClientAddCollectionViewTemplate.hbs +++ /dev/null @@ -1,14 +0,0 @@ -<div class="modal-content"> - <div class="modal-header"> - <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> - <h3>Add Download Client</h3> - </div> - <div class="modal-body"> - <div class="add-download-client add-thingies"> - <ul class="items"></ul> - </div> - </div> - <div class="modal-footer"> - <button class="btn" data-dismiss="modal">Close</button> - </div> -</div> diff --git a/src/UI/Settings/DownloadClient/Add/DownloadClientAddItemView.js b/src/UI/Settings/DownloadClient/Add/DownloadClientAddItemView.js deleted file mode 100644 index 75a39e2b5..000000000 --- a/src/UI/Settings/DownloadClient/Add/DownloadClientAddItemView.js +++ /dev/null @@ -1,58 +0,0 @@ -var _ = require('underscore'); -var $ = require('jquery'); -var AppLayout = require('../../../AppLayout'); -var Marionette = require('marionette'); -var EditView = require('../Edit/DownloadClientEditView'); - -module.exports = Marionette.ItemView.extend({ - template : 'Settings/DownloadClient/Add/DownloadClientAddItemViewTemplate', - tagName : 'li', - className : 'add-thingy-item', - - events : { - 'click .x-preset' : '_addPreset', - 'click' : '_add' - }, - - initialize : function(options) { - this.targetCollection = options.targetCollection; - }, - - _addPreset : function(e) { - var presetName = $(e.target).closest('.x-preset').attr('data-id'); - - var presetData = _.where(this.model.get('presets'), { name : presetName })[0]; - - this.model.set(presetData); - - this.model.set({ - id : undefined, - enable : true - }); - - var editView = new EditView({ - model : this.model, - targetCollection : this.targetCollection - }); - - AppLayout.modalRegion.show(editView); - }, - - _add : function(e) { - if ($(e.target).closest('.btn,.btn-group').length !== 0 && $(e.target).closest('.x-custom').length === 0) { - return; - } - - this.model.set({ - id : undefined, - enable : true - }); - - var editView = new EditView({ - model : this.model, - targetCollection : this.targetCollection - }); - - AppLayout.modalRegion.show(editView); - } -}); \ No newline at end of file diff --git a/src/UI/Settings/DownloadClient/Add/DownloadClientAddItemViewTemplate.hbs b/src/UI/Settings/DownloadClient/Add/DownloadClientAddItemViewTemplate.hbs deleted file mode 100644 index 1cdb3dffc..000000000 --- a/src/UI/Settings/DownloadClient/Add/DownloadClientAddItemViewTemplate.hbs +++ /dev/null @@ -1,30 +0,0 @@ -<div class="add-thingy"> - <div> - {{implementationName}} - </div> - <div class="pull-right"> - {{#if_gt presets.length compare=0}} - <button class="btn btn-xs btn-default x-custom"> - Custom - </button> - <div class="btn-group"> - <button class="btn btn-xs btn-default dropdown-toggle" data-toggle="dropdown"> - Presets - <span class="caret"></span> - </button> - <ul class="dropdown-menu"> - {{#each presets}} - <li class="x-preset" data-id="{{name}}"> - <a>{{name}}</a> - </li> - {{/each}} - </ul> - </div> - {{/if_gt}} - {{#if infoLink}} - <a class="btn btn-xs btn-default x-info" href="{{infoLink}}"> - <i class="icon-lidarr-form-info"/> - </a> - {{/if}} - </div> -</div> \ No newline at end of file diff --git a/src/UI/Settings/DownloadClient/Add/DownloadClientSchemaModal.js b/src/UI/Settings/DownloadClient/Add/DownloadClientSchemaModal.js deleted file mode 100644 index 603a4dfdc..000000000 --- a/src/UI/Settings/DownloadClient/Add/DownloadClientSchemaModal.js +++ /dev/null @@ -1,39 +0,0 @@ -var _ = require('underscore'); -var AppLayout = require('../../../AppLayout'); -var Backbone = require('backbone'); -var SchemaCollection = require('../DownloadClientCollection'); -var AddCollectionView = require('./DownloadClientAddCollectionView'); - -module.exports = { - open : function(collection) { - var schemaCollection = new SchemaCollection(); - var originalUrl = schemaCollection.url; - schemaCollection.url = schemaCollection.url + '/schema'; - schemaCollection.fetch(); - schemaCollection.url = originalUrl; - - var groupedSchemaCollection = new Backbone.Collection(); - - schemaCollection.on('sync', function() { - - var groups = schemaCollection.groupBy(function(model, iterator) { - return model.get('protocol'); - }); - var modelCollection = _.map(groups, function(values, key, list) { - return { - 'header' : key, - collection : values - }; - }); - - groupedSchemaCollection.reset(modelCollection); - }); - - var view = new AddCollectionView({ - collection : groupedSchemaCollection, - targetCollection : collection - }); - - AppLayout.modalRegion.show(view); - } -}; \ No newline at end of file diff --git a/src/UI/Settings/DownloadClient/Delete/DownloadClientDeleteView.js b/src/UI/Settings/DownloadClient/Delete/DownloadClientDeleteView.js deleted file mode 100644 index e2b9e8556..000000000 --- a/src/UI/Settings/DownloadClient/Delete/DownloadClientDeleteView.js +++ /dev/null @@ -1,19 +0,0 @@ -var vent = require('vent'); -var Marionette = require('marionette'); - -module.exports = Marionette.ItemView.extend({ - template : 'Settings/DownloadClient/Delete/DownloadClientDeleteViewTemplate', - - events : { - 'click .x-confirm-delete' : '_delete' - }, - - _delete : function() { - this.model.destroy({ - wait : true, - success : function() { - vent.trigger(vent.Commands.CloseModalCommand); - } - }); - } -}); \ No newline at end of file diff --git a/src/UI/Settings/DownloadClient/Delete/DownloadClientDeleteViewTemplate.hbs b/src/UI/Settings/DownloadClient/Delete/DownloadClientDeleteViewTemplate.hbs deleted file mode 100644 index f31729279..000000000 --- a/src/UI/Settings/DownloadClient/Delete/DownloadClientDeleteViewTemplate.hbs +++ /dev/null @@ -1,13 +0,0 @@ -<div class="modal-content"> - <div class="modal-header"> - <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> - <h3>Delete Download Client</h3> - </div> - <div class="modal-body"> - <p>Are you sure you want to delete '{{name}}'?</p> - </div> - <div class="modal-footer"> - <button class="btn" data-dismiss="modal">Cancel</button> - <button class="btn btn-danger x-confirm-delete">Delete</button> - </div> -</div> diff --git a/src/UI/Settings/DownloadClient/DownloadClientCollection.js b/src/UI/Settings/DownloadClient/DownloadClientCollection.js deleted file mode 100644 index 6e0a37083..000000000 --- a/src/UI/Settings/DownloadClient/DownloadClientCollection.js +++ /dev/null @@ -1,25 +0,0 @@ -var Backbone = require('backbone'); -var DownloadClientModel = require('./DownloadClientModel'); - -module.exports = Backbone.Collection.extend({ - model : DownloadClientModel, - url : window.NzbDrone.ApiRoot + '/downloadclient', - - comparator : function(left, right, collection) { - var result = 0; - - if (left.get('protocol')) { - result = -left.get('protocol').localeCompare(right.get('protocol')); - } - - if (result === 0 && left.get('name')) { - result = left.get('name').localeCompare(right.get('name')); - } - - if (result === 0) { - result = left.get('implementation').localeCompare(right.get('implementation')); - } - - return result; - } -}); \ No newline at end of file diff --git a/src/UI/Settings/DownloadClient/DownloadClientCollectionView.js b/src/UI/Settings/DownloadClient/DownloadClientCollectionView.js deleted file mode 100644 index 457c7afcb..000000000 --- a/src/UI/Settings/DownloadClient/DownloadClientCollectionView.js +++ /dev/null @@ -1,25 +0,0 @@ -var Marionette = require('marionette'); -var ItemView = require('./DownloadClientItemView'); -var SchemaModal = require('./Add/DownloadClientSchemaModal'); - -module.exports = Marionette.CompositeView.extend({ - itemView : ItemView, - itemViewContainer : '.download-client-list', - template : 'Settings/DownloadClient/DownloadClientCollectionViewTemplate', - - ui : { - 'addCard' : '.x-add-card' - }, - - events : { - 'click .x-add-card' : '_openSchemaModal' - }, - - appendHtml : function(collectionView, itemView, index) { - collectionView.ui.addCard.parent('li').before(itemView.el); - }, - - _openSchemaModal : function() { - SchemaModal.open(this.collection); - } -}); \ No newline at end of file diff --git a/src/UI/Settings/DownloadClient/DownloadClientCollectionViewTemplate.hbs b/src/UI/Settings/DownloadClient/DownloadClientCollectionViewTemplate.hbs deleted file mode 100644 index dbf558b5d..000000000 --- a/src/UI/Settings/DownloadClient/DownloadClientCollectionViewTemplate.hbs +++ /dev/null @@ -1,16 +0,0 @@ -<fieldset> - <legend>Download Clients</legend> - <div class="row"> - <div class="col-md-12"> - <ul class="download-client-list thingies"> - <li> - <div class="download-client-item thingy add-card x-add-card"> - <span class="center well"> - <i class="icon-lidarr-add"/> - </span> - </div> - </li> - </ul> - </div> - </div> -</fieldset> \ No newline at end of file diff --git a/src/UI/Settings/DownloadClient/DownloadClientItemView.js b/src/UI/Settings/DownloadClient/DownloadClientItemView.js deleted file mode 100644 index fc8a65b4f..000000000 --- a/src/UI/Settings/DownloadClient/DownloadClientItemView.js +++ /dev/null @@ -1,24 +0,0 @@ -var AppLayout = require('../../AppLayout'); -var Marionette = require('marionette'); -var EditView = require('./Edit/DownloadClientEditView'); - -module.exports = Marionette.ItemView.extend({ - template : 'Settings/DownloadClient/DownloadClientItemViewTemplate', - tagName : 'li', - - events : { - 'click' : '_edit' - }, - - initialize : function() { - this.listenTo(this.model, 'sync', this.render); - }, - - _edit : function() { - var view = new EditView({ - model : this.model, - targetCollection : this.model.collection - }); - AppLayout.modalRegion.show(view); - } -}); \ No newline at end of file diff --git a/src/UI/Settings/DownloadClient/DownloadClientItemViewTemplate.hbs b/src/UI/Settings/DownloadClient/DownloadClientItemViewTemplate.hbs deleted file mode 100644 index ca9fb65f9..000000000 --- a/src/UI/Settings/DownloadClient/DownloadClientItemViewTemplate.hbs +++ /dev/null @@ -1,13 +0,0 @@ -<div class="download-client-item thingy"> - <div> - <h3>{{name}}</h3> - </div> - - <div class="settings"> - {{#if enable}} - <span class="label label-success">Enabled</span> - {{else}} - <span class="label label-default">Not Enabled</span> - {{/if}} - </div> -</div> diff --git a/src/UI/Settings/DownloadClient/DownloadClientLayout.js b/src/UI/Settings/DownloadClient/DownloadClientLayout.js deleted file mode 100644 index fdd6e1b80..000000000 --- a/src/UI/Settings/DownloadClient/DownloadClientLayout.js +++ /dev/null @@ -1,32 +0,0 @@ -var Marionette = require('marionette'); -var DownloadClientCollection = require('./DownloadClientCollection'); -var DownloadClientCollectionView = require('./DownloadClientCollectionView'); -var DownloadHandlingView = require('./DownloadHandling/DownloadHandlingView'); -var DroneFactoryView = require('./DroneFactory/DroneFactoryView'); -var RemotePathMappingCollection = require('./RemotePathMapping/RemotePathMappingCollection'); -var RemotePathMappingCollectionView = require('./RemotePathMapping/RemotePathMappingCollectionView'); - -module.exports = Marionette.Layout.extend({ - template : 'Settings/DownloadClient/DownloadClientLayoutTemplate', - - regions : { - downloadClients : '#x-download-clients-region', - downloadHandling : '#x-download-handling-region', - droneFactory : '#x-dronefactory-region', - remotePathMappings : '#x-remotepath-mapping-region' - }, - - initialize : function() { - this.downloadClientsCollection = new DownloadClientCollection(); - this.downloadClientsCollection.fetch(); - this.remotePathMappingCollection = new RemotePathMappingCollection(); - this.remotePathMappingCollection.fetch(); - }, - - onShow : function() { - this.downloadClients.show(new DownloadClientCollectionView({ collection : this.downloadClientsCollection })); - this.downloadHandling.show(new DownloadHandlingView({ model : this.model })); - this.droneFactory.show(new DroneFactoryView({ model : this.model })); - this.remotePathMappings.show(new RemotePathMappingCollectionView({ collection : this.remotePathMappingCollection })); - } -}); \ No newline at end of file diff --git a/src/UI/Settings/DownloadClient/DownloadClientLayoutTemplate.hbs b/src/UI/Settings/DownloadClient/DownloadClientLayoutTemplate.hbs deleted file mode 100644 index ab039d682..000000000 --- a/src/UI/Settings/DownloadClient/DownloadClientLayoutTemplate.hbs +++ /dev/null @@ -1,6 +0,0 @@ -<div id="x-download-clients-region"></div> -<div class="form-horizontal"> - <div id="x-download-handling-region"></div> - <div id="x-dronefactory-region"></div> - <div id="x-remotepath-mapping-region"></div> -</div> diff --git a/src/UI/Settings/DownloadClient/DownloadClientModel.js b/src/UI/Settings/DownloadClient/DownloadClientModel.js deleted file mode 100644 index 288e45362..000000000 --- a/src/UI/Settings/DownloadClient/DownloadClientModel.js +++ /dev/null @@ -1,3 +0,0 @@ -var ProviderSettingsModelBase = require('../ProviderSettingsModelBase'); - -module.exports = ProviderSettingsModelBase.extend({}); \ No newline at end of file diff --git a/src/UI/Settings/DownloadClient/DownloadClientSettingsModel.js b/src/UI/Settings/DownloadClient/DownloadClientSettingsModel.js deleted file mode 100644 index eef6d7557..000000000 --- a/src/UI/Settings/DownloadClient/DownloadClientSettingsModel.js +++ /dev/null @@ -1,7 +0,0 @@ -var SettingsModelBase = require('../SettingsModelBase'); - -module.exports = SettingsModelBase.extend({ - url : window.NzbDrone.ApiRoot + '/config/downloadclient', - successMessage : 'Download client settings saved', - errorMessage : 'Failed to save download client settings' -}); \ No newline at end of file diff --git a/src/UI/Settings/DownloadClient/DownloadHandling/DownloadHandlingView.js b/src/UI/Settings/DownloadClient/DownloadHandling/DownloadHandlingView.js deleted file mode 100644 index f3411025c..000000000 --- a/src/UI/Settings/DownloadClient/DownloadHandling/DownloadHandlingView.js +++ /dev/null @@ -1,50 +0,0 @@ -var Marionette = require('marionette'); -var AsModelBoundView = require('../../../Mixins/AsModelBoundView'); -var AsValidatedView = require('../../../Mixins/AsValidatedView'); - -var view = Marionette.ItemView.extend({ - template : 'Settings/DownloadClient/DownloadHandling/DownloadHandlingViewTemplate', - - ui : { - completedDownloadHandlingCheckbox : '.x-completed-download-handling', - completedDownloadOptions : '.x-completed-download-options', - failedAutoRedownladCheckbox : '.x-failed-auto-redownload', - failedDownloadOptions : '.x-failed-download-options' - }, - - events : { - 'change .x-completed-download-handling' : '_setCompletedDownloadOptionsVisibility', - 'change .x-failed-auto-redownload' : '_setFailedDownloadOptionsVisibility' - }, - - onRender : function() { - if (!this.ui.completedDownloadHandlingCheckbox.prop('checked')) { - this.ui.completedDownloadOptions.hide(); - } - if (!this.ui.failedAutoRedownladCheckbox.prop('checked')) { - this.ui.failedDownloadOptions.hide(); - } - }, - - _setCompletedDownloadOptionsVisibility : function() { - var checked = this.ui.completedDownloadHandlingCheckbox.prop('checked'); - if (checked) { - this.ui.completedDownloadOptions.slideDown(); - } else { - this.ui.completedDownloadOptions.slideUp(); - } - }, - - _setFailedDownloadOptionsVisibility : function() { - var checked = this.ui.failedAutoRedownladCheckbox.prop('checked'); - if (checked) { - this.ui.failedDownloadOptions.slideDown(); - } else { - this.ui.failedDownloadOptions.slideUp(); - } - } -}); - -AsModelBoundView.call(view); -AsValidatedView.call(view); -module.exports = view; \ No newline at end of file diff --git a/src/UI/Settings/DownloadClient/DownloadHandling/DownloadHandlingViewTemplate.hbs b/src/UI/Settings/DownloadClient/DownloadHandling/DownloadHandlingViewTemplate.hbs deleted file mode 100644 index bf0c70b94..000000000 --- a/src/UI/Settings/DownloadClient/DownloadHandling/DownloadHandlingViewTemplate.hbs +++ /dev/null @@ -1,93 +0,0 @@ -<fieldset> - <legend>Completed Download Handling</legend> - <div class="form-group"> - <label class="col-sm-3 control-label">Enable</label> - - <div class="col-sm-8"> - <div class="input-group"> - <label class="checkbox toggle well"> - <input type="checkbox" name="enableCompletedDownloadHandling" class="x-completed-download-handling"/> - <p> - <span>Yes</span> - <span>No</span> - </p> - - <div class="btn btn-primary slide-button"/> - </label> - <span class="help-inline-checkbox"> - <i class="icon-lidarr-form-info" title="Automatically import completed downloads from download client"/> - </span> - </div> - </div> - </div> - - <div class="x-completed-download-options advanced-setting"> - <div class="form-group"> - <label class="col-sm-3 control-label">Remove</label> - - <div class="col-sm-8"> - <div class="input-group"> - <label class="checkbox toggle well"> - <input type="checkbox" name="removeCompletedDownloads"/> - <p> - <span>Yes</span> - <span>No</span> - </p> - - <div class="btn btn-primary slide-button"/> - </label> - - <span class="help-inline-checkbox"> - <i class="icon-lidarr-form-info" title="Remove imported downloads from download client history"/> - </span> - </div> - </div> - </div> - </div> -</fieldset> - -<fieldset> - <legend>Failed Download Handling</legend> - <div class="form-group"> - <label class="col-sm-3 control-label">Redownload</label> - - <div class="col-sm-8"> - <div class="input-group"> - <label class="checkbox toggle well"> - <input type="checkbox" name="autoRedownloadFailed" class="x-failed-auto-redownload"/> - <p> - <span>Yes</span> - <span>No</span> - </p> - - <div class="btn btn-primary slide-button"/> - </label> - - <span class="help-inline-checkbox"> - <i class="icon-lidarr-form-info" title="Automatically search for and attempt to download a different release"/> - </span> - </div> - </div> - </div> - <div class="x-failed-download-options advanced-setting"> - <div class="form-group "> - <label class="col-sm-3 control-label">Remove</label> - <div class="col-sm-8"> - <div class="input-group"> - <label class="checkbox toggle well"> - <input type="checkbox" name="removeFailedDownloads"/> - <p> - <span>Yes</span> - <span>No</span> - </p> - - <div class="btn btn-primary slide-button"/> - </label> - <span class="help-inline-checkbox"> - <i class="icon-lidarr-form-info" title="Remove failed downloads from download client history"/> - </span> - </div> - </div> - </div> - </div> -</fieldset> \ No newline at end of file diff --git a/src/UI/Settings/DownloadClient/DroneFactory/DroneFactoryView.js b/src/UI/Settings/DownloadClient/DroneFactory/DroneFactoryView.js deleted file mode 100644 index 154be0a4b..000000000 --- a/src/UI/Settings/DownloadClient/DroneFactory/DroneFactoryView.js +++ /dev/null @@ -1,21 +0,0 @@ -var Marionette = require('marionette'); -var AsModelBoundView = require('../../../Mixins/AsModelBoundView'); -var AsValidatedView = require('../../../Mixins/AsValidatedView'); -require('../../../Mixins/FileBrowser'); - -var view = Marionette.ItemView.extend({ - template : 'Settings/DownloadClient/DroneFactory/DroneFactoryViewTemplate', - - ui : { - droneFactory : '.x-path' - }, - - onShow : function() { - this.ui.droneFactory.fileBrowser(); - } -}); - -AsModelBoundView.call(view); -AsValidatedView.call(view); - -module.exports = view; \ No newline at end of file diff --git a/src/UI/Settings/DownloadClient/DroneFactory/DroneFactoryViewTemplate.hbs b/src/UI/Settings/DownloadClient/DroneFactory/DroneFactoryViewTemplate.hbs deleted file mode 100644 index c0385cd43..000000000 --- a/src/UI/Settings/DownloadClient/DroneFactory/DroneFactoryViewTemplate.hbs +++ /dev/null @@ -1,29 +0,0 @@ -<fieldset class="advanced-setting"> - <legend>Drone Factory Options</legend> - <div class="form-group"> - <label class="col-sm-3 control-label">Drone Factory</label> - - <div class="col-sm-1 col-sm-push-8 help-inline"> - <i class="icon-lidarr-form-info" title="Optional folder to periodically scan for possible imports"/> - <i class="icon-lidarr-form-warning" title="Do not use the folder that contains some or all of your sorted and named Music Artists - doing so could cause data loss"></i> - <i class="icon-lidarr-form-warning" title="Download client history items that are stored in the drone factory will be ignored."/> - </div> - - <div class="col-sm-8 col-sm-pull-1"> - <input type="text" name="downloadedAlbumsFolder" class="form-control x-path" /> - </div> - </div> - - <div class="form-group"> - <label class="col-sm-3 control-label">Drone Factory Interval</label> - - <div class="col-sm-1 col-sm-push-2 help-inline"> - <i class="icon-lidarr-form-info" title="Interval in minutes to scan the Drone Factory. Set to zero to disable."/> - <i class="icon-lidarr-form-warning" title="Setting a high interval or disabling scanning will prevent albums from being imported."></i> - </div> - - <div class="col-sm-2 col-sm-pull-1"> - <input type="number" name="downloadedAlbumsScanInterval" class="form-control" /> - </div> - </div> -</fieldset> \ No newline at end of file diff --git a/src/UI/Settings/DownloadClient/Edit/DownloadClientEditView.js b/src/UI/Settings/DownloadClient/Edit/DownloadClientEditView.js deleted file mode 100644 index 1ae48d999..000000000 --- a/src/UI/Settings/DownloadClient/Edit/DownloadClientEditView.js +++ /dev/null @@ -1,56 +0,0 @@ -var vent = require('vent'); -var Marionette = require('marionette'); -var DeleteView = require('../Delete/DownloadClientDeleteView'); -var AsModelBoundView = require('../../../Mixins/AsModelBoundView'); -var AsValidatedView = require('../../../Mixins/AsValidatedView'); -var AsEditModalView = require('../../../Mixins/AsEditModalView'); -require('../../../Form/FormBuilder'); -require('../../../Mixins/FileBrowser'); -require('bootstrap'); - -var view = Marionette.ItemView.extend({ - template : 'Settings/DownloadClient/Edit/DownloadClientEditViewTemplate', - - ui : { - path : '.x-path', - modalBody : '.modal-body' - }, - - events : { - 'click .x-back' : '_back' - }, - - _deleteView : DeleteView, - - initialize : function(options) { - this.targetCollection = options.targetCollection; - }, - - onShow : function() { - if (this.ui.path.length > 0) { - this.ui.modalBody.addClass('modal-overflow'); - } - - this.ui.path.fileBrowser(); - }, - - _onAfterSave : function() { - this.targetCollection.add(this.model, { merge : true }); - vent.trigger(vent.Commands.CloseModalCommand); - }, - - _onAfterSaveAndAdd : function() { - this.targetCollection.add(this.model, { merge : true }); - - require('../Add/DownloadClientSchemaModal').open(this.targetCollection); - }, - _back : function() { - require('../Add/DownloadClientSchemaModal').open(this.targetCollection); - } -}); - -AsModelBoundView.call(view); -AsValidatedView.call(view); -AsEditModalView.call(view); - -module.exports = view; \ No newline at end of file diff --git a/src/UI/Settings/DownloadClient/Edit/DownloadClientEditViewTemplate.hbs b/src/UI/Settings/DownloadClient/Edit/DownloadClientEditViewTemplate.hbs deleted file mode 100644 index 245af60f8..000000000 --- a/src/UI/Settings/DownloadClient/Edit/DownloadClientEditViewTemplate.hbs +++ /dev/null @@ -1,68 +0,0 @@ -<div class="modal-content"> - <div class="modal-header"> - <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> - {{#if id}} - <h3>Edit - {{implementation}}</h3> - {{else}} - <h3>Add - {{implementation}}</h3> - {{/if}} - </div> - <div class="modal-body download-client-modal"> - {{formMessage message}} - - <div class="form-horizontal"> - <div class="form-group"> - <label class="col-sm-3 control-label">Name</label> - - <div class="col-sm-5"> - <input type="text" name="name" class="form-control"/> - </div> - </div> - - <div class="form-group"> - <label class="col-sm-3 control-label">Enable</label> - - <div class="col-sm-5"> - <div class="input-group"> - <label class="checkbox toggle well"> - <input type="checkbox" name="enable"/> - <p> - <span>Yes</span> - <span>No</span> - </p> - - <div class="btn btn-primary slide-button"/> - </label> - </div> - </div> - </div> - - <hr> - - {{formBuilder}} - </div> - </div> - <div class="modal-footer"> - {{#if id}} - <button class="btn btn-danger pull-left x-delete">Delete</button> - {{else}} - <button class="btn pull-left x-back">Back</button> - {{/if}} - - <span class="indicator x-indicator"><i class="icon-lidarr-spinner fa-spin"></i></span> - <button class="btn x-test">test <i class="x-test-icon icon-lidarr-test"/></button> - <button class="btn" data-dismiss="modal">Cancel</button> - - <div class="btn-group"> - <button class="btn btn-primary x-save">Save</button> - <button class="btn btn-icon-only btn-primary dropdown-toggle" data-toggle="dropdown"> - <span class="caret"></span> - </button> - <ul class="dropdown-menu"> - <li class="save-and-add x-save-and-add"> - save and add - </li> - </ul> - </div> - </div> -</div> diff --git a/src/UI/Settings/DownloadClient/RemotePathMapping/RemotePathMappingCollection.js b/src/UI/Settings/DownloadClient/RemotePathMapping/RemotePathMappingCollection.js deleted file mode 100644 index 2906e2254..000000000 --- a/src/UI/Settings/DownloadClient/RemotePathMapping/RemotePathMappingCollection.js +++ /dev/null @@ -1,7 +0,0 @@ -var Backbone = require('backbone'); -var RemotePathMappingModel = require('./RemotePathMappingModel'); - -module.exports = Backbone.Collection.extend({ - model : RemotePathMappingModel, - url : window.NzbDrone.ApiRoot + '/remotePathMapping' -}); \ No newline at end of file diff --git a/src/UI/Settings/DownloadClient/RemotePathMapping/RemotePathMappingCollectionView.js b/src/UI/Settings/DownloadClient/RemotePathMapping/RemotePathMappingCollectionView.js deleted file mode 100644 index 9a24a95d3..000000000 --- a/src/UI/Settings/DownloadClient/RemotePathMapping/RemotePathMappingCollectionView.js +++ /dev/null @@ -1,28 +0,0 @@ -var AppLayout = require('../../../AppLayout'); -var Marionette = require('marionette'); -var RemotePathMappingItemView = require('./RemotePathMappingItemView'); -var EditView = require('./RemotePathMappingEditView'); -var RemotePathMappingModel = require('./RemotePathMappingModel'); -require('bootstrap'); - -module.exports = Marionette.CompositeView.extend({ - template : 'Settings/DownloadClient/RemotePathMapping/RemotePathMappingCollectionViewTemplate', - itemViewContainer : '.x-rows', - itemView : RemotePathMappingItemView, - - events : { - 'click .x-add' : '_addMapping' - }, - - _addMapping : function() { - var model = new RemotePathMappingModel(); - model.collection = this.collection; - - var view = new EditView({ - model : model, - targetCollection : this.collection - }); - - AppLayout.modalRegion.show(view); - } -}); \ No newline at end of file diff --git a/src/UI/Settings/DownloadClient/RemotePathMapping/RemotePathMappingCollectionViewTemplate.hbs b/src/UI/Settings/DownloadClient/RemotePathMapping/RemotePathMappingCollectionViewTemplate.hbs deleted file mode 100644 index fcaa91061..000000000 --- a/src/UI/Settings/DownloadClient/RemotePathMapping/RemotePathMappingCollectionViewTemplate.hbs +++ /dev/null @@ -1,24 +0,0 @@ -<fieldset class="advanced-setting"> - <legend>Remote Path Mappings</legend> - - <div class="col-md-12"> - <div class="rule-setting-list"> - <div class="rule-setting-header x-header hidden-xs"> - <div class="row"> - <span class="col-sm-2">Host</span> - <span class="col-sm-5">Remote Path</span> - <span class="col-sm-4">Local Path</span> - </div> - </div> - <div class="rows x-rows"> - </div> - <div class="rule-setting-footer"> - <div class="pull-right"> - <span class="add-rule-setting-mapping"> - <i class="icon-lidarr-add x-add" title="Add new mapping" /> - </span> - </div> - </div> - </div> - </div> -</fieldset> \ No newline at end of file diff --git a/src/UI/Settings/DownloadClient/RemotePathMapping/RemotePathMappingDeleteView.js b/src/UI/Settings/DownloadClient/RemotePathMapping/RemotePathMappingDeleteView.js deleted file mode 100644 index 1ddf5f94b..000000000 --- a/src/UI/Settings/DownloadClient/RemotePathMapping/RemotePathMappingDeleteView.js +++ /dev/null @@ -1,19 +0,0 @@ -var vent = require('vent'); -var Marionette = require('marionette'); - -module.exports = Marionette.ItemView.extend({ - template : 'Settings/DownloadClient/RemotePathMapping/RemotePathMappingDeleteViewTemplate', - - events : { - 'click .x-confirm-delete' : '_delete' - }, - - _delete : function() { - this.model.destroy({ - wait : true, - success : function() { - vent.trigger(vent.Commands.CloseModalCommand); - } - }); - } -}); \ No newline at end of file diff --git a/src/UI/Settings/DownloadClient/RemotePathMapping/RemotePathMappingDeleteViewTemplate.hbs b/src/UI/Settings/DownloadClient/RemotePathMapping/RemotePathMappingDeleteViewTemplate.hbs deleted file mode 100644 index 10d94278a..000000000 --- a/src/UI/Settings/DownloadClient/RemotePathMapping/RemotePathMappingDeleteViewTemplate.hbs +++ /dev/null @@ -1,13 +0,0 @@ -<div class="modal-content"> - <div class="modal-header"> - <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> - <h3>Delete Mapping</h3> - </div> - <div class="modal-body"> - <p>Are you sure you want to delete the mapping for '{{localPath}}'?</p> - </div> - <div class="modal-footer"> - <button class="btn" data-dismiss="modal">Cancel</button> - <button class="btn btn-danger x-confirm-delete">Delete</button> - </div> -</div> diff --git a/src/UI/Settings/DownloadClient/RemotePathMapping/RemotePathMappingEditView.js b/src/UI/Settings/DownloadClient/RemotePathMapping/RemotePathMappingEditView.js deleted file mode 100644 index 642901162..000000000 --- a/src/UI/Settings/DownloadClient/RemotePathMapping/RemotePathMappingEditView.js +++ /dev/null @@ -1,45 +0,0 @@ -var _ = require('underscore'); -var vent = require('vent'); -var AppLayout = require('../../../AppLayout'); -var Marionette = require('marionette'); -var DeleteView = require('./RemotePathMappingDeleteView'); -var CommandController = require('../../../Commands/CommandController'); -var AsModelBoundView = require('../../../Mixins/AsModelBoundView'); -var AsValidatedView = require('../../../Mixins/AsValidatedView'); -var AsEditModalView = require('../../../Mixins/AsEditModalView'); -require('../../../Mixins/FileBrowser'); -require('bootstrap'); - -var view = Marionette.ItemView.extend({ - template : 'Settings/DownloadClient/RemotePathMapping/RemotePathMappingEditViewTemplate', - - ui : { - path : '.x-path', - modalBody : '.modal-body' - }, - - _deleteView : DeleteView, - - initialize : function(options) { - this.targetCollection = options.targetCollection; - }, - - onShow : function() { - if (this.ui.path.length > 0) { - this.ui.modalBody.addClass('modal-overflow'); - } - - this.ui.path.fileBrowser(); - }, - - _onAfterSave : function() { - this.targetCollection.add(this.model, { merge : true }); - vent.trigger(vent.Commands.CloseModalCommand); - } -}); - -AsModelBoundView.call(view); -AsValidatedView.call(view); -AsEditModalView.call(view); - -module.exports = view; \ No newline at end of file diff --git a/src/UI/Settings/DownloadClient/RemotePathMapping/RemotePathMappingEditViewTemplate.hbs b/src/UI/Settings/DownloadClient/RemotePathMapping/RemotePathMappingEditViewTemplate.hbs deleted file mode 100644 index b28850d15..000000000 --- a/src/UI/Settings/DownloadClient/RemotePathMapping/RemotePathMappingEditViewTemplate.hbs +++ /dev/null @@ -1,63 +0,0 @@ -<div class="modal-content"> - <div class="modal-header"> - <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> - {{#if id}} - <h3>Edit Mapping</h3> - {{else}} - <h3>Add Mapping</h3> - {{/if}} - </div> - <div class="modal-body remotepath-mapping-modal"> - <div class="form-horizontal"> - <div> - <p>Use this feature if you have a remotely running Download Client. Lidarr will use the information provided to translate the paths provided by the Download Client API to something Lidarr can access and import.</p> - </div> - <div class="form-group"> - <label class="col-sm-3 control-label">Host</label> - - <div class="col-sm-1 col-sm-push-3 help-inline"> - <i class="icon-lidarr-form-info" title="Host you specified for the remote Download Client." /> - </div> - - <div class="col-sm-3 col-sm-pull-1"> - <input type="text" name="host" class="form-control"/> - </div> - </div> - - <div class="form-group"> - <label class="col-sm-3 control-label">Remote Path</label> - - <div class="col-sm-1 col-sm-push-5 help-inline"> - <i class="icon-lidarr-form-info" title="Root path to the directory that the Download Client accesses." /> - </div> - - <div class="col-sm-5 col-sm-pull-1"> - <input type="text" name="remotePath" class="form-control"/> - </div> - </div> - - <div class="form-group"> - <label class="col-sm-3 control-label">Local Path</label> - - <div class="col-sm-1 col-sm-push-5 help-inline"> - <i class="icon-lidarr-form-info" title="Path that Lidarr should use to access the same directory remotely." /> - </div> - - <div class="col-sm-5 col-sm-pull-1"> - <input type="text" name="localPath" class="form-control x-path"/> - </div> - </div> - </div> - </div> - <div class="modal-footer"> - {{#if id}} - <button class="btn btn-danger pull-left x-delete">Delete</button> - {{/if}} - - <button class="btn" data-dismiss="modal">Cancel</button> - - <div class="btn-group"> - <button class="btn btn-primary x-save">Save</button> - </div> - </div> -</div> \ No newline at end of file diff --git a/src/UI/Settings/DownloadClient/RemotePathMapping/RemotePathMappingItemView.js b/src/UI/Settings/DownloadClient/RemotePathMapping/RemotePathMappingItemView.js deleted file mode 100644 index d81690e57..000000000 --- a/src/UI/Settings/DownloadClient/RemotePathMapping/RemotePathMappingItemView.js +++ /dev/null @@ -1,25 +0,0 @@ -var AppLayout = require('../../../AppLayout'); -var Marionette = require('marionette'); -var EditView = require('./RemotePathMappingEditView'); - -module.exports = Marionette.ItemView.extend({ - template : 'Settings/DownloadClient/RemotePathMapping/RemotePathMappingItemViewTemplate', - className : 'row', - - events : { - 'click .x-edit' : '_editMapping' - }, - - initialize : function() { - this.listenTo(this.model, 'sync', this.render); - }, - - _editMapping : function() { - var view = new EditView({ - model : this.model, - targetCollection : this.model.collection - }); - - AppLayout.modalRegion.show(view); - } -}); \ No newline at end of file diff --git a/src/UI/Settings/DownloadClient/RemotePathMapping/RemotePathMappingItemViewTemplate.hbs b/src/UI/Settings/DownloadClient/RemotePathMapping/RemotePathMappingItemViewTemplate.hbs deleted file mode 100644 index b17a77b38..000000000 --- a/src/UI/Settings/DownloadClient/RemotePathMapping/RemotePathMappingItemViewTemplate.hbs +++ /dev/null @@ -1,12 +0,0 @@ - <div class="col-sm-2"> - {{host}} - </div> - <div class="col-sm-5"> - {{remotePath}} - </div> - <div class="col-sm-4"> - {{localPath}} - </div> - <div class="col-sm-1"> - <div class="pull-right"><i class="icon-lidarr-edit x-edit" title="" data-original-title="Edit Mapping"></i></div> - </div> \ No newline at end of file diff --git a/src/UI/Settings/DownloadClient/RemotePathMapping/RemotePathMappingModel.js b/src/UI/Settings/DownloadClient/RemotePathMapping/RemotePathMappingModel.js deleted file mode 100644 index e8ea08465..000000000 --- a/src/UI/Settings/DownloadClient/RemotePathMapping/RemotePathMappingModel.js +++ /dev/null @@ -1,4 +0,0 @@ -var $ = require('jquery'); -var DeepModel = require('backbone.deepmodel'); - -module.exports = DeepModel.extend({}); \ No newline at end of file diff --git a/src/UI/Settings/DownloadClient/downloadclient.less b/src/UI/Settings/DownloadClient/downloadclient.less deleted file mode 100644 index cc2f8f77e..000000000 --- a/src/UI/Settings/DownloadClient/downloadclient.less +++ /dev/null @@ -1,33 +0,0 @@ -@import "../../Shared/Styles/clickable.less"; - -.download-client-list { - li { - display: inline-block; - vertical-align: top; - } -} - -.download-client-item { - - .clickable; - - width: 290px; - height: 90px; - padding: 10px 15px; - - &.add-card { - .center { - margin-top: -3px; - } - } -} - -.modal-overflow { - overflow-y: visible; -} - -.add-download-client { - li.add-thingy-item { - width: 33%; - } -} diff --git a/src/UI/Settings/General/GeneralSettingsModel.js b/src/UI/Settings/General/GeneralSettingsModel.js deleted file mode 100644 index b8ef7de49..000000000 --- a/src/UI/Settings/General/GeneralSettingsModel.js +++ /dev/null @@ -1,7 +0,0 @@ -var SettingsModelBase = require('../SettingsModelBase'); - -module.exports = SettingsModelBase.extend({ - url : window.NzbDrone.ApiRoot + '/config/host', - successMessage : 'General settings saved', - errorMessage : 'Failed to save general settings' -}); \ No newline at end of file diff --git a/src/UI/Settings/General/GeneralView.js b/src/UI/Settings/General/GeneralView.js deleted file mode 100644 index 81f638f34..000000000 --- a/src/UI/Settings/General/GeneralView.js +++ /dev/null @@ -1,136 +0,0 @@ -var vent = require('../../vent'); -var Marionette = require('marionette'); -var CommandController = require('../../Commands/CommandController'); -var AsModelBoundView = require('../../Mixins/AsModelBoundView'); -var AsValidatedView = require('../../Mixins/AsValidatedView'); - -require('../../Mixins/CopyToClipboard'); - -var view = Marionette.ItemView.extend({ - template : 'Settings/General/GeneralViewTemplate', - - events : { - 'change .x-auth' : '_setAuthOptionsVisibility', - 'change .x-proxy' : '_setProxyOptionsVisibility', - 'change .x-ssl' : '_setSslOptionsVisibility', - 'click .x-reset-api-key' : '_resetApiKey', - 'change .x-update-mechanism' : '_setScriptGroupVisibility' - }, - - ui : { - authToggle : '.x-auth', - authOptions : '.x-auth-options', - sslToggle : '.x-ssl', - sslOptions : '.x-ssl-options', - resetApiKey : '.x-reset-api-key', - copyApiKey : '.x-copy-api-key', - apiKeyInput : '.x-api-key', - updateMechanism : '.x-update-mechanism', - scriptGroup : '.x-script-group', - proxyToggle : '.x-proxy', - proxyOptions : '.x-proxy-settings' - }, - - initialize : function() { - this.listenTo(vent, vent.Events.CommandComplete, this._commandComplete); - }, - - onRender : function() { - if (this.ui.authToggle.val() === 'none') { - this.ui.authOptions.hide(); - } - - if (!this.ui.proxyToggle.prop('checked')) { - this.ui.proxyOptions.hide(); - } - - if (!this.ui.sslToggle.prop('checked')) { - this.ui.sslOptions.hide(); - } - - if (!this._showScriptGroup()) { - this.ui.scriptGroup.hide(); - } - - CommandController.bindToCommand({ - element : this.ui.resetApiKey, - command : { - name : 'resetApiKey' - } - }); - }, - - onShow : function() { - this.ui.copyApiKey.copyToClipboard(this.ui.apiKeyInput); - }, - - _setAuthOptionsVisibility : function() { - - var showAuthOptions = this.ui.authToggle.val() !== 'none'; - - if (showAuthOptions) { - this.ui.authOptions.slideDown(); - } - - else { - this.ui.authOptions.slideUp(); - } - }, - - _setProxyOptionsVisibility : function() { - if (this.ui.proxyToggle.prop('checked')) { - this.ui.proxyOptions.slideDown(); - } - else { - this.ui.proxyOptions.slideUp(); - } - }, - - _setSslOptionsVisibility : function() { - - var showSslOptions = this.ui.sslToggle.prop('checked'); - - if (showSslOptions) { - this.ui.sslOptions.slideDown(); - } - - else { - this.ui.sslOptions.slideUp(); - } - }, - - _resetApiKey : function() { - if (window.confirm('Reset API Key?')) { - CommandController.Execute('resetApiKey', { - name : 'resetApiKey' - }); - } - }, - - _commandComplete : function(options) { - if (options.command.get('name') === 'resetapikey') { - this.model.fetch(); - } - }, - - _setScriptGroupVisibility : function() { - - if (this._showScriptGroup()) { - this.ui.scriptGroup.slideDown(); - } - - else { - this.ui.scriptGroup.slideUp(); - } - }, - - _showScriptGroup : function() { - return this.ui.updateMechanism.val() === 'script'; - } -}); - -AsModelBoundView.call(view); -AsValidatedView.call(view); - -module.exports = view; - diff --git a/src/UI/Settings/General/GeneralViewTemplate.hbs b/src/UI/Settings/General/GeneralViewTemplate.hbs deleted file mode 100644 index 225ef9996..000000000 --- a/src/UI/Settings/General/GeneralViewTemplate.hbs +++ /dev/null @@ -1,382 +0,0 @@ -<div class="form-horizontal"> - <fieldset> - <legend>Start-Up</legend> - - <div class="form-group advanced-setting"> - <label class="col-sm-3 control-label">Bind Address</label> - - <div class="col-sm-1 col-sm-push-4 help-inline"> - <i class="icon-lidarr-form-warning" title="Requires restart to take effect" /> - <i class="icon-lidarr-form-info" title="Valid IP4 address or '*' for all interfaces"/> - </div> - - <div class="col-sm-4 col-sm-pull-1"> - <input type="text" name="bindAddress" class="form-control" /> - </div> - </div> - - <div class="form-group"> - <label class="col-sm-3 control-label">Port Number</label> - - <div class="col-sm-1 col-sm-push-4 help-inline"> - <i class="icon-lidarr-form-warning" title="Requires restart to take effect"/> - </div> - - <div class="col-sm-4 col-sm-pull-1"> - <input type="number" placeholder="8686" name="port" class="form-control"/> - </div> - </div> - - <div class="form-group"> - <label class="col-sm-3 control-label">URL Base</label> - - <div class="col-sm-1 col-sm-push-4 help-inline"> - <i class="icon-lidarr-form-warning" title="Requires restart to take effect"/> - <i class="icon-lidarr-form-info" title="For reverse proxy support, default is empty"/> - </div> - - <div class="col-sm-4 col-sm-pull-1"> - <input type="text" name="urlBase" class="form-control"/> - </div> - </div> - - <div class="form-group advanced-setting"> - <label class="col-sm-3 control-label">Enable SSL</label> - - <div class="col-sm-8"> - <div class="input-group"> - <label class="checkbox toggle well"> - <input type="checkbox" name="enableSsl" class="x-ssl"/> - - <p> - <span>Yes</span> - <span>No</span> - </p> - - <div class="btn btn-primary slide-button"/> - </label> - - <span class="help-inline-checkbox"> - <i class="icon-lidarr-form-warning" title="Requires restart running as administrator to take effect"/> - </span> - </div> - </div> - </div> - - <div class="x-ssl-options"> - <div class="form-group advanced-setting"> - <label class="col-sm-3 control-label">SSL Port Number</label> - - <div class="col-sm-4"> - <input type="number" placeholder="8989" name="sslPort" class="form-control"/> - </div> - </div> - - {{#if_windows}} - <div class="form-group advanced-setting"> - <label class="col-sm-3 control-label">SSL Cert Hash</label> - - <div class="col-sm-4"> - <input type="text" name="sslCertHash" class="form-control"/> - </div> - </div> - {{/if_windows}} - </div> - - <div class="form-group"> - <label class="col-sm-3 control-label">Open browser on start</label> - - <div class="col-sm-8"> - <div class="input-group"> - <label class="checkbox toggle well"> - <input type="checkbox" name="launchBrowser" class="form-control"/> - - <p> - <span>Yes</span> - <span>No</span> - </p> - - <div class="btn btn-primary slide-button"/> - </label> - - <span class="help-inline-checkbox"> - <i class="icon-lidarr-form-info" title="Open a web browser and navigate to Lidarr homepage on app start. Has no effect if installed as a windows service"/> - </span> - </div> - </div> - </div> - </fieldset> - - <fieldset> - <legend>Security</legend> - <div class="form-group"> - <label class="col-sm-3 control-label">Authentication</label> - - <div class="col-sm-1 col-sm-push-4 help-inline"> - <i class="icon-lidarr-form-warning" title="Requires restart to take effect"/> - <i class="icon-lidarr-form-info" title="Require Username and Password to access Lidarr"/> - </div> - - <div class="col-sm-4 col-sm-pull-1"> - <select name="authenticationMethod" class="form-control x-auth"> - <option value="none">None</option> - <option value="basic">Basic (Browser popup)</option> - <option value="forms">Forms (Login page)</option> - </select> - </div> - </div> - - <div class="x-auth-options"> - <div class="form-group"> - <label class="col-sm-3 control-label">Username</label> - - <div class="col-sm-4"> - <input type="text" placeholder="Username" name="username" spellcheck="false" class="form-control"/> - </div> - </div> - - <div class="form-group"> - <label class="col-sm-3 control-label">Password</label> - - <div class="col-sm-4"> - <input type="password" name="password" autocomplete="new-password" class="form-control"/> - </div> - </div> - </div> - - <div class="form-group api-key"> - <label class="col-sm-3 control-label">API Key</label> - - <div class="col-sm-1 col-sm-push-4 help-inline"> - <i class="icon-lidarr-form-warning" title="Requires restart to take effect"/> - </div> - - <div class="col-sm-4 col-sm-pull-1"> - <div class="input-group"> - <input type="text" name="apiKey" readonly="readonly" class="form-control x-api-key"/> - <div class="input-group-btn"> - <button class="btn btn-icon-only x-copy-api-key hidden-xs"><i class="icon-lidarr-copy"></i></button> - <button class="btn btn-danger btn-icon-only x-reset-api-key" title="Reset API Key"><i class="icon-lidarr-refresh"></i></button> - </div> - </div> - </div> - </div> - </fieldset> - - <fieldset> - <legend>Proxy Settings</legend> - - <div class="form-group"> - <label class="col-sm-3 control-label">Use Proxy</label> - - <div class="col-sm-8"> - <div class="input-group"> - <label class="checkbox toggle well"> - <input type="checkbox" name="proxyEnabled" class="form-control x-proxy"/> - - <p> - <span>Yes</span> - <span>No</span> - </p> - - <div class="btn btn-primary slide-button"/> - </label> - </div> - </div> - </div> - - <div class="x-proxy-settings"> - <div class="form-group"> - <label class="col-sm-3 control-label">Proxy Type</label> - - <div class="col-sm-4"> - <select name="proxyType" class="form-control"> - <option value="http" selected="selected">HTTP(S)</option> - <option value="socks4">Socks4</option> - <option value="socks5">Socks5 (This option supports Tor)</option> - </select> - </div> - </div> - - <div class="form-group"> - <label class="col-sm-3 control-label">Hostname</label> - - <div class="col-sm-4"> - <input type="text" placeholder="localhost" name="proxyHostname" class="form-control"/> - </div> - </div> - - <div class="form-group"> - <label class="col-sm-3 control-label">Port</label> - - <div class="col-sm-4"> - <input type="number" placeholder="8080" name="proxyPort" class="form-control"/> - </div> - </div> - - <div class="form-group"> - <label class="col-sm-3 control-label">Username</label> - - <div class="col-sm-1 col-sm-push-4 help-inline"> - <i class="icon-lidarr-form-info" title="You only need to enter a username and password if one is required. Leave them blank otherwise."/> - </div> - - <div class="col-sm-4 col-sm-pull-1"> - <input type="text" name="proxyUsername" class="form-control"/> - </div> - </div> - - <div class="form-group"> - <label class="col-sm-3 control-label">Password</label> - - <div class="col-sm-1 col-sm-push-4 help-inline"> - <i class="icon-lidarr-form-info" title="You only need to enter a username and password if one is required. Leave them blank otherwise."/> - </div> - - <div class="col-sm-4 col-sm-pull-1"> - <input type="password" name="proxyPassword" class="form-control"/> - </div> - </div> - - <div class="form-group"> - <label class="col-sm-3 control-label">Addresses for the proxy to ignore</label> - - <div class="col-sm-1 col-sm-push-4 help-inline"> - <i class="icon-lidarr-form-info" title="Use ',' as a separator, and '*.' as a wildcard for subdomains"/> - </div> - - <div class="col-sm-4 col-sm-pull-1"> - <input type="text" name="proxyBypassFilter" class="form-control"/> - </div> - </div> - - <div class="form-group"> - <label class="col-sm-3 control-label">Bypass Proxy for Local Addresses</label> - - <div class="col-sm-8"> - <div class="input-group"> - <label class="checkbox toggle well"> - <input type="checkbox" name="proxyBypassLocalAddresses" class="form-control"/> - - <p> - <span>Yes</span> - <span>No</span> - </p> - - <div class="btn btn-primary slide-button"/> - </label> - </div> - </div> - </div> - </div> - </fieldset> - - <fieldset> - <legend>Logging</legend> - - <div class="form-group"> - <label class="col-sm-3 control-label">Log Level</label> - - <div class="col-sm-1 col-sm-push-2 help-inline"> - <i class="icon-lidarr-form-warning" title="Trace logging should only be enabled temporarily"/> - </div> - - <div class="col-sm-2 col-sm-pull-1"> - <select name="logLevel" class="form-control"> - <option value="Trace">Trace</option> - <option value="Debug">Debug</option> - <option value="Info">Info</option> - </select> - </div> - </div> - </fieldset> - <fieldset> - <legend>Analytics</legend> - - <div class="form-group"> - <label class="col-sm-3 control-label">Enable</label> - - <div class="col-sm-8"> - <div class="input-group"> - <label class="checkbox toggle well"> - <input type="checkbox" name="analyticsEnabled" class="form-control"/> - <p> - <span>Yes</span> - <span>No</span> - </p> - <div class="btn btn-primary slide-button"/> - </label> - - <span class="help-inline-checkbox"> - <i class="icon-lidarr-form-info" title="Send anonymous usage and error information to Lidarr's servers. This includes information on your browser, which Lidarr WebUI pages you use, error reporting as well as OS and runtime version. We will use this information to prioritize features and bug fixes."/> - <i class="icon-lidarr-form-warning" title="Requires restart to take effect"/> - </span> - </div> - </div> - </div> - </fieldset> - - <fieldset class="advanced-setting"> - <legend>Updates</legend> - - <div class="form-group"> - <label class="col-sm-3 control-label">Branch</label> - - <div class="col-sm-4"> - <input type="text" placeholder="master" name="branch" class="form-control"/> - </div> - </div> - - {{#if_mono}} - <div class="alert alert-warning">Please see: <a href="https://github.com/NzbDrone/NzbDrone/wiki/Updating">the wiki</a> for more information</div> - - <div class="form-group"> - <label class="col-sm-3 control-label">Automatic</label> - - <div class="col-sm-8"> - <div class="input-group"> - <label class="checkbox toggle well"> - <input type="checkbox" name="updateAutomatically"/> - <p> - <span>On</span> - <span>Off</span> - </p> - <div class="btn btn-primary slide-button"/> - </label> - - <span class="help-inline-checkbox"> - <i class="icon-lidarr-form-info" title="Automatically download and install updates. You will still be able to install from System: Updates"/> - </span> - </div> - </div> - </div> - - <div class="form-group"> - <label class="col-sm-3 control-label">Mechanism</label> - - <div class="col-sm-1 col-sm-push-4 help-inline"> - <i class="icon-lidarr-form-info" title="Use built-in updater or external script"/> - </div> - - <div class="col-sm-4 col-sm-pull-1"> - <select name="updateMechanism" class="form-control x-update-mechanism"> - <option value="builtIn">Built-in</option> - <option value="script">Script</option> - </select> - </div> - </div> - - <div class="form-group x-script-group"> - <label class="col-sm-3 control-label">Script Path</label> - - <div class="col-sm-1 col-sm-push-4 help-inline"> - <i class="icon-lidarr-form-info" title="Path to a custom script that take an extracted update package and handle the remainder of the update process"/> - </div> - - <div class="col-sm-4 col-sm-pull-1"> - <input type="text" name="updateScriptPath" class="form-control"/> - </div> - </div> - {{/if_mono}} - </fieldset> -</div> diff --git a/src/UI/Settings/Indexers/Add/IndexerAddCollectionView.js b/src/UI/Settings/Indexers/Add/IndexerAddCollectionView.js deleted file mode 100644 index 5a4102cf2..000000000 --- a/src/UI/Settings/Indexers/Add/IndexerAddCollectionView.js +++ /dev/null @@ -1,9 +0,0 @@ -var ThingyAddCollectionView = require('../../ThingyAddCollectionView'); -var ThingyHeaderGroupView = require('../../ThingyHeaderGroupView'); -var AddItemView = require('./IndexerAddItemView'); - -module.exports = ThingyAddCollectionView.extend({ - itemView : ThingyHeaderGroupView.extend({ itemView : AddItemView }), - itemViewContainer : '.add-indexer .items', - template : 'Settings/Indexers/Add/IndexerAddCollectionViewTemplate' -}); \ No newline at end of file diff --git a/src/UI/Settings/Indexers/Add/IndexerAddCollectionViewTemplate.hbs b/src/UI/Settings/Indexers/Add/IndexerAddCollectionViewTemplate.hbs deleted file mode 100644 index f6e1c9339..000000000 --- a/src/UI/Settings/Indexers/Add/IndexerAddCollectionViewTemplate.hbs +++ /dev/null @@ -1,18 +0,0 @@ -<div class="modal-content"> - <div class="modal-header"> - <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> - <h3>Add Indexer</h3> - </div> - <div class="modal-body"> - <div class="alert alert-info"> - Lidarr supports any indexer that uses the Newznab standard, as well as other indexers listed below.<br/> - For more information on the individual indexers, click on the info buttons. - </div> - <div class="add-indexer add-thingies"> - <ul class="items"></ul> - </div> - </div> - <div class="modal-footer"> - <button class="btn" data-dismiss="modal">Close</button> - </div> -</div> diff --git a/src/UI/Settings/Indexers/Add/IndexerAddItemView.js b/src/UI/Settings/Indexers/Add/IndexerAddItemView.js deleted file mode 100644 index 3a8b0493a..000000000 --- a/src/UI/Settings/Indexers/Add/IndexerAddItemView.js +++ /dev/null @@ -1,52 +0,0 @@ -var _ = require('underscore'); -var $ = require('jquery'); -var AppLayout = require('../../../AppLayout'); -var Marionette = require('marionette'); -var EditView = require('../Edit/IndexerEditView'); - -module.exports = Marionette.ItemView.extend({ - template : 'Settings/Indexers/Add/IndexerAddItemViewTemplate', - tagName : 'li', - className : 'add-thingy-item', - - events : { - 'click .x-preset' : '_addPreset', - 'click' : '_add' - }, - - initialize : function(options) { - this.targetCollection = options.targetCollection; - }, - - _addPreset : function(e) { - var presetName = $(e.target).closest('.x-preset').attr('data-id'); - var presetData = _.where(this.model.get('presets'), { name : presetName })[0]; - - this.model.set(presetData); - - this._openEdit(); - }, - - _add : function(e) { - if ($(e.target).closest('.btn,.btn-group').length !== 0 && $(e.target).closest('.x-custom').length === 0) { - return; - } - - this._openEdit(); - }, - - _openEdit : function() { - this.model.set({ - id : undefined, - enableRss : this.model.get('supportsRss'), - enableSearch : this.model.get('supportsSearch') - }); - - var editView = new EditView({ - model : this.model, - targetCollection : this.targetCollection - }); - - AppLayout.modalRegion.show(editView); - } -}); \ No newline at end of file diff --git a/src/UI/Settings/Indexers/Add/IndexerAddItemViewTemplate.hbs b/src/UI/Settings/Indexers/Add/IndexerAddItemViewTemplate.hbs deleted file mode 100644 index 1cdb3dffc..000000000 --- a/src/UI/Settings/Indexers/Add/IndexerAddItemViewTemplate.hbs +++ /dev/null @@ -1,30 +0,0 @@ -<div class="add-thingy"> - <div> - {{implementationName}} - </div> - <div class="pull-right"> - {{#if_gt presets.length compare=0}} - <button class="btn btn-xs btn-default x-custom"> - Custom - </button> - <div class="btn-group"> - <button class="btn btn-xs btn-default dropdown-toggle" data-toggle="dropdown"> - Presets - <span class="caret"></span> - </button> - <ul class="dropdown-menu"> - {{#each presets}} - <li class="x-preset" data-id="{{name}}"> - <a>{{name}}</a> - </li> - {{/each}} - </ul> - </div> - {{/if_gt}} - {{#if infoLink}} - <a class="btn btn-xs btn-default x-info" href="{{infoLink}}"> - <i class="icon-lidarr-form-info"/> - </a> - {{/if}} - </div> -</div> \ No newline at end of file diff --git a/src/UI/Settings/Indexers/Add/IndexerSchemaModal.js b/src/UI/Settings/Indexers/Add/IndexerSchemaModal.js deleted file mode 100644 index 52b430e89..000000000 --- a/src/UI/Settings/Indexers/Add/IndexerSchemaModal.js +++ /dev/null @@ -1,39 +0,0 @@ -var _ = require('underscore'); -var AppLayout = require('../../../AppLayout'); -var Backbone = require('backbone'); -var SchemaCollection = require('../IndexerCollection'); -var AddCollectionView = require('./IndexerAddCollectionView'); - -module.exports = { - open : function(collection) { - var schemaCollection = new SchemaCollection(); - var originalUrl = schemaCollection.url; - schemaCollection.url = schemaCollection.url + '/schema'; - schemaCollection.fetch(); - schemaCollection.url = originalUrl; - - var groupedSchemaCollection = new Backbone.Collection(); - - schemaCollection.on('sync', function() { - - var groups = schemaCollection.groupBy(function(model, iterator) { - return model.get('protocol'); - }); - var modelCollection = _.map(groups, function(values, key, list) { - return { - "header" : key, - collection : values - }; - }); - - groupedSchemaCollection.reset(modelCollection); - }); - - var view = new AddCollectionView({ - collection : groupedSchemaCollection, - targetCollection : collection - }); - - AppLayout.modalRegion.show(view); - } -}; \ No newline at end of file diff --git a/src/UI/Settings/Indexers/Delete/IndexerDeleteView.js b/src/UI/Settings/Indexers/Delete/IndexerDeleteView.js deleted file mode 100644 index 58e7e3eb5..000000000 --- a/src/UI/Settings/Indexers/Delete/IndexerDeleteView.js +++ /dev/null @@ -1,19 +0,0 @@ -var vent = require('vent'); -var Marionette = require('marionette'); - -module.exports = Marionette.ItemView.extend({ - template : 'Settings/Indexers/Delete/IndexerDeleteViewTemplate', - - events : { - 'click .x-confirm-delete' : '_delete' - }, - - _delete : function() { - this.model.destroy({ - wait : true, - success : function() { - vent.trigger(vent.Commands.CloseModalCommand); - } - }); - } -}); \ No newline at end of file diff --git a/src/UI/Settings/Indexers/Delete/IndexerDeleteViewTemplate.hbs b/src/UI/Settings/Indexers/Delete/IndexerDeleteViewTemplate.hbs deleted file mode 100644 index c5c7ad7db..000000000 --- a/src/UI/Settings/Indexers/Delete/IndexerDeleteViewTemplate.hbs +++ /dev/null @@ -1,13 +0,0 @@ -<div class="modal-content"> - <div class="modal-header"> - <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> - <h3>Delete Indexer</h3> - </div> - <div class="modal-body"> - <p>Are you sure you want to delete '{{name}}'?</p> - </div> - <div class="modal-footer"> - <button class="btn" data-dismiss="modal">Cancel</button> - <button class="btn btn-danger x-confirm-delete">Delete</button> - </div> -</div> \ No newline at end of file diff --git a/src/UI/Settings/Indexers/Edit/IndexerEditView.js b/src/UI/Settings/Indexers/Edit/IndexerEditView.js deleted file mode 100644 index 616c863a7..000000000 --- a/src/UI/Settings/Indexers/Edit/IndexerEditView.js +++ /dev/null @@ -1,122 +0,0 @@ -var _ = require('underscore'); -var $ = require('jquery'); -var vent = require('vent'); -var Marionette = require('marionette'); -var DeleteView = require('../Delete/IndexerDeleteView'); -var AsModelBoundView = require('../../../Mixins/AsModelBoundView'); -var AsValidatedView = require('../../../Mixins/AsValidatedView'); -var AsEditModalView = require('../../../Mixins/AsEditModalView'); -require('../../../Form/FormBuilder'); -require('../../../Mixins/AutoComplete'); -require('bootstrap'); - -var view = Marionette.ItemView.extend({ - template : 'Settings/Indexers/Edit/IndexerEditViewTemplate', - - events : { - 'click .x-back' : '_back', - 'click .x-captcha-refresh' : '_onRefreshCaptcha' - }, - - _deleteView : DeleteView, - - initialize : function(options) { - this.targetCollection = options.targetCollection; - }, - - _onAfterSave : function() { - this.targetCollection.add(this.model, { merge : true }); - vent.trigger(vent.Commands.CloseModalCommand); - }, - - _onAfterSaveAndAdd : function() { - this.targetCollection.add(this.model, { merge : true }); - - require('../Add/IndexerSchemaModal').open(this.targetCollection); - }, - - _back : function() { - if (this.model.isNew()) { - this.model.destroy(); - } - - require('../Add/IndexerSchemaModal').open(this.targetCollection); - }, - - _onRefreshCaptcha : function(event) { - var self = this; - - var target = $(event.target).parents('.input-group'); - - this.ui.indicator.show(); - - this.model.requestAction("checkCaptcha") - .then(function(result) { - if (!result.captchaRequest) { - self.model.setFieldValue('CaptchaToken', ''); - - return result; - } - - return self._showCaptcha(target, result.captchaRequest); - }) - .always(function() { - self.ui.indicator.hide(); - }); - }, - - _showCaptcha : function(target, captchaRequest) { - var self = this; - - var widget = $('<div class="g-recaptcha"></div>').insertAfter(target); - - return this._loadRecaptchaWidget(widget[0], captchaRequest.siteKey, captchaRequest.secretToken) - .then(function(captchaResponse) { - target.parents('.form-group').removeAllErrors(); - widget.remove(); - - var queryParams = { - responseUrl : captchaRequest.responseUrl, - ray : captchaRequest.ray, - captchaResponse: captchaResponse - }; - - return self.model.requestAction("getCaptchaCookie", queryParams); - }) - .then(function(response) { - self.model.setFieldValue('CaptchaToken', response.captchaToken); - }); - }, - - _loadRecaptchaWidget : function(widget, sitekey, stoken) { - var promise = $.Deferred(); - - var renderWidget = function() { - window.grecaptcha.render(widget, { - 'sitekey' : sitekey, - 'stoken' : stoken, - 'callback' : promise.resolve - }); - }; - - if (window.grecaptcha) { - renderWidget(); - } else { - window.grecaptchaLoadCallback = function() { - delete window.grecaptchaLoadCallback; - renderWidget(); - }; - - $.getScript('https://www.google.com/recaptcha/api.js?onload=grecaptchaLoadCallback&render=explicit') - .fail(function() { promise.reject(); }); - } - - return promise; - } -}); - -AsModelBoundView.call(view); -AsValidatedView.call(view); -AsEditModalView.call(view); - -module.exports = view; \ No newline at end of file diff --git a/src/UI/Settings/Indexers/Edit/IndexerEditViewTemplate.hbs b/src/UI/Settings/Indexers/Edit/IndexerEditViewTemplate.hbs deleted file mode 100644 index 81ac7c8e3..000000000 --- a/src/UI/Settings/Indexers/Edit/IndexerEditViewTemplate.hbs +++ /dev/null @@ -1,92 +0,0 @@ -<div class="modal-content"> - <div class="modal-header"> - <button type="button" class="close" aria-hidden="true" data-dismiss="modal">×</button> - {{#if id}} - <h3>Edit - {{implementationName}}</h3> - {{else}} - <h3>Add - {{implementationName}}</h3> - {{/if}} - </div> - <div class="modal-body indexer-modal"> - <div class="form-horizontal"> - <div class="form-group"> - <label class="col-sm-3 control-label">Name</label> - - <div class="col-sm-5"> - <input type="text" name="name" class="form-control"/> - </div> - </div> - - <div class="form-group"> - <label class="col-sm-3 control-label">Enable RSS Sync</label> - - <div class="col-sm-5"> - <div class="input-group"> - <label class="checkbox toggle well"> - <input type="checkbox" name="enableRss" {{#unless supportsRss}}disabled="disabled"{{/unless}}/> - <p> - <span>Yes</span> - <span>No</span> - </p> - - <div class="btn btn-primary slide-button"/> - </label> - {{#unless supportsRss}} - <span class="help-inline-checkbox"> - <i class="icon-lidarr-form-warning" title="" data-original-title="RSS is not supported with this indexer"></i> - </span> - {{/unless}} - </div> - </div> - </div> - - <div class="form-group"> - <label class="col-sm-3 control-label">Enable Search</label> - - <div class="col-sm-5"> - <div class="input-group"> - <label class="checkbox toggle well"> - - <input type="checkbox" name="enableSearch" {{#unless supportsSearch}}disabled="disabled"{{/unless}}/> - <p> - <span>Yes</span> - <span>No</span> - </p> - - <div class="btn btn-primary slide-button"/> - </label> - {{#unless supportsSearch}} - <span class="help-inline-checkbox"> - <i class="icon-lidarr-form-warning" title="" data-original-title="Search is not supported with this indexer"></i> - </span> - {{/unless}} - </div> - </div> - </div> - - {{formBuilder}} - </div> - </div> - <div class="modal-footer"> - {{#if id}} - <button class="btn btn-danger pull-left x-delete">Delete</button> - {{else}} - <button class="btn pull-left x-back">Back</button> - {{/if}} - <span class="indicator x-indicator"><i class="icon-lidarr-spinner fa-spin"></i></span> - <button class="btn x-test">test <i class="x-test-icon icon-lidarr-test"/></button> - <button class="btn" data-dismiss="modal">Cancel</button> - - <div class="btn-group"> - <button class="btn btn-primary x-save">Save</button> - <button class="btn btn-icon-only btn-primary dropdown-toggle" data-toggle="dropdown"> - <span class="caret"></span> - </button> - <ul class="dropdown-menu"> - <li class="save-and-add x-save-and-add"> - save and add - </li> - </ul> - </div> - </div> -</div> diff --git a/src/UI/Settings/Indexers/IndexerCollection.js b/src/UI/Settings/Indexers/IndexerCollection.js deleted file mode 100644 index 3eb447392..000000000 --- a/src/UI/Settings/Indexers/IndexerCollection.js +++ /dev/null @@ -1,25 +0,0 @@ -var Backbone = require('backbone'); -var IndexerModel = require('./IndexerModel'); - -module.exports = Backbone.Collection.extend({ - model : IndexerModel, - url : window.NzbDrone.ApiRoot + '/indexer', - - comparator : function(left, right, collection) { - var result = 0; - - if (left.get('protocol')) { - result = -left.get('protocol').localeCompare(right.get('protocol')); - } - - if (result === 0 && left.get('name')) { - result = left.get('name').localeCompare(right.get('name')); - } - - if (result === 0) { - result = left.get('implementation').localeCompare(right.get('implementation')); - } - - return result; - } -}); \ No newline at end of file diff --git a/src/UI/Settings/Indexers/IndexerCollectionView.js b/src/UI/Settings/Indexers/IndexerCollectionView.js deleted file mode 100644 index df6ae9596..000000000 --- a/src/UI/Settings/Indexers/IndexerCollectionView.js +++ /dev/null @@ -1,25 +0,0 @@ -var Marionette = require('marionette'); -var ItemView = require('./IndexerItemView'); -var SchemaModal = require('./Add/IndexerSchemaModal'); - -module.exports = Marionette.CompositeView.extend({ - itemView : ItemView, - itemViewContainer : '.indexer-list', - template : 'Settings/Indexers/IndexerCollectionViewTemplate', - - ui : { - 'addCard' : '.x-add-card' - }, - - events : { - 'click .x-add-card' : '_openSchemaModal' - }, - - appendHtml : function(collectionView, itemView, index) { - collectionView.ui.addCard.parent('li').before(itemView.el); - }, - - _openSchemaModal : function() { - SchemaModal.open(this.collection); - } -}); \ No newline at end of file diff --git a/src/UI/Settings/Indexers/IndexerCollectionViewTemplate.hbs b/src/UI/Settings/Indexers/IndexerCollectionViewTemplate.hbs deleted file mode 100644 index 571a31b3a..000000000 --- a/src/UI/Settings/Indexers/IndexerCollectionViewTemplate.hbs +++ /dev/null @@ -1,16 +0,0 @@ -<fieldset> - <legend>Indexers</legend> - <div class="row"> - <div class="col-md-12"> - <ul class="indexer-list thingies"> - <li> - <div class="indexer-item thingy add-card x-add-card"> - <span class="center well"> - <i class="icon-lidarr-add"/> - </span> - </div> - </li> - </ul> - </div> - </div> -</fieldset> \ No newline at end of file diff --git a/src/UI/Settings/Indexers/IndexerItemView.js b/src/UI/Settings/Indexers/IndexerItemView.js deleted file mode 100644 index 29cf3d7c5..000000000 --- a/src/UI/Settings/Indexers/IndexerItemView.js +++ /dev/null @@ -1,24 +0,0 @@ -var AppLayout = require('../../AppLayout'); -var Marionette = require('marionette'); -var EditView = require('./Edit/IndexerEditView'); - -module.exports = Marionette.ItemView.extend({ - template : 'Settings/Indexers/IndexerItemViewTemplate', - tagName : 'li', - - events : { - 'click' : '_edit' - }, - - initialize : function() { - this.listenTo(this.model, 'sync', this.render); - }, - - _edit : function() { - var view = new EditView({ - model : this.model, - targetCollection : this.model.collection - }); - AppLayout.modalRegion.show(view); - } -}); \ No newline at end of file diff --git a/src/UI/Settings/Indexers/IndexerItemViewTemplate.hbs b/src/UI/Settings/Indexers/IndexerItemViewTemplate.hbs deleted file mode 100644 index abef39886..000000000 --- a/src/UI/Settings/Indexers/IndexerItemViewTemplate.hbs +++ /dev/null @@ -1,27 +0,0 @@ -<div class="indexer-item thingy"> - <div> - <h3>{{name}}</h3> - </div> - - <div class="settings"> - {{#if supportsRss}} - {{#if enableRss}} - <span class="label label-success">RSS</span> - {{else}} - <span class="label label-default">RSS</span> - {{/if}} - {{else}} - <span class="label label-default label-disabled">RSS</span> - {{/if}} - - {{#if supportsSearch}} - {{#if enableSearch}} - <span class="label label-success">Search</span> - {{else}} - <span class="label label-default">Search</span> - {{/if}} - {{else}} - <span class="label label-default label-disabled">Search</span> - {{/if}} - </div> -</div> diff --git a/src/UI/Settings/Indexers/IndexerLayout.js b/src/UI/Settings/Indexers/IndexerLayout.js deleted file mode 100644 index f6cbd1ab6..000000000 --- a/src/UI/Settings/Indexers/IndexerLayout.js +++ /dev/null @@ -1,30 +0,0 @@ -var Marionette = require('marionette'); -var IndexerCollection = require('./IndexerCollection'); -var CollectionView = require('./IndexerCollectionView'); -var OptionsView = require('./Options/IndexerOptionsView'); -var RestrictionCollection = require('./Restriction/RestrictionCollection'); -var RestrictionCollectionView = require('./Restriction/RestrictionCollectionView'); - -module.exports = Marionette.Layout.extend({ - template : 'Settings/Indexers/IndexerLayoutTemplate', - - regions : { - indexers : '#x-indexers-region', - indexerOptions : '#x-indexer-options-region', - restriction : '#x-restriction-region' - }, - - initialize : function() { - this.indexersCollection = new IndexerCollection(); - this.indexersCollection.fetch(); - - this.restrictionCollection = new RestrictionCollection(); - this.restrictionCollection.fetch(); - }, - - onShow : function() { - this.indexers.show(new CollectionView({ collection : this.indexersCollection })); - this.indexerOptions.show(new OptionsView({ model : this.model })); - this.restriction.show(new RestrictionCollectionView({ collection : this.restrictionCollection })); - } -}); \ No newline at end of file diff --git a/src/UI/Settings/Indexers/IndexerLayoutTemplate.hbs b/src/UI/Settings/Indexers/IndexerLayoutTemplate.hbs deleted file mode 100644 index b82535642..000000000 --- a/src/UI/Settings/Indexers/IndexerLayoutTemplate.hbs +++ /dev/null @@ -1,5 +0,0 @@ -<div id="x-indexers-region"></div> -<div class="form-horizontal"> - <div id="x-indexer-options-region"></div> - <div id="x-restriction-region"></div> -</div> diff --git a/src/UI/Settings/Indexers/IndexerModel.js b/src/UI/Settings/Indexers/IndexerModel.js deleted file mode 100644 index 288e45362..000000000 --- a/src/UI/Settings/Indexers/IndexerModel.js +++ /dev/null @@ -1,3 +0,0 @@ -var ProviderSettingsModelBase = require('../ProviderSettingsModelBase'); - -module.exports = ProviderSettingsModelBase.extend({}); \ No newline at end of file diff --git a/src/UI/Settings/Indexers/IndexerSettingsModel.js b/src/UI/Settings/Indexers/IndexerSettingsModel.js deleted file mode 100644 index 14b9db863..000000000 --- a/src/UI/Settings/Indexers/IndexerSettingsModel.js +++ /dev/null @@ -1,7 +0,0 @@ -var SettingsModelBase = require('../SettingsModelBase'); - -module.exports = SettingsModelBase.extend({ - url : window.NzbDrone.ApiRoot + '/config/indexer', - successMessage : 'Indexer settings saved', - errorMessage : 'Failed to save indexer settings' -}); \ No newline at end of file diff --git a/src/UI/Settings/Indexers/Options/IndexerOptionsView.js b/src/UI/Settings/Indexers/Options/IndexerOptionsView.js deleted file mode 100644 index 5d4386faa..000000000 --- a/src/UI/Settings/Indexers/Options/IndexerOptionsView.js +++ /dev/null @@ -1,12 +0,0 @@ -var Marionette = require('marionette'); -var AsModelBoundView = require('../../../Mixins/AsModelBoundView'); -var AsValidatedView = require('../../../Mixins/AsValidatedView'); - -var view = Marionette.ItemView.extend({ - template : 'Settings/Indexers/Options/IndexerOptionsViewTemplate' -}); - -AsModelBoundView.call(view); -AsValidatedView.call(view); - -module.exports = view; \ No newline at end of file diff --git a/src/UI/Settings/Indexers/Options/IndexerOptionsViewTemplate.hbs b/src/UI/Settings/Indexers/Options/IndexerOptionsViewTemplate.hbs deleted file mode 100644 index d4c3940aa..000000000 --- a/src/UI/Settings/Indexers/Options/IndexerOptionsViewTemplate.hbs +++ /dev/null @@ -1,40 +0,0 @@ -<fieldset> - <legend>Options</legend> - - <div class="form-group"> - <label class="col-sm-3 control-label">Minimum Age</label> - - <div class="col-sm-1 col-sm-push-2 help-inline"> - <i class="icon-lidarr-form-info" title="Usenet only: Minimum age in minutes of NZBs before they are grabbed. Use this to give new releases time to propagate to your usenet provider."/> - </div> - - <div class="col-sm-2 col-sm-pull-1"> - <input type="number" min="0" name="minimumAge" class="form-control"/> - </div> - </div> - - <div class="form-group"> - <label class="col-sm-3 control-label">Retention</label> - - <div class="col-sm-1 col-sm-push-2 help-inline"> - <i class="icon-lidarr-form-info" title="Usenet only: Set to zero to set to unlimited"/> - </div> - - <div class="col-sm-2 col-sm-pull-1"> - <input type="number" min="0" name="retention" class="form-control"/> - </div> - </div> - - <div class="form-group advanced-setting"> - <label class="col-sm-3 control-label">RSS Sync Interval</label> - - <div class="col-sm-1 col-sm-push-2 help-inline"> - <i class="icon-lidarr-form-warning" title="This will apply to all indexers, please follow the rules set forth by them"/> - <i class="icon-lidarr-form-info" title="Interval in minutes. Set to zero to disable (this will stop all automatic release grabbing)"/> - </div> - - <div class="col-sm-2 col-sm-pull-1"> - <input type="number" name="rssSyncInterval" class="form-control" min="0" max="120"/> - </div> - </div> -</fieldset> diff --git a/src/UI/Settings/Indexers/Restriction/RestrictionCollection.js b/src/UI/Settings/Indexers/Restriction/RestrictionCollection.js deleted file mode 100644 index 369250343..000000000 --- a/src/UI/Settings/Indexers/Restriction/RestrictionCollection.js +++ /dev/null @@ -1,7 +0,0 @@ -var Backbone = require('backbone'); -var RestrictionModel = require('./RestrictionModel'); - -module.exports = Backbone.Collection.extend({ - model : RestrictionModel, - url : window.NzbDrone.ApiRoot + '/Restriction' -}); \ No newline at end of file diff --git a/src/UI/Settings/Indexers/Restriction/RestrictionCollectionView.js b/src/UI/Settings/Indexers/Restriction/RestrictionCollectionView.js deleted file mode 100644 index 58b3a6bfa..000000000 --- a/src/UI/Settings/Indexers/Restriction/RestrictionCollectionView.js +++ /dev/null @@ -1,26 +0,0 @@ -var AppLayout = require('../../../AppLayout'); -var Marionette = require('marionette'); -var RestrictionItemView = require('./RestrictionItemView'); -var EditView = require('./RestrictionEditView'); -require('../../../Tags/TagHelpers'); -require('bootstrap'); - -module.exports = Marionette.CompositeView.extend({ - template : 'Settings/Indexers/Restriction/RestrictionCollectionViewTemplate', - itemViewContainer : '.x-rows', - itemView : RestrictionItemView, - - events : { - 'click .x-add' : '_addMapping' - }, - - _addMapping : function() { - var model = this.collection.create({ tags : [] }); - var view = new EditView({ - model : model, - targetCollection : this.collection - }); - - AppLayout.modalRegion.show(view); - } -}); \ No newline at end of file diff --git a/src/UI/Settings/Indexers/Restriction/RestrictionCollectionViewTemplate.hbs b/src/UI/Settings/Indexers/Restriction/RestrictionCollectionViewTemplate.hbs deleted file mode 100644 index fa7f55dee..000000000 --- a/src/UI/Settings/Indexers/Restriction/RestrictionCollectionViewTemplate.hbs +++ /dev/null @@ -1,24 +0,0 @@ -<fieldset class="advanced-setting"> - <legend>Restrictions</legend> - - <div class="col-md-12"> - <div class="rule-setting-list"> - <div class="rule-setting-header x-header hidden-xs"> - <div class="row"> - <span class="col-sm-4">Must Contain</span> - <span class="col-sm-4">Must Not Contain</span> - <span class="col-sm-3">Tags</span> - </div> - </div> - <div class="rows x-rows"> - </div> - <div class="rule-setting-footer"> - <div class="pull-right"> - <span class="add-rule-setting-mapping"> - <i class="icon-lidarr-add x-add" title="Add new restriction" /> - </span> - </div> - </div> - </div> - </div> -</fieldset> \ No newline at end of file diff --git a/src/UI/Settings/Indexers/Restriction/RestrictionDeleteView.js b/src/UI/Settings/Indexers/Restriction/RestrictionDeleteView.js deleted file mode 100644 index d2166c5ed..000000000 --- a/src/UI/Settings/Indexers/Restriction/RestrictionDeleteView.js +++ /dev/null @@ -1,19 +0,0 @@ -var vent = require('vent'); -var Marionette = require('marionette'); - -module.exports = Marionette.ItemView.extend({ - template : 'Settings/Indexers/Restriction/RestrictionDeleteViewTemplate', - - events : { - 'click .x-confirm-delete' : '_delete' - }, - - _delete : function() { - this.model.destroy({ - wait : true, - success : function() { - vent.trigger(vent.Commands.CloseModalCommand); - } - }); - } -}); \ No newline at end of file diff --git a/src/UI/Settings/Indexers/Restriction/RestrictionDeleteViewTemplate.hbs b/src/UI/Settings/Indexers/Restriction/RestrictionDeleteViewTemplate.hbs deleted file mode 100644 index 215631e5b..000000000 --- a/src/UI/Settings/Indexers/Restriction/RestrictionDeleteViewTemplate.hbs +++ /dev/null @@ -1,13 +0,0 @@ -<div class="modal-content"> - <div class="modal-header"> - <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> - <h3>Delete Restriction</h3> - </div> - <div class="modal-body"> - <p>Are you sure you want to delete this restriction?</p> - </div> - <div class="modal-footer"> - <button class="btn" data-dismiss="modal">Cancel</button> - <button class="btn btn-danger x-confirm-delete">Delete</button> - </div> -</div> diff --git a/src/UI/Settings/Indexers/Restriction/RestrictionEditView.js b/src/UI/Settings/Indexers/Restriction/RestrictionEditView.js deleted file mode 100644 index e8540d1a5..000000000 --- a/src/UI/Settings/Indexers/Restriction/RestrictionEditView.js +++ /dev/null @@ -1,55 +0,0 @@ -var _ = require('underscore'); -var vent = require('vent'); -var AppLayout = require('../../../AppLayout'); -var Marionette = require('marionette'); -var DeleteView = require('./RestrictionDeleteView'); -var CommandController = require('../../../Commands/CommandController'); -var AsModelBoundView = require('../../../Mixins/AsModelBoundView'); -var AsValidatedView = require('../../../Mixins/AsValidatedView'); -var AsEditModalView = require('../../../Mixins/AsEditModalView'); -require('../../../Mixins/TagInput'); -require('bootstrap'); -require('bootstrap.tagsinput'); - -var view = Marionette.ItemView.extend({ - template : 'Settings/Indexers/Restriction/RestrictionEditViewTemplate', - - ui : { - required : '.x-required', - ignored : '.x-ignored', - tags : '.x-tags' - }, - - _deleteView : DeleteView, - - initialize : function(options) { - this.targetCollection = options.targetCollection; - }, - - onRender : function() { - this.ui.required.tagsinput({ - trimValue : true, - tagClass : 'label label-success' - }); - - this.ui.ignored.tagsinput({ - trimValue : true, - tagClass : 'label label-danger' - }); - - this.ui.tags.tagInput({ - model : this.model, - property : 'tags' - }); - }, - - _onAfterSave : function() { - this.targetCollection.add(this.model, { merge : true }); - vent.trigger(vent.Commands.CloseModalCommand); - } -}); - -AsModelBoundView.call(view); -AsValidatedView.call(view); -AsEditModalView.call(view); -module.exports = view; \ No newline at end of file diff --git a/src/UI/Settings/Indexers/Restriction/RestrictionEditViewTemplate.hbs b/src/UI/Settings/Indexers/Restriction/RestrictionEditViewTemplate.hbs deleted file mode 100644 index 0133bcf0d..000000000 --- a/src/UI/Settings/Indexers/Restriction/RestrictionEditViewTemplate.hbs +++ /dev/null @@ -1,60 +0,0 @@ -<div class="modal-content"> - <div class="modal-header"> - <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> - {{#if id}} - <h3>Edit Restriction</h3> - {{else}} - <h3>Add Restriction</h3> - {{/if}} - </div> - <div class="modal-body remotepath-mapping-modal"> - <div class="form-horizontal"> - <div class="form-group"> - <label class="col-sm-3 control-label">Must contain</label> - - <div class="col-sm-1 col-sm-push-5 help-inline"> - <i class="icon-lidarr-form-info" title="The release must contain at least one of these terms (case insensitive)" /> - </div> - - <div class="col-sm-5 col-sm-pull-1"> - <input type="text" name="required" class="form-control x-required"/> - </div> - </div> - - <div class="form-group"> - <label class="col-sm-3 control-label">Must not contain</label> - - <div class="col-sm-1 col-sm-push-5 help-inline"> - <i class="icon-lidarr-form-info" title="The release will be rejected if it contains one or more of terms (case insensitive)" /> - </div> - - <div class="col-sm-5 col-sm-pull-1"> - <input type="text" name="ignored" class="form-control x-ignored"/> - </div> - </div> - - <div class="form-group"> - <label class="col-sm-3 control-label">Tags</label> - - <div class="col-sm-1 col-sm-push-5 help-inline"> - <i class="icon-lidarr-form-info" title="Restrictions will apply to series with one or more matching tags. Leave blank to apply to all series" /> - </div> - - <div class="col-sm-5 col-sm-pull-1"> - <input type="text" class="form-control x-tags"> - </div> - </div> - </div> - </div> - <div class="modal-footer"> - {{#if id}} - <button class="btn btn-danger pull-left x-delete">Delete</button> - {{/if}} - - <button class="btn" data-dismiss="modal">Cancel</button> - - <div class="btn-group"> - <button class="btn btn-primary x-save">Save</button> - </div> - </div> -</div> diff --git a/src/UI/Settings/Indexers/Restriction/RestrictionItemView.js b/src/UI/Settings/Indexers/Restriction/RestrictionItemView.js deleted file mode 100644 index 729d8ef7d..000000000 --- a/src/UI/Settings/Indexers/Restriction/RestrictionItemView.js +++ /dev/null @@ -1,28 +0,0 @@ -var AppLayout = require('../../../AppLayout'); -var Marionette = require('marionette'); -var EditView = require('./RestrictionEditView'); - -module.exports = Marionette.ItemView.extend({ - template : 'Settings/Indexers/Restriction/RestrictionItemViewTemplate', - className : 'row', - - ui : { - tags : '.x-tags' - }, - - events : { - 'click .x-edit' : '_edit' - }, - - initialize : function() { - this.listenTo(this.model, 'sync', this.render); - }, - - _edit : function() { - var view = new EditView({ - model : this.model, - targetCollection : this.model.collection - }); - AppLayout.modalRegion.show(view); - } -}); \ No newline at end of file diff --git a/src/UI/Settings/Indexers/Restriction/RestrictionItemViewTemplate.hbs b/src/UI/Settings/Indexers/Restriction/RestrictionItemViewTemplate.hbs deleted file mode 100644 index d7e583d38..000000000 --- a/src/UI/Settings/Indexers/Restriction/RestrictionItemViewTemplate.hbs +++ /dev/null @@ -1,12 +0,0 @@ - <div class="col-sm-4"> - {{genericTagDisplay required 'label label-success'}} - </div> - <div class="col-sm-4"> - {{genericTagDisplay ignored 'label label-danger'}} - </div> - <div class="col-sm-3"> - {{tagDisplay tags}} - </div> - <div class="col-sm-1"> - <div class="pull-right"><i class="icon-lidarr-edit x-edit" title="" data-original-title="Edit"></i></div> - </div> \ No newline at end of file diff --git a/src/UI/Settings/Indexers/Restriction/RestrictionModel.js b/src/UI/Settings/Indexers/Restriction/RestrictionModel.js deleted file mode 100644 index e8ea08465..000000000 --- a/src/UI/Settings/Indexers/Restriction/RestrictionModel.js +++ /dev/null @@ -1,4 +0,0 @@ -var $ = require('jquery'); -var DeepModel = require('backbone.deepmodel'); - -module.exports = DeepModel.extend({}); \ No newline at end of file diff --git a/src/UI/Settings/Indexers/indexers.less b/src/UI/Settings/Indexers/indexers.less deleted file mode 100644 index 3fed3ef5f..000000000 --- a/src/UI/Settings/Indexers/indexers.less +++ /dev/null @@ -1,33 +0,0 @@ -@import "../../Shared/Styles/clickable.less"; - -.indexer-list { - li { - display: inline-block; - vertical-align: top; - } -} - -.indexer-item { - - .clickable; - - width: 290px; - height: 90px; - padding: 10px 15px; - - &.add-card { - .center { - margin-top: -3px; - } - } -} - -.modal-overflow { - overflow-y: visible; -} - -.add-indexer { - li.add-thingy-item { - width: 33%; - } -} \ No newline at end of file diff --git a/src/UI/Settings/MediaManagement/FileManagement/FileManagementView.js b/src/UI/Settings/MediaManagement/FileManagement/FileManagementView.js deleted file mode 100644 index 49c2cad37..000000000 --- a/src/UI/Settings/MediaManagement/FileManagement/FileManagementView.js +++ /dev/null @@ -1,23 +0,0 @@ -var vent = require('vent'); -var Marionette = require('marionette'); -var AsModelBoundView = require('../../../Mixins/AsModelBoundView'); -var AsValidatedView = require('../../../Mixins/AsValidatedView'); -require('../../../Mixins/DirectoryAutoComplete'); -require('../../../Mixins/FileBrowser'); - -var view = Marionette.ItemView.extend({ - template : 'Settings/MediaManagement/FileManagement/FileManagementViewTemplate', - - ui : { - recyclingBin : '.x-path' - }, - - onShow : function() { - this.ui.recyclingBin.fileBrowser(); - } -}); - -AsModelBoundView.call(view); -AsValidatedView.call(view); - -module.exports = view; \ No newline at end of file diff --git a/src/UI/Settings/MediaManagement/FileManagement/FileManagementViewTemplate.hbs b/src/UI/Settings/MediaManagement/FileManagement/FileManagementViewTemplate.hbs deleted file mode 100644 index a20331425..000000000 --- a/src/UI/Settings/MediaManagement/FileManagement/FileManagementViewTemplate.hbs +++ /dev/null @@ -1,97 +0,0 @@ -<fieldset> - <legend>File Management</legend> - - <div class="form-group"> - <label class="col-sm-3 control-label">Ignore Deleted Tracks</label> - - <div class="col-sm-9"> - <div class="input-group"> - <label class="checkbox toggle well"> - <input type="checkbox" name="autoUnmonitorPreviouslyDownloadedTracks"/> - <p> - <span>Yes</span> - <span>No</span> - </p> - - <div class="btn btn-primary slide-button"/> - </label> - - <span class="help-inline-checkbox"> - <i class="icon-lidarr-form-info" title="Tracks deleted from disk are automatically unmonitored in Lidarr"/> - </span> - </div> - </div> - </div> - - <div class="form-group advanced-setting"> - <label class="col-sm-3 control-label">Download Propers</label> - - <div class="col-sm-9"> - <div class="input-group"> - <label class="checkbox toggle well"> - <input type="checkbox" name="autoDownloadPropers"/> - <p> - <span>Yes</span> - <span>No</span> - </p> - - <div class="btn btn-primary slide-button"/> - </label> - - <span class="help-inline-checkbox"> - <i class="icon-lidarr-form-info" title="Should Lidarr automatically upgrade to propers when available?"/> - </span> - </div> - </div> - </div> - - <div class="form-group advanced-setting"> - <label class="col-sm-3 control-label">Analyse Audio Files</label> - - <div class="col-sm-9"> - <div class="input-group"> - <label class="checkbox toggle well"> - <input type="checkbox" name="enableMediaInfo"/> - <p> - <span>Yes</span> - <span>No</span> - </p> - - <div class="btn btn-primary slide-button"/> - </label> - - <span class="help-inline-checkbox"> - <i class="icon-lidarr-form-info" title="Extract audio information such as bitrate, runtime and codec information from files. This requires Lidarr to read parts of the file which may cause high disk or network activity during scans."/> - </span> - </div> - </div> - </div> - - <div class="form-group advanced-setting"> - <label class="col-sm-3 control-label">Change File Date</label> - - <div class="col-sm-1 col-sm-push-2 help-inline"> - <i class="icon-lidarr-form-info" title="Change file date on import/rescan"/> - </div> - - <div class="col-sm-3 col-sm-pull-1"> - <select class="form-control" name="fileDate"> - <option value="none">None</option> - <option value="albumReleaseDate">Album Release Date</option> - </select> - </div> - </div> - - <div class="form-group"> - <label class="col-sm-3 control-label">Recycling Bin</label> - - <div class="col-sm-1 col-sm-push-8 help-inline"> - <i class="icon-lidarr-form-info" title="Track files will go here when deleted instead of being permanently deleted"/> - </div> - - <div class="col-sm-8 col-sm-pull-1"> - <input type="text" name="recycleBin" class="form-control x-path"/> - </div> - - </div> -</fieldset> diff --git a/src/UI/Settings/MediaManagement/MediaManagementLayout.js b/src/UI/Settings/MediaManagement/MediaManagementLayout.js deleted file mode 100644 index da6ea2954..000000000 --- a/src/UI/Settings/MediaManagement/MediaManagementLayout.js +++ /dev/null @@ -1,28 +0,0 @@ -var Marionette = require('marionette'); -var NamingView = require('./Naming/NamingView'); -var SortingView = require('./Sorting/SortingView'); -var FileManagementView = require('./FileManagement/FileManagementView'); -var PermissionsView = require('./Permissions/PermissionsView'); - -module.exports = Marionette.Layout.extend({ - template : 'Settings/MediaManagement/MediaManagementLayoutTemplate', - - regions : { - episodeNaming : '#episode-naming', - sorting : '#sorting', - fileManagement : '#file-management', - permissions : '#permissions' - }, - - initialize : function(options) { - this.settings = options.settings; - this.namingSettings = options.namingSettings; - }, - - onShow : function() { - this.episodeNaming.show(new NamingView({ model : this.namingSettings })); - this.sorting.show(new SortingView({ model : this.settings })); - this.fileManagement.show(new FileManagementView({ model : this.settings })); - this.permissions.show(new PermissionsView({ model : this.settings })); - } -}); \ No newline at end of file diff --git a/src/UI/Settings/MediaManagement/MediaManagementLayoutTemplate.hbs b/src/UI/Settings/MediaManagement/MediaManagementLayoutTemplate.hbs deleted file mode 100644 index 44fb14ac3..000000000 --- a/src/UI/Settings/MediaManagement/MediaManagementLayoutTemplate.hbs +++ /dev/null @@ -1,6 +0,0 @@ -<div class="form-horizontal"> - <div id="episode-naming"></div> - <div id="sorting"></div> - <div id="file-management"></div> - {{#if_mono}}<div id="permissions"></div>{{/if_mono}} -</div> \ No newline at end of file diff --git a/src/UI/Settings/MediaManagement/MediaManagementSettingsModel.js b/src/UI/Settings/MediaManagement/MediaManagementSettingsModel.js deleted file mode 100644 index f80d74800..000000000 --- a/src/UI/Settings/MediaManagement/MediaManagementSettingsModel.js +++ /dev/null @@ -1,7 +0,0 @@ -var SettingsModelBase = require('../SettingsModelBase'); - -module.exports = SettingsModelBase.extend({ - url : window.NzbDrone.ApiRoot + '/config/mediamanagement', - successMessage : 'Media management settings saved', - errorMessage : 'Failed to save media management settings' -}); \ No newline at end of file diff --git a/src/UI/Settings/MediaManagement/Naming/Basic/BasicNamingModel.js b/src/UI/Settings/MediaManagement/Naming/Basic/BasicNamingModel.js deleted file mode 100644 index 3986a5948..000000000 --- a/src/UI/Settings/MediaManagement/Naming/Basic/BasicNamingModel.js +++ /dev/null @@ -1,3 +0,0 @@ -var Backbone = require('backbone'); - -module.exports = Backbone.Model.extend({}); \ No newline at end of file diff --git a/src/UI/Settings/MediaManagement/Naming/Basic/BasicNamingView.js b/src/UI/Settings/MediaManagement/Naming/Basic/BasicNamingView.js deleted file mode 100644 index 279febf54..000000000 --- a/src/UI/Settings/MediaManagement/Naming/Basic/BasicNamingView.js +++ /dev/null @@ -1,112 +0,0 @@ -var _ = require('underscore'); -var Marionette = require('marionette'); -var Config = require('../../../../Config'); -var NamingSampleModel = require('../NamingSampleModel'); -var BasicNamingModel = require('./BasicNamingModel'); -var AsModelBoundView = require('../../../../Mixins/AsModelBoundView'); - -var view = Marionette.ItemView.extend({ - template : 'Settings/MediaManagement/Naming/Basic/BasicNamingViewTemplate', - - ui : { - namingOptions : '.x-naming-options', - singleTrackExample : '.x-single-track-example' - }, - - initialize : function(options) { - this.namingModel = options.model; - this.model = new BasicNamingModel(); - - this._parseNamingModel(); - - this.listenTo(this.model, 'change', this._buildFormat); - this.listenTo(this.namingModel, 'sync', this._parseNamingModel); - }, - - _parseNamingModel : function() { - var standardFormat = this.namingModel.get('standardTrackFormat'); - - var includeArtistName = standardFormat.match(/\{Artist[-_. ]Name\}/i); - var includeAlbumTitle = standardFormat.match(/\{Album[-_. ]Title\}/i); - var includeQuality = standardFormat.match(/\{Quality[-_. ]Title\}/i); - var numberStyle = standardFormat.match(/\{track(?:\:0+)?\}/i); - var replaceSpaces = standardFormat.indexOf(' ') === -1; - var separator = standardFormat.match(/\}( - |\.-\.|\.| )|( - |\.-\.|\.| )\{/i); - - if (separator === null || separator[1] === '.-.') { - separator = ' - '; - } else { - separator = separator[1]; - } - - if (numberStyle === null) { - numberStyle = '{track:00}'; - } else { - numberStyle = numberStyle[0]; - } - - this.model.set({ - includeArtistName : includeArtistName !== null, - includeAlbumTitle : includeAlbumTitle !== null, - includeQuality : includeQuality !== null, - numberStyle : numberStyle, - replaceSpaces : replaceSpaces, - separator : separator - }, { silent : true }); - }, - - _buildFormat : function() { - if (Config.getValueBoolean(Config.Keys.AdvancedSettings)) { - return; - } - - var standardTrackFormat = ''; - - if (this.model.get('includeArtistName')) { - if (this.model.get('replaceSpaces')) { - standardTrackFormat += '{Artist.Name}'; - } else { - standardTrackFormat += '{Artist Name}'; - } - - standardTrackFormat += this.model.get('separator'); - } - - if (this.model.get('includeAlbumTitle')) { - if (this.model.get('replaceSpaces')) { - standardTrackFormat += '{Album.Title}'; - } else { - standardTrackFormat += '{Album Title}'; - } - - standardTrackFormat += this.model.get('separator'); - } - - standardTrackFormat += this.model.get('numberStyle'); - - standardTrackFormat += this.model.get('separator'); - - if (this.model.get('replaceSpaces')) { - standardTrackFormat += '{Track.Title}'; - } else { - standardTrackFormat += '{Track Title}'; - } - - - if (this.model.get('includeQuality')) { - if (this.model.get('replaceSpaces')) { - standardTrackFormat += ' {Quality.Title}'; - } else { - standardTrackFormat += ' {Quality Title}'; - } - } - - if (this.model.get('replaceSpaces')) { - standardTrackFormat = standardTrackFormat.replace(/\s/g, '.'); - } - - this.namingModel.set('standardTrackFormat', standardTrackFormat); - } -}); - -module.exports = AsModelBoundView.call(view); \ No newline at end of file diff --git a/src/UI/Settings/MediaManagement/Naming/Basic/BasicNamingViewTemplate.hbs b/src/UI/Settings/MediaManagement/Naming/Basic/BasicNamingViewTemplate.hbs deleted file mode 100644 index 512245138..000000000 --- a/src/UI/Settings/MediaManagement/Naming/Basic/BasicNamingViewTemplate.hbs +++ /dev/null @@ -1,98 +0,0 @@ -<div class="form-group"> - <label class="col-sm-3 control-label">Include Artist Name</label> - - <div class="col-sm-9"> - <div class="input-group"> - <label class="checkbox toggle well"> - <input type="checkbox" name="includeArtistName"/> - - <p> - <span>Yes</span> - <span>No</span> - </p> - - <div class="btn btn-primary slide-button"/> - </label> - </div> - </div> -</div> - -<div class="form-group"> - <label class="col-sm-3 control-label">Include Album Title</label> - - <div class="col-sm-9"> - <div class="input-group"> - <label class="checkbox toggle well"> - <input type="checkbox" name="includeAlbumTitle"/> - - <p> - <span>Yes</span> - <span>No</span> - </p> - - <div class="btn btn-primary slide-button"/> - </label> - </div> - </div> -</div> - -<div class="form-group"> - <label class="col-sm-3 control-label">Include Quality</label> - - <div class="col-sm-9"> - <div class="input-group"> - <label class="checkbox toggle well"> - <input type="checkbox" name="includeQuality"/> - - <p> - <span>Yes</span> - <span>No</span> - </p> - - <div class="btn btn-primary slide-button"/> - </label> - </div> - </div> -</div> - -<div class="form-group"> - <label class="col-sm-3 control-label">Replace Spaces</label> - - <div class="col-sm-9"> - <div class="input-group"> - <label class="checkbox toggle well"> - <input type="checkbox" name="replaceSpaces"/> - - <p> - <span>Yes</span> - <span>No</span> - </p> - - <div class="btn btn-primary slide-button"/> - </label> - </div> - </div> -</div> - -<div class="form-group"> - <label class="col-sm-3 control-label">Separator</label> - - <div class="col-sm-9"> - <select class="form-control" name="separator"> - <option value=" - ">Dash</option> - <option value=" ">Space</option> - <option value=".">Period</option> - </select> - </div> -</div> - -<div class="form-group"> - <label class="col-sm-3 control-label">Numbering Style</label> - - <div class="col-sm-9"> - <select class="form-control" name="numberStyle"> - <option value="{track}">1</option> - <option value="{track:00}">01</option> - </select> - </div> -</div> diff --git a/src/UI/Settings/MediaManagement/Naming/NamingModel.js b/src/UI/Settings/MediaManagement/Naming/NamingModel.js deleted file mode 100644 index 5ff713850..000000000 --- a/src/UI/Settings/MediaManagement/Naming/NamingModel.js +++ /dev/null @@ -1,7 +0,0 @@ -var ModelBase = require('../../SettingsModelBase'); - -module.exports = ModelBase.extend({ - url : window.NzbDrone.ApiRoot + '/config/naming', - successMessage : 'MediaManagement settings saved', - errorMessage : 'Couldn\'t save naming settings' -}); \ No newline at end of file diff --git a/src/UI/Settings/MediaManagement/Naming/NamingSampleModel.js b/src/UI/Settings/MediaManagement/Naming/NamingSampleModel.js deleted file mode 100644 index 375d74a6f..000000000 --- a/src/UI/Settings/MediaManagement/Naming/NamingSampleModel.js +++ /dev/null @@ -1,3 +0,0 @@ -var Backbone = require('backbone'); - -module.exports = Backbone.Model.extend({ url : window.NzbDrone.ApiRoot + '/config/naming/samples' }); \ No newline at end of file diff --git a/src/UI/Settings/MediaManagement/Naming/NamingView.js b/src/UI/Settings/MediaManagement/Naming/NamingView.js deleted file mode 100644 index 55dbb860f..000000000 --- a/src/UI/Settings/MediaManagement/Naming/NamingView.js +++ /dev/null @@ -1,72 +0,0 @@ -var _ = require('underscore'); -var Marionette = require('marionette'); -var NamingSampleModel = require('./NamingSampleModel'); -var BasicNamingView = require('./Basic/BasicNamingView'); -var AsModelBoundView = require('../../../Mixins/AsModelBoundView'); -var AsValidatedView = require('../../../Mixins/AsValidatedView'); - -module.exports = (function() { - var view = Marionette.Layout.extend({ - template : 'Settings/MediaManagement/Naming/NamingViewTemplate', - ui : { - namingOptions : '.x-naming-options', - renameTracksCheckbox : '.x-rename-tracks', - singleTrackExample : '.x-single-track-example', - namingTokenHelper : '.x-naming-token-helper', - artistFolderExample : '.x-artist-folder-example', - albumFolderExample : '.x-album-folder-example' - }, - events : { - "change .x-rename-tracks" : '_setFailedDownloadOptionsVisibility', - "click .x-show-wizard" : '_showWizard', - "click .x-naming-token-helper a" : '_addToken' - }, - regions : { basicNamingRegion : '.x-basic-naming' }, - onRender : function() { - if (!this.model.get('renameTracks')) { - this.ui.namingOptions.hide(); - } - var basicNamingView = new BasicNamingView({ model : this.model }); - this.basicNamingRegion.show(basicNamingView); - this.namingSampleModel = new NamingSampleModel(); - this.listenTo(this.model, 'change', this._updateSamples); - this.listenTo(this.namingSampleModel, 'sync', this._showSamples); - this._updateSamples(); - }, - _setFailedDownloadOptionsVisibility : function() { - var checked = this.ui.renameTracksCheckbox.prop('checked'); - if (checked) { - this.ui.namingOptions.slideDown(); - } else { - this.ui.namingOptions.slideUp(); - } - }, - _updateSamples : function() { - this.namingSampleModel.fetch({ data : this.model.toJSON() }); - }, - _showSamples : function() { - this.ui.singleTrackExample.html(this.namingSampleModel.get('singleTrackExample')); - this.ui.artistFolderExample.html(this.namingSampleModel.get('artistFolderExample')); - this.ui.albumFolderExample.html(this.namingSampleModel.get('albumFolderExample')); - }, - _addToken : function(e) { - e.preventDefault(); - e.stopPropagation(); - var target = e.target; - var token = ''; - var input = this.$(target).closest('.x-helper-input').children('input'); - if (this.$(target).attr('data-token')) { - token = '{{0}}'.format(this.$(target).attr('data-token')); - } else { - token = this.$(target).attr('data-separator'); - } - input.val(input.val() + token); - input.change(); - this.ui.namingTokenHelper.removeClass('open'); - input.focus(); - }, - }); - AsModelBoundView.call(view); - AsValidatedView.call(view); - return view; -}).call(this); \ No newline at end of file diff --git a/src/UI/Settings/MediaManagement/Naming/NamingViewTemplate.hbs b/src/UI/Settings/MediaManagement/Naming/NamingViewTemplate.hbs deleted file mode 100644 index 6b46ace90..000000000 --- a/src/UI/Settings/MediaManagement/Naming/NamingViewTemplate.hbs +++ /dev/null @@ -1,153 +0,0 @@ -<fieldset> - <legend>Track Naming</legend> - - <div class="form-group"> - <label class="col-sm-3 control-label">Rename Tracks</label> - - <div class="col-sm-8"> - <div class="input-group"> - <label class="checkbox toggle well"> - <input type="checkbox" name="renameTracks" class="x-rename-tracks"/> - - <p> - <span>Yes</span> - <span>No</span> - </p> - - <div class="btn btn-primary slide-button"/> - </label> - - <span class="help-inline-checkbox"> - <i class="icon-lidarr-form-warning" title="Lidarr will use the existing file name if set to no"/> - </span> - </div> - </div> - </div> - - <div class="form-group advanced-setting"> - <label class="col-sm-3 control-label">Replace Illegal Characters</label> - - <div class="col-sm-8"> - <div class="input-group"> - <label class="checkbox toggle well"> - <input type="checkbox" name="replaceIllegalCharacters" /> - - <p> - <span>Yes</span> - <span>No</span> - </p> - - <div class="btn btn-primary slide-button"/> - </label> - - <span class="help-inline-checkbox"> - <i class="icon-lidarr-form-info" title="Replace or Remove illegal characters"/> - </span> - </div> - </div> - </div> - - <div class="x-naming-options"> - <div class="basic-setting x-basic-naming"></div> - - <div class="form-group advanced-setting"> - <label class="col-sm-3 control-label">Standard Track Format</label> - - <div class="col-sm-1 col-sm-push-8 help-inline"> - <i class="icon-lidarr-form-info" title="" data-original-title="All caps or all lower-case can also be used"></i> - <a href="https://github.com/NzbDrone/NzbDrone/wiki/Sorting-and-Renaming" class="help-link" title="More information"><i class="icon-lidarr-form-info-link"/></a> - </div> - - <div class="col-sm-8 col-sm-pull-1"> - <div class="input-group x-helper-input"> - <input type="text" class="form-control naming-format" name="standardTrackFormat" data-onkeyup="true" /> - <div class="input-group-btn btn-group x-naming-token-helper"> - <button class="btn btn-icon-only dropdown-toggle" data-toggle="dropdown"> - <i class="icon-lidarr-add"></i> - </button> - <ul class="dropdown-menu"> - {{> ArtistNameNamingPartial}} - {{> AlbumTitleNamingPartial}} - {{> ReleaseYearNamingPartial}} - {{> TrackNumNamingPartial}} - {{> TrackTitleNamingPartial}} - {{> QualityNamingPartial}} - {{> MediaInfoNamingPartial}} - {{> ReleaseGroupNamingPartial}} - {{> OriginalTitleNamingPartial}} - {{> SeparatorNamingPartial}} - </ul> - </div> - </div> - </div> - </div> - - </div> - - <div class="form-group advanced-setting"> - <label class="col-sm-3 control-label">Artist Folder Format</label> - - <div class="col-sm-1 col-sm-push-8 help-inline"> - <i class="icon-lidarr-form-info" title="" data-original-title="All caps or all lower-case can also be used. Only used when adding a new artist."></i> - </div> - - <div class="col-sm-8 col-sm-pull-1"> - <div class="input-group x-helper-input"> - <input type="text" class="form-control naming-format" name="artistFolderFormat" data-onkeyup="true"/> - <div class="input-group-btn btn-group x-naming-token-helper"> - <button class="btn btn-icon-only dropdown-toggle" data-toggle="dropdown"> - <i class="icon-lidarr-add"></i> - </button> - <ul class="dropdown-menu"> - {{> ArtistNameNamingPartial}} - </ul> - </div> - </div> - </div> - </div> - - <div class="form-group"> - <label class="col-sm-3 control-label">Album Folder Format</label> - - <div class="col-sm-8"> - <div class="input-group x-helper-input"> - <input type="text" class="form-control naming-format" name="albumFolderFormat" data-onkeyup="true"/> - <div class="input-group-btn btn-group x-naming-token-helper"> - <button class="btn btn-icon-only dropdown-toggle" data-toggle="dropdown"> - <i class="icon-lidarr-add"></i> - </button> - <ul class="dropdown-menu"> - {{> ArtistNameNamingPartial}} - {{> AlbumTitleNamingPartial}} - {{> ReleaseYearNamingPartial}} - {{> SeparatorNamingPartial}} - </ul> - </div> - </div> - </div> - </div> - - <div class="form-group"> - <label class="col-sm-3 control-label">Single Track Example</label> - - <div class="col-sm-8"> - <p class="form-control-static x-single-track-example naming-example"></p> - </div> - </div> - - <div class="form-group"> - <label class="col-sm-3 control-label">Artist Folder Example</label> - - <div class="col-sm-8"> - <p class="form-control-static x-artist-folder-example naming-example"></p> - </div> - </div> - - <div class="form-group"> - <label class="col-sm-3 control-label">Album Folder Example</label> - - <div class="col-sm-8"> - <p class="form-control-static x-album-folder-example naming-example"></p> - </div> - </div> -</fieldset> diff --git a/src/UI/Settings/MediaManagement/Naming/Partials/AbsoluteEpisodeNamingPartial.hbs b/src/UI/Settings/MediaManagement/Naming/Partials/AbsoluteEpisodeNamingPartial.hbs deleted file mode 100644 index ba31a196e..000000000 --- a/src/UI/Settings/MediaManagement/Naming/Partials/AbsoluteEpisodeNamingPartial.hbs +++ /dev/null @@ -1,8 +0,0 @@ -<li class="dropdown-submenu"> - <a href="#" tabindex="-1" data-token="absolute">Absolute</a> - <ul class="dropdown-menu"> - <li><a href="#" data-token="absolute">1</a></li> - <li><a href="#" data-token="absolute:00">01</a></li> - <li><a href="#" data-token="absolute:000">001</a></li> - </ul> -</li> diff --git a/src/UI/Settings/MediaManagement/Naming/Partials/AirDateNamingPartial.hbs b/src/UI/Settings/MediaManagement/Naming/Partials/AirDateNamingPartial.hbs deleted file mode 100644 index ed845e2c0..000000000 --- a/src/UI/Settings/MediaManagement/Naming/Partials/AirDateNamingPartial.hbs +++ /dev/null @@ -1,9 +0,0 @@ -<li class="dropdown-submenu"> - <a href="#" tabindex="-1" data-token="Air-Date">Air-Date</a> - <ul class="dropdown-menu"> - <li><a href="#" data-token="Air-Date">Air-Date</a></li> - <li><a href="#" data-token="Air Date">Air Date</a></li> - <li><a href="#" data-token="Air.Date">Air.Date</a></li> - <li><a href="#" data-token="Air_Date">Air_Date</a></li> - </ul> -</li> diff --git a/src/UI/Settings/MediaManagement/Naming/Partials/AlbumTitleNamingPartial.hbs b/src/UI/Settings/MediaManagement/Naming/Partials/AlbumTitleNamingPartial.hbs deleted file mode 100644 index 9e3da7a54..000000000 --- a/src/UI/Settings/MediaManagement/Naming/Partials/AlbumTitleNamingPartial.hbs +++ /dev/null @@ -1,11 +0,0 @@ -<li class="dropdown-submenu"> - <a href="#" tabindex="-1" data-token="Album Title">Album Title</a> - <ul class="dropdown-menu"> - <li><a href="#" data-token="Album Title">Album Title</a></li> - <li><a href="#" data-token="Album.Title">Album.Title</a></li> - <li><a href="#" data-token="Album_Title">Album_Title</a></li> - <li><a href="#" data-token="Album CleanTitle">Album CleanTitle</a></li> - <li><a href="#" data-token="Album.CleanTitle">Album.CleanTitle</a></li> - <li><a href="#" data-token="Album_CleanTitle">Album_CleanTitle</a></li> - </ul> -</li> diff --git a/src/UI/Settings/MediaManagement/Naming/Partials/ArtistNameNamingPartial.hbs b/src/UI/Settings/MediaManagement/Naming/Partials/ArtistNameNamingPartial.hbs deleted file mode 100644 index c951bd123..000000000 --- a/src/UI/Settings/MediaManagement/Naming/Partials/ArtistNameNamingPartial.hbs +++ /dev/null @@ -1,11 +0,0 @@ -<li class="dropdown-submenu"> - <a href="#" tabindex="-1" data-token="Artist Name">Artist Name</a> - <ul class="dropdown-menu"> - <li><a href="#" data-token="Artist Name">Artist Name</a></li> - <li><a href="#" data-token="Artist.Name">Artist.Name</a></li> - <li><a href="#" data-token="Artist_Name">Artist_Name</a></li> - <li><a href="#" data-token="Artist CleanName">Artist CleanName</a></li> - <li><a href="#" data-token="Artist.CleanName">Artist.CleanName</a></li> - <li><a href="#" data-token="Artist_CleanName">Artist_CleanName</a></li> - </ul> -</li> diff --git a/src/UI/Settings/MediaManagement/Naming/Partials/EpisodeNamingPartial.hbs b/src/UI/Settings/MediaManagement/Naming/Partials/EpisodeNamingPartial.hbs deleted file mode 100644 index 4c20f4ffa..000000000 --- a/src/UI/Settings/MediaManagement/Naming/Partials/EpisodeNamingPartial.hbs +++ /dev/null @@ -1,7 +0,0 @@ -<li class="dropdown-submenu"> - <a href="#" tabindex="-1" data-token="episode">Episode</a> - <ul class="dropdown-menu"> - <li><a href="#" data-token="episode">1</a></li> - <li><a href="#" data-token="episode:00">01</a></li> - </ul> -</li> diff --git a/src/UI/Settings/MediaManagement/Naming/Partials/EpisodeTitleNamingPartial.hbs b/src/UI/Settings/MediaManagement/Naming/Partials/EpisodeTitleNamingPartial.hbs deleted file mode 100644 index 10f2ec67e..000000000 --- a/src/UI/Settings/MediaManagement/Naming/Partials/EpisodeTitleNamingPartial.hbs +++ /dev/null @@ -1,11 +0,0 @@ -<li class="dropdown-submenu"> - <a href="#" tabindex="-1" data-token="Episode Title">Episode Title</a> - <ul class="dropdown-menu"> - <li><a href="#" data-token="Episode Title">Episode Title</a></li> - <li><a href="#" data-token="Episode.Title">Episode.Title</a></li> - <li><a href="#" data-token="Episode_Title">Episode_Title</a></li> - <li><a href="#" data-token="Episode CleanTitle">Episode CleanTitle</a></li> - <li><a href="#" data-token="Episode.CleanTitle">Episode.CleanTitle</a></li> - <li><a href="#" data-token="Episode_CleanTitle">Episode_CleanTitle</a></li> - </ul> -</li> diff --git a/src/UI/Settings/MediaManagement/Naming/Partials/MediaInfoNamingPartial.hbs b/src/UI/Settings/MediaManagement/Naming/Partials/MediaInfoNamingPartial.hbs deleted file mode 100644 index 49203cafc..000000000 --- a/src/UI/Settings/MediaManagement/Naming/Partials/MediaInfoNamingPartial.hbs +++ /dev/null @@ -1,11 +0,0 @@ -<li class="dropdown-submenu"> - <a href="#" tabindex="-1" data-token="MediaInfo.Simple">MediaInfo</a> - <ul class="dropdown-menu"> - <li><a href="#" data-token="MediaInfo Simple">MediaInfo Simple</a></li> - <li><a href="#" data-token="MediaInfo.Simple">MediaInfo.Simple</a></li> - <li><a href="#" data-token="MediaInfo_Simple">MediaInfo_Simple</a></li> - <li><a href="#" data-token="MediaInfo Full">MediaInfo Full</a></li> - <li><a href="#" data-token="MediaInfo.Full">MediaInfo.Full</a></li> - <li><a href="#" data-token="MediaInfo_Full">MediaInfo_Full</a></li> - </ul> -</li> diff --git a/src/UI/Settings/MediaManagement/Naming/Partials/OriginalTitleNamingPartial.hbs b/src/UI/Settings/MediaManagement/Naming/Partials/OriginalTitleNamingPartial.hbs deleted file mode 100644 index cef96b894..000000000 --- a/src/UI/Settings/MediaManagement/Naming/Partials/OriginalTitleNamingPartial.hbs +++ /dev/null @@ -1 +0,0 @@ -<li><a href="#" data-token="Original Title">Original Title</a></li> \ No newline at end of file diff --git a/src/UI/Settings/MediaManagement/Naming/Partials/QualityNamingPartial.hbs b/src/UI/Settings/MediaManagement/Naming/Partials/QualityNamingPartial.hbs deleted file mode 100644 index b3da5f0af..000000000 --- a/src/UI/Settings/MediaManagement/Naming/Partials/QualityNamingPartial.hbs +++ /dev/null @@ -1,11 +0,0 @@ -<li class="dropdown-submenu"> - <a href="#" tabindex="-1" data-token="Quality Full">Quality</a> - <ul class="dropdown-menu"> - <li><a href="#" data-token="Quality Full">Quality Full</a></li> - <li><a href="#" data-token="Quality.Full">Quality.Full</a></li> - <li><a href="#" data-token="Quality_Full">Quality_Full</a></li> - <li><a href="#" data-token="Quality Title">Quality Title</a></li> - <li><a href="#" data-token="Quality.Title">Quality.Title</a></li> - <li><a href="#" data-token="Quality_Title">Quality_Title</a></li> - </ul> -</li> diff --git a/src/UI/Settings/MediaManagement/Naming/Partials/ReleaseGroupNamingPartial.hbs b/src/UI/Settings/MediaManagement/Naming/Partials/ReleaseGroupNamingPartial.hbs deleted file mode 100644 index bf9ea50a4..000000000 --- a/src/UI/Settings/MediaManagement/Naming/Partials/ReleaseGroupNamingPartial.hbs +++ /dev/null @@ -1,8 +0,0 @@ -<li class="dropdown-submenu"> - <a href="#" tabindex="-1" data-token="Release Group">Release Group</a> - <ul class="dropdown-menu"> - <li><a href="#" data-token="Release Group">Release Group</a></li> - <li><a href="#" data-token="Release.Group">Release.Group</a></li> - <li><a href="#" data-token="Release_Group">Release_Group</a></li> - </ul> -</li> diff --git a/src/UI/Settings/MediaManagement/Naming/Partials/ReleaseYearNamingPartial.hbs b/src/UI/Settings/MediaManagement/Naming/Partials/ReleaseYearNamingPartial.hbs deleted file mode 100644 index 0a4153d66..000000000 --- a/src/UI/Settings/MediaManagement/Naming/Partials/ReleaseYearNamingPartial.hbs +++ /dev/null @@ -1 +0,0 @@ -<li><a href="#" data-token="Release Year">Release Year</a></li> \ No newline at end of file diff --git a/src/UI/Settings/MediaManagement/Naming/Partials/SeasonNamingPartial.hbs b/src/UI/Settings/MediaManagement/Naming/Partials/SeasonNamingPartial.hbs deleted file mode 100644 index 2c56024da..000000000 --- a/src/UI/Settings/MediaManagement/Naming/Partials/SeasonNamingPartial.hbs +++ /dev/null @@ -1,7 +0,0 @@ -<li class="dropdown-submenu"> - <a href="#" tabindex="-1" data-token="season">Season</a> - <ul class="dropdown-menu"> - <li><a href="#" data-token="season">1</a></li> - <li><a href="#" data-token="season:00">01</a></li> - </ul> -</li> diff --git a/src/UI/Settings/MediaManagement/Naming/Partials/SeparatorNamingPartial.hbs b/src/UI/Settings/MediaManagement/Naming/Partials/SeparatorNamingPartial.hbs deleted file mode 100644 index 2b19d32b5..000000000 --- a/src/UI/Settings/MediaManagement/Naming/Partials/SeparatorNamingPartial.hbs +++ /dev/null @@ -1,10 +0,0 @@ -<li class="dropdown-submenu"> - <a href="#" tabindex="-1" data-separator=" - ">Separator</a> - <ul class="dropdown-menu"> - <li><a href="#" data-separator=" - ">Space-Dash-Space</a></li> - <li><a href="#" data-separator="-">Dash</a></li> - <li><a href="#" data-separator=" ">Space</a></li> - <li><a href="#" data-separator=".">Period</a></li> - <li><a href="#" data-separator="_">Underscore</a></li> - </ul> -</li> diff --git a/src/UI/Settings/MediaManagement/Naming/Partials/SeriesTitleNamingPartial.hbs b/src/UI/Settings/MediaManagement/Naming/Partials/SeriesTitleNamingPartial.hbs deleted file mode 100644 index cc76c95b5..000000000 --- a/src/UI/Settings/MediaManagement/Naming/Partials/SeriesTitleNamingPartial.hbs +++ /dev/null @@ -1,11 +0,0 @@ -<li class="dropdown-submenu"> - <a href="#" tabindex="-1" data-token="Series Title">Series Title</a> - <ul class="dropdown-menu"> - <li><a href="#" data-token="Series Title">Series Title</a></li> - <li><a href="#" data-token="Series.Title">Series.Title</a></li> - <li><a href="#" data-token="Series_Title">Series_Title</a></li> - <li><a href="#" data-token="Series CleanTitle">Series CleanTitle</a></li> - <li><a href="#" data-token="Series.CleanTitle">Series.CleanTitle</a></li> - <li><a href="#" data-token="Series_CleanTitle">Series_CleanTitle</a></li> - </ul> -</li> diff --git a/src/UI/Settings/MediaManagement/Naming/Partials/TrackNumNamingPartial.hbs b/src/UI/Settings/MediaManagement/Naming/Partials/TrackNumNamingPartial.hbs deleted file mode 100644 index 68ddc8ff5..000000000 --- a/src/UI/Settings/MediaManagement/Naming/Partials/TrackNumNamingPartial.hbs +++ /dev/null @@ -1,7 +0,0 @@ -<li class="dropdown-submenu"> - <a href="#" tabindex="-1" data-token="track">Track</a> - <ul class="dropdown-menu"> - <li><a href="#" data-token="track">1</a></li> - <li><a href="#" data-token="track:00">01</a></li> - </ul> -</li> diff --git a/src/UI/Settings/MediaManagement/Naming/Partials/TrackTitleNamingPartial.hbs b/src/UI/Settings/MediaManagement/Naming/Partials/TrackTitleNamingPartial.hbs deleted file mode 100644 index 591c57098..000000000 --- a/src/UI/Settings/MediaManagement/Naming/Partials/TrackTitleNamingPartial.hbs +++ /dev/null @@ -1,11 +0,0 @@ -<li class="dropdown-submenu"> - <a href="#" tabindex="-1" data-token="Track Title">Track Title</a> - <ul class="dropdown-menu"> - <li><a href="#" data-token="Track Title">Track Title</a></li> - <li><a href="#" data-token="Track.Title">Track.Title</a></li> - <li><a href="#" data-token="Track_Title">Track_Title</a></li> - <li><a href="#" data-token="Track CleanTitle">Track CleanTitle</a></li> - <li><a href="#" data-token="Track.CleanTitle">Track.CleanTitle</a></li> - <li><a href="#" data-token="Track_CleanTitle">Track_CleanTitle</a></li> - </ul> -</li> diff --git a/src/UI/Settings/MediaManagement/Permissions/PermissionsView.js b/src/UI/Settings/MediaManagement/Permissions/PermissionsView.js deleted file mode 100644 index 6bf74221b..000000000 --- a/src/UI/Settings/MediaManagement/Permissions/PermissionsView.js +++ /dev/null @@ -1,11 +0,0 @@ -var Marionette = require('marionette'); -var AsModelBoundView = require('../../../Mixins/AsModelBoundView'); -var AsValidatedView = require('../../../Mixins/AsValidatedView'); - -var view = Marionette.ItemView.extend({ - template : 'Settings/MediaManagement/Permissions/PermissionsViewTemplate' -}); -AsModelBoundView.call(view); -AsValidatedView.call(view); - -module.exports = view; \ No newline at end of file diff --git a/src/UI/Settings/MediaManagement/Permissions/PermissionsViewTemplate.hbs b/src/UI/Settings/MediaManagement/Permissions/PermissionsViewTemplate.hbs deleted file mode 100644 index 75f8ddf48..000000000 --- a/src/UI/Settings/MediaManagement/Permissions/PermissionsViewTemplate.hbs +++ /dev/null @@ -1,74 +0,0 @@ -<fieldset class="advanced-setting"> - <legend>Permissions</legend> - - <div class="form-group"> - <label class="col-sm-3 control-label">Set Permissions</label> - - <div class="col-sm-8"> - <div class="input-group"> - <label class="checkbox toggle well"> - <input type="checkbox" name="setPermissionsLinux"/> - <p> - <span>Yes</span> - <span>No</span> - </p> - - <div class="btn btn-primary slide-button"/> - </label> - - <span class="help-inline-checkbox"> - <i class="icon-lidarr-form-info" title="Should chmod/chown be run when files are imported/renamed?"/> - <i class="icon-lidarr-form-warning" title="If you're unsure what these settings do, do not alter them."/> - </span> - </div> - </div> - </div> - - <div class="form-group"> - <label class="col-sm-3 control-label">File chmod mask</label> - - <div class="col-sm-1 col-sm-push-4 help-inline"> - <i class="icon-lidarr-form-info" title="Octal, applied to media files when imported/renamed by Lidarr"/> - </div> - - <div class="col-sm-4 col-sm-pull-1"> - <input type="text" name="fileChmod" class="form-control"/> - </div> - </div> - - <div class="form-group"> - <label class="col-sm-3 control-label">Folder chmod mask</label> - - <div class="col-sm-1 col-sm-push-4 help-inline"> - <i class="icon-lidarr-form-info" title="Octal, applied to artist/album folders created by Lidarr"/> - </div> - - <div class="col-sm-4 col-sm-pull-1"> - <input type="text" name="folderChmod" class="form-control"/> - </div> - </div> - - <div class="form-group"> - <label class="col-sm-3 control-label">chown User</label> - - <div class="col-sm-1 col-sm-push-4 help-inline"> - <i class="icon-lidarr-form-info" title="Username or uid. Use uid for remote file systems."/> - </div> - - <div class="col-sm-4 col-sm-pull-1"> - <input type="text" name="chownUser" class="form-control"/> - </div> - </div> - - <div class="form-group"> - <label class="col-sm-3 control-label">chown Group</label> - - <div class="col-sm-1 col-sm-push-4 help-inline"> - <i class="icon-lidarr-form-info" title="Group name or gid. Use gid for remote file systems."/> - </div> - - <div class="col-sm-4 col-sm-pull-1"> - <input type="text" name="chownGroup" class="form-control"/> - </div> - </div> -</fieldset> diff --git a/src/UI/Settings/MediaManagement/Sorting/SortingView.js b/src/UI/Settings/MediaManagement/Sorting/SortingView.js deleted file mode 100644 index 9194f895e..000000000 --- a/src/UI/Settings/MediaManagement/Sorting/SortingView.js +++ /dev/null @@ -1,39 +0,0 @@ -var Marionette = require('marionette'); -var AsModelBoundView = require('../../../Mixins/AsModelBoundView'); -var AsValidatedView = require('../../../Mixins/AsValidatedView'); - -var view = Marionette.ItemView.extend({ - template : 'Settings/MediaManagement/Sorting/SortingViewTemplate', - - events : { - 'change .x-import-extra-files' : '_setExtraFileExtensionVisibility' - }, - - ui : { - importExtraFiles : '.x-import-extra-files', - extraFileExtensions : '.x-extra-file-extensions' - }, - - onRender : function() { - if (!this.ui.importExtraFiles.prop('checked')) { - this.ui.extraFileExtensions.hide(); - } - }, - - _setExtraFileExtensionVisibility : function() { - var showExtraFileExtensions = this.ui.importExtraFiles.prop('checked'); - - if (showExtraFileExtensions) { - this.ui.extraFileExtensions.slideDown(); - } - - else { - this.ui.extraFileExtensions.slideUp(); - } - } -}); - -AsModelBoundView.call(view); -AsValidatedView.call(view); - -module.exports = view; diff --git a/src/UI/Settings/MediaManagement/Sorting/SortingViewTemplate.hbs b/src/UI/Settings/MediaManagement/Sorting/SortingViewTemplate.hbs deleted file mode 100644 index dcc6fa291..000000000 --- a/src/UI/Settings/MediaManagement/Sorting/SortingViewTemplate.hbs +++ /dev/null @@ -1,115 +0,0 @@ -<fieldset class="advanced-setting"> - <legend>Folders</legend> - - <div class="form-group"> - <label class="col-sm-3 control-label">Create Empty Artist Folders</label> - - <div class="col-sm-9"> - <div class="input-group"> - <label class="checkbox toggle well"> - <input type="checkbox" name="createEmptyArtistFolders"/> - - <p> - <span>Yes</span> - <span>No</span> - </p> - - <div class="btn btn-primary slide-button"/> - </label> - - <span class="help-inline-checkbox"> - <i class="icon-lidarr-form-info" title="Create missing artist folders during disk scan"/> - </span> - </div> - </div> - </div> -</fieldset> - -<fieldset> - <legend>Importing</legend> - -{{#if_mono}} - <div class="form-group advanced-setting"> - <label class="col-sm-3 control-label">Skip Free Space Check</label> - - <div class="col-sm-9"> - <div class="input-group"> - <label class="checkbox toggle well"> - <input type="checkbox" name="skipFreeSpaceCheckWhenImporting"/> - - <p> - <span>Yes</span> - <span>No</span> - </p> - - <div class="btn btn-primary slide-button"/> - </label> - - <span class="help-inline-checkbox"> - <i class="icon-lidarr-form-info" title="Use when drone is unable to detect free space from your artist root folder"/> - </span> - </div> - </div> - </div> -{{/if_mono}} - - <div class="form-group advanced-setting"> - <label class="col-sm-3 control-label">Use Hardlinks instead of Copy</label> - - <div class="col-sm-9"> - <div class="input-group"> - <label class="checkbox toggle well"> - <input type="checkbox" name="copyUsingHardlinks"/> - - <p> - <span>Yes</span> - <span>No</span> - </p> - - <div class="btn btn-primary slide-button"/> - </label> - - <span class="help-inline-checkbox"> - <i class="icon-lidarr-form-info" title="Use Hardlinks when trying to copy files from torrents that are still being seeded"/> - <i class="icon-lidarr-form-warning" title="Occasionally, file locks may prevent renaming files that are being seeded. You may temporarily disable seeding and use Lidarr's rename function as a work around."/> - </span> - </div> - </div> - </div> - - <div class="form-group"> - <label class="col-sm-3 control-label">Import Extra Files</label> - - <div class="col-sm-9"> - <div class="input-group"> - <label class="checkbox toggle well"> - <input type="checkbox" name="importExtraFiles" class="x-import-extra-files"/> - - <p> - <span>Yes</span> - <span>No</span> - </p> - - <div class="btn btn-primary slide-button"/> - </label> - - <span class="help-inline-checkbox"> - <i class="icon-lidarr-form-info" title="Import matching extra files (subtitles, nfo, etc) after importing an album or track file"/> - </span> - </div> - </div> - </div> - - <div class="form-group x-extra-file-extensions"> - <label class="col-sm-3 control-label">Extra File Extensions</label> - - <div class="col-sm-1 col-sm-push-5 help-inline"> - <i class="icon-lidarr-form-info" title="Comma separated list of extra files to import, ie sub,nfo (.nfo will be imported as .nfo-orig)"/> - </div> - - <div class="col-sm-5 col-sm-pull-1"> - <input type="text" name="extraFileExtensions" class="form-control"/> - </div> - </div> - -</fieldset> diff --git a/src/UI/Settings/Metadata/MetadataCollection.js b/src/UI/Settings/Metadata/MetadataCollection.js deleted file mode 100644 index f37d80961..000000000 --- a/src/UI/Settings/Metadata/MetadataCollection.js +++ /dev/null @@ -1,7 +0,0 @@ -var Backbone = require('backbone'); -var MetadataModel = require('./MetadataModel'); - -module.exports = Backbone.Collection.extend({ - model : MetadataModel, - url : window.NzbDrone.ApiRoot + '/metadata' -}); \ No newline at end of file diff --git a/src/UI/Settings/Metadata/MetadataCollectionView.js b/src/UI/Settings/Metadata/MetadataCollectionView.js deleted file mode 100644 index 1f60d8fe0..000000000 --- a/src/UI/Settings/Metadata/MetadataCollectionView.js +++ /dev/null @@ -1,9 +0,0 @@ -var AppLayout = require('../../AppLayout'); -var Marionette = require('marionette'); -var MetadataItemView = require('./MetadataItemView'); - -module.exports = Marionette.CompositeView.extend({ - itemView : MetadataItemView, - itemViewContainer : '#x-metadata', - template : 'Settings/Metadata/MetadataCollectionViewTemplate' -}); \ No newline at end of file diff --git a/src/UI/Settings/Metadata/MetadataCollectionViewTemplate.hbs b/src/UI/Settings/Metadata/MetadataCollectionViewTemplate.hbs deleted file mode 100644 index a5c034668..000000000 --- a/src/UI/Settings/Metadata/MetadataCollectionViewTemplate.hbs +++ /dev/null @@ -1,8 +0,0 @@ -<fieldset> - <legend>Metadata</legend> - <div class="row"> - <div class="col-md-12"> - <ul id="x-metadata" class="metadata-list"></ul> - </div> - </div> -</fieldset> \ No newline at end of file diff --git a/src/UI/Settings/Metadata/MetadataEditView.js b/src/UI/Settings/Metadata/MetadataEditView.js deleted file mode 100644 index ed364824f..000000000 --- a/src/UI/Settings/Metadata/MetadataEditView.js +++ /dev/null @@ -1,19 +0,0 @@ -var vent = require('vent'); -var Marionette = require('marionette'); -var AsModelBoundView = require('../../Mixins/AsModelBoundView'); -var AsValidatedView = require('../../Mixins/AsValidatedView'); -var AsEditModalView = require('../../Mixins/AsEditModalView'); - -var view = Marionette.ItemView.extend({ - template : 'Settings/Metadata/MetadataEditViewTemplate', - - _onAfterSave : function() { - vent.trigger(vent.Commands.CloseModalCommand); - } -}); - -AsModelBoundView.call(view); -AsValidatedView.call(view); -AsEditModalView.call(view); - -module.exports = view; \ No newline at end of file diff --git a/src/UI/Settings/Metadata/MetadataEditViewTemplate.hbs b/src/UI/Settings/Metadata/MetadataEditViewTemplate.hbs deleted file mode 100644 index 86aa18ca2..000000000 --- a/src/UI/Settings/Metadata/MetadataEditViewTemplate.hbs +++ /dev/null @@ -1,45 +0,0 @@ -<div class="modal-content"> - <div class="modal-header"> - <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> - <h3>Edit</h3> - </div> - <div class="modal-body"> - <div class="form-horizontal"> - <div class="form-group"> - <label class="col-sm-3 control-label">Name</label> - - <div class="col-sm-5 controls"> - <input type="text" name="name" class="form-control"/> - </div> - </div> - - <div class="form-group"> - <label class="col-sm-3 control-label">Enable</label> - - <div class="col-sm-5"> - <div class="input-group"> - <label class="checkbox toggle well"> - <input type="checkbox" name="enable"/> - <p> - <span>Yes</span> - <span>No</span> - </p> - - <div class="btn btn-primary slide-button"/> - </label> - </div> - </div> - </div> - - <hr> - - {{formBuilder}} - </div> - </div> - <div class="modal-footer"> - <span class="indicator x-indicator"><i class="icon-lidarr-spinner fa-spin"></i></span> - - <button class="btn" data-dismiss="modal">Cancel</button> - <button class="btn btn-primary x-save">Save</button> - </div> -</div> diff --git a/src/UI/Settings/Metadata/MetadataItemView.js b/src/UI/Settings/Metadata/MetadataItemView.js deleted file mode 100644 index c72066d6c..000000000 --- a/src/UI/Settings/Metadata/MetadataItemView.js +++ /dev/null @@ -1,24 +0,0 @@ -var AppLayout = require('../../AppLayout'); -var Marionette = require('marionette'); -var EditView = require('./MetadataEditView'); -var AsModelBoundView = require('../../Mixins/AsModelBoundView'); - -var view = Marionette.ItemView.extend({ - template : 'Settings/Metadata/MetadataItemViewTemplate', - tagName : 'li', - - events : { - 'click' : '_edit' - }, - - initialize : function() { - this.listenTo(this.model, 'sync', this.render); - }, - - _edit : function() { - var view = new EditView({ model : this.model }); - AppLayout.modalRegion.show(view); - } -}); - -module.exports = AsModelBoundView.call(view); \ No newline at end of file diff --git a/src/UI/Settings/Metadata/MetadataItemViewTemplate.hbs b/src/UI/Settings/Metadata/MetadataItemViewTemplate.hbs deleted file mode 100644 index af9adc982..000000000 --- a/src/UI/Settings/Metadata/MetadataItemViewTemplate.hbs +++ /dev/null @@ -1,23 +0,0 @@ -<div class="metadata-item"> - <div> - <h3>{{implementationName}}</h3> - </div> - - <div class="settings"> - {{#if enable}} - <span class="label label-success">Enabled</span> - {{else}} - <span class="label label-default">Not Enabled</span> - {{/if}} - <hr> - {{#each fields}} - {{#if_eq type compare="checkbox"}} - {{#if value}} - <span class="label label-success">{{label}}</span> - {{else}} - <span class="label">{{label}}</span> - {{/if}} - {{/if_eq}} - {{/each}} - </div> -</div> diff --git a/src/UI/Settings/Metadata/MetadataLayout.js b/src/UI/Settings/Metadata/MetadataLayout.js deleted file mode 100644 index 66b5f5901..000000000 --- a/src/UI/Settings/Metadata/MetadataLayout.js +++ /dev/null @@ -1,20 +0,0 @@ -var Marionette = require('marionette'); -var MetadataCollection = require('./MetadataCollection'); -var MetadataCollectionView = require('./MetadataCollectionView'); - -module.exports = Marionette.Layout.extend({ - template : 'Settings/Metadata/MetadataLayoutTemplate', - - regions : { - metadata : '#x-metadata-providers' - }, - - initialize : function(options) { - this.settings = options.settings; - this.metadataCollection = new MetadataCollection(); - this.metadataCollection.fetch(); - }, - onShow : function() { - this.metadata.show(new MetadataCollectionView({ collection : this.metadataCollection })); - } -}); \ No newline at end of file diff --git a/src/UI/Settings/Metadata/MetadataLayoutTemplate.hbs b/src/UI/Settings/Metadata/MetadataLayoutTemplate.hbs deleted file mode 100644 index a32fe464e..000000000 --- a/src/UI/Settings/Metadata/MetadataLayoutTemplate.hbs +++ /dev/null @@ -1,3 +0,0 @@ -<div class="row"> - <div class="col-md-12" id="x-metadata-providers"/> -</div> diff --git a/src/UI/Settings/Metadata/MetadataModel.js b/src/UI/Settings/Metadata/MetadataModel.js deleted file mode 100644 index 288e45362..000000000 --- a/src/UI/Settings/Metadata/MetadataModel.js +++ /dev/null @@ -1,3 +0,0 @@ -var ProviderSettingsModelBase = require('../ProviderSettingsModelBase'); - -module.exports = ProviderSettingsModelBase.extend({}); \ No newline at end of file diff --git a/src/UI/Settings/Metadata/metadata.less b/src/UI/Settings/Metadata/metadata.less deleted file mode 100644 index 566114a39..000000000 --- a/src/UI/Settings/Metadata/metadata.less +++ /dev/null @@ -1,37 +0,0 @@ -@import "../../Shared/Styles/card"; - -.metadata-list { - li { - display: inline-block; - vertical-align: top; - } -} - -.metadata-item { - - .card; - .clickable; - - width: 200px; - height: 230px; - padding: 10px 15px; - - h3 { - margin-top: 0px; - display: inline-block; - width: 180px; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - } - - .btn-group { - margin-top: 8px; - } - - .label { - margin-top : 3px; - display : block; - text-align : center; - } -} \ No newline at end of file diff --git a/src/UI/Settings/Notifications/Add/NotificationAddCollectionView.js b/src/UI/Settings/Notifications/Add/NotificationAddCollectionView.js deleted file mode 100644 index 68a304fd9..000000000 --- a/src/UI/Settings/Notifications/Add/NotificationAddCollectionView.js +++ /dev/null @@ -1,8 +0,0 @@ -var ThingyAddCollectionView = require('../../ThingyAddCollectionView'); -var AddItemView = require('./NotificationAddItemView'); - -module.exports = ThingyAddCollectionView.extend({ - itemView : AddItemView, - itemViewContainer : '.add-notifications .items', - template : 'Settings/Notifications/Add/NotificationAddCollectionViewTemplate' -}); \ No newline at end of file diff --git a/src/UI/Settings/Notifications/Add/NotificationAddCollectionViewTemplate.hbs b/src/UI/Settings/Notifications/Add/NotificationAddCollectionViewTemplate.hbs deleted file mode 100644 index 0075fa504..000000000 --- a/src/UI/Settings/Notifications/Add/NotificationAddCollectionViewTemplate.hbs +++ /dev/null @@ -1,14 +0,0 @@ -<div class="modal-content"> - <div class="modal-header"> - <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> - <h3>Add Notification</h3> - </div> - <div class="modal-body"> - <div class="add-notifications add-thingies"> - <ul class="items"></ul> - </div> - </div> - <div class="modal-footer"> - <button class="btn" data-dismiss="modal">Close</button> - </div> -</div> diff --git a/src/UI/Settings/Notifications/Add/NotificationAddItemView.js b/src/UI/Settings/Notifications/Add/NotificationAddItemView.js deleted file mode 100644 index 04b7c8944..000000000 --- a/src/UI/Settings/Notifications/Add/NotificationAddItemView.js +++ /dev/null @@ -1,64 +0,0 @@ -var _ = require('underscore'); -var $ = require('jquery'); -var AppLayout = require('../../../AppLayout'); -var Marionette = require('marionette'); -var EditView = require('../Edit/NotificationEditView'); - -module.exports = Marionette.ItemView.extend({ - template : 'Settings/Notifications/Add/NotificationAddItemViewTemplate', - tagName : 'li', - className : 'add-thingy-item', - - events : { - 'click .x-preset' : '_addPreset', - 'click' : '_add' - }, - - initialize : function(options) { - this.targetCollection = options.targetCollection; - }, - - _addPreset : function(e) { - var presetName = $(e.target).closest('.x-preset').attr('data-id'); - - var presetData = _.where(this.model.get('presets'), { name : presetName })[0]; - - this.model.set(presetData); - - this.model.set({ - id : undefined, - onGrab : this.model.get('supportsOnGrab'), - onDownload : this.model.get('supportsOnDownload'), - onUpgrade : this.model.get('supportsOnUpgrade'), - onRename : this.model.get('supportsOnRename') - }); - - var editView = new EditView({ - model : this.model, - targetCollection : this.targetCollection - }); - - AppLayout.modalRegion.show(editView); - }, - - _add : function(e) { - if ($(e.target).closest('.btn,.btn-group').length !== 0 && $(e.target).closest('.x-custom').length === 0) { - return; - } - - this.model.set({ - id : undefined, - onGrab : this.model.get('supportsOnGrab'), - onDownload : this.model.get('supportsOnDownload'), - onUpgrade : this.model.get('supportsOnUpgrade'), - onRename : this.model.get('supportsOnRename') - }); - - var editView = new EditView({ - model : this.model, - targetCollection : this.targetCollection - }); - - AppLayout.modalRegion.show(editView); - } -}); \ No newline at end of file diff --git a/src/UI/Settings/Notifications/Add/NotificationAddItemViewTemplate.hbs b/src/UI/Settings/Notifications/Add/NotificationAddItemViewTemplate.hbs deleted file mode 100644 index 1cdb3dffc..000000000 --- a/src/UI/Settings/Notifications/Add/NotificationAddItemViewTemplate.hbs +++ /dev/null @@ -1,30 +0,0 @@ -<div class="add-thingy"> - <div> - {{implementationName}} - </div> - <div class="pull-right"> - {{#if_gt presets.length compare=0}} - <button class="btn btn-xs btn-default x-custom"> - Custom - </button> - <div class="btn-group"> - <button class="btn btn-xs btn-default dropdown-toggle" data-toggle="dropdown"> - Presets - <span class="caret"></span> - </button> - <ul class="dropdown-menu"> - {{#each presets}} - <li class="x-preset" data-id="{{name}}"> - <a>{{name}}</a> - </li> - {{/each}} - </ul> - </div> - {{/if_gt}} - {{#if infoLink}} - <a class="btn btn-xs btn-default x-info" href="{{infoLink}}"> - <i class="icon-lidarr-form-info"/> - </a> - {{/if}} - </div> -</div> \ No newline at end of file diff --git a/src/UI/Settings/Notifications/Add/NotificationSchemaModal.js b/src/UI/Settings/Notifications/Add/NotificationSchemaModal.js deleted file mode 100644 index 54b60973b..000000000 --- a/src/UI/Settings/Notifications/Add/NotificationSchemaModal.js +++ /dev/null @@ -1,18 +0,0 @@ -var AppLayout = require('../../../AppLayout'); -var SchemaCollection = require('../NotificationCollection'); -var AddCollectionView = require('./NotificationAddCollectionView'); - -module.exports = { - open : function(collection) { - var schemaCollection = new SchemaCollection(); - var originalUrl = schemaCollection.url; - schemaCollection.url = schemaCollection.url + '/schema'; - schemaCollection.fetch(); - schemaCollection.url = originalUrl; - var view = new AddCollectionView({ - collection : schemaCollection, - targetCollection : collection - }); - AppLayout.modalRegion.show(view); - } -}; \ No newline at end of file diff --git a/src/UI/Settings/Notifications/Delete/NotificationDeleteView.js b/src/UI/Settings/Notifications/Delete/NotificationDeleteView.js deleted file mode 100644 index f80ab92a7..000000000 --- a/src/UI/Settings/Notifications/Delete/NotificationDeleteView.js +++ /dev/null @@ -1,18 +0,0 @@ -var vent = require('vent'); -var Marionette = require('marionette'); - -module.exports = Marionette.ItemView.extend({ - template : 'Settings/Notifications/Delete/NotificationDeleteViewTemplate', - - events : { - 'click .x-confirm-delete' : '_delete' - }, - _delete : function() { - this.model.destroy({ - wait : true, - success : function() { - vent.trigger(vent.Commands.CloseModalCommand); - } - }); - } -}); \ No newline at end of file diff --git a/src/UI/Settings/Notifications/Delete/NotificationDeleteViewTemplate.hbs b/src/UI/Settings/Notifications/Delete/NotificationDeleteViewTemplate.hbs deleted file mode 100644 index 1e6a52b73..000000000 --- a/src/UI/Settings/Notifications/Delete/NotificationDeleteViewTemplate.hbs +++ /dev/null @@ -1,13 +0,0 @@ -<div class="modal-content"> - <div class="modal-header"> - <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> - <h3>Delete Notification</h3> - </div> - <div class="modal-body"> - <p>Are you sure you want to delete '{{name}}'?</p> - </div> - <div class="modal-footer"> - <button class="btn" data-dismiss="modal">Cancel</button> - <button class="btn btn-danger x-confirm-delete">Delete</button> - </div> -</div> \ No newline at end of file diff --git a/src/UI/Settings/Notifications/Edit/NotificationEditView.js b/src/UI/Settings/Notifications/Edit/NotificationEditView.js deleted file mode 100644 index f305d4c0e..000000000 --- a/src/UI/Settings/Notifications/Edit/NotificationEditView.js +++ /dev/null @@ -1,141 +0,0 @@ -var _ = require('underscore'); -var $ = require('jquery'); -var vent = require('vent'); -var Marionette = require('marionette'); -var DeleteView = require('../Delete/NotificationDeleteView'); -var AsModelBoundView = require('../../../Mixins/AsModelBoundView'); -var AsValidatedView = require('../../../Mixins/AsValidatedView'); -var AsEditModalView = require('../../../Mixins/AsEditModalView'); -require('../../../Form/FormBuilder'); -require('../../../Mixins/TagInput'); -require('../../../Mixins/FileBrowser'); -require('bootstrap.tagsinput'); - -var view = Marionette.ItemView.extend({ - template : 'Settings/Notifications/Edit/NotificationEditViewTemplate', - - ui : { - onDownloadToggle : '.x-on-download', - onUpgradeSection : '.x-on-upgrade', - tags : '.x-tags', - modalBody : '.x-modal-body', - formTag : '.x-form-tag', - path : '.x-path', - authorizedNotificationButton : '.AuthorizeNotification' - }, - - events : { - 'click .x-back' : '_back', - 'change .x-on-download' : '_onDownloadChanged', - 'click .AuthorizeNotification' : '_onAuthorizeNotification' - }, - - _deleteView : DeleteView, - - initialize : function(options) { - this.targetCollection = options.targetCollection; - }, - - onRender : function() { - this._onDownloadChanged(); - - this.ui.tags.tagInput({ - model : this.model, - property : 'tags' - }); - - this.ui.formTag.tagsinput({ - trimValue : true, - tagClass : 'label label-default' - }); - }, - - onShow : function() { - if (this.ui.path.length > 0) { - this.ui.modalBody.addClass('modal-overflow'); - } - - this.ui.path.fileBrowser(); - }, - - _onAfterSave : function() { - this.targetCollection.add(this.model, { merge : true }); - vent.trigger(vent.Commands.CloseModalCommand); - }, - - _onAfterSaveAndAdd : function() { - this.targetCollection.add(this.model, { merge : true }); - - require('../Add/NotificationSchemaModal').open(this.targetCollection); - }, - - _back : function() { - if (this.model.isNew()) { - this.model.destroy(); - } - - require('../Add/NotificationSchemaModal').open(this.targetCollection); - }, - - _onDownloadChanged : function() { - var checked = this.ui.onDownloadToggle.prop('checked'); - - if (checked) { - this.ui.onUpgradeSection.show(); - } else { - this.ui.onUpgradeSection.hide(); - } - }, - - _onAuthorizeNotification : function() { - this.ui.indicator.show(); - - var self = this; - var callbackUrl = window.location.origin + window.NzbDrone.UrlBase + '/oauth.html'; - - var promise = this.model.requestAction('startOAuth', { callbackUrl: callbackUrl }) - .then(function(response) { - return self._showOAuthWindow(response.oauthUrl); - }) - .then(function(responseQueryParams) { - return self.model.requestAction('getOAuthToken', responseQueryParams); - }) - .then(function(response) { - self.model.setFieldValue('AccessToken', response.accessToken); - self.model.setFieldValue('AccessTokenSecret', response.accessTokenSecret); - }); - - promise.always(function() { - self.ui.indicator.hide(); - }); - }, - - _showOAuthWindow : function(oauthUrl) { - var promise = $.Deferred(); - - window.open(oauthUrl); - var selfWindow = window; - selfWindow.onCompleteOauth = function(query, callback) { - delete selfWindow.onCompleteOauth; - - var queryParams = {}; - var splitQuery = query.substring(1).split('&'); - _.each(splitQuery, function (param) { - var paramSplit = param.split('='); - queryParams[paramSplit[0]] = paramSplit[1]; - }); - - callback(); - - promise.resolve(queryParams); - }; - - return promise; - } -}); - -AsModelBoundView.call(view); -AsValidatedView.call(view); -AsEditModalView.call(view); - -module.exports = view; \ No newline at end of file diff --git a/src/UI/Settings/Notifications/Edit/NotificationEditViewTemplate.hbs b/src/UI/Settings/Notifications/Edit/NotificationEditViewTemplate.hbs deleted file mode 100644 index 148d78118..000000000 --- a/src/UI/Settings/Notifications/Edit/NotificationEditViewTemplate.hbs +++ /dev/null @@ -1,148 +0,0 @@ -<div class="modal-content"> - <div class="modal-header"> - <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> - {{#if id}} - <h3>Edit - {{implementationName}}</h3> - {{else}} - <h3>Add - {{implementationName}}</h3> - {{/if}} - </div> - <div class="modal-body notification-modal x-modal"> - <div class="form-horizontal"> - <div class="form-group"> - <label class="col-sm-3 control-label">Name</label> - - <div class="col-sm-5"> - <input type="text" name="name" class="form-control"/> - </div> - </div> - - <div class="form-group"> - <label class="col-sm-3 control-label">On Grab</label> - - <div class="col-sm-5"> - <div class="input-group"> - <label class="checkbox toggle well"> - <input type="checkbox" name="onGrab" {{#unless supportsOnGrab}}disabled="disabled"{{/unless}}/> - <p> - <span>Yes</span> - <span>No</span> - </p> - - <div class="btn btn-primary slide-button"/> - </label> - - <span class="help-inline-checkbox"> - <i class="icon-lidarr-form-info" title="Be notified when albums are available for download and has been sent to a download client"/> - </span> - </div> - </div> - </div> - - <div class="form-group"> - <label class="col-sm-3 control-label">On Download</label> - - <div class="col-sm-5"> - <div class="input-group"> - <label class="checkbox toggle well"> - <input type="checkbox" name="onDownload" class="x-on-download" {{#unless supportsOnDownload}}disabled="disabled"{{/unless}}/> - <p> - <span>Yes</span> - <span>No</span> - </p> - - <div class="btn btn-primary slide-button"/> - </label> - - <span class="help-inline-checkbox"> - <i class="icon-lidarr-form-info" title="Be notified when albums are successfully downloaded"/> - </span> - </div> - </div> - </div> - - <div class="form-group x-on-upgrade"> - <label class="col-sm-3 control-label">On Upgrade</label> - - <div class="col-sm-5"> - <div class="input-group"> - <label class="checkbox toggle well"> - <input type="checkbox" name="onUpgrade" {{#unless supportsOnUpgrade}}disabled="disabled"{{/unless}}/> - <p> - <span>Yes</span> - <span>No</span> - </p> - - <div class="btn btn-primary slide-button"/> - </label> - - <span class="help-inline-checkbox"> - <i class="icon-lidarr-form-info" title="Be notified when albums are upgraded to a better quality"/> - </span> - </div> - </div> - </div> - - <div class="form-group x-on-upgrade"> - <label class="col-sm-3 control-label">On Rename</label> - - <div class="col-sm-5"> - <div class="input-group"> - <label class="checkbox toggle well"> - <input type="checkbox" name="onRename" {{#unless supportsOnRename}}disabled="disabled"{{/unless}}/> - <p> - <span>Yes</span> - <span>No</span> - </p> - - <div class="btn btn-primary slide-button"/> - </label> - - <span class="help-inline-checkbox"> - <i class="icon-lidarr-form-info" title="Be notified when albums are renamed"/> - </span> - </div> - </div> - </div> - - <div class="form-group"> - <label class="col-sm-3 control-label">Filter Series Tags</label> - - <div class="col-sm-5"> - <input type="text" class="form-control x-tags"> - </div> - - <div class="col-sm-1 help-inline"> - <i class="icon-lidarr-form-info" title="Only send notifications for artist with matching tags"/> - </div> - </div> - - <hr> - - {{formBuilder}} - </div> - </div> - <div class="modal-footer"> - {{#if id}} - <button class="btn btn-danger pull-left x-delete">Delete</button> - {{else}} - <button class="btn pull-left x-back">Back</button> - {{/if}} - - <span class="indicator x-indicator"><i class="icon-lidarr-spinner fa-spin"></i></span> - <button class="btn x-test">test <i class="x-test-icon icon-lidarr-test"/></button> - <button class="btn" data-dismiss="modal">Cancel</button> - - <div class="btn-group"> - <button class="btn btn-primary x-save">Save</button> - <button class="btn btn-icon-only btn-primary dropdown-toggle" data-toggle="dropdown"> - <span class="caret"></span> - </button> - <ul class="dropdown-menu"> - <li class="save-and-add x-save-and-add"> - save and add - </li> - </ul> - </div> - </div> -</div> diff --git a/src/UI/Settings/Notifications/NotificationCollection.js b/src/UI/Settings/Notifications/NotificationCollection.js deleted file mode 100644 index 25160c33a..000000000 --- a/src/UI/Settings/Notifications/NotificationCollection.js +++ /dev/null @@ -1,7 +0,0 @@ -var Backbone = require('backbone'); -var NotificationModel = require('./NotificationModel'); - -module.exports = Backbone.Collection.extend({ - model : NotificationModel, - url : window.NzbDrone.ApiRoot + '/notification' -}); \ No newline at end of file diff --git a/src/UI/Settings/Notifications/NotificationCollectionView.js b/src/UI/Settings/Notifications/NotificationCollectionView.js deleted file mode 100644 index c25433e5c..000000000 --- a/src/UI/Settings/Notifications/NotificationCollectionView.js +++ /dev/null @@ -1,25 +0,0 @@ -var Marionette = require('marionette'); -var ItemView = require('./NotificationItemView'); -var SchemaModal = require('./Add/NotificationSchemaModal'); - -module.exports = Marionette.CompositeView.extend({ - itemView : ItemView, - itemViewContainer : '.notification-list', - template : 'Settings/Notifications/NotificationCollectionViewTemplate', - - ui : { - 'addCard' : '.x-add-card' - }, - - events : { - 'click .x-add-card' : '_openSchemaModal' - }, - - appendHtml : function(collectionView, itemView, index) { - collectionView.ui.addCard.parent('li').before(itemView.el); - }, - - _openSchemaModal : function() { - SchemaModal.open(this.collection); - } -}); \ No newline at end of file diff --git a/src/UI/Settings/Notifications/NotificationCollectionViewTemplate.hbs b/src/UI/Settings/Notifications/NotificationCollectionViewTemplate.hbs deleted file mode 100644 index 47e34c168..000000000 --- a/src/UI/Settings/Notifications/NotificationCollectionViewTemplate.hbs +++ /dev/null @@ -1,16 +0,0 @@ -<fieldset> - <legend>Connections</legend> - <div class="row"> - <div class="col-md-12"> - <ul class="notification-list thingies"> - <li> - <div class="notification-item thingy add-card x-add-card"> - <span class="center well"> - <i class="icon-lidarr-add"/> - </span> - </div> - </li> - </ul> - </div> - </div> -</fieldset> \ No newline at end of file diff --git a/src/UI/Settings/Notifications/NotificationItemView.js b/src/UI/Settings/Notifications/NotificationItemView.js deleted file mode 100644 index 6f3665b2f..000000000 --- a/src/UI/Settings/Notifications/NotificationItemView.js +++ /dev/null @@ -1,24 +0,0 @@ -var AppLayout = require('../../AppLayout'); -var Marionette = require('marionette'); -var EditView = require('./Edit/NotificationEditView'); - -module.exports = Marionette.ItemView.extend({ - template : 'Settings/Notifications/NotificationItemViewTemplate', - tagName : 'li', - - events : { - 'click' : '_edit' - }, - - initialize : function() { - this.listenTo(this.model, 'sync', this.render); - }, - - _edit : function() { - var view = new EditView({ - model : this.model, - targetCollection : this.model.collection - }); - AppLayout.modalRegion.show(view); - } -}); \ No newline at end of file diff --git a/src/UI/Settings/Notifications/NotificationItemViewTemplate.hbs b/src/UI/Settings/Notifications/NotificationItemViewTemplate.hbs deleted file mode 100644 index bbfa0a3af..000000000 --- a/src/UI/Settings/Notifications/NotificationItemViewTemplate.hbs +++ /dev/null @@ -1,47 +0,0 @@ -<div class="notification-item thingy"> - <div> - <h3>{{name}}</h3> - </div> - - <div class="settings"> - {{#if supportsOnGrab}} - {{#if onGrab}} - <span class="label label-success">On Grab</span> - {{else}} - <span class="label label-default">On Grab</span> - {{/if}} - {{else}} - <span class="label label-default label-disabled">On Grab</span> - {{/if}} - - {{#if supportsOnDownload}} - {{#if onDownload}} - <span class="label label-success">On Download</span> - {{else}} - <span class="label label-default">On Download</span> - {{/if}} - {{else}} - <span class="label label-default label-disabled">On Download</span> - {{/if}} - - {{#if supportsOnUpgrade}} - {{#if onUpgrade}} - <span class="label label-success">On Upgrade</span> - {{else}} - <span class="label label-default">On Upgrade</span> - {{/if}} - {{else}} - <span class="label label-default label-disabled">On Upgrade</span> - {{/if}} - - {{#if supportsOnRename}} - {{#if onRename}} - <span class="label label-success">On Rename</span> - {{else}} - <span class="label label-default">On Rename</span> - {{/if}} - {{else}} - <span class="label label-default label-disabled">On Rename</span> - {{/if}} - </div> -</div> diff --git a/src/UI/Settings/Notifications/NotificationModel.js b/src/UI/Settings/Notifications/NotificationModel.js deleted file mode 100644 index 288e45362..000000000 --- a/src/UI/Settings/Notifications/NotificationModel.js +++ /dev/null @@ -1,3 +0,0 @@ -var ProviderSettingsModelBase = require('../ProviderSettingsModelBase'); - -module.exports = ProviderSettingsModelBase.extend({}); \ No newline at end of file diff --git a/src/UI/Settings/Notifications/notifications.less b/src/UI/Settings/Notifications/notifications.less deleted file mode 100644 index fb3a0e4a0..000000000 --- a/src/UI/Settings/Notifications/notifications.less +++ /dev/null @@ -1,37 +0,0 @@ -@import "../../Shared/Styles/clickable.less"; - -//.notifications { -// width: -webkit-fit-content; -// width: -moz-fit-content; -// width: fit-content; -//} - -.notification-item { - .clickable; - - width: 290px; - height: 115px; - padding: 20px 20px; - - .settings { - margin-top: 5px; - - .label { - display : inline-block; - margin-bottom : 2px; - padding : 4px 6px 3px 6px; - } - } - - &.add-card { - .center { - margin-top: -4px; - } - } -} - -.add-notifications { - li.add-thingy-item { - width: 40%; - } -} \ No newline at end of file diff --git a/src/UI/Settings/Profile/AllowedLabeler.js b/src/UI/Settings/Profile/AllowedLabeler.js deleted file mode 100644 index c5da373c3..000000000 --- a/src/UI/Settings/Profile/AllowedLabeler.js +++ /dev/null @@ -1,19 +0,0 @@ -var Handlebars = require('handlebars'); -var _ = require('underscore'); - -Handlebars.registerHelper('allowedLabeler', function() { - var ret = ''; - var cutoff = this.cutoff; - - _.each(this.items, function(item) { - if (item.allowed) { - if (item.quality.id === cutoff.id) { - ret += '<li><span class="label label-info" title="Cutoff">' + item.quality.name + '</span></li>'; - } else { - ret += '<li><span class="label label-default">' + item.quality.name + '</span></li>'; - } - } - }); - - return new Handlebars.SafeString(ret); -}); \ No newline at end of file diff --git a/src/UI/Settings/Profile/Delay/DelayProfileCollection.js b/src/UI/Settings/Profile/Delay/DelayProfileCollection.js deleted file mode 100644 index fcb240a5b..000000000 --- a/src/UI/Settings/Profile/Delay/DelayProfileCollection.js +++ /dev/null @@ -1,7 +0,0 @@ -var Backbone = require('backbone'); -var DelayProfileModel = require('./DelayProfileModel'); - -module.exports = Backbone.Collection.extend({ - model : DelayProfileModel, - url : window.NzbDrone.ApiRoot + '/delayprofile' -}); \ No newline at end of file diff --git a/src/UI/Settings/Profile/Delay/DelayProfileCollectionView.js b/src/UI/Settings/Profile/Delay/DelayProfileCollectionView.js deleted file mode 100644 index 87cc93d2d..000000000 --- a/src/UI/Settings/Profile/Delay/DelayProfileCollectionView.js +++ /dev/null @@ -1,13 +0,0 @@ -var BackboneSortableCollectionView = require('backbone.collectionview'); -var DelayProfileItemView = require('./DelayProfileItemView'); - -module.exports = BackboneSortableCollectionView.extend({ - className : 'delay-profiles', - modelView : DelayProfileItemView, - - events : { - 'click li, td' : '_listItem_onMousedown', - 'dblclick li, td' : '_listItem_onDoubleClick', - 'keydown' : '_onKeydown' - } -}); \ No newline at end of file diff --git a/src/UI/Settings/Profile/Delay/DelayProfileItemView.js b/src/UI/Settings/Profile/Delay/DelayProfileItemView.js deleted file mode 100644 index b8d89364b..000000000 --- a/src/UI/Settings/Profile/Delay/DelayProfileItemView.js +++ /dev/null @@ -1,25 +0,0 @@ -var $ = require('jquery'); -var AppLayout = require('../../../AppLayout'); -var Marionette = require('marionette'); -var EditView = require('./Edit/DelayProfileEditView'); - -module.exports = Marionette.ItemView.extend({ - template : 'Settings/Profile/Delay/DelayProfileItemViewTemplate', - className : 'row', - - events : { - 'click .x-edit' : '_edit' - }, - - initialize : function() { - this.listenTo(this.model, 'sync', this.render); - }, - - _edit : function() { - var view = new EditView({ - model : this.model, - targetCollection : this.model.collection - }); - AppLayout.modalRegion.show(view); - } -}); \ No newline at end of file diff --git a/src/UI/Settings/Profile/Delay/DelayProfileItemViewTemplate.hbs b/src/UI/Settings/Profile/Delay/DelayProfileItemViewTemplate.hbs deleted file mode 100644 index 5f7450195..000000000 --- a/src/UI/Settings/Profile/Delay/DelayProfileItemViewTemplate.hbs +++ /dev/null @@ -1,57 +0,0 @@ - <div class="col-sm-2"> - {{#if enableUsenet}} - {{#if enableTorrent}} - {{#if_eq preferredProtocol compare="usenet"}} - Prefer Usenet - {{else}} - Prefer Torrent - {{/if_eq}} - {{else}} - Only Usenet - {{/if}} - {{else}} - Only Torrent - {{/if}} - </div> - <div class="col-sm-2"> - {{#if enableUsenet}} - {{#if_eq usenetDelay compare="0"}} - No delay - {{else}} - {{#if_eq usenetDelay compare="1"}} - 1 minute - {{else}} - {{usenetDelay}} minutes - {{/if_eq}} - {{/if_eq}} - {{else}} - - - {{/if}} - </div> - <div class="col-sm-2"> - {{#if enableTorrent}} - {{#if_eq torrentDelay compare="0"}} - No delay - {{else}} - {{#if_eq torrentDelay compare="1"}} - 1 minute - {{else}} - {{torrentDelay}} minutes - {{/if_eq}} - {{/if_eq}} - {{else}} - - - {{/if}} - </div> - <div class="col-sm-5"> - {{tagDisplay tags}} - </div> - <div class="col-sm-1"> - <div class="pull-right"> - {{#unless_eq id compare="1"}} - <i class="drag-handle icon-lidarr-reorder x-drag-handle" title="Reorder"/> - {{/unless_eq}} - - <i class="icon-lidarr-edit x-edit" title="Edit"></i> - </div> - </div> \ No newline at end of file diff --git a/src/UI/Settings/Profile/Delay/DelayProfileLayout.js b/src/UI/Settings/Profile/Delay/DelayProfileLayout.js deleted file mode 100644 index 024be5a99..000000000 --- a/src/UI/Settings/Profile/Delay/DelayProfileLayout.js +++ /dev/null @@ -1,101 +0,0 @@ -var $ = require('jquery'); -var _ = require('underscore'); -var vent = require('vent'); -var AppLayout = require('../../../AppLayout'); -var Marionette = require('marionette'); -var Backbone = require('backbone'); -var DelayProfileCollectionView = require('./DelayProfileCollectionView'); -var EditView = require('./Edit/DelayProfileEditView'); -var Model = require('./DelayProfileModel'); - -module.exports = Marionette.Layout.extend({ - template : 'Settings/Profile/Delay/DelayProfileLayoutTemplate', - - regions : { - delayProfiles : '.x-rows' - }, - - events : { - 'click .x-add' : '_add' - }, - - initialize : function(options) { - this.collection = options.collection; - - this._updateOrderedCollection(); - - this.listenTo(this.collection, 'sync', this._updateOrderedCollection); - this.listenTo(this.collection, 'add', this._updateOrderedCollection); - this.listenTo(this.collection, 'remove', function() { - this.collection.fetch(); - }); - }, - - onRender : function() { - - this.sortableListView = new DelayProfileCollectionView({ - sortable : true, - collection : this.orderedCollection, - - sortableOptions : { - handle : '.x-drag-handle' - }, - - sortableModelsFilter : function(model) { - return model.get('id') !== 1; - } - }); - - this.delayProfiles.show(this.sortableListView); - - this.listenTo(this.sortableListView, 'sortStop', this._updateOrder); - }, - - _updateOrder : function() { - var self = this; - - this.collection.forEach(function(model) { - if (model.get('id') === 1) { - return; - } - - var orderedModel = self.orderedCollection.get(model); - var order = self.orderedCollection.indexOf(orderedModel) + 1; - - if (model.get('order') !== order) { - model.set('order', order); - model.save(); - } - }); - }, - - _add : function() { - var model = new Model({ - enableUsenet : true, - enableTorrent : true, - preferredProtocol : 'usenet', - usenetDelay : 0, - torrentDelay : 0, - order : this.collection.length, - tags : [] - }); - - model.collection = this.collection; - var view = new EditView({ - model : model, - targetCollection : this.collection - }); - - AppLayout.modalRegion.show(view); - }, - - _updateOrderedCollection : function() { - if (!this.orderedCollection) { - this.orderedCollection = new Backbone.Collection(); - } - - this.orderedCollection.reset(_.sortBy(this.collection.models, function(model) { - return model.get('order'); - })); - } -}); \ No newline at end of file diff --git a/src/UI/Settings/Profile/Delay/DelayProfileLayoutTemplate.hbs b/src/UI/Settings/Profile/Delay/DelayProfileLayoutTemplate.hbs deleted file mode 100644 index 7787c1456..000000000 --- a/src/UI/Settings/Profile/Delay/DelayProfileLayoutTemplate.hbs +++ /dev/null @@ -1,24 +0,0 @@ -<fieldset> - <legend>Delay Profiles</legend> - - <div class="col-md-12"> - <div class="rule-setting-list"> - <div class="rule-setting-header x-header hidden-xs"> - <div class="row"> - <span class="col-sm-2">Protocol</span> - <span class="col-sm-2">Usenet Delay</span> - <span class="col-sm-2">Torrent Delay</span> - <span class="col-sm-5">Tags</span> - </div> - </div> - <div class="rows x-rows"></div> - <div class="rule-setting-footer"> - <div class="pull-right"> - <span class="add-rule-setting-mapping"> - <i class="icon-lidarr-add x-add" title="Add new delay profile" /> - </span> - </div> - </div> - </div> - </div> -</fieldset> diff --git a/src/UI/Settings/Profile/Delay/DelayProfileModel.js b/src/UI/Settings/Profile/Delay/DelayProfileModel.js deleted file mode 100644 index 3986a5948..000000000 --- a/src/UI/Settings/Profile/Delay/DelayProfileModel.js +++ /dev/null @@ -1,3 +0,0 @@ -var Backbone = require('backbone'); - -module.exports = Backbone.Model.extend({}); \ No newline at end of file diff --git a/src/UI/Settings/Profile/Delay/Delete/DelayProfileDeleteView.js b/src/UI/Settings/Profile/Delay/Delete/DelayProfileDeleteView.js deleted file mode 100644 index 6b948d782..000000000 --- a/src/UI/Settings/Profile/Delay/Delete/DelayProfileDeleteView.js +++ /dev/null @@ -1,21 +0,0 @@ -var vent = require('vent'); -var Marionette = require('marionette'); - -module.exports = Marionette.ItemView.extend({ - template : 'Settings/Profile/Delay/Delete/DelayProfileDeleteViewTemplate', - - events : { - 'click .x-confirm-delete' : '_delete' - }, - - _delete : function() { - var collection = this.model.collection; - - this.model.destroy({ - wait : true, - success : function() { - vent.trigger(vent.Commands.CloseModalCommand); - } - }); - } -}); \ No newline at end of file diff --git a/src/UI/Settings/Profile/Delay/Delete/DelayProfileDeleteViewTemplate.hbs b/src/UI/Settings/Profile/Delay/Delete/DelayProfileDeleteViewTemplate.hbs deleted file mode 100644 index dc6b5125f..000000000 --- a/src/UI/Settings/Profile/Delay/Delete/DelayProfileDeleteViewTemplate.hbs +++ /dev/null @@ -1,13 +0,0 @@ -<div class="modal-content"> - <div class="modal-header"> - <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> - <h3>Delete Delay Profile</h3> - </div> - <div class="modal-body"> - <p>Are you sure you want to delete this delay profile?</p> - </div> - <div class="modal-footer"> - <button class="btn" data-dismiss="modal">Cancel</button> - <button class="btn btn-danger x-confirm-delete">Delete</button> - </div> -</div> diff --git a/src/UI/Settings/Profile/Delay/Edit/DelayProfileEditView.js b/src/UI/Settings/Profile/Delay/Edit/DelayProfileEditView.js deleted file mode 100644 index 277527f79..000000000 --- a/src/UI/Settings/Profile/Delay/Edit/DelayProfileEditView.js +++ /dev/null @@ -1,122 +0,0 @@ -var vent = require('vent'); -var AppLayout = require('../../../../AppLayout'); -var Marionette = require('marionette'); -var DeleteView = require('../Delete/DelayProfileDeleteView'); -var AsModelBoundView = require('../../../../Mixins/AsModelBoundView'); -var AsValidatedView = require('../../../../Mixins/AsValidatedView'); -var AsEditModalView = require('../../../../Mixins/AsEditModalView'); -require('../../../../Mixins/TagInput'); -require('bootstrap'); - -var view = Marionette.ItemView.extend({ - template : 'Settings/Profile/Delay/Edit/DelayProfileEditViewTemplate', - - _deleteView : DeleteView, - - ui : { - tags : '.x-tags', - usenetDelay : '.x-usenet-delay', - torrentDelay : '.x-torrent-delay', - protocol : '.x-protocol' - }, - - events : { - 'change .x-protocol' : '_updateModel' - }, - - initialize : function(options) { - this.targetCollection = options.targetCollection; - }, - - onRender : function() { - if (this.model.id !== 1) { - this.ui.tags.tagInput({ - model : this.model, - property : 'tags' - }); - } - - this._toggleControls(); - }, - - _onAfterSave : function() { - this.targetCollection.add(this.model, { merge : true }); - vent.trigger(vent.Commands.CloseModalCommand); - }, - - _updateModel : function() { - var protocol = this.ui.protocol.val(); - - if (protocol === 'preferUsenet') { - this.model.set({ - enableUsenet : true, - enableTorrent : true, - preferredProtocol : 'usenet' - }); - } - - if (protocol === 'preferTorrent') { - this.model.set({ - enableUsenet : true, - enableTorrent : true, - preferredProtocol : 'torrent' - }); - } - - if (protocol === 'onlyUsenet') { - this.model.set({ - enableUsenet : true, - enableTorrent : false, - preferredProtocol : 'usenet' - }); - } - - if (protocol === 'onlyTorrent') { - this.model.set({ - enableUsenet : false, - enableTorrent : true, - preferredProtocol : 'torrent' - }); - } - - this._toggleControls(); - }, - - _toggleControls : function() { - var enableUsenet = this.model.get('enableUsenet'); - var enableTorrent = this.model.get('enableTorrent'); - var preferred = this.model.get('preferredProtocol'); - - if (preferred === 'usenet') { - this.ui.protocol.val('preferUsenet'); - } - - else { - this.ui.protocol.val('preferTorrent'); - } - - if (enableUsenet) { - this.ui.usenetDelay.show(); - } - - else { - this.ui.protocol.val('onlyTorrent'); - this.ui.usenetDelay.hide(); - } - - if (enableTorrent) { - this.ui.torrentDelay.show(); - } - - else { - this.ui.protocol.val('onlyUsenet'); - this.ui.torrentDelay.hide(); - } - } -}); - -AsModelBoundView.call(view); -AsValidatedView.call(view); -AsEditModalView.call(view); - -module.exports = view; \ No newline at end of file diff --git a/src/UI/Settings/Profile/Delay/Edit/DelayProfileEditViewTemplate.hbs b/src/UI/Settings/Profile/Delay/Edit/DelayProfileEditViewTemplate.hbs deleted file mode 100644 index dedef6482..000000000 --- a/src/UI/Settings/Profile/Delay/Edit/DelayProfileEditViewTemplate.hbs +++ /dev/null @@ -1,80 +0,0 @@ -<div class="modal-content"> - <div class="modal-header"> - <button type="button" class="close" aria-hidden="true" data-dismiss="modal">×</button> - {{#if id}} - <h3>Edit - Delay Profile</h3> - {{else}} - <h3>Add - Delay Profile</h3> - {{/if}} - </div> - <div class="modal-body indexer-modal"> - <div class="form-horizontal"> - <div class="form-group"> - <label class="col-sm-3 control-label">Protocol</label> - - <div class="col-sm-1 col-sm-push-5 help-inline"> - <i class="icon-lidarr-form-info" title="Choose which protocol(s) to use and which one is preferred when choosing between otherwise equal releases" /> - </div> - - <div class="col-sm-5 col-sm-pull-1"> - <select class="form-control x-protocol"> - <option value="preferUsenet">Prefer Usenet</option> - <option value="preferTorrent">Prefer Torrent</option> - <option value="onlyUsenet">Only Usenet</option> - <option value="onlyTorrent">Only Torrent</option> - </select> - </div> - </div> - - <div class="form-group x-usenet-delay"> - <label class="col-sm-3 control-label">Usenet Delay</label> - - <div class="col-sm-1 col-sm-push-5 help-inline"> - <i class="icon-lidarr-form-info" title="Delay in minutes to wait before grabbing a release from Usenet" /> - </div> - - <div class="col-sm-5 col-sm-pull-1"> - <input type="number" class="form-control" name="usenetDelay"/> - </div> - </div> - - <div class="form-group x-torrent-delay"> - <label class="col-sm-3 control-label">Torrent Delay</label> - - <div class="col-sm-1 col-sm-push-5 help-inline"> - <i class="icon-lidarr-form-info" title="Delay in minutes to wait before grabbing a torrent" /> - </div> - - <div class="col-sm-5 col-sm-pull-1"> - <input type="number" class="form-control" name="torrentDelay"/> - </div> - </div> - - {{#if_eq id compare="1"}} - <div class="alert alert-info" role="alert">This is the default profile. It applies to all series that don't have an explicit profile.</div> - {{else}} - <div class="form-group"> - <label class="col-sm-3 control-label">Tags</label> - - <div class="col-sm-1 col-sm-push-5 help-inline"> - <i class="icon-lidarr-form-info" title="One or more tags to apply these rules to matching series" /> - </div> - - <div class="col-sm-5 col-sm-pull-1"> - <input type="text" class="form-control x-tags"> - </div> - </div> - {{/if_eq}} - </div> - </div> - <div class="modal-footer"> - {{#if id}} - {{#if_gt id compare="1"}} - <button class="btn btn-danger pull-left x-delete">Delete</button> - {{/if_gt}} - {{/if}} - <span class="indicator x-indicator"><i class="icon-lidarr-spinner fa-spin"></i></span> - <button class="btn" data-dismiss="modal">Cancel</button> - <button class="btn btn-primary x-save">Save</button> - </div> -</div> diff --git a/src/UI/Settings/Profile/DeleteProfileView.js b/src/UI/Settings/Profile/DeleteProfileView.js deleted file mode 100644 index 4b91e0e07..000000000 --- a/src/UI/Settings/Profile/DeleteProfileView.js +++ /dev/null @@ -1,16 +0,0 @@ -var vent = require('vent'); -var Marionette = require('marionette'); - -module.exports = Marionette.ItemView.extend({ - template : 'Settings/Profile/DeleteProfileViewTemplate', - - events : { - 'click .x-confirm-delete' : '_removeProfile' - }, - - _removeProfile : function() { - this.model.destroy({ wait : true }).done(function() { - vent.trigger(vent.Commands.CloseModalCommand); - }); - } -}); \ No newline at end of file diff --git a/src/UI/Settings/Profile/DeleteProfileViewTemplate.hbs b/src/UI/Settings/Profile/DeleteProfileViewTemplate.hbs deleted file mode 100644 index c9a826855..000000000 --- a/src/UI/Settings/Profile/DeleteProfileViewTemplate.hbs +++ /dev/null @@ -1,13 +0,0 @@ -<div class="modal-content"> - <div class="modal-header"> - <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> - <h3>Delete: {{name}}</h3> - </div> - <div class="modal-body"> - <p>Are you sure you want to delete '{{name}}'?</p> - </div> - <div class="modal-footer"> - <button class="btn" data-dismiss="modal">Cancel</button> - <button class="btn btn-danger x-confirm-delete">Delete</button> - </div> -</div> diff --git a/src/UI/Settings/Profile/Edit/EditProfileItemView.js b/src/UI/Settings/Profile/Edit/EditProfileItemView.js deleted file mode 100644 index 535fff211..000000000 --- a/src/UI/Settings/Profile/Edit/EditProfileItemView.js +++ /dev/null @@ -1,5 +0,0 @@ -var Marionette = require('marionette'); - -module.exports = Marionette.ItemView.extend({ - template : 'Settings/Profile/Edit/EditProfileItemViewTemplate' -}); \ No newline at end of file diff --git a/src/UI/Settings/Profile/Edit/EditProfileItemViewTemplate.hbs b/src/UI/Settings/Profile/Edit/EditProfileItemViewTemplate.hbs deleted file mode 100644 index 7feea8fac..000000000 --- a/src/UI/Settings/Profile/Edit/EditProfileItemViewTemplate.hbs +++ /dev/null @@ -1,3 +0,0 @@ -<i class="select-handle pull-left x-select" /> -<span class="quality-label">{{quality.name}}</span> -<i class="drag-handle pull-right icon-lidarr-reorder advanced-setting x-drag-handle" /> diff --git a/src/UI/Settings/Profile/Edit/EditProfileLayout.js b/src/UI/Settings/Profile/Edit/EditProfileLayout.js deleted file mode 100644 index 721b04479..000000000 --- a/src/UI/Settings/Profile/Edit/EditProfileLayout.js +++ /dev/null @@ -1,117 +0,0 @@ -var _ = require('underscore'); -var vent = require('vent'); -var AppLayout = require('../../../AppLayout'); -var Marionette = require('marionette'); -var Backbone = require('backbone'); -var EditProfileItemView = require('./EditProfileItemView'); -var QualitySortableCollectionView = require('./QualitySortableCollectionView'); -var EditProfileView = require('./EditProfileView'); -var DeleteView = require('../DeleteProfileView'); -var SeriesCollection = require('../../../Artist/ArtistCollection'); -var Config = require('../../../Config'); -var AsEditModalView = require('../../../Mixins/AsEditModalView'); - -var view = Marionette.Layout.extend({ - template : 'Settings/Profile/Edit/EditProfileLayoutTemplate', - - regions : { - fields : '#x-fields', - qualities : '#x-qualities' - }, - - ui : { - deleteButton : '.x-delete' - }, - - _deleteView : DeleteView, - - initialize : function(options) { - this.profileCollection = options.profileCollection; - this.itemsCollection = new Backbone.Collection(_.toArray(this.model.get('items')).reverse()); - this.listenTo(SeriesCollection, 'all', this._updateDisableStatus); - }, - - onRender : function() { - this._updateDisableStatus(); - }, - - onShow : function() { - this.fieldsView = new EditProfileView({ model : this.model }); - this._showFieldsView(); - var advancedShown = Config.getValueBoolean(Config.Keys.AdvancedSettings, false); - - this.sortableListView = new QualitySortableCollectionView({ - selectable : true, - selectMultiple : true, - clickToSelect : true, - clickToToggle : true, - sortable : advancedShown, - - sortableOptions : { - handle : '.x-drag-handle' - }, - - visibleModelsFilter : function(model) { - return model.get('quality').id !== 0 || advancedShown; - }, - - collection : this.itemsCollection, - model : this.model - }); - - this.sortableListView.setSelectedModels(this.itemsCollection.filter(function(item) { - return item.get('allowed') === true; - })); - this.qualities.show(this.sortableListView); - - this.listenTo(this.sortableListView, 'selectionChanged', this._selectionChanged); - this.listenTo(this.sortableListView, 'sortStop', this._updateModel); - }, - - _onBeforeSave : function() { - var cutoff = this.fieldsView.getCutoff(); - this.model.set('cutoff', cutoff); - }, - - _onAfterSave : function() { - this.profileCollection.add(this.model, { merge : true }); - vent.trigger(vent.Commands.CloseModalCommand); - }, - - _selectionChanged : function(newSelectedModels, oldSelectedModels) { - var addedModels = _.difference(newSelectedModels, oldSelectedModels); - var removeModels = _.difference(oldSelectedModels, newSelectedModels); - - _.each(removeModels, function(item) { - item.set('allowed', false); - }); - _.each(addedModels, function(item) { - item.set('allowed', true); - }); - this._updateModel(); - }, - - _updateModel : function() { - this.model.set('items', this.itemsCollection.toJSON().reverse()); - - this._showFieldsView(); - }, - - _showFieldsView : function() { - this.fields.show(this.fieldsView); - }, - - _updateDisableStatus : function() { - if (this._isQualityInUse()) { - this.ui.deleteButton.addClass('disabled'); - this.ui.deleteButton.attr('title', 'Can\'t delete a profile that is attached to a artist.'); - } else { - this.ui.deleteButton.removeClass('disabled'); - } - }, - - _isQualityInUse : function() { - return SeriesCollection.where({ 'profileId' : this.model.id }).length !== 0; - } -}); -module.exports = AsEditModalView.call(view); diff --git a/src/UI/Settings/Profile/Edit/EditProfileLayoutTemplate.hbs b/src/UI/Settings/Profile/Edit/EditProfileLayoutTemplate.hbs deleted file mode 100644 index d68c46fb6..000000000 --- a/src/UI/Settings/Profile/Edit/EditProfileLayoutTemplate.hbs +++ /dev/null @@ -1,36 +0,0 @@ -<div class="modal-content"> - <div class="modal-header"> - <button type="button" class="close" aria-hidden="true" data-dismiss="modal">×</button> - {{#if id}} - <h3>Edit</h3> - {{else}} - <h3>Add</h3> - {{/if}} -</div> - <div class="modal-body"> - <div class="form-horizontal"> - <div id="x-fields"></div> - <div class="form-group"> - <label class="col-sm-3 control-label">Qualities</label> - - <div class="col-sm-5"> - <div class="controls qualities-controls"> - <span id="x-qualities"></span> - </div> - </div> - - <div class="col-sm-1 help-inline"> - <i class="icon-lidarr-form-info" title="Qualities higher in the list are more preferred. Only checked qualities will be wanted."/> - </div> - </div> - </div> - </div> - <div class="modal-footer"> - {{#if id}} - <button class="btn btn-danger pull-left x-delete">Delete</button> - {{/if}} - <span class="indicator x-indicator"><i class="icon-lidarr-spinner fa-spin"></i></span> - <button class="btn" data-dismiss="modal">Cancel</button> - <button class="btn btn-primary x-save">Save</button> - </div> -</div> diff --git a/src/UI/Settings/Profile/Edit/EditProfileView.js b/src/UI/Settings/Profile/Edit/EditProfileView.js deleted file mode 100644 index 23535d9e6..000000000 --- a/src/UI/Settings/Profile/Edit/EditProfileView.js +++ /dev/null @@ -1,28 +0,0 @@ -var _ = require('underscore'); -var Marionette = require('marionette'); -var LanguageCollection = require('../Language/LanguageCollection'); -var Config = require('../../../Config'); -var AsModelBoundView = require('../../../Mixins/AsModelBoundView'); -var AsValidatedView = require('../../../Mixins/AsValidatedView'); - -var view = Marionette.ItemView.extend({ - template : 'Settings/Profile/Edit/EditProfileViewTemplate', - - ui : { cutoff : '.x-cutoff' }, - - templateHelpers : function() { - return { - languages : LanguageCollection.toJSON() - }; - }, - - getCutoff : function() { - var self = this; - - return _.findWhere(_.pluck(this.model.get('items'), 'quality'), { id : parseInt(self.ui.cutoff.val(), 10) }); - } -}); - -AsValidatedView.call(view); - -module.exports = AsModelBoundView.call(view); \ No newline at end of file diff --git a/src/UI/Settings/Profile/Edit/EditProfileViewTemplate.hbs b/src/UI/Settings/Profile/Edit/EditProfileViewTemplate.hbs deleted file mode 100644 index 894fb68cf..000000000 --- a/src/UI/Settings/Profile/Edit/EditProfileViewTemplate.hbs +++ /dev/null @@ -1,45 +0,0 @@ -<div class="form-group"> - <label class="col-sm-3 control-label">Name</label> - - <div class="col-sm-5"> - <input type="text" name="name" class="form-control"> - </div> -</div> - -<hr> - -<div class="form-group"> - <label class="col-sm-3 control-label">Language</label> - - <div class="col-sm-5"> - <select class="form-control" name="language"> - {{#each languages}} - {{#unless_eq nameLower compare="unknown"}} - <option value="{{nameLower}}">{{name}}</option> - {{/unless_eq}} - {{/each}} - </select> - </div> - - <div class="col-sm-1 help-inline"> - <i class="icon-lidarr-form-info" title="Artists assigned this profile will be looking for albums with the selected language"/> - </div> -</div> - -<div class="form-group"> - <label class="col-sm-3 control-label">Cutoff</label> - - <div class="col-sm-5"> - <select class="form-control x-cutoff" name="cutoff.id" validation-name="cutoff"> - {{#eachReverse items}} - {{#if allowed}} - <option value="{{quality.id}}">{{quality.name}}</option> - {{/if}} - {{/eachReverse}} - </select> - </div> - - <div class="col-sm-1 help-inline"> - <i class="icon-lidarr-form-info" title="Once this quality is reached Lidarr will no longer download albums"/> - </div> -</div> diff --git a/src/UI/Settings/Profile/Edit/QualitySortableCollectionView.js b/src/UI/Settings/Profile/Edit/QualitySortableCollectionView.js deleted file mode 100644 index 6fc6253aa..000000000 --- a/src/UI/Settings/Profile/Edit/QualitySortableCollectionView.js +++ /dev/null @@ -1,17 +0,0 @@ -var BackboneSortableCollectionView = require('backbone.collectionview'); -var EditProfileItemView = require('./EditProfileItemView'); - -module.exports = BackboneSortableCollectionView.extend({ - className : 'qualities', - modelView : EditProfileItemView, - - attributes : { - 'validation-name' : 'items' - }, - - events : { - 'click li, td' : '_listItem_onMousedown', - 'dblclick li, td' : '_listItem_onDoubleClick', - 'keydown' : '_onKeydown' - } -}); \ No newline at end of file diff --git a/src/UI/Settings/Profile/Language/LanguageCollection.js b/src/UI/Settings/Profile/Language/LanguageCollection.js deleted file mode 100644 index d016c0441..000000000 --- a/src/UI/Settings/Profile/Language/LanguageCollection.js +++ /dev/null @@ -1,12 +0,0 @@ -var Backbone = require('backbone'); -var LanguageModel = require('./LanguageModel'); - -var LanuageCollection = Backbone.Collection.extend({ - model : LanguageModel, - url : window.NzbDrone.ApiRoot + '/language' -}); - -var languages = new LanuageCollection(); -languages.fetch(); - -module.exports = languages; \ No newline at end of file diff --git a/src/UI/Settings/Profile/Language/LanguageModel.js b/src/UI/Settings/Profile/Language/LanguageModel.js deleted file mode 100644 index 3986a5948..000000000 --- a/src/UI/Settings/Profile/Language/LanguageModel.js +++ /dev/null @@ -1,3 +0,0 @@ -var Backbone = require('backbone'); - -module.exports = Backbone.Model.extend({}); \ No newline at end of file diff --git a/src/UI/Settings/Profile/LanguageLabel.js b/src/UI/Settings/Profile/LanguageLabel.js deleted file mode 100644 index b162d5683..000000000 --- a/src/UI/Settings/Profile/LanguageLabel.js +++ /dev/null @@ -1,15 +0,0 @@ -var _ = require('underscore'); -var Handlebars = require('handlebars'); -var LanguageCollection = require('./Language/LanguageCollection'); - -Handlebars.registerHelper('languageLabel', function() { - var wantedLanguage = this.language; - - var language = LanguageCollection.find(function(lang) { - return lang.get('nameLower') === wantedLanguage; - }); - - var result = '<span class="label label-primary">' + language.get('name') + '</span>'; - - return new Handlebars.SafeString(result); -}); \ No newline at end of file diff --git a/src/UI/Settings/Profile/ProfileCollectionTemplate.hbs b/src/UI/Settings/Profile/ProfileCollectionTemplate.hbs deleted file mode 100644 index 194082875..000000000 --- a/src/UI/Settings/Profile/ProfileCollectionTemplate.hbs +++ /dev/null @@ -1,16 +0,0 @@ -<fieldset> - <legend>Profiles</legend> - <div class="row"> - <div class="col-md-12"> - <ul class="profiles thingies"> - <li> - <div class="profile-item thingy add-card x-add-card"> - <span class="center well"> - <i class="icon-lidarr-add"/> - </span> - </div> - </li> - </ul> - </div> - </div> -</fieldset> \ No newline at end of file diff --git a/src/UI/Settings/Profile/ProfileCollectionView.js b/src/UI/Settings/Profile/ProfileCollectionView.js deleted file mode 100644 index 1f96fc44f..000000000 --- a/src/UI/Settings/Profile/ProfileCollectionView.js +++ /dev/null @@ -1,43 +0,0 @@ -var AppLayout = require('../../AppLayout'); -var Marionette = require('marionette'); -var ProfileView = require('./ProfileView'); -var EditProfileView = require('./Edit/EditProfileLayout'); -var ProfileCollection = require('./ProfileSchemaCollection'); -var _ = require('underscore'); - -module.exports = Marionette.CompositeView.extend({ - itemView : ProfileView, - itemViewContainer : '.profiles', - template : 'Settings/Profile/ProfileCollectionTemplate', - - ui : { - 'addCard' : '.x-add-card' - }, - - events : { - 'click .x-add-card' : '_addProfile' - }, - - appendHtml : function(collectionView, itemView, index) { - collectionView.ui.addCard.parent('li').before(itemView.el); - }, - - _addProfile : function() { - var self = this; - var schemaCollection = new ProfileCollection(); - schemaCollection.fetch({ - success : function(collection) { - var model = _.first(collection.models); - model.set('id', undefined); - model.set('name', ''); - model.collection = self.collection; - var view = new EditProfileView({ - model : model, - profileCollection : self.collection - }); - - AppLayout.modalRegion.show(view); - } - }); - } -}); \ No newline at end of file diff --git a/src/UI/Settings/Profile/ProfileLayout.js b/src/UI/Settings/Profile/ProfileLayout.js deleted file mode 100644 index d8f226271..000000000 --- a/src/UI/Settings/Profile/ProfileLayout.js +++ /dev/null @@ -1,28 +0,0 @@ -var Marionette = require('marionette'); -var ProfileCollection = require('../../Profile/ProfileCollection'); -var ProfileCollectionView = require('./ProfileCollectionView'); -var DelayProfileLayout = require('./Delay/DelayProfileLayout'); -var DelayProfileCollection = require('./Delay/DelayProfileCollection'); -var LanguageCollection = require('./Language/LanguageCollection'); - -module.exports = Marionette.Layout.extend({ - template : 'Settings/Profile/ProfileLayoutTemplate', - - regions : { - profile : '#profile', - delayProfile : '#delay-profile' - }, - - initialize : function(options) { - this.settings = options.settings; - ProfileCollection.fetch(); - - this.delayProfileCollection = new DelayProfileCollection(); - this.delayProfileCollection.fetch(); - }, - - onShow : function() { - this.profile.show(new ProfileCollectionView({ collection : ProfileCollection })); - this.delayProfile.show(new DelayProfileLayout({ collection : this.delayProfileCollection })); - } -}); \ No newline at end of file diff --git a/src/UI/Settings/Profile/ProfileLayoutTemplate.hbs b/src/UI/Settings/Profile/ProfileLayoutTemplate.hbs deleted file mode 100644 index 99adeab97..000000000 --- a/src/UI/Settings/Profile/ProfileLayoutTemplate.hbs +++ /dev/null @@ -1,5 +0,0 @@ -<div class="row"> - <div class="col-md-12" id="profile"/> - - <div class="col-md-12 delay-profile-region" id="delay-profile"/> -</div> diff --git a/src/UI/Settings/Profile/ProfileSchemaCollection.js b/src/UI/Settings/Profile/ProfileSchemaCollection.js deleted file mode 100644 index 6f32ff2e8..000000000 --- a/src/UI/Settings/Profile/ProfileSchemaCollection.js +++ /dev/null @@ -1,7 +0,0 @@ -var Backbone = require('backbone'); -var ProfileModel = require('../../Profile/ProfileModel'); - -module.exports = Backbone.Collection.extend({ - model : ProfileModel, - url : window.NzbDrone.ApiRoot + '/profile/schema' -}); \ No newline at end of file diff --git a/src/UI/Settings/Profile/ProfileView.js b/src/UI/Settings/Profile/ProfileView.js deleted file mode 100644 index 4241c3f12..000000000 --- a/src/UI/Settings/Profile/ProfileView.js +++ /dev/null @@ -1,35 +0,0 @@ -var AppLayout = require('../../AppLayout'); -var Marionette = require('marionette'); -var EditProfileView = require('./Edit/EditProfileLayout'); -var AsModelBoundView = require('../../Mixins/AsModelBoundView'); -require('./AllowedLabeler'); -require('./LanguageLabel'); -require('bootstrap'); - -var view = Marionette.ItemView.extend({ - template : 'Settings/Profile/ProfileViewTemplate', - tagName : 'li', - - ui : { - "progressbar" : '.progress .bar', - "deleteButton" : '.x-delete' - }, - - events : { - 'click' : '_editProfile' - }, - - initialize : function() { - this.listenTo(this.model, 'sync', this.render); - }, - - _editProfile : function() { - var view = new EditProfileView({ - model : this.model, - profileCollection : this.model.collection - }); - AppLayout.modalRegion.show(view); - } -}); - -module.exports = AsModelBoundView.call(view); \ No newline at end of file diff --git a/src/UI/Settings/Profile/ProfileViewTemplate.hbs b/src/UI/Settings/Profile/ProfileViewTemplate.hbs deleted file mode 100644 index 4f5b3eef0..000000000 --- a/src/UI/Settings/Profile/ProfileViewTemplate.hbs +++ /dev/null @@ -1,13 +0,0 @@ -<div class="profile-item thingy"> - <div> - <h3 name="name"></h3> - </div> - - <div class="language"> - {{languageLabel}} - </div> - - <ul class="allowed-qualities"> - {{allowedLabeler}} - </ul> -</div> \ No newline at end of file diff --git a/src/UI/Settings/Profile/profile.less b/src/UI/Settings/Profile/profile.less deleted file mode 100644 index df217a398..000000000 --- a/src/UI/Settings/Profile/profile.less +++ /dev/null @@ -1,43 +0,0 @@ -@import "../../Content/Bootstrap/mixins"; -@import "../../Content/FontAwesome/font-awesome"; -@import "../../Shared/Styles/clickable.less"; - -.profile-item { - .clickable; - - width: 300px; - height: 158px; - padding: 10px 15px; - - &.add-card { - .center { - margin-top: 30px; - } - } - - .allowed-qualities { - - padding-left: 0px; - - li { - list-style-type : none; - margin: 1px; - } - } - - .language { - margin-bottom: 3px; - } -} - -.delay-profile-region { - margin-top : 30px; -} - -.delay-profiles { - padding-left : 0px; - - li { - list-style-type : none; - } -} \ No newline at end of file diff --git a/src/UI/Settings/ProviderSettingsModelBase.js b/src/UI/Settings/ProviderSettingsModelBase.js deleted file mode 100644 index 674aba4e5..000000000 --- a/src/UI/Settings/ProviderSettingsModelBase.js +++ /dev/null @@ -1,71 +0,0 @@ -var $ = require('jquery'); -var _ = require('underscore'); -var DeepModel = require('backbone.deepmodel'); -var Messenger = require('../Shared/Messenger'); - -module.exports = DeepModel.extend({ - - getFieldValue : function(name) { - var index = _.indexOf(_.pluck(this.get('fields'), 'name'), name); - return this.get('fields.' + index + '.value'); - }, - - setFieldValue : function(name, value) { - var index = _.indexOf(_.pluck(this.get('fields'), 'name'), name); - return this.set('fields.' + index + '.value', value); - }, - - requestAction : function(action, queryParams) { - var self = this; - - this.trigger('validation:sync'); - - var params = { - url : this.collection.url + '/action/' + action, - contentType : 'application/json', - data : JSON.stringify(this.toJSON()), - type : 'POST', - isValidatedCall : true - }; - - if (queryParams) { - params.url += '?' + $.param(queryParams, true); - } - - var promise = $.ajax(params); - - promise.fail(function(response) { - self.trigger('validation:failed', response); - }); - - return promise; - }, - - test : function() { - var self = this; - - this.trigger('validation:sync'); - - var params = {}; - - params.url = this.collection.url + '/test'; - params.contentType = 'application/json'; - params.data = JSON.stringify(this.toJSON()); - params.type = 'POST'; - params.isValidatedCall = true; - - var promise = $.ajax(params); - - Messenger.monitor({ - promise : promise, - successMessage : 'Testing \'{0}\' succeeded'.format(this.get('name')), - errorMessage : 'Testing \'{0}\' failed'.format(this.get('name')) - }); - - promise.fail(function(response) { - self.trigger('validation:failed', response); - }); - - return promise; - } -}); \ No newline at end of file diff --git a/src/UI/Settings/Quality/Definition/QualityDefinitionCollectionTemplate.hbs b/src/UI/Settings/Quality/Definition/QualityDefinitionCollectionTemplate.hbs deleted file mode 100644 index ee0a5faef..000000000 --- a/src/UI/Settings/Quality/Definition/QualityDefinitionCollectionTemplate.hbs +++ /dev/null @@ -1,16 +0,0 @@ -<fieldset> - <legend>Quality Definitions</legend> - <div class="col-md-11"> - <div id="quality-definition-list"> - <div class="quality-header x-header hidden-xs"> - <div class="row"> - <span class="col-md-2 col-sm-3">Quality</span> - <span class="col-md-2 col-sm-3">Title</span> - <span class="col-md-4 col-sm-6">Size Limit <i class="icon-lidarr-info" title="Limits are automatically adjusted for the number of tracks in the file." /></span> - </div> - </div> - <div class="rows x-rows"> - </div> - </div> - </div> -</fieldset> diff --git a/src/UI/Settings/Quality/Definition/QualityDefinitionCollectionView.js b/src/UI/Settings/Quality/Definition/QualityDefinitionCollectionView.js deleted file mode 100644 index be2743d5b..000000000 --- a/src/UI/Settings/Quality/Definition/QualityDefinitionCollectionView.js +++ /dev/null @@ -1,10 +0,0 @@ -var Marionette = require('marionette'); -var QualityDefinitionItemView = require('./QualityDefinitionItemView'); - -module.exports = Marionette.CompositeView.extend({ - template : 'Settings/Quality/Definition/QualityDefinitionCollectionTemplate', - - itemViewContainer : '.x-rows', - - itemView : QualityDefinitionItemView -}); \ No newline at end of file diff --git a/src/UI/Settings/Quality/Definition/QualityDefinitionItemView.js b/src/UI/Settings/Quality/Definition/QualityDefinitionItemView.js deleted file mode 100644 index d7531ab8d..000000000 --- a/src/UI/Settings/Quality/Definition/QualityDefinitionItemView.js +++ /dev/null @@ -1,95 +0,0 @@ -var Marionette = require('marionette'); -var AsModelBoundView = require('../../../Mixins/AsModelBoundView'); -require('jquery-ui'); -var FormatHelpers = require('../../../Shared/FormatHelpers'); - -var view = Marionette.ItemView.extend({ - template : 'Settings/Quality/Definition/QualityDefinitionItemViewTemplate', - className : 'row', - - slider : { - min : 0, - max : 10, - step : 0.01 - }, - - ui : { - sizeSlider : '.x-slider', - thirtyMinuteMinSize : '.x-min-thirty', - sixtyMinuteMinSize : '.x-min-sixty', - thirtyMinuteMaxSize : '.x-max-thirty', - sixtyMinuteMaxSize : '.x-max-sixty' - }, - - events : { - 'slide .x-slider' : '_updateSize' - }, - - initialize : function(options) { - this.profileCollection = options.profiles; - }, - - onRender : function() { - if (this.model.get('quality').id === 0) { - this.$el.addClass('row advanced-setting'); - } - - this.ui.sizeSlider.slider({ - range : true, - min : this.slider.min, - max : this.slider.max, - step : this.slider.step, - values : [ - this.model.get('minSize') || this.slider.min, - this.model.get('maxSize') || this.slider.max - ] - }); - - this._changeSize(); - }, - - _updateSize : function(event, ui) { - var minSize = ui.values[0]; - var maxSize = ui.values[1]; - - if (maxSize === this.slider.max) { - maxSize = null; - } - - this.model.set('minSize', minSize); - this.model.set('maxSize', maxSize); - - this._changeSize(); - }, - - _changeSize : function() { - var minSize = this.model.get('minSize') || this.slider.min; - var maxSize = this.model.get('maxSize') || null; - { - var minBytes = minSize * 1024 * 1024; - var minThirty = FormatHelpers.bytes(minBytes * 30, 2); - var minSixty = FormatHelpers.bytes(minBytes * 60, 2); - - this.ui.thirtyMinuteMinSize.html(minThirty); - this.ui.sixtyMinuteMinSize.html(minSixty); - } - - { - if (maxSize === 0 || maxSize === null) { - this.ui.thirtyMinuteMaxSize.html('Unlimited'); - this.ui.sixtyMinuteMaxSize.html('Unlimited'); - } else { - var maxBytes = maxSize * 1024 * 1024; - var maxThirty = FormatHelpers.bytes(maxBytes * 30, 2); - var maxSixty = FormatHelpers.bytes(maxBytes * 60, 2); - - this.ui.thirtyMinuteMaxSize.html(maxThirty); - this.ui.sixtyMinuteMaxSize.html(maxSixty); - } - } - } -}); - -view = AsModelBoundView.call(view); - -module.exports = view; \ No newline at end of file diff --git a/src/UI/Settings/Quality/Definition/QualityDefinitionItemViewTemplate.hbs b/src/UI/Settings/Quality/Definition/QualityDefinitionItemViewTemplate.hbs deleted file mode 100644 index a0eaac784..000000000 --- a/src/UI/Settings/Quality/Definition/QualityDefinitionItemViewTemplate.hbs +++ /dev/null @@ -1,31 +0,0 @@ - <span class="col-md-2 col-sm-3"> - {{quality.name}} - </span> - <span class="col-md-2 col-sm-3"> - <input type="text" class="form-control" name="title"> - </span> - <span class="col-md-4 col-sm-6"> - <div class="x-slider"></div> - <div class="size-label-wrapper"> - <div class="pull-left"> - <span class="label label-warning x-min-thirty" - name="thirtyMinuteMinSize" - title="Minimum size for a 30 minute album"> - </span> - <span class="label label-info x-min-sixty" - name="sixtyMinuteMinSize" - title="Minimum size for a 60 minute album"> - </span> - </div> - <div class="pull-right"> - <span class="label label-warning x-max-thirty" - name="thirtyMinuteMaxSize" - title="Maximum size for a 30 minute album"> - </span> - <span class="label label-info x-max-sixty" - name="sixtyMinuteMaxSize" - title="Maximum size for a 60 minute album"> - </span> - </div> - </div> - </span> \ No newline at end of file diff --git a/src/UI/Settings/Quality/QualityLayout.js b/src/UI/Settings/Quality/QualityLayout.js deleted file mode 100644 index e93ca1854..000000000 --- a/src/UI/Settings/Quality/QualityLayout.js +++ /dev/null @@ -1,21 +0,0 @@ -var Marionette = require('marionette'); -var QualityDefinitionCollection = require('../../Quality/QualityDefinitionCollection'); -var QualityDefinitionCollectionView = require('./Definition/QualityDefinitionCollectionView'); - -module.exports = Marionette.Layout.extend({ - template : 'Settings/Quality/QualityLayoutTemplate', - - regions : { - qualityDefinition : '#quality-definition' - }, - - initialize : function(options) { - this.settings = options.settings; - this.qualityDefinitionCollection = new QualityDefinitionCollection(); - this.qualityDefinitionCollection.fetch(); - }, - - onShow : function() { - this.qualityDefinition.show(new QualityDefinitionCollectionView({ collection : this.qualityDefinitionCollection })); - } -}); \ No newline at end of file diff --git a/src/UI/Settings/Quality/QualityLayoutTemplate.hbs b/src/UI/Settings/Quality/QualityLayoutTemplate.hbs deleted file mode 100644 index a12f1926a..000000000 --- a/src/UI/Settings/Quality/QualityLayoutTemplate.hbs +++ /dev/null @@ -1,3 +0,0 @@ -<div class="row"> - <div class="col-md-12" id="quality-definition"/> -</div> diff --git a/src/UI/Settings/Quality/quality.less b/src/UI/Settings/Quality/quality.less deleted file mode 100644 index 23732cdfd..000000000 --- a/src/UI/Settings/Quality/quality.less +++ /dev/null @@ -1,135 +0,0 @@ -@import "../../Content/Bootstrap/mixins"; -@import (reference) "../../Content/icons"; -@import "../../Shared/Styles/clickable.less"; - -ul.qualities { - .user-select(none); - - min-height: 100px; - margin: 0; - padding: 0; - list-style-type: none; - outline: none; - width: 220px; - display: inline-block; - - li { - margin: 2px; - padding: 2px 4px; - line-height: 20px; - border: 1px solid #aaaaaa; - border-radius: 4px; /* may need vendor varients */ - background: #fafafa; - cursor: pointer; - - &.selected { - .select-handle { - opacity: 1.0; - cursor: pointer; - } - - .quality-label { - color: #444444; - } - - .select-handle { - .fa-icon-content(@fa-var-check-square-o); - } - } - - &:hover { - border-color: #888888; - background: #eeeeee; - - .drag-handle { - opacity: 1.0; - cursor: move; - } - } - - .quality-label { - color: #c6c6c6; - } - - .drag-handle, .select-handle { - opacity: 0.2; - line-height: 20px; - cursor: pointer; - } - - .select-handle { - .fa-icon-content(@fa-var-square-o); - - &:before { - display : inline-block; - width : 14px; - margin-top : 3px; - } - } - } -} - -.qualities-controls { - .help-inline { - vertical-align: top; - margin-top: 5px; - } -} - -#quality-definition-list { - - .quality-header .row { - font-weight: bold; - line-height: 40px; - } - - .rows .row { - line-height: 30px; - border-top: 1px solid #ddd; - vertical-align: middle; - padding: 5px; - - input { - margin-bottom: 0px; - } - - .size-label-wrapper { - line-height: 20px; - } - - .label { - min-width: 70px; - text-align: center; - margin: 0px 1px; - padding: 1px 4px; - } - - .ui-slider { - position: relative; - text-align: left; - background-color: #f5f5f5; - border-radius: 3px; - border: 1px solid #ccc; - height: 8px; - - .ui-slider-range { - position: absolute; - display: block; - background-color: #ddd; - height: 100%; - } - - .ui-slider-handle { - position: absolute; - z-index: 2; - width: 6px; - height: 12px; - cursor: default; - background-color: #ccc; - border: 1px solid #aaa; - border-radius: 3px; - top: -3px; - } - } - } -} diff --git a/src/UI/Settings/SettingsLayout.js b/src/UI/Settings/SettingsLayout.js deleted file mode 100644 index 429d702cd..000000000 --- a/src/UI/Settings/SettingsLayout.js +++ /dev/null @@ -1,252 +0,0 @@ -var $ = require('jquery'); -var _ = require('underscore'); -var vent = require('vent'); -var Marionette = require('marionette'); -var Backbone = require('backbone'); -var GeneralSettingsModel = require('./General/GeneralSettingsModel'); -var NamingModel = require('./MediaManagement/Naming/NamingModel'); -var MediaManagementLayout = require('./MediaManagement/MediaManagementLayout'); -var MediaManagementSettingsModel = require('./MediaManagement/MediaManagementSettingsModel'); -var ProfileLayout = require('./Profile/ProfileLayout'); -var QualityLayout = require('./Quality/QualityLayout'); -var IndexerLayout = require('./Indexers/IndexerLayout'); -var IndexerCollection = require('./Indexers/IndexerCollection'); -var IndexerSettingsModel = require('./Indexers/IndexerSettingsModel'); -var DownloadClientLayout = require('./DownloadClient/DownloadClientLayout'); -var DownloadClientSettingsModel = require('./DownloadClient/DownloadClientSettingsModel'); -var NotificationCollectionView = require('./Notifications/NotificationCollectionView'); -var NotificationCollection = require('./Notifications/NotificationCollection'); -var MetadataLayout = require('./Metadata/MetadataLayout'); -var GeneralView = require('./General/GeneralView'); -var UiView = require('./UI/UiView'); -var UiSettingsModel = require('./UI/UiSettingsModel'); -var LoadingView = require('../Shared/LoadingView'); -var Config = require('../Config'); - -module.exports = Marionette.Layout.extend({ - template : 'Settings/SettingsLayoutTemplate', - - regions : { - mediaManagement : '#media-management', - profiles : '#profiles', - quality : '#quality', - indexers : '#indexers', - downloadClient : '#download-client', - notifications : '#notifications', - metadata : '#metadata', - general : '#general', - uiRegion : '#ui', - loading : '#loading-region' - }, - - ui : { - mediaManagementTab : '.x-media-management-tab', - profilesTab : '.x-profiles-tab', - qualityTab : '.x-quality-tab', - indexersTab : '.x-indexers-tab', - downloadClientTab : '.x-download-client-tab', - notificationsTab : '.x-notifications-tab', - metadataTab : '.x-metadata-tab', - generalTab : '.x-general-tab', - uiTab : '.x-ui-tab', - advancedSettings : '.x-advanced-settings' - }, - - events : { - 'click .x-media-management-tab' : '_showMediaManagement', - 'click .x-profiles-tab' : '_showProfiles', - 'click .x-quality-tab' : '_showQuality', - 'click .x-indexers-tab' : '_showIndexers', - 'click .x-download-client-tab' : '_showDownloadClient', - 'click .x-notifications-tab' : '_showNotifications', - 'click .x-metadata-tab' : '_showMetadata', - 'click .x-general-tab' : '_showGeneral', - 'click .x-ui-tab' : '_showUi', - 'click .x-save-settings' : '_save', - 'change .x-advanced-settings' : '_toggleAdvancedSettings' - }, - - initialize : function(options) { - if (options.action) { - this.action = options.action.toLowerCase(); - } - - this.listenTo(vent, vent.Hotkeys.SaveSettings, this._save); - }, - - onRender : function() { - this.loading.show(new LoadingView()); - var self = this; - - this.mediaManagementSettings = new MediaManagementSettingsModel(); - this.namingSettings = new NamingModel(); - this.indexerSettings = new IndexerSettingsModel(); - this.downloadClientSettings = new DownloadClientSettingsModel(); - this.notificationCollection = new NotificationCollection(); - this.generalSettings = new GeneralSettingsModel(); - this.uiSettings = new UiSettingsModel(); - Backbone.$.when(this.mediaManagementSettings.fetch(), this.namingSettings.fetch(), this.indexerSettings.fetch(), this.downloadClientSettings.fetch(), - this.notificationCollection.fetch(), this.generalSettings.fetch(), this.uiSettings.fetch()).done(function() { - if (!self.isClosed) { - self.loading.$el.hide(); - self.mediaManagement.show(new MediaManagementLayout({ - settings : self.mediaManagementSettings, - namingSettings : self.namingSettings - })); - self.profiles.show(new ProfileLayout()); - self.quality.show(new QualityLayout()); - self.indexers.show(new IndexerLayout({ model : self.indexerSettings })); - self.downloadClient.show(new DownloadClientLayout({ model : self.downloadClientSettings })); - self.notifications.show(new NotificationCollectionView({ collection : self.notificationCollection })); - self.metadata.show(new MetadataLayout()); - self.general.show(new GeneralView({ model : self.generalSettings })); - self.uiRegion.show(new UiView({ model : self.uiSettings })); - } - }); - - this._setAdvancedSettingsState(); - }, - - onShow : function() { - switch (this.action) { - case 'profiles': - this._showProfiles(); - break; - case 'quality': - this._showQuality(); - break; - case 'indexers': - this._showIndexers(); - break; - case 'downloadclient': - this._showDownloadClient(); - break; - case 'connect': - this._showNotifications(); - break; - case 'notifications': - this._showNotifications(); - break; - case 'metadata': - this._showMetadata(); - break; - case 'general': - this._showGeneral(); - break; - case 'ui': - this._showUi(); - break; - default: - this._showMediaManagement(); - } - }, - - _showMediaManagement : function(e) { - if (e) { - e.preventDefault(); - } - - this.ui.mediaManagementTab.tab('show'); - this._navigate('settings/mediamanagement'); - }, - - _showProfiles : function(e) { - if (e) { - e.preventDefault(); - } - - this.ui.profilesTab.tab('show'); - this._navigate('settings/profiles'); - }, - - _showQuality : function(e) { - if (e) { - e.preventDefault(); - } - - this.ui.qualityTab.tab('show'); - this._navigate('settings/quality'); - }, - - _showIndexers : function(e) { - if (e) { - e.preventDefault(); - } - - this.ui.indexersTab.tab('show'); - this._navigate('settings/indexers'); - }, - - _showDownloadClient : function(e) { - if (e) { - e.preventDefault(); - } - - this.ui.downloadClientTab.tab('show'); - this._navigate('settings/downloadclient'); - }, - - _showNotifications : function(e) { - if (e) { - e.preventDefault(); - } - - this.ui.notificationsTab.tab('show'); - this._navigate('settings/connect'); - }, - - _showMetadata : function(e) { - if (e) { - e.preventDefault(); - } - this.ui.metadataTab.tab('show'); - this._navigate('settings/metadata'); - }, - - _showGeneral : function(e) { - if (e) { - e.preventDefault(); - } - this.ui.generalTab.tab('show'); - this._navigate('settings/general'); - }, - - _showUi : function(e) { - if (e) { - e.preventDefault(); - } - this.ui.uiTab.tab('show'); - this._navigate('settings/ui'); - }, - - _navigate : function(route) { - Backbone.history.navigate(route, { - trigger : false, - replace : true - }); - }, - - _save : function() { - vent.trigger(vent.Commands.SaveSettings); - }, - - _setAdvancedSettingsState : function() { - var checked = Config.getValueBoolean(Config.Keys.AdvancedSettings); - this.ui.advancedSettings.prop('checked', checked); - - if (checked) { - $('body').addClass('show-advanced-settings'); - } - }, - - _toggleAdvancedSettings : function() { - var checked = this.ui.advancedSettings.prop('checked'); - Config.setValue(Config.Keys.AdvancedSettings, checked); - - if (checked) { - $('body').addClass('show-advanced-settings'); - } else { - $('body').removeClass('show-advanced-settings'); - } - } -}); \ No newline at end of file diff --git a/src/UI/Settings/SettingsLayoutTemplate.hbs b/src/UI/Settings/SettingsLayoutTemplate.hbs deleted file mode 100644 index c69ba9f16..000000000 --- a/src/UI/Settings/SettingsLayoutTemplate.hbs +++ /dev/null @@ -1,49 +0,0 @@ -<ul class="nav nav-tabs nav-justified settings-tabs"> - <li><a href="#media-management" class="x-media-management-tab no-router">Media Management</a></li> - <li><a href="#profiles" class="x-profiles-tab no-router">Profiles</a></li> - <li><a href="#quality" class="x-quality-tab no-router">Quality</a></li> - <li><a href="#indexers" class="x-indexers-tab no-router">Indexers</a></li> - <li><a href="#download-client" class="x-download-client-tab no-router">Download Client</a></li> - <li><a href="#notifications" class="x-notifications-tab no-router">Connect</a></li> - <li><a href="#metadata" class="x-metadata-tab no-router">Metadata</a></li> - <li><a href="#general" class="x-general-tab no-router">General</a></li> - <li><a href="#ui" class="x-ui-tab no-router">UI</a></li> -</ul> - -<div class="row settings-controls"> - <div class="col-sm-4 col-sm-offset-7 col-md-3 col-md-offset-8"> - <div class="advanced-settings-toggle"> - <span class="help-inline-checkbox hidden-xs"> - Advanced Settings - </span> - <label class="checkbox toggle well"> - <input type="checkbox" class="x-advanced-settings"/> - <p> - <span>Shown</span> - <span>Hidden</span> - </p> - <div class="btn btn-warning slide-button"/> - </label> - <span class="help-inline-checkbox hidden-sm hidden-md hidden-lg"> - Advanced Settings - </span> - </div> - </div> - <div class="col-sm-1 col-md-1"> - <button class="btn btn-primary x-save-settings">Save</button> - </div> -</div> - -<div class="tab-content"> - <div class="tab-pane" id="media-management"></div> - <div class="tab-pane" id="profiles"></div> - <div class="tab-pane" id="quality"></div> - <div class="tab-pane" id="indexers"></div> - <div class="tab-pane" id="download-client"></div> - <div class="tab-pane" id="notifications"></div> - <div class="tab-pane" id="metadata"></div> - <div class="tab-pane" id="general"></div> - <div class="tab-pane" id="ui"></div> -</div> - -<div id="loading-region"></div> \ No newline at end of file diff --git a/src/UI/Settings/SettingsModelBase.js b/src/UI/Settings/SettingsModelBase.js deleted file mode 100644 index f08773f91..000000000 --- a/src/UI/Settings/SettingsModelBase.js +++ /dev/null @@ -1,34 +0,0 @@ -var vent = require('vent'); -var DeepModel = require('backbone.deepmodel'); -var AsChangeTrackingModel = require('../Mixins/AsChangeTrackingModel'); -var Messenger = require('../Shared/Messenger'); - -var model = DeepModel.extend({ - - initialize : function() { - this.listenTo(vent, vent.Commands.SaveSettings, this.saveSettings); - this.listenTo(this, 'destroy', this._stopListening); - }, - - saveSettings : function() { - if (!this.isSaved) { - var savePromise = this.save(); - - Messenger.monitor({ - promise : savePromise, - successMessage : this.successMessage, - errorMessage : this.errorMessage - }); - - return savePromise; - } - - return undefined; - }, - - _stopListening : function() { - this.stopListening(vent, vent.Commands.SaveSettings); - } -}); - -module.exports = AsChangeTrackingModel.call(model); diff --git a/src/UI/Settings/ThingyAddCollectionView.js b/src/UI/Settings/ThingyAddCollectionView.js deleted file mode 100644 index ecce0dd7b..000000000 --- a/src/UI/Settings/ThingyAddCollectionView.js +++ /dev/null @@ -1,13 +0,0 @@ -var Marionette = require('marionette'); - -module.exports = Marionette.CompositeView.extend({ - itemViewOptions : function() { - return { - targetCollection : this.targetCollection || this.options.targetCollection - }; - }, - - initialize : function(options) { - this.targetCollection = options.targetCollection; - } -}); \ No newline at end of file diff --git a/src/UI/Settings/ThingyHeaderGroupView.js b/src/UI/Settings/ThingyHeaderGroupView.js deleted file mode 100644 index 0f7e9a2f8..000000000 --- a/src/UI/Settings/ThingyHeaderGroupView.js +++ /dev/null @@ -1,18 +0,0 @@ -var Backbone = require('backbone'); -var Marionette = require('marionette'); - -module.exports = Marionette.CompositeView.extend({ - itemViewContainer : '.item-list', - template : 'Settings/ThingyHeaderGroupViewTemplate', - tagName : 'div', - - itemViewOptions : function() { - return { - targetCollection : this.targetCollection || this.options.targetCollection - }; - }, - - initialize : function() { - this.collection = new Backbone.Collection(this.model.get('collection')); - } -}); \ No newline at end of file diff --git a/src/UI/Settings/ThingyHeaderGroupViewTemplate.hbs b/src/UI/Settings/ThingyHeaderGroupViewTemplate.hbs deleted file mode 100644 index 310a29241..000000000 --- a/src/UI/Settings/ThingyHeaderGroupViewTemplate.hbs +++ /dev/null @@ -1,2 +0,0 @@ -<legend>{{header}}</legend> -<ul class="item-list" /> \ No newline at end of file diff --git a/src/UI/Settings/UI/UiSettingsModel.js b/src/UI/Settings/UI/UiSettingsModel.js deleted file mode 100644 index baf6a5297..000000000 --- a/src/UI/Settings/UI/UiSettingsModel.js +++ /dev/null @@ -1,7 +0,0 @@ -var SettingsModelBase = require('../SettingsModelBase'); - -module.exports = SettingsModelBase.extend({ - url : window.NzbDrone.ApiRoot + '/config/ui', - successMessage : 'UI settings saved', - errorMessage : 'Failed to save UI settings' -}); \ No newline at end of file diff --git a/src/UI/Settings/UI/UiView.js b/src/UI/Settings/UI/UiView.js deleted file mode 100644 index 5e8664036..000000000 --- a/src/UI/Settings/UI/UiView.js +++ /dev/null @@ -1,22 +0,0 @@ -var vent = require('vent'); -var Marionette = require('marionette'); -var UiSettingsModel = require('../../Shared/UiSettingsModel'); -var AsModelBoundView = require('../../Mixins/AsModelBoundView'); -var AsValidatedView = require('../../Mixins/AsValidatedView'); - -var view = Marionette.ItemView.extend({ - template : 'Settings/UI/UiViewTemplate', - - initialize : function() { - this.listenTo(this.model, 'sync', this._reloadUiSettings); - }, - - _reloadUiSettings : function() { - UiSettingsModel.fetch(); - } -}); - -AsModelBoundView.call(view); -AsValidatedView.call(view); - -module.exports = view; \ No newline at end of file diff --git a/src/UI/Settings/UI/UiViewTemplate.hbs b/src/UI/Settings/UI/UiViewTemplate.hbs deleted file mode 100644 index 4b5228870..000000000 --- a/src/UI/Settings/UI/UiViewTemplate.hbs +++ /dev/null @@ -1,124 +0,0 @@ -<div class="form-horizontal"> - <fieldset> - <legend>Calendar</legend> - - <div class="form-group"> - <label class="col-sm-3 control-label">First Day of Week</label> - - <div class="col-sm-4"> - <select name="firstDayOfWeek" class="form-control"> - <option value="0">Sunday</option> - <option value="1">Monday</option> - </select> - </div> - </div> - - <div class="form-group"> - <label class="col-sm-3 control-label">Week Column Header</label> - - <div class="col-sm-1 col-sm-push-4 help-inline"> - <i class="icon-lidarr-form-info" title="Shown above each column when week is the active view"/> - </div> - - <div class="col-sm-4 col-sm-pull-1"> - <select name="calendarWeekColumnHeader" class="form-control"> - <option value="ddd M/D">Tue 3/5</option> - <option value="ddd MM/DD">Tue 03/05</option> - <option value="ddd D/M">Tue 5/3</option> - <option value="ddd DD/MM">Tue 05/03</option> - </select> - </div> - </div> - </fieldset> - - <fieldset> - <legend>Dates</legend> - - <div class="form-group"> - <label class="col-sm-3 control-label">Short Date Format</label> - - <div class="col-sm-4"> - <select name="shortDateFormat" class="form-control"> - <option value="MMM D YYYY">Mar 5 2014</option> - <option value="DD MMM YYYY">05 Mar 2014</option> - <option value="MM/D/YYYY">03/5/2014</option> - <option value="MM/DD/YYYY">03/05/2014</option> - <option value="DD/MM/YYYY">05/03/2014</option> - <option value="YYYY-MM-DD">2014-03-05</option> - </select> - </div> - </div> - - <div class="form-group"> - <label class="col-sm-3 control-label">Long Date Format</label> - - <div class="col-sm-4"> - <select name="longDateFormat" class="form-control"> - <option value="dddd, MMMM D YYYY">Tuesday, March 5, 2014</option> - <option value="dddd, D MMMM YYYY">Tuesday, 5 March, 2014</option> - </select> - </div> - </div> - - <div class="form-group"> - <label class="col-sm-3 control-label">Time Format</label> - - <div class="col-sm-4"> - <select name="timeFormat" class="form-control"> - <option value="h(:mm)a">5pm/5:30pm</option> - <option value="HH:mm">17:00/17:30</option> - </select> - </div> - </div> - - <div class="form-group"> - <label class="col-sm-3 control-label">Show Relative Dates</label> - - <div class="col-sm-8"> - <div class="input-group"> - <label class="checkbox toggle well"> - <input type="checkbox" name="showRelativeDates"/> - - <p> - <span>Yes</span> - <span>No</span> - </p> - - <div class="btn btn-primary slide-button"/> - </label> - - <span class="help-inline-checkbox"> - <i class="icon-lidarr-form-info" title="Show relative (Today/Yesterday/etc) or absolute dates"/> - </span> - </div> - </div> - </div> - </fieldset> - - <fieldset> - <legend>Style</legend> - - <div class="form-group"> - <label class="col-sm-3 control-label">Enable Color-Impaired mode</label> - - <div class="col-sm-8"> - <div class="input-group"> - <label class="checkbox toggle well"> - <input type="checkbox" name="enableColorImpairedMode" /> - - <p> - <span>Yes</span> - <span>No</span> - </p> - - <div class="btn btn-primary slide-button"/> - </label> - - <span class="help-inline-checkbox"> - <i class="icon-lidarr-form-info" title="Altered style to allow color-impaired users to better distinguish color coded information"/> - </span> - </div> - </div> - </div> - </fieldset> -</div> diff --git a/src/UI/Settings/settings.less b/src/UI/Settings/settings.less deleted file mode 100644 index ec6bd2a1c..000000000 --- a/src/UI/Settings/settings.less +++ /dev/null @@ -1,161 +0,0 @@ -@import "../Content/Bootstrap/variables"; -@import "../Shared/Styles/clickable.less"; -@import "Indexers/indexers"; -@import "Quality/quality"; -@import "Profile/profile"; -@import "Notifications/notifications"; -@import "Metadata/metadata"; -@import "DownloadClient/downloadclient"; -@import "thingy"; - -li.save-and-add { - .clickable; - - display: block; - padding: 3px 20px; - clear: both; - font-weight: normal; - line-height: 20px; - color: rgb(51, 51, 51); - white-space: nowrap; -} - -li.save-and-add:hover { - text-decoration: none; - color: rgb(255, 255, 255); - background-color: rgb(0, 129, 194); -} - -.add-card { - .clickable; - color: #adadad; - font-size: 50px; - text-align: center; - background-color: #f5f5f5; - - .center { - display: inline-block; - padding: 5px 20px 0px; - background-color: white; - } - - i { - .clickable; - } -} - -.naming-example { - display: inline-block; - margin-top: 5px; -} - -.naming-format { - width: 500px; -} - -.settings-controls { - margin-top: 10px; -} - -.advanced-settings-toggle { - display: inline-block; - margin-bottom: 10px; - - .checkbox { - width : 100px; - margin-left : 0px; - display : inline-block; - padding-top : 0px; - margin-bottom : -10px; - margin-top : -1px; - } - - .help-inline-checkbox { - display : inline-block; - margin-top : -3px; - margin-bottom : 0; - vertical-align : middle; - } -} - -.advanced-setting { - display: none; - - .control-label { - color: @brand-warning; - } -} - -.basic-setting { - display: block; -} - -.show-advanced-settings { - .advanced-setting { - display: block; - } - - .basic-setting { - display: none; - } -} - -.api-key { - - input { - width : 280px; - cursor : text; - } -} - -.settings-tabs { - li>a { - padding : 10px; - } - - @media (min-width: @screen-sm-min) and (max-width: @screen-md-max) { - li { - a { - white-space : nowrap; - padding : 10px; - } - } - } -} - -.indicator { - display : none; - padding-right : 5px; -} - -.add-rule-setting-mapping { - cursor : pointer; - font-size : 14px; - text-align : center; - display : inline-block; - padding : 2px 6px; - - i { - cursor : pointer; - } -} - -.rule-setting-list { - - .rule-setting-header .row { - font-weight : bold; - line-height : 40px; - } - - .rows .row { - line-height : 30px; - border-top : 1px solid #ddd; - vertical-align : middle; - padding : 5px; - - i { - cursor : pointer; - margin-left : 5px; - } - } -} diff --git a/src/UI/Settings/thingy.less b/src/UI/Settings/thingy.less deleted file mode 100644 index 2368240b7..000000000 --- a/src/UI/Settings/thingy.less +++ /dev/null @@ -1,65 +0,0 @@ -@import "../Shared/Styles/card"; -@import "../Shared/Styles/clickable"; - -.add-thingy { - .card; - cursor: pointer; - font-size: 24px; - font-weight: lighter; - text-align: center; - height: 85px; -} - -.add-thingies { - text-align: center; - - legend { - text-align: left; - text-transform: capitalize; - } - - ul.items { - list-style-type: none; - margin: 0px; - padding: 0px; - - li.add-thingy-item { - display: inline-block; - vertical-align: top; - } - } -} - -.thingy { - - .card; - - h3 { - margin-top: 0px; - display: inline-block; - white-space: nowrap; - overflow: hidden; - line-height: 30px; - text-overflow: ellipsis; - text-transform: none; - } - - .btn-group { - margin-top: 8px; - } - - .settings { - margin-top: 5px; - } -} - -.thingies { - li { - display: inline-block; - vertical-align: top; - } - - @media (max-width: @screen-xs-max) { - padding-left: 0px; - } -} \ No newline at end of file diff --git a/src/UI/Shared/ApiData.js b/src/UI/Shared/ApiData.js deleted file mode 100644 index 6d8e62043..000000000 --- a/src/UI/Shared/ApiData.js +++ /dev/null @@ -1,17 +0,0 @@ -var $ = require('jquery'); - -module.exports = { - get : function(resource) { - var url = window.NzbDrone.ApiRoot + '/' + resource; - var _data; - $.ajax({ - url : url, - async : false - }).done(function(data) { - _data = data; - }).error(function(xhr, status, error) { - throw error; - }); - return _data; - } -}; \ No newline at end of file diff --git a/src/UI/Shared/ControlPanel/ControlPanelController.js b/src/UI/Shared/ControlPanel/ControlPanelController.js deleted file mode 100644 index c2a31c3cc..000000000 --- a/src/UI/Shared/ControlPanel/ControlPanelController.js +++ /dev/null @@ -1,18 +0,0 @@ -var vent = require('vent'); -var AppLayout = require('../../AppLayout'); -var Marionette = require('marionette'); - -module.exports = Marionette.AppRouter.extend({ - initialize : function() { - vent.on(vent.Commands.OpenControlPanelCommand, this._openControlPanel, this); - vent.on(vent.Commands.CloseControlPanelCommand, this._closeControlPanel, this); - }, - - _openControlPanel : function(view) { - AppLayout.controlPanelRegion.show(view); - }, - - _closeControlPanel : function() { - AppLayout.controlPanelRegion.closePanel(); - } -}); \ No newline at end of file diff --git a/src/UI/Shared/ControlPanel/ControlPanelRegion.js b/src/UI/Shared/ControlPanel/ControlPanelRegion.js deleted file mode 100644 index e32c02552..000000000 --- a/src/UI/Shared/ControlPanel/ControlPanelRegion.js +++ /dev/null @@ -1,41 +0,0 @@ -var $ = require('jquery'); -var Backbone = require('backbone'); -var Marionette = require('marionette'); -var region = Marionette.Region.extend({ - el : '#control-panel-region', - - constructor : function() { - Backbone.Marionette.Region.prototype.constructor.apply(this, arguments); - this.on('show', this.showPanel, this); - }, - - getEl : function(selector) { - var $el = $(selector); - - return $el; - }, - - showPanel : function() { - $('body').addClass('control-panel-visible'); - this.$el.animate({ - 'margin-bottom' : 0, - 'opacity' : 1 - }, { - queue : false, - duration : 300 - }); - }, - - closePanel : function() { - $('body').removeClass('control-panel-visible'); - this.$el.animate({ - 'margin-bottom' : -100, - 'opacity' : 0 - }, { - queue : false, - duration : 300 - }); - this.reset(); - } -}); -module.exports = region; \ No newline at end of file diff --git a/src/UI/Shared/FileBrowser/EmptyView.js b/src/UI/Shared/FileBrowser/EmptyView.js deleted file mode 100644 index 3bd8ddc93..000000000 --- a/src/UI/Shared/FileBrowser/EmptyView.js +++ /dev/null @@ -1,5 +0,0 @@ -var Marionette = require('marionette'); - -module.exports = Marionette.CompositeView.extend({ - template : 'Shared/FileBrowser/EmptyViewTemplate' -}); \ No newline at end of file diff --git a/src/UI/Shared/FileBrowser/EmptyViewTemplate.hbs b/src/UI/Shared/FileBrowser/EmptyViewTemplate.hbs deleted file mode 100644 index 53469ac16..000000000 --- a/src/UI/Shared/FileBrowser/EmptyViewTemplate.hbs +++ /dev/null @@ -1,3 +0,0 @@ -<div class="text-center col-md-12 file-browser-empty"> - <span>No files/folders were found, edit the path above, or clear to start again</span> -</div> diff --git a/src/UI/Shared/FileBrowser/FileBrowserCollection.js b/src/UI/Shared/FileBrowser/FileBrowserCollection.js deleted file mode 100644 index d2771b15d..000000000 --- a/src/UI/Shared/FileBrowser/FileBrowserCollection.js +++ /dev/null @@ -1,28 +0,0 @@ -var $ = require('jquery'); -var Backbone = require('backbone'); -var FileBrowserModel = require('./FileBrowserModel'); - -module.exports = Backbone.Collection.extend({ - model : FileBrowserModel, - url : window.NzbDrone.ApiRoot + '/filesystem', - - parse : function(response) { - var contents = []; - if (response.parent || response.parent === '') { - var type = 'parent'; - var name = '...'; - if (response.parent === '') { - type = 'computer'; - name = 'My Computer'; - } - contents.push({ - type : type, - name : name, - path : response.parent - }); - } - $.merge(contents, response.directories); - $.merge(contents, response.files); - return contents; - } -}); \ No newline at end of file diff --git a/src/UI/Shared/FileBrowser/FileBrowserLayout.js b/src/UI/Shared/FileBrowser/FileBrowserLayout.js deleted file mode 100644 index 82ae8b32b..000000000 --- a/src/UI/Shared/FileBrowser/FileBrowserLayout.js +++ /dev/null @@ -1,162 +0,0 @@ -var _ = require('underscore'); -var vent = require('vent'); -var Marionette = require('marionette'); -var Backgrid = require('backgrid'); -var FileBrowserCollection = require('./FileBrowserCollection'); -var EmptyView = require('./EmptyView'); -var FileBrowserRow = require('./FileBrowserRow'); -var FileBrowserTypeCell = require('./FileBrowserTypeCell'); -var FileBrowserNameCell = require('./FileBrowserNameCell'); -var RelativeDateCell = require('../../Cells/RelativeDateCell'); -var FileSizeCell = require('../../Cells/FileSizeCell'); -var LoadingView = require('../LoadingView'); -require('../../Mixins/DirectoryAutoComplete'); - -module.exports = Marionette.Layout.extend({ - template : 'Shared/FileBrowser/FileBrowserLayoutTemplate', - - regions : { - browser : '#x-browser' - }, - - ui : { - path : '.x-path', - indicator : '.x-indicator' - }, - - events : { - 'typeahead:selected .x-path' : '_pathChanged', - 'typeahead:autocompleted .x-path' : '_pathChanged', - 'keyup .x-path' : '_inputChanged', - 'click .x-ok' : '_selectPath' - }, - - initialize : function(options) { - this.collection = new FileBrowserCollection(); - this.collection.showFiles = options.showFiles || false; - this.collection.showLastModified = options.showLastModified || false; - this.input = options.input; - this._setColumns(); - this.listenTo(this.collection, 'sync', this._showGrid); - this.listenTo(this.collection, 'filebrowser:row:folderselected', this._rowSelected); - this.listenTo(this.collection, 'filebrowser:row:fileselected', this._fileSelected); - }, - - onRender : function() { - this.browser.show(new LoadingView()); - this.ui.path.directoryAutoComplete(); - this._fetchCollection(this.input.val()); - this._updatePath(this.input.val()); - }, - - _setColumns : function() { - this.columns = [ - { - name : 'type', - label : '', - sortable : false, - cell : FileBrowserTypeCell - }, - { - name : 'name', - label : 'Name', - sortable : false, - cell : FileBrowserNameCell - } - ]; - if (this.collection.showLastModified) { - this.columns.push({ - name : 'lastModified', - label : 'Last Modified', - sortable : false, - cell : RelativeDateCell - }); - } - if (this.collection.showFiles) { - this.columns.push({ - name : 'size', - label : 'Size', - sortable : false, - cell : FileSizeCell - }); - } - }, - - _fetchCollection : function(path) { - this.ui.indicator.show(); - var data = { includeFiles : this.collection.showFiles }; - if (path) { - data.path = path; - } - this.collection.fetch({ data : data }); - }, - - _showGrid : function() { - this.ui.indicator.hide(); - if (this.collection.models.length === 0) { - this.browser.show(new EmptyView()); - return; - } - var grid = new Backgrid.Grid({ - row : FileBrowserRow, - collection : this.collection, - columns : this.columns, - className : 'table table-hover' - }); - this.browser.show(grid); - }, - - _rowSelected : function(model) { - var path = model.get('path'); - - this._updatePath(path); - this._fetchCollection(path); - }, - - _fileSelected : function(model) { - var path = model.get('path'); - var type = model.get('type'); - - this.input.val(path); - this.input.trigger('change'); - - this.input.trigger('filebrowser:fileselected', { - type : type, - path : path - }); - - vent.trigger(vent.Commands.CloseFileBrowser); - }, - - _pathChanged : function(e, path) { - this._fetchCollection(path.value); - this._updatePath(path.value); - }, - - _inputChanged : function() { - var path = this.ui.path.val(); - if (path === '' || path.endsWith('\\') || path.endsWith('/')) { - this._fetchCollection(path); - } - }, - - _updatePath : function(path) { - if (path !== undefined || path !== null) { - this.ui.path.val(path); - } - }, - - _selectPath : function() { - var path = this.ui.path.val(); - - this.input.val(path); - this.input.trigger('change'); - - this.input.trigger('filebrowser:folderselected', { - type: 'folder', - path: path - }); - - vent.trigger(vent.Commands.CloseFileBrowser); - } -}); diff --git a/src/UI/Shared/FileBrowser/FileBrowserLayoutTemplate.hbs b/src/UI/Shared/FileBrowser/FileBrowserLayoutTemplate.hbs deleted file mode 100644 index 3c09e7ecc..000000000 --- a/src/UI/Shared/FileBrowser/FileBrowserLayoutTemplate.hbs +++ /dev/null @@ -1,26 +0,0 @@ -<div class="modal-content"> - <div class="modal-header"> - <button type="button" class="close" aria-hidden="true" data-dismiss="modal">×</button> - <h3>File Browser</h3> -</div> - <div class="modal-body"> - <div class="row"> - <div class="col-sm-12"> - <div class="form-group"> - <input type="text" class="form-control x-path" placeholder="Start typing or select a path below"/> - </div> - </div> - </div> - - <div class="row"> - <div class="col-sm-12"> - <div id="x-browser"></div> - </div> - </div> - </div> - <div class="modal-footer"> - <span class="indicator x-indicator"><i class="icon-lidarr-spinner fa-spin"></i></span> - <button class="btn" data-dismiss="modal">Close</button> - <button class="btn btn-primary x-ok">Ok</button> - </div> -</div> diff --git a/src/UI/Shared/FileBrowser/FileBrowserModel.js b/src/UI/Shared/FileBrowser/FileBrowserModel.js deleted file mode 100644 index 3986a5948..000000000 --- a/src/UI/Shared/FileBrowser/FileBrowserModel.js +++ /dev/null @@ -1,3 +0,0 @@ -var Backbone = require('backbone'); - -module.exports = Backbone.Model.extend({}); \ No newline at end of file diff --git a/src/UI/Shared/FileBrowser/FileBrowserNameCell.js b/src/UI/Shared/FileBrowser/FileBrowserNameCell.js deleted file mode 100644 index 90cb704be..000000000 --- a/src/UI/Shared/FileBrowser/FileBrowserNameCell.js +++ /dev/null @@ -1,18 +0,0 @@ -var vent = require('vent'); -var NzbDroneCell = require('../../Cells/NzbDroneCell'); - -module.exports = NzbDroneCell.extend({ - className : 'file-browser-name-cell', - - render : function() { - this.$el.empty(); - - var name = this.model.get(this.column.get('name')); - - this.$el.html(name); - - this.delegateEvents(); - - return this; - } -}); \ No newline at end of file diff --git a/src/UI/Shared/FileBrowser/FileBrowserRow.js b/src/UI/Shared/FileBrowser/FileBrowserRow.js deleted file mode 100644 index af982cf72..000000000 --- a/src/UI/Shared/FileBrowser/FileBrowserRow.js +++ /dev/null @@ -1,24 +0,0 @@ -var _ = require('underscore'); -var Backgrid = require('backgrid'); - -module.exports = Backgrid.Row.extend({ - className : 'file-browser-row', - - events : { - 'click' : '_selectRow' - }, - - _originalInit : Backgrid.Row.prototype.initialize, - - initialize : function() { - this._originalInit.apply(this, arguments); - }, - - _selectRow : function() { - if (this.model.get('type') === 'file') { - this.model.collection.trigger('filebrowser:row:fileselected', this.model); - } else { - this.model.collection.trigger('filebrowser:row:folderselected', this.model); - } - } -}); \ No newline at end of file diff --git a/src/UI/Shared/FileBrowser/FileBrowserTypeCell.js b/src/UI/Shared/FileBrowser/FileBrowserTypeCell.js deleted file mode 100644 index 81c31efcb..000000000 --- a/src/UI/Shared/FileBrowser/FileBrowserTypeCell.js +++ /dev/null @@ -1,28 +0,0 @@ -var vent = require('vent'); -var NzbDroneCell = require('../../Cells/NzbDroneCell'); - -module.exports = NzbDroneCell.extend({ - className : 'file-browser-type-cell', - - render : function() { - this.$el.empty(); - - var type = this.model.get(this.column.get('name')); - var icon = 'icon-lidarr-hdd'; - - if (type === 'computer') { - icon = 'icon-lidarr-browser-computer'; - } else if (type === 'parent') { - icon = 'icon-lidarr-browser-up'; - } else if (type === 'folder') { - icon = 'icon-lidarr-browser-folder'; - } else if (type === 'file') { - icon = 'icon-lidarr-browser-file'; - } - - this.$el.html('<i class="{0}"></i>'.format(icon)); - this.delegateEvents(); - - return this; - } -}); \ No newline at end of file diff --git a/src/UI/Shared/FileBrowser/filebrowser.less b/src/UI/Shared/FileBrowser/filebrowser.less deleted file mode 100644 index c8810147b..000000000 --- a/src/UI/Shared/FileBrowser/filebrowser.less +++ /dev/null @@ -1,24 +0,0 @@ -.file-browser-row { - cursor : pointer; - - .file-size-cell { - white-space : nowrap; - } - - .relative-date-cell { - width : 120px; - white-space : nowrap; - } -} - -.file-browser-type-cell { - width : 16px; -} - -.file-browser-name-cell { - word-break : break-all; -} - -.file-browser-empty { - margin-top : 20px; -} \ No newline at end of file diff --git a/src/UI/Shared/FormatHelpers.js b/src/UI/Shared/FormatHelpers.js deleted file mode 100644 index cf81a0702..000000000 --- a/src/UI/Shared/FormatHelpers.js +++ /dev/null @@ -1,93 +0,0 @@ -var moment = require('moment'); -var filesize = require('filesize'); -var UiSettings = require('./UiSettingsModel'); - -module.exports = { - bytes : function(sourceSize, sourceRounding) { - var size = Number(sourceSize); - var rounding = Number(sourceRounding); - - if (isNaN(size)) { - return ''; - } - - if (isNaN(rounding)) { - rounding = 1; - } - - return filesize(size, { - base : 2, - round : rounding - }); - }, - - relativeDate : function(sourceDate) { - if (!sourceDate) { - return ''; - } - - var date = moment(sourceDate); - var calendarDate = date.calendar(); - - //TODO: It would be nice to not have to hack this... - var strippedCalendarDate = calendarDate.substring(0, calendarDate.indexOf(' at ')); - - if (strippedCalendarDate) { - return strippedCalendarDate; - } - - if (date.isAfter(moment())) { - return 'in ' + date.fromNow(true); - } - - if (date.isBefore(moment().add('years', -1))) { - return date.format(UiSettings.get('shortDateFormat')); - } - - return date.fromNow(); - }, - - pad : function(n, width, z) { - z = z || '0'; - n = n + ''; - return n.length >= width ? n : new Array(width - n.length + 1).join(z) + n; - }, - - number : function(input) { - if (!input) { - return '0'; - } - - return input.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ','); - }, - - plural : function(input, unit) { - if (input === 1) { - return unit; - } - - return unit + 's'; - }, - - timeMinSec : function (s, format) { - - function pad(n, z) { - z = z || 2; - return ('00' + n).slice(-z); - } - - var ms = s % 1000; - s = (s - ms) / 1000; - var secs = s % 60; - s = (s - secs) / 60; - var mins = s; - - if (format === 'ms') { - return pad(mins) + ':' + pad(secs); - } else { - return Math.round(mins,0); - } - }, - - -}; \ No newline at end of file diff --git a/src/UI/Shared/Grid/HeaderCell.js b/src/UI/Shared/Grid/HeaderCell.js deleted file mode 100644 index 73a906c49..000000000 --- a/src/UI/Shared/Grid/HeaderCell.js +++ /dev/null @@ -1,155 +0,0 @@ -module.exports = function() { - var Backgrid = this; - - Backgrid.LidarrHeaderCell = Backgrid.HeaderCell.extend({ - events : { - 'click' : 'onClick' - }, - - _originalInit : Backgrid.HeaderCell.prototype.initialize, - - initialize : function(options) { - this._originalInit.call(this, options); - - this.listenTo(this.collection, 'drone:sort', this.render); - }, - - render : function() { - this.$el.empty(); - this.$el.append(this.column.get('label')); - if (this.column.get('tooltip')) { - this.$el.attr({ - 'title' : this.column.get('tooltip'), - 'data-container' : '.table' - }); - } - - var column = this.column; - var sortable = Backgrid.callByNeed(column.sortable(), column, this.collection); - - if (sortable) { - this.$el.addClass('sortable'); - this.$el.prepend(' <i class="sort-direction-icon"></i>'); - } - - //Do we need this? - this.$el.addClass(column.get('name')); - - if (column.has('className')) { - this.$el.addClass(column.get('className')); - } - - this.delegateEvents(); - this.direction(column.get('direction')); - - if (this.collection.state) { - var name = this._getSortMapping().name; - var order = this.collection.state.order; - - if (name === column.get('name')) { - this._setSortIcon(order); - } else { - this._removeSortIcon(); - } - } - - return this; - }, - - direction : function(dir) { - this.$el.children('i.sort-direction-icon').removeClass('icon-lidarr-sort-asc icon-lidarr-sort-desc'); - - if (arguments.length) { - if (dir) { - this._setSortIcon(dir); - } - - this.column.set('direction', dir); - } - - var columnDirection = this.column.get('direction'); - - if (!columnDirection && this.collection.state) { - var name = this._getSortMapping().name; - var order = this.collection.state.order; - - if (name === this.column.get('name')) { - columnDirection = order; - } - } - - return columnDirection; - }, - - _getSortMapping : function() { - var sortKey = this.collection.state.sortKey; - - if (this.collection._getSortMapping) { - return this.collection._getSortMapping(sortKey); - } - - return { - name : sortKey, - sortKey : sortKey - }; - }, - - onClick : function(e) { - e.preventDefault(); - - var collection = this.collection; - var event = 'backgrid:sort'; - - var column = this.column; - var sortable = Backgrid.callByNeed(column.sortable(), column, collection); - if (sortable) { - var isSorted = this.$el.children('.icon-lidarr-sort-asc,.icon-lidarr-sort-desc').length !== 0; - var direction = collection.state.order; - if (column.get('sortType') === 'fixed' || !isSorted) { - direction = column.get('direction') || 'ascending'; - } else { - if (direction === 'ascending' || direction === -1) { - direction = 'descending'; - } else { - direction = 'ascending'; - } - } - - if (collection.setSorting) { - collection.setSorting(column.get('name'), direction); - } else { - collection.state.sortKey = column.get('name'); - collection.state.order = direction; - } - collection.trigger(event, column, direction); - } - }, - - _resetCellDirection : function(columnToSort, direction) { - if (columnToSort !== this.column) { - this.direction(null); - } else { - this.direction(direction); - } - }, - - _convertDirectionToIcon : function(dir) { - if (dir === 'ascending' || dir === -1) { - return 'icon-lidarr-sort-asc'; - } - - return 'icon-lidarr-sort-desc'; - }, - - _setSortIcon : function(dir) { - this._removeSortIcon(); - this.$el.children('i.sort-direction-icon').addClass(this._convertDirectionToIcon(dir)); - }, - - _removeSortIcon : function() { - this.$el.children('i.sort-direction-icon').removeClass('icon-lidarr-sort-asc icon-lidarr-sort-desc'); - } - }); - - return Backgrid.LidarrHeaderCell; -}; diff --git a/src/UI/Shared/Grid/JumpToPageTemplate.hbs b/src/UI/Shared/Grid/JumpToPageTemplate.hbs deleted file mode 100644 index 9a157ece6..000000000 --- a/src/UI/Shared/Grid/JumpToPageTemplate.hbs +++ /dev/null @@ -1,9 +0,0 @@ -<select class="x-page-select"> - {{#each pages}} - {{#if current}} - <option value="{{page}}" selected="selected">{{page}}</option> - {{else}} - <option value="{{page}}">{{page}}</option> - {{/if}} - {{/each}} -</select> \ No newline at end of file diff --git a/src/UI/Shared/Grid/Pager.js b/src/UI/Shared/Grid/Pager.js deleted file mode 100644 index 36cda08f7..000000000 --- a/src/UI/Shared/Grid/Pager.js +++ /dev/null @@ -1,188 +0,0 @@ -var $ = require('jquery'); -var Marionette = require('marionette'); -var Paginator = require('backgrid.paginator'); - -module.exports = Paginator.extend({ - template : 'Shared/Grid/PagerTemplate', - - events : { - 'click .pager-btn' : 'changePage', - 'click .x-page-number' : '_showPageJumper', - 'change .x-page-select' : '_jumpToPage', - 'blur .x-page-select' : 'render' - }, - - windowSize : 1, - - fastForwardHandleLabels : { - first : 'icon-lidarr-pager-first', - prev : 'icon-lidarr-pager-previous', - next : 'icon-lidarr-pager-next', - last : 'icon-lidarr-pager-last' - }, - - changePage : function(e) { - e.preventDefault(); - - var target = this.$(e.target); - - if (target.closest('li').hasClass('disabled')) { - return; - } - - var icon = target.closest('li i'); - var iconClasses = icon.attr('class').match(/(?:^|\s)icon\-.+?(?:$|\s)/); - var iconClass = $.trim(iconClasses[0]); - - icon.removeClass(iconClass); - icon.addClass('icon-lidarr-spinner fa-spin'); - - var label = target.attr('data-action'); - var ffLabels = this.fastForwardHandleLabels; - - var collection = this.collection; - - if (ffLabels) { - switch (label) { - case 'first': - collection.getFirstPage(); - return; - case 'prev': - if (collection.hasPrevious()) { - collection.getPreviousPage(); - } - return; - case 'next': - if (collection.hasNext()) { - collection.getNextPage(); - } - return; - case 'last': - collection.getLastPage(); - return; - } - } - - var state = collection.state; - var pageIndex = target.text(); - collection.getPage(state.firstPage === 0 ? pageIndex - 1 : pageIndex); - }, - - makeHandles : function() { - var handles = []; - - var collection = this.collection; - var state = collection.state; - - // convert all indices to 0-based here - var firstPage = state.firstPage; - var lastPage = +state.lastPage; - lastPage = Math.max(0, firstPage ? lastPage - 1 : lastPage); - var currentPage = Math.max(state.currentPage, state.firstPage); - currentPage = firstPage ? currentPage - 1 : currentPage; - var windowStart = Math.floor(currentPage / this.windowSize) * this.windowSize; - var windowEnd = Math.min(lastPage + 1, windowStart + this.windowSize); - - if (collection.mode !== 'infinite') { - for (var i = windowStart; i < windowEnd; i++) { - handles.push({ - label : i + 1, - title : 'No. ' + (i + 1), - className : currentPage === i ? 'active' : undefined, - pageNumber : i + 1, - lastPage : lastPage + 1 - }); - } - } - - var ffLabels = this.fastForwardHandleLabels; - if (ffLabels) { - if (ffLabels.prev) { - handles.unshift({ - label : ffLabels.prev, - className : collection.hasPrevious() ? void 0 : 'disabled', - action : 'prev' - }); - } - - if (ffLabels.first) { - handles.unshift({ - label : ffLabels.first, - className : collection.hasPrevious() ? void 0 : 'disabled', - action : 'first' - }); - } - - if (ffLabels.next) { - handles.push({ - label : ffLabels.next, - className : collection.hasNext() ? void 0 : 'disabled', - action : 'next' - }); - } - - if (ffLabels.last) { - handles.push({ - label : ffLabels.last, - className : collection.hasNext() ? void 0 : 'disabled', - action : 'last' - }); - } - } - - return handles; - }, - - render : function() { - this.$el.empty(); - - var templateFunction = Marionette.TemplateCache.get(this.template); - - this.$el.html(templateFunction({ - handles : this.makeHandles(), - state : this.collection.state - })); - - this.delegateEvents(); - - return this; - }, - - _showPageJumper : function(e) { - if ($(e.target).is('select')) { - return; - } - - var templateFunction = Marionette.TemplateCache.get('Shared/Grid/JumpToPageTemplate'); - var state = this.collection.state; - var currentPage = Math.max(state.currentPage, state.firstPage); - currentPage = state.firstPage ? currentPage - 1 : currentPage; - - var pages = []; - - for (var i = 0; i < this.collection.state.lastPage; i++) { - if (i === currentPage) { - pages.push({ - page : i + 1, - current : true - }); - } else { - pages.push({ page : i + 1 }); - } - } - - this.$el.find('.x-page-number').html(templateFunction({ pages : pages })); - }, - - _jumpToPage : function() { - var target = this.$el.find('.x-page-select'); - - //Remove event handlers so the blur event is not triggered - this.undelegateEvents(); - - var selectedPage = parseInt(target.val(), 10); - - this.$el.find('.x-page-number').html('<i class="icon-lidarr-spinner fa-spin"></i>'); - this.collection.getPage(selectedPage); - } -}); \ No newline at end of file diff --git a/src/UI/Shared/Grid/PagerTemplate.hbs b/src/UI/Shared/Grid/PagerTemplate.hbs deleted file mode 100644 index 795e76e6e..000000000 --- a/src/UI/Shared/Grid/PagerTemplate.hbs +++ /dev/null @@ -1,16 +0,0 @@ -<ul> - {{#each handles}} - <li {{#if className}}class="{{className}}"{{/if}} > - {{#if pageNumber}} - <span class="x-page-number">{{pageNumber}} / {{lastPage}}</span> - {{else}} - <i class="pager-btn clickable {{label}}" data-action="{{action}}"/> - {{/if}} - </li> - {{/each}} -</ul> - -<span class="total-records"> - <span class="hidden-xs">Total records: {{Number state.totalRecords}}</span> - <span class="visible-xs label label-info" title="Total records">{{Number state.totalRecords}}</span> -</span> \ No newline at end of file diff --git a/src/UI/Shared/LoadingView.js b/src/UI/Shared/LoadingView.js deleted file mode 100644 index 1b703940e..000000000 --- a/src/UI/Shared/LoadingView.js +++ /dev/null @@ -1,6 +0,0 @@ -var Marionette = require('marionette'); - -module.exports = Marionette.ItemView.extend({ - template : 'Shared/LoadingViewTemplate', - className : 'nz-loading row' -}); \ No newline at end of file diff --git a/src/UI/Shared/LoadingViewTemplate.hbs b/src/UI/Shared/LoadingViewTemplate.hbs deleted file mode 100644 index 1ae3d7f54..000000000 --- a/src/UI/Shared/LoadingViewTemplate.hbs +++ /dev/null @@ -1,10 +0,0 @@ -<div class="row"> - <div class="col-md-12"> - <div id="followingBalls"> - <div id="ball-1" class="ball"></div> - <div id="ball-2" class="ball"></div> - <div id="ball-3" class="ball"></div> - <div id="ball-4" class="ball"></div> - </div> - </div> -</div> diff --git a/src/UI/Shared/Messenger.js b/src/UI/Shared/Messenger.js deleted file mode 100644 index 11837396a..000000000 --- a/src/UI/Shared/Messenger.js +++ /dev/null @@ -1,66 +0,0 @@ -require('messenger'); - -var messenger = require('messenger'); -module.exports = { - show : function(options) { - if (!options.type) { - options.type = 'info'; - } - - if (options.hideAfter === undefined) { - switch (options.type) { - case 'info': - options.hideAfter = 5; - break; - - case 'success': - options.hideAfter = 5; - break; - - default: - options.hideAfter = 5; - } - } - - options.hideOnNavigate = options.hideOnNavigate || false; - - return messenger().post({ - message : options.message, - type : options.type, - showCloseButton : true, - hideAfter : options.hideAfter, - id : options.id, - actions : options.actions, - hideOnNavigate : options.hideOnNavigate - }); - }, - - monitor : function(options) { - if (!options.promise) { - throw 'promise is required'; - } - - if (!options.successMessage) { - throw 'success message is required'; - } - - if (!options.errorMessage) { - throw 'error message is required'; - } - - var self = this; - - options.promise.done(function() { - self.show({ message : options.successMessage }); - }); - - options.promise.fail(function() { - self.show({ - message : options.errorMessage, - type : 'error' - }); - }); - - return options.promise; - } -}; \ No newline at end of file diff --git a/src/UI/Shared/Modal/ModalController.js b/src/UI/Shared/Modal/ModalController.js deleted file mode 100644 index 173399ade..000000000 --- a/src/UI/Shared/Modal/ModalController.js +++ /dev/null @@ -1,102 +0,0 @@ -var vent = require('vent'); -var AppLayout = require('../../AppLayout'); -var Marionette = require('marionette'); -var EditArtistView = require('../../Artist/Edit/EditArtistView'); -var DeleteArtistView = require('../../Artist/Delete/DeleteArtistView'); -var EpisodeDetailsLayout = require('../../Episode/EpisodeDetailsLayout'); -var AlbumDetailsLayout = require('../../Album/AlbumDetailsLayout'); -var HistoryDetailsLayout = require('../../Activity/History/Details/HistoryDetailsLayout'); -var LogDetailsView = require('../../System/Logs/Table/Details/LogDetailsView'); -var RenamePreviewLayout = require('../../Rename/RenamePreviewLayout'); -var ManualImportLayout = require('../../ManualImport/ManualImportLayout'); -var FileBrowserLayout = require('../FileBrowser/FileBrowserLayout'); - -module.exports = Marionette.AppRouter.extend({ - initialize : function() { - vent.on(vent.Commands.OpenModalCommand, this._openModal, this); - vent.on(vent.Commands.CloseModalCommand, this._closeModal, this); - vent.on(vent.Commands.OpenModal2Command, this._openModal2, this); - vent.on(vent.Commands.CloseModal2Command, this._closeModal2, this); - vent.on(vent.Commands.EditArtistCommand, this._editArtist, this); - vent.on(vent.Commands.DeleteArtistCommand, this._deleteArtist, this); - vent.on(vent.Commands.ShowEpisodeDetails, this._showEpisode, this); - vent.on(vent.Commands.ShowAlbumDetails, this._showAlbum, this); - vent.on(vent.Commands.ShowHistoryDetails, this._showHistory, this); - vent.on(vent.Commands.ShowLogDetails, this._showLogDetails, this); - vent.on(vent.Commands.ShowRenamePreview, this._showRenamePreview, this); - vent.on(vent.Commands.ShowManualImport, this._showManualImport, this); - vent.on(vent.Commands.ShowFileBrowser, this._showFileBrowser, this); - vent.on(vent.Commands.CloseFileBrowser, this._closeFileBrowser, this); - }, - - _openModal : function(view) { - AppLayout.modalRegion.show(view); - }, - - _closeModal : function() { - AppLayout.modalRegion.closeModal(); - }, - - _openModal2 : function(view) { - AppLayout.modalRegion2.show(view); - }, - - _closeModal2 : function() { - AppLayout.modalRegion2.closeModal(); - }, - - _editArtist : function(options) { - var view = new EditArtistView({ model : options.artist }); - AppLayout.modalRegion.show(view); - }, - - _deleteArtist : function(options) { - var view = new DeleteArtistView({ model : options.artist }); - AppLayout.modalRegion.show(view); - }, - - _showEpisode : function(options) { - var view = new EpisodeDetailsLayout({ - model : options.episode, - hideSeriesLink : options.hideSeriesLink, - openingTab : options.openingTab - }); - AppLayout.modalRegion.show(view); - }, - - _showAlbum : function(options) { - var view = new AlbumDetailsLayout({ - model : options.album - }); - AppLayout.modalRegion.show(view); - }, - - _showHistory : function(options) { - var view = new HistoryDetailsLayout({ model : options.model }); - AppLayout.modalRegion.show(view); - }, - - _showLogDetails : function(options) { - var view = new LogDetailsView({ model : options.model }); - AppLayout.modalRegion.show(view); - }, - - _showRenamePreview : function(options) { - var view = new RenamePreviewLayout(options); - AppLayout.modalRegion.show(view); - }, - - _showManualImport : function(options) { - var view = new ManualImportLayout(options); - AppLayout.modalRegion.show(view); - }, - - _showFileBrowser : function(options) { - var view = new FileBrowserLayout(options); - AppLayout.modalRegion2.show(view); - }, - - _closeFileBrowser : function() { - AppLayout.modalRegion2.closeModal(); - } -}); \ No newline at end of file diff --git a/src/UI/Shared/Modal/ModalRegion.js b/src/UI/Shared/Modal/ModalRegion.js deleted file mode 100644 index 91fccccc5..000000000 --- a/src/UI/Shared/Modal/ModalRegion.js +++ /dev/null @@ -1,7 +0,0 @@ -var ModalRegionBase = require('./ModalRegionBase'); - -var region = ModalRegionBase.extend({ - el : '#modal-region' -}); - -module.exports = region; \ No newline at end of file diff --git a/src/UI/Shared/Modal/ModalRegion2.js b/src/UI/Shared/Modal/ModalRegion2.js deleted file mode 100644 index f9f38bea4..000000000 --- a/src/UI/Shared/Modal/ModalRegion2.js +++ /dev/null @@ -1,30 +0,0 @@ -var $ = require('jquery'); -var ModalRegionBase = require('./ModalRegionBase'); - -var region = ModalRegionBase.extend({ - el : '#modal-region2', - - initialize : function () { - this.listenTo(this, 'modal:beforeShow', this.onBeforeShow); - }, - - onBeforeShow : function () { - this.$el.addClass('modal fade'); - this.$el.attr('tabindex', '-1'); - this.$el.css('z-index', '1060'); - - this.$el.on('shown.bs.modal', function() { - $('.modal-backdrop:last').css('z-index', 1059); - }); - }, - - _closed : function () { - ModalRegionBase.prototype._closed.apply(this, arguments); - - if (require('../../AppLayout').modalRegion.currentView) { - $('body').addClass('modal-open'); - } - } -}); - -module.exports = region; \ No newline at end of file diff --git a/src/UI/Shared/Modal/ModalRegionBase.js b/src/UI/Shared/Modal/ModalRegionBase.js deleted file mode 100644 index 91c8ab32d..000000000 --- a/src/UI/Shared/Modal/ModalRegionBase.js +++ /dev/null @@ -1,65 +0,0 @@ -var _ = require('underscore'); -var $ = require('jquery'); -var Backbone = require('backbone'); -var Marionette = require('marionette'); -require('bootstrap'); -var region = Marionette.Region.extend({ - el : '#modal-region', - - constructor : function() { - Backbone.Marionette.Region.prototype.constructor.apply(this, arguments); - this.on('show', this.showModal, this); - }, - - getEl : function(selector) { - var $el = $(selector); - $el.on('hidden', this.close); - return $el; - }, - - showModal : function() { - this.trigger('modal:beforeShow'); - this.$el.addClass('modal fade'); - - //need tab index so close on escape works - //https://github.com/twitter/bootstrap/issues/4663 - this.$el.attr('tabindex', '-1'); - this.$el.modal({ - show : true, - keyboard : true, - backdrop : true - }); - - this.$el.on('hide.bs.modal', $.proxy(this._closing, this)); - this.$el.on('hidden.bs.modal', $.proxy(this._closed, this)); - - this.currentView.$el.addClass('modal-dialog'); - - this.$el.on('shown.bs.modal', _.bind(function() { - this.trigger('modal:afterShow'); - this.currentView.trigger('modal:afterShow'); - }, this)); - }, - - closeModal : function() { - $(this.el).modal('hide'); - this.reset(); - }, - - _closing : function() { - if (this.$el) { - this.$el.off('hide.bs.modal'); - this.$el.off('shown.bs.modal'); - } - - this.reset(); - }, - - _closed: function () { - if (this.$el) { - this.$el.off('hidden.bs.modal'); - } - } -}); - -module.exports = region; \ No newline at end of file diff --git a/src/UI/Shared/NotFoundView.js b/src/UI/Shared/NotFoundView.js deleted file mode 100644 index f0b34039a..000000000 --- a/src/UI/Shared/NotFoundView.js +++ /dev/null @@ -1,5 +0,0 @@ -var Marionette = require('marionette'); - -module.exports = Marionette.ItemView.extend({ - template : 'Shared/NotFoundViewTemplate' -}); \ No newline at end of file diff --git a/src/UI/Shared/NotFoundViewTemplate.hbs b/src/UI/Shared/NotFoundViewTemplate.hbs deleted file mode 100644 index 4073bba9f..000000000 --- a/src/UI/Shared/NotFoundViewTemplate.hbs +++ /dev/null @@ -1,4 +0,0 @@ -<div> - <img src="{{UrlBase}}/Content/Images/404.png" style="height:400px; margin-top: 50px"/> - -</div> diff --git a/src/UI/Shared/NzbDroneController.js b/src/UI/Shared/NzbDroneController.js deleted file mode 100644 index 4cdbd2510..000000000 --- a/src/UI/Shared/NzbDroneController.js +++ /dev/null @@ -1,67 +0,0 @@ -var vent = require('vent'); -var AppLayout = require('../AppLayout'); -var Marionette = require('marionette'); -var NotFoundView = require('./NotFoundView'); -var Messenger = require('./Messenger'); - -module.exports = Marionette.AppRouter.extend({ - initialize : function() { - this.listenTo(vent, vent.Events.ServerUpdated, this._onServerUpdated); - }, - - showNotFound : function() { - this.setTitle('Not Found'); - this.showMainRegion(new NotFoundView(this)); - }, - - setTitle : function(title) { - title = title; - if (title === 'Lidarr') { - document.title = 'Lidarr'; - } else { - document.title = title + ' - Lidarr'; - } - - if (window.NzbDrone.Analytics && window.Piwik) { - try { - var piwik = window.Piwik.getTracker(window.location.protocol + '//piwik.nzbdrone.com/piwik.php', 1); - piwik.setReferrerUrl(''); - piwik.setCustomUrl('http://local' + window.location.pathname); - piwik.setCustomVariable(1, 'version', window.NzbDrone.Version, 'page'); - piwik.setCustomVariable(2, 'branch', window.NzbDrone.Branch, 'page'); - piwik.trackPageView(title); - } - catch (e) { - console.error(e); - } - } - }, - - _onServerUpdated : function() { - var label = window.location.pathname === window.NzbDrone.UrlBase + '/system/updates' ? 'Reload' : 'View Changes'; - - Messenger.show({ - message : 'Lidarr has been updated', - hideAfter : 0, - id : 'lidarrUpdated', - actions : { - viewChanges : { - label : label, - action : function() { - window.location = window.NzbDrone.UrlBase + '/system/updates'; - } - } - } - }); - - this.pendingUpdate = true; - }, - - showMainRegion : function(view) { - if (this.pendingUpdate) { - window.location.reload(); - } else { - AppLayout.mainRegion.show(view); - } - } -}); \ No newline at end of file diff --git a/src/UI/Shared/SignalRBroadcaster.js b/src/UI/Shared/SignalRBroadcaster.js deleted file mode 100644 index 204f77ab5..000000000 --- a/src/UI/Shared/SignalRBroadcaster.js +++ /dev/null @@ -1,76 +0,0 @@ -var vent = require('vent'); -var $ = require('jquery'); -var Messenger = require('./Messenger'); -var StatusModel = require('../System/StatusModel'); -require('signalR'); - -module.exports = { - appInitializer : function() { - console.log('starting signalR'); - - var getStatus = function(status) { - switch (status) { - case 0: - return 'connecting'; - case 1: - return 'connected'; - case 2: - return 'reconnecting'; - case 4: - return 'disconnected'; - default: - throw 'invalid status ' + status; - } - }; - - var tryingToReconnect = false; - var messengerId = 'signalR'; - - this.signalRconnection = $.connection(StatusModel.get('urlBase') + '/signalr', { apiKey: window.NzbDrone.ApiKey }); - - this.signalRconnection.stateChanged(function(change) { - console.debug('SignalR: [{0}]'.format(getStatus(change.newState))); - }); - - this.signalRconnection.received(function(message) { - vent.trigger('server:' + message.name, message.body); - }); - - this.signalRconnection.reconnecting(function() { - if (window.NzbDrone.unloading) { - return; - } - - tryingToReconnect = true; - }); - - this.signalRconnection.reconnected(function() { - tryingToReconnect = false; - }); - - this.signalRconnection.disconnected(function() { - if (tryingToReconnect) { - $('<div class="modal-backdrop fade in"></div>').appendTo(document.body); - - Messenger.show({ - id : messengerId, - type : 'error', - hideAfter : 0, - message : 'Connection to backend lost', - actions : { - cancel : { - label : 'Reload', - action : function() { - window.location.reload(); - } - } - } - }); - } - }); - - this.signalRconnection.start({ transport : ['longPolling'] }); - - return this; - } -}; diff --git a/src/UI/Shared/Styles/card.less b/src/UI/Shared/Styles/card.less deleted file mode 100644 index 92c275a8b..000000000 --- a/src/UI/Shared/Styles/card.less +++ /dev/null @@ -1,10 +0,0 @@ -@import "../../Content/prefixer"; - -.card(@color : #e1e1e1 ) { - margin : 10px; - background-color : #ffffff; - padding : 10px; - color : #444444; - .box-shadow( 0px 0px 10px 1px @color); - .border-radius(3px); -} diff --git a/src/UI/Shared/Toolbar/Button/ButtonCollectionView.js b/src/UI/Shared/Toolbar/Button/ButtonCollectionView.js deleted file mode 100644 index 097df89ab..000000000 --- a/src/UI/Shared/Toolbar/Button/ButtonCollectionView.js +++ /dev/null @@ -1,22 +0,0 @@ -var Marionette = require('marionette'); -var ButtonView = require('./ButtonView'); - -module.exports = Marionette.CollectionView.extend({ - className : 'btn-group', - itemView : ButtonView, - - initialize : function(options) { - this.menu = options.menu; - this.className = 'btn-group'; - - if (options.menu.collapse) { - this.className += ' btn-group-collapse'; - } - }, - - onRender : function() { - if (this.menu.collapse) { - this.$el.addClass('btn-group-collapse'); - } - } -}); \ No newline at end of file diff --git a/src/UI/Shared/Toolbar/Button/ButtonView.js b/src/UI/Shared/Toolbar/Button/ButtonView.js deleted file mode 100644 index 20e77e4e9..000000000 --- a/src/UI/Shared/Toolbar/Button/ButtonView.js +++ /dev/null @@ -1,85 +0,0 @@ -var Backbone = require('backbone'); -var Marionette = require('marionette'); -var _ = require('underscore'); -var CommandController = require('../../../Commands/CommandController'); - -module.exports = Marionette.ItemView.extend({ - template : 'Shared/Toolbar/ButtonTemplate', - className : 'btn btn-default btn-icon-only-xs', - - ui : { - icon : 'i' - }, - - events : { - 'click' : 'onClick' - }, - - initialize : function() { - this.storageKey = this.model.get('menuKey') + ':' + this.model.get('key'); - }, - - onRender : function() { - if (this.model.get('active')) { - this.$el.addClass('active'); - this.invokeCallback(); - } - - if (!this.model.get('title')) { - this.$el.addClass('btn-icon-only'); - } - - if (this.model.get('className')) { - this.$el.addClass(this.model.get('className')); - } - - if (this.model.get('tooltip')) { - this.$el.attr('title', this.model.get('tooltip')); - } - - var command = this.model.get('command'); - if (command) { - var properties = _.extend({ name : command }, this.model.get('properties')); - - CommandController.bindToCommand({ - command : properties, - element : this.$el - }); - } - }, - - onClick : function() { - if (this.$el.hasClass('disabled')) { - return; - } - - this.invokeCallback(); - this.invokeRoute(); - this.invokeCommand(); - }, - - invokeCommand : function() { - var command = this.model.get('command'); - if (command) { - CommandController.Execute(command, this.model.get('properties')); - } - }, - - invokeRoute : function() { - var route = this.model.get('route'); - if (route) { - Backbone.history.navigate(route, { trigger : true }); - } - }, - - invokeCallback : function() { - if (!this.model.ownerContext) { - throw 'ownerContext must be set.'; - } - - var callback = this.model.get('callback'); - if (callback) { - callback.call(this.model.ownerContext, this); - } - } -}); \ No newline at end of file diff --git a/src/UI/Shared/Toolbar/ButtonCollection.js b/src/UI/Shared/Toolbar/ButtonCollection.js deleted file mode 100644 index a48a04574..000000000 --- a/src/UI/Shared/Toolbar/ButtonCollection.js +++ /dev/null @@ -1,6 +0,0 @@ -var Backbone = require('backbone'); -var ButtonModel = require('./ButtonModel'); - -module.exports = Backbone.Collection.extend({ - model : ButtonModel -}); \ No newline at end of file diff --git a/src/UI/Shared/Toolbar/ButtonModel.js b/src/UI/Shared/Toolbar/ButtonModel.js deleted file mode 100644 index 055a4ef8f..000000000 --- a/src/UI/Shared/Toolbar/ButtonModel.js +++ /dev/null @@ -1,11 +0,0 @@ -var _ = require('underscore'); -var Backbone = require('backbone'); - -module.exports = Backbone.Model.extend({ - defaults : { - 'target' : '/nzbdrone/route', - 'title' : '', - 'active' : false, - 'tooltip' : undefined - } -}); \ No newline at end of file diff --git a/src/UI/Shared/Toolbar/ButtonTemplate.hbs b/src/UI/Shared/Toolbar/ButtonTemplate.hbs deleted file mode 100644 index d21cdbc9d..000000000 --- a/src/UI/Shared/Toolbar/ButtonTemplate.hbs +++ /dev/null @@ -1 +0,0 @@ -<i class="{{icon}} x-icon"/><span> {{title}}</span> diff --git a/src/UI/Shared/Toolbar/Radio/RadioButtonCollectionView.js b/src/UI/Shared/Toolbar/Radio/RadioButtonCollectionView.js deleted file mode 100644 index 70c4fb188..000000000 --- a/src/UI/Shared/Toolbar/Radio/RadioButtonCollectionView.js +++ /dev/null @@ -1,37 +0,0 @@ -var Marionette = require('marionette'); -var RadioButtonView = require('./RadioButtonView'); -var Config = require('../../../Config'); - -module.exports = Marionette.CollectionView.extend({ - className : 'btn-group', - itemView : RadioButtonView, - - attributes : { - 'data-toggle' : 'buttons' - }, - - initialize : function(options) { - this.menu = options.menu; - - this.setActive(); - }, - - setActive : function() { - var storedKey = this.menu.defaultAction; - - if (this.menu.storeState) { - storedKey = Config.getValue(this.menu.menuKey, storedKey); - } - - if (!storedKey) { - return; - } - this.collection.each(function(model) { - if (model.get('key').toLocaleLowerCase() === storedKey.toLowerCase()) { - model.set('active', true); - } else { - model.set('active, false'); - } - }); - } -}); \ No newline at end of file diff --git a/src/UI/Shared/Toolbar/Radio/RadioButtonView.js b/src/UI/Shared/Toolbar/Radio/RadioButtonView.js deleted file mode 100644 index 90fa0bd0c..000000000 --- a/src/UI/Shared/Toolbar/Radio/RadioButtonView.js +++ /dev/null @@ -1,50 +0,0 @@ -var Marionette = require('marionette'); -var Config = require('../../../Config'); - -module.exports = Marionette.ItemView.extend({ - template : 'Shared/Toolbar/RadioButtonTemplate', - className : 'btn btn-default', - - ui : { - icon : 'i' - }, - - events : { - 'click' : 'onClick' - }, - - initialize : function() { - this.storageKey = this.model.get('menuKey') + ':' + this.model.get('key'); - }, - - onRender : function() { - if (this.model.get('active')) { - this.$el.addClass('active'); - this.invokeCallback(); - } - - if (!this.model.get('title')) { - this.$el.addClass('btn-icon-only'); - } - - if (this.model.get('tooltip')) { - this.$el.attr('title', this.model.get('tooltip')); - } - }, - - onClick : function() { - Config.setValue(this.model.get('menuKey'), this.model.get('key')); - this.invokeCallback(); - }, - - invokeCallback : function() { - if (!this.model.ownerContext) { - throw 'ownerContext must be set.'; - } - - var callback = this.model.get('callback'); - if (callback) { - callback.call(this.model.ownerContext, this); - } - } -}); \ No newline at end of file diff --git a/src/UI/Shared/Toolbar/RadioButtonTemplate.hbs b/src/UI/Shared/Toolbar/RadioButtonTemplate.hbs deleted file mode 100644 index aaff67405..000000000 --- a/src/UI/Shared/Toolbar/RadioButtonTemplate.hbs +++ /dev/null @@ -1 +0,0 @@ -<input type="radio"><i class="{{icon}} x-icon"/><span> {{title}}</span> diff --git a/src/UI/Shared/Toolbar/Sorting/SortingButtonCollectionView.js b/src/UI/Shared/Toolbar/Sorting/SortingButtonCollectionView.js deleted file mode 100644 index 6db8995a2..000000000 --- a/src/UI/Shared/Toolbar/Sorting/SortingButtonCollectionView.js +++ /dev/null @@ -1,38 +0,0 @@ -var PageableCollection = require('backbone.pageable'); -var Marionette = require('marionette'); -var ButtonView = require('./SortingButtonView'); - -module.exports = Marionette.CompositeView.extend({ - itemView : ButtonView, - template : 'Shared/Toolbar/Sorting/SortingButtonCollectionViewTemplate', - itemViewContainer : '.dropdown-menu', - - initialize : function(options) { - this.viewCollection = options.viewCollection; - this.listenTo(this.viewCollection, 'drone:sort', this.sort); - }, - - itemViewOptions : function() { - return { - viewCollection : this.viewCollection - }; - }, - - sort : function(sortModel, sortDirection) { - var collection = this.viewCollection; - - var order; - if (sortDirection === 'ascending') { - order = -1; - } else if (sortDirection === 'descending') { - order = 1; - } else { - order = null; - } - - collection.setSorting(sortModel.get('name'), order); - collection.fullCollection.sort(); - - return this; - } -}); \ No newline at end of file diff --git a/src/UI/Shared/Toolbar/Sorting/SortingButtonCollectionViewTemplate.hbs b/src/UI/Shared/Toolbar/Sorting/SortingButtonCollectionViewTemplate.hbs deleted file mode 100644 index 80d4888de..000000000 --- a/src/UI/Shared/Toolbar/Sorting/SortingButtonCollectionViewTemplate.hbs +++ /dev/null @@ -1,8 +0,0 @@ -<div class="btn-group sorting-buttons"> - <a class="btn btn-default dropdown-toggle" data-toggle="dropdown" href="#"> - Sort <span class="caret"></span> - </a> - <ul class="dropdown-menu"> - - </ul> -</div> diff --git a/src/UI/Shared/Toolbar/Sorting/SortingButtonView.js b/src/UI/Shared/Toolbar/Sorting/SortingButtonView.js deleted file mode 100644 index 9ffb4f1e8..000000000 --- a/src/UI/Shared/Toolbar/Sorting/SortingButtonView.js +++ /dev/null @@ -1,70 +0,0 @@ -var Backbone = require('backbone'); -var Marionette = require('marionette'); -var _ = require('underscore'); - -module.exports = Marionette.ItemView.extend({ - template : 'Shared/Toolbar/Sorting/SortingButtonViewTemplate', - tagName : 'li', - - ui : { - icon : 'i' - }, - - events : { - 'click' : 'onClick' - }, - - initialize : function(options) { - this.viewCollection = options.viewCollection; - this.listenTo(this.viewCollection, 'drone:sort', this.render); - this.listenTo(this.viewCollection, 'backgrid:sort', this.render); - }, - - onRender : function() { - if (this.viewCollection.state) { - var sortKey = this.viewCollection.state.sortKey; - var name = this.viewCollection._getSortMapping(sortKey).name; - var order = this.viewCollection.state.order; - - if (name === this.model.get('name')) { - this._setSortIcon(order); - } else { - this._removeSortIcon(); - } - } - }, - - onClick : function(e) { - e.preventDefault(); - - var collection = this.viewCollection; - var event = 'drone:sort'; - - var direction = collection.state.order; - if (direction === 'ascending' || direction === -1) { - direction = 'descending'; - } else { - direction = 'ascending'; - } - - collection.setSorting(this.model.get('name'), direction); - collection.trigger(event, this.model, direction); - }, - - _convertDirectionToIcon : function(dir) { - if (dir === 'ascending' || dir === -1) { - return 'icon-lidarr-sort-asc'; - } - - return 'icon-lidarr-sort-desc'; - }, - - _setSortIcon : function(dir) { - this._removeSortIcon(); - this.ui.icon.addClass(this._convertDirectionToIcon(dir)); - }, - - _removeSortIcon : function() { - this.ui.icon.removeClass('icon-lidarr-sort-asc icon-lidarr-sort-desc'); - } -}); \ No newline at end of file diff --git a/src/UI/Shared/Toolbar/Sorting/SortingButtonViewTemplate.hbs b/src/UI/Shared/Toolbar/Sorting/SortingButtonViewTemplate.hbs deleted file mode 100644 index 57018028d..000000000 --- a/src/UI/Shared/Toolbar/Sorting/SortingButtonViewTemplate.hbs +++ /dev/null @@ -1,4 +0,0 @@ -<a href="#"> - <span class="sorting-title">{{title}}</span> - <i class=""></i> -</a> \ No newline at end of file diff --git a/src/UI/Shared/Toolbar/ToolbarLayout.js b/src/UI/Shared/Toolbar/ToolbarLayout.js deleted file mode 100644 index ebad4c789..000000000 --- a/src/UI/Shared/Toolbar/ToolbarLayout.js +++ /dev/null @@ -1,108 +0,0 @@ -var Marionette = require('marionette'); -var ButtonCollection = require('./ButtonCollection'); -var ButtonModel = require('./ButtonModel'); -var RadioButtonCollectionView = require('./Radio/RadioButtonCollectionView'); -var ButtonCollectionView = require('./Button/ButtonCollectionView'); -var SortingButtonCollectionView = require('./Sorting/SortingButtonCollectionView'); -var _ = require('underscore'); - -module.exports = Marionette.Layout.extend({ - template : 'Shared/Toolbar/ToolbarLayoutTemplate', - className : 'toolbar', - - ui : { - left_x : '.x-toolbar-left', - right_x : '.x-toolbar-right' - }, - - initialize : function(options) { - if (!options) { - throw 'options needs to be passed'; - } - - if (!options.context) { - throw 'context needs to be passed'; - } - - this.templateHelpers = { - floatOnMobile : options.floatOnMobile || false - }; - - this.left = options.left; - this.right = options.right; - this.toolbarContext = options.context; - }, - - onShow : function() { - if (this.left) { - _.each(this.left, this._showToolbarLeft, this); - } - if (this.right) { - _.each(this.right, this._showToolbarRight, this); - } - }, - - _showToolbarLeft : function(element, index) { - this._showToolbar(element, index, 'left'); - }, - - _showToolbarRight : function(element, index) { - this._showToolbar(element, index, 'right'); - }, - - _showToolbar : function(buttonGroup, index, position) { - var groupCollection = new ButtonCollection(); - - _.each(buttonGroup.items, function(button) { - if (buttonGroup.storeState && !button.key) { - throw 'must provide key for all buttons when storeState is enabled'; - } - - var model = new ButtonModel(button); - model.set('menuKey', buttonGroup.menuKey); - model.ownerContext = this.toolbarContext; - groupCollection.add(model); - }, this); - - var buttonGroupView; - - switch (buttonGroup.type) { - case 'radio': - { - buttonGroupView = new RadioButtonCollectionView({ - collection : groupCollection, - menu : buttonGroup - }); - break; - } - case 'sorting': - { - buttonGroupView = new SortingButtonCollectionView({ - collection : groupCollection, - menu : buttonGroup, - viewCollection : buttonGroup.viewCollection - }); - break; - } - default: - { - buttonGroupView = new ButtonCollectionView({ - collection : groupCollection, - menu : buttonGroup - }); - break; - } - } - - var regionId = position + '_' + (index + 1); - var region = this[regionId]; - - if (!region) { - var regionClassName = 'x-toolbar-' + position + '-' + (index + 1); - this.ui[position + '_x'].append('<div class="toolbar-group ' + regionClassName + '" />\r\n'); - region = this.addRegion(regionId, '.' + regionClassName); - } - - region.show(buttonGroupView); - } -}); \ No newline at end of file diff --git a/src/UI/Shared/Toolbar/ToolbarLayoutTemplate.hbs b/src/UI/Shared/Toolbar/ToolbarLayoutTemplate.hbs deleted file mode 100644 index 0cd0e21e0..000000000 --- a/src/UI/Shared/Toolbar/ToolbarLayoutTemplate.hbs +++ /dev/null @@ -1,2 +0,0 @@ -<div class="page-toolbar pull-left {{#unless floatOnMobile}}pull-none-xs{{/unless}} x-toolbar-left" /> -<div class="page-toolbar pull-right {{#unless floatOnMobile}}pull-none-xs{{/unless}} x-toolbar-right" /> diff --git a/src/UI/Shared/Tooltip.js b/src/UI/Shared/Tooltip.js deleted file mode 100644 index c19b369fb..000000000 --- a/src/UI/Shared/Tooltip.js +++ /dev/null @@ -1,47 +0,0 @@ -var $ = require('jquery'); -require('bootstrap'); - -var Tooltip = $.fn.tooltip.Constructor; - -var origGetOptions = Tooltip.prototype.getOptions; -Tooltip.prototype.getOptions = function(options) { - var result = origGetOptions.call(this, options); - - if (result.container === false) { - - var container = this.$element.closest('.btn-group,.input-group').parent(); - - if (container.length) { - result.container = container; - } - } - - return result; -}; - -var onElementRemoved = function(event) { - event.data.hide(); -}; - -var origShow = Tooltip.prototype.show; -Tooltip.prototype.show = function() { - origShow.call(this); - - this.$element.on('remove', this, onElementRemoved); -}; - -var origHide = Tooltip.prototype.hide; -Tooltip.prototype.hide = function() { - origHide.call(this); - - this.$element.off('remove', onElementRemoved); -}; - -module.exports = { - appInitializer : function() { - - $('body').tooltip({ selector : '[title]' }); - - return this; - } -}; \ No newline at end of file diff --git a/src/UI/Shared/UiSettingsController.js b/src/UI/Shared/UiSettingsController.js deleted file mode 100644 index eb9210659..000000000 --- a/src/UI/Shared/UiSettingsController.js +++ /dev/null @@ -1,26 +0,0 @@ -var $ = require('jquery'); -var _ = require('underscore'); -var UiSettingsModel = require('./UiSettingsModel'); - -var Controller = { - - appInitializer : function() { - - UiSettingsModel.on('sync', this._updateUiSettings); - - this._updateUiSettings(); - }, - - _updateUiSettings: function() { - - if (UiSettingsModel.get('enableColorImpairedMode')) { - $('body').addClass('color-impaired-mode'); - } else { - $('body').removeClass('color-impaired-mode'); - } - } -}; - -_.bindAll(Controller, 'appInitializer'); - -module.exports = Controller; \ No newline at end of file diff --git a/src/UI/Shared/UiSettingsModel.js b/src/UI/Shared/UiSettingsModel.js deleted file mode 100644 index a517b5aba..000000000 --- a/src/UI/Shared/UiSettingsModel.js +++ /dev/null @@ -1,29 +0,0 @@ -var Backbone = require('backbone'); -var ApiData = require('./ApiData'); - -var UiSettings = Backbone.Model.extend({ - url : window.NzbDrone.ApiRoot + '/config/ui', - - shortDateTime : function(includeSeconds) { - return this.get('shortDateFormat') + ' ' + this.time(true, includeSeconds); - }, - - longDateTime : function(includeSeconds) { - return this.get('longDateFormat') + ' ' + this.time(true, includeSeconds); - }, - - time : function(includeMinuteZero, includeSeconds) { - if (includeSeconds) { - return this.get('timeFormat').replace(/\(?\:mm\)?/, ':mm:ss'); - } - if (includeMinuteZero) { - return this.get('timeFormat').replace('(', '').replace(')', ''); - } - - return this.get('timeFormat').replace(/\(\:mm\)/, ''); - } -}); - -var instance = new UiSettings(ApiData.get('config/ui')); - -module.exports = instance; \ No newline at end of file diff --git a/src/UI/Shared/VersionChangeMonitor.js b/src/UI/Shared/VersionChangeMonitor.js deleted file mode 100644 index 932d97f6c..000000000 --- a/src/UI/Shared/VersionChangeMonitor.js +++ /dev/null @@ -1,13 +0,0 @@ -var $ = require('jquery'); -var vent = require('vent'); - -$(document).ajaxSuccess(function(event, xhr) { - var version = xhr.getResponseHeader('X-ApplicationVersion'); - if (!version || !window.NzbDrone || !window.NzbDrone.Version) { - return; - } - - if (version !== window.NzbDrone.Version) { - vent.trigger(vent.Events.ServerUpdated); - } -}); diff --git a/src/UI/Shared/piwikCheck.js b/src/UI/Shared/piwikCheck.js deleted file mode 100644 index fdda0639c..000000000 --- a/src/UI/Shared/piwikCheck.js +++ /dev/null @@ -1,12 +0,0 @@ -'use strict'; - -if(window.NzbDrone.Analytics) { - var d = document; - var g = d.createElement('script'); - var s = d.getElementsByTagName('script')[0]; - g.type = 'text/javascript'; - g.async = true; - g.defer = true; - g.src = '//piwik.lidarr.tv/piwik.js'; - s.parentNode.insertBefore(g, s); -} diff --git a/src/UI/Shims/backbone.backgrid.selectall.js b/src/UI/Shims/backbone.backgrid.selectall.js deleted file mode 100644 index 10b92f5d9..000000000 --- a/src/UI/Shims/backbone.backgrid.selectall.js +++ /dev/null @@ -1,4 +0,0 @@ -var backgrid = require('backgrid'); -require('../JsLibraries/backbone.backgrid.selectall'); - -module.exports = backgrid.Extension.SelectRowCell; \ No newline at end of file diff --git a/src/UI/Shims/backbone.collectionview.js b/src/UI/Shims/backbone.collectionview.js deleted file mode 100644 index a4080a462..000000000 --- a/src/UI/Shims/backbone.collectionview.js +++ /dev/null @@ -1,4 +0,0 @@ -require('backbone'); -require('../JsLibraries/backbone.collectionview'); - -module.exports = window.Backbone.CollectionView; \ No newline at end of file diff --git a/src/UI/Shims/backbone.deep.model.js b/src/UI/Shims/backbone.deep.model.js deleted file mode 100644 index dc7b47265..000000000 --- a/src/UI/Shims/backbone.deep.model.js +++ /dev/null @@ -1,4 +0,0 @@ -require('backbone'); -require('../JsLibraries/backbone.deep.model'); - -module.exports = window.Backbone.DeepModel; \ No newline at end of file diff --git a/src/UI/Shims/backbone.js b/src/UI/Shims/backbone.js deleted file mode 100644 index 0896076d8..000000000 --- a/src/UI/Shims/backbone.js +++ /dev/null @@ -1,7 +0,0 @@ -var jquery = require('jquery'); -var Backbone = require('../JsLibraries/backbone'); - -window.Backbone = Backbone; -Backbone.$ = jquery; - -module.exports = Backbone; \ No newline at end of file diff --git a/src/UI/Shims/backbone.marionette.js b/src/UI/Shims/backbone.marionette.js deleted file mode 100644 index 50b3bf182..000000000 --- a/src/UI/Shims/backbone.marionette.js +++ /dev/null @@ -1,10 +0,0 @@ -require('backbone'); -require('../JsLibraries/backbone.marionette'); - -var templateMixin = require('../Handlebars/backbone.marionette.templates'); -var asNamedView = require('../Mixins/AsNamedView'); - -templateMixin.call(window.Marionette.TemplateCache); -asNamedView.call(window.Marionette.ItemView.prototype); - -module.exports = window.Marionette; \ No newline at end of file diff --git a/src/UI/Shims/backbone.validation.js b/src/UI/Shims/backbone.validation.js deleted file mode 100644 index 158e42265..000000000 --- a/src/UI/Shims/backbone.validation.js +++ /dev/null @@ -1,8 +0,0 @@ -require('backbone'); -require('../JsLibraries/backbone.validation'); -var $ = require('jquery'); - -var jqueryValidation = require('../jQuery/jquery.validation'); -jqueryValidation.call($); - -module.exports = window.Backbone.Validation; \ No newline at end of file diff --git a/src/UI/Shims/backgrid.js b/src/UI/Shims/backgrid.js deleted file mode 100644 index 40853cc90..000000000 --- a/src/UI/Shims/backgrid.js +++ /dev/null @@ -1,19 +0,0 @@ -require('backbone'); - -var backgrid = require('../JsLibraries/backbone.backgrid'); -var header = require('../Shared/Grid/HeaderCell'); - -header.call(backgrid); - -backgrid.Column.prototype.defaults = { - name : undefined, - label : undefined, - sortable : true, - editable : false, - renderable : true, - formatter : undefined, - cell : undefined, - headerCell : 'Lidarr', - sortType : 'toggle' -}; -module.exports = backgrid; \ No newline at end of file diff --git a/src/UI/Shims/backgrid.paginator.js b/src/UI/Shims/backgrid.paginator.js deleted file mode 100644 index 874fb4006..000000000 --- a/src/UI/Shims/backgrid.paginator.js +++ /dev/null @@ -1,5 +0,0 @@ -require('backbone'); -var backgrid = require('backgrid'); -require('../JsLibraries/backbone.backgrid.paginator'); - -module.exports = backgrid.Extension.Paginator; \ No newline at end of file diff --git a/src/UI/Shims/handlebars.js b/src/UI/Shims/handlebars.js deleted file mode 100644 index 539e73271..000000000 --- a/src/UI/Shims/handlebars.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = window.Handlebars; \ No newline at end of file diff --git a/src/UI/Shims/jquery.js b/src/UI/Shims/jquery.js deleted file mode 100644 index 19e901944..000000000 --- a/src/UI/Shims/jquery.js +++ /dev/null @@ -1,11 +0,0 @@ -var jquery = require('../JsLibraries/jquery'); -require('../Instrumentation/StringFormat'); -var spin = require('../jQuery/jquery.spin'); -var ajax = require('../jQuery/jquery.ajax'); - -spin.call(jquery); -ajax.call(jquery); - -window.$ = jquery; -window.jQuery = jquery; -module.exports = jquery; diff --git a/src/UI/Shims/jquery.signalR.js b/src/UI/Shims/jquery.signalR.js deleted file mode 100644 index 70139ccef..000000000 --- a/src/UI/Shims/jquery.signalR.js +++ /dev/null @@ -1,4 +0,0 @@ -require('jquery'); -var signalR = require('../JsLibraries/jquery.signalR'); - -module.exports = signalR; \ No newline at end of file diff --git a/src/UI/Shims/messenger.js b/src/UI/Shims/messenger.js deleted file mode 100644 index f070bb991..000000000 --- a/src/UI/Shims/messenger.js +++ /dev/null @@ -1,6 +0,0 @@ -require('jquery'); -var m = require('../JsLibraries/messenger'); - -window.Messenger.options = { theme : 'flat' }; - -module.exports = window.Messenger; \ No newline at end of file diff --git a/src/UI/Shims/underscore.js b/src/UI/Shims/underscore.js deleted file mode 100644 index 67b9b808b..000000000 --- a/src/UI/Shims/underscore.js +++ /dev/null @@ -1,4 +0,0 @@ -var _ = require('../JsLibraries/lodash.underscore'); -window._ = window._ || _; - -module.exports = window._; \ No newline at end of file diff --git a/src/UI/System/Backup/BackupCollection.js b/src/UI/System/Backup/BackupCollection.js deleted file mode 100644 index 5bee1fc35..000000000 --- a/src/UI/System/Backup/BackupCollection.js +++ /dev/null @@ -1,15 +0,0 @@ -var PageableCollection = require('backbone.pageable'); -var BackupModel = require('./BackupModel'); - -module.exports = PageableCollection.extend({ - url : window.NzbDrone.ApiRoot + '/system/backup', - model : BackupModel, - - state : { - sortKey : 'time', - order : 1, - pageSize : 100000 - }, - - mode : 'client' -}); \ No newline at end of file diff --git a/src/UI/System/Backup/BackupEmptyView.js b/src/UI/System/Backup/BackupEmptyView.js deleted file mode 100644 index a86ba42bc..000000000 --- a/src/UI/System/Backup/BackupEmptyView.js +++ /dev/null @@ -1,5 +0,0 @@ -var Marionette = require('marionette'); - -module.exports = Marionette.ItemView.extend({ - template : 'System/Backup/BackupEmptyViewTemplate' -}); \ No newline at end of file diff --git a/src/UI/System/Backup/BackupEmptyViewTemplate.hbs b/src/UI/System/Backup/BackupEmptyViewTemplate.hbs deleted file mode 100644 index 2e14e7145..000000000 --- a/src/UI/System/Backup/BackupEmptyViewTemplate.hbs +++ /dev/null @@ -1 +0,0 @@ -<div>No backups are available</div> \ No newline at end of file diff --git a/src/UI/System/Backup/BackupFilenameCell.js b/src/UI/System/Backup/BackupFilenameCell.js deleted file mode 100644 index c8a57d1f9..000000000 --- a/src/UI/System/Backup/BackupFilenameCell.js +++ /dev/null @@ -1,6 +0,0 @@ -var TemplatedCell = require('../../Cells/TemplatedCell'); - -module.exports = TemplatedCell.extend({ - className : 'series-title-cell', - template : 'System/Backup/BackupFilenameCellTemplate' -}); \ No newline at end of file diff --git a/src/UI/System/Backup/BackupFilenameCellTemplate.hbs b/src/UI/System/Backup/BackupFilenameCellTemplate.hbs deleted file mode 100644 index e129039c3..000000000 --- a/src/UI/System/Backup/BackupFilenameCellTemplate.hbs +++ /dev/null @@ -1 +0,0 @@ -<a href="{{UrlBase}}/backup/{{type}}/{{name}}" class="no-router">{{name}}</a> diff --git a/src/UI/System/Backup/BackupLayout.js b/src/UI/System/Backup/BackupLayout.js deleted file mode 100644 index 3fa3be030..000000000 --- a/src/UI/System/Backup/BackupLayout.js +++ /dev/null @@ -1,94 +0,0 @@ -var vent = require('vent'); -var Marionette = require('marionette'); -var Backgrid = require('backgrid'); -var BackupCollection = require('./BackupCollection'); -var RelativeDateCell = require('../../Cells/RelativeDateCell'); -var BackupFilenameCell = require('./BackupFilenameCell'); -var BackupTypeCell = require('./BackupTypeCell'); -var EmptyView = require('./BackupEmptyView'); -var LoadingView = require('../../Shared/LoadingView'); -var ToolbarLayout = require('../../Shared/Toolbar/ToolbarLayout'); - -module.exports = Marionette.Layout.extend({ - template : 'System/Backup/BackupLayoutTemplate', - - regions : { - backups : '#x-backups', - toolbar : '#x-backup-toolbar' - }, - - columns : [ - { - name : 'type', - label : '', - sortable : false, - cell : BackupTypeCell - }, - { - name : 'this', - label : 'Name', - sortable : false, - cell : BackupFilenameCell - }, - { - name : 'time', - label : 'Time', - sortable : false, - cell : RelativeDateCell - } - ], - - leftSideButtons : { - type : 'default', - storeState : false, - collapse : false, - items : [ - { - title : 'Backup', - icon : 'icon-lidarr-file-text', - command : 'backup', - properties : { type : 'manual' }, - successMessage : 'Database and settings were backed up successfully', - errorMessage : 'Backup Failed!' - } - ] - }, - - initialize : function() { - this.backupCollection = new BackupCollection(); - - this.listenTo(this.backupCollection, 'sync', this._showBackups); - this.listenTo(vent, vent.Events.CommandComplete, this._commandComplete); - }, - - onRender : function() { - this._showToolbar(); - this.backups.show(new LoadingView()); - - this.backupCollection.fetch(); - }, - - _showBackups : function() { - if (this.backupCollection.length === 0) { - this.backups.show(new EmptyView()); - } else { - this.backups.show(new Backgrid.Grid({ - columns : this.columns, - collection : this.backupCollection, - className : 'table table-hover' - })); - } - }, - - _showToolbar : function() { - this.toolbar.show(new ToolbarLayout({ - left : [this.leftSideButtons], - context : this - })); - }, - _commandComplete : function(options) { - if (options.command.get('name') === 'backup') { - this.backupCollection.fetch(); - } - } -}); \ No newline at end of file diff --git a/src/UI/System/Backup/BackupLayoutTemplate.hbs b/src/UI/System/Backup/BackupLayoutTemplate.hbs deleted file mode 100644 index b50db1799..000000000 --- a/src/UI/System/Backup/BackupLayoutTemplate.hbs +++ /dev/null @@ -1,10 +0,0 @@ -<div class="row"> - <div class="col-md-12"> - <div id="x-backup-toolbar"/> - </div> -</div> -<div class="row"> - <div class="col-md-12"> - <div id="x-backups" class="table-responsive"/> - </div> -</div> diff --git a/src/UI/System/Backup/BackupModel.js b/src/UI/System/Backup/BackupModel.js deleted file mode 100644 index 3986a5948..000000000 --- a/src/UI/System/Backup/BackupModel.js +++ /dev/null @@ -1,3 +0,0 @@ -var Backbone = require('backbone'); - -module.exports = Backbone.Model.extend({}); \ No newline at end of file diff --git a/src/UI/System/Backup/BackupTypeCell.js b/src/UI/System/Backup/BackupTypeCell.js deleted file mode 100644 index 36bee0ccf..000000000 --- a/src/UI/System/Backup/BackupTypeCell.js +++ /dev/null @@ -1,26 +0,0 @@ -var NzbDroneCell = require('../../Cells/NzbDroneCell'); - -module.exports = NzbDroneCell.extend({ - className : 'backup-type-cell', - - render : function() { - this.$el.empty(); - - var icon = 'icon-lidarr-backup-scheduled'; - var title = 'Scheduled'; - - var type = this.model.get(this.column.get('name')); - - if (type === 'manual') { - icon = 'icon-lidarr-backup-manual'; - title = 'Manual'; - } else if (type === 'update') { - icon = 'icon-lidarr-backup-update'; - title = 'Before update'; - } - - this.$el.html('<i class="{0}" title="{1}"></i>'.format(icon, title)); - - return this; - } -}); \ No newline at end of file diff --git a/src/UI/System/Info/About/AboutView.js b/src/UI/System/Info/About/AboutView.js deleted file mode 100644 index 494b9a3ef..000000000 --- a/src/UI/System/Info/About/AboutView.js +++ /dev/null @@ -1,10 +0,0 @@ -var Marionette = require('marionette'); -var StatusModel = require('../../StatusModel'); - -module.exports = Marionette.ItemView.extend({ - template : 'System/Info/About/AboutViewTemplate', - - initialize : function() { - this.model = StatusModel; - } -}); \ No newline at end of file diff --git a/src/UI/System/Info/About/AboutViewTemplate.hbs b/src/UI/System/Info/About/AboutViewTemplate.hbs deleted file mode 100644 index a7fff2483..000000000 --- a/src/UI/System/Info/About/AboutViewTemplate.hbs +++ /dev/null @@ -1,20 +0,0 @@ -<fieldset> - <legend>About</legend> - - <dl class="dl-horizontal info"> - <dt>Version</dt> - <dd>{{version}}</dd> - - {{#if isMonoRuntime}} - <dt>Mono Version</dt> - <dd>{{runtimeVersion}}</dd> - {{/if}} - - <dt>AppData directory</dt> - <dd>{{appData}}</dd> - - <dt>Startup directory</dt> - <dd>{{startupPath}}</dd> - </dl> -</fieldset> - diff --git a/src/UI/System/Info/DiskSpace/DiskSpaceCollection.js b/src/UI/System/Info/DiskSpace/DiskSpaceCollection.js deleted file mode 100644 index 9769ba7fb..000000000 --- a/src/UI/System/Info/DiskSpace/DiskSpaceCollection.js +++ /dev/null @@ -1,7 +0,0 @@ -var Backbone = require('backbone'); -var DiskSpaceModel = require('./DiskSpaceModel'); - -module.exports = Backbone.Collection.extend({ - url : window.NzbDrone.ApiRoot + '/diskspace', - model : DiskSpaceModel -}); \ No newline at end of file diff --git a/src/UI/System/Info/DiskSpace/DiskSpaceLayout.js b/src/UI/System/Info/DiskSpace/DiskSpaceLayout.js deleted file mode 100644 index bd3470750..000000000 --- a/src/UI/System/Info/DiskSpace/DiskSpaceLayout.js +++ /dev/null @@ -1,58 +0,0 @@ -var vent = require('vent'); -var Marionette = require('marionette'); -var Backgrid = require('backgrid'); -var DiskSpaceCollection = require('./DiskSpaceCollection'); -var LoadingView = require('../../../Shared/LoadingView'); -var DiskSpacePathCell = require('./DiskSpacePathCell'); -var FileSizeCell = require('../../../Cells/FileSizeCell'); - -module.exports = Marionette.Layout.extend({ - template : 'System/Info/DiskSpace/DiskSpaceLayoutTemplate', - - regions : { - grid : '#x-grid' - }, - - columns : [ - { - name : 'path', - label : 'Location', - cell : DiskSpacePathCell, - sortable : false - }, - { - name : 'freeSpace', - label : 'Free Space', - cell : FileSizeCell, - sortable : false - }, - { - name : 'totalSpace', - label : 'Total Space', - cell : FileSizeCell, - sortable : false - } - ], - - initialize : function() { - this.collection = new DiskSpaceCollection(); - this.listenTo(this.collection, 'sync', this._showTable); - }, - - onRender : function() { - this.grid.show(new LoadingView()); - }, - - onShow : function() { - this.collection.fetch(); - }, - - _showTable : function() { - this.grid.show(new Backgrid.Grid({ - row : Backgrid.Row, - columns : this.columns, - collection : this.collection, - className : 'table table-hover' - })); - } -}); \ No newline at end of file diff --git a/src/UI/System/Info/DiskSpace/DiskSpaceLayoutTemplate.hbs b/src/UI/System/Info/DiskSpace/DiskSpaceLayoutTemplate.hbs deleted file mode 100644 index 99c218b67..000000000 --- a/src/UI/System/Info/DiskSpace/DiskSpaceLayoutTemplate.hbs +++ /dev/null @@ -1,5 +0,0 @@ -<fieldset> - <legend>Disk Space</legend> - - <div id="x-grid"/> -</fieldset> \ No newline at end of file diff --git a/src/UI/System/Info/DiskSpace/DiskSpaceModel.js b/src/UI/System/Info/DiskSpace/DiskSpaceModel.js deleted file mode 100644 index 3986a5948..000000000 --- a/src/UI/System/Info/DiskSpace/DiskSpaceModel.js +++ /dev/null @@ -1,3 +0,0 @@ -var Backbone = require('backbone'); - -module.exports = Backbone.Model.extend({}); \ No newline at end of file diff --git a/src/UI/System/Info/DiskSpace/DiskSpacePathCell.js b/src/UI/System/Info/DiskSpace/DiskSpacePathCell.js deleted file mode 100644 index de2ceb9b6..000000000 --- a/src/UI/System/Info/DiskSpace/DiskSpacePathCell.js +++ /dev/null @@ -1,22 +0,0 @@ -var Backgrid = require('backgrid'); - -module.exports = Backgrid.Cell.extend({ - className : 'disk-space-path-cell', - - render : function() { - this.$el.empty(); - - var path = this.model.get('path'); - var label = this.model.get('label'); - - var contents = path; - - if (label) { - contents += ' ({0})'.format(label); - } - - this.$el.html(contents); - - return this; - } -}); \ No newline at end of file diff --git a/src/UI/System/Info/Health/HealthCell.js b/src/UI/System/Info/Health/HealthCell.js deleted file mode 100644 index 594ece55c..000000000 --- a/src/UI/System/Info/Health/HealthCell.js +++ /dev/null @@ -1,12 +0,0 @@ -var NzbDroneCell = require('../../../Cells/NzbDroneCell'); - -module.exports = NzbDroneCell.extend({ - className : 'log-level-cell', - - render : function() { - var level = this._getValue(); - this.$el.html('<i class="icon-lidarr-health-{0}" title="{1}"/>'.format(this._getValue().toLowerCase(), level)); - - return this; - } -}); \ No newline at end of file diff --git a/src/UI/System/Info/Health/HealthLayout.js b/src/UI/System/Info/Health/HealthLayout.js deleted file mode 100644 index bc2bc33eb..000000000 --- a/src/UI/System/Info/Health/HealthLayout.js +++ /dev/null @@ -1,57 +0,0 @@ -var Marionette = require('marionette'); -var Backgrid = require('backgrid'); -var HealthCollection = require('../../../Health/HealthCollection'); -var HealthCell = require('./HealthCell'); -var HealthWikiCell = require('./HealthWikiCell'); -var HealthOkView = require('./HealthOkView'); - -module.exports = Marionette.Layout.extend({ - template : 'System/Info/Health/HealthLayoutTemplate', - - regions : { - grid : '#x-health-grid' - }, - - columns : [ - { - name : 'type', - label : '', - cell : HealthCell, - sortable : false - }, - { - name : 'message', - label : 'Message', - cell : 'string', - sortable : false - }, - { - name : 'wikiUrl', - label : '', - cell : HealthWikiCell, - sortable : false - } - ], - - initialize : function() { - this.listenTo(HealthCollection, 'sync', this.render); - HealthCollection.fetch(); - }, - - onRender : function() { - if (HealthCollection.length === 0) { - this.grid.show(new HealthOkView()); - } else { - this._showTable(); - } - }, - - _showTable : function() { - this.grid.show(new Backgrid.Grid({ - row : Backgrid.Row, - columns : this.columns, - collection : HealthCollection, - className : 'table table-hover' - })); - } -}); \ No newline at end of file diff --git a/src/UI/System/Info/Health/HealthLayoutTemplate.hbs b/src/UI/System/Info/Health/HealthLayoutTemplate.hbs deleted file mode 100644 index eda20b205..000000000 --- a/src/UI/System/Info/Health/HealthLayoutTemplate.hbs +++ /dev/null @@ -1,6 +0,0 @@ -<fieldset class="x-health"> - <legend>Health</legend> - - <div id="x-health-grid"/> -</fieldset> - diff --git a/src/UI/System/Info/Health/HealthOkView.js b/src/UI/System/Info/Health/HealthOkView.js deleted file mode 100644 index 662d9d278..000000000 --- a/src/UI/System/Info/Health/HealthOkView.js +++ /dev/null @@ -1,5 +0,0 @@ -var Marionette = require('marionette'); - -module.exports = Marionette.ItemView.extend({ - template : 'System/Info/Health/HealthOkViewTemplate' -}); \ No newline at end of file diff --git a/src/UI/System/Info/Health/HealthOkViewTemplate.hbs b/src/UI/System/Info/Health/HealthOkViewTemplate.hbs deleted file mode 100644 index b33a62360..000000000 --- a/src/UI/System/Info/Health/HealthOkViewTemplate.hbs +++ /dev/null @@ -1,3 +0,0 @@ -<div class="row health-ok"> - <div class="col-md-12">No issues with your configuration</div> -</div> \ No newline at end of file diff --git a/src/UI/System/Info/Health/HealthWikiCell.js b/src/UI/System/Info/Health/HealthWikiCell.js deleted file mode 100644 index bb93f0606..000000000 --- a/src/UI/System/Info/Health/HealthWikiCell.js +++ /dev/null @@ -1,24 +0,0 @@ -var $ = require('jquery'); -var Backgrid = require('backgrid'); - -module.exports = Backgrid.UriCell.extend({ - className : 'wiki-link-cell', - - title : 'Read the Wiki for more information', - - text : 'Wiki', - - render : function() { - this.$el.empty(); - var rawValue = this.model.get(this.column.get('name')); - var formattedValue = this.formatter.fromRaw(rawValue, this.model); - this.$el.append($('<a>', { - tabIndex : -1, - href : rawValue, - title : this.title || formattedValue, - target : this.target - }).text(this.text)); - this.delegateEvents(); - return this; - } -}); \ No newline at end of file diff --git a/src/UI/System/Info/MoreInfo/MoreInfoView.js b/src/UI/System/Info/MoreInfo/MoreInfoView.js deleted file mode 100644 index 0217ed742..000000000 --- a/src/UI/System/Info/MoreInfo/MoreInfoView.js +++ /dev/null @@ -1,5 +0,0 @@ -var Marionette = require('marionette'); - -module.exports = Marionette.ItemView.extend({ - template : 'System/Info/MoreInfo/MoreInfoViewTemplate' -}); \ No newline at end of file diff --git a/src/UI/System/Info/MoreInfo/MoreInfoViewTemplate.hbs b/src/UI/System/Info/MoreInfo/MoreInfoViewTemplate.hbs deleted file mode 100644 index b74f00ac5..000000000 --- a/src/UI/System/Info/MoreInfo/MoreInfoViewTemplate.hbs +++ /dev/null @@ -1,32 +0,0 @@ -<fieldset> - <legend>More Info</legend> - - <dl class="dl-horizontal info"> - <dt>Home page</dt> - <dd><a href="https://lidarr.audio/">lidarr.audio</a></dd> - - <dt>Wiki</dt> - <dd><a href="https://wiki.lidarr.audio/">wiki.lidarr.audio</a></dd> - - <dt>Forums</dt> - <dd><a href="https://forums.lidarr.audio/">forums.lidarr.audio</a></dd> - - <dt>Reddit</dt> - <dd><a href="https://reddit.com/r/Lidarr/">reddit.com/r/Lidarr</a></dd> - - <dt>Source</dt> - <dd><a href="https://github.com/mattman86/Lidarr/">github.com/Lidarr/Lidarr</a></dd> - - <dt>Contributors</dt> - <dd>DB and API - <a href="https://github.com/majora2007">Majora2007</a></dd> - <dd>UI and Website - <a href="https://github.com/mattman86">Mattman86</a></dd> - <dd>UI and Logo - <a href="https://github.com/skoden">Skoden</a></dd> - <dd>DB and Search - <a href="https://github.com/runraid">Runraid</a></dd> - <dd>Consultation - <a href="https://github.com/galli-leo">Galli-leo</a></dd> - - <dt>Feature Requests</dt> - <dd><a href="http://feathub.com/lidarr/Lidarr/">feathub.com/lidarr/Lidarr</a></dd> - <dd><a href="https://github.com/lidarr/Lidarr/issues">github.com/lidarr/Lidarr/issues</a> <b>(Please post issues on the forum first and not on github)</b></dd> - </dl> -</fieldset> - diff --git a/src/UI/System/Info/SystemInfoLayout.js b/src/UI/System/Info/SystemInfoLayout.js deleted file mode 100644 index 0b56318ea..000000000 --- a/src/UI/System/Info/SystemInfoLayout.js +++ /dev/null @@ -1,24 +0,0 @@ -var Backbone = require('backbone'); -var Marionette = require('marionette'); -var AboutView = require('./About/AboutView'); -var DiskSpaceLayout = require('./DiskSpace/DiskSpaceLayout'); -var HealthLayout = require('./Health/HealthLayout'); -var MoreInfoView = require('./MoreInfo/MoreInfoView'); - -module.exports = Marionette.Layout.extend({ - template : 'System/Info/SystemInfoLayoutTemplate', - - regions : { - about : '#about', - diskSpace : '#diskspace', - health : '#health', - moreInfo : '#more-info' - }, - - onRender : function() { - this.health.show(new HealthLayout()); - this.diskSpace.show(new DiskSpaceLayout()); - this.about.show(new AboutView()); - this.moreInfo.show(new MoreInfoView()); - } -}); \ No newline at end of file diff --git a/src/UI/System/Info/SystemInfoLayoutTemplate.hbs b/src/UI/System/Info/SystemInfoLayoutTemplate.hbs deleted file mode 100644 index d6eef7abd..000000000 --- a/src/UI/System/Info/SystemInfoLayoutTemplate.hbs +++ /dev/null @@ -1,15 +0,0 @@ -<div class="row"> - <div class="col-md-12" id="health"></div> -</div> - -<div class="row"> - <div class="col-md-12" id="diskspace"></div> -</div> - -<div class="row"> - <div class="col-md-12" id="about"></div> -</div> - -<div class="row"> - <div class="col-md-12" id="more-info"></div> -</div> diff --git a/src/UI/System/Info/info.less b/src/UI/System/Info/info.less deleted file mode 100644 index 59746b7d8..000000000 --- a/src/UI/System/Info/info.less +++ /dev/null @@ -1,3 +0,0 @@ -.health-ok { - margin-bottom: 30px; -} \ No newline at end of file diff --git a/src/UI/System/Logs/Files/ContentsModel.js b/src/UI/System/Logs/Files/ContentsModel.js deleted file mode 100644 index c9c47b1bb..000000000 --- a/src/UI/System/Logs/Files/ContentsModel.js +++ /dev/null @@ -1,13 +0,0 @@ -var Backbone = require('backbone'); - -module.exports = Backbone.Model.extend({ - url : function() { - return this.get('contentsUrl'); - }, - - parse : function(contents) { - var response = {}; - response.contents = contents; - return response; - } -}); \ No newline at end of file diff --git a/src/UI/System/Logs/Files/ContentsView.js b/src/UI/System/Logs/Files/ContentsView.js deleted file mode 100644 index 6b5b9e067..000000000 --- a/src/UI/System/Logs/Files/ContentsView.js +++ /dev/null @@ -1,5 +0,0 @@ -var Marionette = require('marionette'); - -module.exports = Marionette.ItemView.extend({ - template : 'System/Logs/Files/ContentsViewTemplate' -}); \ No newline at end of file diff --git a/src/UI/System/Logs/Files/ContentsViewTemplate.hbs b/src/UI/System/Logs/Files/ContentsViewTemplate.hbs deleted file mode 100644 index c9a21b736..000000000 --- a/src/UI/System/Logs/Files/ContentsViewTemplate.hbs +++ /dev/null @@ -1,11 +0,0 @@ -<div class="row"> - <div class="col-md-12"> - <h3>{{filename}}</h3> - </div> -</div> - -<div class="row"> - <div class="col-md-12"> - <pre>{{contents}}</pre> - </div> -</div> \ No newline at end of file diff --git a/src/UI/System/Logs/Files/DownloadLogCell.js b/src/UI/System/Logs/Files/DownloadLogCell.js deleted file mode 100644 index 8be2d0176..000000000 --- a/src/UI/System/Logs/Files/DownloadLogCell.js +++ /dev/null @@ -1,12 +0,0 @@ -var NzbDroneCell = require('../../../Cells/NzbDroneCell'); - -module.exports = NzbDroneCell.extend({ - className : 'download-log-cell', - - render : function() { - this.$el.empty(); - this.$el.html('<a href="{0}" class="no-router" target="_blank">Download</a>'.format(this.cellValue)); - - return this; - } -}); \ No newline at end of file diff --git a/src/UI/System/Logs/Files/FilenameCell.js b/src/UI/System/Logs/Files/FilenameCell.js deleted file mode 100644 index aedbb8cfb..000000000 --- a/src/UI/System/Logs/Files/FilenameCell.js +++ /dev/null @@ -1,12 +0,0 @@ -var NzbDroneCell = require('../../../Cells/NzbDroneCell'); - -module.exports = NzbDroneCell.extend({ - className : 'log-filename-cell', - - render : function() { - var filename = this._getValue(); - this.$el.html(filename); - - return this; - } -}); \ No newline at end of file diff --git a/src/UI/System/Logs/Files/LogFileCollection.js b/src/UI/System/Logs/Files/LogFileCollection.js deleted file mode 100644 index 590dda677..000000000 --- a/src/UI/System/Logs/Files/LogFileCollection.js +++ /dev/null @@ -1,12 +0,0 @@ -var Backbone = require('backbone'); -var LogFileModel = require('./LogFileModel'); - -module.exports = Backbone.Collection.extend({ - url : window.NzbDrone.ApiRoot + '/log/file', - model : LogFileModel, - - state : { - sortKey : 'lastWriteTime', - order : 1 - } -}); \ No newline at end of file diff --git a/src/UI/System/Logs/Files/LogFileLayout.js b/src/UI/System/Logs/Files/LogFileLayout.js deleted file mode 100644 index 39516091e..000000000 --- a/src/UI/System/Logs/Files/LogFileLayout.js +++ /dev/null @@ -1,135 +0,0 @@ -var vent = require('vent'); -var Marionette = require('marionette'); -var Backgrid = require('backgrid'); -var FilenameCell = require('./FilenameCell'); -var RelativeDateCell = require('../../../Cells/RelativeDateCell'); -var DownloadLogCell = require('./DownloadLogCell'); -var LogFileRow = require('./Row'); -var ContentsView = require('./ContentsView'); -var ContentsModel = require('./ContentsModel'); -var ToolbarLayout = require('../../../Shared/Toolbar/ToolbarLayout'); -var LoadingView = require('../../../Shared/LoadingView'); -require('../../../jQuery/jquery.spin'); - -module.exports = Marionette.Layout.extend({ - template : 'System/Logs/Files/LogFileLayoutTemplate', - - regions : { - toolbar : '#x-toolbar', - grid : '#x-grid', - contents : '#x-contents' - }, - - columns : [ - { - name : 'filename', - label : 'Filename', - cell : FilenameCell, - sortable : false - }, - { - name : 'lastWriteTime', - label : 'Last Write Time', - cell : RelativeDateCell, - sortable : false - }, - { - name : 'downloadUrl', - label : '', - cell : DownloadLogCell, - sortable : false - } - ], - - initialize : function(options) { - this.collection = options.collection; - this.deleteFilesCommand = options.deleteFilesCommand; - - this.listenTo(vent, vent.Commands.ShowLogFile, this._fetchLogFileContents); - this.listenTo(vent, vent.Events.CommandComplete, this._commandComplete); - this.listenTo(this.collection, 'sync', this._collectionSynced); - - this.collection.fetch(); - }, - - onShow : function() { - this._showToolbar(); - this._showTable(); - }, - - _showToolbar : function() { - var leftSideButtons = { - type : 'default', - storeState : false, - items : [ - { - title : 'Refresh', - icon : 'icon-lidarr-refresh', - ownerContext : this, - callback : this._refreshTable - }, - { - title : 'Clear Log Files', - icon : 'icon-lidarr-clear', - command : this.deleteFilesCommand, - successMessage : 'Log files have been deleted', - errorMessage : 'Failed to delete log files' - } - ] - }; - - this.toolbar.show(new ToolbarLayout({ - left : [leftSideButtons], - context : this - })); - }, - - _showTable : function() { - this.grid.show(new Backgrid.Grid({ - row : LogFileRow, - columns : this.columns, - collection : this.collection, - className : 'table table-hover' - })); - }, - - _collectionSynced : function() { - if (!this.collection.any()) { - return; - } - - var model = this.collection.first(); - this._fetchLogFileContents({ model : model }); - }, - - _fetchLogFileContents : function(options) { - this.contents.show(new LoadingView()); - - var model = options.model; - var contentsModel = new ContentsModel(model.toJSON()); - - this.listenToOnce(contentsModel, 'sync', this._showDetails); - - contentsModel.fetch({ dataType : 'text' }); - }, - - _showDetails : function(model) { - this.contents.show(new ContentsView({ model : model })); - }, - - _refreshTable : function(buttonContext) { - this.contents.close(); - var promise = this.collection.fetch(); - - //Would be nice to spin the icon on the refresh button - if (buttonContext) { - buttonContext.ui.icon.spinForPromise(promise); - } - }, - - _commandComplete : function(options) { - if (options.command.get('name') === this.deleteFilesCommand.toLowerCase()) { - this._refreshTable(); - } - } -}); \ No newline at end of file diff --git a/src/UI/System/Logs/Files/LogFileLayoutTemplate.hbs b/src/UI/System/Logs/Files/LogFileLayoutTemplate.hbs deleted file mode 100644 index 0188f1d0e..000000000 --- a/src/UI/System/Logs/Files/LogFileLayoutTemplate.hbs +++ /dev/null @@ -1,12 +0,0 @@ -<div id="x-toolbar"/> -<div class="row"> - <div class="col-md-12"> - <div id="x-grid"/> - </div> -</div> - -<div class="row"> - <div class="col-md-12"> - <div id="x-contents"/> - </div> -</div> \ No newline at end of file diff --git a/src/UI/System/Logs/Files/LogFileModel.js b/src/UI/System/Logs/Files/LogFileModel.js deleted file mode 100644 index 3986a5948..000000000 --- a/src/UI/System/Logs/Files/LogFileModel.js +++ /dev/null @@ -1,3 +0,0 @@ -var Backbone = require('backbone'); - -module.exports = Backbone.Model.extend({}); \ No newline at end of file diff --git a/src/UI/System/Logs/Files/Row.js b/src/UI/System/Logs/Files/Row.js deleted file mode 100644 index 01e6bd55a..000000000 --- a/src/UI/System/Logs/Files/Row.js +++ /dev/null @@ -1,14 +0,0 @@ -var vent = require('vent'); -var Backgrid = require('backgrid'); - -module.exports = Backgrid.Row.extend({ - className : 'log-file-row', - - events : { - 'click' : '_showDetails' - }, - - _showDetails : function() { - vent.trigger(vent.Commands.ShowLogFile, { model : this.model }); - } -}); \ No newline at end of file diff --git a/src/UI/System/Logs/LogsCollection.js b/src/UI/System/Logs/LogsCollection.js deleted file mode 100644 index c233a9d63..000000000 --- a/src/UI/System/Logs/LogsCollection.js +++ /dev/null @@ -1,64 +0,0 @@ -var PagableCollection = require('backbone.pageable'); -var LogsModel = require('./LogsModel'); -var AsFilteredCollection = require('../../Mixins/AsFilteredCollection'); -var AsPersistedStateCollection = require('../../Mixins/AsPersistedStateCollection'); - -var collection = PagableCollection.extend({ - url : window.NzbDrone.ApiRoot + '/log', - model : LogsModel, - tableName : 'logs', - - state : { - pageSize : 50, - sortKey : 'time', - order : 1 - }, - - queryParams : { - totalPages : null, - totalRecords : null, - pageSize : 'pageSize', - sortKey : 'sortKey', - order : 'sortDir', - directions : { - '-1' : 'asc', - '1' : 'desc' - } - }, - - // Filter Modes - filterModes : { - "all" : [ - null, - null - ], - "info" : [ - 'level', - 'Info' - ], - "warn" : [ - 'level', - 'Warn' - ], - "error" : [ - 'level', - 'Error' - ] - }, - - parseState : function(resp, queryParams, state) { - return { totalRecords : resp.totalRecords }; - }, - - parseRecords : function(resp) { - if (resp) { - return resp.records; - } - - return resp; - } -}); - -collection = AsFilteredCollection.apply(collection); - -module.exports = AsPersistedStateCollection.apply(collection); \ No newline at end of file diff --git a/src/UI/System/Logs/LogsLayout.js b/src/UI/System/Logs/LogsLayout.js deleted file mode 100644 index d064cff64..000000000 --- a/src/UI/System/Logs/LogsLayout.js +++ /dev/null @@ -1,64 +0,0 @@ -var Marionette = require('marionette'); -var LogsTableLayout = require('./Table/LogsTableLayout'); -var LogsFileLayout = require('./Files/LogFileLayout'); -var LogFileCollection = require('./Files/LogFileCollection'); -var UpdateLogFileCollection = require('./Updates/LogFileCollection'); - -module.exports = Marionette.Layout.extend({ - template : 'System/Logs/LogsLayoutTemplate', - - ui : { - tableTab : '.x-table-tab', - filesTab : '.x-files-tab', - updateFilesTab : '.x-update-files-tab' - }, - - regions : { - table : '#table', - files : '#files', - updateFiles : '#update-files' - }, - - events : { - 'click .x-table-tab' : '_showTable', - 'click .x-files-tab' : '_showFiles', - 'click .x-update-files-tab' : '_showUpdateFiles' - }, - - onShow : function() { - this._showTable(); - }, - - _showTable : function(e) { - if (e) { - e.preventDefault(); - } - - this.ui.tableTab.tab('show'); - this.table.show(new LogsTableLayout()); - }, - - _showFiles : function(e) { - if (e) { - e.preventDefault(); - } - - this.ui.filesTab.tab('show'); - this.files.show(new LogsFileLayout({ - collection : new LogFileCollection(), - deleteFilesCommand : 'deleteLogFiles' - })); - }, - - _showUpdateFiles : function(e) { - if (e) { - e.preventDefault(); - } - - this.ui.updateFilesTab.tab('show'); - this.updateFiles.show(new LogsFileLayout({ - collection : new UpdateLogFileCollection(), - deleteFilesCommand : 'deleteUpdateLogFiles' - })); - } -}); \ No newline at end of file diff --git a/src/UI/System/Logs/LogsLayoutTemplate.hbs b/src/UI/System/Logs/LogsLayoutTemplate.hbs deleted file mode 100644 index 1bf663f29..000000000 --- a/src/UI/System/Logs/LogsLayoutTemplate.hbs +++ /dev/null @@ -1,17 +0,0 @@ -<div class="row"> - <div class="col-md-2 col-sm-2"> - <ul class="nav nav-pills nav-stacked"> - <li><a href="#table" class="x-table-tab no-router">Table</a></li> - <li><a href="#files" class="x-files-tab no-router">Files</a></li> - <li><a href="#update-files" class="x-update-files-tab no-router">Updates</a></li> - </ul> - </div> - - <div class="col-md-10 col-sm-10"> - <div class="tab-content"> - <div class="tab-pane" id="table"></div> - <div class="tab-pane" id="files"></div> - <div class="tab-pane" id="update-files"></div> - </div> - </div> -</div> \ No newline at end of file diff --git a/src/UI/System/Logs/LogsModel.js b/src/UI/System/Logs/LogsModel.js deleted file mode 100644 index 3986a5948..000000000 --- a/src/UI/System/Logs/LogsModel.js +++ /dev/null @@ -1,3 +0,0 @@ -var Backbone = require('backbone'); - -module.exports = Backbone.Model.extend({}); \ No newline at end of file diff --git a/src/UI/System/Logs/Table/Details/LogDetailsView.js b/src/UI/System/Logs/Table/Details/LogDetailsView.js deleted file mode 100644 index dcdadcf0b..000000000 --- a/src/UI/System/Logs/Table/Details/LogDetailsView.js +++ /dev/null @@ -1,6 +0,0 @@ -var vent = require('vent'); -var Marionette = require('marionette'); - -module.exports = Marionette.ItemView.extend({ - template : 'System/Logs/Table/Details/LogDetailsViewTemplate' -}); \ No newline at end of file diff --git a/src/UI/System/Logs/Table/Details/LogDetailsViewTemplate.hbs b/src/UI/System/Logs/Table/Details/LogDetailsViewTemplate.hbs deleted file mode 100644 index 80a8f7d26..000000000 --- a/src/UI/System/Logs/Table/Details/LogDetailsViewTemplate.hbs +++ /dev/null @@ -1,23 +0,0 @@ -<div class="modal-content"> - <div class="log-details-modal"> - <div class="modal-header"> - <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> - - <h3>Details</h3> - - </div> - <div class="modal-body"> - Message - <pre>{{message}}</pre> - - {{#if exception}} - <br/> - Exception - <pre>{{exception}}</pre> - {{/if}} - </div> - <div class="modal-footer"> - <button class="btn" data-dismiss="modal">Close</button> - </div> - </div> -</div> diff --git a/src/UI/System/Logs/Table/LogLevelCell.js b/src/UI/System/Logs/Table/LogLevelCell.js deleted file mode 100644 index e09264cd8..000000000 --- a/src/UI/System/Logs/Table/LogLevelCell.js +++ /dev/null @@ -1,12 +0,0 @@ -var NzbDroneCell = require('../../../Cells/NzbDroneCell'); - -module.exports = NzbDroneCell.extend({ - className : 'log-level-cell', - - render : function() { - var level = this._getValue(); - this.$el.html('<i class="icon-lidarr-log-{0}" title="{1}"/>'.format(this._getValue().toLowerCase(), level)); - - return this; - } -}); \ No newline at end of file diff --git a/src/UI/System/Logs/Table/LogRow.js b/src/UI/System/Logs/Table/LogRow.js deleted file mode 100644 index c8cf6eb97..000000000 --- a/src/UI/System/Logs/Table/LogRow.js +++ /dev/null @@ -1,14 +0,0 @@ -var vent = require('vent'); -var Backgrid = require('backgrid'); - -module.exports = Backgrid.Row.extend({ - className : 'log-row', - - events : { - 'click' : '_showDetails' - }, - - _showDetails : function() { - vent.trigger(vent.Commands.ShowLogDetails, { model : this.model }); - } -}); \ No newline at end of file diff --git a/src/UI/System/Logs/Table/LogTimeCell.js b/src/UI/System/Logs/Table/LogTimeCell.js deleted file mode 100644 index 1adbab10e..000000000 --- a/src/UI/System/Logs/Table/LogTimeCell.js +++ /dev/null @@ -1,31 +0,0 @@ -var NzbDroneCell = require('../../../Cells/NzbDroneCell'); -var moment = require('moment'); -var FormatHelpers = require('../../../Shared/FormatHelpers'); -var UiSettings = require('../../../Shared/UiSettingsModel'); - -module.exports = NzbDroneCell.extend({ - className : 'log-time-cell', - - render : function() { - var dateStr = this._getValue(); - var date = moment(dateStr); - var diff = date.diff(moment().zone(date.zone()).startOf('day'), 'days', true); - var result = '<span title="{0}">{1}</span>'; - var tooltip = date.format(UiSettings.longDateTime(true)); - var text; - - if (diff > 0 && diff < 1) { - text = date.format(UiSettings.time(true, false)); - } else { - if (UiSettings.get('showRelativeDates')) { - text = FormatHelpers.relativeDate(dateStr); - } else { - text = date.format(UiSettings.get('shortDateFormat')); - } - } - - this.$el.html(result.format(tooltip, text)); - - return this; - } -}); \ No newline at end of file diff --git a/src/UI/System/Logs/Table/LogsTableLayout.js b/src/UI/System/Logs/Table/LogsTableLayout.js deleted file mode 100644 index d5668a9de..000000000 --- a/src/UI/System/Logs/Table/LogsTableLayout.js +++ /dev/null @@ -1,175 +0,0 @@ -var vent = require('vent'); -var Marionette = require('marionette'); -var Backgrid = require('backgrid'); -var LogTimeCell = require('./LogTimeCell'); -var LogLevelCell = require('./LogLevelCell'); -var LogRow = require('./LogRow'); -var GridPager = require('../../../Shared/Grid/Pager'); -var LogCollection = require('../LogsCollection'); -var ToolbarLayout = require('../../../Shared/Toolbar/ToolbarLayout'); -var LoadingView = require('../../../Shared/LoadingView'); -require('../../../jQuery/jquery.spin'); - -module.exports = Marionette.Layout.extend({ - template : 'System/Logs/Table/LogsTableLayoutTemplate', - - regions : { - grid : '#x-grid', - toolbar : '#x-toolbar', - pager : '#x-pager' - }, - - attributes : { - id : 'logs-screen' - }, - - columns : [ - { - name : 'level', - label : '', - sortable : true, - cell : LogLevelCell - }, - { - name : 'logger', - label : 'Component', - sortable : true, - cell : Backgrid.StringCell.extend({ - className : 'log-logger-cell' - }) - }, - { - name : 'message', - label : 'Message', - sortable : false, - cell : Backgrid.StringCell.extend({ - className : 'log-message-cell' - }) - }, - { - name : 'time', - label : 'Time', - cell : LogTimeCell - } - ], - - initialize : function() { - this.collection = new LogCollection(); - - this.listenTo(this.collection, 'sync', this._showTable); - this.listenTo(vent, vent.Events.CommandComplete, this._commandComplete); - }, - - onRender : function() { - this.grid.show(new LoadingView()); - }, - - onShow : function() { - this._showToolbar(); - }, - - _showTable : function() { - this.grid.show(new Backgrid.Grid({ - row : LogRow, - columns : this.columns, - collection : this.collection, - className : 'table table-hover' - })); - - this.pager.show(new GridPager({ - columns : this.columns, - collection : this.collection - })); - }, - - _showToolbar : function() { - var filterButtons = { - type : 'radio', - storeState : true, - menuKey : 'logs.filterMode', - defaultAction : 'all', - items : [ - { - key : 'all', - title : '', - tooltip : 'All', - icon : 'icon-lidarr-all', - callback : this._setFilter - }, - { - key : 'info', - title : '', - tooltip : 'Info', - icon : 'icon-lidarr-log-info', - callback : this._setFilter - }, - { - key : 'warn', - title : '', - tooltip : 'Warn', - icon : 'icon-lidarr-log-warn', - callback : this._setFilter - }, - { - key : 'error', - title : '', - tooltip : 'Error', - icon : 'icon-lidarr-log-error', - callback : this._setFilter - } - ] - }; - - var leftSideButtons = { - type : 'default', - storeState : false, - items : [ - { - title : 'Refresh', - icon : 'icon-lidarr-refresh', - ownerContext : this, - callback : this._refreshTable - }, - { - title : 'Clear Logs', - icon : 'icon-lidarr-clear', - command : 'clearLog' - } - ] - }; - - this.toolbar.show(new ToolbarLayout({ - left : [leftSideButtons], - right : [filterButtons], - context : this - })); - }, - - _refreshTable : function(buttonContext) { - this.collection.state.currentPage = 1; - var promise = this.collection.fetch({ reset : true }); - - if (buttonContext) { - buttonContext.ui.icon.spinForPromise(promise); - } - }, - - _setFilter : function(buttonContext) { - var mode = buttonContext.model.get('key'); - - this.collection.setFilterMode(mode, { reset : false }); - - this.collection.state.currentPage = 1; - var promise = this.collection.fetch({ reset : true }); - - if (buttonContext) { - buttonContext.ui.icon.spinForPromise(promise); - } - }, - - _commandComplete : function(options) { - if (options.command.get('name') === 'clearlog') { - this._refreshTable(); - } - } -}); \ No newline at end of file diff --git a/src/UI/System/Logs/Table/LogsTableLayoutTemplate.hbs b/src/UI/System/Logs/Table/LogsTableLayoutTemplate.hbs deleted file mode 100644 index 1d579ffcd..000000000 --- a/src/UI/System/Logs/Table/LogsTableLayoutTemplate.hbs +++ /dev/null @@ -1,11 +0,0 @@ -<div id="x-toolbar"/> -<div class="row"> - <div class="col-md-12"> - <div id="x-grid" class="table-responsive"/> - </div> -</div> -<div class="row"> - <div class="col-md-12"> - <div id="x-pager"/> - </div> -</div> diff --git a/src/UI/System/Logs/Updates/LogFileCollection.js b/src/UI/System/Logs/Updates/LogFileCollection.js deleted file mode 100644 index 1f957dbf1..000000000 --- a/src/UI/System/Logs/Updates/LogFileCollection.js +++ /dev/null @@ -1,12 +0,0 @@ -var Backbone = require('backbone'); -var LogFileModel = require('./LogFileModel'); - -module.exports = Backbone.Collection.extend({ - url : window.NzbDrone.ApiRoot + '/log/file/update', - model : LogFileModel, - - state : { - sortKey : 'lastWriteTime', - order : 1 - } -}); \ No newline at end of file diff --git a/src/UI/System/Logs/Updates/LogFileModel.js b/src/UI/System/Logs/Updates/LogFileModel.js deleted file mode 100644 index 3986a5948..000000000 --- a/src/UI/System/Logs/Updates/LogFileModel.js +++ /dev/null @@ -1,3 +0,0 @@ -var Backbone = require('backbone'); - -module.exports = Backbone.Model.extend({}); \ No newline at end of file diff --git a/src/UI/System/Logs/logs.less b/src/UI/System/Logs/logs.less deleted file mode 100644 index 7142583ad..000000000 --- a/src/UI/System/Logs/logs.less +++ /dev/null @@ -1,25 +0,0 @@ -@import "../../Shared/Styles/clickable"; - -#logs-screen { - - .log-time-cell{ - width: 100px; - } - - .log-level-cell{ - width: 12px; - font-size: 14px; - } - - td{ - font-size: 13px; - } -} - -.log-file-row { - .clickable; -} - -.log-row { - .clickable; -} \ No newline at end of file diff --git a/src/UI/System/StatusModel.js b/src/UI/System/StatusModel.js deleted file mode 100644 index 075dd0918..000000000 --- a/src/UI/System/StatusModel.js +++ /dev/null @@ -1,9 +0,0 @@ -var Backbone = require('backbone'); -var ApiData = require('../Shared/ApiData'); - -var StatusModel = Backbone.Model.extend({ - url : window.NzbDrone.ApiRoot + '/system/status' -}); -var instance = new StatusModel(ApiData.get('system/status')); - -module.exports = instance; \ No newline at end of file diff --git a/src/UI/System/SystemLayout.js b/src/UI/System/SystemLayout.js deleted file mode 100644 index 33e18db34..000000000 --- a/src/UI/System/SystemLayout.js +++ /dev/null @@ -1,150 +0,0 @@ -var $ = require('jquery'); -var Backbone = require('backbone'); -var Marionette = require('marionette'); -var SystemInfoLayout = require('./Info/SystemInfoLayout'); -var LogsLayout = require('./Logs/LogsLayout'); -var UpdateLayout = require('./Update/UpdateLayout'); -var BackupLayout = require('./Backup/BackupLayout'); -var TaskLayout = require('./Task/TaskLayout'); -var Messenger = require('../Shared/Messenger'); -var StatusModel = require('./StatusModel'); - -module.exports = Marionette.Layout.extend({ - template : 'System/SystemLayoutTemplate', - - regions : { - status : '#status', - logs : '#logs', - updates : '#updates', - backup : '#backup', - tasks : '#tasks' - }, - - ui : { - statusTab : '.x-status-tab', - logsTab : '.x-logs-tab', - updatesTab : '.x-updates-tab', - backupTab : '.x-backup-tab', - tasksTab : '.x-tasks-tab' - }, - - events : { - 'click .x-status-tab' : '_showStatus', - 'click .x-logs-tab' : '_showLogs', - 'click .x-updates-tab' : '_showUpdates', - 'click .x-backup-tab' : '_showBackup', - 'click .x-tasks-tab' : '_showTasks', - 'click .x-shutdown' : '_shutdown', - 'click .x-restart' : '_restart' - }, - - initialize : function(options) { - if (options.action) { - this.action = options.action.toLowerCase(); - } - - this.templateHelpers = { - authentication : StatusModel.get('authentication') - }; - }, - - onShow : function() { - switch (this.action) { - case 'logs': - this._showLogs(); - break; - case 'updates': - this._showUpdates(); - break; - case 'backup': - this._showBackup(); - break; - case 'tasks': - this._showTasks(); - break; - default: - this._showStatus(); - } - }, - - _navigate : function(route) { - Backbone.history.navigate(route, { - trigger : true, - replace : true - }); - }, - - _showStatus : function(e) { - if (e) { - e.preventDefault(); - } - - this.status.show(new SystemInfoLayout()); - this.ui.statusTab.tab('show'); - this._navigate('system/status'); - }, - - _showLogs : function(e) { - if (e) { - e.preventDefault(); - } - - this.logs.show(new LogsLayout()); - this.ui.logsTab.tab('show'); - this._navigate('system/logs'); - }, - - _showUpdates : function(e) { - if (e) { - e.preventDefault(); - } - - this.updates.show(new UpdateLayout()); - this.ui.updatesTab.tab('show'); - this._navigate('system/updates'); - }, - - _showBackup : function(e) { - if (e) { - e.preventDefault(); - } - - this.backup.show(new BackupLayout()); - this.ui.backupTab.tab('show'); - this._navigate('system/backup'); - }, - - _showTasks : function(e) { - if (e) { - e.preventDefault(); - } - - this.tasks.show(new TaskLayout()); - this.ui.tasksTab.tab('show'); - this._navigate('system/tasks'); - }, - - _shutdown : function() { - $.ajax({ - url : window.NzbDrone.ApiRoot + '/system/shutdown', - type : 'POST' - }); - - Messenger.show({ - message : 'Lidarr will shutdown shortly', - type : 'info' - }); - }, - - _restart : function() { - $.ajax({ - url : window.NzbDrone.ApiRoot + '/system/restart', - type : 'POST' - }); - - Messenger.show({ - message : 'Lidarr will restart shortly', - type : 'info' - }); - } -}); \ No newline at end of file diff --git a/src/UI/System/SystemLayoutTemplate.hbs b/src/UI/System/SystemLayoutTemplate.hbs deleted file mode 100644 index af2adf692..000000000 --- a/src/UI/System/SystemLayoutTemplate.hbs +++ /dev/null @@ -1,31 +0,0 @@ -<ul class="nav nav-tabs"> - <li><a href="#status" class="x-status-tab no-router">Status</a></li> - <li><a href="#updates" class="x-updates-tab no-router">Updates</a></li> - <li><a href="#tasks" class="x-tasks-tab no-router">Tasks</a></li> - <li><a href="#backup" class="x-backup-tab no-router">Backup</a></li> - <li><a href="#logs" class="x-logs-tab no-router">Logs</a></li> - <li class="lifecycle-controls pull-right"> - <div class="btn-group"> - <button class="btn btn-default btn-icon-only x-shutdown" title="Shutdown"> - <i class="icon-lidarr-shutdown"></i> - </button> - <button class="btn btn-default btn-icon-only x-restart" title="Restart"> - <i class="icon-lidarr-restart"></i> - </button> - - {{#if_eq authentication compare="forms"}} - <a href="{{UrlBase}}/logout" class="btn btn-default btn-icon-only" title="Logout"> - <i class="icon-lidarr-logout"></i> - </a> - {{/if_eq}} - </div> - </li> -</ul> - -<div class="tab-content"> - <div class="tab-pane" id="status"></div> - <div class="tab-pane" id="updates"></div> - <div class="tab-pane" id="tasks"></div> - <div class="tab-pane" id="backup"></div> - <div class="tab-pane" id="logs"></div> -</div> \ No newline at end of file diff --git a/src/UI/System/Task/ExecuteTaskCell.js b/src/UI/System/Task/ExecuteTaskCell.js deleted file mode 100644 index 10a510dbb..000000000 --- a/src/UI/System/Task/ExecuteTaskCell.js +++ /dev/null @@ -1,30 +0,0 @@ -var NzbDroneCell = require('../../Cells/NzbDroneCell'); -var CommandController = require('../../Commands/CommandController'); - -module.exports = NzbDroneCell.extend({ - className : 'execute-task-cell', - - events : { - 'click .x-execute' : '_executeTask' - }, - - render : function() { - this.$el.empty(); - - var name = this.model.get('name'); - var task = this.model.get('taskName'); - - this.$el.html('<i class="icon-lidarr-refresh icon-can-spin x-execute" title="Execute {0}"></i>'.format(name)); - - CommandController.bindToCommand({ - element : this.$el.find('.x-execute'), - command : { name : task } - }); - - return this; - }, - - _executeTask : function() { - CommandController.Execute(this.model.get('taskName'), { name : this.model.get('taskName') }); - } -}); \ No newline at end of file diff --git a/src/UI/System/Task/NextExecutionCell.js b/src/UI/System/Task/NextExecutionCell.js deleted file mode 100644 index 39140f5a9..000000000 --- a/src/UI/System/Task/NextExecutionCell.js +++ /dev/null @@ -1,34 +0,0 @@ -var NzbDroneCell = require('../../Cells/NzbDroneCell'); -var moment = require('moment'); -var UiSettings = require('../../Shared/UiSettingsModel'); - -module.exports = NzbDroneCell.extend({ - className : 'next-execution-cell', - - render : function() { - this.$el.empty(); - - var interval = this.model.get('interval'); - var nextExecution = moment(this.model.get('nextExecution')); - - if (interval === 0) { - this.$el.html('-'); - } else if (moment().isAfter(nextExecution)) { - this.$el.html('now'); - } else { - var result = '<span title="{0}">{1}</span>'; - var tooltip = nextExecution.format(UiSettings.longDateTime()); - var text; - - if (UiSettings.get('showRelativeDates')) { - text = nextExecution.fromNow(); - } else { - text = nextExecution.format(UiSettings.shortDateTime()); - } - - this.$el.html(result.format(tooltip, text)); - } - - return this; - } -}); \ No newline at end of file diff --git a/src/UI/System/Task/TaskCollection.js b/src/UI/System/Task/TaskCollection.js deleted file mode 100644 index bca599554..000000000 --- a/src/UI/System/Task/TaskCollection.js +++ /dev/null @@ -1,15 +0,0 @@ -var PageableCollection = require('backbone.pageable'); -var TaskModel = require('./TaskModel'); - -module.exports = PageableCollection.extend({ - url : window.NzbDrone.ApiRoot + '/system/task', - model : TaskModel, - - state : { - sortKey : 'name', - order : -1, - pageSize : 100000 - }, - - mode : 'client' -}); \ No newline at end of file diff --git a/src/UI/System/Task/TaskIntervalCell.js b/src/UI/System/Task/TaskIntervalCell.js deleted file mode 100644 index b2f246c48..000000000 --- a/src/UI/System/Task/TaskIntervalCell.js +++ /dev/null @@ -1,21 +0,0 @@ -var NzbDroneCell = require('../../Cells/NzbDroneCell'); -var moment = require('moment'); - -module.exports = NzbDroneCell.extend({ - className : 'task-interval-cell', - - render : function() { - this.$el.empty(); - - var interval = this.model.get('interval'); - var duration = moment.duration(interval, 'minutes').humanize().replace(/an?(?=\s)/, '1'); - - if (interval === 0) { - this.$el.html('disabled'); - } else { - this.$el.html(duration); - } - - return this; - } -}); \ No newline at end of file diff --git a/src/UI/System/Task/TaskLayout.js b/src/UI/System/Task/TaskLayout.js deleted file mode 100644 index 621797e3c..000000000 --- a/src/UI/System/Task/TaskLayout.js +++ /dev/null @@ -1,71 +0,0 @@ -var Marionette = require('marionette'); -var Backgrid = require('backgrid'); -var BackupCollection = require('./TaskCollection'); -var RelativeTimeCell = require('../../Cells/RelativeTimeCell'); -var TaskIntervalCell = require('./TaskIntervalCell'); -var ExecuteTaskCell = require('./ExecuteTaskCell'); -var NextExecutionCell = require('./NextExecutionCell'); -var LoadingView = require('../../Shared/LoadingView'); -require('../../Mixins/backbone.signalr.mixin'); - -module.exports = Marionette.Layout.extend({ - template : 'System/Task/TaskLayoutTemplate', - - regions : { - tasks : '#x-tasks' - }, - - columns : [ - { - name : 'name', - label : 'Name', - sortable : true, - cell : 'string' - }, - { - name : 'interval', - label : 'Interval', - sortable : true, - cell : TaskIntervalCell - }, - { - name : 'lastExecution', - label : 'Last Execution', - sortable : true, - cell : RelativeTimeCell - }, - { - name : 'nextExecution', - label : 'Next Execution', - sortable : true, - cell : NextExecutionCell - }, - { - name : 'this', - label : '', - sortable : false, - cell : ExecuteTaskCell - } - ], - - initialize : function() { - this.taskCollection = new BackupCollection(); - - this.listenTo(this.taskCollection, 'sync', this._showTasks); - this.taskCollection.bindSignalR(); - }, - - onRender : function() { - this.tasks.show(new LoadingView()); - - this.taskCollection.fetch(); - }, - - _showTasks : function() { - this.tasks.show(new Backgrid.Grid({ - columns : this.columns, - collection : this.taskCollection, - className : 'table table-hover' - })); - } -}); \ No newline at end of file diff --git a/src/UI/System/Task/TaskLayoutTemplate.hbs b/src/UI/System/Task/TaskLayoutTemplate.hbs deleted file mode 100644 index 0a3631541..000000000 --- a/src/UI/System/Task/TaskLayoutTemplate.hbs +++ /dev/null @@ -1,5 +0,0 @@ -<div class="row"> - <div class="col-md-12"> - <div id="x-tasks" class="tasks table-responsive"/> - </div> -</div> diff --git a/src/UI/System/Task/TaskModel.js b/src/UI/System/Task/TaskModel.js deleted file mode 100644 index 3986a5948..000000000 --- a/src/UI/System/Task/TaskModel.js +++ /dev/null @@ -1,3 +0,0 @@ -var Backbone = require('backbone'); - -module.exports = Backbone.Model.extend({}); \ No newline at end of file diff --git a/src/UI/System/Update/EmptyView.js b/src/UI/System/Update/EmptyView.js deleted file mode 100644 index a18f84f4d..000000000 --- a/src/UI/System/Update/EmptyView.js +++ /dev/null @@ -1,5 +0,0 @@ -var Marionette = require('marionette'); - -module.exports = Marionette.ItemView.extend({ - template : 'System/Update/EmptyViewTemplate' -}); \ No newline at end of file diff --git a/src/UI/System/Update/EmptyViewTemplate.hbs b/src/UI/System/Update/EmptyViewTemplate.hbs deleted file mode 100644 index 728e10d93..000000000 --- a/src/UI/System/Update/EmptyViewTemplate.hbs +++ /dev/null @@ -1 +0,0 @@ -<div>No updates are available</div> \ No newline at end of file diff --git a/src/UI/System/Update/UpdateCollection.js b/src/UI/System/Update/UpdateCollection.js deleted file mode 100644 index 2b21a6616..000000000 --- a/src/UI/System/Update/UpdateCollection.js +++ /dev/null @@ -1,7 +0,0 @@ -var Backbone = require('backbone'); -var UpdateModel = require('./UpdateModel'); - -module.exports = Backbone.Collection.extend({ - url : window.NzbDrone.ApiRoot + '/update', - model : UpdateModel -}); \ No newline at end of file diff --git a/src/UI/System/Update/UpdateCollectionView.js b/src/UI/System/Update/UpdateCollectionView.js deleted file mode 100644 index 7af0bfc73..000000000 --- a/src/UI/System/Update/UpdateCollectionView.js +++ /dev/null @@ -1,8 +0,0 @@ -var Marionette = require('marionette'); -var UpdateItemView = require('./UpdateItemView'); -var EmptyView = require('./EmptyView'); - -module.exports = Marionette.CollectionView.extend({ - itemView : UpdateItemView, - emptyView : EmptyView -}); \ No newline at end of file diff --git a/src/UI/System/Update/UpdateItemView.js b/src/UI/System/Update/UpdateItemView.js deleted file mode 100644 index 73ed31e0c..000000000 --- a/src/UI/System/Update/UpdateItemView.js +++ /dev/null @@ -1,31 +0,0 @@ -var Marionette = require('marionette'); -var CommandController = require('../../Commands/CommandController'); - -module.exports = Marionette.ItemView.extend({ - template : 'System/Update/UpdateItemViewTemplate', - - events : { - 'click .x-install-update' : '_installUpdate' - }, - - initialize : function() { - this.updating = false; - }, - - _installUpdate : function() { - if (this.updating) { - return; - } - - this.updating = true; - var self = this; - - var promise = CommandController.Execute('applicationUpdate'); - - promise.done(function() { - window.setTimeout(function() { - self.updating = false; - }, 5000); - }); - } -}); \ No newline at end of file diff --git a/src/UI/System/Update/UpdateItemViewTemplate.hbs b/src/UI/System/Update/UpdateItemViewTemplate.hbs deleted file mode 100644 index f9a2dce19..000000000 --- a/src/UI/System/Update/UpdateItemViewTemplate.hbs +++ /dev/null @@ -1,43 +0,0 @@ -<div class="update"> - <fieldset> - <legend>{{version}} - <span class="date"> - - {{ShortDate releaseDate}} - </span> - <span class="status"> - {{#unless_eq branch compare="master"}} - <span class="label label-default">{{branch}}</span> - {{/unless_eq}} - {{#if installed}} - <span class="label label-success">Installed</span> - {{else}} - {{#if latest}} - {{#if installable}} - <span class="label label-info install-update x-install-update">Install Latest</span> - {{else}} - <span class="label label-info label-disabled" title="Cannot install an older version">Install Latest</span> - {{/if}} - {{/if}} - {{/if}} - </span> - </legend> - - {{#with changes}} - {{#each new}} - <div class="change"> - <span class="label label-success">New</span> {{this}} - </div> - {{/each}} - - {{#each fixed}} - <div class="change"> - <span class="label label-info">Fixed</span> {{this}} - </div> - {{/each}} - {{/with}} - - {{#unless changes}} - Maintenance release - {{/unless}} - </fieldset> -</div> diff --git a/src/UI/System/Update/UpdateLayout.js b/src/UI/System/Update/UpdateLayout.js deleted file mode 100644 index a1fd84d06..000000000 --- a/src/UI/System/Update/UpdateLayout.js +++ /dev/null @@ -1,29 +0,0 @@ -var Marionette = require('marionette'); -var Backgrid = require('backgrid'); -var UpdateCollection = require('./UpdateCollection'); -var UpdateCollectionView = require('./UpdateCollectionView'); -var LoadingView = require('../../Shared/LoadingView'); - -module.exports = Marionette.Layout.extend({ - template : 'System/Update/UpdateLayoutTemplate', - - regions : { - updates : '#x-updates' - }, - - initialize : function() { - this.updateCollection = new UpdateCollection(); - - this.listenTo(this.updateCollection, 'sync', this._showUpdates); - }, - - onRender : function() { - this.updates.show(new LoadingView()); - - this.updateCollection.fetch(); - }, - - _showUpdates : function() { - this.updates.show(new UpdateCollectionView({ collection : this.updateCollection })); - } -}); \ No newline at end of file diff --git a/src/UI/System/Update/UpdateLayoutTemplate.hbs b/src/UI/System/Update/UpdateLayoutTemplate.hbs deleted file mode 100644 index 0bd69dc20..000000000 --- a/src/UI/System/Update/UpdateLayoutTemplate.hbs +++ /dev/null @@ -1,5 +0,0 @@ -<div class="row"> - <div class="col-md-12"> - <div id="x-updates"/> - </div> -</div> diff --git a/src/UI/System/Update/UpdateModel.js b/src/UI/System/Update/UpdateModel.js deleted file mode 100644 index 3986a5948..000000000 --- a/src/UI/System/Update/UpdateModel.js +++ /dev/null @@ -1,3 +0,0 @@ -var Backbone = require('backbone'); - -module.exports = Backbone.Model.extend({}); \ No newline at end of file diff --git a/src/UI/System/Update/update.less b/src/UI/System/Update/update.less deleted file mode 100644 index 1db354fd6..000000000 --- a/src/UI/System/Update/update.less +++ /dev/null @@ -1,51 +0,0 @@ -@import '../../Shared/Styles/clickable'; - -.update { - margin-bottom: 30px; - - legend { - cursor : default; - margin-bottom : 5px; - line-height : 30px; - display : inline-block; - - .date { - font-size : 16px; - display : inline-block; - } - - .status { - margin-left : 5px; - font-size : 14px; - margin-top : -3px; - display : inline-block; - vertical-align : middle; - } - - .install-update { - .clickable(); - } - } - - .changes-header { - font-size: 18px; - } - - .label { - width: 40px; - text-align: center; - } - .change { - margin-bottom: 2px; - font-size: 13px; - } - - a { - color: white; - text-decoration: none; - } - - a:hover { - text-decoration: none; - } -} \ No newline at end of file diff --git a/src/UI/Tags/TagCollection.js b/src/UI/Tags/TagCollection.js deleted file mode 100644 index 287f6eaef..000000000 --- a/src/UI/Tags/TagCollection.js +++ /dev/null @@ -1,14 +0,0 @@ -var Backbone = require('backbone'); -var TagModel = require('./TagModel'); -var ApiData = require('../Shared/ApiData'); - -var Collection = Backbone.Collection.extend({ - url : window.NzbDrone.ApiRoot + '/tag', - model : TagModel, - - comparator : function(model) { - return model.get('label'); - } -}); - -module.exports = new Collection(ApiData.get('tag')); diff --git a/src/UI/Tags/TagHelpers.js b/src/UI/Tags/TagHelpers.js deleted file mode 100644 index a745de057..000000000 --- a/src/UI/Tags/TagHelpers.js +++ /dev/null @@ -1,25 +0,0 @@ -var _ = require('underscore'); -var Handlebars = require('handlebars'); -var TagCollection = require('./TagCollection'); - -Handlebars.registerHelper('tagDisplay', function(tags) { - var tagLabels = _.map(TagCollection.filter(function(tag) { - return _.contains(tags, tag.get('id')); - }), function(tag) { - return '<span class="label label-info">{0}</span>'.format(tag.get('label')); - }); - - return new Handlebars.SafeString(tagLabels.join(' ')); -}); - -Handlebars.registerHelper('genericTagDisplay', function(tags, classes) { - if (!tags) { - return new Handlebars.SafeString(''); - } - - var tagLabels = _.map(tags.split(','), function(tag) { - return '<span class="{0}">{1}</span>'.format(classes, tag); - }); - - return new Handlebars.SafeString(tagLabels.join(' ')); -}); \ No newline at end of file diff --git a/src/UI/Tags/TagModel.js b/src/UI/Tags/TagModel.js deleted file mode 100644 index 3986a5948..000000000 --- a/src/UI/Tags/TagModel.js +++ /dev/null @@ -1,3 +0,0 @@ -var Backbone = require('backbone'); - -module.exports = Backbone.Model.extend({}); \ No newline at end of file diff --git a/src/UI/Wanted/ControlsColumnTemplate.hbs b/src/UI/Wanted/ControlsColumnTemplate.hbs deleted file mode 100644 index 75f689167..000000000 --- a/src/UI/Wanted/ControlsColumnTemplate.hbs +++ /dev/null @@ -1 +0,0 @@ -<i class="icon-lidarr-search x-search" title="Search"/> diff --git a/src/UI/Wanted/Cutoff/CutoffUnmetCollection.js b/src/UI/Wanted/Cutoff/CutoffUnmetCollection.js deleted file mode 100644 index 5f2a6546f..000000000 --- a/src/UI/Wanted/Cutoff/CutoffUnmetCollection.js +++ /dev/null @@ -1,63 +0,0 @@ -var _ = require('underscore'); -var EpisodeModel = require('../../Series/EpisodeModel'); -var PagableCollection = require('backbone.pageable'); -var AsFilteredCollection = require('../../Mixins/AsFilteredCollection'); -var AsSortedCollection = require('../../Mixins/AsSortedCollection'); -var AsPersistedStateCollection = require('../../Mixins/AsPersistedStateCollection'); - -var Collection = PagableCollection.extend({ - url : window.NzbDrone.ApiRoot + '/wanted/cutoff', - model : EpisodeModel, - tableName : 'wanted.cutoff', - - state : { - pageSize : 15, - sortKey : 'airDateUtc', - order : 1 - }, - - queryParams : { - totalPages : null, - totalRecords : null, - pageSize : 'pageSize', - sortKey : 'sortKey', - order : 'sortDir', - directions : { - '-1' : 'asc', - '1' : 'desc' - } - }, - - // Filter Modes - filterModes : { - 'monitored' : [ - 'monitored', - 'true' - ], - 'unmonitored' : [ - 'monitored', - 'false' - ], - }, - - sortMappings : { - 'series' : { sortKey : 'series.sortTitle' } - }, - - parseState : function(resp) { - return { totalRecords : resp.totalRecords }; - }, - - parseRecords : function(resp) { - if (resp) { - return resp.records; - } - - return resp; - } -}); - -Collection = AsFilteredCollection.call(Collection); -Collection = AsSortedCollection.call(Collection); - -module.exports = AsPersistedStateCollection.call(Collection); \ No newline at end of file diff --git a/src/UI/Wanted/Cutoff/CutoffUnmetLayout.js b/src/UI/Wanted/Cutoff/CutoffUnmetLayout.js deleted file mode 100644 index 69006e827..000000000 --- a/src/UI/Wanted/Cutoff/CutoffUnmetLayout.js +++ /dev/null @@ -1,188 +0,0 @@ -var _ = require('underscore'); -var Marionette = require('marionette'); -var Backgrid = require('backgrid'); -var CutoffUnmetCollection = require('./CutoffUnmetCollection'); -var SelectAllCell = require('../../Cells/SelectAllCell'); -var SeriesTitleCell = require('../../Cells/SeriesTitleCell'); -var EpisodeNumberCell = require('../../Cells/EpisodeNumberCell'); -var EpisodeTitleCell = require('../../Cells/EpisodeTitleCell'); -var RelativeDateCell = require('../../Cells/RelativeDateCell'); -var EpisodeStatusCell = require('../../Cells/EpisodeStatusCell'); -var GridPager = require('../../Shared/Grid/Pager'); -var ToolbarLayout = require('../../Shared/Toolbar/ToolbarLayout'); -var LoadingView = require('../../Shared/LoadingView'); -var Messenger = require('../../Shared/Messenger'); -var CommandController = require('../../Commands/CommandController'); -require('backgrid.selectall'); -require('../../Mixins/backbone.signalr.mixin'); - -module.exports = Marionette.Layout.extend({ - template : 'Wanted/Cutoff/CutoffUnmetLayoutTemplate', - - regions : { - cutoff : '#x-cutoff-unmet', - toolbar : '#x-toolbar', - pager : '#x-pager' - }, - - ui : { - searchSelectedButton : '.btn i.icon-lidarr-search' - }, - - columns : [ - { - name : '', - cell : SelectAllCell, - headerCell : 'select-all', - sortable : false - }, - { - name : 'series', - label : 'Artist', - cell : SeriesTitleCell, - sortValue : 'series.sortTitle' - }, -// { -// name : 'this', -// label : 'Track Number', -// cell : EpisodeNumberCell, -// sortable : false -// }, - { - name : 'this', - label : 'Track Title', - cell : EpisodeTitleCell, - sortable : false - }, - { - name : 'airDateUtc', - label : 'Release Date', - cell : RelativeDateCell - }, - { - name : 'status', - label : 'Status', - cell : EpisodeStatusCell, - sortable : false - } - ], - - initialize : function() { - this.collection = new CutoffUnmetCollection().bindSignalR({ updateOnly : true }); - - this.listenTo(this.collection, 'sync', this._showTable); - }, - - onShow : function() { - this.cutoff.show(new LoadingView()); - this._showToolbar(); - this.collection.fetch(); - }, - - _showTable : function() { - this.cutoffGrid = new Backgrid.Grid({ - columns : this.columns, - collection : this.collection, - className : 'table table-hover' - }); - - this.cutoff.show(this.cutoffGrid); - - this.pager.show(new GridPager({ - columns : this.columns, - collection : this.collection - })); - }, - - _showToolbar : function() { - var leftSideButtons = { - type : 'default', - storeState : false, - items : [ - { - title : 'Search Selected', - icon : 'icon-lidarr-search', - callback : this._searchSelected, - ownerContext : this, - className : 'x-search-selected' - }, - { - title : 'Album Studio', - icon : 'icon-lidarr-monitored', - route : 'albumstudio' - } - ] - }; - - var filterOptions = { - type : 'radio', - storeState : false, - menuKey : 'wanted.filterMode', - defaultAction : 'monitored', - items : [ - { - key : 'monitored', - title : '', - tooltip : 'Monitored Only', - icon : 'icon-lidarr-monitored', - callback : this._setFilter - }, - { - key : 'unmonitored', - title : '', - tooltip : 'Unmonitored Only', - icon : 'icon-lidarr-unmonitored', - callback : this._setFilter - } - ] - }; - - this.toolbar.show(new ToolbarLayout({ - left : [ - leftSideButtons - ], - right : [ - filterOptions - ], - context : this - })); - - CommandController.bindToCommand({ - element : this.$('.x-search-selected'), - command : { - name : 'episodeSearch' - } - }); - }, - - _setFilter : function(buttonContext) { - var mode = buttonContext.model.get('key'); - - this.collection.state.currentPage = 1; - var promise = this.collection.setFilterMode(mode); - - if (buttonContext) { - buttonContext.ui.icon.spinForPromise(promise); - } - }, - - _searchSelected : function() { - var selected = this.cutoffGrid.getSelectedModels(); - - if (selected.length === 0) { - Messenger.show({ - type : 'error', - message : 'No episodes selected' - }); - - return; - } - - var ids = _.pluck(selected, 'id'); - - CommandController.Execute('episodeSearch', { - name : 'episodeSearch', - episodeIds : ids - }); - } -}); \ No newline at end of file diff --git a/src/UI/Wanted/Cutoff/CutoffUnmetLayoutTemplate.hbs b/src/UI/Wanted/Cutoff/CutoffUnmetLayoutTemplate.hbs deleted file mode 100644 index 7c6d095c0..000000000 --- a/src/UI/Wanted/Cutoff/CutoffUnmetLayoutTemplate.hbs +++ /dev/null @@ -1,11 +0,0 @@ -<div id="x-toolbar"/> -<div class="row"> - <div class="col-md-12"> - <div id="x-cutoff-unmet" class="table-responsive"/> - </div> -</div> -<div class="row"> - <div class="col-md-12"> - <div id="x-pager"/> - </div> -</div> diff --git a/src/UI/Wanted/Missing/MissingCollection.js b/src/UI/Wanted/Missing/MissingCollection.js deleted file mode 100644 index eb2c8ae8d..000000000 --- a/src/UI/Wanted/Missing/MissingCollection.js +++ /dev/null @@ -1,61 +0,0 @@ -var _ = require('underscore'); -var EpisodeModel = require('../../Series/EpisodeModel'); -var PagableCollection = require('backbone.pageable'); -var AsFilteredCollection = require('../../Mixins/AsFilteredCollection'); -var AsSortedCollection = require('../../Mixins/AsSortedCollection'); -var AsPersistedStateCollection = require('../../Mixins/AsPersistedStateCollection'); - -var Collection = PagableCollection.extend({ - url : window.NzbDrone.ApiRoot + '/wanted/missing', - model : EpisodeModel, - tableName : 'wanted.missing', - - state : { - pageSize : 15, - sortKey : 'releaseDate', - order : 1 - }, - - queryParams : { - totalPages : null, - totalRecords : null, - pageSize : 'pageSize', - sortKey : 'sortKey', - order : 'sortDir', - directions : { - '-1' : 'asc', - '1' : 'desc' - } - }, - - filterModes : { - 'monitored' : [ - 'monitored', - 'true' - ], - 'unmonitored' : [ - 'monitored', - 'false' - ] - }, - - sortMappings : { - 'artist' : { sortKey : 'artist.sortName' } - }, - - parseState : function(resp) { - return { totalRecords : resp.totalRecords }; - }, - - parseRecords : function(resp) { - if (resp) { - return resp.records; - } - - return resp; - } -}); -Collection = AsFilteredCollection.call(Collection); -Collection = AsSortedCollection.call(Collection); - -module.exports = AsPersistedStateCollection.call(Collection); \ No newline at end of file diff --git a/src/UI/Wanted/Missing/MissingLayout.js b/src/UI/Wanted/Missing/MissingLayout.js deleted file mode 100644 index f7c750892..000000000 --- a/src/UI/Wanted/Missing/MissingLayout.js +++ /dev/null @@ -1,240 +0,0 @@ -var $ = require('jquery'); -var _ = require('underscore'); -var vent = require('../../vent'); -var Marionette = require('marionette'); -var Backgrid = require('backgrid'); -var MissingCollection = require('./MissingCollection'); -var SelectAllCell = require('../../Cells/SelectAllCell'); -var ArtistTitleCell = require('../../Cells/ArtistTitleCell'); -var EpisodeNumberCell = require('../../Cells/EpisodeNumberCell'); -var AlbumTitleCell = require('../../Cells/AlbumTitleCell'); -var RelativeDateCell = require('../../Cells/RelativeDateCell'); -var EpisodeStatusCell = require('../../Cells/EpisodeStatusCell'); -var GridPager = require('../../Shared/Grid/Pager'); -var ToolbarLayout = require('../../Shared/Toolbar/ToolbarLayout'); -var LoadingView = require('../../Shared/LoadingView'); -var Messenger = require('../../Shared/Messenger'); -var CommandController = require('../../Commands/CommandController'); - -require('backgrid.selectall'); -require('../../Mixins/backbone.signalr.mixin'); - -module.exports = Marionette.Layout.extend({ - template : 'Wanted/Missing/MissingLayoutTemplate', - - regions : { - missing : '#x-missing', - toolbar : '#x-toolbar', - pager : '#x-pager' - }, - - ui : { - searchSelectedButton : '.btn i.icon-lidarr-search' - }, - - columns : [ - { - name : '', - cell : SelectAllCell, - headerCell : 'select-all', - sortable : false - }, - { - name : 'artist', - label : 'Artist', - cell : ArtistTitleCell, - sortValue : 'artist.sortName' - }, -// { -// name : 'this', -// label : 'Ttack Number', -// cell : EpisodeNumberCell, -// sortable : false -// }, - { - name : 'this', - label : 'Album Title', - cell : AlbumTitleCell, - sortable : false - }, - { - name : 'releaseDate', - label : 'Release Date', - cell : RelativeDateCell - }, - { - name : 'status', - label : 'Status', - cell : EpisodeStatusCell, - sortable : false - } - ], - - initialize : function() { - this.collection = new MissingCollection().bindSignalR({ updateOnly : true }); - - this.listenTo(this.collection, 'sync', this._showTable); - }, - - onShow : function() { - this.missing.show(new LoadingView()); - this._showToolbar(); - this.collection.fetch(); - }, - - _showTable : function() { - this.missingGrid = new Backgrid.Grid({ - columns : this.columns, - collection : this.collection, - className : 'table table-hover' - }); - - this.missing.show(this.missingGrid); - - this.pager.show(new GridPager({ - columns : this.columns, - collection : this.collection - })); - }, - - _showToolbar : function() { - var leftSideButtons = { - type : 'default', - storeState : false, - collapse : true, - items : [ - { - title : 'Search Selected', - icon : 'icon-lidarr-search', - callback : this._searchSelected, - ownerContext : this, - className : 'x-search-selected' - }, - { - title : 'Search All Missing', - icon : 'icon-lidarr-search', - callback : this._searchMissing, - ownerContext : this, - className : 'x-search-missing' - }, - { - title : 'Toggle Selected', - icon : 'icon-lidarr-monitored', - tooltip : 'Toggle monitored status of selected', - callback : this._toggleMonitoredOfSelected, - ownerContext : this, - className : 'x-unmonitor-selected' - }, - { - title : 'Album Studio', - icon : 'icon-lidarr-monitored', - route : 'albumstudio' - }, - { - title : 'Rescan Drone Factory Folder', - icon : 'icon-lidarr-refresh', - command : 'downloadedalbumsscan', - properties : { sendUpdates : true } - }, - { - title : 'Manual Import', - icon : 'icon-lidarr-search-manual', - callback : this._manualImport, - ownerContext : this - } - ] - }; - var filterOptions = { - type : 'radio', - storeState : false, - menuKey : 'wanted.filterMode', - defaultAction : 'monitored', - items : [ - { - key : 'monitored', - title : '', - tooltip : 'Monitored Only', - icon : 'icon-lidarr-monitored', - callback : this._setFilter - }, - { - key : 'unmonitored', - title : '', - tooltip : 'Unmonitored Only', - icon : 'icon-lidarr-unmonitored', - callback : this._setFilter - } - ] - }; - this.toolbar.show(new ToolbarLayout({ - left : [leftSideButtons], - right : [filterOptions], - context : this - })); - CommandController.bindToCommand({ - element : this.$('.x-search-selected'), - command : { name : 'episodeSearch' } - }); - CommandController.bindToCommand({ - element : this.$('.x-search-missing'), - command : { name : 'missingEpisodeSearch' } - }); - }, - - _setFilter : function(buttonContext) { - var mode = buttonContext.model.get('key'); - this.collection.state.currentPage = 1; - var promise = this.collection.setFilterMode(mode); - if (buttonContext) { - buttonContext.ui.icon.spinForPromise(promise); - } - }, - - _searchSelected : function() { - var selected = this.missingGrid.getSelectedModels(); - if (selected.length === 0) { - Messenger.show({ - type : 'error', - message : 'No albums selected' - }); - return; - } - var ids = _.pluck(selected, 'id'); - CommandController.Execute('episodeSearch', { - name : 'episodeSearch', - episodeIds : ids - }); - }, - _searchMissing : function() { - if (window.confirm('Are you sure you want to search for {0} missing albums? '.format(this.collection.state.totalRecords) + - 'One API request to each indexer will be used for each album. ' + 'This cannot be stopped once started.')) { - CommandController.Execute('missingEpisodeSearch', { name : 'missingEpisodeSearch' }); - } - }, - _toggleMonitoredOfSelected : function() { - var selected = this.missingGrid.getSelectedModels(); - - if (selected.length === 0) { - Messenger.show({ - type : 'error', - message : 'No albums selected' - }); - return; - } - - var promises = []; - var self = this; - - _.each(selected, function (episode) { - episode.set('monitored', !episode.get('monitored')); - promises.push(episode.save()); - }); - - $.when(promises).done(function () { - self.collection.fetch(); - }); - }, - _manualImport : function () { - vent.trigger(vent.Commands.ShowManualImport); - } -}); \ No newline at end of file diff --git a/src/UI/Wanted/Missing/MissingLayoutTemplate.hbs b/src/UI/Wanted/Missing/MissingLayoutTemplate.hbs deleted file mode 100644 index 4fd573b09..000000000 --- a/src/UI/Wanted/Missing/MissingLayoutTemplate.hbs +++ /dev/null @@ -1,11 +0,0 @@ -<div id="x-toolbar"/> -<div class="row"> - <div class="col-md-12"> - <div id="x-missing" class="table-responsive"/> - </div> -</div> -<div class="row"> - <div class="col-md-12"> - <div id="x-pager"/> - </div> -</div> diff --git a/src/UI/Wanted/WantedLayout.js b/src/UI/Wanted/WantedLayout.js deleted file mode 100644 index f7cce2109..000000000 --- a/src/UI/Wanted/WantedLayout.js +++ /dev/null @@ -1,68 +0,0 @@ -var Marionette = require('marionette'); -var Backbone = require('backbone'); -var Backgrid = require('backgrid'); -var MissingLayout = require('./Missing/MissingLayout'); -var CutoffUnmetLayout = require('./Cutoff/CutoffUnmetLayout'); - -module.exports = Marionette.Layout.extend({ - template : 'Wanted/WantedLayoutTemplate', - - regions : { - content : '#content' - //missing : '#missing', - //cutoff : '#cutoff' - }, - - ui : { - missingTab : '.x-missing-tab', - cutoffTab : '.x-cutoff-tab' - }, - - events : { - 'click .x-missing-tab' : '_showMissing', - 'click .x-cutoff-tab' : '_showCutoffUnmet' - }, - - initialize : function(options) { - if (options.action) { - this.action = options.action.toLowerCase(); - } - }, - - onShow : function() { - switch (this.action) { - case 'cutoff': - this._showCutoffUnmet(); - break; - default: - this._showMissing(); - } - }, - - _navigate : function(route) { - Backbone.history.navigate(route, { - trigger : false, - replace : true - }); - }, - - _showMissing : function(e) { - if (e) { - e.preventDefault(); - } - - this.content.show(new MissingLayout()); - this.ui.missingTab.tab('show'); - this._navigate('/wanted/missing'); - }, - - _showCutoffUnmet : function(e) { - if (e) { - e.preventDefault(); - } - - this.content.show(new CutoffUnmetLayout()); - this.ui.cutoffTab.tab('show'); - this._navigate('/wanted/cutoff'); - } -}); \ No newline at end of file diff --git a/src/UI/Wanted/WantedLayoutTemplate.hbs b/src/UI/Wanted/WantedLayoutTemplate.hbs deleted file mode 100644 index 973feb838..000000000 --- a/src/UI/Wanted/WantedLayoutTemplate.hbs +++ /dev/null @@ -1,10 +0,0 @@ -<ul class="nav nav-tabs"> - <li><a href="#missing" class="x-missing-tab no-router">Missing</a></li> - <li><a href="#cutoff" class="x-cutoff-tab no-router">Cutoff Unmet</a></li> -</ul> - -<div class="tab-pane" id="content"></div> -<!--<div class="tab-content"> - <div class="tab-pane" id="missing"></div> - <div class="tab-pane" id="cutoff"></div> -</div>--> \ No newline at end of file diff --git a/src/UI/app.js b/src/UI/app.js deleted file mode 100644 index 3ebfafdb0..000000000 --- a/src/UI/app.js +++ /dev/null @@ -1,159 +0,0 @@ -'use strict'; -require.config({ - - paths : { - 'backbone' : 'JsLibraries/backbone', - 'moment' : 'JsLibraries/moment', - 'filesize' : 'JsLibraries/filesize', - 'handlebars' : 'Shared/Shims/handlebars', - 'handlebars.helpers' : 'JsLibraries/handlebars.helpers', - 'bootstrap' : 'JsLibraries/bootstrap', - 'bootstrap.tagsinput' : 'JsLibraries/bootstrap.tagsinput', - 'backbone.deepmodel' : 'JsLibraries/backbone.deep.model', - 'backbone.pageable' : 'JsLibraries/backbone.pageable', - 'backbone.validation' : 'JsLibraries/backbone.validation', - 'backbone.modelbinder' : 'JsLibraries/backbone.modelbinder', - 'backbone.collectionview' : 'JsLibraries/backbone.collectionview', - 'backgrid' : 'JsLibraries/backbone.backgrid', - 'backgrid.paginator' : 'JsLibraries/backbone.backgrid.paginator', - 'backgrid.selectall' : 'JsLibraries/backbone.backgrid.selectall', - 'fullcalendar' : 'JsLibraries/fullcalendar', - 'backstrech' : 'JsLibraries/jquery.backstretch', - 'underscore' : 'JsLibraries/lodash.underscore', - 'marionette' : 'JsLibraries/backbone.marionette', - 'signalR' : 'JsLibraries/jquery.signalR', - 'jquery-ui' : 'JsLibraries/jquery-ui', - 'jquery.knob' : 'JsLibraries/jquery.knob', - 'jquery.easypiechart' : 'JsLibraries/jquery.easypiechart', - 'jquery.dotdotdot' : 'JsLibraries/jquery.dotdotdot', - 'messenger' : 'JsLibraries/messenger', - 'jquery' : 'JsLibraries/jquery', - 'typeahead' : 'JsLibraries/typeahead', - 'zero.clipboard' : 'JsLibraries/zero.clipboard', - 'libs' : 'JsLibraries/' - }, - - shim : { - api : { - deps : ['jquery'] - }, - jquery : { - exports : '$' - }, - messenger : { - deps : ['jquery'], - exports : 'Messenger', - init : function() { - window.Messenger.options = { - theme : 'flat' - }; - } - }, - signalR : { - deps : ['jquery'] - }, - bootstrap : { - deps : ['jquery'] - }, - 'bootstrap.tagsinput' : { - deps : [ - 'bootstrap', - 'typeahead' - ] - }, - backstrech : { - deps : ['jquery'] - }, - underscore : { - deps : ['jquery'], - exports : '_' - }, - backbone : { - deps : [ - 'jquery', - 'Instrumentation/ErrorHandler', - 'underscore', - 'Mixins/jquery.ajax', - 'jQuery/ToTheTop' - ], - exports : 'Backbone' - }, - marionette : { - deps : [ - 'backbone', - 'Handlebars/backbone.marionette.templates', - 'Mixins/AsNamedView' - ], - exports : 'Marionette', - init : function(Backbone, TemplateMixin, AsNamedView) { - TemplateMixin.call(window.Marionette.TemplateCache); - AsNamedView.call(window.Marionette.ItemView.prototype); - } - }, - 'typeahead' : { - deps : ['jquery'] - }, - 'jquery-ui' : { - deps : ['jquery'] - }, - 'jquery.knob' : { - deps : ['jquery'] - }, - 'jquery.easypiechart' : { - deps : ['jquery'] - }, - 'jquery.dotdotdot' : { - deps : ['jquery'] - }, - 'backbone.pageable' : { - deps : ['backbone'] - }, - 'backbone.deepmodel' : { - deps : [ - 'backbone', - 'underscore' - ] - }, - 'backbone.validation' : { - deps : ['backbone'], - exports : 'Backbone.Validation' - }, - 'backbone.modelbinder' : { - deps : ['backbone'] - }, - 'backbone.collectionview' : { - deps : [ - 'backbone', - 'jquery-ui' - ], - exports : 'Backbone.CollectionView' - }, - backgrid : { - deps : ['backbone'], - exports : 'Backgrid', - init : function() { - require(['Shared/Grid/HeaderCell'], function() { - window.Backgrid.Column.prototype.defaults = { - name : undefined, - label : undefined, - sortable : true, - editable : false, - renderable : true, - formatter : undefined, - cell : undefined, - headerCell : 'NzbDrone', - sortType : 'toggle' - }; - }); - } - }, - 'backgrid.paginator' : { - deps : ['backgrid'], - exports : 'Backgrid.Extension.Paginator' - }, - 'backgrid.selectall' : { - deps : ['backgrid'], - exports : 'Backgrid.Extension.SelectRowCell' - } - } -}); \ No newline at end of file diff --git a/src/UI/index.html b/src/UI/index.html deleted file mode 100644 index a4ab8c22f..000000000 --- a/src/UI/index.html +++ /dev/null @@ -1,101 +0,0 @@ -<!doctype html> -<html> -<head> - <title>Lidarr - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- -
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- - -
-
- -
- -
-
- - - - - - - - - diff --git a/src/UI/jQuery/RouteBinder.js b/src/UI/jQuery/RouteBinder.js deleted file mode 100644 index a09b42a41..000000000 --- a/src/UI/jQuery/RouteBinder.js +++ /dev/null @@ -1,63 +0,0 @@ -var Backbone = require('backbone'); -var $ = require('jquery'); -var StatusModel = require('../System/StatusModel'); - -//This module will automatically route all relative links through backbone router rather than -//causing links to reload pages. - -var routeBinder = { - - bind : function() { - var self = this; - $(document).on('click contextmenu', 'a[href]', function(event) { - self._handleClick(event); - }); - }, - - _handleClick : function(event) { - var $target = $(event.target); - - //check if tab nav - if ($target.parents('.nav-tabs').length) { - return; - } - - var linkElement = $target.closest('a').first(); - var href = linkElement.attr('href'); - - if (href && href.startsWith('http')) { - // Set noreferrer for external links. - if (!linkElement.attr('rel')) { - linkElement.attr('rel', 'noreferrer'); - } - // Open all external links in new windows. - if (!linkElement.attr('target')) { - linkElement.attr('target', '_blank'); - } - } - - if (linkElement.hasClass('no-router') || event.type !== 'click') { - return; - } - - if (!href) { - throw 'couldn\'t find route target'; - } - - if (!href.startsWith('http')) { - event.preventDefault(); - - if (event.ctrlKey) { - window.open(href, '_blank'); - } - - else { - var relativeHref = href.replace(StatusModel.get('urlBase'), ''); - - Backbone.history.navigate(relativeHref, { trigger : true }); - } - } - } -}; - -module.exports = routeBinder; \ No newline at end of file diff --git a/src/UI/jQuery/ToTheTop.js b/src/UI/jQuery/ToTheTop.js deleted file mode 100644 index 696f903a4..000000000 --- a/src/UI/jQuery/ToTheTop.js +++ /dev/null @@ -1,23 +0,0 @@ -var $ = require('jquery'); -var _ = require('underscore'); - -$(document).ready(function() { - var _window = $(window); - var _scrollContainer = $('#scroll-up'); - var _scrollButton = $('#scroll-up i'); - - var _scrollHandler = function() { - if (_window.scrollTop() > 400) { - _scrollContainer.fadeIn(); - } else { - _scrollContainer.fadeOut(); - } - }; - - $(window).scroll(_.throttle(_scrollHandler, 500)); - _scrollButton.click(function() { - $('html, body').animate({ scrollTop : 0 }, 600); - return false; - }); -}); - diff --git a/src/UI/jQuery/jquery.ajax.js b/src/UI/jQuery/jquery.ajax.js deleted file mode 100644 index 0073e8619..000000000 --- a/src/UI/jQuery/jquery.ajax.js +++ /dev/null @@ -1,23 +0,0 @@ -module.exports = function() { - - var $ = this; - - var original = $.ajax; - $.ajax = function(xhr) { - 'use strict'; - if (xhr && xhr.data && xhr.type === 'DELETE') { - if (xhr.url.contains('?')) { - xhr.url += '&'; - } else { - xhr.url += '?'; - } - xhr.url += $.param(xhr.data); - delete xhr.data; - } - if (xhr) { - xhr.headers = xhr.headers || {}; - xhr.headers['X-Api-Key'] = window.NzbDrone.ApiKey; - } - return original.apply(this, arguments); - }; -}; diff --git a/src/UI/jQuery/jquery.spin.js b/src/UI/jQuery/jquery.spin.js deleted file mode 100644 index a5daaf671..000000000 --- a/src/UI/jQuery/jquery.spin.js +++ /dev/null @@ -1,62 +0,0 @@ -module.exports = function() { - 'use strict'; - - var $ = this; - - $.fn.spinForPromise = function(promise) { - var self = this; - - if (!promise || promise.state() !== 'pending') { - return this; - } - promise.always(function() { - self.stopSpin(); - }); - - return this.startSpin(); - }; - - $.fn.startSpin = function() { - var icon = this.find('i').andSelf('i'); - - if (!icon || !icon.attr('class')) { - return this; - } - - var iconClasses = icon.attr('class').match(/(?:^|\s)icon\-.+?(?:$|\s)/); - - if (!iconClasses || iconClasses.length === 0) { - return this; - } - - var iconClass = $.trim(iconClasses[0]); - - this.addClass('disabled'); - - if (icon.hasClass('icon-can-spin')) { - icon.addClass('fa-spin'); - } else { - icon.attr('data-idle-icon', iconClass); - icon.removeClass(iconClass); - icon.addClass('fa-spin-overlay'); - icon.html(''); - } - - return this; - }; - - $.fn.stopSpin = function() { - var icon = this.find('i').andSelf('i'); - - icon.empty(); - this.removeClass('disabled'); - icon.removeClass('fa-spin fa-spin-overlay'); - var idleIcon = icon.attr('data-idle-icon'); - - if (idleIcon) { - icon.addClass(idleIcon); - } - - return this; - }; -}; \ No newline at end of file diff --git a/src/UI/jQuery/jquery.validation.js b/src/UI/jQuery/jquery.validation.js deleted file mode 100644 index 7844c3a8c..000000000 --- a/src/UI/jQuery/jquery.validation.js +++ /dev/null @@ -1,105 +0,0 @@ -module.exports = function() { - 'use strict'; - var $ = this; - $.fn.processServerError = function(error) { - var validationName = error.propertyName.toLowerCase(); - - var errorMessage = this.formatErrorMessage(error); - - this.find('.validation-errors').addClass('alert alert-danger').append('
' + errorMessage + '
'); - - if (!validationName || validationName === '') { - this.addFormError(error); - return this; - } - - var input = this.find('[name]').filter(function() { - return this.name.toLowerCase() === validationName; - }); - - if (input.length === 0) { - input = this.find('[validation-name]').filter(function() { - return $(this).attr('validation-name').toLowerCase() === validationName; - }); - - //still not found? - if (input.length === 0) { - this.addFormError(error); - console.error('couldn\'t find input for ' + error.propertyName); - return this; - } - } - - var formGroup = input.parents('.form-group'); - - if (formGroup.length === 0) { - formGroup = input.parent(); - } else { - var inputGroup = formGroup.find('.input-group'); - - var validationClass = error.isWarning ? 'validation-warning' : 'validation-error'; - - if (inputGroup.length === 0) { - formGroup.append('{1}'.format(validationClass, errorMessage)); - } - else { - inputGroup.parent().append('{1}'.format(validationClass, errorMessage)); - } - } - - if (error.isWarning) { - formGroup.addClass('has-warning'); - } else { - formGroup.addClass('has-error'); - } - - return formGroup.find('.help-inline').text(); - }; - - $.fn.processClientError = function(error) { - - }; - - $.fn.addFormError = function(error) { - - var errorMessage = this.formatErrorMessage(error); - - var target = this.find('.modal-body'); - if (!target.length) { - target = this; - } - - var validationClass = error.isWarning ? 'alert alert-warning validation-warning' : 'alert alert-danger validation-error'; - - target.prepend('
{1}
'.format(validationClass, errorMessage)); - }; - - $.fn.removeAllErrors = function() { - this.removeClass('has-error'); - this.removeClass('has-warning'); - this.find('.has-error').removeClass('has-error'); - this.find('.has-warning').removeClass('has-warning'); - this.find('.error').removeClass('error'); - this.find('.validation-errors').removeClass('alert').removeClass('alert-danger').removeClass('alert-warning').html(''); - this.find('.validation-error').remove(); - this.find('.validation-warning').remove(); - return this.find('.help-inline.error-message').remove(); - }; - - $.fn.formatErrorMessage = function(error) { - - var errorMessage = error.errorMessage; - - if (error.infoLink) { - if (error.detailedDescription) { - errorMessage += ' '; - } else { - errorMessage += ' '; - } - } else if (error.detailedDescription) { - errorMessage += ' '; - } - - return errorMessage; - }; -}; \ No newline at end of file diff --git a/src/UI/login.html b/src/UI/login.html deleted file mode 100644 index 00c698dc1..000000000 --- a/src/UI/login.html +++ /dev/null @@ -1,54 +0,0 @@ - - - - Lidarr - Login - - - - - - - - - - - - - - - -
- -
-
-
-
-
-
- -
-
-
-
-
- - diff --git a/src/UI/main.js b/src/UI/main.js deleted file mode 100644 index 40b9e9efe..000000000 --- a/src/UI/main.js +++ /dev/null @@ -1,60 +0,0 @@ -var $ = require('jquery'); -var Backbone = require('backbone'); -var Marionette = require('marionette'); -var RouteBinder = require('./jQuery/RouteBinder'); -var SignalRBroadcaster = require('./Shared/SignalRBroadcaster'); -var NavbarLayout = require('./Navbar/NavbarLayout'); -var AppLayout = require('./AppLayout'); -var ArtistController = require('./Artist/ArtistController'); -var Router = require('./Router'); -var ModalController = require('./Shared/Modal/ModalController'); -var ControlPanelController = require('./Shared/ControlPanel/ControlPanelController'); -var serverStatusModel = require('./System/StatusModel'); -var Tooltip = require('./Shared/Tooltip'); -var UiSettingsController = require('./Shared/UiSettingsController'); - -require('./jQuery/ToTheTop'); -require('./Instrumentation/StringFormat'); -require('./LifeCycle'); -require('./Hotkeys/Hotkeys'); -require('./Shared/piwikCheck'); -require('./Shared/VersionChangeMonitor'); - -new ArtistController(); -new ModalController(); -new ControlPanelController(); -new Router(); - -var app = new Marionette.Application(); - -app.addInitializer(function() { - console.log('starting application'); -}); - -app.addInitializer(SignalRBroadcaster.appInitializer, { app : app }); - -app.addInitializer(Tooltip.appInitializer, { app : app }); - -app.addInitializer(function() { - Backbone.history.start({ - pushState : true, - root : serverStatusModel.get('urlBase') - }); - RouteBinder.bind(); - AppLayout.navbarRegion.show(new NavbarLayout()); - $('body').addClass('started'); -}); - -app.addInitializer(UiSettingsController.appInitializer); - -app.addInitializer(function() { - var footerText = serverStatusModel.get('version'); - if (serverStatusModel.get('branch') !== 'master') { - footerText += '
' + serverStatusModel.get('branch'); - } - $('#footer-region .version').html(footerText); -}); - -app.start(); - -module.exports = app; diff --git a/src/UI/polyfills.js b/src/UI/polyfills.js deleted file mode 100644 index fef657524..000000000 --- a/src/UI/polyfills.js +++ /dev/null @@ -1,39 +0,0 @@ -window.console = window.console || {}; -window.console.log = window.console.log || function() {}; -window.console.group = window.console.group || function() {}; -window.console.groupEnd = window.console.groupEnd || function() {}; -window.console.debug = window.console.debug || function() {}; -window.console.warn = window.console.warn || function() {}; -window.console.assert = window.console.assert || function() {}; - -if (!String.prototype.startsWith) { - Object.defineProperty(String.prototype, 'startsWith', { - enumerable : false, - configurable : false, - writable : false, - value : function(searchString, position) { - position = position || 0; - return this.indexOf(searchString, position) === position; - } - }); -} - -if (!String.prototype.endsWith) { - Object.defineProperty(String.prototype, 'endsWith', { - enumerable : false, - configurable : false, - writable : false, - value : function(searchString, position) { - position = position || this.length; - position = position - searchString.length; - var lastIndex = this.lastIndexOf(searchString); - return lastIndex !== -1 && lastIndex === position; - } - }); -} - -if (!('contains' in String.prototype)) { - String.prototype.contains = function(str, startIndex) { - return -1 !== String.prototype.indexOf.call(this, str, startIndex); - }; -} \ No newline at end of file diff --git a/src/UI/reqres.js b/src/UI/reqres.js deleted file mode 100644 index 1904f3602..000000000 --- a/src/UI/reqres.js +++ /dev/null @@ -1,10 +0,0 @@ -var Wreqr = require('./JsLibraries/backbone.wreqr'); - -var reqres = new Wreqr.RequestResponse(); - -reqres.Requests = { - GetEpisodeFileById : 'GetEpisodeFileById', - GetAlternateNameBySeasonNumber : 'GetAlternateNameBySeasonNumber' -}; - -module.exports = reqres; \ No newline at end of file diff --git a/src/UI/vendor.js b/src/UI/vendor.js deleted file mode 100644 index dc343bb35..000000000 --- a/src/UI/vendor.js +++ /dev/null @@ -1,34 +0,0 @@ -/*Base*/ -require('jquery'); -require('underscore'); -require('messenger'); -require('moment'); -require('fullcalendar'); -require('backstrech'); -require('signalR'); -require('jquery-ui'); -require('jquery.knob'); -require('jquery.easypiechart'); -require('jquery.dotdotdot'); -require('typeahead'); -require('zero.clipboard'); - -/*Bootstrap*/ -require('bootstrap'); -require('bootstrap.tagsinput'); - -/*Backbone*/ -require('backbone'); -require('backbone.deepmodel'); -require('backbone.pageable'); -require('backbone-pageable'); -require('backbone.validation'); - -require('backbone.modelbinder'); -require('backbone.collectionview'); -require('backgrid'); -require('backgrid.paginator'); -require('backgrid.selectall'); - -require('marionette'); //this brings in a bunch of our code into this chunk because of template helpers. -require('vent'); diff --git a/src/UI/vent.js b/src/UI/vent.js deleted file mode 100644 index 7b5eaaef1..000000000 --- a/src/UI/vent.js +++ /dev/null @@ -1,42 +0,0 @@ -var Wreqr = require('./JsLibraries/backbone.wreqr'); - -var vent = new Wreqr.EventAggregator(); - -vent.Events = { - SeriesAdded : 'series:added', - SeriesDeleted : 'series:deleted', - ArtistAdded : 'artist:added', - ArtistDeleted : 'artist:deleted', - CommandComplete : 'command:complete', - ServerUpdated : 'server:updated', - EpisodeFileDeleted : 'episodefile:deleted' -}; - -vent.Commands = { - EditArtistCommand : 'EditArtistCommand', - DeleteArtistCommand : 'DeleteArtistCommand', - OpenModalCommand : 'OpenModalCommand', - CloseModalCommand : 'CloseModalCommand', - OpenModal2Command : 'OpenModal2Command', - CloseModal2Command : 'CloseModal2Command', - ShowEpisodeDetails : 'ShowEpisodeDetails', - ShowAlbumDetails : 'ShowAlbumDetails', - ShowHistoryDetails : 'ShowHistoryDetails', - ShowLogDetails : 'ShowLogDetails', - SaveSettings : 'saveSettings', - ShowLogFile : 'showLogFile', - ShowRenamePreview : 'showRenamePreview', - ShowManualImport : 'showManualImport', - ShowFileBrowser : 'showFileBrowser', - CloseFileBrowser : 'closeFileBrowser', - OpenControlPanelCommand : 'OpenControlPanelCommand', - CloseControlPanelCommand : 'CloseControlPanelCommand' -}; - -vent.Hotkeys = { - NavbarSearch : 'navbar:search', - SaveSettings : 'settings:save', - ShowHotkeys : 'hotkeys:show' -}; - -module.exports = vent; \ No newline at end of file diff --git a/test.sh b/test.sh index 77b58f2e5..0e4d1d381 100644 --- a/test.sh +++ b/test.sh @@ -4,14 +4,20 @@ WHERE="cat != ManualTest" TEST_DIR="." TEST_PATTERN="*Test.dll" ASSEMBLIES="" +TEST_LOG_FILE="TestLog.txt" if [ -d "$TEST_DIR/_tests" ]; then TEST_DIR="$TEST_DIR/_tests" fi +rm -f "$TEST_LOG_FILE" + +# Uncomment to log test output to a file instead of the console +# export LIDARR_TESTS_LOG_OUTPUT="File" + NUNIT="$TEST_DIR/NUnit.ConsoleRunner.3.2.0/tools/nunit3-console.exe" NUNIT_COMMAND="$NUNIT" -NUNIT_PARAMS="--teamcity" +NUNIT_PARAMS="--teamcity --workers=1" if [ "$PLATFORM" = "Windows" ]; then WHERE="$WHERE && cat != LINUX" diff --git a/webpack.config.js b/webpack.config.js index 5a15477c9..e47ff06a8 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -1,5 +1,4 @@ var path = require('path'); -var stylish = require('jshint-stylish'); var webpack = require('webpack'); var uglifyJsPlugin = new webpack.optimize.UglifyJsPlugin(); @@ -17,38 +16,7 @@ module.exports = { resolve: { root: root, alias: { - 'vent': 'vent', - 'backbone': 'Shims/backbone', - 'moment': 'JsLibraries/moment', - 'filesize': 'JsLibraries/filesize', - 'handlebars': 'Shims/handlebars', - 'handlebars.helpers': 'JsLibraries/handlebars.helpers', - 'bootstrap': 'JsLibraries/bootstrap', - 'backbone.deepmodel': 'Shims/backbone.deep.model', - 'backbone.pageable': 'JsLibraries/backbone.pageable', - 'backbone-pageable': 'JsLibraries/backbone.pageable', - 'backbone.paginator': 'JsLibraries/backbone.paginator', - 'backbone.validation': 'Shims/backbone.validation', - 'backbone.modelbinder': 'JsLibraries/backbone.modelbinder', - 'backbone.collectionview': 'Shims/backbone.collectionview', - 'backgrid': 'Shims/backgrid', - 'backgrid.paginator': 'Shims/backgrid.paginator', - 'backgrid.selectall': 'Shims/backbone.backgrid.selectall', - 'fullcalendar': 'JsLibraries/fullcalendar', - 'backstrech': 'JsLibraries/jquery.backstretch', - 'underscore': 'Shims/underscore', - 'marionette': 'Shims/backbone.marionette', - 'signalR': 'Shims/jquery.signalR', - 'jquery-ui': 'JsLibraries/jquery-ui', - 'jquery.knob': 'JsLibraries/jquery.knob', - 'jquery.easypiechart': 'JsLibraries/jquery.easypiechart', - 'jquery.dotdotdot': 'JsLibraries/jquery.dotdotdot', - 'jquery.lazyload': 'JsLibraries/jquery.lazyload', - 'messenger': 'Shims/messenger', - 'jquery': 'Shims/jquery', - 'typeahead': 'JsLibraries/typeahead', - 'zero.clipboard': 'JsLibraries/zero.clipboard', - 'bootstrap.tagsinput': 'JsLibraries/bootstrap.tagsinput', + 'jdu': 'JsLibraries/jdu', 'libs': 'JsLibraries/' } }, diff --git a/yarn.lock b/yarn.lock new file mode 100644 index 000000000..b968e859d --- /dev/null +++ b/yarn.lock @@ -0,0 +1,6913 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@gulp-sourcemaps/identity-map@1.X": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@gulp-sourcemaps/identity-map/-/identity-map-1.0.1.tgz#cfa23bc5840f9104ce32a65e74db7e7a974bbee1" + dependencies: + acorn "^5.0.3" + css "^2.2.1" + normalize-path "^2.1.1" + source-map "^0.5.6" + through2 "^2.0.3" + +"@gulp-sourcemaps/map-sources@1.X": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@gulp-sourcemaps/map-sources/-/map-sources-1.0.0.tgz#890ae7c5d8c877f6d384860215ace9d7ec945bda" + dependencies: + normalize-path "^2.0.1" + through2 "^2.0.3" + +abbrev@1: + version "1.1.0" + resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.0.tgz#d0554c2256636e2f56e7c2e5ad183f859428d81f" + +acorn-dynamic-import@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/acorn-dynamic-import/-/acorn-dynamic-import-2.0.2.tgz#c752bd210bef679501b6c6cb7fc84f8f47158cc4" + dependencies: + acorn "^4.0.3" + +acorn-jsx@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-3.0.1.tgz#afdf9488fb1ecefc8348f6fb22f464e32a58b36b" + dependencies: + acorn "^3.0.4" + +acorn-to-esprima@^2.0.6, acorn-to-esprima@^2.0.8: + version "2.0.8" + resolved "https://registry.yarnpkg.com/acorn-to-esprima/-/acorn-to-esprima-2.0.8.tgz#003f0c642eb92132f417d3708f14ada82adf2eb1" + +acorn@4.X, acorn@^4.0.3: + version "4.0.13" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-4.0.13.tgz#105495ae5361d697bd195c825192e1ad7f253787" + +acorn@^3.0.4: + version "3.3.0" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-3.3.0.tgz#45e37fb39e8da3f25baee3ff5369e2bb5f22017a" + +acorn@^5.0.0, acorn@^5.0.3, acorn@^5.1.1: + version "5.1.2" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.1.2.tgz#911cb53e036807cf0fa778dc5d370fbd864246d7" + +add-px-to-style@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/add-px-to-style/-/add-px-to-style-1.0.0.tgz#d0c135441fa8014a8137904531096f67f28f263a" + +ajv-keywords@^1.0.0: + version "1.5.1" + resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-1.5.1.tgz#314dd0a4b3368fad3dfcdc54ede6171b886daf3c" + +ajv-keywords@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-2.1.0.tgz#a296e17f7bfae7c1ce4f7e0de53d29cb32162df0" + +ajv@^4.7.0: + version "4.11.8" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-4.11.8.tgz#82ffb02b29e662ae53bdc20af15947706739c536" + dependencies: + co "^4.6.0" + json-stable-stringify "^1.0.1" + +ajv@^5.0.0, ajv@^5.1.5, ajv@^5.2.0: + version "5.2.3" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-5.2.3.tgz#c06f598778c44c6b161abafe3466b81ad1814ed2" + dependencies: + co "^4.6.0" + fast-deep-equal "^1.0.0" + json-schema-traverse "^0.3.0" + json-stable-stringify "^1.0.1" + +ajv@^5.1.0: + version "5.2.2" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-5.2.2.tgz#47c68d69e86f5d953103b0074a9430dc63da5e39" + dependencies: + co "^4.6.0" + fast-deep-equal "^1.0.0" + json-schema-traverse "^0.3.0" + json-stable-stringify "^1.0.1" + +align-text@^0.1.1, align-text@^0.1.3: + version "0.1.4" + resolved "https://registry.yarnpkg.com/align-text/-/align-text-0.1.4.tgz#0cd90a561093f35d0a99256c22b7069433fad117" + dependencies: + kind-of "^3.0.2" + longest "^1.0.1" + repeat-string "^1.5.2" + +alphanum-sort@^1.0.1, alphanum-sort@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/alphanum-sort/-/alphanum-sort-1.0.2.tgz#97a1119649b211ad33691d9f9f486a8ec9fbe0a3" + +amdefine@>=0.0.4: + version "1.0.1" + resolved "https://registry.yarnpkg.com/amdefine/-/amdefine-1.0.1.tgz#4a5282ac164729e93619bcfd3ad151f817ce91f5" + +ansi-escapes@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-3.0.0.tgz#ec3e8b4e9f8064fc02c3ac9b65f1c275bda8ef92" + +ansi-regex@^0.2.0, ansi-regex@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-0.2.1.tgz#0d8e946967a3d8143f93e24e298525fc1b2235f9" + +ansi-regex@^2.0.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df" + +ansi-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998" + +ansi-styles@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-1.1.0.tgz#eaecbf66cd706882760b2f4691582b8f55d7a7de" + +ansi-styles@^2.0.1, ansi-styles@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe" + +ansi-styles@^3.1.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.0.tgz#c159b8d5be0f9e5a6f346dab94f16ce022161b88" + dependencies: + color-convert "^1.9.0" + +anymatch@^1.3.0: + version "1.3.2" + resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-1.3.2.tgz#553dcb8f91e3c889845dfdba34c77721b90b9d7a" + dependencies: + micromatch "^2.1.5" + normalize-path "^2.0.0" + +aproba@^1.0.3: + version "1.2.0" + resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a" + +archy@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/archy/-/archy-1.0.0.tgz#f9c8c13757cc1dd7bc379ac77b2c62a5c2868c40" + +are-we-there-yet@~1.1.2: + version "1.1.4" + resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-1.1.4.tgz#bb5dca382bb94f05e15194373d16fd3ba1ca110d" + dependencies: + delegates "^1.0.0" + readable-stream "^2.0.6" + +argparse@^1.0.7: + version "1.0.9" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.9.tgz#73d83bc263f86e97f8cc4f6bae1b0e90a7d22c86" + dependencies: + sprintf-js "~1.0.2" + +arr-diff@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-2.0.0.tgz#8f3b827f955a8bd669697e4a4256ac3ceae356cf" + dependencies: + arr-flatten "^1.0.1" + +arr-flatten@^1.0.1: + version "1.1.0" + resolved "https://registry.yarnpkg.com/arr-flatten/-/arr-flatten-1.1.0.tgz#36048bbff4e7b47e136644316c99669ea5ae91f1" + +array-differ@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/array-differ/-/array-differ-1.0.0.tgz#eff52e3758249d33be402b8bb8e564bb2b5d4031" + +array-each@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/array-each/-/array-each-1.0.1.tgz#a794af0c05ab1752846ee753a1f211a05ba0c44f" + +array-find-index@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/array-find-index/-/array-find-index-1.0.2.tgz#df010aa1287e164bbda6f9723b0a96a1ec4187a1" + +array-includes@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.0.3.tgz#184b48f62d92d7452bb31b323165c7f8bd02266d" + dependencies: + define-properties "^1.1.2" + es-abstract "^1.7.0" + +array-slice@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/array-slice/-/array-slice-1.0.0.tgz#e73034f00dcc1f40876008fd20feae77bd4b7c2f" + +array-union@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/array-union/-/array-union-1.0.2.tgz#9a34410e4f4e3da23dea375be5be70f24778ec39" + dependencies: + array-uniq "^1.0.1" + +array-uniq@^1.0.1, array-uniq@^1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/array-uniq/-/array-uniq-1.0.3.tgz#af6ac877a25cc7f74e058894753858dfdb24fdb6" + +array-unique@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.2.1.tgz#a1d97ccafcbc2625cc70fadceb36a50c58b01a53" + +arrify@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d" + +asap@^2.0.6, asap@~2.0.3: + version "2.0.6" + resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46" + +asn1.js@^4.0.0: + version "4.9.1" + resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-4.9.1.tgz#48ba240b45a9280e94748990ba597d216617fd40" + dependencies: + bn.js "^4.0.0" + inherits "^2.0.1" + minimalistic-assert "^1.0.0" + +asn1@~0.2.3: + version "0.2.3" + resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.3.tgz#dac8787713c9966849fc8180777ebe9c1ddf3b86" + +assert-plus@1.0.0, assert-plus@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525" + +assert@^1.1.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/assert/-/assert-1.4.1.tgz#99912d591836b5a6f5b345c0f07eefc08fc65d91" + dependencies: + util "0.10.3" + +async-each@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.1.tgz#19d386a1d9edc6e7c1c85d388aedbcc56d33602d" + +async@^2.1.2, async@^2.4.1: + version "2.5.0" + resolved "https://registry.yarnpkg.com/async/-/async-2.5.0.tgz#843190fd6b7357a0b9e1c956edddd5ec8462b54d" + dependencies: + lodash "^4.14.0" + +asynckit@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" + +atob@~1.1.0: + version "1.1.3" + resolved "https://registry.yarnpkg.com/atob/-/atob-1.1.3.tgz#95f13629b12c3a51a5d215abdce2aa9f32f80773" + +autoprefixer@7.1.5: + version "7.1.5" + resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-7.1.5.tgz#d65d14b83c7cd1dd7bc801daa00557addf5a06b2" + dependencies: + browserslist "^2.5.0" + caniuse-lite "^1.0.30000744" + normalize-range "^0.1.2" + num2fraction "^1.2.2" + postcss "^6.0.13" + postcss-value-parser "^3.2.3" + +autoprefixer@^6.3.1: + version "6.3.6" + resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-6.3.6.tgz#de772e1fcda08dce0e992cecf79252d5f008e367" + dependencies: + browserslist "~1.3.1" + caniuse-db "^1.0.30000444" + normalize-range "^0.1.2" + num2fraction "^1.2.2" + postcss "^5.0.19" + postcss-value-parser "^3.2.3" + +autoprefixer@^7.1.2: + version "7.1.4" + resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-7.1.4.tgz#960847dbaa4016bc8e8e52ec891cbf8f1257a748" + dependencies: + browserslist "^2.4.0" + caniuse-lite "^1.0.30000726" + normalize-range "^0.1.2" + num2fraction "^1.2.2" + postcss "^6.0.11" + postcss-value-parser "^3.2.3" + +aws-sign2@~0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8" + +aws4@^1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.6.0.tgz#83ef5ca860b2b32e4a0deedee8c771b9db57471e" + +babel-code-frame@7.0.0-beta.0: + version "7.0.0-beta.0" + resolved "https://registry.yarnpkg.com/babel-code-frame/-/babel-code-frame-7.0.0-beta.0.tgz#418a7b5f3f7dc9a4670e61b1158b4c5661bec98d" + dependencies: + chalk "^2.0.0" + esutils "^2.0.2" + js-tokens "^3.0.0" + +babel-code-frame@^6.11.0, babel-code-frame@^6.22.0, babel-code-frame@^6.26.0: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-code-frame/-/babel-code-frame-6.26.0.tgz#63fd43f7dc1e3bb7ce35947db8fe369a3f58c74b" + dependencies: + chalk "^1.1.3" + esutils "^2.0.2" + js-tokens "^3.0.2" + +babel-core@6.26.0, babel-core@^6.26.0: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-core/-/babel-core-6.26.0.tgz#af32f78b31a6fcef119c87b0fd8d9753f03a0bb8" + dependencies: + babel-code-frame "^6.26.0" + babel-generator "^6.26.0" + babel-helpers "^6.24.1" + babel-messages "^6.23.0" + babel-register "^6.26.0" + babel-runtime "^6.26.0" + babel-template "^6.26.0" + babel-traverse "^6.26.0" + babel-types "^6.26.0" + babylon "^6.18.0" + convert-source-map "^1.5.0" + debug "^2.6.8" + json5 "^0.5.1" + lodash "^4.17.4" + minimatch "^3.0.4" + path-is-absolute "^1.0.1" + private "^0.1.7" + slash "^1.0.0" + source-map "^0.5.6" + +babel-eslint@8.0.1: + version "8.0.1" + resolved "https://registry.yarnpkg.com/babel-eslint/-/babel-eslint-8.0.1.tgz#5d718be7a328625d006022eb293ed3008cbd6346" + dependencies: + babel-code-frame "7.0.0-beta.0" + babel-traverse "7.0.0-beta.0" + babel-types "7.0.0-beta.0" + babylon "7.0.0-beta.22" + +babel-generator@^6.26.0: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-generator/-/babel-generator-6.26.0.tgz#ac1ae20070b79f6e3ca1d3269613053774f20dc5" + dependencies: + babel-messages "^6.23.0" + babel-runtime "^6.26.0" + babel-types "^6.26.0" + detect-indent "^4.0.0" + jsesc "^1.3.0" + lodash "^4.17.4" + source-map "^0.5.6" + trim-right "^1.0.1" + +babel-helper-bindify-decorators@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-helper-bindify-decorators/-/babel-helper-bindify-decorators-6.24.1.tgz#14c19e5f142d7b47f19a52431e52b1ccbc40a330" + dependencies: + babel-runtime "^6.22.0" + babel-traverse "^6.24.1" + babel-types "^6.24.1" + +babel-helper-builder-binary-assignment-operator-visitor@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-helper-builder-binary-assignment-operator-visitor/-/babel-helper-builder-binary-assignment-operator-visitor-6.24.1.tgz#cce4517ada356f4220bcae8a02c2b346f9a56664" + dependencies: + babel-helper-explode-assignable-expression "^6.24.1" + babel-runtime "^6.22.0" + babel-types "^6.24.1" + +babel-helper-builder-react-jsx@^6.24.1: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-helper-builder-react-jsx/-/babel-helper-builder-react-jsx-6.26.0.tgz#39ff8313b75c8b65dceff1f31d383e0ff2a408a0" + dependencies: + babel-runtime "^6.26.0" + babel-types "^6.26.0" + esutils "^2.0.2" + +babel-helper-call-delegate@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-helper-call-delegate/-/babel-helper-call-delegate-6.24.1.tgz#ece6aacddc76e41c3461f88bfc575bd0daa2df8d" + dependencies: + babel-helper-hoist-variables "^6.24.1" + babel-runtime "^6.22.0" + babel-traverse "^6.24.1" + babel-types "^6.24.1" + +babel-helper-define-map@^6.24.1: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-helper-define-map/-/babel-helper-define-map-6.26.0.tgz#a5f56dab41a25f97ecb498c7ebaca9819f95be5f" + dependencies: + babel-helper-function-name "^6.24.1" + babel-runtime "^6.26.0" + babel-types "^6.26.0" + lodash "^4.17.4" + +babel-helper-explode-assignable-expression@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-helper-explode-assignable-expression/-/babel-helper-explode-assignable-expression-6.24.1.tgz#f25b82cf7dc10433c55f70592d5746400ac22caa" + dependencies: + babel-runtime "^6.22.0" + babel-traverse "^6.24.1" + babel-types "^6.24.1" + +babel-helper-explode-class@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-helper-explode-class/-/babel-helper-explode-class-6.24.1.tgz#7dc2a3910dee007056e1e31d640ced3d54eaa9eb" + dependencies: + babel-helper-bindify-decorators "^6.24.1" + babel-runtime "^6.22.0" + babel-traverse "^6.24.1" + babel-types "^6.24.1" + +babel-helper-function-name@7.0.0-beta.0: + version "7.0.0-beta.0" + resolved "https://registry.yarnpkg.com/babel-helper-function-name/-/babel-helper-function-name-7.0.0-beta.0.tgz#d1b6779b647e5c5c31ebeb05e13b998e4d352d56" + dependencies: + babel-helper-get-function-arity "7.0.0-beta.0" + babel-template "7.0.0-beta.0" + babel-traverse "7.0.0-beta.0" + babel-types "7.0.0-beta.0" + +babel-helper-function-name@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-helper-function-name/-/babel-helper-function-name-6.24.1.tgz#d3475b8c03ed98242a25b48351ab18399d3580a9" + dependencies: + babel-helper-get-function-arity "^6.24.1" + babel-runtime "^6.22.0" + babel-template "^6.24.1" + babel-traverse "^6.24.1" + babel-types "^6.24.1" + +babel-helper-get-function-arity@7.0.0-beta.0: + version "7.0.0-beta.0" + resolved "https://registry.yarnpkg.com/babel-helper-get-function-arity/-/babel-helper-get-function-arity-7.0.0-beta.0.tgz#9d1ab7213bb5efe1ef1638a8ea1489969b5a8b6e" + dependencies: + babel-types "7.0.0-beta.0" + +babel-helper-get-function-arity@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-helper-get-function-arity/-/babel-helper-get-function-arity-6.24.1.tgz#8f7782aa93407c41d3aa50908f89b031b1b6853d" + dependencies: + babel-runtime "^6.22.0" + babel-types "^6.24.1" + +babel-helper-hoist-variables@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-helper-hoist-variables/-/babel-helper-hoist-variables-6.24.1.tgz#1ecb27689c9d25513eadbc9914a73f5408be7a76" + dependencies: + babel-runtime "^6.22.0" + babel-types "^6.24.1" + +babel-helper-optimise-call-expression@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-helper-optimise-call-expression/-/babel-helper-optimise-call-expression-6.24.1.tgz#f7a13427ba9f73f8f4fa993c54a97882d1244257" + dependencies: + babel-runtime "^6.22.0" + babel-types "^6.24.1" + +babel-helper-regex@^6.24.1: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-helper-regex/-/babel-helper-regex-6.26.0.tgz#325c59f902f82f24b74faceed0363954f6495e72" + dependencies: + babel-runtime "^6.26.0" + babel-types "^6.26.0" + lodash "^4.17.4" + +babel-helper-remap-async-to-generator@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-helper-remap-async-to-generator/-/babel-helper-remap-async-to-generator-6.24.1.tgz#5ec581827ad723fecdd381f1c928390676e4551b" + dependencies: + babel-helper-function-name "^6.24.1" + babel-runtime "^6.22.0" + babel-template "^6.24.1" + babel-traverse "^6.24.1" + babel-types "^6.24.1" + +babel-helper-replace-supers@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-helper-replace-supers/-/babel-helper-replace-supers-6.24.1.tgz#bf6dbfe43938d17369a213ca8a8bf74b6a90ab1a" + dependencies: + babel-helper-optimise-call-expression "^6.24.1" + babel-messages "^6.23.0" + babel-runtime "^6.22.0" + babel-template "^6.24.1" + babel-traverse "^6.24.1" + babel-types "^6.24.1" + +babel-helpers@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-helpers/-/babel-helpers-6.24.1.tgz#3471de9caec388e5c850e597e58a26ddf37602b2" + dependencies: + babel-runtime "^6.22.0" + babel-template "^6.24.1" + +babel-loader@7.1.2: + version "7.1.2" + resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-7.1.2.tgz#f6cbe122710f1aa2af4d881c6d5b54358ca24126" + dependencies: + find-cache-dir "^1.0.0" + loader-utils "^1.0.2" + mkdirp "^0.5.1" + +babel-messages@7.0.0-beta.0: + version "7.0.0-beta.0" + resolved "https://registry.yarnpkg.com/babel-messages/-/babel-messages-7.0.0-beta.0.tgz#6df01296e49fc8fbd0637394326a167f36da817b" + +babel-messages@^6.23.0: + version "6.23.0" + resolved "https://registry.yarnpkg.com/babel-messages/-/babel-messages-6.23.0.tgz#f3cdf4703858035b2a2951c6ec5edf6c62f2630e" + dependencies: + babel-runtime "^6.22.0" + +babel-plugin-check-es2015-constants@^6.22.0: + version "6.22.0" + resolved "https://registry.yarnpkg.com/babel-plugin-check-es2015-constants/-/babel-plugin-check-es2015-constants-6.22.0.tgz#35157b101426fd2ffd3da3f75c7d1e91835bbf8a" + dependencies: + babel-runtime "^6.22.0" + +babel-plugin-syntax-async-functions@^6.8.0: + version "6.13.0" + resolved "https://registry.yarnpkg.com/babel-plugin-syntax-async-functions/-/babel-plugin-syntax-async-functions-6.13.0.tgz#cad9cad1191b5ad634bf30ae0872391e0647be95" + +babel-plugin-syntax-async-generators@^6.5.0: + version "6.13.0" + resolved "https://registry.yarnpkg.com/babel-plugin-syntax-async-generators/-/babel-plugin-syntax-async-generators-6.13.0.tgz#6bc963ebb16eccbae6b92b596eb7f35c342a8b9a" + +babel-plugin-syntax-class-properties@^6.8.0: + version "6.13.0" + resolved "https://registry.yarnpkg.com/babel-plugin-syntax-class-properties/-/babel-plugin-syntax-class-properties-6.13.0.tgz#d7eb23b79a317f8543962c505b827c7d6cac27de" + +babel-plugin-syntax-decorators@^6.1.18, babel-plugin-syntax-decorators@^6.13.0: + version "6.13.0" + resolved "https://registry.yarnpkg.com/babel-plugin-syntax-decorators/-/babel-plugin-syntax-decorators-6.13.0.tgz#312563b4dbde3cc806cee3e416cceeaddd11ac0b" + +babel-plugin-syntax-dynamic-import@^6.18.0: + version "6.18.0" + resolved "https://registry.yarnpkg.com/babel-plugin-syntax-dynamic-import/-/babel-plugin-syntax-dynamic-import-6.18.0.tgz#8d6a26229c83745a9982a441051572caa179b1da" + +babel-plugin-syntax-exponentiation-operator@^6.8.0: + version "6.13.0" + resolved "https://registry.yarnpkg.com/babel-plugin-syntax-exponentiation-operator/-/babel-plugin-syntax-exponentiation-operator-6.13.0.tgz#9ee7e8337290da95288201a6a57f4170317830de" + +babel-plugin-syntax-flow@^6.18.0: + version "6.18.0" + resolved "https://registry.yarnpkg.com/babel-plugin-syntax-flow/-/babel-plugin-syntax-flow-6.18.0.tgz#4c3ab20a2af26aa20cd25995c398c4eb70310c8d" + +babel-plugin-syntax-jsx@^6.3.13, babel-plugin-syntax-jsx@^6.8.0: + version "6.18.0" + resolved "https://registry.yarnpkg.com/babel-plugin-syntax-jsx/-/babel-plugin-syntax-jsx-6.18.0.tgz#0af32a9a6e13ca7a3fd5069e62d7b0f58d0d8946" + +babel-plugin-syntax-object-rest-spread@^6.8.0: + version "6.13.0" + resolved "https://registry.yarnpkg.com/babel-plugin-syntax-object-rest-spread/-/babel-plugin-syntax-object-rest-spread-6.13.0.tgz#fd6536f2bce13836ffa3a5458c4903a597bb3bf5" + +babel-plugin-syntax-trailing-function-commas@^6.22.0: + version "6.22.0" + resolved "https://registry.yarnpkg.com/babel-plugin-syntax-trailing-function-commas/-/babel-plugin-syntax-trailing-function-commas-6.22.0.tgz#ba0360937f8d06e40180a43fe0d5616fff532cf3" + +babel-plugin-transform-async-generator-functions@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-async-generator-functions/-/babel-plugin-transform-async-generator-functions-6.24.1.tgz#f058900145fd3e9907a6ddf28da59f215258a5db" + dependencies: + babel-helper-remap-async-to-generator "^6.24.1" + babel-plugin-syntax-async-generators "^6.5.0" + babel-runtime "^6.22.0" + +babel-plugin-transform-async-to-generator@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-async-to-generator/-/babel-plugin-transform-async-to-generator-6.24.1.tgz#6536e378aff6cb1d5517ac0e40eb3e9fc8d08761" + dependencies: + babel-helper-remap-async-to-generator "^6.24.1" + babel-plugin-syntax-async-functions "^6.8.0" + babel-runtime "^6.22.0" + +babel-plugin-transform-class-properties@6.24.1, babel-plugin-transform-class-properties@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-class-properties/-/babel-plugin-transform-class-properties-6.24.1.tgz#6a79763ea61d33d36f37b611aa9def81a81b46ac" + dependencies: + babel-helper-function-name "^6.24.1" + babel-plugin-syntax-class-properties "^6.8.0" + babel-runtime "^6.22.0" + babel-template "^6.24.1" + +babel-plugin-transform-decorators-legacy@^1.3.4: + version "1.3.4" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-decorators-legacy/-/babel-plugin-transform-decorators-legacy-1.3.4.tgz#741b58f6c5bce9e6027e0882d9c994f04f366925" + dependencies: + babel-plugin-syntax-decorators "^6.1.18" + babel-runtime "^6.2.0" + babel-template "^6.3.0" + +babel-plugin-transform-decorators@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-decorators/-/babel-plugin-transform-decorators-6.24.1.tgz#788013d8f8c6b5222bdf7b344390dfd77569e24d" + dependencies: + babel-helper-explode-class "^6.24.1" + babel-plugin-syntax-decorators "^6.13.0" + babel-runtime "^6.22.0" + babel-template "^6.24.1" + babel-types "^6.24.1" + +babel-plugin-transform-es2015-arrow-functions@^6.22.0: + version "6.22.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-arrow-functions/-/babel-plugin-transform-es2015-arrow-functions-6.22.0.tgz#452692cb711d5f79dc7f85e440ce41b9f244d221" + dependencies: + babel-runtime "^6.22.0" + +babel-plugin-transform-es2015-block-scoped-functions@^6.22.0: + version "6.22.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-block-scoped-functions/-/babel-plugin-transform-es2015-block-scoped-functions-6.22.0.tgz#bbc51b49f964d70cb8d8e0b94e820246ce3a6141" + dependencies: + babel-runtime "^6.22.0" + +babel-plugin-transform-es2015-block-scoping@^6.24.1: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-block-scoping/-/babel-plugin-transform-es2015-block-scoping-6.26.0.tgz#d70f5299c1308d05c12f463813b0a09e73b1895f" + dependencies: + babel-runtime "^6.26.0" + babel-template "^6.26.0" + babel-traverse "^6.26.0" + babel-types "^6.26.0" + lodash "^4.17.4" + +babel-plugin-transform-es2015-classes@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-classes/-/babel-plugin-transform-es2015-classes-6.24.1.tgz#5a4c58a50c9c9461e564b4b2a3bfabc97a2584db" + dependencies: + babel-helper-define-map "^6.24.1" + babel-helper-function-name "^6.24.1" + babel-helper-optimise-call-expression "^6.24.1" + babel-helper-replace-supers "^6.24.1" + babel-messages "^6.23.0" + babel-runtime "^6.22.0" + babel-template "^6.24.1" + babel-traverse "^6.24.1" + babel-types "^6.24.1" + +babel-plugin-transform-es2015-computed-properties@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-computed-properties/-/babel-plugin-transform-es2015-computed-properties-6.24.1.tgz#6fe2a8d16895d5634f4cd999b6d3480a308159b3" + dependencies: + babel-runtime "^6.22.0" + babel-template "^6.24.1" + +babel-plugin-transform-es2015-destructuring@^6.22.0: + version "6.23.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-destructuring/-/babel-plugin-transform-es2015-destructuring-6.23.0.tgz#997bb1f1ab967f682d2b0876fe358d60e765c56d" + dependencies: + babel-runtime "^6.22.0" + +babel-plugin-transform-es2015-duplicate-keys@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-duplicate-keys/-/babel-plugin-transform-es2015-duplicate-keys-6.24.1.tgz#73eb3d310ca969e3ef9ec91c53741a6f1576423e" + dependencies: + babel-runtime "^6.22.0" + babel-types "^6.24.1" + +babel-plugin-transform-es2015-for-of@^6.22.0: + version "6.23.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-for-of/-/babel-plugin-transform-es2015-for-of-6.23.0.tgz#f47c95b2b613df1d3ecc2fdb7573623c75248691" + dependencies: + babel-runtime "^6.22.0" + +babel-plugin-transform-es2015-function-name@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-function-name/-/babel-plugin-transform-es2015-function-name-6.24.1.tgz#834c89853bc36b1af0f3a4c5dbaa94fd8eacaa8b" + dependencies: + babel-helper-function-name "^6.24.1" + babel-runtime "^6.22.0" + babel-types "^6.24.1" + +babel-plugin-transform-es2015-literals@^6.22.0: + version "6.22.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-literals/-/babel-plugin-transform-es2015-literals-6.22.0.tgz#4f54a02d6cd66cf915280019a31d31925377ca2e" + dependencies: + babel-runtime "^6.22.0" + +babel-plugin-transform-es2015-modules-amd@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-amd/-/babel-plugin-transform-es2015-modules-amd-6.24.1.tgz#3b3e54017239842d6d19c3011c4bd2f00a00d154" + dependencies: + babel-plugin-transform-es2015-modules-commonjs "^6.24.1" + babel-runtime "^6.22.0" + babel-template "^6.24.1" + +babel-plugin-transform-es2015-modules-commonjs@^6.24.1: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-commonjs/-/babel-plugin-transform-es2015-modules-commonjs-6.26.0.tgz#0d8394029b7dc6abe1a97ef181e00758dd2e5d8a" + dependencies: + babel-plugin-transform-strict-mode "^6.24.1" + babel-runtime "^6.26.0" + babel-template "^6.26.0" + babel-types "^6.26.0" + +babel-plugin-transform-es2015-modules-systemjs@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-systemjs/-/babel-plugin-transform-es2015-modules-systemjs-6.24.1.tgz#ff89a142b9119a906195f5f106ecf305d9407d23" + dependencies: + babel-helper-hoist-variables "^6.24.1" + babel-runtime "^6.22.0" + babel-template "^6.24.1" + +babel-plugin-transform-es2015-modules-umd@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-umd/-/babel-plugin-transform-es2015-modules-umd-6.24.1.tgz#ac997e6285cd18ed6176adb607d602344ad38468" + dependencies: + babel-plugin-transform-es2015-modules-amd "^6.24.1" + babel-runtime "^6.22.0" + babel-template "^6.24.1" + +babel-plugin-transform-es2015-object-super@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-object-super/-/babel-plugin-transform-es2015-object-super-6.24.1.tgz#24cef69ae21cb83a7f8603dad021f572eb278f8d" + dependencies: + babel-helper-replace-supers "^6.24.1" + babel-runtime "^6.22.0" + +babel-plugin-transform-es2015-parameters@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-parameters/-/babel-plugin-transform-es2015-parameters-6.24.1.tgz#57ac351ab49caf14a97cd13b09f66fdf0a625f2b" + dependencies: + babel-helper-call-delegate "^6.24.1" + babel-helper-get-function-arity "^6.24.1" + babel-runtime "^6.22.0" + babel-template "^6.24.1" + babel-traverse "^6.24.1" + babel-types "^6.24.1" + +babel-plugin-transform-es2015-shorthand-properties@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-shorthand-properties/-/babel-plugin-transform-es2015-shorthand-properties-6.24.1.tgz#24f875d6721c87661bbd99a4622e51f14de38aa0" + dependencies: + babel-runtime "^6.22.0" + babel-types "^6.24.1" + +babel-plugin-transform-es2015-spread@^6.22.0: + version "6.22.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-spread/-/babel-plugin-transform-es2015-spread-6.22.0.tgz#d6d68a99f89aedc4536c81a542e8dd9f1746f8d1" + dependencies: + babel-runtime "^6.22.0" + +babel-plugin-transform-es2015-sticky-regex@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-sticky-regex/-/babel-plugin-transform-es2015-sticky-regex-6.24.1.tgz#00c1cdb1aca71112cdf0cf6126c2ed6b457ccdbc" + dependencies: + babel-helper-regex "^6.24.1" + babel-runtime "^6.22.0" + babel-types "^6.24.1" + +babel-plugin-transform-es2015-template-literals@^6.22.0: + version "6.22.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-template-literals/-/babel-plugin-transform-es2015-template-literals-6.22.0.tgz#a84b3450f7e9f8f1f6839d6d687da84bb1236d8d" + dependencies: + babel-runtime "^6.22.0" + +babel-plugin-transform-es2015-typeof-symbol@^6.22.0: + version "6.23.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-typeof-symbol/-/babel-plugin-transform-es2015-typeof-symbol-6.23.0.tgz#dec09f1cddff94b52ac73d505c84df59dcceb372" + dependencies: + babel-runtime "^6.22.0" + +babel-plugin-transform-es2015-unicode-regex@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-unicode-regex/-/babel-plugin-transform-es2015-unicode-regex-6.24.1.tgz#d38b12f42ea7323f729387f18a7c5ae1faeb35e9" + dependencies: + babel-helper-regex "^6.24.1" + babel-runtime "^6.22.0" + regexpu-core "^2.0.0" + +babel-plugin-transform-exponentiation-operator@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-exponentiation-operator/-/babel-plugin-transform-exponentiation-operator-6.24.1.tgz#2ab0c9c7f3098fa48907772bb813fe41e8de3a0e" + dependencies: + babel-helper-builder-binary-assignment-operator-visitor "^6.24.1" + babel-plugin-syntax-exponentiation-operator "^6.8.0" + babel-runtime "^6.22.0" + +babel-plugin-transform-flow-strip-types@^6.22.0: + version "6.22.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-flow-strip-types/-/babel-plugin-transform-flow-strip-types-6.22.0.tgz#84cb672935d43714fdc32bce84568d87441cf7cf" + dependencies: + babel-plugin-syntax-flow "^6.18.0" + babel-runtime "^6.22.0" + +babel-plugin-transform-object-rest-spread@^6.22.0: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-object-rest-spread/-/babel-plugin-transform-object-rest-spread-6.26.0.tgz#0f36692d50fef6b7e2d4b3ac1478137a963b7b06" + dependencies: + babel-plugin-syntax-object-rest-spread "^6.8.0" + babel-runtime "^6.26.0" + +babel-plugin-transform-react-display-name@^6.23.0: + version "6.25.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-react-display-name/-/babel-plugin-transform-react-display-name-6.25.0.tgz#67e2bf1f1e9c93ab08db96792e05392bf2cc28d1" + dependencies: + babel-runtime "^6.22.0" + +babel-plugin-transform-react-jsx-self@^6.22.0: + version "6.22.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-react-jsx-self/-/babel-plugin-transform-react-jsx-self-6.22.0.tgz#df6d80a9da2612a121e6ddd7558bcbecf06e636e" + dependencies: + babel-plugin-syntax-jsx "^6.8.0" + babel-runtime "^6.22.0" + +babel-plugin-transform-react-jsx-source@^6.22.0: + version "6.22.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-react-jsx-source/-/babel-plugin-transform-react-jsx-source-6.22.0.tgz#66ac12153f5cd2d17b3c19268f4bf0197f44ecd6" + dependencies: + babel-plugin-syntax-jsx "^6.8.0" + babel-runtime "^6.22.0" + +babel-plugin-transform-react-jsx@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-react-jsx/-/babel-plugin-transform-react-jsx-6.24.1.tgz#840a028e7df460dfc3a2d29f0c0d91f6376e66a3" + dependencies: + babel-helper-builder-react-jsx "^6.24.1" + babel-plugin-syntax-jsx "^6.8.0" + babel-runtime "^6.22.0" + +babel-plugin-transform-regenerator@^6.24.1: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-regenerator/-/babel-plugin-transform-regenerator-6.26.0.tgz#e0703696fbde27f0a3efcacf8b4dca2f7b3a8f2f" + dependencies: + regenerator-transform "^0.10.0" + +babel-plugin-transform-strict-mode@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-strict-mode/-/babel-plugin-transform-strict-mode-6.24.1.tgz#d5faf7aa578a65bbe591cf5edae04a0c67020758" + dependencies: + babel-runtime "^6.22.0" + babel-types "^6.24.1" + +babel-preset-decorators-legacy@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/babel-preset-decorators-legacy/-/babel-preset-decorators-legacy-1.0.0.tgz#87772ec5303c5a3b748ce450c8400975662d1731" + dependencies: + babel-plugin-transform-decorators-legacy "^1.3.4" + +babel-preset-es2015@6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-preset-es2015/-/babel-preset-es2015-6.24.1.tgz#d44050d6bc2c9feea702aaf38d727a0210538939" + dependencies: + babel-plugin-check-es2015-constants "^6.22.0" + babel-plugin-transform-es2015-arrow-functions "^6.22.0" + babel-plugin-transform-es2015-block-scoped-functions "^6.22.0" + babel-plugin-transform-es2015-block-scoping "^6.24.1" + babel-plugin-transform-es2015-classes "^6.24.1" + babel-plugin-transform-es2015-computed-properties "^6.24.1" + babel-plugin-transform-es2015-destructuring "^6.22.0" + babel-plugin-transform-es2015-duplicate-keys "^6.24.1" + babel-plugin-transform-es2015-for-of "^6.22.0" + babel-plugin-transform-es2015-function-name "^6.24.1" + babel-plugin-transform-es2015-literals "^6.22.0" + babel-plugin-transform-es2015-modules-amd "^6.24.1" + babel-plugin-transform-es2015-modules-commonjs "^6.24.1" + babel-plugin-transform-es2015-modules-systemjs "^6.24.1" + babel-plugin-transform-es2015-modules-umd "^6.24.1" + babel-plugin-transform-es2015-object-super "^6.24.1" + babel-plugin-transform-es2015-parameters "^6.24.1" + babel-plugin-transform-es2015-shorthand-properties "^6.24.1" + babel-plugin-transform-es2015-spread "^6.22.0" + babel-plugin-transform-es2015-sticky-regex "^6.24.1" + babel-plugin-transform-es2015-template-literals "^6.22.0" + babel-plugin-transform-es2015-typeof-symbol "^6.22.0" + babel-plugin-transform-es2015-unicode-regex "^6.24.1" + babel-plugin-transform-regenerator "^6.24.1" + +babel-preset-flow@^6.23.0: + version "6.23.0" + resolved "https://registry.yarnpkg.com/babel-preset-flow/-/babel-preset-flow-6.23.0.tgz#e71218887085ae9a24b5be4169affb599816c49d" + dependencies: + babel-plugin-transform-flow-strip-types "^6.22.0" + +babel-preset-react@6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-preset-react/-/babel-preset-react-6.24.1.tgz#ba69dfaea45fc3ec639b6a4ecea6e17702c91380" + dependencies: + babel-plugin-syntax-jsx "^6.3.13" + babel-plugin-transform-react-display-name "^6.23.0" + babel-plugin-transform-react-jsx "^6.24.1" + babel-plugin-transform-react-jsx-self "^6.22.0" + babel-plugin-transform-react-jsx-source "^6.22.0" + babel-preset-flow "^6.23.0" + +babel-preset-stage-2@6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-preset-stage-2/-/babel-preset-stage-2-6.24.1.tgz#d9e2960fb3d71187f0e64eec62bc07767219bdc1" + dependencies: + babel-plugin-syntax-dynamic-import "^6.18.0" + babel-plugin-transform-class-properties "^6.24.1" + babel-plugin-transform-decorators "^6.24.1" + babel-preset-stage-3 "^6.24.1" + +babel-preset-stage-3@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-preset-stage-3/-/babel-preset-stage-3-6.24.1.tgz#836ada0a9e7a7fa37cb138fb9326f87934a48395" + dependencies: + babel-plugin-syntax-trailing-function-commas "^6.22.0" + babel-plugin-transform-async-generator-functions "^6.24.1" + babel-plugin-transform-async-to-generator "^6.24.1" + babel-plugin-transform-exponentiation-operator "^6.24.1" + babel-plugin-transform-object-rest-spread "^6.22.0" + +babel-register@^6.26.0: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-register/-/babel-register-6.26.0.tgz#6ed021173e2fcb486d7acb45c6009a856f647071" + dependencies: + babel-core "^6.26.0" + babel-runtime "^6.26.0" + core-js "^2.5.0" + home-or-tmp "^2.0.0" + lodash "^4.17.4" + mkdirp "^0.5.1" + source-map-support "^0.4.15" + +babel-runtime@^6.18.0, babel-runtime@^6.2.0, babel-runtime@^6.22.0, babel-runtime@^6.23.0, babel-runtime@^6.26.0: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.26.0.tgz#965c7058668e82b55d7bfe04ff2337bc8b5647fe" + dependencies: + core-js "^2.4.0" + regenerator-runtime "^0.11.0" + +babel-template@7.0.0-beta.0: + version "7.0.0-beta.0" + resolved "https://registry.yarnpkg.com/babel-template/-/babel-template-7.0.0-beta.0.tgz#85083cf9e4395d5e48bf5154d7a8d6991cafecfb" + dependencies: + babel-traverse "7.0.0-beta.0" + babel-types "7.0.0-beta.0" + babylon "7.0.0-beta.22" + lodash "^4.2.0" + +babel-template@^6.24.1, babel-template@^6.26.0, babel-template@^6.3.0: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-template/-/babel-template-6.26.0.tgz#de03e2d16396b069f46dd9fff8521fb1a0e35e02" + dependencies: + babel-runtime "^6.26.0" + babel-traverse "^6.26.0" + babel-types "^6.26.0" + babylon "^6.18.0" + lodash "^4.17.4" + +babel-traverse@7.0.0-beta.0: + version "7.0.0-beta.0" + resolved "https://registry.yarnpkg.com/babel-traverse/-/babel-traverse-7.0.0-beta.0.tgz#da14be9b762f62a2f060db464eaafdd8cd072a41" + dependencies: + babel-code-frame "7.0.0-beta.0" + babel-helper-function-name "7.0.0-beta.0" + babel-messages "7.0.0-beta.0" + babel-types "7.0.0-beta.0" + babylon "7.0.0-beta.22" + debug "^3.0.1" + globals "^10.0.0" + invariant "^2.2.0" + lodash "^4.2.0" + +babel-traverse@^6.24.1, babel-traverse@^6.26.0, babel-traverse@^6.4.5, babel-traverse@^6.9.0: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-traverse/-/babel-traverse-6.26.0.tgz#46a9cbd7edcc62c8e5c064e2d2d8d0f4035766ee" + dependencies: + babel-code-frame "^6.26.0" + babel-messages "^6.23.0" + babel-runtime "^6.26.0" + babel-types "^6.26.0" + babylon "^6.18.0" + debug "^2.6.8" + globals "^9.18.0" + invariant "^2.2.2" + lodash "^4.17.4" + +babel-types@7.0.0-beta.0: + version "7.0.0-beta.0" + resolved "https://registry.yarnpkg.com/babel-types/-/babel-types-7.0.0-beta.0.tgz#eb8b6e556470e6dcc4aef982d79ad229469b5169" + dependencies: + esutils "^2.0.2" + lodash "^4.2.0" + to-fast-properties "^2.0.0" + +babel-types@^6.19.0, babel-types@^6.24.1, babel-types@^6.26.0: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-types/-/babel-types-6.26.0.tgz#a3b073f94ab49eb6fa55cd65227a334380632497" + dependencies: + babel-runtime "^6.26.0" + esutils "^2.0.2" + lodash "^4.17.4" + to-fast-properties "^1.0.3" + +babylon@7.0.0-beta.22: + version "7.0.0-beta.22" + resolved "https://registry.yarnpkg.com/babylon/-/babylon-7.0.0-beta.22.tgz#74f0ad82ed7c7c3cfeab74cf684f815104161b65" + +babylon@^6.18.0, babylon@^6.8.0: + version "6.18.0" + resolved "https://registry.yarnpkg.com/babylon/-/babylon-6.18.0.tgz#af2f3b88fa6f5c1e4c634d1a0f8eac4f55b395e3" + +balanced-match@^0.4.2: + version "0.4.2" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-0.4.2.tgz#cb3f3e3c732dc0f01ee70b403f302e61d7709838" + +balanced-match@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" + +base64-js@^1.0.2: + version "1.2.1" + resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.2.1.tgz#a91947da1f4a516ea38e5b4ec0ec3773675e0886" + +bcrypt-pbkdf@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz#63bc5dcb61331b92bc05fd528953c33462a06f8d" + dependencies: + tweetnacl "^0.14.3" + +beeper@^1.0.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/beeper/-/beeper-1.1.1.tgz#e6d5ea8c5dad001304a70b22638447f69cb2f809" + +big.js@^3.1.3: + version "3.2.0" + resolved "https://registry.yarnpkg.com/big.js/-/big.js-3.2.0.tgz#a5fc298b81b9e0dca2e458824784b65c52ba588e" + +binary-extensions@^1.0.0: + version "1.10.0" + resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.10.0.tgz#9aeb9a6c5e88638aad171e167f5900abe24835d0" + +bindings@^1.2.1: + version "1.3.0" + resolved "https://registry.yarnpkg.com/bindings/-/bindings-1.3.0.tgz#b346f6ecf6a95f5a815c5839fc7cdb22502f1ed7" + +bl@^1.1.2: + version "1.2.1" + resolved "https://registry.yarnpkg.com/bl/-/bl-1.2.1.tgz#cac328f7bee45730d404b692203fcb590e172d5e" + dependencies: + readable-stream "^2.0.5" + +block-stream@*: + version "0.0.9" + resolved "https://registry.yarnpkg.com/block-stream/-/block-stream-0.0.9.tgz#13ebfe778a03205cfe03751481ebb4b3300c126a" + dependencies: + inherits "~2.0.0" + +bluebird@^2.9.34: + version "2.11.0" + resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-2.11.0.tgz#534b9033c022c9579c56ba3b3e5a5caafbb650e1" + +bluebird@^3.1.1: + version "3.5.0" + resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.0.tgz#791420d7f551eea2897453a8a77653f96606d67c" + +bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.1.1, bn.js@^4.4.0: + version "4.11.8" + resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.8.tgz#2cde09eb5ee341f484746bb0309b3253b1b1442f" + +body-parser@~1.14.0: + version "1.14.2" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.14.2.tgz#1015cb1fe2c443858259581db53332f8d0cf50f9" + dependencies: + bytes "2.2.0" + content-type "~1.0.1" + debug "~2.2.0" + depd "~1.1.0" + http-errors "~1.3.1" + iconv-lite "0.4.13" + on-finished "~2.3.0" + qs "5.2.0" + raw-body "~2.1.5" + type-is "~1.6.10" + +boom@4.x.x: + version "4.3.1" + resolved "https://registry.yarnpkg.com/boom/-/boom-4.3.1.tgz#4f8a3005cb4a7e3889f749030fd25b96e01d2e31" + dependencies: + hoek "4.x.x" + +boom@5.x.x: + version "5.2.0" + resolved "https://registry.yarnpkg.com/boom/-/boom-5.2.0.tgz#5dd9da6ee3a5f302077436290cb717d3f4a54e02" + dependencies: + hoek "4.x.x" + +brace-expansion@^1.0.0, brace-expansion@^1.1.7: + version "1.1.8" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.8.tgz#c07b211c7c952ec1f8efd51a77ef0d1d3990a292" + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + +braces@^1.8.2: + version "1.8.5" + resolved "https://registry.yarnpkg.com/braces/-/braces-1.8.5.tgz#ba77962e12dff969d6b76711e914b737857bf6a7" + dependencies: + expand-range "^1.8.1" + preserve "^0.2.0" + repeat-element "^1.1.2" + +brorand@^1.0.1: + version "1.1.0" + resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f" + +browserify-aes@^1.0.0, browserify-aes@^1.0.4: + version "1.0.8" + resolved "https://registry.yarnpkg.com/browserify-aes/-/browserify-aes-1.0.8.tgz#c8fa3b1b7585bb7ba77c5560b60996ddec6d5309" + dependencies: + buffer-xor "^1.0.3" + cipher-base "^1.0.0" + create-hash "^1.1.0" + evp_bytestokey "^1.0.3" + inherits "^2.0.1" + safe-buffer "^5.0.1" + +browserify-cipher@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/browserify-cipher/-/browserify-cipher-1.0.0.tgz#9988244874bf5ed4e28da95666dcd66ac8fc363a" + dependencies: + browserify-aes "^1.0.4" + browserify-des "^1.0.0" + evp_bytestokey "^1.0.0" + +browserify-des@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/browserify-des/-/browserify-des-1.0.0.tgz#daa277717470922ed2fe18594118a175439721dd" + dependencies: + cipher-base "^1.0.1" + des.js "^1.0.0" + inherits "^2.0.1" + +browserify-rsa@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/browserify-rsa/-/browserify-rsa-4.0.1.tgz#21e0abfaf6f2029cf2fafb133567a701d4135524" + dependencies: + bn.js "^4.1.0" + randombytes "^2.0.1" + +browserify-sign@^4.0.0: + version "4.0.4" + resolved "https://registry.yarnpkg.com/browserify-sign/-/browserify-sign-4.0.4.tgz#aa4eb68e5d7b658baa6bf6a57e630cbd7a93d298" + dependencies: + bn.js "^4.1.1" + browserify-rsa "^4.0.0" + create-hash "^1.1.0" + create-hmac "^1.1.2" + elliptic "^6.0.0" + inherits "^2.0.1" + parse-asn1 "^5.0.0" + +browserify-zlib@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/browserify-zlib/-/browserify-zlib-0.1.4.tgz#bb35f8a519f600e0fa6b8485241c979d0141fb2d" + dependencies: + pako "~0.2.0" + +browserslist@^1.3.6, browserslist@^1.5.2: + version "1.7.7" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-1.7.7.tgz#0bd76704258be829b2398bb50e4b62d1a166b0b9" + dependencies: + caniuse-db "^1.0.30000639" + electron-to-chromium "^1.2.7" + +browserslist@^2.4.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-2.4.0.tgz#693ee93d01e66468a6348da5498e011f578f87f8" + dependencies: + caniuse-lite "^1.0.30000718" + electron-to-chromium "^1.3.18" + +browserslist@^2.5.0: + version "2.5.0" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-2.5.0.tgz#0ea00d22813a4dfae5786485225a9c584b3ef37c" + dependencies: + caniuse-lite "^1.0.30000744" + electron-to-chromium "^1.3.24" + +browserslist@~1.3.1: + version "1.3.6" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-1.3.6.tgz#952ff48d56463d3b538f85ef2f8eaddfd284b133" + dependencies: + caniuse-db "^1.0.30000525" + +bser@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/bser/-/bser-2.0.0.tgz#9ac78d3ed5d915804fd87acb158bc797147a1719" + dependencies: + node-int64 "^0.4.0" + +buffer-xor@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/buffer-xor/-/buffer-xor-1.0.3.tgz#26e61ed1422fb70dd42e6e36729ed51d855fe8d9" + +buffer@^4.3.0: + version "4.9.1" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-4.9.1.tgz#6d1bb601b07a4efced97094132093027c95bc298" + dependencies: + base64-js "^1.0.2" + ieee754 "^1.1.4" + isarray "^1.0.0" + +bufferstreams@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/bufferstreams/-/bufferstreams-1.0.1.tgz#cfb1ad9568d3ba3cfe935ba9abdd952de88aab2a" + dependencies: + readable-stream "^1.0.33" + +builtin-modules@^1.0.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-1.1.1.tgz#270f076c5a72c02f5b65a47df94c5fe3a278892f" + +builtin-status-codes@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz#85982878e21b98e1c66425e03d0174788f569ee8" + +bytes@2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-2.2.0.tgz#fd35464a403f6f9117c2de3609ecff9cae000588" + +bytes@2.4.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-2.4.0.tgz#7d97196f9d5baf7f6935e25985549edd2a6c2339" + +caller-path@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/caller-path/-/caller-path-0.1.0.tgz#94085ef63581ecd3daa92444a8fe94e82577751f" + dependencies: + callsites "^0.2.0" + +callsites@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/callsites/-/callsites-0.2.0.tgz#afab96262910a7f33c19a5775825c69f34e350ca" + +camelcase-css@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/camelcase-css/-/camelcase-css-1.0.1.tgz#157c4238265f5cf94a1dffde86446552cbf3f705" + +camelcase-keys@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/camelcase-keys/-/camelcase-keys-2.1.0.tgz#308beeaffdf28119051efa1d932213c91b8f92e7" + dependencies: + camelcase "^2.0.0" + map-obj "^1.0.0" + +camelcase@^1.0.2: + version "1.2.1" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-1.2.1.tgz#9bb5304d2e0b56698b2c758b08a3eaa9daa58a39" + +camelcase@^2.0.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-2.1.1.tgz#7c1d16d679a1bbe59ca02cacecfb011e201f5a1f" + +camelcase@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-4.1.0.tgz#d545635be1e33c542649c69173e5de6acfae34dd" + +caniuse-api@^1.5.2: + version "1.6.1" + resolved "https://registry.yarnpkg.com/caniuse-api/-/caniuse-api-1.6.1.tgz#b534e7c734c4f81ec5fbe8aca2ad24354b962c6c" + dependencies: + browserslist "^1.3.6" + caniuse-db "^1.0.30000529" + lodash.memoize "^4.1.2" + lodash.uniq "^4.5.0" + +caniuse-db@^1.0.30000444, caniuse-db@^1.0.30000525, caniuse-db@^1.0.30000529, caniuse-db@^1.0.30000639: + version "1.0.30000733" + resolved "https://registry.yarnpkg.com/caniuse-db/-/caniuse-db-1.0.30000733.tgz#3a625bc41c7a9f99d59d64552857dd1af0edd9d4" + +caniuse-lite@^1.0.30000718, caniuse-lite@^1.0.30000726: + version "1.0.30000733" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30000733.tgz#ebfc48254117cc0c66197a4536cb4397a6cfbccd" + +caniuse-lite@^1.0.30000744: + version "1.0.30000744" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30000744.tgz#860fa5c83ba34fe619397d607f30bb474821671b" + +caseless@~0.12.0: + version "0.12.0" + resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" + +center-align@^0.1.1: + version "0.1.3" + resolved "https://registry.yarnpkg.com/center-align/-/center-align-0.1.3.tgz#aa0d32629b6ee972200411cbd4461c907bc2b7ad" + dependencies: + align-text "^0.1.3" + lazy-cache "^1.0.3" + +chalk@^0.5.1: + version "0.5.1" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-0.5.1.tgz#663b3a648b68b55d04690d49167aa837858f2174" + dependencies: + ansi-styles "^1.1.0" + escape-string-regexp "^1.0.0" + has-ansi "^0.1.0" + strip-ansi "^0.3.0" + supports-color "^0.2.0" + +chalk@^1.0.0, chalk@^1.1.1, chalk@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98" + dependencies: + ansi-styles "^2.2.1" + escape-string-regexp "^1.0.2" + has-ansi "^2.0.0" + strip-ansi "^3.0.0" + supports-color "^2.0.0" + +chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.1.0.tgz#ac5becf14fa21b99c6c92ca7a7d7cfd5b17e743e" + dependencies: + ansi-styles "^3.1.0" + escape-string-regexp "^1.0.5" + supports-color "^4.0.0" + +chokidar@^1.6.1, chokidar@^1.7.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-1.7.0.tgz#798e689778151c8076b4b360e5edd28cda2bb468" + dependencies: + anymatch "^1.3.0" + async-each "^1.0.0" + glob-parent "^2.0.0" + inherits "^2.0.1" + is-binary-path "^1.0.0" + is-glob "^2.0.0" + path-is-absolute "^1.0.0" + readdirp "^2.0.0" + optionalDependencies: + fsevents "^1.0.0" + +cipher-base@^1.0.0, cipher-base@^1.0.1, cipher-base@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/cipher-base/-/cipher-base-1.0.4.tgz#8760e4ecc272f4c363532f926d874aae2c1397de" + dependencies: + inherits "^2.0.1" + safe-buffer "^5.0.1" + +circular-json@^0.3.1: + version "0.3.3" + resolved "https://registry.yarnpkg.com/circular-json/-/circular-json-0.3.3.tgz#815c99ea84f6809529d2f45791bdf82711352d66" + +clap@^1.0.9: + version "1.2.2" + resolved "https://registry.yarnpkg.com/clap/-/clap-1.2.2.tgz#683f6f93a320794d129386d74b2a1d2d66fede7e" + dependencies: + chalk "^1.1.3" + +classnames@2.2.5, classnames@^2.2.0, classnames@^2.2.3: + version "2.2.5" + resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.2.5.tgz#fb3801d453467649ef3603c7d61a02bd129bde6d" + +clean-css@4.1.9: + version "4.1.9" + resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-4.1.9.tgz#35cee8ae7687a49b98034f70de00c4edd3826301" + dependencies: + source-map "0.5.x" + +cli-cursor@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-2.1.0.tgz#b35dac376479facc3e94747d41d0d0f5238ffcb5" + dependencies: + restore-cursor "^2.0.0" + +cli-width@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-2.2.0.tgz#ff19ede8a9a5e579324147b0c11f0fbcbabed639" + +clipboard@1.7.1: + version "1.7.1" + resolved "https://registry.yarnpkg.com/clipboard/-/clipboard-1.7.1.tgz#360d6d6946e99a7a1fef395e42ba92b5e9b5a16b" + dependencies: + good-listener "^1.2.2" + select "^1.1.2" + tiny-emitter "^2.0.0" + +cliui@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-2.1.0.tgz#4b475760ff80264c762c3a1719032e91c7fea0d1" + dependencies: + center-align "^0.1.1" + right-align "^0.1.1" + wordwrap "0.0.2" + +cliui@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-3.2.0.tgz#120601537a916d29940f934da3b48d585a39213d" + dependencies: + string-width "^1.0.1" + strip-ansi "^3.0.1" + wrap-ansi "^2.0.0" + +clone-buffer@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/clone-buffer/-/clone-buffer-1.0.0.tgz#e3e25b207ac4e701af721e2cb5a16792cac3dc58" + +clone-regexp@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/clone-regexp/-/clone-regexp-1.0.0.tgz#eae0a2413f55c0942f818c229fefce845d7f3b1c" + dependencies: + is-regexp "^1.0.0" + is-supported-regexp-flag "^1.0.0" + +clone-stats@^0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/clone-stats/-/clone-stats-0.0.1.tgz#b88f94a82cf38b8791d58046ea4029ad88ca99d1" + +clone-stats@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/clone-stats/-/clone-stats-1.0.0.tgz#b3782dff8bb5474e18b9b6bf0fdfe782f8777680" + +clone@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/clone/-/clone-0.2.0.tgz#c6126a90ad4f72dbf5acdb243cc37724fe93fc1f" + +clone@^1.0.0, clone@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.2.tgz#260b7a99ebb1edfe247538175f783243cb19d149" + +clone@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/clone/-/clone-2.1.1.tgz#d217d1e961118e3ac9a4b8bba3285553bf647cdb" + +cloneable-readable@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/cloneable-readable/-/cloneable-readable-1.0.0.tgz#a6290d413f217a61232f95e458ff38418cfb0117" + dependencies: + inherits "^2.0.1" + process-nextick-args "^1.0.6" + through2 "^2.0.1" + +co@^4.6.0: + version "4.6.0" + resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" + +coa@~1.0.1: + version "1.0.4" + resolved "https://registry.yarnpkg.com/coa/-/coa-1.0.4.tgz#a9ef153660d6a86a8bdec0289a5c684d217432fd" + dependencies: + q "^1.1.2" + +code-point-at@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77" + +color-convert@^1.3.0, color-convert@^1.9.0: + version "1.9.0" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.0.tgz#1accf97dd739b983bf994d56fec8f95853641b7a" + dependencies: + color-name "^1.1.1" + +color-name@^1.0.0, color-name@^1.1.1: + version "1.1.3" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" + +color-string@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/color-string/-/color-string-0.3.0.tgz#27d46fb67025c5c2fa25993bfbf579e47841b991" + dependencies: + color-name "^1.0.0" + +color@^0.11.0: + version "0.11.4" + resolved "https://registry.yarnpkg.com/color/-/color-0.11.4.tgz#6d7b5c74fb65e841cd48792ad1ed5e07b904d764" + dependencies: + clone "^1.0.2" + color-convert "^1.3.0" + color-string "^0.3.0" + +colormin@^1.0.5: + version "1.1.2" + resolved "https://registry.yarnpkg.com/colormin/-/colormin-1.1.2.tgz#ea2f7420a72b96881a38aae59ec124a6f7298133" + dependencies: + color "^0.11.0" + css-color-names "0.0.4" + has "^1.0.1" + +colors@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/colors/-/colors-1.1.2.tgz#168a4701756b6a7f51a12ce0c97bfa28c084ed63" + +combined-stream@^1.0.5, combined-stream@~1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.5.tgz#938370a57b4a51dea2c77c15d5c5fdf895164009" + dependencies: + delayed-stream "~1.0.0" + +commander@^2.2.0, commander@^2.8.1: + version "2.11.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.11.0.tgz#157152fd1e7a6c8d98a5b715cf376df928004563" + +commondir@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b" + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + +concat-stream@^1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.0.tgz#0aac662fd52be78964d5532f694784e70110acf7" + dependencies: + inherits "^2.0.3" + readable-stream "^2.2.2" + typedarray "^0.0.6" + +concat-with-sourcemaps@^1.0.0: + version "1.0.4" + resolved "https://registry.yarnpkg.com/concat-with-sourcemaps/-/concat-with-sourcemaps-1.0.4.tgz#f55b3be2aeb47601b10a2d5259ccfb70fd2f1dd6" + dependencies: + source-map "^0.5.1" + +console-browserify@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/console-browserify/-/console-browserify-1.1.0.tgz#f0241c45730a9fc6323b206dbf38edc741d0bb10" + dependencies: + date-now "^0.1.4" + +console-control-strings@^1.0.0, console-control-strings@~1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e" + +consolidate@^0.14.1: + version "0.14.5" + resolved "https://registry.yarnpkg.com/consolidate/-/consolidate-0.14.5.tgz#5a25047bc76f73072667c8cb52c989888f494c63" + dependencies: + bluebird "^3.1.1" + +constants-browserify@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/constants-browserify/-/constants-browserify-1.0.0.tgz#c20b96d8c617748aaf1c16021760cd27fcb8cb75" + +content-type@~1.0.1: + version "1.0.4" + resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b" + +convert-source-map@1.X, convert-source-map@^1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.5.0.tgz#9acd70851c6d5dfdd93d9282e5edf94a03ff46b5" + +core-js@^1.0.0: + version "1.2.7" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-1.2.7.tgz#652294c14651db28fa93bd2d5ff2983a4f08c636" + +core-js@^2.4.0, core-js@^2.5.0: + version "2.5.1" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.5.1.tgz#ae6874dc66937789b80754ff5428df66819ca50b" + +core-util-is@1.0.2, core-util-is@~1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" + +cosmiconfig@^2.1.0, cosmiconfig@^2.1.1: + version "2.2.2" + resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-2.2.2.tgz#6173cebd56fac042c1f4390edf7af6c07c7cb892" + dependencies: + is-directory "^0.3.1" + js-yaml "^3.4.3" + minimist "^1.2.0" + object-assign "^4.1.0" + os-homedir "^1.0.1" + parse-json "^2.2.0" + require-from-string "^1.1.0" + +cosmiconfig@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-3.1.0.tgz#640a94bf9847f321800403cd273af60665c73397" + dependencies: + is-directory "^0.3.1" + js-yaml "^3.9.0" + parse-json "^3.0.0" + require-from-string "^2.0.1" + +create-ecdh@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/create-ecdh/-/create-ecdh-4.0.0.tgz#888c723596cdf7612f6498233eebd7a35301737d" + dependencies: + bn.js "^4.1.0" + elliptic "^6.0.0" + +create-hash@^1.1.0, create-hash@^1.1.2: + version "1.1.3" + resolved "https://registry.yarnpkg.com/create-hash/-/create-hash-1.1.3.tgz#606042ac8b9262750f483caddab0f5819172d8fd" + dependencies: + cipher-base "^1.0.1" + inherits "^2.0.1" + ripemd160 "^2.0.0" + sha.js "^2.4.0" + +create-hmac@^1.1.0, create-hmac@^1.1.2, create-hmac@^1.1.4: + version "1.1.6" + resolved "https://registry.yarnpkg.com/create-hmac/-/create-hmac-1.1.6.tgz#acb9e221a4e17bdb076e90657c42b93e3726cf06" + dependencies: + cipher-base "^1.0.3" + create-hash "^1.1.0" + inherits "^2.0.1" + ripemd160 "^2.0.0" + safe-buffer "^5.0.1" + sha.js "^2.4.8" + +create-react-class@^15.5.2, create-react-class@^15.6.2: + version "15.6.2" + resolved "https://registry.yarnpkg.com/create-react-class/-/create-react-class-15.6.2.tgz#cf1ed15f12aad7f14ef5f2dfe05e6c42f91ef02a" + dependencies: + fbjs "^0.8.9" + loose-envify "^1.3.1" + object-assign "^4.1.1" + +cross-spawn@^5.0.1, cross-spawn@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-5.1.0.tgz#e8bd0efee58fcff6f8f94510a0a554bbfa235449" + dependencies: + lru-cache "^4.0.1" + shebang-command "^1.2.0" + which "^1.2.9" + +cryptiles@3.x.x: + version "3.1.2" + resolved "https://registry.yarnpkg.com/cryptiles/-/cryptiles-3.1.2.tgz#a89fbb220f5ce25ec56e8c4aa8a4fd7b5b0d29fe" + dependencies: + boom "5.x.x" + +crypto-browserify@^3.11.0: + version "3.11.1" + resolved "https://registry.yarnpkg.com/crypto-browserify/-/crypto-browserify-3.11.1.tgz#948945efc6757a400d6e5e5af47194d10064279f" + dependencies: + browserify-cipher "^1.0.0" + browserify-sign "^4.0.0" + create-ecdh "^4.0.0" + create-hash "^1.1.0" + create-hmac "^1.1.0" + diffie-hellman "^5.0.0" + inherits "^2.0.1" + pbkdf2 "^3.0.3" + public-encrypt "^4.0.0" + randombytes "^2.0.0" + +css-color-names@0.0.4: + version "0.0.4" + resolved "https://registry.yarnpkg.com/css-color-names/-/css-color-names-0.0.4.tgz#808adc2e79cf84738069b646cb20ec27beb629e0" + +css-loader@0.28.7: + version "0.28.7" + resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-0.28.7.tgz#5f2ee989dd32edd907717f953317656160999c1b" + dependencies: + babel-code-frame "^6.11.0" + css-selector-tokenizer "^0.7.0" + cssnano ">=2.6.1 <4" + icss-utils "^2.1.0" + loader-utils "^1.0.2" + lodash.camelcase "^4.3.0" + object-assign "^4.0.1" + postcss "^5.0.6" + postcss-modules-extract-imports "^1.0.0" + postcss-modules-local-by-default "^1.0.1" + postcss-modules-scope "^1.0.0" + postcss-modules-values "^1.1.0" + postcss-value-parser "^3.3.0" + source-list-map "^2.0.0" + +css-selector-tokenizer@^0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/css-selector-tokenizer/-/css-selector-tokenizer-0.7.0.tgz#e6988474ae8c953477bf5e7efecfceccd9cf4c86" + dependencies: + cssesc "^0.1.0" + fastparse "^1.1.1" + regexpu-core "^1.0.0" + +css@2.X, css@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/css/-/css-2.2.1.tgz#73a4c81de85db664d4ee674f7d47085e3b2d55dc" + dependencies: + inherits "^2.0.1" + source-map "^0.1.38" + source-map-resolve "^0.3.0" + urix "^0.1.0" + +cssesc@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-0.1.0.tgz#c814903e45623371a0477b40109aaafbeeaddbb4" + +"cssnano@>=2.6.1 <4": + version "3.10.0" + resolved "https://registry.yarnpkg.com/cssnano/-/cssnano-3.10.0.tgz#4f38f6cea2b9b17fa01490f23f1dc68ea65c1c38" + dependencies: + autoprefixer "^6.3.1" + decamelize "^1.1.2" + defined "^1.0.0" + has "^1.0.1" + object-assign "^4.0.1" + postcss "^5.0.14" + postcss-calc "^5.2.0" + postcss-colormin "^2.1.8" + postcss-convert-values "^2.3.4" + postcss-discard-comments "^2.0.4" + postcss-discard-duplicates "^2.0.1" + postcss-discard-empty "^2.0.1" + postcss-discard-overridden "^0.1.1" + postcss-discard-unused "^2.2.1" + postcss-filter-plugins "^2.0.0" + postcss-merge-idents "^2.1.5" + postcss-merge-longhand "^2.0.1" + postcss-merge-rules "^2.0.3" + postcss-minify-font-values "^1.0.2" + postcss-minify-gradients "^1.0.1" + postcss-minify-params "^1.0.4" + postcss-minify-selectors "^2.0.4" + postcss-normalize-charset "^1.1.0" + postcss-normalize-url "^3.0.7" + postcss-ordered-values "^2.1.0" + postcss-reduce-idents "^2.2.2" + postcss-reduce-initial "^1.0.0" + postcss-reduce-transforms "^1.0.3" + postcss-svgo "^2.1.1" + postcss-unique-selectors "^2.0.2" + postcss-value-parser "^3.2.3" + postcss-zindex "^2.0.1" + +csso@~2.3.1: + version "2.3.2" + resolved "https://registry.yarnpkg.com/csso/-/csso-2.3.2.tgz#ddd52c587033f49e94b71fc55569f252e8ff5f85" + dependencies: + clap "^1.0.9" + source-map "^0.5.3" + +currently-unhandled@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/currently-unhandled/-/currently-unhandled-0.4.1.tgz#988df33feab191ef799a61369dd76c17adf957ea" + dependencies: + array-find-index "^1.0.1" + +d@1: + version "1.0.0" + resolved "https://registry.yarnpkg.com/d/-/d-1.0.0.tgz#754bb5bfe55451da69a58b94d45f4c5b0462d58f" + dependencies: + es5-ext "^0.10.9" + +dashdash@^1.12.0: + version "1.14.1" + resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0" + dependencies: + assert-plus "^1.0.0" + +date-now@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/date-now/-/date-now-0.1.4.tgz#eaf439fd4d4848ad74e5cc7dbef200672b9e345b" + +dateformat@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-2.0.0.tgz#2743e3abb5c3fc2462e527dca445e04e9f4dee17" + +debug-fabulous@>=0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/debug-fabulous/-/debug-fabulous-0.1.1.tgz#1b970878c9fa4fbd1c88306eab323c830c58f1d6" + dependencies: + debug "2.3.0" + memoizee "^0.4.5" + object-assign "4.1.0" + +debug@2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.3.0.tgz#3912dc55d7167fc3af17d2b85c13f93deaedaa43" + dependencies: + ms "0.7.2" + +debug@^0.7.4: + version "0.7.4" + resolved "https://registry.yarnpkg.com/debug/-/debug-0.7.4.tgz#06e1ea8082c2cb14e39806e22e2f6f757f92af39" + +debug@^2.1.0, debug@^2.1.3, debug@^2.2.0, debug@^2.6.8: + version "2.6.8" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.8.tgz#e731531ca2ede27d188222427da17821d68ff4fc" + dependencies: + ms "2.0.0" + +debug@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/debug/-/debug-3.0.1.tgz#0564c612b521dc92d9f2988f0549e34f9c98db64" + dependencies: + ms "2.0.0" + +debug@^3.0.1: + version "3.1.0" + resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" + dependencies: + ms "2.0.0" + +debug@~2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.2.0.tgz#f87057e995b1a1f6ae6a4960664137bc56f039da" + dependencies: + ms "0.7.1" + +decamelize@^1.0.0, decamelize@^1.1.1, decamelize@^1.1.2: + version "1.2.0" + resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" + +decode-uri-component@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545" + +deep-equal@~1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.0.1.tgz#f5d260292b660e084eff4cdbc9f08ad3247448b5" + +deep-extend@~0.4.0: + version "0.4.2" + resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.4.2.tgz#48b699c27e334bf89f10892be432f6e4c7d34a7f" + +deep-is@~0.1.3: + version "0.1.3" + resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34" + +defaults@^1.0.0: + version "1.0.3" + resolved "https://registry.yarnpkg.com/defaults/-/defaults-1.0.3.tgz#c656051e9817d9ff08ed881477f3fe4019f3ef7d" + dependencies: + clone "^1.0.2" + +define-properties@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.2.tgz#83a73f2fea569898fb737193c8f873caf6d45c94" + dependencies: + foreach "^2.0.5" + object-keys "^1.0.8" + +defined@^1.0.0, defined@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/defined/-/defined-1.0.0.tgz#c98d9bcef75674188e110969151199e39b1fa693" + +del@3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/del/-/del-3.0.0.tgz#53ecf699ffcbcb39637691ab13baf160819766e5" + dependencies: + globby "^6.1.0" + is-path-cwd "^1.0.0" + is-path-in-cwd "^1.0.0" + p-map "^1.1.1" + pify "^3.0.0" + rimraf "^2.2.8" + +del@^2.0.2: + version "2.2.0" + resolved "https://registry.yarnpkg.com/del/-/del-2.2.0.tgz#9a50f04bf37325e283b4f44e985336c252456bd5" + dependencies: + globby "^4.0.0" + is-path-cwd "^1.0.0" + is-path-in-cwd "^1.0.0" + object-assign "^4.0.1" + pify "^2.0.0" + pinkie-promise "^2.0.0" + rimraf "^2.2.8" + +delayed-stream@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" + +delegate@^3.1.2: + version "3.1.3" + resolved "https://registry.yarnpkg.com/delegate/-/delegate-3.1.3.tgz#9a8251a777d7025faa55737bc3b071742127a9fd" + +delegates@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a" + +depd@~1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.1.tgz#5783b4e1c459f06fa5ca27f991f3d06e7a310359" + +deprecated@^0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/deprecated/-/deprecated-0.0.1.tgz#f9c9af5464afa1e7a971458a8bdef2aa94d5bb19" + +des.js@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/des.js/-/des.js-1.0.0.tgz#c074d2e2aa6a8a9a07dbd61f9a15c2cd83ec8ecc" + dependencies: + inherits "^2.0.1" + minimalistic-assert "^1.0.0" + +detect-file@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/detect-file/-/detect-file-0.1.0.tgz#4935dedfd9488648e006b0129566e9386711ea63" + dependencies: + fs-exists-sync "^0.1.0" + +detect-indent@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-4.0.0.tgz#f76d064352cdf43a1cb6ce619c4ee3a9475de208" + dependencies: + repeating "^2.0.0" + +detect-newline@2.X: + version "2.1.0" + resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-2.1.0.tgz#f41f1c10be4b00e87b5f13da680759f2c5bfd3e2" + +diff@^1.3.2: + version "1.4.0" + resolved "https://registry.yarnpkg.com/diff/-/diff-1.4.0.tgz#7f28d2eb9ee7b15a97efd89ce63dcfdaa3ccbabf" + +diffie-hellman@^5.0.0: + version "5.0.2" + resolved "https://registry.yarnpkg.com/diffie-hellman/-/diffie-hellman-5.0.2.tgz#b5835739270cfe26acf632099fded2a07f209e5e" + dependencies: + bn.js "^4.1.0" + miller-rabin "^4.0.0" + randombytes "^2.0.0" + +disparity@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/disparity/-/disparity-2.0.0.tgz#57ddacb47324ae5f58d2cc0da886db4ce9eeb718" + dependencies: + ansi-styles "^2.0.1" + diff "^1.3.2" + +disposables@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/disposables/-/disposables-1.0.1.tgz#064727a25b54f502bd82b89aa2dfb8df9f1b39e3" + +dnd-core@^2.5.4: + version "2.5.4" + resolved "https://registry.yarnpkg.com/dnd-core/-/dnd-core-2.5.4.tgz#0c70a8dcbb609c0b222e275fcae9fa83e5897397" + dependencies: + asap "^2.0.6" + invariant "^2.0.0" + lodash "^4.2.0" + redux "^3.7.1" + +dnode-protocol@~0.2.2: + version "0.2.2" + resolved "https://registry.yarnpkg.com/dnode-protocol/-/dnode-protocol-0.2.2.tgz#51151d16fc3b5f84815ee0b9497a1061d0d1949d" + dependencies: + jsonify "~0.0.0" + traverse "~0.6.3" + +dnode@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/dnode/-/dnode-1.2.2.tgz#4ac3cfe26e292b3b39b8258ae7d94edc58132efa" + dependencies: + dnode-protocol "~0.2.2" + jsonify "~0.0.0" + optionalDependencies: + weak "^1.0.0" + +doctrine@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-2.0.0.tgz#c73d8d2909d22291e1a007a395804da8b665fe63" + dependencies: + esutils "^2.0.2" + isarray "^1.0.0" + +dom-css@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/dom-css/-/dom-css-2.1.0.tgz#fdbc2d5a015d0a3e1872e11472bbd0e7b9e6a202" + dependencies: + add-px-to-style "1.0.0" + prefix-style "2.0.1" + to-camel-case "1.0.0" + +"dom-helpers@^2.4.0 || ^3.0.0": + version "3.2.1" + resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-3.2.1.tgz#3203e07fed217bd1f424b019735582fc37b2825a" + +domain-browser@^1.1.1: + version "1.1.7" + resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.1.7.tgz#867aa4b093faa05f1de08c06f4d7b21fdf8698bc" + +duplexer2@0.0.2: + version "0.0.2" + resolved "https://registry.yarnpkg.com/duplexer2/-/duplexer2-0.0.2.tgz#c614dcf67e2fb14995a91711e5a617e8a60a31db" + dependencies: + readable-stream "~1.1.9" + +duplexer@~0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/duplexer/-/duplexer-0.1.1.tgz#ace6ff808c1ce66b57d1ebf97977acb02334cfc1" + +ecc-jsbn@~0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz#0fc73a9ed5f0d53c38193398523ef7e543777505" + dependencies: + jsbn "~0.1.0" + +ee-first@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" + +electron-to-chromium@^1.2.7, electron-to-chromium@^1.3.18: + version "1.3.21" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.21.tgz#a967ebdcfe8ed0083fc244d1894022a8e8113ea2" + +electron-to-chromium@^1.3.24: + version "1.3.24" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.24.tgz#9b7b88bb05ceb9fa016a177833cc2dde388f21b6" + +element-class@0.2.2: + version "0.2.2" + resolved "https://registry.yarnpkg.com/element-class/-/element-class-0.2.2.tgz#9d3bbd0767f9013ef8e1c8ebe722c1402a60050e" + +elliptic@^6.0.0: + version "6.4.0" + resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.4.0.tgz#cac9af8762c85836187003c8dfe193e5e2eae5df" + dependencies: + bn.js "^4.4.0" + brorand "^1.0.1" + hash.js "^1.0.0" + hmac-drbg "^1.0.0" + inherits "^2.0.1" + minimalistic-assert "^1.0.0" + minimalistic-crypto-utils "^1.0.0" + +emojis-list@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-2.1.0.tgz#4daa4d9db00f9819880c79fa457ae5b09a1fd389" + +encoding@^0.1.11: + version "0.1.12" + resolved "https://registry.yarnpkg.com/encoding/-/encoding-0.1.12.tgz#538b66f3ee62cd1ab51ec323829d1f9480c74beb" + dependencies: + iconv-lite "~0.4.13" + +end-of-stream@~0.1.5: + version "0.1.5" + resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-0.1.5.tgz#8e177206c3c80837d85632e8b9359dfe8b2f6eaf" + dependencies: + once "~1.3.0" + +enhanced-resolve@^3.4.0: + version "3.4.1" + resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-3.4.1.tgz#0421e339fd71419b3da13d129b3979040230476e" + dependencies: + graceful-fs "^4.1.2" + memory-fs "^0.4.0" + object-assign "^4.0.1" + tapable "^0.2.7" + +errno@^0.1.3, errno@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/errno/-/errno-0.1.4.tgz#b896e23a9e5e8ba33871fc996abd3635fc9a1c7d" + dependencies: + prr "~0.0.0" + +error-ex@^1.2.0, error-ex@^1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.1.tgz#f855a86ce61adc4e8621c3cda21e7a7612c3a8dc" + dependencies: + is-arrayish "^0.2.1" + +es-abstract@^1.5.0, es-abstract@^1.7.0: + version "1.8.2" + resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.8.2.tgz#25103263dc4decbda60e0c737ca32313518027ee" + dependencies: + es-to-primitive "^1.1.1" + function-bind "^1.1.1" + has "^1.0.1" + is-callable "^1.1.3" + is-regex "^1.0.4" + +es-to-primitive@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.1.1.tgz#45355248a88979034b6792e19bb81f2b7975dd0d" + dependencies: + is-callable "^1.1.1" + is-date-object "^1.0.1" + is-symbol "^1.0.1" + +es5-ext@^0.10.14, es5-ext@^0.10.30, es5-ext@^0.10.9, es5-ext@~0.10.14, es5-ext@~0.10.2: + version "0.10.30" + resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.30.tgz#7141a16836697dbabfaaaeee41495ce29f52c939" + dependencies: + es6-iterator "2" + es6-symbol "~3.1" + +es6-iterator@2, es6-iterator@^2.0.1, es6-iterator@~2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/es6-iterator/-/es6-iterator-2.0.1.tgz#8e319c9f0453bf575d374940a655920e59ca5512" + dependencies: + d "1" + es5-ext "^0.10.14" + es6-symbol "^3.1" + +es6-map@^0.1.3: + version "0.1.5" + resolved "https://registry.yarnpkg.com/es6-map/-/es6-map-0.1.5.tgz#9136e0503dcc06a301690f0bb14ff4e364e949f0" + dependencies: + d "1" + es5-ext "~0.10.14" + es6-iterator "~2.0.1" + es6-set "~0.1.5" + es6-symbol "~3.1.1" + event-emitter "~0.3.5" + +es6-promise@^3.1.2: + version "3.3.1" + resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-3.3.1.tgz#a08cdde84ccdbf34d027a1451bc91d4bcd28a613" + +es6-set@~0.1.5: + version "0.1.5" + resolved "https://registry.yarnpkg.com/es6-set/-/es6-set-0.1.5.tgz#d2b3ec5d4d800ced818db538d28974db0a73ccb1" + dependencies: + d "1" + es5-ext "~0.10.14" + es6-iterator "~2.0.1" + es6-symbol "3.1.1" + event-emitter "~0.3.5" + +es6-symbol@3.1.1, es6-symbol@^3.1, es6-symbol@^3.1.1, es6-symbol@~3.1, es6-symbol@~3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/es6-symbol/-/es6-symbol-3.1.1.tgz#bf00ef4fdab6ba1b46ecb7b629b4c7ed5715cc77" + dependencies: + d "1" + es5-ext "~0.10.14" + +es6-weak-map@^2.0.1, es6-weak-map@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/es6-weak-map/-/es6-weak-map-2.0.2.tgz#5e3ab32251ffd1538a1f8e5ffa1357772f92d96f" + dependencies: + d "1" + es5-ext "^0.10.14" + es6-iterator "^2.0.1" + es6-symbol "^3.1.1" + +escape-string-regexp@^1.0.0, escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" + +escope@^3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/escope/-/escope-3.6.0.tgz#e01975e812781a163a6dadfdd80398dc64c889c3" + dependencies: + es6-map "^0.1.3" + es6-weak-map "^2.0.1" + esrecurse "^4.1.0" + estraverse "^4.1.1" + +esformatter-parser@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/esformatter-parser/-/esformatter-parser-1.0.0.tgz#0854072d0487539ed39cae38d8a5432c17ec11d3" + dependencies: + acorn-to-esprima "^2.0.8" + babel-traverse "^6.9.0" + babylon "^6.8.0" + rocambole "^0.7.0" + +esformatter@0.10.0: + version "0.10.0" + resolved "https://registry.yarnpkg.com/esformatter/-/esformatter-0.10.0.tgz#e321ecc3d94083372cdfcf5c6f942cef6fec59d3" + dependencies: + acorn-to-esprima "^2.0.6" + babel-traverse "^6.4.5" + debug "^0.7.4" + disparity "^2.0.0" + esformatter-parser "^1.0.0" + glob "^7.0.5" + minimatch "^3.0.2" + minimist "^1.1.1" + mout ">=0.9 <2.0" + npm-run "^3.0.0" + resolve "^1.1.5" + rocambole ">=0.7 <2.0" + rocambole-indent "^2.0.4" + rocambole-linebreak "^1.0.2" + rocambole-node "~1.0" + rocambole-token "^1.1.2" + rocambole-whitespace "^1.0.0" + stdin "*" + strip-json-comments "~0.1.1" + supports-color "^1.3.1" + user-home "^2.0.0" + +eslint-loader@1.9.0: + version "1.9.0" + resolved "https://registry.yarnpkg.com/eslint-loader/-/eslint-loader-1.9.0.tgz#7e1be9feddca328d3dcfaef1ad49d5beffe83a13" + dependencies: + loader-fs-cache "^1.0.0" + loader-utils "^1.0.2" + object-assign "^4.0.1" + object-hash "^1.1.4" + rimraf "^2.6.1" + +eslint-plugin-filenames@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-filenames/-/eslint-plugin-filenames-1.2.0.tgz#aee9c1c90189c95d2e49902c160eceefecd99f53" + dependencies: + lodash.camelcase "4.3.0" + lodash.kebabcase "4.1.1" + lodash.snakecase "4.1.1" + lodash.upperfirst "4.3.1" + +eslint-plugin-react@7.4.0: + version "7.4.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.4.0.tgz#300a95861b9729c087d362dd64abcc351a74364a" + dependencies: + doctrine "^2.0.0" + has "^1.0.1" + jsx-ast-utils "^2.0.0" + prop-types "^15.5.10" + +eslint-scope@^3.7.1: + version "3.7.1" + resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-3.7.1.tgz#3d63c3edfda02e06e01a452ad88caacc7cdcb6e8" + dependencies: + esrecurse "^4.1.0" + estraverse "^4.1.1" + +eslint@4.8.0: + version "4.8.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-4.8.0.tgz#229ef0e354e0e61d837c7a80fdfba825e199815e" + dependencies: + ajv "^5.2.0" + babel-code-frame "^6.22.0" + chalk "^2.1.0" + concat-stream "^1.6.0" + cross-spawn "^5.1.0" + debug "^3.0.1" + doctrine "^2.0.0" + eslint-scope "^3.7.1" + espree "^3.5.1" + esquery "^1.0.0" + estraverse "^4.2.0" + esutils "^2.0.2" + file-entry-cache "^2.0.0" + functional-red-black-tree "^1.0.1" + glob "^7.1.2" + globals "^9.17.0" + ignore "^3.3.3" + imurmurhash "^0.1.4" + inquirer "^3.0.6" + is-resolvable "^1.0.0" + js-yaml "^3.9.1" + json-stable-stringify "^1.0.1" + levn "^0.3.0" + lodash "^4.17.4" + minimatch "^3.0.2" + mkdirp "^0.5.1" + natural-compare "^1.4.0" + optionator "^0.8.2" + path-is-inside "^1.0.2" + pluralize "^7.0.0" + progress "^2.0.0" + require-uncached "^1.0.3" + semver "^5.3.0" + strip-ansi "^4.0.0" + strip-json-comments "~2.0.1" + table "^4.0.1" + text-table "~0.2.0" + +espree@^3.5.1: + version "3.5.1" + resolved "https://registry.yarnpkg.com/espree/-/espree-3.5.1.tgz#0c988b8ab46db53100a1954ae4ba995ddd27d87e" + dependencies: + acorn "^5.1.1" + acorn-jsx "^3.0.0" + +esprima@^2.1, esprima@^2.6.0: + version "2.7.3" + resolved "https://registry.yarnpkg.com/esprima/-/esprima-2.7.3.tgz#96e3b70d5779f6ad49cd032673d1c312767ba581" + +esprima@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.0.tgz#4499eddcd1110e0b218bacf2fa7f7f59f55ca804" + +esprint@0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/esprint/-/esprint-0.4.0.tgz#f89c9bace36d90407968a8f9ceb0800ff786aab0" + dependencies: + dnode "^1.2.2" + fb-watchman "^2.0.0" + glob "^7.1.1" + sane "^1.6.0" + worker-farm "^1.3.1" + yargs "^8.0.1" + +esquery@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.0.0.tgz#cfba8b57d7fba93f17298a8a006a04cda13d80fa" + dependencies: + estraverse "^4.0.0" + +esrecurse@^4.1.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.2.0.tgz#fa9568d98d3823f9a41d91e902dcab9ea6e5b163" + dependencies: + estraverse "^4.1.0" + object-assign "^4.0.1" + +estraverse@^4.0.0, estraverse@^4.1.0, estraverse@^4.1.1, estraverse@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.2.0.tgz#0dee3fed31fcd469618ce7342099fc1afa0bdb13" + +esutils@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.2.tgz#0abf4f1caa5bcb1f7a9d8acc6dea4faaa04bac9b" + +event-emitter@^0.3.5, event-emitter@~0.3.5: + version "0.3.5" + resolved "https://registry.yarnpkg.com/event-emitter/-/event-emitter-0.3.5.tgz#df8c69eef1647923c7157b9ce83840610b02cc39" + dependencies: + d "1" + es5-ext "~0.10.14" + +event-stream@^3.1.7: + version "3.3.4" + resolved "https://registry.yarnpkg.com/event-stream/-/event-stream-3.3.4.tgz#4ab4c9a0f5a54db9338b4c34d86bfce8f4b35571" + dependencies: + duplexer "~0.1.1" + from "~0" + map-stream "~0.1.0" + pause-stream "0.0.11" + split "0.3" + stream-combiner "~0.0.4" + through "~2.3.1" + +events@^1.0.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/events/-/events-1.1.1.tgz#9ebdb7635ad099c70dcc4c2a1f5004288e8bd924" + +evp_bytestokey@^1.0.0, evp_bytestokey@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz#7fcbdb198dc71959432efe13842684e0525acb02" + dependencies: + md5.js "^1.3.4" + safe-buffer "^5.1.1" + +exec-sh@^0.2.0: + version "0.2.1" + resolved "https://registry.yarnpkg.com/exec-sh/-/exec-sh-0.2.1.tgz#163b98a6e89e6b65b47c2a28d215bc1f63989c38" + dependencies: + merge "^1.1.3" + +execa@^0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/execa/-/execa-0.7.0.tgz#944becd34cc41ee32a63a9faf27ad5a65fc59777" + dependencies: + cross-spawn "^5.0.1" + get-stream "^3.0.0" + is-stream "^1.1.0" + npm-run-path "^2.0.0" + p-finally "^1.0.0" + signal-exit "^3.0.0" + strip-eof "^1.0.0" + +execall@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/execall/-/execall-1.0.0.tgz#73d0904e395b3cab0658b08d09ec25307f29bb73" + dependencies: + clone-regexp "^1.0.0" + +exenv@^1.2.1: + version "1.2.2" + resolved "https://registry.yarnpkg.com/exenv/-/exenv-1.2.2.tgz#2ae78e85d9894158670b03d47bec1f03bd91bb9d" + +expand-brackets@^0.1.4: + version "0.1.5" + resolved "https://registry.yarnpkg.com/expand-brackets/-/expand-brackets-0.1.5.tgz#df07284e342a807cd733ac5af72411e581d1177b" + dependencies: + is-posix-bracket "^0.1.0" + +expand-range@^1.8.1: + version "1.8.2" + resolved "https://registry.yarnpkg.com/expand-range/-/expand-range-1.8.2.tgz#a299effd335fe2721ebae8e257ec79644fc85337" + dependencies: + fill-range "^2.1.0" + +expand-tilde@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/expand-tilde/-/expand-tilde-1.2.2.tgz#0b81eba897e5a3d31d1c3d102f8f01441e559449" + dependencies: + os-homedir "^1.0.1" + +expand-tilde@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/expand-tilde/-/expand-tilde-2.0.2.tgz#97e801aa052df02454de46b02bf621642cdc8502" + dependencies: + homedir-polyfill "^1.0.1" + +extend@^3.0.0, extend@~3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.1.tgz#a755ea7bc1adfcc5a31ce7e762dbaadc5e636444" + +external-editor@^2.0.4: + version "2.0.5" + resolved "https://registry.yarnpkg.com/external-editor/-/external-editor-2.0.5.tgz#52c249a3981b9ba187c7cacf5beb50bf1d91a6bc" + dependencies: + iconv-lite "^0.4.17" + jschardet "^1.4.2" + tmp "^0.0.33" + +extglob@^0.3.1: + version "0.3.2" + resolved "https://registry.yarnpkg.com/extglob/-/extglob-0.3.2.tgz#2e18ff3d2f49ab2765cec9023f011daa8d8349a1" + dependencies: + is-extglob "^1.0.0" + +extract-text-webpack-plugin@3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/extract-text-webpack-plugin/-/extract-text-webpack-plugin-3.0.1.tgz#605a8893faca1dd49bb0d2ca87493f33fd43d102" + dependencies: + async "^2.4.1" + loader-utils "^1.1.0" + schema-utils "^0.3.0" + webpack-sources "^1.0.1" + +extsprintf@1.3.0, extsprintf@^1.2.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05" + +fancy-log@^1.1.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/fancy-log/-/fancy-log-1.3.0.tgz#45be17d02bb9917d60ccffd4995c999e6c8c9948" + dependencies: + chalk "^1.1.1" + time-stamp "^1.0.0" + +fast-deep-equal@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-1.0.0.tgz#96256a3bc975595eb36d82e9929d060d893439ff" + +fast-levenshtein@~2.0.4: + version "2.0.6" + resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" + +fastparse@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/fastparse/-/fastparse-1.1.1.tgz#d1e2643b38a94d7583b479060e6c4affc94071f8" + +faye-websocket@~0.7.2: + version "0.7.3" + resolved "https://registry.yarnpkg.com/faye-websocket/-/faye-websocket-0.7.3.tgz#cc4074c7f4a4dfd03af54dd65c354b135132ce11" + dependencies: + websocket-driver ">=0.3.6" + +fb-watchman@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/fb-watchman/-/fb-watchman-2.0.0.tgz#54e9abf7dfa2f26cd9b1636c588c1afc05de5d58" + dependencies: + bser "^2.0.0" + +fbjs@^0.8.16: + version "0.8.16" + resolved "https://registry.yarnpkg.com/fbjs/-/fbjs-0.8.16.tgz#5e67432f550dc41b572bf55847b8aca64e5337db" + dependencies: + core-js "^1.0.0" + isomorphic-fetch "^2.1.1" + loose-envify "^1.0.0" + object-assign "^4.1.0" + promise "^7.1.1" + setimmediate "^1.0.5" + ua-parser-js "^0.7.9" + +fbjs@^0.8.4, fbjs@^0.8.9: + version "0.8.15" + resolved "https://registry.yarnpkg.com/fbjs/-/fbjs-0.8.15.tgz#4f0695fdfcc16c37c0b07facec8cb4c4091685b9" + dependencies: + core-js "^1.0.0" + isomorphic-fetch "^2.1.1" + loose-envify "^1.0.0" + object-assign "^4.1.0" + promise "^7.1.1" + setimmediate "^1.0.5" + ua-parser-js "^0.7.9" + +figures@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/figures/-/figures-2.0.0.tgz#3ab1a2d2a62c8bfb431a0c94cb797a2fce27c962" + dependencies: + escape-string-regexp "^1.0.5" + +file-entry-cache@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-2.0.0.tgz#c392990c3e684783d838b8c84a45d8a048458361" + dependencies: + flat-cache "^1.2.1" + object-assign "^4.0.1" + +file-loader@1.1.5: + version "1.1.5" + resolved "https://registry.yarnpkg.com/file-loader/-/file-loader-1.1.5.tgz#91c25b6b6fbe56dae99f10a425fd64933b5c9daa" + dependencies: + loader-utils "^1.0.2" + schema-utils "^0.3.0" + +filename-regex@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/filename-regex/-/filename-regex-2.0.1.tgz#c1c4b9bee3e09725ddb106b75c1e301fe2f18b26" + +filesize@3.5.10: + version "3.5.10" + resolved "https://registry.yarnpkg.com/filesize/-/filesize-3.5.10.tgz#fc8fa23ddb4ef9e5e0ab6e1e64f679a24a56761f" + +fill-range@^2.1.0: + version "2.2.3" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-2.2.3.tgz#50b77dfd7e469bc7492470963699fe7a8485a723" + dependencies: + is-number "^2.1.0" + isobject "^2.0.0" + randomatic "^1.1.3" + repeat-element "^1.1.2" + repeat-string "^1.5.2" + +find-cache-dir@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-0.1.1.tgz#c8defae57c8a52a8a784f9e31c57c742e993a0b9" + dependencies: + commondir "^1.0.1" + mkdirp "^0.5.1" + pkg-dir "^1.0.0" + +find-cache-dir@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-1.0.0.tgz#9288e3e9e3cc3748717d39eade17cf71fc30ee6f" + dependencies: + commondir "^1.0.1" + make-dir "^1.0.0" + pkg-dir "^2.0.0" + +find-index@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/find-index/-/find-index-0.1.1.tgz#675d358b2ca3892d795a1ab47232f8b6e2e0dde4" + +find-up@^1.0.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-1.1.2.tgz#6b2e9822b1a2ce0a60ab64d610eccad53cb24d0f" + dependencies: + path-exists "^2.0.0" + pinkie-promise "^2.0.0" + +find-up@^2.0.0, find-up@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-2.1.0.tgz#45d1b7e506c717ddd482775a2b77920a3c0c57a7" + dependencies: + locate-path "^2.0.0" + +findup-sync@^0.4.2: + version "0.4.3" + resolved "https://registry.yarnpkg.com/findup-sync/-/findup-sync-0.4.3.tgz#40043929e7bc60adf0b7f4827c4c6e75a0deca12" + dependencies: + detect-file "^0.1.0" + is-glob "^2.0.1" + micromatch "^2.3.7" + resolve-dir "^0.1.0" + +fined@^1.0.1: + version "1.1.0" + resolved "https://registry.yarnpkg.com/fined/-/fined-1.1.0.tgz#b37dc844b76a2f5e7081e884f7c0ae344f153476" + dependencies: + expand-tilde "^2.0.2" + is-plain-object "^2.0.3" + object.defaults "^1.1.0" + object.pick "^1.2.0" + parse-filepath "^1.0.1" + +first-chunk-stream@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/first-chunk-stream/-/first-chunk-stream-1.0.0.tgz#59bfb50cd905f60d7c394cd3d9acaab4e6ad934e" + +first-chunk-stream@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/first-chunk-stream/-/first-chunk-stream-2.0.0.tgz#1bdecdb8e083c0664b91945581577a43a9f31d70" + dependencies: + readable-stream "^2.0.2" + +flagged-respawn@^0.3.2: + version "0.3.2" + resolved "https://registry.yarnpkg.com/flagged-respawn/-/flagged-respawn-0.3.2.tgz#ff191eddcd7088a675b2610fffc976be9b8074b5" + +flat-cache@^1.2.1: + version "1.2.2" + resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-1.2.2.tgz#fa86714e72c21db88601761ecf2f555d1abc6b96" + dependencies: + circular-json "^0.3.1" + del "^2.0.2" + graceful-fs "^4.1.2" + write "^0.2.1" + +flatten@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/flatten/-/flatten-1.0.2.tgz#dae46a9d78fbe25292258cc1e780a41d95c03782" + +for-each@~0.3.2: + version "0.3.2" + resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.2.tgz#2c40450b9348e97f281322593ba96704b9abd4d4" + dependencies: + is-function "~1.0.0" + +for-in@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80" + +for-own@^0.1.4: + version "0.1.5" + resolved "https://registry.yarnpkg.com/for-own/-/for-own-0.1.5.tgz#5265c681a4f294dabbf17c9509b6763aa84510ce" + dependencies: + for-in "^1.0.1" + +for-own@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/for-own/-/for-own-1.0.0.tgz#c63332f415cedc4b04dbfe70cf836494c53cb44b" + dependencies: + for-in "^1.0.1" + +foreach@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/foreach/-/foreach-2.0.5.tgz#0bee005018aeb260d0a3af3ae658dd0136ec1b99" + +forever-agent@~0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" + +form-data@~2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.1.tgz#6fb94fbd71885306d73d15cc497fe4cc4ecd44bf" + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.5" + mime-types "^2.1.12" + +from@~0: + version "0.1.7" + resolved "https://registry.yarnpkg.com/from/-/from-0.1.7.tgz#83c60afc58b9c56997007ed1a768b3ab303a44fe" + +fs-exists-sync@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/fs-exists-sync/-/fs-exists-sync-0.1.0.tgz#982d6893af918e72d08dec9e8673ff2b5a8d6add" + +fs-readfile-promise@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/fs-readfile-promise/-/fs-readfile-promise-2.0.1.tgz#80023823981f9ffffe01609e8be668f69ae49e70" + dependencies: + graceful-fs "^4.1.2" + +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + +fsevents@^1.0.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.1.2.tgz#3282b713fb3ad80ede0e9fcf4611b5aa6fc033f4" + dependencies: + nan "^2.3.0" + node-pre-gyp "^0.6.36" + +fstream-ignore@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/fstream-ignore/-/fstream-ignore-1.0.5.tgz#9c31dae34767018fe1d249b24dada67d092da105" + dependencies: + fstream "^1.0.0" + inherits "2" + minimatch "^3.0.0" + +fstream@^1.0.0, fstream@^1.0.10, fstream@^1.0.2, fstream@^1.0.8: + version "1.0.11" + resolved "https://registry.yarnpkg.com/fstream/-/fstream-1.0.11.tgz#5c1fb1f117477114f0632a0eb4b71b3cb0fd3171" + dependencies: + graceful-fs "^4.1.2" + inherits "~2.0.0" + mkdirp ">=0.5 0" + rimraf "2" + +function-bind@^1.0.2, function-bind@^1.1.1, function-bind@~1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" + +functional-red-black-tree@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327" + +gauge@~2.7.3: + version "2.7.4" + resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.4.tgz#2c03405c7538c39d7eb37b317022e325fb018bf7" + dependencies: + aproba "^1.0.3" + console-control-strings "^1.0.0" + has-unicode "^2.0.0" + object-assign "^4.1.0" + signal-exit "^3.0.0" + string-width "^1.0.1" + strip-ansi "^3.0.1" + wide-align "^1.1.0" + +gaze@^0.5.1: + version "0.5.2" + resolved "https://registry.yarnpkg.com/gaze/-/gaze-0.5.2.tgz#40b709537d24d1d45767db5a908689dfe69ac44f" + dependencies: + globule "~0.1.0" + +get-caller-file@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-1.0.2.tgz#f702e63127e7e231c160a80c1554acb70d5047e5" + +get-node-dimensions@^1.2.0: + version "1.2.2" + resolved "https://registry.yarnpkg.com/get-node-dimensions/-/get-node-dimensions-1.2.2.tgz#7a71e8624cf9e1ab74599bb05b7e5116e995e45b" + +get-stdin@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-4.0.1.tgz#b968c6b0a04384324902e8bf1a5df32579a450fe" + +get-stdin@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-5.0.1.tgz#122e161591e21ff4c52530305693f20e6393a398" + +get-stream@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-3.0.0.tgz#8e943d1358dc37555054ecbe2edb05aa174ede14" + +getpass@^0.1.1: + version "0.1.7" + resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa" + dependencies: + assert-plus "^1.0.0" + +glob-base@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/glob-base/-/glob-base-0.3.0.tgz#dbb164f6221b1c0b1ccf82aea328b497df0ea3c4" + dependencies: + glob-parent "^2.0.0" + is-glob "^2.0.0" + +glob-parent@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-2.0.0.tgz#81383d72db054fcccf5336daa902f182f6edbb28" + dependencies: + is-glob "^2.0.0" + +glob-parent@^3.0.1: + version "3.1.0" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-3.1.0.tgz#9e6af6299d8d3bd2bd40430832bd113df906c5ae" + dependencies: + is-glob "^3.1.0" + path-dirname "^1.0.0" + +glob-stream@^3.1.5: + version "3.1.18" + resolved "https://registry.yarnpkg.com/glob-stream/-/glob-stream-3.1.18.tgz#9170a5f12b790306fdfe598f313f8f7954fd143b" + dependencies: + glob "^4.3.1" + glob2base "^0.0.12" + minimatch "^2.0.1" + ordered-read-streams "^0.1.0" + through2 "^0.6.1" + unique-stream "^1.0.0" + +glob-watcher@^0.0.6: + version "0.0.6" + resolved "https://registry.yarnpkg.com/glob-watcher/-/glob-watcher-0.0.6.tgz#b95b4a8df74b39c83298b0c05c978b4d9a3b710b" + dependencies: + gaze "^0.5.1" + +glob2base@^0.0.12: + version "0.0.12" + resolved "https://registry.yarnpkg.com/glob2base/-/glob2base-0.0.12.tgz#9d419b3e28f12e83a362164a277055922c9c0d56" + dependencies: + find-index "^0.1.1" + +glob@^4.3.1: + version "4.5.3" + resolved "https://registry.yarnpkg.com/glob/-/glob-4.5.3.tgz#c6cb73d3226c1efef04de3c56d012f03377ee15f" + dependencies: + inflight "^1.0.4" + inherits "2" + minimatch "^2.0.1" + once "^1.3.0" + +glob@^6.0.1: + version "6.0.4" + resolved "https://registry.yarnpkg.com/glob/-/glob-6.0.4.tgz#0f08860f6a155127b2fadd4f9ce24b1aab6e4d22" + dependencies: + inflight "^1.0.4" + inherits "2" + minimatch "2 || 3" + once "^1.3.0" + path-is-absolute "^1.0.0" + +glob@^7.0.3, glob@^7.0.5, glob@^7.1.1, glob@^7.1.2, glob@~7.1.2: + version "7.1.2" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15" + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.4" + once "^1.3.0" + path-is-absolute "^1.0.0" + +glob@~3.1.21: + version "3.1.21" + resolved "https://registry.yarnpkg.com/glob/-/glob-3.1.21.tgz#d29e0a055dea5138f4d07ed40e8982e83c2066cd" + dependencies: + graceful-fs "~1.2.0" + inherits "1" + minimatch "~0.2.11" + +global-modules@^0.2.3: + version "0.2.3" + resolved "https://registry.yarnpkg.com/global-modules/-/global-modules-0.2.3.tgz#ea5a3bed42c6d6ce995a4f8a1269b5dae223828d" + dependencies: + global-prefix "^0.1.4" + is-windows "^0.2.0" + +global-prefix@^0.1.4: + version "0.1.5" + resolved "https://registry.yarnpkg.com/global-prefix/-/global-prefix-0.1.5.tgz#8d3bc6b8da3ca8112a160d8d496ff0462bfef78f" + dependencies: + homedir-polyfill "^1.0.0" + ini "^1.3.4" + is-windows "^0.2.0" + which "^1.2.12" + +globals@^10.0.0: + version "10.1.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-10.1.0.tgz#4425a1881be0d336b4a823a82a7be725d5dd987c" + +globals@^9.17.0, globals@^9.18.0: + version "9.18.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-9.18.0.tgz#aa3896b3e69b487f17e31ed2143d69a8e30c2d8a" + +globby@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/globby/-/globby-4.1.0.tgz#080f54549ec1b82a6c60e631fc82e1211dbe95f8" + dependencies: + array-union "^1.0.1" + arrify "^1.0.0" + glob "^6.0.1" + object-assign "^4.0.1" + pify "^2.0.0" + pinkie-promise "^2.0.0" + +globby@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/globby/-/globby-6.1.0.tgz#f5a6d70e8395e21c858fb0489d64df02424d506c" + dependencies: + array-union "^1.0.1" + glob "^7.0.3" + object-assign "^4.0.1" + pify "^2.0.0" + pinkie-promise "^2.0.0" + +globjoin@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/globjoin/-/globjoin-0.1.4.tgz#2f4494ac8919e3767c5cbb691e9f463324285d43" + +globule@~0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/globule/-/globule-0.1.0.tgz#d9c8edde1da79d125a151b79533b978676346ae5" + dependencies: + glob "~3.1.21" + lodash "~1.0.1" + minimatch "~0.2.11" + +glogg@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/glogg/-/glogg-1.0.0.tgz#7fe0f199f57ac906cf512feead8f90ee4a284fc5" + dependencies: + sparkles "^1.0.0" + +good-listener@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/good-listener/-/good-listener-1.2.2.tgz#d53b30cdf9313dffb7dc9a0d477096aa6d145c50" + dependencies: + delegate "^3.1.2" + +graceful-fs@4.X, graceful-fs@^4.1.2: + version "4.1.11" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.11.tgz#0e8bdfe4d1ddb8854d64e04ea7c00e2a026e5658" + +graceful-fs@^3.0.0: + version "3.0.11" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-3.0.11.tgz#7613c778a1afea62f25c630a086d7f3acbbdd818" + dependencies: + natives "^1.1.0" + +graceful-fs@~1.2.0: + version "1.2.3" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-1.2.3.tgz#15a4806a57547cb2d2dbf27f42e89a8c3451b364" + +gulp-cached@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/gulp-cached/-/gulp-cached-1.1.1.tgz#fe7cd4f87f37601e6073cfedee5c2bdaf8b6acce" + dependencies: + lodash.defaults "^4.2.0" + through2 "^2.0.1" + +gulp-clean-css@3.9.0: + version "3.9.0" + resolved "https://registry.yarnpkg.com/gulp-clean-css/-/gulp-clean-css-3.9.0.tgz#e43e4c8d695060f6ba08a154d8e76d0d87b1c822" + dependencies: + clean-css "4.1.9" + gulp-util "3.0.8" + through2 "2.0.3" + vinyl-sourcemaps-apply "0.2.1" + +gulp-concat@2.6.1: + version "2.6.1" + resolved "https://registry.yarnpkg.com/gulp-concat/-/gulp-concat-2.6.1.tgz#633d16c95d88504628ad02665663cee5a4793353" + dependencies: + concat-with-sourcemaps "^1.0.0" + through2 "^2.0.0" + vinyl "^2.0.0" + +gulp-declare@0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/gulp-declare/-/gulp-declare-0.3.0.tgz#86830fc6faa88e06382162c8664b8e94957afcd9" + dependencies: + nsdeclare "^0.1.0" + vinyl-map "^1.0.1" + xtend "^4.0.0" + +gulp-livereload@3.8.1: + version "3.8.1" + resolved "https://registry.yarnpkg.com/gulp-livereload/-/gulp-livereload-3.8.1.tgz#00f744b2d749d3e9e3746589c8a44acac779b50f" + dependencies: + chalk "^0.5.1" + debug "^2.1.0" + event-stream "^3.1.7" + gulp-util "^3.0.2" + lodash.assign "^3.0.0" + mini-lr "^0.1.8" + +gulp-postcss@7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/gulp-postcss/-/gulp-postcss-7.0.0.tgz#cfb62a19fa947f8be67ce9ecae89ceb959f0cf93" + dependencies: + gulp-util "^3.0.8" + postcss "^6.0.0" + postcss-load-config "^1.2.0" + vinyl-sourcemaps-apply "^0.2.1" + +gulp-print@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/gulp-print/-/gulp-print-2.0.1.tgz#1acee58eac8af2d3c4ad3329dbe465758393c414" + dependencies: + gulp-util "^3.0.6" + map-stream "~0.0.6" + +gulp-sourcemaps@2.6.1: + version "2.6.1" + resolved "https://registry.yarnpkg.com/gulp-sourcemaps/-/gulp-sourcemaps-2.6.1.tgz#833a4e28f0b8f4661075032cd782417f7cd8fb0b" + dependencies: + "@gulp-sourcemaps/identity-map" "1.X" + "@gulp-sourcemaps/map-sources" "1.X" + acorn "4.X" + convert-source-map "1.X" + css "2.X" + debug-fabulous ">=0.1.1" + detect-newline "2.X" + graceful-fs "4.X" + source-map "0.X" + strip-bom-string "1.X" + through2 "2.X" + vinyl "1.X" + +gulp-stripbom@1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/gulp-stripbom/-/gulp-stripbom-1.0.4.tgz#58c1d03e85e008a7aab47d81b1297c8c1bc828eb" + dependencies: + gulp-util "^3.0.0" + log-symbols "^1.0.0" + strip-bom "^1.0.0" + through2 "^0.5.1" + +gulp-util@3.0.8, gulp-util@^3.0.0, gulp-util@^3.0.2, gulp-util@^3.0.3, gulp-util@^3.0.6, gulp-util@^3.0.7, gulp-util@^3.0.8: + version "3.0.8" + resolved "https://registry.yarnpkg.com/gulp-util/-/gulp-util-3.0.8.tgz#0054e1e744502e27c04c187c3ecc505dd54bbb4f" + dependencies: + array-differ "^1.0.0" + array-uniq "^1.0.2" + beeper "^1.0.0" + chalk "^1.0.0" + dateformat "^2.0.0" + fancy-log "^1.1.0" + gulplog "^1.0.0" + has-gulplog "^0.1.0" + lodash._reescape "^3.0.0" + lodash._reevaluate "^3.0.0" + lodash._reinterpolate "^3.0.0" + lodash.template "^3.0.0" + minimist "^1.1.0" + multipipe "^0.1.2" + object-assign "^3.0.0" + replace-ext "0.0.1" + through2 "^2.0.0" + vinyl "^0.5.0" + +gulp-watch@4.3.11: + version "4.3.11" + resolved "https://registry.yarnpkg.com/gulp-watch/-/gulp-watch-4.3.11.tgz#162fc563de9fc770e91f9a7ce3955513a9a118c0" + dependencies: + anymatch "^1.3.0" + chokidar "^1.6.1" + glob-parent "^3.0.1" + gulp-util "^3.0.7" + object-assign "^4.1.0" + path-is-absolute "^1.0.1" + readable-stream "^2.2.2" + slash "^1.0.0" + vinyl "^1.2.0" + vinyl-file "^2.0.0" + +gulp-wrap@0.13.0: + version "0.13.0" + resolved "https://registry.yarnpkg.com/gulp-wrap/-/gulp-wrap-0.13.0.tgz#90fb0b4a27a266433832ff7c6122db5c1ee894c6" + dependencies: + consolidate "^0.14.1" + es6-promise "^3.1.2" + fs-readfile-promise "^2.0.1" + gulp-util "^3.0.3" + js-yaml "^3.2.6" + lodash "^4.11.1" + node.extend "^1.1.2" + through2 "^2.0.1" + tryit "^1.0.1" + vinyl-bufferstream "^1.0.1" + +gulp@3.9.1: + version "3.9.1" + resolved "https://registry.yarnpkg.com/gulp/-/gulp-3.9.1.tgz#571ce45928dd40af6514fc4011866016c13845b4" + dependencies: + archy "^1.0.0" + chalk "^1.0.0" + deprecated "^0.0.1" + gulp-util "^3.0.0" + interpret "^1.0.0" + liftoff "^2.1.0" + minimist "^1.1.0" + orchestrator "^0.3.0" + pretty-hrtime "^1.0.0" + semver "^4.1.0" + tildify "^1.0.0" + v8flags "^2.0.2" + vinyl-fs "^0.3.0" + +gulplog@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/gulplog/-/gulplog-1.0.0.tgz#e28c4d45d05ecbbed818363ce8f9c5926229ffe5" + dependencies: + glogg "^1.0.0" + +har-schema@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92" + +har-validator@~5.0.3: + version "5.0.3" + resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.0.3.tgz#ba402c266194f15956ef15e0fcf242993f6a7dfd" + dependencies: + ajv "^5.1.0" + har-schema "^2.0.0" + +has-ansi@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-0.1.0.tgz#84f265aae8c0e6a88a12d7022894b7568894c62e" + dependencies: + ansi-regex "^0.2.0" + +has-ansi@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91" + dependencies: + ansi-regex "^2.0.0" + +has-flag@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-1.0.0.tgz#9d9e793165ce017a00f00418c43f942a7b1d11fa" + +has-flag@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-2.0.0.tgz#e8207af1cc7b30d446cc70b734b5e8be18f88d51" + +has-gulplog@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/has-gulplog/-/has-gulplog-0.1.0.tgz#6414c82913697da51590397dafb12f22967811ce" + dependencies: + sparkles "^1.0.0" + +has-unicode@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9" + +has@^1.0.1, has@~1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/has/-/has-1.0.1.tgz#8461733f538b0837c9361e39a9ab9e9704dc2f28" + dependencies: + function-bind "^1.0.2" + +hash-base@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/hash-base/-/hash-base-2.0.2.tgz#66ea1d856db4e8a5470cadf6fce23ae5244ef2e1" + dependencies: + inherits "^2.0.1" + +hash-base@^3.0.0: + version "3.0.4" + resolved "https://registry.yarnpkg.com/hash-base/-/hash-base-3.0.4.tgz#5fc8686847ecd73499403319a6b0a3f3f6ae4918" + dependencies: + inherits "^2.0.1" + safe-buffer "^5.0.1" + +hash.js@^1.0.0, hash.js@^1.0.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/hash.js/-/hash.js-1.1.3.tgz#340dedbe6290187151c1ea1d777a3448935df846" + dependencies: + inherits "^2.0.3" + minimalistic-assert "^1.0.0" + +hawk@~6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/hawk/-/hawk-6.0.2.tgz#af4d914eb065f9b5ce4d9d11c1cb2126eecc3038" + dependencies: + boom "4.x.x" + cryptiles "3.x.x" + hoek "4.x.x" + sntp "2.x.x" + +history@4.7.2, history@^4.7.2: + version "4.7.2" + resolved "https://registry.yarnpkg.com/history/-/history-4.7.2.tgz#22b5c7f31633c5b8021c7f4a8a954ac139ee8d5b" + dependencies: + invariant "^2.2.1" + loose-envify "^1.2.0" + resolve-pathname "^2.2.0" + value-equal "^0.4.0" + warning "^3.0.0" + +history@^4.5.1: + version "4.6.3" + resolved "https://registry.yarnpkg.com/history/-/history-4.6.3.tgz#6d723a8712c581d6bef37e8c26f4aedc6eb86967" + dependencies: + invariant "^2.2.1" + loose-envify "^1.2.0" + resolve-pathname "^2.0.0" + value-equal "^0.2.0" + warning "^3.0.0" + +hmac-drbg@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1" + dependencies: + hash.js "^1.0.3" + minimalistic-assert "^1.0.0" + minimalistic-crypto-utils "^1.0.1" + +hoek@4.x.x: + version "4.2.0" + resolved "https://registry.yarnpkg.com/hoek/-/hoek-4.2.0.tgz#72d9d0754f7fe25ca2d01ad8f8f9a9449a89526d" + +hoist-non-react-statics@^2.1.0, hoist-non-react-statics@^2.2.1, hoist-non-react-statics@^2.3.0: + version "2.3.1" + resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-2.3.1.tgz#343db84c6018c650778898240135a1420ee22ce0" + +home-or-tmp@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/home-or-tmp/-/home-or-tmp-2.0.0.tgz#e36c3f2d2cae7d746a857e38d18d5f32a7882db8" + dependencies: + os-homedir "^1.0.0" + os-tmpdir "^1.0.1" + +homedir-polyfill@^1.0.0, homedir-polyfill@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/homedir-polyfill/-/homedir-polyfill-1.0.1.tgz#4c2bbc8a758998feebf5ed68580f76d46768b4bc" + dependencies: + parse-passwd "^1.0.0" + +hosted-git-info@^2.1.4: + version "2.5.0" + resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.5.0.tgz#6d60e34b3abbc8313062c3b798ef8d901a07af3c" + +html-comment-regex@^1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/html-comment-regex/-/html-comment-regex-1.1.1.tgz#668b93776eaae55ebde8f3ad464b307a4963625e" + +html-tags@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/html-tags/-/html-tags-2.0.0.tgz#10b30a386085f43cede353cc8fa7cb0deeea668b" + +http-errors@~1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.3.1.tgz#197e22cdebd4198585e8694ef6786197b91ed942" + dependencies: + inherits "~2.0.1" + statuses "1" + +http-parser-js@>=0.4.0: + version "0.4.6" + resolved "https://registry.yarnpkg.com/http-parser-js/-/http-parser-js-0.4.6.tgz#195273f58704c452d671076be201329dd341dc55" + +http-signature@~1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1" + dependencies: + assert-plus "^1.0.0" + jsprim "^1.2.2" + sshpk "^1.7.0" + +https-browserify@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-0.0.1.tgz#3f91365cabe60b77ed0ebba24b454e3e09d95a82" + +iconv-lite@0.4.13, iconv-lite@~0.4.13: + version "0.4.13" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.13.tgz#1f88aba4ab0b1508e8312acc39345f36e992e2f2" + +iconv-lite@^0.4.17: + version "0.4.19" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.19.tgz#f7468f60135f5e5dad3399c0a81be9a1603a082b" + +icss-replace-symbols@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/icss-replace-symbols/-/icss-replace-symbols-1.1.0.tgz#06ea6f83679a7749e386cfe1fe812ae5db223ded" + +icss-utils@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/icss-utils/-/icss-utils-2.1.0.tgz#83f0a0ec378bf3246178b6c2ad9136f135b1c962" + dependencies: + postcss "^6.0.1" + +ieee754@^1.1.4: + version "1.1.8" + resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.8.tgz#be33d40ac10ef1926701f6f08a2d86fbfd1ad3e4" + +ignore@^3.3.3: + version "3.3.5" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-3.3.5.tgz#c4e715455f6073a8d7e5dae72d2fc9d71663dba6" + +imurmurhash@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" + +indent-string@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-2.1.0.tgz#8e2d48348742121b4a8218b7a137e9a52049dc80" + dependencies: + repeating "^2.0.0" + +indexes-of@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/indexes-of/-/indexes-of-1.0.1.tgz#f30f716c8e2bd346c7b67d3df3915566a7c05607" + +indexof@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/indexof/-/indexof-0.0.1.tgz#82dc336d232b9062179d05ab3293a66059fd435d" + +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-1.0.2.tgz#ca4309dadee6b54cc0b8d247e8d7c7a0975bdc9b" + +inherits@2, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.0, inherits@~2.0.1, inherits@~2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" + +inherits@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.1.tgz#b17d08d326b4423e568eff719f91b0b1cbdf69f1" + +ini@^1.3.4, ini@~1.3.0: + version "1.3.4" + resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.4.tgz#0537cb79daf59b59a1a517dff706c86ec039162e" + +inquirer@^3.0.6: + version "3.3.0" + resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-3.3.0.tgz#9dd2f2ad765dcab1ff0443b491442a20ba227dc9" + dependencies: + ansi-escapes "^3.0.0" + chalk "^2.0.0" + cli-cursor "^2.1.0" + cli-width "^2.0.0" + external-editor "^2.0.4" + figures "^2.0.0" + lodash "^4.3.0" + mute-stream "0.0.7" + run-async "^2.2.0" + rx-lite "^4.0.8" + rx-lite-aggregates "^4.0.8" + string-width "^2.1.0" + strip-ansi "^4.0.0" + through "^2.3.6" + +interpret@^1.0.0: + version "1.0.4" + resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.0.4.tgz#820cdd588b868ffb191a809506d6c9c8f212b1b0" + +invariant@^2.0.0, invariant@^2.1.0, invariant@^2.2.0, invariant@^2.2.1, invariant@^2.2.2: + version "2.2.2" + resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.2.tgz#9e1f56ac0acdb6bf303306f338be3b204ae60360" + dependencies: + loose-envify "^1.0.0" + +invert-kv@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/invert-kv/-/invert-kv-1.0.0.tgz#104a8e4aaca6d3d8cd157a8ef8bfab2d7a3ffdb6" + +is-absolute-url@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-absolute-url/-/is-absolute-url-2.1.0.tgz#50530dfb84fcc9aa7dbe7852e83a37b93b9f2aa6" + +is-absolute@^0.2.3: + version "0.2.6" + resolved "https://registry.yarnpkg.com/is-absolute/-/is-absolute-0.2.6.tgz#20de69f3db942ef2d87b9c2da36f172235b1b5eb" + dependencies: + is-relative "^0.2.1" + is-windows "^0.2.0" + +is-arrayish@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" + +is-binary-path@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-1.0.1.tgz#75f16642b480f187a711c814161fd3a4a7655898" + dependencies: + binary-extensions "^1.0.0" + +is-buffer@^1.1.5: + version "1.1.5" + resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.5.tgz#1f3b26ef613b214b88cbca23cc6c01d87961eecc" + +is-builtin-module@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-builtin-module/-/is-builtin-module-1.0.0.tgz#540572d34f7ac3119f8f76c30cbc1b1e037affbe" + dependencies: + builtin-modules "^1.0.0" + +is-callable@^1.1.1, is-callable@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.1.3.tgz#86eb75392805ddc33af71c92a0eedf74ee7604b2" + +is-date-object@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.1.tgz#9aa20eb6aeebbff77fbd33e74ca01b33581d3a16" + +is-directory@^0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/is-directory/-/is-directory-0.3.1.tgz#61339b6f2475fc772fd9c9d83f5c8575dc154ae1" + +is-dotfile@^1.0.0: + version "1.0.3" + resolved "https://registry.yarnpkg.com/is-dotfile/-/is-dotfile-1.0.3.tgz#a6a2f32ffd2dfb04f5ca25ecd0f6b83cf798a1e1" + +is-equal-shallow@^0.1.3: + version "0.1.3" + resolved "https://registry.yarnpkg.com/is-equal-shallow/-/is-equal-shallow-0.1.3.tgz#2238098fc221de0bcfa5d9eac4c45d638aa1c534" + dependencies: + is-primitive "^2.0.0" + +is-extendable@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-0.1.1.tgz#62b110e289a471418e3ec36a617d472e301dfc89" + +is-extglob@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-1.0.0.tgz#ac468177c4943405a092fc8f29760c6ffc6206c0" + +is-extglob@^2.1.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" + +is-finite@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-finite/-/is-finite-1.0.2.tgz#cc6677695602be550ef11e8b4aa6305342b6d0aa" + dependencies: + number-is-nan "^1.0.0" + +is-fullwidth-code-point@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz#ef9e31386f031a7f0d643af82fde50c457ef00cb" + dependencies: + number-is-nan "^1.0.0" + +is-fullwidth-code-point@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f" + +is-function@~1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-function/-/is-function-1.0.1.tgz#12cfb98b65b57dd3d193a3121f5f6e2f437602b5" + +is-glob@^2.0.0, is-glob@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-2.0.1.tgz#d096f926a3ded5600f3fdfd91198cb0888c2d863" + dependencies: + is-extglob "^1.0.0" + +is-glob@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-3.1.0.tgz#7ba5ae24217804ac70707b96922567486cc3e84a" + dependencies: + is-extglob "^2.1.0" + +is-number@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-2.1.0.tgz#01fcbbb393463a548f2f466cce16dece49db908f" + dependencies: + kind-of "^3.0.2" + +is-number@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-3.0.0.tgz#24fd6201a4782cf50561c810276afc7d12d71195" + dependencies: + kind-of "^3.0.2" + +is-path-cwd@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-path-cwd/-/is-path-cwd-1.0.0.tgz#d225ec23132e89edd38fda767472e62e65f1106d" + +is-path-in-cwd@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-path-in-cwd/-/is-path-in-cwd-1.0.0.tgz#6477582b8214d602346094567003be8a9eac04dc" + dependencies: + is-path-inside "^1.0.0" + +is-path-inside@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-1.0.0.tgz#fc06e5a1683fbda13de667aff717bbc10a48f37f" + dependencies: + path-is-inside "^1.0.1" + +is-plain-obj@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-1.1.0.tgz#71a50c8429dfca773c92a390a4a03b39fcd51d3e" + +is-plain-object@^2.0.3: + version "2.0.4" + resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677" + dependencies: + isobject "^3.0.1" + +is-posix-bracket@^0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/is-posix-bracket/-/is-posix-bracket-0.1.1.tgz#3334dc79774368e92f016e6fbc0a88f5cd6e6bc4" + +is-primitive@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-primitive/-/is-primitive-2.0.0.tgz#207bab91638499c07b2adf240a41a87210034575" + +is-promise@^2.1, is-promise@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-promise/-/is-promise-2.1.0.tgz#79a2a9ece7f096e80f36d2b2f3bc16c1ff4bf3fa" + +is-regex@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.0.4.tgz#5517489b547091b0930e095654ced25ee97e9491" + dependencies: + has "^1.0.1" + +is-regexp@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-regexp/-/is-regexp-1.0.0.tgz#fd2d883545c46bac5a633e7b9a09e87fa2cb5069" + +is-relative@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/is-relative/-/is-relative-0.2.1.tgz#d27f4c7d516d175fb610db84bbeef23c3bc97aa5" + dependencies: + is-unc-path "^0.1.1" + +is-resolvable@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-resolvable/-/is-resolvable-1.0.0.tgz#8df57c61ea2e3c501408d100fb013cf8d6e0cc62" + dependencies: + tryit "^1.0.1" + +is-stream@^1.0.1, is-stream@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" + +is-supported-regexp-flag@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-supported-regexp-flag/-/is-supported-regexp-flag-1.0.0.tgz#8b520c85fae7a253382d4b02652e045576e13bb8" + +is-svg@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-svg/-/is-svg-2.1.0.tgz#cf61090da0d9efbcab8722deba6f032208dbb0e9" + dependencies: + html-comment-regex "^1.1.0" + +is-symbol@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.1.tgz#3cc59f00025194b6ab2e38dbae6689256b660572" + +is-typedarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" + +is-unc-path@^0.1.1: + version "0.1.2" + resolved "https://registry.yarnpkg.com/is-unc-path/-/is-unc-path-0.1.2.tgz#6ab053a72573c10250ff416a3814c35178af39b9" + dependencies: + unc-path-regex "^0.1.0" + +is-utf8@^0.2.0: + version "0.2.1" + resolved "https://registry.yarnpkg.com/is-utf8/-/is-utf8-0.2.1.tgz#4b0da1442104d1b336340e80797e865cf39f7d72" + +is-windows@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-0.2.0.tgz#de1aa6d63ea29dd248737b69f1ff8b8002d2108c" + +is@^3.1.0: + version "3.2.1" + resolved "https://registry.yarnpkg.com/is/-/is-3.2.1.tgz#d0ac2ad55eb7b0bec926a5266f6c662aaa83dca5" + +isarray@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf" + +isarray@1.0.0, isarray@^1.0.0, isarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" + +isexe@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" + +isobject@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/isobject/-/isobject-2.1.0.tgz#f065561096a3f1da2ef46272f815c840d87e0c89" + dependencies: + isarray "1.0.0" + +isobject@^3.0.0, isobject@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df" + +isomorphic-fetch@^2.1.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/isomorphic-fetch/-/isomorphic-fetch-2.2.1.tgz#611ae1acf14f5e81f729507472819fe9733558a9" + dependencies: + node-fetch "^1.0.1" + whatwg-fetch ">=0.10.0" + +isstream@^0.1.2, isstream@~0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" + +jdu@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/jdu/-/jdu-1.0.0.tgz#28f1e388501785ae0a1d93e93ed0b14dd41e51ce" + +jquery@3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/jquery/-/jquery-3.2.1.tgz#5c4d9de652af6cd0a770154a631bba12b015c787" + +jquery@>=1.6.4: + version "2.2.4" + resolved "https://registry.yarnpkg.com/jquery/-/jquery-2.2.4.tgz#2c89d6889b5eac522a7eea32c14521559c6cbf02" + +js-base64@^2.1.9: + version "2.3.2" + resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-2.3.2.tgz#a79a923666372b580f8e27f51845c6f7e8fbfbaf" + +js-tokens@^3.0.0, js-tokens@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b" + +js-yaml@^3.2.6, js-yaml@^3.4.3, js-yaml@^3.9.0, js-yaml@^3.9.1: + version "3.10.0" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.10.0.tgz#2e78441646bd4682e963f22b6e92823c309c62dc" + dependencies: + argparse "^1.0.7" + esprima "^4.0.0" + +js-yaml@~3.7.0: + version "3.7.0" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.7.0.tgz#5c967ddd837a9bfdca5f2de84253abe8a1c03b80" + dependencies: + argparse "^1.0.7" + esprima "^2.6.0" + +jsbn@~0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" + +jschardet@^1.4.2: + version "1.5.1" + resolved "https://registry.yarnpkg.com/jschardet/-/jschardet-1.5.1.tgz#c519f629f86b3a5bedba58a88d311309eec097f9" + +jsesc@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-1.3.0.tgz#46c3fec8c1892b12b0833db9bc7622176dbab34b" + +jsesc@~0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-0.5.0.tgz#e7dee66e35d6fc16f710fe91d5cf69f70f08911d" + +json-loader@^0.5.4: + version "0.5.7" + resolved "https://registry.yarnpkg.com/json-loader/-/json-loader-0.5.7.tgz#dca14a70235ff82f0ac9a3abeb60d337a365185d" + +json-schema-traverse@^0.3.0: + version "0.3.1" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz#349a6d44c53a51de89b40805c5d5e59b417d3340" + +json-schema@0.2.3: + version "0.2.3" + resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13" + +json-stable-stringify@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz#9a759d39c5f2ff503fd5300646ed445f88c4f9af" + dependencies: + jsonify "~0.0.0" + +json-stringify-safe@~5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" + +json5@^0.5.0, json5@^0.5.1: + version "0.5.1" + resolved "https://registry.yarnpkg.com/json5/-/json5-0.5.1.tgz#1eade7acc012034ad84e2396767ead9fa5495821" + +jsonify@~0.0.0: + version "0.0.0" + resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.0.tgz#2c74b6ee41d93ca51b7b5aaee8f503631d252a73" + +jsprim@^1.2.2: + version "1.4.1" + resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2" + dependencies: + assert-plus "1.0.0" + extsprintf "1.3.0" + json-schema "0.2.3" + verror "1.10.0" + +jsx-ast-utils@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/jsx-ast-utils/-/jsx-ast-utils-2.0.1.tgz#e801b1b39985e20fffc87b40e3748080e2dcac7f" + dependencies: + array-includes "^3.0.3" + +kind-of@^3.0.2: + version "3.2.2" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64" + dependencies: + is-buffer "^1.1.5" + +kind-of@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-4.0.0.tgz#20813df3d712928b207378691a45066fae72dd57" + dependencies: + is-buffer "^1.1.5" + +known-css-properties@^0.4.0: + version "0.4.1" + resolved "https://registry.yarnpkg.com/known-css-properties/-/known-css-properties-0.4.1.tgz#baaaf704e5f8a5f10e0e221212aae3ea738ea372" + +lazy-cache@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/lazy-cache/-/lazy-cache-1.0.4.tgz#a1d78fc3a50474cb80845d3b3b6e1da49a446e8e" + +lcid@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/lcid/-/lcid-1.0.0.tgz#308accafa0bc483a3867b4b6f2b9506251d1b835" + dependencies: + invert-kv "^1.0.0" + +levn@^0.3.0, levn@~0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/levn/-/levn-0.3.0.tgz#3b09924edf9f083c0490fdd4c0bc4421e04764ee" + dependencies: + prelude-ls "~1.1.2" + type-check "~0.3.2" + +liftoff@^2.1.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/liftoff/-/liftoff-2.3.0.tgz#a98f2ff67183d8ba7cfaca10548bd7ff0550b385" + dependencies: + extend "^3.0.0" + findup-sync "^0.4.2" + fined "^1.0.1" + flagged-respawn "^0.3.2" + lodash.isplainobject "^4.0.4" + lodash.isstring "^4.0.1" + lodash.mapvalues "^4.4.0" + rechoir "^0.6.2" + resolve "^1.1.7" + +livereload-js@^2.2.0: + version "2.2.2" + resolved "https://registry.yarnpkg.com/livereload-js/-/livereload-js-2.2.2.tgz#6c87257e648ab475bc24ea257457edcc1f8d0bc2" + +load-json-file@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-1.1.0.tgz#956905708d58b4bab4c2261b04f59f31c99374c0" + dependencies: + graceful-fs "^4.1.2" + parse-json "^2.2.0" + pify "^2.0.0" + pinkie-promise "^2.0.0" + strip-bom "^2.0.0" + +load-json-file@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-2.0.0.tgz#7947e42149af80d696cbf797bcaabcfe1fe29ca8" + dependencies: + graceful-fs "^4.1.2" + parse-json "^2.2.0" + pify "^2.0.0" + strip-bom "^3.0.0" + +loader-fs-cache@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/loader-fs-cache/-/loader-fs-cache-1.0.1.tgz#56e0bf08bd9708b26a765b68509840c8dec9fdbc" + dependencies: + find-cache-dir "^0.1.1" + mkdirp "0.5.1" + +loader-runner@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-2.3.0.tgz#f482aea82d543e07921700d5a46ef26fdac6b8a2" + +loader-utils@^1.0.2, loader-utils@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.1.0.tgz#c98aef488bcceda2ffb5e2de646d6a754429f5cd" + dependencies: + big.js "^3.1.3" + emojis-list "^2.0.0" + json5 "^0.5.0" + +locate-path@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-2.0.0.tgz#2b568b265eec944c6d9c0de9c3dbbbca0354cd8e" + dependencies: + p-locate "^2.0.0" + path-exists "^3.0.0" + +lodash-es@^4.17.4, lodash-es@^4.2.0, lodash-es@^4.2.1: + version "4.17.4" + resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.4.tgz#dcc1d7552e150a0640073ba9cb31d70f032950e7" + +lodash._baseassign@^3.0.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/lodash._baseassign/-/lodash._baseassign-3.2.0.tgz#8c38a099500f215ad09e59f1722fd0c52bfe0a4e" + dependencies: + lodash._basecopy "^3.0.0" + lodash.keys "^3.0.0" + +lodash._basecopy@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/lodash._basecopy/-/lodash._basecopy-3.0.1.tgz#8da0e6a876cf344c0ad8a54882111dd3c5c7ca36" + +lodash._basetostring@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/lodash._basetostring/-/lodash._basetostring-3.0.1.tgz#d1861d877f824a52f669832dcaf3ee15566a07d5" + +lodash._basevalues@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/lodash._basevalues/-/lodash._basevalues-3.0.0.tgz#5b775762802bde3d3297503e26300820fdf661b7" + +lodash._bindcallback@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/lodash._bindcallback/-/lodash._bindcallback-3.0.1.tgz#e531c27644cf8b57a99e17ed95b35c748789392e" + +lodash._createassigner@^3.0.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/lodash._createassigner/-/lodash._createassigner-3.1.1.tgz#838a5bae2fdaca63ac22dee8e19fa4e6d6970b11" + dependencies: + lodash._bindcallback "^3.0.0" + lodash._isiterateecall "^3.0.0" + lodash.restparam "^3.0.0" + +lodash._getnative@^3.0.0: + version "3.9.1" + resolved "https://registry.yarnpkg.com/lodash._getnative/-/lodash._getnative-3.9.1.tgz#570bc7dede46d61cdcde687d65d3eecbaa3aaff5" + +lodash._isiterateecall@^3.0.0: + version "3.0.9" + resolved "https://registry.yarnpkg.com/lodash._isiterateecall/-/lodash._isiterateecall-3.0.9.tgz#5203ad7ba425fae842460e696db9cf3e6aac057c" + +lodash._reescape@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/lodash._reescape/-/lodash._reescape-3.0.0.tgz#2b1d6f5dfe07c8a355753e5f27fac7f1cde1616a" + +lodash._reevaluate@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/lodash._reevaluate/-/lodash._reevaluate-3.0.0.tgz#58bc74c40664953ae0b124d806996daca431e2ed" + +lodash._reinterpolate@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz#0ccf2d89166af03b3663c796538b75ac6e114d9d" + +lodash._root@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/lodash._root/-/lodash._root-3.0.1.tgz#fba1c4524c19ee9a5f8136b4609f017cf4ded692" + +lodash.assign@^3.0.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/lodash.assign/-/lodash.assign-3.2.0.tgz#3ce9f0234b4b2223e296b8fa0ac1fee8ebca64fa" + dependencies: + lodash._baseassign "^3.0.0" + lodash._createassigner "^3.0.0" + lodash.keys "^3.0.0" + +lodash.camelcase@4.3.0, lodash.camelcase@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6" + +lodash.clone@^4.3.2: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.clone/-/lodash.clone-4.5.0.tgz#195870450f5a13192478df4bc3d23d2dea1907b6" + +lodash.defaults@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/lodash.defaults/-/lodash.defaults-4.2.0.tgz#d09178716ffea4dde9e5fb7b37f6f0802274580c" + +lodash.escape@^3.0.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/lodash.escape/-/lodash.escape-3.2.0.tgz#995ee0dc18c1b48cc92effae71a10aab5b487698" + dependencies: + lodash._root "^3.0.0" + +lodash.isarguments@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz#2f573d85c6a24289ff00663b491c1d338ff3458a" + +lodash.isarray@^3.0.0: + version "3.0.4" + resolved "https://registry.yarnpkg.com/lodash.isarray/-/lodash.isarray-3.0.4.tgz#79e4eb88c36a8122af86f844aa9bcd851b5fbb55" + +lodash.isplainobject@^4.0.4: + version "4.0.6" + resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb" + +lodash.isstring@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/lodash.isstring/-/lodash.isstring-4.0.1.tgz#d527dfb5456eca7cc9bb95d5daeaf88ba54a5451" + +lodash.kebabcase@4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/lodash.kebabcase/-/lodash.kebabcase-4.1.1.tgz#8489b1cb0d29ff88195cceca448ff6d6cc295c36" + +lodash.keys@^3.0.0: + version "3.1.2" + resolved "https://registry.yarnpkg.com/lodash.keys/-/lodash.keys-3.1.2.tgz#4dbc0472b156be50a0b286855d1bd0b0c656098a" + dependencies: + lodash._getnative "^3.0.0" + lodash.isarguments "^3.0.0" + lodash.isarray "^3.0.0" + +lodash.mapvalues@^4.4.0: + version "4.6.0" + resolved "https://registry.yarnpkg.com/lodash.mapvalues/-/lodash.mapvalues-4.6.0.tgz#1bafa5005de9dd6f4f26668c30ca37230cc9689c" + +lodash.memoize@^4.1.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe" + +lodash.restparam@^3.0.0: + version "3.6.1" + resolved "https://registry.yarnpkg.com/lodash.restparam/-/lodash.restparam-3.6.1.tgz#936a4e309ef330a7645ed4145986c85ae5b20805" + +lodash.snakecase@4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/lodash.snakecase/-/lodash.snakecase-4.1.1.tgz#39d714a35357147837aefd64b5dcbb16becd8f8d" + +lodash.some@^4.2.2: + version "4.6.0" + resolved "https://registry.yarnpkg.com/lodash.some/-/lodash.some-4.6.0.tgz#1bb9f314ef6b8baded13b549169b2a945eb68e4d" + +lodash.template@^3.0.0: + version "3.6.2" + resolved "https://registry.yarnpkg.com/lodash.template/-/lodash.template-3.6.2.tgz#f8cdecc6169a255be9098ae8b0c53d378931d14f" + dependencies: + lodash._basecopy "^3.0.0" + lodash._basetostring "^3.0.0" + lodash._basevalues "^3.0.0" + lodash._isiterateecall "^3.0.0" + lodash._reinterpolate "^3.0.0" + lodash.escape "^3.0.0" + lodash.keys "^3.0.0" + lodash.restparam "^3.0.0" + lodash.templatesettings "^3.0.0" + +lodash.templatesettings@^3.0.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/lodash.templatesettings/-/lodash.templatesettings-3.1.1.tgz#fb307844753b66b9f1afa54e262c745307dba8e5" + dependencies: + lodash._reinterpolate "^3.0.0" + lodash.escape "^3.0.0" + +lodash.uniq@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773" + +lodash.upperfirst@4.3.1: + version "4.3.1" + resolved "https://registry.yarnpkg.com/lodash.upperfirst/-/lodash.upperfirst-4.3.1.tgz#1365edf431480481ef0d1c68957a5ed99d49f7ce" + +lodash@4.17.4, lodash@^4.0.0, lodash@^4.11.1, lodash@^4.13.1, lodash@^4.14.0, lodash@^4.17.4, lodash@^4.2.0, lodash@^4.2.1, lodash@^4.3.0: + version "4.17.4" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae" + +lodash@~1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-1.0.2.tgz#8f57560c83b59fc270bd3d561b690043430e2551" + +log-symbols@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-1.0.2.tgz#376ff7b58ea3086a0f09facc74617eca501e1a18" + dependencies: + chalk "^1.0.0" + +log-symbols@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-2.0.0.tgz#595e63be4d5c8cbf294a9e09e0d5629f5913fc0c" + dependencies: + chalk "^2.0.1" + +longest@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/longest/-/longest-1.0.1.tgz#30a0b2da38f73770e8294a0d22e6625ed77d0097" + +loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.2.0, loose-envify@^1.3.0, loose-envify@^1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.3.1.tgz#d1a8ad33fa9ce0e713d65fdd0ac8b748d478c848" + dependencies: + js-tokens "^3.0.0" + +loud-rejection@^1.0.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/loud-rejection/-/loud-rejection-1.6.0.tgz#5b46f80147edee578870f086d04821cf998e551f" + dependencies: + currently-unhandled "^0.4.1" + signal-exit "^3.0.0" + +lru-cache@2: + version "2.7.3" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-2.7.3.tgz#6d4524e8b955f95d4f5b58851ce21dd72fb4e952" + +lru-cache@^4.0.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.1.tgz#622e32e82488b49279114a4f9ecf45e7cd6bba55" + dependencies: + pseudomap "^1.0.2" + yallist "^2.1.2" + +lru-queue@0.1: + version "0.1.0" + resolved "https://registry.yarnpkg.com/lru-queue/-/lru-queue-0.1.0.tgz#2738bd9f0d3cf4f84490c5736c48699ac632cda3" + dependencies: + es5-ext "~0.10.2" + +macaddress@^0.2.8: + version "0.2.8" + resolved "https://registry.yarnpkg.com/macaddress/-/macaddress-0.2.8.tgz#5904dc537c39ec6dbefeae902327135fa8511f12" + +make-dir@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-1.0.0.tgz#97a011751e91dd87cfadef58832ebb04936de978" + dependencies: + pify "^2.3.0" + +makeerror@1.0.x: + version "1.0.11" + resolved "https://registry.yarnpkg.com/makeerror/-/makeerror-1.0.11.tgz#e01a5c9109f2af79660e4e8b9587790184f5a96c" + dependencies: + tmpl "1.0.x" + +map-cache@^0.2.0: + version "0.2.2" + resolved "https://registry.yarnpkg.com/map-cache/-/map-cache-0.2.2.tgz#c32abd0bd6525d9b051645bb4f26ac5dc98a0dbf" + +map-obj@^1.0.0, map-obj@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-1.0.1.tgz#d933ceb9205d82bdcf4886f6742bdc2b4dea146d" + +map-stream@~0.0.6: + version "0.0.7" + resolved "https://registry.yarnpkg.com/map-stream/-/map-stream-0.0.7.tgz#8a1f07896d82b10926bd3744a2420009f88974a8" + +map-stream@~0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/map-stream/-/map-stream-0.1.0.tgz#e56aa94c4c8055a16404a0674b78f215f7c8e194" + +math-expression-evaluator@^1.2.14: + version "1.2.17" + resolved "https://registry.yarnpkg.com/math-expression-evaluator/-/math-expression-evaluator-1.2.17.tgz#de819fdbcd84dccd8fae59c6aeb79615b9d266ac" + +mathml-tag-names@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/mathml-tag-names/-/mathml-tag-names-2.0.1.tgz#8d41268168bf86d1102b98109e28e531e7a34578" + +md5.js@^1.3.4: + version "1.3.4" + resolved "https://registry.yarnpkg.com/md5.js/-/md5.js-1.3.4.tgz#e9bdbde94a20a5ac18b04340fc5764d5b09d901d" + dependencies: + hash-base "^3.0.0" + inherits "^2.0.1" + +media-typer@0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" + +mem@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/mem/-/mem-1.1.0.tgz#5edd52b485ca1d900fe64895505399a0dfa45f76" + dependencies: + mimic-fn "^1.0.0" + +memoizee@^0.4.5: + version "0.4.11" + resolved "https://registry.yarnpkg.com/memoizee/-/memoizee-0.4.11.tgz#bde9817663c9e40fdb2a4ea1c367296087ae8c8f" + dependencies: + d "1" + es5-ext "^0.10.30" + es6-weak-map "^2.0.2" + event-emitter "^0.3.5" + is-promise "^2.1" + lru-queue "0.1" + next-tick "1" + timers-ext "^0.1.2" + +memory-fs@^0.4.0, memory-fs@^0.4.1, memory-fs@~0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/memory-fs/-/memory-fs-0.4.1.tgz#3a9a20b8462523e447cfbc7e8bb80ed667bfc552" + dependencies: + errno "^0.1.3" + readable-stream "^2.0.1" + +meow@^3.7.0: + version "3.7.0" + resolved "https://registry.yarnpkg.com/meow/-/meow-3.7.0.tgz#72cb668b425228290abbfa856892587308a801fb" + dependencies: + camelcase-keys "^2.0.0" + decamelize "^1.1.2" + loud-rejection "^1.0.0" + map-obj "^1.0.1" + minimist "^1.1.3" + normalize-package-data "^2.3.4" + object-assign "^4.0.1" + read-pkg-up "^1.0.1" + redent "^1.0.0" + trim-newlines "^1.0.0" + +merge@^1.1.3: + version "1.2.0" + resolved "https://registry.yarnpkg.com/merge/-/merge-1.2.0.tgz#7531e39d4949c281a66b8c5a6e0265e8b05894da" + +micromatch@^2.1.5, micromatch@^2.3.11, micromatch@^2.3.7: + version "2.3.11" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-2.3.11.tgz#86677c97d1720b363431d04d0d15293bd38c1565" + dependencies: + arr-diff "^2.0.0" + array-unique "^0.2.1" + braces "^1.8.2" + expand-brackets "^0.1.4" + extglob "^0.3.1" + filename-regex "^2.0.0" + is-extglob "^1.0.0" + is-glob "^2.0.1" + kind-of "^3.0.2" + normalize-path "^2.0.1" + object.omit "^2.0.0" + parse-glob "^3.0.4" + regex-cache "^0.4.2" + +miller-rabin@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/miller-rabin/-/miller-rabin-4.0.0.tgz#4a62fb1d42933c05583982f4c716f6fb9e6c6d3d" + dependencies: + bn.js "^4.0.0" + brorand "^1.0.1" + +mime-db@~1.30.0: + version "1.30.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.30.0.tgz#74c643da2dd9d6a45399963465b26d5ca7d71f01" + +mime-types@^2.1.12, mime-types@~2.1.15, mime-types@~2.1.17: + version "2.1.17" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.17.tgz#09d7a393f03e995a79f8af857b70a9e0ab16557a" + dependencies: + mime-db "~1.30.0" + +mime@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/mime/-/mime-1.4.1.tgz#121f9ebc49e3766f311a76e1fa1c8003c4b03aa6" + +mimic-fn@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.1.0.tgz#e667783d92e89dbd342818b5230b9d62a672ad18" + +mini-lr@^0.1.8: + version "0.1.9" + resolved "https://registry.yarnpkg.com/mini-lr/-/mini-lr-0.1.9.tgz#02199d27347953d1fd1d6dbded4261f187b2d0f6" + dependencies: + body-parser "~1.14.0" + debug "^2.2.0" + faye-websocket "~0.7.2" + livereload-js "^2.2.0" + parseurl "~1.3.0" + qs "~2.2.3" + +minimalistic-assert@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.0.tgz#702be2dda6b37f4836bcb3f5db56641b64a1d3d3" + +minimalistic-crypto-utils@^1.0.0, minimalistic-crypto-utils@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a" + +"minimatch@2 || 3", minimatch@^2.0.1: + version "2.0.10" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-2.0.10.tgz#8d087c39c6b38c001b97fca7ce6d0e1e80afbac7" + dependencies: + brace-expansion "^1.0.0" + +minimatch@^3.0.0, minimatch@^3.0.2, minimatch@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" + dependencies: + brace-expansion "^1.1.7" + +minimatch@~0.2.11: + version "0.2.14" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-0.2.14.tgz#c74e780574f63c6f9a090e90efbe6ef53a6a756a" + dependencies: + lru-cache "2" + sigmund "~1.0.0" + +minimist@0.0.8: + version "0.0.8" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" + +minimist@^1.1.0, minimist@^1.1.1, minimist@^1.1.3, minimist@^1.2.0, minimist@~1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284" + +mkdirp@0.5.1, "mkdirp@>=0.5 0", mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@~0.5.0, mkdirp@~0.5.1: + version "0.5.1" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" + dependencies: + minimist "0.0.8" + +mobile-detect@1.3.7: + version "1.3.7" + resolved "https://registry.yarnpkg.com/mobile-detect/-/mobile-detect-1.3.7.tgz#c1aa7e6617ae1bfd27b511f55022ac40eea97370" + +moment@2.18.1: + version "2.18.1" + resolved "https://registry.yarnpkg.com/moment/-/moment-2.18.1.tgz#c36193dd3ce1c2eed2adb7c802dbbc77a81b1c0f" + +mousetrap@1.6.1: + version "1.6.1" + resolved "https://registry.yarnpkg.com/mousetrap/-/mousetrap-1.6.1.tgz#2a085f5c751294c75e7e81f6ec2545b29cbf42d9" + +"mout@>=0.9 <2.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/mout/-/mout-1.0.0.tgz#9bdf1d4af57d66d47cb353a6335a3281098e1501" + +mout@^0.11.0: + version "0.11.1" + resolved "https://registry.yarnpkg.com/mout/-/mout-0.11.1.tgz#ba3611df5f0e5b1ffbfd01166b8f02d1f5fa2b99" + +ms@0.7.1: + version "0.7.1" + resolved "https://registry.yarnpkg.com/ms/-/ms-0.7.1.tgz#9cd13c03adbff25b65effde7ce864ee952017098" + +ms@0.7.2: + version "0.7.2" + resolved "https://registry.yarnpkg.com/ms/-/ms-0.7.2.tgz#ae25cf2512b3885a1d95d7f037868d8431124765" + +ms@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" + +multipipe@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/multipipe/-/multipipe-0.1.2.tgz#2a8f2ddf70eed564dff2d57f1e1a137d9f05078b" + dependencies: + duplexer2 "0.0.2" + +mute-stream@0.0.7: + version "0.0.7" + resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab" + +nan@^2.0.5, nan@^2.3.0: + version "2.7.0" + resolved "https://registry.yarnpkg.com/nan/-/nan-2.7.0.tgz#d95bf721ec877e08db276ed3fc6eb78f9083ad46" + +natives@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/natives/-/natives-1.1.0.tgz#e9ff841418a6b2ec7a495e939984f78f163e6e31" + +natural-compare@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" + +new-from@0.0.3: + version "0.0.3" + resolved "https://registry.yarnpkg.com/new-from/-/new-from-0.0.3.tgz#1c4ad13613de3e15d6321b70ed5c23937ea25e67" + dependencies: + readable-stream "~1.1.8" + +next-tick@1: + version "1.0.0" + resolved "https://registry.yarnpkg.com/next-tick/-/next-tick-1.0.0.tgz#ca86d1fe8828169b0120208e3dc8424b9db8342c" + +node-fetch@^1.0.1: + version "1.7.3" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-1.7.3.tgz#980f6f72d85211a5347c6b2bc18c5b84c3eb47ef" + dependencies: + encoding "^0.1.11" + is-stream "^1.0.1" + +node-int64@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b" + +node-libs-browser@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/node-libs-browser/-/node-libs-browser-2.0.0.tgz#a3a59ec97024985b46e958379646f96c4b616646" + dependencies: + assert "^1.1.1" + browserify-zlib "^0.1.4" + buffer "^4.3.0" + console-browserify "^1.1.0" + constants-browserify "^1.0.0" + crypto-browserify "^3.11.0" + domain-browser "^1.1.1" + events "^1.0.0" + https-browserify "0.0.1" + os-browserify "^0.2.0" + path-browserify "0.0.0" + process "^0.11.0" + punycode "^1.2.4" + querystring-es3 "^0.2.0" + readable-stream "^2.0.5" + stream-browserify "^2.0.1" + stream-http "^2.3.1" + string_decoder "^0.10.25" + timers-browserify "^2.0.2" + tty-browserify "0.0.0" + url "^0.11.0" + util "^0.10.3" + vm-browserify "0.0.4" + +node-pre-gyp@^0.6.36: + version "0.6.37" + resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.6.37.tgz#3c872b236b2e266e4140578fe1ee88f693323a05" + dependencies: + mkdirp "^0.5.1" + nopt "^4.0.1" + npmlog "^4.0.2" + rc "^1.1.7" + request "^2.81.0" + rimraf "^2.6.1" + semver "^5.3.0" + tape "^4.6.3" + tar "^2.2.1" + tar-pack "^3.4.0" + +node.extend@^1.1.2: + version "1.1.6" + resolved "https://registry.yarnpkg.com/node.extend/-/node.extend-1.1.6.tgz#a7b882c82d6c93a4863a5504bd5de8ec86258b96" + dependencies: + is "^3.1.0" + +nopt@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/nopt/-/nopt-4.0.1.tgz#d0d4685afd5415193c8c7505602d0d17cd64474d" + dependencies: + abbrev "1" + osenv "^0.1.4" + +normalize-package-data@^2.3.2, normalize-package-data@^2.3.4: + version "2.4.0" + resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.4.0.tgz#12f95a307d58352075a04907b84ac8be98ac012f" + dependencies: + hosted-git-info "^2.1.4" + is-builtin-module "^1.0.0" + semver "2 || 3 || 4 || 5" + validate-npm-package-license "^3.0.1" + +normalize-path@^2.0.0, normalize-path@^2.0.1, normalize-path@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-2.1.1.tgz#1ab28b556e198363a8c1a6f7e6fa20137fe6aed9" + dependencies: + remove-trailing-separator "^1.0.1" + +normalize-range@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/normalize-range/-/normalize-range-0.1.2.tgz#2d10c06bdfd312ea9777695a4d28439456b75942" + +normalize-selector@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/normalize-selector/-/normalize-selector-0.2.0.tgz#d0b145eb691189c63a78d201dc4fdb1293ef0c03" + +normalize-url@^1.4.0: + version "1.9.1" + resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-1.9.1.tgz#2cc0d66b31ea23036458436e3620d85954c66c3c" + dependencies: + object-assign "^4.0.1" + prepend-http "^1.0.0" + query-string "^4.1.0" + sort-keys "^1.0.0" + +normalize.css@7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/normalize.css/-/normalize.css-7.0.0.tgz#abfb1dd82470674e0322b53ceb1aaf412938e4bf" + +npm-path@^1.0.0, npm-path@^1.0.1: + version "1.1.0" + resolved "https://registry.yarnpkg.com/npm-path/-/npm-path-1.1.0.tgz#0474ae00419c327d54701b7cf2cd05dc88be1140" + dependencies: + which "^1.2.4" + +npm-run-path@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f" + dependencies: + path-key "^2.0.0" + +npm-run@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/npm-run/-/npm-run-3.0.0.tgz#568920f840a98fd8e2299db66b2616e2476caf69" + dependencies: + minimist "^1.1.1" + npm-path "^1.0.1" + npm-which "^2.0.0" + serializerr "^1.0.1" + sync-exec "^0.6.2" + +npm-which@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/npm-which/-/npm-which-2.0.0.tgz#0c46982160b783093661d1d01bd4496d2feabbac" + dependencies: + commander "^2.2.0" + npm-path "^1.0.0" + which "^1.0.5" + +npmlog@^4.0.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.1.2.tgz#08a7f2a8bf734604779a9efa4ad5cc717abb954b" + dependencies: + are-we-there-yet "~1.1.2" + console-control-strings "~1.1.0" + gauge "~2.7.3" + set-blocking "~2.0.0" + +nsdeclare@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/nsdeclare/-/nsdeclare-0.1.0.tgz#10daa153642382d3cf2c01a916f4eb20a128b19f" + +num2fraction@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/num2fraction/-/num2fraction-1.2.2.tgz#6f682b6a027a4e9ddfa4564cd2589d1d4e669ede" + +number-is-nan@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d" + +oauth-sign@~0.8.2: + version "0.8.2" + resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.8.2.tgz#46a6ab7f0aead8deae9ec0565780b7d4efeb9d43" + +object-assign@4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.0.tgz#7a3b3d0e98063d43f4c03f2e8ae6cd51a86883a0" + +object-assign@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-3.0.0.tgz#9bedd5ca0897949bca47e7ff408062d549f587f2" + +object-assign@^4.0.1, object-assign@^4.1.0, object-assign@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" + +object-hash@^1.1.4: + version "1.1.8" + resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-1.1.8.tgz#28a659cf987d96a4dabe7860289f3b5326c4a03c" + +object-inspect@~1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.3.0.tgz#5b1eb8e6742e2ee83342a637034d844928ba2f6d" + +object-keys@^1.0.8: + version "1.0.11" + resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.0.11.tgz#c54601778ad560f1142ce0e01bcca8b56d13426d" + +object-keys@~0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-0.4.0.tgz#28a6aae7428dd2c3a92f3d95f21335dd204e0336" + +object.defaults@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/object.defaults/-/object.defaults-1.1.0.tgz#3a7f868334b407dea06da16d88d5cd29e435fecf" + dependencies: + array-each "^1.0.1" + array-slice "^1.0.0" + for-own "^1.0.0" + isobject "^3.0.0" + +object.omit@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/object.omit/-/object.omit-2.0.1.tgz#1a9c744829f39dbb858c76ca3579ae2a54ebd1fa" + dependencies: + for-own "^0.1.4" + is-extendable "^0.1.1" + +object.pick@^1.2.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/object.pick/-/object.pick-1.3.0.tgz#87a10ac4c1694bd2e1cbf53591a66141fb5dd747" + dependencies: + isobject "^3.0.1" + +on-finished@~2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" + dependencies: + ee-first "1.1.1" + +once@^1.3.0, once@^1.3.3: + version "1.4.0" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + dependencies: + wrappy "1" + +once@~1.3.0: + version "1.3.3" + resolved "https://registry.yarnpkg.com/once/-/once-1.3.3.tgz#b2e261557ce4c314ec8304f3fa82663e4297ca20" + dependencies: + wrappy "1" + +onetime@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/onetime/-/onetime-2.0.1.tgz#067428230fd67443b2794b22bba528b6867962d4" + dependencies: + mimic-fn "^1.0.0" + +optionator@^0.8.2: + version "0.8.2" + resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.2.tgz#364c5e409d3f4d6301d6c0b4c05bba50180aeb64" + dependencies: + deep-is "~0.1.3" + fast-levenshtein "~2.0.4" + levn "~0.3.0" + prelude-ls "~1.1.2" + type-check "~0.3.2" + wordwrap "~1.0.0" + +orchestrator@^0.3.0: + version "0.3.8" + resolved "https://registry.yarnpkg.com/orchestrator/-/orchestrator-0.3.8.tgz#14e7e9e2764f7315fbac184e506c7aa6df94ad7e" + dependencies: + end-of-stream "~0.1.5" + sequencify "~0.0.7" + stream-consume "~0.1.0" + +ordered-read-streams@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/ordered-read-streams/-/ordered-read-streams-0.1.0.tgz#fd565a9af8eb4473ba69b6ed8a34352cb552f126" + +os-browserify@^0.2.0: + version "0.2.1" + resolved "https://registry.yarnpkg.com/os-browserify/-/os-browserify-0.2.1.tgz#63fc4ccee5d2d7763d26bbf8601078e6c2e0044f" + +os-homedir@^1.0.0, os-homedir@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3" + +os-locale@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/os-locale/-/os-locale-2.1.0.tgz#42bc2900a6b5b8bd17376c8e882b65afccf24bf2" + dependencies: + execa "^0.7.0" + lcid "^1.0.0" + mem "^1.1.0" + +os-tmpdir@^1.0.0, os-tmpdir@^1.0.1, os-tmpdir@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" + +osenv@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/osenv/-/osenv-0.1.4.tgz#42fe6d5953df06c8064be6f176c3d05aaaa34644" + dependencies: + os-homedir "^1.0.0" + os-tmpdir "^1.0.0" + +p-finally@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae" + +p-limit@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-1.1.0.tgz#b07ff2d9a5d88bec806035895a2bab66a27988bc" + +p-locate@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-2.0.0.tgz#20a0103b222a70c8fd39cc2e580680f3dde5ec43" + dependencies: + p-limit "^1.1.0" + +p-map@^1.1.1: + version "1.2.0" + resolved "https://registry.yarnpkg.com/p-map/-/p-map-1.2.0.tgz#e4e94f311eabbc8633a1e79908165fca26241b6b" + +pako@~0.2.0: + version "0.2.9" + resolved "https://registry.yarnpkg.com/pako/-/pako-0.2.9.tgz#f3f7522f4ef782348da8161bad9ecfd51bf83a75" + +parse-asn1@^5.0.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/parse-asn1/-/parse-asn1-5.1.0.tgz#37c4f9b7ed3ab65c74817b5f2480937fbf97c712" + dependencies: + asn1.js "^4.0.0" + browserify-aes "^1.0.0" + create-hash "^1.1.0" + evp_bytestokey "^1.0.0" + pbkdf2 "^3.0.3" + +parse-filepath@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/parse-filepath/-/parse-filepath-1.0.1.tgz#159d6155d43904d16c10ef698911da1e91969b73" + dependencies: + is-absolute "^0.2.3" + map-cache "^0.2.0" + path-root "^0.1.1" + +parse-glob@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/parse-glob/-/parse-glob-3.0.4.tgz#b2c376cfb11f35513badd173ef0bb6e3a388391c" + dependencies: + glob-base "^0.3.0" + is-dotfile "^1.0.0" + is-extglob "^1.0.0" + is-glob "^2.0.0" + +parse-json@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-2.2.0.tgz#f480f40434ef80741f8469099f8dea18f55a4dc9" + dependencies: + error-ex "^1.2.0" + +parse-json@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-3.0.0.tgz#fa6f47b18e23826ead32f263e744d0e1e847fb13" + dependencies: + error-ex "^1.3.1" + +parse-passwd@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/parse-passwd/-/parse-passwd-1.0.0.tgz#6d5b934a456993b23d37f40a382d6f1666a8e5c6" + +parseurl@~1.3.0: + version "1.3.2" + resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.2.tgz#fc289d4ed8993119460c156253262cdc8de65bf3" + +path-browserify@0.0.0: + version "0.0.0" + resolved "https://registry.yarnpkg.com/path-browserify/-/path-browserify-0.0.0.tgz#a0b870729aae214005b7d5032ec2cbbb0fb4451a" + +path-dirname@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/path-dirname/-/path-dirname-1.0.2.tgz#cc33d24d525e099a5388c0336c6e32b9160609e0" + +path-exists@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-2.1.0.tgz#0feb6c64f0fc518d9a754dd5efb62c7022761f4b" + dependencies: + pinkie-promise "^2.0.0" + +path-exists@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515" + +path-is-absolute@^1.0.0, path-is-absolute@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + +path-is-inside@^1.0.1, path-is-inside@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/path-is-inside/-/path-is-inside-1.0.2.tgz#365417dede44430d1c11af61027facf074bdfc53" + +path-key@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40" + +path-parse@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.5.tgz#3c1adf871ea9cd6c9431b6ea2bd74a0ff055c4c1" + +path-root-regex@^0.1.0: + version "0.1.2" + resolved "https://registry.yarnpkg.com/path-root-regex/-/path-root-regex-0.1.2.tgz#bfccdc8df5b12dc52c8b43ec38d18d72c04ba96d" + +path-root@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/path-root/-/path-root-0.1.1.tgz#9a4a6814cac1c0cd73360a95f32083c8ea4745b7" + dependencies: + path-root-regex "^0.1.0" + +path-to-regexp@^1.7.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-1.7.0.tgz#59fde0f435badacba103a84e9d3bc64e96b9937d" + dependencies: + isarray "0.0.1" + +path-type@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/path-type/-/path-type-1.1.0.tgz#59c44f7ee491da704da415da5a4070ba4f8fe441" + dependencies: + graceful-fs "^4.1.2" + pify "^2.0.0" + pinkie-promise "^2.0.0" + +path-type@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/path-type/-/path-type-2.0.0.tgz#f012ccb8415b7096fc2daa1054c3d72389594c73" + dependencies: + pify "^2.0.0" + +pause-stream@0.0.11: + version "0.0.11" + resolved "https://registry.yarnpkg.com/pause-stream/-/pause-stream-0.0.11.tgz#fe5a34b0cbce12b5aa6a2b403ee2e73b602f1445" + dependencies: + through "~2.3" + +pbkdf2@^3.0.3: + version "3.0.14" + resolved "https://registry.yarnpkg.com/pbkdf2/-/pbkdf2-3.0.14.tgz#a35e13c64799b06ce15320f459c230e68e73bade" + dependencies: + create-hash "^1.1.2" + create-hmac "^1.1.4" + ripemd160 "^2.0.1" + safe-buffer "^5.0.1" + sha.js "^2.4.8" + +performance-now@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" + +pify@^2.0.0, pify@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" + +pify@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/pify/-/pify-3.0.0.tgz#e5a4acd2c101fdf3d9a4d07f0dbc4db49dd28176" + +pinkie-promise@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/pinkie-promise/-/pinkie-promise-2.0.1.tgz#2135d6dfa7a358c069ac9b178776288228450ffa" + dependencies: + pinkie "^2.0.0" + +pinkie@^2.0.0: + version "2.0.4" + resolved "https://registry.yarnpkg.com/pinkie/-/pinkie-2.0.4.tgz#72556b80cfa0d48a974e80e77248e80ed4f7f870" + +pkg-dir@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-1.0.0.tgz#7a4b508a8d5bb2d629d447056ff4e9c9314cf3d4" + dependencies: + find-up "^1.0.0" + +pkg-dir@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-2.0.0.tgz#f6d5d1109e19d63edf428e0bd57e12777615334b" + dependencies: + find-up "^2.1.0" + +pluralize@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-7.0.0.tgz#298b89df8b93b0221dbf421ad2b1b1ea23fc6777" + +postcss-calc@^5.2.0: + version "5.3.1" + resolved "https://registry.yarnpkg.com/postcss-calc/-/postcss-calc-5.3.1.tgz#77bae7ca928ad85716e2fda42f261bf7c1d65b5e" + dependencies: + postcss "^5.0.2" + postcss-message-helpers "^2.0.0" + reduce-css-calc "^1.2.6" + +postcss-colormin@^2.1.8: + version "2.2.2" + resolved "https://registry.yarnpkg.com/postcss-colormin/-/postcss-colormin-2.2.2.tgz#6631417d5f0e909a3d7ec26b24c8a8d1e4f96e4b" + dependencies: + colormin "^1.0.5" + postcss "^5.0.13" + postcss-value-parser "^3.2.3" + +postcss-convert-values@^2.3.4: + version "2.6.1" + resolved "https://registry.yarnpkg.com/postcss-convert-values/-/postcss-convert-values-2.6.1.tgz#bbd8593c5c1fd2e3d1c322bb925dcae8dae4d62d" + dependencies: + postcss "^5.0.11" + postcss-value-parser "^3.1.2" + +postcss-discard-comments@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/postcss-discard-comments/-/postcss-discard-comments-2.0.4.tgz#befe89fafd5b3dace5ccce51b76b81514be00e3d" + dependencies: + postcss "^5.0.14" + +postcss-discard-duplicates@^2.0.1: + version "2.1.0" + resolved "https://registry.yarnpkg.com/postcss-discard-duplicates/-/postcss-discard-duplicates-2.1.0.tgz#b9abf27b88ac188158a5eb12abcae20263b91932" + dependencies: + postcss "^5.0.4" + +postcss-discard-empty@^2.0.1: + version "2.1.0" + resolved "https://registry.yarnpkg.com/postcss-discard-empty/-/postcss-discard-empty-2.1.0.tgz#d2b4bd9d5ced5ebd8dcade7640c7d7cd7f4f92b5" + dependencies: + postcss "^5.0.14" + +postcss-discard-overridden@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/postcss-discard-overridden/-/postcss-discard-overridden-0.1.1.tgz#8b1eaf554f686fb288cd874c55667b0aa3668d58" + dependencies: + postcss "^5.0.16" + +postcss-discard-unused@^2.2.1: + version "2.2.3" + resolved "https://registry.yarnpkg.com/postcss-discard-unused/-/postcss-discard-unused-2.2.3.tgz#bce30b2cc591ffc634322b5fb3464b6d934f4433" + dependencies: + postcss "^5.0.14" + uniqs "^2.0.0" + +postcss-filter-plugins@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/postcss-filter-plugins/-/postcss-filter-plugins-2.0.2.tgz#6d85862534d735ac420e4a85806e1f5d4286d84c" + dependencies: + postcss "^5.0.4" + uniqid "^4.0.0" + +postcss-js@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/postcss-js/-/postcss-js-1.0.1.tgz#ffaf29226e399ea74b5dce02cab1729d7addbc7b" + dependencies: + camelcase-css "^1.0.1" + postcss "^6.0.11" + +postcss-less@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/postcss-less/-/postcss-less-1.1.0.tgz#bdcc76be64c4324d873fbc5cd9fa2e799e4305fa" + dependencies: + postcss "^5.2.16" + +postcss-load-config@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/postcss-load-config/-/postcss-load-config-1.2.0.tgz#539e9afc9ddc8620121ebf9d8c3673e0ce50d28a" + dependencies: + cosmiconfig "^2.1.0" + object-assign "^4.1.0" + postcss-load-options "^1.2.0" + postcss-load-plugins "^2.3.0" + +postcss-load-options@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/postcss-load-options/-/postcss-load-options-1.2.0.tgz#b098b1559ddac2df04bc0bb375f99a5cfe2b6d8c" + dependencies: + cosmiconfig "^2.1.0" + object-assign "^4.1.0" + +postcss-load-plugins@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/postcss-load-plugins/-/postcss-load-plugins-2.3.0.tgz#745768116599aca2f009fad426b00175049d8d92" + dependencies: + cosmiconfig "^2.1.1" + object-assign "^4.1.0" + +postcss-loader@2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/postcss-loader/-/postcss-loader-2.0.6.tgz#8c7e0055a3df1889abc6bad52dd45b2f41bbc6fc" + dependencies: + loader-utils "^1.1.0" + postcss "^6.0.2" + postcss-load-config "^1.2.0" + schema-utils "^0.3.0" + +postcss-media-query-parser@^0.2.3: + version "0.2.3" + resolved "https://registry.yarnpkg.com/postcss-media-query-parser/-/postcss-media-query-parser-0.2.3.tgz#27b39c6f4d94f81b1a73b8f76351c609e5cef244" + +postcss-merge-idents@^2.1.5: + version "2.1.7" + resolved "https://registry.yarnpkg.com/postcss-merge-idents/-/postcss-merge-idents-2.1.7.tgz#4c5530313c08e1d5b3bbf3d2bbc747e278eea270" + dependencies: + has "^1.0.1" + postcss "^5.0.10" + postcss-value-parser "^3.1.1" + +postcss-merge-longhand@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/postcss-merge-longhand/-/postcss-merge-longhand-2.0.2.tgz#23d90cd127b0a77994915332739034a1a4f3d658" + dependencies: + postcss "^5.0.4" + +postcss-merge-rules@^2.0.3: + version "2.1.2" + resolved "https://registry.yarnpkg.com/postcss-merge-rules/-/postcss-merge-rules-2.1.2.tgz#d1df5dfaa7b1acc3be553f0e9e10e87c61b5f721" + dependencies: + browserslist "^1.5.2" + caniuse-api "^1.5.2" + postcss "^5.0.4" + postcss-selector-parser "^2.2.2" + vendors "^1.0.0" + +postcss-message-helpers@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/postcss-message-helpers/-/postcss-message-helpers-2.0.0.tgz#a4f2f4fab6e4fe002f0aed000478cdf52f9ba60e" + +postcss-minify-font-values@^1.0.2: + version "1.0.5" + resolved "https://registry.yarnpkg.com/postcss-minify-font-values/-/postcss-minify-font-values-1.0.5.tgz#4b58edb56641eba7c8474ab3526cafd7bbdecb69" + dependencies: + object-assign "^4.0.1" + postcss "^5.0.4" + postcss-value-parser "^3.0.2" + +postcss-minify-gradients@^1.0.1: + version "1.0.5" + resolved "https://registry.yarnpkg.com/postcss-minify-gradients/-/postcss-minify-gradients-1.0.5.tgz#5dbda11373703f83cfb4a3ea3881d8d75ff5e6e1" + dependencies: + postcss "^5.0.12" + postcss-value-parser "^3.3.0" + +postcss-minify-params@^1.0.4: + version "1.2.2" + resolved "https://registry.yarnpkg.com/postcss-minify-params/-/postcss-minify-params-1.2.2.tgz#ad2ce071373b943b3d930a3fa59a358c28d6f1f3" + dependencies: + alphanum-sort "^1.0.1" + postcss "^5.0.2" + postcss-value-parser "^3.0.2" + uniqs "^2.0.0" + +postcss-minify-selectors@^2.0.4: + version "2.1.1" + resolved "https://registry.yarnpkg.com/postcss-minify-selectors/-/postcss-minify-selectors-2.1.1.tgz#b2c6a98c0072cf91b932d1a496508114311735bf" + dependencies: + alphanum-sort "^1.0.2" + has "^1.0.1" + postcss "^5.0.14" + postcss-selector-parser "^2.0.0" + +postcss-mixins@6.1.1: + version "6.1.1" + resolved "https://registry.yarnpkg.com/postcss-mixins/-/postcss-mixins-6.1.1.tgz#d77b9cfeab082d9674770e463757657ff7274d4e" + dependencies: + globby "^6.1.0" + postcss "^6.0.12" + postcss-js "^1.0.1" + postcss-simple-vars "^4.1.0" + sugarss "^1.0.0" + +postcss-modules-extract-imports@^1.0.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/postcss-modules-extract-imports/-/postcss-modules-extract-imports-1.2.0.tgz#66140ecece38ef06bf0d3e355d69bf59d141ea85" + dependencies: + postcss "^6.0.1" + +postcss-modules-local-by-default@^1.0.1: + version "1.2.0" + resolved "https://registry.yarnpkg.com/postcss-modules-local-by-default/-/postcss-modules-local-by-default-1.2.0.tgz#f7d80c398c5a393fa7964466bd19500a7d61c069" + dependencies: + css-selector-tokenizer "^0.7.0" + postcss "^6.0.1" + +postcss-modules-scope@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/postcss-modules-scope/-/postcss-modules-scope-1.1.0.tgz#d6ea64994c79f97b62a72b426fbe6056a194bb90" + dependencies: + css-selector-tokenizer "^0.7.0" + postcss "^6.0.1" + +postcss-modules-values@^1.1.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/postcss-modules-values/-/postcss-modules-values-1.3.0.tgz#ecffa9d7e192518389f42ad0e83f72aec456ea20" + dependencies: + icss-replace-symbols "^1.1.0" + postcss "^6.0.1" + +postcss-nested@2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/postcss-nested/-/postcss-nested-2.1.2.tgz#04057281f9631fef684857fb0119bae04ede03c6" + dependencies: + postcss "^6.0.9" + postcss-selector-parser "^2.2.3" + +postcss-normalize-charset@^1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/postcss-normalize-charset/-/postcss-normalize-charset-1.1.1.tgz#ef9ee71212d7fe759c78ed162f61ed62b5cb93f1" + dependencies: + postcss "^5.0.5" + +postcss-normalize-url@^3.0.7: + version "3.0.8" + resolved "https://registry.yarnpkg.com/postcss-normalize-url/-/postcss-normalize-url-3.0.8.tgz#108f74b3f2fcdaf891a2ffa3ea4592279fc78222" + dependencies: + is-absolute-url "^2.0.0" + normalize-url "^1.4.0" + postcss "^5.0.14" + postcss-value-parser "^3.2.3" + +postcss-ordered-values@^2.1.0: + version "2.2.3" + resolved "https://registry.yarnpkg.com/postcss-ordered-values/-/postcss-ordered-values-2.2.3.tgz#eec6c2a67b6c412a8db2042e77fe8da43f95c11d" + dependencies: + postcss "^5.0.4" + postcss-value-parser "^3.0.1" + +postcss-reduce-idents@^2.2.2: + version "2.4.0" + resolved "https://registry.yarnpkg.com/postcss-reduce-idents/-/postcss-reduce-idents-2.4.0.tgz#c2c6d20cc958284f6abfbe63f7609bf409059ad3" + dependencies: + postcss "^5.0.4" + postcss-value-parser "^3.0.2" + +postcss-reduce-initial@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/postcss-reduce-initial/-/postcss-reduce-initial-1.0.1.tgz#68f80695f045d08263a879ad240df8dd64f644ea" + dependencies: + postcss "^5.0.4" + +postcss-reduce-transforms@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/postcss-reduce-transforms/-/postcss-reduce-transforms-1.0.4.tgz#ff76f4d8212437b31c298a42d2e1444025771ae1" + dependencies: + has "^1.0.1" + postcss "^5.0.8" + postcss-value-parser "^3.0.1" + +postcss-reporter@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/postcss-reporter/-/postcss-reporter-5.0.0.tgz#a14177fd1342829d291653f2786efd67110332c3" + dependencies: + chalk "^2.0.1" + lodash "^4.17.4" + log-symbols "^2.0.0" + postcss "^6.0.8" + +postcss-resolve-nested-selector@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/postcss-resolve-nested-selector/-/postcss-resolve-nested-selector-0.1.1.tgz#29ccbc7c37dedfac304e9fff0bf1596b3f6a0e4e" + +postcss-safe-parser@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/postcss-safe-parser/-/postcss-safe-parser-3.0.1.tgz#b753eff6c7c0aea5e8375fbe4cde8bf9063ff142" + dependencies: + postcss "^6.0.6" + +postcss-scss@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/postcss-scss/-/postcss-scss-1.0.2.tgz#ff45cf3354b879ee89a4eb68680f46ac9bb14f94" + dependencies: + postcss "^6.0.3" + +postcss-selector-parser@^2.0.0, postcss-selector-parser@^2.2.2, postcss-selector-parser@^2.2.3: + version "2.2.3" + resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-2.2.3.tgz#f9437788606c3c9acee16ffe8d8b16297f27bb90" + dependencies: + flatten "^1.0.2" + indexes-of "^1.0.1" + uniq "^1.0.1" + +postcss-simple-vars@4.1.0, postcss-simple-vars@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/postcss-simple-vars/-/postcss-simple-vars-4.1.0.tgz#043248cfef8d3f51b3486a28c09f8375dbf1b2f9" + dependencies: + postcss "^6.0.9" + +postcss-sorting@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/postcss-sorting/-/postcss-sorting-3.0.2.tgz#73e6913b715426201d22e8a1769b05022a37aafc" + dependencies: + lodash "^4.17.4" + postcss "^6.0.11" + +postcss-svgo@^2.1.1: + version "2.1.6" + resolved "https://registry.yarnpkg.com/postcss-svgo/-/postcss-svgo-2.1.6.tgz#b6df18aa613b666e133f08adb5219c2684ac108d" + dependencies: + is-svg "^2.0.0" + postcss "^5.0.14" + postcss-value-parser "^3.2.3" + svgo "^0.7.0" + +postcss-unique-selectors@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/postcss-unique-selectors/-/postcss-unique-selectors-2.0.2.tgz#981d57d29ddcb33e7b1dfe1fd43b8649f933ca1d" + dependencies: + alphanum-sort "^1.0.1" + postcss "^5.0.4" + uniqs "^2.0.0" + +postcss-value-parser@^3.0.1, postcss-value-parser@^3.0.2, postcss-value-parser@^3.1.1, postcss-value-parser@^3.1.2, postcss-value-parser@^3.2.3, postcss-value-parser@^3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-3.3.0.tgz#87f38f9f18f774a4ab4c8a232f5c5ce8872a9d15" + +postcss-zindex@^2.0.1: + version "2.2.0" + resolved "https://registry.yarnpkg.com/postcss-zindex/-/postcss-zindex-2.2.0.tgz#d2109ddc055b91af67fc4cb3b025946639d2af22" + dependencies: + has "^1.0.1" + postcss "^5.0.4" + uniqs "^2.0.0" + +postcss@^5.0.10, postcss@^5.0.11, postcss@^5.0.12, postcss@^5.0.13, postcss@^5.0.14, postcss@^5.0.16, postcss@^5.0.19, postcss@^5.0.2, postcss@^5.0.4, postcss@^5.0.5, postcss@^5.0.6, postcss@^5.0.8, postcss@^5.2.16: + version "5.2.17" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-5.2.17.tgz#cf4f597b864d65c8a492b2eabe9d706c879c388b" + dependencies: + chalk "^1.1.3" + js-base64 "^2.1.9" + source-map "^0.5.6" + supports-color "^3.2.3" + +postcss@^6.0.0, postcss@^6.0.1, postcss@^6.0.11, postcss@^6.0.3, postcss@^6.0.6, postcss@^6.0.8: + version "6.0.11" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-6.0.11.tgz#f48db210b1d37a7f7ab6499b7a54982997ab6f72" + dependencies: + chalk "^2.1.0" + source-map "^0.5.7" + supports-color "^4.4.0" + +postcss@^6.0.12, postcss@^6.0.2, postcss@^6.0.9: + version "6.0.12" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-6.0.12.tgz#6b0155089d2d212f7bd6a0cecd4c58c007403535" + dependencies: + chalk "^2.1.0" + source-map "^0.5.7" + supports-color "^4.4.0" + +postcss@^6.0.13: + version "6.0.13" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-6.0.13.tgz#b9ecab4ee00c89db3ec931145bd9590bbf3f125f" + dependencies: + chalk "^2.1.0" + source-map "^0.6.1" + supports-color "^4.4.0" + +prefix-style@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/prefix-style/-/prefix-style-2.0.1.tgz#66bba9a870cfda308a5dc20e85e9120932c95a06" + +prelude-ls@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" + +prepend-http@^1.0.0: + version "1.0.4" + resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-1.0.4.tgz#d4f4562b0ce3696e41ac52d0e002e57a635dc6dc" + +preserve@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/preserve/-/preserve-0.2.0.tgz#815ed1f6ebc65926f865b310c0713bcb3315ce4b" + +pretty-hrtime@^1.0.0: + version "1.0.3" + resolved "https://registry.yarnpkg.com/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz#b7e3ea42435a4c9b2759d99e0f201eb195802ee1" + +private@^0.1.6, private@^0.1.7: + version "0.1.7" + resolved "https://registry.yarnpkg.com/private/-/private-0.1.7.tgz#68ce5e8a1ef0a23bb570cc28537b5332aba63ef1" + +process-nextick-args@^1.0.6, process-nextick-args@~1.0.6: + version "1.0.7" + resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-1.0.7.tgz#150e20b756590ad3f91093f25a4f2ad8bff30ba3" + +process@^0.11.0: + version "0.11.10" + resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182" + +progress@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.0.tgz#8a1be366bf8fc23db2bd23f10c6fe920b4389d1f" + +promise@^7.1.1: + version "7.3.1" + resolved "https://registry.yarnpkg.com/promise/-/promise-7.3.1.tgz#064b72602b18f90f29192b8b1bc418ffd1ebd3bf" + dependencies: + asap "~2.0.3" + +prop-types@15.6.0, prop-types@^15.5.7: + version "15.6.0" + resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.6.0.tgz#ceaf083022fc46b4a35f69e13ef75aed0d639856" + dependencies: + fbjs "^0.8.16" + loose-envify "^1.3.1" + object-assign "^4.1.1" + +prop-types@>=15.5.0, prop-types@^15.5.0, prop-types@^15.5.10, prop-types@^15.5.4, prop-types@^15.5.6, prop-types@^15.5.8, prop-types@~15.5.7: + version "15.5.10" + resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.5.10.tgz#2797dfc3126182e3a95e3dfbb2e893ddd7456154" + dependencies: + fbjs "^0.8.9" + loose-envify "^1.3.1" + +protochain@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/protochain/-/protochain-1.0.5.tgz#991c407e99de264aadf8f81504b5e7faf7bfa260" + +prr@~0.0.0: + version "0.0.0" + resolved "https://registry.yarnpkg.com/prr/-/prr-0.0.0.tgz#1a84b85908325501411853d0081ee3fa86e2926a" + +pseudomap@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3" + +public-encrypt@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/public-encrypt/-/public-encrypt-4.0.0.tgz#39f699f3a46560dd5ebacbca693caf7c65c18cc6" + dependencies: + bn.js "^4.1.0" + browserify-rsa "^4.0.0" + create-hash "^1.1.0" + parse-asn1 "^5.0.0" + randombytes "^2.0.1" + +punycode@1.3.2: + version "1.3.2" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.3.2.tgz#9653a036fb7c1ee42342f2325cceefea3926c48d" + +punycode@^1.2.4, punycode@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" + +q@^1.1.2: + version "1.5.0" + resolved "https://registry.yarnpkg.com/q/-/q-1.5.0.tgz#dd01bac9d06d30e6f219aecb8253ee9ebdc308f1" + +qs@5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-5.2.0.tgz#a9f31142af468cb72b25b30136ba2456834916be" + +qs@6.5.1, qs@~6.5.1: + version "6.5.1" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.1.tgz#349cdf6eef89ec45c12d7d5eb3fc0c870343a6d8" + +qs@~2.2.3: + version "2.2.5" + resolved "https://registry.yarnpkg.com/qs/-/qs-2.2.5.tgz#1088abaf9dcc0ae5ae45b709e6c6b5888b23923c" + +query-string@5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/query-string/-/query-string-5.0.0.tgz#fbdf7004b4d2aff792f9871981b7a2794f555947" + dependencies: + decode-uri-component "^0.2.0" + object-assign "^4.1.0" + strict-uri-encode "^1.0.0" + +query-string@^4.1.0: + version "4.2.2" + resolved "https://registry.npmjs.org/query-string/-/query-string-4.2.2.tgz#888a6fcb6f76070ba39f2f3025c87099defa1645" + dependencies: + object-assign "^4.1.0" + strict-uri-encode "^1.0.0" + +querystring-es3@^0.2.0: + version "0.2.1" + resolved "https://registry.yarnpkg.com/querystring-es3/-/querystring-es3-0.2.1.tgz#9ec61f79049875707d69414596fd907a4d711e73" + +querystring@0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.0.tgz#b209849203bb25df820da756e747005878521620" + +raf@^3.1.0: + version "3.3.2" + resolved "https://registry.yarnpkg.com/raf/-/raf-3.3.2.tgz#0c13be0b5b49b46f76d6669248d527cf2b02fe27" + dependencies: + performance-now "^2.1.0" + +randomatic@^1.1.3: + version "1.1.7" + resolved "https://registry.yarnpkg.com/randomatic/-/randomatic-1.1.7.tgz#c7abe9cc8b87c0baa876b19fde83fd464797e38c" + dependencies: + is-number "^3.0.0" + kind-of "^4.0.0" + +randombytes@^2.0.0, randombytes@^2.0.1: + version "2.0.5" + resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.0.5.tgz#dc009a246b8d09a177b4b7a0ae77bc570f4b1b79" + dependencies: + safe-buffer "^5.1.0" + +raven-for-redux@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/raven-for-redux/-/raven-for-redux-1.0.0.tgz#3671c2fd39b155b92e7013895806ff75ff2d5e96" + +raven-js@3.17.0: + version "3.17.0" + resolved "https://registry.yarnpkg.com/raven-js/-/raven-js-3.17.0.tgz#779457ac7910512c3c2cc9bb6d0a9eeb59a969ec" + +raw-body@~2.1.5: + version "2.1.7" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.1.7.tgz#adfeace2e4fb3098058014d08c072dcc59758774" + dependencies: + bytes "2.4.0" + iconv-lite "0.4.13" + unpipe "1.0.0" + +rc@^1.1.7: + version "1.2.1" + resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.1.tgz#2e03e8e42ee450b8cb3dce65be1bf8974e1dfd95" + dependencies: + deep-extend "~0.4.0" + ini "~1.3.0" + minimist "^1.2.0" + strip-json-comments "~2.0.1" + +react-addons-shallow-compare@15.6.2: + version "15.6.2" + resolved "https://registry.yarnpkg.com/react-addons-shallow-compare/-/react-addons-shallow-compare-15.6.2.tgz#198a00b91fc37623db64a28fd17b596ba362702f" + dependencies: + fbjs "^0.8.4" + object-assign "^4.1.0" + +react-async-script@0.9.1: + version "0.9.1" + resolved "https://registry.yarnpkg.com/react-async-script/-/react-async-script-0.9.1.tgz#d4a496c726fab0c6ef05998ab16e50c742820b60" + dependencies: + prop-types ">=15.5.0" + +react-autosuggest@9.3.2: + version "9.3.2" + resolved "https://registry.yarnpkg.com/react-autosuggest/-/react-autosuggest-9.3.2.tgz#dd8c0fbe9c25aa94afe296180353647f6ecc10a7" + dependencies: + prop-types "^15.5.10" + react-autowhatever "^10.1.0" + shallow-equal "^1.0.0" + +react-autowhatever@^10.1.0: + version "10.1.0" + resolved "https://registry.yarnpkg.com/react-autowhatever/-/react-autowhatever-10.1.0.tgz#41f6d69382437d3447a0a3c8913bb8ca2feaabc1" + dependencies: + prop-types "^15.5.8" + react-themeable "^1.1.0" + section-iterator "^2.0.0" + +react-custom-scrollbars@4.1.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/react-custom-scrollbars/-/react-custom-scrollbars-4.1.2.tgz#0e60c4a46c4a61f9e4994a7663e2b9cbbc5187a3" + dependencies: + dom-css "^2.0.0" + prop-types "^15.5.10" + raf "^3.1.0" + +react-dnd-html5-backend@2.5.4: + version "2.5.4" + resolved "https://registry.yarnpkg.com/react-dnd-html5-backend/-/react-dnd-html5-backend-2.5.4.tgz#974ad083f67b12d56977a5b171f5ffeb29d78352" + dependencies: + lodash "^4.2.0" + +react-dnd@2.5.4: + version "2.5.4" + resolved "https://registry.yarnpkg.com/react-dnd/-/react-dnd-2.5.4.tgz#0b6dc5e9d0dfc2909f4f4fe736e5534f3afd1bd9" + dependencies: + disposables "^1.0.1" + dnd-core "^2.5.4" + hoist-non-react-statics "^2.1.0" + invariant "^2.1.0" + lodash "^4.2.0" + prop-types "^15.5.10" + +react-document-title@2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/react-document-title/-/react-document-title-2.0.3.tgz#bbf922a0d71412fc948245e4283b2412df70f2b9" + dependencies: + prop-types "^15.5.6" + react-side-effect "^1.0.2" + +react-dom@15.6.0: + version "15.6.0" + resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-15.6.0.tgz#8bc23cb0c80e706355b76ca9f8ce47cf7bdfb6d1" + dependencies: + fbjs "^0.8.9" + loose-envify "^1.1.0" + object-assign "^4.1.0" + prop-types "~15.5.7" + +react-google-recaptcha@0.9.7: + version "0.9.7" + resolved "https://registry.yarnpkg.com/react-google-recaptcha/-/react-google-recaptcha-0.9.7.tgz#13ad944011d02566e8bd06ee8567ace21a1d0ed6" + dependencies: + prop-types ">=15.5.0" + +react-lazyload@2.2.7: + version "2.2.7" + resolved "https://registry.yarnpkg.com/react-lazyload/-/react-lazyload-2.2.7.tgz#6adf0713f32240a21c630ab5d1e8cf62d0c264af" + dependencies: + prop-types "^15.5.6" + +react-measure@1.4.7: + version "1.4.7" + resolved "https://registry.yarnpkg.com/react-measure/-/react-measure-1.4.7.tgz#a1d2ca0dcfef04978b7ac263a765dcb6a0936fdb" + dependencies: + get-node-dimensions "^1.2.0" + prop-types "^15.5.4" + resize-observer-polyfill "^1.4.1" + +react-portal@3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/react-portal/-/react-portal-3.1.0.tgz#865c44fb72a1da106c649206936559ce891ee899" + dependencies: + prop-types "^15.5.8" + +react-redux@5.0.6: + version "5.0.6" + resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-5.0.6.tgz#23ed3a4f986359d68b5212eaaa681e60d6574946" + dependencies: + hoist-non-react-statics "^2.2.1" + invariant "^2.0.0" + lodash "^4.2.0" + lodash-es "^4.2.0" + loose-envify "^1.1.0" + prop-types "^15.5.10" + +react-router-dom@4.2.2: + version "4.2.2" + resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-4.2.2.tgz#c8a81df3adc58bba8a76782e946cbd4eae649b8d" + dependencies: + history "^4.7.2" + invariant "^2.2.2" + loose-envify "^1.3.1" + prop-types "^15.5.4" + react-router "^4.2.0" + warning "^3.0.0" + +react-router-redux@5.0.0-alpha.6: + version "5.0.0-alpha.6" + resolved "https://registry.yarnpkg.com/react-router-redux/-/react-router-redux-5.0.0-alpha.6.tgz#7418663c2ecd3c51be856fcf28f3d1deecc1a576" + dependencies: + history "^4.5.1" + prop-types "^15.5.4" + react-router "^4.1.1" + +react-router@^4.1.1, react-router@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/react-router/-/react-router-4.2.0.tgz#61f7b3e3770daeb24062dae3eedef1b054155986" + dependencies: + history "^4.7.2" + hoist-non-react-statics "^2.3.0" + invariant "^2.2.2" + loose-envify "^1.3.1" + path-to-regexp "^1.7.0" + prop-types "^15.5.4" + warning "^3.0.0" + +react-side-effect@^1.0.2: + version "1.1.3" + resolved "https://registry.yarnpkg.com/react-side-effect/-/react-side-effect-1.1.3.tgz#512c25abe0dec172834c4001ec5c51e04d41bc5c" + dependencies: + exenv "^1.2.1" + shallowequal "^1.0.1" + +react-slider@0.9.0: + version "0.9.0" + resolved "https://registry.yarnpkg.com/react-slider/-/react-slider-0.9.0.tgz#584ae63325be115581fc08bf7753adb8e4d4a0b1" + +react-tabs@2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/react-tabs/-/react-tabs-2.1.0.tgz#ba184a519e0a0803cf790a1eb19bc4fdba5fd0ea" + dependencies: + classnames "^2.2.0" + prop-types "^15.5.0" + +react-tag-autocomplete@5.4.1: + version "5.4.1" + resolved "https://registry.yarnpkg.com/react-tag-autocomplete/-/react-tag-autocomplete-5.4.1.tgz#941266c73f26e32f151ed94fc9285258fdff0eff" + +react-tether@0.5.7: + version "0.5.7" + resolved "https://registry.yarnpkg.com/react-tether/-/react-tether-0.5.7.tgz#418ea61041b65b958271478489b71a3572f01422" + dependencies: + prop-types "^15.5.8" + tether "^1.3.7" + +react-text-truncate@0.12.0: + version "0.12.0" + resolved "https://registry.yarnpkg.com/react-text-truncate/-/react-text-truncate-0.12.0.tgz#489b199f218be48ab3df9be1df57845348f77e23" + dependencies: + prop-types "^15.5.7" + +react-themeable@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/react-themeable/-/react-themeable-1.1.0.tgz#7d4466dd9b2b5fa75058727825e9f152ba379a0e" + dependencies: + object-assign "^3.0.0" + +react-truncate@2.2.2: + version "2.2.2" + resolved "https://registry.yarnpkg.com/react-truncate/-/react-truncate-2.2.2.tgz#c13e420df82cf6d0385bb2a6eb10212743085550" + +react-virtualized@9.10.1: + version "9.10.1" + resolved "https://registry.yarnpkg.com/react-virtualized/-/react-virtualized-9.10.1.tgz#d32365d0edf49debbe25fbfe73b5f55f6d9d8c72" + dependencies: + babel-runtime "^6.23.0" + classnames "^2.2.3" + dom-helpers "^2.4.0 || ^3.0.0" + loose-envify "^1.3.0" + prop-types "^15.5.4" + +react@15.6.0: + version "15.6.0" + resolved "https://registry.yarnpkg.com/react/-/react-15.6.0.tgz#c23299b48e30ed302508ce89e1a02c919f826bce" + dependencies: + create-react-class "^15.5.2" + fbjs "^0.8.9" + loose-envify "^1.1.0" + object-assign "^4.1.0" + prop-types "^15.5.7" + +read-pkg-up@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-1.0.1.tgz#9d63c13276c065918d57f002a57f40a1b643fb02" + dependencies: + find-up "^1.0.0" + read-pkg "^1.0.0" + +read-pkg-up@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-2.0.0.tgz#6b72a8048984e0c41e79510fd5e9fa99b3b549be" + dependencies: + find-up "^2.0.0" + read-pkg "^2.0.0" + +read-pkg@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-1.1.0.tgz#f5ffaa5ecd29cb31c0474bca7d756b6bb29e3f28" + dependencies: + load-json-file "^1.0.0" + normalize-package-data "^2.3.2" + path-type "^1.0.0" + +read-pkg@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-2.0.0.tgz#8ef1c0623c6a6db0dc6713c4bfac46332b2368f8" + dependencies: + load-json-file "^2.0.0" + normalize-package-data "^2.3.2" + path-type "^2.0.0" + +"readable-stream@>=1.0.33-1 <1.1.0-0", readable-stream@~1.0.17, readable-stream@~1.0.33: + version "1.0.34" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.0.34.tgz#125820e34bc842d2f2aaafafe4c2916ee32c157c" + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.1" + isarray "0.0.1" + string_decoder "~0.10.x" + +readable-stream@^1.0.33, readable-stream@~1.1.8, readable-stream@~1.1.9: + version "1.1.14" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.1.14.tgz#7cf4c54ef648e3813084c636dd2079e166c081d9" + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.1" + isarray "0.0.1" + string_decoder "~0.10.x" + +readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.5, readable-stream@^2.0.6, readable-stream@^2.1.4, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.2.6: + version "2.3.3" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.3.tgz#368f2512d79f9d46fdfc71349ae7878bbc1eb95c" + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.3" + isarray "~1.0.0" + process-nextick-args "~1.0.6" + safe-buffer "~5.1.1" + string_decoder "~1.0.3" + util-deprecate "~1.0.1" + +readdirp@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-2.1.0.tgz#4ed0ad060df3073300c48440373f72d1cc642d78" + dependencies: + graceful-fs "^4.1.2" + minimatch "^3.0.2" + readable-stream "^2.0.2" + set-immediate-shim "^1.0.1" + +rechoir@^0.6.2: + version "0.6.2" + resolved "https://registry.yarnpkg.com/rechoir/-/rechoir-0.6.2.tgz#85204b54dba82d5742e28c96756ef43af50e3384" + dependencies: + resolve "^1.1.6" + +redent@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/redent/-/redent-1.0.0.tgz#cf916ab1fd5f1f16dfb20822dd6ec7f730c2afde" + dependencies: + indent-string "^2.1.0" + strip-indent "^1.0.1" + +reduce-css-calc@^1.2.6: + version "1.3.0" + resolved "https://registry.yarnpkg.com/reduce-css-calc/-/reduce-css-calc-1.3.0.tgz#747c914e049614a4c9cfbba629871ad1d2927716" + dependencies: + balanced-match "^0.4.2" + math-expression-evaluator "^1.2.14" + reduce-function-call "^1.0.1" + +reduce-function-call@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/reduce-function-call/-/reduce-function-call-1.0.2.tgz#5a200bf92e0e37751752fe45b0ab330fd4b6be99" + dependencies: + balanced-match "^0.4.2" + +reduce-reducers@^0.1.0: + version "0.1.2" + resolved "https://registry.yarnpkg.com/reduce-reducers/-/reduce-reducers-0.1.2.tgz#fa1b4718bc5292a71ddd1e5d839c9bea9770f14b" + +redux-actions@2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/redux-actions/-/redux-actions-2.2.1.tgz#d64186b25649a13c05478547d7cd7537b892410d" + dependencies: + invariant "^2.2.1" + lodash "^4.13.1" + lodash-es "^4.17.4" + reduce-reducers "^0.1.0" + +redux-batched-actions@0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/redux-batched-actions/-/redux-batched-actions-0.2.0.tgz#da0000c882b0e6c861a96d5823bd36adf5d9c0dd" + +redux-localstorage@0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/redux-localstorage/-/redux-localstorage-0.4.1.tgz#faf6d719c581397294d811473ffcedee065c933c" + +redux-thunk@2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/redux-thunk/-/redux-thunk-2.2.0.tgz#e615a16e16b47a19a515766133d1e3e99b7852e5" + +redux@3.7.2, redux@^3.7.1: + version "3.7.2" + resolved "https://registry.yarnpkg.com/redux/-/redux-3.7.2.tgz#06b73123215901d25d065be342eb026bc1c8537b" + dependencies: + lodash "^4.2.1" + lodash-es "^4.2.1" + loose-envify "^1.1.0" + symbol-observable "^1.0.3" + +regenerate@^1.2.1: + version "1.3.3" + resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.3.3.tgz#0c336d3980553d755c39b586ae3b20aa49c82b7f" + +regenerator-runtime@^0.11.0: + version "0.11.0" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.11.0.tgz#7e54fe5b5ccd5d6624ea6255c3473be090b802e1" + +regenerator-transform@^0.10.0: + version "0.10.1" + resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.10.1.tgz#1e4996837231da8b7f3cf4114d71b5691a0680dd" + dependencies: + babel-runtime "^6.18.0" + babel-types "^6.19.0" + private "^0.1.6" + +regex-cache@^0.4.2: + version "0.4.4" + resolved "https://registry.yarnpkg.com/regex-cache/-/regex-cache-0.4.4.tgz#75bdc58a2a1496cec48a12835bc54c8d562336dd" + dependencies: + is-equal-shallow "^0.1.3" + +regexpu-core@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-1.0.0.tgz#86a763f58ee4d7c2f6b102e4764050de7ed90c6b" + dependencies: + regenerate "^1.2.1" + regjsgen "^0.2.0" + regjsparser "^0.1.4" + +regexpu-core@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-2.0.0.tgz#49d038837b8dcf8bfa5b9a42139938e6ea2ae240" + dependencies: + regenerate "^1.2.1" + regjsgen "^0.2.0" + regjsparser "^0.1.4" + +regjsgen@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/regjsgen/-/regjsgen-0.2.0.tgz#6c016adeac554f75823fe37ac05b92d5a4edb1f7" + +regjsparser@^0.1.4: + version "0.1.5" + resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.1.5.tgz#7ee8f84dc6fa792d3fd0ae228d24bd949ead205c" + dependencies: + jsesc "~0.5.0" + +remove-trailing-separator@^1.0.1: + version "1.1.0" + resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz#c24bce2a283adad5bc3f58e0d48249b92379d8ef" + +repeat-element@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/repeat-element/-/repeat-element-1.1.2.tgz#ef089a178d1483baae4d93eb98b4f9e4e11d990a" + +repeat-string@^1.5.0, repeat-string@^1.5.2: + version "1.6.1" + resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637" + +repeating@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/repeating/-/repeating-2.0.1.tgz#5214c53a926d3552707527fbab415dbc08d06dda" + dependencies: + is-finite "^1.0.0" + +replace-ext@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/replace-ext/-/replace-ext-0.0.1.tgz#29bbd92078a739f0bcce2b4ee41e837953522924" + +replace-ext@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/replace-ext/-/replace-ext-1.0.0.tgz#de63128373fcbf7c3ccfa4de5a480c45a67958eb" + +request@^2.81.0: + version "2.82.0" + resolved "https://registry.yarnpkg.com/request/-/request-2.82.0.tgz#2ba8a92cd7ac45660ea2b10a53ae67cd247516ea" + dependencies: + aws-sign2 "~0.7.0" + aws4 "^1.6.0" + caseless "~0.12.0" + combined-stream "~1.0.5" + extend "~3.0.1" + forever-agent "~0.6.1" + form-data "~2.3.1" + har-validator "~5.0.3" + hawk "~6.0.2" + http-signature "~1.2.0" + is-typedarray "~1.0.0" + isstream "~0.1.2" + json-stringify-safe "~5.0.1" + mime-types "~2.1.17" + oauth-sign "~0.8.2" + performance-now "^2.1.0" + qs "~6.5.1" + safe-buffer "^5.1.1" + stringstream "~0.0.5" + tough-cookie "~2.3.2" + tunnel-agent "^0.6.0" + uuid "^3.1.0" + +require-directory@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" + +require-from-string@^1.1.0: + version "1.2.1" + resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-1.2.1.tgz#529c9ccef27380adfec9a2f965b649bbee636418" + +require-from-string@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.1.tgz#c545233e9d7da6616e9d59adfb39fc9f588676ff" + +require-main-filename@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-1.0.1.tgz#97f717b69d48784f5f526a6c5aa8ffdda055a4d1" + +require-nocache@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/require-nocache/-/require-nocache-1.0.0.tgz#a665d0b60a07e8249875790a4d350219d3c85fa3" + +require-uncached@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/require-uncached/-/require-uncached-1.0.3.tgz#4e0d56d6c9662fd31e43011c4b95aa49955421d3" + dependencies: + caller-path "^0.1.0" + resolve-from "^1.0.0" + +reselect@3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/reselect/-/reselect-3.0.1.tgz#efdaa98ea7451324d092b2b2163a6a1d7a9a2147" + +resize-observer-polyfill@^1.4.1: + version "1.4.2" + resolved "https://registry.yarnpkg.com/resize-observer-polyfill/-/resize-observer-polyfill-1.4.2.tgz#a37198e6209e888acb1532a9968e06d38b6788e5" + +resolve-dir@^0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/resolve-dir/-/resolve-dir-0.1.1.tgz#b219259a5602fac5c5c496ad894a6e8cc430261e" + dependencies: + expand-tilde "^1.2.2" + global-modules "^0.2.3" + +resolve-from@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-1.0.1.tgz#26cbfe935d1aeeeabb29bc3fe5aeb01e93d44226" + +resolve-from@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" + +resolve-pathname@^2.0.0, resolve-pathname@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/resolve-pathname/-/resolve-pathname-2.2.0.tgz#7e9ae21ed815fd63ab189adeee64dc831eefa879" + +resolve-url@~0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a" + +resolve@^1.1.5, resolve@^1.1.6, resolve@^1.1.7, resolve@~1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.4.0.tgz#a75be01c53da25d934a98ebd0e4c4a7312f92a86" + dependencies: + path-parse "^1.0.5" + +restore-cursor@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-2.0.0.tgz#9f7ee287f82fd326d4fd162923d62129eee0dfaf" + dependencies: + onetime "^2.0.0" + signal-exit "^3.0.2" + +resumer@~0.0.0: + version "0.0.0" + resolved "https://registry.yarnpkg.com/resumer/-/resumer-0.0.0.tgz#f1e8f461e4064ba39e82af3cdc2a8c893d076759" + dependencies: + through "~2.3.4" + +right-align@^0.1.1: + version "0.1.3" + resolved "https://registry.yarnpkg.com/right-align/-/right-align-0.1.3.tgz#61339b722fe6a3515689210d24e14c96148613ef" + dependencies: + align-text "^0.1.1" + +rimraf@2, rimraf@^2.2.8, rimraf@^2.5.1, rimraf@^2.6.1: + version "2.6.2" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.2.tgz#2ed8150d24a16ea8651e6d6ef0f47c4158ce7a36" + dependencies: + glob "^7.0.5" + +ripemd160@^2.0.0, ripemd160@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/ripemd160/-/ripemd160-2.0.1.tgz#0f4584295c53a3628af7e6d79aca21ce57d1c6e7" + dependencies: + hash-base "^2.0.0" + inherits "^2.0.1" + +rocambole-indent@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/rocambole-indent/-/rocambole-indent-2.0.4.tgz#a18a24977ca0400b861daa4631e861dcb52d085c" + dependencies: + debug "^2.1.3" + mout "^0.11.0" + rocambole-token "^1.2.1" + +rocambole-linebreak@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/rocambole-linebreak/-/rocambole-linebreak-1.0.2.tgz#03621515b43b4721c97e5a1c1bca5a0366368f2f" + dependencies: + debug "^2.1.3" + rocambole-token "^1.2.1" + semver "^4.3.1" + +rocambole-node@~1.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/rocambole-node/-/rocambole-node-1.0.0.tgz#db5b49de7407b0080dd514872f28e393d0f7ff3f" + +rocambole-token@^1.1.2, rocambole-token@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/rocambole-token/-/rocambole-token-1.2.1.tgz#c785df7428dc3cb27ad7897047bd5238cc070d35" + +rocambole-whitespace@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/rocambole-whitespace/-/rocambole-whitespace-1.0.0.tgz#63330949256b29941f59b190459f999c6b1d3bf9" + dependencies: + debug "^2.1.3" + repeat-string "^1.5.0" + rocambole-token "^1.2.1" + +"rocambole@>=0.7 <2.0", rocambole@^0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/rocambole/-/rocambole-0.7.0.tgz#f6c79505517dc42b6fb840842b8b953b0f968585" + dependencies: + esprima "^2.1" + +run-async@^2.2.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.3.0.tgz#0371ab4ae0bdd720d4166d7dfda64ff7a445a6c0" + dependencies: + is-promise "^2.1.0" + +run-sequence@2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/run-sequence/-/run-sequence-2.2.0.tgz#b3f8d42836db89d08b2fe704eaf0c93dfd8335e2" + dependencies: + chalk "^1.1.3" + gulp-util "^3.0.8" + +rx-lite-aggregates@^4.0.8: + version "4.0.8" + resolved "https://registry.yarnpkg.com/rx-lite-aggregates/-/rx-lite-aggregates-4.0.8.tgz#753b87a89a11c95467c4ac1626c4efc4e05c67be" + dependencies: + rx-lite "*" + +rx-lite@*, rx-lite@^4.0.8: + version "4.0.8" + resolved "https://registry.yarnpkg.com/rx-lite/-/rx-lite-4.0.8.tgz#0b1e11af8bc44836f04a6407e92da42467b79444" + +safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@~5.1.0, safe-buffer@~5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.1.tgz#893312af69b2123def71f57889001671eeb2c853" + +sane@^1.6.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/sane/-/sane-1.7.0.tgz#b3579bccb45c94cf20355cc81124990dfd346e30" + dependencies: + anymatch "^1.3.0" + exec-sh "^0.2.0" + fb-watchman "^2.0.0" + minimatch "^3.0.2" + minimist "^1.1.1" + walker "~1.0.5" + watch "~0.10.0" + +sax@~1.2.1: + version "1.2.4" + resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" + +schema-utils@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-0.3.0.tgz#f5877222ce3e931edae039f17eb3716e7137f8cf" + dependencies: + ajv "^5.0.0" + +section-iterator@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/section-iterator/-/section-iterator-2.0.0.tgz#bf444d7afeeb94ad43c39ad2fb26151627ccba2a" + +select@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/select/-/select-1.1.2.tgz#0e7350acdec80b1108528786ec1d4418d11b396d" + +"semver@2 || 3 || 4 || 5", semver@^4.1.0, semver@^4.3.1: + version "4.3.6" + resolved "https://registry.yarnpkg.com/semver/-/semver-4.3.6.tgz#300bc6e0e86374f7ba61068b5b1ecd57fc6532da" + +semver@^5.3.0: + version "5.4.1" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.4.1.tgz#e059c09d8571f0540823733433505d3a2f00b18e" + +sequencify@~0.0.7: + version "0.0.7" + resolved "https://registry.yarnpkg.com/sequencify/-/sequencify-0.0.7.tgz#90cff19d02e07027fd767f5ead3e7b95d1e7380c" + +serializerr@^1.0.1: + version "1.0.3" + resolved "https://registry.yarnpkg.com/serializerr/-/serializerr-1.0.3.tgz#12d4c5aa1c3ffb8f6d1dc5f395aa9455569c3f91" + dependencies: + protochain "^1.0.5" + +set-blocking@^2.0.0, set-blocking@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" + +set-immediate-shim@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz#4b2b1b27eb808a9f8dcc481a58e5e56f599f3f61" + +setimmediate@^1.0.4, setimmediate@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285" + +sha.js@^2.4.0, sha.js@^2.4.8: + version "2.4.9" + resolved "https://registry.yarnpkg.com/sha.js/-/sha.js-2.4.9.tgz#98f64880474b74f4a38b8da9d3c0f2d104633e7d" + dependencies: + inherits "^2.0.1" + safe-buffer "^5.0.1" + +shallow-equal@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/shallow-equal/-/shallow-equal-1.0.0.tgz#508d1838b3de590ab8757b011b25e430900945f7" + +shallowequal@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/shallowequal/-/shallowequal-1.0.2.tgz#1561dbdefb8c01408100319085764da3fcf83f8f" + +shebang-command@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea" + dependencies: + shebang-regex "^1.0.0" + +shebang-regex@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3" + +sigmund@~1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/sigmund/-/sigmund-1.0.1.tgz#3ff21f198cad2175f9f3b781853fd94d0d19b590" + +signal-exit@^3.0.0, signal-exit@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d" + +signalr@2.2.2: + version "2.2.2" + resolved "https://registry.yarnpkg.com/signalr/-/signalr-2.2.2.tgz#ba07915eb5e08cfbc87765047ec6972694d008e2" + dependencies: + jquery ">=1.6.4" + +slash@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/slash/-/slash-1.0.0.tgz#c41f2f6c39fc16d1cd17ad4b5d896114ae470d55" + +slice-ansi@0.0.4: + version "0.0.4" + resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-0.0.4.tgz#edbf8903f66f7ce2f8eafd6ceed65e264c831b35" + +sntp@2.x.x: + version "2.0.2" + resolved "https://registry.yarnpkg.com/sntp/-/sntp-2.0.2.tgz#5064110f0af85f7cfdb7d6b67a40028ce52b4b2b" + dependencies: + hoek "4.x.x" + +sort-keys@^1.0.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/sort-keys/-/sort-keys-1.1.2.tgz#441b6d4d346798f1b4e49e8920adfba0e543f9ad" + dependencies: + is-plain-obj "^1.0.0" + +source-list-map@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-2.0.0.tgz#aaa47403f7b245a92fbc97ea08f250d6087ed085" + +source-map-resolve@^0.3.0: + version "0.3.1" + resolved "https://registry.yarnpkg.com/source-map-resolve/-/source-map-resolve-0.3.1.tgz#610f6122a445b8dd51535a2a71b783dfc1248761" + dependencies: + atob "~1.1.0" + resolve-url "~0.2.1" + source-map-url "~0.3.0" + urix "~0.1.0" + +source-map-support@^0.4.15: + version "0.4.18" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.4.18.tgz#0286a6de8be42641338594e97ccea75f0a2c585f" + dependencies: + source-map "^0.5.6" + +source-map-url@~0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/source-map-url/-/source-map-url-0.3.0.tgz#7ecaf13b57bcd09da8a40c5d269db33799d4aaf9" + +source-map@0.5.x, source-map@^0.5.1, source-map@^0.5.3, source-map@^0.5.6, source-map@^0.5.7, source-map@~0.5.1, source-map@~0.5.3: + version "0.5.7" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" + +source-map@0.X: + version "0.6.0" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.0.tgz#36446016b0e5b626cf0315d6ff14b15bafb9dc10" + +source-map@^0.1.38: + version "0.1.43" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.1.43.tgz#c24bc146ca517c1471f5dacbe2571b2b7f9e3346" + dependencies: + amdefine ">=0.0.4" + +source-map@^0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" + +sparkles@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/sparkles/-/sparkles-1.0.0.tgz#1acbbfb592436d10bbe8f785b7cc6f82815012c3" + +spdx-correct@~1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-1.0.2.tgz#4b3073d933ff51f3912f03ac5519498a4150db40" + dependencies: + spdx-license-ids "^1.0.2" + +spdx-expression-parse@~1.0.0: + version "1.0.4" + resolved "https://registry.yarnpkg.com/spdx-expression-parse/-/spdx-expression-parse-1.0.4.tgz#9bdf2f20e1f40ed447fbe273266191fced51626c" + +spdx-license-ids@^1.0.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-1.2.2.tgz#c9df7a3424594ade6bd11900d596696dc06bac57" + +specificity@^0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/specificity/-/specificity-0.3.1.tgz#f1b068424ce317ae07478d95de3c21cf85e8d567" + +split@0.3: + version "0.3.3" + resolved "https://registry.yarnpkg.com/split/-/split-0.3.3.tgz#cd0eea5e63a211dfff7eb0f091c4133e2d0dd28f" + dependencies: + through "2" + +sprintf-js@~1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" + +sshpk@^1.7.0: + version "1.13.1" + resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.13.1.tgz#512df6da6287144316dc4c18fe1cf1d940739be3" + dependencies: + asn1 "~0.2.3" + assert-plus "^1.0.0" + dashdash "^1.12.0" + getpass "^0.1.1" + optionalDependencies: + bcrypt-pbkdf "^1.0.0" + ecc-jsbn "~0.1.1" + jsbn "~0.1.0" + tweetnacl "~0.14.0" + +statuses@1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.3.1.tgz#faf51b9eb74aaef3b3acf4ad5f61abf24cb7b93e" + +stdin@*: + version "0.0.1" + resolved "https://registry.yarnpkg.com/stdin/-/stdin-0.0.1.tgz#d3041981aaec3dfdbc77a1b38d6372e38f5fb71e" + +stream-browserify@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/stream-browserify/-/stream-browserify-2.0.1.tgz#66266ee5f9bdb9940a4e4514cafb43bb71e5c9db" + dependencies: + inherits "~2.0.1" + readable-stream "^2.0.2" + +stream-combiner@~0.0.4: + version "0.0.4" + resolved "https://registry.yarnpkg.com/stream-combiner/-/stream-combiner-0.0.4.tgz#4d5e433c185261dde623ca3f44c586bcf5c4ad14" + dependencies: + duplexer "~0.1.1" + +stream-consume@~0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/stream-consume/-/stream-consume-0.1.0.tgz#a41ead1a6d6081ceb79f65b061901b6d8f3d1d0f" + +stream-http@^2.3.1: + version "2.7.2" + resolved "https://registry.yarnpkg.com/stream-http/-/stream-http-2.7.2.tgz#40a050ec8dc3b53b33d9909415c02c0bf1abfbad" + dependencies: + builtin-status-codes "^3.0.0" + inherits "^2.0.1" + readable-stream "^2.2.6" + to-arraybuffer "^1.0.0" + xtend "^4.0.0" + +streamqueue@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/streamqueue/-/streamqueue-1.1.1.tgz#d3ad76686be924bbf9ca2c74a814a2182475d6d7" + dependencies: + isstream "^0.1.2" + readable-stream "~1.0.33" + +strict-uri-encode@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz#279b225df1d582b1f54e65addd4352e18faa0713" + +string-width@^1.0.1, string-width@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3" + dependencies: + code-point-at "^1.0.0" + is-fullwidth-code-point "^1.0.0" + strip-ansi "^3.0.0" + +string-width@^2.0.0, string-width@^2.1.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e" + dependencies: + is-fullwidth-code-point "^2.0.0" + strip-ansi "^4.0.0" + +string.prototype.trim@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/string.prototype.trim/-/string.prototype.trim-1.1.2.tgz#d04de2c89e137f4d7d206f086b5ed2fae6be8cea" + dependencies: + define-properties "^1.1.2" + es-abstract "^1.5.0" + function-bind "^1.0.2" + +string_decoder@^0.10.25, string_decoder@~0.10.x: + version "0.10.31" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94" + +string_decoder@~1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.0.3.tgz#0fc67d7c141825de94282dd536bec6b9bce860ab" + dependencies: + safe-buffer "~5.1.0" + +stringstream@~0.0.5: + version "0.0.5" + resolved "https://registry.yarnpkg.com/stringstream/-/stringstream-0.0.5.tgz#4e484cd4de5a0bbbee18e46307710a8a81621878" + +strip-ansi@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-0.3.0.tgz#25f48ea22ca79187f3174a4db8759347bb126220" + dependencies: + ansi-regex "^0.2.1" + +strip-ansi@^3.0.0, strip-ansi@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf" + dependencies: + ansi-regex "^2.0.0" + +strip-ansi@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f" + dependencies: + ansi-regex "^3.0.0" + +strip-bom-stream@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/strip-bom-stream/-/strip-bom-stream-2.0.0.tgz#f87db5ef2613f6968aa545abfe1ec728b6a829ca" + dependencies: + first-chunk-stream "^2.0.0" + strip-bom "^2.0.0" + +strip-bom-string@1.X: + version "1.0.0" + resolved "https://registry.yarnpkg.com/strip-bom-string/-/strip-bom-string-1.0.0.tgz#e5211e9224369fbb81d633a2f00044dc8cedad92" + +strip-bom@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-1.0.0.tgz#85b8862f3844b5a6d5ec8467a93598173a36f794" + dependencies: + first-chunk-stream "^1.0.0" + is-utf8 "^0.2.0" + +strip-bom@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-2.0.0.tgz#6219a85616520491f35788bdbf1447a99c7e6b0e" + dependencies: + is-utf8 "^0.2.0" + +strip-bom@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" + +strip-eof@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf" + +strip-indent@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-1.0.1.tgz#0c7962a6adefa7bbd4ac366460a638552ae1a0a2" + dependencies: + get-stdin "^4.0.1" + +strip-json-comments@~0.1.1: + version "0.1.3" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-0.1.3.tgz#164c64e370a8a3cc00c9e01b539e569823f0ee54" + +strip-json-comments@~2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" + +style-loader@0.19.0: + version "0.19.0" + resolved "https://registry.yarnpkg.com/style-loader/-/style-loader-0.19.0.tgz#7258e788f0fee6a42d710eaf7d6c2412a4c50759" + dependencies: + loader-utils "^1.0.2" + schema-utils "^0.3.0" + +style-search@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/style-search/-/style-search-0.1.0.tgz#7958c793e47e32e07d2b5cafe5c0bf8e12e77902" + +stylelint-order@0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/stylelint-order/-/stylelint-order-0.7.0.tgz#ceab5cbe24aa33fa63590024995395f6edfc9ab7" + dependencies: + lodash "^4.17.4" + postcss "^6.0.11" + postcss-sorting "^3.0.2" + +stylelint@8.2.0: + version "8.2.0" + resolved "https://registry.yarnpkg.com/stylelint/-/stylelint-8.2.0.tgz#6a15044553fb5c3143b16d62013a370314495b0d" + dependencies: + autoprefixer "^7.1.2" + balanced-match "^1.0.0" + chalk "^2.0.1" + cosmiconfig "^3.1.0" + debug "^3.0.0" + execall "^1.0.0" + file-entry-cache "^2.0.0" + get-stdin "^5.0.1" + globby "^6.1.0" + globjoin "^0.1.4" + html-tags "^2.0.0" + ignore "^3.3.3" + imurmurhash "^0.1.4" + known-css-properties "^0.4.0" + lodash "^4.17.4" + log-symbols "^2.0.0" + mathml-tag-names "^2.0.1" + meow "^3.7.0" + micromatch "^2.3.11" + normalize-selector "^0.2.0" + pify "^3.0.0" + postcss "^6.0.6" + postcss-less "^1.1.0" + postcss-media-query-parser "^0.2.3" + postcss-reporter "^5.0.0" + postcss-resolve-nested-selector "^0.1.1" + postcss-safe-parser "^3.0.1" + postcss-scss "^1.0.2" + postcss-selector-parser "^2.2.3" + postcss-value-parser "^3.3.0" + resolve-from "^4.0.0" + specificity "^0.3.1" + string-width "^2.1.0" + style-search "^0.1.0" + sugarss "^1.0.0" + svg-tags "^1.0.0" + table "^4.0.1" + +sugarss@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/sugarss/-/sugarss-1.0.0.tgz#65e51b3958432fb70d5451a68bb33e32d0cf1ef7" + dependencies: + postcss "^6.0.0" + +supports-color@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-0.2.0.tgz#d92de2694eb3f67323973d7ae3d8b55b4c22190a" + +supports-color@^1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-1.3.1.tgz#15758df09d8ff3b4acc307539fabe27095e1042d" + +supports-color@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7" + +supports-color@^3.2.3: + version "3.2.3" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-3.2.3.tgz#65ac0504b3954171d8a64946b2ae3cbb8a5f54f6" + dependencies: + has-flag "^1.0.0" + +supports-color@^4.0.0, supports-color@^4.2.1, supports-color@^4.4.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-4.4.0.tgz#883f7ddabc165142b2a61427f3352ded195d1a3e" + dependencies: + has-flag "^2.0.0" + +svg-tags@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/svg-tags/-/svg-tags-1.0.0.tgz#58f71cee3bd519b59d4b2a843b6c7de64ac04764" + +svgo@^0.7.0: + version "0.7.2" + resolved "https://registry.yarnpkg.com/svgo/-/svgo-0.7.2.tgz#9f5772413952135c6fefbf40afe6a4faa88b4bb5" + dependencies: + coa "~1.0.1" + colors "~1.1.2" + csso "~2.3.1" + js-yaml "~3.7.0" + mkdirp "~0.5.1" + sax "~1.2.1" + whet.extend "~0.9.9" + +symbol-observable@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.0.4.tgz#29bf615d4aa7121bdd898b22d4b3f9bc4e2aa03d" + +sync-exec@^0.6.2: + version "0.6.2" + resolved "https://registry.yarnpkg.com/sync-exec/-/sync-exec-0.6.2.tgz#717d22cc53f0ce1def5594362f3a89a2ebb91105" + +table@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/table/-/table-4.0.1.tgz#a8116c133fac2c61f4a420ab6cdf5c4d61f0e435" + dependencies: + ajv "^4.7.0" + ajv-keywords "^1.0.0" + chalk "^1.1.1" + lodash "^4.0.0" + slice-ansi "0.0.4" + string-width "^2.0.0" + +tapable@^0.2.7: + version "0.2.8" + resolved "https://registry.yarnpkg.com/tapable/-/tapable-0.2.8.tgz#99372a5c999bf2df160afc0d74bed4f47948cd22" + +tape@^4.6.3: + version "4.8.0" + resolved "https://registry.yarnpkg.com/tape/-/tape-4.8.0.tgz#f6a9fec41cc50a1de50fa33603ab580991f6068e" + dependencies: + deep-equal "~1.0.1" + defined "~1.0.0" + for-each "~0.3.2" + function-bind "~1.1.0" + glob "~7.1.2" + has "~1.0.1" + inherits "~2.0.3" + minimist "~1.2.0" + object-inspect "~1.3.0" + resolve "~1.4.0" + resumer "~0.0.0" + string.prototype.trim "~1.1.2" + through "~2.3.8" + +tar-pack@^3.4.0: + version "3.4.0" + resolved "https://registry.yarnpkg.com/tar-pack/-/tar-pack-3.4.0.tgz#23be2d7f671a8339376cbdb0b8fe3fdebf317984" + dependencies: + debug "^2.2.0" + fstream "^1.0.10" + fstream-ignore "^1.0.5" + once "^1.3.3" + readable-stream "^2.1.4" + rimraf "^2.5.1" + tar "^2.2.1" + uid-number "^0.0.6" + +tar.gz@1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/tar.gz/-/tar.gz-1.0.5.tgz#e1ada7e45ef2241b4b1ee58123c8f40b5d3c1bc4" + dependencies: + bluebird "^2.9.34" + commander "^2.8.1" + fstream "^1.0.8" + mout "^0.11.0" + tar "^2.1.1" + +tar@^2.1.1, tar@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/tar/-/tar-2.2.1.tgz#8e4d2a256c0e2185c6b18ad694aec968b83cb1d1" + dependencies: + block-stream "*" + fstream "^1.0.2" + inherits "2" + +tether@^1.3.7: + version "1.4.0" + resolved "https://registry.yarnpkg.com/tether/-/tether-1.4.0.tgz#0f9fa171f75bf58485d8149e94799d7ae74d1c1a" + +text-table@~0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" + +through2@2.0.3, through2@2.X, through2@^2.0.0, through2@^2.0.1, through2@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.3.tgz#0004569b37c7c74ba39c43f3ced78d1ad94140be" + dependencies: + readable-stream "^2.1.5" + xtend "~4.0.1" + +through2@^0.4.1: + version "0.4.2" + resolved "https://registry.yarnpkg.com/through2/-/through2-0.4.2.tgz#dbf5866031151ec8352bb6c4db64a2292a840b9b" + dependencies: + readable-stream "~1.0.17" + xtend "~2.1.1" + +through2@^0.5.1: + version "0.5.1" + resolved "https://registry.yarnpkg.com/through2/-/through2-0.5.1.tgz#dfdd012eb9c700e2323fd334f38ac622ab372da7" + dependencies: + readable-stream "~1.0.17" + xtend "~3.0.0" + +through2@^0.6.1: + version "0.6.5" + resolved "https://registry.yarnpkg.com/through2/-/through2-0.6.5.tgz#41ab9c67b29d57209071410e1d7a7a968cd3ad48" + dependencies: + readable-stream ">=1.0.33-1 <1.1.0-0" + xtend ">=4.0.0 <4.1.0-0" + +through@2, through@^2.3.6, through@^2.3.8, through@~2.3, through@~2.3.1, through@~2.3.4, through@~2.3.8: + version "2.3.8" + resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" + +tildify@^1.0.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/tildify/-/tildify-1.2.0.tgz#dcec03f55dca9b7aa3e5b04f21817eb56e63588a" + dependencies: + os-homedir "^1.0.0" + +time-stamp@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/time-stamp/-/time-stamp-1.1.0.tgz#764a5a11af50561921b133f3b44e618687e0f5c3" + +timers-browserify@^2.0.2: + version "2.0.4" + resolved "https://registry.yarnpkg.com/timers-browserify/-/timers-browserify-2.0.4.tgz#96ca53f4b794a5e7c0e1bd7cc88a372298fa01e6" + dependencies: + setimmediate "^1.0.4" + +timers-ext@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/timers-ext/-/timers-ext-0.1.2.tgz#61cc47a76c1abd3195f14527f978d58ae94c5204" + dependencies: + es5-ext "~0.10.14" + next-tick "1" + +tiny-emitter@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/tiny-emitter/-/tiny-emitter-2.0.2.tgz#82d27468aca5ade8e5fd1e6d22b57dd43ebdfb7c" + +tmp@^0.0.33: + version "0.0.33" + resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9" + dependencies: + os-tmpdir "~1.0.2" + +tmpl@1.0.x: + version "1.0.4" + resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.4.tgz#23640dd7b42d00433911140820e5cf440e521dd1" + +to-arraybuffer@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz#7d229b1fcc637e466ca081180836a7aabff83f43" + +to-camel-case@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/to-camel-case/-/to-camel-case-1.0.0.tgz#1a56054b2f9d696298ce66a60897322b6f423e46" + dependencies: + to-space-case "^1.0.0" + +to-fast-properties@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-1.0.3.tgz#b83571fa4d8c25b82e231b06e3a3055de4ca1a47" + +to-fast-properties@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e" + +to-no-case@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/to-no-case/-/to-no-case-1.0.2.tgz#c722907164ef6b178132c8e69930212d1b4aa16a" + +to-space-case@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/to-space-case/-/to-space-case-1.0.0.tgz#b052daafb1b2b29dc770cea0163e5ec0ebc9fc17" + dependencies: + to-no-case "^1.0.0" + +tough-cookie@~2.3.2: + version "2.3.2" + resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.3.2.tgz#f081f76e4c85720e6c37a5faced737150d84072a" + dependencies: + punycode "^1.4.1" + +traverse@~0.6.3: + version "0.6.6" + resolved "https://registry.yarnpkg.com/traverse/-/traverse-0.6.6.tgz#cbdf560fd7b9af632502fed40f918c157ea97137" + +trim-newlines@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-1.0.0.tgz#5887966bb582a4503a41eb524f7d35011815a613" + +trim-right@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/trim-right/-/trim-right-1.0.1.tgz#cb2e1203067e0c8de1f614094b9fe45704ea6003" + +tryit@^1.0.1: + version "1.0.3" + resolved "https://registry.yarnpkg.com/tryit/-/tryit-1.0.3.tgz#393be730a9446fd1ead6da59a014308f36c289cb" + +tty-browserify@0.0.0: + version "0.0.0" + resolved "https://registry.yarnpkg.com/tty-browserify/-/tty-browserify-0.0.0.tgz#a157ba402da24e9bf957f9aa69d524eed42901a6" + +tunnel-agent@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd" + dependencies: + safe-buffer "^5.0.1" + +tweetnacl@^0.14.3, tweetnacl@~0.14.0: + version "0.14.5" + resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" + +type-check@~0.3.2: + version "0.3.2" + resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.3.2.tgz#5884cab512cf1d355e3fb784f30804b2b520db72" + dependencies: + prelude-ls "~1.1.2" + +type-is@~1.6.10: + version "1.6.15" + resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.15.tgz#cab10fb4909e441c82842eafe1ad646c81804410" + dependencies: + media-typer "0.3.0" + mime-types "~2.1.15" + +typedarray@^0.0.6: + version "0.0.6" + resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" + +ua-parser-js@^0.7.9: + version "0.7.14" + resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.14.tgz#110d53fa4c3f326c121292bbeac904d2e03387ca" + +uglify-js@^2.8.29: + version "2.8.29" + resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-2.8.29.tgz#29c5733148057bb4e1f75df35b7a9cb72e6a59dd" + dependencies: + source-map "~0.5.1" + yargs "~3.10.0" + optionalDependencies: + uglify-to-browserify "~1.0.0" + +uglify-to-browserify@~1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz#6e0924d6bda6b5afe349e39a6d632850a0f882b7" + +uglifyjs-webpack-plugin@^0.4.6: + version "0.4.6" + resolved "https://registry.yarnpkg.com/uglifyjs-webpack-plugin/-/uglifyjs-webpack-plugin-0.4.6.tgz#b951f4abb6bd617e66f63eb891498e391763e309" + dependencies: + source-map "^0.5.6" + uglify-js "^2.8.29" + webpack-sources "^1.0.1" + +uid-number@^0.0.6: + version "0.0.6" + resolved "https://registry.yarnpkg.com/uid-number/-/uid-number-0.0.6.tgz#0ea10e8035e8eb5b8e4449f06da1c730663baa81" + +unc-path-regex@^0.1.0: + version "0.1.2" + resolved "https://registry.yarnpkg.com/unc-path-regex/-/unc-path-regex-0.1.2.tgz#e73dd3d7b0d7c5ed86fbac6b0ae7d8c6a69d50fa" + +uniq@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/uniq/-/uniq-1.0.1.tgz#b31c5ae8254844a3a8281541ce2b04b865a734ff" + +uniqid@^4.0.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/uniqid/-/uniqid-4.1.1.tgz#89220ddf6b751ae52b5f72484863528596bb84c1" + dependencies: + macaddress "^0.2.8" + +uniqs@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/uniqs/-/uniqs-2.0.0.tgz#ffede4b36b25290696e6e165d4a59edb998e6b02" + +unique-stream@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/unique-stream/-/unique-stream-1.0.0.tgz#d59a4a75427447d9aa6c91e70263f8d26a4b104b" + +unpipe@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" + +urix@^0.1.0, urix@~0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/urix/-/urix-0.1.0.tgz#da937f7a62e21fec1fd18d49b35c2935067a6c72" + +url-loader@0.6.2: + version "0.6.2" + resolved "https://registry.yarnpkg.com/url-loader/-/url-loader-0.6.2.tgz#a007a7109620e9d988d14bce677a1decb9a993f7" + dependencies: + loader-utils "^1.0.2" + mime "^1.4.1" + schema-utils "^0.3.0" + +url@^0.11.0: + version "0.11.0" + resolved "https://registry.yarnpkg.com/url/-/url-0.11.0.tgz#3838e97cfc60521eb73c525a8e55bfdd9e2e28f1" + dependencies: + punycode "1.3.2" + querystring "0.2.0" + +user-home@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/user-home/-/user-home-1.1.1.tgz#2b5be23a32b63a7c9deb8d0f28d485724a3df190" + +user-home@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/user-home/-/user-home-2.0.0.tgz#9c70bfd8169bc1dcbf48604e0f04b8b49cde9e9f" + dependencies: + os-homedir "^1.0.0" + +util-deprecate@~1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" + +util@0.10.3, util@^0.10.3: + version "0.10.3" + resolved "https://registry.yarnpkg.com/util/-/util-0.10.3.tgz#7afb1afe50805246489e3db7fe0ed379336ac0f9" + dependencies: + inherits "2.0.1" + +uuid@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.1.0.tgz#3dd3d3e790abc24d7b0d3a034ffababe28ebbc04" + +v8flags@^2.0.2: + version "2.1.1" + resolved "https://registry.yarnpkg.com/v8flags/-/v8flags-2.1.1.tgz#aab1a1fa30d45f88dd321148875ac02c0b55e5b4" + dependencies: + user-home "^1.1.1" + +validate-npm-package-license@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.1.tgz#2804babe712ad3379459acfbe24746ab2c303fbc" + dependencies: + spdx-correct "~1.0.0" + spdx-expression-parse "~1.0.0" + +value-equal@^0.2.0: + version "0.2.1" + resolved "https://registry.yarnpkg.com/value-equal/-/value-equal-0.2.1.tgz#c220a304361fce6994dbbedaa3c7e1a1b895871d" + +value-equal@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/value-equal/-/value-equal-0.4.0.tgz#c5bdd2f54ee093c04839d71ce2e4758a6890abc7" + +vendors@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/vendors/-/vendors-1.0.1.tgz#37ad73c8ee417fb3d580e785312307d274847f22" + +verror@1.10.0: + version "1.10.0" + resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400" + dependencies: + assert-plus "^1.0.0" + core-util-is "1.0.2" + extsprintf "^1.2.0" + +vinyl-bufferstream@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/vinyl-bufferstream/-/vinyl-bufferstream-1.0.1.tgz#0537869f580effa4ca45acb47579e4b9fe63081a" + dependencies: + bufferstreams "1.0.1" + +vinyl-file@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/vinyl-file/-/vinyl-file-2.0.0.tgz#a7ebf5ffbefda1b7d18d140fcb07b223efb6751a" + dependencies: + graceful-fs "^4.1.2" + pify "^2.3.0" + pinkie-promise "^2.0.0" + strip-bom "^2.0.0" + strip-bom-stream "^2.0.0" + vinyl "^1.1.0" + +vinyl-fs@^0.3.0: + version "0.3.14" + resolved "https://registry.yarnpkg.com/vinyl-fs/-/vinyl-fs-0.3.14.tgz#9a6851ce1cac1c1cea5fe86c0931d620c2cfa9e6" + dependencies: + defaults "^1.0.0" + glob-stream "^3.1.5" + glob-watcher "^0.0.6" + graceful-fs "^3.0.0" + mkdirp "^0.5.0" + strip-bom "^1.0.0" + through2 "^0.6.1" + vinyl "^0.4.0" + +vinyl-map@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/vinyl-map/-/vinyl-map-1.0.2.tgz#a8b296025f973fa7cad62817967a48f1d176bf7c" + dependencies: + bl "^1.1.2" + new-from "0.0.3" + through2 "^0.4.1" + +vinyl-sourcemaps-apply@0.2.1, vinyl-sourcemaps-apply@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/vinyl-sourcemaps-apply/-/vinyl-sourcemaps-apply-0.2.1.tgz#ab6549d61d172c2b1b87be5c508d239c8ef87705" + dependencies: + source-map "^0.5.1" + +vinyl@1.X, vinyl@^1.1.0, vinyl@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/vinyl/-/vinyl-1.2.0.tgz#5c88036cf565e5df05558bfc911f8656df218884" + dependencies: + clone "^1.0.0" + clone-stats "^0.0.1" + replace-ext "0.0.1" + +vinyl@^0.4.0: + version "0.4.6" + resolved "https://registry.yarnpkg.com/vinyl/-/vinyl-0.4.6.tgz#2f356c87a550a255461f36bbeb2a5ba8bf784847" + dependencies: + clone "^0.2.0" + clone-stats "^0.0.1" + +vinyl@^0.5.0: + version "0.5.3" + resolved "https://registry.yarnpkg.com/vinyl/-/vinyl-0.5.3.tgz#b0455b38fc5e0cf30d4325132e461970c2091cde" + dependencies: + clone "^1.0.0" + clone-stats "^0.0.1" + replace-ext "0.0.1" + +vinyl@^2.0.0, vinyl@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/vinyl/-/vinyl-2.1.0.tgz#021f9c2cf951d6b939943c89eb5ee5add4fd924c" + dependencies: + clone "^2.1.1" + clone-buffer "^1.0.0" + clone-stats "^1.0.0" + cloneable-readable "^1.0.0" + remove-trailing-separator "^1.0.1" + replace-ext "^1.0.0" + +vm-browserify@0.0.4: + version "0.0.4" + resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-0.0.4.tgz#5d7ea45bbef9e4a6ff65f95438e0a87c357d5a73" + dependencies: + indexof "0.0.1" + +walker@~1.0.5: + version "1.0.7" + resolved "https://registry.yarnpkg.com/walker/-/walker-1.0.7.tgz#2f7f9b8fd10d677262b18a884e28d19618e028fb" + dependencies: + makeerror "1.0.x" + +warning@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/warning/-/warning-3.0.0.tgz#32e5377cb572de4ab04753bdf8821c01ed605b7c" + dependencies: + loose-envify "^1.0.0" + +watch@~0.10.0: + version "0.10.0" + resolved "https://registry.yarnpkg.com/watch/-/watch-0.10.0.tgz#77798b2da0f9910d595f1ace5b0c2258521f21dc" + +watchpack@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-1.4.0.tgz#4a1472bcbb952bd0a9bb4036801f954dfb39faac" + dependencies: + async "^2.1.2" + chokidar "^1.7.0" + graceful-fs "^4.1.2" + +weak@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/weak/-/weak-1.0.1.tgz#ab99aab30706959aa0200cb8cf545bb9cb33b99e" + dependencies: + bindings "^1.2.1" + nan "^2.0.5" + +webpack-sources@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-1.0.1.tgz#c7356436a4d13123be2e2426a05d1dad9cbe65cf" + dependencies: + source-list-map "^2.0.0" + source-map "~0.5.3" + +webpack-stream@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/webpack-stream/-/webpack-stream-4.0.0.tgz#f3673dd907d6d9b1ea7bf51fcd1db85b5fd9e0f2" + dependencies: + gulp-util "^3.0.7" + lodash.clone "^4.3.2" + lodash.some "^4.2.2" + memory-fs "^0.4.1" + through "^2.3.8" + vinyl "^2.1.0" + webpack "^3.4.1" + +webpack@3.6.0, webpack@^3.4.1: + version "3.6.0" + resolved "https://registry.yarnpkg.com/webpack/-/webpack-3.6.0.tgz#a89a929fbee205d35a4fa2cc487be9cbec8898bc" + dependencies: + acorn "^5.0.0" + acorn-dynamic-import "^2.0.0" + ajv "^5.1.5" + ajv-keywords "^2.0.0" + async "^2.1.2" + enhanced-resolve "^3.4.0" + escope "^3.6.0" + interpret "^1.0.0" + json-loader "^0.5.4" + json5 "^0.5.1" + loader-runner "^2.3.0" + loader-utils "^1.1.0" + memory-fs "~0.4.1" + mkdirp "~0.5.0" + node-libs-browser "^2.0.0" + source-map "^0.5.3" + supports-color "^4.2.1" + tapable "^0.2.7" + uglifyjs-webpack-plugin "^0.4.6" + watchpack "^1.4.0" + webpack-sources "^1.0.1" + yargs "^8.0.2" + +websocket-driver@>=0.3.6: + version "0.7.0" + resolved "https://registry.yarnpkg.com/websocket-driver/-/websocket-driver-0.7.0.tgz#0caf9d2d755d93aee049d4bdd0d3fe2cca2a24eb" + dependencies: + http-parser-js ">=0.4.0" + websocket-extensions ">=0.1.1" + +websocket-extensions@>=0.1.1: + version "0.1.2" + resolved "https://registry.yarnpkg.com/websocket-extensions/-/websocket-extensions-0.1.2.tgz#0e18781de629a18308ce1481650f67ffa2693a5d" + +whatwg-fetch@>=0.10.0: + version "2.0.3" + resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-2.0.3.tgz#9c84ec2dcf68187ff00bc64e1274b442176e1c84" + +whet.extend@~0.9.9: + version "0.9.9" + resolved "https://registry.yarnpkg.com/whet.extend/-/whet.extend-0.9.9.tgz#f877d5bf648c97e5aa542fadc16d6a259b9c11a1" + +which-module@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a" + +which@^1.0.5, which@^1.2.12, which@^1.2.4, which@^1.2.9: + version "1.3.0" + resolved "https://registry.yarnpkg.com/which/-/which-1.3.0.tgz#ff04bdfc010ee547d780bec38e1ac1c2777d253a" + dependencies: + isexe "^2.0.0" + +wide-align@^1.1.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.2.tgz#571e0f1b0604636ebc0dfc21b0339bbe31341710" + dependencies: + string-width "^1.0.2" + +window-size@0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/window-size/-/window-size-0.1.0.tgz#5438cd2ea93b202efa3a19fe8887aee7c94f9c9d" + +wordwrap@0.0.2: + version "0.0.2" + resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.2.tgz#b79669bb42ecb409f83d583cad52ca17eaa1643f" + +wordwrap@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb" + +worker-farm@^1.3.1: + version "1.5.0" + resolved "https://registry.yarnpkg.com/worker-farm/-/worker-farm-1.5.0.tgz#adfdf0cd40581465ed0a1f648f9735722afd5c8d" + dependencies: + errno "^0.1.4" + xtend "^4.0.1" + +wrap-ansi@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-2.1.0.tgz#d8fc3d284dd05794fe84973caecdd1cf824fdd85" + dependencies: + string-width "^1.0.1" + strip-ansi "^3.0.1" + +wrappy@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + +write@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/write/-/write-0.2.1.tgz#5fc03828e264cea3fe91455476f7a3c566cb0757" + dependencies: + mkdirp "^0.5.1" + +"xtend@>=4.0.0 <4.1.0-0", xtend@^4.0.0, xtend@^4.0.1, xtend@~4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af" + +xtend@~2.1.1: + version "2.1.2" + resolved "https://registry.yarnpkg.com/xtend/-/xtend-2.1.2.tgz#6efecc2a4dad8e6962c4901b337ce7ba87b5d28b" + dependencies: + object-keys "~0.4.0" + +xtend@~3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/xtend/-/xtend-3.0.0.tgz#5cce7407baf642cba7becda568111c493f59665a" + +y18n@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/y18n/-/y18n-3.2.1.tgz#6d15fba884c08679c0d77e88e7759e811e07fa41" + +yallist@^2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52" + +yargs-parser@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-7.0.0.tgz#8d0ac42f16ea55debd332caf4c4038b3e3f5dfd9" + dependencies: + camelcase "^4.1.0" + +yargs@^8.0.1, yargs@^8.0.2: + version "8.0.2" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-8.0.2.tgz#6299a9055b1cefc969ff7e79c1d918dceb22c360" + dependencies: + camelcase "^4.1.0" + cliui "^3.2.0" + decamelize "^1.1.1" + get-caller-file "^1.0.1" + os-locale "^2.0.0" + read-pkg-up "^2.0.0" + require-directory "^2.1.1" + require-main-filename "^1.0.1" + set-blocking "^2.0.0" + string-width "^2.0.0" + which-module "^2.0.0" + y18n "^3.2.1" + yargs-parser "^7.0.0" + +yargs@~3.10.0: + version "3.10.0" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-3.10.0.tgz#f7ee7bd857dd7c1d2d38c0e74efbd681d1431fd1" + dependencies: + camelcase "^1.0.2" + cliui "^2.1.0" + decamelize "^1.0.0" + window-size "0.1.0"